Bug 1341990 - Part 1: Import ExoPlayer sources, minus the ui/ directory. r?sebastian draft
authorNick Alexander <nalexander@mozilla.com>
Fri, 24 Feb 2017 11:13:32 -0800
changeset 489320 b36ecd99350706e425dee12a82459f16bce88059
parent 489279 6f2117c0b9895dfeb06fa74d2dc91bff660386ce
child 489321 c7739fc5a3975df34f9364338a11354231764946
push id46805
push usernalexander@mozilla.com
push dateFri, 24 Feb 2017 19:18:22 +0000
reviewerssebastian
bugs1341990
milestone54.0a1
Bug 1341990 - Part 1: Import ExoPlayer sources, minus the ui/ directory. r?sebastian From https://github.com/google/ExoPlayer: "ExoPlayer is an application level media player for Android. It provides an alternative to Android’s MediaPlayer API for playing audio and video both locally and over the Internet. ExoPlayer supports features not currently supported by Android’s MediaPlayer API, including DASH and SmoothStreaming adaptive playbacks. Unlike the MediaPlayer API, ExoPlayer is easy to customize and extend, and can be updated through Play Store application updates." ExoPlayer is licensed Apache 2.0, so it's fully compatible with Mozilla's MPL. This import is the contents of https://github.com/google/ExoPlayer/tree/25a966dc2300161448fa74dee5ee98322ff65604/library/src/main/java/com/google/android/exoplayer2, minus the ui/ directory. By excluding the ui/ directory, there's no need to take the res/ directory, nor to make aapt generate com.google.android.exoplayer2.R. This makes the moz.build integration trivial. A follow-up patch will need to copy https://github.com/google/ExoPlayer/blob/25a966dc2300161448fa74dee5ee98322ff65604/library/proguard-rules.txt into the tree, and configure our Proguard configuration to respect those rules. MozReview-Commit-ID: AyoKECS9P7a
mobile/android/thirdparty/com/google/android/exoplayer2/BaseRenderer.java
mobile/android/thirdparty/com/google/android/exoplayer2/C.java
mobile/android/thirdparty/com/google/android/exoplayer2/DefaultLoadControl.java
mobile/android/thirdparty/com/google/android/exoplayer2/ExoPlaybackException.java
mobile/android/thirdparty/com/google/android/exoplayer2/ExoPlayer.java
mobile/android/thirdparty/com/google/android/exoplayer2/ExoPlayerFactory.java
mobile/android/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java
mobile/android/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java
mobile/android/thirdparty/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
mobile/android/thirdparty/com/google/android/exoplayer2/Format.java
mobile/android/thirdparty/com/google/android/exoplayer2/FormatHolder.java
mobile/android/thirdparty/com/google/android/exoplayer2/IllegalSeekPositionException.java
mobile/android/thirdparty/com/google/android/exoplayer2/LoadControl.java
mobile/android/thirdparty/com/google/android/exoplayer2/ParserException.java
mobile/android/thirdparty/com/google/android/exoplayer2/Renderer.java
mobile/android/thirdparty/com/google/android/exoplayer2/RendererCapabilities.java
mobile/android/thirdparty/com/google/android/exoplayer2/RendererConfiguration.java
mobile/android/thirdparty/com/google/android/exoplayer2/SimpleExoPlayer.java
mobile/android/thirdparty/com/google/android/exoplayer2/Timeline.java
mobile/android/thirdparty/com/google/android/exoplayer2/audio/Ac3Util.java
mobile/android/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilities.java
mobile/android/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.java
mobile/android/thirdparty/com/google/android/exoplayer2/audio/AudioDecoderException.java
mobile/android/thirdparty/com/google/android/exoplayer2/audio/AudioRendererEventListener.java
mobile/android/thirdparty/com/google/android/exoplayer2/audio/AudioTrack.java
mobile/android/thirdparty/com/google/android/exoplayer2/audio/DtsUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
mobile/android/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
mobile/android/thirdparty/com/google/android/exoplayer2/decoder/Buffer.java
mobile/android/thirdparty/com/google/android/exoplayer2/decoder/CryptoInfo.java
mobile/android/thirdparty/com/google/android/exoplayer2/decoder/Decoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/decoder/DecoderCounters.java
mobile/android/thirdparty/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java
mobile/android/thirdparty/com/google/android/exoplayer2/decoder/OutputBuffer.java
mobile/android/thirdparty/com/google/android/exoplayer2/decoder/SimpleDecoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java
mobile/android/thirdparty/com/google/android/exoplayer2/drm/DecryptionException.java
mobile/android/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
mobile/android/thirdparty/com/google/android/exoplayer2/drm/DrmInitData.java
mobile/android/thirdparty/com/google/android/exoplayer2/drm/DrmSession.java
mobile/android/thirdparty/com/google/android/exoplayer2/drm/DrmSessionManager.java
mobile/android/thirdparty/com/google/android/exoplayer2/drm/ExoMediaCrypto.java
mobile/android/thirdparty/com/google/android/exoplayer2/drm/ExoMediaDrm.java
mobile/android/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java
mobile/android/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
mobile/android/thirdparty/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java
mobile/android/thirdparty/com/google/android/exoplayer2/drm/KeysExpiredException.java
mobile/android/thirdparty/com/google/android/exoplayer2/drm/MediaDrmCallback.java
mobile/android/thirdparty/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java
mobile/android/thirdparty/com/google/android/exoplayer2/drm/UnsupportedDrmException.java
mobile/android/thirdparty/com/google/android/exoplayer2/drm/WidevineUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/Extractor.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/PositionHolder.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/flv/TagPayloadReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mkv/VarintReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/DefaultSampleValues.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/NalUnitTargetBuffer.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionPayloadReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.java
mobile/android/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java
mobile/android/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
mobile/android/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
mobile/android/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java
mobile/android/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/Metadata.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderException.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/MetadataInputBuffer.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/MetadataRenderer.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceNullCommand.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java
mobile/android/thirdparty/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/BehindLiveWindowException.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/ClippingMediaSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/LoopingMediaSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/MediaPeriod.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/MediaSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/MergingMediaPeriod.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/MergingMediaSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/SampleStream.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/SequenceableLoader.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/TrackGroup.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/TrackGroupArray.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/UnrecognizedInputFormatException.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkHolder.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/DashChunkSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/DashMediaSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/DashSegmentIndex.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/Period.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/RangedUri.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/Representation.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/UrlTemplate.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/UtcTimingElement.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/Aes128DataSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStream.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/WebvttExtractor.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java
mobile/android/thirdparty/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/CaptionStyleCompat.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/Cue.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/SimpleSubtitleOutputBuffer.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/Subtitle.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderException.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/SubtitleInputBuffer.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/TextRenderer.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Cue.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Decoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/cea/CeaDecoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/cea/CeaSubtitle.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/cea/CeaUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/subrip/SubripDecoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/subrip/SubripSubtitle.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlNode.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRegion.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRenderUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlStyle.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gSubtitle.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/CssParser.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttSubtitle.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCue.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java
mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveVideoTrackSelection.java
mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java
mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java
mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelection.java
mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java
mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelector.java
mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/Allocation.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/Allocator.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/AssetDataSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/BandwidthMeter.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/ContentDataSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DataSink.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DataSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DataSourceException.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DataSourceInputStream.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DataSpec.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DefaultAllocator.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/FileDataSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/FileDataSourceFactory.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/HttpDataSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/Loader.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/LoaderErrorThrower.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/ParsingLoadable.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/RawResourceDataSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/TeeDataSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/TransferListener.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/UdpDataSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/Cache.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheSpan.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContent.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCache.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.java
mobile/android/thirdparty/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/Assertions.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/AtomicFile.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/Clock.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/ColorParser.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/ConditionVariable.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/FlacStreamInfo.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/LibraryLoader.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/LongArray.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/MediaClock.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/MimeTypes.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/NalUnitUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/ParsableBitArray.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/ParsableByteArray.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/Predicate.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/PriorityHandlerThread.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/PriorityTaskManager.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/ReusableBufferedOutputStream.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/SlidingPercentile.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/StandaloneMediaClock.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/SystemClock.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/TimestampAdjuster.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/TraceUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/UriUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/Util.java
mobile/android/thirdparty/com/google/android/exoplayer2/util/XmlPullParserUtil.java
mobile/android/thirdparty/com/google/android/exoplayer2/video/AvcConfig.java
mobile/android/thirdparty/com/google/android/exoplayer2/video/HevcConfig.java
mobile/android/thirdparty/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
mobile/android/thirdparty/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java
mobile/android/thirdparty/com/google/android/exoplayer2/video/VideoRendererEventListener.java
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/BaseRenderer.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 com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MediaClock;
+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 RendererConfiguration configuration;
+  private int index;
+  private int state;
+  private SampleStream stream;
+  private long streamOffsetUs;
+  private boolean readEndOfStream;
+  private boolean streamIsFinal;
+
+  /**
+   * @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;
+    readEndOfStream = true;
+  }
+
+  @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
+  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;
+    readEndOfStream = false;
+    streamOffsetUs = offsetUs;
+    onStreamChanged(formats);
+  }
+
+  @Override
+  public final SampleStream getStream() {
+    return stream;
+  }
+
+  @Override
+  public final boolean hasReadStreamToEnd() {
+    return readEndOfStream;
+  }
+
+  @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;
+    readEndOfStream = 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;
+    onDisabled();
+    stream = null;
+    streamIsFinal = false;
+  }
+
+  // RendererCapabilities implementation.
+
+  @Override
+  public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
+    return ADAPTIVE_NOT_SUPPORTED;
+  }
+
+  // ExoPlayerComponent implementation.
+
+  @Override
+  public void handleMessage(int what, 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.
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+    // Do nothing.
+  }
+
+  /**
+   * Called when the position is reset. This occurs when the renderer is enabled after
+   * {@link #onStreamChanged(Format[])} has been called, and also when a position discontinuity
+   * is encountered.
+   * <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.
+  }
+
+  // Methods to be called by subclasses.
+
+  /**
+   * Returns the configuration set when the renderer was most recently enabled.
+   */
+  protected final RendererConfiguration getConfiguration() {
+    return configuration;
+  }
+
+  /**
+   * Returns the index of the renderer within the player.
+   */
+  protected final int getIndex() {
+    return index;
+  }
+
+  /**
+   * 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. May be null if the
+   *     caller requires that the format of the stream be read even if it's not changing.
+   * @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) {
+    int result = stream.readData(formatHolder, buffer);
+    if (result == C.RESULT_BUFFER_READ) {
+      if (buffer.isEndOfStream()) {
+        readEndOfStream = true;
+        return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ;
+      }
+      buffer.timeUs += streamOffsetUs;
+    } 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;
+  }
+
+  /**
+   * Returns whether the upstream source is ready.
+   *
+   * @return Whether the source is ready.
+   */
+  protected final boolean isSourceReady() {
+    return readEndOfStream ? streamIsFinal : stream.isReady();
+  }
+
+  /**
+   * Attempts to skip to the keyframe before the specified time.
+   *
+   * @param timeUs The specified time.
+   */
+  protected void skipToKeyframeBefore(long timeUs) {
+    stream.skipToKeyframeBefore(timeUs);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/C.java
@@ -0,0 +1,564 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.MediaCodec;
+import android.support.annotation.IntDef;
+import android.view.Surface;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.UUID;
+
+/**
+ * Defines constants used by the library.
+ */
+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;
+
+  /**
+   * 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 name of the UTF-8 charset.
+   */
+  public static final String UTF8_NAME = "UTF-8";
+
+  /**
+   * Crypto modes for a codec.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({CRYPTO_MODE_UNENCRYPTED, CRYPTO_MODE_AES_CTR, CRYPTO_MODE_AES_CBC})
+  public @interface CryptoMode {}
+  /**
+   * @see MediaCodec#CRYPTO_MODE_UNENCRYPTED
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED;
+  /**
+   * @see MediaCodec#CRYPTO_MODE_AES_CTR
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR;
+  /**
+   * @see MediaCodec#CRYPTO_MODE_AES_CBC
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC;
+
+  /**
+   * Represents an unset {@link android.media.AudioTrack} session identifier. Equal to
+   * {@link AudioManager#AUDIO_SESSION_ID_GENERATE}.
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE;
+
+  /**
+   * Represents an audio encoding, or an invalid or unset value.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
+      ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_AC3, ENCODING_E_AC3, ENCODING_DTS,
+      ENCODING_DTS_HD})
+  public @interface Encoding {}
+
+  /**
+   * Represents a PCM audio encoding, or an invalid or unset value.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
+      ENCODING_PCM_24BIT, ENCODING_PCM_32BIT})
+  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;
+  /**
+   * PCM encoding with 24 bits per sample.
+   */
+  public static final int ENCODING_PCM_24BIT = 0x80000000;
+  /**
+   * PCM encoding with 32 bits per sample.
+   */
+  public static final int ENCODING_PCM_32BIT = 0x40000000;
+  /**
+   * @see AudioFormat#ENCODING_AC3
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
+  /**
+   * @see AudioFormat#ENCODING_E_AC3
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
+  /**
+   * @see AudioFormat#ENCODING_DTS
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS;
+  /**
+   * @see AudioFormat#ENCODING_DTS_HD
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD;
+
+  /**
+   * @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND
+   */
+  @SuppressWarnings({"InlinedApi", "deprecation"})
+  public static final int CHANNEL_OUT_7POINT1_SURROUND = Util.SDK_INT < 23
+      ? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
+
+  /**
+   * Stream types for an {@link android.media.AudioTrack}.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION, STREAM_TYPE_RING,
+      STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL})
+  public @interface StreamType {}
+  /**
+   * @see AudioManager#STREAM_ALARM
+   */
+  public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM;
+  /**
+   * @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;
+  /**
+   * The default stream type used by audio renderers.
+   */
+  public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
+
+  /**
+   * Flags which can apply to a buffer containing a media sample.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef(flag = true, value = {BUFFER_FLAG_KEY_FRAME, BUFFER_FLAG_END_OF_STREAM,
+      BUFFER_FLAG_ENCRYPTED, BUFFER_FLAG_DECODE_ONLY})
+  public @interface BufferFlags {}
+  /**
+   * Indicates that a buffer holds a synchronization sample.
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int BUFFER_FLAG_KEY_FRAME = MediaCodec.BUFFER_FLAG_KEY_FRAME;
+  /**
+   * Flag for empty buffers that signal that the end of the stream was reached.
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
+  /**
+   * Indicates that a buffer is (at least partially) encrypted.
+   */
+  public static final int BUFFER_FLAG_ENCRYPTED = 0x40000000;
+  /**
+   * Indicates that a buffer should be decoded but not rendered.
+   */
+  public static final int BUFFER_FLAG_DECODE_ONLY = 0x80000000;
+
+  /**
+   * Video scaling modes for {@link MediaCodec}-based {@link Renderer}s.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef(value = {VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING})
+  public @interface VideoScalingMode {}
+  /**
+   * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT =
+      MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT;
+  /**
+   * @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING =
+      MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING;
+  /**
+   * 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.
+   */
+  @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 = 2;
+  /**
+   * Indicates that the player may choose to play the track in absence of an explicit user
+   * preference.
+   */
+  public static final int SELECTION_FLAG_AUTOSELECT = 4;
+
+  /**
+   * Represents a streaming or other media type.
+   */
+  @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;
+  /**
+   * 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;
+  /**
+   * 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;
+
+  /**
+   * A default size in bytes for a video buffer.
+   */
+  public static final int DEFAULT_VIDEO_BUFFER_SIZE = 200 * DEFAULT_BUFFER_SEGMENT_SIZE;
+
+  /**
+   * A default size in bytes for an audio buffer.
+   */
+  public static final int DEFAULT_AUDIO_BUFFER_SIZE = 54 * DEFAULT_BUFFER_SEGMENT_SIZE;
+
+  /**
+   * A default size in bytes for a text buffer.
+   */
+  public static final int DEFAULT_TEXT_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE;
+
+  /**
+   * A default size in bytes for a metadata buffer.
+   */
+  public static final int DEFAULT_METADATA_BUFFER_SIZE = 2 * DEFAULT_BUFFER_SEGMENT_SIZE;
+
+  /**
+   * A default size in bytes for a muxed buffer (e.g. containing video, audio and text).
+   */
+  public static final int DEFAULT_MUXED_BUFFER_SIZE = DEFAULT_VIDEO_BUFFER_SIZE
+      + DEFAULT_AUDIO_BUFFER_SIZE + DEFAULT_TEXT_BUFFER_SIZE;
+
+  /**
+   * 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 Widevine DRM scheme.
+   * <p></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#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
+   * should be the target {@link Surface}, or null.
+   */
+  public static final int MSG_SET_SURFACE = 1;
+
+  /**
+   * A type of a message that can be passed to an audio {@link Renderer} via
+   * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
+   * should be a {@link Float} with 0 being silence and 1 being unity gain.
+   */
+  public static final int MSG_SET_VOLUME = 2;
+
+  /**
+   * A type of a message that can be passed to an audio {@link Renderer} via
+   * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
+   * should be a {@link android.media.PlaybackParams}, or null, which will be used to configure the
+   * underlying {@link android.media.AudioTrack}. The message object should not be modified by the
+   * caller after it has been passed
+   */
+  public static final int MSG_SET_PLAYBACK_PARAMS = 3;
+
+  /**
+   * A type of a message that can be passed to an audio {@link Renderer} via
+   * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
+   * should be one of the integer stream types in {@link C.StreamType}, and will specify the stream
+   * type of the underlying {@link android.media.AudioTrack}. See also
+   * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}. If the stream type
+   * is not set, audio renderers use {@link #STREAM_TYPE_DEFAULT}.
+   * <p>
+   * Note that when the stream type changes, the AudioTrack must be reinitialized, which can
+   * introduce a brief gap in audio output. Note also that tracks in the same audio session must
+   * share the same routing, so a new audio session id will be generated.
+   */
+  public static final int MSG_SET_STREAM_TYPE = 4;
+
+  /**
+   * The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer}
+   * via {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message
+   * object should be one of the integer scaling modes in {@link C.VideoScalingMode}.
+   * <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 = 5;
+
+  /**
+   * Applications or extensions may define custom {@code MSG_*} constants greater than or equal to
+   * this value.
+   */
+  public static final int MSG_CUSTOM_BASE = 10000;
+
+  /**
+   * The stereo mode for 360/3D/VR videos.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({Format.NO_VALUE, STEREO_MODE_MONO, STEREO_MODE_TOP_BOTTOM, STEREO_MODE_LEFT_RIGHT})
+  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;
+
+  /**
+   * Converts a time in microseconds to the corresponding time in milliseconds, preserving
+   * {@link #TIME_UNSET} values.
+   *
+   * @param timeUs The time in microseconds.
+   * @return The corresponding time in milliseconds.
+   */
+  public static long usToMs(long timeUs) {
+    return timeUs == TIME_UNSET ? TIME_UNSET : (timeUs / 1000);
+  }
+
+  /**
+   * Converts a time in milliseconds to the corresponding time in microseconds, preserving
+   * {@link #TIME_UNSET} values.
+   *
+   * @param timeMs The time in milliseconds.
+   * @return The corresponding time in microseconds.
+   */
+  public static long msToUs(long timeMs) {
+    return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000);
+  }
+
+  /**
+   * Returns a newly generated {@link android.media.AudioTrack} session identifier.
+   */
+  @TargetApi(21)
+  public static int generateAudioSessionIdV21(Context context) {
+    return ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE))
+        .generateAudioSessionId();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/DefaultLoadControl.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 com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DefaultAllocator;
+import com.google.android.exoplayer2.util.PriorityTaskManager;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * The default {@link LoadControl} implementation.
+ */
+public final class DefaultLoadControl implements LoadControl {
+
+  /**
+   * 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 = 30000;
+
+  /**
+   * 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;
+
+  /**
+   * Priority for media loading.
+   */
+  public static final int LOADING_PRIORITY = 0;
+
+  private static final int ABOVE_HIGH_WATERMARK = 0;
+  private static final int BETWEEN_WATERMARKS = 1;
+  private static final int BELOW_LOW_WATERMARK = 2;
+
+  private final DefaultAllocator allocator;
+
+  private final long minBufferUs;
+  private final long maxBufferUs;
+  private final long bufferForPlaybackUs;
+  private final long bufferForPlaybackAfterRebufferUs;
+  private final PriorityTaskManager priorityTaskManager;
+
+  private int targetBufferSize;
+  private boolean isBuffering;
+
+  /**
+   * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
+   */
+  public DefaultLoadControl() {
+    this(new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE));
+  }
+
+  /**
+   * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
+   *
+   * @param allocator The {@link DefaultAllocator} used by the loader.
+   */
+  public DefaultLoadControl(DefaultAllocator allocator) {
+    this(allocator, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS,
+        DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
+  }
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param allocator The {@link DefaultAllocator} used by the loader.
+   * @param minBufferMs The minimum duration of media that the player will attempt to ensure is
+   *     buffered at all times, in milliseconds.
+   * @param maxBufferMs The maximum duration of media that the player will attempt buffer, in
+   *     milliseconds.
+   * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or
+   *     resume following a user action such as a seek, in milliseconds.
+   * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
+   *     playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
+   *     buffer depletion rather than a user action.
+   */
+  public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
+      long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) {
+    this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs,
+        null);
+  }
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param allocator The {@link DefaultAllocator} used by the loader.
+   * @param minBufferMs The minimum duration of media that the player will attempt to ensure is
+   *     buffered at all times, in milliseconds.
+   * @param maxBufferMs The maximum duration of media that the player will attempt buffer, in
+   *     milliseconds.
+   * @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or
+   *     resume following a user action such as a seek, in milliseconds.
+   * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
+   *     playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
+   *     buffer depletion rather than a user action.
+   * @param priorityTaskManager If not null, registers itself as a task with priority
+   *     {@link #LOADING_PRIORITY} during loading periods, and unregisters itself during draining
+   *     periods.
+   */
+  public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
+      long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs,
+      PriorityTaskManager priorityTaskManager) {
+    this.allocator = allocator;
+    minBufferUs = minBufferMs * 1000L;
+    maxBufferUs = maxBufferMs * 1000L;
+    bufferForPlaybackUs = bufferForPlaybackMs * 1000L;
+    bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L;
+    this.priorityTaskManager = priorityTaskManager;
+  }
+
+  @Override
+  public void onPrepared() {
+    reset(false);
+  }
+
+  @Override
+  public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
+      TrackSelectionArray trackSelections) {
+    targetBufferSize = 0;
+    for (int i = 0; i < renderers.length; i++) {
+      if (trackSelections.get(i) != null) {
+        targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType());
+      }
+    }
+    allocator.setTargetBufferSize(targetBufferSize);
+  }
+
+  @Override
+  public void onStopped() {
+    reset(true);
+  }
+
+  @Override
+  public void onReleased() {
+    reset(true);
+  }
+
+  @Override
+  public Allocator getAllocator() {
+    return allocator;
+  }
+
+  @Override
+  public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) {
+    long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs;
+    return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs;
+  }
+
+  @Override
+  public boolean shouldContinueLoading(long bufferedDurationUs) {
+    int bufferTimeState = getBufferTimeState(bufferedDurationUs);
+    boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
+    boolean wasBuffering = isBuffering;
+    isBuffering = bufferTimeState == BELOW_LOW_WATERMARK
+        || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached);
+    if (priorityTaskManager != null && isBuffering != wasBuffering) {
+      if (isBuffering) {
+        priorityTaskManager.add(LOADING_PRIORITY);
+      } else {
+        priorityTaskManager.remove(LOADING_PRIORITY);
+      }
+    }
+    return isBuffering;
+  }
+
+  private int getBufferTimeState(long bufferedDurationUs) {
+    return bufferedDurationUs > maxBufferUs ? ABOVE_HIGH_WATERMARK
+        : (bufferedDurationUs < minBufferUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS);
+  }
+
+  private void reset(boolean resetAllocator) {
+    targetBufferSize = 0;
+    if (priorityTaskManager != null && isBuffering) {
+      priorityTaskManager.remove(LOADING_PRIORITY);
+    }
+    isBuffering = false;
+    if (resetAllocator) {
+      allocator.reset();
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/ExoPlaybackException.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 com.google.android.exoplayer2;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+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.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED})
+  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 type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and
+   * {@link #TYPE_UNEXPECTED}.
+   */
+  @Type
+  public final int type;
+
+  /**
+   * If {@link #type} is {@link #TYPE_RENDERER}, this is the index of the renderer.
+   */
+  public final int rendererIndex;
+
+  /**
+   * Creates an instance of type {@link #TYPE_RENDERER}.
+   *
+   * @param cause The cause of the failure.
+   * @param rendererIndex The index of the renderer in which the failure occurred.
+   * @return The created instance.
+   */
+  public static ExoPlaybackException createForRenderer(Exception cause, int rendererIndex) {
+    return new ExoPlaybackException(TYPE_RENDERER, null, cause, rendererIndex);
+  }
+
+  /**
+   * 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, null, cause, C.INDEX_UNSET);
+  }
+
+  /**
+   * Creates an instance of type {@link #TYPE_UNEXPECTED}.
+   *
+   * @param cause The cause of the failure.
+   * @return The created instance.
+   */
+  /* package */ static ExoPlaybackException createForUnexpected(RuntimeException cause) {
+    return new ExoPlaybackException(TYPE_UNEXPECTED, null, cause, C.INDEX_UNSET);
+  }
+
+  private ExoPlaybackException(@Type int type, String message, Throwable cause,
+      int rendererIndex) {
+    super(message, cause);
+    this.type = type;
+    this.rendererIndex = rendererIndex;
+  }
+
+  /**
+   * 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) getCause();
+  }
+
+  /**
+   * 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) getCause();
+  }
+
+  /**
+   * 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) getCause();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/ExoPlayer.java
@@ -0,0 +1,466 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
+import com.google.android.exoplayer2.metadata.MetadataRenderer;
+import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
+import com.google.android.exoplayer2.source.ExtractorMediaSource;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MergingMediaSource;
+import com.google.android.exoplayer2.source.SingleSampleMediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.source.dash.DashMediaSource;
+import com.google.android.exoplayer2.source.hls.HlsMediaSource;
+import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
+import com.google.android.exoplayer2.text.TextRenderer;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
+
+/**
+ * An extensible media player exposing traditional high-level media player functionality, such as
+ * the ability to buffer media, play, pause and seek. Instances can be obtained from
+ * {@link ExoPlayerFactory}.
+ *
+ * <h3>Player composition</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} at the start
+ *   of playback. The library provides default implementations for regular media files
+ *   ({@link ExtractorMediaSource}), DASH ({@link DashMediaSource}), SmoothStreaming
+ *   ({@link SsMediaSource}) and HLS ({@link HlsMediaSource}), implementations for merging
+ *   ({@link MergingMediaSource}) and concatenating ({@link ConcatenatingMediaSource}) other
+ *   MediaSources, and an implementation for loading single samples
+ *   ({@link SingleSampleMediaSource}) most often used for side-loaded subtitle and closed
+ *   caption files.</li>
+ *   <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 of its corresponding type from the MediaSource being played. Renderers are
+ *   injected when the player is created.</li>
+ *   <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>
+ *   <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.</li>
+ * </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 use 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>
+ * <p align="center">
+ *   <img src="doc-files/exoplayer-threading-model.svg" alt="ExoPlayer's threading model">
+ * </p>
+ *
+ * <ul>
+ * <li>It is recommended that ExoPlayer instances are created and accessed from a single application
+ * thread. The application's main thread is ideal. Accessing an instance from multiple threads is
+ * discouraged, however if an application does wish to do this then it may do so provided that it
+ * ensures accesses are synchronized.</li>
+ * <li>Registered listeners are called on the thread that created the ExoPlayer instance.</li>
+ * <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>
+ * <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>
+ * <li>Injected player components may use additional background threads. For example a MediaSource
+ * may use a background thread to load data. These are implementation specific.</li>
+ * </ul>
+ */
+public interface ExoPlayer {
+
+  /**
+   * Listener of changes in player state.
+   */
+  interface EventListener {
+
+    /**
+     * 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. The will <em>not</em> be reported via a separate call to
+     * {@link #onPositionDiscontinuity()}.
+     *
+     * @param timeline The latest timeline. Never null, but may be empty.
+     * @param manifest The latest manifest. May be null.
+     */
+    void onTimelineChanged(Timeline timeline, Object manifest);
+
+    /**
+     * Called when the available or selected tracks change.
+     *
+     * @param trackGroups The available tracks. Never null, but may be of length zero.
+     * @param trackSelections The track selections for each {@link Renderer}. Never null and always
+     *     of length {@link #getRendererCount()}, but may contain null elements.
+     */
+    void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections);
+
+    /**
+     * Called when the player starts or stops loading the source.
+     *
+     * @param isLoading Whether the source is currently being loaded.
+     */
+    void onLoadingChanged(boolean isLoading);
+
+    /**
+     * Called when the value returned from either {@link #getPlayWhenReady()} or
+     * {@link #getPlaybackState()} changes.
+     *
+     * @param playWhenReady Whether playback will proceed when ready.
+     * @param playbackState One of the {@code STATE} constants defined in the {@link ExoPlayer}
+     *     interface.
+     */
+    void onPlayerStateChanged(boolean playWhenReady, int playbackState);
+
+    /**
+     * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE}
+     * immediately after this method is called. The player instance can still be used, and
+     * {@link #release()} must still be called on the player should it no longer be required.
+     *
+     * @param error The error.
+     */
+    void onPlayerError(ExoPlaybackException error);
+
+    /**
+     * Called when a position discontinuity occurs without a change to the timeline. A position
+     * discontinuity occurs when the current window or period index changes (as a result of playback
+     * transitioning from one period in the timeline to the next), or when the playback position
+     * jumps within the period currently being played (as a result of a seek being performed, or
+     * when the source introduces a discontinuity internally).
+     * <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, Object)} is called in this case.
+     */
+    void onPositionDiscontinuity();
+
+  }
+
+  /**
+   * A component of an {@link ExoPlayer} that can receive messages on the playback thread.
+   * <p>
+   * Messages can be delivered to a component via {@link #sendMessages} and
+   * {@link #blockingSendMessages}.
+   */
+  interface ExoPlayerComponent {
+
+    /**
+     * Handles a message delivered to the component. Called on the playback thread.
+     *
+     * @param messageType The message type.
+     * @param message The message.
+     * @throws ExoPlaybackException If an error occurred whilst handling the message.
+     */
+    void handleMessage(int messageType, Object message) throws ExoPlaybackException;
+
+  }
+
+  /**
+   * Defines a message and a target {@link ExoPlayerComponent} to receive it.
+   */
+  final class ExoPlayerMessage {
+
+    /**
+     * The target to receive the message.
+     */
+    public final ExoPlayerComponent target;
+    /**
+     * The type of the message.
+     */
+    public final int messageType;
+    /**
+     * The message.
+     */
+    public final Object message;
+
+    /**
+     * @param target The target of the message.
+     * @param messageType The message type.
+     * @param message The message.
+     */
+    public ExoPlayerMessage(ExoPlayerComponent target, int messageType, Object message) {
+      this.target = target;
+      this.messageType = messageType;
+      this.message = message;
+    }
+
+  }
+
+  /**
+   * The player does not have a source to play, so it is neither buffering nor ready to play.
+   */
+  int STATE_IDLE = 1;
+  /**
+   * The player not able to immediately play from the current position. The cause is
+   * {@link Renderer} specific, but this state typically occurs when more data needs to be
+   * loaded to be ready to play, or more data needs to be buffered for playback to resume.
+   */
+  int STATE_BUFFERING = 2;
+  /**
+   * The player is able to immediately play from the current position. The player will be playing if
+   * {@link #getPlayWhenReady()} returns true, and paused otherwise.
+   */
+  int STATE_READY = 3;
+  /**
+   * The player has finished playing the media.
+   */
+  int STATE_ENDED = 4;
+
+  /**
+   * Register a listener to receive events from the player. The listener's methods will be called on
+   * the thread that was used to construct the player.
+   *
+   * @param listener The listener to register.
+   */
+  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 state of the player.
+   *
+   * @return One of the {@code STATE} constants defined in this interface.
+   */
+  int getPlaybackState();
+
+  /**
+   * Prepares the player to play the provided {@link MediaSource}. Equivalent to
+   * {@code prepare(mediaSource, true, true)}.
+   */
+  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);
+
+  /**
+   * 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();
+
+  /**
+   * 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 source passed to {@link #prepare(MediaSource)}. For live streams it will typically
+   * be the live edge of the window. For other streams it will typically be the start of the window.
+   */
+  void seekToDefaultPosition();
+
+  /**
+   * Seeks to the default position associated with the specified window. The position can depend on
+   * the type of source passed to {@link #prepare(MediaSource)}. For live streams it will typically
+   * be the live edge of the window. For other streams it will typically be the start of the window.
+   *
+   * @param windowIndex The index of the window whose associated default position should be seeked
+   *     to.
+   */
+  void seekToDefaultPosition(int windowIndex);
+
+  /**
+   * Seeks to a position specified in milliseconds in the current window.
+   *
+   * @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.
+   */
+  void seekTo(int windowIndex, long positionMs);
+
+  /**
+   * Stops playback. 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();
+
+  /**
+   * Releases the player. This method must be called when the player is no longer required. The
+   * player must not be used after calling this method.
+   */
+  void release();
+
+  /**
+   * Sends messages to their target components. The messages are delivered on the playback thread.
+   * If a component throws an {@link ExoPlaybackException} then it is propagated out of the player
+   * as an error.
+   *
+   * @param messages The messages to be sent.
+   */
+  void sendMessages(ExoPlayerMessage... messages);
+
+  /**
+   * Variant of {@link #sendMessages(ExoPlayerMessage...)} that blocks until after the messages have
+   * been delivered.
+   *
+   * @param messages The messages to be sent.
+   */
+  void blockingSendMessages(ExoPlayerMessage... messages);
+
+  /**
+   * Returns the number of renderers.
+   */
+  int getRendererCount();
+
+  /**
+   * Returns the track type that the renderer at a given index handles.
+   *
+   * @see Renderer#getTrackType()
+   * @param index The index of the renderer.
+   * @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
+   */
+  int getRendererType(int index);
+
+  /**
+   * Returns the available track groups.
+   */
+  TrackGroupArray getCurrentTrackGroups();
+
+  /**
+   * Returns the current track selections for each renderer.
+   */
+  TrackSelectionArray getCurrentTrackSelections();
+
+  /**
+   * Returns the current manifest. The type depends on the {@link MediaSource} passed to
+   * {@link #prepare}. May be null.
+   */
+  Object getCurrentManifest();
+
+  /**
+   * Returns the current {@link Timeline}. Never null, but may be empty.
+   */
+  Timeline getCurrentTimeline();
+
+  /**
+   * Returns the index of the period currently being played.
+   */
+  int getCurrentPeriodIndex();
+
+  /**
+   * Returns the index of the window currently being played.
+   */
+  int getCurrentWindowIndex();
+
+  /**
+   * Returns the duration of the current window in milliseconds, or {@link C#TIME_UNSET} if the
+   * duration is not known.
+   */
+  long getDuration();
+
+  /**
+   * Returns the playback position in the current window, in milliseconds.
+   */
+  long getCurrentPosition();
+
+  /**
+   * Returns an estimate of the position in the current window up to which data is buffered, in
+   * milliseconds.
+   */
+  long getBufferedPosition();
+
+  /**
+   * Returns an estimate of the percentage in the current window up to which data is buffered, or 0
+   * if no estimate is available.
+   */
+  int getBufferedPercentage();
+
+  /**
+   * Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is
+   * empty.
+   *
+   * @see Timeline.Window#isDynamic
+   */
+  boolean isCurrentWindowDynamic();
+
+  /**
+   * Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is
+   * empty.
+   *
+   * @see Timeline.Window#isSeekable
+   */
+  boolean isCurrentWindowSeekable();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/ExoPlayerFactory.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.content.Context;
+import android.os.Looper;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+
+/**
+ * A factory for {@link ExoPlayer} instances.
+ */
+public final class ExoPlayerFactory {
+
+  /**
+   * 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;
+
+  private ExoPlayerFactory() {}
+
+  /**
+   * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+   * {@link Looper}.
+   *
+   * @param context A {@link Context}.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   * @param loadControl The {@link LoadControl} that will be used by the instance.
+   */
+  public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
+      LoadControl loadControl) {
+    return newSimpleInstance(context, trackSelector, loadControl, null);
+  }
+
+  /**
+   * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+   * {@link Looper}. Available extension renderers are not used.
+   *
+   * @param context A {@link Context}.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   * @param loadControl The {@link LoadControl} that will be used by the instance.
+   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+   *     will not be used for DRM protected playbacks.
+   */
+  public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
+      LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
+    return newSimpleInstance(context, trackSelector, loadControl,
+        drmSessionManager, SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF);
+  }
+
+  /**
+   * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+   * {@link Looper}.
+   *
+   * @param context A {@link Context}.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   * @param loadControl The {@link LoadControl} that will be used by the instance.
+   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+   *     will not be used for DRM protected playbacks.
+   * @param extensionRendererMode The extension renderer mode, which determines if and how available
+   *     extension renderers are used. Note that extensions must be included in the application
+   *     build for them to be considered available.
+   */
+  public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
+      LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode) {
+    return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager,
+        extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
+  }
+
+  /**
+   * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
+   * {@link Looper}.
+   *
+   * @param context A {@link Context}.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   * @param loadControl The {@link LoadControl} that will be used by the instance.
+   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
+   *     will not be used for DRM protected playbacks.
+   * @param extensionRendererMode The extension renderer mode, which determines if and how available
+   *     extension renderers are used. Note that extensions must be included in the application
+   *     build for them to be considered available.
+   * @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to
+   *     seamlessly join an ongoing playback.
+   */
+  public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
+      LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode,
+      long allowedVideoJoiningTimeMs) {
+    return new SimpleExoPlayer(context, trackSelector, loadControl, drmSessionManager,
+        extensionRendererMode, allowedVideoJoiningTimeMs);
+  }
+
+  /**
+   * Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated
+   * {@link Looper}.
+   *
+   * @param renderers The {@link Renderer}s that will be used by the instance.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   */
+  public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector) {
+    return newInstance(renderers, trackSelector, new DefaultLoadControl());
+  }
+
+  /**
+   * Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated
+   * {@link Looper}.
+   *
+   * @param renderers The {@link Renderer}s that will be used by the instance.
+   * @param trackSelector The {@link TrackSelector} that will be used by the instance.
+   * @param loadControl The {@link LoadControl} that will be used by the instance.
+   */
+  public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector,
+      LoadControl loadControl) {
+    return new ExoPlayerImpl(renderers, trackSelector, loadControl);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -0,0 +1,389 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo;
+import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * An {@link ExoPlayer} implementation. Instances can be obtained from {@link ExoPlayerFactory}.
+ */
+/* package */ final class ExoPlayerImpl implements ExoPlayer {
+
+  private static final String TAG = "ExoPlayerImpl";
+
+  private final Renderer[] renderers;
+  private final TrackSelector trackSelector;
+  private final TrackSelectionArray emptyTrackSelections;
+  private final Handler eventHandler;
+  private final ExoPlayerImplInternal internalPlayer;
+  private final CopyOnWriteArraySet<EventListener> listeners;
+  private final Timeline.Window window;
+  private final Timeline.Period period;
+
+  private boolean tracksSelected;
+  private boolean playWhenReady;
+  private int playbackState;
+  private int pendingSeekAcks;
+  private boolean isLoading;
+  private Timeline timeline;
+  private Object manifest;
+  private TrackGroupArray trackGroups;
+  private TrackSelectionArray trackSelections;
+
+  // 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 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.
+   */
+  @SuppressLint("HandlerLeak")
+  public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) {
+    Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION + " [" + Util.DEVICE_DEBUG_INFO + "]");
+    Assertions.checkState(renderers.length > 0);
+    this.renderers = Assertions.checkNotNull(renderers);
+    this.trackSelector = Assertions.checkNotNull(trackSelector);
+    this.playWhenReady = false;
+    this.playbackState = STATE_IDLE;
+    this.listeners = new CopyOnWriteArraySet<>();
+    emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]);
+    timeline = Timeline.EMPTY;
+    window = new Timeline.Window();
+    period = new Timeline.Period();
+    trackGroups = TrackGroupArray.EMPTY;
+    trackSelections = emptyTrackSelections;
+    eventHandler = new Handler() {
+      @Override
+      public void handleMessage(Message msg) {
+        ExoPlayerImpl.this.handleEvent(msg);
+      }
+    };
+    playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0);
+    internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady,
+        eventHandler, playbackInfo, this);
+  }
+
+  @Override
+  public void addListener(EventListener listener) {
+    listeners.add(listener);
+  }
+
+  @Override
+  public void removeListener(EventListener listener) {
+    listeners.remove(listener);
+  }
+
+  @Override
+  public int getPlaybackState() {
+    return playbackState;
+  }
+
+  @Override
+  public void prepare(MediaSource mediaSource) {
+    prepare(mediaSource, true, true);
+  }
+
+  @Override
+  public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
+    if (resetState) {
+      if (!timeline.isEmpty() || manifest != null) {
+        timeline = Timeline.EMPTY;
+        manifest = null;
+        for (EventListener listener : listeners) {
+          listener.onTimelineChanged(timeline, manifest);
+        }
+      }
+      if (tracksSelected) {
+        tracksSelected = false;
+        trackGroups = TrackGroupArray.EMPTY;
+        trackSelections = emptyTrackSelections;
+        trackSelector.onSelectionActivated(null);
+        for (EventListener listener : listeners) {
+          listener.onTracksChanged(trackGroups, trackSelections);
+        }
+      }
+    }
+    internalPlayer.prepare(mediaSource, resetPosition);
+  }
+
+  @Override
+  public void setPlayWhenReady(boolean playWhenReady) {
+    if (this.playWhenReady != playWhenReady) {
+      this.playWhenReady = playWhenReady;
+      internalPlayer.setPlayWhenReady(playWhenReady);
+      for (EventListener listener : listeners) {
+        listener.onPlayerStateChanged(playWhenReady, playbackState);
+      }
+    }
+  }
+
+  @Override
+  public boolean getPlayWhenReady() {
+    return playWhenReady;
+  }
+
+  @Override
+  public boolean isLoading() {
+    return isLoading;
+  }
+
+  @Override
+  public void seekToDefaultPosition() {
+    seekToDefaultPosition(getCurrentWindowIndex());
+  }
+
+  @Override
+  public void seekToDefaultPosition(int windowIndex) {
+    seekTo(windowIndex, C.TIME_UNSET);
+  }
+
+  @Override
+  public void seekTo(long positionMs) {
+    seekTo(getCurrentWindowIndex(), positionMs);
+  }
+
+  @Override
+  public void seekTo(int windowIndex, long positionMs) {
+    if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) {
+      throw new IllegalSeekPositionException(timeline, windowIndex, positionMs);
+    }
+    pendingSeekAcks++;
+    maskingWindowIndex = windowIndex;
+    if (positionMs == C.TIME_UNSET) {
+      maskingWindowPositionMs = 0;
+      internalPlayer.seekTo(timeline, windowIndex, C.TIME_UNSET);
+    } else {
+      maskingWindowPositionMs = positionMs;
+      internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs));
+      for (EventListener listener : listeners) {
+        listener.onPositionDiscontinuity();
+      }
+    }
+  }
+
+  @Override
+  public void stop() {
+    internalPlayer.stop();
+  }
+
+  @Override
+  public void release() {
+    internalPlayer.release();
+    eventHandler.removeCallbacksAndMessages(null);
+  }
+
+  @Override
+  public void sendMessages(ExoPlayerMessage... messages) {
+    internalPlayer.sendMessages(messages);
+  }
+
+  @Override
+  public void blockingSendMessages(ExoPlayerMessage... messages) {
+    internalPlayer.blockingSendMessages(messages);
+  }
+
+  @Override
+  public int getCurrentPeriodIndex() {
+    return playbackInfo.periodIndex;
+  }
+
+  @Override
+  public int getCurrentWindowIndex() {
+    if (timeline.isEmpty() || pendingSeekAcks > 0) {
+      return maskingWindowIndex;
+    } else {
+      return timeline.getPeriod(playbackInfo.periodIndex, period).windowIndex;
+    }
+  }
+
+  @Override
+  public long getDuration() {
+    if (timeline.isEmpty()) {
+      return C.TIME_UNSET;
+    }
+    return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
+  }
+
+  @Override
+  public long getCurrentPosition() {
+    if (timeline.isEmpty() || pendingSeekAcks > 0) {
+      return maskingWindowPositionMs;
+    } else {
+      timeline.getPeriod(playbackInfo.periodIndex, period);
+      return period.getPositionInWindowMs() + C.usToMs(playbackInfo.positionUs);
+    }
+  }
+
+  @Override
+  public long getBufferedPosition() {
+    // TODO - Implement this properly.
+    if (timeline.isEmpty() || pendingSeekAcks > 0) {
+      return maskingWindowPositionMs;
+    } else {
+      timeline.getPeriod(playbackInfo.periodIndex, period);
+      return period.getPositionInWindowMs() + C.usToMs(playbackInfo.bufferedPositionUs);
+    }
+  }
+
+  @Override
+  public int getBufferedPercentage() {
+    if (timeline.isEmpty()) {
+      return 0;
+    }
+    long bufferedPosition = getBufferedPosition();
+    long duration = getDuration();
+    return (bufferedPosition == C.TIME_UNSET || duration == C.TIME_UNSET) ? 0
+        : (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration);
+  }
+
+  @Override
+  public boolean isCurrentWindowDynamic() {
+    if (timeline.isEmpty()) {
+      return false;
+    }
+    return timeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
+  }
+
+  @Override
+  public boolean isCurrentWindowSeekable() {
+    if (timeline.isEmpty()) {
+      return false;
+    }
+    return timeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
+  }
+
+  @Override
+  public int getRendererCount() {
+    return renderers.length;
+  }
+
+  @Override
+  public int getRendererType(int index) {
+    return renderers[index].getTrackType();
+  }
+
+  @Override
+  public TrackGroupArray getCurrentTrackGroups() {
+    return trackGroups;
+  }
+
+  @Override
+  public TrackSelectionArray getCurrentTrackSelections() {
+    return trackSelections;
+  }
+
+  @Override
+  public Timeline getCurrentTimeline() {
+    return timeline;
+  }
+
+  @Override
+  public Object getCurrentManifest() {
+    return manifest;
+  }
+
+  // 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_STATE_CHANGED: {
+        playbackState = msg.arg1;
+        for (EventListener listener : listeners) {
+          listener.onPlayerStateChanged(playWhenReady, playbackState);
+        }
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_LOADING_CHANGED: {
+        isLoading = msg.arg1 != 0;
+        for (EventListener listener : listeners) {
+          listener.onLoadingChanged(isLoading);
+        }
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: {
+        TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj;
+        tracksSelected = true;
+        trackGroups = trackSelectorResult.groups;
+        trackSelections = trackSelectorResult.selections;
+        trackSelector.onSelectionActivated(trackSelectorResult.info);
+        for (EventListener listener : listeners) {
+          listener.onTracksChanged(trackGroups, trackSelections);
+        }
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_SEEK_ACK: {
+        if (--pendingSeekAcks == 0) {
+          playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj;
+          if (msg.arg1 != 0) {
+            for (EventListener listener : listeners) {
+              listener.onPositionDiscontinuity();
+            }
+          }
+        }
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: {
+        if (pendingSeekAcks == 0) {
+          playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj;
+          for (EventListener listener : listeners) {
+            listener.onPositionDiscontinuity();
+          }
+        }
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: {
+        SourceInfo sourceInfo = (SourceInfo) msg.obj;
+        timeline = sourceInfo.timeline;
+        manifest = sourceInfo.manifest;
+        playbackInfo = sourceInfo.playbackInfo;
+        pendingSeekAcks -= sourceInfo.seekAcks;
+        for (EventListener listener : listeners) {
+          listener.onTimelineChanged(timeline, manifest);
+        }
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_ERROR: {
+        ExoPlaybackException exception = (ExoPlaybackException) msg.obj;
+        for (EventListener listener : listeners) {
+          listener.onPlayerError(exception);
+        }
+        break;
+      }
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -0,0 +1,1536 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MediaClock;
+import com.google.android.exoplayer2.util.PriorityHandlerThread;
+import com.google.android.exoplayer2.util.StandaloneMediaClock;
+import com.google.android.exoplayer2.util.TraceUtil;
+import java.io.IOException;
+
+/**
+ * Implements the internal behavior of {@link ExoPlayerImpl}.
+ */
+/* package */ final class ExoPlayerImplInternal implements Handler.Callback,
+    MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener {
+
+  /**
+   * Playback position information which is read on the application's thread by
+   * {@link ExoPlayerImpl} and read/written internally on the player's thread.
+   */
+  public static final class PlaybackInfo {
+
+    public final int periodIndex;
+    public final long startPositionUs;
+
+    public volatile long positionUs;
+    public volatile long bufferedPositionUs;
+
+    public PlaybackInfo(int periodIndex, long startPositionUs) {
+      this.periodIndex = periodIndex;
+      this.startPositionUs = startPositionUs;
+      positionUs = startPositionUs;
+      bufferedPositionUs = startPositionUs;
+    }
+
+    public PlaybackInfo copyWithPeriodIndex(int periodIndex) {
+      PlaybackInfo playbackInfo = new PlaybackInfo(periodIndex, startPositionUs);
+      playbackInfo.positionUs = positionUs;
+      playbackInfo.bufferedPositionUs = bufferedPositionUs;
+      return playbackInfo;
+    }
+
+  }
+
+  public static final class SourceInfo {
+
+    public final Timeline timeline;
+    public final Object manifest;
+    public final PlaybackInfo playbackInfo;
+    public final int seekAcks;
+
+    public SourceInfo(Timeline timeline, Object manifest, PlaybackInfo playbackInfo, int seekAcks) {
+      this.timeline = timeline;
+      this.manifest = manifest;
+      this.playbackInfo = playbackInfo;
+      this.seekAcks = seekAcks;
+    }
+
+  }
+
+  private static final String TAG = "ExoPlayerImplInternal";
+
+  // External messages
+  public static final int MSG_STATE_CHANGED = 1;
+  public static final int MSG_LOADING_CHANGED = 2;
+  public static final int MSG_TRACKS_CHANGED = 3;
+  public static final int MSG_SEEK_ACK = 4;
+  public static final int MSG_POSITION_DISCONTINUITY = 5;
+  public static final int MSG_SOURCE_INFO_REFRESHED = 6;
+  public static final int MSG_ERROR = 7;
+
+  // 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_STOP = 4;
+  private static final int MSG_RELEASE = 5;
+  private static final int MSG_REFRESH_SOURCE_INFO = 6;
+  private static final int MSG_PERIOD_PREPARED = 7;
+  private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 8;
+  private static final int MSG_TRACK_SELECTION_INVALIDATED = 9;
+  private static final int MSG_CUSTOM = 10;
+
+  private static final int PREPARING_SOURCE_INTERVAL_MS = 10;
+  private static final int RENDERING_INTERVAL_MS = 10;
+  private static final int IDLE_INTERVAL_MS = 1000;
+
+  /**
+   * Limits the maximum number of periods to buffer ahead of the current playing period. The
+   * buffering policy normally prevents buffering too far ahead, but the policy could allow too many
+   * small periods to be buffered if the period count were not limited.
+   */
+  private static final int MAXIMUM_BUFFER_AHEAD_PERIODS = 100;
+
+  /**
+   * Offset added to all sample timestamps read by renderers to make them non-negative. This is
+   * provided for convenience of sources that may return negative timestamps due to prerolling
+   * samples from a keyframe before their first sample with timestamp zero, so it must be set to a
+   * value greater than or equal to the maximum key-frame interval in seekable periods.
+   */
+  private static final int RENDERER_TIMESTAMP_OFFSET_US = 60000000;
+
+  private final Renderer[] renderers;
+  private final RendererCapabilities[] rendererCapabilities;
+  private final TrackSelector trackSelector;
+  private final LoadControl loadControl;
+  private final StandaloneMediaClock standaloneMediaClock;
+  private final Handler handler;
+  private final HandlerThread internalPlaybackThread;
+  private final Handler eventHandler;
+  private final ExoPlayer player;
+  private final Timeline.Window window;
+  private final Timeline.Period period;
+
+  private PlaybackInfo playbackInfo;
+  private Renderer rendererMediaClockSource;
+  private MediaClock rendererMediaClock;
+  private MediaSource mediaSource;
+  private Renderer[] enabledRenderers;
+  private boolean released;
+  private boolean playWhenReady;
+  private boolean rebuffering;
+  private boolean isLoading;
+  private int state;
+  private int customMessagesSent;
+  private int customMessagesProcessed;
+  private long elapsedRealtimeUs;
+
+  private int pendingInitialSeekCount;
+  private SeekPosition pendingSeekPosition;
+  private long rendererPositionUs;
+
+  private MediaPeriodHolder loadingPeriodHolder;
+  private MediaPeriodHolder readingPeriodHolder;
+  private MediaPeriodHolder playingPeriodHolder;
+
+  private Timeline timeline;
+
+  public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector,
+      LoadControl loadControl, boolean playWhenReady, Handler eventHandler,
+      PlaybackInfo playbackInfo, ExoPlayer player) {
+    this.renderers = renderers;
+    this.trackSelector = trackSelector;
+    this.loadControl = loadControl;
+    this.playWhenReady = playWhenReady;
+    this.eventHandler = eventHandler;
+    this.state = ExoPlayer.STATE_IDLE;
+    this.playbackInfo = playbackInfo;
+    this.player = player;
+
+    rendererCapabilities = new RendererCapabilities[renderers.length];
+    for (int i = 0; i < renderers.length; i++) {
+      renderers[i].setIndex(i);
+      rendererCapabilities[i] = renderers[i].getCapabilities();
+    }
+    standaloneMediaClock = new StandaloneMediaClock();
+    enabledRenderers = new Renderer[0];
+    window = new Timeline.Window();
+    period = new Timeline.Period();
+    trackSelector.init(this);
+
+    // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
+    // not normally change to this priority" is incorrect.
+    internalPlaybackThread = new PriorityHandlerThread("ExoPlayerImplInternal:Handler",
+        Process.THREAD_PRIORITY_AUDIO);
+    internalPlaybackThread.start();
+    handler = new Handler(internalPlaybackThread.getLooper(), this);
+  }
+
+  public void prepare(MediaSource mediaSource, boolean resetPosition) {
+    handler.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, 0, mediaSource)
+        .sendToTarget();
+  }
+
+  public void setPlayWhenReady(boolean playWhenReady) {
+    handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 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 stop() {
+    handler.sendEmptyMessage(MSG_STOP);
+  }
+
+  public void sendMessages(ExoPlayerMessage... messages) {
+    if (released) {
+      Log.w(TAG, "Ignoring messages sent after release.");
+      return;
+    }
+    customMessagesSent++;
+    handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget();
+  }
+
+  public synchronized void blockingSendMessages(ExoPlayerMessage... messages) {
+    if (released) {
+      Log.w(TAG, "Ignoring messages sent after release.");
+      return;
+    }
+    int messageNumber = customMessagesSent++;
+    handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget();
+    while (customMessagesProcessed <= messageNumber) {
+      try {
+        wait();
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+      }
+    }
+  }
+
+  public synchronized void release() {
+    if (released) {
+      return;
+    }
+    handler.sendEmptyMessage(MSG_RELEASE);
+    while (!released) {
+      try {
+        wait();
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+      }
+    }
+    internalPlaybackThread.quit();
+  }
+
+  // MediaSource.Listener implementation.
+
+  @Override
+  public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+    handler.obtainMessage(MSG_REFRESH_SOURCE_INFO, Pair.create(timeline, manifest)).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);
+  }
+
+  // Handler.Callback implementation.
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public boolean handleMessage(Message msg) {
+    try {
+      switch (msg.what) {
+        case MSG_PREPARE: {
+          prepareInternal((MediaSource) msg.obj, msg.arg1 != 0);
+          return true;
+        }
+        case MSG_SET_PLAY_WHEN_READY: {
+          setPlayWhenReadyInternal(msg.arg1 != 0);
+          return true;
+        }
+        case MSG_DO_SOME_WORK: {
+          doSomeWork();
+          return true;
+        }
+        case MSG_SEEK_TO: {
+          seekToInternal((SeekPosition) msg.obj);
+          return true;
+        }
+        case MSG_STOP: {
+          stopInternal();
+          return true;
+        }
+        case MSG_RELEASE: {
+          releaseInternal();
+          return true;
+        }
+        case MSG_PERIOD_PREPARED: {
+          handlePeriodPrepared((MediaPeriod) msg.obj);
+          return true;
+        }
+        case MSG_REFRESH_SOURCE_INFO: {
+          handleSourceInfoRefreshed((Pair<Timeline, Object>) msg.obj);
+          return true;
+        }
+        case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: {
+          handleContinueLoadingRequested((MediaPeriod) msg.obj);
+          return true;
+        }
+        case MSG_TRACK_SELECTION_INVALIDATED: {
+          reselectTracksInternal();
+          return true;
+        }
+        case MSG_CUSTOM: {
+          sendMessagesInternal((ExoPlayerMessage[]) msg.obj);
+          return true;
+        }
+        default:
+          return false;
+      }
+    } catch (ExoPlaybackException e) {
+      Log.e(TAG, "Renderer error.", e);
+      eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
+      stopInternal();
+      return true;
+    } catch (IOException e) {
+      Log.e(TAG, "Source error.", e);
+      eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget();
+      stopInternal();
+      return true;
+    } catch (RuntimeException e) {
+      Log.e(TAG, "Internal runtime error.", e);
+      eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e))
+          .sendToTarget();
+      stopInternal();
+      return true;
+    }
+  }
+
+  // Private methods.
+
+  private void setState(int state) {
+    if (this.state != state) {
+      this.state = state;
+      eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget();
+    }
+  }
+
+  private void setIsLoading(boolean isLoading) {
+    if (this.isLoading != isLoading) {
+      this.isLoading = isLoading;
+      eventHandler.obtainMessage(MSG_LOADING_CHANGED, isLoading ? 1 : 0, 0).sendToTarget();
+    }
+  }
+
+  private void prepareInternal(MediaSource mediaSource, boolean resetPosition) {
+    resetInternal(true);
+    loadControl.onPrepared();
+    if (resetPosition) {
+      playbackInfo = new PlaybackInfo(0, C.TIME_UNSET);
+    }
+    this.mediaSource = mediaSource;
+    mediaSource.prepareSource(player, true, this);
+    setState(ExoPlayer.STATE_BUFFERING);
+    handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+  }
+
+  private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
+    rebuffering = false;
+    this.playWhenReady = playWhenReady;
+    if (!playWhenReady) {
+      stopRenderers();
+      updatePlaybackPositions();
+    } else {
+      if (state == ExoPlayer.STATE_READY) {
+        startRenderers();
+        handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+      } else if (state == ExoPlayer.STATE_BUFFERING) {
+        handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+      }
+    }
+  }
+
+  private void startRenderers() throws ExoPlaybackException {
+    rebuffering = false;
+    standaloneMediaClock.start();
+    for (Renderer renderer : enabledRenderers) {
+      renderer.start();
+    }
+  }
+
+  private void stopRenderers() throws ExoPlaybackException {
+    standaloneMediaClock.stop();
+    for (Renderer renderer : enabledRenderers) {
+      ensureStopped(renderer);
+    }
+  }
+
+  private void updatePlaybackPositions() throws ExoPlaybackException {
+    if (playingPeriodHolder == null) {
+      return;
+    }
+
+    // Update the playback position.
+    long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity();
+    if (periodPositionUs != C.TIME_UNSET) {
+      resetRendererPosition(periodPositionUs);
+    } else {
+      if (rendererMediaClockSource != null && !rendererMediaClockSource.isEnded()) {
+        rendererPositionUs = rendererMediaClock.getPositionUs();
+        standaloneMediaClock.setPositionUs(rendererPositionUs);
+      } else {
+        rendererPositionUs = standaloneMediaClock.getPositionUs();
+      }
+      periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs);
+    }
+    playbackInfo.positionUs = periodPositionUs;
+    elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
+
+    // Update the buffered position.
+    long bufferedPositionUs = enabledRenderers.length == 0 ? C.TIME_END_OF_SOURCE
+        : playingPeriodHolder.mediaPeriod.getBufferedPositionUs();
+    playbackInfo.bufferedPositionUs = bufferedPositionUs == C.TIME_END_OF_SOURCE
+        ? timeline.getPeriod(playingPeriodHolder.index, period).getDurationUs()
+        : bufferedPositionUs;
+  }
+
+  private void doSomeWork() throws ExoPlaybackException, IOException {
+    long operationStartTimeMs = SystemClock.elapsedRealtime();
+    updatePeriods();
+    if (playingPeriodHolder == null) {
+      // We're still waiting for the first period to be prepared.
+      maybeThrowPeriodPrepareError();
+      scheduleNextWork(operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS);
+      return;
+    }
+
+    TraceUtil.beginSection("doSomeWork");
+
+    updatePlaybackPositions();
+    boolean allRenderersEnded = true;
+    boolean allRenderersReadyOrEnded = true;
+    for (Renderer renderer : enabledRenderers) {
+      // TODO: Each renderer should return the maximum delay before which it wishes to be called
+      // again. The minimum of these values should then be used as the delay before the next
+      // invocation of this method.
+      renderer.render(rendererPositionUs, elapsedRealtimeUs);
+      allRenderersEnded = allRenderersEnded && renderer.isEnded();
+      // Determine whether the renderer is ready (or ended). If it's not, throw an error that's
+      // preventing the renderer from making progress, if such an error exists.
+      boolean rendererReadyOrEnded = renderer.isReady() || renderer.isEnded();
+      if (!rendererReadyOrEnded) {
+        renderer.maybeThrowStreamError();
+      }
+      allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded;
+    }
+
+    if (!allRenderersReadyOrEnded) {
+      maybeThrowPeriodPrepareError();
+    }
+
+    long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.index, period)
+        .getDurationUs();
+    if (allRenderersEnded
+        && (playingPeriodDurationUs == C.TIME_UNSET
+        || playingPeriodDurationUs <= playbackInfo.positionUs)
+        && playingPeriodHolder.isLast) {
+      setState(ExoPlayer.STATE_ENDED);
+      stopRenderers();
+    } else if (state == ExoPlayer.STATE_BUFFERING) {
+      boolean isNewlyReady = enabledRenderers.length > 0
+          ? (allRenderersReadyOrEnded && haveSufficientBuffer(rebuffering))
+          : isTimelineReady(playingPeriodDurationUs);
+      if (isNewlyReady) {
+        setState(ExoPlayer.STATE_READY);
+        if (playWhenReady) {
+          startRenderers();
+        }
+      }
+    } else if (state == ExoPlayer.STATE_READY) {
+      boolean isStillReady = enabledRenderers.length > 0 ? allRenderersReadyOrEnded
+          : isTimelineReady(playingPeriodDurationUs);
+      if (!isStillReady) {
+        rebuffering = playWhenReady;
+        setState(ExoPlayer.STATE_BUFFERING);
+        stopRenderers();
+      }
+    }
+
+    if (state == ExoPlayer.STATE_BUFFERING) {
+      for (Renderer renderer : enabledRenderers) {
+        renderer.maybeThrowStreamError();
+      }
+    }
+
+    if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) {
+      scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS);
+    } else if (enabledRenderers.length != 0) {
+      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);
+    long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs;
+    long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime();
+    if (nextOperationDelayMs <= 0) {
+      handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+    } else {
+      handler.sendEmptyMessageDelayed(MSG_DO_SOME_WORK, nextOperationDelayMs);
+    }
+  }
+
+  private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
+    if (timeline == null) {
+      pendingInitialSeekCount++;
+      pendingSeekPosition = seekPosition;
+      return;
+    }
+
+    Pair<Integer, Long> periodPosition = resolveSeekPosition(seekPosition);
+    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.
+      playbackInfo = new PlaybackInfo(0, 0);
+      eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, playbackInfo).sendToTarget();
+      // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't
+      // ignored.
+      playbackInfo = new PlaybackInfo(0, C.TIME_UNSET);
+      setState(ExoPlayer.STATE_ENDED);
+      // Reset, but retain the source so that it can still be used should a seek occur.
+      resetInternal(false);
+      return;
+    }
+
+    boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET;
+    int periodIndex = periodPosition.first;
+    long periodPositionUs = periodPosition.second;
+
+    try {
+      if (periodIndex == playbackInfo.periodIndex
+          && ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000))) {
+        // Seek position equals the current position. Do nothing.
+        return;
+      }
+      long newPeriodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs);
+      seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs;
+      periodPositionUs = newPeriodPositionUs;
+    } finally {
+      playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs);
+      eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo)
+          .sendToTarget();
+    }
+  }
+
+  private long seekToPeriodPosition(int periodIndex, long periodPositionUs)
+      throws ExoPlaybackException {
+    stopRenderers();
+    rebuffering = false;
+    setState(ExoPlayer.STATE_BUFFERING);
+
+    MediaPeriodHolder newPlayingPeriodHolder = null;
+    if (playingPeriodHolder == null) {
+      // We're still waiting for the first period to be prepared.
+      if (loadingPeriodHolder != null) {
+        loadingPeriodHolder.release();
+      }
+    } else {
+      // Clear the timeline, but keep the requested period if it is already prepared.
+      MediaPeriodHolder periodHolder = playingPeriodHolder;
+      while (periodHolder != null) {
+        if (periodHolder.index == periodIndex && periodHolder.prepared) {
+          newPlayingPeriodHolder = periodHolder;
+        } else {
+          periodHolder.release();
+        }
+        periodHolder = periodHolder.next;
+      }
+    }
+
+    // Disable all the renderers if the period being played is changing, or if the renderers are
+    // reading from a period other than the one being played.
+    if (playingPeriodHolder != newPlayingPeriodHolder
+        || playingPeriodHolder != readingPeriodHolder) {
+      for (Renderer renderer : enabledRenderers) {
+        renderer.disable();
+      }
+      enabledRenderers = new Renderer[0];
+      rendererMediaClock = null;
+      rendererMediaClockSource = null;
+      playingPeriodHolder = null;
+    }
+
+    // Update the holders.
+    if (newPlayingPeriodHolder != null) {
+      newPlayingPeriodHolder.next = null;
+      loadingPeriodHolder = newPlayingPeriodHolder;
+      readingPeriodHolder = newPlayingPeriodHolder;
+      setPlayingPeriodHolder(newPlayingPeriodHolder);
+      if (playingPeriodHolder.hasEnabledTracks) {
+        periodPositionUs = playingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs);
+      }
+      resetRendererPosition(periodPositionUs);
+      maybeContinueLoading();
+    } else {
+      loadingPeriodHolder = null;
+      readingPeriodHolder = null;
+      playingPeriodHolder = null;
+      resetRendererPosition(periodPositionUs);
+    }
+
+    handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+    return periodPositionUs;
+  }
+
+  private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException {
+    rendererPositionUs = playingPeriodHolder == null
+        ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US
+        : playingPeriodHolder.toRendererTime(periodPositionUs);
+    standaloneMediaClock.setPositionUs(rendererPositionUs);
+    for (Renderer renderer : enabledRenderers) {
+      renderer.resetPosition(rendererPositionUs);
+    }
+  }
+
+  private void stopInternal() {
+    resetInternal(true);
+    loadControl.onStopped();
+    setState(ExoPlayer.STATE_IDLE);
+  }
+
+  private void releaseInternal() {
+    resetInternal(true);
+    loadControl.onReleased();
+    setState(ExoPlayer.STATE_IDLE);
+    synchronized (this) {
+      released = true;
+      notifyAll();
+    }
+  }
+
+  private void resetInternal(boolean releaseMediaSource) {
+    handler.removeMessages(MSG_DO_SOME_WORK);
+    rebuffering = false;
+    standaloneMediaClock.stop();
+    rendererMediaClock = null;
+    rendererMediaClockSource = null;
+    rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US;
+    for (Renderer renderer : enabledRenderers) {
+      try {
+        ensureStopped(renderer);
+        renderer.disable();
+      } catch (ExoPlaybackException | RuntimeException e) {
+        // There's nothing we can do.
+        Log.e(TAG, "Stop failed.", e);
+      }
+    }
+    enabledRenderers = new Renderer[0];
+    releasePeriodHoldersFrom(playingPeriodHolder != null ? playingPeriodHolder
+        : loadingPeriodHolder);
+    loadingPeriodHolder = null;
+    readingPeriodHolder = null;
+    playingPeriodHolder = null;
+    setIsLoading(false);
+    if (releaseMediaSource) {
+      if (mediaSource != null) {
+        mediaSource.releaseSource();
+        mediaSource = null;
+      }
+      timeline = null;
+    }
+  }
+
+  private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException {
+    try {
+      for (ExoPlayerMessage message : messages) {
+        message.target.handleMessage(message.messageType, message.message);
+      }
+      if (mediaSource != null) {
+        // The message may have caused something to change that now requires us to do work.
+        handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+      }
+    } finally {
+      synchronized (this) {
+        customMessagesProcessed++;
+        notifyAll();
+      }
+    }
+  }
+
+  private void ensureStopped(Renderer renderer) throws ExoPlaybackException {
+    if (renderer.getState() == Renderer.STATE_STARTED) {
+      renderer.stop();
+    }
+  }
+
+  private void reselectTracksInternal() throws ExoPlaybackException {
+    if (playingPeriodHolder == null) {
+      // We don't have tracks yet, so we don't care.
+      return;
+    }
+    // Reselect tracks on each period in turn, until the selection changes.
+    MediaPeriodHolder periodHolder = playingPeriodHolder;
+    boolean selectionsChangedForReadPeriod = true;
+    while (true) {
+      if (periodHolder == null || !periodHolder.prepared) {
+        // The reselection did not change any prepared periods.
+        return;
+      }
+      if (periodHolder.selectTracks()) {
+        // 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.next;
+    }
+
+    if (selectionsChangedForReadPeriod) {
+      // Update streams and rebuffer for the new selection, recreating all streams if reading ahead.
+      boolean recreateStreams = readingPeriodHolder != playingPeriodHolder;
+      releasePeriodHoldersFrom(playingPeriodHolder.next);
+      playingPeriodHolder.next = null;
+      loadingPeriodHolder = playingPeriodHolder;
+      readingPeriodHolder = playingPeriodHolder;
+
+      boolean[] streamResetFlags = new boolean[renderers.length];
+      long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection(
+          playbackInfo.positionUs, recreateStreams, streamResetFlags);
+      if (periodPositionUs != playbackInfo.positionUs) {
+        playbackInfo.positionUs = periodPositionUs;
+        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.
+            if (renderer == rendererMediaClockSource) {
+              // The renderer is providing the media clock.
+              if (sampleStream == null) {
+                // The renderer won't be re-enabled. Sync standaloneMediaClock so that it can take
+                // over timing responsibilities.
+                standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs());
+              }
+              rendererMediaClock = null;
+              rendererMediaClockSource = null;
+            }
+            ensureStopped(renderer);
+            renderer.disable();
+          } else if (streamResetFlags[i]) {
+            // The renderer will continue to consume from its current stream, but needs to be reset.
+            renderer.resetPosition(rendererPositionUs);
+          }
+        }
+      }
+      eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult)
+          .sendToTarget();
+      enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
+    } else {
+      // Release and re-prepare/buffer periods after the one whose selection changed.
+      loadingPeriodHolder = periodHolder;
+      periodHolder = loadingPeriodHolder.next;
+      while (periodHolder != null) {
+        periodHolder.release();
+        periodHolder = periodHolder.next;
+      }
+      loadingPeriodHolder.next = null;
+      if (loadingPeriodHolder.prepared) {
+        long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.startPositionUs,
+            loadingPeriodHolder.toPeriodTime(rendererPositionUs));
+        loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false);
+      }
+    }
+    maybeContinueLoading();
+    updatePlaybackPositions();
+    handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+  }
+
+  private boolean isTimelineReady(long playingPeriodDurationUs) {
+    return playingPeriodDurationUs == C.TIME_UNSET
+        || playbackInfo.positionUs < playingPeriodDurationUs
+        || (playingPeriodHolder.next != null && playingPeriodHolder.next.prepared);
+  }
+
+  private boolean haveSufficientBuffer(boolean rebuffering) {
+    long loadingPeriodBufferedPositionUs = !loadingPeriodHolder.prepared
+        ? loadingPeriodHolder.startPositionUs
+        : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs();
+    if (loadingPeriodBufferedPositionUs == C.TIME_END_OF_SOURCE) {
+      if (loadingPeriodHolder.isLast) {
+        return true;
+      }
+      loadingPeriodBufferedPositionUs = timeline.getPeriod(loadingPeriodHolder.index, period)
+          .getDurationUs();
+    }
+    return loadControl.shouldStartPlayback(
+        loadingPeriodBufferedPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs),
+        rebuffering);
+  }
+
+  private void maybeThrowPeriodPrepareError() throws IOException {
+    if (loadingPeriodHolder != null && !loadingPeriodHolder.prepared
+        && (readingPeriodHolder == null || readingPeriodHolder.next == loadingPeriodHolder)) {
+      for (Renderer renderer : enabledRenderers) {
+        if (!renderer.hasReadStreamToEnd()) {
+          return;
+        }
+      }
+      loadingPeriodHolder.mediaPeriod.maybeThrowPrepareError();
+    }
+  }
+
+  private void handleSourceInfoRefreshed(Pair<Timeline, Object> timelineAndManifest)
+      throws ExoPlaybackException {
+    Timeline oldTimeline = timeline;
+    timeline = timelineAndManifest.first;
+    Object manifest = timelineAndManifest.second;
+
+    int processedInitialSeekCount = 0;
+    if (oldTimeline == null) {
+      if (pendingInitialSeekCount > 0) {
+        Pair<Integer, Long> periodPosition = resolveSeekPosition(pendingSeekPosition);
+        processedInitialSeekCount = pendingInitialSeekCount;
+        pendingInitialSeekCount = 0;
+        pendingSeekPosition = null;
+        if (periodPosition == null) {
+          // The seek position was valid for the timeline that it was performed into, but the
+          // timeline has changed and a suitable seek position could not be resolved in the new one.
+          handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount);
+          return;
+        }
+        playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second);
+      } else if (playbackInfo.startPositionUs == C.TIME_UNSET) {
+        if (timeline.isEmpty()) {
+          handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount);
+          return;
+        }
+        Pair<Integer, Long> defaultPosition = getPeriodPosition(0, C.TIME_UNSET);
+        playbackInfo = new PlaybackInfo(defaultPosition.first, defaultPosition.second);
+      }
+    }
+
+    MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder
+        : loadingPeriodHolder;
+    if (periodHolder == null) {
+      // We don't have any period holders, so we're done.
+      notifySourceInfoRefresh(manifest, processedInitialSeekCount);
+      return;
+    }
+
+    int periodIndex = timeline.getIndexOfPeriod(periodHolder.uid);
+    if (periodIndex == C.INDEX_UNSET) {
+      // We didn't find the current period in the new timeline. Attempt to resolve a subsequent
+      // period whose window we can restart from.
+      int newPeriodIndex = resolveSubsequentPeriod(periodHolder.index, oldTimeline, timeline);
+      if (newPeriodIndex == C.INDEX_UNSET) {
+        // We failed to resolve a suitable restart position.
+        handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount);
+        return;
+      }
+      // We resolved a subsequent period. Seek to the default position in the corresponding window.
+      Pair<Integer, Long> defaultPosition = getPeriodPosition(
+          timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET);
+      newPeriodIndex = defaultPosition.first;
+      long newPositionUs = defaultPosition.second;
+      timeline.getPeriod(newPeriodIndex, period, true);
+      // Clear the index of each holder that doesn't contain the default position. If a holder
+      // contains the default position then update its index so it can be re-used when seeking.
+      Object newPeriodUid = period.uid;
+      periodHolder.index = C.INDEX_UNSET;
+      while (periodHolder.next != null) {
+        periodHolder = periodHolder.next;
+        periodHolder.index = periodHolder.uid.equals(newPeriodUid) ? newPeriodIndex : C.INDEX_UNSET;
+      }
+      // Actually do the seek.
+      newPositionUs = seekToPeriodPosition(newPeriodIndex, newPositionUs);
+      playbackInfo = new PlaybackInfo(newPeriodIndex, newPositionUs);
+      notifySourceInfoRefresh(manifest, processedInitialSeekCount);
+      return;
+    }
+
+    // The current period is in the new timeline. Update the holder and playbackInfo.
+    timeline.getPeriod(periodIndex, period);
+    boolean isLastPeriod = periodIndex == timeline.getPeriodCount() - 1
+        && !timeline.getWindow(period.windowIndex, window).isDynamic;
+    periodHolder.setIndex(periodIndex, isLastPeriod);
+    boolean seenReadingPeriod = periodHolder == readingPeriodHolder;
+    if (periodIndex != playbackInfo.periodIndex) {
+      playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex);
+    }
+
+    // If there are subsequent holders, update the index for each of them. If we find a holder
+    // that's inconsistent with the new timeline then take appropriate action.
+    while (periodHolder.next != null) {
+      MediaPeriodHolder previousPeriodHolder = periodHolder;
+      periodHolder = periodHolder.next;
+      periodIndex++;
+      timeline.getPeriod(periodIndex, period, true);
+      isLastPeriod = periodIndex == timeline.getPeriodCount() - 1
+          && !timeline.getWindow(period.windowIndex, window).isDynamic;
+      if (periodHolder.uid.equals(period.uid)) {
+        // The holder is consistent with the new timeline. Update its index and continue.
+        periodHolder.setIndex(periodIndex, isLastPeriod);
+        seenReadingPeriod |= (periodHolder == readingPeriodHolder);
+      } else {
+        // The holder is inconsistent with the new timeline.
+        if (!seenReadingPeriod) {
+          // Renderers may have read from a period that's been removed. Seek back to the current
+          // position of the playing period to make sure none of the removed period is played.
+          periodIndex = playingPeriodHolder.index;
+          long newPositionUs = seekToPeriodPosition(periodIndex, playbackInfo.positionUs);
+          playbackInfo = new PlaybackInfo(periodIndex, newPositionUs);
+        } else {
+          // Update the loading period to be the last period that's still valid, and release all
+          // subsequent periods.
+          loadingPeriodHolder = previousPeriodHolder;
+          loadingPeriodHolder.next = null;
+          // Release the rest of the timeline.
+          releasePeriodHoldersFrom(periodHolder);
+        }
+        break;
+      }
+    }
+
+    notifySourceInfoRefresh(manifest, processedInitialSeekCount);
+  }
+
+  private void handleSourceInfoRefreshEndedPlayback(Object manifest,
+      int processedInitialSeekCount) {
+    // Set the playback position to (0,0) for notifying the eventHandler.
+    playbackInfo = new PlaybackInfo(0, 0);
+    notifySourceInfoRefresh(manifest, processedInitialSeekCount);
+    // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't ignored.
+    playbackInfo = new PlaybackInfo(0, C.TIME_UNSET);
+    setState(ExoPlayer.STATE_ENDED);
+    // Reset, but retain the source so that it can still be used should a seek occur.
+    resetInternal(false);
+  }
+
+  private void notifySourceInfoRefresh(Object manifest, int processedInitialSeekCount) {
+    eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED,
+        new SourceInfo(timeline, manifest, playbackInfo, processedInitialSeekCount)).sendToTarget();
+  }
+
+  /**
+   * Given a period index into an old timeline, finds the first subsequent period that also exists
+   * in a new timeline. The index of this period in the new timeline is returned.
+   *
+   * @param oldPeriodIndex The index of the period in the old timeline.
+   * @param oldTimeline The old timeline.
+   * @param newTimeline The new timeline.
+   * @return The index in the new timeline of the first subsequent period, or {@link C#INDEX_UNSET}
+   *     if no such period was found.
+   */
+  private int resolveSubsequentPeriod(int oldPeriodIndex, Timeline oldTimeline,
+      Timeline newTimeline) {
+    int newPeriodIndex = C.INDEX_UNSET;
+    while (newPeriodIndex == C.INDEX_UNSET && oldPeriodIndex < oldTimeline.getPeriodCount() - 1) {
+      newPeriodIndex = newTimeline.getIndexOfPeriod(
+          oldTimeline.getPeriod(++oldPeriodIndex, period, true).uid);
+    }
+    return newPeriodIndex;
+  }
+
+  /**
+   * Converts a {@link SeekPosition} into the corresponding (periodIndex, periodPositionUs) for the
+   * internal timeline.
+   *
+   * @param seekPosition The position to resolve.
+   * @return The resolved position, or null if resolution was not successful.
+   * @throws IllegalSeekPositionException If the window index of the seek position is outside the
+   *     bounds of the timeline.
+   */
+  private Pair<Integer, Long> resolveSeekPosition(SeekPosition seekPosition) {
+    Timeline seekTimeline = seekPosition.timeline;
+    if (seekTimeline.isEmpty()) {
+      // The application performed a blind seek without a non-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<Integer, Long> periodPosition;
+    try {
+      periodPosition = getPeriodPosition(seekTimeline, seekPosition.windowIndex,
+          seekPosition.windowPositionUs);
+    } catch (IndexOutOfBoundsException e) {
+      // The window index of the seek position was outside the bounds of the timeline.
+      throw new IllegalSeekPositionException(timeline, seekPosition.windowIndex,
+          seekPosition.windowPositionUs);
+    }
+    if (timeline == seekTimeline) {
+      // Our internal timeline is the seek timeline, so the mapped position is correct.
+      return periodPosition;
+    }
+    // Attempt to find the mapped period in the internal timeline.
+    int periodIndex = timeline.getIndexOfPeriod(
+        seekTimeline.getPeriod(periodPosition.first, period, true).uid);
+    if (periodIndex != C.INDEX_UNSET) {
+      // We successfully located the period in the internal timeline.
+      return Pair.create(periodIndex, periodPosition.second);
+    }
+    // Try and find a subsequent period from the seek timeline in the internal timeline.
+    periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline);
+    if (periodIndex != C.INDEX_UNSET) {
+      // We found one. Map the SeekPosition onto the corresponding default position.
+      return getPeriodPosition(timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET);
+    }
+    // We didn't find one. Give up.
+    return null;
+  }
+
+  /**
+   * Calls {@link #getPeriodPosition(Timeline, int, long)} using the current timeline.
+   */
+  private Pair<Integer, Long> getPeriodPosition(int windowIndex, long windowPositionUs) {
+    return getPeriodPosition(timeline, windowIndex, windowPositionUs);
+  }
+
+  /**
+   * Calls {@link #getPeriodPosition(Timeline, int, long, long)} with a zero default position
+   * projection.
+   */
+  private Pair<Integer, Long> getPeriodPosition(Timeline timeline, int windowIndex,
+      long windowPositionUs) {
+    return getPeriodPosition(timeline, windowIndex, windowPositionUs, 0);
+  }
+
+  /**
+   * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs).
+   *
+   * @param timeline The timeline containing the window.
+   * @param windowIndex The window index.
+   * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default
+   *     start position.
+   * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the
+   *     duration into the future by which the window's position should be projected.
+   * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs}
+   *     is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's
+   *     position could not be projected by {@code defaultPositionProjectionUs}.
+   */
+  private Pair<Integer, Long> getPeriodPosition(Timeline timeline, int windowIndex,
+      long windowPositionUs, long defaultPositionProjectionUs) {
+    Assertions.checkIndex(windowIndex, 0, timeline.getWindowCount());
+    timeline.getWindow(windowIndex, window, false, defaultPositionProjectionUs);
+    if (windowPositionUs == C.TIME_UNSET) {
+      windowPositionUs = window.getDefaultPositionUs();
+      if (windowPositionUs == C.TIME_UNSET) {
+        return null;
+      }
+    }
+    int periodIndex = window.firstPeriodIndex;
+    long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs;
+    long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs();
+    while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs
+        && periodIndex < window.lastPeriodIndex) {
+      periodPositionUs -= periodDurationUs;
+      periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs();
+    }
+    return Pair.create(periodIndex, periodPositionUs);
+  }
+
+  private void updatePeriods() throws ExoPlaybackException, IOException {
+    if (timeline == null) {
+      // We're waiting to get information about periods.
+      mediaSource.maybeThrowSourceInfoRefreshError();
+      return;
+    }
+
+    // Update the loading period if required.
+    maybeUpdateLoadingPeriod();
+    if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) {
+      setIsLoading(false);
+    } else if (loadingPeriodHolder != null && loadingPeriodHolder.needsContinueLoading) {
+      maybeContinueLoading();
+    }
+
+    if (playingPeriodHolder == null) {
+      // We're waiting for the first period to be prepared.
+      return;
+    }
+
+    // Update the playing and reading periods.
+    while (playingPeriodHolder != readingPeriodHolder
+        && rendererPositionUs >= playingPeriodHolder.next.rendererPositionOffsetUs) {
+      // All enabled renderers' streams have been read to the end, and the playback position reached
+      // the end of the playing period, so advance playback to the next period.
+      playingPeriodHolder.release();
+      setPlayingPeriodHolder(playingPeriodHolder.next);
+      playbackInfo = new PlaybackInfo(playingPeriodHolder.index,
+          playingPeriodHolder.startPositionUs);
+      updatePlaybackPositions();
+      eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget();
+    }
+
+    if (readingPeriodHolder.isLast) {
+      for (int i = 0; i < renderers.length; i++) {
+        Renderer renderer = renderers[i];
+        SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
+        // Defer setting the stream as final until the renderer has actually consumed the whole
+        // stream in case of playlist changes that cause the stream to be no longer final.
+        if (sampleStream != null && renderer.getStream() == sampleStream
+            && renderer.hasReadStreamToEnd()) {
+          renderer.setCurrentStreamFinal();
+        }
+      }
+      return;
+    }
+
+    for (int i = 0; i < renderers.length; i++) {
+      Renderer renderer = renderers[i];
+      SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
+      if (renderer.getStream() != sampleStream
+          || (sampleStream != null && !renderer.hasReadStreamToEnd())) {
+        return;
+      }
+    }
+
+    if (readingPeriodHolder.next != null && readingPeriodHolder.next.prepared) {
+      TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
+      readingPeriodHolder = readingPeriodHolder.next;
+      TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
+
+      boolean initialDiscontinuity =
+          readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET;
+      for (int i = 0; i < renderers.length; i++) {
+        Renderer renderer = renderers[i];
+        TrackSelection oldSelection = oldTrackSelectorResult.selections.get(i);
+        if (oldSelection == null) {
+          // The renderer has no current stream and will be enabled when we play the next period.
+        } else if (initialDiscontinuity) {
+          // The new period starts with a discontinuity, so the renderer will play out all data then
+          // be disabled and re-enabled when it starts playing the next period.
+          renderer.setCurrentStreamFinal();
+        } else if (!renderer.isCurrentStreamFinal()) {
+          TrackSelection newSelection = newTrackSelectorResult.selections.get(i);
+          RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i];
+          RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i];
+          if (newSelection != null && newConfig.equals(oldConfig)) {
+            // Replace the renderer's SampleStream so the transition to playing the next period can
+            // be seamless.
+            Format[] formats = new Format[newSelection.length()];
+            for (int j = 0; j < formats.length; j++) {
+              formats[j] = newSelection.getFormat(j);
+            }
+            renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i],
+                readingPeriodHolder.getRendererOffset());
+          } else {
+            // The renderer will be disabled when transitioning to playing the next period, either
+            // because there's no new selection or because a configuration change is required. Mark
+            // the SampleStream as final to play out any remaining data.
+            renderer.setCurrentStreamFinal();
+          }
+        }
+      }
+    }
+  }
+
+  private void maybeUpdateLoadingPeriod() throws IOException {
+    int newLoadingPeriodIndex;
+    if (loadingPeriodHolder == null) {
+      newLoadingPeriodIndex = playbackInfo.periodIndex;
+    } else {
+      int loadingPeriodIndex = loadingPeriodHolder.index;
+      if (loadingPeriodHolder.isLast || !loadingPeriodHolder.isFullyBuffered()
+          || timeline.getPeriod(loadingPeriodIndex, period).getDurationUs() == C.TIME_UNSET) {
+        // Either the existing loading period is the last period, or we are not ready to advance to
+        // loading the next period because it hasn't been fully buffered or its duration is unknown.
+        return;
+      }
+      if (playingPeriodHolder != null
+          && loadingPeriodIndex - playingPeriodHolder.index == MAXIMUM_BUFFER_AHEAD_PERIODS) {
+        // We are already buffering the maximum number of periods ahead.
+        return;
+      }
+      newLoadingPeriodIndex = loadingPeriodHolder.index + 1;
+    }
+
+    if (newLoadingPeriodIndex >= timeline.getPeriodCount()) {
+      // The next period is not available yet.
+      mediaSource.maybeThrowSourceInfoRefreshError();
+      return;
+    }
+
+    long newLoadingPeriodStartPositionUs;
+    if (loadingPeriodHolder == null) {
+      newLoadingPeriodStartPositionUs = playbackInfo.positionUs;
+    } else {
+      int newLoadingWindowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex;
+      if (newLoadingPeriodIndex
+          != timeline.getWindow(newLoadingWindowIndex, window).firstPeriodIndex) {
+        // We're starting to buffer a new period in the current window. Always start from the
+        // beginning of the period.
+        newLoadingPeriodStartPositionUs = 0;
+      } else {
+        // We're starting to buffer a new window. When playback transitions to this window we'll
+        // want it to be from its default start position. The expected delay until playback
+        // transitions is equal the duration of media that's currently buffered (assuming no
+        // interruptions). Hence we project the default start position forward by the duration of
+        // the buffer, and start buffering from this point.
+        long defaultPositionProjectionUs = loadingPeriodHolder.getRendererOffset()
+            + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()
+            - rendererPositionUs;
+        Pair<Integer, Long> defaultPosition = getPeriodPosition(timeline, newLoadingWindowIndex,
+            C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs));
+        if (defaultPosition == null) {
+          return;
+        }
+
+        newLoadingPeriodIndex = defaultPosition.first;
+        newLoadingPeriodStartPositionUs = defaultPosition.second;
+      }
+    }
+
+    long rendererPositionOffsetUs = loadingPeriodHolder == null
+        ? newLoadingPeriodStartPositionUs + RENDERER_TIMESTAMP_OFFSET_US
+        : (loadingPeriodHolder.getRendererOffset()
+            + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs());
+    timeline.getPeriod(newLoadingPeriodIndex, period, true);
+    boolean isLastPeriod = newLoadingPeriodIndex == timeline.getPeriodCount() - 1
+        && !timeline.getWindow(period.windowIndex, window).isDynamic;
+    MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities,
+        rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid,
+        newLoadingPeriodIndex, isLastPeriod, newLoadingPeriodStartPositionUs);
+    if (loadingPeriodHolder != null) {
+      loadingPeriodHolder.next = newPeriodHolder;
+    }
+    loadingPeriodHolder = newPeriodHolder;
+    loadingPeriodHolder.mediaPeriod.prepare(this);
+    setIsLoading(true);
+  }
+
+  private void handlePeriodPrepared(MediaPeriod period) throws ExoPlaybackException {
+    if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) {
+      // Stale event.
+      return;
+    }
+    loadingPeriodHolder.handlePrepared();
+    if (playingPeriodHolder == null) {
+      // This is the first prepared period, so start playing it.
+      readingPeriodHolder = loadingPeriodHolder;
+      resetRendererPosition(readingPeriodHolder.startPositionUs);
+      setPlayingPeriodHolder(readingPeriodHolder);
+    }
+    maybeContinueLoading();
+  }
+
+  private void handleContinueLoadingRequested(MediaPeriod period) {
+    if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) {
+      // Stale event.
+      return;
+    }
+    maybeContinueLoading();
+  }
+
+  private void maybeContinueLoading() {
+    long nextLoadPositionUs = !loadingPeriodHolder.prepared ? 0
+        : loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs();
+    if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
+      setIsLoading(false);
+    } else {
+      long loadingPeriodPositionUs = loadingPeriodHolder.toPeriodTime(rendererPositionUs);
+      long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs;
+      boolean continueLoading = loadControl.shouldContinueLoading(bufferedDurationUs);
+      setIsLoading(continueLoading);
+      if (continueLoading) {
+        loadingPeriodHolder.needsContinueLoading = false;
+        loadingPeriodHolder.mediaPeriod.continueLoading(loadingPeriodPositionUs);
+      } else {
+        loadingPeriodHolder.needsContinueLoading = true;
+      }
+    }
+  }
+
+  private void releasePeriodHoldersFrom(MediaPeriodHolder periodHolder) {
+    while (periodHolder != null) {
+      periodHolder.release();
+      periodHolder = periodHolder.next;
+    }
+  }
+
+  private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) throws ExoPlaybackException {
+    if (playingPeriodHolder == periodHolder) {
+      return;
+    }
+
+    int enabledRendererCount = 0;
+    boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
+    for (int i = 0; i < renderers.length; i++) {
+      Renderer renderer = renderers[i];
+      rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;
+      TrackSelection newSelection = periodHolder.trackSelectorResult.selections.get(i);
+      if (newSelection != null) {
+        enabledRendererCount++;
+      }
+      if (rendererWasEnabledFlags[i] && (newSelection == null
+          || (renderer.isCurrentStreamFinal()
+          && renderer.getStream() == playingPeriodHolder.sampleStreams[i]))) {
+        // The renderer should be disabled before playing the next period, either because it's not
+        // needed to play the next period, or because we need to re-enable it as its current stream
+        // is final and it's not reading ahead.
+        if (renderer == rendererMediaClockSource) {
+          // Sync standaloneMediaClock so that it can take over timing responsibilities.
+          standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs());
+          rendererMediaClock = null;
+          rendererMediaClockSource = null;
+        }
+        ensureStopped(renderer);
+        renderer.disable();
+      }
+    }
+
+    playingPeriodHolder = periodHolder;
+    eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult).sendToTarget();
+    enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
+  }
+
+  private void enableRenderers(boolean[] rendererWasEnabledFlags, int enabledRendererCount)
+      throws ExoPlaybackException {
+    enabledRenderers = new Renderer[enabledRendererCount];
+    enabledRendererCount = 0;
+    for (int i = 0; i < renderers.length; i++) {
+      Renderer renderer = renderers[i];
+      TrackSelection newSelection = playingPeriodHolder.trackSelectorResult.selections.get(i);
+      if (newSelection != null) {
+        enabledRenderers[enabledRendererCount++] = renderer;
+        if (renderer.getState() == Renderer.STATE_DISABLED) {
+          RendererConfiguration rendererConfiguration =
+              playingPeriodHolder.trackSelectorResult.rendererConfigurations[i];
+          // The renderer needs enabling with its new track selection.
+          boolean playing = playWhenReady && state == ExoPlayer.STATE_READY;
+          // Consider as joining only if the renderer was previously disabled.
+          boolean joining = !rendererWasEnabledFlags[i] && playing;
+          // Build an array of formats contained by the selection.
+          Format[] formats = new Format[newSelection.length()];
+          for (int j = 0; j < formats.length; j++) {
+            formats[j] = newSelection.getFormat(j);
+          }
+          // Enable the renderer.
+          renderer.enable(rendererConfiguration, formats, playingPeriodHolder.sampleStreams[i],
+              rendererPositionUs, joining, playingPeriodHolder.getRendererOffset());
+          MediaClock mediaClock = renderer.getMediaClock();
+          if (mediaClock != null) {
+            if (rendererMediaClock != null) {
+              throw ExoPlaybackException.createForUnexpected(
+                  new IllegalStateException("Multiple renderer media clocks enabled."));
+            }
+            rendererMediaClock = mediaClock;
+            rendererMediaClockSource = renderer;
+          }
+          // Start the renderer if playing.
+          if (playing) {
+            renderer.start();
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Holds a {@link MediaPeriod} with information required to play it as part of a timeline.
+   */
+  private static final class MediaPeriodHolder {
+
+    public final MediaPeriod mediaPeriod;
+    public final Object uid;
+    public final SampleStream[] sampleStreams;
+    public final boolean[] mayRetainStreamFlags;
+    public final long rendererPositionOffsetUs;
+
+    public int index;
+    public long startPositionUs;
+    public boolean isLast;
+    public boolean prepared;
+    public boolean hasEnabledTracks;
+    public MediaPeriodHolder next;
+    public boolean needsContinueLoading;
+    public TrackSelectorResult trackSelectorResult;
+
+    private final Renderer[] renderers;
+    private final RendererCapabilities[] rendererCapabilities;
+    private final TrackSelector trackSelector;
+    private final LoadControl loadControl;
+    private final MediaSource mediaSource;
+
+    private TrackSelectorResult periodTrackSelectorResult;
+
+    public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities,
+        long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl,
+        MediaSource mediaSource, Object periodUid, int periodIndex, boolean isLastPeriod,
+        long startPositionUs) {
+      this.renderers = renderers;
+      this.rendererCapabilities = rendererCapabilities;
+      this.rendererPositionOffsetUs = rendererPositionOffsetUs;
+      this.trackSelector = trackSelector;
+      this.loadControl = loadControl;
+      this.mediaSource = mediaSource;
+      this.uid = Assertions.checkNotNull(periodUid);
+      this.index = periodIndex;
+      this.isLast = isLastPeriod;
+      this.startPositionUs = startPositionUs;
+      sampleStreams = new SampleStream[renderers.length];
+      mayRetainStreamFlags = new boolean[renderers.length];
+      mediaPeriod = mediaSource.createPeriod(periodIndex, loadControl.getAllocator(),
+          startPositionUs);
+    }
+
+    public long toRendererTime(long periodTimeUs) {
+      return periodTimeUs + getRendererOffset();
+    }
+
+    public long toPeriodTime(long rendererTimeUs) {
+      return rendererTimeUs - getRendererOffset();
+    }
+
+    public long getRendererOffset() {
+      return rendererPositionOffsetUs - startPositionUs;
+    }
+
+    public void setIndex(int index, boolean isLast) {
+      this.index = index;
+      this.isLast = isLast;
+    }
+
+    public boolean isFullyBuffered() {
+      return prepared
+          && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE);
+    }
+
+    public void handlePrepared() throws ExoPlaybackException {
+      prepared = true;
+      selectTracks();
+      startPositionUs = updatePeriodTrackSelection(startPositionUs, false);
+    }
+
+    public boolean selectTracks() throws ExoPlaybackException {
+      TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities,
+          mediaPeriod.getTrackGroups());
+      if (selectorResult.isEquivalent(periodTrackSelectorResult)) {
+        return false;
+      }
+      trackSelectorResult = selectorResult;
+      return true;
+    }
+
+    public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams) {
+      return updatePeriodTrackSelection(positionUs, forceRecreateStreams,
+          new boolean[renderers.length]);
+    }
+
+    public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams,
+        boolean[] streamResetFlags) {
+      TrackSelectionArray trackSelections = trackSelectorResult.selections;
+      for (int i = 0; i < trackSelections.length; i++) {
+        mayRetainStreamFlags[i] = !forceRecreateStreams
+            && trackSelectorResult.isEquivalent(periodTrackSelectorResult, i);
+      }
+
+      // Disable streams on the period and get new streams for updated/newly-enabled tracks.
+      positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags,
+          sampleStreams, streamResetFlags, positionUs);
+      periodTrackSelectorResult = trackSelectorResult;
+
+      // Update whether we have enabled tracks and sanity check the expected streams are non-null.
+      hasEnabledTracks = false;
+      for (int i = 0; i < sampleStreams.length; i++) {
+        if (sampleStreams[i] != null) {
+          Assertions.checkState(trackSelections.get(i) != null);
+          hasEnabledTracks = true;
+        } else {
+          Assertions.checkState(trackSelections.get(i) == null);
+        }
+      }
+
+      // The track selection has changed.
+      loadControl.onTracksSelected(renderers, trackSelectorResult.groups, trackSelections);
+      return positionUs;
+    }
+
+    public void release() {
+      try {
+        mediaSource.releasePeriod(mediaPeriod);
+      } catch (RuntimeException e) {
+        // There's nothing we can do.
+        Log.e(TAG, "Period release failed.", e);
+      }
+    }
+
+  }
+
+  private 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;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/ExoPlayerLibraryInfo.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 com.google.android.exoplayer2;
+
+/**
+ * Information about the ExoPlayer library.
+ */
+public interface ExoPlayerLibraryInfo {
+
+  /**
+   * The version of the library, expressed as a string.
+   */
+  String VERSION = "2.2.0";
+
+  /**
+   * The version of the library, expressed as an integer.
+   * <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).
+   */
+  int VERSION_INT = 2002000;
+
+  /**
+   * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
+   * checks enabled.
+   */
+  boolean ASSERTIONS_ENABLED = true;
+
+  /**
+   * Whether the library was compiled with {@link com.google.android.exoplayer2.util.TraceUtil}
+   * trace enabled.
+   */
+  boolean TRACE_ENABLED = true;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/Format.java
@@ -0,0 +1,692 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.MediaFormat;
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
+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.
+   */
+  public final String id;
+  /**
+   * The average bandwidth in bits per second, or {@link #NO_VALUE} if unknown or not applicable.
+   */
+  public final int bitrate;
+  /**
+   * Codecs of the format as described in RFC 6381, or null if unknown or not applicable.
+   */
+  public final String codecs;
+  /**
+   * Metadata, or null if unknown or not applicable.
+   */
+  public final Metadata metadata;
+
+  // Container specific.
+
+  /**
+   * The mime type of the container, or null if unknown or not applicable.
+   */
+  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.
+   */
+  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.
+   */
+  public final DrmInitData drmInitData;
+
+  // 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 {@link #NO_VALUE} if unknown or not applicable. Only 0, 90, 180 and 270 are
+   * supported.
+   */
+  public final int rotationDegrees;
+  /**
+   * The width to height ratio of pixels in the video, or {@link #NO_VALUE} if unknown or not
+   * applicable.
+   */
+  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}.
+   */
+  @C.StereoMode
+  public final int stereoMode;
+  /**
+   * The projection data for 360/VR video, or null if not applicable.
+   */
+  public final byte[] projectionData;
+
+  // 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 encoding for PCM audio streams. If {@link #sampleMimeType} is {@link MimeTypes#AUDIO_RAW}
+   * then one of {@link C#ENCODING_PCM_8BIT}, {@link C#ENCODING_PCM_16BIT},
+   * {@link C#ENCODING_PCM_24BIT} and {@link C#ENCODING_PCM_32BIT}. Set to {@link #NO_VALUE} for
+   * other media types.
+   */
+  @C.PcmEncoding
+  public final int pcmEncoding;
+  /**
+   * The number of samples to trim from the start of the decoded audio stream.
+   */
+  public final int encoderDelay;
+  /**
+   * The number of samples to trim from the end of the decoded audio stream.
+   */
+  public final int encoderPadding;
+
+  // Text specific.
+
+  /**
+   * For samples that contain subsamples, this is an offset that should be added to subsample
+   * timestamps. A value of {@link #OFFSET_SAMPLE_RELATIVE} indicates that subsample timestamps are
+   * relative to the timestamps of their parent samples.
+   */
+  public final long subsampleOffsetUs;
+
+  // Audio and text specific.
+
+  /**
+   * Track selection flags.
+   */
+  @C.SelectionFlags
+  public final int selectionFlags;
+
+  /**
+   * The language, or null if unknown or not applicable.
+   */
+  public final String language;
+
+  /**
+   * The Accessibility channel, or {@link #NO_VALUE} if not known or applicable.
+   */
+  public final int accessibilityChannel;
+
+  // Lazily initialized hashcode.
+  private int hashCode;
+
+  // Video.
+
+  public static Format createVideoContainerFormat(String id, String containerMimeType,
+      String sampleMimeType, String codecs, int bitrate, int width, int height,
+      float frameRate, List<byte[]> initializationData, @C.SelectionFlags int selectionFlags) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width,
+        height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, selectionFlags, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
+        initializationData, null, null);
+  }
+
+  public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, int maxInputSize, int width, int height, float frameRate,
+      List<byte[]> initializationData, DrmInitData drmInitData) {
+    return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width,
+        height, frameRate, initializationData, NO_VALUE, NO_VALUE, drmInitData);
+  }
+
+  public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, int maxInputSize, int width, int height, float frameRate,
+      List<byte[]> initializationData, int rotationDegrees, float pixelWidthHeightRatio,
+      DrmInitData drmInitData) {
+    return createVideoSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, width,
+        height, frameRate, initializationData, rotationDegrees, pixelWidthHeightRatio, null,
+        NO_VALUE, drmInitData);
+  }
+
+  public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, int maxInputSize, int width, int height, float frameRate,
+      List<byte[]> initializationData, int rotationDegrees, float pixelWidthHeightRatio,
+      byte[] projectionData, @C.StereoMode int stereoMode, DrmInitData drmInitData) {
+    return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height,
+        frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
+        initializationData, drmInitData, null);
+  }
+
+  // Audio.
+
+  public static Format createAudioContainerFormat(String id, String containerMimeType,
+      String sampleMimeType, String codecs, int bitrate, int channelCount, int sampleRate,
+      List<byte[]> initializationData, @C.SelectionFlags int selectionFlags, String language) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, NO_VALUE,
+        NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
+        initializationData, null, null);
+  }
+
+  public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, int maxInputSize, int channelCount, int sampleRate,
+      List<byte[]> initializationData, DrmInitData drmInitData,
+      @C.SelectionFlags int selectionFlags, String language) {
+    return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount,
+        sampleRate, NO_VALUE, initializationData, drmInitData, selectionFlags, language);
+  }
+
+  public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, int maxInputSize, int channelCount, int sampleRate,
+      @C.PcmEncoding int pcmEncoding, List<byte[]> initializationData, DrmInitData drmInitData,
+      @C.SelectionFlags int selectionFlags, String language) {
+    return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount,
+        sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, initializationData, drmInitData,
+        selectionFlags, language, null);
+  }
+
+  public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, int maxInputSize, int channelCount, int sampleRate,
+      @C.PcmEncoding int pcmEncoding, int encoderDelay, int encoderPadding,
+      List<byte[]> initializationData, DrmInitData drmInitData,
+      @C.SelectionFlags int selectionFlags, String language, Metadata metadata) {
+    return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, pcmEncoding,
+        encoderDelay, encoderPadding, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
+        initializationData, drmInitData, metadata);
+  }
+
+  // Text.
+
+  public static Format createTextContainerFormat(String id, String containerMimeType,
+      String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
+      String language) {
+    return createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate,
+        selectionFlags, language, NO_VALUE);
+  }
+
+  public static Format createTextContainerFormat(String id, String containerMimeType,
+      String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
+      String language, int accessibilityChannel) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, selectionFlags, language, accessibilityChannel,
+        OFFSET_SAMPLE_RELATIVE, null, null, null);
+  }
+
+  public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) {
+    return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
+        NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE);
+  }
+
+  public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel,
+      DrmInitData drmInitData) {
+    return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
+        accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE);
+  }
+
+  public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData,
+      long subsampleOffsetUs) {
+    return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
+        NO_VALUE, drmInitData, subsampleOffsetUs);
+  }
+
+  public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, @C.SelectionFlags int selectionFlags, String language,
+      int accessibilityChannel, DrmInitData drmInitData, long subsampleOffsetUs) {
+    return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, null,
+        drmInitData, null);
+  }
+
+  // Image.
+
+  public static Format createImageSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, List<byte[]> initializationData, String language, DrmInitData drmInitData) {
+    return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, 0, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData,
+        null);
+  }
+
+  // Generic.
+
+  public static Format createContainerFormat(String id, String containerMimeType,
+      String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
+      String language) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null,
+        null);
+  }
+
+  public static Format createSampleFormat(String id, String sampleMimeType,
+      long subsampleOffsetUs) {
+    return new Format(id, null, sampleMimeType, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, 0, null, NO_VALUE, subsampleOffsetUs, null, null, null);
+  }
+
+  public static Format createSampleFormat(String id, String sampleMimeType, String codecs,
+      int bitrate, DrmInitData drmInitData) {
+    return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null);
+  }
+
+  /* package */ Format(String id, String containerMimeType, String sampleMimeType, String codecs,
+      int bitrate, int maxInputSize, int width, int height, float frameRate, int rotationDegrees,
+      float pixelWidthHeightRatio, byte[] projectionData, @C.StereoMode int stereoMode,
+      int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, int encoderDelay,
+      int encoderPadding, @C.SelectionFlags int selectionFlags, String language,
+      int accessibilityChannel, long subsampleOffsetUs, List<byte[]> initializationData,
+      DrmInitData drmInitData, Metadata metadata) {
+    this.id = id;
+    this.containerMimeType = containerMimeType;
+    this.sampleMimeType = sampleMimeType;
+    this.codecs = codecs;
+    this.bitrate = bitrate;
+    this.maxInputSize = maxInputSize;
+    this.width = width;
+    this.height = height;
+    this.frameRate = frameRate;
+    this.rotationDegrees = rotationDegrees;
+    this.pixelWidthHeightRatio = pixelWidthHeightRatio;
+    this.projectionData = projectionData;
+    this.stereoMode = stereoMode;
+    this.channelCount = channelCount;
+    this.sampleRate = sampleRate;
+    this.pcmEncoding = pcmEncoding;
+    this.encoderDelay = encoderDelay;
+    this.encoderPadding = encoderPadding;
+    this.selectionFlags = selectionFlags;
+    this.language = language;
+    this.accessibilityChannel = accessibilityChannel;
+    this.subsampleOffsetUs = subsampleOffsetUs;
+    this.initializationData = initializationData == null ? Collections.<byte[]>emptyList()
+        : initializationData;
+    this.drmInitData = drmInitData;
+    this.metadata = metadata;
+  }
+
+  @SuppressWarnings("ResourceType")
+  /* package */ Format(Parcel in) {
+    id = in.readString();
+    containerMimeType = in.readString();
+    sampleMimeType = in.readString();
+    codecs = in.readString();
+    bitrate = in.readInt();
+    maxInputSize = in.readInt();
+    width = in.readInt();
+    height = in.readInt();
+    frameRate = in.readFloat();
+    rotationDegrees = in.readInt();
+    pixelWidthHeightRatio = in.readFloat();
+    boolean hasProjectionData = in.readInt() != 0;
+    projectionData = hasProjectionData ? in.createByteArray() : null;
+    stereoMode = in.readInt();
+    channelCount = in.readInt();
+    sampleRate = in.readInt();
+    pcmEncoding = in.readInt();
+    encoderDelay = in.readInt();
+    encoderPadding = in.readInt();
+    selectionFlags = in.readInt();
+    language = in.readString();
+    accessibilityChannel = in.readInt();
+    subsampleOffsetUs = in.readLong();
+    int initializationDataSize = in.readInt();
+    initializationData = new ArrayList<>(initializationDataSize);
+    for (int i = 0; i < initializationDataSize; i++) {
+      initializationData.add(in.createByteArray());
+    }
+    drmInitData = in.readParcelable(DrmInitData.class.getClassLoader());
+    metadata = in.readParcelable(Metadata.class.getClassLoader());
+  }
+
+  public Format copyWithMaxInputSize(int maxInputSize) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+        width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+        stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
+        selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
+        drmInitData, metadata);
+  }
+
+  public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+        width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+        stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
+        selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
+        drmInitData, metadata);
+  }
+
+  public Format copyWithContainerInfo(String id, String codecs, int bitrate, int width, int height,
+      @C.SelectionFlags int selectionFlags, String language) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+        width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+        stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
+        selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
+        drmInitData, metadata);
+  }
+
+  public Format copyWithManifestFormatInfo(Format manifestFormat,
+      boolean preferManifestDrmInitData) {
+    String id = manifestFormat.id;
+    String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs;
+    int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate;
+    float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate;
+    @C.SelectionFlags int selectionFlags = this.selectionFlags |  manifestFormat.selectionFlags;
+    String language = this.language == null ? manifestFormat.language : this.language;
+    DrmInitData drmInitData = (preferManifestDrmInitData && manifestFormat.drmInitData != null)
+        || this.drmInitData == null ? manifestFormat.drmInitData : this.drmInitData;
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
+        height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
+        channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags,
+        language, accessibilityChannel, subsampleOffsetUs, initializationData, drmInitData,
+        metadata);
+  }
+
+  public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+        width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+        stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
+        selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
+        drmInitData, metadata);
+  }
+
+  public Format copyWithDrmInitData(DrmInitData drmInitData) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+        width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+        stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
+        selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
+        drmInitData, metadata);
+  }
+
+  public Format copyWithMetadata(Metadata metadata) {
+    return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
+        width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
+        stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
+        selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
+        drmInitData, metadata);
+  }
+
+  /**
+   * 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);
+  }
+
+  /**
+   * Returns a {@link MediaFormat} representation of this format.
+   */
+  @SuppressLint("InlinedApi")
+  @TargetApi(16)
+  public final MediaFormat getFrameworkMediaFormatV16() {
+    MediaFormat format = new MediaFormat();
+    format.setString(MediaFormat.KEY_MIME, sampleMimeType);
+    maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language);
+    maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
+    maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width);
+    maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height);
+    maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate);
+    maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees);
+    maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount);
+    maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate);
+    maybeSetIntegerV16(format, "encoder-delay", encoderDelay);
+    maybeSetIntegerV16(format, "encoder-padding", encoderPadding);
+    for (int i = 0; i < initializationData.size(); i++) {
+      format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
+    }
+    return format;
+  }
+
+  @Override
+  public String toString() {
+    return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", "
+        + language + ", [" + width + ", " + height + ", " + frameRate + "]"
+        + ", [" + channelCount + ", " + sampleRate + "])";
+  }
+
+  @Override
+  public int hashCode() {
+    if (hashCode == 0) {
+      int result = 17;
+      result = 31 * result + (id == null ? 0 : id.hashCode());
+      result = 31 * result + (containerMimeType == null ? 0 : containerMimeType.hashCode());
+      result = 31 * result + (sampleMimeType == null ? 0 : sampleMimeType.hashCode());
+      result = 31 * result + (codecs == null ? 0 : codecs.hashCode());
+      result = 31 * result + bitrate;
+      result = 31 * result + width;
+      result = 31 * result + height;
+      result = 31 * result + channelCount;
+      result = 31 * result + sampleRate;
+      result = 31 * result + (language == null ? 0 : language.hashCode());
+      result = 31 * result + accessibilityChannel;
+      result = 31 * result + (drmInitData == null ? 0 : drmInitData.hashCode());
+      result = 31 * result + (metadata == null ? 0 : metadata.hashCode());
+      hashCode = result;
+    }
+    return hashCode;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null || getClass() != obj.getClass()) {
+      return false;
+    }
+    Format other = (Format) obj;
+    if (bitrate != other.bitrate || maxInputSize != other.maxInputSize
+        || width != other.width || height != other.height || frameRate != other.frameRate
+        || rotationDegrees != other.rotationDegrees
+        || pixelWidthHeightRatio != other.pixelWidthHeightRatio || stereoMode != other.stereoMode
+        || channelCount != other.channelCount || sampleRate != other.sampleRate
+        || pcmEncoding != other.pcmEncoding || encoderDelay != other.encoderDelay
+        || encoderPadding != other.encoderPadding || subsampleOffsetUs != other.subsampleOffsetUs
+        || selectionFlags != other.selectionFlags || !Util.areEqual(id, other.id)
+        || !Util.areEqual(language, other.language)
+        || accessibilityChannel != other.accessibilityChannel
+        || !Util.areEqual(containerMimeType, other.containerMimeType)
+        || !Util.areEqual(sampleMimeType, other.sampleMimeType)
+        || !Util.areEqual(codecs, other.codecs)
+        || !Util.areEqual(drmInitData, other.drmInitData)
+        || !Util.areEqual(metadata, other.metadata)
+        || !Arrays.equals(projectionData, other.projectionData)
+        || 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;
+  }
+
+  @TargetApi(16)
+  private static void maybeSetStringV16(MediaFormat format, String key, String value) {
+    if (value != null) {
+      format.setString(key, value);
+    }
+  }
+
+  @TargetApi(16)
+  private static void maybeSetIntegerV16(MediaFormat format, String key, int value) {
+    if (value != NO_VALUE) {
+      format.setInteger(key, value);
+    }
+  }
+
+  @TargetApi(16)
+  private static void maybeSetFloatV16(MediaFormat format, String key, float value) {
+    if (value != NO_VALUE) {
+      format.setFloat(key, value);
+    }
+  }
+
+  // Utility methods
+
+  /**
+   * Returns a prettier {@link String} than {@link #toString()}, intended for logging.
+   */
+  public static String toLogString(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.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);
+    }
+    return builder.toString();
+  }
+
+  // Parcelable implementation.
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+  @Override
+  public void writeToParcel(Parcel dest, int flags) {
+    dest.writeString(id);
+    dest.writeString(containerMimeType);
+    dest.writeString(sampleMimeType);
+    dest.writeString(codecs);
+    dest.writeInt(bitrate);
+    dest.writeInt(maxInputSize);
+    dest.writeInt(width);
+    dest.writeInt(height);
+    dest.writeFloat(frameRate);
+    dest.writeInt(rotationDegrees);
+    dest.writeFloat(pixelWidthHeightRatio);
+    dest.writeInt(projectionData != null ? 1 : 0);
+    if (projectionData != null) {
+      dest.writeByteArray(projectionData);
+    }
+    dest.writeInt(stereoMode);
+    dest.writeInt(channelCount);
+    dest.writeInt(sampleRate);
+    dest.writeInt(pcmEncoding);
+    dest.writeInt(encoderDelay);
+    dest.writeInt(encoderPadding);
+    dest.writeInt(selectionFlags);
+    dest.writeString(language);
+    dest.writeInt(accessibilityChannel);
+    dest.writeLong(subsampleOffsetUs);
+    int initializationDataSize = initializationData.size();
+    dest.writeInt(initializationDataSize);
+    for (int i = 0; i < initializationDataSize; i++) {
+      dest.writeByteArray(initializationData.get(i));
+    }
+    dest.writeParcelable(drmInitData, 0);
+    dest.writeParcelable(metadata, 0);
+  }
+
+  /**
+   * {@link Creator} implementation.
+   */
+  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];
+    }
+
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/FormatHolder.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 com.google.android.exoplayer2;
+
+/**
+ * Holds a {@link Format}.
+ */
+public final class FormatHolder {
+
+  /**
+   * The held {@link Format}.
+   */
+  public Format format;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/LoadControl.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import 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();
+
+  /**
+   * Called by the player to determine whether sufficient media is buffered for playback to be
+   * started or resumed.
+   *
+   * @param bufferedDurationUs The duration of media that's currently buffered.
+   * @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by
+   *     buffer depletion rather than a user action. Hence this parameter is false during initial
+   *     buffering and when buffering as a result of a seek operation.
+   * @return Whether playback should be allowed to start or resume.
+   */
+  boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering);
+
+  /**
+   * Called by the player to determine whether it should continue to load the source.
+   *
+   * @param bufferedDurationUs The duration of media that's currently buffered.
+   * @return Whether the loading should continue.
+   */
+  boolean shouldContinueLoading(long bufferedDurationUs);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 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);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/Renderer.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.util.MediaClock;
+import java.io.IOException;
+
+/**
+ * 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 changes. The valid state
+ * transitions are shown below, annotated with the methods that are called during each transition.
+ * <p align="center">
+ *   <img src="doc-files/renderer-states.svg" alt="Renderer state transitions">
+ * </p>
+ */
+public interface Renderer extends ExoPlayerComponent {
+
+  /**
+   * The renderer is disabled.
+   */
+  int STATE_DISABLED = 0;
+  /**
+   * The renderer is enabled but not started. A renderer in this state is not actively rendering
+   * media, but will typically hold resources that it requires for rendering (e.g. media decoders).
+   */
+  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.
+   */
+  MediaClock getMediaClock();
+
+  /**
+   * Returns the current state of the renderer.
+   *
+   * @return The current state (one of the {@code STATE_*} constants).
+   */
+  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.
+   */
+  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();
+
+  /**
+   * 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;
+
+  /**
+   * 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 ExoPlayer#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();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/RendererCapabilities.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import com.google.android.exoplayer2.util.MimeTypes;
+
+/**
+ * Defines the capabilities of a {@link Renderer}.
+ */
+public interface RendererCapabilities {
+
+  /**
+   * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
+   * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES},
+   * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}.
+   */
+  int FORMAT_SUPPORT_MASK = 0b11;
+  /**
+   * The {@link Renderer} is capable of rendering the format.
+   */
+  int FORMAT_HANDLED = 0b11;
+  /**
+   * The {@link Renderer} is capable of rendering formats with the same mime type, but the
+   * properties of the format exceed the renderer's capability.
+   * <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 = 0b10;
+  /**
+   * 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 = 0b01;
+  /**
+   * 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 = 0b00;
+
+  /**
+   * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
+   * {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and {@link #ADAPTIVE_NOT_SUPPORTED}.
+   */
+  int ADAPTIVE_SUPPORT_MASK = 0b1100;
+  /**
+   * The {@link Renderer} can seamlessly adapt between formats.
+   */
+  int ADAPTIVE_SEAMLESS = 0b1000;
+  /**
+   * The {@link Renderer} can adapt between formats, but may suffer a brief discontinuity
+   * (~50-100ms) when adaptation occurs.
+   */
+  int ADAPTIVE_NOT_SEAMLESS = 0b0100;
+  /**
+   * The {@link Renderer} does not support adaptation between formats.
+   */
+  int ADAPTIVE_NOT_SUPPORTED = 0b0000;
+
+  /**
+   * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
+   * {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}.
+   */
+  int TUNNELING_SUPPORT_MASK = 0b10000;
+  /**
+   * The {@link Renderer} supports tunneled output.
+   */
+  int TUNNELING_SUPPORTED = 0b10000;
+  /**
+   * The {@link Renderer} does not support tunneled output.
+   */
+  int TUNNELING_NOT_SUPPORTED = 0b00000;
+
+  /**
+   * 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. The returned value is
+   * the bitwise OR of three properties:
+   * <ul>
+   * <li>The level of support for the format itself. One of {@link #FORMAT_HANDLED},
+   * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_SUBTYPE} and
+   * {@link #FORMAT_UNSUPPORTED_TYPE}.</li>
+   * <li>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}.</li>
+   * <li>The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and
+   * {@link #TUNNELING_NOT_SUPPORTED}.</li>
+   * </ul>
+   * The individual properties can be retrieved by performing a bitwise AND with
+   * {@link #FORMAT_SUPPORT_MASK}, {@link #ADAPTIVE_SUPPORT_MASK} and
+   * {@link #TUNNELING_SUPPORT_MASK} respectively.
+   *
+   * @param format The format.
+   * @return The extent to which the renderer is capable of supporting the given format.
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  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 extent to which the renderer supports adapting between supported formats that have
+   *     different mime types. One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and
+   *     {@link #ADAPTIVE_NOT_SUPPORTED}.
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/RendererConfiguration.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 com.google.android.exoplayer2;
+
+/**
+ * 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(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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/SimpleExoPlayer.java
@@ -0,0 +1,1025 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.media.MediaCodec;
+import android.media.PlaybackParams;
+import android.os.Handler;
+import android.support.annotation.IntDef;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import com.google.android.exoplayer2.audio.AudioCapabilities;
+import com.google.android.exoplayer2.audio.AudioRendererEventListener;
+import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
+import com.google.android.exoplayer2.decoder.DecoderCounters;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.MetadataRenderer;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.TextRenderer;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
+import com.google.android.exoplayer2.video.VideoRendererEventListener;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can
+ * be obtained from {@link ExoPlayerFactory}.
+ */
+@TargetApi(16)
+public class SimpleExoPlayer implements ExoPlayer {
+
+  /**
+   * A listener for video rendering information from a {@link SimpleExoPlayer}.
+   */
+  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.
+     */
+    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 a video track was selected.
+     */
+    void onRenderedFirstFrame();
+
+  }
+
+  /**
+   * Modes for using extension renderers.
+   */
+  @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 = "SimpleExoPlayer";
+  protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;
+
+  private final ExoPlayer player;
+  private final Renderer[] renderers;
+  private final ComponentListener componentListener;
+  private final Handler mainHandler;
+  private final int videoRendererCount;
+  private final int audioRendererCount;
+
+  private Format videoFormat;
+  private Format audioFormat;
+
+  private Surface surface;
+  private boolean ownsSurface;
+  @C.VideoScalingMode
+  private int videoScalingMode;
+  private SurfaceHolder surfaceHolder;
+  private TextureView textureView;
+  private TextRenderer.Output textOutput;
+  private MetadataRenderer.Output metadataOutput;
+  private VideoListener videoListener;
+  private AudioRendererEventListener audioDebugListener;
+  private VideoRendererEventListener videoDebugListener;
+  private DecoderCounters videoDecoderCounters;
+  private DecoderCounters audioDecoderCounters;
+  private int audioSessionId;
+  @C.StreamType
+  private int audioStreamType;
+  private float audioVolume;
+  private PlaybackParamsHolder playbackParamsHolder;
+
+  protected SimpleExoPlayer(Context context, TrackSelector trackSelector, LoadControl loadControl,
+      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) {
+    mainHandler = new Handler();
+    componentListener = new ComponentListener();
+
+    // Build the renderers.
+    ArrayList<Renderer> renderersList = new ArrayList<>();
+    buildRenderers(context, mainHandler, drmSessionManager, extensionRendererMode,
+        allowedVideoJoiningTimeMs, renderersList);
+    renderers = renderersList.toArray(new Renderer[renderersList.size()]);
+
+    // Obtain counts of video and audio renderers.
+    int videoRendererCount = 0;
+    int audioRendererCount = 0;
+    for (Renderer renderer : renderers) {
+      switch (renderer.getTrackType()) {
+        case C.TRACK_TYPE_VIDEO:
+          videoRendererCount++;
+          break;
+        case C.TRACK_TYPE_AUDIO:
+          audioRendererCount++;
+          break;
+      }
+    }
+    this.videoRendererCount = videoRendererCount;
+    this.audioRendererCount = audioRendererCount;
+
+    // Set initial values.
+    audioVolume = 1;
+    audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+    audioStreamType = C.STREAM_TYPE_DEFAULT;
+    videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
+
+    // Build the player and associated objects.
+    player = new ExoPlayerImpl(renderers, trackSelector, loadControl);
+  }
+
+  /**
+   * 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.
+   */
+  public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) {
+    this.videoScalingMode = videoScalingMode;
+    ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount];
+    int count = 0;
+    for (Renderer renderer : renderers) {
+      if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {
+        messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SCALING_MODE,
+            videoScalingMode);
+      }
+    }
+    player.sendMessages(messages);
+  }
+
+  /**
+   * Returns the video scaling mode.
+   */
+  public @C.VideoScalingMode int getVideoScalingMode() {
+    return videoScalingMode;
+  }
+
+  /**
+   * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView}
+   * currently set on the player.
+   */
+  public void clearVideoSurface() {
+    setVideoSurface(null);
+  }
+
+  /**
+   * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for
+   * tracking the lifecycle of the surface, and must clear the surface by calling
+   * {@code setVideoSurface(null)} if the surface is destroyed.
+   * <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}.
+   */
+  public void setVideoSurface(Surface surface) {
+    removeSurfaceCallbacks();
+    setVideoSurfaceInternal(surface, false);
+  }
+
+  /**
+   * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be
+   * rendered. The player will track the lifecycle of the surface automatically.
+   *
+   * @param surfaceHolder The surface holder.
+   */
+  public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) {
+    removeSurfaceCallbacks();
+    this.surfaceHolder = surfaceHolder;
+    if (surfaceHolder == null) {
+      setVideoSurfaceInternal(null, false);
+    } else {
+      setVideoSurfaceInternal(surfaceHolder.getSurface(), false);
+      surfaceHolder.addCallback(componentListener);
+    }
+  }
+
+  /**
+   * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the
+   * lifecycle of the surface automatically.
+   *
+   * @param surfaceView The surface view.
+   */
+  public void setVideoSurfaceView(SurfaceView surfaceView) {
+    setVideoSurfaceHolder(surfaceView.getHolder());
+  }
+
+  /**
+   * Sets the {@link TextureView} onto which video will be rendered. The player will track the
+   * lifecycle of the surface automatically.
+   *
+   * @param textureView The texture view.
+   */
+  public void setVideoTextureView(TextureView textureView) {
+    removeSurfaceCallbacks();
+    this.textureView = textureView;
+    if (textureView == null) {
+      setVideoSurfaceInternal(null, true);
+    } else {
+      if (textureView.getSurfaceTextureListener() != null) {
+        Log.w(TAG, "Replacing existing SurfaceTextureListener.");
+      }
+      SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
+      setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true);
+      textureView.setSurfaceTextureListener(componentListener);
+    }
+  }
+
+  /**
+   * Sets the stream type for audio playback (see {@link C.StreamType} and
+   * {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}). If the stream type
+   * is not set, audio renderers use {@link C#STREAM_TYPE_DEFAULT}.
+   * <p>
+   * Note that when the stream type changes, the AudioTrack must be reinitialized, which can
+   * introduce a brief gap in audio output. Note also that tracks in the same audio session must
+   * share the same routing, so a new audio session id will be generated.
+   *
+   * @param audioStreamType The stream type for audio playback.
+   */
+  public void setAudioStreamType(@C.StreamType int audioStreamType) {
+    this.audioStreamType = audioStreamType;
+    ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount];
+    int count = 0;
+    for (Renderer renderer : renderers) {
+      if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
+        messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_STREAM_TYPE, audioStreamType);
+      }
+    }
+    player.sendMessages(messages);
+  }
+
+  /**
+   * Returns the stream type for audio playback.
+   */
+  public @C.StreamType int getAudioStreamType() {
+    return audioStreamType;
+  }
+
+  /**
+   * Sets the audio volume, with 0 being silence and 1 being unity gain.
+   *
+   * @param audioVolume The audio volume.
+   */
+  public void setVolume(float audioVolume) {
+    this.audioVolume = audioVolume;
+    ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount];
+    int count = 0;
+    for (Renderer renderer : renderers) {
+      if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
+        messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_VOLUME, audioVolume);
+      }
+    }
+    player.sendMessages(messages);
+  }
+
+  /**
+   * Returns the audio volume, with 0 being silence and 1 being unity gain.
+   */
+  public float getVolume() {
+    return audioVolume;
+  }
+
+  /**
+   * Sets the {@link PlaybackParams} governing audio playback.
+   *
+   * @param params The {@link PlaybackParams}, or null to clear any previously set parameters.
+   */
+  @TargetApi(23)
+  public void setPlaybackParams(PlaybackParams params) {
+    if (params != null) {
+      // The audio renderers will call this on the playback thread to ensure they can query
+      // parameters without failure. We do the same up front, which is redundant except that it
+      // ensures an immediate call to getPlaybackParams will retrieve the instance with defaults
+      // allowed, rather than this change becoming visible sometime later once the audio renderers
+      // receive the parameters.
+      params.allowDefaults();
+      playbackParamsHolder = new PlaybackParamsHolder(params);
+    } else {
+      playbackParamsHolder = null;
+    }
+    ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount];
+    int count = 0;
+    for (Renderer renderer : renderers) {
+      if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
+        messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_PLAYBACK_PARAMS, params);
+      }
+    }
+    player.sendMessages(messages);
+  }
+
+  /**
+   * Returns the {@link PlaybackParams} governing audio playback, or null if not set.
+   */
+  @TargetApi(23)
+  public PlaybackParams getPlaybackParams() {
+    return playbackParamsHolder == null ? null : playbackParamsHolder.params;
+  }
+
+  /**
+   * Returns the video format currently being played, or null if no video is being played.
+   */
+  public Format getVideoFormat() {
+    return videoFormat;
+  }
+
+  /**
+   * Returns the audio format currently being played, or null if no audio is being played.
+   */
+  public Format getAudioFormat() {
+    return audioFormat;
+  }
+
+  /**
+   * Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set.
+   */
+  public int getAudioSessionId() {
+    return audioSessionId;
+  }
+
+  /**
+   * Returns {@link DecoderCounters} for video, or null if no video is being played.
+   */
+  public DecoderCounters getVideoDecoderCounters() {
+    return videoDecoderCounters;
+  }
+
+  /**
+   * Returns {@link DecoderCounters} for audio, or null if no audio is being played.
+   */
+  public DecoderCounters getAudioDecoderCounters() {
+    return audioDecoderCounters;
+  }
+
+  /**
+   * Sets a listener to receive video events.
+   *
+   * @param listener The listener.
+   */
+  public void setVideoListener(VideoListener listener) {
+    videoListener = listener;
+  }
+
+  /**
+   * Sets a listener to receive debug events from the video renderer.
+   *
+   * @param listener The listener.
+   */
+  public void setVideoDebugListener(VideoRendererEventListener listener) {
+    videoDebugListener = listener;
+  }
+
+  /**
+   * Sets a listener to receive debug events from the audio renderer.
+   *
+   * @param listener The listener.
+   */
+  public void setAudioDebugListener(AudioRendererEventListener listener) {
+    audioDebugListener = listener;
+  }
+
+  /**
+   * Sets an output to receive text events.
+   *
+   * @param output The output.
+   */
+  public void setTextOutput(TextRenderer.Output output) {
+    textOutput = output;
+  }
+
+  /**
+   * Sets a listener to receive metadata events.
+   *
+   * @param output The output.
+   */
+  public void setMetadataOutput(MetadataRenderer.Output output) {
+    metadataOutput = output;
+  }
+
+  // ExoPlayer implementation
+
+  @Override
+  public void addListener(EventListener listener) {
+    player.addListener(listener);
+  }
+
+  @Override
+  public void removeListener(EventListener listener) {
+    player.removeListener(listener);
+  }
+
+  @Override
+  public int getPlaybackState() {
+    return player.getPlaybackState();
+  }
+
+  @Override
+  public void prepare(MediaSource mediaSource) {
+    player.prepare(mediaSource);
+  }
+
+  @Override
+  public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
+    player.prepare(mediaSource, resetPosition, resetState);
+  }
+
+  @Override
+  public void setPlayWhenReady(boolean playWhenReady) {
+    player.setPlayWhenReady(playWhenReady);
+  }
+
+  @Override
+  public boolean getPlayWhenReady() {
+    return player.getPlayWhenReady();
+  }
+
+  @Override
+  public boolean isLoading() {
+    return player.isLoading();
+  }
+
+  @Override
+  public void seekToDefaultPosition() {
+    player.seekToDefaultPosition();
+  }
+
+  @Override
+  public void seekToDefaultPosition(int windowIndex) {
+    player.seekToDefaultPosition(windowIndex);
+  }
+
+  @Override
+  public void seekTo(long positionMs) {
+    player.seekTo(positionMs);
+  }
+
+  @Override
+  public void seekTo(int windowIndex, long positionMs) {
+    player.seekTo(windowIndex, positionMs);
+  }
+
+  @Override
+  public void stop() {
+    player.stop();
+  }
+
+  @Override
+  public void release() {
+    player.release();
+    removeSurfaceCallbacks();
+    if (surface != null) {
+      if (ownsSurface) {
+        surface.release();
+      }
+      surface = null;
+    }
+  }
+
+  @Override
+  public void sendMessages(ExoPlayerMessage... messages) {
+    player.sendMessages(messages);
+  }
+
+  @Override
+  public void blockingSendMessages(ExoPlayerMessage... messages) {
+    player.blockingSendMessages(messages);
+  }
+
+  @Override
+  public int getRendererCount() {
+    return player.getRendererCount();
+  }
+
+  @Override
+  public int getRendererType(int index) {
+    return player.getRendererType(index);
+  }
+
+  @Override
+  public TrackGroupArray getCurrentTrackGroups() {
+    return player.getCurrentTrackGroups();
+  }
+
+  @Override
+  public TrackSelectionArray getCurrentTrackSelections() {
+    return player.getCurrentTrackSelections();
+  }
+
+  @Override
+  public Timeline getCurrentTimeline() {
+    return player.getCurrentTimeline();
+  }
+
+  @Override
+  public Object getCurrentManifest() {
+    return player.getCurrentManifest();
+  }
+
+  @Override
+  public int getCurrentPeriodIndex() {
+    return player.getCurrentPeriodIndex();
+  }
+
+  @Override
+  public int getCurrentWindowIndex() {
+    return player.getCurrentWindowIndex();
+  }
+
+  @Override
+  public long getDuration() {
+    return player.getDuration();
+  }
+
+  @Override
+  public long getCurrentPosition() {
+    return player.getCurrentPosition();
+  }
+
+  @Override
+  public long getBufferedPosition() {
+    return player.getBufferedPosition();
+  }
+
+  @Override
+  public int getBufferedPercentage() {
+    return player.getBufferedPercentage();
+  }
+
+  @Override
+  public boolean isCurrentWindowDynamic() {
+    return player.isCurrentWindowDynamic();
+  }
+
+  @Override
+  public boolean isCurrentWindowSeekable() {
+    return player.isCurrentWindowSeekable();
+  }
+
+  // Renderer building.
+
+  private void buildRenderers(Context context, Handler mainHandler,
+      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs,
+      ArrayList<Renderer> out) {
+    buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode,
+        componentListener, allowedVideoJoiningTimeMs, out);
+    buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode,
+        componentListener, out);
+    buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out);
+    buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out);
+    buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out);
+  }
+
+  /**
+   * Builds video renderers for use by the player.
+   *
+   * @param context The {@link Context} associated with the player.
+   * @param mainHandler A handler associated with the main thread's looper.
+   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
+   * not be used for DRM protected playbacks.
+   * @param extensionRendererMode The extension renderer mode.
+   * @param eventListener An event listener.
+   * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video renderers
+   *     can attempt to seamlessly join an ongoing playback.
+   * @param out An array to which the built renderers should be appended.
+   */
+  protected void buildVideoRenderers(Context context, Handler mainHandler,
+      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      @ExtensionRendererMode int extensionRendererMode, VideoRendererEventListener eventListener,
+      long allowedVideoJoiningTimeMs, ArrayList<Renderer> out) {
+    out.add(new MediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT,
+        allowedVideoJoiningTimeMs, drmSessionManager, false, mainHandler, 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 {
+      Class<?> clazz =
+          Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer");
+      Constructor<?> constructor = clazz.getConstructor(boolean.class, long.class, Handler.class,
+          VideoRendererEventListener.class, int.class);
+      Renderer renderer = (Renderer) constructor.newInstance(true, allowedVideoJoiningTimeMs,
+          mainHandler, componentListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
+      out.add(extensionRendererIndex++, renderer);
+      Log.i(TAG, "Loaded LibvpxVideoRenderer.");
+    } catch (ClassNotFoundException e) {
+      // Expected if the app was built without the extension.
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * Builds audio renderers for use by the player.
+   *
+   * @param context The {@link Context} associated with the player.
+   * @param mainHandler A handler associated with the main thread's looper.
+   * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
+   * not be used for DRM protected playbacks.
+   * @param extensionRendererMode The extension renderer mode.
+   * @param eventListener An event listener.
+   * @param out An array to which the built renderers should be appended.
+   */
+  protected void buildAudioRenderers(Context context, Handler mainHandler,
+      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      @ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener,
+      ArrayList<Renderer> out) {
+    out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true,
+        mainHandler, eventListener, AudioCapabilities.getCapabilities(context)));
+
+    if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
+      return;
+    }
+    int extensionRendererIndex = out.size();
+    if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {
+      extensionRendererIndex--;
+    }
+
+    try {
+      Class<?> clazz =
+          Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer");
+      Constructor<?> constructor = clazz.getConstructor(Handler.class,
+          AudioRendererEventListener.class);
+      Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener);
+      out.add(extensionRendererIndex++, renderer);
+      Log.i(TAG, "Loaded LibopusAudioRenderer.");
+    } catch (ClassNotFoundException e) {
+      // Expected if the app was built without the extension.
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+
+    try {
+      Class<?> clazz =
+          Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer");
+      Constructor<?> constructor = clazz.getConstructor(Handler.class,
+          AudioRendererEventListener.class);
+      Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener);
+      out.add(extensionRendererIndex++, renderer);
+      Log.i(TAG, "Loaded LibflacAudioRenderer.");
+    } catch (ClassNotFoundException e) {
+      // Expected if the app was built without the extension.
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+
+    try {
+      Class<?> clazz =
+          Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer");
+      Constructor<?> constructor = clazz.getConstructor(Handler.class,
+          AudioRendererEventListener.class);
+      Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener);
+      out.add(extensionRendererIndex++, renderer);
+      Log.i(TAG, "Loaded FfmpegAudioRenderer.");
+    } catch (ClassNotFoundException e) {
+      // Expected if the app was built without the extension.
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /**
+   * Builds text renderers for use by the player.
+   *
+   * @param context The {@link Context} associated with the player.
+   * @param mainHandler A handler associated with the main thread's looper.
+   * @param extensionRendererMode The extension renderer mode.
+   * @param output An output for the renderers.
+   * @param out An array to which the built renderers should be appended.
+   */
+  protected void buildTextRenderers(Context context, Handler mainHandler,
+      @ExtensionRendererMode int extensionRendererMode, TextRenderer.Output output,
+      ArrayList<Renderer> out) {
+    out.add(new TextRenderer(output, mainHandler.getLooper()));
+  }
+
+  /**
+   * Builds metadata renderers for use by the player.
+   *
+   * @param context The {@link Context} associated with the player.
+   * @param mainHandler A handler associated with the main thread's looper.
+   * @param extensionRendererMode The extension renderer mode.
+   * @param output An output for the renderers.
+   * @param out An array to which the built renderers should be appended.
+   */
+  protected void buildMetadataRenderers(Context context, Handler mainHandler,
+      @ExtensionRendererMode int extensionRendererMode, MetadataRenderer.Output output,
+      ArrayList<Renderer> out) {
+    out.add(new MetadataRenderer(output, mainHandler.getLooper()));
+  }
+
+  /**
+   * Builds any miscellaneous renderers used by the player.
+   *
+   * @param context The {@link Context} associated with the player.
+   * @param mainHandler A handler associated with the main thread's looper.
+   * @param extensionRendererMode The extension renderer mode.
+   * @param out An array to which the built renderers should be appended.
+   */
+  protected void buildMiscellaneousRenderers(Context context, Handler mainHandler,
+      @ExtensionRendererMode int extensionRendererMode, ArrayList<Renderer> out) {
+    // Do nothing.
+  }
+
+  // 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(Surface surface, boolean ownsSurface) {
+    // Note: We don't turn this method into a no-op if the surface is being replaced with itself
+    // so as to ensure onRenderedFirstFrame callbacks are still called in this case.
+    ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount];
+    int count = 0;
+    for (Renderer renderer : renderers) {
+      if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {
+        messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface);
+      }
+    }
+    if (this.surface != null && this.surface != surface) {
+      // If we created this surface, we are responsible for releasing it.
+      if (this.ownsSurface) {
+        this.surface.release();
+      }
+      // We're replacing a surface. Block to ensure that it's not accessed after the method returns.
+      player.blockingSendMessages(messages);
+    } else {
+      player.sendMessages(messages);
+    }
+    this.surface = surface;
+    this.ownsSurface = ownsSurface;
+  }
+
+  private final class ComponentListener implements VideoRendererEventListener,
+      AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output,
+      SurfaceHolder.Callback, TextureView.SurfaceTextureListener {
+
+    // VideoRendererEventListener implementation
+
+    @Override
+    public void onVideoEnabled(DecoderCounters counters) {
+      videoDecoderCounters = counters;
+      if (videoDebugListener != null) {
+        videoDebugListener.onVideoEnabled(counters);
+      }
+    }
+
+    @Override
+    public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs,
+        long initializationDurationMs) {
+      if (videoDebugListener != null) {
+        videoDebugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs,
+            initializationDurationMs);
+      }
+    }
+
+    @Override
+    public void onVideoInputFormatChanged(Format format) {
+      videoFormat = format;
+      if (videoDebugListener != null) {
+        videoDebugListener.onVideoInputFormatChanged(format);
+      }
+    }
+
+    @Override
+    public void onDroppedFrames(int count, long elapsed) {
+      if (videoDebugListener != null) {
+        videoDebugListener.onDroppedFrames(count, elapsed);
+      }
+    }
+
+    @Override
+    public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
+        float pixelWidthHeightRatio) {
+      if (videoListener != null) {
+        videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
+            pixelWidthHeightRatio);
+      }
+      if (videoDebugListener != null) {
+        videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
+            pixelWidthHeightRatio);
+      }
+    }
+
+    @Override
+    public void onRenderedFirstFrame(Surface surface) {
+      if (videoListener != null && SimpleExoPlayer.this.surface == surface) {
+        videoListener.onRenderedFirstFrame();
+      }
+      if (videoDebugListener != null) {
+        videoDebugListener.onRenderedFirstFrame(surface);
+      }
+    }
+
+    @Override
+    public void onVideoDisabled(DecoderCounters counters) {
+      if (videoDebugListener != null) {
+        videoDebugListener.onVideoDisabled(counters);
+      }
+      videoFormat = null;
+      videoDecoderCounters = null;
+    }
+
+    // AudioRendererEventListener implementation
+
+    @Override
+    public void onAudioEnabled(DecoderCounters counters) {
+      audioDecoderCounters = counters;
+      if (audioDebugListener != null) {
+        audioDebugListener.onAudioEnabled(counters);
+      }
+    }
+
+    @Override
+    public void onAudioSessionId(int sessionId) {
+      audioSessionId = sessionId;
+      if (audioDebugListener != null) {
+        audioDebugListener.onAudioSessionId(sessionId);
+      }
+    }
+
+    @Override
+    public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs,
+        long initializationDurationMs) {
+      if (audioDebugListener != null) {
+        audioDebugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs,
+            initializationDurationMs);
+      }
+    }
+
+    @Override
+    public void onAudioInputFormatChanged(Format format) {
+      audioFormat = format;
+      if (audioDebugListener != null) {
+        audioDebugListener.onAudioInputFormatChanged(format);
+      }
+    }
+
+    @Override
+    public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
+        long elapsedSinceLastFeedMs) {
+      if (audioDebugListener != null) {
+        audioDebugListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+      }
+    }
+
+    @Override
+    public void onAudioDisabled(DecoderCounters counters) {
+      if (audioDebugListener != null) {
+        audioDebugListener.onAudioDisabled(counters);
+      }
+      audioFormat = null;
+      audioDecoderCounters = null;
+      audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+    }
+
+    // TextRenderer.Output implementation
+
+    @Override
+    public void onCues(List<Cue> cues) {
+      if (textOutput != null) {
+        textOutput.onCues(cues);
+      }
+    }
+
+    // MetadataRenderer.Output implementation
+
+    @Override
+    public void onMetadata(Metadata metadata) {
+      if (metadataOutput != null) {
+        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) {
+      // Do nothing.
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+      setVideoSurfaceInternal(null, false);
+    }
+
+    // TextureView.SurfaceTextureListener implementation
+
+    @Override
+    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
+      setVideoSurfaceInternal(new Surface(surfaceTexture), true);
+    }
+
+    @Override
+    public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
+      // Do nothing.
+    }
+
+    @Override
+    public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+      setVideoSurfaceInternal(null, true);
+      return true;
+    }
+
+    @Override
+    public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+      // Do nothing.
+    }
+
+  }
+
+  @TargetApi(23)
+  private static final class PlaybackParamsHolder {
+
+    public final PlaybackParams params;
+
+    public PlaybackParamsHolder(PlaybackParams params) {
+      this.params = params;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/Timeline.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 com.google.android.exoplayer2;
+
+/**
+ * A representation of media currently available for playback.
+ * <p>
+ * Timeline instances are immutable. For cases where the available media is changing dynamically
+ * (e.g. live streams) a timeline provides a snapshot of the media currently available.
+ * <p>
+ * A timeline consists of related {@link Period}s and {@link Window}s. A period defines a single
+ * logical piece of media, for example a media file. A window spans one or more periods, defining
+ * the region within those periods that's currently available for playback along with additional
+ * information such as whether seeking is supported within the window. Each window defines a default
+ * position, which is the position from which playback will start when the player starts playing the
+ * window. The following examples illustrate timelines for various use cases.
+ *
+ * <h3 id="single-file">Single media file or on-demand stream</h3>
+ * <p align="center">
+ *   <img src="doc-files/timeline-single-file.svg" alt="Example timeline for a single file">
+ * </p>
+ * 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 align="center">
+ *   <img src="doc-files/timeline-playlist.svg" alt="Example timeline for a playlist of files">
+ * </p>
+ * 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 align="center">
+ *   <img src="doc-files/timeline-live-limited.svg" alt="Example timeline for a live stream with
+ *       limited availability">
+ * </p>
+ * A timeline for a live stream consists of a period whose duration is unknown, since it's
+ * continually extending as more content is broadcast. If content only remains available for a
+ * limited period of time then the window may start at a non-zero position, defining the region of
+ * content that can still be played. The window will have {@link Window#isDynamic} set to true if
+ * the stream is still live. Its default position is typically near to the live edge (indicated by
+ * the black dot in the figure above).
+ *
+ * <h3>Live stream with indefinite availability</h3>
+ * <p align="center">
+ *   <img src="doc-files/timeline-live-indefinite.svg" alt="Example timeline for a live stream with
+ *       indefinite availability">
+ * </p>
+ * 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 align="center">
+ *   <img src="doc-files/timeline-live-multi-period.svg" alt="Example timeline for a live stream
+ *       with multiple periods">
+ * </p>
+ * This case arises when a live stream is explicitly divided into separate periods, for example at
+ * content and advert boundaries. This case is similar to the <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 pre-roll followed by live stream</h3>
+ * <p align="center">
+ *   <img src="doc-files/timeline-advanced.svg" alt="Example timeline for an on-demand pre-roll
+ *       followed by a live stream">
+ * </p>
+ * 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 pre-roll ends, playback of the live stream will start from its default position near the
+ * live edge.
+ */
+public abstract class Timeline {
+
+  /**
+   * An empty timeline.
+   */
+  public static final Timeline EMPTY = new Timeline() {
+
+    @Override
+    public int getWindowCount() {
+      return 0;
+    }
+
+    @Override
+    public Window getWindow(int windowIndex, Window window, boolean setIds,
+        long defaultPositionProjectionUs) {
+      throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    public int getPeriodCount() {
+      return 0;
+    }
+
+    @Override
+    public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+      throw new IndexOutOfBoundsException();
+    }
+
+    @Override
+    public int getIndexOfPeriod(Object uid) {
+      return C.INDEX_UNSET;
+    }
+
+  };
+
+  /**
+   * Returns whether the timeline is empty.
+   */
+  public final boolean isEmpty() {
+    return getWindowCount() == 0;
+  }
+
+  /**
+   * Returns the number of windows in the timeline.
+   */
+  public abstract int getWindowCount();
+
+  /**
+   * Populates a {@link Window} with data for the window at the specified index. Does not populate
+   * {@link Window#id}.
+   *
+   * @param windowIndex The index of the window.
+   * @param window The {@link Window} to populate. Must not be null.
+   * @return The populated {@link Window}, for convenience.
+   */
+  public final Window getWindow(int windowIndex, Window window) {
+    return getWindow(windowIndex, window, false);
+  }
+
+  /**
+   * Populates a {@link Window} with data for the window at the specified index.
+   *
+   * @param windowIndex The index of the window.
+   * @param window The {@link Window} to populate. Must not be null.
+   * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
+   *     null. The caller should pass false for efficiency reasons unless the field is required.
+   * @return The populated {@link Window}, for convenience.
+   */
+  public Window getWindow(int windowIndex, Window window, boolean setIds) {
+    return getWindow(windowIndex, window, setIds, 0);
+  }
+
+  /**
+   * Populates a {@link Window} with data for the window at the specified index.
+   *
+   * @param windowIndex The index of the window.
+   * @param window The {@link Window} to populate. Must not be null.
+   * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
+   *     null. The caller should pass false for efficiency reasons unless the field is required.
+   * @param defaultPositionProjectionUs A duration into the future that the populated window's
+   *     default start position should be projected.
+   * @return The populated {@link Window}, for convenience.
+   */
+  public abstract Window getWindow(int windowIndex, Window window, boolean setIds,
+      long defaultPositionProjectionUs);
+
+  /**
+   * Returns the number of periods in the timeline.
+   */
+  public abstract int getPeriodCount();
+
+  /**
+   * Populates a {@link Period} with data for the period at the specified index. Does not populate
+   * {@link Period#id} and {@link Period#uid}.
+   *
+   * @param periodIndex The index of the period.
+   * @param period The {@link Period} to populate. Must not be null.
+   * @return The populated {@link Period}, for convenience.
+   */
+  public final Period getPeriod(int periodIndex, Period period) {
+    return getPeriod(periodIndex, period, false);
+  }
+
+  /**
+   * Populates a {@link Period} with data for the period at the specified index.
+   *
+   * @param periodIndex The index of the period.
+   * @param period The {@link Period} to populate. Must not be null.
+   * @param setIds Whether {@link Period#id} and {@link Period#uid} should be populated. If false,
+   *     the fields will be set to null. The caller should pass false for efficiency reasons unless
+   *     the fields are required.
+   * @return The populated {@link Period}, for convenience.
+   */
+  public abstract Period getPeriod(int periodIndex, Period period, boolean setIds);
+
+  /**
+   * Returns the index of the period identified by its unique {@code id}, or {@link C#INDEX_UNSET}
+   * if the period is not in the timeline.
+   *
+   * @param uid A unique identifier for a period.
+   * @return The index of the period, or {@link C#INDEX_UNSET} if the period was not found.
+   */
+  public abstract int getIndexOfPeriod(Object uid);
+
+  /**
+   * Holds information about a window in a {@link Timeline}. A window defines a region of media
+   * currently available for playback along with additional information such as whether seeking is
+   * supported within the window. See {@link Timeline} for more details. The figure below shows some
+   * of the information defined by a window, as well as how this information relates to
+   * corresponding {@link Period}s in the timeline.
+   * <p align="center">
+   *   <img src="doc-files/timeline-window.svg" alt="Information defined by a timeline window">
+   * </p>
+   */
+  public static final class Window {
+
+    /**
+     * An identifier for the window. Not necessarily unique.
+     */
+    public Object id;
+
+    /**
+     * 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;
+
+    /**
+     * Whether this window may change when the timeline is updated.
+     */
+    public boolean isDynamic;
+
+    /**
+     * 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;
+
+    /**
+     * Sets the data held by this window.
+     */
+    public Window set(Object id, long presentationStartTimeMs, long windowStartTimeMs,
+        boolean isSeekable, boolean isDynamic, long defaultPositionUs, long durationUs,
+        int firstPeriodIndex, int lastPeriodIndex, long positionInFirstPeriodUs) {
+      this.id = id;
+      this.presentationStartTimeMs = presentationStartTimeMs;
+      this.windowStartTimeMs = windowStartTimeMs;
+      this.isSeekable = isSeekable;
+      this.isDynamic = isDynamic;
+      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 a media file. See {@link Timeline} for more details. The figure below
+   * shows some of the information defined by a period, as well as how this information relates to a
+   * corresponding {@link Window} in the timeline.
+   * <p align="center">
+   *   <img src="doc-files/timeline-period.svg" alt="Information defined by a period">
+   * </p>
+   */
+  public static final class Period {
+
+    /**
+     * An identifier for the period. Not necessarily unique.
+     */
+    public Object id;
+
+    /**
+     * A unique identifier for the period.
+     */
+    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;
+
+    /**
+     * Sets the data held by this period.
+     */
+    public Period set(Object id, Object uid, int windowIndex, long durationUs,
+        long positionInWindowUs) {
+      this.id = id;
+      this.uid = uid;
+      this.windowIndex = windowIndex;
+      this.durationUs = durationUs;
+      this.positionInWindowUs = positionInWindowUs;
+      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;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/audio/Ac3Util.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 com.google.android.exoplayer2.audio;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.nio.ByteBuffer;
+
+/**
+ * Utility methods for parsing (E-)AC-3 syncframes, which are access units in (E-)AC-3 bitstreams.
+ */
+public final class Ac3Util {
+
+  /**
+   * The number of new samples per (E-)AC-3 audio block.
+   */
+  private static final int AUDIO_SAMPLES_PER_AUDIO_BLOCK = 256;
+  /**
+   * Each syncframe has 6 blocks that provide 256 new audio samples. See ETSI TS 102 366 4.1.
+   */
+  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 ETSI TS 102 366 table 4.13.)
+   */
+  private static final int[] BITRATE_BY_HALF_FRMSIZECOD = new int[] {32, 40, 48, 56, 64, 80, 96,
+      112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640};
+  /**
+   * 16-bit words per syncframe, indexed by frmsizecod / 2. (See ETSI TS 102 366 table 4.13.)
+   */
+  private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = new int[] {69, 87, 104,
+      121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393};
+
+  /**
+   * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to
+   * ETSI TS 102 366 Annex F. The reading position of {@code data} will be modified.
+   *
+   * @param data The AC3SpecificBox to parse.
+   * @param trackId The track identifier to set on the format, or null.
+   * @param language The language to set on the format.
+   * @param drmInitData {@link DrmInitData} to be included in the format.
+   * @return The AC-3 format parsed from data in the header.
+   */
+  public static Format parseAc3AnnexFFormat(ParsableByteArray data, String trackId,
+      String language, DrmInitData drmInitData) {
+    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, null, Format.NO_VALUE,
+        Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language);
+  }
+
+  /**
+   * Returns the E-AC-3 format given {@code data} containing the EC3SpecificBox according to
+   * ETSI TS 102 366 Annex F. The reading position of {@code data} will be modified.
+   *
+   * @param data The EC3SpecificBox to parse.
+   * @param trackId The track identifier to set on the format, or null.
+   * @param language The language to set on the format.
+   * @param drmInitData {@link DrmInitData} to be included in the format.
+   * @return The E-AC-3 format parsed from data in the header.
+   */
+  public static Format parseEAc3AnnexFFormat(ParsableByteArray data, String trackId,
+      String language, DrmInitData drmInitData) {
+    data.skipBytes(2); // data_rate, num_ind_sub
+
+    // Read only the first substream.
+    // TODO: Read later substreams?
+    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++;
+    }
+    return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, null, Format.NO_VALUE,
+        Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language);
+  }
+
+  /**
+   * Returns the AC-3 format 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.
+   * @param trackId The track identifier to set on the format, or null.
+   * @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 parseAc3SyncframeFormat(ParsableBitArray data, String trackId,
+      String language, DrmInitData drmInitData) {
+    data.skipBits(16 + 16); // syncword, crc1
+    int fscod = data.readBits(2);
+    data.skipBits(6 + 5 + 3); // frmsizecod, bsid, bsmod
+    int 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
+    }
+    boolean lfeon = data.readBit();
+    return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_AC3, null, Format.NO_VALUE,
+        Format.NO_VALUE, CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0),
+        SAMPLE_RATE_BY_FSCOD[fscod], null, drmInitData, 0, language);
+  }
+
+  /**
+   * Returns the E-AC-3 format 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.
+   * @param trackId The track identifier to set on the format, or null.
+   * @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 parseEac3SyncframeFormat(ParsableBitArray data, String trackId,
+      String language, DrmInitData drmInitData) {
+    data.skipBits(16 + 2 + 3 + 11); // syncword, strmtype, substreamid, frmsiz
+    int sampleRate;
+    int fscod = data.readBits(2);
+    if (fscod == 3) {
+      sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)];
+    } else {
+      data.skipBits(2); // numblkscod
+      sampleRate = SAMPLE_RATE_BY_FSCOD[fscod];
+    }
+    int acmod = data.readBits(3);
+    boolean lfeon = data.readBit();
+    return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, null, Format.NO_VALUE,
+        Format.NO_VALUE, CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0), sampleRate, null,
+        drmInitData, 0, language);
+  }
+
+  /**
+   * Returns the size in bytes of the given AC-3 syncframe.
+   *
+   * @param data The syncframe to parse.
+   * @return The syncframe size in bytes. {@link C#LENGTH_UNSET} if the input is invalid.
+   */
+  public static int parseAc3SyncframeSize(byte[] data) {
+    if (data.length < 5) {
+      return C.LENGTH_UNSET;
+    }
+    int fscod = (data[4] & 0xC0) >> 6;
+    int frmsizecod = data[4] & 0x3F;
+    return getAc3SyncframeSize(fscod, frmsizecod);
+  }
+
+  /**
+   * Returns the size in bytes of the given E-AC-3 syncframe.
+   *
+   * @param data The syncframe to parse.
+   * @return The syncframe size in bytes.
+   */
+  public static int parseEAc3SyncframeSize(byte[] data) {
+    return 2 * (((data[2] & 0x07) << 8) + (data[3] & 0xFF) + 1); // frmsiz
+  }
+
+  /**
+   * Returns the number of audio samples in an AC-3 syncframe.
+   */
+  public static int getAc3SyncframeAudioSampleCount() {
+    return AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT;
+  }
+
+  /**
+   * Returns the number of audio samples represented by the given E-AC-3 syncframe.
+   *
+   * @param data The syncframe to parse.
+   * @return The number of audio samples represented by the syncframe.
+   */
+  public static int parseEAc3SyncframeAudioSampleCount(byte[] data) {
+    // See ETSI TS 102 366 subsection E.1.2.2.
+    return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (((data[4] & 0xC0) >> 6) == 0x03 ? 6 // fscod
+        : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(data[4] & 0x30) >> 4]);
+  }
+
+  /**
+   * Like {@link #parseEAc3SyncframeAudioSampleCount(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 parseEAc3SyncframeAudioSampleCount(ByteBuffer buffer) {
+    // See ETSI TS 102 366 subsection E.1.2.2.
+    int fscod = (buffer.get(buffer.position() + 4) & 0xC0) >> 6;
+    return AUDIO_SAMPLES_PER_AUDIO_BLOCK * (fscod == 0x03 ? 6
+        : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]);
+  }
+
+  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() {}
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilities.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.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 java.util.Arrays;
+
+/**
+ * Represents the set of audio formats that a device is capable of playing.
+ */
+@TargetApi(21)
+public final class AudioCapabilities {
+
+  /**
+   * The minimum audio capabilities supported by all devices.
+   */
+  public static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES =
+      new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, 2);
+
+  /**
+   * 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) {
+    return getCapabilities(
+        context.registerReceiver(null, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG)));
+  }
+
+  @SuppressLint("InlinedApi")
+  /* package */ static AudioCapabilities getCapabilities(Intent intent) {
+    if (intent == null || intent.getIntExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 0) == 0) {
+      return DEFAULT_AUDIO_CAPABILITIES;
+    }
+    return new AudioCapabilities(intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS),
+        intent.getIntExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, 0));
+  }
+
+  private final int[] supportedEncodings;
+  private final int maxChannelCount;
+
+  /**
+   * Constructs new audio capabilities based on a set of supported encodings and a maximum channel
+   * count.
+   *
+   * @param supportedEncodings Supported audio encodings from {@link android.media.AudioFormat}'s
+   *     {@code ENCODING_*} constants.
+   * @param maxChannelCount The maximum number of audio channels that can be played simultaneously.
+   */
+  /* package */ AudioCapabilities(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(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) + "]";
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/audio/AudioCapabilitiesReceiver.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 com.google.android.exoplayer2.audio;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import com.google.android.exoplayer2.util.Assertions;
+import 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 BroadcastReceiver receiver;
+
+  /* package */ AudioCapabilities audioCapabilities;
+
+  /**
+   * @param context A context for registering the receiver.
+   * @param listener The listener to notify when audio capabilities change.
+   */
+  public AudioCapabilitiesReceiver(Context context, Listener listener) {
+    this.context = Assertions.checkNotNull(context);
+    this.listener = Assertions.checkNotNull(listener);
+    this.receiver = Util.SDK_INT >= 21 ? new HdmiAudioPlugBroadcastReceiver() : 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() {
+    Intent stickyIntent = receiver == null ? null
+        : context.registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG));
+    audioCapabilities = AudioCapabilities.getCapabilities(stickyIntent);
+    return audioCapabilities;
+  }
+
+  /**
+   * Unregisters the receiver, meaning it will no longer notify the listener when audio capability
+   * changes occur.
+   */
+  public void unregister() {
+    if (receiver != null) {
+      context.unregisterReceiver(receiver);
+    }
+  }
+
+  private final class HdmiAudioPlugBroadcastReceiver extends BroadcastReceiver {
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+      if (!isInitialStickyBroadcast()) {
+        AudioCapabilities newAudioCapabilities = AudioCapabilities.getCapabilities(intent);
+        if (!newAudioCapabilities.equals(audioCapabilities)) {
+          audioCapabilities = newAudioCapabilities;
+          listener.onAudioCapabilitiesChanged(newAudioCapabilities);
+        }
+      }
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/audio/AudioDecoderException.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 com.google.android.exoplayer2.audio;
+
+/**
+ * Thrown when an audio decoder error occurs.
+ */
+public abstract class AudioDecoderException extends Exception {
+
+  /**
+   * @param detailMessage The detail message for this exception.
+   */
+  public AudioDecoderException(String detailMessage) {
+    super(detailMessage);
+  }
+
+  /**
+   * @param detailMessage 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 detailMessage, Throwable cause) {
+    super(detailMessage, cause);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/audio/AudioRendererEventListener.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.audio;
+
+import android.os.Handler;
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.Renderer;
+import com.google.android.exoplayer2.decoder.DecoderCounters;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * Listener of audio {@link Renderer} events.
+ */
+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.
+   */
+  void onAudioEnabled(DecoderCounters counters);
+
+  /**
+   * Called when the audio session is set.
+   *
+   * @param audioSessionId The audio session id.
+   */
+  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.
+   */
+  void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs,
+      long initializationDurationMs);
+
+  /**
+   * Called when the format of the media being consumed by the renderer changes.
+   *
+   * @param format The new format.
+   */
+  void onAudioInputFormatChanged(Format format);
+
+  /**
+   * Called when an {@link AudioTrack} underrun occurs.
+   *
+   * @param bufferSize The size of the {@link AudioTrack}'s buffer, in bytes.
+   * @param bufferSizeMs The size of the {@link AudioTrack}'s buffer, in milliseconds, if it is
+   *     configured for PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output,
+   *     as the buffered media can have a variable bitrate so the duration may be unknown.
+   * @param elapsedSinceLastFeedMs The time since the {@link AudioTrack} was last fed data.
+   */
+  void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
+
+  /**
+   * Called when the renderer is disabled.
+   *
+   * @param counters {@link DecoderCounters} that were updated by the renderer.
+   */
+  void onAudioDisabled(DecoderCounters counters);
+
+  /**
+   * Dispatches events to a {@link AudioRendererEventListener}.
+   */
+  final class EventDispatcher {
+
+    private final Handler handler;
+    private final AudioRendererEventListener listener;
+
+    /**
+     * @param handler A handler for dispatching events, or null if creating a dummy instance.
+     * @param listener The listener to which events should be dispatched, or null if creating a
+     *     dummy instance.
+     */
+    public EventDispatcher(Handler handler, AudioRendererEventListener listener) {
+      this.handler = listener != null ? Assertions.checkNotNull(handler) : null;
+      this.listener = listener;
+    }
+
+    /**
+     * Invokes {@link AudioRendererEventListener#onAudioEnabled(DecoderCounters)}.
+     */
+    public void enabled(final DecoderCounters decoderCounters) {
+      if (listener != null) {
+        handler.post(new Runnable() {
+          @Override
+          public void run() {
+            listener.onAudioEnabled(decoderCounters);
+          }
+        });
+      }
+    }
+
+    /**
+     * Invokes {@link AudioRendererEventListener#onAudioDecoderInitialized(String, long, long)}.
+     */
+    public void decoderInitialized(final String decoderName,
+        final long initializedTimestampMs, final long initializationDurationMs) {
+      if (listener != null) {
+        handler.post(new Runnable() {
+          @Override
+          public void run() {
+            listener.onAudioDecoderInitialized(decoderName, initializedTimestampMs,
+                initializationDurationMs);
+          }
+        });
+      }
+    }
+
+    /**
+     * Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}.
+     */
+    public void inputFormatChanged(final Format format) {
+      if (listener != null) {
+        handler.post(new Runnable() {
+          @Override
+          public void run() {
+            listener.onAudioInputFormatChanged(format);
+          }
+        });
+      }
+    }
+
+    /**
+     * Invokes {@link AudioRendererEventListener#onAudioTrackUnderrun(int, long, long)}.
+     */
+    public void audioTrackUnderrun(final int bufferSize, final long bufferSizeMs,
+        final long elapsedSinceLastFeedMs) {
+      if (listener != null) {
+        handler.post(new Runnable()  {
+          @Override
+          public void run() {
+            listener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+          }
+        });
+      }
+    }
+
+    /**
+     * Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}.
+     */
+    public void disabled(final DecoderCounters counters) {
+      if (listener != null) {
+        handler.post(new Runnable() {
+          @Override
+          public void run() {
+            counters.ensureUpdated();
+            listener.onAudioDisabled(counters);
+          }
+        });
+      }
+    }
+
+    /**
+     * Invokes {@link AudioRendererEventListener#onAudioSessionId(int)}.
+     */
+    public void audioSessionId(final int audioSessionId) {
+      if (listener != null) {
+        handler.post(new Runnable() {
+          @Override
+          public void run() {
+            listener.onAudioSessionId(audioSessionId);
+          }
+        });
+      }
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/audio/AudioTrack.java
@@ -0,0 +1,1553 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.audio;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioTimestamp;
+import android.media.PlaybackParams;
+import android.os.ConditionVariable;
+import android.os.SystemClock;
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles
+ * playback position smoothing, non-blocking writes and reconfiguration.
+ * <p>
+ * Before starting playback, specify the input format by calling
+ * {@link #configure(String, int, int, int, int)}. Optionally call {@link #setAudioSessionId(int)},
+ * {@link #setStreamType(int)}, {@link #enableTunnelingV21(int)} and {@link #disableTunneling()}
+ * to configure audio playback. These methods may be called after writing data to the track, in
+ * which case it will be reinitialized as required.
+ * <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(String, int, int, int, int)} whenever the input format changes. The track
+ * will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, long)}.
+ * <p>
+ * Calling {@link #reset()} releases the underlying {@link android.media.AudioTrack} (and so does
+ * calling {@link #configure(String, int, int, int, int)} unless the format is unchanged). It is
+ * safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} without calling
+ * {@link #configure(String, int, int, int, int)}.
+ * <p>
+ * Call {@link #handleEndOfStream()} to play out all data when no more input buffers will be
+ * provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset}. Call
+ * {@link #release()} when the instance is no longer required.
+ */
+public final class AudioTrack {
+
+  /**
+   * Listener for audio track events.
+   */
+  public interface Listener {
+
+    /**
+     * Called when the audio track has been initialized with a newly generated audio session id.
+     *
+     * @param audioSessionId The newly generated audio session id.
+     */
+    void onAudioSessionId(int audioSessionId);
+
+    /**
+     * Called when the audio track handles a buffer whose timestamp is discontinuous with the last
+     * buffer handled since it was reset.
+     */
+    void onPositionDiscontinuity();
+
+    /**
+     * Called when the audio track underruns.
+     *
+     * @param bufferSize The size of the track's buffer, in bytes.
+     * @param bufferSizeMs The size of the track's buffer, in milliseconds, if it is configured for
+     *     PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, as the
+     *     buffered media can have a variable bitrate so the duration may be unknown.
+     * @param elapsedSinceLastFeedMs The time since the track was last fed data, in milliseconds.
+     */
+    void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
+
+  }
+
+  /**
+   * Thrown when a failure occurs initializing an {@link android.media.AudioTrack}.
+   */
+  public static final class InitializationException extends Exception {
+
+    /**
+     * The state as reported by {@link android.media.AudioTrack#getState()}.
+     */
+    public final int audioTrackState;
+
+    /**
+     * @param audioTrackState The state as reported by {@link android.media.AudioTrack#getState()}.
+     * @param sampleRate The requested sample rate in Hz.
+     * @param channelConfig The requested channel configuration.
+     * @param bufferSize The requested buffer size in bytes.
+     */
+    public InitializationException(int audioTrackState, int sampleRate, int channelConfig,
+        int bufferSize) {
+      super("AudioTrack init failed: " + audioTrackState + ", Config(" + sampleRate + ", "
+          + channelConfig + ", " + bufferSize + ")");
+      this.audioTrackState = audioTrackState;
+    }
+
+  }
+
+  /**
+   * Thrown when a failure occurs writing to an {@link android.media.AudioTrack}.
+   */
+  public static final class WriteException extends Exception {
+
+    /**
+     * The error value returned from {@link android.media.AudioTrack#write(byte[], int, int)} or
+     *     {@link android.media.AudioTrack#write(ByteBuffer, int, int)}.
+     */
+    public final int errorCode;
+
+    /**
+     * @param errorCode The error value returned from
+     *     {@link android.media.AudioTrack#write(byte[], int, int)} or
+     *     {@link android.media.AudioTrack#write(ByteBuffer, int, int)}.
+     */
+    public WriteException(int errorCode) {
+      super("AudioTrack write failed: " + errorCode);
+      this.errorCode = errorCode;
+    }
+
+  }
+
+  /**
+   * Thrown when {@link android.media.AudioTrack#getTimestamp} returns a spurious timestamp, if
+   * {@code AudioTrack#failOnSpuriousAudioTimestamp} is set.
+   */
+  public static final class InvalidAudioTrackTimestampException extends RuntimeException {
+
+    /**
+     * @param detailMessage The detail message for this exception.
+     */
+    public InvalidAudioTrackTimestampException(String detailMessage) {
+      super(detailMessage);
+    }
+
+  }
+
+  /**
+   * Returned by {@link #getCurrentPositionUs} when the position is not set.
+   */
+  public static final long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE;
+
+  /**
+   * A minimum length for the {@link android.media.AudioTrack} buffer, in microseconds.
+   */
+  private static final long MIN_BUFFER_DURATION_US = 250000;
+  /**
+   * A maximum length for the {@link android.media.AudioTrack} buffer, in microseconds.
+   */
+  private static final long MAX_BUFFER_DURATION_US = 750000;
+  /**
+   * The length for passthrough {@link android.media.AudioTrack} buffers, in microseconds.
+   */
+  private static final long PASSTHROUGH_BUFFER_DURATION_US = 250000;
+  /**
+   * A multiplication factor to apply to the minimum buffer size requested by the underlying
+   * {@link android.media.AudioTrack}.
+   */
+  private static final int BUFFER_MULTIPLICATION_FACTOR = 4;
+
+  /**
+   * @see android.media.AudioTrack#PLAYSTATE_STOPPED
+   */
+  private static final int PLAYSTATE_STOPPED = android.media.AudioTrack.PLAYSTATE_STOPPED;
+  /**
+   * @see android.media.AudioTrack#PLAYSTATE_PAUSED
+   */
+  private static final int PLAYSTATE_PAUSED = android.media.AudioTrack.PLAYSTATE_PAUSED;
+  /**
+   * @see android.media.AudioTrack#PLAYSTATE_PLAYING
+   */
+  private static final int PLAYSTATE_PLAYING = android.media.AudioTrack.PLAYSTATE_PLAYING;
+  /**
+   * @see android.media.AudioTrack#ERROR_BAD_VALUE
+   */
+  private static final int ERROR_BAD_VALUE = android.media.AudioTrack.ERROR_BAD_VALUE;
+  /**
+   * @see android.media.AudioTrack#MODE_STATIC
+   */
+  private static final int MODE_STATIC = android.media.AudioTrack.MODE_STATIC;
+  /**
+   * @see android.media.AudioTrack#MODE_STREAM
+   */
+  private static final int MODE_STREAM = android.media.AudioTrack.MODE_STREAM;
+  /**
+   * @see android.media.AudioTrack#STATE_INITIALIZED
+   */
+  private static final int STATE_INITIALIZED = android.media.AudioTrack.STATE_INITIALIZED;
+  /**
+   * @see android.media.AudioTrack#WRITE_NON_BLOCKING
+   */
+  @SuppressLint("InlinedApi")
+  private static final int WRITE_NON_BLOCKING = android.media.AudioTrack.WRITE_NON_BLOCKING;
+
+  private static final String TAG = "AudioTrack";
+
+  /**
+   * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more
+   * than this amount.
+   * <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 int START_NOT_SET = 0;
+  private static final int START_IN_SYNC = 1;
+  private static final int START_NEED_SYNC = 2;
+
+  private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10;
+  private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;
+  private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000;
+
+  /**
+   * 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 android.media.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;
+
+  private final AudioCapabilities audioCapabilities;
+  private final Listener listener;
+  private final ConditionVariable releasingConditionVariable;
+  private final long[] playheadOffsets;
+  private final AudioTrackUtil audioTrackUtil;
+
+  /**
+   * Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}).
+   */
+  private android.media.AudioTrack keepSessionIdAudioTrack;
+
+  private android.media.AudioTrack audioTrack;
+  private int sampleRate;
+  private int channelConfig;
+  @C.StreamType
+  private int streamType;
+  @C.Encoding
+  private int sourceEncoding;
+  @C.Encoding
+  private int targetEncoding;
+  private boolean passthrough;
+  private int pcmFrameSize;
+  private int bufferSize;
+  private long bufferSizeUs;
+
+  private ByteBuffer avSyncHeader;
+  private int bytesUntilNextAvSync;
+
+  private int nextPlayheadOffsetIndex;
+  private int playheadOffsetCount;
+  private long smoothedPlayheadOffsetUs;
+  private long lastPlayheadSampleTimeUs;
+  private boolean audioTimestampSet;
+  private long lastTimestampSampleTimeUs;
+
+  private Method getLatencyMethod;
+  private long submittedPcmBytes;
+  private long submittedEncodedFrames;
+  private int framesPerEncodedSample;
+  private int startMediaTimeState;
+  private long startMediaTimeUs;
+  private long resumeSystemTimeUs;
+  private long latencyUs;
+  private float volume;
+
+  private byte[] temporaryBuffer;
+  private int temporaryBufferOffset;
+  private ByteBuffer currentSourceBuffer;
+
+  private ByteBuffer resampledBuffer;
+  private boolean useResampledBuffer;
+
+  private boolean playing;
+  private int audioSessionId;
+  private boolean tunneling;
+  private boolean hasData;
+  private long lastFeedElapsedRealtimeMs;
+
+  /**
+   * @param audioCapabilities The current audio capabilities.
+   * @param listener Listener for audio track events.
+   */
+  public AudioTrack(AudioCapabilities audioCapabilities, Listener listener) {
+    this.audioCapabilities = audioCapabilities;
+    this.listener = listener;
+    releasingConditionVariable = new ConditionVariable(true);
+    if (Util.SDK_INT >= 18) {
+      try {
+        getLatencyMethod =
+            android.media.AudioTrack.class.getMethod("getLatency", (Class<?>[]) null);
+      } catch (NoSuchMethodException e) {
+        // There's no guarantee this method exists. Do nothing.
+      }
+    }
+    if (Util.SDK_INT >= 23) {
+      audioTrackUtil = new AudioTrackUtilV23();
+    } else if (Util.SDK_INT >= 19) {
+      audioTrackUtil = new AudioTrackUtilV19();
+    } else {
+      audioTrackUtil = new AudioTrackUtil();
+    }
+    playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];
+    volume = 1.0f;
+    startMediaTimeState = START_NOT_SET;
+    streamType = C.STREAM_TYPE_DEFAULT;
+    audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+  }
+
+  /**
+   * Returns whether it's possible to play audio in the specified format using encoded passthrough.
+   *
+   * @param mimeType The format mime type.
+   * @return Whether it's possible to play audio in the format using encoded passthrough.
+   */
+  public boolean isPassthroughSupported(String mimeType) {
+    return audioCapabilities != null
+        && audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType));
+  }
+
+  /**
+   * Returns the playback position in the stream starting at zero, in microseconds, or
+   * {@link #CURRENT_POSITION_NOT_SET} if it is not yet available.
+   *
+   * <p>If the device supports it, the method uses the playback timestamp from
+   * {@link android.media.AudioTrack#getTimestamp}. Otherwise, it derives a smoothed position by
+   * sampling the {@link android.media.AudioTrack}'s frame position.
+   *
+   * @param sourceEnded Specify {@code true} if no more input buffers will be provided.
+   * @return The playback position relative to the start of playback, in microseconds.
+   */
+  public long getCurrentPositionUs(boolean sourceEnded) {
+    if (!hasCurrentPositionUs()) {
+      return CURRENT_POSITION_NOT_SET;
+    }
+
+    if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) {
+      maybeSampleSyncParams();
+    }
+
+    long systemClockUs = System.nanoTime() / 1000;
+    long currentPositionUs;
+    if (audioTimestampSet) {
+      // How long ago in the past the audio timestamp is (negative if it's in the future).
+      long presentationDiff = systemClockUs - (audioTrackUtil.getTimestampNanoTime() / 1000);
+      // Fixes such difference if the playback speed is not real time speed.
+      long actualSpeedPresentationDiff = (long) (presentationDiff
+          * audioTrackUtil.getPlaybackSpeed());
+      long framesDiff = durationUsToFrames(actualSpeedPresentationDiff);
+      // The position of the frame that's currently being presented.
+      long currentFramePosition = audioTrackUtil.getTimestampFramePosition() + framesDiff;
+      currentPositionUs = framesToDurationUs(currentFramePosition) + startMediaTimeUs;
+    } else {
+      if (playheadOffsetCount == 0) {
+        // The AudioTrack has started, but we don't have any samples to compute a smoothed position.
+        currentPositionUs = audioTrackUtil.getPlaybackHeadPositionUs() + startMediaTimeUs;
+      } else {
+        // getPlayheadPositionUs() only has a granularity of ~20 ms, so we base the position off the
+        // system clock (and a smoothed offset between it and the playhead position) so as to
+        // prevent jitter in the reported positions.
+        currentPositionUs = systemClockUs + smoothedPlayheadOffsetUs + startMediaTimeUs;
+      }
+      if (!sourceEnded) {
+        currentPositionUs -= latencyUs;
+      }
+    }
+
+    return currentPositionUs;
+  }
+
+  /**
+   * Configures (or reconfigures) the audio track.
+   *
+   * @param mimeType The mime type.
+   * @param channelCount The number of channels.
+   * @param sampleRate The sample rate in Hz.
+   * @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT},
+   *     {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and
+   *     {@link C#ENCODING_PCM_32BIT}.
+   * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a
+   *     suitable buffer size automatically.
+   */
+  public void configure(String mimeType, int channelCount, int sampleRate,
+      @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) {
+    int channelConfig;
+    switch (channelCount) {
+      case 1:
+        channelConfig = AudioFormat.CHANNEL_OUT_MONO;
+        break;
+      case 2:
+        channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
+        break;
+      case 3:
+        channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
+        break;
+      case 4:
+        channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
+        break;
+      case 5:
+        channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
+        break;
+      case 6:
+        channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
+        break;
+      case 7:
+        channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
+        break;
+      case 8:
+        channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND;
+        break;
+      default:
+        throw new IllegalArgumentException("Unsupported channel count: " + channelCount);
+    }
+
+    // Workaround for overly strict channel configuration checks on nVidia Shield.
+    if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) {
+      switch (channelCount) {
+        case 7:
+          channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND;
+          break;
+        case 3:
+        case 5:
+          channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
+          break;
+        default:
+          break;
+      }
+    }
+
+    boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType);
+
+    // Workaround for Nexus Player not reporting support for mono passthrough.
+    // (See [Internal: b/34268671].)
+    if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) {
+      channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
+    }
+
+    @C.Encoding int sourceEncoding;
+    if (passthrough) {
+      sourceEncoding = getEncodingForMimeType(mimeType);
+    } else if (pcmEncoding == C.ENCODING_PCM_8BIT || pcmEncoding == C.ENCODING_PCM_16BIT
+        || pcmEncoding == C.ENCODING_PCM_24BIT || pcmEncoding == C.ENCODING_PCM_32BIT) {
+      sourceEncoding = pcmEncoding;
+    } else {
+      throw new IllegalArgumentException("Unsupported PCM encoding: " + pcmEncoding);
+    }
+
+    if (isInitialized() && this.sourceEncoding == sourceEncoding && this.sampleRate == sampleRate
+        && this.channelConfig == channelConfig) {
+      // We already have an audio track with the correct sample rate, channel config and encoding.
+      return;
+    }
+
+    reset();
+
+    this.sourceEncoding = sourceEncoding;
+    this.passthrough = passthrough;
+    this.sampleRate = sampleRate;
+    this.channelConfig = channelConfig;
+    targetEncoding = passthrough ? sourceEncoding : C.ENCODING_PCM_16BIT;
+    pcmFrameSize = 2 * channelCount; // 2 bytes per 16-bit sample * number of channels.
+
+    if (specifiedBufferSize != 0) {
+      bufferSize = specifiedBufferSize;
+    } else if (passthrough) {
+      // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into
+      // account. [Internal: b/25181305]
+      if (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3) {
+        // AC-3 allows bitrates up to 640 kbit/s.
+        bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND);
+      } else /* (targetEncoding == C.ENCODING_DTS || targetEncoding == C.ENCODING_DTS_HD */ {
+        // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s.
+        bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND);
+      }
+    } else {
+      int minBufferSize =
+          android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, targetEncoding);
+      Assertions.checkState(minBufferSize != ERROR_BAD_VALUE);
+      int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR;
+      int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * pcmFrameSize;
+      int maxAppBufferSize = (int) Math.max(minBufferSize,
+          durationUsToFrames(MAX_BUFFER_DURATION_US) * pcmFrameSize);
+      bufferSize = multipliedBufferSize < minAppBufferSize ? minAppBufferSize
+          : multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize
+          : multipliedBufferSize;
+    }
+    bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(pcmBytesToFrames(bufferSize));
+  }
+
+  private void initialize() throws InitializationException {
+    // If we're asynchronously releasing a previous audio track then we block until it has been
+    // released. This guarantees that we cannot end up in a state where we have multiple audio
+    // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust
+    // the shared memory that's available for audio track buffers. This would in turn cause the
+    // initialization of the audio track to fail.
+    releasingConditionVariable.block();
+
+    if (tunneling) {
+      audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding,
+          bufferSize, audioSessionId);
+    } else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
+      audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
+          targetEncoding, bufferSize, MODE_STREAM);
+    } else {
+      // Re-attach to the same audio session.
+      audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
+          targetEncoding, bufferSize, MODE_STREAM, audioSessionId);
+    }
+    checkAudioTrackInitialized();
+
+    int audioSessionId = audioTrack.getAudioSessionId();
+    if (enablePreV21AudioSessionWorkaround) {
+      if (Util.SDK_INT < 21) {
+        // The workaround creates an audio track with a two byte buffer on the same session, and
+        // does not release it until this object is released, which keeps the session active.
+        if (keepSessionIdAudioTrack != null
+            && audioSessionId != keepSessionIdAudioTrack.getAudioSessionId()) {
+          releaseKeepSessionIdAudioTrack();
+        }
+        if (keepSessionIdAudioTrack == null) {
+          int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE.
+          int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
+          @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT;
+          int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback.
+          keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate,
+              channelConfig, encoding, bufferSize, MODE_STATIC, audioSessionId);
+        }
+      }
+    }
+    if (this.audioSessionId != audioSessionId) {
+      this.audioSessionId = audioSessionId;
+      listener.onAudioSessionId(audioSessionId);
+    }
+
+    audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds());
+    setVolumeInternal();
+    hasData = false;
+  }
+
+  /**
+   * Starts or resumes playing audio if the audio track has been initialized.
+   */
+  public void play() {
+    playing = true;
+    if (isInitialized()) {
+      resumeSystemTimeUs = System.nanoTime() / 1000;
+      audioTrack.play();
+    }
+  }
+
+  /**
+   * Signals to the audio track that the next buffer is discontinuous with the previous buffer.
+   */
+  public void handleDiscontinuity() {
+    // Force resynchronization after a skipped buffer.
+    if (startMediaTimeState == START_IN_SYNC) {
+      startMediaTimeState = START_NEED_SYNC;
+    }
+  }
+
+  /**
+   * Attempts to write data from a {@link ByteBuffer} to the audio track, 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 successfully written.
+   * {@link Listener#onPositionDiscontinuity()} will be called if {@code presentationTimeUs} is
+   * discontinuous with the last buffer handled since the track was reset.
+   * <p>
+   * Returns whether the data was written in full. If the data was not written in full then the same
+   * {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed,
+   * except in the case of an interleaving call to {@link #reset()} (or an interleaving call to
+   * {@link #configure(String, int, int, int, int)} that caused the track to be reset).
+   *
+   * @param buffer The buffer containing audio data to play back.
+   * @param presentationTimeUs Presentation timestamp of the next buffer in microseconds.
+   * @return Whether the buffer was consumed fully.
+   * @throws InitializationException If an error occurs initializing the track.
+   * @throws WriteException If an error occurs writing the audio data.
+   */
+  public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs)
+      throws InitializationException, WriteException {
+    if (!isInitialized()) {
+      initialize();
+      if (playing) {
+        play();
+      }
+    }
+
+    boolean hadData = hasData;
+    hasData = hasPendingData();
+    if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) {
+      long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
+      listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs);
+    }
+    boolean result = writeBuffer(buffer, presentationTimeUs);
+    lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
+    return result;
+  }
+
+  @SuppressWarnings("ReferenceEquality")
+  private boolean writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
+    boolean isNewSourceBuffer = currentSourceBuffer == null;
+    Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer);
+    currentSourceBuffer = buffer;
+
+    if (needsPassthroughWorkarounds()) {
+      // An AC-3 audio track continues to play data written while it is paused. Stop writing so its
+      // buffer empties. See [Internal: b/18899620].
+      if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) {
+        return false;
+      }
+
+      // A new AC-3 audio track's playback position continues to increase from the old track's
+      // position for a short time after is has been released. Avoid writing data until the playback
+      // head position actually returns to zero.
+      if (audioTrack.getPlayState() == PLAYSTATE_STOPPED
+          && audioTrackUtil.getPlaybackHeadPosition() != 0) {
+        return false;
+      }
+    }
+
+    if (isNewSourceBuffer) {
+      // We're seeing this buffer for the first time.
+
+      if (!currentSourceBuffer.hasRemaining()) {
+        // The buffer is empty.
+        currentSourceBuffer = null;
+        return true;
+      }
+
+      useResampledBuffer = targetEncoding != sourceEncoding;
+      if (useResampledBuffer) {
+        Assertions.checkState(targetEncoding == C.ENCODING_PCM_16BIT);
+        // Resample the buffer to get the data in the target encoding.
+        resampledBuffer = resampleTo16BitPcm(currentSourceBuffer, sourceEncoding, resampledBuffer);
+        buffer = resampledBuffer;
+      }
+
+      if (passthrough && framesPerEncodedSample == 0) {
+        // If this is the first encoded sample, calculate the sample size in frames.
+        framesPerEncodedSample = getFramesPerEncodedSample(targetEncoding, buffer);
+      }
+      if (startMediaTimeState == START_NOT_SET) {
+        startMediaTimeUs = Math.max(0, presentationTimeUs);
+        startMediaTimeState = START_IN_SYNC;
+      } else {
+        // Sanity check that presentationTimeUs is consistent with the expected value.
+        long expectedPresentationTimeUs = startMediaTimeUs
+            + framesToDurationUs(getSubmittedFrames());
+        if (startMediaTimeState == START_IN_SYNC
+            && Math.abs(expectedPresentationTimeUs - presentationTimeUs) > 200000) {
+          Log.e(TAG, "Discontinuity detected [expected " + expectedPresentationTimeUs + ", got "
+              + presentationTimeUs + "]");
+          startMediaTimeState = START_NEED_SYNC;
+        }
+        if (startMediaTimeState == START_NEED_SYNC) {
+          // Adjust startMediaTimeUs to be consistent with the current buffer's start time and the
+          // number of bytes submitted.
+          startMediaTimeUs += (presentationTimeUs - expectedPresentationTimeUs);
+          startMediaTimeState = START_IN_SYNC;
+          listener.onPositionDiscontinuity();
+        }
+      }
+      if (Util.SDK_INT < 21) {
+        // Copy {@code buffer} into {@code temporaryBuffer}.
+        int bytesRemaining = buffer.remaining();
+        if (temporaryBuffer == null || temporaryBuffer.length < bytesRemaining) {
+          temporaryBuffer = new byte[bytesRemaining];
+        }
+        int originalPosition = buffer.position();
+        buffer.get(temporaryBuffer, 0, bytesRemaining);
+        buffer.position(originalPosition);
+        temporaryBufferOffset = 0;
+      }
+    }
+
+    buffer = useResampledBuffer ? resampledBuffer : buffer;
+    int bytesRemaining = buffer.remaining();
+    int bytesWritten = 0;
+    if (Util.SDK_INT < 21) { // passthrough == false
+      // Work out how many bytes we can write without the risk of blocking.
+      int bytesPending =
+          (int) (submittedPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * pcmFrameSize));
+      int bytesToWrite = bufferSize - bytesPending;
+      if (bytesToWrite > 0) {
+        bytesToWrite = Math.min(bytesRemaining, bytesToWrite);
+        bytesWritten = audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite);
+        if (bytesWritten >= 0) {
+          temporaryBufferOffset += bytesWritten;
+        }
+        buffer.position(buffer.position() + bytesWritten);
+      }
+    } else {
+      bytesWritten = tunneling
+          ? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs)
+          : writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
+    }
+
+    if (bytesWritten < 0) {
+      throw new WriteException(bytesWritten);
+    }
+
+    if (!passthrough) {
+      submittedPcmBytes += bytesWritten;
+    }
+    if (bytesWritten == bytesRemaining) {
+      if (passthrough) {
+        submittedEncodedFrames += framesPerEncodedSample;
+      }
+      currentSourceBuffer = null;
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Ensures that the last data passed to {@link #handleBuffer(ByteBuffer, long)} is played in full.
+   */
+  public void handleEndOfStream() {
+    if (isInitialized()) {
+      audioTrackUtil.handleEndOfStream(getSubmittedFrames());
+      bytesUntilNextAvSync = 0;
+    }
+  }
+
+  /**
+   * Returns whether the audio track has more data pending that will be played back.
+   */
+  public boolean hasPendingData() {
+    return isInitialized()
+        && (getSubmittedFrames() > audioTrackUtil.getPlaybackHeadPosition()
+        || overrideHasPendingData());
+  }
+
+  /**
+   * Sets the playback parameters. Only available for {@link Util#SDK_INT} &gt;= 23
+   *
+   * @param playbackParams The playback parameters to be used by the
+   *     {@link android.media.AudioTrack}.
+   * @throws UnsupportedOperationException if the Playback Parameters are not supported. That is,
+   *     {@link Util#SDK_INT} &lt; 23.
+   */
+  public void setPlaybackParams(PlaybackParams playbackParams) {
+    audioTrackUtil.setPlaybackParams(playbackParams);
+  }
+
+  /**
+   * Sets the stream type for audio track. If the stream type has changed and if the audio track
+   * is not configured for use with tunneling, then the audio track is reset and the audio session
+   * id is cleared.
+   * <p>
+   * If the audio track is configured for use with tunneling then the stream type is ignored, the
+   * audio track is not reset and the audio session id is not cleared. The passed stream type will
+   * be used if the audio track is later re-configured into non-tunneled mode.
+   *
+   * @param streamType The {@link C.StreamType} to use for audio output.
+   */
+  public void setStreamType(@C.StreamType int streamType) {
+    if (this.streamType == streamType) {
+      return;
+    }
+    this.streamType = streamType;
+    if (tunneling) {
+      // The stream type is ignored in tunneling mode, so no need to reset.
+      return;
+    }
+    reset();
+    audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+  }
+
+  /**
+   * Sets the audio session id. The audio track is reset if the audio session id has changed.
+   */
+  public void setAudioSessionId(int audioSessionId) {
+    if (this.audioSessionId != audioSessionId) {
+      this.audioSessionId = audioSessionId;
+      reset();
+    }
+  }
+
+  /**
+   * Enables tunneling. The audio track is reset if tunneling was previously disabled or if the
+   * audio session id has changed. Enabling tunneling requires platform API version 21 onwards.
+   *
+   * @param tunnelingAudioSessionId The audio session id to use.
+   * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21.
+   */
+  public void enableTunnelingV21(int tunnelingAudioSessionId) {
+    Assertions.checkState(Util.SDK_INT >= 21);
+    if (!tunneling || audioSessionId != tunnelingAudioSessionId) {
+      tunneling = true;
+      audioSessionId = tunnelingAudioSessionId;
+      reset();
+    }
+  }
+
+  /**
+   * Disables tunneling. If tunneling was previously enabled then the audio track is reset and the
+   * audio session id is cleared.
+   */
+  public void disableTunneling() {
+    if (tunneling) {
+      tunneling = false;
+      audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+      reset();
+    }
+  }
+
+  /**
+   * Sets the playback volume.
+   *
+   * @param volume A volume in the range [0.0, 1.0].
+   */
+  public void setVolume(float volume) {
+    if (this.volume != volume) {
+      this.volume = volume;
+      setVolumeInternal();
+    }
+  }
+
+  private void setVolumeInternal() {
+    if (!isInitialized()) {
+      // Do nothing.
+    } else if (Util.SDK_INT >= 21) {
+      setVolumeInternalV21(audioTrack, volume);
+    } else {
+      setVolumeInternalV3(audioTrack, volume);
+    }
+  }
+
+  /**
+   * Pauses playback.
+   */
+  public void pause() {
+    playing = false;
+    if (isInitialized()) {
+      resetSyncParams();
+      audioTrackUtil.pause();
+    }
+  }
+
+  /**
+   * Releases the underlying audio track asynchronously.
+   * <p>
+   * Calling {@link #handleBuffer(ByteBuffer, long)} will block until the audio track has been
+   * released, so it is safe to use the audio track immediately after a reset. The audio session may
+   * remain active until {@link #release()} is called.
+   */
+  public void reset() {
+    if (isInitialized()) {
+      submittedPcmBytes = 0;
+      submittedEncodedFrames = 0;
+      framesPerEncodedSample = 0;
+      currentSourceBuffer = null;
+      avSyncHeader = null;
+      bytesUntilNextAvSync = 0;
+      startMediaTimeState = START_NOT_SET;
+      latencyUs = 0;
+      resetSyncParams();
+      int playState = audioTrack.getPlayState();
+      if (playState == PLAYSTATE_PLAYING) {
+        audioTrack.pause();
+      }
+      // AudioTrack.release can take some time, so we call it on a background thread.
+      final android.media.AudioTrack toRelease = audioTrack;
+      audioTrack = null;
+      audioTrackUtil.reconfigure(null, false);
+      releasingConditionVariable.close();
+      new Thread() {
+        @Override
+        public void run() {
+          try {
+            toRelease.flush();
+            toRelease.release();
+          } finally {
+            releasingConditionVariable.open();
+          }
+        }
+      }.start();
+    }
+  }
+
+  /**
+   * Releases all resources associated with this instance.
+   */
+  public void release() {
+    reset();
+    releaseKeepSessionIdAudioTrack();
+    audioSessionId = C.AUDIO_SESSION_ID_UNSET;
+    playing = false;
+  }
+
+  /**
+   * Releases {@link #keepSessionIdAudioTrack} asynchronously, if it is non-{@code null}.
+   */
+  private void releaseKeepSessionIdAudioTrack() {
+    if (keepSessionIdAudioTrack == null) {
+      return;
+    }
+
+    // AudioTrack.release can take some time, so we call it on a background thread.
+    final android.media.AudioTrack toRelease = keepSessionIdAudioTrack;
+    keepSessionIdAudioTrack = null;
+    new Thread() {
+      @Override
+      public void run() {
+        toRelease.release();
+      }
+    }.start();
+  }
+
+  /**
+   * Returns whether {@link #getCurrentPositionUs} can return the current playback position.
+   */
+  private boolean hasCurrentPositionUs() {
+    return isInitialized() && startMediaTimeState != START_NOT_SET;
+  }
+
+  /**
+   * Updates the audio track latency and playback position parameters.
+   */
+  private void maybeSampleSyncParams() {
+    long playbackPositionUs = audioTrackUtil.getPlaybackHeadPositionUs();
+    if (playbackPositionUs == 0) {
+      // The AudioTrack hasn't output anything yet.
+      return;
+    }
+    long systemClockUs = System.nanoTime() / 1000;
+    if (systemClockUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) {
+      // Take a new sample and update the smoothed offset between the system clock and the playhead.
+      playheadOffsets[nextPlayheadOffsetIndex] = playbackPositionUs - systemClockUs;
+      nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT;
+      if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) {
+        playheadOffsetCount++;
+      }
+      lastPlayheadSampleTimeUs = systemClockUs;
+      smoothedPlayheadOffsetUs = 0;
+      for (int i = 0; i < playheadOffsetCount; i++) {
+        smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount;
+      }
+    }
+
+    if (needsPassthroughWorkarounds()) {
+      // Don't sample the timestamp and latency if this is an AC-3 passthrough AudioTrack on
+      // platform API versions 21/22, as incorrect values are returned. See [Internal: b/21145353].
+      return;
+    }
+
+    if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) {
+      audioTimestampSet = audioTrackUtil.updateTimestamp();
+      if (audioTimestampSet) {
+        // Perform sanity checks on the timestamp.
+        long audioTimestampUs = audioTrackUtil.getTimestampNanoTime() / 1000;
+        long audioTimestampFramePosition = audioTrackUtil.getTimestampFramePosition();
+        if (audioTimestampUs < resumeSystemTimeUs) {
+          // The timestamp corresponds to a time before the track was most recently resumed.
+          audioTimestampSet = false;
+        } else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) {
+          // The timestamp time base is probably wrong.
+          String message = "Spurious audio timestamp (system clock mismatch): "
+              + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", "
+              + playbackPositionUs;
+          if (failOnSpuriousAudioTimestamp) {
+            throw new InvalidAudioTrackTimestampException(message);
+          }
+          Log.w(TAG, message);
+          audioTimestampSet = false;
+        } else if (Math.abs(framesToDurationUs(audioTimestampFramePosition) - playbackPositionUs)
+            > MAX_AUDIO_TIMESTAMP_OFFSET_US) {
+          // The timestamp frame position is probably wrong.
+          String message = "Spurious audio timestamp (frame position mismatch): "
+              + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", "
+              + playbackPositionUs;
+          if (failOnSpuriousAudioTimestamp) {
+            throw new InvalidAudioTrackTimestampException(message);
+          }
+          Log.w(TAG, message);
+          audioTimestampSet = false;
+        }
+      }
+      if (getLatencyMethod != null && !passthrough) {
+        try {
+          // Compute the audio track latency, excluding the latency due to the buffer (leaving
+          // latency due to the mixer and audio hardware driver).
+          latencyUs = (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L
+              - bufferSizeUs;
+          // Sanity check that the latency is non-negative.
+          latencyUs = Math.max(latencyUs, 0);
+          // Sanity check that the latency isn't too large.
+          if (latencyUs > MAX_LATENCY_US) {
+            Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs);
+            latencyUs = 0;
+          }
+        } catch (Exception e) {
+          // The method existed, but doesn't work. Don't try again.
+          getLatencyMethod = null;
+        }
+      }
+      lastTimestampSampleTimeUs = systemClockUs;
+    }
+  }
+
+  /**
+   * Checks that {@link #audioTrack} has been successfully initialized. If it has then calling this
+   * method is a no-op. If it hasn't then {@link #audioTrack} is released and set to null, and an
+   * exception is thrown.
+   *
+   * @throws InitializationException If {@link #audioTrack} has not been successfully initialized.
+   */
+  private void checkAudioTrackInitialized() throws InitializationException {
+    int state = audioTrack.getState();
+    if (state == STATE_INITIALIZED) {
+      return;
+    }
+    // The track is not successfully initialized. Release and null the track.
+    try {
+      audioTrack.release();
+    } catch (Exception e) {
+      // The track has already failed to initialize, so it wouldn't be that surprising if release
+      // were to fail too. Swallow the exception.
+    } finally {
+      audioTrack = null;
+    }
+
+    throw new InitializationException(state, sampleRate, channelConfig, bufferSize);
+  }
+
+  private boolean isInitialized() {
+    return audioTrack != null;
+  }
+
+  private long pcmBytesToFrames(long byteCount) {
+    return byteCount / pcmFrameSize;
+  }
+
+  private long framesToDurationUs(long frameCount) {
+    return (frameCount * C.MICROS_PER_SECOND) / sampleRate;
+  }
+
+  private long durationUsToFrames(long durationUs) {
+    return (durationUs * sampleRate) / C.MICROS_PER_SECOND;
+  }
+
+  private long getSubmittedFrames() {
+    return passthrough ? submittedEncodedFrames : pcmBytesToFrames(submittedPcmBytes);
+  }
+
+  private void resetSyncParams() {
+    smoothedPlayheadOffsetUs = 0;
+    playheadOffsetCount = 0;
+    nextPlayheadOffsetIndex = 0;
+    lastPlayheadSampleTimeUs = 0;
+    audioTimestampSet = false;
+    lastTimestampSampleTimeUs = 0;
+  }
+
+  /**
+   * Returns whether to work around problems with passthrough audio tracks.
+   * See [Internal: b/18899620, b/19187573, b/21145353].
+   */
+  private boolean needsPassthroughWorkarounds() {
+    return Util.SDK_INT < 23
+        && (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3);
+  }
+
+  /**
+   * Returns whether the audio track should behave as though it has pending data. This is to work
+   * around an issue on platform API versions 21/22 where AC-3 audio tracks can't be paused, so we
+   * empty their buffers when paused. In this case, they should still behave as if they have
+   * pending data, otherwise writing will never resume.
+   */
+  private boolean overrideHasPendingData() {
+    return needsPassthroughWorkarounds()
+        && audioTrack.getPlayState() == PLAYSTATE_PAUSED
+        && audioTrack.getPlaybackHeadPosition() == 0;
+  }
+
+  /**
+   * Instantiates an {@link android.media.AudioTrack} to be used with tunneling video playback.
+   */
+  @TargetApi(21)
+  private static android.media.AudioTrack createHwAvSyncAudioTrackV21(int sampleRate,
+      int channelConfig, int encoding, int bufferSize, int sessionId) {
+    AudioAttributes attributesBuilder = new AudioAttributes.Builder()
+        .setUsage(AudioAttributes.USAGE_MEDIA)
+        .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
+        .setFlags(AudioAttributes.FLAG_HW_AV_SYNC)
+        .build();
+    AudioFormat format = new AudioFormat.Builder()
+        .setChannelMask(channelConfig)
+        .setEncoding(encoding)
+        .setSampleRate(sampleRate)
+        .build();
+    return new android.media.AudioTrack(attributesBuilder, format, bufferSize, MODE_STREAM,
+        sessionId);
+  }
+
+  /**
+   * Converts the provided buffer into 16-bit PCM.
+   *
+   * @param buffer The buffer containing the data to convert.
+   * @param sourceEncoding The data encoding.
+   * @param out A buffer into which the output should be written, if its capacity is sufficient.
+   * @return The 16-bit PCM output. Different to the out parameter if null was passed, or if the
+   *     capacity was insufficient for the output.
+   */
+  private static ByteBuffer resampleTo16BitPcm(ByteBuffer buffer, @C.PcmEncoding int sourceEncoding,
+      ByteBuffer out) {
+    int offset = buffer.position();
+    int limit = buffer.limit();
+    int size = limit - offset;
+
+    int resampledSize;
+    switch (sourceEncoding) {
+      case C.ENCODING_PCM_8BIT:
+        resampledSize = size * 2;
+        break;
+      case C.ENCODING_PCM_24BIT:
+        resampledSize = (size / 3) * 2;
+        break;
+      case C.ENCODING_PCM_32BIT:
+        resampledSize = size / 2;
+        break;
+      case C.ENCODING_PCM_16BIT:
+      case C.ENCODING_INVALID:
+      case Format.NO_VALUE:
+      default:
+        // Never happens.
+        throw new IllegalStateException();
+    }
+
+    ByteBuffer resampledBuffer = out;
+    if (resampledBuffer == null || resampledBuffer.capacity() < resampledSize) {
+      resampledBuffer = ByteBuffer.allocateDirect(resampledSize);
+    }
+    resampledBuffer.position(0);
+    resampledBuffer.limit(resampledSize);
+
+    // Samples are little endian.
+    switch (sourceEncoding) {
+      case C.ENCODING_PCM_8BIT:
+        // 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up.
+        for (int i = offset; i < limit; i++) {
+          resampledBuffer.put((byte) 0);
+          resampledBuffer.put((byte) ((buffer.get(i) & 0xFF) - 128));
+        }
+        break;
+      case C.ENCODING_PCM_24BIT:
+        // 24->16 bit resampling. Drop the least significant byte.
+        for (int i = offset; i < limit; i += 3) {
+          resampledBuffer.put(buffer.get(i + 1));
+          resampledBuffer.put(buffer.get(i + 2));
+        }
+        break;
+      case C.ENCODING_PCM_32BIT:
+        // 32->16 bit resampling. Drop the two least significant bytes.
+        for (int i = offset; i < limit; i += 4) {
+          resampledBuffer.put(buffer.get(i + 2));
+          resampledBuffer.put(buffer.get(i + 3));
+        }
+        break;
+      case C.ENCODING_PCM_16BIT:
+      case C.ENCODING_INVALID:
+      case Format.NO_VALUE:
+      default:
+        // Never happens.
+        throw new IllegalStateException();
+    }
+
+    resampledBuffer.position(0);
+    return resampledBuffer;
+  }
+
+  @C.Encoding
+  private static int getEncodingForMimeType(String mimeType) {
+    switch (mimeType) {
+      case MimeTypes.AUDIO_AC3:
+        return C.ENCODING_AC3;
+      case MimeTypes.AUDIO_E_AC3:
+        return C.ENCODING_E_AC3;
+      case MimeTypes.AUDIO_DTS:
+        return C.ENCODING_DTS;
+      case MimeTypes.AUDIO_DTS_HD:
+        return C.ENCODING_DTS_HD;
+      default:
+        return C.ENCODING_INVALID;
+    }
+  }
+
+  private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) {
+    if (encoding == C.ENCODING_DTS || encoding == C.ENCODING_DTS_HD) {
+      return DtsUtil.parseDtsAudioSampleCount(buffer);
+    } else if (encoding == C.ENCODING_AC3) {
+      return Ac3Util.getAc3SyncframeAudioSampleCount();
+    } else if (encoding == C.ENCODING_E_AC3) {
+      return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer);
+    } else {
+      throw new IllegalStateException("Unexpected audio encoding: " + encoding);
+    }
+  }
+
+  @TargetApi(21)
+  private static int writeNonBlockingV21(android.media.AudioTrack audioTrack, ByteBuffer buffer,
+      int size) {
+    return audioTrack.write(buffer, size, WRITE_NON_BLOCKING);
+  }
+
+  @TargetApi(21)
+  private int writeNonBlockingWithAvSyncV21(android.media.AudioTrack audioTrack,
+      ByteBuffer buffer, int size, long presentationTimeUs) {
+    // TODO: Uncomment this when [Internal ref b/33627517] is clarified or fixed.
+    // if (Util.SDK_INT >= 23) {
+    //   // The underlying platform AudioTrack writes AV sync headers directly.
+    //   return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);
+    // }
+    if (avSyncHeader == null) {
+      avSyncHeader = ByteBuffer.allocate(16);
+      avSyncHeader.order(ByteOrder.BIG_ENDIAN);
+      avSyncHeader.putInt(0x55550001);
+    }
+    if (bytesUntilNextAvSync == 0) {
+      avSyncHeader.putInt(4, size);
+      avSyncHeader.putLong(8, presentationTimeUs * 1000);
+      avSyncHeader.position(0);
+      bytesUntilNextAvSync = size;
+    }
+    int avSyncHeaderBytesRemaining = avSyncHeader.remaining();
+    if (avSyncHeaderBytesRemaining > 0) {
+      int result = audioTrack.write(avSyncHeader, avSyncHeaderBytesRemaining, WRITE_NON_BLOCKING);
+      if (result < 0) {
+        bytesUntilNextAvSync = 0;
+        return result;
+      }
+      if (result < avSyncHeaderBytesRemaining) {
+        return 0;
+      }
+    }
+    int result = writeNonBlockingV21(audioTrack, buffer, size);
+    if (result < 0) {
+      bytesUntilNextAvSync = 0;
+      return result;
+    }
+    bytesUntilNextAvSync -= result;
+    return result;
+  }
+
+  @TargetApi(21)
+  private static void setVolumeInternalV21(android.media.AudioTrack audioTrack, float volume) {
+    audioTrack.setVolume(volume);
+  }
+
+  @SuppressWarnings("deprecation")
+  private static void setVolumeInternalV3(android.media.AudioTrack audioTrack, float volume) {
+    audioTrack.setStereoVolume(volume, volume);
+  }
+
+  /**
+   * Wraps an {@link android.media.AudioTrack} to expose useful utility methods.
+   */
+  private static class AudioTrackUtil {
+
+    protected android.media.AudioTrack audioTrack;
+    private boolean needsPassthroughWorkaround;
+    private int sampleRate;
+    private long lastRawPlaybackHeadPosition;
+    private long rawPlaybackHeadWrapCount;
+    private long passthroughWorkaroundPauseOffset;
+
+    private long stopTimestampUs;
+    private long stopPlaybackHeadPosition;
+    private long endPlaybackHeadPosition;
+
+    /**
+     * Reconfigures the audio track utility helper to use the specified {@code audioTrack}.
+     *
+     * @param audioTrack The audio track to wrap.
+     * @param needsPassthroughWorkaround Whether to workaround issues with pausing AC-3 passthrough
+     *     audio tracks on platform API version 21/22.
+     */
+    public void reconfigure(android.media.AudioTrack audioTrack,
+        boolean needsPassthroughWorkaround) {
+      this.audioTrack = audioTrack;
+      this.needsPassthroughWorkaround = needsPassthroughWorkaround;
+      stopTimestampUs = C.TIME_UNSET;
+      lastRawPlaybackHeadPosition = 0;
+      rawPlaybackHeadWrapCount = 0;
+      passthroughWorkaroundPauseOffset = 0;
+      if (audioTrack != null) {
+        sampleRate = audioTrack.getSampleRate();
+      }
+    }
+
+    /**
+     * Stops the audio track in a way that ensures media written to it is played out in full, and
+     * that {@link #getPlaybackHeadPosition()} and {@link #getPlaybackHeadPositionUs()} continue to
+     * increment as the remaining media is played out.
+     *
+     * @param submittedFrames The total number of frames that have been submitted.
+     */
+    public void handleEndOfStream(long submittedFrames) {
+      stopPlaybackHeadPosition = getPlaybackHeadPosition();
+      stopTimestampUs = SystemClock.elapsedRealtime() * 1000;
+      endPlaybackHeadPosition = submittedFrames;
+      audioTrack.stop();
+    }
+
+    /**
+     * Pauses the audio track unless the end of the stream has been handled, in which case calling
+     * this method does nothing.
+     */
+    public void pause() {
+      if (stopTimestampUs != C.TIME_UNSET) {
+        // We don't want to knock the audio track back into the paused state.
+        return;
+      }
+      audioTrack.pause();
+    }
+
+    /**
+     * {@link android.media.AudioTrack#getPlaybackHeadPosition()} returns a value intended to be
+     * interpreted as an unsigned 32 bit integer, which also wraps around periodically. This method
+     * returns the playback head position as a long that will only wrap around if the value exceeds
+     * {@link Long#MAX_VALUE} (which in practice will never happen).
+     *
+     * @return {@link android.media.AudioTrack#getPlaybackHeadPosition()} of {@link #audioTrack}
+     *     expressed as a long.
+     */
+    public long getPlaybackHeadPosition() {
+      if (stopTimestampUs != C.TIME_UNSET) {
+        // Simulate the playback head position up to the total number of frames submitted.
+        long elapsedTimeSinceStopUs = (SystemClock.elapsedRealtime() * 1000) - stopTimestampUs;
+        long framesSinceStop = (elapsedTimeSinceStopUs * sampleRate) / C.MICROS_PER_SECOND;
+        return Math.min(endPlaybackHeadPosition, stopPlaybackHeadPosition + framesSinceStop);
+      }
+
+      int state = audioTrack.getPlayState();
+      if (state == PLAYSTATE_STOPPED) {
+        // The audio track hasn't been started.
+        return 0;
+      }
+
+      long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition();
+      if (needsPassthroughWorkaround) {
+        // Work around an issue with passthrough/direct AudioTracks on platform API versions 21/22
+        // where the playback head position jumps back to zero on paused passthrough/direct audio
+        // tracks. See [Internal: b/19187573].
+        if (state == PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) {
+          passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition;
+        }
+        rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset;
+      }
+      if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) {
+        // The value must have wrapped around.
+        rawPlaybackHeadWrapCount++;
+      }
+      lastRawPlaybackHeadPosition = rawPlaybackHeadPosition;
+      return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32);
+    }
+
+    /**
+     * Returns {@link #getPlaybackHeadPosition()} expressed as microseconds.
+     */
+    public long getPlaybackHeadPositionUs() {
+      return (getPlaybackHeadPosition() * C.MICROS_PER_SECOND) / sampleRate;
+    }
+
+    /**
+     * Updates the values returned by {@link #getTimestampNanoTime()} and
+     * {@link #getTimestampFramePosition()}.
+     *
+     * @return Whether the timestamp values were updated.
+     */
+    public boolean updateTimestamp() {
+      return false;
+    }
+
+    /**
+     * Returns the {@link android.media.AudioTimestamp#nanoTime} obtained during the most recent
+     * call to {@link #updateTimestamp()} that returned true.
+     *
+     * @return The nanoTime obtained during the most recent call to {@link #updateTimestamp()} that
+     *     returned true.
+     * @throws UnsupportedOperationException If the implementation does not support audio timestamp
+     *     queries. {@link #updateTimestamp()} will always return false in this case.
+     */
+    public long getTimestampNanoTime() {
+      // Should never be called if updateTimestamp() returned false.
+      throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Returns the {@link android.media.AudioTimestamp#framePosition} obtained during the most
+     * recent call to {@link #updateTimestamp()} that returned true. The value is adjusted so that
+     * wrap around only occurs if the value exceeds {@link Long#MAX_VALUE} (which in practice will
+     * never happen).
+     *
+     * @return The framePosition obtained during the most recent call to {@link #updateTimestamp()}
+     *     that returned true.
+     * @throws UnsupportedOperationException If the implementation does not support audio timestamp
+     *     queries. {@link #updateTimestamp()} will always return false in this case.
+     */
+    public long getTimestampFramePosition() {
+      // Should never be called if updateTimestamp() returned false.
+      throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Sets the Playback Parameters to be used by the underlying {@link android.media.AudioTrack}.
+     *
+     * @param playbackParams The playback parameters to be used by the
+     *     {@link android.media.AudioTrack}.
+     * @throws UnsupportedOperationException If Playback Parameters are not supported
+     *     (i.e. {@link Util#SDK_INT} &lt; 23).
+     */
+    public void setPlaybackParams(PlaybackParams playbackParams) {
+      throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Returns the configured playback speed according to the used Playback Parameters. If these are
+     * not supported, 1.0f(normal speed) is returned.
+     *
+     * @return The speed factor used by the underlying {@link android.media.AudioTrack}.
+     */
+    public float getPlaybackSpeed() {
+      return 1.0f;
+    }
+
+  }
+
+  @TargetApi(19)
+  private static class AudioTrackUtilV19 extends AudioTrackUtil {
+
+    private final AudioTimestamp audioTimestamp;
+
+    private long rawTimestampFramePositionWrapCount;
+    private long lastRawTimestampFramePosition;
+    private long lastTimestampFramePosition;
+
+    public AudioTrackUtilV19() {
+      audioTimestamp = new AudioTimestamp();
+    }
+
+    @Override
+    public void reconfigure(android.media.AudioTrack audioTrack,
+        boolean needsPassthroughWorkaround) {
+      super.reconfigure(audioTrack, needsPassthroughWorkaround);
+      rawTimestampFramePositionWrapCount = 0;
+      lastRawTimestampFramePosition = 0;
+      lastTimestampFramePosition = 0;
+    }
+
+    @Override
+    public boolean updateTimestamp() {
+      boolean updated = audioTrack.getTimestamp(audioTimestamp);
+      if (updated) {
+        long rawFramePosition = audioTimestamp.framePosition;
+        if (lastRawTimestampFramePosition > rawFramePosition) {
+          // The value must have wrapped around.
+          rawTimestampFramePositionWrapCount++;
+        }
+        lastRawTimestampFramePosition = rawFramePosition;
+        lastTimestampFramePosition = rawFramePosition + (rawTimestampFramePositionWrapCount << 32);
+      }
+      return updated;
+    }
+
+    @Override
+    public long getTimestampNanoTime() {
+      return audioTimestamp.nanoTime;
+    }
+
+    @Override
+    public long getTimestampFramePosition() {
+      return lastTimestampFramePosition;
+    }
+
+  }
+
+  @TargetApi(23)
+  private static class AudioTrackUtilV23 extends AudioTrackUtilV19 {
+
+    private PlaybackParams playbackParams;
+    private float playbackSpeed;
+
+    public AudioTrackUtilV23() {
+      playbackSpeed = 1.0f;
+    }
+
+    @Override
+    public void reconfigure(android.media.AudioTrack audioTrack,
+        boolean needsPassthroughWorkaround) {
+      super.reconfigure(audioTrack, needsPassthroughWorkaround);
+      maybeApplyPlaybackParams();
+    }
+
+    @Override
+    public void setPlaybackParams(PlaybackParams playbackParams) {
+      playbackParams = (playbackParams != null ? playbackParams : new PlaybackParams())
+          .allowDefaults();
+      this.playbackParams = playbackParams;
+      playbackSpeed = playbackParams.getSpeed();
+      maybeApplyPlaybackParams();
+    }
+
+    @Override
+    public float getPlaybackSpeed() {
+      return playbackSpeed;
+    }
+
+    private void maybeApplyPlaybackParams() {
+      if (audioTrack != null && playbackParams != null) {
+        audioTrack.setPlaybackParams(playbackParams);
+      }
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/audio/DtsUtil.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 com.google.android.exoplayer2.audio;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import java.nio.ByteBuffer;
+
+/**
+ * Utility methods for parsing DTS frames.
+ */
+public final class DtsUtil {
+
+  /**
+   * 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 the DTS format given {@code data} containing the DTS frame according to ETSI TS 102 114
+   * subsections 5.3/5.4.
+   *
+   * @param frame The DTS frame to parse.
+   * @param trackId The track identifier to set on the format, or null.
+   * @param language The language to set on the format.
+   * @param drmInitData {@link DrmInitData} to be included in the format.
+   * @return The DTS format parsed from data in the header.
+   */
+  public static Format parseDtsFormat(byte[] frame, String trackId, String language,
+      DrmInitData drmInitData) {
+    ParsableBitArray frameBits = new ParsableBitArray(frame);
+    frameBits.skipBits(4 * 8 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE
+    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) {
+    // See ETSI TS 102 114 subsection 5.4.1.
+    int 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 = ((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) {
+    return (((data[5] & 0x02) << 12)
+        | ((data[6] & 0xFF) << 4)
+        | ((data[7] & 0xF0) >> 4)) + 1;
+  }
+
+  private DtsUtil() {}
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.audio;
+
+import android.annotation.TargetApi;
+import android.media.MediaCodec;
+import android.media.MediaCrypto;
+import android.media.MediaFormat;
+import android.media.PlaybackParams;
+import android.media.audiofx.Virtualizer;
+import android.os.Handler;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
+import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
+import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
+import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+import com.google.android.exoplayer2.util.MediaClock;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
+
+/**
+ * Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}.
+ */
+@TargetApi(16)
+public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
+
+  private final EventDispatcher eventDispatcher;
+  private final AudioTrack audioTrack;
+
+  private boolean passthroughEnabled;
+  private android.media.MediaFormat passthroughMediaFormat;
+  private int pcmEncoding;
+  private long currentPositionUs;
+  private boolean allowPositionDiscontinuity;
+
+  /**
+   * @param mediaCodecSelector A decoder selector.
+   */
+  public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector) {
+    this(mediaCodecSelector, null, true);
+  }
+
+  /**
+   * @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.
+   */
+  public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
+      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      boolean playClearSamplesWithoutKeys) {
+    this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null);
+  }
+
+  /**
+   * @param mediaCodecSelector A decoder selector.
+   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+   *     null if delivery of events is not required.
+   * @param eventListener A listener of events. May be null if delivery of events is not required.
+   */
+  public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, Handler eventHandler,
+      AudioRendererEventListener eventListener) {
+    this(mediaCodecSelector, null, true, eventHandler, eventListener);
+  }
+
+  /**
+   * @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.
+   */
+  public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
+      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      boolean playClearSamplesWithoutKeys, Handler eventHandler,
+      AudioRendererEventListener eventListener) {
+    this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,
+        eventListener, null);
+  }
+
+  /**
+   * @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.
+   */
+  public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
+      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      boolean playClearSamplesWithoutKeys, Handler eventHandler,
+      AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
+    super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
+    audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
+    eventDispatcher = new EventDispatcher(eventHandler, eventListener);
+  }
+
+  @Override
+  protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format)
+      throws DecoderQueryException {
+    String mimeType = format.sampleMimeType;
+    if (!MimeTypes.isAudio(mimeType)) {
+      return FORMAT_UNSUPPORTED_TYPE;
+    }
+    int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
+    if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) {
+      return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED;
+    }
+    MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false);
+    if (decoderInfo == null) {
+      return FORMAT_UNSUPPORTED_SUBTYPE;
+    }
+    // Note: We assume support for unknown sampleRate and channelCount.
+    boolean decoderCapable = Util.SDK_INT < 21
+        || ((format.sampleRate == Format.NO_VALUE
+        || decoderInfo.isAudioSampleRateSupportedV21(format.sampleRate))
+        && (format.channelCount == Format.NO_VALUE
+        ||  decoderInfo.isAudioChannelCountSupportedV21(format.channelCount)));
+    int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;
+    return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;
+  }
+
+  @Override
+  protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector,
+      Format format, boolean requiresSecureDecoder) throws DecoderQueryException {
+    if (allowPassthrough(format.sampleMimeType)) {
+      MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo();
+      if (passthroughDecoderInfo != null) {
+        passthroughEnabled = true;
+        return passthroughDecoderInfo;
+      }
+    }
+    passthroughEnabled = false;
+    return super.getDecoderInfo(mediaCodecSelector, format, requiresSecureDecoder);
+  }
+
+  /**
+   * Returns whether encoded audio passthrough should be used for playing back the input format.
+   * This implementation returns true if the {@link AudioTrack}'s audio capabilities indicate that
+   * passthrough is supported.
+   *
+   * @param mimeType The type of input media.
+   * @return Whether passthrough playback should be used.
+   */
+  protected boolean allowPassthrough(String mimeType) {
+    return audioTrack.isPassthroughSupported(mimeType);
+  }
+
+  @Override
+  protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
+      MediaCrypto crypto) {
+    if (passthroughEnabled) {
+      // Override the MIME type used to configure the codec if we are using a passthrough decoder.
+      passthroughMediaFormat = format.getFrameworkMediaFormatV16();
+      passthroughMediaFormat.setString(MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW);
+      codec.configure(passthroughMediaFormat, null, crypto, 0);
+      passthroughMediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType);
+    } else {
+      codec.configure(format.getFrameworkMediaFormatV16(), null, crypto, 0);
+      passthroughMediaFormat = null;
+    }
+  }
+
+  @Override
+  public MediaClock getMediaClock() {
+    return this;
+  }
+
+  @Override
+  protected void onCodecInitialized(String name, long initializedTimestampMs,
+      long initializationDurationMs) {
+    eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs);
+  }
+
+  @Override
+  protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
+    super.onInputFormatChanged(newFormat);
+    eventDispatcher.inputFormatChanged(newFormat);
+    // If the input format is anything other than PCM then we assume that the audio decoder will
+    // output 16-bit PCM.
+    pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding
+        : C.ENCODING_PCM_16BIT;
+  }
+
+  @Override
+  protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) {
+    boolean passthrough = passthroughMediaFormat != null;
+    String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME)
+        : MimeTypes.AUDIO_RAW;
+    MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat;
+    int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
+    int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
+    audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0);
+  }
+
+  /**
+   * 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 AudioTrack.Listener#onAudioSessionId(int)
+   */
+  protected void onAudioSessionId(int audioSessionId) {
+    // Do nothing.
+  }
+
+  /**
+   * @see AudioTrack.Listener#onPositionDiscontinuity()
+   */
+  protected void onAudioTrackPositionDiscontinuity() {
+    // Do nothing.
+  }
+
+  /**
+   * @see AudioTrack.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) {
+      audioTrack.enableTunnelingV21(tunnelingAudioSessionId);
+    } else {
+      audioTrack.disableTunneling();
+    }
+  }
+
+  @Override
+  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+    super.onPositionReset(positionUs, joining);
+    audioTrack.reset();
+    currentPositionUs = positionUs;
+    allowPositionDiscontinuity = true;
+  }
+
+  @Override
+  protected void onStarted() {
+    super.onStarted();
+    audioTrack.play();
+  }
+
+  @Override
+  protected void onStopped() {
+    audioTrack.pause();
+    super.onStopped();
+  }
+
+  @Override
+  protected void onDisabled() {
+    try {
+      audioTrack.release();
+    } finally {
+      try {
+        super.onDisabled();
+      } finally {
+        decoderCounters.ensureUpdated();
+        eventDispatcher.disabled(decoderCounters);
+      }
+    }
+  }
+
+  @Override
+  public boolean isEnded() {
+    return super.isEnded() && !audioTrack.hasPendingData();
+  }
+
+  @Override
+  public boolean isReady() {
+    return audioTrack.hasPendingData() || super.isReady();
+  }
+
+  @Override
+  public long getPositionUs() {
+    long newCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded());
+    if (newCurrentPositionUs != AudioTrack.CURRENT_POSITION_NOT_SET) {
+      currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs
+          : Math.max(currentPositionUs, newCurrentPositionUs);
+      allowPositionDiscontinuity = false;
+    }
+    return currentPositionUs;
+  }
+
+  @Override
+  protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
+      ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs,
+      boolean shouldSkip) throws ExoPlaybackException {
+    if (passthroughEnabled && (bufferFlags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
+      // Discard output buffers from the passthrough (raw) decoder containing codec specific data.
+      codec.releaseOutputBuffer(bufferIndex, false);
+      return true;
+    }
+
+    if (shouldSkip) {
+      codec.releaseOutputBuffer(bufferIndex, false);
+      decoderCounters.skippedOutputBufferCount++;
+      audioTrack.handleDiscontinuity();
+      return true;
+    }
+
+    try {
+      if (audioTrack.handleBuffer(buffer, bufferPresentationTimeUs)) {
+        codec.releaseOutputBuffer(bufferIndex, false);
+        decoderCounters.renderedOutputBufferCount++;
+        return true;
+      }
+    } catch (AudioTrack.InitializationException | AudioTrack.WriteException e) {
+      throw ExoPlaybackException.createForRenderer(e, getIndex());
+    }
+    return false;
+  }
+
+  @Override
+  protected void onOutputStreamEnded() {
+    audioTrack.handleEndOfStream();
+  }
+
+  @Override
+  public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+    switch (messageType) {
+      case C.MSG_SET_VOLUME:
+        audioTrack.setVolume((Float) message);
+        break;
+      case C.MSG_SET_PLAYBACK_PARAMS:
+        audioTrack.setPlaybackParams((PlaybackParams) message);
+        break;
+      case C.MSG_SET_STREAM_TYPE:
+        @C.StreamType int streamType = (Integer) message;
+        audioTrack.setStreamType(streamType);
+        break;
+      default:
+        super.handleMessage(messageType, message);
+        break;
+    }
+  }
+
+  private final class AudioTrackListener implements AudioTrack.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);
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
@@ -0,0 +1,602 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.audio;
+
+import android.media.PlaybackParams;
+import android.media.audiofx.Virtualizer;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.BaseRenderer;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;
+import com.google.android.exoplayer2.decoder.DecoderCounters;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.decoder.SimpleDecoder;
+import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
+import com.google.android.exoplayer2.drm.DrmSession;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.ExoMediaCrypto;
+import com.google.android.exoplayer2.util.MediaClock;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.TraceUtil;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Decodes and renders audio using a {@link SimpleDecoder}.
+ */
+public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock {
+
+  @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 boolean playClearSamplesWithoutKeys;
+
+  private final EventDispatcher eventDispatcher;
+  private final AudioTrack audioTrack;
+  private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
+  private final FormatHolder formatHolder;
+
+  private DecoderCounters decoderCounters;
+  private Format inputFormat;
+  private SimpleDecoder<DecoderInputBuffer, ? extends SimpleOutputBuffer,
+        ? extends AudioDecoderException> decoder;
+  private DecoderInputBuffer inputBuffer;
+  private SimpleOutputBuffer outputBuffer;
+  private DrmSession<ExoMediaCrypto> drmSession;
+  private DrmSession<ExoMediaCrypto> pendingDrmSession;
+
+  @ReinitializationState
+  private int decoderReinitializationState;
+  private boolean decoderReceivedBuffers;
+  private boolean audioTrackNeedsConfigure;
+
+  private long currentPositionUs;
+  private boolean allowPositionDiscontinuity;
+  private boolean inputStreamEnded;
+  private boolean outputStreamEnded;
+  private boolean waitingForKeys;
+
+  public SimpleDecoderAudioRenderer() {
+    this(null, 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.
+   */
+  public SimpleDecoderAudioRenderer(Handler eventHandler,
+      AudioRendererEventListener eventListener) {
+    this(eventHandler, 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 audioCapabilities The audio capabilities for playback on this device. May be null if the
+   *     default capabilities (no encoded audio passthrough support) should be assumed.
+   */
+  public SimpleDecoderAudioRenderer(Handler eventHandler,
+      AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
+    this(eventHandler, eventListener, audioCapabilities, null, false);
+  }
+
+  /**
+   * @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.
+   */
+  public SimpleDecoderAudioRenderer(Handler eventHandler,
+      AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
+      DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys) {
+    super(C.TRACK_TYPE_AUDIO);
+    eventDispatcher = new EventDispatcher(eventHandler, eventListener);
+    audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
+    this.drmSessionManager = drmSessionManager;
+    formatHolder = new FormatHolder();
+    this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
+    decoderReinitializationState = REINITIALIZATION_STATE_NONE;
+    audioTrackNeedsConfigure = true;
+  }
+
+  @Override
+  public MediaClock getMediaClock() {
+    return this;
+  }
+
+  @Override
+  public final int supportsFormat(Format format) {
+    int formatSupport = supportsFormatInternal(format);
+    if (formatSupport == FORMAT_UNSUPPORTED_TYPE || formatSupport == FORMAT_UNSUPPORTED_SUBTYPE) {
+      return formatSupport;
+    }
+    int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
+    return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;
+  }
+
+  /**
+   * Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for
+   * {@link #supportsFormat(Format)}.
+   *
+   * @param format The format.
+   * @return The extent to which the renderer supports the format itself.
+   */
+  protected abstract int supportsFormatInternal(Format format);
+
+  @Override
+  public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
+    if (outputStreamEnded) {
+      return;
+    }
+
+    // Try and read a format if we don't have one already.
+    if (inputFormat == null && !readFormat()) {
+      // We 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 (AudioTrack.InitializationException | AudioTrack.WriteException
+          | AudioDecoderException e) {
+        throw ExoPlaybackException.createForRenderer(e, getIndex());
+      }
+      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 AudioTrack.Listener#onAudioSessionId(int)
+   */
+  protected void onAudioSessionId(int audioSessionId) {
+    // Do nothing.
+  }
+
+  /**
+   * @see AudioTrack.Listener#onPositionDiscontinuity()
+   */
+  protected void onAudioTrackPositionDiscontinuity() {
+    // Do nothing.
+  }
+
+  /**
+   * @see AudioTrack.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, 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.
+   * <p>
+   * The default implementation returns a 16-bit PCM format with the same channel count and sample
+   * rate as the input.
+   */
+  protected Format getOutputFormat() {
+    return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE,
+        Format.NO_VALUE, inputFormat.channelCount, inputFormat.sampleRate, C.ENCODING_PCM_16BIT,
+        null, null, 0, null);
+  }
+
+  private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException,
+      AudioTrack.InitializationException, AudioTrack.WriteException {
+    if (outputBuffer == null) {
+      outputBuffer = decoder.dequeueOutputBuffer();
+      if (outputBuffer == null) {
+        return false;
+      }
+      decoderCounters.skippedOutputBufferCount += 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();
+        // The audio track may need to be recreated once the new output format is known.
+        audioTrackNeedsConfigure = true;
+      } else {
+        outputBuffer.release();
+        outputBuffer = null;
+        outputStreamEnded = true;
+        audioTrack.handleEndOfStream();
+      }
+      return false;
+    }
+
+    if (audioTrackNeedsConfigure) {
+      Format outputFormat = getOutputFormat();
+      audioTrack.configure(outputFormat.sampleMimeType, outputFormat.channelCount,
+          outputFormat.sampleRate, outputFormat.pcmEncoding, 0);
+      audioTrackNeedsConfigure = false;
+    }
+
+    if (audioTrack.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;
+    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);
+    }
+
+    if (result == C.RESULT_NOTHING_READ) {
+      return false;
+    }
+    if (result == C.RESULT_FORMAT_READ) {
+      onInputFormatChanged(formatHolder.format);
+      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();
+    decoder.queueInputBuffer(inputBuffer);
+    decoderReceivedBuffers = true;
+    decoderCounters.inputBufferCount++;
+    inputBuffer = null;
+    return true;
+  }
+
+  private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
+    if (drmSession == null) {
+      return false;
+    }
+    @DrmSession.State int drmSessionState = drmSession.getState();
+    if (drmSessionState == DrmSession.STATE_ERROR) {
+      throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+    }
+    return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
+        && (bufferEncrypted || !playClearSamplesWithoutKeys);
+  }
+
+  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 && !audioTrack.hasPendingData();
+  }
+
+  @Override
+  public boolean isReady() {
+    return audioTrack.hasPendingData()
+        || (inputFormat != null && !waitingForKeys && (isSourceReady() || outputBuffer != null));
+  }
+
+  @Override
+  public long getPositionUs() {
+    long newCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded());
+    if (newCurrentPositionUs != AudioTrack.CURRENT_POSITION_NOT_SET) {
+      currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs
+          : Math.max(currentPositionUs, newCurrentPositionUs);
+      allowPositionDiscontinuity = false;
+    }
+    return currentPositionUs;
+  }
+
+  @Override
+  protected void onEnabled(boolean joining) throws ExoPlaybackException {
+    decoderCounters = new DecoderCounters();
+    eventDispatcher.enabled(decoderCounters);
+    int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
+    if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
+      audioTrack.enableTunnelingV21(tunnelingAudioSessionId);
+    } else {
+      audioTrack.disableTunneling();
+    }
+  }
+
+  @Override
+  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+    audioTrack.reset();
+    currentPositionUs = positionUs;
+    allowPositionDiscontinuity = true;
+    inputStreamEnded = false;
+    outputStreamEnded = false;
+    if (decoder != null) {
+      flushDecoder();
+    }
+  }
+
+  @Override
+  protected void onStarted() {
+    audioTrack.play();
+  }
+
+  @Override
+  protected void onStopped() {
+    audioTrack.pause();
+  }
+
+  @Override
+  protected void onDisabled() {
+    inputFormat = null;
+    audioTrackNeedsConfigure = true;
+    waitingForKeys = false;
+    try {
+      releaseDecoder();
+      audioTrack.release();
+    } finally {
+      try {
+        if (drmSession != null) {
+          drmSessionManager.releaseSession(drmSession);
+        }
+      } finally {
+        try {
+          if (pendingDrmSession != null && pendingDrmSession != drmSession) {
+            drmSessionManager.releaseSession(pendingDrmSession);
+          }
+        } finally {
+          drmSession = null;
+          pendingDrmSession = null;
+          decoderCounters.ensureUpdated();
+          eventDispatcher.disabled(decoderCounters);
+        }
+      }
+    }
+  }
+
+  private void maybeInitDecoder() throws ExoPlaybackException {
+    if (decoder != null) {
+      return;
+    }
+
+    drmSession = pendingDrmSession;
+    ExoMediaCrypto mediaCrypto = null;
+    if (drmSession != null) {
+      @DrmSession.State int drmSessionState = drmSession.getState();
+      if (drmSessionState == DrmSession.STATE_ERROR) {
+        throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+      } else if (drmSessionState == DrmSession.STATE_OPENED
+          || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
+        mediaCrypto = drmSession.getMediaCrypto();
+      } else {
+        // The drm session isn't open yet.
+        return;
+      }
+    }
+
+    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 ExoPlaybackException.createForRenderer(e, getIndex());
+    }
+  }
+
+  private void releaseDecoder() {
+    if (decoder == null) {
+      return;
+    }
+
+    inputBuffer = null;
+    outputBuffer = null;
+    decoder.release();
+    decoder = null;
+    decoderCounters.decoderReleaseCount++;
+    decoderReinitializationState = REINITIALIZATION_STATE_NONE;
+    decoderReceivedBuffers = false;
+  }
+
+  private boolean readFormat() throws ExoPlaybackException {
+    int result = readSource(formatHolder, null);
+    if (result == C.RESULT_FORMAT_READ) {
+      onInputFormatChanged(formatHolder.format);
+      return true;
+    }
+    return false;
+  }
+
+  private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
+    Format oldFormat = inputFormat;
+    inputFormat = newFormat;
+
+    boolean drmInitDataChanged = !Util.areEqual(inputFormat.drmInitData, oldFormat == null ? null
+        : oldFormat.drmInitData);
+    if (drmInitDataChanged) {
+      if (inputFormat.drmInitData != null) {
+        if (drmSessionManager == null) {
+          throw ExoPlaybackException.createForRenderer(
+              new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
+        }
+        pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(),
+            inputFormat.drmInitData);
+        if (pendingDrmSession == drmSession) {
+          drmSessionManager.releaseSession(pendingDrmSession);
+        }
+      } else {
+        pendingDrmSession = null;
+      }
+    }
+
+    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;
+    }
+
+    eventDispatcher.inputFormatChanged(newFormat);
+  }
+
+  @Override
+  public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+    switch (messageType) {
+      case C.MSG_SET_VOLUME:
+        audioTrack.setVolume((Float) message);
+        break;
+      case C.MSG_SET_PLAYBACK_PARAMS:
+        audioTrack.setPlaybackParams((PlaybackParams) message);
+        break;
+      case C.MSG_SET_STREAM_TYPE:
+        @C.StreamType int streamType = (Integer) message;
+        audioTrack.setStreamType(streamType);
+        break;
+      default:
+        super.handleMessage(messageType, message);
+        break;
+    }
+  }
+
+  private final class AudioTrackListener implements AudioTrack.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);
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/decoder/Buffer.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 com.google.android.exoplayer2.decoder;
+
+import 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);
+  }
+
+  /**
+   * 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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/decoder/CryptoInfo.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 com.google.android.exoplayer2.decoder;
+
+import android.annotation.TargetApi;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Compatibility wrapper for {@link android.media.MediaCodec.CryptoInfo}.
+ */
+public final class CryptoInfo {
+
+  /**
+   * @see android.media.MediaCodec.CryptoInfo#iv
+   */
+  public byte[] iv;
+  /**
+   * @see android.media.MediaCodec.CryptoInfo#key
+   */
+  public byte[] key;
+  /**
+   * @see android.media.MediaCodec.CryptoInfo#mode
+   */
+  @C.CryptoMode
+  public int mode;
+  /**
+   * @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData
+   */
+  public int[] numBytesOfClearData;
+  /**
+   * @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData
+   */
+  public int[] numBytesOfEncryptedData;
+  /**
+   * @see android.media.MediaCodec.CryptoInfo#numSubSamples
+   */
+  public int numSubSamples;
+
+  private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo;
+
+  public CryptoInfo() {
+    frameworkCryptoInfo = Util.SDK_INT >= 16 ? newFrameworkCryptoInfoV16() : 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) {
+    this.numSubSamples = numSubSamples;
+    this.numBytesOfClearData = numBytesOfClearData;
+    this.numBytesOfEncryptedData = numBytesOfEncryptedData;
+    this.key = key;
+    this.iv = iv;
+    this.mode = mode;
+    if (Util.SDK_INT >= 16) {
+      updateFrameworkCryptoInfoV16();
+    }
+  }
+
+  /**
+   * 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.
+   */
+  @TargetApi(16)
+  public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() {
+    return frameworkCryptoInfo;
+  }
+
+  @TargetApi(16)
+  private android.media.MediaCodec.CryptoInfo newFrameworkCryptoInfoV16() {
+    return new android.media.MediaCodec.CryptoInfo();
+  }
+
+  @TargetApi(16)
+  private void updateFrameworkCryptoInfoV16() {
+    frameworkCryptoInfo.set(numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, key, iv,
+        mode);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/decoder/Decoder.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 com.google.android.exoplayer2.decoder;
+
+/**
+ * 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.
+   */
+  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.
+   */
+  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();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/decoder/DecoderCounters.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 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 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 output buffers.
+   * <p>
+   * A dropped output buffer is an output buffer that was supposed to be rendered, but was instead
+   * dropped because it could not be rendered in time.
+   */
+  public int droppedOutputBufferCount;
+  /**
+   * The maximum number of dropped output buffers without an interleaving rendered output buffer.
+   * <p>
+   * Skipped output buffers are ignored for the purposes of calculating this value.
+   */
+  public int maxConsecutiveDroppedOutputBufferCount;
+
+  /**
+   * 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;
+    renderedOutputBufferCount += other.renderedOutputBufferCount;
+    skippedOutputBufferCount += other.skippedOutputBufferCount;
+    droppedOutputBufferCount += other.droppedOutputBufferCount;
+    maxConsecutiveDroppedOutputBufferCount = Math.max(maxConsecutiveDroppedOutputBufferCount,
+        other.maxConsecutiveDroppedOutputBufferCount);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/decoder/DecoderInputBuffer.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 com.google.android.exoplayer2.decoder;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.C;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.ByteBuffer;
+
+/**
+ * Holds input for a decoder.
+ */
+public class DecoderInputBuffer extends Buffer {
+
+  /**
+   * The buffer replacement mode, which may disable replacement.
+   */
+  @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.
+   */
+  public ByteBuffer data;
+
+  /**
+   * The time at which the sample should be presented.
+   */
+  public long timeUs;
+
+  @BufferReplacementMode
+  private final int bufferReplacementMode;
+
+  /**
+   * @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;
+  }
+
+  /**
+   * 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}.
+   */
+  public void ensureSpaceForWrite(int length) throws IllegalStateException {
+    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);
+    // Copy data up to the current position from the old buffer to the new one.
+    if (position > 0) {
+      data.position(0);
+      data.limit(position);
+      newData.put(data);
+    }
+    // Set the new buffer.
+    data = newData;
+  }
+
+  /**
+   * Returns whether the {@link C#BUFFER_FLAG_ENCRYPTED} flag is set.
+   */
+  public final boolean isEncrypted() {
+    return getFlag(C.BUFFER_FLAG_ENCRYPTED);
+  }
+
+  /**
+   * Flips {@link #data} in preparation for being queued to a decoder.
+   *
+   * @see java.nio.Buffer#flip()
+   */
+  public final void flip() {
+    data.flip();
+  }
+
+  @Override
+  public void clear() {
+    super.clear();
+    if (data != null) {
+      data.clear();
+    }
+  }
+
+  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 + ")");
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 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();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/decoder/SimpleDecoder.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 com.google.android.exoplayer2.decoder;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.LinkedList;
+
+/**
+ * Base class for {@link Decoder}s that use their own decode thread.
+ */
+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 LinkedList<I> queuedInputBuffers;
+  private final LinkedList<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 LinkedList<>();
+    queuedOutputBuffers = new LinkedList<>();
+    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
+  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
+  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.
+   */
+  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()) {
+        releaseOutputBufferInternal(queuedOutputBuffers.removeFirst());
+      }
+    }
+  }
+
+  @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);
+      }
+      exception = decode(inputBuffer, outputBuffer, resetDecoder);
+      if (exception != null) {
+        // Memory barrier to ensure that the decoder exception is visible from the playback thread.
+        synchronized (lock) {}
+        return false;
+      }
+    }
+
+    synchronized (lock) {
+      if (flushed) {
+        releaseOutputBufferInternal(outputBuffer);
+      } else if (outputBuffer.isDecodeOnly()) {
+        skippedOutputBufferCount++;
+        releaseOutputBufferInternal(outputBuffer);
+      } 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();
+
+  /**
+   * 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.
+   */
+  protected abstract E decode(I inputBuffer, O outputBuffer, boolean reset);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.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 com.google.android.exoplayer2.decoder;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Buffer for {@link SimpleDecoder} output.
+ */
+public class SimpleOutputBuffer extends OutputBuffer {
+
+  private final SimpleDecoder<?, SimpleOutputBuffer, ?> owner;
+
+  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);
+    }
+    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);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/drm/DecryptionException.java
@@ -0,0 +1,20 @@
+package com.google.android.exoplayer2.drm;
+
+/**
+ * An exception when doing drm decryption using the In-App Drm
+ */
+public class DecryptionException extends Exception {
+  private final int errorCode;
+
+  public DecryptionException(int errorCode, String message) {
+    super(message);
+    this.errorCode = errorCode;
+  }
+
+  /**
+   * Get error code
+   */
+  public int getErrorCode() {
+    return errorCode;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java
@@ -0,0 +1,705 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.drm;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.DeniedByServerException;
+import android.media.MediaDrm;
+import android.media.NotProvisionedException;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.support.annotation.IntDef;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
+import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
+import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
+import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}.
+ */
+@TargetApi(18)
+public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T>,
+    DrmSession<T> {
+
+  /**
+   * Listener of {@link DefaultDrmSessionManager} events.
+   */
+  public interface EventListener {
+
+    /**
+     * Called each time keys are loaded.
+     */
+    void onDrmKeysLoaded();
+
+    /**
+     * Called when a drm error occurs.
+     *
+     * @param e The corresponding exception.
+     */
+    void onDrmSessionManagerError(Exception e);
+
+    /**
+     * Called each time offline keys are restored.
+     */
+    void onDrmKeysRestored();
+
+    /**
+     * Called each time offline keys are removed.
+     */
+    void onDrmKeysRemoved();
+
+  }
+
+  /**
+   * The key to use when passing CustomData to a PlayReady instance in an optional parameter map.
+   */
+  public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData";
+
+  /** Determines the action to be done after a session acquired. */
+  @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. If the offline license is
+   * expired sets state to {@link #STATE_ERROR}.
+   */
+  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;
+
+  private static final String TAG = "OfflineDrmSessionMngr";
+
+  private static final int MSG_PROVISION = 0;
+  private static final int MSG_KEYS = 1;
+
+  private static final int MAX_LICENSE_DURATION_TO_RENEW = 60;
+
+  private final Handler eventHandler;
+  private final EventListener eventListener;
+  private final ExoMediaDrm<T> mediaDrm;
+  private final HashMap<String, String> optionalKeyRequestParameters;
+
+  /* package */ final MediaDrmCallback callback;
+  /* package */ final UUID uuid;
+
+  /* package */ MediaDrmHandler mediaDrmHandler;
+  /* package */ PostResponseHandler postResponseHandler;
+
+  private Looper playbackLooper;
+  private HandlerThread requestHandlerThread;
+  private Handler postRequestHandler;
+
+  private int mode;
+  private int openCount;
+  private boolean provisioningInProgress;
+  @DrmSession.State
+  private int state;
+  private T mediaCrypto;
+  private DrmSessionException lastException;
+  private byte[] schemeInitData;
+  private String schemeMimeType;
+  private byte[] sessionId;
+  private byte[] offlineLicenseKeySetId;
+
+  /**
+   * Instantiates a new instance using the Widevine scheme.
+   *
+   * @param callback Performs key and provisioning requests.
+   * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
+   *     to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
+   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+   *     null if delivery of events is not required.
+   * @param eventListener A listener of events. May be null if delivery of events is not required.
+   * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
+   */
+  public static DefaultDrmSessionManager<FrameworkMediaCrypto> newWidevineInstance(
+      MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters,
+      Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
+    return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters,
+        eventHandler, eventListener);
+  }
+
+  /**
+   * Instantiates a new instance using the PlayReady scheme.
+   * <p>
+   * Note that PlayReady is unsupported by most Android devices, with the exception of Android TV
+   * devices, which do provide support.
+   *
+   * @param callback Performs key and provisioning requests.
+   * @param customData Optional custom data to include in requests generated by the instance.
+   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+   *     null if delivery of events is not required.
+   * @param eventListener A listener of events. May be null if delivery of events is not required.
+   * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
+   */
+  public static DefaultDrmSessionManager<FrameworkMediaCrypto> newPlayReadyInstance(
+      MediaDrmCallback callback, String customData, Handler eventHandler,
+      EventListener eventListener) throws UnsupportedDrmException {
+    HashMap<String, String> optionalKeyRequestParameters;
+    if (!TextUtils.isEmpty(customData)) {
+      optionalKeyRequestParameters = new HashMap<>();
+      optionalKeyRequestParameters.put(PLAYREADY_CUSTOM_DATA_KEY, customData);
+    } else {
+      optionalKeyRequestParameters = null;
+    }
+    return newFrameworkInstance(C.PLAYREADY_UUID, callback, optionalKeyRequestParameters,
+        eventHandler, eventListener);
+  }
+
+  /**
+   * Instantiates a new instance.
+   *
+   * @param uuid The UUID of the drm scheme.
+   * @param callback Performs key and provisioning requests.
+   * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
+   *     to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
+   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+   *     null if delivery of events is not required.
+   * @param eventListener A listener of events. May be null if delivery of events is not required.
+   * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
+   */
+  public static DefaultDrmSessionManager<FrameworkMediaCrypto> newFrameworkInstance(
+      UUID uuid, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters,
+      Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
+    return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback,
+        optionalKeyRequestParameters, eventHandler, eventListener);
+  }
+
+  /**
+   * @param uuid The UUID of the drm scheme.
+   * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
+   * @param callback Performs key and provisioning requests.
+   * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
+   *     to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
+   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+   *     null if delivery of events is not required.
+   * @param eventListener A listener of events. May be null if delivery of events is not required.
+   */
+  public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback,
+      HashMap<String, String> optionalKeyRequestParameters, Handler eventHandler,
+      EventListener eventListener) {
+    this.uuid = uuid;
+    this.mediaDrm = mediaDrm;
+    this.callback = callback;
+    this.optionalKeyRequestParameters = optionalKeyRequestParameters;
+    this.eventHandler = eventHandler;
+    this.eventListener = eventListener;
+    mediaDrm.setOnEventListener(new MediaDrmEventListener());
+    state = STATE_CLOSED;
+    mode = MODE_PLAYBACK;
+  }
+
+  /**
+   * Provides access to {@link MediaDrm#getPropertyString(String)}.
+   * <p>
+   * This method may be called when the manager is in any state.
+   *
+   * @param key The key to request.
+   * @return The retrieved property.
+   */
+  public final String getPropertyString(String key) {
+    return mediaDrm.getPropertyString(key);
+  }
+
+  /**
+   * Provides access to {@link MediaDrm#setPropertyString(String, String)}.
+   * <p>
+   * This method may be called when the manager is in any state.
+   *
+   * @param key The property to write.
+   * @param value The value to write.
+   */
+  public final void setPropertyString(String key, String value) {
+    mediaDrm.setPropertyString(key, value);
+  }
+
+  /**
+   * Provides access to {@link MediaDrm#getPropertyByteArray(String)}.
+   * <p>
+   * This method may be called when the manager is in any state.
+   *
+   * @param key The key to request.
+   * @return The retrieved property.
+   */
+  public final byte[] getPropertyByteArray(String key) {
+    return mediaDrm.getPropertyByteArray(key);
+  }
+
+  /**
+   * Provides access to {@link MediaDrm#setPropertyByteArray(String, byte[])}.
+   * <p>
+   * This method may be called when the manager is in any state.
+   *
+   * @param key The property to write.
+   * @param value The value to write.
+   */
+  public final void setPropertyByteArray(String key, byte[] value) {
+    mediaDrm.setPropertyByteArray(key, value);
+  }
+
+  /**
+   * Sets the mode, which determines the role of sessions acquired from the instance. This must be
+   * called before {@link #acquireSession(Looper, DrmInitData)} 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:
+   * <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.
+   *
+   * @param mode The mode to be set.
+   * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode.
+   */
+  public void setMode(@Mode int mode, byte[] offlineLicenseKeySetId) {
+    Assertions.checkState(openCount == 0);
+    if (mode == MODE_QUERY || mode == MODE_RELEASE) {
+      Assertions.checkNotNull(offlineLicenseKeySetId);
+    }
+    this.mode = mode;
+    this.offlineLicenseKeySetId = offlineLicenseKeySetId;
+  }
+
+  // DrmSessionManager implementation.
+
+  @Override
+  public DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData) {
+    Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper);
+    if (++openCount != 1) {
+      return this;
+    }
+
+    if (this.playbackLooper == null) {
+      this.playbackLooper = playbackLooper;
+      mediaDrmHandler = new MediaDrmHandler(playbackLooper);
+      postResponseHandler = new PostResponseHandler(playbackLooper);
+    }
+
+    requestHandlerThread = new HandlerThread("DrmRequestHandler");
+    requestHandlerThread.start();
+    postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
+
+    if (offlineLicenseKeySetId == null) {
+      SchemeData schemeData = drmInitData.get(uuid);
+      if (schemeData == null) {
+        onError(new IllegalStateException("Media does not support uuid: " + uuid));
+        return this;
+      }
+      schemeInitData = schemeData.data;
+      schemeMimeType = schemeData.mimeType;
+      if (Util.SDK_INT < 21) {
+        // Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
+        byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, C.WIDEVINE_UUID);
+        if (psshData == null) {
+          // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
+        } else {
+          schemeInitData = psshData;
+        }
+      }
+    }
+    state = STATE_OPENING;
+    openInternal(true);
+    return this;
+  }
+
+  @Override
+  public void releaseSession(DrmSession<T> session) {
+    if (--openCount != 0) {
+      return;
+    }
+    state = STATE_CLOSED;
+    provisioningInProgress = false;
+    mediaDrmHandler.removeCallbacksAndMessages(null);
+    postResponseHandler.removeCallbacksAndMessages(null);
+    postRequestHandler.removeCallbacksAndMessages(null);
+    postRequestHandler = null;
+    requestHandlerThread.quit();
+    requestHandlerThread = null;
+    schemeInitData = null;
+    schemeMimeType = null;
+    mediaCrypto = null;
+    lastException = null;
+    if (sessionId != null) {
+      mediaDrm.closeSession(sessionId);
+      sessionId = null;
+    }
+  }
+
+  // DrmSession implementation.
+
+  @Override
+  @DrmSession.State
+  public final int getState() {
+    return state;
+  }
+
+  @Override
+  public final T getMediaCrypto() {
+    if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+      throw new IllegalStateException();
+    }
+    return mediaCrypto;
+  }
+
+  @Override
+  public boolean requiresSecureDecoderComponent(String mimeType) {
+    if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+      throw new IllegalStateException();
+    }
+    return mediaCrypto.requiresSecureDecoderComponent(mimeType);
+  }
+
+  @Override
+  public final DrmSessionException getError() {
+    return state == STATE_ERROR ? lastException : null;
+  }
+
+  @Override
+  public Map<String, String> queryKeyStatus() {
+    // User may call this method rightfully even if state == STATE_ERROR. So only check if there is
+    // a sessionId
+    if (sessionId == null) {
+      throw new IllegalStateException();
+    }
+    return mediaDrm.queryKeyStatus(sessionId);
+  }
+
+  @Override
+  public byte[] getOfflineLicenseKeySetId() {
+    return offlineLicenseKeySetId;
+  }
+
+  // Internal methods.
+
+  private void openInternal(boolean allowProvisioning) {
+    try {
+      sessionId = mediaDrm.openSession();
+      mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId);
+      state = STATE_OPENED;
+      doLicense();
+    } catch (NotProvisionedException e) {
+      if (allowProvisioning) {
+        postProvisionRequest();
+      } else {
+        onError(e);
+      }
+    } catch (Exception e) {
+      onError(e);
+    }
+  }
+
+  private void postProvisionRequest() {
+    if (provisioningInProgress) {
+      return;
+    }
+    provisioningInProgress = true;
+    ProvisionRequest request = mediaDrm.getProvisionRequest();
+    postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget();
+  }
+
+  private void onProvisionResponse(Object response) {
+    provisioningInProgress = false;
+    if (state != STATE_OPENING && state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+      // This event is stale.
+      return;
+    }
+
+    if (response instanceof Exception) {
+      onError((Exception) response);
+      return;
+    }
+
+    try {
+      mediaDrm.provideProvisionResponse((byte[]) response);
+      if (state == STATE_OPENING) {
+        openInternal(false);
+      } else {
+        doLicense();
+      }
+    } catch (DeniedByServerException e) {
+      onError(e);
+    }
+  }
+
+  private void doLicense() {
+    switch (mode) {
+      case MODE_PLAYBACK:
+      case MODE_QUERY:
+        if (offlineLicenseKeySetId == null) {
+          postKeyRequest(sessionId, MediaDrm.KEY_TYPE_STREAMING);
+        } else {
+          if (restoreKeys()) {
+            long licenseDurationRemainingSec = getLicenseDurationRemainingSec();
+            if (mode == MODE_PLAYBACK
+                && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) {
+              Log.d(TAG, "Offline license has expired or will expire soon. "
+                  + "Remaining seconds: " + licenseDurationRemainingSec);
+              postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE);
+            } else if (licenseDurationRemainingSec <= 0) {
+              onError(new KeysExpiredException());
+            } else {
+              state = STATE_OPENED_WITH_KEYS;
+              if (eventHandler != null && eventListener != null) {
+                eventHandler.post(new Runnable() {
+                  @Override
+                  public void run() {
+                    eventListener.onDrmKeysRestored();
+                  }
+                });
+              }
+            }
+          }
+        }
+        break;
+      case MODE_DOWNLOAD:
+        if (offlineLicenseKeySetId == null) {
+          postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE);
+        } else {
+          // Renew
+          if (restoreKeys()) {
+            postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE);
+          }
+        }
+        break;
+      case MODE_RELEASE:
+        if (restoreKeys()) {
+          postKeyRequest(offlineLicenseKeySetId, MediaDrm.KEY_TYPE_RELEASE);
+        }
+        break;
+    }
+  }
+
+  private boolean restoreKeys() {
+    try {
+      mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId);
+      return true;
+    } catch (Exception e) {
+      Log.e(TAG, "Error trying to restore Widevine keys.", e);
+      onError(e);
+    }
+    return false;
+  }
+
+  private long getLicenseDurationRemainingSec() {
+    if (!C.WIDEVINE_UUID.equals(uuid)) {
+      return Long.MAX_VALUE;
+    }
+    Pair<Long, Long> pair = WidevineUtil.getLicenseDurationRemainingSec(this);
+    return Math.min(pair.first, pair.second);
+  }
+
+  private void postKeyRequest(byte[] scope, int keyType) {
+    try {
+      KeyRequest keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType,
+          optionalKeyRequestParameters);
+      postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
+    } catch (Exception e) {
+      onKeysError(e);
+    }
+  }
+
+  private void onKeyResponse(Object response) {
+    if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+      // This event is stale.
+      return;
+    }
+
+    if (response instanceof Exception) {
+      onKeysError((Exception) response);
+      return;
+    }
+
+    try {
+      if (mode == MODE_RELEASE) {
+        mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response);
+        if (eventHandler != null && eventListener != null) {
+          eventHandler.post(new Runnable() {
+            @Override
+            public void run() {
+              eventListener.onDrmKeysRemoved();
+            }
+          });
+        }
+      } else {
+        byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
+        if ((mode == MODE_DOWNLOAD || (mode == MODE_PLAYBACK && offlineLicenseKeySetId != null))
+            && keySetId != null && keySetId.length != 0) {
+          offlineLicenseKeySetId = keySetId;
+        }
+        state = STATE_OPENED_WITH_KEYS;
+        if (eventHandler != null && eventListener != null) {
+          eventHandler.post(new Runnable() {
+            @Override
+            public void run() {
+              eventListener.onDrmKeysLoaded();
+            }
+          });
+        }
+      }
+    } catch (Exception e) {
+      onKeysError(e);
+    }
+  }
+
+  private void onKeysError(Exception e) {
+    if (e instanceof NotProvisionedException) {
+      postProvisionRequest();
+    } else {
+      onError(e);
+    }
+  }
+
+  private void onError(final Exception e) {
+    lastException = new DrmSessionException(e);
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable() {
+        @Override
+        public void run() {
+          eventListener.onDrmSessionManagerError(e);
+        }
+      });
+    }
+    if (state != STATE_OPENED_WITH_KEYS) {
+      state = STATE_ERROR;
+    }
+  }
+
+  @SuppressLint("HandlerLeak")
+  private class MediaDrmHandler extends Handler {
+
+    public MediaDrmHandler(Looper looper) {
+      super(looper);
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    public void handleMessage(Message msg) {
+      if (openCount == 0 || (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS)) {
+        return;
+      }
+      switch (msg.what) {
+        case MediaDrm.EVENT_KEY_REQUIRED:
+          doLicense();
+          break;
+        case MediaDrm.EVENT_KEY_EXPIRED:
+          // When an already expired key is loaded MediaDrm sends this event immediately. Ignore
+          // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still
+          // waiting for key response.
+          if (state == STATE_OPENED_WITH_KEYS) {
+            state = STATE_OPENED;
+            onError(new KeysExpiredException());
+          }
+          break;
+        case MediaDrm.EVENT_PROVISION_REQUIRED:
+          state = STATE_OPENED;
+          postProvisionRequest();
+          break;
+      }
+    }
+
+  }
+
+  private class MediaDrmEventListener implements OnEventListener<T> {
+
+    @Override
+    public void onEvent(ExoMediaDrm<? extends T> md, byte[] sessionId, int event, int extra,
+        byte[] data) {
+      if (mode == MODE_PLAYBACK) {
+        mediaDrmHandler.sendEmptyMessage(event);
+      }
+    }
+
+  }
+
+  @SuppressLint("HandlerLeak")
+  private class PostResponseHandler extends Handler {
+
+    public PostResponseHandler(Looper looper) {
+      super(looper);
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+      switch (msg.what) {
+        case MSG_PROVISION:
+          onProvisionResponse(msg.obj);
+          break;
+        case MSG_KEYS:
+          onKeyResponse(msg.obj);
+          break;
+      }
+    }
+
+  }
+
+  @SuppressLint("HandlerLeak")
+  private class PostRequestHandler extends Handler {
+
+    public PostRequestHandler(Looper backgroundLooper) {
+      super(backgroundLooper);
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+      Object response;
+      try {
+        switch (msg.what) {
+          case MSG_PROVISION:
+            response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj);
+            break;
+          case MSG_KEYS:
+            response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj);
+            break;
+          default:
+            throw new RuntimeException();
+        }
+      } catch (Exception e) {
+        response = e;
+      }
+      postResponseHandler.obtainMessage(msg.what, response).sendToTarget();
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/drm/DrmInitData.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.drm;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+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 {
+
+  private final SchemeData[] schemeDatas;
+
+  // Lazily initialized hashcode.
+  private int hashCode;
+
+  /**
+   * 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(false, schemeDatas.toArray(new SchemeData[schemeDatas.size()]));
+  }
+
+  /**
+   * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.
+   */
+  public DrmInitData(SchemeData... schemeDatas) {
+    this(true, schemeDatas);
+  }
+
+  private DrmInitData(boolean cloneSchemeDatas, SchemeData... schemeDatas) {
+    if (cloneSchemeDatas) {
+      schemeDatas = schemeDatas.clone();
+    }
+    // Sorting ensures that universal scheme data(i.e. data that applies to all schemes) is matched
+    // last. It's also required by the equals and hashcode implementations.
+    Arrays.sort(schemeDatas, this);
+    // Check for no duplicates.
+    for (int i = 1; i < schemeDatas.length; i++) {
+      if (schemeDatas[i - 1].uuid.equals(schemeDatas[i].uuid)) {
+        throw new IllegalArgumentException("Duplicate data for uuid: " + schemeDatas[i].uuid);
+      }
+    }
+    this.schemeDatas = schemeDatas;
+    schemeDataCount = schemeDatas.length;
+  }
+
+  /* package */ DrmInitData(Parcel in) {
+    schemeDatas = in.createTypedArray(SchemeData.CREATOR);
+    schemeDataCount = schemeDatas.length;
+  }
+
+  /**
+   * Retrieves data for a given DRM scheme, specified by its UUID.
+   *
+   * @param uuid The DRM scheme's UUID.
+   * @return The initialization data for the scheme, or null if the scheme is not supported.
+   */
+  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 index of the scheme to return.
+   * @return The {@link SchemeData} at the index.
+   */
+  public SchemeData get(int index) {
+    return schemeDatas[index];
+  }
+
+  @Override
+  public int hashCode() {
+    if (hashCode == 0) {
+      hashCode = Arrays.hashCode(schemeDatas);
+    }
+    return hashCode;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null || getClass() != obj.getClass()) {
+      return false;
+    }
+    return Arrays.equals(schemeDatas, ((DrmInitData) obj).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.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];
+    }
+
+  };
+
+  /**
+   * 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 mimeType of {@link #data}.
+     */
+    public final String mimeType;
+    /**
+     * The initialization data.
+     */
+    public final byte[] data;
+    /**
+     * Whether secure decryption is required.
+     */
+    public final boolean requiresSecureDecryption;
+
+    /**
+     * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is
+     *     universal (i.e. applies to all schemes).
+     * @param mimeType The mimeType of the initialization data.
+     * @param data The initialization data.
+     */
+    public SchemeData(UUID uuid, String mimeType, byte[] data) {
+      this(uuid, mimeType, data, false);
+    }
+
+    /**
+     * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is
+     *     universal (i.e. applies to all schemes).
+     * @param mimeType The mimeType of the initialization data.
+     * @param data The initialization data.
+     * @param requiresSecureDecryption Whether secure decryption is required.
+     */
+    public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) {
+      this.uuid = Assertions.checkNotNull(uuid);
+      this.mimeType = Assertions.checkNotNull(mimeType);
+      this.data = Assertions.checkNotNull(data);
+      this.requiresSecureDecryption = requiresSecureDecryption;
+    }
+
+    /* package */ SchemeData(Parcel in) {
+      uuid = new UUID(in.readLong(), in.readLong());
+      mimeType = in.readString();
+      data = in.createByteArray();
+      requiresSecureDecryption = in.readByte() != 0;
+    }
+
+    /**
+     * 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);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (!(obj instanceof SchemeData)) {
+        return false;
+      }
+      if (obj == this) {
+        return true;
+      }
+      SchemeData other = (SchemeData) obj;
+      return mimeType.equals(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 + 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(mimeType);
+      dest.writeByteArray(data);
+      dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0));
+    }
+
+    @SuppressWarnings("hiding")
+    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];
+      }
+
+    };
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/drm/DrmSession.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 com.google.android.exoplayer2.drm;
+
+import android.annotation.TargetApi;
+import android.media.MediaDrm;
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Map;
+
+/**
+ * A DRM session.
+ */
+@TargetApi(16)
+public interface DrmSession<T extends ExoMediaCrypto> {
+
+  /** Wraps the exception which is the cause of the error state. */
+  class DrmSessionException extends Exception {
+
+    DrmSessionException(Exception e) {
+      super(e);
+    }
+
+  }
+
+  /**
+   * The state of the DRM session.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({STATE_ERROR, STATE_CLOSED, STATE_OPENING, STATE_OPENED, STATE_OPENED_WITH_KEYS})
+  @interface State {}
+  /**
+   * The session has encountered an error. {@link #getError()} can be used to retrieve the cause.
+   */
+  int STATE_ERROR = 0;
+  /**
+   * The session is closed.
+   */
+  int STATE_CLOSED = 1;
+  /**
+   * The session is being opened.
+   */
+  int STATE_OPENING = 2;
+  /**
+   * The session is open, but does not yet have the keys required for decryption.
+   */
+  int STATE_OPENED = 3;
+  /**
+   * The session is open and has the keys required for decryption.
+   */
+  int STATE_OPENED_WITH_KEYS = 4;
+
+  /**
+   * Returns the current state of the session.
+   *
+   * @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING},
+   *     {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}.
+   */
+  @State
+  int getState();
+
+  /**
+   * Returns a {@link ExoMediaCrypto} for the open session.
+   * <p>
+   * This method may be called when the session is in the following states:
+   * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
+   *
+   * @return A {@link ExoMediaCrypto} for the open session.
+   * @throws IllegalStateException If called when a session isn't opened.
+   */
+  T getMediaCrypto();
+
+  /**
+   * Whether the session requires a secure decoder for the specified mime type.
+   * <p>
+   * Normally this method should return
+   * {@link ExoMediaCrypto#requiresSecureDecoderComponent(String)}, however in some cases
+   * implementations may wish to modify the return value (i.e. to force a secure decoder even when
+   * one is not required).
+   * <p>
+   * This method may be called when the session is in the following states:
+   * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
+   *
+   * @return Whether the open session requires a secure decoder for the specified mime type.
+   * @throws IllegalStateException If called when a session isn't opened.
+   */
+  boolean requiresSecureDecoderComponent(String mimeType);
+
+  /**
+   * Returns the cause of the error state.
+   * <p>
+   * This method may be called when the session is in any state.
+   *
+   * @return An exception if the state is {@link #STATE_ERROR}. Null otherwise.
+   */
+  DrmSessionException getError();
+
+  /**
+   * Returns an informative description of the key status for the session. The status is in the form
+   * of {name, value} pairs.
+   *
+   * <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 of key status.
+   * @throws IllegalStateException If called when the session isn't opened.
+   * @see MediaDrm#queryKeyStatus(byte[])
+   */
+  Map<String, String> queryKeyStatus();
+
+  /**
+   * Returns the key set id of the offline license loaded into this session, if there is one. Null
+   * otherwise.
+   */
+  byte[] getOfflineLicenseKeySetId();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/drm/DrmSessionManager.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.drm;
+
+import android.annotation.TargetApi;
+import android.os.Looper;
+
+/**
+ * Manages a DRM session.
+ */
+@TargetApi(16)
+public interface DrmSessionManager<T extends ExoMediaCrypto> {
+
+  /**
+   * Acquires a {@link DrmSession} for the specified {@link DrmInitData}. The {@link DrmSession}
+   * must be returned to {@link #releaseSession(DrmSession)} when it is no longer required.
+   *
+   * @param playbackLooper The looper associated with the media playback thread.
+   * @param drmInitData DRM initialization data.
+   * @return The DRM session.
+   */
+  DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData);
+
+  /**
+   * Releases a {@link DrmSession}.
+   */
+  void releaseSession(DrmSession<T> drmSession);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/drm/ExoMediaCrypto.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 com.google.android.exoplayer2.drm;
+
+/**
+ * An opaque {@link android.media.MediaCrypto} equivalent.
+ */
+public interface ExoMediaCrypto {
+
+  /**
+   * @see android.media.MediaCrypto#requiresSecureDecoderComponent(String)
+   */
+  boolean requiresSecureDecoderComponent(String mimeType);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/drm/ExoMediaDrm.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 com.google.android.exoplayer2.drm;
+
+import android.media.DeniedByServerException;
+import android.media.MediaCryptoException;
+import android.media.MediaDrm;
+import android.media.NotProvisionedException;
+import android.media.ResourceBusyException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Used to obtain keys for decrypting protected media streams. See {@link android.media.MediaDrm}.
+ */
+public interface ExoMediaDrm<T extends ExoMediaCrypto> {
+
+  /**
+   * @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 an secondary error code
+     * @param data optional byte array of data that may be associated with the event
+     */
+    void onEvent(ExoMediaDrm<? extends T> mediaDrm, byte[] sessionId, int event, int extra,
+        byte[] data);
+  }
+
+  /**
+   * @see android.media.MediaDrm.KeyRequest
+   */
+  interface KeyRequest {
+    byte[] getData();
+    String getDefaultUrl();
+  }
+
+  /**
+   * @see android.media.MediaDrm.ProvisionRequest
+   */
+  interface ProvisionRequest {
+    byte[] getData();
+    String getDefaultUrl();
+  }
+
+  /**
+   * @see MediaDrm#setOnEventListener(MediaDrm.OnEventListener)
+   */
+  void setOnEventListener(OnEventListener<? super T> listener);
+
+  /**
+   * @see MediaDrm#openSession()
+   */
+  byte[] openSession() throws NotProvisionedException, ResourceBusyException;
+
+  /**
+   * @see MediaDrm#closeSession(byte[])
+   */
+  void closeSession(byte[] sessionId);
+
+  /**
+   * @see MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)
+   */
+  KeyRequest getKeyRequest(byte[] scope, byte[] init, String mimeType, int keyType,
+      HashMap<String, String> optionalParameters) throws NotProvisionedException;
+
+  /**
+   * @see MediaDrm#provideKeyResponse(byte[], byte[])
+   */
+  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);
+
+  /**
+   * @see MediaDrm#release()
+   */
+  void release();
+
+  /**
+   * @see MediaDrm#restoreKeys(byte[], byte[])
+   */
+  void restoreKeys(byte[] sessionId, byte[] keySetId);
+
+  /**
+   * @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 uuid The UUID of the crypto scheme.
+   * @param initData Opaque initialization data specific to the crypto scheme.
+   * @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data.
+   * @throws MediaCryptoException
+   */
+  T createMediaCrypto(UUID uuid, byte[] initData) throws MediaCryptoException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.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 com.google.android.exoplayer2.drm;
+
+import android.annotation.TargetApi;
+import android.media.MediaCrypto;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * An {@link ExoMediaCrypto} implementation that wraps the framework {@link MediaCrypto}.
+ */
+@TargetApi(16)
+public final class FrameworkMediaCrypto implements ExoMediaCrypto {
+
+  private final MediaCrypto mediaCrypto;
+
+  /* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto) {
+    this.mediaCrypto = Assertions.checkNotNull(mediaCrypto);
+  }
+
+  public MediaCrypto getWrappedMediaCrypto() {
+    return mediaCrypto;
+  }
+
+  @Override
+  public boolean requiresSecureDecoderComponent(String mimeType) {
+    return mediaCrypto.requiresSecureDecoderComponent(mimeType);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.drm;
+
+import android.annotation.TargetApi;
+import android.media.DeniedByServerException;
+import android.media.MediaCrypto;
+import android.media.MediaCryptoException;
+import android.media.MediaDrm;
+import android.media.NotProvisionedException;
+import android.media.ResourceBusyException;
+import android.media.UnsupportedSchemeException;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * An {@link ExoMediaDrm} implementation that wraps the framework {@link MediaDrm}.
+ */
+@TargetApi(18)
+public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto> {
+
+  private final MediaDrm mediaDrm;
+
+  /**
+   * Creates an instance for the specified scheme UUID.
+   *
+   * @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 {
+    this.mediaDrm = new MediaDrm(Assertions.checkNotNull(uuid));
+  }
+
+  @Override
+  public void setOnEventListener(
+      final ExoMediaDrm.OnEventListener<? super FrameworkMediaCrypto> listener) {
+    mediaDrm.setOnEventListener(listener == null ? null : new MediaDrm.OnEventListener() {
+      @Override
+      public void onEvent(MediaDrm md, byte[] sessionId, int event, int extra, byte[] data) {
+        listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data);
+      }
+    });
+  }
+
+  @Override
+  public byte[] openSession() throws NotProvisionedException, ResourceBusyException {
+    return mediaDrm.openSession();
+  }
+
+  @Override
+  public void closeSession(byte[] sessionId) {
+    mediaDrm.closeSession(sessionId);
+  }
+
+  @Override
+  public KeyRequest getKeyRequest(byte[] scope, byte[] init, String mimeType, int keyType,
+      HashMap<String, String> optionalParameters) throws NotProvisionedException {
+    final MediaDrm.KeyRequest request = mediaDrm.getKeyRequest(scope, init, mimeType, keyType,
+        optionalParameters);
+    return new KeyRequest() {
+      @Override
+      public byte[] getData() {
+        return request.getData();
+      }
+
+      @Override
+      public String getDefaultUrl() {
+        return request.getDefaultUrl();
+      }
+    };
+  }
+
+  @Override
+  public byte[] provideKeyResponse(byte[] scope, byte[] response)
+      throws NotProvisionedException, DeniedByServerException {
+    return mediaDrm.provideKeyResponse(scope, response);
+  }
+
+  @Override
+  public ProvisionRequest getProvisionRequest() {
+    final MediaDrm.ProvisionRequest provisionRequest = mediaDrm.getProvisionRequest();
+    return new ProvisionRequest() {
+      @Override
+      public byte[] getData() {
+        return provisionRequest.getData();
+      }
+
+      @Override
+      public String getDefaultUrl() {
+        return provisionRequest.getDefaultUrl();
+      }
+    };
+  }
+
+  @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 void release() {
+    mediaDrm.release();
+  }
+
+  @Override
+  public void restoreKeys(byte[] sessionId, byte[] keySetId) {
+    mediaDrm.restoreKeys(sessionId, keySetId);
+  }
+
+  @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(UUID uuid, byte[] initData)
+      throws MediaCryptoException {
+    return new FrameworkMediaCrypto(new MediaCrypto(uuid, initData));
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.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 com.google.android.exoplayer2.drm;
+
+import android.annotation.TargetApi;
+import android.net.Uri;
+import android.text.TextUtils;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
+import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
+import com.google.android.exoplayer2.upstream.DataSourceInputStream;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.HashMap;
+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 Map<String, String> PLAYREADY_KEY_REQUEST_PROPERTIES;
+  static {
+    PLAYREADY_KEY_REQUEST_PROPERTIES = new HashMap<>();
+    PLAYREADY_KEY_REQUEST_PROPERTIES.put("Content-Type", "text/xml");
+    PLAYREADY_KEY_REQUEST_PROPERTIES.put("SOAPAction",
+        "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense");
+  }
+
+  private final HttpDataSource.Factory dataSourceFactory;
+  private final String defaultUrl;
+  private final Map<String, String> keyRequestProperties;
+
+  /**
+   * @param defaultUrl The default license URL.
+   * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
+   */
+  public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory) {
+    this(defaultUrl, dataSourceFactory, null);
+  }
+
+  /**
+   * @param defaultUrl The default license URL.
+   * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
+   * @param keyRequestProperties Request properties to set when making key requests, or null.
+   */
+  public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory,
+      Map<String, String> keyRequestProperties) {
+    this.dataSourceFactory = dataSourceFactory;
+    this.defaultUrl = defaultUrl;
+    this.keyRequestProperties = keyRequestProperties;
+  }
+
+  @Override
+  public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
+    String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
+    return executePost(url, new byte[0], null);
+  }
+
+  @Override
+  public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception {
+    String url = request.getDefaultUrl();
+    if (TextUtils.isEmpty(url)) {
+      url = defaultUrl;
+    }
+    Map<String, String> requestProperties = new HashMap<>();
+    requestProperties.put("Content-Type", "application/octet-stream");
+    if (C.PLAYREADY_UUID.equals(uuid)) {
+      requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES);
+    }
+    if (keyRequestProperties != null) {
+      requestProperties.putAll(keyRequestProperties);
+    }
+    return executePost(url, request.getData(), requestProperties);
+  }
+
+  private byte[] executePost(String url, byte[] data, 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());
+      }
+    }
+    DataSpec dataSpec = new DataSpec(Uri.parse(url), data, 0, 0, C.LENGTH_UNSET, null,
+        DataSpec.FLAG_ALLOW_GZIP);
+    DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
+    try {
+      return Util.toByteArray(inputStream);
+    } finally {
+      Util.closeQuietly(inputStream);
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.drm;
+
+/**
+ * Thrown when the drm keys loaded into an open session expire.
+ */
+public final class KeysExpiredException extends Exception {
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/drm/MediaDrmCallback.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 com.google.android.exoplayer2.drm;
+
+import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
+import 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;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.exoplayer2.drm;
+
+import android.media.MediaDrm;
+import android.net.Uri;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.EventListener;
+import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode;
+import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
+import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
+import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;
+import com.google.android.exoplayer2.source.chunk.InitializationChunk;
+import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
+import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
+import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
+import com.google.android.exoplayer2.source.dash.manifest.Period;
+import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
+import com.google.android.exoplayer2.source.dash.manifest.Representation;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSourceInputStream;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.HttpDataSource;
+import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * Helper class to download, renew and release offline licenses. It utilizes {@link
+ * DefaultDrmSessionManager}.
+ */
+public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
+
+  private final ConditionVariable conditionVariable;
+  private final DefaultDrmSessionManager<T> drmSessionManager;
+  private final HandlerThread handlerThread;
+
+  /**
+   * Helper method to download a DASH manifest.
+   *
+   * @param dataSource The {@link HttpDataSource} from which the manifest should be read.
+   * @param manifestUriString The URI of the manifest to be read.
+   * @return An instance of {@link DashManifest}.
+   * @throws IOException If an error occurs reading data from the stream.
+   * @see DashManifestParser
+   */
+  public static DashManifest downloadManifest(HttpDataSource dataSource, String manifestUriString)
+      throws IOException {
+    DataSourceInputStream inputStream = new DataSourceInputStream(
+        dataSource, new DataSpec(Uri.parse(manifestUriString)));
+    try {
+      inputStream.open();
+      DashManifestParser parser = new DashManifestParser();
+      return parser.parse(dataSource.getUri(), inputStream);
+    } finally {
+      inputStream.close();
+    }
+  }
+
+  /**
+   * Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when
+   * you're done with the helper instance.
+   *
+   * @param licenseUrl The default 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 licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException {
+    return newWidevineInstance(
+        new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory, null), null);
+  }
+
+  /**
+   * Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when
+   * you're done with the helper instance.
+   *
+   * @param callback Performs key and provisioning requests.
+   * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
+   *     to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
+   * @return A new instance which uses Widevine CDM.
+   * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be
+   *     instantiated.
+   * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm,
+   *     MediaDrmCallback, HashMap, Handler, EventListener)
+   */
+  public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(
+      MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters)
+      throws UnsupportedDrmException {
+    return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), callback,
+        optionalKeyRequestParameters);
+  }
+
+  /**
+   * Constructs an instance. Call {@link #releaseResources()} when you're done with it.
+   *
+   * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
+   * @param callback Performs key and provisioning requests.
+   * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
+   *     to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
+   * @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm,
+   *     MediaDrmCallback, HashMap, Handler, EventListener)
+   */
+  public OfflineLicenseHelper(ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback,
+      HashMap<String, String> optionalKeyRequestParameters) {
+    handlerThread = new HandlerThread("OfflineLicenseHelper");
+    handlerThread.start();
+
+    conditionVariable = new ConditionVariable();
+    EventListener eventListener = new EventListener() {
+      @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();
+      }
+    };
+    drmSessionManager = new DefaultDrmSessionManager<>(C.WIDEVINE_UUID, mediaDrm, callback,
+        optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener);
+  }
+
+  /** Releases the used resources. */
+  public void releaseResources() {
+    handlerThread.quit();
+  }
+
+  /**
+   * Downloads an offline license.
+   *
+   * @param dataSource The {@link HttpDataSource} to be used for download.
+   * @param manifestUriString The URI of the manifest to be read.
+   * @return The downloaded offline license key set id.
+   * @throws IOException If an error occurs reading data from the stream.
+   * @throws InterruptedException If the thread has been interrupted.
+   * @throws DrmSessionException Thrown when there is an error during DRM session.
+   */
+  public byte[] download(HttpDataSource dataSource, String manifestUriString)
+      throws IOException, InterruptedException, DrmSessionException {
+    return download(dataSource, downloadManifest(dataSource, manifestUriString));
+  }
+
+  /**
+   * Downloads an offline license.
+   *
+   * @param dataSource The {@link HttpDataSource} to be used for download.
+   * @param dashManifest The {@link DashManifest} of the DASH content.
+   * @return The downloaded offline license key set id.
+   * @throws IOException If an error occurs reading data from the stream.
+   * @throws InterruptedException If the thread has been interrupted.
+   * @throws DrmSessionException Thrown when there is an error during DRM session.
+   */
+  public byte[] download(HttpDataSource dataSource, DashManifest dashManifest)
+      throws IOException, InterruptedException, DrmSessionException {
+    // Get DrmInitData
+    // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream,
+    // as per DASH IF Interoperability Recommendations V3.0, 7.5.3.
+    if (dashManifest.getPeriodCount() < 1) {
+      return null;
+    }
+    Period period = dashManifest.getPeriod(0);
+    int adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO);
+    if (adaptationSetIndex == C.INDEX_UNSET) {
+      adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_AUDIO);
+      if (adaptationSetIndex == C.INDEX_UNSET) {
+        return null;
+      }
+    }
+    AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex);
+    if (adaptationSet.representations.isEmpty()) {
+      return null;
+    }
+    Representation representation = adaptationSet.representations.get(0);
+    DrmInitData drmInitData = representation.format.drmInitData;
+    if (drmInitData == null) {
+      InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation);
+      if (initializationChunk == null) {
+        return null;
+      }
+      Format sampleFormat = initializationChunk.getSampleFormat();
+      if (sampleFormat != null) {
+        drmInitData = sampleFormat.drmInitData;
+      }
+      if (drmInitData == null) {
+        return null;
+      }
+    }
+    blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, drmInitData);
+    return drmSessionManager.getOfflineLicenseKeySetId();
+  }
+
+  /**
+   * Renews an offline license.
+   *
+   * @param offlineLicenseKeySetId The key set id of the license to be renewed.
+   * @return Renewed offline license key set id.
+   * @throws DrmSessionException Thrown when there is an error during DRM session.
+   */
+  public byte[] renew(byte[] offlineLicenseKeySetId) throws DrmSessionException {
+    Assertions.checkNotNull(offlineLicenseKeySetId);
+    blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, offlineLicenseKeySetId, null);
+    return drmSessionManager.getOfflineLicenseKeySetId();
+  }
+
+  /**
+   * Releases an offline license.
+   *
+   * @param offlineLicenseKeySetId The key set id of the license to be released.
+   * @throws DrmSessionException Thrown when there is an error during DRM session.
+   */
+  public void release(byte[] offlineLicenseKeySetId) throws DrmSessionException {
+    Assertions.checkNotNull(offlineLicenseKeySetId);
+    blockingKeyRequest(DefaultDrmSessionManager.MODE_RELEASE, offlineLicenseKeySetId, null);
+  }
+
+  /**
+   * Returns license and playback durations remaining in seconds of the given offline license.
+   *
+   * @param offlineLicenseKeySetId The key set id of the license.
+   */
+  public Pair<Long, Long> getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId)
+      throws DrmSessionException {
+    Assertions.checkNotNull(offlineLicenseKeySetId);
+    DrmSession<T> session = openBlockingKeyRequest(DefaultDrmSessionManager.MODE_QUERY,
+        offlineLicenseKeySetId, null);
+    Pair<Long, Long> licenseDurationRemainingSec =
+        WidevineUtil.getLicenseDurationRemainingSec(drmSessionManager);
+    drmSessionManager.releaseSession(session);
+    return licenseDurationRemainingSec;
+  }
+
+  private void blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId,
+      DrmInitData drmInitData) throws DrmSessionException {
+    DrmSession<T> session = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId,
+        drmInitData);
+    DrmSessionException error = session.getError();
+    if (error != null) {
+      throw error;
+    }
+    drmSessionManager.releaseSession(session);
+  }
+
+  private DrmSession<T> openBlockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId,
+      DrmInitData drmInitData) {
+    drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId);
+    conditionVariable.close();
+    DrmSession<T> session = drmSessionManager.acquireSession(handlerThread.getLooper(),
+        drmInitData);
+    // Block current thread until key loading is finished
+    conditionVariable.block();
+    return session;
+  }
+
+  private static InitializationChunk loadInitializationChunk(final DataSource dataSource,
+      final Representation representation) throws IOException, InterruptedException {
+    RangedUri rangedUri = representation.getInitializationUri();
+    if (rangedUri == null) {
+      return null;
+    }
+    DataSpec dataSpec = new DataSpec(rangedUri.resolveUri(representation.baseUrl), rangedUri.start,
+        rangedUri.length, representation.getCacheKey());
+    InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec,
+        representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */,
+        newWrappedExtractor(representation.format));
+    initializationChunk.load();
+    return initializationChunk;
+  }
+
+  private static ChunkExtractorWrapper newWrappedExtractor(final Format format) {
+    final String mimeType = format.containerMimeType;
+    final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM)
+        || mimeType.startsWith(MimeTypes.AUDIO_WEBM);
+    final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor();
+    return new ChunkExtractorWrapper(extractor, format, false /* preferManifestDrmInitData */,
+        false /* resendFormatOnInit */);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/drm/UnsupportedDrmException.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 com.google.android.exoplayer2.drm;
+
+import android.support.annotation.IntDef;
+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.
+   */
+  @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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/drm/WidevineUtil.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 com.google.android.exoplayer2.drm;
+
+import android.util.Pair;
+import 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.
+   *
+   * @return A {@link Pair} consisting of the remaining license and playback durations in seconds.
+   * @throws IllegalStateException If called when a session isn't opened.
+   * @param drmSession
+   */
+  public static Pair<Long, Long> getLicenseDurationRemainingSec(DrmSession drmSession) {
+    Map<String, String> keyStatus = drmSession.queryKeyStatus();
+    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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ChunkIndex.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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * 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;
+    durationUs = durationsUs[length - 1] + timesUs[length - 1];
+  }
+
+  /**
+   * 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 long getPosition(long timeUs) {
+    return offsets[getChunkIndex(timeUs)];
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorInput.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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.util.Assertions;
+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 byte[] SCRATCH_SPACE = new byte[4096];
+
+  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[8 * 1024];
+  }
+
+  @Override
+  public int read(byte[] target, int offset, int length) throws IOException, InterruptedException {
+    int bytesRead = readFromPeekBuffer(target, offset, length);
+    if (bytesRead == 0) {
+      bytesRead = readFromDataSource(target, offset, length, 0, true);
+    }
+    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(SCRATCH_SPACE, 0, Math.min(length, SCRATCH_SPACE.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) {
+      bytesSkipped = readFromDataSource(SCRATCH_SPACE, -bytesSkipped,
+          Math.min(length, bytesSkipped + SCRATCH_SPACE.length), 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 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 = Math.min(peekBufferLength - peekBufferPosition, length);
+    while (bytesPeeked < length) {
+      bytesPeeked = readFromDataSource(peekBuffer, peekBufferPosition, length, bytesPeeked,
+          allowEndOfInput);
+      if (bytesPeeked == C.RESULT_END_OF_INPUT) {
+        return false;
+      }
+    }
+    peekBufferPosition += length;
+    peekBufferLength = Math.max(peekBufferLength, peekBufferPosition);
+    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) {
+      peekBuffer = Arrays.copyOf(peekBuffer, Math.max(peekBuffer.length * 2, requiredLength));
+    }
+  }
+
+  /**
+   * 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;
+    System.arraycopy(peekBuffer, bytesConsumed, peekBuffer, 0, peekBufferLength);
+  }
+
+  /**
+   * 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;
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.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 com.google.android.exoplayer2.extractor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An {@link ExtractorsFactory} that provides an array of extractors for the following formats:
+ *
+ * <ul>
+ * <li>MP4, including M4A ({@link com.google.android.exoplayer2.extractor.mp4.Mp4Extractor})</li>
+ * <li>fMP4 ({@link com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor})</li>
+ * <li>Matroska and WebM ({@link com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor})
+ * </li>
+ * <li>Ogg Vorbis/FLAC ({@link com.google.android.exoplayer2.extractor.ogg.OggExtractor}</li>
+ * <li>MP3 ({@link com.google.android.exoplayer2.extractor.mp3.Mp3Extractor})</li>
+ * <li>AAC ({@link com.google.android.exoplayer2.extractor.ts.AdtsExtractor})</li>
+ * <li>MPEG TS ({@link com.google.android.exoplayer2.extractor.ts.TsExtractor})</li>
+ * <li>MPEG PS ({@link com.google.android.exoplayer2.extractor.ts.PsExtractor})</li>
+ * <li>FLV ({@link com.google.android.exoplayer2.extractor.flv.FlvExtractor})</li>
+ * <li>WAV ({@link com.google.android.exoplayer2.extractor.wav.WavExtractor})</li>
+ * <li>FLAC (only available if the FLAC extension is built and included)</li>
+ * </ul>
+ */
+public final class DefaultExtractorsFactory implements ExtractorsFactory {
+
+  // Lazily initialized default extractor classes in priority order.
+  private static List<Class<? extends Extractor>> defaultExtractorClasses;
+
+  /**
+   * Creates a new factory for the default extractors.
+   */
+  public DefaultExtractorsFactory() {
+    synchronized (DefaultExtractorsFactory.class) {
+      if (defaultExtractorClasses == null) {
+        // Lazily initialize defaultExtractorClasses.
+        List<Class<? extends Extractor>> extractorClasses = new ArrayList<>();
+        // We reference extractors using reflection so that they can be deleted cleanly.
+        // Class.forName is used so that automated tools like proguard can detect the use of
+        // reflection (see http://proguard.sourceforge.net/FAQ.html#forname).
+        try {
+          extractorClasses.add(
+              Class.forName("com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor")
+                  .asSubclass(Extractor.class));
+        } catch (ClassNotFoundException e) {
+          // Extractor not found.
+        }
+        try {
+          extractorClasses.add(
+              Class.forName("com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor")
+                  .asSubclass(Extractor.class));
+        } catch (ClassNotFoundException e) {
+          // Extractor not found.
+        }
+        try {
+          extractorClasses.add(
+              Class.forName("com.google.android.exoplayer2.extractor.mp4.Mp4Extractor")
+                  .asSubclass(Extractor.class));
+        } catch (ClassNotFoundException e) {
+          // Extractor not found.
+        }
+        try {
+          extractorClasses.add(
+              Class.forName("com.google.android.exoplayer2.extractor.mp3.Mp3Extractor")
+                  .asSubclass(Extractor.class));
+        } catch (ClassNotFoundException e) {
+          // Extractor not found.
+        }
+        try {
+          extractorClasses.add(
+              Class.forName("com.google.android.exoplayer2.extractor.ts.AdtsExtractor")
+                  .asSubclass(Extractor.class));
+        } catch (ClassNotFoundException e) {
+          // Extractor not found.
+        }
+        try {
+          extractorClasses.add(
+              Class.forName("com.google.android.exoplayer2.extractor.ts.Ac3Extractor")
+                  .asSubclass(Extractor.class));
+        } catch (ClassNotFoundException e) {
+          // Extractor not found.
+        }
+        try {
+          extractorClasses.add(
+              Class.forName("com.google.android.exoplayer2.extractor.ts.TsExtractor")
+                  .asSubclass(Extractor.class));
+        } catch (ClassNotFoundException e) {
+          // Extractor not found.
+        }
+        try {
+          extractorClasses.add(
+              Class.forName("com.google.android.exoplayer2.extractor.flv.FlvExtractor")
+                  .asSubclass(Extractor.class));
+        } catch (ClassNotFoundException e) {
+          // Extractor not found.
+        }
+        try {
+          extractorClasses.add(
+              Class.forName("com.google.android.exoplayer2.extractor.ogg.OggExtractor")
+                  .asSubclass(Extractor.class));
+        } catch (ClassNotFoundException e) {
+          // Extractor not found.
+        }
+        try {
+          extractorClasses.add(
+              Class.forName("com.google.android.exoplayer2.extractor.ts.PsExtractor")
+                  .asSubclass(Extractor.class));
+        } catch (ClassNotFoundException e) {
+          // Extractor not found.
+        }
+        try {
+          extractorClasses.add(
+              Class.forName("com.google.android.exoplayer2.extractor.wav.WavExtractor")
+                  .asSubclass(Extractor.class));
+        } catch (ClassNotFoundException e) {
+          // Extractor not found.
+        }
+        try {
+          extractorClasses.add(
+              Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor")
+                  .asSubclass(Extractor.class));
+        } catch (ClassNotFoundException e) {
+          // Extractor not found.
+        }
+        defaultExtractorClasses = extractorClasses;
+      }
+    }
+  }
+
+  @Override
+  public Extractor[] createExtractors() {
+    Extractor[] extractors = new Extractor[defaultExtractorClasses.size()];
+    for (int i = 0; i < extractors.length; i++) {
+      try {
+        extractors[i] = defaultExtractorClasses.get(i).getConstructor().newInstance();
+      } catch (Exception e) {
+        // Should never happen.
+        throw new IllegalStateException("Unexpected error creating default extractor", e);
+      }
+    }
+    return extractors;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/DefaultTrackOutput.java
@@ -0,0 +1,960 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.upstream.Allocation;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A {@link TrackOutput} that buffers extracted samples in a queue and allows for consumption from
+ * that queue.
+ */
+public final class DefaultTrackOutput implements TrackOutput {
+
+  /**
+   * A listener for changes to the upstream format.
+   */
+  public interface UpstreamFormatChangedListener {
+
+    /**
+     * Called on the loading thread when an upstream format change occurs.
+     *
+     * @param format The new upstream format.
+     */
+    void onUpstreamFormatChanged(Format format);
+
+  }
+
+  private static final int INITIAL_SCRATCH_SIZE = 32;
+
+  private static final int STATE_ENABLED = 0;
+  private static final int STATE_ENABLED_WRITING = 1;
+  private static final int STATE_DISABLED = 2;
+
+  private final Allocator allocator;
+  private final int allocationLength;
+
+  private final InfoQueue infoQueue;
+  private final LinkedBlockingDeque<Allocation> dataQueue;
+  private final BufferExtrasHolder extrasHolder;
+  private final ParsableByteArray scratch;
+  private final AtomicInteger state;
+
+  // Accessed only by the consuming thread.
+  private long totalBytesDropped;
+  private Format downstreamFormat;
+
+  // Accessed only by the loading thread (or the consuming thread when there is no loading thread).
+  private long sampleOffsetUs;
+  private long totalBytesWritten;
+  private Allocation lastAllocation;
+  private int lastAllocationOffset;
+  private boolean needKeyframe;
+  private boolean pendingSplice;
+  private UpstreamFormatChangedListener upstreamFormatChangeListener;
+
+  /**
+   * @param allocator An {@link Allocator} from which allocations for sample data can be obtained.
+   */
+  public DefaultTrackOutput(Allocator allocator) {
+    this.allocator = allocator;
+    allocationLength = allocator.getIndividualAllocationLength();
+    infoQueue = new InfoQueue();
+    dataQueue = new LinkedBlockingDeque<>();
+    extrasHolder = new BufferExtrasHolder();
+    scratch = new ParsableByteArray(INITIAL_SCRATCH_SIZE);
+    state = new AtomicInteger();
+    lastAllocationOffset = allocationLength;
+    needKeyframe = true;
+  }
+
+  // Called by the consuming thread, but only when there is no loading thread.
+
+  /**
+   * Resets the output.
+   *
+   * @param enable Whether the output should be enabled. False if it should be disabled.
+   */
+  public void reset(boolean enable) {
+    int previousState = state.getAndSet(enable ? STATE_ENABLED : STATE_DISABLED);
+    clearSampleData();
+    infoQueue.resetLargestParsedTimestamps();
+    if (previousState == STATE_DISABLED) {
+      downstreamFormat = null;
+    }
+  }
+
+  /**
+   * Sets a source identifier for subsequent samples.
+   *
+   * @param sourceId The source identifier.
+   */
+  public void sourceId(int sourceId) {
+    infoQueue.sourceId(sourceId);
+  }
+
+  /**
+   * Indicates that samples subsequently queued to the buffer should be spliced into those already
+   * queued.
+   */
+  public void splice() {
+    pendingSplice = true;
+  }
+
+  /**
+   * Returns the current absolute write index.
+   */
+  public int getWriteIndex() {
+    return infoQueue.getWriteIndex();
+  }
+
+  /**
+   * Discards samples from the write side of the buffer.
+   *
+   * @param discardFromIndex The absolute index of the first sample to be discarded.
+   */
+  public void discardUpstreamSamples(int discardFromIndex) {
+    totalBytesWritten = infoQueue.discardUpstreamSamples(discardFromIndex);
+    dropUpstreamFrom(totalBytesWritten);
+  }
+
+  /**
+   * Discards data from the write side of the buffer. Data is discarded from the specified absolute
+   * position. Any allocations that are fully discarded are returned to the allocator.
+   *
+   * @param absolutePosition The absolute position (inclusive) from which to discard data.
+   */
+  private void dropUpstreamFrom(long absolutePosition) {
+    int relativePosition = (int) (absolutePosition - totalBytesDropped);
+    // Calculate the index of the allocation containing the position, and the offset within it.
+    int allocationIndex = relativePosition / allocationLength;
+    int allocationOffset = relativePosition % allocationLength;
+    // We want to discard any allocations after the one at allocationIdnex.
+    int allocationDiscardCount = dataQueue.size() - allocationIndex - 1;
+    if (allocationOffset == 0) {
+      // If the allocation at allocationIndex is empty, we should discard that one too.
+      allocationDiscardCount++;
+    }
+    // Discard the allocations.
+    for (int i = 0; i < allocationDiscardCount; i++) {
+      allocator.release(dataQueue.removeLast());
+    }
+    // Update lastAllocation and lastAllocationOffset to reflect the new position.
+    lastAllocation = dataQueue.peekLast();
+    lastAllocationOffset = allocationOffset == 0 ? allocationLength : allocationOffset;
+  }
+
+  // Called by the consuming thread.
+
+  /**
+   * Disables buffering of sample data and metadata.
+   */
+  public void disable() {
+    if (state.getAndSet(STATE_DISABLED) == STATE_ENABLED) {
+      clearSampleData();
+    }
+  }
+
+  /**
+   * Returns whether the buffer is empty.
+   */
+  public boolean isEmpty() {
+    return infoQueue.isEmpty();
+  }
+
+  /**
+   * Returns the current absolute read index.
+   */
+  public int getReadIndex() {
+    return infoQueue.getReadIndex();
+  }
+
+  /**
+   * Peeks the source id of the next sample, or the current upstream source id if the buffer is
+   * empty.
+   *
+   * @return The source id.
+   */
+  public int peekSourceId() {
+    return infoQueue.peekSourceId();
+  }
+
+  /**
+   * Returns the upstream {@link Format} in which samples are being queued.
+   */
+  public Format getUpstreamFormat() {
+    return infoQueue.getUpstreamFormat();
+  }
+
+  /**
+   * Returns the largest sample timestamp that has been queued since the last {@link #reset}.
+   * <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 long getLargestQueuedTimestampUs() {
+    return infoQueue.getLargestQueuedTimestampUs();
+  }
+
+  /**
+   * Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer
+   * contains a keyframe with a timestamp of {@code timeUs} or earlier, and if {@code timeUs} falls
+   * within the currently buffered media.
+   * <p>
+   * This method is equivalent to {@code skipToKeyframeBefore(timeUs, false)}.
+   *
+   * @param timeUs The seek time.
+   * @return Whether the skip was successful.
+   */
+  public boolean skipToKeyframeBefore(long timeUs) {
+    return skipToKeyframeBefore(timeUs, false);
+  }
+
+  /**
+   * Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer
+   * contains a keyframe with a timestamp of {@code timeUs} or earlier. If
+   * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs}
+   * falls within the buffer.
+   *
+   * @param timeUs The seek time.
+   * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
+   *     of the buffer.
+   * @return Whether the skip was successful.
+   */
+  public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
+    long nextOffset = infoQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer);
+    if (nextOffset == C.POSITION_UNSET) {
+      return false;
+    }
+    dropDownstreamTo(nextOffset);
+    return true;
+  }
+
+  /**
+   * Attempts to read from the queue.
+   *
+   * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
+   * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
+   *     end of the stream. If the end of the stream has been reached, the
+   *     {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.  May be null if the
+   *     caller requires that the format of the stream be read even if it's not changing.
+   * @param loadingFinished True if an empty queue should be considered the end of the stream.
+   * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will
+   *     be set if the buffer's timestamp is less than this value.
+   * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
+   *     {@link C#RESULT_BUFFER_READ}.
+   */
+  public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean loadingFinished,
+      long decodeOnlyUntilUs) {
+    switch (infoQueue.readData(formatHolder, buffer, downstreamFormat, extrasHolder)) {
+      case C.RESULT_NOTHING_READ:
+        if (loadingFinished) {
+          buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+          return C.RESULT_BUFFER_READ;
+        }
+        return C.RESULT_NOTHING_READ;
+      case C.RESULT_FORMAT_READ:
+        downstreamFormat = formatHolder.format;
+        return C.RESULT_FORMAT_READ;
+      case C.RESULT_BUFFER_READ:
+        if (buffer.timeUs < decodeOnlyUntilUs) {
+          buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
+        }
+        // Read encryption data if the sample is encrypted.
+        if (buffer.isEncrypted()) {
+          readEncryptionData(buffer, extrasHolder);
+        }
+        // Write the sample data into the holder.
+        buffer.ensureSpaceForWrite(extrasHolder.size);
+        readData(extrasHolder.offset, buffer.data, extrasHolder.size);
+        // Advance the read head.
+        dropDownstreamTo(extrasHolder.nextOffset);
+        return C.RESULT_BUFFER_READ;
+      default:
+        throw new IllegalStateException();
+    }
+  }
+
+  /**
+   * Reads encryption data for the current sample.
+   * <p>
+   * The encryption data is written into {@link DecoderInputBuffer#cryptoInfo}, and
+   * {@link BufferExtrasHolder#size} is adjusted to subtract the number of bytes that were read. The
+   * same value is added to {@link BufferExtrasHolder#offset}.
+   *
+   * @param buffer The buffer into which the encryption data should be written.
+   * @param extrasHolder The extras holder whose offset should be read and subsequently adjusted.
+   */
+  private void readEncryptionData(DecoderInputBuffer buffer, BufferExtrasHolder extrasHolder) {
+    long offset = extrasHolder.offset;
+
+    // Read the signal byte.
+    scratch.reset(1);
+    readData(offset, scratch.data, 1);
+    offset++;
+    byte signalByte = scratch.data[0];
+    boolean subsampleEncryption = (signalByte & 0x80) != 0;
+    int ivSize = signalByte & 0x7F;
+
+    // Read the initialization vector.
+    if (buffer.cryptoInfo.iv == null) {
+      buffer.cryptoInfo.iv = new byte[16];
+    }
+    readData(offset, buffer.cryptoInfo.iv, ivSize);
+    offset += ivSize;
+
+    // Read the subsample count, if present.
+    int subsampleCount;
+    if (subsampleEncryption) {
+      scratch.reset(2);
+      readData(offset, scratch.data, 2);
+      offset += 2;
+      subsampleCount = scratch.readUnsignedShort();
+    } else {
+      subsampleCount = 1;
+    }
+
+    // Write the clear and encrypted subsample sizes.
+    int[] clearDataSizes = buffer.cryptoInfo.numBytesOfClearData;
+    if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
+      clearDataSizes = new int[subsampleCount];
+    }
+    int[] encryptedDataSizes = buffer.cryptoInfo.numBytesOfEncryptedData;
+    if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
+      encryptedDataSizes = new int[subsampleCount];
+    }
+    if (subsampleEncryption) {
+      int subsampleDataLength = 6 * subsampleCount;
+      scratch.reset(subsampleDataLength);
+      readData(offset, scratch.data, subsampleDataLength);
+      offset += subsampleDataLength;
+      scratch.setPosition(0);
+      for (int i = 0; i < subsampleCount; i++) {
+        clearDataSizes[i] = scratch.readUnsignedShort();
+        encryptedDataSizes[i] = scratch.readUnsignedIntToInt();
+      }
+    } else {
+      clearDataSizes[0] = 0;
+      encryptedDataSizes[0] = extrasHolder.size - (int) (offset - extrasHolder.offset);
+    }
+
+    // Populate the cryptoInfo.
+    buffer.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes,
+        extrasHolder.encryptionKeyId, buffer.cryptoInfo.iv, C.CRYPTO_MODE_AES_CTR);
+
+    // Adjust the offset and size to take into account the bytes read.
+    int bytesRead = (int) (offset - extrasHolder.offset);
+    extrasHolder.offset += bytesRead;
+    extrasHolder.size -= bytesRead;
+  }
+
+  /**
+   * Reads data from the front of the rolling buffer.
+   *
+   * @param absolutePosition The absolute position from which data should be read.
+   * @param target The buffer into which data should be written.
+   * @param length The number of bytes to read.
+   */
+  private void readData(long absolutePosition, ByteBuffer target, int length) {
+    int remaining = length;
+    while (remaining > 0) {
+      dropDownstreamTo(absolutePosition);
+      int positionInAllocation = (int) (absolutePosition - totalBytesDropped);
+      int toCopy = Math.min(remaining, allocationLength - positionInAllocation);
+      Allocation allocation = dataQueue.peek();
+      target.put(allocation.data, allocation.translateOffset(positionInAllocation), toCopy);
+      absolutePosition += toCopy;
+      remaining -= toCopy;
+    }
+  }
+
+  /**
+   * Reads data from the front of the rolling buffer.
+   *
+   * @param absolutePosition The absolute position from which data should be read.
+   * @param target The array into which data should be written.
+   * @param length The number of bytes to read.
+   */
+  private void readData(long absolutePosition, byte[] target, int length) {
+    int bytesRead = 0;
+    while (bytesRead < length) {
+      dropDownstreamTo(absolutePosition);
+      int positionInAllocation = (int) (absolutePosition - totalBytesDropped);
+      int toCopy = Math.min(length - bytesRead, allocationLength - positionInAllocation);
+      Allocation allocation = dataQueue.peek();
+      System.arraycopy(allocation.data, allocation.translateOffset(positionInAllocation), target,
+          bytesRead, toCopy);
+      absolutePosition += toCopy;
+      bytesRead += toCopy;
+    }
+  }
+
+  /**
+   * Discard any allocations that hold data prior to the specified absolute position, returning
+   * them to the allocator.
+   *
+   * @param absolutePosition The absolute position up to which allocations can be discarded.
+   */
+  private void dropDownstreamTo(long absolutePosition) {
+    int relativePosition = (int) (absolutePosition - totalBytesDropped);
+    int allocationIndex = relativePosition / allocationLength;
+    for (int i = 0; i < allocationIndex; i++) {
+      allocator.release(dataQueue.remove());
+      totalBytesDropped += allocationLength;
+    }
+  }
+
+  // Called by the loading thread.
+
+  /**
+   * Sets a listener to be notified of changes to the upstream format.
+   *
+   * @param listener The listener.
+   */
+  public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) {
+    upstreamFormatChangeListener = listener;
+  }
+
+  /**
+   * Like {@link #format(Format)}, but with an offset that will be added to the timestamps of
+   * samples subsequently queued to the buffer. The offset is also used to adjust
+   * {@link Format#subsampleOffsetUs} for both the {@link Format} passed and those subsequently
+   * passed to {@link #format(Format)}.
+   *
+   * @param format The format.
+   * @param sampleOffsetUs The timestamp offset in microseconds.
+   */
+  public void formatWithOffset(Format format, long sampleOffsetUs) {
+    this.sampleOffsetUs = sampleOffsetUs;
+    format(format);
+  }
+
+  @Override
+  public void format(Format format) {
+    Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs);
+    boolean formatChanged = infoQueue.format(adjustedFormat);
+    if (upstreamFormatChangeListener != null && formatChanged) {
+      upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat);
+    }
+  }
+
+  @Override
+  public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+      throws IOException, InterruptedException {
+    if (!startWriteOperation()) {
+      int bytesSkipped = input.skip(length);
+      if (bytesSkipped == C.RESULT_END_OF_INPUT) {
+        if (allowEndOfInput) {
+          return C.RESULT_END_OF_INPUT;
+        }
+        throw new EOFException();
+      }
+      return bytesSkipped;
+    }
+    try {
+      length = prepareForAppend(length);
+      int bytesAppended = input.read(lastAllocation.data,
+          lastAllocation.translateOffset(lastAllocationOffset), length);
+      if (bytesAppended == C.RESULT_END_OF_INPUT) {
+        if (allowEndOfInput) {
+          return C.RESULT_END_OF_INPUT;
+        }
+        throw new EOFException();
+      }
+      lastAllocationOffset += bytesAppended;
+      totalBytesWritten += bytesAppended;
+      return bytesAppended;
+    } finally {
+      endWriteOperation();
+    }
+  }
+
+  @Override
+  public void sampleData(ParsableByteArray buffer, int length) {
+    if (!startWriteOperation()) {
+      buffer.skipBytes(length);
+      return;
+    }
+    while (length > 0) {
+      int thisAppendLength = prepareForAppend(length);
+      buffer.readBytes(lastAllocation.data, lastAllocation.translateOffset(lastAllocationOffset),
+          thisAppendLength);
+      lastAllocationOffset += thisAppendLength;
+      totalBytesWritten += thisAppendLength;
+      length -= thisAppendLength;
+    }
+    endWriteOperation();
+  }
+
+  @Override
+  public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
+      byte[] encryptionKey) {
+    if (!startWriteOperation()) {
+      infoQueue.commitSampleTimestamp(timeUs);
+      return;
+    }
+    try {
+      if (pendingSplice) {
+        if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0 || !infoQueue.attemptSplice(timeUs)) {
+          return;
+        }
+        pendingSplice = false;
+      }
+      if (needKeyframe) {
+        if ((flags & C.BUFFER_FLAG_KEY_FRAME) == 0) {
+          return;
+        }
+        needKeyframe = false;
+      }
+      timeUs += sampleOffsetUs;
+      long absoluteOffset = totalBytesWritten - size - offset;
+      infoQueue.commitSample(timeUs, flags, absoluteOffset, size, encryptionKey);
+    } finally {
+      endWriteOperation();
+    }
+  }
+
+  // Private methods.
+
+  private boolean startWriteOperation() {
+    return state.compareAndSet(STATE_ENABLED, STATE_ENABLED_WRITING);
+  }
+
+  private void endWriteOperation() {
+    if (!state.compareAndSet(STATE_ENABLED_WRITING, STATE_ENABLED)) {
+      clearSampleData();
+    }
+  }
+
+  private void clearSampleData() {
+    infoQueue.clearSampleData();
+    allocator.release(dataQueue.toArray(new Allocation[dataQueue.size()]));
+    dataQueue.clear();
+    allocator.trim();
+    totalBytesDropped = 0;
+    totalBytesWritten = 0;
+    lastAllocation = null;
+    lastAllocationOffset = allocationLength;
+    needKeyframe = true;
+  }
+
+  /**
+   * Prepares the rolling sample buffer for an append of up to {@code length} bytes, returning the
+   * number of bytes that can actually be appended.
+   */
+  private int prepareForAppend(int length) {
+    if (lastAllocationOffset == allocationLength) {
+      lastAllocationOffset = 0;
+      lastAllocation = allocator.allocate();
+      dataQueue.add(lastAllocation);
+    }
+    return Math.min(length, allocationLength - lastAllocationOffset);
+  }
+
+  /**
+   * Adjusts a {@link Format} to incorporate a sample offset into {@link Format#subsampleOffsetUs}.
+   *
+   * @param format The {@link Format} to adjust.
+   * @param sampleOffsetUs The offset to apply.
+   * @return The adjusted {@link Format}.
+   */
+  private static Format getAdjustedSampleFormat(Format format, long sampleOffsetUs) {
+    if (format == null) {
+      return null;
+    }
+    if (sampleOffsetUs != 0 && format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) {
+      format = format.copyWithSubsampleOffsetUs(format.subsampleOffsetUs + sampleOffsetUs);
+    }
+    return format;
+  }
+
+  /**
+   * Holds information about the samples in the rolling buffer.
+   */
+  private static final class InfoQueue {
+
+    private static final int SAMPLE_CAPACITY_INCREMENT = 1000;
+
+    private int capacity;
+
+    private int[] sourceIds;
+    private long[] offsets;
+    private int[] sizes;
+    private int[] flags;
+    private long[] timesUs;
+    private byte[][] encryptionKeys;
+    private Format[] formats;
+
+    private int queueSize;
+    private int absoluteReadIndex;
+    private int relativeReadIndex;
+    private int relativeWriteIndex;
+
+    private long largestDequeuedTimestampUs;
+    private long largestQueuedTimestampUs;
+    private boolean upstreamFormatRequired;
+    private Format upstreamFormat;
+    private int upstreamSourceId;
+
+    public InfoQueue() {
+      capacity = SAMPLE_CAPACITY_INCREMENT;
+      sourceIds = new int[capacity];
+      offsets = new long[capacity];
+      timesUs = new long[capacity];
+      flags = new int[capacity];
+      sizes = new int[capacity];
+      encryptionKeys = new byte[capacity][];
+      formats = new Format[capacity];
+      largestDequeuedTimestampUs = Long.MIN_VALUE;
+      largestQueuedTimestampUs = Long.MIN_VALUE;
+      upstreamFormatRequired = true;
+    }
+
+    public void clearSampleData() {
+      absoluteReadIndex = 0;
+      relativeReadIndex = 0;
+      relativeWriteIndex = 0;
+      queueSize = 0;
+    }
+
+    // Called by the consuming thread, but only when there is no loading thread.
+
+    public void resetLargestParsedTimestamps() {
+      largestDequeuedTimestampUs = Long.MIN_VALUE;
+      largestQueuedTimestampUs = Long.MIN_VALUE;
+    }
+
+    /**
+     * Returns the current absolute write index.
+     */
+    public int getWriteIndex() {
+      return absoluteReadIndex + queueSize;
+    }
+
+    /**
+     * Discards samples from the write side of the buffer.
+     *
+     * @param discardFromIndex The absolute index of the first sample to be discarded.
+     * @return The reduced total number of bytes written, after the samples have been discarded.
+     */
+    public long discardUpstreamSamples(int discardFromIndex) {
+      int discardCount = getWriteIndex() - discardFromIndex;
+      Assertions.checkArgument(0 <= discardCount && discardCount <= queueSize);
+
+      if (discardCount == 0) {
+        if (absoluteReadIndex == 0) {
+          // queueSize == absoluteReadIndex == 0, so nothing has been written to the queue.
+          return 0;
+        }
+        int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1;
+        return offsets[lastWriteIndex] + sizes[lastWriteIndex];
+      }
+
+      queueSize -= discardCount;
+      relativeWriteIndex = (relativeWriteIndex + capacity - discardCount) % capacity;
+      // Update the largest queued timestamp, assuming that the timestamps prior to a keyframe are
+      // always less than the timestamp of the keyframe itself, and of subsequent frames.
+      largestQueuedTimestampUs = Long.MIN_VALUE;
+      for (int i = queueSize - 1; i >= 0; i--) {
+        int sampleIndex = (relativeReadIndex + i) % capacity;
+        largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timesUs[sampleIndex]);
+        if ((flags[sampleIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+          break;
+        }
+      }
+      return offsets[relativeWriteIndex];
+    }
+
+    public void sourceId(int sourceId) {
+      upstreamSourceId = sourceId;
+    }
+
+    // Called by the consuming thread.
+
+    /**
+     * Returns the current absolute read index.
+     */
+    public int getReadIndex() {
+      return absoluteReadIndex;
+    }
+
+    /**
+     * Peeks the source id of the next sample, or the current upstream source id if the queue is
+     * empty.
+     */
+    public int peekSourceId() {
+      return queueSize == 0 ? upstreamSourceId : sourceIds[relativeReadIndex];
+    }
+
+    /**
+     * Returns whether the queue is empty.
+     */
+    public synchronized boolean isEmpty() {
+      return queueSize == 0;
+    }
+
+    /**
+     * Returns the upstream {@link Format} in which samples are being queued.
+     */
+    public synchronized Format getUpstreamFormat() {
+      return upstreamFormatRequired ? null : upstreamFormat;
+    }
+
+    /**
+     * Returns the largest sample timestamp that has been queued since the last {@link #reset}.
+     * <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 synchronized long getLargestQueuedTimestampUs() {
+      return Math.max(largestDequeuedTimestampUs, largestQueuedTimestampUs);
+    }
+
+    /**
+     * Attempts to read from the queue.
+     *
+     * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
+     * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
+     *     end of the stream. If a sample is read then the buffer is populated with information
+     *     about the sample, but not its data. The size and absolute position of the data in the
+     *     rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present
+     *     and the absolute position of the first byte that may still be required after the current
+     *     sample has been read. May be null if the caller requires that the format of the stream be
+     *     read even if it's not changing.
+     * @param downstreamFormat The current downstream {@link Format}. If the format of the next
+     *     sample is different to the current downstream format then a format will be read.
+     * @param extrasHolder The holder into which extra sample information should be written.
+     * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ}
+     *     or {@link C#RESULT_BUFFER_READ}.
+     */
+    public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
+        Format downstreamFormat, BufferExtrasHolder extrasHolder) {
+      if (queueSize == 0) {
+        if (upstreamFormat != null && (buffer == null || upstreamFormat != downstreamFormat)) {
+          formatHolder.format = upstreamFormat;
+          return C.RESULT_FORMAT_READ;
+        }
+        return C.RESULT_NOTHING_READ;
+      }
+
+      if (buffer == null || formats[relativeReadIndex] != downstreamFormat) {
+        formatHolder.format = formats[relativeReadIndex];
+        return C.RESULT_FORMAT_READ;
+      }
+
+      buffer.timeUs = timesUs[relativeReadIndex];
+      buffer.setFlags(flags[relativeReadIndex]);
+      extrasHolder.size = sizes[relativeReadIndex];
+      extrasHolder.offset = offsets[relativeReadIndex];
+      extrasHolder.encryptionKeyId = encryptionKeys[relativeReadIndex];
+
+      largestDequeuedTimestampUs = Math.max(largestDequeuedTimestampUs, buffer.timeUs);
+      queueSize--;
+      relativeReadIndex++;
+      absoluteReadIndex++;
+      if (relativeReadIndex == capacity) {
+        // Wrap around.
+        relativeReadIndex = 0;
+      }
+
+      extrasHolder.nextOffset = queueSize > 0 ? offsets[relativeReadIndex]
+          : extrasHolder.offset + extrasHolder.size;
+      return C.RESULT_BUFFER_READ;
+    }
+
+    /**
+     * Attempts to locate the keyframe before or at the specified time. If
+     * {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs}
+     * falls within the buffer.
+     *
+     * @param timeUs The seek time.
+     * @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
+     *     of the buffer.
+     * @return The offset of the keyframe's data if the keyframe was present.
+     *     {@link C#POSITION_UNSET} otherwise.
+     */
+    public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
+      if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) {
+        return C.POSITION_UNSET;
+      }
+
+      if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) {
+        return C.POSITION_UNSET;
+      }
+
+      // This could be optimized to use a binary search, however in practice callers to this method
+      // often pass times near to the start of the buffer. Hence it's unclear whether switching to
+      // a binary search would yield any real benefit.
+      int sampleCount = 0;
+      int sampleCountToKeyframe = -1;
+      int searchIndex = relativeReadIndex;
+      while (searchIndex != relativeWriteIndex) {
+        if (timesUs[searchIndex] > timeUs) {
+          // We've gone too far.
+          break;
+        } else if ((flags[searchIndex] & C.BUFFER_FLAG_KEY_FRAME) != 0) {
+          // We've found a keyframe, and we're still before the seek position.
+          sampleCountToKeyframe = sampleCount;
+        }
+        searchIndex = (searchIndex + 1) % capacity;
+        sampleCount++;
+      }
+
+      if (sampleCountToKeyframe == -1) {
+        return C.POSITION_UNSET;
+      }
+
+      queueSize -= sampleCountToKeyframe;
+      relativeReadIndex = (relativeReadIndex + sampleCountToKeyframe) % capacity;
+      absoluteReadIndex += sampleCountToKeyframe;
+      return offsets[relativeReadIndex];
+    }
+
+    // Called by the loading thread.
+
+    public synchronized boolean format(Format format) {
+      if (format == null) {
+        upstreamFormatRequired = true;
+        return false;
+      }
+      upstreamFormatRequired = false;
+      if (Util.areEqual(format, upstreamFormat)) {
+        // Suppress changes between equal formats so we can use referential equality in readData.
+        return false;
+      } else {
+        upstreamFormat = format;
+        return true;
+      }
+    }
+
+    public synchronized void commitSample(long timeUs, @C.BufferFlags int sampleFlags, long offset,
+        int size, byte[] encryptionKey) {
+      Assertions.checkState(!upstreamFormatRequired);
+      commitSampleTimestamp(timeUs);
+      timesUs[relativeWriteIndex] = timeUs;
+      offsets[relativeWriteIndex] = offset;
+      sizes[relativeWriteIndex] = size;
+      flags[relativeWriteIndex] = sampleFlags;
+      encryptionKeys[relativeWriteIndex] = encryptionKey;
+      formats[relativeWriteIndex] = upstreamFormat;
+      sourceIds[relativeWriteIndex] = upstreamSourceId;
+      // Increment the write index.
+      queueSize++;
+      if (queueSize == capacity) {
+        // Increase the capacity.
+        int newCapacity = capacity + SAMPLE_CAPACITY_INCREMENT;
+        int[] newSourceIds = new int[newCapacity];
+        long[] newOffsets = new long[newCapacity];
+        long[] newTimesUs = new long[newCapacity];
+        int[] newFlags = new int[newCapacity];
+        int[] newSizes = new int[newCapacity];
+        byte[][] newEncryptionKeys = new byte[newCapacity][];
+        Format[] newFormats = new Format[newCapacity];
+        int beforeWrap = capacity - relativeReadIndex;
+        System.arraycopy(offsets, relativeReadIndex, newOffsets, 0, beforeWrap);
+        System.arraycopy(timesUs, relativeReadIndex, newTimesUs, 0, beforeWrap);
+        System.arraycopy(flags, relativeReadIndex, newFlags, 0, beforeWrap);
+        System.arraycopy(sizes, relativeReadIndex, newSizes, 0, beforeWrap);
+        System.arraycopy(encryptionKeys, relativeReadIndex, newEncryptionKeys, 0, beforeWrap);
+        System.arraycopy(formats, relativeReadIndex, newFormats, 0, beforeWrap);
+        System.arraycopy(sourceIds, relativeReadIndex, newSourceIds, 0, beforeWrap);
+        int afterWrap = relativeReadIndex;
+        System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap);
+        System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap);
+        System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap);
+        System.arraycopy(sizes, 0, newSizes, beforeWrap, afterWrap);
+        System.arraycopy(encryptionKeys, 0, newEncryptionKeys, beforeWrap, afterWrap);
+        System.arraycopy(formats, 0, newFormats, beforeWrap, afterWrap);
+        System.arraycopy(sourceIds, 0, newSourceIds, beforeWrap, afterWrap);
+        offsets = newOffsets;
+        timesUs = newTimesUs;
+        flags = newFlags;
+        sizes = newSizes;
+        encryptionKeys = newEncryptionKeys;
+        formats = newFormats;
+        sourceIds = newSourceIds;
+        relativeReadIndex = 0;
+        relativeWriteIndex = capacity;
+        queueSize = capacity;
+        capacity = newCapacity;
+      } else {
+        relativeWriteIndex++;
+        if (relativeWriteIndex == capacity) {
+          // Wrap around.
+          relativeWriteIndex = 0;
+        }
+      }
+    }
+
+    public synchronized void commitSampleTimestamp(long timeUs) {
+      largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs);
+    }
+
+    /**
+     * Attempts to discard samples from the tail of the queue to allow samples starting from the
+     * specified timestamp to be spliced in.
+     *
+     * @param timeUs The timestamp at which the splice occurs.
+     * @return Whether the splice was successful.
+     */
+    public synchronized boolean attemptSplice(long timeUs) {
+      if (largestDequeuedTimestampUs >= timeUs) {
+        return false;
+      }
+      int retainCount = queueSize;
+      while (retainCount > 0
+          && timesUs[(relativeReadIndex + retainCount - 1) % capacity] >= timeUs) {
+        retainCount--;
+      }
+      discardUpstreamSamples(absoluteReadIndex + retainCount);
+      return true;
+    }
+
+  }
+
+  /**
+   * Holds additional buffer information not held by {@link DecoderInputBuffer}.
+   */
+  private static final class BufferExtrasHolder {
+
+    public int size;
+    public long offset;
+    public long nextOffset;
+    public byte[] encryptionKeyId;
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/DummyTrackOutput.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import 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,
+      byte[] encryptionKey) {
+    // Do nothing.
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/Extractor.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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+import java.io.IOException;
+
+/**
+ * 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;
+
+  /**
+   * 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}.
+   * <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.
+   *
+   * @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.
+   */
+  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();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ExtractorInput.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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Provides data to be consumed by an {@link Extractor}.
+ */
+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.
+   * <p>
+   * If the end of the input is found having read no data, then behavior is dependent on
+   * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned.
+   * Otherwise an {@link EOFException} is thrown.
+   * <p>
+   * Encountering the end of input having partially satisfied the read is always considered an
+   * error, and will result in an {@link EOFException} being thrown.
+   *
+   * @param target A target array into which data should be written.
+   * @param offset The offset into the target array at which to write.
+   * @param length The number of bytes to read from the input.
+   * @param allowEndOfInput True if encountering the end of the input having read no data is
+   *     allowed, and should result in {@code false} being returned. False if it should be
+   *     considered an error, causing an {@link EOFException} to be thrown.
+   * @return True if the read was successful. False if the end of the input was encountered having
+   *     read no data.
+   * @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 {@code 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.
+   * @return True if the skip was successful. False if 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 {@code length} bytes from the peek position, writing them into {@code target} at index
+   * {@code offset}. The current read position is left unchanged.
+   * <p>
+   * If the end of the input is found having peeked no data, then behavior is dependent on
+   * {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is returned.
+   * Otherwise an {@link EOFException} is thrown.
+   * <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 number of bytes to peek from the input.
+   * @param allowEndOfInput True if encountering the end of the input having peeked no data is
+   *     allowed, and should result in {@code false} being returned. False if it should be
+   *     considered an error, causing an {@link EOFException} to be thrown.
+   * @return True if the peek was successful. False if the end of the input was encountered having
+   *     peeked no data.
+   * @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;
+
+  /**
+   * Peeks {@code length} bytes from the peek position, writing them into {@code target} at index
+   * {@code offset}. The current read position is left unchanged.
+   * <p>
+   * Calling {@link #resetPeekPosition()} resets the peek position to equal the current read
+   * position, so the caller can peek the same data again. Reading and skipping also reset the peek
+   * position.
+   *
+   * @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.
+   * <p>
+   * If the end of the input is encountered before advancing the peek position, then behavior is
+   * dependent on {@code allowEndOfInput}. If {@code allowEndOfInput == true} then {@code false} is
+   * returned. Otherwise an {@link EOFException} is thrown.
+   *
+   * @param length The number of bytes by which to advance the peek position.
+   * @param allowEndOfInput True if encountering the end of the input before advancing is allowed,
+   *     and should result in {@code false} being returned. False if it should be considered an
+   *     error, causing an {@link EOFException} to be thrown.
+   * @return True if advancing the peek position was successful. False if the end of the input was
+   *     encountered before the peek position could be advanced.
+   * @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.
+   *
+   * @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;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ExtractorOutput.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 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 trackId}.
+   *
+   * @param trackId A track identifier.
+   * @return The {@link TrackOutput} for the given track identifier.
+   */
+  TrackOutput track(int trackId);
+
+  /**
+   * Called when all tracks have been identified, meaning no new {@code trackId} values will be
+   * passed to {@link #track(int)}.
+   */
+  void endTracks();
+
+  /**
+   * Called when a {@link SeekMap} has been extracted from the stream.
+   *
+   * @param seekMap The extracted {@link SeekMap}.
+   */
+  void seekMap(SeekMap seekMap);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ExtractorsFactory.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 com.google.android.exoplayer2.extractor;
+
+/**
+ * Factory for arrays of {@link Extractor}s.
+ */
+public interface ExtractorsFactory {
+
+  /**
+   * Returns an array of new {@link Extractor} instances.
+   */
+  Extractor[] createExtractors();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/GaplessInfoHolder.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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.id3.CommentFrame;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Holder for gapless playback information.
+ */
+public final class GaplessInfoHolder {
+
+  private static final String GAPLESS_COMMENT_ID = "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 (setFromComment(commentFrame.description, commentFrame.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 name The comment's identifier.
+   * @param data The comment's payload data.
+   * @return Whether the holder was populated.
+   */
+  private boolean setFromComment(String name, String data) {
+    if (!GAPLESS_COMMENT_ID.equals(name)) {
+      return false;
+    }
+    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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/MpegAudioHeader.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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+import 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 =
+      {32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448};
+  private static final int[] BITRATE_V2_L1 =
+      {32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256};
+  private static final int[] BITRATE_V1_L2 =
+      {32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384};
+  private static final int[] BITRATE_V1_L3 =
+      {32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320};
+  private static final int[] BITRATE_V2 =
+      {8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160};
+
+  /**
+   * Returns the size of the frame associated with {@code header}, or {@link C#LENGTH_UNSET} if it
+   * is invalid.
+   */
+  public static int getFrameSize(int header) {
+    if ((header & 0xFFE00000) != 0xFFE00000) {
+      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 (12000 * 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 144000 * bitrate / samplingRate + padding;
+    } else {
+      // Version 2 or 2.5
+      return (layer == 1 ? 72000 : 144000) * bitrate / samplingRate + padding;
+    }
+  }
+
+  /**
+   * 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 ((headerData & 0xFFE00000) != 0xFFE00000) {
+      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, frameSize, samplesPerFrame;
+    if (layer == 3) {
+      // Layer I (layer == 3)
+      bitrate = version == 3 ? BITRATE_V1_L1[bitrateIndex - 1] : BITRATE_V2_L1[bitrateIndex - 1];
+      frameSize = (12000 * bitrate / sampleRate + padding) * 4;
+      samplesPerFrame = 384;
+    } else {
+      // Layer II (layer == 2) or III (layer == 1)
+      if (version == 3) {
+        // Version 1
+        bitrate = layer == 2 ? BITRATE_V1_L2[bitrateIndex - 1] : BITRATE_V1_L3[bitrateIndex - 1];
+        samplesPerFrame = 1152;
+        frameSize = 144000 * bitrate / sampleRate + padding;
+      } else {
+        // Version 2 or 2.5.
+        bitrate = BITRATE_V2[bitrateIndex - 1];
+        samplesPerFrame = layer == 1 ? 576 : 1152;
+        frameSize = (layer == 1 ? 72000 : 144000) * bitrate / sampleRate + padding;
+      }
+    }
+
+    String mimeType = MIME_TYPE_BY_LAYER[3 - layer];
+    int channels = ((headerData >> 6) & 3) == 3 ? 1 : 2;
+    header.setValues(version, mimeType, frameSize, sampleRate, channels, bitrate * 1000,
+        samplesPerFrame);
+    return true;
+  }
+
+  /** MPEG audio header version. */
+  public int version;
+  /** The mime type. */
+  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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.extractor;
+
+/**
+ * Holds a position in the stream.
+ */
+public final class PositionHolder {
+
+  /**
+   * The held position.
+   */
+  public long position;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/SeekMap.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 com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+
+/**
+ * Maps seek positions (in microseconds) to corresponding positions (byte offsets) in the stream.
+ */
+public interface SeekMap {
+
+  /**
+   * A {@link SeekMap} that does not support seeking.
+   */
+  final class Unseekable implements SeekMap {
+
+    private final long durationUs;
+
+    /**
+     * @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 = durationUs;
+    }
+
+    @Override
+    public boolean isSeekable() {
+      return false;
+    }
+
+    @Override
+    public long getDurationUs() {
+      return durationUs;
+    }
+
+    @Override
+    public long getPosition(long timeUs) {
+      return 0;
+    }
+
+  }
+
+  /**
+   * Returns whether seeking is supported.
+   * <p>
+   * If seeking is not supported then the only valid seek position is the start of the file, and so
+   * {@link #getPosition(long)} will return 0 for all input values.
+   *
+   * @return Whether seeking is supported.
+   */
+  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();
+
+  /**
+   * Maps a seek position in microseconds to a corresponding position (byte offset) in the stream
+   * from which data can be provided to the extractor.
+   *
+   * @param timeUs A seek position in microseconds.
+   * @return The corresponding position (byte offset) in the stream from which data can be provided
+   *     to the extractor, or 0 if {@code #isSeekable()} returns false.
+   */
+  long getPosition(long timeUs);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/TrackOutput.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Receives track level data extracted by an {@link Extractor}.
+ */
+public interface TrackOutput {
+
+  /**
+   * 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.
+   */
+  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 encryptionKey The encryption key associated with the sample. May be null.
+   */
+  void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
+      byte[] encryptionKey);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.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 com.google.android.exoplayer2.extractor.flv;
+
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
+import com.google.android.exoplayer2.util.MimeTypes;
+import 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_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;
+
+  // 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;
+      // TODO: Add support for MP3.
+      if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) {
+        String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW
+            : MimeTypes.AUDIO_ULAW;
+        int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT;
+        Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE,
+            Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null);
+        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 void parsePayload(ParsableByteArray data, long timeUs) {
+    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;
+    } 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);
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/flv/FlvExtractor.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.flv;
+
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * Facilitates the extraction of data from the FLV container format.
+ */
+public final class FlvExtractor implements Extractor, SeekMap {
+
+  /**
+   * Factory for {@link FlvExtractor} instances.
+   */
+  public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+    @Override
+    public Extractor[] createExtractors() {
+      return new Extractor[] {new FlvExtractor()};
+    }
+
+  };
+
+  // Header sizes.
+  private static final int FLV_HEADER_SIZE = 9;
+  private static final int FLV_TAG_HEADER_SIZE = 11;
+
+  // Parser states.
+  private static final int STATE_READING_FLV_HEADER = 1;
+  private static final int STATE_SKIPPING_TO_TAG_HEADER = 2;
+  private static final int STATE_READING_TAG_HEADER = 3;
+  private static final int STATE_READING_TAG_DATA = 4;
+
+  // Tag types.
+  private static final int TAG_TYPE_AUDIO = 8;
+  private static final int TAG_TYPE_VIDEO = 9;
+  private static final int TAG_TYPE_SCRIPT_DATA = 18;
+
+  // FLV container identifier.
+  private static final int FLV_TAG = Util.getIntegerCodeForString("FLV");
+
+  // Temporary buffers.
+  private final ParsableByteArray scratch;
+  private final ParsableByteArray headerBuffer;
+  private final ParsableByteArray tagHeaderBuffer;
+  private final ParsableByteArray tagData;
+
+  // Extractor outputs.
+  private ExtractorOutput extractorOutput;
+
+  // State variables.
+  private int parserState;
+  private int bytesToNextTagHeader;
+  public int tagType;
+  public int tagDataSize;
+  public long tagTimestampUs;
+
+  // Tags readers.
+  private AudioTagPayloadReader audioReader;
+  private VideoTagPayloadReader videoReader;
+  private ScriptTagPayloadReader metadataReader;
+
+  public FlvExtractor() {
+    scratch = new ParsableByteArray(4);
+    headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE);
+    tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE);
+    tagData = new ParsableByteArray();
+    parserState = STATE_READING_FLV_HEADER;
+  }
+
+  @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) {
+    parserState = STATE_READING_FLV_HEADER;
+    bytesToNextTagHeader = 0;
+  }
+
+  @Override
+  public void release() {
+    // Do nothing
+  }
+
+  @Override
+  public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
+      InterruptedException {
+    while (true) {
+      switch (parserState) {
+        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;
+      }
+    }
+  }
+
+  /**
+   * 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));
+    }
+    if (hasVideo && videoReader == null) {
+      videoReader = new VideoTagPayloadReader(extractorOutput.track(TAG_TYPE_VIDEO));
+    }
+    if (metadataReader == null) {
+      metadataReader = new ScriptTagPayloadReader(null);
+    }
+    extractorOutput.endTracks();
+    extractorOutput.seekMap(this);
+
+    // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size.
+    bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4;
+    parserState = STATE_SKIPPING_TO_TAG_HEADER;
+    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;
+    parserState = 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
+    parserState = 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;
+    if (tagType == TAG_TYPE_AUDIO && audioReader != null) {
+      audioReader.consume(prepareTagData(input), tagTimestampUs);
+    } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) {
+      videoReader.consume(prepareTagData(input), tagTimestampUs);
+    } else if (tagType == TAG_TYPE_SCRIPT_DATA && metadataReader != null) {
+      metadataReader.consume(prepareTagData(input), tagTimestampUs);
+    } else {
+      input.skipFully(tagDataSize);
+      wasConsumed = false;
+    }
+    bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header.
+    parserState = 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;
+  }
+
+  // SeekMap implementation.
+
+  @Override
+  public boolean isSeekable() {
+    return false;
+  }
+
+  @Override
+  public long getDurationUs() {
+    return metadataReader.getDurationUs();
+  }
+
+  @Override
+  public long getPosition(long timeUs) {
+    return 0;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/flv/ScriptTagPayloadReader.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.flv;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import 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;
+
+  /**
+   * @param output A {@link TrackOutput} to which samples should be written.
+   */
+  public ScriptTagPayloadReader(TrackOutput output) {
+    super(output);
+    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 void 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;
+    }
+    int type = readAmfType(data);
+    if (type != AMF_TYPE_ECMA_ARRAY) {
+      // Should never happen.
+      throw new ParserException();
+    }
+    // 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);
+      }
+    }
+  }
+
+  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);
+      list.add(readAmfData(data, type));
+    }
+    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;
+      }
+      array.put(key, readAmfData(data, type));
+    }
+    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);
+      array.put(key, readAmfData(data, type));
+    }
+    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;
+  }
+
+  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:
+        return null;
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.extractor.flv;
+
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import 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.
+   * @throws ParserException If an error occurs parsing the data.
+   */
+  public final void consume(ParsableByteArray data, long timeUs) throws ParserException {
+    if (parseHeader(data)) {
+      parsePayload(data, timeUs);
+    }
+  }
+
+  /**
+   * 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
+   * @throws ParserException If an error occurs parsing the payload.
+   */
+  protected abstract void parsePayload(ParsableByteArray data, long timeUs) throws ParserException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/flv/VideoTagPayloadReader.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 com.google.android.exoplayer2.extractor.flv;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import 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 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() {
+    // Do nothing.
+  }
+
+  @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 void parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
+    int packetType = data.readUnsignedByte();
+    int compositionTimeMs = data.readUnsignedInt24();
+    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;
+    } else if (packetType == AVC_PACKET_TYPE_AVC_NALU) {
+      // 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, frameType == VIDEO_FRAME_KEYFRAME ? C.BUFFER_FLAG_KEY_FRAME : 0,
+          bytesWritten, 0, null);
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReader.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.mkv;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Stack;
+
+/**
+ * Default implementation of {@link EbmlReader}.
+ */
+/* package */ final class DefaultEbmlReader implements EbmlReader {
+
+  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 = new byte[8];
+  private final Stack<MasterElement> masterElementsStack = new Stack<>();
+  private final VarintReader varintReader = new VarintReader();
+
+  private EbmlReaderOutput output;
+  private int elementState;
+  private int elementId;
+  private long elementContentSize;
+
+  @Override
+  public void init(EbmlReaderOutput eventHandler) {
+    this.output = eventHandler;
+  }
+
+  @Override
+  public void reset() {
+    elementState = ELEMENT_STATE_READ_ID;
+    masterElementsStack.clear();
+    varintReader.reset();
+  }
+
+  @Override
+  public boolean read(ExtractorInput input) throws IOException, InterruptedException {
+    Assertions.checkState(output != null);
+    while (true) {
+      if (!masterElementsStack.isEmpty()
+          && input.getPosition() >= masterElementsStack.peek().elementEndPosition) {
+        output.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;
+      }
+
+      int type = output.getElementType(elementId);
+      switch (type) {
+        case TYPE_MASTER:
+          long elementContentPosition = input.getPosition();
+          long elementEndPosition = elementContentPosition + elementContentSize;
+          masterElementsStack.add(new MasterElement(elementId, elementEndPosition));
+          output.startMasterElement(elementId, elementContentPosition, elementContentSize);
+          elementState = ELEMENT_STATE_READ_ID;
+          return true;
+        case TYPE_UNSIGNED_INT:
+          if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
+            throw new ParserException("Invalid integer size: " + elementContentSize);
+          }
+          output.integerElement(elementId, readInteger(input, (int) elementContentSize));
+          elementState = ELEMENT_STATE_READ_ID;
+          return true;
+        case TYPE_FLOAT:
+          if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
+              && elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
+            throw new ParserException("Invalid float size: " + elementContentSize);
+          }
+          output.floatElement(elementId, readFloat(input, (int) elementContentSize));
+          elementState = ELEMENT_STATE_READ_ID;
+          return true;
+        case TYPE_STRING:
+          if (elementContentSize > Integer.MAX_VALUE) {
+            throw new ParserException("String element size: " + elementContentSize);
+          }
+          output.stringElement(elementId, readString(input, (int) elementContentSize));
+          elementState = ELEMENT_STATE_READ_ID;
+          return true;
+        case TYPE_BINARY:
+          output.binaryElement(elementId, (int) elementContentSize, input);
+          elementState = ELEMENT_STATE_READ_ID;
+          return true;
+        case 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 (output.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 and returns a string 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 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);
+    return new String(stringBytes);
+  }
+
+  /**
+   * Used in {@link #masterElementsStack} to track when the current master element ends, so that
+   * {@link EbmlReaderOutput#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;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReader.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.mkv;
+
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import java.io.IOException;
+
+/**
+ * Event-driven EBML reader that delivers events to an {@link EbmlReaderOutput}.
+ * <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 {
+
+  /**
+   * Type for unknown elements.
+   */
+  int TYPE_UNKNOWN = 0;
+  /**
+   * Type for elements that contain child elements.
+   */
+  int TYPE_MASTER = 1;
+  /**
+   * Type for integer value elements of up to 8 bytes.
+   */
+  int TYPE_UNSIGNED_INT = 2;
+  /**
+   * Type for string elements.
+   */
+  int TYPE_STRING = 3;
+  /**
+   * Type for binary elements.
+   */
+  int TYPE_BINARY = 4;
+  /**
+   * Type for IEEE floating point value elements of either 4 or 8 bytes.
+   */
+  int TYPE_FLOAT = 5;
+
+  /**
+   * Initializes the extractor with an {@link EbmlReaderOutput}.
+   *
+   * @param output An {@link EbmlReaderOutput} to receive events.
+   */
+  void init(EbmlReaderOutput output);
+
+  /**
+   * 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;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mkv/EbmlReaderOutput.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 com.google.android.exoplayer2.extractor.mkv;
+
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import java.io.IOException;
+
+/**
+ * Defines EBML element IDs/types and reacts to events.
+ */
+/* package */ interface EbmlReaderOutput {
+
+  /**
+   * Maps an element ID to a corresponding type.
+   * <p>
+   * If {@link EbmlReader#TYPE_UNKNOWN} is returned then the element is skipped. Note that all
+   * children of a skipped element are also skipped.
+   *
+   * @param id The element ID to map.
+   * @return One of the {@code TYPE_} constants defined in {@link EbmlReader}.
+   */
+  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;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
@@ -0,0 +1,1615 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.mkv;
+
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import com.google.android.exoplayer2.extractor.ChunkIndex;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.LongArray;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.AvcConfig;
+import com.google.android.exoplayer2.video.HevcConfig;
+import java.io.IOException;
+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 a Matroska or WebM file.
+ */
+public final class MatroskaExtractor implements Extractor {
+
+  /**
+   * Factory for {@link MatroskaExtractor} instances.
+   */
+  public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+    @Override
+    public Extractor[] createExtractors() {
+      return new Extractor[] {new 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_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_VOBSUB = "S_VOBSUB";
+  private static final String CODEC_ID_PGS = "S_HDMV/PGS";
+
+  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_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_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_PRIVATE = 0x7672;
+  private static final int ID_STEREO_MODE = 0x53B8;
+
+  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_VC1 = 0x31435657;
+
+  /**
+   * A template for the prefix that must be added to each subrip sample. The 12 byte end timecode
+   * starting at {@link #SUBRIP_PREFIX_END_TIMECODE_OFFSET} is set to a dummy value, and must be
+   * replaced with the duration of the subtitle.
+   * <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};
+  /**
+   * A special end timecode indicating that a subtitle should be displayed until the next subtitle,
+   * or until the end of the media in the case of the last subtitle.
+   * <p>
+   * Equivalent to the UTF-8 string: "            ".
+   */
+  private static final byte[] SUBRIP_TIMECODE_EMPTY =
+      new byte[] {32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32};
+  /**
+   * The byte offset of the end timecode in {@link #SUBRIP_PREFIX}.
+   */
+  private static final int SUBRIP_PREFIX_END_TIMECODE_OFFSET = 19;
+  /**
+   * The length in bytes of a timecode in a subrip prefix.
+   */
+  private static final int SUBRIP_TIMECODE_LENGTH = 12;
+
+  /**
+   * 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;
+
+  // 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 subripSample;
+  private final ParsableByteArray encryptionInitializationVector;
+  private final ParsableByteArray encryptionSubsampleData;
+  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;
+
+  // Block reading state.
+  private int blockState;
+  private long blockTimeUs;
+  private long blockDurationUs;
+  private int blockLacingSampleIndex;
+  private int blockLacingSampleCount;
+  private int[] blockLacingSampleSizes;
+  private int blockTrackNumber;
+  private int blockTrackNumberLength;
+  @C.BufferFlags
+  private int blockFlags;
+
+  // Sample reading state.
+  private int sampleBytesRead;
+  private boolean sampleEncodingHandled;
+  private boolean sampleSignalByteRead;
+  private boolean sampleInitializationVectorRead;
+  private boolean samplePartitionCountRead;
+  private byte sampleSignalByte;
+  private int samplePartitionCount;
+  private int sampleCurrentNalBytesRemaining;
+  private int sampleBytesWritten;
+  private boolean sampleRead;
+  private boolean sampleSeenReferenceBlock;
+
+  // Extractor outputs.
+  private ExtractorOutput extractorOutput;
+
+  public MatroskaExtractor() {
+    this(new DefaultEbmlReader());
+  }
+
+  /* package */ MatroskaExtractor(EbmlReader reader) {
+    this.reader = reader;
+    this.reader.init(new InnerEbmlReaderOutput());
+    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();
+    subripSample = new ParsableByteArray();
+    encryptionInitializationVector = new ParsableByteArray(ENCRYPTION_IV_SIZE);
+    encryptionSubsampleData = new ParsableByteArray();
+  }
+
+  @Override
+  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+    return new Sniffer().sniff(input);
+  }
+
+  @Override
+  public void init(ExtractorOutput output) {
+    extractorOutput = output;
+  }
+
+  @Override
+  public void seek(long position, long timeUs) {
+    clusterTimecodeUs = C.TIME_UNSET;
+    blockState = BLOCK_STATE_START;
+    reader.reset();
+    varintReader.reset();
+    resetSample();
+  }
+
+  @Override
+  public void release() {
+    // Do nothing
+  }
+
+  @Override
+  public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException,
+      InterruptedException {
+    sampleRead = false;
+    boolean continueReading = true;
+    while (continueReading && !sampleRead) {
+      continueReading = reader.read(input);
+      if (continueReading && maybeSeekForCues(seekPosition, input.getPosition())) {
+        return Extractor.RESULT_SEEK;
+      }
+    }
+    return continueReading ? Extractor.RESULT_CONTINUE : Extractor.RESULT_END_OF_INPUT;
+  }
+
+  /* package */ 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_PROJECTION:
+        return EbmlReader.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_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:
+        return EbmlReader.TYPE_UNSIGNED_INT;
+      case ID_DOC_TYPE:
+      case ID_CODEC_ID:
+      case ID_LANGUAGE:
+        return EbmlReader.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:
+        return EbmlReader.TYPE_BINARY;
+      case ID_DURATION:
+      case ID_SAMPLING_FREQUENCY:
+        return EbmlReader.TYPE_FLOAT;
+      default:
+        return EbmlReader.TYPE_UNKNOWN;
+    }
+  }
+
+  /* package */ boolean isLevel1Element(int id) {
+    return id == ID_SEGMENT_INFO || id == ID_CLUSTER || id == ID_CUES || id == ID_TRACKS;
+  }
+
+  /* package */ void startMasterElement(int id, long contentPosition, long contentSize)
+      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 (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:
+        sampleSeenReferenceBlock = 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;
+      default:
+        break;
+    }
+  }
+
+  /* package */ 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;
+        }
+        // If the ReferenceBlock element was not found for this sample, then it is a keyframe.
+        if (!sampleSeenReferenceBlock) {
+          blockFlags |= C.BUFFER_FLAG_KEY_FRAME;
+        }
+        commitSampleToOutput(tracks.get(blockTrackNumber), blockTimeUs);
+        blockState = BLOCK_STATE_START;
+        break;
+      case ID_CONTENT_ENCODING:
+        if (currentTrack.hasContentEncryption) {
+          if (currentTrack.encryptionKeyId == null) {
+            throw new ParserException("Encrypted Track found but ContentEncKeyID was not found");
+          }
+          currentTrack.drmInitData = new DrmInitData(
+              new SchemeData(C.UUID_NIL, MimeTypes.VIDEO_WEBM, currentTrack.encryptionKeyId));
+        }
+        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;
+    }
+  }
+
+  /* package */ 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.flagForced = value == 1;
+        break;
+      case ID_FLAG_FORCED:
+        currentTrack.flagDefault = value == 1;
+        break;
+      case ID_TRACK_TYPE:
+        currentTrack.type = (int) value;
+        break;
+      case ID_DEFAULT_DURATION:
+        currentTrack.defaultSampleDurationNs = (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:
+        sampleSeenReferenceBlock = 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;
+          default:
+            break;
+        }
+        break;
+      default:
+        break;
+    }
+  }
+
+  /* package */ void floatElement(int id, double value) {
+    switch (id) {
+      case ID_DURATION:
+        durationTimecode = (long) value;
+        break;
+      case ID_SAMPLING_FREQUENCY:
+        currentTrack.sampleRate = (int) value;
+        break;
+      default:
+        break;
+    }
+  }
+
+  /* package */ 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_CODEC_ID:
+        currentTrack.codecId = value;
+        break;
+      case ID_LANGUAGE:
+        currentTrack.language = value;
+        break;
+      default:
+        break;
+    }
+  }
+
+  /* package */ 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:
+        currentTrack.encryptionKeyId = new byte[contentSize];
+        input.readFully(currentTrack.encryptionKeyId, 0, contentSize);
+        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) {
+            blockLacingSampleCount = 1;
+            blockLacingSampleSizes = ensureArrayCapacity(blockLacingSampleSizes, 1);
+            blockLacingSampleSizes[0] = contentSize - blockTrackNumberLength - 3;
+          } else {
+            if (id != ID_SIMPLE_BLOCK) {
+              throw new ParserException("Lacing only supported in SimpleBlocks.");
+            }
+
+            // Read the sample count (1 byte).
+            readScratch(input, 4);
+            blockLacingSampleCount = (scratch.data[3] & 0xFF) + 1;
+            blockLacingSampleSizes =
+                ensureArrayCapacity(blockLacingSampleSizes, blockLacingSampleCount);
+            if (lacing == LACING_FIXED_SIZE) {
+              int blockLacingSampleSize =
+                  (contentSize - blockTrackNumberLength - 4) / blockLacingSampleCount;
+              Arrays.fill(blockLacingSampleSizes, 0, blockLacingSampleCount, blockLacingSampleSize);
+            } else if (lacing == LACING_XIPH) {
+              int totalSamplesSize = 0;
+              int headerSize = 4;
+              for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) {
+                blockLacingSampleSizes[sampleIndex] = 0;
+                int byteValue;
+                do {
+                  readScratch(input, ++headerSize);
+                  byteValue = scratch.data[headerSize - 1] & 0xFF;
+                  blockLacingSampleSizes[sampleIndex] += byteValue;
+                } while (byteValue == 0xFF);
+                totalSamplesSize += blockLacingSampleSizes[sampleIndex];
+              }
+              blockLacingSampleSizes[blockLacingSampleCount - 1] =
+                  contentSize - blockTrackNumberLength - headerSize - totalSamplesSize;
+            } else if (lacing == LACING_EBML) {
+              int totalSamplesSize = 0;
+              int headerSize = 4;
+              for (int sampleIndex = 0; sampleIndex < blockLacingSampleCount - 1; sampleIndex++) {
+                blockLacingSampleSizes[sampleIndex] = 0;
+                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;
+                blockLacingSampleSizes[sampleIndex] = sampleIndex == 0
+                    ? intReadValue : blockLacingSampleSizes[sampleIndex - 1] + intReadValue;
+                totalSamplesSize += blockLacingSampleSizes[sampleIndex];
+              }
+              blockLacingSampleSizes[blockLacingSampleCount - 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;
+          blockLacingSampleIndex = 0;
+        }
+
+        if (id == ID_SIMPLE_BLOCK) {
+          // For SimpleBlock, we have metadata for each sample here.
+          while (blockLacingSampleIndex < blockLacingSampleCount) {
+            writeSampleData(input, track, blockLacingSampleSizes[blockLacingSampleIndex]);
+            long sampleTimeUs = this.blockTimeUs
+                + (blockLacingSampleIndex * track.defaultSampleDurationNs) / 1000;
+            commitSampleToOutput(track, sampleTimeUs);
+            blockLacingSampleIndex++;
+          }
+          blockState = BLOCK_STATE_START;
+        } else {
+          // For Block, we send the metadata at the end of the BlockGroup element since we'll know
+          // if the sample is a keyframe or not only at that point.
+          writeSampleData(input, track, blockLacingSampleSizes[0]);
+        }
+
+        break;
+      default:
+        throw new ParserException("Unexpected id: " + id);
+    }
+  }
+
+  private void commitSampleToOutput(Track track, long timeUs) {
+    if (CODEC_ID_SUBRIP.equals(track.codecId)) {
+      writeSubripSample(track);
+    }
+    track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.encryptionKeyId);
+    sampleRead = true;
+    resetSample();
+  }
+
+  private void resetSample() {
+    sampleBytesRead = 0;
+    sampleBytesWritten = 0;
+    sampleCurrentNalBytesRemaining = 0;
+    sampleEncodingHandled = false;
+    sampleSignalByteRead = false;
+    samplePartitionCountRead = false;
+    samplePartitionCount = 0;
+    sampleSignalByte = (byte) 0;
+    sampleInitializationVectorRead = false;
+    sampleStrippedBytes.reset();
+  }
+
+  /**
+   * 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);
+  }
+
+  private void writeSampleData(ExtractorInput input, Track track, int size)
+      throws IOException, InterruptedException {
+    if (CODEC_ID_SUBRIP.equals(track.codecId)) {
+      int sizeWithPrefix = SUBRIP_PREFIX.length + size;
+      if (subripSample.capacity() < sizeWithPrefix) {
+        // Initialize subripSample to contain the required prefix and have space to hold a subtitle
+        // twice as long as this one.
+        subripSample.data = Arrays.copyOf(SUBRIP_PREFIX, sizeWithPrefix + size);
+      }
+      input.readFully(subripSample.data, SUBRIP_PREFIX.length, size);
+      subripSample.setPosition(0);
+      subripSample.setLimit(sizeWithPrefix);
+      // Defer writing the data to the track output. We need to modify the sample data by setting
+      // the correct end timecode, which we might not have yet.
+      return;
+    }
+
+    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);
+      }
+      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.
+          readToTarget(input, nalLengthData, nalUnitLengthFieldLengthDiff,
+              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.
+          sampleCurrentNalBytesRemaining -=
+              readToOutput(input, output, sampleCurrentNalBytesRemaining);
+        }
+      }
+    } else {
+      while (sampleBytesRead < size) {
+        readToOutput(input, output, size - sampleBytesRead);
+      }
+    }
+
+    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;
+    }
+  }
+
+  private void writeSubripSample(Track track) {
+    setSubripSampleEndTimecode(subripSample.data, blockDurationUs);
+    // Note: If we ever want to support DRM protected subtitles then we'll need to output the
+    // appropriate encryption data here.
+    track.output.sampleData(subripSample, subripSample.limit());
+    sampleBytesWritten += subripSample.limit();
+  }
+
+  private static void setSubripSampleEndTimecode(byte[] subripSampleData, long timeUs) {
+    byte[] timeCodeData;
+    if (timeUs == C.TIME_UNSET) {
+      timeCodeData = SUBRIP_TIMECODE_EMPTY;
+    } else {
+      int hours = (int) (timeUs / 3600000000L);
+      timeUs -= (hours * 3600000000L);
+      int minutes = (int) (timeUs / 60000000);
+      timeUs -= (minutes * 60000000);
+      int seconds = (int) (timeUs / 1000000);
+      timeUs -= (seconds * 1000000);
+      int milliseconds = (int) (timeUs / 1000);
+      timeCodeData = Util.getUtf8Bytes(String.format(Locale.US, "%02d:%02d:%02d,%03d", hours,
+          minutes, seconds, milliseconds));
+    }
+    System.arraycopy(timeCodeData, 0, subripSampleData, SUBRIP_PREFIX_END_TIMECODE_OFFSET,
+        SUBRIP_TIMECODE_LENGTH);
+  }
+
+  /**
+   * Writes {@code length} bytes of sample data into {@code target} at {@code offset}, consisting of
+   * pending {@link #sampleStrippedBytes} and any remaining data read from {@code input}.
+   */
+  private void readToTarget(ExtractorInput input, byte[] target, int offset, int length)
+      throws IOException, InterruptedException {
+    int pendingStrippedBytes = Math.min(length, sampleStrippedBytes.bytesLeft());
+    input.readFully(target, offset + pendingStrippedBytes, length - pendingStrippedBytes);
+    if (pendingStrippedBytes > 0) {
+      sampleStrippedBytes.readBytes(target, offset, pendingStrippedBytes);
+    }
+    sampleBytesRead += length;
+  }
+
+  /**
+   * Outputs up to {@code length} bytes of sample data to {@code output}, consisting of either
+   * {@link #sampleStrippedBytes} or data read from {@code input}.
+   */
+  private int readToOutput(ExtractorInput input, TrackOutput output, int length)
+      throws IOException, InterruptedException {
+    int bytesRead;
+    int strippedBytesLeft = sampleStrippedBytes.bytesLeft();
+    if (strippedBytesLeft > 0) {
+      bytesRead = Math.min(length, strippedBytesLeft);
+      output.sampleData(sampleStrippedBytes, bytesRead);
+    } else {
+      bytesRead = output.sampleData(input, length, false);
+    }
+    sampleBytesRead += bytesRead;
+    sampleBytesWritten += bytesRead;
+    return bytesRead;
+  }
+
+  /**
+   * 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];
+    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_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_VOBSUB.equals(codecId)
+        || CODEC_ID_PGS.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 InnerEbmlReaderOutput implements EbmlReaderOutput {
+
+    @Override
+    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);
+    }
+
+  }
+
+  private static final class Track {
+
+    private static final int DISPLAY_UNIT_PIXELS = 0;
+
+    // Common elements.
+    public String codecId;
+    public int number;
+    public int type;
+    public int defaultSampleDurationNs;
+    public boolean hasContentEncryption;
+    public byte[] sampleStrippedBytes;
+    public byte[] encryptionKeyId;
+    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;
+    public byte[] projectionData = null;
+    @C.StereoMode
+    public int stereoMode = 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;
+
+    // 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_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:
+          initializationData = parseFourCcVc1Private(new ParsableByteArray(codecPrivate));
+          mimeType = initializationData == null ? MimeTypes.VIDEO_UNKNOWN : MimeTypes.VIDEO_VC1;
+          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.nativeOrder()).putLong(codecDelayNs).array());
+          initializationData.add(
+              ByteBuffer.allocate(8).order(ByteOrder.nativeOrder()).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;
+          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))) {
+            throw new ParserException("Non-PCM MS/ACM is unsupported");
+          }
+          pcmEncoding = Util.getPcmEncoding(audioBitDepth);
+          if (pcmEncoding == C.ENCODING_INVALID) {
+            throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth);
+          }
+          break;
+        case CODEC_ID_PCM_INT_LIT:
+          mimeType = MimeTypes.AUDIO_RAW;
+          pcmEncoding = Util.getPcmEncoding(audioBitDepth);
+          if (pcmEncoding == C.ENCODING_INVALID) {
+            throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth);
+          }
+          break;
+        case CODEC_ID_SUBRIP:
+          mimeType = MimeTypes.APPLICATION_SUBRIP;
+          break;
+        case CODEC_ID_VOBSUB:
+          mimeType = MimeTypes.APPLICATION_VOBSUB;
+          initializationData = Collections.singletonList(codecPrivate);
+          break;
+        case CODEC_ID_PGS:
+          mimeType = MimeTypes.APPLICATION_PGS;
+          break;
+        default:
+          throw new ParserException("Unrecognized codec identifier.");
+      }
+
+      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)) {
+        format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
+            Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding,
+            initializationData, drmInitData, selectionFlags, language);
+      } else if (MimeTypes.isVideo(mimeType)) {
+        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);
+        }
+        format = Format.createVideoSampleFormat(Integer.toString(trackId), mimeType, null,
+            Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData,
+            Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, drmInitData);
+      } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) {
+        format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
+            Format.NO_VALUE, selectionFlags, language, drmInitData);
+      } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType)
+          || MimeTypes.APPLICATION_PGS.equals(mimeType)) {
+        format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null,
+            Format.NO_VALUE, initializationData, language, drmInitData);
+      } else {
+        throw new ParserException("Unexpected MIME type.");
+      }
+
+      this.output = output.track(number);
+      this.output.format(format);
+    }
+
+    /**
+     * Builds initialization data for a {@link Format} from FourCC codec private data.
+     * <p>
+     * VC1 is the only supported compression type.
+     *
+     * @return The initialization data for the {@link Format}, or null if the compression type is
+     *     not VC1.
+     * @throws ParserException If the initialization data could not be built.
+     */
+    private static List<byte[]> parseFourCcVc1Private(ParsableByteArray buffer)
+        throws ParserException {
+      try {
+        buffer.skipBytes(16); // size(4), width(4), height(4), planes(2), bitcount(2).
+        long compression = buffer.readLittleEndianUnsignedInt();
+        if (compression != FOURCC_COMPRESSION_VC1) {
+          return null;
+        }
+
+        // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20
+        // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4).
+        int startOffset = buffer.getPosition() + 20;
+        byte[] bufferData = buffer.data;
+        for (int offset = startOffset; offset < bufferData.length - 4; offset++) {
+          if (bufferData[offset] == 0x00 && bufferData[offset + 1] == 0x00
+              && bufferData[offset + 2] == 0x01 && bufferData[offset + 3] == 0x0F) {
+            // We've found the initialization data.
+            byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length);
+            return Collections.singletonList(initializationData);
+          }
+        }
+
+        throw new ParserException("Failed to find FourCC VC1 initialization data");
+      } catch (ArrayIndexOutOfBoundsException e) {
+        throw new ParserException("Error parsing FourCC VC1 codec private");
+      }
+    }
+
+    /**
+     * 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");
+      }
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mkv/Sniffer.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 com.google.android.exoplayer2.extractor.mkv;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import 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) {
+        input.advancePeekPosition((int) size);
+        peekLength += size;
+      }
+    }
+    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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.extractor.mkv;
+
+import com.google.android.exoplayer2.C;
+import 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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.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 com.google.android.exoplayer2.extractor.mp3;
+
+import com.google.android.exoplayer2.C;
+
+/**
+ * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate.
+ */
+/* package */ final class ConstantBitrateSeeker implements Mp3Extractor.Seeker {
+
+  private static final int BITS_PER_BYTE = 8;
+
+  private final long firstFramePosition;
+  private final int bitrate;
+  private final long durationUs;
+
+  public ConstantBitrateSeeker(long firstFramePosition, int bitrate, long inputLength) {
+    this.firstFramePosition = firstFramePosition;
+    this.bitrate = bitrate;
+    durationUs = inputLength == C.LENGTH_UNSET ? C.TIME_UNSET : getTimeUs(inputLength);
+  }
+
+  @Override
+  public boolean isSeekable() {
+    return durationUs != C.TIME_UNSET;
+  }
+
+  @Override
+  public long getPosition(long timeUs) {
+    return durationUs == C.TIME_UNSET ? 0
+        : firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE);
+  }
+
+  @Override
+  public long getTimeUs(long position) {
+    return (Math.max(0, position - firstFramePosition) * C.MICROS_PER_SECOND * BITS_PER_BYTE)
+        / bitrate;
+  }
+
+  @Override
+  public long getDurationUs() {
+    return durationUs;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.mp3;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
+import com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Extracts data from an MP3 file.
+ */
+public final class Mp3Extractor implements Extractor {
+
+  /**
+   * Factory for {@link Mp3Extractor} instances.
+   */
+  public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+    @Override
+    public Extractor[] createExtractors() {
+      return new Extractor[] {new Mp3Extractor()};
+    }
+
+  };
+
+  /**
+   * 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 = MpegAudioHeader.MAX_FRAME_SIZE_BYTES;
+  /**
+   * 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 HEADER_MASK = 0xFFFE0C00;
+  private static final int XING_HEADER = Util.getIntegerCodeForString("Xing");
+  private static final int INFO_HEADER = Util.getIntegerCodeForString("Info");
+  private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI");
+
+  private final long forcedFirstSampleTimestampUs;
+  private final ParsableByteArray scratch;
+  private final MpegAudioHeader synchronizedHeader;
+  private final GaplessInfoHolder gaplessInfoHolder;
+
+  // Extractor outputs.
+  private ExtractorOutput extractorOutput;
+  private TrackOutput trackOutput;
+
+  private int synchronizedHeaderData;
+
+  private Metadata metadata;
+  private Seeker seeker;
+  private long basisTimeUs;
+  private long samplesRead;
+  private int sampleBytesRemaining;
+
+  /**
+   * Constructs a new {@link Mp3Extractor}.
+   */
+  public Mp3Extractor() {
+    this(C.TIME_UNSET);
+  }
+
+  /**
+   * Constructs a new {@link Mp3Extractor}.
+   *
+   * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or
+   *     {@link C#TIME_UNSET} if forcing is not required.
+   */
+  public Mp3Extractor(long forcedFirstSampleTimestampUs) {
+    this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
+    scratch = new ParsableByteArray(SCRATCH_LENGTH);
+    synchronizedHeader = new MpegAudioHeader();
+    gaplessInfoHolder = new GaplessInfoHolder();
+    basisTimeUs = C.TIME_UNSET;
+  }
+
+  @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);
+    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) {
+      seeker = setupSeeker(input);
+      extractorOutput.seekMap(seeker);
+      trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
+          Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
+          synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay,
+          gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata));
+    }
+    return readSample(input);
+  }
+
+  private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException {
+    if (sampleBytesRemaining == 0) {
+      extractorInput.resetPeekPosition();
+      if (!extractorInput.peekFully(scratch.data, 0, 4, true)) {
+        return RESULT_END_OF_INPUT;
+      }
+      scratch.setPosition(0);
+      int sampleHeaderData = scratch.readInt();
+      if ((sampleHeaderData & HEADER_MASK) != (synchronizedHeaderData & HEADER_MASK)
+          || 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) {
+      peekId3Data(input);
+      peekedId3Bytes = (int) input.getPeekPosition();
+      if (!sniffing) {
+        input.skipFully(peekedId3Bytes);
+      }
+    }
+    while (true) {
+      if (!input.peekFully(scratch.data, 0, 4, validFrameCount > 0)) {
+        // We reached the end of the stream but found at least one valid frame.
+        break;
+      }
+      scratch.setPosition(0);
+      int headerData = scratch.readInt();
+      int frameSize;
+      if ((candidateSynchronizedHeaderData != 0
+          && (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK))
+          || (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;
+  }
+
+  /**
+   * Peeks ID3 data from the input, including gapless playback information.
+   *
+   * @param input The {@link ExtractorInput} from which data should be peeked.
+   * @throws IOException If an error occurred peeking from the input.
+   * @throws InterruptedException If the thread was interrupted.
+   */
+  private void peekId3Data(ExtractorInput input) throws IOException, InterruptedException {
+    int peekedId3Bytes = 0;
+    while (true) {
+      input.peekFully(scratch.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
+      scratch.setPosition(0);
+      if (scratch.readUnsignedInt24() != Id3Decoder.ID3_TAG) {
+        // Not an ID3 tag.
+        break;
+      }
+      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().decode(id3Data, tagLength);
+        if (metadata != null) {
+          gaplessInfoHolder.setFromMetadata(metadata);
+        }
+      } else {
+        input.advancePeekPosition(framesLength);
+      }
+
+      peekedId3Bytes += tagLength;
+    }
+
+    input.resetPeekPosition();
+    input.advancePeekPosition(peekedId3Bytes);
+  }
+
+  /**
+   * Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide
+   * data from the start of the first frame in the stream. On returning, the input's position will
+   * be set to the start of the first frame of audio.
+   *
+   * @param input The {@link ExtractorInput} from which to read.
+   * @throws IOException Thrown if there was an error reading from the stream. Not expected if the
+   *     next two frames were already peeked during synchronization.
+   * @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if
+   *     the next two frames were already peeked during synchronization.
+   * @return a {@link Seeker}.
+   */
+  private Seeker setupSeeker(ExtractorInput input) throws IOException, InterruptedException {
+    // Read the first frame which may contain a Xing or VBRI header with seeking metadata.
+    ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize);
+    input.peekFully(frame.data, 0, synchronizedHeader.frameSize);
+
+    long position = input.getPosition();
+    long length = input.getLength();
+    int headerData = 0;
+    Seeker seeker = null;
+
+    // Check if there is a Xing header.
+    int xingBase = (synchronizedHeader.version & 1) != 0
+        ? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1
+        : (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5
+    if (frame.limit() >= xingBase + 4) {
+      frame.setPosition(xingBase);
+      headerData = frame.readInt();
+    }
+    if (headerData == XING_HEADER || headerData == INFO_HEADER) {
+      seeker = XingSeeker.create(synchronizedHeader, frame, position, length);
+      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);
+    } else if (frame.limit() >= 40) {
+      // Check if there is a VBRI header.
+      frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes.
+      headerData = frame.readInt();
+      if (headerData == VBRI_HEADER) {
+        seeker = VbriSeeker.create(synchronizedHeader, frame, position, length);
+        input.skipFully(synchronizedHeader.frameSize);
+      }
+    }
+
+    if (seeker == null) {
+      // Repopulate the synchronized header in case we had to skip an invalid seeking header, which
+      // would give an invalid CBR bitrate.
+      input.resetPeekPosition();
+      input.peekFully(scratch.data, 0, 4);
+      scratch.setPosition(0);
+      MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader);
+      seeker = new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, length);
+    }
+
+    return seeker;
+  }
+
+  /**
+   * {@link SeekMap} that also allows mapping from position (byte offset) back to time, which can be
+   * used to work out the new sample basis timestamp after seeking and resynchronization.
+   */
+  /* 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);
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.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 com.google.android.exoplayer2.extractor.mp3;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * MP3 seeker that uses metadata from a VBRI header.
+ */
+/* package */ final class VbriSeeker implements Mp3Extractor.Seeker {
+
+  /**
+   * 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 mpegAudioHeader The MPEG audio header associated with the frame.
+   * @param frame The data in this audio frame, with its position set to immediately after the
+   *     'VBRI' tag.
+   * @param position The position (byte offset) of the start of this frame in the stream.
+   * @param inputLength The length of the stream in bytes.
+   * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required
+   *     information is not present.
+   */
+  public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame,
+      long position, long inputLength) {
+    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);
+
+    // Skip the frame containing the VBRI header.
+    position += mpegAudioHeader.frameSize;
+
+    // Read table of contents entries.
+    long[] timesUs = new long[entryCount + 1];
+    long[] positions = new long[entryCount + 1];
+    timesUs[0] = 0L;
+    positions[0] = position;
+    for (int index = 1; index < timesUs.length; index++) {
+      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;
+      timesUs[index] = index * durationUs / entryCount;
+      positions[index] =
+          inputLength == C.LENGTH_UNSET ? position : Math.min(inputLength, position);
+    }
+    return new VbriSeeker(timesUs, positions, durationUs);
+  }
+
+  private final long[] timesUs;
+  private final long[] positions;
+  private final long durationUs;
+
+  private VbriSeeker(long[] timesUs, long[] positions, long durationUs) {
+    this.timesUs = timesUs;
+    this.positions = positions;
+    this.durationUs = durationUs;
+  }
+
+  @Override
+  public boolean isSeekable() {
+    return true;
+  }
+
+  @Override
+  public long getPosition(long timeUs) {
+    return positions[Util.binarySearchFloor(timesUs, timeUs, true, true)];
+  }
+
+  @Override
+  public long getTimeUs(long position) {
+    return timesUs[Util.binarySearchFloor(positions, position, true, true)];
+  }
+
+  @Override
+  public long getDurationUs() {
+    return durationUs;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.mp3;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * MP3 seeker that uses metadata from a Xing header.
+ */
+/* package */ final class XingSeeker implements Mp3Extractor.Seeker {
+
+  /**
+   * 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 mpegAudioHeader The MPEG audio header associated with the frame.
+   * @param frame The data in this audio frame, with its position set to immediately after the
+   *    'Xing' or 'Info' tag.
+   * @param position The position (byte offset) of the start of this frame in the stream.
+   * @param inputLength The length of the stream in bytes.
+   * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required
+   *     information is not present.
+   */
+  public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame,
+      long position, long inputLength) {
+    int samplesPerFrame = mpegAudioHeader.samplesPerFrame;
+    int sampleRate = mpegAudioHeader.sampleRate;
+    long firstFramePosition = position + mpegAudioHeader.frameSize;
+
+    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(firstFramePosition, durationUs, inputLength);
+    }
+
+    long sizeBytes = frame.readUnsignedIntToInt();
+    frame.skipBytes(1);
+    long[] tableOfContents = new long[99];
+    for (int i = 0; i < 99; i++) {
+      tableOfContents[i] = frame.readUnsignedByte();
+    }
+
+    // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes:
+    // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4);
+    // padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte();
+    return new XingSeeker(firstFramePosition, durationUs, inputLength, tableOfContents,
+        sizeBytes, mpegAudioHeader.frameSize);
+  }
+
+  private final long firstFramePosition;
+  private final long durationUs;
+  private final long inputLength;
+  /**
+   * Entries are in the range [0, 255], but are stored as long integers for convenience.
+   */
+  private final long[] tableOfContents;
+  private final long sizeBytes;
+  private final int headerSize;
+
+  private XingSeeker(long firstFramePosition, long durationUs, long inputLength) {
+    this(firstFramePosition, durationUs, inputLength, null, 0, 0);
+  }
+
+  private XingSeeker(long firstFramePosition, long durationUs, long inputLength,
+      long[] tableOfContents, long sizeBytes, int headerSize) {
+    this.firstFramePosition = firstFramePosition;
+    this.durationUs = durationUs;
+    this.inputLength = inputLength;
+    this.tableOfContents = tableOfContents;
+    this.sizeBytes = sizeBytes;
+    this.headerSize = headerSize;
+  }
+
+  @Override
+  public boolean isSeekable() {
+    return tableOfContents != null;
+  }
+
+  @Override
+  public long getPosition(long timeUs) {
+    if (!isSeekable()) {
+      return firstFramePosition;
+    }
+    float percent = timeUs * 100f / durationUs;
+    float fx;
+    if (percent <= 0f) {
+      fx = 0f;
+    } else if (percent >= 100f) {
+      fx = 256f;
+    } else {
+      int a = (int) percent;
+      float fa, fb;
+      if (a == 0) {
+        fa = 0f;
+      } else {
+        fa = tableOfContents[a - 1];
+      }
+      if (a < 99) {
+        fb = tableOfContents[a];
+      } else {
+        fb = 256f;
+      }
+      fx = fa + (fb - fa) * (percent - a);
+    }
+
+    long position = Math.round((1.0 / 256) * fx * sizeBytes) + firstFramePosition;
+    long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1
+        : firstFramePosition - headerSize + sizeBytes - 1;
+    return Math.min(position, maximumPosition);
+  }
+
+  @Override
+  public long getTimeUs(long position) {
+    if (!isSeekable() || position < firstFramePosition) {
+      return 0L;
+    }
+    double offsetByte = 256.0 * (position - firstFramePosition) / sizeBytes;
+    int previousTocPosition =
+        Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, false) + 1;
+    long previousTime = getTimeUsForTocPosition(previousTocPosition);
+
+    // Linearly interpolate the time taking into account the next entry.
+    long previousByte = previousTocPosition == 0 ? 0 : tableOfContents[previousTocPosition - 1];
+    long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition];
+    long nextTime = getTimeUsForTocPosition(previousTocPosition + 1);
+    long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime)
+        * (offsetByte - previousByte) / (nextByte - previousByte));
+    return previousTime + timeOffset;
+  }
+
+  @Override
+  public long getDurationUs() {
+    return durationUs;
+  }
+
+  /**
+   * Returns the time in microseconds corresponding to a table of contents position, which is
+   * interpreted as a percentage of the stream's duration between 0 and 100.
+   */
+  private long getTimeUsForTocPosition(int tocPosition) {
+    return durationUs * tocPosition / 100;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/Atom.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.mp4;
+
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/* 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 first 32 bits of atomSize when the atom size is actually a long value.
+   */
+  public static final int LONG_SIZE_PREFIX = 1;
+
+  public static final int TYPE_ftyp = Util.getIntegerCodeForString("ftyp");
+  public static final int TYPE_avc1 = Util.getIntegerCodeForString("avc1");
+  public static final int TYPE_avc3 = Util.getIntegerCodeForString("avc3");
+  public static final int TYPE_hvc1 = Util.getIntegerCodeForString("hvc1");
+  public static final int TYPE_hev1 = Util.getIntegerCodeForString("hev1");
+  public static final int TYPE_s263 = Util.getIntegerCodeForString("s263");
+  public static final int TYPE_d263 = Util.getIntegerCodeForString("d263");
+  public static final int TYPE_mdat = Util.getIntegerCodeForString("mdat");
+  public static final int TYPE_mp4a = Util.getIntegerCodeForString("mp4a");
+  public static final int TYPE__mp3 = Util.getIntegerCodeForString(".mp3");
+  public static final int TYPE_wave = Util.getIntegerCodeForString("wave");
+  public static final int TYPE_lpcm = Util.getIntegerCodeForString("lpcm");
+  public static final int TYPE_sowt = Util.getIntegerCodeForString("sowt");
+  public static final int TYPE_ac_3 = Util.getIntegerCodeForString("ac-3");
+  public static final int TYPE_dac3 = Util.getIntegerCodeForString("dac3");
+  public static final int TYPE_ec_3 = Util.getIntegerCodeForString("ec-3");
+  public static final int TYPE_dec3 = Util.getIntegerCodeForString("dec3");
+  public static final int TYPE_dtsc = Util.getIntegerCodeForString("dtsc");
+  public static final int TYPE_dtsh = Util.getIntegerCodeForString("dtsh");
+  public static final int TYPE_dtsl = Util.getIntegerCodeForString("dtsl");
+  public static final int TYPE_dtse = Util.getIntegerCodeForString("dtse");
+  public static final int TYPE_ddts = Util.getIntegerCodeForString("ddts");
+  public static final int TYPE_tfdt = Util.getIntegerCodeForString("tfdt");
+  public static final int TYPE_tfhd = Util.getIntegerCodeForString("tfhd");
+  public static final int TYPE_trex = Util.getIntegerCodeForString("trex");
+  public static final int TYPE_trun = Util.getIntegerCodeForString("trun");
+  public static final int TYPE_sidx = Util.getIntegerCodeForString("sidx");
+  public static final int TYPE_moov = Util.getIntegerCodeForString("moov");
+  public static final int TYPE_mvhd = Util.getIntegerCodeForString("mvhd");
+  public static final int TYPE_trak = Util.getIntegerCodeForString("trak");
+  public static final int TYPE_mdia = Util.getIntegerCodeForString("mdia");
+  public static final int TYPE_minf = Util.getIntegerCodeForString("minf");
+  public static final int TYPE_stbl = Util.getIntegerCodeForString("stbl");
+  public static final int TYPE_avcC = Util.getIntegerCodeForString("avcC");
+  public static final int TYPE_hvcC = Util.getIntegerCodeForString("hvcC");
+  public static final int TYPE_esds = Util.getIntegerCodeForString("esds");
+  public static final int TYPE_moof = Util.getIntegerCodeForString("moof");
+  public static final int TYPE_traf = Util.getIntegerCodeForString("traf");
+  public static final int TYPE_mvex = Util.getIntegerCodeForString("mvex");
+  public static final int TYPE_mehd = Util.getIntegerCodeForString("mehd");
+  public static final int TYPE_tkhd = Util.getIntegerCodeForString("tkhd");
+  public static final int TYPE_edts = Util.getIntegerCodeForString("edts");
+  public static final int TYPE_elst = Util.getIntegerCodeForString("elst");
+  public static final int TYPE_mdhd = Util.getIntegerCodeForString("mdhd");
+  public static final int TYPE_hdlr = Util.getIntegerCodeForString("hdlr");
+  public static final int TYPE_stsd = Util.getIntegerCodeForString("stsd");
+  public static final int TYPE_pssh = Util.getIntegerCodeForString("pssh");
+  public static final int TYPE_sinf = Util.getIntegerCodeForString("sinf");
+  public static final int TYPE_schm = Util.getIntegerCodeForString("schm");
+  public static final int TYPE_schi = Util.getIntegerCodeForString("schi");
+  public static final int TYPE_tenc = Util.getIntegerCodeForString("tenc");
+  public static final int TYPE_encv = Util.getIntegerCodeForString("encv");
+  public static final int TYPE_enca = Util.getIntegerCodeForString("enca");
+  public static final int TYPE_frma = Util.getIntegerCodeForString("frma");
+  public static final int TYPE_saiz = Util.getIntegerCodeForString("saiz");
+  public static final int TYPE_saio = Util.getIntegerCodeForString("saio");
+  public static final int TYPE_sbgp = Util.getIntegerCodeForString("sbgp");
+  public static final int TYPE_sgpd = Util.getIntegerCodeForString("sgpd");
+  public static final int TYPE_uuid = Util.getIntegerCodeForString("uuid");
+  public static final int TYPE_senc = Util.getIntegerCodeForString("senc");
+  public static final int TYPE_pasp = Util.getIntegerCodeForString("pasp");
+  public static final int TYPE_TTML = Util.getIntegerCodeForString("TTML");
+  public static final int TYPE_vmhd = Util.getIntegerCodeForString("vmhd");
+  public static final int TYPE_mp4v = Util.getIntegerCodeForString("mp4v");
+  public static final int TYPE_stts = Util.getIntegerCodeForString("stts");
+  public static final int TYPE_stss = Util.getIntegerCodeForString("stss");
+  public static final int TYPE_ctts = Util.getIntegerCodeForString("ctts");
+  public static final int TYPE_stsc = Util.getIntegerCodeForString("stsc");
+  public static final int TYPE_stsz = Util.getIntegerCodeForString("stsz");
+  public static final int TYPE_stz2 = Util.getIntegerCodeForString("stz2");
+  public static final int TYPE_stco = Util.getIntegerCodeForString("stco");
+  public static final int TYPE_co64 = Util.getIntegerCodeForString("co64");
+  public static final int TYPE_tx3g = Util.getIntegerCodeForString("tx3g");
+  public static final int TYPE_wvtt = Util.getIntegerCodeForString("wvtt");
+  public static final int TYPE_stpp = Util.getIntegerCodeForString("stpp");
+  public static final int TYPE_c608 = Util.getIntegerCodeForString("c608");
+  public static final int TYPE_samr = Util.getIntegerCodeForString("samr");
+  public static final int TYPE_sawb = Util.getIntegerCodeForString("sawb");
+  public static final int TYPE_udta = Util.getIntegerCodeForString("udta");
+  public static final int TYPE_meta = Util.getIntegerCodeForString("meta");
+  public static final int TYPE_ilst = Util.getIntegerCodeForString("ilst");
+  public static final int TYPE_mean = Util.getIntegerCodeForString("mean");
+  public static final int TYPE_name = Util.getIntegerCodeForString("name");
+  public static final int TYPE_data = Util.getIntegerCodeForString("data");
+  public static final int TYPE_emsg = Util.getIntegerCodeForString("emsg");
+  public static final int TYPE_st3d = Util.getIntegerCodeForString("st3d");
+  public static final int TYPE_sv3d = Util.getIntegerCodeForString("sv3d");
+  public static final int TYPE_proj = Util.getIntegerCodeForString("proj");
+  public static final int TYPE_vp08 = Util.getIntegerCodeForString("vp08");
+  public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09");
+  public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC");
+  public static final int TYPE_camm = Util.getIntegerCodeForString("camm");
+  public static final int TYPE_alac = Util.getIntegerCodeForString("alac");
+
+  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.
+     */
+    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.
+     */
+    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);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java
@@ -0,0 +1,1294 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.mp4;
+
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.audio.Ac3Util;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.AvcConfig;
+import 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.
+ */
+/* package */ final class AtomParsers {
+
+  private static final String TAG = "AtomParsers";
+
+  private static final int TYPE_vide = Util.getIntegerCodeForString("vide");
+  private static final int TYPE_soun = Util.getIntegerCodeForString("soun");
+  private static final int TYPE_text = Util.getIntegerCodeForString("text");
+  private static final int TYPE_sbtl = Util.getIntegerCodeForString("sbtl");
+  private static final int TYPE_subt = Util.getIntegerCodeForString("subt");
+  private static final int TYPE_clcp = Util.getIntegerCodeForString("clcp");
+  private static final int TYPE_cenc = Util.getIntegerCodeForString("cenc");
+  private static final int TYPE_meta = Util.getIntegerCodeForString("meta");
+
+  /**
+   * 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 isQuickTime True for QuickTime media. False otherwise.
+   * @return A {@link Track} instance, or {@code null} if the track's type isn't supported.
+   */
+  public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long duration,
+      DrmInitData drmInitData, boolean isQuickTime) throws ParserException {
+    Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia);
+    int trackType = 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);
+    Pair<long[], long[]> edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts));
+    return stsdData.format == null ? null
+        : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs,
+            stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes,
+            stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second);
+  }
+
+  /**
+   * 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 If the resulting sample sequence does not contain a sync sample.
+   */
+  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(new long[0], new int[0], 0, new long[0], new int[0]);
+    }
+
+    // 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;
+      }
+    }
+
+    // True if we can rechunk fixed-sample-size data. Note that we only rechunk raw audio.
+    boolean isRechunkable = sampleSizeBox.isFixedSampleSize()
+        && MimeTypes.AUDIO_RAW.equals(track.format.sampleMimeType)
+        && remainingTimestampDeltaChanges == 0 && remainingTimestampOffsetChanges == 0
+        && remainingSynchronizationSamples == 0;
+
+    long[] offsets;
+    int[] sizes;
+    int maximumSize = 0;
+    long[] timestamps;
+    int[] flags;
+    long timestampTimeUnits = 0;
+
+    if (!isRechunkable) {
+      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.
+        while (remainingSamplesInChunk == 0) {
+          Assertions.checkState(chunkIterator.moveNext());
+          offset = chunkIterator.offset;
+          remainingSamplesInChunk = chunkIterator.numSamples;
+        }
+
+        // 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();
+          timestampDeltaInTimeUnits = stts.readUnsignedIntToInt();
+          remainingTimestampDeltaChanges--;
+        }
+
+        offset += sizes[i];
+        remainingSamplesInChunk--;
+      }
+
+      Assertions.checkArgument(remainingSamplesAtTimestampOffset == 0);
+      // Remove trailing ctts entries with 0-valued sample counts.
+      while (remainingTimestampOffsetChanges > 0) {
+        Assertions.checkArgument(ctts.readUnsignedIntToInt() == 0);
+        ctts.readInt(); // Ignore offset.
+        remainingTimestampOffsetChanges--;
+      }
+
+      // If the stbl's child boxes are not consistent the container is malformed, but the stream may
+      // still be playable.
+      if (remainingSynchronizationSamples != 0 || remainingSamplesAtTimestampDelta != 0
+          || remainingSamplesInChunk != 0 || remainingTimestampDeltaChanges != 0) {
+        Log.w(TAG, "Inconsistent stbl box for track " + track.id
+            + ": remainingSynchronizationSamples " + remainingSynchronizationSamples
+            + ", remainingSamplesAtTimestampDelta " + remainingSamplesAtTimestampDelta
+            + ", remainingSamplesInChunk " + remainingSamplesInChunk
+            + ", remainingTimestampDeltaChanges " + remainingTimestampDeltaChanges);
+      }
+    } 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 = sampleSizeBox.readNextSampleSize();
+      FixedSampleSizeRechunker.Results rechunkedResults = FixedSampleSizeRechunker.rechunk(
+          fixedSampleSize, chunkOffsetsBytes, chunkSampleCounts, timestampDeltaInTimeUnits);
+      offsets = rechunkedResults.offsets;
+      sizes = rechunkedResults.sizes;
+      maximumSize = rechunkedResults.maximumSize;
+      timestamps = rechunkedResults.timestamps;
+      flags = rechunkedResults.flags;
+    }
+
+    if (track.editListDurations == null || gaplessInfoHolder.hasGaplessInfo()) {
+      // There is no edit list, or we are ignoring it as we already have gapless metadata to apply.
+      // This implementation does not support applying both gapless metadata and an edit list.
+      Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
+      return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
+    }
+
+    // See the BMFF spec (ISO 14496-12) subsection 8.6.6. Edit lists that require prerolling from a
+    // sync sample after reordering are not supported. Partial audio sample truncation is only
+    // supported in edit lists with one edit that removes less than one sample from the start/end of
+    // the track, for gapless audio playback. This implementation handles simple discarding/delaying
+    // of samples. The extractor may place further restrictions on what edited streams are playable.
+
+    if (track.editListDurations.length == 1 && track.type == C.TRACK_TYPE_AUDIO
+        && timestamps.length >= 2) {
+      // Handle the edit by setting gapless playback metadata, if possible. This implementation
+      // assumes that only one "roll" sample is needed, which is the case for AAC, so the start/end
+      // points of the edit must lie within the first/last samples respectively.
+      long editStartTime = track.editListMediaTimes[0];
+      long editEndTime = editStartTime + Util.scaleLargeTimestamp(track.editListDurations[0],
+          track.timescale, track.movieTimescale);
+      long lastSampleEndTime = timestampTimeUnits;
+      if (timestamps[0] <= editStartTime && editStartTime < timestamps[1]
+          && timestamps[timestamps.length - 1] < editEndTime && editEndTime <= lastSampleEndTime) {
+        long paddingTimeUnits = lastSampleEndTime - editEndTime;
+        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);
+          return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
+        }
+      }
+    }
+
+    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.
+      for (int i = 0; i < timestamps.length; i++) {
+        timestamps[i] = Util.scaleLargeTimestamp(timestamps[i] - track.editListMediaTimes[0],
+            C.MICROS_PER_SECOND, track.timescale);
+      }
+      return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
+    }
+
+    // Count the number of samples after applying edits.
+    int editedSampleCount = 0;
+    int nextSampleIndex = 0;
+    boolean copyMetadata = false;
+    for (int i = 0; i < track.editListDurations.length; i++) {
+      long mediaTime = track.editListMediaTimes[i];
+      if (mediaTime != -1) {
+        long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale,
+            track.movieTimescale);
+        int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
+        int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, true, false);
+        editedSampleCount += endIndex - startIndex;
+        copyMetadata |= nextSampleIndex != startIndex;
+        nextSampleIndex = endIndex;
+      }
+    }
+    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 mediaTime = track.editListMediaTimes[i];
+      long duration = track.editListDurations[i];
+      if (mediaTime != -1) {
+        long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale,
+            track.movieTimescale);
+        int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
+        int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, true, false);
+        if (copyMetadata) {
+          int count = endIndex - startIndex;
+          System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count);
+          System.arraycopy(sizes, startIndex, editedSizes, sampleIndex, count);
+          System.arraycopy(flags, startIndex, editedFlags, sampleIndex, count);
+        }
+        for (int j = startIndex; j < endIndex; j++) {
+          long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
+          long timeInSegmentUs = Util.scaleLargeTimestamp(timestamps[j] - mediaTime,
+              C.MICROS_PER_SECOND, track.timescale);
+          editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs;
+          if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) {
+            editedMaximumSize = sizes[j];
+          }
+          sampleIndex++;
+        }
+      }
+      pts += duration;
+    }
+
+    boolean hasSyncSample = false;
+    for (int i = 0; i < editedFlags.length && !hasSyncSample; i++) {
+      hasSyncSample |= (editedFlags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0;
+    }
+    if (!hasSyncSample) {
+      throw new ParserException("The edited sample sequence does not contain a sync sample.");
+    }
+
+    return new TrackSampleTable(editedOffsets, editedSizes, editedMaximumSize, editedTimestamps,
+        editedFlags);
+  }
+
+  /**
+   * 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.
+   */
+  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 parseMetaAtom(udtaData, atomPosition + atomSize);
+      }
+      udtaData.skipBytes(atomSize - Atom.HEADER_SIZE);
+    }
+    return null;
+  }
+
+  private static Metadata parseMetaAtom(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.skipBytes(atomSize - Atom.HEADER_SIZE);
+    }
+    return null;
+  }
+
+  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 track type.
+   */
+  private static int parseHdlr(ParsableByteArray hdlr) {
+    hdlr.setPosition(Atom.FULL_HEADER_SIZE + 4);
+    int trackType = hdlr.readInt();
+    if (trackType == TYPE_soun) {
+      return C.TRACK_TYPE_AUDIO;
+    } else if (trackType == TYPE_vide) {
+      return C.TRACK_TYPE_VIDEO;
+    } else if (trackType == TYPE_text || trackType == TYPE_sbtl || trackType == TYPE_subt
+        || trackType == TYPE_clcp) {
+      return C.TRACK_TYPE_TEXT;
+    } else if (trackType == 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) {
+        parseVideoSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
+            rotationDegrees, drmInitData, out, i);
+      } else if (childAtomType == Atom.TYPE_mp4a || childAtomType == Atom.TYPE_enca
+          || childAtomType == Atom.TYPE_ac_3 || childAtomType == Atom.TYPE_ec_3
+          || childAtomType == Atom.TYPE_dtsc || childAtomType == Atom.TYPE_dtse
+          || childAtomType == Atom.TYPE_dtsh || childAtomType == Atom.TYPE_dtsl
+          || childAtomType == Atom.TYPE_samr || childAtomType == Atom.TYPE_sawb
+          || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt
+          || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac) {
+        parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
+            language, isQuickTime, drmInitData, out, i);
+      } else if (childAtomType == Atom.TYPE_TTML) {
+        out.format = Format.createTextSampleFormat(Integer.toString(trackId),
+            MimeTypes.APPLICATION_TTML, null, Format.NO_VALUE, 0, language, drmInitData);
+      } else if (childAtomType == Atom.TYPE_tx3g) {
+        out.format = Format.createTextSampleFormat(Integer.toString(trackId),
+            MimeTypes.APPLICATION_TX3G, null, Format.NO_VALUE, 0, language, drmInitData);
+      } else if (childAtomType == Atom.TYPE_wvtt) {
+        out.format = Format.createTextSampleFormat(Integer.toString(trackId),
+            MimeTypes.APPLICATION_MP4VTT, null, Format.NO_VALUE, 0, language, drmInitData);
+      } else if (childAtomType == Atom.TYPE_stpp) {
+        out.format = Format.createTextSampleFormat(Integer.toString(trackId),
+            MimeTypes.APPLICATION_TTML, null, Format.NO_VALUE, 0, language, drmInitData,
+            0 /* subsample timing is absolute */);
+      } else if (childAtomType == Atom.TYPE_c608) {
+        // Defined by the QuickTime File Format specification.
+        out.format = Format.createTextSampleFormat(Integer.toString(trackId),
+            MimeTypes.APPLICATION_MP4CEA608, null, Format.NO_VALUE, 0, language, drmInitData);
+        out.requiredSampleTransformation = Track.TRANSFORMATION_CEA608_CDAT;
+      } else if (childAtomType == Atom.TYPE_camm) {
+        out.format = Format.createSampleFormat(Integer.toString(trackId),
+            MimeTypes.APPLICATION_CAMERA_MOTION, null, Format.NO_VALUE, drmInitData);
+      }
+      stsd.setPosition(childStartPosition + childAtomSize);
+    }
+    return out;
+  }
+
+  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);
+
+    parent.skipBytes(24);
+    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) {
+      atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex);
+      parent.setPosition(childPosition);
+    }
+
+    List<byte[]> initializationData = null;
+    String mimeType = 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_vpcC) {
+        Assertions.checkState(mimeType == null);
+        mimeType = (atomType == Atom.TYPE_vp08) ? MimeTypes.VIDEO_VP8 : MimeTypes.VIDEO_VP9;
+      } 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;
+            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, null,
+        Format.NO_VALUE, Format.NO_VALUE, width, height, Format.NO_VALUE, initializationData,
+        rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, 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) {
+    parent.setPosition(position + Atom.HEADER_SIZE);
+
+    int quickTimeSoundDescriptionVersion = 0;
+    if (isQuickTime) {
+      parent.skipBytes(8);
+      quickTimeSoundDescriptionVersion = parent.readUnsignedShort();
+      parent.skipBytes(6);
+    } else {
+      parent.skipBytes(16);
+    }
+
+    int channelCount;
+    int sampleRate;
+
+    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) {
+      atomType = parseSampleEntryEncryptionData(parent, position, size, out, entryIndex);
+      parent.setPosition(childPosition);
+    }
+
+    // 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_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;
+    } else if (atomType == Atom.TYPE__mp3) {
+      mimeType = MimeTypes.AUDIO_MPEG;
+    } else if (atomType == Atom.TYPE_alac) {
+      mimeType = MimeTypes.AUDIO_ALAC;
+    }
+
+    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)) {
+            // TODO: Do we really need to do this? See [Internal: b/10903778]
+            // Update sampleRate and channelCount from the AudioSpecificConfig initialization data.
+            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_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_alac) {
+        initializationData = new byte[childAtomSize];
+        parent.setPosition(childPosition);
+        parent.readBytes(initializationData, 0, childAtomSize);
+      }
+      childPosition += childAtomSize;
+    }
+
+    if (out.format == null && mimeType != null) {
+      // TODO: Determine the correct PCM encoding.
+      @C.PcmEncoding int pcmEncoding =
+          MimeTypes.AUDIO_RAW.equals(mimeType) ? C.ENCODING_PCM_16BIT : Format.NO_VALUE;
+      out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
+          Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, pcmEncoding,
+          initializationData == null ? null : Collections.singletonList(initializationData),
+          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;
+    switch (objectTypeIndication) {
+      case 0x6B:
+        mimeType = MimeTypes.AUDIO_MPEG;
+        return Pair.create(mimeType, null);
+      case 0x20:
+        mimeType = MimeTypes.VIDEO_MP4V;
+        break;
+      case 0x21:
+        mimeType = MimeTypes.VIDEO_H264;
+        break;
+      case 0x23:
+        mimeType = MimeTypes.VIDEO_H265;
+        break;
+      case 0x40:
+      case 0x66:
+      case 0x67:
+      case 0x68:
+        mimeType = MimeTypes.AUDIO_AAC;
+        break;
+      case 0xA5:
+        mimeType = MimeTypes.AUDIO_AC3;
+        break;
+      case 0xA6:
+        mimeType = MimeTypes.AUDIO_E_AC3;
+        break;
+      case 0xA9:
+      case 0xAC:
+        mimeType = MimeTypes.AUDIO_DTS;
+        return Pair.create(mimeType, null);
+      case 0xAA:
+      case 0xAB:
+        mimeType = MimeTypes.AUDIO_DTS_HD;
+        return Pair.create(mimeType, null);
+      default:
+        mimeType = null;
+        break;
+    }
+
+    parent.skipBytes(12);
+
+    // Start of the AudioSpecificConfig.
+    parent.skipBytes(1); // AudioSpecificConfig 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, populating {@code out} and returning
+   * the unencrypted atom type, or 0 if no common encryption sinf atom was present.
+   */
+  private static int parseSampleEntryEncryptionData(ParsableByteArray parent, int position,
+      int size, StsdData out, int entryIndex) {
+    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 = parseSinfFromParent(parent, childPosition,
+            childAtomSize);
+        if (result != null) {
+          out.trackEncryptionBoxes[entryIndex] = result.second;
+          return result.first;
+        }
+      }
+      childPosition += childAtomSize;
+    }
+    // This enca/encv box does not have a data format so return an invalid atom type.
+    return 0;
+  }
+
+  private static Pair<Integer, TrackEncryptionBox> parseSinfFromParent(ParsableByteArray parent,
+      int position, int size) {
+    int childPosition = position + Atom.HEADER_SIZE;
+
+    boolean isCencScheme = false;
+    TrackEncryptionBox trackEncryptionBox = 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);
+        isCencScheme = parent.readInt() == TYPE_cenc;
+      } else if (childAtomType == Atom.TYPE_schi) {
+        trackEncryptionBox = parseSchiFromParent(parent, childPosition, childAtomSize);
+      }
+      childPosition += childAtomSize;
+    }
+
+    if (isCencScheme) {
+      Assertions.checkArgument(dataFormat != null, "frma atom is mandatory");
+      Assertions.checkArgument(trackEncryptionBox != null, "schi->tenc atom is mandatory");
+      return Pair.create(dataFormat, trackEncryptionBox);
+    } else {
+      return null;
+    }
+  }
+
+  private static TrackEncryptionBox parseSchiFromParent(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_tenc) {
+        parent.skipBytes(6);
+        boolean defaultIsEncrypted = parent.readUnsignedByte() == 1;
+        int defaultInitVectorSize = parent.readUnsignedByte();
+        byte[] defaultKeyId = new byte[16];
+        parent.readBytes(defaultKeyId, 0, defaultKeyId.length);
+        return new TrackEncryptionBox(defaultIsEncrypted, defaultInitVectorSize, defaultKeyId);
+      }
+      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;
+  }
+
+  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 = C.INDEX_UNSET;
+    }
+
+    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 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;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/FixedSampleSizeRechunker.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 com.google.android.exoplayer2.extractor.mp4;
+
+import com.google.android.exoplayer2.C;
+import 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;
+
+    private Results(long[] offsets, int[] sizes, int maximumSize, long[] timestamps, int[] flags) {
+      this.offsets = offsets;
+      this.sizes = sizes;
+      this.maximumSize = maximumSize;
+      this.timestamps = timestamps;
+      this.flags = flags;
+    }
+
+  }
+
+  /**
+   * 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++;
+      }
+    }
+
+    return new Results(offsets, sizes, maximumSize, timestamps, flags);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
@@ -0,0 +1,1307 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.mp4;
+
+import android.support.annotation.IntDef;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import com.google.android.exoplayer2.extractor.ChunkIndex;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
+import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom;
+import com.google.android.exoplayer2.text.cea.CeaUtil;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Stack;
+import java.util.UUID;
+
+/**
+ * Facilitates the extraction of data from the fragmented mp4 container format.
+ */
+public final class FragmentedMp4Extractor implements Extractor {
+
+  /**
+   * Factory for {@link FragmentedMp4Extractor} instances.
+   */
+  public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+    @Override
+    public Extractor[] createExtractors() {
+      return new Extractor[] {new FragmentedMp4Extractor()};
+    }
+
+  };
+
+  /**
+   * Flags controlling the behavior of the extractor.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME,
+      FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK,
+      FLAG_SIDELOADED})
+  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 = 2;
+  /**
+   * Flag to indicate that the extractor should output an event message metadata track. Any event
+   * messages in the stream will be delivered as samples to this track.
+   */
+  public static final int FLAG_ENABLE_EMSG_TRACK = 4;
+  /**
+   * Flag to indicate that the extractor should output a CEA-608 text track. Any CEA-608 messages
+   * contained within SEI NAL units in the stream will be delivered as samples to this track.
+   */
+  public static final int FLAG_ENABLE_CEA608_TRACK = 8;
+  /**
+   * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4
+   * container.
+   */
+  private static final int FLAG_SIDELOADED = 16;
+
+  private static final String TAG = "FragmentedMp4Extractor";
+  private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig");
+  private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information
+  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};
+
+  // 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;
+  private final Track sideloadedTrack;
+
+  // Track-linked data bundle, accessible as a whole through trackID.
+  private final SparseArray<TrackBundle> trackBundles;
+
+  // Temporary arrays.
+  private final ParsableByteArray nalStartCode;
+  private final ParsableByteArray nalLength;
+  private final ParsableByteArray nalPayload;
+  private final ParsableByteArray encryptionSignalByte;
+
+  // Adjusts sample timestamps.
+  private final TimestampAdjuster timestampAdjuster;
+
+  // Parser state.
+  private final ParsableByteArray atomHeader;
+  private final byte[] extendedTypeScratch;
+  private final Stack<ContainerAtom> containerAtoms;
+  private final LinkedList<MetadataSampleInfo> pendingMetadataSampleInfos;
+
+  private int parserState;
+  private int atomType;
+  private long atomSize;
+  private int atomHeaderBytesRead;
+  private ParsableByteArray atomData;
+  private long endOfMdatPosition;
+  private int pendingMetadataSampleBytes;
+
+  private long durationUs;
+  private long segmentIndexEarliestPresentationTimeUs;
+  private TrackBundle currentTrackBundle;
+  private int sampleSize;
+  private int sampleBytesWritten;
+  private int sampleCurrentNalBytesRemaining;
+
+  // Extractor output.
+  private ExtractorOutput extractorOutput;
+  private TrackOutput eventMessageTrackOutput;
+  private TrackOutput cea608TrackOutput;
+
+  // Whether extractorOutput.seekMap has been called.
+  private boolean haveOutputSeekMap;
+
+  public FragmentedMp4Extractor() {
+    this(0, null);
+  }
+
+  /**
+   * @param flags Flags that control the extractor's behavior.
+   * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
+   */
+  public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) {
+    this(flags, null, timestampAdjuster);
+  }
+
+  /**
+   * @param flags Flags that control the extractor's behavior.
+   * @param sideloadedTrack Sideloaded track information, in the case that the extractor
+   *     will not receive a moov box in the input data.
+   * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
+   */
+  public FragmentedMp4Extractor(@Flags int flags, Track sideloadedTrack,
+      TimestampAdjuster timestampAdjuster) {
+    this.sideloadedTrack = sideloadedTrack;
+    this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0);
+    this.timestampAdjuster = timestampAdjuster;
+    atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
+    nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
+    nalLength = new ParsableByteArray(4);
+    nalPayload = new ParsableByteArray(1);
+    encryptionSignalByte = new ParsableByteArray(1);
+    extendedTypeScratch = new byte[16];
+    containerAtoms = new Stack<>();
+    pendingMetadataSampleInfos = new LinkedList<>();
+    trackBundles = new SparseArray<>();
+    durationUs = 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));
+      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;
+    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.LONG_SIZE_PREFIX) {
+      // Read the extended atom size.
+      int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE;
+      input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining);
+      atomHeaderBytesRead += headerBytesRemaining;
+      atomSize = atomHeader.readUnsignedLongToLong();
+    }
+
+    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) {
+        extractorOutput.seekMap(new SeekMap.Unseekable(durationUs));
+        haveOutputSeekMap = true;
+      }
+      parserState = STATE_READING_ENCRYPTION_DATA;
+      return true;
+    }
+
+    if (shouldParseContainerAtom(atomType)) {
+      long endPosition = input.getPosition() + atomSize - Atom.HEADER_SIZE;
+      containerAtoms.add(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.");
+
+    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 = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), duration,
+            drmInitData, 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));
+        trackBundle.init(track, defaultSampleValuesArray.get(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, defaultSampleValuesArray.get(track.id));
+      }
+    }
+  }
+
+  private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException {
+    parseMoof(moof, trackBundles, flags, extendedTypeScratch);
+    DrmInitData drmInitData = getDrmInitDataFromAtoms(moof.leafChildren);
+    if (drmInitData != null) {
+      int trackCount = trackBundles.size();
+      for (int i = 0; i < trackCount; i++) {
+        trackBundles.valueAt(i).updateDrmInitData(drmInitData);
+      }
+    }
+  }
+
+  private void maybeInitExtraTracks() {
+    if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) {
+      eventMessageTrackOutput = extractorOutput.track(trackBundles.size());
+      eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG,
+          Format.OFFSET_SAMPLE_RELATIVE));
+    }
+    if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutput == null) {
+      cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1);
+      cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608,
+          null, Format.NO_VALUE, 0, null, null));
+    }
+  }
+
+  /**
+   * Handles an emsg atom (defined in 23009-1).
+   */
+  private void onEmsgLeafAtomRead(ParsableByteArray atom) {
+    if (eventMessageTrackOutput == null) {
+      return;
+    }
+    // Parse the event's presentation time delta.
+    atom.setPosition(Atom.FULL_HEADER_SIZE);
+    atom.readNullTerminatedString(); // schemeIdUri
+    atom.readNullTerminatedString(); // value
+    long timescale = atom.readUnsignedInt();
+    long presentationTimeDeltaUs =
+        Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale);
+    // Output the sample data.
+    atom.setPosition(Atom.FULL_HEADER_SIZE);
+    int sampleSize = atom.bytesLeft();
+    eventMessageTrackOutput.sampleData(atom, sampleSize);
+    // Output the sample metadata.
+    if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) {
+      // We can output the sample metadata immediately.
+      eventMessageTrackOutput.sampleMetadata(
+          segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs,
+          C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null);
+    } else {
+      // We need the first sample timestamp in the segment before we can output the metadata.
+      pendingMetadataSampleInfos.addLast(
+          new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize));
+      pendingMetadataSampleBytes += sampleSize;
+    }
+  }
+
+  /**
+   * 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, flags);
+    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);
+
+    LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
+    if (saiz != null) {
+      TrackEncryptionBox trackEncryptionBox = trackBundle.track
+          .sampleDescriptionEncryptionBoxes[fragment.header.sampleDescriptionIndex];
+      parseSaiz(trackEncryptionBox, 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, 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.initializationVectorSize;
+    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, int flags) {
+    tfhd.setPosition(Atom.HEADER_SIZE);
+    int fullAtom = tfhd.readInt();
+    int atomFlags = Atom.parseFullAtomFlags(fullAtom);
+    int trackId = tfhd.readInt();
+    TrackBundle trackBundle = trackBundles.get((flags & FLAG_SIDELOADED) == 0 ? trackId : 0);
+    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;
+  }
+
+  /**
+   * 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], 1000, 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 * 1000) / timescale);
+      } else {
+        sampleCompositionTimeOffsetTable[i] = 0;
+      }
+      sampleDecodingTimeTable[i] =
+          Util.scaleLargeTimestamp(cumulativeTime, 1000, 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, 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);
+    }
+    if (sbgp.readInt() != 1) {
+      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 decription in sgpd found (unsupported)");
+      }
+    } else if (sgpdVersion >= 2) {
+      sgpd.skipBytes(4);
+    }
+    if (sgpd.readUnsignedInt() != 1) {
+      throw new ParserException("Entry count in sgpd != 1 (unsupported).");
+    }
+    // CencSampleEncryptionInformationGroupEntry
+    sgpd.skipBytes(2);
+    boolean isProtected = sgpd.readUnsignedByte() == 1;
+    if (!isProtected) {
+      return;
+    }
+    int initVectorSize = sgpd.readUnsignedByte();
+    byte[] keyId = new byte[16];
+    sgpd.readBytes(keyId, 0, keyId.length);
+    out.definesEncryptionData = true;
+    out.trackEncryptionBox = new TrackEncryptionBox(isProtected, initVectorSize, keyId);
+  }
+
+  /**
+   * 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 extract the next sample in the current mdat atom.
+   * <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 extracted in part in the case that an exception is thrown. In
+   * this case the method can be called again to extract the remainder of the sample.
+   *
+   * @param input The {@link ExtractorInput} from which to read data.
+   * @return Whether a sample was extracted.
+   * @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.fragment.definesEncryptionData) {
+        sampleBytesWritten = appendSampleEncryptionData(currentTrackBundle);
+        sampleSize += sampleBytesWritten;
+      } else {
+        sampleBytesWritten = 0;
+      }
+      if (currentTrackBundle.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) {
+        sampleSize -= Atom.HEADER_SIZE;
+        input.skipFully(Atom.HEADER_SIZE);
+      }
+      parserState = STATE_READING_SAMPLE_CONTINUE;
+      sampleCurrentNalBytesRemaining = 0;
+    }
+
+    TrackFragment fragment = currentTrackBundle.fragment;
+    Track track = currentTrackBundle.track;
+    TrackOutput output = currentTrackBundle.output;
+    int sampleIndex = currentTrackBundle.currentSampleIndex;
+    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[] 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 (sampleBytesWritten < sampleSize) {
+        if (sampleCurrentNalBytesRemaining == 0) {
+          // Read the NAL length so that we know where we find the next one.
+          input.readFully(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
+          nalLength.setPosition(0);
+          sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
+          // Write a start code for the current NAL unit.
+          nalStartCode.setPosition(0);
+          output.sampleData(nalStartCode, 4);
+          sampleBytesWritten += 4;
+          sampleSize += nalUnitLengthFieldLengthDiff;
+          if (cea608TrackOutput != null) {
+            byte[] nalPayloadData = nalPayload.data;
+            // Peek the NAL unit type byte.
+            input.peekFully(nalPayloadData, 0, 1);
+            if ((nalPayloadData[0] & 0x1F) == NAL_UNIT_TYPE_SEI) {
+              // Read the whole SEI NAL unit into nalWrapper, including the NAL unit type byte.
+              nalPayload.reset(sampleCurrentNalBytesRemaining);
+              input.readFully(nalPayloadData, 0, sampleCurrentNalBytesRemaining);
+              // Write the SEI unit straight to the output.
+              output.sampleData(nalPayload, sampleCurrentNalBytesRemaining);
+              sampleBytesWritten += sampleCurrentNalBytesRemaining;
+              sampleCurrentNalBytesRemaining = 0;
+              // Unescape and process the SEI unit.
+              int unescapedLength = NalUnitUtil.unescapeStream(nalPayloadData, nalPayload.limit());
+              nalPayload.setPosition(1); // Skip the NAL unit type byte.
+              nalPayload.setLimit(unescapedLength);
+              CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalPayload,
+                  cea608TrackOutput);
+            }
+          }
+        } else {
+          // Write the payload of the NAL unit.
+          int writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false);
+          sampleBytesWritten += writtenBytes;
+          sampleCurrentNalBytesRemaining -= writtenBytes;
+        }
+      }
+    } else {
+      while (sampleBytesWritten < sampleSize) {
+        int writtenBytes = output.sampleData(input, sampleSize - sampleBytesWritten, false);
+        sampleBytesWritten += writtenBytes;
+      }
+    }
+
+    long sampleTimeUs = fragment.getSamplePresentationTime(sampleIndex) * 1000L;
+    @C.BufferFlags int sampleFlags = (fragment.definesEncryptionData ? C.BUFFER_FLAG_ENCRYPTED : 0)
+        | (fragment.sampleIsSyncFrameTable[sampleIndex] ? C.BUFFER_FLAG_KEY_FRAME : 0);
+    int sampleDescriptionIndex = fragment.header.sampleDescriptionIndex;
+    byte[] encryptionKey = null;
+    if (fragment.definesEncryptionData) {
+      encryptionKey = fragment.trackEncryptionBox != null
+          ? fragment.trackEncryptionBox.keyId
+          : track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex].keyId;
+    }
+    if (timestampAdjuster != null) {
+      sampleTimeUs = timestampAdjuster.adjustSampleTimestamp(sampleTimeUs);
+    }
+    output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey);
+
+    while (!pendingMetadataSampleInfos.isEmpty()) {
+      MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst();
+      pendingMetadataSampleBytes -= sampleInfo.size;
+      eventMessageTrackOutput.sampleMetadata(
+          sampleTimeUs + sampleInfo.presentationTimeDeltaUs,
+          C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null);
+    }
+
+    currentTrackBundle.currentSampleIndex++;
+    currentTrackBundle.currentSampleInTrackRun++;
+    if (currentTrackBundle.currentSampleInTrackRun
+        == fragment.trunLength[currentTrackBundle.currentTrackRunIndex]) {
+      currentTrackBundle.currentTrackRunIndex++;
+      currentTrackBundle.currentSampleInTrackRun = 0;
+      currentTrackBundle = null;
+    }
+    parserState = STATE_READING_SAMPLE_START;
+    return true;
+  }
+
+  /**
+   * 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;
+  }
+
+  /**
+   * Appends the corresponding encryption data to the {@link TrackOutput} contained in the given
+   * {@link TrackBundle}.
+   *
+   * @param trackBundle The {@link TrackBundle} that contains the {@link Track} for which the
+   *     Sample encryption data must be output.
+   * @return The number of written bytes.
+   */
+  private int appendSampleEncryptionData(TrackBundle trackBundle) {
+    TrackFragment trackFragment = trackBundle.fragment;
+    ParsableByteArray sampleEncryptionData = trackFragment.sampleEncryptionData;
+    int sampleDescriptionIndex = trackFragment.header.sampleDescriptionIndex;
+    TrackEncryptionBox encryptionBox = trackFragment.trackEncryptionBox != null
+        ? trackFragment.trackEncryptionBox
+        : trackBundle.track.sampleDescriptionEncryptionBoxes[sampleDescriptionIndex];
+    int vectorSize = encryptionBox.initializationVectorSize;
+    boolean subsampleEncryption = trackFragment
+        .sampleHasSubsampleEncryptionTable[trackBundle.currentSampleIndex];
+
+    // Write the signal byte, containing the vector size and the subsample encryption flag.
+    encryptionSignalByte.data[0] = (byte) (vectorSize | (subsampleEncryption ? 0x80 : 0));
+    encryptionSignalByte.setPosition(0);
+    TrackOutput output = trackBundle.output;
+    output.sampleData(encryptionSignalByte, 1);
+    // Write the vector.
+    output.sampleData(sampleEncryptionData, vectorSize);
+    // If we don't have subsample encryption data, we're done.
+    if (!subsampleEncryption) {
+      return 1 + vectorSize;
+    }
+    // Write the subsample encryption data.
+    int subsampleCount = sampleEncryptionData.readUnsignedShort();
+    sampleEncryptionData.skipBytes(-2);
+    int subsampleDataLength = 2 + 6 * subsampleCount;
+    output.sampleData(sampleEncryptionData, subsampleDataLength);
+    return 1 + vectorSize + subsampleDataLength;
+  }
+
+
+  /** Returns DrmInitData from leaf atoms. */
+  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 {
+
+    public final TrackFragment fragment;
+    public final TrackOutput output;
+
+    public Track track;
+    public DefaultSampleValues defaultSampleValues;
+    public int currentSampleIndex;
+    public int currentSampleInTrackRun;
+    public int currentTrackRunIndex;
+
+    public TrackBundle(TrackOutput output) {
+      fragment = new TrackFragment();
+      this.output = output;
+    }
+
+    public void init(Track track, DefaultSampleValues defaultSampleValues) {
+      this.track = Assertions.checkNotNull(track);
+      this.defaultSampleValues = Assertions.checkNotNull(defaultSampleValues);
+      output.format(track.format);
+      reset();
+    }
+
+    public void reset() {
+      fragment.reset();
+      currentSampleIndex = 0;
+      currentTrackRunIndex = 0;
+      currentSampleInTrackRun = 0;
+    }
+
+    public void updateDrmInitData(DrmInitData drmInitData) {
+      output.format(track.format.copyWithDrmInitData(drmInitData));
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/MetadataUtil.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.mp4;
+
+import android.util.Log;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.id3.ApicFrame;
+import com.google.android.exoplayer2.metadata.id3.CommentFrame;
+import com.google.android.exoplayer2.metadata.id3.Id3Frame;
+import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Parses metadata items stored in ilst atoms.
+ */
+/* package */ final class MetadataUtil {
+
+  private static final String TAG = "MetadataUtil";
+
+  // Codes that start with the copyright character (omitted) and have equivalent ID3 frames.
+  private static final int SHORT_TYPE_NAME_1 = Util.getIntegerCodeForString("nam");
+  private static final int SHORT_TYPE_NAME_2 = Util.getIntegerCodeForString("trk");
+  private static final int SHORT_TYPE_COMMENT = Util.getIntegerCodeForString("cmt");
+  private static final int SHORT_TYPE_YEAR = Util.getIntegerCodeForString("day");
+  private static final int SHORT_TYPE_ARTIST = Util.getIntegerCodeForString("ART");
+  private static final int SHORT_TYPE_ENCODER = Util.getIntegerCodeForString("too");
+  private static final int SHORT_TYPE_ALBUM = Util.getIntegerCodeForString("alb");
+  private static final int SHORT_TYPE_COMPOSER_1 = Util.getIntegerCodeForString("com");
+  private static final int SHORT_TYPE_COMPOSER_2 = Util.getIntegerCodeForString("wrt");
+  private static final int SHORT_TYPE_LYRICS = Util.getIntegerCodeForString("lyr");
+  private static final int SHORT_TYPE_GENRE = Util.getIntegerCodeForString("gen");
+
+  // Codes that have equivalent ID3 frames.
+  private static final int TYPE_COVER_ART = Util.getIntegerCodeForString("covr");
+  private static final int TYPE_GENRE = Util.getIntegerCodeForString("gnre");
+  private static final int TYPE_GROUPING = Util.getIntegerCodeForString("grp");
+  private static final int TYPE_DISK_NUMBER = Util.getIntegerCodeForString("disk");
+  private static final int TYPE_TRACK_NUMBER = Util.getIntegerCodeForString("trkn");
+  private static final int TYPE_TEMPO = Util.getIntegerCodeForString("tmpo");
+  private static final int TYPE_COMPILATION = Util.getIntegerCodeForString("cpil");
+  private static final int TYPE_ALBUM_ARTIST = Util.getIntegerCodeForString("aART");
+  private static final int TYPE_SORT_TRACK_NAME = Util.getIntegerCodeForString("sonm");
+  private static final int TYPE_SORT_ALBUM = Util.getIntegerCodeForString("soal");
+  private static final int TYPE_SORT_ARTIST = Util.getIntegerCodeForString("soar");
+  private static final int TYPE_SORT_ALBUM_ARTIST = Util.getIntegerCodeForString("soaa");
+  private static final int TYPE_SORT_COMPOSER = Util.getIntegerCodeForString("soco");
+
+  // Types that do not have equivalent ID3 frames.
+  private static final int TYPE_RATING = Util.getIntegerCodeForString("rtng");
+  private static final int TYPE_GAPLESS_ALBUM = Util.getIntegerCodeForString("pgap");
+  private static final int TYPE_TV_SORT_SHOW = Util.getIntegerCodeForString("sosn");
+  private static final int TYPE_TV_SHOW = Util.getIntegerCodeForString("tvsh");
+
+  // Type for items that are intended for internal use by the player.
+  private static final int TYPE_INTERNAL = Util.getIntegerCodeForString("----");
+
+  // Standard genres.
+  private static final String[] STANDARD_GENRES = new String[] {
+      // These are the official ID3v1 genres.
+      "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz",
+      "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno",
+      "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno",
+      "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental",
+      "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass", "Soul",
+      "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic",
+      "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream",
+      "Southern Rock", "Comedy", "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle",
+      "Native American", "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer",
+      "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll",
+      "Hard Rock",
+      // These were made up by the authors of Winamp and later added to the ID3 spec.
+      "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival",
+      "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock",
+      "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour",
+      "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
+      "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad",
+      "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella",
+      "Euro-House", "Dance Hall",
+      // These were med up by the authors of Winamp but have not been added to the ID3 spec.
+      "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "BritPop", "Negerpunk",
+      "Polsk Punk", "Beat", "Christian Gangsta Rap", "Heavy Metal", "Black Metal", "Crossover",
+      "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime",
+      "Jpop", "Synthpop"
+  };
+
+  private static final String LANGUAGE_UNDEFINED = "und";
+
+  private MetadataUtil() {}
+
+  /**
+   * Parses a single ilst element from a {@link ParsableByteArray}. The element is read starting
+   * from the current position of the {@link ParsableByteArray}, and the position is advanced by
+   * the size of the element. The position is advanced even if the element's type is unrecognized.
+   *
+   * @param ilst Holds the data to be parsed.
+   * @return The parsed element, or null if the element's type was not recognized.
+   */
+  public static Metadata.Entry parseIlstElement(ParsableByteArray ilst) {
+    int position = ilst.getPosition();
+    int endPosition = position + ilst.readInt();
+    int type = ilst.readInt();
+    int typeTopByte = (type >> 24) & 0xFF;
+    try {
+      if (typeTopByte == '\u00A9' /* Copyright char */
+          || typeTopByte == '\uFFFD' /* Replacement char */) {
+        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);
+    }
+  }
+
+  private static TextInformationFrame parseTextAttribute(int type, String id,
+      ParsableByteArray data) {
+    int atomSize = data.readInt();
+    int atomType = data.readInt();
+    if (atomType == Atom.TYPE_data) {
+      data.skipBytes(8); // version (1), flags (3), empty (4)
+      String value = data.readNullTerminatedString(atomSize - 16);
+      return new TextInformationFrame(id, null, value);
+    }
+    Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type));
+    return null;
+  }
+
+  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;
+  }
+
+  private static Id3Frame parseUint8Attribute(int type, String id, ParsableByteArray data,
+      boolean isTextInformationFrame, boolean isBoolean) {
+    int value = parseUint8AttributeValue(data);
+    if (isBoolean) {
+      value = Math.min(1, value);
+    }
+    if (value >= 0) {
+      return isTextInformationFrame ? new TextInformationFrame(id, null, Integer.toString(value))
+          : new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value));
+    }
+    Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type));
+    return null;
+  }
+
+  private static TextInformationFrame parseIndexAndCountAttribute(int type, String attributeName,
+      ParsableByteArray data) {
+    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, null, value);
+      }
+    }
+    Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type));
+    return null;
+  }
+
+  private static TextInformationFrame parseStandardGenreAttribute(ParsableByteArray data) {
+    int genreCode = parseUint8AttributeValue(data);
+    String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
+        ? STANDARD_GENRES[genreCode - 1] : null;
+    if (genreString != null) {
+      return new TextInformationFrame("TCON", null, genreString);
+    }
+    Log.w(TAG, "Failed to parse standard genre code");
+    return null;
+  }
+
+  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, null, 3 /* Cover (front) */, pictureData);
+    }
+    Log.w(TAG, "Failed to parse cover art attribute");
+    return null;
+  }
+
+  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 (!"com.apple.iTunes".equals(domain) || !"iTunSMPB".equals(name) || dataAtomPosition == -1) {
+      // We're only interested in iTunSMPB.
+      return null;
+    }
+    data.setPosition(dataAtomPosition);
+    data.skipBytes(16); // size (4), type (4), version (1), flags (3), empty (4)
+    String value = data.readNullTerminatedString(dataAtomSize - 16);
+    return new CommentFrame(LANGUAGE_UNDEFINED, name, value);
+  }
+
+  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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
@@ -0,0 +1,536 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.mp4;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Stack;
+
+/**
+ * Extracts data from an unfragmented MP4 file.
+ */
+public final class Mp4Extractor implements Extractor, SeekMap {
+
+  /**
+   * Factory for {@link Mp4Extractor} instances.
+   */
+  public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+    @Override
+    public Extractor[] createExtractors() {
+      return new Extractor[] {new Mp4Extractor()};
+    }
+
+  };
+
+  /**
+   * Parser states.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({STATE_READING_ATOM_HEADER, STATE_READING_ATOM_PAYLOAD, STATE_READING_SAMPLE})
+  private @interface State {}
+  private static final int STATE_READING_ATOM_HEADER = 0;
+  private static final int STATE_READING_ATOM_PAYLOAD = 1;
+  private static final int STATE_READING_SAMPLE = 2;
+
+  // Brand stored in the ftyp atom for QuickTime media.
+  private static final int BRAND_QUICKTIME = Util.getIntegerCodeForString("qt  ");
+
+  /**
+   * 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;
+
+  // Temporary arrays.
+  private final ParsableByteArray nalStartCode;
+  private final ParsableByteArray nalLength;
+
+  private final ParsableByteArray atomHeader;
+  private final Stack<ContainerAtom> containerAtoms;
+
+  @State
+  private int parserState;
+  private int atomType;
+  private long atomSize;
+  private int atomHeaderBytesRead;
+  private ParsableByteArray atomData;
+
+  private int sampleBytesWritten;
+  private int sampleCurrentNalBytesRemaining;
+
+  // Extractor outputs.
+  private ExtractorOutput extractorOutput;
+  private Mp4Track[] tracks;
+  private long durationUs;
+  private boolean isQuickTime;
+
+  public Mp4Extractor() {
+    atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
+    containerAtoms = new Stack<>();
+    nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
+    nalLength = new ParsableByteArray(4);
+  }
+
+  @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;
+    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 long getPosition(long timeUs) {
+    long earliestSamplePosition = Long.MAX_VALUE;
+    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);
+      }
+      long offset = sampleTable.offsets[sampleIndex];
+      if (offset < earliestSamplePosition) {
+        earliestSamplePosition = offset;
+      }
+    }
+    return earliestSamplePosition;
+  }
+
+  // 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.LONG_SIZE_PREFIX) {
+      // Read the extended atom size.
+      int headerBytesRemaining = Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE;
+      input.readFully(atomHeader.data, Atom.HEADER_SIZE, headerBytesRemaining);
+      atomHeaderBytesRead += headerBytesRemaining;
+      atomSize = atomHeader.readUnsignedLongToLong();
+    }
+
+    if (shouldParseContainerAtom(atomType)) {
+      long endPosition = input.getPosition() + atomSize - atomHeaderBytesRead;
+      containerAtoms.add(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();
+    }
+  }
+
+  /**
+   * Process an ftyp atom to determine whether the media is QuickTime.
+   *
+   * @param atomData The ftyp atom data.
+   * @return Whether the media is QuickTime.
+   */
+  private static boolean processFtypAtom(ParsableByteArray atomData) {
+    atomData.setPosition(Atom.HEADER_SIZE);
+    int majorBrand = atomData.readInt();
+    if (majorBrand == BRAND_QUICKTIME) {
+      return true;
+    }
+    atomData.skipBytes(4); // minor_version
+    while (atomData.bytesLeft() > 0) {
+      if (atomData.readInt() == BRAND_QUICKTIME) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Updates the stored track metadata to reflect the contents of the specified moov atom.
+   */
+  private void processMoovAtom(ContainerAtom moov) throws ParserException {
+    long durationUs = C.TIME_UNSET;
+    List<Mp4Track> tracks = new ArrayList<>();
+    long earliestSampleOffset = Long.MAX_VALUE;
+
+    Metadata metadata = null;
+    GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
+    Atom.LeafAtom udta = moov.getLeafAtomOfType(Atom.TYPE_udta);
+    if (udta != null) {
+      metadata = AtomParsers.parseUdta(udta, isQuickTime);
+      if (metadata != null) {
+        gaplessInfoHolder.setFromMetadata(metadata);
+      }
+    }
+
+    for (int i = 0; i < moov.containerChildren.size(); i++) {
+      Atom.ContainerAtom atom = moov.containerChildren.get(i);
+      if (atom.type != Atom.TYPE_trak) {
+        continue;
+      }
+
+      Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd),
+          C.TIME_UNSET, null, isQuickTime);
+      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;
+      }
+
+      Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i));
+      // Each sample has up to three bytes of overhead for the start code that replaces its length.
+      // Allow ten source samples per output sample, like the platform extractor.
+      int maxInputSize = trackSampleTable.maximumSize + 3 * 10;
+      Format format = track.format.copyWithMaxInputSize(maxInputSize);
+      if (track.type == C.TRACK_TYPE_AUDIO) {
+        if (gaplessInfoHolder.hasGaplessInfo()) {
+          format = format.copyWithGaplessInfo(gaplessInfoHolder.encoderDelay,
+              gaplessInfoHolder.encoderPadding);
+        }
+        if (metadata != null) {
+          format = format.copyWithMetadata(metadata);
+        }
+      }
+      mp4Track.trackOutput.format(format);
+
+      durationUs = Math.max(durationUs, track.durationUs);
+      tracks.add(mp4Track);
+
+      long firstSampleOffset = trackSampleTable.offsets[0];
+      if (firstSampleOffset < earliestSampleOffset) {
+        earliestSampleOffset = firstSampleOffset;
+      }
+    }
+    this.durationUs = durationUs;
+    this.tracks = tracks.toArray(new Mp4Track[tracks.size()]);
+    extractorOutput.endTracks();
+    extractorOutput.seekMap(this);
+  }
+
+  /**
+   * 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 {
+    int trackIndex = getTrackIndexOfEarliestCurrentSample();
+    if (trackIndex == C.INDEX_UNSET) {
+      return RESULT_END_OF_INPUT;
+    }
+    Mp4Track track = tracks[trackIndex];
+    TrackOutput trackOutput = track.trackOutput;
+    int sampleIndex = track.sampleIndex;
+    long position = track.sampleTable.offsets[sampleIndex];
+    int sampleSize = track.sampleTable.sizes[sampleIndex];
+    if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) {
+      // The sample information is contained in a cdat atom. The header must be discarded for
+      // committing.
+      position += Atom.HEADER_SIZE;
+      sampleSize -= Atom.HEADER_SIZE;
+    }
+    long skipAmount = position - input.getPosition() + sampleBytesWritten;
+    if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) {
+      positionHolder.position = position;
+      return RESULT_SEEK;
+    }
+    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(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
+          nalLength.setPosition(0);
+          sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
+          // 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);
+          sampleBytesWritten += writtenBytes;
+          sampleCurrentNalBytesRemaining -= writtenBytes;
+        }
+      }
+    } else {
+      while (sampleBytesWritten < sampleSize) {
+        int writtenBytes = trackOutput.sampleData(input, sampleSize - sampleBytesWritten, false);
+        sampleBytesWritten += writtenBytes;
+        sampleCurrentNalBytesRemaining -= writtenBytes;
+      }
+    }
+    trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex],
+        track.sampleTable.flags[sampleIndex], sampleSize, 0, null);
+    track.sampleIndex++;
+    sampleBytesWritten = 0;
+    sampleCurrentNalBytesRemaining = 0;
+    return RESULT_CONTINUE;
+  }
+
+  /**
+   * Returns the index of the track that contains the earliest current sample, or
+   * {@link C#INDEX_UNSET} if no samples remain.
+   */
+  private int getTrackIndexOfEarliestCurrentSample() {
+    int earliestSampleTrackIndex = C.INDEX_UNSET;
+    long earliestSampleOffset = Long.MAX_VALUE;
+    for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
+      Mp4Track track = tracks[trackIndex];
+      int sampleIndex = track.sampleIndex;
+      if (sampleIndex == track.sampleTable.sampleCount) {
+        continue;
+      }
+
+      long trackSampleOffset = track.sampleTable.offsets[sampleIndex];
+      if (trackSampleOffset < earliestSampleOffset) {
+        earliestSampleOffset = trackSampleOffset;
+        earliestSampleTrackIndex = trackIndex;
+      }
+    }
+
+    return earliestSampleTrackIndex;
+  }
+
+  /**
+   * 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;
+    }
+  }
+
+  /**
+   * 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;
+  }
+
+  /**
+   * 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;
+  }
+
+  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;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtil.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.mp4;
+
+import android.util.Log;
+import android.util.Pair;
+import 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 PSSH atom for a given {@link UUID} containing the given scheme specific data.
+   *
+   * @param uuid The UUID of the scheme.
+   * @param data The scheme specific data.
+   * @return The PSSH atom.
+   */
+  public static byte[] buildPsshAtom(UUID uuid, byte[] data) {
+    int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */ + data.length;
+    ByteBuffer psshBox = ByteBuffer.allocate(psshBoxLength);
+    psshBox.putInt(psshBoxLength);
+    psshBox.putInt(Atom.TYPE_pssh);
+    psshBox.putInt(0 /* version=0, flags=0 */);
+    psshBox.putLong(uuid.getMostSignificantBits());
+    psshBox.putLong(uuid.getLeastSignificantBits());
+    psshBox.putInt(data.length);
+    psshBox.put(data);
+    return psshBox.array();
+  }
+
+  /**
+   * 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 UUID parseUuid(byte[] atom) {
+    Pair<UUID, byte[]> parsedAtom = parsePsshAtom(atom);
+    if (parsedAtom == null) {
+      return null;
+    }
+    return parsedAtom.first;
+  }
+
+  /**
+   * 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 byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) {
+    Pair<UUID, byte[]> parsedAtom = parsePsshAtom(atom);
+    if (parsedAtom == null) {
+      return null;
+    }
+    if (uuid != null && !uuid.equals(parsedAtom.first)) {
+      Log.w(TAG, "UUID mismatch. Expected: " + uuid + ", got: " + parsedAtom.first + ".");
+      return null;
+    }
+    return parsedAtom.second;
+  }
+
+  /**
+   * Parses the UUID and scheme specific data from a PSSH atom. Version 0 and 1 PSSH atoms are
+   * supported.
+   *
+   * @param atom The atom to parse.
+   * @return A pair consisting of the parsed UUID and scheme specific data. Null if the input is
+   *     not a valid PSSH atom, or if the PSSH atom has an unsupported version.
+   */
+  private static Pair<UUID, byte[]> 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 Pair.create(uuid, data);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/Sniffer.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.mp4;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+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[] {
+      Util.getIntegerCodeForString("isom"),
+      Util.getIntegerCodeForString("iso2"),
+      Util.getIntegerCodeForString("iso3"),
+      Util.getIntegerCodeForString("iso4"),
+      Util.getIntegerCodeForString("iso5"),
+      Util.getIntegerCodeForString("iso6"),
+      Util.getIntegerCodeForString("avc1"),
+      Util.getIntegerCodeForString("hvc1"),
+      Util.getIntegerCodeForString("hev1"),
+      Util.getIntegerCodeForString("mp41"),
+      Util.getIntegerCodeForString("mp42"),
+      Util.getIntegerCodeForString("3g2a"),
+      Util.getIntegerCodeForString("3g2b"),
+      Util.getIntegerCodeForString("3gr6"),
+      Util.getIntegerCodeForString("3gs6"),
+      Util.getIntegerCodeForString("3ge6"),
+      Util.getIntegerCodeForString("3gg6"),
+      Util.getIntegerCodeForString("M4V "),
+      Util.getIntegerCodeForString("M4A "),
+      Util.getIntegerCodeForString("f4v "),
+      Util.getIntegerCodeForString("kddi"),
+      Util.getIntegerCodeForString("M4VP"),
+      Util.getIntegerCodeForString("qt  "), // Apple QuickTime
+      Util.getIntegerCodeForString("MSNV"), // Sony PSP
+  };
+
+  /**
+   * 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.LONG_SIZE_PREFIX) {
+        headerSize = Atom.LONG_HEADER_SIZE;
+        input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE);
+        buffer.setLimit(Atom.LONG_HEADER_SIZE);
+        atomSize = buffer.readUnsignedLongToLong();
+      }
+
+      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) {
+        // 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 == Util.getIntegerCodeForString("3gp")) {
+      return true;
+    }
+    for (int compatibleBrand : COMPATIBLE_BRANDS) {
+      if (compatibleBrand == brand) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private Sniffer() {
+    // Prevent instantiation.
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/Track.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 com.google.android.exoplayer2.extractor.mp4;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+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.
+   */
+  @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;
+
+  /**
+   * Track encryption boxes for the different track sample descriptions. Entries may be null.
+   */
+  public final TrackEncryptionBox[] sampleDescriptionEncryptionBoxes;
+
+  /**
+   * Durations of edit list segments in the movie timescale. Null if there is no edit list.
+   */
+  public final long[] editListDurations;
+
+  /**
+   * Media times for edit list segments in the track timescale. Null if there is no edit list.
+   */
+  public final long[] editListMediaTimes;
+
+  /**
+   * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. -1 for
+   * other track types.
+   */
+  public final int nalUnitLengthFieldLength;
+
+  public Track(int id, int type, long timescale, long movieTimescale, long durationUs,
+      Format format, @Transformation int sampleTransformation,
+      TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength,
+      long[] editListDurations, long[] editListMediaTimes) {
+    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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackEncryptionBox.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 com.google.android.exoplayer2.extractor.mp4;
+
+/**
+ * Encapsulates information parsed from a track encryption (tenc) box or sample group description 
+ * (sgpd) box in an MP4 stream.
+ */
+public final class TrackEncryptionBox {
+
+  /**
+   * Indicates the encryption state of the samples in the sample group.
+   */
+  public final boolean isEncrypted;
+
+  /**
+   * The initialization vector size in bytes for the samples in the corresponding sample group.
+   */
+  public final int initializationVectorSize;
+
+  /**
+   * The key identifier for the samples in the corresponding sample group.
+   */
+  public final byte[] keyId;
+
+  /**
+   * @param isEncrypted Indicates the encryption state of the samples in the sample group.
+   * @param initializationVectorSize The initialization vector size in bytes for the samples in the
+   *     corresponding sample group.
+   * @param keyId The key identifier for the samples in the corresponding sample group.
+   */
+  public TrackEncryptionBox(boolean isEncrypted, int initializationVectorSize, byte[] keyId) {
+    this.isEncrypted = isEncrypted;
+    this.initializationVectorSize = initializationVectorSize;
+    this.keyId = keyId;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackFragment.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.mp4;
+
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import 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];
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.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 com.google.android.exoplayer2.extractor.mp4;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Sample table for a track in an MP4 file.
+ */
+/* package */ final class TrackSampleTable {
+
+  /**
+   * 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;
+
+  public TrackSampleTable(long[] offsets, int[] sizes, int maximumSize, long[] timestampsUs,
+      int[] flags) {
+    Assertions.checkArgument(sizes.length == timestampsUs.length);
+    Assertions.checkArgument(offsets.length == timestampsUs.length);
+    Assertions.checkArgument(flags.length == timestampsUs.length);
+
+    this.offsets = offsets;
+    this.sizes = sizes;
+    this.maximumSize = maximumSize;
+    this.timestampsUs = timestampsUs;
+    this.flags = flags;
+    sampleCount = offsets.length;
+  }
+
+  /**
+   * 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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * Used to seek in an Ogg stream.
+ */
+/* package */ final class DefaultOggSeeker implements OggSeeker {
+
+  //@VisibleForTesting
+  public static final int MATCH_RANGE = 72000;
+  //@VisibleForTesting
+  public static final int MATCH_BYTE_RANGE = 100000;
+  private static final int DEFAULT_OFFSET = 30000;
+
+  private static final int STATE_SEEK_TO_END = 0;
+  private static final int STATE_READ_LAST_PAGE = 1;
+  private static final int STATE_SEEK = 2;
+  private static final int STATE_IDLE = 3;
+
+  private final OggPageHeader pageHeader = new OggPageHeader();
+  private final long startPosition;
+  private final long endPosition;
+  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 startPosition Start position of the payload (inclusive).
+   * @param endPosition End position of the payload (exclusive).
+   * @param streamReader StreamReader instance which owns this OggSeeker
+   * @param firstPayloadPageSize The total size of the first payload page, in bytes.
+   * @param firstPayloadPageGranulePosition The granule position of the first payload page.
+   */
+  public DefaultOggSeeker(long startPosition, long endPosition, StreamReader streamReader,
+      int firstPayloadPageSize, long firstPayloadPageGranulePosition) {
+    Assertions.checkArgument(startPosition >= 0 && endPosition > startPosition);
+    this.streamReader = streamReader;
+    this.startPosition = startPosition;
+    this.endPosition = endPosition;
+    if (firstPayloadPageSize == endPosition - startPosition) {
+      totalGranules = firstPayloadPageGranulePosition;
+      state = STATE_IDLE;
+    } else {
+      state = STATE_SEEK_TO_END;
+    }
+  }
+
+  @Override
+  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 = endPosition - 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 currentGranule;
+        if (targetGranule == 0) {
+          currentGranule = 0;
+        } else {
+          long position = getNextSeekPosition(targetGranule, input);
+          if (position >= 0) {
+            return position;
+          }
+          currentGranule = skipToPageOfGranule(input, targetGranule, -(position + 2));
+        }
+        state = STATE_IDLE;
+        return -(currentGranule + 2);
+      default:
+        // Never happens.
+        throw new IllegalStateException();
+    }
+  }
+
+  @Override
+  public long startSeek(long timeUs) {
+    Assertions.checkArgument(state == STATE_IDLE || state == STATE_SEEK);
+    targetGranule = timeUs == 0 ? 0 : streamReader.convertTimeToGranule(timeUs);
+    state = STATE_SEEK;
+    resetSeeking();
+    return targetGranule;
+  }
+
+  @Override
+  public OggSeekMap createSeekMap() {
+    return totalGranules != 0 ? new OggSeekMap() : null;
+  }
+
+  //@VisibleForTesting
+  public void resetSeeking() {
+    start = startPosition;
+    end = endPosition;
+    startGranule = 0;
+    endGranule = totalGranules;
+  }
+
+  /**
+   * Returns a position converging to the {@code targetGranule} to which the {@link ExtractorInput}
+   * has to seek and then be passed for another call until a negative number is returned. If a
+   * negative number is returned the input is at a position which is before the target page and at
+   * which it is sensible to just skip pages to the target granule and pre-roll instead of doing
+   * another seek request.
+   *
+   * @param targetGranule the target granule position to seek to.
+   * @param input the {@link ExtractorInput} to read from.
+   * @return the position to seek the {@link ExtractorInput} to for a next call or
+   *     -(currentGranule + 2) if it's close enough to skip to the target page.
+   * @throws IOException thrown if reading from the input fails.
+   * @throws InterruptedException thrown if interrupted while reading from the input.
+   */
+  //@VisibleForTesting
+  public long getNextSeekPosition(long targetGranule, ExtractorInput input)
+      throws IOException, InterruptedException {
+    if (start == end) {
+      return -(startGranule + 2);
+    }
+
+    long initialPosition = input.getPosition();
+    if (!skipToNextPage(input, end)) {
+      if (start == initialPosition) {
+        throw new IOException("No ogg page can be found.");
+      }
+      return start;
+    }
+
+    pageHeader.populate(input, false);
+    input.resetPeekPosition();
+
+    long granuleDistance = targetGranule - pageHeader.granulePosition;
+    int pageSize = pageHeader.headerSize + pageHeader.bodySize;
+    if (granuleDistance < 0 || granuleDistance > MATCH_RANGE) {
+      if (granuleDistance < 0) {
+        end = initialPosition;
+        endGranule = pageHeader.granulePosition;
+      } else {
+        start = input.getPosition() + pageSize;
+        startGranule = pageHeader.granulePosition;
+        if (end - start + pageSize < MATCH_BYTE_RANGE) {
+          input.skipFully(pageSize);
+          return -(startGranule + 2);
+        }
+      }
+
+      if (end - start < MATCH_BYTE_RANGE) {
+        end = start;
+        return start;
+      }
+
+      long offset = pageSize * (granuleDistance <= 0 ? 2 : 1);
+      long nextPosition = input.getPosition() - offset
+          + (granuleDistance * (end - start) / (endGranule - startGranule));
+
+      nextPosition = Math.max(nextPosition, start);
+      nextPosition = Math.min(nextPosition, end - 1);
+      return nextPosition;
+    }
+
+    // position accepted (before target granule and within MATCH_RANGE)
+    input.skipFully(pageSize);
+    return -(pageHeader.granulePosition + 2);
+  }
+
+  private long getEstimatedPosition(long position, long granuleDistance, long offset) {
+    position += (granuleDistance * (endPosition - startPosition) / totalGranules) - offset;
+    if (position < startPosition) {
+      position = startPosition;
+    }
+    if (position >= endPosition) {
+      position = endPosition - 1;
+    }
+    return position;
+  }
+
+  private class OggSeekMap implements SeekMap {
+
+    @Override
+    public boolean isSeekable() {
+      return true;
+    }
+
+    @Override
+    public long getPosition(long timeUs) {
+      if (timeUs == 0) {
+        return startPosition;
+      }
+      long granule = streamReader.convertTimeToGranule(timeUs);
+      return getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET);
+    }
+
+    @Override
+    public long getDurationUs() {
+      return streamReader.convertGranuleToTime(totalGranules);
+    }
+
+  }
+
+  /**
+   * Skips to the next page.
+   *
+   * @param input The {@code ExtractorInput} to skip to the next page.
+   * @throws IOException thrown if peeking/reading from the input fails.
+   * @throws InterruptedException thrown if interrupted while peeking/reading from the input.
+   * @throws EOFException if the next page can't be found before the end of the input.
+   */
+  //@VisibleForTesting
+  void skipToNextPage(ExtractorInput input) throws IOException, InterruptedException {
+    if (!skipToNextPage(input, endPosition)) {
+      // 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 until Searches until this position.
+   * @return true if the next page is found.
+   * @throws IOException thrown if peeking/reading from the input fails.
+   * @throws InterruptedException thrown if interrupted while peeking/reading from the input.
+   */
+  //@VisibleForTesting
+  boolean skipToNextPage(ExtractorInput input, long until)
+      throws IOException, InterruptedException {
+    until = Math.min(until + 3, endPosition);
+    byte[] buffer = new byte[2048];
+    int peekLength = buffer.length;
+    while (true) {
+      if (input.getPosition() + peekLength > until) {
+        // Make sure to not peek beyond the end of the input.
+        peekLength = (int) (until - 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 thrown if reading from the input fails.
+   * @throws InterruptedException thrown if interrupted while reading from the input.
+   */
+  //@VisibleForTesting
+  long readGranuleOfLastPage(ExtractorInput input)
+      throws IOException, InterruptedException {
+    skipToNextPage(input);
+    pageHeader.reset();
+    while ((pageHeader.type & 0x04) != 0x04 && input.getPosition() < endPosition) {
+      pageHeader.populate(input, false);
+      input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
+    }
+    return pageHeader.granulePosition;
+  }
+
+  /**
+   * Skips to the position of the start of the page containing the {@code targetGranule} and
+   * returns the granule of the page previous to the target page.
+   *
+   * @param input the {@link ExtractorInput} to read from.
+   * @param targetGranule the target granule.
+   * @param currentGranule the current granule or -1 if it's unknown.
+   * @return the granule of the prior page or the {@code currentGranule} if there isn't a prior
+   *     page.
+   * @throws ParserException thrown if populating the page header fails.
+   * @throws IOException thrown if reading from the input fails.
+   * @throws InterruptedException thrown if interrupted while reading from the input.
+   */
+  //@VisibleForTesting
+  long skipToPageOfGranule(ExtractorInput input, long targetGranule, long currentGranule)
+      throws IOException, InterruptedException {
+    pageHeader.populate(input, false);
+    while (pageHeader.granulePosition < targetGranule) {
+      input.skipFully(pageHeader.headerSize + pageHeader.bodySize);
+      // Store in a member field to be able to resume after IOExceptions.
+      currentGranule = pageHeader.granulePosition;
+      // Peek next header.
+      pageHeader.populate(input, false);
+    }
+    input.resetPeekPosition();
+    return currentGranule;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/FlacReader.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.util.FlacStreamInfo;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * {@link StreamReader} to extract Flac data out of Ogg byte stream.
+ */
+/* package */ final class FlacReader extends StreamReader {
+
+  private static final byte AUDIO_PACKET_TYPE = (byte) 0xFF;
+  private static final byte SEEKTABLE_PACKET_TYPE = 0x03;
+
+  private static final int FRAME_HEADER_SAMPLE_NUMBER_OFFSET = 4;
+
+  private FlacStreamInfo streamInfo;
+  private 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) {
+      streamInfo = 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)
+      throws IOException, InterruptedException {
+    byte[] data = packet.data;
+    if (streamInfo == null) {
+      streamInfo = new FlacStreamInfo(data, 17);
+      byte[] metadata = Arrays.copyOfRange(data, 9, packet.limit());
+      metadata[4] = (byte) 0x80; // Set the last metadata block flag, ignore the other blocks
+      List<byte[]> initializationData = Collections.singletonList(metadata);
+      setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_FLAC, null,
+          Format.NO_VALUE, streamInfo.bitRate(), streamInfo.channels, streamInfo.sampleRate,
+          initializationData, null, 0, null);
+    } else if ((data[0] & 0x7F) == SEEKTABLE_PACKET_TYPE) {
+      flacOggSeeker = new FlacOggSeeker();
+      flacOggSeeker.parseSeekTable(packet);
+    } else if (isAudioPacket(data)) {
+      if (flacOggSeeker != null) {
+        flacOggSeeker.setFirstFrameOffset(position);
+        setupData.oggSeeker = flacOggSeeker;
+      }
+      return false;
+    }
+    return true;
+  }
+
+  private int getFlacFrameBlockSize(ParsableByteArray packet) {
+    int blockSizeCode = (packet.data[2] & 0xFF) >> 4;
+    switch (blockSizeCode) {
+      case 1:
+        return 192;
+      case 2:
+      case 3:
+      case 4:
+      case 5:
+        return 576 << (blockSizeCode - 2);
+      case 6:
+      case 7:
+        // skip the sample number
+        packet.skipBytes(FRAME_HEADER_SAMPLE_NUMBER_OFFSET);
+        packet.readUtf8EncodedLong();
+        int value = blockSizeCode == 6 ? packet.readUnsignedByte() : packet.readUnsignedShort();
+        packet.setPosition(0);
+        return value + 1;
+      case 8:
+      case 9:
+      case 10:
+      case 11:
+      case 12:
+      case 13:
+      case 14:
+      case 15:
+        return 256 << (blockSizeCode - 8);
+    }
+    return -1;
+  }
+
+  private class FlacOggSeeker implements OggSeeker, SeekMap {
+
+    private static final int METADATA_LENGTH_OFFSET = 1;
+    private static final int SEEK_POINT_SIZE = 18;
+
+    private long[] seekPointGranules;
+    private long[] seekPointOffsets;
+    private long firstFrameOffset;
+    private long pendingSeekGranule;
+
+    public FlacOggSeeker() {
+      firstFrameOffset = -1;
+      pendingSeekGranule = -1;
+    }
+
+    public void setFirstFrameOffset(long firstFrameOffset) {
+      this.firstFrameOffset = firstFrameOffset;
+    }
+
+    /**
+     * Parses a FLAC file seek table metadata structure and initializes internal fields.
+     *
+     * @param data A {@link ParsableByteArray} including whole seek table metadata block. Its
+     *     position should be set to the beginning of the block.
+     * @see <a href="https://xiph.org/flac/format.html#metadata_block_seektable">FLAC format
+     *     METADATA_BLOCK_SEEKTABLE</a>
+     */
+    public void parseSeekTable(ParsableByteArray data) {
+      data.skipBytes(METADATA_LENGTH_OFFSET);
+      int length = data.readUnsignedInt24();
+      int numberOfSeekPoints = length / SEEK_POINT_SIZE;
+      seekPointGranules = new long[numberOfSeekPoints];
+      seekPointOffsets = new long[numberOfSeekPoints];
+      for (int i = 0; i < numberOfSeekPoints; i++) {
+        seekPointGranules[i] = data.readLong();
+        seekPointOffsets[i] = data.readLong();
+        data.skipBytes(2); // Skip "Number of samples in the target frame."
+      }
+    }
+
+    @Override
+    public long read(ExtractorInput input) throws IOException, InterruptedException {
+      if (pendingSeekGranule >= 0) {
+        long result = -(pendingSeekGranule + 2);
+        pendingSeekGranule = -1;
+        return result;
+      }
+      return -1;
+    }
+
+    @Override
+    public long startSeek(long timeUs) {
+      long granule = convertTimeToGranule(timeUs);
+      int index = Util.binarySearchFloor(seekPointGranules, granule, true, true);
+      pendingSeekGranule = seekPointGranules[index];
+      return granule;
+    }
+
+    @Override
+    public SeekMap createSeekMap() {
+      return this;
+    }
+
+    @Override
+    public boolean isSeekable() {
+      return true;
+    }
+
+    @Override
+    public long getPosition(long timeUs) {
+      long granule = convertTimeToGranule(timeUs);
+      int index = Util.binarySearchFloor(seekPointGranules, granule, true, true);
+      return firstFrameOffset + seekPointOffsets[index];
+    }
+
+    @Override
+    public long getDurationUs() {
+      return streamInfo.durationUs();
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggExtractor.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * Ogg {@link Extractor}.
+ */
+public class OggExtractor implements Extractor {
+
+  /**
+   * Factory for {@link OggExtractor} instances.
+   */
+  public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+    @Override
+    public Extractor[] createExtractors() {
+      return new Extractor[] {new OggExtractor()};
+    }
+
+  };
+
+  private static final int MAX_VERIFICATION_BYTES = 8;
+
+  private StreamReader streamReader;
+
+  @Override
+  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+    try {
+      OggPageHeader header = new OggPageHeader();
+      if (!header.populate(input, true) || (header.type & 0x02) != 0x02) {
+        return false;
+      }
+
+      int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES);
+      ParsableByteArray scratch = new ParsableByteArray(length);
+      input.peekFully(scratch.data, 0, length);
+
+      if (FlacReader.verifyBitstreamType(resetPosition(scratch))) {
+        streamReader = new FlacReader();
+      } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) {
+        streamReader = new VorbisReader();
+      } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) {
+        streamReader = new OpusReader();
+      } else {
+        return false;
+      }
+      return true;
+    } catch (ParserException e) {
+      return false;
+    }
+  }
+
+  @Override
+  public void init(ExtractorOutput output) {
+    TrackOutput trackOutput = output.track(0);
+    output.endTracks();
+    // TODO: fix the case if sniff() isn't called
+    streamReader.init(output, trackOutput);
+  }
+
+  @Override
+  public void seek(long position, long timeUs) {
+    streamReader.seek(position, timeUs);
+  }
+
+  @Override
+  public void release() {
+    // Do nothing
+  }
+
+  @Override
+  public int read(ExtractorInput input, PositionHolder seekPosition)
+      throws IOException, InterruptedException {
+    return streamReader.read(input, seekPosition);
+  }
+
+  //@VisibleForTesting
+  /* package */ StreamReader getStreamReader() {
+    return streamReader;
+  }
+
+  private static ParsableByteArray resetPosition(ParsableByteArray scratch) {
+    scratch.setPosition(0);
+    return scratch;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPacket.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 com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * 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. {@code false} if the end of the input was
+   *    encountered having read no data.
+   * @throws IOException thrown if reading from the input fails.
+   * @throws InterruptedException thrown if interrupted while reading from input.
+   */
+  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) {
+        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.
+   */
+  //@VisibleForTesting
+  public OggPageHeader getPageHeader() {
+    return pageHeader;
+  }
+
+  /**
+   * Returns a {@link ParsableByteArray} containing the packet's payload.
+   */
+  public ParsableByteArray getPayload() {
+    return packetArray;
+  }
+
+  /**
+   * 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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggPageHeader.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 com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+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 = Util.getIntegerCodeForString("OggS");
+
+  public int revision;
+  public int type;
+  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 if {@code true} no Exceptions are thrown but {@code false} is return if something
+   *    goes wrong.
+   * @return {@code true} if the read was successful. {@code false} if the end of the input was
+   *    encountered having read no data.
+   * @throws IOException thrown if reading data fails or the stream is invalid.
+   * @throws InterruptedException thrown if thread is interrupted when reading/peeking.
+   */
+  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;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/OggSeeker.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 com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import 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();
+
+  /**
+   * Initializes a seek operation.
+   *
+   * @param timeUs The seek position in microseconds.
+   * @return The granule position targeted by the seek.
+   */
+  long startSeek(long timeUs);
+
+  /**
+   * Reads data from the {@link ExtractorInput} to build the {@link SeekMap} or to continue a
+   * progressive 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;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/OpusReader.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 com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+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 = Util.getIntegerCodeForString("Opus");
+  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)
+      throws IOException, InterruptedException {
+    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 frames * length;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/StreamReader.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 com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * StreamReader abstract class.
+ */
+/* 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 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;
+
+  void init(ExtractorOutput output, TrackOutput trackOutput) {
+    this.extractorOutput = output;
+    this.trackOutput = trackOutput;
+    this.oggPacket = new OggPacket();
+
+    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 = oggSeeker.startSeek(timeUs);
+        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();
+      oggSeeker = new DefaultOggSeeker(payloadStartPosition, input.getLength(), this,
+          firstPayloadPageHeader.headerSize + firstPayloadPageHeader.bodySize,
+          firstPayloadPageHeader.granulePosition);
+    }
+
+    setupData = null;
+    state = STATE_READ_PAYLOAD;
+    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) throws IOException, InterruptedException {
+      return -1;
+    }
+
+    @Override
+    public long startSeek(long timeUs) {
+      return 0;
+    }
+
+    @Override
+    public SeekMap createSeekMap() {
+      return new SeekMap.Unseekable(C.TIME_UNSET);
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisBitArray.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 com.google.android.exoplayer2.extractor.ogg;
+
+import 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>
+ */
+/* package */ final class VorbisBitArray {
+
+  public final byte[] data;
+  private final int limit;
+  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.length);
+  }
+
+  /**
+   * Creates a new instance that wraps an existing array.
+   *
+   * @param data the array to wrap.
+   * @param limit the limit in bytes.
+   */
+  public VorbisBitArray(byte[] data, int limit) {
+    this.data = data;
+    this.limit = limit * 8;
+  }
+
+  /**
+   * 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() {
+    return readBits(1) == 1;
+  }
+
+  /**
+   * 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) {
+    Assertions.checkState(getPosition() + numBits <= limit);
+    if (numBits == 0) {
+      return 0;
+    }
+    int result = 0;
+    int bitCount = 0;
+    if (bitOffset != 0) {
+      bitCount = Math.min(numBits, 8 - bitOffset);
+      int mask = 0xFF >>> (8 - bitCount);
+      result = (data[byteOffset] >>> bitOffset) & mask;
+      bitOffset += bitCount;
+      if (bitOffset == 8) {
+        byteOffset++;
+        bitOffset = 0;
+      }
+    }
+
+    if (numBits - bitCount > 7) {
+      int numBytes = (numBits - bitCount) / 8;
+      for (int i = 0; i < numBytes; i++) {
+        result |= (data[byteOffset++] & 0xFFL) << bitCount;
+        bitCount += 8;
+      }
+    }
+
+    if (numBits > bitCount) {
+      int bitsOnNextByte = numBits - bitCount;
+      int mask = 0xFF >>> (8 - bitsOnNextByte);
+      result |= (data[byteOffset] & mask) << bitCount;
+      bitOffset += bitsOnNextByte;
+    }
+    return result;
+  }
+
+  /**
+   * Skips {@code numberOfBits} bits.
+   *
+   * @param numberOfBits The number of bits to skip.
+   */
+  public void skipBits(int numberOfBits) {
+    Assertions.checkState(getPosition() + numberOfBits <= limit);
+    byteOffset += numberOfBits / 8;
+    bitOffset += numberOfBits % 8;
+    if (bitOffset > 7) {
+      byteOffset++;
+      bitOffset -= 8;
+    }
+  }
+
+  /**
+   * 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) {
+    Assertions.checkArgument(position < limit && position >= 0);
+    byteOffset = position / 8;
+    bitOffset = position - (byteOffset * 8);
+  }
+
+  /**
+   * Returns the number of remaining bits.
+   */
+  public int bitsLeft() {
+    return limit - getPosition();
+  }
+
+  /**
+   * Returns the limit in bits.
+   **/
+  public int limit() {
+    return limit;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisReader.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ogg;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ogg.VorbisUtil.Mode;
+import com.google.android.exoplayer2.util.MimeTypes;
+import 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, OggPageHeader.MAX_PAGE_PAYLOAD,
+        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;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ogg/VorbisUtil.java
@@ -0,0 +1,493 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ogg;
+
+import android.util.Log;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Arrays;
+
+/**
+ * Utility methods for parsing vorbis streams.
+ */
+/* package */ final class VorbisUtil {
+
+  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 thrown if invalid capture pattern is detected.
+   */
+  public static CommentHeader readVorbisCommentHeader(ParsableByteArray headerData)
+      throws ParserException {
+
+    verifyVorbisHeaderCapturePattern(0x03, headerData, 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 ((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);
+      switch (mappingType) {
+        case 0:
+          int submaps;
+          if (bitArray.readBit()) {
+            submaps = bitArray.readBits(4) + 1;
+          } else {
+            submaps = 1;
+          }
+          int couplingSteps;
+          if (bitArray.readBit()) {
+            couplingSteps = bitArray.readBits(8) + 1;
+            for (int j = 0; j < couplingSteps; j++) {
+              bitArray.skipBits(iLog(channels - 1)); // magnitude
+              bitArray.skipBits(iLog(channels - 1)); // angle
+            }
+          } /*else {
+            couplingSteps = 0;
+          }*/
+          if (bitArray.readBits(2) != 0x00) {
+            throw new ParserException("to reserved bits must be zero after mapping coupling steps");
+          }
+          if (submaps > 1) {
+            for (int j = 0; j < channels; j++) {
+              bitArray.skipBits(4); // mappingMux
+            }
+          }
+          for (int j = 0; j < submaps; j++) {
+            bitArray.skipBits(8); // discard
+            bitArray.skipBits(8); // submapFloor
+            bitArray.skipBits(8); // submapResidue
+          }
+          break;
+        default:
+          Log.e(TAG, "mapping type other than 0 not supported: " + mappingType);
+      }
+    }
+  }
+
+  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] = bitArray.readBits(5) + 1;
+          } else { // entry unused
+            lengthMap[i] = 0;
+          }
+        } else { // not sparse
+          lengthMap[i] = 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 = 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));
+  }
+
+  public 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;
+    }
+
+  }
+
+  public static final class CommentHeader {
+
+    public final String vendor;
+    public final String[] comments;
+    public final int length;
+
+    public CommentHeader(String vendor, String[] comments, int length) {
+      this.vendor = vendor;
+      this.comments = comments;
+      this.length = length;
+    }
+
+  }
+
+  public static final class VorbisIdHeader {
+
+    public final long version;
+    public final int channels;
+    public final long sampleRate;
+    public final int bitrateMax;
+    public final int bitrateNominal;
+    public final int bitrateMin;
+    public final int blockSize0;
+    public final int blockSize1;
+    public final boolean framingFlag;
+    public final byte[] data;
+
+    public VorbisIdHeader(long version, int channels, long sampleRate, int bitrateMax,
+        int bitrateNominal, int bitrateMin, int blockSize0, int blockSize1, boolean framingFlag,
+        byte[] data) {
+      this.version = version;
+      this.channels = channels;
+      this.sampleRate = sampleRate;
+      this.bitrateMax = bitrateMax;
+      this.bitrateNominal = bitrateNominal;
+      this.bitrateMin = bitrateMin;
+      this.blockSize0 = blockSize0;
+      this.blockSize1 = blockSize1;
+      this.framingFlag = framingFlag;
+      this.data = data;
+    }
+
+    public int getApproximateBitrate() {
+      return bitrateNominal == 0 ? (bitrateMin + bitrateMax) / 2 : bitrateNominal;
+    }
+
+  }
+
+  public static final class Mode {
+
+    public final boolean blockFlag;
+    public final int windowType;
+    public final int transformType;
+    public final int mapping;
+
+    public Mode(boolean blockFlag, int windowType, int transformType, int mapping) {
+      this.blockFlag = blockFlag;
+      this.windowType = windowType;
+      this.transformType = transformType;
+      this.mapping = mapping;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractor.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 com.google.android.exoplayer2.extractor.rawcc;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * Extracts CEA data from a RawCC file.
+ */
+public final class RawCcExtractor implements Extractor {
+
+  private static final int SCRATCH_SIZE = 9;
+  private static final int HEADER_SIZE = 8;
+  private static final int HEADER_ID = Util.getIntegerCodeForString("RCC\u0001");
+  private static final int 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);
+    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);
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Extractor.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 com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.audio.Ac3Util;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+
+import java.io.IOException;
+
+/**
+ * Facilitates the extraction of AC-3 samples from elementary audio files formatted as AC-3
+ * bitstreams.
+ */
+public final class Ac3Extractor implements Extractor {
+
+  /**
+   * Factory for {@link Ac3Extractor} instances.
+   */
+  public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+    @Override
+    public Extractor[] createExtractors() {
+      return new Extractor[] {new Ac3Extractor()};
+    }
+
+  };
+
+  /**
+   * 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 static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
+
+  private final long firstSampleTimestampUs;
+  private final ParsableByteArray sampleData;
+
+  private Ac3Reader reader;
+  private boolean startedPacket;
+
+  public Ac3Extractor() {
+    this(0);
+  }
+
+  public Ac3Extractor(long firstSampleTimestampUs) {
+    this.firstSampleTimestampUs = firstSampleTimestampUs;
+    sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE);
+  }
+
+  @Override
+  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+    // Skip any ID3 headers.
+    ParsableByteArray scratch = new ParsableByteArray(10);
+    int startPosition = 0;
+    while (true) {
+      input.peekFully(scratch.data, 0, 10);
+      scratch.setPosition(0);
+      if (scratch.readUnsignedInt24() != ID3_TAG) {
+        break;
+      }
+      scratch.skipBytes(3);
+      int length = scratch.readSynchSafeInt();
+      startPosition += 10 + length;
+      input.advancePeekPosition(length);
+    }
+    input.resetPeekPosition();
+    input.advancePeekPosition(startPosition);
+
+    int headerPosition = startPosition;
+    int validFramesCount = 0;
+    while (true) {
+      input.peekFully(scratch.data, 0, 5);
+      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 - 5);
+      }
+    }
+  }
+
+  @Override
+  public void init(ExtractorOutput output) {
+    reader = new Ac3Reader(); // TODO: Add support for embedded ID3.
+    reader.createTracks(output, new TrackIdGenerator(0, 1));
+    output.endTracks();
+    output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+  }
+
+  @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(firstSampleTimestampUs, true);
+      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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/Ac3Reader.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 com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.audio.Ac3Util;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Parses a continuous (E-)AC-3 byte stream and extracts individual samples.
+ */
+/* package */ final class Ac3Reader 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 = 8;
+
+  private final ParsableBitArray headerScratchBits;
+  private final ParsableByteArray headerScratchBytes;
+  private final String language;
+
+  private TrackOutput output;
+
+  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;
+  private boolean isEac3;
+
+  // 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) {
+    output = extractorOutput.track(generator.getNextId());
+  }
+
+  @Override
+  public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+    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;
+      }
+    }
+  }
+
+  @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.
+   */
+  private void parseHeader() {
+    if (format == null) {
+      // We read ahead to distinguish between AC-3 and E-AC-3.
+      headerScratchBits.skipBits(40);
+      isEac3 = headerScratchBits.readBits(5) == 16;
+      headerScratchBits.setPosition(headerScratchBits.getPosition() - 45);
+      format = isEac3 ? Ac3Util.parseEac3SyncframeFormat(headerScratchBits, null, language , null)
+          : Ac3Util.parseAc3SyncframeFormat(headerScratchBits, null, language, null);
+      output.format(format);
+    }
+    sampleSize = isEac3 ? Ac3Util.parseEAc3SyncframeSize(headerScratchBits.data)
+        : Ac3Util.parseAc3SyncframeSize(headerScratchBits.data);
+    int audioSamplesPerSyncframe = isEac3
+        ? Ac3Util.parseEAc3SyncframeAudioSampleCount(headerScratchBits.data)
+        : Ac3Util.getAc3SyncframeAudioSampleCount();
+    // In this class a sample is an access unit (syncframe in AC-3), but the MediaFormat sample rate
+    // specifies the number of PCM audio samples per second.
+    sampleDurationUs =
+        (int) (C.MICROS_PER_SECOND * audioSamplesPerSyncframe / format.sampleRate);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsExtractor.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 com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS
+ * headers.
+ */
+public final class AdtsExtractor implements Extractor {
+
+  /**
+   * Factory for {@link AdtsExtractor} instances.
+   */
+  public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+    @Override
+    public Extractor[] createExtractors() {
+      return new Extractor[] {new AdtsExtractor()};
+    }
+
+  };
+
+  private static final int MAX_PACKET_SIZE = 200;
+  private static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
+  /**
+   * 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;
+
+  private final long firstSampleTimestampUs;
+  private final ParsableByteArray packetBuffer;
+
+  // Accessed only by the loading thread.
+  private AdtsReader reader;
+  private boolean startedPacket;
+
+  public AdtsExtractor() {
+    this(0);
+  }
+
+  public AdtsExtractor(long firstSampleTimestampUs) {
+    this.firstSampleTimestampUs = firstSampleTimestampUs;
+    packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);
+  }
+
+  @Override
+  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+    // Skip any ID3 headers.
+    ParsableByteArray scratch = new ParsableByteArray(10);
+    ParsableBitArray scratchBits = new ParsableBitArray(scratch.data);
+    int startPosition = 0;
+    while (true) {
+      input.peekFully(scratch.data, 0, 10);
+      scratch.setPosition(0);
+      if (scratch.readUnsignedInt24() != ID3_TAG) {
+        break;
+      }
+      scratch.skipBytes(3);
+      int length = scratch.readSynchSafeInt();
+      startPosition += 10 + length;
+      input.advancePeekPosition(length);
+    }
+    input.resetPeekPosition();
+    input.advancePeekPosition(startPosition);
+
+    // Try to find four or more consecutive AAC audio frames, exceeding the MPEG TS packet size.
+    int headerPosition = startPosition;
+    int validFramesSize = 0;
+    int validFramesCount = 0;
+    while (true) {
+      input.peekFully(scratch.data, 0, 2);
+      scratch.setPosition(0);
+      int syncBytes = scratch.readUnsignedShort();
+      if ((syncBytes & 0xFFF6) != 0xFFF0) {
+        validFramesCount = 0;
+        validFramesSize = 0;
+        input.resetPeekPosition();
+        if (++headerPosition - startPosition >= MAX_SNIFF_BYTES) {
+          return false;
+        }
+        input.advancePeekPosition(headerPosition);
+      } else {
+        if (++validFramesCount >= 4 && validFramesSize > 188) {
+          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);
+        validFramesSize += frameSize;
+      }
+    }
+  }
+
+  @Override
+  public void init(ExtractorOutput output) {
+    reader = new AdtsReader(true);
+    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(packetBuffer.data, 0, MAX_PACKET_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.
+    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, true);
+      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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/AdtsReader.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.DummyTrackOutput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Parses a continuous ADTS byte stream and extracts individual frames.
+ */
+/* package */ final class AdtsReader implements ElementaryStreamReader {
+
+  private static final String TAG = "AdtsReader";
+
+  private static final int STATE_FINDING_SAMPLE = 0;
+  private static final int STATE_READING_ID3_HEADER = 1;
+  private static final int STATE_READING_ADTS_HEADER = 2;
+  private static final int STATE_READING_SAMPLE = 3;
+
+  private static final int 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 final boolean exposeId3;
+  private final ParsableBitArray adtsScratch;
+  private final ParsableByteArray id3HeaderBuffer;
+  private final String language;
+
+  private TrackOutput output;
+  private TrackOutput id3Output;
+
+  private int state;
+  private int bytesRead;
+
+  private int matchState;
+
+  private boolean hasCrc;
+
+  // 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();
+    this.exposeId3 = exposeId3;
+    this.language = language;
+  }
+
+  @Override
+  public void seek() {
+    setFindingSampleState();
+  }
+
+  @Override
+  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+    output = extractorOutput.track(idGenerator.getNextId());
+    if (exposeId3) {
+      id3Output = extractorOutput.track(idGenerator.getNextId());
+      id3Output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null,
+          Format.NO_VALUE, null));
+    } else {
+      id3Output = new DummyTrackOutput();
+    }
+  }
+
+  @Override
+  public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+    timeUs = pesTimeUs;
+  }
+
+  @Override
+  public void consume(ParsableByteArray data) {
+    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_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;
+      }
+    }
+  }
+
+  @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;
+  }
+
+  /**
+   * 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;
+  }
+
+  /**
+   * 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 && data >= 0xF0 && data != 0xFF) {
+        hasCrc = (data & 0x1) == 0;
+        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);
+  }
+
+  /**
+   * 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() {
+    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;
+      }
+
+      int sampleRateIndex = adtsScratch.readBits(4);
+      adtsScratch.skipBits(1);
+      int channelConfig = adtsScratch.readBits(3);
+
+      byte[] audioSpecificConfig = CodecSpecificDataUtil.buildAacAudioSpecificConfig(
+          audioObjectType, sampleRateIndex, channelConfig);
+      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, 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();
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.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 com.google.android.exoplayer2.extractor.ts;
+
+import android.support.annotation.IntDef;
+import android.util.SparseArray;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Default implementation for {@link TsPayloadReader.Factory}.
+ */
+public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Factory {
+
+  /**
+   * Flags controlling elementary stream readers behaviour.
+   */
+  @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})
+  public @interface Flags {
+  }
+  public static final int FLAG_ALLOW_NON_IDR_KEYFRAMES = 1;
+  public static final int FLAG_IGNORE_AAC_STREAM = 2;
+  public static final int FLAG_IGNORE_H264_STREAM = 4;
+  public static final int FLAG_DETECT_ACCESS_UNITS = 8;
+  public static final int FLAG_IGNORE_SPLICE_INFO_STREAM = 16;
+
+  @Flags
+  private final int flags;
+
+  public DefaultTsPayloadReaderFactory() {
+    this(0);
+  }
+
+  public DefaultTsPayloadReaderFactory(@Flags int flags) {
+    this.flags = flags;
+  }
+
+  @Override
+  public SparseArray<TsPayloadReader> createInitialPayloadReaders() {
+    return new SparseArray<>();
+  }
+
+  @Override
+  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:
+        return isSet(FLAG_IGNORE_AAC_STREAM)
+            ? null : new PesReader(new AdtsReader(false, esInfo.language));
+      case TsExtractor.TS_STREAM_TYPE_AC3:
+      case TsExtractor.TS_STREAM_TYPE_E_AC3:
+        return new PesReader(new Ac3Reader(esInfo.language));
+      case TsExtractor.TS_STREAM_TYPE_DTS:
+      case TsExtractor.TS_STREAM_TYPE_HDMV_DTS:
+        return new PesReader(new DtsReader(esInfo.language));
+      case TsExtractor.TS_STREAM_TYPE_H262:
+        return new PesReader(new H262Reader());
+      case TsExtractor.TS_STREAM_TYPE_H264:
+        return isSet(FLAG_IGNORE_H264_STREAM) ? null : new PesReader(
+            new H264Reader(isSet(FLAG_ALLOW_NON_IDR_KEYFRAMES), isSet(FLAG_DETECT_ACCESS_UNITS)));
+      case TsExtractor.TS_STREAM_TYPE_H265:
+        return new PesReader(new H265Reader());
+      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());
+      default:
+        return null;
+    }
+  }
+
+  private boolean isSet(@Flags int flag) {
+    return (flags & flag) != 0;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/DtsReader.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.audio.DtsUtil;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Parses a continuous DTS byte stream and extracts individual samples.
+ */
+/* package */ 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 = 15;
+  private static final int SYNC_VALUE = 0x7FFE8001;
+  private static final int SYNC_VALUE_SIZE = 4;
+
+  private final ParsableByteArray headerScratchBytes;
+  private final String language;
+
+  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]);
+    headerScratchBytes.data[0] = (byte) ((SYNC_VALUE >> 24) & 0xFF);
+    headerScratchBytes.data[1] = (byte) ((SYNC_VALUE >> 16) & 0xFF);
+    headerScratchBytes.data[2] = (byte) ((SYNC_VALUE >> 8) & 0xFF);
+    headerScratchBytes.data[3] = (byte) (SYNC_VALUE & 0xFF);
+    state = STATE_FINDING_SYNC;
+    this.language = language;
+  }
+
+  @Override
+  public void seek() {
+    state = STATE_FINDING_SYNC;
+    bytesRead = 0;
+    syncBytes = 0;
+  }
+
+  @Override
+  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+    output = extractorOutput.track(idGenerator.getNextId());
+  }
+
+  @Override
+  public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+    timeUs = pesTimeUs;
+  }
+
+  @Override
+  public void consume(ParsableByteArray data) {
+    while (data.bytesLeft() > 0) {
+      switch (state) {
+        case STATE_FINDING_SYNC:
+          if (skipToNextSync(data)) {
+            bytesRead = SYNC_VALUE_SIZE;
+            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;
+      }
+    }
+  }
+
+  @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 (syncBytes == SYNC_VALUE) {
+        syncBytes = 0;
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Parses the sample header.
+   */
+  private void parseHeader() {
+    byte[] frameData = headerScratchBytes.data;
+    if (format == null) {
+      format = DtsUtil.parseDtsFormat(frameData, null, 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);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import 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 dataAlignmentIndicator The data alignment indicator associated with the packet.
+   */
+  void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator);
+
+  /**
+   * Consumes (possibly partial) data from the current packet.
+   *
+   * @param data The data to consume.
+   */
+  void consume(ParsableByteArray data);
+
+  /**
+   * Called when a packet ends.
+   */
+  void packetFinished();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/H262Reader.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Parses a continuous H262 byte stream and extracts individual frames.
+ */
+/* package */ 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 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;
+
+  // State that should be reset on seek.
+  private final boolean[] prefixFlags;
+  private final CsdBuffer csdBuffer;
+  private boolean foundFirstFrameInGroup;
+  private long totalBytesWritten;
+
+  // Per packet state that gets reset at the start of each packet.
+  private long pesTimeUs;
+  private boolean pesPtsUsAvailable;
+
+  // Per sample state that gets reset at the start of each frame.
+  private boolean isKeyframe;
+  private long framePosition;
+  private long frameTimeUs;
+
+  public H262Reader() {
+    prefixFlags = new boolean[4];
+    csdBuffer = new CsdBuffer(128);
+  }
+
+  @Override
+  public void seek() {
+    NalUnitUtil.clearPrefixFlags(prefixFlags);
+    csdBuffer.reset();
+    pesPtsUsAvailable = false;
+    foundFirstFrameInGroup = false;
+    totalBytesWritten = 0;
+  }
+
+  @Override
+  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+    output = extractorOutput.track(idGenerator.getNextId());
+  }
+
+  @Override
+  public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+    pesPtsUsAvailable = pesTimeUs != C.TIME_UNSET;
+    if (pesPtsUsAvailable) {
+      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());
+
+    int searchOffset = offset;
+    while (true) {
+      int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, searchOffset, 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);
+        }
+        return;
+      }
+
+      // We've found a start code with the following value.
+      int startCodeValue = data.data[startCodeOffset + 3] & 0xFF;
+
+      if (!hasOutputFormat) {
+        // This is the number of bytes from the current offset to the start of the next start
+        // code. It may be negative if the start code started in the previously consumed data.
+        int lengthToStartCode = startCodeOffset - offset;
+        if (lengthToStartCode > 0) {
+          csdBuffer.onData(dataArray, offset, startCodeOffset);
+        }
+        // This is the number of bytes belonging to the next start code that have already been
+        // passed to csdDataTargetBuffer.
+        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);
+          output.format(result.first);
+          frameDurationUs = result.second;
+          hasOutputFormat = true;
+        }
+      }
+
+      if (hasOutputFormat && (startCodeValue == START_GROUP || startCodeValue == START_PICTURE)) {
+        int bytesWrittenPastStartCode = limit - startCodeOffset;
+        if (foundFirstFrameInGroup) {
+          @C.BufferFlags int flags = isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0;
+          int size = (int) (totalBytesWritten - framePosition) - bytesWrittenPastStartCode;
+          output.sampleMetadata(frameTimeUs, flags, size, bytesWrittenPastStartCode, null);
+          isKeyframe = false;
+        }
+        if (startCodeValue == START_GROUP) {
+          foundFirstFrameInGroup = false;
+          isKeyframe = true;
+        } else /* startCodeValue == START_PICTURE */ {
+          frameTimeUs = pesPtsUsAvailable ? pesTimeUs : (frameTimeUs + frameDurationUs);
+          framePosition = totalBytesWritten - bytesWrittenPastStartCode;
+          pesPtsUsAvailable = false;
+          foundFirstFrameInGroup = true;
+        }
+      }
+
+      offset = startCodeOffset;
+      searchOffset = offset + 3;
+    }
+  }
+
+  @Override
+  public void packetFinished() {
+    // Do nothing.
+  }
+
+  /**
+   * Parses the {@link Format} and frame duration from a csd buffer.
+   *
+   * @param csdBuffer The csd buffer.
+   * @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) {
+    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(null, 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 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 already been
+     *     passed to {@link #onData(byte[], int, int)}, or 0.
+     * @return Whether the csd data is now complete. If true is returned, neither
+     *     this method or {@link #onData(byte[], int, int)} should be called again without an
+     *     interleaving call to {@link #reset()}.
+     */
+    public boolean onStartCode(int startCodeValue, int bytesAlreadyPassed) {
+      if (isFilling) {
+        if (sequenceExtensionPosition == 0 && startCodeValue == START_EXTENSION) {
+          sequenceExtensionPosition = length;
+        } else {
+          length -= bytesAlreadyPassed;
+          isFilling = false;
+          return true;
+        }
+      } else if (startCodeValue == START_SEQUENCE_HEADER) {
+        isFilling = true;
+      }
+      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;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/H264Reader.java
@@ -0,0 +1,516 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.NalUnitUtil.SpsData;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import 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.
+ */
+/* package */ 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 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 TrackOutput output;
+  private SeiReader seiReader;
+  private SampleReader sampleReader;
+
+  // State that should not be reset on seek.
+  private boolean hasOutputFormat;
+
+  // 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 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(boolean allowNonIdrKeyframes, boolean detectAccessUnits) {
+    prefixFlags = new boolean[3];
+    this.allowNonIdrKeyframes = allowNonIdrKeyframes;
+    this.detectAccessUnits = detectAccessUnits;
+    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;
+  }
+
+  @Override
+  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+    output = extractorOutput.track(idGenerator.getNextId());
+    sampleReader = new SampleReader(output, allowNonIdrKeyframes, detectAccessUnits);
+    seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId()));
+  }
+
+  @Override
+  public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+    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());
+
+    // 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(null, MimeTypes.VIDEO_H264, null,
+              Format.NO_VALUE, Format.NO_VALUE, spsData.width, spsData.height, Format.NO_VALUE,
+              initializationData, Format.NO_VALUE, spsData.pixelWidthAspectRatio, 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);
+    }
+    sampleReader.endNalUnit(position, offset);
+  }
+
+  /**
+   * 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.skipBits(1); // 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 void endNalUnit(long position, int offset) {
+      if (nalUnitType == NAL_UNIT_TYPE_AUD
+          || (detectAccessUnits && sliceHeader.isFirstVclNalUnitOfPicture(previousSliceHeader))) {
+        // If the NAL unit ending is the start of a new sample, output the previous one.
+        if (readingSample) {
+          int nalUnitLength = (int) (position - nalUnitStartPosition);
+          outputSample(offset + nalUnitLength);
+        }
+        samplePosition = nalUnitStartPosition;
+        sampleTimeUs = nalUnitTimeUs;
+        sampleIsKeyframe = false;
+        readingSample = true;
+      }
+      sampleIsKeyframe |= nalUnitType == NAL_UNIT_TYPE_IDR || (allowNonIdrKeyframes
+          && nalUnitType == NAL_UNIT_TYPE_NON_IDR && sliceHeader.isISlice());
+    }
+
+    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));
+      }
+
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/H265Reader.java
@@ -0,0 +1,485 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.ParsableNalUnitBitArray;
+import java.util.Collections;
+
+/**
+ * Parses a continuous H.265 byte stream and extracts individual frames.
+ */
+/* package */ 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 TrackOutput output;
+  private SampleReader sampleReader;
+  private SeiReader seiReader;
+
+  // 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;
+
+  public H265Reader() {
+    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) {
+    output = extractorOutput.track(idGenerator.getNextId());
+    sampleReader = new SampleReader(output);
+    seiReader = new SeiReader(extractorOutput.track(idGenerator.getNextId()));
+  }
+
+  @Override
+  public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+    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(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(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.skipBits(1); // 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.skipBits(1); // 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.skipBits(1); // 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(null, 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.skipBits(1); // delta_rps_sign
+        bitArray.readUnsignedExpGolombCodedInt(); // abs_delta_rps_minus1
+        for (int j = 0; j <= previousNumDeltaPocs; j++) {
+          if (bitArray.readBit()) { // used_by_curr_pic_flag[j]
+            bitArray.skipBits(1); // use_delta_flag[j]
+          }
+        }
+      } 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.skipBits(1); // used_by_curr_pic_s0_flag[i]
+        }
+        for (int i = 0; i < numPositivePics; i++) {
+          bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s1_minus1[i]
+          bitArray.skipBits(1); // used_by_curr_pic_s1_flag[i]
+        }
+      }
+    }
+  }
+
+  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);
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/Id3Reader.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 com.google.android.exoplayer2.extractor.ts;
+
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Parses ID3 data and extracts individual text information frames.
+ */
+/* package */ final class Id3Reader implements ElementaryStreamReader {
+
+  private static final String TAG = "Id3Reader";
+
+  private static final int ID3_HEADER_SIZE = 10;
+
+  private final ParsableByteArray id3Header;
+
+  private TrackOutput output;
+
+  // 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_SIZE);
+  }
+
+  @Override
+  public void seek() {
+    writingSample = false;
+  }
+
+  @Override
+  public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
+    output = extractorOutput.track(idGenerator.getNextId());
+    output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE,
+        null));
+  }
+
+  @Override
+  public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+    if (!dataAlignmentIndicator) {
+      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_SIZE) {
+      // We're still reading the ID3 header.
+      int headerBytesAvailable = Math.min(bytesAvailable, ID3_HEADER_SIZE - sampleBytesRead);
+      System.arraycopy(data.data, data.getPosition(), id3Header.data, sampleBytesRead,
+          headerBytesAvailable);
+      if (sampleBytesRead + headerBytesAvailable == ID3_HEADER_SIZE) {
+        // 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_SIZE + 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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/MpegAudioReader.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Parses a continuous MPEG Audio byte stream and extracts individual frames.
+ */
+/* package */ 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 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) {
+    output = extractorOutput.track(idGenerator.getNextId());
+  }
+
+  @Override
+  public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
+    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;
+      }
+    }
+  }
+
+  @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(null, 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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.extractor.ts;
+
+import 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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/PesReader.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import 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, boolean payloadUnitStartIndicator) {
+    if (payloadUnitStartIndicator) {
+      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;
+      }
+      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();
+            reader.packetStarted(timeUs, dataAlignmentIndicator);
+            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;
+      }
+    }
+  }
+
+  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);
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/PsExtractor.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.io.IOException;
+
+/**
+ * Facilitates the extraction of data from the MPEG-2 TS container format.
+ */
+public final class PsExtractor implements Extractor {
+
+  /**
+   * Factory for {@link PsExtractor} instances.
+   */
+  public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+    @Override
+    public Extractor[] createExtractors() {
+      return new Extractor[] {new PsExtractor()};
+    }
+
+  };
+
+  private static final int PACK_START_CODE = 0x000001BA;
+  private static final int SYSTEM_HEADER_START_CODE = 0x000001BB;
+  private static final int PACKET_START_CODE_PREFIX = 0x000001;
+  private static final int MPEG_PROGRAM_END_CODE = 0x000001B9;
+  private static final int MAX_STREAM_ID_PLUS_ONE = 0x100;
+  private static final long MAX_SEARCH_LENGTH = 1024 * 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 boolean foundAllTracks;
+  private boolean foundAudioTrack;
+  private boolean foundVideoTrack;
+
+  // Accessed only by the loading thread.
+  private ExtractorOutput output;
+
+  public PsExtractor() {
+    this(new TimestampAdjuster(0));
+  }
+
+  public PsExtractor(TimestampAdjuster timestampAdjuster) {
+    this.timestampAdjuster = timestampAdjuster;
+    psPacketBuffer = new ParsableByteArray(4096);
+    psPayloadReaders = new SparseArray<>();
+  }
+
+  // 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;
+    output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+  }
+
+  @Override
+  public void seek(long position, long timeUs) {
+    timestampAdjuster.reset();
+    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 {
+    // 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 (!foundAudioTrack && streamId == PRIVATE_STREAM_1) {
+          // Private stream, used for AC3 audio.
+          // NOTE: This may need further parsing to determine if its DTS, but that's likely only
+          // valid for DVDs.
+          elementaryStreamReader = new Ac3Reader();
+          foundAudioTrack = true;
+        } else if (!foundAudioTrack && (streamId & AUDIO_STREAM_MASK) == AUDIO_STREAM) {
+          elementaryStreamReader = new MpegAudioReader();
+          foundAudioTrack = true;
+        } else if (!foundVideoTrack && (streamId & VIDEO_STREAM_MASK) == VIDEO_STREAM) {
+          elementaryStreamReader = new H262Reader();
+          foundVideoTrack = true;
+        }
+        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);
+        }
+      }
+      if ((foundAudioTrack && foundVideoTrack) || input.getPosition() > MAX_SEARCH_LENGTH) {
+        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.
+
+  /**
+   * 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.
+     */
+    public void consume(ParsableByteArray data) {
+      data.readBytes(pesScratch.data, 0, 3);
+      pesScratch.setPosition(0);
+      parseHeader();
+      data.readBytes(pesScratch.data, 0, extendedHeaderLength);
+      pesScratch.setPosition(0);
+      parseHeaderExtension();
+      pesPayloadReader.packetStarted(timeUs, true);
+      pesPayloadReader.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);
+      }
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import 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);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/SectionReader.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import 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, boolean payloadUnitStartIndicator) {
+    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.crc(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;
+        }
+      }
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/SeiReader.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 com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.text.cea.CeaUtil;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Consumes SEI buffers, outputting contained CEA-608 messages to a {@link TrackOutput}.
+ */
+/* package */ final class SeiReader {
+
+  private final TrackOutput output;
+
+  public SeiReader(TrackOutput output) {
+    this.output = output;
+    output.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, null,
+        Format.NO_VALUE, 0, null, null));
+  }
+
+  public void consume(long pesTimeUs, ParsableByteArray seiBuffer) {
+    CeaUtil.consume(pesTimeUs, seiBuffer, output);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/SpliceInfoSectionReader.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import 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;
+    output = extractorOutput.track(idGenerator.getNextId());
+    output.format(Format.createSampleFormat(null, 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);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
+import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Facilitates the extraction of data from the MPEG-2 TS container format.
+ */
+public final class TsExtractor implements Extractor {
+
+  /**
+   * Factory for {@link TsExtractor} instances.
+   */
+  public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+    @Override
+    public Extractor[] createExtractors() {
+      return new Extractor[] {new TsExtractor()};
+    }
+
+  };
+
+  public static final int TS_STREAM_TYPE_MPA = 0x03;
+  public static final int TS_STREAM_TYPE_MPA_LSF = 0x04;
+  public static final int TS_STREAM_TYPE_AAC = 0x0F;
+  public static final int TS_STREAM_TYPE_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_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;
+
+  private static final int TS_PACKET_SIZE = 188;
+  private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet.
+  private static final int TS_PAT_PID = 0;
+  private static final int MAX_PID_PLUS_ONE = 0x2000;
+
+  private static final long AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("AC-3");
+  private static final long E_AC3_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("EAC3");
+  private static final long HEVC_FORMAT_IDENTIFIER = Util.getIntegerCodeForString("HEVC");
+
+  private static final int BUFFER_PACKET_COUNT = 5; // Should be at least 2
+  private static final int BUFFER_SIZE = TS_PACKET_SIZE * BUFFER_PACKET_COUNT;
+
+  private final boolean hlsMode;
+  private final TimestampAdjuster timestampAdjuster;
+  private final ParsableByteArray tsPacketBuffer;
+  private final ParsableBitArray tsScratch;
+  private final SparseIntArray continuityCounters;
+  private final TsPayloadReader.Factory payloadReaderFactory;
+  private final SparseArray<TsPayloadReader> tsPayloadReaders; // Indexed by pid
+  private final SparseBooleanArray trackIds;
+
+  // Accessed only by the loading thread.
+  private ExtractorOutput output;
+  private boolean tracksEnded;
+  private TsPayloadReader id3Reader;
+
+  public TsExtractor() {
+    this(new TimestampAdjuster(0));
+  }
+
+  /**
+   * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
+   */
+  public TsExtractor(TimestampAdjuster timestampAdjuster) {
+    this(timestampAdjuster, new DefaultTsPayloadReaderFactory(), false);
+  }
+
+  /**
+   * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps.
+   * @param payloadReaderFactory Factory for injecting a custom set of payload readers.
+   * @param hlsMode Whether the extractor should be used in HLS mode. If true, {@link TrackOutput}s
+   *     are mapped by their type (instead of PID) and continuity counters are ignored.
+   */
+  public TsExtractor(TimestampAdjuster timestampAdjuster,
+      TsPayloadReader.Factory payloadReaderFactory, boolean hlsMode) {
+    this.timestampAdjuster = timestampAdjuster;
+    this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory);
+    this.hlsMode = hlsMode;
+    tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE);
+    tsScratch = new ParsableBitArray(new byte[3]);
+    trackIds = new SparseBooleanArray();
+    tsPayloadReaders = new SparseArray<>();
+    continuityCounters = new SparseIntArray();
+    resetPayloadReaders();
+  }
+
+  // Extractor implementation.
+
+  @Override
+  public boolean sniff(ExtractorInput input) throws IOException, InterruptedException {
+    byte[] buffer = tsPacketBuffer.data;
+    input.peekFully(buffer, 0, BUFFER_SIZE);
+    for (int j = 0; j < TS_PACKET_SIZE; j++) {
+      for (int i = 0; true; i++) {
+        if (i == BUFFER_PACKET_COUNT) {
+          input.skipFully(j);
+          return true;
+        }
+        if (buffer[j + i * TS_PACKET_SIZE] != TS_SYNC_BYTE) {
+          break;
+        }
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public void init(ExtractorOutput output) {
+    this.output = output;
+    output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET));
+  }
+
+  @Override
+  public void seek(long position, long timeUs) {
+    timestampAdjuster.reset();
+    tsPacketBuffer.reset();
+    continuityCounters.clear();
+    // Elementary stream readers' state should be cleared to get consistent behaviours when seeking.
+    resetPayloadReaders();
+  }
+
+  @Override
+  public void release() {
+    // Do nothing
+  }
+
+  @Override
+  public int read(ExtractorInput input, PositionHolder seekPosition)
+      throws IOException, InterruptedException {
+    byte[] data = tsPacketBuffer.data;
+    // Shift bytes to the start of the buffer if there isn't enough space left at the end
+    if (BUFFER_SIZE - tsPacketBuffer.getPosition() < TS_PACKET_SIZE) {
+      int bytesLeft = tsPacketBuffer.bytesLeft();
+      if (bytesLeft > 0) {
+        System.arraycopy(data, tsPacketBuffer.getPosition(), data, 0, bytesLeft);
+      }
+      tsPacketBuffer.reset(data, bytesLeft);
+    }
+    // Read more bytes until there is at least one packet size
+    while (tsPacketBuffer.bytesLeft() < TS_PACKET_SIZE) {
+      int limit = tsPacketBuffer.limit();
+      int read = input.read(data, limit, BUFFER_SIZE - limit);
+      if (read == C.RESULT_END_OF_INPUT) {
+        return RESULT_END_OF_INPUT;
+      }
+      tsPacketBuffer.setLimit(limit + read);
+    }
+
+    // Note: see ISO/IEC 13818-1, section 2.4.3.2 for detailed information on the format of
+    // the header.
+    final int limit = tsPacketBuffer.limit();
+    int position = tsPacketBuffer.getPosition();
+    while (position < limit && data[position] != TS_SYNC_BYTE) {
+      position++;
+    }
+    tsPacketBuffer.setPosition(position);
+
+    int endOfPacket = position + TS_PACKET_SIZE;
+    if (endOfPacket > limit) {
+      return RESULT_CONTINUE;
+    }
+
+    tsPacketBuffer.skipBytes(1);
+    tsPacketBuffer.readBytes(tsScratch, 3);
+    if (tsScratch.readBit()) { // transport_error_indicator
+      // There are uncorrectable errors in this packet.
+      tsPacketBuffer.setPosition(endOfPacket);
+      return RESULT_CONTINUE;
+    }
+    boolean payloadUnitStartIndicator = tsScratch.readBit();
+    tsScratch.skipBits(1); // transport_priority
+    int pid = tsScratch.readBits(13);
+    tsScratch.skipBits(2); // transport_scrambling_control
+    boolean adaptationFieldExists = tsScratch.readBit();
+    boolean payloadExists = tsScratch.readBit();
+
+    // Discontinuity check.
+    boolean discontinuityFound = false;
+    int continuityCounter = tsScratch.readBits(4);
+    if (!hlsMode) {
+      int previousCounter = continuityCounters.get(pid, continuityCounter - 1);
+      continuityCounters.put(pid, continuityCounter);
+      if (previousCounter == continuityCounter) {
+        if (payloadExists) {
+          // Duplicate packet found.
+          tsPacketBuffer.setPosition(endOfPacket);
+          return RESULT_CONTINUE;
+        }
+      } else if (continuityCounter != (previousCounter + 1) % 16) {
+        discontinuityFound = true;
+      }
+    }
+
+    // Skip the adaptation field.
+    if (adaptationFieldExists) {
+      int adaptationFieldLength = tsPacketBuffer.readUnsignedByte();
+      tsPacketBuffer.skipBytes(adaptationFieldLength);
+    }
+
+    // Read the payload.
+    if (payloadExists) {
+      TsPayloadReader payloadReader = tsPayloadReaders.get(pid);
+      if (payloadReader != null) {
+        if (discontinuityFound) {
+          payloadReader.seek();
+        }
+        tsPacketBuffer.setLimit(endOfPacket);
+        payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator);
+        Assertions.checkState(tsPacketBuffer.getPosition() <= endOfPacket);
+        tsPacketBuffer.setLimit(limit);
+      }
+    }
+
+    tsPacketBuffer.setPosition(endOfPacket);
+    return RESULT_CONTINUE;
+  }
+
+  // Internals.
+
+  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)));
+        }
+      }
+    }
+
+  }
+
+  /**
+   * 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 final ParsableBitArray pmtScratch;
+    private final int pid;
+
+    public PmtReader(int pid) {
+      pmtScratch = new ParsableBitArray(new byte[5]);
+      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;
+      }
+      // section_syntax_indicator(1), '0'(1), reserved(2), section_length(12), program_number (16),
+      // reserved (2), version_number (5), current_next_indicator (1), // section_number (8),
+      // last_section_number (8), reserved (3), PCR_PID (13)
+      sectionData.skipBytes(9);
+
+      // Read program_info_length.
+      sectionData.readBytes(pmtScratch, 2);
+      pmtScratch.skipBits(4);
+      int programInfoLength = pmtScratch.readBits(12);
+
+      // Skip the descriptors.
+      sectionData.skipBytes(programInfoLength);
+
+      if (hlsMode && 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, new byte[0]);
+        id3Reader = payloadReaderFactory.createPayloadReader(TS_STREAM_TYPE_ID3, dummyEsInfo);
+        id3Reader.init(timestampAdjuster, output,
+            new TrackIdGenerator(TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE));
+      }
+
+      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 = hlsMode ? streamType : elementaryPid;
+        if (trackIds.get(trackId)) {
+          continue;
+        }
+        trackIds.put(trackId, true);
+
+        TsPayloadReader reader;
+        if (hlsMode && streamType == TS_STREAM_TYPE_ID3) {
+          reader = id3Reader;
+        } else {
+          reader = payloadReaderFactory.createPayloadReader(streamType, esInfo);
+          if (reader != null) {
+            reader.init(timestampAdjuster, output, new TrackIdGenerator(trackId, MAX_PID_PLUS_ONE));
+          }
+        }
+
+        if (reader != null) {
+          tsPayloadReaders.put(elementaryPid, reader);
+        }
+      }
+      if (hlsMode) {
+        if (!tracksEnded) {
+          output.endTracks();
+        }
+      } else {
+        tsPayloadReaders.remove(TS_PAT_PID);
+        tsPayloadReaders.remove(pid);
+        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;
+      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 == 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_DTS) { // DTS_descriptor
+          streamType = TS_STREAM_TYPE_DTS;
+        } else if (descriptorTag == TS_PMT_DESC_ISO639_LANG) {
+          language = new String(data.data, data.getPosition(), 3).trim();
+          // Audio type is ignored.
+        }
+        // Skip unused bytes of current descriptor.
+        data.skipBytes(positionOfNextDescriptor - data.getPosition());
+      }
+      data.setPosition(descriptorsEndPosition);
+      return new EsInfo(streamType, language,
+          Arrays.copyOfRange(data.data, descriptorsStartPosition, descriptorsEndPosition));
+    }
+
+  }
+
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.ts;
+
+import android.util.SparseArray;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+
+/**
+ * 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 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 descriptorBytes The descriptor bytes associated to the stream.
+     */
+    public EsInfo(int streamType, String language, byte[] descriptorBytes) {
+      this.streamType = streamType;
+      this.language = language;
+      this.descriptorBytes = descriptorBytes;
+    }
+
+  }
+
+  /**
+   * Generates track ids for initializing {@link TsPayloadReader}s' {@link TrackOutput}s.
+   */
+  final class TrackIdGenerator {
+
+    private final int firstId;
+    private final int idIncrement;
+    private int generatedIdCount;
+
+    public TrackIdGenerator(int firstId, int idIncrement) {
+      this.firstId = firstId;
+      this.idIncrement = idIncrement;
+    }
+
+    public int getNextId() {
+      return firstId + idIncrement * generatedIdCount++;
+    }
+
+  }
+
+  /**
+   * 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(ParsableByteArray, boolean)} 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 payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet.
+   */
+  void consume(ParsableByteArray data, boolean payloadUnitStartIndicator);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/wav/WavExtractor.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 com.google.android.exoplayer2.extractor.wav;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.MimeTypes;
+import java.io.IOException;
+
+/** {@link Extractor} to extract samples from a WAV byte stream. */
+public final class WavExtractor implements Extractor, SeekMap {
+
+  /**
+   * Factory for {@link WavExtractor} instances.
+   */
+  public static final ExtractorsFactory FACTORY = new ExtractorsFactory() {
+
+    @Override
+    public Extractor[] createExtractors() {
+      return new Extractor[] {new WavExtractor()};
+    }
+
+  };
+
+  /** Arbitrary maximum input size of 32KB, which is ~170ms of 16-bit stereo PCM audio at 48KHz. */
+  private static final int MAX_INPUT_SIZE = 32 * 1024;
+
+  private ExtractorOutput extractorOutput;
+  private TrackOutput trackOutput;
+  private WavHeader wavHeader;
+  private int bytesPerFrame;
+  private int pendingBytes;
+
+  @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);
+    wavHeader = null;
+    output.endTracks();
+  }
+
+  @Override
+  public void seek(long position, long timeUs) {
+    pendingBytes = 0;
+  }
+
+  @Override
+  public void release() {
+    // Do nothing
+  }
+
+  @Override
+  public int read(ExtractorInput input, PositionHolder seekPosition)
+      throws IOException, InterruptedException {
+    if (wavHeader == null) {
+      wavHeader = WavHeaderReader.peek(input);
+      if (wavHeader == null) {
+        // Should only happen if the media wasn't sniffed.
+        throw new ParserException("Unsupported or unrecognized wav header.");
+      }
+      Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null,
+          wavHeader.getBitrate(), MAX_INPUT_SIZE, wavHeader.getNumChannels(),
+          wavHeader.getSampleRateHz(), wavHeader.getEncoding(), null, null, 0, null);
+      trackOutput.format(format);
+      bytesPerFrame = wavHeader.getBytesPerFrame();
+    }
+
+    if (!wavHeader.hasDataBounds()) {
+      WavHeaderReader.skipToData(input, wavHeader);
+      extractorOutput.seekMap(this);
+    }
+
+    int bytesAppended = trackOutput.sampleData(input, MAX_INPUT_SIZE - pendingBytes, true);
+    if (bytesAppended != RESULT_END_OF_INPUT) {
+      pendingBytes += bytesAppended;
+    }
+
+    // Samples must consist of a whole number of frames.
+    int pendingFrames = pendingBytes / bytesPerFrame;
+    if (pendingFrames > 0) {
+      long timeUs = wavHeader.getTimeUs(input.getPosition() - pendingBytes);
+      int size = pendingFrames * bytesPerFrame;
+      pendingBytes -= size;
+      trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, size, pendingBytes, null);
+    }
+
+    return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
+  }
+
+  // SeekMap implementation.
+
+  @Override
+  public long getDurationUs() {
+    return wavHeader.getDurationUs();
+  }
+
+  @Override
+  public boolean isSeekable() {
+    return true;
+  }
+
+  @Override
+  public long getPosition(long timeUs) {
+    return wavHeader.getPosition(timeUs);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeader.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 com.google.android.exoplayer2.extractor.wav;
+
+import com.google.android.exoplayer2.C;
+
+/** Header for a WAV file. */
+/*package*/ final class WavHeader {
+
+  /** Number of audio chanels. */
+  private final int numChannels;
+  /** Sample rate in Hertz. */
+  private final int sampleRateHz;
+  /** Average bytes per second for the sample data. */
+  private final int averageBytesPerSecond;
+  /** Alignment for frames of audio data; should equal {@code numChannels * bitsPerSample / 8}. */
+  private final int blockAlignment;
+  /** Bits per sample for the audio data. */
+  private final int bitsPerSample;
+  /** The PCM encoding */
+  @C.PcmEncoding
+  private final int encoding;
+
+  /** Offset to the start of sample data. */
+  private long dataStartPosition;
+  /** Total size in bytes of the sample data. */
+  private long dataSize;
+
+  public WavHeader(int numChannels, int sampleRateHz, int averageBytesPerSecond, int blockAlignment,
+      int bitsPerSample, @C.PcmEncoding int encoding) {
+    this.numChannels = numChannels;
+    this.sampleRateHz = sampleRateHz;
+    this.averageBytesPerSecond = averageBytesPerSecond;
+    this.blockAlignment = blockAlignment;
+    this.bitsPerSample = bitsPerSample;
+    this.encoding = encoding;
+  }
+
+  /** Returns the duration in microseconds of this WAV. */
+  public long getDurationUs() {
+    long numFrames = dataSize / blockAlignment;
+    return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz;
+  }
+
+  /** Returns the bytes per frame of this WAV. */
+  public int getBytesPerFrame() {
+    return blockAlignment;
+  }
+
+  /** Returns the bitrate of this WAV. */
+  public int getBitrate() {
+    return sampleRateHz * bitsPerSample * numChannels;
+  }
+
+  /** Returns the sample rate in Hertz of this WAV. */
+  public int getSampleRateHz() {
+    return sampleRateHz;
+  }
+
+  /** Returns the number of audio channels in this WAV. */
+  public int getNumChannels() {
+    return numChannels;
+  }
+
+  /** Returns the position in bytes in this WAV for the given time in microseconds. */
+  public long getPosition(long timeUs) {
+    long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND;
+    // Round down to nearest frame.
+    long position = (unroundedPosition / blockAlignment) * blockAlignment;
+    return Math.min(position, dataSize - blockAlignment) + dataStartPosition;
+  }
+
+  /** Returns the time in microseconds for the given position in bytes in this WAV. */
+  public long getTimeUs(long position) {
+    return position * C.MICROS_PER_SECOND / averageBytesPerSecond;
+  }
+
+  /** Returns true if the data start position and size have been set. */
+  public boolean hasDataBounds() {
+    return dataStartPosition != 0 && dataSize != 0;
+  }
+
+  /** Sets the start position and size in bytes of sample data in this WAV. */
+  public void setDataBounds(long dataStartPosition, long dataSize) {
+    this.dataStartPosition = dataStartPosition;
+    this.dataSize = dataSize;
+  }
+
+  /** Returns the PCM encoding. **/
+  @C.PcmEncoding
+  public int getEncoding() {
+    return encoding;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/extractor/wav/WavHeaderReader.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.extractor.wav;
+
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import 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";
+
+  /** Integer PCM audio data. */
+  private static final int TYPE_PCM = 0x0001;
+  /** Extended WAVE format. */
+  private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
+
+  /**
+   * Peeks and returns a {@code WavHeader}.
+   *
+   * @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.
+   */
+  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 != Util.getIntegerCodeForString("RIFF")) {
+      return null;
+    }
+
+    input.peekFully(scratch.data, 0, 4);
+    scratch.setPosition(0);
+    int riffFormat = scratch.readInt();
+    if (riffFormat != Util.getIntegerCodeForString("WAVE")) {
+      Log.e(TAG, "Unsupported RIFF format: " + riffFormat);
+      return null;
+    }
+
+    // Skip chunks until we find the format chunk.
+    chunkHeader = ChunkHeader.peek(input, scratch);
+    while (chunkHeader.id != Util.getIntegerCodeForString("fmt ")) {
+      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 type = scratch.readLittleEndianUnsignedShort();
+    int numChannels = scratch.readLittleEndianUnsignedShort();
+    int sampleRateHz = scratch.readLittleEndianUnsignedIntToInt();
+    int averageBytesPerSecond = scratch.readLittleEndianUnsignedIntToInt();
+    int blockAlignment = scratch.readLittleEndianUnsignedShort();
+    int bitsPerSample = scratch.readLittleEndianUnsignedShort();
+
+    int expectedBlockAlignment = numChannels * bitsPerSample / 8;
+    if (blockAlignment != expectedBlockAlignment) {
+      throw new ParserException("Expected block alignment: " + expectedBlockAlignment + "; got: "
+          + blockAlignment);
+    }
+
+    @C.PcmEncoding int encoding = Util.getPcmEncoding(bitsPerSample);
+    if (encoding == C.ENCODING_INVALID) {
+      Log.e(TAG, "Unsupported WAV bit depth: " + bitsPerSample);
+      return null;
+    }
+
+    if (type != TYPE_PCM && type != TYPE_WAVE_FORMAT_EXTENSIBLE) {
+      Log.e(TAG, "Unsupported WAV format type: " + type);
+      return null;
+    }
+
+    // If present, skip extensionSize, validBitsPerSample, channelMask, subFormatGuid, ...
+    input.advancePeekPosition((int) chunkHeader.size - 16);
+
+    return new WavHeader(numChannels, sampleRateHz, averageBytesPerSecond, blockAlignment,
+        bitsPerSample, encoding);
+  }
+
+  /**
+   * Skips to the data in the given WAV input stream and returns its data size. After calling, the
+   * input stream's position will point to the start of sample data in the WAV.
+   * <p>
+   * If an exception is thrown, the input position will be left pointing to a chunk header.
+   *
+   * @param input Input stream to skip to the data chunk in. Its peek position must be pointing to
+   *     a valid chunk header.
+   * @param wavHeader WAV header to populate with data bounds.
+   * @throws ParserException If an error occurs parsing chunks.
+   * @throws IOException If reading from the input fails.
+   * @throws InterruptedException If interrupted while reading from input.
+   */
+  public static void skipToData(ExtractorInput input, WavHeader wavHeader)
+      throws IOException, InterruptedException {
+    Assertions.checkNotNull(input);
+    Assertions.checkNotNull(wavHeader);
+
+    // Make sure the peek position is set to the read position before we peek the first header.
+    input.resetPeekPosition();
+
+    ParsableByteArray scratch = new ParsableByteArray(ChunkHeader.SIZE_IN_BYTES);
+    // Skip all chunks until we hit the data header.
+    ChunkHeader chunkHeader = ChunkHeader.peek(input, scratch);
+    while (chunkHeader.id != Util.getIntegerCodeForString("data")) {
+      Log.w(TAG, "Ignoring unknown WAV chunk: " + chunkHeader.id);
+      long bytesToSkip = ChunkHeader.SIZE_IN_BYTES + chunkHeader.size;
+      // Override size of RIFF chunk, since it describes its size as the entire file.
+      if (chunkHeader.id == Util.getIntegerCodeForString("RIFF")) {
+        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);
+
+    wavHeader.setDataBounds(input.getPosition(), chunkHeader.size);
+  }
+
+  /** 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, 0, SIZE_IN_BYTES);
+      scratch.setPosition(0);
+
+      int id = scratch.readInt();
+      long size = scratch.readLittleEndianUnsignedInt();
+
+      return new ChunkHeader(id, size);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.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.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Information about a {@link MediaCodec} for a given mime type.
+ */
+@TargetApi(16)
+public final class MediaCodecInfo {
+
+  public static final String TAG = "MediaCodecInfo";
+
+  /**
+   * 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;
+
+  /**
+   * 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;
+
+  private final String mimeType;
+  private final CodecCapabilities capabilities;
+
+  /**
+   * 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, null, null);
+  }
+
+  /**
+   * Creates an instance.
+   *
+   * @param name The name of the {@link MediaCodec}.
+   * @param mimeType A mime type supported by the {@link MediaCodec}.
+   * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type.
+   * @return The created instance.
+   */
+  public static MediaCodecInfo newInstance(String name, String mimeType,
+      CodecCapabilities capabilities) {
+    return new MediaCodecInfo(name, mimeType, capabilities);
+  }
+
+  /**
+   * @param name The name of the decoder.
+   * @param capabilities The capabilities of the decoder.
+   */
+  private MediaCodecInfo(String name, String mimeType, CodecCapabilities capabilities) {
+    this.name = Assertions.checkNotNull(name);
+    this.mimeType = mimeType;
+    this.capabilities = capabilities;
+    adaptive = capabilities != null && isAdaptive(capabilities);
+    tunneling = capabilities != null && isTunneling(capabilities);
+  }
+
+  /**
+   * 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;
+  }
+
+  /**
+   * Whether the decoder supports the given {@code codec}. If there is insufficient information to
+   * decide, returns true.
+   *
+   * @param codec Codec string as defined in RFC 6381.
+   * @return True if the given codec is supported by the decoder.
+   */
+  public boolean isCodecSupported(String codec) {
+    if (codec == null || mimeType == null) {
+      return true;
+    }
+    String codecMimeType = MimeTypes.getMediaMimeType(codec);
+    if (codecMimeType == null) {
+      return true;
+    }
+    if (!mimeType.equals(codecMimeType)) {
+      logNoSupport("codec.mime " + codec + ", " + codecMimeType);
+      return false;
+    }
+    Pair<Integer, Integer> codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(codec);
+    if (codecProfileAndLevel == null) {
+      // If we don't know any better, we assume that the profile and level are supported.
+      return true;
+    }
+    for (CodecProfileLevel capabilities : getProfileLevels()) {
+      if (capabilities.profile == codecProfileAndLevel.first
+          && capabilities.level >= codecProfileAndLevel.second) {
+        return true;
+      }
+    }
+    logNoSupport("codec.profileLevel, " + codec + ", " + codecMimeType);
+    return false;
+  }
+
+  /**
+   * 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 (!areSizeAndRateSupported(videoCapabilities, width, height, frameRate)) {
+      // Capabilities are known to be inaccurately reported for vertical resolutions on some devices
+      // (b/31387661). If the video is vertical and the capabilities indicate support if the width
+      // and height are swapped, we assume that the vertical resolution is also supported.
+      if (width >= height
+          || !areSizeAndRateSupported(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) {
+      logNoSupport("align.caps");
+      return null;
+    }
+    VideoCapabilities videoCapabilities = capabilities.getVideoCapabilities();
+    if (videoCapabilities == null) {
+      logNoSupport("align.vCaps");
+      return null;
+    }
+    int widthAlignment = videoCapabilities.getWidthAlignment();
+    int heightAlignment = videoCapabilities.getHeightAlignment();
+    return new Point(Util.ceilDivide(width, widthAlignment) * widthAlignment,
+        Util.ceilDivide(height, heightAlignment) * heightAlignment);
+  }
+
+  /**
+   * 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;
+    }
+    if (audioCapabilities.getMaxInputChannelCount() < 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 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);
+  }
+
+  @TargetApi(21)
+  private static boolean areSizeAndRateSupported(VideoCapabilities capabilities, int width,
+      int height, double frameRate) {
+    return frameRate == Format.NO_VALUE || frameRate <= 0
+        ? capabilities.isSizeSupported(width, height)
+        : capabilities.areSizeAndRateSupported(width, height, frameRate);
+  }
+
+  private static boolean isTunneling(CodecCapabilities capabilities) {
+    return Util.SDK_INT >= 21 && isTunnelingV21(capabilities);
+  }
+
+  @TargetApi(21)
+  private static boolean isTunnelingV21(CodecCapabilities capabilities) {
+    return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
@@ -0,0 +1,1110 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.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.MediaFormat;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.Log;
+import com.google.android.exoplayer2.BaseRenderer;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderCounters;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.drm.DrmSession;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.TraceUtil;
+import com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An abstract renderer that uses {@link MediaCodec} to decode samples for rendering.
+ */
+@TargetApi(16)
+public abstract class MediaCodecRenderer extends BaseRenderer {
+
+  /**
+   * Thrown when a failure occurs instantiating a decoder.
+   */
+  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 name of the decoder that failed to initialize. Null if no suitable decoder was found.
+     */
+    public final String decoderName;
+
+    /**
+     * An optional developer-readable diagnostic information string. May be null.
+     */
+    public final String diagnosticInfo;
+
+    public DecoderInitializationException(Format format, Throwable cause,
+        boolean secureDecoderRequired, int errorCode) {
+      super("Decoder init failed: [" + errorCode + "], " + format, cause);
+      this.mimeType = format.sampleMimeType;
+      this.secureDecoderRequired = secureDecoderRequired;
+      this.decoderName = null;
+      this.diagnosticInfo = buildCustomDiagnosticInfo(errorCode);
+    }
+
+    public DecoderInitializationException(Format format, Throwable cause,
+        boolean secureDecoderRequired, String decoderName) {
+      super("Decoder init failed: " + decoderName + ", " + format, cause);
+      this.mimeType = format.sampleMimeType;
+      this.secureDecoderRequired = secureDecoderRequired;
+      this.decoderName = decoderName;
+      this.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;
+    }
+
+    private static String buildCustomDiagnosticInfo(int errorCode) {
+      String sign = errorCode < 0 ? "neg_" : "";
+      return "com.google.android.exoplayer.MediaCodecTrackRenderer_" + sign + Math.abs(errorCode);
+    }
+
+  }
+
+  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;
+
+  /**
+   * 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;
+
+  /**
+   * The codec 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 codec to be re-initialized, but we
+   * haven't yet signaled an end of stream to the existing codec. We need to do so in order to
+   * ensure that it outputs any remaining buffers before we release it.
+   */
+  private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1;
+  /**
+   * The input format has changed in a way that requires the codec to be re-initialized, and we've
+   * signaled an end of stream to the existing codec. We're waiting for the codec to output an end
+   * of stream signal to indicate that it has output any remaining buffers before we release it.
+   */
+  private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
+
+  /**
+   * H.264/AVC buffer to queue when using the adaptation workaround (see
+   * {@link #codecNeedsAdaptationWorkaround(String)}. Consists of three NAL units with start codes:
+   * Baseline sequence/picture parameter sets and a 32 * 32 pixel IDR slice. This stream can be
+   * queued to force a resolution change when adapting to a new format.
+   */
+  private static final byte[] ADAPTATION_WORKAROUND_BUFFER = Util.getBytesFromHexString(
+      "0000016742C00BDA259000000168CE0F13200000016588840DCE7118A0002FBF1C31C3275D78");
+  private static final int ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT = 32;
+
+  private final MediaCodecSelector mediaCodecSelector;
+  private final DrmSessionManager<FrameworkMediaCrypto> drmSessionManager;
+  private final boolean playClearSamplesWithoutKeys;
+  private final DecoderInputBuffer buffer;
+  private final FormatHolder formatHolder;
+  private final List<Long> decodeOnlyPresentationTimestamps;
+  private final MediaCodec.BufferInfo outputBufferInfo;
+
+  private Format format;
+  private MediaCodec codec;
+  private DrmSession<FrameworkMediaCrypto> drmSession;
+  private DrmSession<FrameworkMediaCrypto> pendingDrmSession;
+  private boolean codecIsAdaptive;
+  private boolean codecNeedsDiscardToSpsWorkaround;
+  private boolean codecNeedsFlushWorkaround;
+  private boolean codecNeedsAdaptationWorkaround;
+  private boolean codecNeedsEosPropagationWorkaround;
+  private boolean codecNeedsEosFlushWorkaround;
+  private boolean codecNeedsMonoChannelCountWorkaround;
+  private boolean codecNeedsAdaptationWorkaroundBuffer;
+  private boolean shouldSkipAdaptationWorkaroundOutputBuffer;
+  private ByteBuffer[] inputBuffers;
+  private ByteBuffer[] outputBuffers;
+  private long codecHotswapDeadlineMs;
+  private int inputIndex;
+  private int outputIndex;
+  private boolean shouldSkipOutputBuffer;
+  private boolean codecReconfigured;
+  private int codecReconfigurationState;
+  private int codecReinitializationState;
+  private boolean codecReceivedBuffers;
+  private boolean codecReceivedEos;
+
+  private boolean inputStreamEnded;
+  private boolean outputStreamEnded;
+  private boolean waitingForKeys;
+  private boolean waitingForFirstSyncFrame;
+
+  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.
+   */
+  public MediaCodecRenderer(int trackType, MediaCodecSelector mediaCodecSelector,
+      DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      boolean playClearSamplesWithoutKeys) {
+    super(trackType);
+    Assertions.checkState(Util.SDK_INT >= 16);
+    this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector);
+    this.drmSessionManager = drmSessionManager;
+    this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
+    buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
+    formatHolder = new FormatHolder();
+    decodeOnlyPresentationTimestamps = new ArrayList<>();
+    outputBufferInfo = new MediaCodec.BufferInfo();
+    codecReconfigurationState = RECONFIGURATION_STATE_NONE;
+    codecReinitializationState = REINITIALIZATION_STATE_NONE;
+  }
+
+  @Override
+  public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
+    return ADAPTIVE_NOT_SEAMLESS;
+  }
+
+  @Override
+  public final int supportsFormat(Format format) throws ExoPlaybackException {
+    try {
+      return supportsFormat(mediaCodecSelector, format);
+    } catch (DecoderQueryException e) {
+      throw ExoPlaybackException.createForRenderer(e, getIndex());
+    }
+  }
+
+  /**
+   * Returns the extent to which the renderer is capable of supporting a given format.
+   *
+   * @param mediaCodecSelector The decoder selector.
+   * @param format The format.
+   * @return The extent to which the renderer is capable of supporting the given format. See
+   *     {@link #supportsFormat(Format)} for more detail.
+   * @throws DecoderQueryException If there was an error querying decoders.
+   */
+  protected abstract int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format)
+      throws DecoderQueryException;
+
+  /**
+   * Returns a {@link MediaCodecInfo} for a given format.
+   *
+   * @param mediaCodecSelector The decoder selector.
+   * @param format The format for which a decoder is required.
+   * @param requiresSecureDecoder Whether a secure decoder is required.
+   * @return A {@link MediaCodecInfo} describing the decoder to instantiate, or null if no
+   *     suitable decoder exists.
+   * @throws DecoderQueryException Thrown if there was an error querying decoders.
+   */
+  protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector,
+      Format format, boolean requiresSecureDecoder) throws DecoderQueryException {
+    return mediaCodecSelector.getDecoderInfo(format.sampleMimeType, requiresSecureDecoder);
+  }
+
+  /**
+   * Configures a newly created {@link MediaCodec}.
+   *
+   * @param codecInfo Information about the {@link MediaCodec} being configured.
+   * @param codec The {@link MediaCodec} to configure.
+   * @param format The format for which the codec is being configured.
+   * @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption.
+   * @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
+   */
+  protected abstract void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
+      MediaCrypto crypto) throws DecoderQueryException;
+
+  @SuppressWarnings("deprecation")
+  protected final void maybeInitCodec() throws ExoPlaybackException {
+    if (!shouldInitCodec()) {
+      return;
+    }
+
+    drmSession = pendingDrmSession;
+    String mimeType = format.sampleMimeType;
+    MediaCrypto mediaCrypto = null;
+    boolean drmSessionRequiresSecureDecoder = false;
+    if (drmSession != null) {
+      @DrmSession.State int drmSessionState = drmSession.getState();
+      if (drmSessionState == DrmSession.STATE_ERROR) {
+        throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+      } else if (drmSessionState == DrmSession.STATE_OPENED
+          || drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
+        mediaCrypto = drmSession.getMediaCrypto().getWrappedMediaCrypto();
+        drmSessionRequiresSecureDecoder = drmSession.requiresSecureDecoderComponent(mimeType);
+      } else {
+        // The drm session isn't open yet.
+        return;
+      }
+    }
+
+    MediaCodecInfo decoderInfo = null;
+    try {
+      decoderInfo = getDecoderInfo(mediaCodecSelector, format, drmSessionRequiresSecureDecoder);
+      if (decoderInfo == null && drmSessionRequiresSecureDecoder) {
+        // The drm session indicates that a secure decoder is required, but the device does not have
+        // one. Assuming that supportsFormat indicated support for the media being played, we know
+        // that it does not require a secure output path. Most CDM implementations allow playback to
+        // proceed with a non-secure decoder in this case, so we try our luck.
+        decoderInfo = getDecoderInfo(mediaCodecSelector, format, false);
+        if (decoderInfo != null) {
+          Log.w(TAG, "Drm session requires secure decoder for " + mimeType + ", but "
+              + "no secure decoder available. Trying to proceed with " + decoderInfo.name + ".");
+        }
+      }
+    } catch (DecoderQueryException e) {
+      throwDecoderInitError(new DecoderInitializationException(format, e,
+          drmSessionRequiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR));
+    }
+
+    if (decoderInfo == null) {
+      throwDecoderInitError(new DecoderInitializationException(format, null,
+          drmSessionRequiresSecureDecoder,
+          DecoderInitializationException.NO_SUITABLE_DECODER_ERROR));
+    }
+
+    String codecName = decoderInfo.name;
+    codecIsAdaptive = decoderInfo.adaptive;
+    codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format);
+    codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName);
+    codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName);
+    codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName);
+    codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName);
+    codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround(codecName, format);
+    try {
+      long codecInitializingTimestamp = SystemClock.elapsedRealtime();
+      TraceUtil.beginSection("createCodec:" + codecName);
+      codec = MediaCodec.createByCodecName(codecName);
+      TraceUtil.endSection();
+      TraceUtil.beginSection("configureCodec");
+      configureCodec(decoderInfo, codec, format, mediaCrypto);
+      TraceUtil.endSection();
+      TraceUtil.beginSection("startCodec");
+      codec.start();
+      TraceUtil.endSection();
+      long codecInitializedTimestamp = SystemClock.elapsedRealtime();
+      onCodecInitialized(codecName, codecInitializedTimestamp,
+          codecInitializedTimestamp - codecInitializingTimestamp);
+      inputBuffers = codec.getInputBuffers();
+      outputBuffers = codec.getOutputBuffers();
+    } catch (Exception e) {
+      throwDecoderInitError(new DecoderInitializationException(format, e,
+          drmSessionRequiresSecureDecoder, codecName));
+    }
+    codecHotswapDeadlineMs = getState() == STATE_STARTED
+        ? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS) : C.TIME_UNSET;
+    inputIndex = C.INDEX_UNSET;
+    outputIndex = C.INDEX_UNSET;
+    waitingForFirstSyncFrame = true;
+    decoderCounters.decoderInitCount++;
+  }
+
+  private void throwDecoderInitError(DecoderInitializationException e)
+      throws ExoPlaybackException {
+    throw ExoPlaybackException.createForRenderer(e, getIndex());
+  }
+
+  protected boolean shouldInitCodec() {
+    return codec == null && format != null;
+  }
+
+  protected final MediaCodec getCodec() {
+    return codec;
+  }
+
+  @Override
+  protected void onEnabled(boolean joining) throws ExoPlaybackException {
+    decoderCounters = new DecoderCounters();
+  }
+
+  @Override
+  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+    inputStreamEnded = false;
+    outputStreamEnded = false;
+    if (codec != null) {
+      flushCodec();
+    }
+  }
+
+  @Override
+  protected void onDisabled() {
+    format = null;
+    try {
+      releaseCodec();
+    } finally {
+      try {
+        if (drmSession != null) {
+          drmSessionManager.releaseSession(drmSession);
+        }
+      } finally {
+        try {
+          if (pendingDrmSession != null && pendingDrmSession != drmSession) {
+            drmSessionManager.releaseSession(pendingDrmSession);
+          }
+        } finally {
+          drmSession = null;
+          pendingDrmSession = null;
+        }
+      }
+    }
+  }
+
+  protected void releaseCodec() {
+    if (codec != null) {
+      codecHotswapDeadlineMs = C.TIME_UNSET;
+      inputIndex = C.INDEX_UNSET;
+      outputIndex = C.INDEX_UNSET;
+      waitingForKeys = false;
+      shouldSkipOutputBuffer = false;
+      decodeOnlyPresentationTimestamps.clear();
+      inputBuffers = null;
+      outputBuffers = null;
+      codecReconfigured = false;
+      codecReceivedBuffers = false;
+      codecIsAdaptive = false;
+      codecNeedsDiscardToSpsWorkaround = false;
+      codecNeedsFlushWorkaround = false;
+      codecNeedsAdaptationWorkaround = false;
+      codecNeedsEosPropagationWorkaround = false;
+      codecNeedsEosFlushWorkaround = false;
+      codecNeedsMonoChannelCountWorkaround = false;
+      codecNeedsAdaptationWorkaroundBuffer = false;
+      shouldSkipAdaptationWorkaroundOutputBuffer = false;
+      codecReceivedEos = false;
+      codecReconfigurationState = RECONFIGURATION_STATE_NONE;
+      codecReinitializationState = REINITIALIZATION_STATE_NONE;
+      decoderCounters.decoderReleaseCount++;
+      try {
+        codec.stop();
+      } finally {
+        try {
+          codec.release();
+        } finally {
+          codec = null;
+          if (drmSession != null && pendingDrmSession != drmSession) {
+            try {
+              drmSessionManager.releaseSession(drmSession);
+            } finally {
+              drmSession = 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 (outputStreamEnded) {
+      return;
+    }
+    if (format == null) {
+      readFormat();
+    }
+    maybeInitCodec();
+    if (codec != null) {
+      TraceUtil.beginSection("drainAndFeed");
+      while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
+      while (feedInputBuffer()) {}
+      TraceUtil.endSection();
+    } else if (format != null) {
+      skipToKeyframeBefore(positionUs);
+    }
+    decoderCounters.ensureUpdated();
+  }
+
+  private void readFormat() throws ExoPlaybackException {
+    int result = readSource(formatHolder, null);
+    if (result == C.RESULT_FORMAT_READ) {
+      onInputFormatChanged(formatHolder.format);
+    }
+  }
+
+  protected void flushCodec() throws ExoPlaybackException {
+    codecHotswapDeadlineMs = C.TIME_UNSET;
+    inputIndex = C.INDEX_UNSET;
+    outputIndex = C.INDEX_UNSET;
+    waitingForFirstSyncFrame = true;
+    waitingForKeys = false;
+    shouldSkipOutputBuffer = false;
+    decodeOnlyPresentationTimestamps.clear();
+    codecNeedsAdaptationWorkaroundBuffer = false;
+    shouldSkipAdaptationWorkaroundOutputBuffer = false;
+    if (codecNeedsFlushWorkaround || (codecNeedsEosFlushWorkaround && codecReceivedEos)) {
+      // Workaround framework bugs. See [Internal: b/8347958, b/8578467, b/8543366, b/23361053].
+      releaseCodec();
+      maybeInitCodec();
+    } else if (codecReinitializationState != REINITIALIZATION_STATE_NONE) {
+      // We're already waiting to release and re-initialize the codec. Since we're now flushing,
+      // there's no need to wait any longer.
+      releaseCodec();
+      maybeInitCodec();
+    } else {
+      // We can flush and re-use the existing decoder.
+      codec.flush();
+      codecReceivedBuffers = false;
+    }
+    if (codecReconfigured && format != null) {
+      // Any reconfiguration data that we send shortly before the flush may be discarded. We
+      // avoid this issue by sending reconfiguration data following every flush.
+      codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+    }
+  }
+
+  /**
+   * @return Whether it may be possible to feed more input data.
+   * @throws ExoPlaybackException If an error occurs feeding the input buffer.
+   */
+  private boolean feedInputBuffer() throws ExoPlaybackException {
+    if (codec == null || codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM
+        || inputStreamEnded) {
+      // We need to reinitialize the codec or the input stream has ended.
+      return false;
+    }
+
+    if (inputIndex < 0) {
+      inputIndex = codec.dequeueInputBuffer(0);
+      if (inputIndex < 0) {
+        return false;
+      }
+      buffer.data = inputBuffers[inputIndex];
+      buffer.clear();
+    }
+
+    if (codecReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {
+      // We need to re-initialize the codec. Send an end of stream signal to the existing codec so
+      // that it outputs any remaining buffers before we release it.
+      if (codecNeedsEosPropagationWorkaround) {
+        // Do nothing.
+      } else {
+        codecReceivedEos = true;
+        codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+        inputIndex = C.INDEX_UNSET;
+      }
+      codecReinitializationState = REINITIALIZATION_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);
+      inputIndex = C.INDEX_UNSET;
+      codecReceivedBuffers = true;
+      return true;
+    }
+
+    int result;
+    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 < format.initializationData.size(); i++) {
+          byte[] data = format.initializationData.get(i);
+          buffer.data.put(data);
+        }
+        codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;
+      }
+      adaptiveReconfigurationBytes = buffer.data.position();
+      result = readSource(formatHolder, buffer);
+    }
+
+    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.format);
+      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 (codecNeedsEosPropagationWorkaround) {
+          // Do nothing.
+        } else {
+          codecReceivedEos = true;
+          codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+          inputIndex = C.INDEX_UNSET;
+        }
+      } catch (CryptoException e) {
+        throw ExoPlaybackException.createForRenderer(e, getIndex());
+      }
+      return false;
+    }
+    if (waitingForFirstSyncFrame && !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;
+    }
+    waitingForFirstSyncFrame = 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);
+      }
+
+      buffer.flip();
+      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);
+      }
+      inputIndex = C.INDEX_UNSET;
+      codecReceivedBuffers = true;
+      codecReconfigurationState = RECONFIGURATION_STATE_NONE;
+      decoderCounters.inputBufferCount++;
+    } catch (CryptoException e) {
+      throw ExoPlaybackException.createForRenderer(e, getIndex());
+    }
+    return true;
+  }
+
+  private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(DecoderInputBuffer buffer,
+      int adaptiveReconfigurationBytes) {
+    MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfoV16();
+    if (adaptiveReconfigurationBytes == 0) {
+      return cryptoInfo;
+    }
+    // There must be at least one sub-sample, although numBytesOfClearData is permitted to be
+    // null if it contains no clear data. Instantiate it if needed, and add the reconfiguration
+    // bytes to the clear byte count of the first sub-sample.
+    if (cryptoInfo.numBytesOfClearData == null) {
+      cryptoInfo.numBytesOfClearData = new int[1];
+    }
+    cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes;
+    return cryptoInfo;
+  }
+
+  private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
+    if (drmSession == null) {
+      return false;
+    }
+    @DrmSession.State int drmSessionState = drmSession.getState();
+    if (drmSessionState == DrmSession.STATE_ERROR) {
+      throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+    }
+    return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
+        && (bufferEncrypted || !playClearSamplesWithoutKeys);
+  }
+
+  /**
+   * 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 format is read from the upstream {@link MediaPeriod}.
+   *
+   * @param newFormat The new format.
+   * @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}.
+   */
+  protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
+    Format oldFormat = format;
+    format = newFormat;
+
+    boolean drmInitDataChanged = !Util.areEqual(format.drmInitData, oldFormat == null ? null
+        : oldFormat.drmInitData);
+    if (drmInitDataChanged) {
+      if (format.drmInitData != null) {
+        if (drmSessionManager == null) {
+          throw ExoPlaybackException.createForRenderer(
+              new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
+        }
+        pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData);
+        if (pendingDrmSession == drmSession) {
+          drmSessionManager.releaseSession(pendingDrmSession);
+        }
+      } else {
+        pendingDrmSession = null;
+      }
+    }
+
+    if (pendingDrmSession == drmSession && codec != null
+        && canReconfigureCodec(codec, codecIsAdaptive, oldFormat, format)) {
+      codecReconfigured = true;
+      codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+      codecNeedsAdaptationWorkaroundBuffer = codecNeedsAdaptationWorkaround
+          && format.width == oldFormat.width && format.height == oldFormat.height;
+    } else {
+      if (codecReceivedBuffers) {
+        // Signal end of stream and wait for any final output buffers before re-initialization.
+        codecReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
+      } else {
+        // There aren't any final output buffers, so perform re-initialization immediately.
+        releaseCodec();
+        maybeInitCodec();
+      }
+    }
+  }
+
+  /**
+   * Called when the output format of the {@link MediaCodec} changes.
+   * <p>
+   * The default implementation is a no-op.
+   *
+   * @param codec The {@link MediaCodec} instance.
+   * @param outputFormat The new output format.
+   */
+  protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) {
+    // Do nothing.
+  }
+
+  /**
+   * Called when the output stream ends, meaning that the last output buffer has been processed and
+   * the {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag has been propagated through the decoder.
+   * <p>
+   * The default implementation is a no-op.
+   */
+  protected void onOutputStreamEnded() {
+    // 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} should be reconfigured for a new format by
+   * sending codec specific initialization data at the start of the next input buffer. If true is
+   * returned then the {@link MediaCodec} instance will be reconfigured in this way. If false is
+   * returned then the instance will be released, and a new instance will be created for the new
+   * format.
+   * <p>
+   * The default implementation returns false.
+   *
+   * @param codec The existing {@link MediaCodec} instance.
+   * @param codecIsAdaptive Whether the codec is adaptive.
+   * @param oldFormat The format for which the existing instance is configured.
+   * @param newFormat The new format.
+   * @return Whether the existing instance can be reconfigured.
+   */
+  protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive, Format oldFormat,
+      Format newFormat) {
+    return false;
+  }
+
+  @Override
+  public boolean isEnded() {
+    return outputStreamEnded;
+  }
+
+  @Override
+  public boolean isReady() {
+    return format != null && !waitingForKeys && (isSourceReady() || outputIndex >= 0
+        || (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;
+  }
+
+  /**
+   * @return Whether it may be possible to drain more output data.
+   * @throws ExoPlaybackException If an error occurs draining the output buffer.
+   */
+  @SuppressWarnings("deprecation")
+  private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
+      throws ExoPlaybackException {
+    if (outputIndex < 0) {
+      outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs());
+      if (outputIndex >= 0) {
+        // We've dequeued a buffer.
+        if (shouldSkipAdaptationWorkaroundOutputBuffer) {
+          shouldSkipAdaptationWorkaroundOutputBuffer = false;
+          codec.releaseOutputBuffer(outputIndex, false);
+          outputIndex = C.INDEX_UNSET;
+          return true;
+        }
+        if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+          // The dequeued buffer indicates the end of the stream. Process it immediately.
+          processEndOfStream();
+          outputIndex = C.INDEX_UNSET;
+          return false;
+        } else {
+          // The dequeued buffer is a media buffer. Do some initial setup. The buffer will be
+          // processed by calling processOutputBuffer (possibly multiple times) below.
+          ByteBuffer outputBuffer = outputBuffers[outputIndex];
+          if (outputBuffer != null) {
+            outputBuffer.position(outputBufferInfo.offset);
+            outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size);
+          }
+          shouldSkipOutputBuffer = shouldSkipOutputBuffer(outputBufferInfo.presentationTimeUs);
+        }
+      } else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED /* (-2) */) {
+        processOutputFormat();
+        return true;
+      } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED /* (-3) */) {
+        processOutputBuffersChanged();
+        return true;
+      } else /* MediaCodec.INFO_TRY_AGAIN_LATER (-1) or unknown negative return value */ {
+        if (codecNeedsEosPropagationWorkaround && (inputStreamEnded
+            || codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM)) {
+          processEndOfStream();
+        }
+        return false;
+      }
+    }
+
+    if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex],
+        outputIndex, outputBufferInfo.flags, outputBufferInfo.presentationTimeUs,
+        shouldSkipOutputBuffer)) {
+      onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs);
+      outputIndex = C.INDEX_UNSET;
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Processes a new output format.
+   */
+  private void processOutputFormat() {
+    MediaFormat format = codec.getOutputFormat();
+    if (codecNeedsAdaptationWorkaround
+        && format.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT
+        && format.getInteger(MediaFormat.KEY_HEIGHT) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT) {
+      // We assume this format changed event was caused by the adaptation workaround.
+      shouldSkipAdaptationWorkaroundOutputBuffer = true;
+      return;
+    }
+    if (codecNeedsMonoChannelCountWorkaround) {
+      format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
+    }
+    onOutputFormatChanged(codec, format);
+  }
+
+  /**
+   * Processes a change in the output buffers.
+   */
+  @SuppressWarnings("deprecation")
+  private void processOutputBuffersChanged() {
+    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 android.os.SystemClock#elapsedRealtime()} in microseconds,
+   *     measured at the start of the current iteration of the rendering loop.
+   * @param codec The {@link MediaCodec} instance.
+   * @param buffer The output buffer to process.
+   * @param bufferIndex The index of the output buffer.
+   * @param bufferFlags The flags attached to the output buffer.
+   * @param bufferPresentationTimeUs The presentation time of the output buffer in microseconds.
+   * @param shouldSkip Whether the buffer should be skipped (i.e. not rendered).
+   *
+   * @return Whether the output buffer was fully processed (e.g. rendered or skipped).
+   * @throws ExoPlaybackException If an error occurs processing the output buffer.
+   */
+  protected abstract boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs,
+      MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags,
+      long bufferPresentationTimeUs, boolean shouldSkip) throws ExoPlaybackException;
+
+  /**
+   * Processes an end of stream signal.
+   *
+   * @throws ExoPlaybackException If an error occurs processing the signal.
+   */
+  private void processEndOfStream() throws ExoPlaybackException {
+    if (codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
+      // We're waiting to re-initialize the codec, and have now processed all final buffers.
+      releaseCodec();
+      maybeInitCodec();
+    } else {
+      outputStreamEnded = true;
+      onOutputStreamEnded();
+    }
+  }
+
+  private boolean shouldSkipOutputBuffer(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;
+  }
+
+  /**
+   * 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.
+   *
+   * @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 whether the decoder is known to get stuck during some adaptations where the resolution
+   * does not change.
+   * <p>
+   * If true is returned, the renderer will work around the issue by queueing and discarding a blank
+   * frame at a different resolution, which resets the codec's internal state.
+   * <p>
+   * See [Internal: b/27807182].
+   *
+   * @param name The name of the decoder.
+   * @return True if the decoder is known to get stuck during some adaptations.
+   */
+  private static boolean codecNeedsAdaptationWorkaround(String name) {
+    return 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));
+  }
+
+  /**
+   * 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 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 name The name of the decoder.
+   * @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM}
+   *     propagation incorrectly on the host device. False otherwise.
+   */
+  private static boolean codecNeedsEosPropagationWorkaround(String name) {
+    return Util.SDK_INT <= 17 && ("OMX.rk.video_decoder.avc".equals(name)
+        || "OMX.allwinner.video.decoder.avc".equals(name));
+  }
+
+  /**
+   * 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.
+   *
+   * @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)
+            && ("OMX.amlogic.avc.decoder.awesome".equals(name)
+                || "OMX.amlogic.avc.decoder.awesome.secure".equals(name)));
+  }
+
+  /**
+   * Returns whether the decoder is known to set the number of audio channels in the output format
+   * to 2 for the given input format, whilst only actually outputting a single channel.
+   * <p>
+   * If true is returned then we explicitly override the number of channels in the output format,
+   * setting it to 1.
+   *
+   * @param name The decoder name.
+   * @param format The input format.
+   * @return True if the device is known to set the number of audio channels in the output format
+   *     to 2 for the given input format, whilst only actually outputting a single channel. False
+   *     otherwise.
+   */
+  private static boolean codecNeedsMonoChannelCountWorkaround(String name, Format format) {
+    return Util.SDK_INT <= 18 && format.channelCount == 1
+        && "OMX.MTK.AUDIO.DECODER.MP3".equals(name);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.mediacodec;
+
+import android.media.MediaCodec;
+import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+
+/**
+ * Selector of {@link MediaCodec} instances.
+ */
+public interface MediaCodecSelector {
+
+  /**
+   * Default implementation of {@link MediaCodecSelector}.
+   */
+  MediaCodecSelector DEFAULT = new MediaCodecSelector() {
+
+    @Override
+    public MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
+        throws DecoderQueryException {
+      return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder);
+    }
+
+    @Override
+    public MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException {
+      return MediaCodecUtil.getPassthroughDecoderInfo();
+    }
+
+  };
+
+  /**
+   * Selects a decoder to instantiate for a given mime type.
+   *
+   * @param mimeType The mime type for which a decoder is required.
+   * @param requiresSecureDecoder Whether a secure decoder is required.
+   * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists.
+   * @throws DecoderQueryException Thrown if there was an error querying decoders.
+   */
+  MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
+      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.
+   */
+  MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
@@ -0,0 +1,616 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.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.Log;
+import android.util.Pair;
+import android.util.SparseIntArray;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+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;
+
+/**
+ * A utility class for querying the available codecs.
+ */
+@TargetApi(16)
+@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 MediaCodecInfo PASSTHROUGH_DECODER_INFO =
+      MediaCodecInfo.newPassthroughInstance("OMX.google.raw.decoder");
+  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";
+  // 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";
+
+  // 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)}
+   * and {@link #getDecoderInfos(String, 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.
+   */
+  public static void warmDecoderInfoCache(String mimeType, boolean secure) {
+    try {
+      getDecoderInfos(mimeType, secure);
+    } 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.
+   */
+  public static MediaCodecInfo getPassthroughDecoderInfo() {
+    // TODO: Return null if the raw decoder doesn't exist.
+    return PASSTHROUGH_DECODER_INFO;
+  }
+
+  /**
+   * 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.
+   * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder
+   *     exists.
+   * @throws DecoderQueryException If there was an error querying the available decoders.
+   */
+  public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure)
+      throws DecoderQueryException {
+    List<MediaCodecInfo> decoderInfos = getDecoderInfos(mimeType, secure);
+    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.
+   * @return A 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) throws DecoderQueryException {
+    CodecKey key = new CodecKey(mimeType, secure);
+    List<MediaCodecInfo> decoderInfos = decoderInfosCache.get(key);
+    if (decoderInfos != null) {
+      return decoderInfos;
+    }
+    MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21
+        ? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16();
+    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);
+      }
+    }
+    decoderInfos = Collections.unmodifiableList(decoderInfos);
+    decoderInfosCache.put(key, decoderInfos);
+    return decoderInfos;
+  }
+
+  private static List<MediaCodecInfo> getDecoderInfosInternal(
+      CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException {
+    try {
+      List<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);
+        String codecName = codecInfo.getName();
+        if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit)) {
+          for (String supportedType : codecInfo.getSupportedTypes()) {
+            if (supportedType.equalsIgnoreCase(mimeType)) {
+              try {
+                CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(supportedType);
+                boolean secure = mediaCodecList.isSecurePlaybackSupported(mimeType, capabilities);
+                if ((secureDecodersExplicit && key.secure == secure)
+                    || (!secureDecodersExplicit && !key.secure)) {
+                  decoderInfos.add(MediaCodecInfo.newInstance(codecName, mimeType, capabilities));
+                } else if (!secureDecodersExplicit && secure) {
+                  decoderInfos.add(MediaCodecInfo.newInstance(codecName + ".secure", mimeType,
+                      capabilities));
+                  // It only makes sense to have one synthesized secure decoder, return immediately.
+                  return decoderInfos;
+                }
+              } catch (Exception e) {
+                if (Util.SDK_INT <= 23 && !decoderInfos.isEmpty()) {
+                  // Suppress error querying secondary codec capabilities up to API level 23.
+                  Log.e(TAG, "Skipping codec " + codecName + " (failed to query capabilities)");
+                } else {
+                  // Rethrow error querying primary codec capabilities, or secondary codec
+                  // capabilities if API level is greater than 23.
+                  Log.e(TAG, "Failed to query codec " + codecName + " (" + supportedType + ")");
+                  throw e;
+                }
+              }
+            }
+          }
+        }
+      }
+      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 whether the specified codec is usable for decoding on the current device.
+   */
+  private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name,
+      boolean secureDecodersExplicit) {
+    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/398
+    if (Util.SDK_INT < 18 && "OMX.SEC.MP3.Decoder".equals(name)) {
+      return false;
+    }
+    // Work around https://github.com/google/ExoPlayer/issues/1528
+    if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name)
+        && "a70".equals(Util.DEVICE)) {
+      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/548
+    // VP8 decoder on Samsung Galaxy S3/S4/S4 Mini/Tab 3 does not render video.
+    if (Util.SDK_INT <= 19
+        && (Util.DEVICE.startsWith("d2") || Util.DEVICE.startsWith("serrano")
+        || Util.DEVICE.startsWith("jflte") || Util.DEVICE.startsWith("santos"))
+        && "samsung".equals(Util.MANUFACTURER) && "OMX.SEC.vp8.dec".equals(name)) {
+      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;
+    }
+
+    return true;
+  }
+
+  /**
+   * 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;
+      MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false);
+      if (decoderInfo != null) {
+        for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) {
+          result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result);
+        }
+        // We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are
+        // the levels mandated by the Android CDD.
+        result = Math.max(result, Util.SDK_INT >= 21 ? (720 * 480) : (480 * 360));
+      }
+      maxH264DecodableFrameSize = result;
+    }
+    return maxH264DecodableFrameSize;
+  }
+
+  /**
+   * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given
+   * codec description string (as defined by RFC 6381).
+   *
+   * @param codec A codec description string, as defined by RFC 6381.
+   * @return A pair (profile constant, level constant) if {@code codec} is well-formed and
+   *     recognized, or null otherwise
+   */
+  public static Pair<Integer, Integer> getCodecProfileAndLevel(String codec) {
+    if (codec == null) {
+      return null;
+    }
+    String[] parts = codec.split("\\.");
+    switch (parts[0]) {
+      case CODEC_ID_HEV1:
+      case CODEC_ID_HVC1:
+        return getHevcProfileAndLevel(codec, parts);
+      case CODEC_ID_AVC1:
+      case CODEC_ID_AVC2:
+        return getAvcProfileAndLevel(codec, parts);
+      default:
+        return null;
+    }
+  }
+
+  private static 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;
+    }
+    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;
+    }
+    Integer level = HEVC_CODEC_STRING_TO_PROFILE_LEVEL.get(parts[3]);
+    if (level == null) {
+      Log.w(TAG, "Unknown HEVC level string: " + matcher.group(1));
+      return null;
+    }
+    return new Pair<>(profile, level);
+  }
+
+  private static Pair<Integer, Integer> getAvcProfileAndLevel(String codec, String[] codecsParts) {
+    if (codecsParts.length < 2) {
+      // The codec has fewer parts than required by the AVC codec string format.
+      Log.w(TAG, "Ignoring malformed AVC codec string: " + codec);
+      return null;
+    }
+    Integer profileInteger;
+    Integer levelInteger;
+    try {
+      if (codecsParts[1].length() == 6) {
+        // Format: avc1.xxccyy, where xx is profile and yy level, both hexadecimal.
+        profileInteger = Integer.parseInt(codecsParts[1].substring(0, 2), 16);
+        levelInteger = Integer.parseInt(codecsParts[1].substring(4), 16);
+      } else if (codecsParts.length >= 3) {
+        // Format: avc1.xx.[y]yy where xx is profile and [y]yy level, both decimal.
+        profileInteger = Integer.parseInt(codecsParts[1]);
+        levelInteger = Integer.parseInt(codecsParts[2]);
+      } 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;
+    }
+
+    Integer profile = AVC_PROFILE_NUMBER_TO_CONST.get(profileInteger);
+    if (profile == null) {
+      Log.w(TAG, "Unknown AVC profile: " + profileInteger);
+      return null;
+    }
+    Integer level = AVC_LEVEL_NUMBER_TO_CONST.get(levelInteger);
+    if (level == null) {
+      Log.w(TAG, "Unknown AVC 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: return 99 * 16 * 16;
+      case CodecProfileLevel.AVCLevel1b: return 99 * 16 * 16;
+      case CodecProfileLevel.AVCLevel12: return 396 * 16 * 16;
+      case CodecProfileLevel.AVCLevel13: return 396 * 16 * 16;
+      case CodecProfileLevel.AVCLevel2: return 396 * 16 * 16;
+      case CodecProfileLevel.AVCLevel21: return 792 * 16 * 16;
+      case CodecProfileLevel.AVCLevel22: return 1620 * 16 * 16;
+      case CodecProfileLevel.AVCLevel3: return 1620 * 16 * 16;
+      case CodecProfileLevel.AVCLevel31: return 3600 * 16 * 16;
+      case CodecProfileLevel.AVCLevel32: return 5120 * 16 * 16;
+      case CodecProfileLevel.AVCLevel4: return 8192 * 16 * 16;
+      case CodecProfileLevel.AVCLevel41: return 8192 * 16 * 16;
+      case CodecProfileLevel.AVCLevel42: return 8704 * 16 * 16;
+      case CodecProfileLevel.AVCLevel5: return 22080 * 16 * 16;
+      case CodecProfileLevel.AVCLevel51: return 36864 * 16 * 16;
+      default: return -1;
+    }
+  }
+
+  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 secure playback is supported for the given {@link CodecCapabilities}, which should
+     * have been obtained from a {@link android.media.MediaCodecInfo} obtained from this list.
+     */
+    boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities);
+
+  }
+
+  @TargetApi(21)
+  private static final class MediaCodecListCompatV21 implements MediaCodecListCompat {
+
+    private final int codecKind;
+
+    private android.media.MediaCodecInfo[] mediaCodecInfos;
+
+    public MediaCodecListCompatV21(boolean includeSecure) {
+      codecKind = includeSecure ? MediaCodecList.ALL_CODECS : MediaCodecList.REGULAR_CODECS;
+    }
+
+    @Override
+    public int getCodecCount() {
+      ensureMediaCodecInfosInitialized();
+      return mediaCodecInfos.length;
+    }
+
+    @Override
+    public android.media.MediaCodecInfo getCodecInfoAt(int index) {
+      ensureMediaCodecInfosInitialized();
+      return mediaCodecInfos[index];
+    }
+
+    @Override
+    public boolean secureDecodersExplicit() {
+      return true;
+    }
+
+    @Override
+    public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) {
+      return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback);
+    }
+
+    private void ensureMediaCodecInfosInitialized() {
+      if (mediaCodecInfos == null) {
+        mediaCodecInfos = new MediaCodecList(codecKind).getCodecInfos();
+      }
+    }
+
+  }
+
+  @SuppressWarnings("deprecation")
+  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 isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) {
+      // Secure decoders weren't explicitly listed prior to API level 21. We assume that a secure
+      // H264 decoder exists.
+      return MimeTypes.VIDEO_H264.equals(mimeType);
+    }
+
+  }
+
+  private static final class CodecKey {
+
+    public final String mimeType;
+    public final boolean secure;
+
+    public CodecKey(String mimeType, boolean secure) {
+      this.mimeType = mimeType;
+      this.secure = secure;
+    }
+
+    @Override
+    public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + ((mimeType == null) ? 0 : mimeType.hashCode());
+      result = prime * result + (secure ? 1231 : 1237);
+      return result;
+    }
+
+    @Override
+    public boolean equals(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;
+    }
+
+  }
+
+  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_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);
+
+    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);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/Metadata.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 com.google.android.exoplayer2.metadata;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+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 {}
+
+  private final Entry[] entries;
+
+  /**
+   * @param entries The metadata entries.
+   */
+  public Metadata(Entry... entries) {
+    this.entries = entries == null ? new Entry[0] : entries;
+  }
+
+  /**
+   * @param entries The metadata entries.
+   */
+  public Metadata(List<? extends Entry> entries) {
+    if (entries != null) {
+      this.entries = new Entry[entries.size()];
+      entries.toArray(this.entries);
+    } else {
+      this.entries = new Entry[0];
+    }
+  }
+
+  /* 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];
+  }
+
+  @Override
+  public boolean equals(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 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[0];
+    }
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoder.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 com.google.android.exoplayer2.metadata;
+
+/**
+ * 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.
+   * @throws MetadataDecoderException If a problem occurred decoding the data.
+   */
+  Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderException.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 com.google.android.exoplayer2.metadata;
+
+/**
+ * Thrown when an error occurs decoding metadata.
+ */
+public class MetadataDecoderException extends Exception {
+
+  /**
+   * @param message The detail message for this exception.
+   */
+  public MetadataDecoderException(String message) {
+    super(message);
+  }
+
+  /**
+   * @param message The detail message for this exception.
+   * @param cause The cause of this exception.
+   */
+  public MetadataDecoderException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/MetadataDecoderFactory.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 com.google.android.exoplayer2.metadata;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;
+import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
+import com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder;
+import 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>
+   * <li>EMSG ({@link EventMessageDecoder})</li>
+   * <li>SCTE-35 ({@link SpliceInfoDecoder})</li>
+   * </ul>
+   */
+  MetadataDecoderFactory DEFAULT = new MetadataDecoderFactory() {
+
+    @Override
+    public boolean supportsFormat(Format format) {
+      return getDecoderClass(format.sampleMimeType) != null;
+    }
+
+    @Override
+    public MetadataDecoder createDecoder(Format format) {
+      try {
+        Class<?> clazz = getDecoderClass(format.sampleMimeType);
+        if (clazz == null) {
+          throw new IllegalArgumentException("Attempted to create decoder for unsupported format");
+        }
+        return clazz.asSubclass(MetadataDecoder.class).getConstructor().newInstance();
+      } catch (Exception e) {
+        throw new IllegalStateException("Unexpected error instantiating decoder", e);
+      }
+    }
+
+    private Class<?> getDecoderClass(String mimeType) {
+      if (mimeType == null) {
+        return null;
+      }
+      try {
+        switch (mimeType) {
+          case MimeTypes.APPLICATION_ID3:
+            return Class.forName("com.google.android.exoplayer2.metadata.id3.Id3Decoder");
+          case MimeTypes.APPLICATION_EMSG:
+            return Class.forName("com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder");
+          case MimeTypes.APPLICATION_SCTE35:
+            return Class.forName("com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder");
+          default:
+            return null;
+        }
+      } catch (ClassNotFoundException e) {
+        return null;
+      }
+    }
+
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.metadata;
+
+import com.google.android.exoplayer2.Format;
+import 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);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/MetadataRenderer.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 com.google.android.exoplayer2.metadata;
+
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Looper;
+import android.os.Message;
+import com.google.android.exoplayer2.BaseRenderer;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * A renderer for metadata.
+ */
+public final class MetadataRenderer extends BaseRenderer implements Callback {
+
+  /**
+   * Receives output from a {@link MetadataRenderer}.
+   */
+  public interface Output {
+
+    /**
+     * Called each time there is a metadata associated with current playback time.
+     *
+     * @param metadata The metadata.
+     */
+    void onMetadata(Metadata metadata);
+
+  }
+
+  private static final int MSG_INVOKE_RENDERER = 0;
+
+  private final MetadataDecoderFactory decoderFactory;
+  private final Output output;
+  private final Handler outputHandler;
+  private final FormatHolder formatHolder;
+  private final MetadataInputBuffer buffer;
+
+  private MetadataDecoder decoder;
+  private boolean inputStreamEnded;
+  private long pendingMetadataTimestamp;
+  private Metadata pendingMetadata;
+
+  /**
+   * @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(Output output, 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(Output output, Looper outputLooper,
+      MetadataDecoderFactory decoderFactory) {
+    super(C.TRACK_TYPE_METADATA);
+    this.output = Assertions.checkNotNull(output);
+    this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this);
+    this.decoderFactory = Assertions.checkNotNull(decoderFactory);
+    formatHolder = new FormatHolder();
+    buffer = new MetadataInputBuffer();
+  }
+
+  @Override
+  public int supportsFormat(Format format) {
+    return decoderFactory.supportsFormat(format) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
+  }
+
+  @Override
+  protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+    decoder = decoderFactory.createDecoder(formats[0]);
+  }
+
+  @Override
+  protected void onPositionReset(long positionUs, boolean joining) {
+    pendingMetadata = null;
+    inputStreamEnded = false;
+  }
+
+  @Override
+  public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
+    if (!inputStreamEnded && pendingMetadata == null) {
+      buffer.clear();
+      int result = readSource(formatHolder, buffer);
+      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 {
+          pendingMetadataTimestamp = buffer.timeUs;
+          buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs;
+          buffer.flip();
+          try {
+            pendingMetadata = decoder.decode(buffer);
+          } catch (MetadataDecoderException e) {
+            throw ExoPlaybackException.createForRenderer(e, getIndex());
+          }
+        }
+      }
+    }
+
+    if (pendingMetadata != null && pendingMetadataTimestamp <= positionUs) {
+      invokeRenderer(pendingMetadata);
+      pendingMetadata = null;
+    }
+  }
+
+  @Override
+  protected void onDisabled() {
+    pendingMetadata = null;
+    decoder = null;
+    super.onDisabled();
+  }
+
+  @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);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public boolean handleMessage(Message msg) {
+    switch (msg.what) {
+      case MSG_INVOKE_RENDERER:
+        invokeRendererInternal((Metadata) msg.obj);
+        return true;
+    }
+    return false;
+  }
+
+  private void invokeRendererInternal(Metadata metadata) {
+    output.onMetadata(metadata);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
@@ -0,0 +1,142 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.emsg;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.google.android.exoplayer2.metadata.Metadata;
+import 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 {
+
+  /**
+   * 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 = in.readString();
+    value = in.readString();
+    durationMs = in.readLong();
+    id = in.readLong();
+    messageData = in.createByteArray();
+  }
+
+  @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(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);
+  }
+
+  // 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];
+    }
+
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.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 com.google.android.exoplayer2.metadata.emsg;
+
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.MetadataDecoder;
+import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * Decodes Event Message (emsg) atoms, as defined in ISO 23009-1.
+ * <p>
+ * Atom data should be provided to the decoder without the full atom header (i.e. starting from the
+ * first byte of the scheme_id_uri field).
+ */
+public final class EventMessageDecoder implements MetadataDecoder {
+
+  @Override
+  public Metadata decode(MetadataInputBuffer inputBuffer) {
+    ByteBuffer buffer = inputBuffer.data;
+    byte[] data = buffer.array();
+    int size = buffer.limit();
+    ParsableByteArray emsgData = new ParsableByteArray(data, size);
+    String schemeIdUri = emsgData.readNullTerminatedString();
+    String value = emsgData.readNullTerminatedString();
+    long timescale = emsgData.readUnsignedInt();
+    emsgData.skipBytes(4); // presentation_time_delta
+    long durationMs = (emsgData.readUnsignedInt() * 1000) / timescale;
+    long id = emsgData.readUnsignedInt();
+    byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size);
+    return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData));
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/ApicFrame.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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import 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;
+  public final String description;
+  public final int pictureType;
+  public final byte[] pictureData;
+
+  public ApicFrame(String mimeType, 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 = in.readString();
+    description = in.readString();
+    pictureType = in.readInt();
+    pictureData = in.createByteArray();
+  }
+
+  @Override
+  public boolean equals(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 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];
+    }
+
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/BinaryFrame.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+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(in.readString());
+    data = in.createByteArray();
+  }
+
+  @Override
+  public boolean equals(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];
+        }
+
+      };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterFrame.java
@@ -0,0 +1,142 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import com.google.android.exoplayer2.C;
+import 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 = 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(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];
+    }
+
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/ChapterTocFrame.java
@@ -0,0 +1,123 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import 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 = in.readString();
+    this.isRoot = in.readByte() != 0;
+    this.isOrdered = in.readByte() != 0;
+    this.children = 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(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 (int i = 0; i < subFrames.length; i++) {
+      dest.writeParcelable(subFrames[i], 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];
+    }
+
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/CommentFrame.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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import 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 = in.readString();
+    description = in.readString();
+    text = in.readString();
+  }
+
+  @Override
+  public boolean equals(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 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];
+        }
+
+      };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/GeobFrame.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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import 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 = in.readString();
+    filename = in.readString();
+    description = in.readString();
+    data = in.createByteArray();
+  }
+
+  @Override
+  public boolean equals(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 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];
+    }
+
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
@@ -0,0 +1,677 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.metadata.id3;
+
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.MetadataDecoder;
+import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import 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 {
+
+  private static final String TAG = "Id3Decoder";
+
+  /**
+   * The first three bytes of a well formed ID3 tag header.
+   */
+  public static final int ID3_TAG = Util.getIntegerCodeForString("ID3");
+  /**
+   * Length of an ID3 tag header.
+   */
+  public static final int ID3_HEADER_LENGTH = 10;
+
+  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;
+
+  @Override
+  public Metadata decode(MetadataInputBuffer inputBuffer) {
+    ByteBuffer buffer = 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.
+   */
+  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 framesSize = id3Header.framesSize;
+    if (id3Header.isUnsynchronized) {
+      framesSize = removeUnsynchronization(id3Data, id3Header.framesSize);
+    }
+    id3Data.setLimit(startPosition + framesSize);
+
+    boolean unsignedIntFrameSizeHack = false;
+    if (id3Header.majorVersion == 4) {
+      if (!validateV4Frames(id3Data, false)) {
+        if (validateV4Frames(id3Data, true)) {
+          unsignedIntFrameSizeHack = true;
+        } else {
+          Log.w(TAG, "Failed to validate V4 ID3 tag");
+          return null;
+        }
+      }
+    }
+
+    int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10;
+    while (id3Data.bytesLeft() >= frameHeaderSize) {
+      Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack,
+          frameHeaderSize);
+      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.
+   */
+  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: " + 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 validateV4Frames(ParsableByteArray id3Data,
+      boolean unsignedIntFrameSizeHack) {
+    int startPosition = id3Data.getPosition();
+    try {
+      while (id3Data.bytesLeft() >= 10) {
+        int id = id3Data.readInt();
+        int frameSize = id3Data.readUnsignedIntToInt();
+        int flags = id3Data.readUnsignedShort();
+        if (id == 0 && frameSize == 0 && flags == 0) {
+          return true;
+        } else {
+          if (!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);
+          }
+          int minimumFrameSize = 0;
+          if ((flags & 0x0040) != 0 /* hasGroupIdentifier */) {
+            minimumFrameSize++;
+          }
+          if ((flags & 0x0001) != 0 /* hasDataLength */) {
+            minimumFrameSize += 4;
+          }
+          if (frameSize < minimumFrameSize) {
+            return false;
+          }
+          if (id3Data.bytesLeft() < frameSize) {
+            return false;
+          }
+          id3Data.skipBytes(frameSize); // flags
+        }
+      }
+      return true;
+    } finally {
+      id3Data.setPosition(startPosition);
+    }
+  }
+
+  private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data,
+      boolean unsignedIntFrameSizeHack, int frameHeaderSize) {
+    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;
+    }
+
+    // Frame flags.
+    boolean isCompressed = false;
+    boolean isEncrypted = false;
+    boolean isUnsynchronized = false;
+    boolean hasDataLength = false;
+    boolean hasGroupIdentifier = false;
+    if (majorVersion == 3) {
+      isCompressed = (flags & 0x0080) != 0;
+      isEncrypted = (flags & 0x0040) != 0;
+      hasGroupIdentifier = (flags & 0x0020) != 0;
+      hasDataLength = isCompressed;
+    } else if (majorVersion == 4) {
+      hasGroupIdentifier = (flags & 0x0040) != 0;
+      isCompressed = (flags & 0x0008) != 0;
+      isEncrypted = (flags & 0x0004) != 0;
+      isUnsynchronized = (flags & 0x0002) != 0;
+      hasDataLength = (flags & 0x0001) != 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 = majorVersion == 2
+            ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
+            : String.format(Locale.US, "%c%c%c%c", 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 = majorVersion == 2
+            ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
+            : String.format(Locale.US, "%c%c%c%c", 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);
+      } else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') {
+        frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack,
+            frameHeaderSize);
+      } else {
+        String id = majorVersion == 2
+            ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
+            : String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
+        frame = decodeBinaryFrame(id3Data, frameSize, id);
+      }
+      return frame;
+    } catch (UnsupportedEncodingException e) {
+      Log.w(TAG, "Unsupported character encoding");
+      return null;
+    } finally {
+      id3Data.setPosition(nextFramePosition);
+    }
+  }
+
+  private static TextInformationFrame decodeTxxxFrame(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 descriptionEndIndex = indexOfEos(data, 0, encoding);
+    String description = new String(data, 0, descriptionEndIndex, charset);
+
+    String value;
+    int valueStartIndex = descriptionEndIndex + delimiterLength(encoding);
+    if (valueStartIndex < data.length) {
+      int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
+      value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset);
+    } else {
+      value = "";
+    }
+
+    return new TextInformationFrame("TXXX", description, value);
+  }
+
+  private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data,
+      int frameSize, String id) throws UnsupportedEncodingException {
+    if (frameSize <= 1) {
+      // Frame is empty or contains only the text encoding byte.
+      return new TextInformationFrame(id, 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);
+  }
+
+  private static UrlLinkFrame decodeWxxxFrame(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 descriptionEndIndex = indexOfEos(data, 0, encoding);
+    String description = new String(data, 0, descriptionEndIndex, charset);
+
+    String url;
+    int urlStartIndex = descriptionEndIndex + delimiterLength(encoding);
+    if (urlStartIndex < data.length) {
+      int urlEndIndex = indexOfZeroByte(data, urlStartIndex);
+      url = new String(data, urlStartIndex, urlEndIndex - urlStartIndex, "ISO-8859-1");
+    } else {
+      url = "";
+    }
+
+    return new UrlLinkFrame("WXXX", description, url);
+  }
+
+  private static UrlLinkFrame decodeUrlLinkFrame(ParsableByteArray id3Data, int frameSize,
+      String id) throws UnsupportedEncodingException {
+    if (frameSize == 0) {
+      // Frame is empty.
+      return new UrlLinkFrame(id, null, "");
+    }
+
+    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 = Arrays.copyOfRange(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 = new String(data, filenameStartIndex, filenameEndIndex - filenameStartIndex,
+        charset);
+
+    int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding);
+    int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding);
+    String description = new String(data, descriptionStartIndex,
+        descriptionEndIndex - descriptionStartIndex, charset);
+
+    int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding);
+    byte[] objectData = Arrays.copyOfRange(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 (mimeType.equals("image/jpg")) {
+        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 = Arrays.copyOfRange(data, pictureDataStartIndex, data.length);
+
+    return new ApicFrame(mimeType, description, pictureType, pictureData);
+  }
+
+  private static CommentFrame decodeCommentFrame(ParsableByteArray id3Data, int frameSize)
+      throws UnsupportedEncodingException {
+    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);
+
+    String text;
+    int textStartIndex = descriptionEndIndex + delimiterLength(encoding);
+    if (textStartIndex < data.length) {
+      int textEndIndex = indexOfEos(data, textStartIndex, encoding);
+      text = new String(data, textStartIndex, textEndIndex - textStartIndex, charset);
+    } else {
+      text = "";
+    }
+
+    return new CommentFrame(language, description, text);
+  }
+
+  private static ChapterFrame decodeChapterFrame(ParsableByteArray id3Data, int frameSize,
+      int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize)
+      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);
+      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)
+      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);
+      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 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;
+    for (int i = data.getPosition(); i + 1 < length; i++) {
+      if ((bytes[i] & 0xFF) == 0xFF && bytes[i + 1] == 0x00) {
+        System.arraycopy(bytes, i + 2, bytes, i + 1, length - i - 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_ISO_8859_1:
+        return "ISO-8859-1";
+      case ID3_TEXT_ENCODING_UTF_16:
+        return "UTF-16";
+      case ID3_TEXT_ENCODING_UTF_16BE:
+        return "UTF-16BE";
+      case ID3_TEXT_ENCODING_UTF_8:
+        return "UTF-8";
+      default:
+        return "ISO-8859-1";
+    }
+  }
+
+  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;
+  }
+
+  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;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/Id3Frame.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.metadata.id3;
+
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * 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 = Assertions.checkNotNull(id);
+  }
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/PrivFrame.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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import 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 = in.readString();
+    privateData = in.createByteArray();
+  }
+
+  @Override
+  public boolean equals(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 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];
+    }
+
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/TextInformationFrame.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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Text information ID3 frame.
+ */
+public final class TextInformationFrame extends Id3Frame {
+
+  public final String description;
+  public final String value;
+
+  public TextInformationFrame(String id, String description, String value) {
+    super(id);
+    this.description = description;
+    this.value = value;
+  }
+
+  /* package */ TextInformationFrame(Parcel in) {
+    super(in.readString());
+    description = in.readString();
+    value = in.readString();
+  }
+
+  @Override
+  public boolean equals(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 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];
+        }
+
+      };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/id3/UrlLinkFrame.java
@@ -0,0 +1,86 @@
+/*
+ * 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 com.google.android.exoplayer2.metadata.id3;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * Url link ID3 frame.
+ */
+public final class UrlLinkFrame extends Id3Frame {
+
+  public final String description;
+  public final String url;
+
+  public UrlLinkFrame(String id, String description, String url) {
+    super(id);
+    this.description = description;
+    this.url = url;
+  }
+
+  /* package */ UrlLinkFrame(Parcel in) {
+    super(in.readString());
+    description = in.readString();
+    url = in.readString();
+  }
+
+  @Override
+  public boolean equals(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 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];
+        }
+
+      };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/scte35/PrivateCommand.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 com.google.android.exoplayer2.metadata.scte35;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Represents a private command as defined in SCTE35, Section 9.3.6.
+ */
+public final class PrivateCommand extends SpliceCommand {
+
+  public final long ptsAdjustment;
+  public final long identifier;
+  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 = new byte[in.readInt()];
+    in.readByteArray(commandBytes);
+  }
+
+  /* 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.writeInt(commandBytes.length);
+    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];
+    }
+
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceCommand.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.metadata.scte35;
+
+import com.google.android.exoplayer2.metadata.Metadata;
+
+/**
+ * Superclass for SCTE35 splice commands.
+ */
+public abstract class SpliceCommand implements Metadata.Entry {
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoder.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 com.google.android.exoplayer2.metadata.scte35;
+
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.MetadataDecoder;
+import com.google.android.exoplayer2.metadata.MetadataDecoderException;
+import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.nio.ByteBuffer;
+
+/**
+ * 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;
+
+  private TimestampAdjuster timestampAdjuster;
+
+  public SpliceInfoDecoder() {
+    sectionData = new ParsableByteArray();
+    sectionHeader = new ParsableBitArray();
+  }
+
+  @Override
+  public Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException {
+    // Internal timestamps adjustment.
+    if (timestampAdjuster == null
+        || inputBuffer.subsampleOffsetUs != timestampAdjuster.getTimestampOffsetUs()) {
+      timestampAdjuster = new TimestampAdjuster(inputBuffer.timeUs);
+      timestampAdjuster.adjustSampleTimestamp(inputBuffer.timeUs - inputBuffer.subsampleOffsetUs);
+    }
+
+    ByteBuffer buffer = inputBuffer.data;
+    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);
+    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;
+    }
+    return command == null ? new Metadata() : new Metadata(command);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceInsertCommand.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.metadata.scte35;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import 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 {
+
+  public final long spliceEventId;
+  public final boolean spliceEventCancelIndicator;
+  public final boolean outOfNetworkIndicator;
+  public final boolean programSpliceFlag;
+  public final boolean spliceImmediateFlag;
+  public final long programSplicePts;
+  public final long programSplicePlaybackPositionUs;
+  public final List<ComponentSplice> componentSpliceList;
+  public final boolean autoReturn;
+  public final long breakDuration;
+  public final int uniqueProgramId;
+  public final int availNum;
+  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 breakDuration,
+      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.breakDuration = breakDuration;
+    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;
+    breakDuration = 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 duration = 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;
+        duration = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt();
+      }
+      uniqueProgramId = sectionData.readUnsignedShort();
+      availNum = sectionData.readUnsignedByte();
+      availsExpected = sectionData.readUnsignedByte();
+    }
+    return new SpliceInsertCommand(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator,
+        programSpliceFlag, spliceImmediateFlag, programSplicePts,
+        timestampAdjuster.adjustTsTimestamp(programSplicePts), componentSplices, autoReturn,
+        duration, 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(breakDuration);
+    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];
+    }
+
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 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];
+    }
+
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/scte35/SpliceScheduleCommand.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.metadata.scte35;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.google.android.exoplayer2.C;
+import 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 {
+
+    public final long spliceEventId;
+    public final boolean spliceEventCancelIndicator;
+    public final boolean outOfNetworkIndicator;
+    public final boolean programSpliceFlag;
+    public final long utcSpliceTime;
+    public final List<ComponentSplice> componentSpliceList;
+    public final boolean autoReturn;
+    public final long breakDuration;
+    public final int uniqueProgramId;
+    public final int availNum;
+    public final int availsExpected;
+
+    private Event(long spliceEventId, boolean spliceEventCancelIndicator,
+        boolean outOfNetworkIndicator, boolean programSpliceFlag,
+        List<ComponentSplice> componentSpliceList, long utcSpliceTime, boolean autoReturn,
+        long breakDuration, 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.breakDuration = breakDuration;
+      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.breakDuration = 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 duration = 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;
+          duration = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt();
+        }
+        uniqueProgramId = sectionData.readUnsignedShort();
+        availNum = sectionData.readUnsignedByte();
+        availsExpected = sectionData.readUnsignedByte();
+      }
+      return new Event(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator,
+          programSpliceFlag, componentSplices, utcSpliceTime, autoReturn, duration, 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(breakDuration);
+      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);
+    }
+
+  }
+
+  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];
+    }
+
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/metadata/scte35/TimeSignalCommand.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 com.google.android.exoplayer2.metadata.scte35;
+
+import android.os.Parcel;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import 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 {
+
+  public final long ptsTime;
+  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];
+    }
+
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import android.os.Handler;
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * Interface for callbacks to be notified of adaptive {@link MediaSource} events.
+ */
+public interface AdaptiveMediaSourceEventListener {
+
+  /**
+   * Called when a load begins.
+   *
+   * @param dataSpec Defines the data being loaded.
+   * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
+   *     being loaded.
+   * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
+   *     to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
+   * @param trackFormat The format of the track to which the data belongs. Null if the data does
+   *     not belong to a track.
+   * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
+   *     data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
+   * @param trackSelectionData Optional data associated with the selection of the track to which the
+   *     data belongs. Null if the data does not belong to a track.
+   * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
+   *     the load is not for media data.
+   * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
+   *     load is not for media data.
+   * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began.
+   */
+  void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
+      int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
+      long mediaEndTimeMs, long elapsedRealtimeMs);
+
+  /**
+   * Called when a load ends.
+   *
+   * @param dataSpec Defines the data being loaded.
+   * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
+   *     being loaded.
+   * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
+   *     to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
+   * @param trackFormat The format of the track to which the data belongs. Null if the data does
+   *     not belong to a track.
+   * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
+   *     data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
+   * @param trackSelectionData Optional data associated with the selection of the track to which the
+   *     data belongs. Null if the data does not belong to a track.
+   * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
+   *     the load is not for media data.
+   * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
+   *     load is not for media data.
+   * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended.
+   * @param loadDurationMs The duration of the load.
+   * @param bytesLoaded The number of bytes that were loaded.
+   */
+  void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
+      int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
+      long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded);
+
+  /**
+   * Called when a load is canceled.
+   *
+   * @param dataSpec Defines the data being loaded.
+   * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
+   *     being loaded.
+   * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
+   *     to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
+   * @param trackFormat The format of the track to which the data belongs. Null if the data does
+   *     not belong to a track.
+   * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
+   *     data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
+   * @param trackSelectionData Optional data associated with the selection of the track to which the
+   *     data belongs. Null if the data does not belong to a track.
+   * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
+   *     the load is not for media data.
+   * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
+   *     load is not for media data.
+   * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was
+   *     canceled.
+   * @param loadDurationMs The duration of the load up to the point at which it was canceled.
+   * @param bytesLoaded The number of bytes that were loaded prior to cancelation.
+   */
+  void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
+      int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
+      long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded);
+
+  /**
+   * Called when a load error occurs.
+   * <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.
+   *
+   * @param dataSpec Defines the data being loaded.
+   * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data
+   *     being loaded.
+   * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds
+   *     to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise.
+   * @param trackFormat The format of the track to which the data belongs. Null if the data does
+   *     not belong to a track.
+   * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
+   *     data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
+   * @param trackSelectionData Optional data associated with the selection of the track to which the
+   *     data belongs. Null if the data does not belong to a track.
+   * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
+   *     the load is not for media data.
+   * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
+   *     load is not for media data.
+   * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error
+   *     occurred.
+   * @param loadDurationMs The duration of the load up to the point at which the error occurred.
+   * @param bytesLoaded The number of bytes that were loaded prior to the error.
+   * @param error The load error.
+   * @param wasCanceled Whether the load was canceled as a result of the error.
+   */
+  void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat,
+      int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs,
+      long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded,
+      IOException error, boolean wasCanceled);
+
+  /**
+   * Called when data is removed from the back of a media buffer, typically so that it can be
+   * re-buffered in a different format.
+   *
+   * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants.
+   * @param mediaStartTimeMs The start time of the media being discarded.
+   * @param mediaEndTimeMs The end time of the media being discarded.
+   */
+  void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs);
+
+  /**
+   * Called when a downstream format change occurs (i.e. when the format of the media being read
+   * from one or more {@link SampleStream}s provided by the source changes).
+   *
+   * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants.
+   * @param trackFormat The format of the track to which the data belongs. Null if the data does
+   *     not belong to a track.
+   * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the
+   *     data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise.
+   * @param trackSelectionData Optional data associated with the selection of the track to which the
+   *     data belongs. Null if the data does not belong to a track.
+   * @param mediaTimeMs The media time at which the change occurred.
+   */
+  void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason,
+      Object trackSelectionData, long mediaTimeMs);
+
+  /**
+   * Dispatches events to a {@link AdaptiveMediaSourceEventListener}.
+   */
+  final class EventDispatcher {
+
+    private final Handler handler;
+    private final AdaptiveMediaSourceEventListener listener;
+    private final long mediaTimeOffsetMs;
+
+    public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener) {
+      this(handler, listener, 0);
+    }
+
+    public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener,
+        long mediaTimeOffsetMs) {
+      this.handler = listener != null ? Assertions.checkNotNull(handler) : null;
+      this.listener = listener;
+      this.mediaTimeOffsetMs = mediaTimeOffsetMs;
+    }
+
+    public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) {
+      return new EventDispatcher(handler, listener, mediaTimeOffsetMs);
+    }
+
+    public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) {
+      loadStarted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
+          null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs);
+    }
+
+    public void loadStarted(final DataSpec dataSpec, final int dataType, final int trackType,
+        final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
+        final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs) {
+      if (listener != null) {
+        handler.post(new Runnable()  {
+          @Override
+          public void run() {
+            listener.onLoadStarted(dataSpec, dataType, trackType, trackFormat, trackSelectionReason,
+                trackSelectionData, adjustMediaTime(mediaStartTimeUs),
+                adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs);
+          }
+        });
+      }
+    }
+
+    public void loadCompleted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs,
+        long loadDurationMs, long bytesLoaded) {
+      loadCompleted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
+          null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded);
+    }
+
+    public void loadCompleted(final DataSpec dataSpec, final int dataType, final int trackType,
+        final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
+        final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs,
+        final long loadDurationMs, final long bytesLoaded) {
+      if (listener != null) {
+        handler.post(new Runnable()  {
+          @Override
+          public void run() {
+            listener.onLoadCompleted(dataSpec, dataType, trackType, trackFormat,
+                trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs),
+                adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded);
+          }
+        });
+      }
+    }
+
+    public void loadCanceled(DataSpec dataSpec, int dataType, long elapsedRealtimeMs,
+        long loadDurationMs, long bytesLoaded) {
+      loadCanceled(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
+          null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded);
+    }
+
+    public void loadCanceled(final DataSpec dataSpec, final int dataType, final int trackType,
+        final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
+        final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs,
+        final long loadDurationMs, final long bytesLoaded) {
+      if (listener != null) {
+        handler.post(new Runnable()  {
+          @Override
+          public void run() {
+            listener.onLoadCanceled(dataSpec, dataType, trackType, trackFormat,
+                trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs),
+                adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded);
+          }
+        });
+      }
+    }
+
+    public void loadError(DataSpec dataSpec, int dataType, long elapsedRealtimeMs,
+        long loadDurationMs, long bytesLoaded, IOException error, boolean wasCanceled) {
+      loadError(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN,
+          null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded,
+          error, wasCanceled);
+    }
+
+    public void loadError(final DataSpec dataSpec, final int dataType, final int trackType,
+        final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData,
+        final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs,
+        final long loadDurationMs, final long bytesLoaded, final IOException error,
+        final boolean wasCanceled) {
+      if (listener != null) {
+        handler.post(new Runnable()  {
+          @Override
+          public void run() {
+            listener.onLoadError(dataSpec, dataType, trackType, trackFormat, trackSelectionReason,
+                trackSelectionData, adjustMediaTime(mediaStartTimeUs),
+                adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded,
+                error, wasCanceled);
+          }
+        });
+      }
+    }
+
+    public void upstreamDiscarded(final int trackType, final long mediaStartTimeUs,
+        final long mediaEndTimeUs) {
+      if (listener != null) {
+        handler.post(new Runnable()  {
+          @Override
+          public void run() {
+            listener.onUpstreamDiscarded(trackType, adjustMediaTime(mediaStartTimeUs),
+                adjustMediaTime(mediaEndTimeUs));
+          }
+        });
+      }
+    }
+
+    public void downstreamFormatChanged(final int trackType, final Format trackFormat,
+        final int trackSelectionReason, final Object trackSelectionData,
+        final long mediaTimeUs) {
+      if (listener != null) {
+        handler.post(new Runnable()  {
+          @Override
+          public void run() {
+            listener.onDownstreamFormatChanged(trackType, trackFormat, trackSelectionReason,
+                trackSelectionData, adjustMediaTime(mediaTimeUs));
+          }
+        });
+      }
+    }
+
+    private long adjustMediaTime(long mediaTimeUs) {
+      long mediaTimeMs = C.usToMs(mediaTimeUs);
+      return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 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();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/ClippingMediaPeriod.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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * 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;
+
+  private MediaPeriod.Callback callback;
+  private long startUs;
+  private long endUs;
+  private ClippingSampleStream[] sampleStreams;
+  private boolean pendingInitialDiscontinuity;
+
+  /**
+   * Creates a new clipping media period that provides a clipped view of the specified
+   * {@link MediaPeriod}'s sample streams.
+   * <p>
+   * The clipping start/end positions must be specified by calling {@link #setClipping(long, long)}
+   * on the playback thread before preparation completes.
+   *
+   * @param mediaPeriod The media period to clip.
+   */
+  public ClippingMediaPeriod(MediaPeriod mediaPeriod) {
+    this.mediaPeriod = mediaPeriod;
+    startUs = C.TIME_UNSET;
+    endUs = C.TIME_UNSET;
+    sampleStreams = new ClippingSampleStream[0];
+  }
+
+  /**
+   * Sets the clipping start/end times for this period, in microseconds.
+   *
+   * @param startUs The clipping start time, in microseconds.
+   * @param endUs The clipping end time, in microseconds, or {@link C#TIME_END_OF_SOURCE} to
+   *     indicate the end of the period.
+   */
+  public void setClipping(long startUs, long endUs) {
+    this.startUs = startUs;
+    this.endUs = endUs;
+  }
+
+  @Override
+  public void prepare(MediaPeriod.Callback callback) {
+    this.callback = callback;
+    mediaPeriod.prepare(this);
+  }
+
+  @Override
+  public void maybeThrowPrepareError() throws IOException {
+    mediaPeriod.maybeThrowPrepareError();
+  }
+
+  @Override
+  public TrackGroupArray getTrackGroups() {
+    return mediaPeriod.getTrackGroups();
+  }
+
+  @Override
+  public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+      SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
+    sampleStreams = new ClippingSampleStream[streams.length];
+    SampleStream[] internalStreams = new SampleStream[streams.length];
+    for (int i = 0; i < streams.length; i++) {
+      sampleStreams[i] = (ClippingSampleStream) streams[i];
+      internalStreams[i] = sampleStreams[i] != null ? sampleStreams[i].stream : null;
+    }
+    long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags,
+        internalStreams, streamResetFlags, positionUs + startUs);
+    Assertions.checkState(enablePositionUs == positionUs + startUs
+        || (enablePositionUs >= startUs
+        && (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs)));
+    for (int i = 0; i < streams.length; i++) {
+      if (internalStreams[i] == null) {
+        sampleStreams[i] = null;
+      } else if (streams[i] == null || sampleStreams[i].stream != internalStreams[i]) {
+        sampleStreams[i] = new ClippingSampleStream(this, internalStreams[i], startUs, endUs,
+            pendingInitialDiscontinuity);
+      }
+      streams[i] = sampleStreams[i];
+    }
+    return enablePositionUs - startUs;
+  }
+
+  @Override
+  public long readDiscontinuity() {
+    if (pendingInitialDiscontinuity) {
+      for (ClippingSampleStream sampleStream : sampleStreams) {
+        if (sampleStream != null) {
+          sampleStream.clearPendingDiscontinuity();
+        }
+      }
+      pendingInitialDiscontinuity = false;
+      // Always read an initial discontinuity, using mediaPeriod's discontinuity if set.
+      long discontinuityUs = readDiscontinuity();
+      return discontinuityUs != C.TIME_UNSET ? discontinuityUs : 0;
+    }
+    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 - startUs;
+  }
+
+  @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 Math.max(0, bufferedPositionUs - startUs);
+  }
+
+  @Override
+  public long seekToUs(long positionUs) {
+    for (ClippingSampleStream sampleStream : sampleStreams) {
+      if (sampleStream != null) {
+        sampleStream.clearSentEos();
+      }
+    }
+    long seekUs = mediaPeriod.seekToUs(positionUs + startUs);
+    Assertions.checkState(seekUs == positionUs + startUs
+        || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs)));
+    return seekUs - startUs;
+  }
+
+  @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 - startUs;
+  }
+
+  @Override
+  public boolean continueLoading(long positionUs) {
+    return mediaPeriod.continueLoading(positionUs + startUs);
+  }
+
+  // MediaPeriod.Callback implementation.
+
+  @Override
+  public void onPrepared(MediaPeriod mediaPeriod) {
+    Assertions.checkState(startUs != C.TIME_UNSET && endUs != C.TIME_UNSET);
+    // 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.
+    pendingInitialDiscontinuity = startUs != 0;
+    callback.onPrepared(this);
+  }
+
+  @Override
+  public void onContinueLoadingRequested(MediaPeriod source) {
+    callback.onContinueLoadingRequested(this);
+  }
+
+  /**
+   * Wraps a {@link SampleStream} and clips its samples.
+   */
+  private static final class ClippingSampleStream implements SampleStream {
+
+    private final MediaPeriod mediaPeriod;
+    private final SampleStream stream;
+    private final long startUs;
+    private final long endUs;
+
+    private boolean pendingDiscontinuity;
+    private boolean sentEos;
+
+    public ClippingSampleStream(MediaPeriod mediaPeriod, SampleStream stream, long startUs,
+        long endUs, boolean pendingDiscontinuity) {
+      this.mediaPeriod = mediaPeriod;
+      this.stream = stream;
+      this.startUs = startUs;
+      this.endUs = endUs;
+      this.pendingDiscontinuity = pendingDiscontinuity;
+    }
+
+    public void clearPendingDiscontinuity() {
+      pendingDiscontinuity = false;
+    }
+
+    public void clearSentEos() {
+      sentEos = false;
+    }
+
+    @Override
+    public boolean isReady() {
+      return stream.isReady();
+    }
+
+    @Override
+    public void maybeThrowError() throws IOException {
+      stream.maybeThrowError();
+    }
+
+    @Override
+    public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
+      if (pendingDiscontinuity) {
+        return C.RESULT_NOTHING_READ;
+      }
+      if (buffer == null) {
+        return stream.readData(formatHolder, null);
+      }
+      if (sentEos) {
+        buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+        return C.RESULT_BUFFER_READ;
+      }
+      int result = stream.readData(formatHolder, buffer);
+      // TODO: Clear gapless playback metadata if a format was read (if applicable).
+      if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ
+          && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ
+          && mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) {
+        buffer.clear();
+        buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+        sentEos = true;
+        return C.RESULT_BUFFER_READ;
+      }
+      if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) {
+        buffer.timeUs -= startUs;
+      }
+      return result;
+    }
+
+    @Override
+    public void skipToKeyframeBefore(long timeUs) {
+      stream.skipToKeyframeBefore(startUs + timeUs);
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/ClippingMediaSource.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end
+ * positions. The wrapped source may only have a single period/window and it must not be dynamic
+ * (live).
+ */
+public final class ClippingMediaSource implements MediaSource, MediaSource.Listener {
+
+  private final MediaSource mediaSource;
+  private final long startUs;
+  private final long endUs;
+  private final ArrayList<ClippingMediaPeriod> mediaPeriods;
+
+  private MediaSource.Listener sourceListener;
+  private ClippingTimeline clippingTimeline;
+
+  /**
+   * Creates a new clipping source that wraps the specified source.
+   *
+   * @param mediaSource The single-period, non-dynamic source to wrap.
+   * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to
+   *     start providing samples, in microseconds.
+   * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop
+   *     providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples
+   *     from the specified start point up to the end of the source.
+   */
+  public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) {
+    Assertions.checkArgument(startPositionUs >= 0);
+    this.mediaSource = Assertions.checkNotNull(mediaSource);
+    startUs = startPositionUs;
+    endUs = endPositionUs;
+    mediaPeriods = new ArrayList<>();
+  }
+
+  @Override
+  public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+    this.sourceListener = listener;
+    mediaSource.prepareSource(player, false, this);
+  }
+
+  @Override
+  public void maybeThrowSourceInfoRefreshError() throws IOException {
+    mediaSource.maybeThrowSourceInfoRefreshError();
+  }
+
+  @Override
+  public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+    ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod(
+        mediaSource.createPeriod(index, allocator, startUs + positionUs));
+    mediaPeriods.add(mediaPeriod);
+    mediaPeriod.setClipping(clippingTimeline.startUs, clippingTimeline.endUs);
+    return mediaPeriod;
+  }
+
+  @Override
+  public void releasePeriod(MediaPeriod mediaPeriod) {
+    Assertions.checkState(mediaPeriods.remove(mediaPeriod));
+    mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
+  }
+
+  @Override
+  public void releaseSource() {
+    mediaSource.releaseSource();
+  }
+
+  // MediaSource.Listener implementation.
+
+  @Override
+  public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+    clippingTimeline = new ClippingTimeline(timeline, startUs, endUs);
+    sourceListener.onSourceInfoRefreshed(clippingTimeline, manifest);
+    long startUs = clippingTimeline.startUs;
+    long endUs = clippingTimeline.endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE
+        : clippingTimeline.endUs;
+    int count = mediaPeriods.size();
+    for (int i = 0; i < count; i++) {
+      mediaPeriods.get(i).setClipping(startUs, endUs);
+    }
+  }
+
+  /**
+   * Provides a clipped view of a specified timeline.
+   */
+  private static final class ClippingTimeline extends Timeline {
+
+    private final Timeline timeline;
+    private final long startUs;
+    private final long endUs;
+
+    /**
+     * 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.
+     */
+    public ClippingTimeline(Timeline timeline, long startUs, long endUs) {
+      Assertions.checkArgument(timeline.getWindowCount() == 1);
+      Assertions.checkArgument(timeline.getPeriodCount() == 1);
+      Window window = timeline.getWindow(0, new Window(), false);
+      Assertions.checkArgument(!window.isDynamic);
+      long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs;
+      if (window.durationUs != C.TIME_UNSET) {
+        Assertions.checkArgument(startUs == 0 || window.isSeekable);
+        Assertions.checkArgument(resolvedEndUs <= window.durationUs);
+        Assertions.checkArgument(startUs <= resolvedEndUs);
+      }
+      Period period = timeline.getPeriod(0, new Period());
+      Assertions.checkArgument(period.getPositionInWindowUs() == 0);
+      this.timeline = timeline;
+      this.startUs = startUs;
+      this.endUs = resolvedEndUs;
+    }
+
+    @Override
+    public int getWindowCount() {
+      return 1;
+    }
+
+    @Override
+    public Window getWindow(int windowIndex, Window window, boolean setIds,
+        long defaultPositionProjectionUs) {
+      window = timeline.getWindow(0, window, setIds, defaultPositionProjectionUs);
+      window.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET;
+      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 int getPeriodCount() {
+      return 1;
+    }
+
+    @Override
+    public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+      period = timeline.getPeriod(0, period, setIds);
+      period.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET;
+      return period;
+    }
+
+    @Override
+    public int getIndexOfPeriod(Object uid) {
+      return timeline.getIndexOfPeriod(uid);
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/CompositeSequenceableLoader.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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+
+/**
+ * A {@link SequenceableLoader} that encapsulates multiple other {@link SequenceableLoader}s.
+ */
+public final class CompositeSequenceableLoader implements SequenceableLoader {
+
+  private final SequenceableLoader[] loaders;
+
+  public CompositeSequenceableLoader(SequenceableLoader[] loaders) {
+    this.loaders = loaders;
+  }
+
+  @Override
+  public 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 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) {
+        if (loader.getNextLoadPositionUs() == nextLoadPositionUs) {
+          madeProgressThisIteration |= loader.continueLoading(positionUs);
+        }
+      }
+      madeProgress |= madeProgressThisIteration;
+    } while (madeProgressThisIteration);
+    return madeProgress;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+/**
+ * Concatenates multiple {@link MediaSource}s. It is valid for the same {@link MediaSource} instance
+ * to be present more than once in the concatenation.
+ */
+public final class ConcatenatingMediaSource implements MediaSource {
+
+  private final MediaSource[] mediaSources;
+  private final Timeline[] timelines;
+  private final Object[] manifests;
+  private final Map<MediaPeriod, Integer> sourceIndexByMediaPeriod;
+  private final boolean[] duplicateFlags;
+
+  private Listener listener;
+  private ConcatenatedTimeline timeline;
+
+  /**
+   * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same
+   *     {@link MediaSource} instance to be present more than once in the array.
+   */
+  public ConcatenatingMediaSource(MediaSource... mediaSources) {
+    this.mediaSources = mediaSources;
+    timelines = new Timeline[mediaSources.length];
+    manifests = new Object[mediaSources.length];
+    sourceIndexByMediaPeriod = new HashMap<>();
+    duplicateFlags = buildDuplicateFlags(mediaSources);
+  }
+
+  @Override
+  public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+    this.listener = listener;
+    for (int i = 0; i < mediaSources.length; i++) {
+      if (!duplicateFlags[i]) {
+        final int index = i;
+        mediaSources[i].prepareSource(player, false, new Listener() {
+          @Override
+          public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+            handleSourceInfoRefreshed(index, timeline, manifest);
+          }
+        });
+      }
+    }
+  }
+
+  @Override
+  public void maybeThrowSourceInfoRefreshError() throws IOException {
+    for (int i = 0; i < mediaSources.length; i++) {
+      if (!duplicateFlags[i]) {
+        mediaSources[i].maybeThrowSourceInfoRefreshError();
+      }
+    }
+  }
+
+  @Override
+  public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+    int sourceIndex = timeline.getSourceIndexForPeriod(index);
+    int periodIndexInSource = index - timeline.getFirstPeriodIndexInSource(sourceIndex);
+    MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIndexInSource, allocator,
+        positionUs);
+    sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex);
+    return mediaPeriod;
+  }
+
+  @Override
+  public void releasePeriod(MediaPeriod mediaPeriod) {
+    int sourceIndex = sourceIndexByMediaPeriod.get(mediaPeriod);
+    sourceIndexByMediaPeriod.remove(mediaPeriod);
+    mediaSources[sourceIndex].releasePeriod(mediaPeriod);
+  }
+
+  @Override
+  public void releaseSource() {
+    for (int i = 0; i < mediaSources.length; i++) {
+      if (!duplicateFlags[i]) {
+        mediaSources[i].releaseSource();
+      }
+    }
+  }
+
+  private void handleSourceInfoRefreshed(int sourceFirstIndex, Timeline sourceTimeline,
+      Object sourceManifest) {
+    // Set the timeline and manifest.
+    timelines[sourceFirstIndex] = sourceTimeline;
+    manifests[sourceFirstIndex] = sourceManifest;
+    // Also set the timeline and manifest for any duplicate entries of the same source.
+    for (int i = sourceFirstIndex + 1; i < mediaSources.length; i++) {
+      if (mediaSources[i] == mediaSources[sourceFirstIndex]) {
+        timelines[i] = sourceTimeline;
+        manifests[i] = sourceManifest;
+      }
+    }
+    for (Timeline timeline : timelines) {
+      if (timeline == null) {
+        // Don't invoke the listener until all sources have timelines.
+        return;
+      }
+    }
+    timeline = new ConcatenatedTimeline(timelines.clone());
+    listener.onSourceInfoRefreshed(timeline, manifests.clone());
+  }
+
+  private static boolean[] buildDuplicateFlags(MediaSource[] mediaSources) {
+    boolean[] duplicateFlags = new boolean[mediaSources.length];
+    IdentityHashMap<MediaSource, Void> sources = new IdentityHashMap<>(mediaSources.length);
+    for (int i = 0; i < mediaSources.length; i++) {
+      MediaSource source = mediaSources[i];
+      if (!sources.containsKey(source)) {
+        sources.put(source, null);
+      } else {
+        duplicateFlags[i] = true;
+      }
+    }
+    return duplicateFlags;
+  }
+
+  /**
+   * A {@link Timeline} that is the concatenation of one or more {@link Timeline}s.
+   */
+  private static final class ConcatenatedTimeline extends Timeline {
+
+    private final Timeline[] timelines;
+    private final int[] sourcePeriodOffsets;
+    private final int[] sourceWindowOffsets;
+
+    public ConcatenatedTimeline(Timeline[] timelines) {
+      int[] sourcePeriodOffsets = new int[timelines.length];
+      int[] sourceWindowOffsets = new int[timelines.length];
+      int periodCount = 0;
+      int windowCount = 0;
+      for (int i = 0; i < timelines.length; i++) {
+        Timeline timeline = timelines[i];
+        periodCount += timeline.getPeriodCount();
+        sourcePeriodOffsets[i] = periodCount;
+        windowCount += timeline.getWindowCount();
+        sourceWindowOffsets[i] = windowCount;
+      }
+      this.timelines = timelines;
+      this.sourcePeriodOffsets = sourcePeriodOffsets;
+      this.sourceWindowOffsets = sourceWindowOffsets;
+    }
+
+    @Override
+    public int getWindowCount() {
+      return sourceWindowOffsets[sourceWindowOffsets.length - 1];
+    }
+
+    @Override
+    public Window getWindow(int windowIndex, Window window, boolean setIds,
+        long defaultPositionProjectionUs) {
+      int sourceIndex = getSourceIndexForWindow(windowIndex);
+      int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex);
+      int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex);
+      timelines[sourceIndex].getWindow(windowIndex - firstWindowIndexInSource, window, setIds,
+          defaultPositionProjectionUs);
+      window.firstPeriodIndex += firstPeriodIndexInSource;
+      window.lastPeriodIndex += firstPeriodIndexInSource;
+      return window;
+    }
+
+    @Override
+    public int getPeriodCount() {
+      return sourcePeriodOffsets[sourcePeriodOffsets.length - 1];
+    }
+
+    @Override
+    public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+      int sourceIndex = getSourceIndexForPeriod(periodIndex);
+      int firstWindowIndexInSource = getFirstWindowIndexInSource(sourceIndex);
+      int firstPeriodIndexInSource = getFirstPeriodIndexInSource(sourceIndex);
+      timelines[sourceIndex].getPeriod(periodIndex - firstPeriodIndexInSource, period, setIds);
+      period.windowIndex += firstWindowIndexInSource;
+      if (setIds) {
+        period.uid = Pair.create(sourceIndex, period.uid);
+      }
+      return period;
+    }
+
+    @Override
+    public int getIndexOfPeriod(Object uid) {
+      if (!(uid instanceof Pair)) {
+        return C.INDEX_UNSET;
+      }
+      Pair<?, ?> sourceIndexAndPeriodId = (Pair<?, ?>) uid;
+      if (!(sourceIndexAndPeriodId.first instanceof Integer)) {
+        return C.INDEX_UNSET;
+      }
+      int sourceIndex = (Integer) sourceIndexAndPeriodId.first;
+      Object periodId = sourceIndexAndPeriodId.second;
+      if (sourceIndex < 0 || sourceIndex >= timelines.length) {
+        return C.INDEX_UNSET;
+      }
+      int periodIndexInSource = timelines[sourceIndex].getIndexOfPeriod(periodId);
+      return periodIndexInSource == C.INDEX_UNSET ? C.INDEX_UNSET
+          : getFirstPeriodIndexInSource(sourceIndex) + periodIndexInSource;
+    }
+
+    private int getSourceIndexForPeriod(int periodIndex) {
+      return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex, true, false) + 1;
+    }
+
+    private int getFirstPeriodIndexInSource(int sourceIndex) {
+      return sourceIndex == 0 ? 0 : sourcePeriodOffsets[sourceIndex - 1];
+    }
+
+    private int getSourceIndexForWindow(int windowIndex) {
+      return Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1;
+    }
+
+    private int getFirstWindowIndexInSource(int sourceIndex) {
+      return sourceIndex == 0 ? 0 : sourceWindowOffsets[sourceIndex - 1];
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
@@ -0,0 +1,720 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import android.net.Uri;
+import android.os.Handler;
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
+import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
+import com.google.android.exoplayer2.extractor.DefaultTrackOutput.UpstreamFormatChangedListener;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.Loader;
+import com.google.android.exoplayer2.upstream.Loader.Loadable;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ConditionVariable;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.io.EOFException;
+import java.io.IOException;
+
+/**
+ * A {@link MediaPeriod} that extracts data using an {@link Extractor}.
+ */
+/* package */ final class ExtractorMediaPeriod implements MediaPeriod, ExtractorOutput,
+    Loader.Callback<ExtractorMediaPeriod.ExtractingLoadable>, UpstreamFormatChangedListener {
+
+  /**
+   * When the source's duration is unknown, it is calculated by adding this value to the largest
+   * sample timestamp seen when buffering completes.
+   */
+  private static final long DEFAULT_LAST_SAMPLE_DURATION_US = 10000;
+
+  private final Uri uri;
+  private final DataSource dataSource;
+  private final int minLoadableRetryCount;
+  private final Handler eventHandler;
+  private final ExtractorMediaSource.EventListener eventListener;
+  private final MediaSource.Listener sourceListener;
+  private final Allocator allocator;
+  private final String customCacheKey;
+  private final Loader loader;
+  private final ExtractorHolder extractorHolder;
+  private final ConditionVariable loadCondition;
+  private final Runnable maybeFinishPrepareRunnable;
+  private final Runnable onContinueLoadingRequestedRunnable;
+  private final Handler handler;
+  private final SparseArray<DefaultTrackOutput> sampleQueues;
+
+  private Callback callback;
+  private SeekMap seekMap;
+  private boolean tracksBuilt;
+  private boolean prepared;
+
+  private boolean seenFirstTrackSelection;
+  private boolean notifyReset;
+  private int enabledTrackCount;
+  private TrackGroupArray tracks;
+  private long durationUs;
+  private boolean[] trackEnabledStates;
+  private boolean[] trackIsAudioVideoFlags;
+  private boolean haveAudioVideoTracks;
+  private long length;
+
+  private long lastSeekPositionUs;
+  private long pendingResetPositionUs;
+
+  private int extractedSamplesCountAtStartOfLoad;
+  private boolean loadingFinished;
+  private boolean released;
+
+  /**
+   * @param uri The {@link Uri} of the media stream.
+   * @param dataSource The data source to read the media.
+   * @param extractors The extractors to use to read the data source.
+   * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
+   * @param eventHandler A handler for events. May be null if delivery of events is not required.
+   * @param eventListener A listener of events. May be null if delivery of events is not required.
+   * @param sourceListener A listener to notify when the timeline has been loaded.
+   * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
+   * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
+   *     indexing. May be null.
+   */
+  public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors,
+      int minLoadableRetryCount, Handler eventHandler,
+      ExtractorMediaSource.EventListener eventListener, MediaSource.Listener sourceListener,
+      Allocator allocator, String customCacheKey) {
+    this.uri = uri;
+    this.dataSource = dataSource;
+    this.minLoadableRetryCount = minLoadableRetryCount;
+    this.eventHandler = eventHandler;
+    this.eventListener = eventListener;
+    this.sourceListener = sourceListener;
+    this.allocator = allocator;
+    this.customCacheKey = customCacheKey;
+    loader = new Loader("Loader:ExtractorMediaPeriod");
+    extractorHolder = new ExtractorHolder(extractors, this);
+    loadCondition = new ConditionVariable();
+    maybeFinishPrepareRunnable = new Runnable() {
+      @Override
+      public void run() {
+        maybeFinishPrepare();
+      }
+    };
+    onContinueLoadingRequestedRunnable = new Runnable() {
+      @Override
+      public void run() {
+        if (!released) {
+          callback.onContinueLoadingRequested(ExtractorMediaPeriod.this);
+        }
+      }
+    };
+    handler = new Handler();
+
+    pendingResetPositionUs = C.TIME_UNSET;
+    sampleQueues = new SparseArray<>();
+    length = C.LENGTH_UNSET;
+  }
+
+  public void release() {
+    final ExtractorHolder extractorHolder = this.extractorHolder;
+    loader.release(new Runnable() {
+      @Override
+      public void run() {
+        extractorHolder.release();
+        int trackCount = sampleQueues.size();
+        for (int i = 0; i < trackCount; i++) {
+          sampleQueues.valueAt(i).disable();
+        }
+      }
+    });
+    handler.removeCallbacksAndMessages(null);
+    released = true;
+  }
+
+  @Override
+  public void prepare(Callback callback) {
+    this.callback = callback;
+    loadCondition.open();
+    startLoading();
+  }
+
+  @Override
+  public void maybeThrowPrepareError() throws IOException {
+    maybeThrowError();
+  }
+
+  @Override
+  public TrackGroupArray getTrackGroups() {
+    return tracks;
+  }
+
+  @Override
+  public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+      SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
+    Assertions.checkState(prepared);
+    // Disable old tracks.
+    for (int i = 0; i < selections.length; i++) {
+      if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
+        int track = ((SampleStreamImpl) streams[i]).track;
+        Assertions.checkState(trackEnabledStates[track]);
+        enabledTrackCount--;
+        trackEnabledStates[track] = false;
+        sampleQueues.valueAt(track).disable();
+        streams[i] = null;
+      }
+    }
+    // Enable new tracks.
+    boolean selectedNewTracks = false;
+    for (int i = 0; i < selections.length; i++) {
+      if (streams[i] == null && selections[i] != null) {
+        TrackSelection selection = selections[i];
+        Assertions.checkState(selection.length() == 1);
+        Assertions.checkState(selection.getIndexInTrackGroup(0) == 0);
+        int track = tracks.indexOf(selection.getTrackGroup());
+        Assertions.checkState(!trackEnabledStates[track]);
+        enabledTrackCount++;
+        trackEnabledStates[track] = true;
+        streams[i] = new SampleStreamImpl(track);
+        streamResetFlags[i] = true;
+        selectedNewTracks = true;
+      }
+    }
+    if (!seenFirstTrackSelection) {
+      // At the time of the first track selection all queues will be enabled, so we need to disable
+      // any that are no longer required.
+      int trackCount = sampleQueues.size();
+      for (int i = 0; i < trackCount; i++) {
+        if (!trackEnabledStates[i]) {
+          sampleQueues.valueAt(i).disable();
+        }
+      }
+    }
+    if (enabledTrackCount == 0) {
+      notifyReset = false;
+      if (loader.isLoading()) {
+        loader.cancelLoading();
+      }
+    } else if (seenFirstTrackSelection ? selectedNewTracks : positionUs != 0) {
+      positionUs = seekToUs(positionUs);
+      // We'll need to reset renderers consuming from all streams due to the seek.
+      for (int i = 0; i < streams.length; i++) {
+        if (streams[i] != null) {
+          streamResetFlags[i] = true;
+        }
+      }
+    }
+    seenFirstTrackSelection = true;
+    return positionUs;
+  }
+
+  @Override
+  public boolean continueLoading(long playbackPositionUs) {
+    if (loadingFinished || (prepared && enabledTrackCount == 0)) {
+      return false;
+    }
+    boolean continuedLoading = loadCondition.open();
+    if (!loader.isLoading()) {
+      startLoading();
+      continuedLoading = true;
+    }
+    return continuedLoading;
+  }
+
+  @Override
+  public long getNextLoadPositionUs() {
+    return enabledTrackCount == 0 ? C.TIME_END_OF_SOURCE : getBufferedPositionUs();
+  }
+
+  @Override
+  public long readDiscontinuity() {
+    if (notifyReset) {
+      notifyReset = false;
+      return lastSeekPositionUs;
+    }
+    return C.TIME_UNSET;
+  }
+
+  @Override
+  public long getBufferedPositionUs() {
+    if (loadingFinished) {
+      return C.TIME_END_OF_SOURCE;
+    } else if (isPendingReset()) {
+      return pendingResetPositionUs;
+    }
+    long largestQueuedTimestampUs;
+    if (haveAudioVideoTracks) {
+      // Ignore non-AV tracks, which may be sparse or poorly interleaved.
+      largestQueuedTimestampUs = Long.MAX_VALUE;
+      int trackCount = sampleQueues.size();
+      for (int i = 0; i < trackCount; i++) {
+        if (trackIsAudioVideoFlags[i]) {
+          largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs,
+              sampleQueues.valueAt(i).getLargestQueuedTimestampUs());
+        }
+      }
+    } else {
+      largestQueuedTimestampUs = getLargestQueuedTimestampUs();
+    }
+    return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs
+        : largestQueuedTimestampUs;
+  }
+
+  @Override
+  public long seekToUs(long positionUs) {
+    // Treat all seeks into non-seekable media as being to t=0.
+    positionUs = seekMap.isSeekable() ? positionUs : 0;
+    lastSeekPositionUs = positionUs;
+    int trackCount = sampleQueues.size();
+    // If we're not pending a reset, see if we can seek within the sample queues.
+    boolean seekInsideBuffer = !isPendingReset();
+    for (int i = 0; seekInsideBuffer && i < trackCount; i++) {
+      if (trackEnabledStates[i]) {
+        seekInsideBuffer = sampleQueues.valueAt(i).skipToKeyframeBefore(positionUs);
+      }
+    }
+    // If we failed to seek within the sample queues, we need to restart.
+    if (!seekInsideBuffer) {
+      pendingResetPositionUs = positionUs;
+      loadingFinished = false;
+      if (loader.isLoading()) {
+        loader.cancelLoading();
+      } else {
+        for (int i = 0; i < trackCount; i++) {
+          sampleQueues.valueAt(i).reset(trackEnabledStates[i]);
+        }
+      }
+    }
+    notifyReset = false;
+    return positionUs;
+  }
+
+  // SampleStream methods.
+
+  /* package */ boolean isReady(int track) {
+    return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(track).isEmpty());
+  }
+
+  /* package */ void maybeThrowError() throws IOException {
+    loader.maybeThrowError();
+  }
+
+  /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer) {
+    if (notifyReset || isPendingReset()) {
+      return C.RESULT_NOTHING_READ;
+    }
+
+    return sampleQueues.valueAt(track).readData(formatHolder, buffer, loadingFinished,
+        lastSeekPositionUs);
+  }
+
+  // Loader.Callback implementation.
+
+  @Override
+  public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs,
+      long loadDurationMs) {
+    copyLengthFromLoader(loadable);
+    loadingFinished = true;
+    if (durationUs == C.TIME_UNSET) {
+      long largestQueuedTimestampUs = getLargestQueuedTimestampUs();
+      durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0
+          : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US;
+      sourceListener.onSourceInfoRefreshed(
+          new SinglePeriodTimeline(durationUs, seekMap.isSeekable()), null);
+    }
+  }
+
+  @Override
+  public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs,
+      long loadDurationMs, boolean released) {
+    copyLengthFromLoader(loadable);
+    if (!released && enabledTrackCount > 0) {
+      int trackCount = sampleQueues.size();
+      for (int i = 0; i < trackCount; i++) {
+        sampleQueues.valueAt(i).reset(trackEnabledStates[i]);
+      }
+      callback.onContinueLoadingRequested(this);
+    }
+  }
+
+  @Override
+  public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs,
+      long loadDurationMs, IOException error) {
+    copyLengthFromLoader(loadable);
+    notifyLoadError(error);
+    if (isLoadableExceptionFatal(error)) {
+      return Loader.DONT_RETRY_FATAL;
+    }
+    int extractedSamplesCount = getExtractedSamplesCount();
+    boolean madeProgress = extractedSamplesCount > extractedSamplesCountAtStartOfLoad;
+    configureRetry(loadable); // May reset the sample queues.
+    extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount();
+    return madeProgress ? Loader.RETRY_RESET_ERROR_COUNT : Loader.RETRY;
+  }
+
+  // ExtractorOutput implementation. Called by the loading thread.
+
+  @Override
+  public TrackOutput track(int id) {
+    DefaultTrackOutput trackOutput = sampleQueues.get(id);
+    if (trackOutput == null) {
+      trackOutput = new DefaultTrackOutput(allocator);
+      trackOutput.setUpstreamFormatChangeListener(this);
+      sampleQueues.put(id, trackOutput);
+    }
+    return trackOutput;
+  }
+
+  @Override
+  public void endTracks() {
+    tracksBuilt = true;
+    handler.post(maybeFinishPrepareRunnable);
+  }
+
+  @Override
+  public void seekMap(SeekMap seekMap) {
+    this.seekMap = seekMap;
+    handler.post(maybeFinishPrepareRunnable);
+  }
+
+  // UpstreamFormatChangedListener implementation. Called by the loading thread.
+
+  @Override
+  public void onUpstreamFormatChanged(Format format) {
+    handler.post(maybeFinishPrepareRunnable);
+  }
+
+  // Internal methods.
+
+  private void maybeFinishPrepare() {
+    if (released || prepared || seekMap == null || !tracksBuilt) {
+      return;
+    }
+    int trackCount = sampleQueues.size();
+    for (int i = 0; i < trackCount; i++) {
+      if (sampleQueues.valueAt(i).getUpstreamFormat() == null) {
+        return;
+      }
+    }
+    loadCondition.close();
+    TrackGroup[] trackArray = new TrackGroup[trackCount];
+    trackIsAudioVideoFlags = new boolean[trackCount];
+    trackEnabledStates = new boolean[trackCount];
+    durationUs = seekMap.getDurationUs();
+    for (int i = 0; i < trackCount; i++) {
+      Format trackFormat = sampleQueues.valueAt(i).getUpstreamFormat();
+      trackArray[i] = new TrackGroup(trackFormat);
+      String mimeType = trackFormat.sampleMimeType;
+      boolean isAudioVideo = MimeTypes.isVideo(mimeType) || MimeTypes.isAudio(mimeType);
+      trackIsAudioVideoFlags[i] = isAudioVideo;
+      haveAudioVideoTracks |= isAudioVideo;
+    }
+    tracks = new TrackGroupArray(trackArray);
+    prepared = true;
+    sourceListener.onSourceInfoRefreshed(
+        new SinglePeriodTimeline(durationUs, seekMap.isSeekable()), null);
+    callback.onPrepared(this);
+  }
+
+  private void copyLengthFromLoader(ExtractingLoadable loadable) {
+    if (length == C.LENGTH_UNSET) {
+      length = loadable.length;
+    }
+  }
+
+  private void startLoading() {
+    ExtractingLoadable loadable = new ExtractingLoadable(uri, dataSource, extractorHolder,
+        loadCondition);
+    if (prepared) {
+      Assertions.checkState(isPendingReset());
+      if (durationUs != C.TIME_UNSET && pendingResetPositionUs >= durationUs) {
+        loadingFinished = true;
+        pendingResetPositionUs = C.TIME_UNSET;
+        return;
+      }
+      loadable.setLoadPosition(seekMap.getPosition(pendingResetPositionUs), pendingResetPositionUs);
+      pendingResetPositionUs = C.TIME_UNSET;
+    }
+    extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount();
+
+    int minRetryCount = minLoadableRetryCount;
+    if (minRetryCount == ExtractorMediaSource.MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA) {
+      // We assume on-demand before we're prepared.
+      minRetryCount = !prepared || length != C.LENGTH_UNSET
+          || (seekMap != null && seekMap.getDurationUs() != C.TIME_UNSET)
+          ? ExtractorMediaSource.DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND
+          : ExtractorMediaSource.DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE;
+    }
+    loader.startLoading(loadable, this, minRetryCount);
+  }
+
+  private void configureRetry(ExtractingLoadable loadable) {
+    if (length != C.LENGTH_UNSET
+        || (seekMap != null && seekMap.getDurationUs() != C.TIME_UNSET)) {
+      // We're playing an on-demand stream. Resume the current loadable, which will
+      // request data starting from the point it left off.
+    } else {
+      // We're playing a stream of unknown length and duration. Assume it's live, and
+      // therefore that the data at the uri is a continuously shifting window of the latest
+      // available media. For this case there's no way to continue loading from where a
+      // previous load finished, so it's necessary to load from the start whenever commencing
+      // a new load.
+      lastSeekPositionUs = 0;
+      notifyReset = prepared;
+      int trackCount = sampleQueues.size();
+      for (int i = 0; i < trackCount; i++) {
+        sampleQueues.valueAt(i).reset(!prepared || trackEnabledStates[i]);
+      }
+      loadable.setLoadPosition(0, 0);
+    }
+  }
+
+  private int getExtractedSamplesCount() {
+    int extractedSamplesCount = 0;
+    int trackCount = sampleQueues.size();
+    for (int i = 0; i < trackCount; i++) {
+      extractedSamplesCount += sampleQueues.valueAt(i).getWriteIndex();
+    }
+    return extractedSamplesCount;
+  }
+
+  private long getLargestQueuedTimestampUs() {
+    long largestQueuedTimestampUs = Long.MIN_VALUE;
+    int trackCount = sampleQueues.size();
+    for (int i = 0; i < trackCount; i++) {
+      largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs,
+          sampleQueues.valueAt(i).getLargestQueuedTimestampUs());
+    }
+    return largestQueuedTimestampUs;
+  }
+
+  private boolean isPendingReset() {
+    return pendingResetPositionUs != C.TIME_UNSET;
+  }
+
+  private boolean isLoadableExceptionFatal(IOException e) {
+    return e instanceof UnrecognizedInputFormatException;
+  }
+
+  private void notifyLoadError(final IOException error) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable()  {
+        @Override
+        public void run() {
+          eventListener.onLoadError(error);
+        }
+      });
+    }
+  }
+
+  private final class SampleStreamImpl implements SampleStream {
+
+    private final int track;
+
+    public SampleStreamImpl(int track) {
+      this.track = track;
+    }
+
+    @Override
+    public boolean isReady() {
+      return ExtractorMediaPeriod.this.isReady(track);
+    }
+
+    @Override
+    public void maybeThrowError() throws IOException {
+      ExtractorMediaPeriod.this.maybeThrowError();
+    }
+
+    @Override
+    public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
+      return ExtractorMediaPeriod.this.readData(track, formatHolder, buffer);
+    }
+
+    @Override
+    public void skipToKeyframeBefore(long timeUs) {
+      sampleQueues.valueAt(track).skipToKeyframeBefore(timeUs);
+    }
+
+  }
+
+  /**
+   * Loads the media stream and extracts sample data from it.
+   */
+  /* package */ final class ExtractingLoadable implements Loadable {
+
+    /**
+     * The number of bytes that should be loaded between each each invocation of
+     * {@link Callback#onContinueLoadingRequested(SequenceableLoader)}.
+     */
+    private static final int CONTINUE_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024;
+
+    private final Uri uri;
+    private final DataSource dataSource;
+    private final ExtractorHolder extractorHolder;
+    private final ConditionVariable loadCondition;
+    private final PositionHolder positionHolder;
+
+    private volatile boolean loadCanceled;
+
+    private boolean pendingExtractorSeek;
+    private long seekTimeUs;
+    private long length;
+
+    public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder,
+        ConditionVariable loadCondition) {
+      this.uri = Assertions.checkNotNull(uri);
+      this.dataSource = Assertions.checkNotNull(dataSource);
+      this.extractorHolder = Assertions.checkNotNull(extractorHolder);
+      this.loadCondition = loadCondition;
+      this.positionHolder = new PositionHolder();
+      this.pendingExtractorSeek = true;
+      this.length = C.LENGTH_UNSET;
+    }
+
+    public void setLoadPosition(long position, long timeUs) {
+      positionHolder.position = position;
+      seekTimeUs = timeUs;
+      pendingExtractorSeek = true;
+    }
+
+    @Override
+    public void cancelLoad() {
+      loadCanceled = true;
+    }
+
+    @Override
+    public boolean isLoadCanceled() {
+      return loadCanceled;
+    }
+
+    @Override
+    public void load() throws IOException, InterruptedException {
+      int result = Extractor.RESULT_CONTINUE;
+      while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+        ExtractorInput input = null;
+        try {
+          long position = positionHolder.position;
+          length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey));
+          if (length != C.LENGTH_UNSET) {
+            length += position;
+          }
+          input = new DefaultExtractorInput(dataSource, position, length);
+          Extractor extractor = extractorHolder.selectExtractor(input, dataSource.getUri());
+          if (pendingExtractorSeek) {
+            extractor.seek(position, seekTimeUs);
+            pendingExtractorSeek = false;
+          }
+          while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+            loadCondition.block();
+            result = extractor.read(input, positionHolder);
+            if (input.getPosition() > position + CONTINUE_LOADING_CHECK_INTERVAL_BYTES) {
+              position = input.getPosition();
+              loadCondition.close();
+              handler.post(onContinueLoadingRequestedRunnable);
+            }
+          }
+        } finally {
+          if (result == Extractor.RESULT_SEEK) {
+            result = Extractor.RESULT_CONTINUE;
+          } else if (input != null) {
+            positionHolder.position = input.getPosition();
+          }
+          Util.closeQuietly(dataSource);
+        }
+      }
+    }
+
+  }
+
+  /**
+   * Stores a list of extractors and a selected extractor when the format has been detected.
+   */
+  private static final class ExtractorHolder {
+
+    private final Extractor[] extractors;
+    private final ExtractorOutput extractorOutput;
+    private Extractor extractor;
+
+    /**
+     * Creates a holder that will select an extractor and initialize it using the specified output.
+     *
+     * @param extractors One or more extractors to choose from.
+     * @param extractorOutput The output that will be used to initialize the selected extractor.
+     */
+    public ExtractorHolder(Extractor[] extractors, ExtractorOutput extractorOutput) {
+      this.extractors = extractors;
+      this.extractorOutput = extractorOutput;
+    }
+
+    /**
+     * Returns an initialized extractor for reading {@code input}, and returns the same extractor on
+     * later calls.
+     *
+     * @param input The {@link ExtractorInput} from which data should be read.
+     * @param uri The {@link Uri} of the data.
+     * @return An initialized extractor for reading {@code input}.
+     * @throws UnrecognizedInputFormatException Thrown if the input format could not be detected.
+     * @throws IOException Thrown if the input could not be read.
+     * @throws InterruptedException Thrown if the thread was interrupted.
+     */
+    public Extractor selectExtractor(ExtractorInput input, Uri uri)
+        throws IOException, InterruptedException {
+      if (extractor != null) {
+        return extractor;
+      }
+      for (Extractor extractor : extractors) {
+        try {
+          if (extractor.sniff(input)) {
+            this.extractor = extractor;
+            break;
+          }
+        } catch (EOFException e) {
+          // Do nothing.
+        } finally {
+          input.resetPeekPosition();
+        }
+      }
+      if (extractor == null) {
+        throw new UnrecognizedInputFormatException("None of the available extractors ("
+            + Util.getCommaDelimitedSimpleClassNames(extractors) + ") could read the stream.", uri);
+      }
+      extractor.init(extractorOutput);
+      return extractor;
+    }
+
+    public void release() {
+      if (extractor != null) {
+        extractor.release();
+        extractor = null;
+      }
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/ExtractorMediaSource.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 com.google.android.exoplayer2.source;
+
+import android.net.Uri;
+import android.os.Handler;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorsFactory;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DataSource;
+import 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 extractors for AAC, MPEG PS/TS and FLV streams do not support seeking.
+ */
+public final class ExtractorMediaSource implements MediaSource, MediaSource.Listener {
+
+  /**
+   * Listener of {@link ExtractorMediaSource} events.
+   */
+  public interface EventListener {
+
+    /**
+     * Called when an error occurs loading media data.
+     *
+     * @param error The load error.
+     */
+    void onLoadError(IOException error);
+
+  }
+
+  /**
+   * The default minimum number of times to retry loading prior to failing for on-demand streams.
+   */
+  public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND = 3;
+
+  /**
+   * The default minimum number of times to retry loading prior to failing for live streams.
+   */
+  public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE = 6;
+
+  /**
+   * Value for {@code minLoadableRetryCount} that causes the loader to retry
+   * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE} times for live streams and
+   * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND} for on-demand streams.
+   */
+  public static final int MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA = -1;
+
+  private final Uri uri;
+  private final DataSource.Factory dataSourceFactory;
+  private final ExtractorsFactory extractorsFactory;
+  private final int minLoadableRetryCount;
+  private final Handler eventHandler;
+  private final EventListener eventListener;
+  private final Timeline.Period period;
+  private final String customCacheKey;
+
+  private MediaSource.Listener sourceListener;
+  private Timeline timeline;
+  private boolean timelineHasDuration;
+
+  /**
+   * @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.
+   */
+  public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
+      ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) {
+    this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler,
+        eventListener, null);
+  }
+
+  /**
+   * @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.
+   */
+  public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
+      ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener,
+      String customCacheKey) {
+    this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler,
+        eventListener, customCacheKey);
+  }
+
+  /**
+   * @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 minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
+   * @param eventHandler A handler for events. May be null if delivery of events is not required.
+   * @param eventListener A listener of events. May be null if delivery of events is not required.
+   * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
+   *     indexing. May be null.
+   */
+  public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
+      ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler,
+      EventListener eventListener, String customCacheKey) {
+    this.uri = uri;
+    this.dataSourceFactory = dataSourceFactory;
+    this.extractorsFactory = extractorsFactory;
+    this.minLoadableRetryCount = minLoadableRetryCount;
+    this.eventHandler = eventHandler;
+    this.eventListener = eventListener;
+    this.customCacheKey = customCacheKey;
+    period = new Timeline.Period();
+  }
+
+  @Override
+  public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+    sourceListener = listener;
+    timeline = new SinglePeriodTimeline(C.TIME_UNSET, false);
+    listener.onSourceInfoRefreshed(timeline, null);
+  }
+
+  @Override
+  public void maybeThrowSourceInfoRefreshError() throws IOException {
+    // Do nothing.
+  }
+
+  @Override
+  public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+    Assertions.checkArgument(index == 0);
+    return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(),
+        extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener,
+        this, allocator, customCacheKey);
+  }
+
+  @Override
+  public void releasePeriod(MediaPeriod mediaPeriod) {
+    ((ExtractorMediaPeriod) mediaPeriod).release();
+  }
+
+  @Override
+  public void releaseSource() {
+    sourceListener = null;
+  }
+
+  // MediaSource.Listener implementation.
+
+  @Override
+  public void onSourceInfoRefreshed(Timeline newTimeline, Object manifest) {
+    long newTimelineDurationUs = newTimeline.getPeriod(0, period).getDurationUs();
+    boolean newTimelineHasDuration = newTimelineDurationUs != C.TIME_UNSET;
+    if (timelineHasDuration && !newTimelineHasDuration) {
+      // Suppress source info changes that would make the duration unknown when it is already known.
+      return;
+    }
+    timeline = newTimeline;
+    timelineHasDuration = newTimelineHasDuration;
+    sourceListener.onSourceInfoRefreshed(timeline, null);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/LoopingMediaSource.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * Loops a {@link MediaSource}.
+ */
+public final class LoopingMediaSource implements MediaSource {
+
+  private static final String TAG = "LoopingMediaSource";
+
+  private final MediaSource childSource;
+  private final int loopCount;
+
+  private int childPeriodCount;
+
+  /**
+   * Loops the provided source indefinitely.
+   *
+   * @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. The actual number of
+   *     loops will be capped at the maximum value that can achieved without causing the number of
+   *     periods exposed by the source to exceed {@link Integer#MAX_VALUE}.
+   */
+  public LoopingMediaSource(MediaSource childSource, int loopCount) {
+    Assertions.checkArgument(loopCount > 0);
+    this.childSource = childSource;
+    this.loopCount = loopCount;
+  }
+
+  @Override
+  public void prepareSource(ExoPlayer player, boolean isTopLevelSource, final Listener listener) {
+    childSource.prepareSource(player, false, new Listener() {
+      @Override
+      public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+        childPeriodCount = timeline.getPeriodCount();
+        listener.onSourceInfoRefreshed(new LoopingTimeline(timeline, loopCount), manifest);
+      }
+    });
+  }
+
+  @Override
+  public void maybeThrowSourceInfoRefreshError() throws IOException {
+    childSource.maybeThrowSourceInfoRefreshError();
+  }
+
+  @Override
+  public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+    return childSource.createPeriod(index % childPeriodCount, allocator, positionUs);
+  }
+
+  @Override
+  public void releasePeriod(MediaPeriod mediaPeriod) {
+    childSource.releasePeriod(mediaPeriod);
+  }
+
+  @Override
+  public void releaseSource() {
+    childSource.releaseSource();
+  }
+
+  private static final class LoopingTimeline extends Timeline {
+
+    private final Timeline childTimeline;
+    private final int childPeriodCount;
+    private final int childWindowCount;
+    private final int loopCount;
+
+    public LoopingTimeline(Timeline childTimeline, int loopCount) {
+      this.childTimeline = childTimeline;
+      childPeriodCount = childTimeline.getPeriodCount();
+      childWindowCount = childTimeline.getWindowCount();
+      // This is the maximum number of loops that can be performed without overflow.
+      int maxLoopCount = Integer.MAX_VALUE / childPeriodCount;
+      if (loopCount > maxLoopCount) {
+        if (loopCount != Integer.MAX_VALUE) {
+          Log.w(TAG, "Capped loops to avoid overflow: " + loopCount + " -> " + maxLoopCount);
+        }
+        this.loopCount = maxLoopCount;
+      } else {
+        this.loopCount = loopCount;
+      }
+    }
+
+    @Override
+    public int getWindowCount() {
+      return childWindowCount * loopCount;
+    }
+
+    @Override
+    public Window getWindow(int windowIndex, Window window, boolean setIds,
+        long defaultPositionProjectionUs) {
+      childTimeline.getWindow(windowIndex % childWindowCount, window, setIds,
+          defaultPositionProjectionUs);
+      int periodIndexOffset = (windowIndex / childWindowCount) * childPeriodCount;
+      window.firstPeriodIndex += periodIndexOffset;
+      window.lastPeriodIndex += periodIndexOffset;
+      return window;
+    }
+
+    @Override
+    public int getPeriodCount() {
+      return childPeriodCount * loopCount;
+    }
+
+    @Override
+    public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+      childTimeline.getPeriod(periodIndex % childPeriodCount, period, setIds);
+      int loopCount = (periodIndex / childPeriodCount);
+      period.windowIndex += loopCount * childWindowCount;
+      if (setIds) {
+        period.uid = Pair.create(loopCount, period.uid);
+      }
+      return period;
+    }
+
+    @Override
+    public int getIndexOfPeriod(Object uid) {
+      if (!(uid instanceof Pair)) {
+        return C.INDEX_UNSET;
+      }
+      Pair<?, ?> loopCountAndChildUid = (Pair<?, ?>) uid;
+      if (!(loopCountAndChildUid.first instanceof Integer)) {
+        return C.INDEX_UNSET;
+      }
+      int loopCount = (Integer) loopCountAndChildUid.first;
+      int periodIndexOffset = loopCount * childPeriodCount;
+      return childTimeline.getIndexOfPeriod(loopCountAndChildUid.second) + periodIndexOffset;
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/MediaPeriod.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import java.io.IOException;
+
+/**
+ * A source of a single period of media.
+ */
+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 MediaSource.Listener#onSourceInfoRefreshed(Timeline, Object)} will be
+   * called before {@code callback.onPrepared}.
+   *
+   * @param callback Callback to receive updates from this period, including being notified when
+   *     preparation completes.
+   */
+  void prepare(Callback callback);
+
+  /**
+   * Throws an error that's preventing the period from becoming prepared. Does nothing if no such
+   * error exists.
+   * <p>
+   * This method should only be 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 should only be called after the period has been prepared.
+   *
+   * @return The {@link TrackGroup}s.
+   */
+  TrackGroupArray getTrackGroups();
+
+  /**
+   * Performs a track selection.
+   * <p>
+   * The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags}
+   * indicating whether the existing {@code SampleStream} can be retained for each selection, and
+   * the 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>
+   * This method should only be called after the period has been prepared.
+   *
+   * @param selections The renderer track selections.
+   * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained
+   *     for each selection. A {@code true} value indicates that the selection is unchanged, and
+   *     that the caller does not require that the sample stream be recreated.
+   * @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.
+   * @return The actual position at which the tracks were enabled, in microseconds.
+   */
+  long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+      SampleStream[] streams, boolean[] streamResetFlags, long positionUs);
+
+  /**
+   * 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.
+   *
+   * @return If a discontinuity was read then the playback position in microseconds after the
+   *     discontinuity. Else {@link C#TIME_UNSET}.
+   */
+  long readDiscontinuity();
+
+  /**
+   * Returns an estimate of the position up to which data is buffered for the enabled tracks.
+   * <p>
+   * This method should only be called when at least one track is selected.
+   *
+   * @return An estimate of the absolute position in microseconds up to which data is buffered, or
+   *     {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.
+   */
+  long getBufferedPositionUs();
+
+  /**
+   * Attempts to seek to the specified position in microseconds.
+   * <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 should only be 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);
+
+  // SequenceableLoader interface. Overridden to provide more specific documentation.
+
+  /**
+   * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished.
+   * <p>
+   * This method should only be 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)} to request that this method be called
+   * when the period is permitted to continue loading data. A period may do this both during and
+   * after preparation.
+   *
+   * @param positionUs The current playback position.
+   * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return
+   *     a different value than prior to the call. False otherwise.
+   */
+  @Override
+  boolean continueLoading(long positionUs);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/MediaSource.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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.upstream.Allocator;
+import java.io.IOException;
+
+/**
+ * A source of media consisting of one or more {@link MediaPeriod}s.
+ */
+public interface MediaSource {
+
+  /**
+   * Listener for source events.
+   */
+  interface Listener {
+
+    /**
+     * Called when manifest and/or timeline has been refreshed.
+     *
+     * @param timeline The source's timeline.
+     * @param manifest The loaded manifest.
+     */
+    void onSourceInfoRefreshed(Timeline timeline, Object manifest);
+
+  }
+
+  /**
+   * Starts preparation of the source.
+   *
+   * @param player The player for which this source is being prepared.
+   * @param isTopLevelSource Whether this source has been passed directly to
+   *     {@link ExoPlayer#prepare(MediaSource)} or
+   *     {@link ExoPlayer#prepare(MediaSource, boolean, boolean)}. If {@code false}, this source is
+   *     being prepared by another source (e.g. {@link ConcatenatingMediaSource}) for composition.
+   * @param listener The listener for source events.
+   */
+  void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener);
+
+  /**
+   * Throws any pending error encountered while loading or refreshing source information.
+   */
+  void maybeThrowSourceInfoRefreshError() throws IOException;
+
+  /**
+   * Returns a new {@link MediaPeriod} corresponding to the period at the specified {@code index}.
+   * This method may be called multiple times with the same index without an intervening call to
+   * {@link #releasePeriod(MediaPeriod)}.
+   *
+   * @param index The index of the period.
+   * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
+   * @param positionUs The player's current playback position.
+   * @return A new {@link MediaPeriod}.
+   */
+  MediaPeriod createPeriod(int index, Allocator allocator, long positionUs);
+
+  /**
+   * Releases the period.
+   *
+   * @param mediaPeriod The period to release.
+   */
+  void releasePeriod(MediaPeriod mediaPeriod);
+
+  /**
+   * Releases the source.
+   * <p>
+   * This method should be called when the source is no longer required. It may be called in any
+   * state.
+   */
+  void releaseSource();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/MergingMediaPeriod.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+
+/**
+ * Merges multiple {@link MediaPeriod}s.
+ */
+/* package */ final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
+
+  public final MediaPeriod[] periods;
+
+  private final IdentityHashMap<SampleStream, Integer> streamPeriodIndices;
+
+  private Callback callback;
+  private int pendingChildPrepareCount;
+  private TrackGroupArray trackGroups;
+
+  private MediaPeriod[] enabledPeriods;
+  private SequenceableLoader sequenceableLoader;
+
+  public MergingMediaPeriod(MediaPeriod... periods) {
+    this.periods = periods;
+    streamPeriodIndices = new IdentityHashMap<>();
+  }
+
+  @Override
+  public void prepare(Callback callback) {
+    this.callback = callback;
+    pendingChildPrepareCount = periods.length;
+    for (MediaPeriod period : periods) {
+      period.prepare(this);
+    }
+  }
+
+  @Override
+  public void maybeThrowPrepareError() throws IOException {
+    for (MediaPeriod period : periods) {
+      period.maybeThrowPrepareError();
+    }
+  }
+
+  @Override
+  public TrackGroupArray getTrackGroups() {
+    return trackGroups;
+  }
+
+  @Override
+  public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+      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.
+    SampleStream[] newStreams = new SampleStream[selections.length];
+    SampleStream[] childStreams = new SampleStream[selections.length];
+    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.
+          Assertions.checkState(childStreams[j] != null);
+          newStreams[j] = childStreams[j];
+          periodEnabled = true;
+          streamPeriodIndices.put(childStreams[j], 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);
+    sequenceableLoader = new CompositeSequenceableLoader(enabledPeriods);
+    return positionUs;
+  }
+
+  @Override
+  public boolean continueLoading(long positionUs) {
+    return sequenceableLoader.continueLoading(positionUs);
+  }
+
+  @Override
+  public long getNextLoadPositionUs() {
+    return sequenceableLoader.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("Children seeked to different positions");
+        }
+      }
+    }
+    return positionUs;
+  }
+
+  @Override
+  public long getBufferedPositionUs() {
+    long bufferedPositionUs = Long.MAX_VALUE;
+    for (MediaPeriod period : enabledPeriods) {
+      long rendererBufferedPositionUs = period.getBufferedPositionUs();
+      if (rendererBufferedPositionUs != C.TIME_END_OF_SOURCE) {
+        bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
+      }
+    }
+    return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs;
+  }
+
+  @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("Children seeked to different positions");
+      }
+    }
+    return positionUs;
+  }
+
+  // MediaPeriod.Callback implementation
+
+  @Override
+  public void onPrepared(MediaPeriod ignored) {
+    if (--pendingChildPrepareCount > 0) {
+      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);
+    callback.onPrepared(this);
+  }
+
+  @Override
+  public void onContinueLoadingRequested(MediaPeriod ignored) {
+    if (trackGroups == null) {
+      // Still preparing.
+      return;
+    }
+    callback.onContinueLoadingRequested(this);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/MergingMediaSource.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.upstream.Allocator;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Merges multiple {@link MediaSource}s.
+ * <p>
+ * The {@link Timeline}s of the sources being merged must have the same number of periods, and must
+ * not have any dynamic windows.
+ */
+public final class MergingMediaSource implements MediaSource {
+
+  /**
+   * Thrown when a {@link MergingMediaSource} cannot merge its sources.
+   */
+  public static final class IllegalMergeException extends IOException {
+
+    /**
+     * The reason the merge failed.
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({REASON_WINDOWS_ARE_DYNAMIC, REASON_PERIOD_COUNT_MISMATCH})
+    public @interface Reason {}
+    /**
+     * The merge failed because one of the sources being merged has a dynamic window.
+     */
+    public static final int REASON_WINDOWS_ARE_DYNAMIC = 0;
+    /**
+     * The merge failed because the sources have different period counts.
+     */
+    public static final int REASON_PERIOD_COUNT_MISMATCH = 1;
+
+    /**
+     * The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and
+     * {@link #REASON_PERIOD_COUNT_MISMATCH}.
+     */
+    @Reason
+    public final int reason;
+
+    /**
+     * @param reason The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and
+     *     {@link #REASON_PERIOD_COUNT_MISMATCH}.
+     */
+    public IllegalMergeException(@Reason int reason) {
+      this.reason = reason;
+    }
+
+  }
+
+  private static final int PERIOD_COUNT_UNSET = -1;
+
+  private final MediaSource[] mediaSources;
+  private final ArrayList<MediaSource> pendingTimelineSources;
+  private final Timeline.Window window;
+
+  private Listener listener;
+  private Timeline primaryTimeline;
+  private Object primaryManifest;
+  private int periodCount;
+  private IllegalMergeException mergeError;
+
+  /**
+   * @param mediaSources The {@link MediaSource}s to merge.
+   */
+  public MergingMediaSource(MediaSource... mediaSources) {
+    this.mediaSources = mediaSources;
+    pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources));
+    window = new Timeline.Window();
+    periodCount = PERIOD_COUNT_UNSET;
+  }
+
+  @Override
+  public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+    this.listener = listener;
+    for (int i = 0; i < mediaSources.length; i++) {
+      final int sourceIndex = i;
+      mediaSources[sourceIndex].prepareSource(player, false, new Listener() {
+        @Override
+        public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+          handleSourceInfoRefreshed(sourceIndex, timeline, manifest);
+        }
+      });
+    }
+  }
+
+  @Override
+  public void maybeThrowSourceInfoRefreshError() throws IOException {
+    if (mergeError != null) {
+      throw mergeError;
+    }
+    for (MediaSource mediaSource : mediaSources) {
+      mediaSource.maybeThrowSourceInfoRefreshError();
+    }
+  }
+
+  @Override
+  public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+    MediaPeriod[] periods = new MediaPeriod[mediaSources.length];
+    for (int i = 0; i < periods.length; i++) {
+      periods[i] = mediaSources[i].createPeriod(index, allocator, positionUs);
+    }
+    return new MergingMediaPeriod(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
+  public void releaseSource() {
+    for (MediaSource mediaSource : mediaSources) {
+      mediaSource.releaseSource();
+    }
+  }
+
+  private void handleSourceInfoRefreshed(int sourceIndex, Timeline timeline, Object manifest) {
+    if (mergeError == null) {
+      mergeError = checkTimelineMerges(timeline);
+    }
+    if (mergeError != null) {
+      return;
+    }
+    pendingTimelineSources.remove(mediaSources[sourceIndex]);
+    if (sourceIndex == 0) {
+      primaryTimeline = timeline;
+      primaryManifest = manifest;
+    }
+    if (pendingTimelineSources.isEmpty()) {
+      listener.onSourceInfoRefreshed(primaryTimeline, primaryManifest);
+    }
+  }
+
+  private IllegalMergeException checkTimelineMerges(Timeline timeline) {
+    int windowCount = timeline.getWindowCount();
+    for (int i = 0; i < windowCount; i++) {
+      if (timeline.getWindow(i, window, false).isDynamic) {
+        return new IllegalMergeException(IllegalMergeException.REASON_WINDOWS_ARE_DYNAMIC);
+      }
+    }
+    if (periodCount == PERIOD_COUNT_UNSET) {
+      periodCount = timeline.getPeriodCount();
+    } else if (timeline.getPeriodCount() != periodCount) {
+      return new IllegalMergeException(IllegalMergeException.REASON_PERIOD_COUNT_MISMATCH);
+    }
+    return null;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/SampleStream.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.FormatHolder;
+import 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)}. 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 no data is available then {@link C#RESULT_NOTHING_READ} is returned. If the format of the
+   * media is changing or if {@code buffer == null} 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. May be null if the
+   *     caller requires that the format of the stream be read even if it's not changing.
+   * @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);
+
+  /**
+   * Attempts to skip to the keyframe before the specified time.
+   *
+   * @param timeUs The specified time.
+   */
+  void skipToKeyframeBefore(long timeUs);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/SequenceableLoader.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+
+/**
+ * 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 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.
+   * @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);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Timeline;
+import 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 ID = new Object();
+
+  private final long periodDurationUs;
+  private final long windowDurationUs;
+  private final long windowPositionInPeriodUs;
+  private final long windowDefaultStartPositionUs;
+  private final boolean isSeekable;
+  private final boolean isDynamic;
+
+  /**
+   * Creates a timeline of one period of known duration, and a static window starting at zero and
+   * extending to that duration.
+   *
+   * @param durationUs The duration of the period, in microseconds.
+   * @param isSeekable Whether seeking is supported within the period.
+   */
+  public SinglePeriodTimeline(long durationUs, boolean isSeekable) {
+    this(durationUs, durationUs, 0, 0, isSeekable, false);
+  }
+
+  /**
+   * Creates a timeline with one period of known duration, 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.
+   */
+  public SinglePeriodTimeline(long periodDurationUs, long windowDurationUs,
+      long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable,
+      boolean isDynamic) {
+    this.periodDurationUs = periodDurationUs;
+    this.windowDurationUs = windowDurationUs;
+    this.windowPositionInPeriodUs = windowPositionInPeriodUs;
+    this.windowDefaultStartPositionUs = windowDefaultStartPositionUs;
+    this.isSeekable = isSeekable;
+    this.isDynamic = isDynamic;
+  }
+
+  @Override
+  public int getWindowCount() {
+    return 1;
+  }
+
+  @Override
+  public Window getWindow(int windowIndex, Window window, boolean setIds,
+      long defaultPositionProjectionUs) {
+    Assertions.checkIndex(windowIndex, 0, 1);
+    Object id = setIds ? ID : null;
+    long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs;
+    if (isDynamic) {
+      windowDefaultStartPositionUs += defaultPositionProjectionUs;
+      if (windowDefaultStartPositionUs > windowDurationUs) {
+        // The projection takes us beyond the end of the live window.
+        windowDefaultStartPositionUs = C.TIME_UNSET;
+      }
+    }
+    return window.set(id, C.TIME_UNSET, C.TIME_UNSET, isSeekable, isDynamic,
+        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 id = setIds ? ID : null;
+    return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs);
+  }
+
+  @Override
+  public int getIndexOfPeriod(Object uid) {
+    return ID.equals(uid) ? 0 : C.INDEX_UNSET;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.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 com.google.android.exoplayer2.source;
+
+import android.net.Uri;
+import android.os.Handler;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.source.SingleSampleMediaSource.EventListener;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.Loader;
+import com.google.android.exoplayer2.upstream.Loader.Loadable;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * 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 Uri uri;
+  private final DataSource.Factory dataSourceFactory;
+  private final int minLoadableRetryCount;
+  private final Handler eventHandler;
+  private final EventListener eventListener;
+  private final int eventSourceId;
+  private final TrackGroupArray tracks;
+  private final ArrayList<SampleStreamImpl> sampleStreams;
+  /* package */ final Loader loader;
+  /* package */ final Format format;
+
+  /* package */ boolean loadingFinished;
+  /* package */ byte[] sampleData;
+  /* package */ int sampleSize;
+
+  public SingleSampleMediaPeriod(Uri uri, DataSource.Factory dataSourceFactory, Format format,
+      int minLoadableRetryCount, Handler eventHandler, EventListener eventListener,
+      int eventSourceId) {
+    this.uri = uri;
+    this.dataSourceFactory = dataSourceFactory;
+    this.format = format;
+    this.minLoadableRetryCount = minLoadableRetryCount;
+    this.eventHandler = eventHandler;
+    this.eventListener = eventListener;
+    this.eventSourceId = eventSourceId;
+    tracks = new TrackGroupArray(new TrackGroup(format));
+    sampleStreams = new ArrayList<>();
+    loader = new Loader("Loader:SingleSampleMediaPeriod");
+  }
+
+  public void release() {
+    loader.release();
+  }
+
+  @Override
+  public void prepare(Callback callback) {
+    callback.onPrepared(this);
+  }
+
+  @Override
+  public void maybeThrowPrepareError() throws IOException {
+    loader.maybeThrowError();
+  }
+
+  @Override
+  public TrackGroupArray getTrackGroups() {
+    return tracks;
+  }
+
+  @Override
+  public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+      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 boolean continueLoading(long positionUs) {
+    if (loadingFinished || loader.isLoading()) {
+      return false;
+    }
+    loader.startLoading(new SourceLoadable(uri, dataSourceFactory.createDataSource()), this,
+        minLoadableRetryCount);
+    return true;
+  }
+
+  @Override
+  public long readDiscontinuity() {
+    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).seekToUs(positionUs);
+    }
+    return positionUs;
+  }
+
+  // Loader.Callback implementation.
+
+  @Override
+  public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs,
+      long loadDurationMs) {
+    sampleSize = loadable.sampleSize;
+    sampleData = loadable.sampleData;
+    loadingFinished = true;
+  }
+
+  @Override
+  public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs,
+      boolean released) {
+    // Do nothing.
+  }
+
+  @Override
+  public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs,
+      IOException error) {
+    notifyLoadError(error);
+    return Loader.RETRY;
+  }
+
+  // Internal methods.
+
+  private void notifyLoadError(final IOException e) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable() {
+        @Override
+        public void run() {
+          eventListener.onLoadError(eventSourceId, e);
+        }
+      });
+    }
+  }
+
+  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;
+
+    public void seekToUs(long positionUs) {
+      if (streamState == STREAM_STATE_END_OF_STREAM) {
+        streamState = STREAM_STATE_SEND_SAMPLE;
+      }
+    }
+
+    @Override
+    public boolean isReady() {
+      return loadingFinished;
+    }
+
+    @Override
+    public void maybeThrowError() throws IOException {
+      loader.maybeThrowError();
+    }
+
+    @Override
+    public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
+      if (buffer == null || streamState == STREAM_STATE_SEND_FORMAT) {
+        formatHolder.format = format;
+        streamState = STREAM_STATE_SEND_SAMPLE;
+        return C.RESULT_FORMAT_READ;
+      } else if (streamState == STREAM_STATE_END_OF_STREAM) {
+        buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
+        return C.RESULT_BUFFER_READ;
+      }
+
+      Assertions.checkState(streamState == STREAM_STATE_SEND_SAMPLE);
+      if (!loadingFinished) {
+        return C.RESULT_NOTHING_READ;
+      } else {
+        buffer.timeUs = 0;
+        buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME);
+        buffer.ensureSpaceForWrite(sampleSize);
+        buffer.data.put(sampleData, 0, sampleSize);
+        streamState = STREAM_STATE_END_OF_STREAM;
+        return C.RESULT_BUFFER_READ;
+      }
+    }
+
+    @Override
+    public void skipToKeyframeBefore(long timeUs) {
+      // Do nothing.
+    }
+
+  }
+
+  /* package */ static final class SourceLoadable implements Loadable {
+
+    private final Uri uri;
+    private final DataSource dataSource;
+
+    private int sampleSize;
+    private byte[] sampleData;
+
+    public SourceLoadable(Uri uri, DataSource dataSource) {
+      this.uri = uri;
+      this.dataSource = dataSource;
+    }
+
+    @Override
+    public void cancelLoad() {
+      // Never happens.
+    }
+
+    @Override
+    public boolean isLoadCanceled() {
+      return false;
+    }
+
+    @Override
+    public void load() throws IOException, InterruptedException {
+      // We always load from the beginning, so reset the sampleSize to 0.
+      sampleSize = 0;
+      try {
+        // Create and open the input.
+        dataSource.open(new DataSpec(uri));
+        // Load the sample data.
+        int result = 0;
+        while (result != C.RESULT_END_OF_INPUT) {
+          sampleSize += result;
+          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);
+      }
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/SingleSampleMediaSource.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 com.google.android.exoplayer2.source;
+
+import android.net.Uri;
+import android.os.Handler;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * Loads data at a given {@link Uri} as a single sample belonging to a single {@link MediaPeriod}.
+ */
+public final class SingleSampleMediaSource implements MediaSource {
+
+  /**
+   * Listener of {@link SingleSampleMediaSource} events.
+   */
+  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);
+
+  }
+
+  /**
+   * The default minimum number of times to retry loading data prior to failing.
+   */
+  public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
+
+  private final Uri uri;
+  private final DataSource.Factory dataSourceFactory;
+  private final Format format;
+  private final int minLoadableRetryCount;
+  private final Handler eventHandler;
+  private final EventListener eventListener;
+  private final int eventSourceId;
+  private final Timeline timeline;
+
+  public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format,
+      long durationUs) {
+    this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT);
+  }
+
+  public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format,
+      long durationUs, int minLoadableRetryCount) {
+    this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0);
+  }
+
+  public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format,
+      long durationUs, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener,
+      int eventSourceId) {
+    this.uri = uri;
+    this.dataSourceFactory = dataSourceFactory;
+    this.format = format;
+    this.minLoadableRetryCount = minLoadableRetryCount;
+    this.eventHandler = eventHandler;
+    this.eventListener = eventListener;
+    this.eventSourceId = eventSourceId;
+    timeline = new SinglePeriodTimeline(durationUs, true);
+  }
+
+  // MediaSource implementation.
+
+  @Override
+  public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+    listener.onSourceInfoRefreshed(timeline, null);
+  }
+
+  @Override
+  public void maybeThrowSourceInfoRefreshError() throws IOException {
+    // Do nothing.
+  }
+
+  @Override
+  public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+    Assertions.checkArgument(index == 0);
+    return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount,
+        eventHandler, eventListener, eventSourceId);
+  }
+
+  @Override
+  public void releasePeriod(MediaPeriod mediaPeriod) {
+    ((SingleSampleMediaPeriod) mediaPeriod).release();
+  }
+
+  @Override
+  public void releaseSource() {
+    // Do nothing.
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/TrackGroup.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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import 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 {
+
+  /**
+   * 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 or contain null elements.
+   */
+  public TrackGroup(Format... formats) {
+    Assertions.checkState(formats.length > 0);
+    this.formats = formats;
+    this.length = formats.length;
+  }
+
+  /**
+   * 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.
+   *
+   * @param format The format.
+   * @return The index of the track, or {@link C#INDEX_UNSET} if no such track exists.
+   */
+  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(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);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/TrackGroupArray.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 com.google.android.exoplayer2.source;
+
+import com.google.android.exoplayer2.C;
+import java.util.Arrays;
+
+/**
+ * An array of {@link TrackGroup}s exposed by a {@link MediaPeriod}.
+ */
+public final class TrackGroupArray {
+
+  /**
+   * 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;
+  }
+
+  /**
+   * 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.
+   */
+  public int indexOf(TrackGroup group) {
+    for (int i = 0; i < length; i++) {
+      if (trackGroups[i] == group) {
+        return i;
+      }
+    }
+    return C.INDEX_UNSET;
+  }
+
+  @Override
+  public int hashCode() {
+    if (hashCode == 0) {
+      hashCode = Arrays.hashCode(trackGroups);
+    }
+    return hashCode;
+  }
+
+  @Override
+  public boolean equals(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);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.source;
+
+import android.net.Uri;
+import 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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+
+/**
+ * A base implementation of {@link MediaChunk}, for chunks that contain a single track.
+ * <p>
+ * Loaded samples are output to a {@link DefaultTrackOutput}.
+ */
+public abstract class BaseMediaChunk extends MediaChunk {
+
+  private DefaultTrackOutput trackOutput;
+  private int firstSampleIndex;
+
+  /**
+   * @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.
+   */
+  public BaseMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
+      int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
+      int chunkIndex) {
+    super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs,
+        endTimeUs, chunkIndex);
+  }
+
+  /**
+   * Initializes the chunk for loading, setting the {@link DefaultTrackOutput} that will receive
+   * samples as they are loaded.
+   *
+   * @param trackOutput The output that will receive the loaded samples.
+   */
+  public void init(DefaultTrackOutput trackOutput) {
+    this.trackOutput = trackOutput;
+    this.firstSampleIndex = trackOutput.getWriteIndex();
+  }
+
+  /**
+   * Returns the index of the first sample in the output that was passed to
+   * {@link #init(DefaultTrackOutput)} that will originate from this chunk.
+   */
+  public final int getFirstSampleIndex() {
+    return firstSampleIndex;
+  }
+
+  /**
+   * Returns the track output most recently passed to {@link #init(DefaultTrackOutput)}.
+   */
+  protected final DefaultTrackOutput getTrackOutput() {
+    return trackOutput;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/Chunk.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 com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.Loader.Loadable;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * 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.
+   */
+  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 DataSource 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, Object trackSelectionData, long startTimeUs, long endTimeUs) {
+    this.dataSource = Assertions.checkNotNull(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.
+   */
+  public abstract long bytesLoaded();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.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 com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.io.IOException;
+
+/**
+ * An {@link Extractor} wrapper for loading chunks containing a single track.
+ * <p>
+ * The wrapper allows switching of the {@link SeekMapOutput} and {@link TrackOutput} that receive
+ * parsed data.
+ */
+public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput {
+
+  /**
+   * Receives {@link SeekMap}s extracted by the wrapped {@link Extractor}.
+   */
+  public interface SeekMapOutput {
+
+    /**
+     * @see ExtractorOutput#seekMap(SeekMap)
+     */
+    void seekMap(SeekMap seekMap);
+
+  }
+
+  public final Extractor extractor;
+
+  private final Format manifestFormat;
+  private final boolean preferManifestDrmInitData;
+  private final boolean resendFormatOnInit;
+
+  private boolean extractorInitialized;
+  private SeekMapOutput seekMapOutput;
+  private TrackOutput trackOutput;
+  private Format sentFormat;
+
+  // Accessed only on the loader thread.
+  private boolean seenTrack;
+  private int seenTrackId;
+
+  /**
+   * @param extractor The extractor to wrap.
+   * @param manifestFormat A manifest defined {@link Format} whose data should be merged into any
+   *     sample {@link Format} output from the {@link Extractor}.
+   * @param preferManifestDrmInitData Whether {@link DrmInitData} defined in {@code manifestFormat}
+   *     should be preferred when the sample and manifest {@link Format}s are merged.
+   * @param resendFormatOnInit Whether the extractor should resend the previous {@link Format} when
+   *     it is initialized via {@link #init(SeekMapOutput, TrackOutput)}.
+   */
+  public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat,
+      boolean preferManifestDrmInitData, boolean resendFormatOnInit) {
+    this.extractor = extractor;
+    this.manifestFormat = manifestFormat;
+    this.preferManifestDrmInitData = preferManifestDrmInitData;
+    this.resendFormatOnInit = resendFormatOnInit;
+  }
+
+  /**
+   * Initializes the extractor to output to the provided {@link SeekMapOutput} and
+   * {@link TrackOutput} instances, and configures it to receive data from a new chunk.
+   *
+   * @param seekMapOutput The {@link SeekMapOutput} that will receive extracted {@link SeekMap}s.
+   * @param trackOutput The {@link TrackOutput} that will receive sample data.
+   */
+  public void init(SeekMapOutput seekMapOutput, TrackOutput trackOutput) {
+    this.seekMapOutput = seekMapOutput;
+    this.trackOutput = trackOutput;
+    if (!extractorInitialized) {
+      extractor.init(this);
+      extractorInitialized = true;
+    } else {
+      extractor.seek(0, 0);
+      if (resendFormatOnInit && sentFormat != null) {
+        trackOutput.format(sentFormat);
+      }
+    }
+  }
+
+  // ExtractorOutput implementation.
+
+  @Override
+  public TrackOutput track(int id) {
+    Assertions.checkState(!seenTrack || seenTrackId == id);
+    seenTrack = true;
+    seenTrackId = id;
+    return this;
+  }
+
+  @Override
+  public void endTracks() {
+    Assertions.checkState(seenTrack);
+  }
+
+  @Override
+  public void seekMap(SeekMap seekMap) {
+    seekMapOutput.seekMap(seekMap);
+  }
+
+  // TrackOutput implementation.
+
+  @Override
+  public void format(Format format) {
+    sentFormat = format.copyWithManifestFormatInfo(manifestFormat, preferManifestDrmInitData);
+    trackOutput.format(sentFormat);
+  }
+
+  @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,
+      byte[] encryptionKey) {
+    trackOutput.sampleMetadata(timeUs, flags, size, offset, encryptionKey);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.source.chunk;
+
+/**
+ * Holds a chunk or an indication that the end of the stream has been reached.
+ */
+public final class ChunkHolder {
+
+  /**
+   * The chunk.
+   */
+  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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.source.SequenceableLoader;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.Loader;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A {@link SampleStream} that loads media in {@link Chunk}s, obtained from a {@link ChunkSource}.
+ */
+public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, SequenceableLoader,
+    Loader.Callback<Chunk> {
+
+  private final int trackType;
+  private final T chunkSource;
+  private final SequenceableLoader.Callback<ChunkSampleStream<T>> callback;
+  private final EventDispatcher eventDispatcher;
+  private final int minLoadableRetryCount;
+  private final LinkedList<BaseMediaChunk> mediaChunks;
+  private final List<BaseMediaChunk> readOnlyMediaChunks;
+  private final DefaultTrackOutput sampleQueue;
+  private final ChunkHolder nextChunkHolder;
+  private final Loader loader;
+
+  private Format downstreamTrackFormat;
+
+  private long lastSeekPositionUs;
+  private long pendingResetPositionUs;
+
+  private boolean loadingFinished;
+
+  /**
+   * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants.
+   * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained.
+   * @param callback An {@link Callback} for the stream.
+   * @param allocator An {@link Allocator} from which allocations can be obtained.
+   * @param positionUs The position from which to start loading media.
+   * @param minLoadableRetryCount The minimum number of times that the source should retry a load
+   *     before propagating an error.
+   * @param eventDispatcher A dispatcher to notify of events.
+   */
+  public ChunkSampleStream(int trackType, T chunkSource,
+      SequenceableLoader.Callback<ChunkSampleStream<T>> callback, Allocator allocator,
+      long positionUs, int minLoadableRetryCount, EventDispatcher eventDispatcher) {
+    this.trackType = trackType;
+    this.chunkSource = chunkSource;
+    this.callback = callback;
+    this.eventDispatcher = eventDispatcher;
+    this.minLoadableRetryCount = minLoadableRetryCount;
+    loader = new Loader("Loader:ChunkSampleStream");
+    nextChunkHolder = new ChunkHolder();
+    mediaChunks = new LinkedList<>();
+    readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
+    sampleQueue = new DefaultTrackOutput(allocator);
+    lastSeekPositionUs = positionUs;
+    pendingResetPositionUs = positionUs;
+  }
+
+  /**
+   * Returns the {@link ChunkSource} used by this stream.
+   *
+   * @return The {@link ChunkSource}.
+   */
+  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.
+   */
+  public long getBufferedPositionUs() {
+    if (loadingFinished) {
+      return C.TIME_END_OF_SOURCE;
+    } else if (isPendingReset()) {
+      return pendingResetPositionUs;
+    } else {
+      long bufferedPositionUs = lastSeekPositionUs;
+      BaseMediaChunk lastMediaChunk = mediaChunks.getLast();
+      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, sampleQueue.getLargestQueuedTimestampUs());
+    }
+  }
+
+  /**
+   * Seeks to the specified position in microseconds.
+   *
+   * @param positionUs The seek position in microseconds.
+   */
+  public void seekToUs(long positionUs) {
+    lastSeekPositionUs = positionUs;
+    // If we're not pending a reset, see if we can seek within the sample queue.
+    boolean seekInsideBuffer = !isPendingReset()
+        && sampleQueue.skipToKeyframeBefore(positionUs, positionUs < getNextLoadPositionUs());
+    if (seekInsideBuffer) {
+      // We succeeded. All we need to do is discard any chunks that we've moved past.
+      while (mediaChunks.size() > 1
+          && mediaChunks.get(1).getFirstSampleIndex() <= sampleQueue.getReadIndex()) {
+        mediaChunks.removeFirst();
+      }
+    } else {
+      // We failed, and need to restart.
+      pendingResetPositionUs = positionUs;
+      loadingFinished = false;
+      mediaChunks.clear();
+      if (loader.isLoading()) {
+        loader.cancelLoading();
+      } else {
+        sampleQueue.reset(true);
+      }
+    }
+  }
+
+  /**
+   * Releases the stream.
+   * <p>
+   * This method should be called when the stream is no longer required.
+   */
+  public void release() {
+    sampleQueue.disable();
+    loader.release();
+  }
+
+  // SampleStream implementation.
+
+  @Override
+  public boolean isReady() {
+    return loadingFinished || (!isPendingReset() && !sampleQueue.isEmpty());
+  }
+
+  @Override
+  public void maybeThrowError() throws IOException {
+    loader.maybeThrowError();
+    if (!loader.isLoading()) {
+      chunkSource.maybeThrowError();
+    }
+  }
+
+  @Override
+  public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
+    if (isPendingReset()) {
+      return C.RESULT_NOTHING_READ;
+    }
+
+    while (mediaChunks.size() > 1
+        && mediaChunks.get(1).getFirstSampleIndex() <= sampleQueue.getReadIndex()) {
+      mediaChunks.removeFirst();
+    }
+    BaseMediaChunk currentChunk = mediaChunks.getFirst();
+
+    Format trackFormat = currentChunk.trackFormat;
+    if (!trackFormat.equals(downstreamTrackFormat)) {
+      eventDispatcher.downstreamFormatChanged(trackType, trackFormat,
+          currentChunk.trackSelectionReason, currentChunk.trackSelectionData,
+          currentChunk.startTimeUs);
+    }
+    downstreamTrackFormat = trackFormat;
+    return sampleQueue.readData(formatHolder, buffer, loadingFinished, lastSeekPositionUs);
+  }
+
+  @Override
+  public void skipToKeyframeBefore(long timeUs) {
+    sampleQueue.skipToKeyframeBefore(timeUs);
+  }
+
+  // Loader.Callback implementation.
+
+  @Override
+  public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) {
+    chunkSource.onChunkLoadCompleted(loadable);
+    eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat,
+        loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs,
+        loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded());
+    callback.onContinueLoadingRequested(this);
+  }
+
+  @Override
+  public void onLoadCanceled(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs,
+      boolean released) {
+    eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat,
+        loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs,
+        loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded());
+    if (!released) {
+      sampleQueue.reset(true);
+      callback.onContinueLoadingRequested(this);
+    }
+  }
+
+  @Override
+  public int onLoadError(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs,
+      IOException error) {
+    long bytesLoaded = loadable.bytesLoaded();
+    boolean isMediaChunk = isMediaChunk(loadable);
+    boolean cancelable = !isMediaChunk || bytesLoaded == 0 || mediaChunks.size() > 1;
+    boolean canceled = false;
+    if (chunkSource.onChunkLoadError(loadable, cancelable, error)) {
+      canceled = true;
+      if (isMediaChunk) {
+        BaseMediaChunk removed = mediaChunks.removeLast();
+        Assertions.checkState(removed == loadable);
+        sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex());
+        if (mediaChunks.isEmpty()) {
+          pendingResetPositionUs = lastSeekPositionUs;
+        }
+      }
+    }
+    eventDispatcher.loadError(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat,
+        loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs,
+        loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, bytesLoaded, error,
+        canceled);
+    if (canceled) {
+      callback.onContinueLoadingRequested(this);
+      return Loader.DONT_RETRY;
+    } else {
+      return Loader.RETRY;
+    }
+  }
+
+  // SequenceableLoader implementation
+
+  @Override
+  public boolean continueLoading(long positionUs) {
+    if (loadingFinished || loader.isLoading()) {
+      return false;
+    }
+
+    chunkSource.getNextChunk(mediaChunks.isEmpty() ? null : mediaChunks.getLast(),
+        pendingResetPositionUs != C.TIME_UNSET ? pendingResetPositionUs : positionUs,
+        nextChunkHolder);
+    boolean endOfStream = nextChunkHolder.endOfStream;
+    Chunk loadable = nextChunkHolder.chunk;
+    nextChunkHolder.clear();
+
+    if (endOfStream) {
+      loadingFinished = true;
+      return true;
+    }
+
+    if (loadable == null) {
+      return false;
+    }
+
+    if (isMediaChunk(loadable)) {
+      pendingResetPositionUs = C.TIME_UNSET;
+      BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable;
+      mediaChunk.init(sampleQueue);
+      mediaChunks.add(mediaChunk);
+    }
+    long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount);
+    eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat,
+        loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs,
+        loadable.endTimeUs, elapsedRealtimeMs);
+    return true;
+  }
+
+  @Override
+  public long getNextLoadPositionUs() {
+    if (isPendingReset()) {
+      return pendingResetPositionUs;
+    } else {
+      return loadingFinished ? C.TIME_END_OF_SOURCE : mediaChunks.getLast().endTimeUs;
+    }
+  }
+
+  // Internal methods
+
+  // TODO[REFACTOR]: Call maybeDiscardUpstream for DASH and SmoothStreaming.
+  /**
+   * Discards media chunks from the back of the buffer if conditions have changed such that it's
+   * preferable to re-buffer the media at a different quality.
+   *
+   * @param positionUs The current playback position in microseconds.
+   */
+  private void maybeDiscardUpstream(long positionUs) {
+    int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks);
+    discardUpstreamMediaChunks(Math.max(1, queueSize));
+  }
+
+  private boolean isMediaChunk(Chunk chunk) {
+    return chunk instanceof BaseMediaChunk;
+  }
+
+  private boolean isPendingReset() {
+    return pendingResetPositionUs != C.TIME_UNSET;
+  }
+
+  /**
+   * Discard upstream media chunks until the queue length is equal to the length specified.
+   *
+   * @param queueLength The desired length of the queue.
+   * @return Whether chunks were discarded.
+   */
+  private boolean discardUpstreamMediaChunks(int queueLength) {
+    if (mediaChunks.size() <= queueLength) {
+      return false;
+    }
+    long startTimeUs = 0;
+    long endTimeUs = mediaChunks.getLast().endTimeUs;
+
+    BaseMediaChunk removed = null;
+    while (mediaChunks.size() > queueLength) {
+      removed = mediaChunks.removeLast();
+      startTimeUs = removed.startTimeUs;
+      loadingFinished = false;
+    }
+    sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex());
+    eventDispatcher.upstreamDiscarded(trackType, startTimeUs, endTimeUs);
+    return true;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkSource.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 com.google.android.exoplayer2.source.chunk;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * A provider of {@link Chunk}s for a {@link ChunkSampleStream} to load.
+ */
+public interface ChunkSource {
+
+  /**
+   * 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 previous The most recently loaded media chunk.
+   * @param playbackPositionUs The current playback position. If {@code previous} is null then this
+   *     parameter is the position from which playback is expected to start (or restart) and hence
+   *     should be interpreted as a seek position.
+   * @param out A holder to populate.
+   */
+  void getNextChunk(MediaChunk previous, long playbackPositionUs, 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.
+   * @return Whether the load should be canceled.
+   */
+  boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/ChunkedTrackBlacklistUtil.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 com.google.android.exoplayer2.source.chunk;
+
+import android.util.Log;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
+
+/**
+ * Helper class for blacklisting tracks in a {@link TrackSelection} when 404 (Not Found) and 410
+ * (Gone) HTTP response codes are encountered.
+ */
+public final class ChunkedTrackBlacklistUtil {
+
+  /**
+   * The default duration for which a track is blacklisted in milliseconds.
+   */
+  public static final long DEFAULT_TRACK_BLACKLIST_MS = 60000;
+
+  private static final String TAG = "ChunkedTrackBlacklist";
+
+  /**
+   * Blacklists {@code trackSelectionIndex} in {@code trackSelection} for
+   * {@link #DEFAULT_TRACK_BLACKLIST_MS} if {@code e} is an {@link InvalidResponseCodeException}
+   * with {@link InvalidResponseCodeException#responseCode} equal to 404 or 410. Else does nothing.
+   * Note that blacklisting will fail if the track is the only non-blacklisted track in the
+   * selection.
+   *
+   * @param trackSelection The track selection.
+   * @param trackSelectionIndex The index in the selection to consider blacklisting.
+   * @param e The error to inspect.
+   * @return Whether the track was blacklisted in the selection.
+   */
+  public static boolean maybeBlacklistTrack(TrackSelection trackSelection, int trackSelectionIndex,
+      Exception e) {
+    return maybeBlacklistTrack(trackSelection, trackSelectionIndex, e, DEFAULT_TRACK_BLACKLIST_MS);
+  }
+
+  /**
+   * Blacklists {@code trackSelectionIndex} in {@code trackSelection} for
+   * {@code blacklistDurationMs} if calling {@link #shouldBlacklist(Exception)} for {@code e}
+   * returns true. Else does nothing. Note that blacklisting will fail if the track is the only
+   * non-blacklisted track in the selection.
+   *
+   * @param trackSelection The track selection.
+   * @param trackSelectionIndex The index in the selection to consider blacklisting.
+   * @param e The error to inspect.
+   * @param blacklistDurationMs The duration to blacklist the track for, if it is blacklisted.
+   * @return Whether the track was blacklisted.
+   */
+  public static boolean maybeBlacklistTrack(TrackSelection trackSelection, int trackSelectionIndex,
+      Exception e, long blacklistDurationMs) {
+    if (shouldBlacklist(e)) {
+      boolean blacklisted = trackSelection.blacklist(trackSelectionIndex, blacklistDurationMs);
+      int responseCode = ((InvalidResponseCodeException) e).responseCode;
+      if (blacklisted) {
+        Log.w(TAG, "Blacklisted: duration=" + blacklistDurationMs + ", responseCode="
+            + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex));
+      } else {
+        Log.w(TAG, "Blacklisting failed (cannot blacklist last enabled track): responseCode="
+            + responseCode + ", format=" + trackSelection.getFormat(trackSelectionIndex));
+      }
+      return blacklisted;
+    }
+    return false;
+  }
+
+  /**
+   * Returns whether a loading error is an {@link InvalidResponseCodeException} with
+   * {@link InvalidResponseCodeException#responseCode} equal to 404 or 410.
+   *
+   * @param e The loading error.
+   * @return Wheter the loading error is an {@link InvalidResponseCodeException} with
+   *     {@link InvalidResponseCodeException#responseCode} equal to 404 or 410.
+   */
+  public static boolean shouldBlacklist(Exception e) {
+    if (e instanceof InvalidResponseCodeException) {
+      int responseCode = ((InvalidResponseCodeException) e).responseCode;
+      return responseCode == 404 || responseCode == 410;
+    }
+    return false;
+  }
+
+  private ChunkedTrackBlacklistUtil() {}
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
+import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SeekMapOutput;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.Assertions;
+import 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 implements SeekMapOutput {
+
+  private final int chunkCount;
+  private final long sampleOffsetUs;
+  private final ChunkExtractorWrapper extractorWrapper;
+  private final Format sampleFormat;
+
+  private volatile int bytesLoaded;
+  private volatile boolean loadCanceled;
+  private volatile 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.
+   * @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.
+   * @param sampleFormat The {@link Format} of the samples in the chunk, if known. May be null if
+   *     the data is known to define its own sample format.
+   */
+  public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
+      int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
+      int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper,
+      Format sampleFormat) {
+    super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs,
+        endTimeUs, chunkIndex);
+    this.chunkCount = chunkCount;
+    this.sampleOffsetUs = sampleOffsetUs;
+    this.extractorWrapper = extractorWrapper;
+    this.sampleFormat = sampleFormat;
+  }
+
+  @Override
+  public int getNextChunkIndex() {
+    return chunkIndex + chunkCount;
+  }
+
+  @Override
+  public boolean isLoadCompleted() {
+    return loadCompleted;
+  }
+
+  @Override
+  public final long bytesLoaded() {
+    return bytesLoaded;
+  }
+
+  // SeekMapOutput implementation.
+
+  @Override
+  public final void seekMap(SeekMap seekMap) {
+    // Do nothing.
+  }
+
+  // Loadable implementation.
+
+  @Override
+  public final void cancelLoad() {
+    loadCanceled = true;
+  }
+
+  @Override
+  public final boolean isLoadCanceled() {
+    return loadCanceled;
+  }
+
+  @SuppressWarnings("NonAtomicVolatileUpdate")
+  @Override
+  public final void load() throws IOException, InterruptedException {
+    DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
+    try {
+      // Create and open the input.
+      ExtractorInput input = new DefaultExtractorInput(dataSource,
+          loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
+      if (bytesLoaded == 0) {
+        // Set the target to ourselves.
+        DefaultTrackOutput trackOutput = getTrackOutput();
+        trackOutput.formatWithOffset(sampleFormat, sampleOffsetUs);
+        extractorWrapper.init(this, trackOutput);
+      }
+      // Load and decode the sample data.
+      try {
+        Extractor extractor = extractorWrapper.extractor;
+        int result = Extractor.RESULT_CONTINUE;
+        while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+          result = extractor.read(input, null);
+        }
+        Assertions.checkState(result != Extractor.RESULT_SEEK);
+      } finally {
+        bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
+      }
+    } finally {
+      Util.closeQuietly(dataSource);
+    }
+    loadCompleted = true;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/DataChunk.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import 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 int limit;
+
+  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, 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;
+  }
+
+  @Override
+  public long bytesLoaded() {
+    return limit;
+  }
+
+  // Loadable implementation
+
+  @Override
+  public final void cancelLoad() {
+    loadCanceled = true;
+  }
+
+  @Override
+  public final boolean isLoadCanceled() {
+    return loadCanceled;
+  }
+
+  @Override
+  public final void load() throws IOException, InterruptedException {
+    try {
+      dataSource.open(dataSpec);
+      limit = 0;
+      int bytesRead = 0;
+      while (bytesRead != C.RESULT_END_OF_INPUT && !loadCanceled) {
+        maybeExpandData();
+        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() {
+    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);
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/InitializationChunk.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SeekMapOutput;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * A {@link Chunk} that uses an {@link Extractor} to decode initialization data for single track.
+ */
+public final class InitializationChunk extends Chunk implements SeekMapOutput,
+    TrackOutput {
+
+  private final ChunkExtractorWrapper extractorWrapper;
+
+  // Initialization results. Set by the loader thread and read by any thread that knows loading
+  // has completed. These variables do not need to be volatile, since a memory barrier must occur
+  // for the reading thread to know that loading has completed.
+  private Format sampleFormat;
+  private SeekMap seekMap;
+
+  private volatile int bytesLoaded;
+  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, Object trackSelectionData,
+      ChunkExtractorWrapper extractorWrapper) {
+    super(dataSource, dataSpec, C.DATA_TYPE_MEDIA_INITIALIZATION, trackFormat, trackSelectionReason,
+        trackSelectionData, C.TIME_UNSET, C.TIME_UNSET);
+    this.extractorWrapper = extractorWrapper;
+  }
+
+  @Override
+  public long bytesLoaded() {
+    return bytesLoaded;
+  }
+
+  /**
+   * Returns a {@link Format} parsed from the chunk, or null.
+   * <p>
+   * Should be called after loading has completed.
+   */
+  public Format getSampleFormat() {
+    return sampleFormat;
+  }
+
+  /**
+   * Returns a {@link SeekMap} parsed from the chunk, or null.
+   * <p>
+   * Should be called after loading has completed.
+   */
+  public SeekMap getSeekMap() {
+    return seekMap;
+  }
+
+  // SeekMapOutput implementation.
+
+  @Override
+  public void seekMap(SeekMap seekMap) {
+    this.seekMap = seekMap;
+  }
+
+  // TrackOutput implementation.
+
+  @Override
+  public void format(Format format) {
+    this.sampleFormat = format;
+  }
+
+  @Override
+  public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput)
+      throws IOException, InterruptedException {
+    throw new IllegalStateException("Unexpected sample data in initialization chunk");
+  }
+
+  @Override
+  public void sampleData(ParsableByteArray data, int length) {
+    throw new IllegalStateException("Unexpected sample data in initialization chunk");
+  }
+
+  @Override
+  public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
+      byte[] encryptionKey) {
+    throw new IllegalStateException("Unexpected sample data in initialization chunk");
+  }
+
+  // Loadable implementation.
+
+  @Override
+  public void cancelLoad() {
+    loadCanceled = true;
+  }
+
+  @Override
+  public boolean isLoadCanceled() {
+    return loadCanceled;
+  }
+
+  @SuppressWarnings("NonAtomicVolatileUpdate")
+  @Override
+  public void load() throws IOException, InterruptedException {
+    DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
+    try {
+      // Create and open the input.
+      ExtractorInput input = new DefaultExtractorInput(dataSource,
+          loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
+      if (bytesLoaded == 0) {
+        // Set the target to ourselves.
+        extractorWrapper.init(this, this);
+      }
+      // Load and decode the initialization data.
+      try {
+        Extractor extractor = extractorWrapper.extractor;
+        int result = Extractor.RESULT_CONTINUE;
+        while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+          result = extractor.read(input, null);
+        }
+        Assertions.checkState(result != Extractor.RESULT_SEEK);
+      } finally {
+        bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
+      }
+    } finally {
+      Util.closeQuietly(dataSource);
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/MediaChunk.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 com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import 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.
+   */
+  public final int 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.
+   */
+  public MediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
+      int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
+      int chunkIndex) {
+    super(dataSource, dataSpec, C.DATA_TYPE_MEDIA, trackFormat, trackSelectionReason,
+        trackSelectionData, startTimeUs, endTimeUs);
+    Assertions.checkNotNull(trackFormat);
+    this.chunkIndex = chunkIndex;
+  }
+
+  /**
+   * Returns the next chunk index.
+   */
+  public int getNextChunkIndex() {
+    return chunkIndex + 1;
+  }
+
+  /**
+   * Returns whether the chunk has been fully loaded.
+   */
+  public abstract boolean isLoadCompleted();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.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 com.google.android.exoplayer2.source.chunk;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
+import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import 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 Format sampleFormat;
+
+  private volatile int bytesLoaded;
+  private volatile boolean loadCanceled;
+  private volatile 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.
+   */
+  public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
+      int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
+      int chunkIndex, Format sampleFormat) {
+    super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs,
+        endTimeUs, chunkIndex);
+    this.sampleFormat = sampleFormat;
+  }
+
+  @Override
+  public boolean isLoadCompleted() {
+    return loadCompleted;
+  }
+
+  @Override
+  public long bytesLoaded() {
+    return bytesLoaded;
+  }
+
+  // Loadable implementation.
+
+  @Override
+  public void cancelLoad() {
+    loadCanceled = true;
+  }
+
+  @Override
+  public boolean isLoadCanceled() {
+    return loadCanceled;
+  }
+
+  @SuppressWarnings("NonAtomicVolatileUpdate")
+  @Override
+  public void load() throws IOException, InterruptedException {
+    DataSpec loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
+    try {
+      // Create and open the input.
+      long length = dataSource.open(loadDataSpec);
+      if (length != C.LENGTH_UNSET) {
+        length += bytesLoaded;
+      }
+      ExtractorInput extractorInput = new DefaultExtractorInput(dataSource, bytesLoaded, length);
+      DefaultTrackOutput trackOutput = getTrackOutput();
+      trackOutput.formatWithOffset(sampleFormat, 0);
+      // Load the sample data.
+      int result = 0;
+      while (result != C.RESULT_END_OF_INPUT) {
+        bytesLoaded += result;
+        result = trackOutput.sampleData(extractorInput, Integer.MAX_VALUE, true);
+      }
+      int sampleSize = bytesLoaded;
+      trackOutput.sampleMetadata(startTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
+    } finally {
+      Util.closeQuietly(dataSource);
+    }
+    loadCompleted = true;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/DashChunkSource.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 com.google.android.exoplayer2.source.dash;
+
+import com.google.android.exoplayer2.source.chunk.ChunkSource;
+import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
+
+/**
+ * An {@link ChunkSource} for DASH streams.
+ */
+public interface DashChunkSource extends ChunkSource {
+
+  interface Factory {
+
+    DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
+        DashManifest manifest, int periodIndex, int adaptationSetIndex,
+        TrackSelection trackSelection, long elapsedRealtimeOffsetMs);
+
+  }
+
+  void updateManifest(DashManifest newManifest, int periodIndex);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.dash;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.CompositeSequenceableLoader;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.source.SequenceableLoader;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.source.chunk.ChunkSampleStream;
+import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
+import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
+import com.google.android.exoplayer2.source.dash.manifest.Period;
+import com.google.android.exoplayer2.source.dash.manifest.Representation;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A DASH {@link MediaPeriod}.
+ */
+/* package */ final class DashMediaPeriod implements MediaPeriod,
+    SequenceableLoader.Callback<ChunkSampleStream<DashChunkSource>> {
+
+  /* package */ final int id;
+  private final DashChunkSource.Factory chunkSourceFactory;
+  private final int minLoadableRetryCount;
+  private final EventDispatcher eventDispatcher;
+  private final long elapsedRealtimeOffset;
+  private final LoaderErrorThrower manifestLoaderErrorThrower;
+  private final Allocator allocator;
+  private final TrackGroupArray trackGroups;
+
+  private Callback callback;
+  private ChunkSampleStream<DashChunkSource>[] sampleStreams;
+  private CompositeSequenceableLoader sequenceableLoader;
+  private DashManifest manifest;
+  private int index;
+  private Period period;
+
+  public DashMediaPeriod(int id, DashManifest manifest, int index,
+      DashChunkSource.Factory chunkSourceFactory,  int minLoadableRetryCount,
+      EventDispatcher eventDispatcher, long elapsedRealtimeOffset,
+      LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator) {
+    this.id = id;
+    this.manifest = manifest;
+    this.index = index;
+    this.chunkSourceFactory = chunkSourceFactory;
+    this.minLoadableRetryCount = minLoadableRetryCount;
+    this.eventDispatcher = eventDispatcher;
+    this.elapsedRealtimeOffset = elapsedRealtimeOffset;
+    this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
+    this.allocator = allocator;
+    sampleStreams = newSampleStreamArray(0);
+    sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
+    period = manifest.getPeriod(index);
+    trackGroups = buildTrackGroups(period);
+  }
+
+  public void updateManifest(DashManifest manifest, int index) {
+    this.manifest = manifest;
+    this.index = index;
+    period = manifest.getPeriod(index);
+    if (sampleStreams != null) {
+      for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
+        sampleStream.getChunkSource().updateManifest(manifest, index);
+      }
+      callback.onContinueLoadingRequested(this);
+    }
+  }
+
+  public void release() {
+    for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
+      sampleStream.release();
+    }
+  }
+
+  @Override
+  public void prepare(Callback callback) {
+    this.callback = callback;
+    callback.onPrepared(this);
+  }
+
+  @Override
+  public void maybeThrowPrepareError() throws IOException {
+    manifestLoaderErrorThrower.maybeThrowError();
+  }
+
+  @Override
+  public TrackGroupArray getTrackGroups() {
+    return trackGroups;
+  }
+
+  @Override
+  public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+      SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
+    ArrayList<ChunkSampleStream<DashChunkSource>> sampleStreamsList = new ArrayList<>();
+    for (int i = 0; i < selections.length; i++) {
+      if (streams[i] != null) {
+        @SuppressWarnings("unchecked")
+        ChunkSampleStream<DashChunkSource> stream = (ChunkSampleStream<DashChunkSource>) streams[i];
+        if (selections[i] == null || !mayRetainStreamFlags[i]) {
+          stream.release();
+          streams[i] = null;
+        } else {
+          sampleStreamsList.add(stream);
+        }
+      }
+      if (streams[i] == null && selections[i] != null) {
+        ChunkSampleStream<DashChunkSource> stream = buildSampleStream(selections[i], positionUs);
+        sampleStreamsList.add(stream);
+        streams[i] = stream;
+        streamResetFlags[i] = true;
+      }
+    }
+    sampleStreams = newSampleStreamArray(sampleStreamsList.size());
+    sampleStreamsList.toArray(sampleStreams);
+    sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
+    return positionUs;
+  }
+
+  @Override
+  public boolean continueLoading(long positionUs) {
+    return sequenceableLoader.continueLoading(positionUs);
+  }
+
+  @Override
+  public long getNextLoadPositionUs() {
+    return sequenceableLoader.getNextLoadPositionUs();
+  }
+
+  @Override
+  public long readDiscontinuity() {
+    return C.TIME_UNSET;
+  }
+
+  @Override
+  public long getBufferedPositionUs() {
+    long bufferedPositionUs = Long.MAX_VALUE;
+    for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
+      long rendererBufferedPositionUs = sampleStream.getBufferedPositionUs();
+      if (rendererBufferedPositionUs != C.TIME_END_OF_SOURCE) {
+        bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
+      }
+    }
+    return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs;
+  }
+
+  @Override
+  public long seekToUs(long positionUs) {
+    for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
+      sampleStream.seekToUs(positionUs);
+    }
+    return positionUs;
+  }
+
+  // SequenceableLoader.Callback implementation.
+
+  @Override
+  public void onContinueLoadingRequested(ChunkSampleStream<DashChunkSource> sampleStream) {
+    callback.onContinueLoadingRequested(this);
+  }
+
+  // Internal methods.
+
+  private static TrackGroupArray buildTrackGroups(Period period) {
+    TrackGroup[] trackGroupArray = new TrackGroup[period.adaptationSets.size()];
+    for (int i = 0; i < period.adaptationSets.size(); i++) {
+      AdaptationSet adaptationSet = period.adaptationSets.get(i);
+      List<Representation> representations = adaptationSet.representations;
+      Format[] formats = new Format[representations.size()];
+      for (int j = 0; j < formats.length; j++) {
+        formats[j] = representations.get(j).format;
+      }
+      trackGroupArray[i] = new TrackGroup(formats);
+    }
+    return new TrackGroupArray(trackGroupArray);
+  }
+
+  private ChunkSampleStream<DashChunkSource> buildSampleStream(TrackSelection selection,
+      long positionUs) {
+    int adaptationSetIndex = trackGroups.indexOf(selection.getTrackGroup());
+    AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex);
+    DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource(
+        manifestLoaderErrorThrower, manifest, index, adaptationSetIndex, selection,
+        elapsedRealtimeOffset);
+    return new ChunkSampleStream<>(adaptationSet.type, chunkSource, this, allocator, positionUs,
+        minLoadableRetryCount, eventDispatcher);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static ChunkSampleStream<DashChunkSource>[] newSampleStreamArray(int length) {
+    return new ChunkSampleStream[length];
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/DashMediaSource.java
@@ -0,0 +1,790 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.dash;
+
+import android.net.Uri;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
+import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
+import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.Loader;
+import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
+import com.google.android.exoplayer2.upstream.ParsingLoadable;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * A DASH {@link MediaSource}.
+ */
+public final class DashMediaSource implements MediaSource {
+
+  /**
+   * The default minimum number of times to retry loading data prior to failing.
+   */
+  public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
+  /**
+   * A constant indicating that the presentation delay for live streams should be set to
+   * {@link DashManifest#suggestedPresentationDelay} if specified by the manifest, or
+   * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS} otherwise. The presentation delay is the
+   * duration by which the default start position precedes the end of the live window.
+   */
+  public static final long DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS = -1;
+  /**
+   * A fixed default presentation delay for live streams. The presentation delay is the duration
+   * by which the default start position precedes the end of the live window.
+   */
+  public static final long DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS = 30000;
+
+  /**
+   * The interval in milliseconds between invocations of
+   * {@link MediaSource.Listener#onSourceInfoRefreshed(Timeline, Object)} when the source's
+   * {@link Timeline} is changing dynamically (for example, for incomplete live streams).
+   */
+  private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000;
+  /**
+   * The minimum default start position for live streams, relative to the start of the live window.
+   */
+  private static final long MIN_LIVE_DEFAULT_START_POSITION_US = 5000000;
+
+  private static final String TAG = "DashMediaSource";
+
+  private final boolean sideloadedManifest;
+  private final DataSource.Factory manifestDataSourceFactory;
+  private final DashChunkSource.Factory chunkSourceFactory;
+  private final int minLoadableRetryCount;
+  private final long livePresentationDelayMs;
+  private final EventDispatcher eventDispatcher;
+  private final DashManifestParser manifestParser;
+  private final ManifestCallback manifestCallback;
+  private final Object manifestUriLock;
+  private final SparseArray<DashMediaPeriod> periodsById;
+  private final Runnable refreshManifestRunnable;
+  private final Runnable simulateManifestRefreshRunnable;
+
+  private Listener sourceListener;
+  private DataSource dataSource;
+  private Loader loader;
+  private LoaderErrorThrower loaderErrorThrower;
+
+  private Uri manifestUri;
+  private long manifestLoadStartTimestamp;
+  private long manifestLoadEndTimestamp;
+  private DashManifest manifest;
+  private Handler handler;
+  private long elapsedRealtimeOffsetMs;
+
+  private int firstPeriodId;
+
+  /**
+   * Constructs an instance to play a given {@link DashManifest}, which must be static.
+   *
+   * @param manifest The manifest. {@link DashManifest#dynamic} must be false.
+   * @param chunkSourceFactory A factory for {@link DashChunkSource} instances.
+   * @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.
+   */
+  public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory,
+      Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) {
+    this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler,
+        eventListener);
+  }
+
+  /**
+   * Constructs an instance to play a given {@link DashManifest}, which must be static.
+   *
+   * @param manifest The manifest. {@link DashManifest#dynamic} must be false.
+   * @param chunkSourceFactory A factory for {@link DashChunkSource} instances.
+   * @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.
+   */
+  public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory,
+      int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener
+      eventListener) {
+    this(manifest, null, null, null, chunkSourceFactory, minLoadableRetryCount,
+        DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, eventHandler, eventListener);
+  }
+
+  /**
+   * Constructs an instance to play the manifest at a given {@link Uri}, which may be dynamic or
+   * static.
+   *
+   * @param manifestUri The manifest {@link Uri}.
+   * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used
+   *     to load (and refresh) the manifest.
+   * @param chunkSourceFactory A factory for {@link DashChunkSource} instances.
+   * @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.
+   */
+  public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory,
+      DashChunkSource.Factory chunkSourceFactory, Handler eventHandler,
+      AdaptiveMediaSourceEventListener eventListener) {
+    this(manifestUri, manifestDataSourceFactory, chunkSourceFactory,
+        DEFAULT_MIN_LOADABLE_RETRY_COUNT, DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS,
+        eventHandler, eventListener);
+  }
+
+  /**
+   * Constructs an instance to play the manifest at a given {@link Uri}, which may be dynamic or
+   * static.
+   *
+   * @param manifestUri The manifest {@link Uri}.
+   * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used
+   *     to load (and refresh) the manifest.
+   * @param chunkSourceFactory A factory for {@link DashChunkSource} instances.
+   * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
+   * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the
+   *     default start position should precede the end of the live window. Use
+   *     {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by
+   *     the manifest, if present.
+   * @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.
+   */
+  public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory,
+      DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount,
+      long livePresentationDelayMs, Handler eventHandler,
+      AdaptiveMediaSourceEventListener eventListener) {
+    this(manifestUri, manifestDataSourceFactory, new DashManifestParser(), chunkSourceFactory,
+        minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener);
+  }
+
+  /**
+   * Constructs an instance to play the manifest at a given {@link Uri}, which may be dynamic or
+   * static.
+   *
+   * @param manifestUri The manifest {@link Uri}.
+   * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used
+   *     to load (and refresh) the manifest.
+   * @param manifestParser A parser for loaded manifest data.
+   * @param chunkSourceFactory A factory for {@link DashChunkSource} instances.
+   * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
+   * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the
+   *     default start position should precede the end of the live window. Use
+   *     {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by
+   *     the manifest, if present.
+   * @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.
+   */
+  public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory,
+      DashManifestParser manifestParser, DashChunkSource.Factory chunkSourceFactory,
+      int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler,
+      AdaptiveMediaSourceEventListener eventListener) {
+    this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory,
+        minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener);
+  }
+
+  private DashMediaSource(DashManifest manifest, Uri manifestUri,
+      DataSource.Factory manifestDataSourceFactory, DashManifestParser manifestParser,
+      DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount,
+      long livePresentationDelayMs, Handler eventHandler,
+      AdaptiveMediaSourceEventListener eventListener) {
+    this.manifest = manifest;
+    this.manifestUri = manifestUri;
+    this.manifestDataSourceFactory = manifestDataSourceFactory;
+    this.manifestParser = manifestParser;
+    this.chunkSourceFactory = chunkSourceFactory;
+    this.minLoadableRetryCount = minLoadableRetryCount;
+    this.livePresentationDelayMs = livePresentationDelayMs;
+    sideloadedManifest = manifest != null;
+    eventDispatcher = new EventDispatcher(eventHandler, eventListener);
+    manifestUriLock = new Object();
+    periodsById = new SparseArray<>();
+    if (sideloadedManifest) {
+      Assertions.checkState(!manifest.dynamic);
+      manifestCallback = null;
+      refreshManifestRunnable = null;
+      simulateManifestRefreshRunnable = null;
+    } else {
+      manifestCallback = new ManifestCallback();
+      refreshManifestRunnable = new Runnable() {
+        @Override
+        public void run() {
+          startLoadingManifest();
+        }
+      };
+      simulateManifestRefreshRunnable = new Runnable() {
+        @Override
+        public void run() {
+          processManifest(false);
+        }
+      };
+    }
+  }
+
+  /**
+   * Manually replaces the manifest {@link Uri}.
+   *
+   * @param manifestUri The replacement manifest {@link Uri}.
+   */
+  public void replaceManifestUri(Uri manifestUri) {
+    synchronized (manifestUriLock) {
+      this.manifestUri = manifestUri;
+    }
+  }
+
+  // MediaSource implementation.
+
+  @Override
+  public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+    sourceListener = listener;
+    if (sideloadedManifest) {
+      loaderErrorThrower = new LoaderErrorThrower.Dummy();
+      processManifest(false);
+    } else {
+      dataSource = manifestDataSourceFactory.createDataSource();
+      loader = new Loader("Loader:DashMediaSource");
+      loaderErrorThrower = loader;
+      handler = new Handler();
+      startLoadingManifest();
+    }
+  }
+
+  @Override
+  public void maybeThrowSourceInfoRefreshError() throws IOException {
+    loaderErrorThrower.maybeThrowError();
+  }
+
+  @Override
+  public MediaPeriod createPeriod(int periodIndex, Allocator allocator, long positionUs) {
+    EventDispatcher periodEventDispatcher = eventDispatcher.copyWithMediaTimeOffsetMs(
+        manifest.getPeriod(periodIndex).startMs);
+    DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + periodIndex, manifest,
+        periodIndex, chunkSourceFactory, minLoadableRetryCount, periodEventDispatcher,
+        elapsedRealtimeOffsetMs, loaderErrorThrower, allocator);
+    periodsById.put(mediaPeriod.id, mediaPeriod);
+    return mediaPeriod;
+  }
+
+  @Override
+  public void releasePeriod(MediaPeriod mediaPeriod) {
+    DashMediaPeriod dashMediaPeriod = (DashMediaPeriod) mediaPeriod;
+    dashMediaPeriod.release();
+    periodsById.remove(dashMediaPeriod.id);
+  }
+
+  @Override
+  public void releaseSource() {
+    dataSource = null;
+    loaderErrorThrower = null;
+    if (loader != null) {
+      loader.release();
+      loader = null;
+    }
+    manifestLoadStartTimestamp = 0;
+    manifestLoadEndTimestamp = 0;
+    manifest = null;
+    if (handler != null) {
+      handler.removeCallbacksAndMessages(null);
+      handler = null;
+    }
+    elapsedRealtimeOffsetMs = 0;
+    periodsById.clear();
+  }
+
+  // Loadable callbacks.
+
+  /* package */ void onManifestLoadCompleted(ParsingLoadable<DashManifest> loadable,
+      long elapsedRealtimeMs, long loadDurationMs) {
+    eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs,
+        loadDurationMs, loadable.bytesLoaded());
+    DashManifest newManifest = loadable.getResult();
+
+    int periodCount = manifest == null ? 0 : manifest.getPeriodCount();
+    int removedPeriodCount = 0;
+    long newFirstPeriodStartTimeMs = newManifest.getPeriod(0).startMs;
+    while (removedPeriodCount < periodCount
+        && manifest.getPeriod(removedPeriodCount).startMs < newFirstPeriodStartTimeMs) {
+      removedPeriodCount++;
+    }
+
+    // After discarding old periods, we should never have more periods than listed in the new
+    // manifest. That would mean that a previously announced period is no longer advertised. If
+    // this condition occurs, assume that we are hitting a manifest server that is out of sync and
+    // behind, discard this manifest, and try again later.
+    if (periodCount - removedPeriodCount > newManifest.getPeriodCount()) {
+      Log.w(TAG, "Out of sync manifest");
+      scheduleManifestRefresh();
+      return;
+    }
+
+    manifest = newManifest;
+    manifestLoadStartTimestamp = elapsedRealtimeMs - loadDurationMs;
+    manifestLoadEndTimestamp = elapsedRealtimeMs;
+    if (manifest.location != null) {
+      synchronized (manifestUriLock) {
+        // This condition checks that replaceManifestUri wasn't called between the start and end of
+        // this load. If it was, we ignore the manifest location and prefer the manual replacement.
+        if (loadable.dataSpec.uri == manifestUri) {
+          manifestUri = manifest.location;
+        }
+      }
+    }
+
+    if (periodCount == 0) {
+      if (manifest.utcTiming != null) {
+        resolveUtcTimingElement(manifest.utcTiming);
+      } else {
+        processManifest(true);
+      }
+    } else {
+      firstPeriodId += removedPeriodCount;
+      processManifest(true);
+    }
+  }
+
+  /* package */ int onManifestLoadError(ParsingLoadable<DashManifest> loadable,
+      long elapsedRealtimeMs, long loadDurationMs, IOException error) {
+    boolean isFatal = error instanceof ParserException;
+    eventDispatcher.loadError(loadable.dataSpec, loadable.type, elapsedRealtimeMs, loadDurationMs,
+        loadable.bytesLoaded(), error, isFatal);
+    return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY;
+  }
+
+  /* package */ void onUtcTimestampLoadCompleted(ParsingLoadable<Long> loadable,
+      long elapsedRealtimeMs, long loadDurationMs) {
+    eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs,
+        loadDurationMs, loadable.bytesLoaded());
+    onUtcTimestampResolved(loadable.getResult() - elapsedRealtimeMs);
+  }
+
+  /* package */ int onUtcTimestampLoadError(ParsingLoadable<Long> loadable, long elapsedRealtimeMs,
+      long loadDurationMs, IOException error) {
+    eventDispatcher.loadError(loadable.dataSpec, loadable.type, elapsedRealtimeMs, loadDurationMs,
+        loadable.bytesLoaded(), error, true);
+    onUtcTimestampResolutionError(error);
+    return Loader.DONT_RETRY;
+  }
+
+  /* package */ void onLoadCanceled(ParsingLoadable<?> loadable, long elapsedRealtimeMs,
+      long loadDurationMs) {
+    eventDispatcher.loadCanceled(loadable.dataSpec, loadable.type, elapsedRealtimeMs,
+        loadDurationMs, loadable.bytesLoaded());
+  }
+
+  // Internal methods.
+
+  private void startLoadingManifest() {
+    Uri manifestUri;
+    synchronized (manifestUriLock) {
+      manifestUri = this.manifestUri;
+    }
+    startLoading(new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST,
+        manifestParser), manifestCallback, minLoadableRetryCount);
+  }
+
+  private void resolveUtcTimingElement(UtcTimingElement timingElement) {
+    String scheme = timingElement.schemeIdUri;
+    if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) {
+      resolveUtcTimingElementDirect(timingElement);
+    } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2014")) {
+      resolveUtcTimingElementHttp(timingElement, new Iso8601Parser());
+    } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012")
+        || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")) {
+      resolveUtcTimingElementHttp(timingElement, new XsDateTimeParser());
+    } else {
+      // Unsupported scheme.
+      onUtcTimestampResolutionError(new IOException("Unsupported UTC timing scheme"));
+    }
+  }
+
+  private void resolveUtcTimingElementDirect(UtcTimingElement timingElement) {
+    try {
+      long utcTimestamp = Util.parseXsDateTime(timingElement.value);
+      onUtcTimestampResolved(utcTimestamp - manifestLoadEndTimestamp);
+    } catch (ParserException e) {
+      onUtcTimestampResolutionError(e);
+    }
+  }
+
+  private void resolveUtcTimingElementHttp(UtcTimingElement timingElement,
+      ParsingLoadable.Parser<Long> parser) {
+    startLoading(new ParsingLoadable<>(dataSource, Uri.parse(timingElement.value),
+        C.DATA_TYPE_TIME_SYNCHRONIZATION, parser), new UtcTimestampCallback(), 1);
+  }
+
+  private void onUtcTimestampResolved(long elapsedRealtimeOffsetMs) {
+    this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;
+    processManifest(true);
+  }
+
+  private void onUtcTimestampResolutionError(IOException error) {
+    Log.e(TAG, "Failed to resolve UtcTiming element.", error);
+    // Be optimistic and continue in the hope that the device clock is correct.
+    processManifest(true);
+  }
+
+  private void processManifest(boolean scheduleRefresh) {
+    // Update any periods.
+    for (int i = 0; i < periodsById.size(); i++) {
+      int id = periodsById.keyAt(i);
+      if (id >= firstPeriodId) {
+        periodsById.valueAt(i).updateManifest(manifest, id - firstPeriodId);
+      } else {
+        // This period has been removed from the manifest so it doesn't need to be updated.
+      }
+    }
+    // Update the window.
+    boolean windowChangingImplicitly = false;
+    int lastPeriodIndex = manifest.getPeriodCount() - 1;
+    PeriodSeekInfo firstPeriodSeekInfo = PeriodSeekInfo.createPeriodSeekInfo(manifest.getPeriod(0),
+        manifest.getPeriodDurationUs(0));
+    PeriodSeekInfo lastPeriodSeekInfo = PeriodSeekInfo.createPeriodSeekInfo(
+        manifest.getPeriod(lastPeriodIndex), manifest.getPeriodDurationUs(lastPeriodIndex));
+    // Get the period-relative start/end times.
+    long currentStartTimeUs = firstPeriodSeekInfo.availableStartTimeUs;
+    long currentEndTimeUs = lastPeriodSeekInfo.availableEndTimeUs;
+    if (manifest.dynamic && !lastPeriodSeekInfo.isIndexExplicit) {
+      // The manifest describes an incomplete live stream. Update the start/end times to reflect the
+      // live stream duration and the manifest's time shift buffer depth.
+      long liveStreamDurationUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTime);
+      long liveStreamEndPositionInLastPeriodUs = liveStreamDurationUs
+          - C.msToUs(manifest.getPeriod(lastPeriodIndex).startMs);
+      currentEndTimeUs = Math.min(liveStreamEndPositionInLastPeriodUs, currentEndTimeUs);
+      if (manifest.timeShiftBufferDepth != C.TIME_UNSET) {
+        long timeShiftBufferDepthUs = C.msToUs(manifest.timeShiftBufferDepth);
+        long offsetInPeriodUs = currentEndTimeUs - timeShiftBufferDepthUs;
+        int periodIndex = lastPeriodIndex;
+        while (offsetInPeriodUs < 0 && periodIndex > 0) {
+          offsetInPeriodUs += manifest.getPeriodDurationUs(--periodIndex);
+        }
+        if (periodIndex == 0) {
+          currentStartTimeUs = Math.max(currentStartTimeUs, offsetInPeriodUs);
+        } else {
+          // The time shift buffer starts after the earliest period.
+          // TODO: Does this ever happen?
+          currentStartTimeUs = manifest.getPeriodDurationUs(0);
+        }
+      }
+      windowChangingImplicitly = true;
+    }
+    long windowDurationUs = currentEndTimeUs - currentStartTimeUs;
+    for (int i = 0; i < manifest.getPeriodCount() - 1; i++) {
+      windowDurationUs += manifest.getPeriodDurationUs(i);
+    }
+    long windowDefaultStartPositionUs = 0;
+    if (manifest.dynamic) {
+      long presentationDelayForManifestMs = livePresentationDelayMs;
+      if (presentationDelayForManifestMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS) {
+        presentationDelayForManifestMs = manifest.suggestedPresentationDelay != C.TIME_UNSET
+            ? manifest.suggestedPresentationDelay : DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS;
+      }
+      // Snap the default position to the start of the segment containing it.
+      windowDefaultStartPositionUs = windowDurationUs - C.msToUs(presentationDelayForManifestMs);
+      if (windowDefaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) {
+        // The default start position is too close to the start of the live window. Set it to the
+        // minimum default start position provided the window is at least twice as big. Else set
+        // it to the middle of the window.
+        windowDefaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US,
+            windowDurationUs / 2);
+      }
+    }
+    long windowStartTimeMs = manifest.availabilityStartTime
+        + manifest.getPeriod(0).startMs + C.usToMs(currentStartTimeUs);
+    DashTimeline timeline = new DashTimeline(manifest.availabilityStartTime, windowStartTimeMs,
+        firstPeriodId, currentStartTimeUs, windowDurationUs, windowDefaultStartPositionUs,
+        manifest);
+    sourceListener.onSourceInfoRefreshed(timeline, manifest);
+
+    if (!sideloadedManifest) {
+      // Remove any pending simulated refresh.
+      handler.removeCallbacks(simulateManifestRefreshRunnable);
+      // If the window is changing implicitly, post a simulated manifest refresh to update it.
+      if (windowChangingImplicitly) {
+        handler.postDelayed(simulateManifestRefreshRunnable, NOTIFY_MANIFEST_INTERVAL_MS);
+      }
+      // Schedule an explicit refresh if needed.
+      if (scheduleRefresh) {
+        scheduleManifestRefresh();
+      }
+    }
+  }
+
+  private void scheduleManifestRefresh() {
+    if (!manifest.dynamic) {
+      return;
+    }
+    long minUpdatePeriod = manifest.minUpdatePeriod;
+    if (minUpdatePeriod == 0) {
+      // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where
+      // minUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is explicit
+      // signaling in the stream, according to:
+      // http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/
+      minUpdatePeriod = 5000;
+    }
+    long nextLoadTimestamp = manifestLoadStartTimestamp + minUpdatePeriod;
+    long delayUntilNextLoad = Math.max(0, nextLoadTimestamp - SystemClock.elapsedRealtime());
+    handler.postDelayed(refreshManifestRunnable, delayUntilNextLoad);
+  }
+
+  private <T> void startLoading(ParsingLoadable<T> loadable,
+      Loader.Callback<ParsingLoadable<T>> callback, int minRetryCount) {
+    long elapsedRealtimeMs = loader.startLoading(loadable, callback, minRetryCount);
+    eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs);
+  }
+
+  private long getNowUnixTimeUs() {
+    if (elapsedRealtimeOffsetMs != 0) {
+      return C.msToUs(SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs);
+    } else {
+      return C.msToUs(System.currentTimeMillis());
+    }
+  }
+
+  private static final class PeriodSeekInfo {
+
+    public static PeriodSeekInfo createPeriodSeekInfo(
+        com.google.android.exoplayer2.source.dash.manifest.Period period, long durationUs) {
+      int adaptationSetCount = period.adaptationSets.size();
+      long availableStartTimeUs = 0;
+      long availableEndTimeUs = Long.MAX_VALUE;
+      boolean isIndexExplicit = false;
+      for (int i = 0; i < adaptationSetCount; i++) {
+        DashSegmentIndex index = period.adaptationSets.get(i).representations.get(0).getIndex();
+        if (index == null) {
+          return new PeriodSeekInfo(true, 0, durationUs);
+        }
+        int firstSegmentNum = index.getFirstSegmentNum();
+        int lastSegmentNum = index.getLastSegmentNum(durationUs);
+        isIndexExplicit |= index.isExplicit();
+        long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstSegmentNum);
+        availableStartTimeUs = Math.max(availableStartTimeUs, adaptationSetAvailableStartTimeUs);
+        if (lastSegmentNum != DashSegmentIndex.INDEX_UNBOUNDED) {
+          long adaptationSetAvailableEndTimeUs = index.getTimeUs(lastSegmentNum)
+              + index.getDurationUs(lastSegmentNum, durationUs);
+          availableEndTimeUs = Math.min(availableEndTimeUs, adaptationSetAvailableEndTimeUs);
+        } else {
+          // The available end time is unmodified, because this index is unbounded.
+        }
+      }
+      return new PeriodSeekInfo(isIndexExplicit, availableStartTimeUs, availableEndTimeUs);
+    }
+
+    public final boolean isIndexExplicit;
+    public final long availableStartTimeUs;
+    public final long availableEndTimeUs;
+
+    private PeriodSeekInfo(boolean isIndexExplicit, long availableStartTimeUs,
+        long availableEndTimeUs) {
+      this.isIndexExplicit = isIndexExplicit;
+      this.availableStartTimeUs = availableStartTimeUs;
+      this.availableEndTimeUs = availableEndTimeUs;
+    }
+
+  }
+
+  private static final class DashTimeline extends Timeline {
+
+    private final long presentationStartTimeMs;
+    private final long windowStartTimeMs;
+
+    private final int firstPeriodId;
+    private final long offsetInFirstPeriodUs;
+    private final long windowDurationUs;
+    private final long windowDefaultStartPositionUs;
+    private final DashManifest manifest;
+
+    public DashTimeline(long presentationStartTimeMs, long windowStartTimeMs,
+        int firstPeriodId, long offsetInFirstPeriodUs, long windowDurationUs,
+        long windowDefaultStartPositionUs, DashManifest manifest) {
+      this.presentationStartTimeMs = presentationStartTimeMs;
+      this.windowStartTimeMs = windowStartTimeMs;
+      this.firstPeriodId = firstPeriodId;
+      this.offsetInFirstPeriodUs = offsetInFirstPeriodUs;
+      this.windowDurationUs = windowDurationUs;
+      this.windowDefaultStartPositionUs = windowDefaultStartPositionUs;
+      this.manifest = manifest;
+    }
+
+    @Override
+    public int getPeriodCount() {
+      return manifest.getPeriodCount();
+    }
+
+    @Override
+    public Period getPeriod(int periodIndex, Period period, boolean setIdentifiers) {
+      Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount());
+      Object id = setIdentifiers ? manifest.getPeriod(periodIndex).id : null;
+      Object uid = setIdentifiers ? firstPeriodId
+          + Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()) : null;
+      return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex),
+          C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs)
+              - offsetInFirstPeriodUs);
+    }
+
+    @Override
+    public int getWindowCount() {
+      return 1;
+    }
+
+    @Override
+    public Window getWindow(int windowIndex, Window window, boolean setIdentifier,
+        long defaultPositionProjectionUs) {
+      Assertions.checkIndex(windowIndex, 0, 1);
+      long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs(
+          defaultPositionProjectionUs);
+      return window.set(null, presentationStartTimeMs, windowStartTimeMs, true /* isSeekable */,
+          manifest.dynamic, windowDefaultStartPositionUs, windowDurationUs, 0,
+          manifest.getPeriodCount() - 1, offsetInFirstPeriodUs);
+    }
+
+    @Override
+    public int getIndexOfPeriod(Object uid) {
+      if (!(uid instanceof Integer)) {
+        return C.INDEX_UNSET;
+      }
+      int periodId = (int) uid;
+      return periodId < firstPeriodId || periodId >= firstPeriodId + getPeriodCount()
+          ? C.INDEX_UNSET : (periodId - firstPeriodId);
+    }
+
+    private long getAdjustedWindowDefaultStartPositionUs(long defaultPositionProjectionUs) {
+      long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs;
+      if (!manifest.dynamic) {
+        return windowDefaultStartPositionUs;
+      }
+      if (defaultPositionProjectionUs > 0) {
+        windowDefaultStartPositionUs += defaultPositionProjectionUs;
+        if (windowDefaultStartPositionUs > windowDurationUs) {
+          // The projection takes us beyond the end of the live window.
+          return C.TIME_UNSET;
+        }
+      }
+      // Attempt to snap to the start of the corresponding video segment.
+      int periodIndex = 0;
+      long defaultStartPositionInPeriodUs = offsetInFirstPeriodUs + windowDefaultStartPositionUs;
+      long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);
+      while (periodIndex < manifest.getPeriodCount() - 1
+          && defaultStartPositionInPeriodUs >= periodDurationUs) {
+        defaultStartPositionInPeriodUs -= periodDurationUs;
+        periodIndex++;
+        periodDurationUs = manifest.getPeriodDurationUs(periodIndex);
+      }
+      com.google.android.exoplayer2.source.dash.manifest.Period period =
+          manifest.getPeriod(periodIndex);
+      int videoAdaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO);
+      if (videoAdaptationSetIndex == C.INDEX_UNSET) {
+        // No video adaptation set for snapping.
+        return windowDefaultStartPositionUs;
+      }
+      // If there are multiple video adaptation sets with unaligned segments, the initial time may
+      // not correspond to the start of a segment in both, but this is an edge case.
+      DashSegmentIndex snapIndex = period.adaptationSets.get(videoAdaptationSetIndex)
+          .representations.get(0).getIndex();
+      if (snapIndex == null) {
+        // Video adaptation set does not include an index for snapping.
+        return windowDefaultStartPositionUs;
+      }
+      int segmentNum = snapIndex.getSegmentNum(defaultStartPositionInPeriodUs, periodDurationUs);
+      return windowDefaultStartPositionUs + snapIndex.getTimeUs(segmentNum)
+          - defaultStartPositionInPeriodUs;
+    }
+
+  }
+
+  private final class ManifestCallback implements
+      Loader.Callback<ParsingLoadable<DashManifest>> {
+
+    @Override
+    public void onLoadCompleted(ParsingLoadable<DashManifest> loadable,
+        long elapsedRealtimeMs, long loadDurationMs) {
+      onManifestLoadCompleted(loadable, elapsedRealtimeMs, loadDurationMs);
+    }
+
+    @Override
+    public void onLoadCanceled(ParsingLoadable<DashManifest> loadable,
+        long elapsedRealtimeMs, long loadDurationMs, boolean released) {
+      DashMediaSource.this.onLoadCanceled(loadable, elapsedRealtimeMs, loadDurationMs);
+    }
+
+    @Override
+    public int onLoadError(ParsingLoadable<DashManifest> loadable,
+        long elapsedRealtimeMs, long loadDurationMs, IOException error) {
+      return onManifestLoadError(loadable, elapsedRealtimeMs, loadDurationMs, error);
+    }
+
+  }
+
+  private final class UtcTimestampCallback implements Loader.Callback<ParsingLoadable<Long>> {
+
+    @Override
+    public void onLoadCompleted(ParsingLoadable<Long> loadable, long elapsedRealtimeMs,
+        long loadDurationMs) {
+      onUtcTimestampLoadCompleted(loadable, elapsedRealtimeMs, loadDurationMs);
+    }
+
+    @Override
+    public void onLoadCanceled(ParsingLoadable<Long> loadable, long elapsedRealtimeMs,
+        long loadDurationMs, boolean released) {
+      DashMediaSource.this.onLoadCanceled(loadable, elapsedRealtimeMs, loadDurationMs);
+    }
+
+    @Override
+    public int onLoadError(ParsingLoadable<Long> loadable, long elapsedRealtimeMs,
+        long loadDurationMs, IOException error) {
+      return onUtcTimestampLoadError(loadable, elapsedRealtimeMs, loadDurationMs, error);
+    }
+
+  }
+
+  private static final class XsDateTimeParser implements ParsingLoadable.Parser<Long> {
+
+    @Override
+    public Long parse(Uri uri, InputStream inputStream) throws IOException {
+      String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine();
+      return Util.parseXsDateTime(firstLine);
+    }
+
+  }
+
+  private static final class Iso8601Parser implements ParsingLoadable.Parser<Long> {
+
+    @Override
+    public Long parse(Uri uri, InputStream inputStream) throws IOException {
+      String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine();
+      try {
+        // TODO: It may be necessary to handle timestamp offsets from UTC.
+        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
+        format.setTimeZone(TimeZone.getTimeZone("UTC"));
+        return format.parse(firstLine).getTime();
+      } catch (ParseException e) {
+        throw new ParserException(e);
+      }
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/DashSegmentIndex.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 com.google.android.exoplayer2.source.dash;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
+
+/**
+ * Indexes the segments within a media stream.
+ */
+public interface DashSegmentIndex {
+
+  int INDEX_UNBOUNDED = -1;
+
+  /**
+   * Returns the segment number of the segment containing a given media time.
+   * <p>
+   * If the given media time is outside the range of the index, then the returned segment number is
+   * clamped to {@link #getFirstSegmentNum()} (if the given media time is earlier the start of the
+   * first segment) or {@link #getLastSegmentNum(long)} (if the given media time is later then the
+   * end of the last segment).
+   *
+   * @param timeUs The time in microseconds.
+   * @param periodDurationUs The duration of the enclosing period in microseconds, or
+   *     {@link C#TIME_UNSET} if the period's duration is not yet known.
+   * @return The segment number of the corresponding segment.
+   */
+  int getSegmentNum(long timeUs, long periodDurationUs);
+
+  /**
+   * Returns the start time of a segment.
+   *
+   * @param segmentNum The segment number.
+   * @return The corresponding start time in microseconds.
+   */
+  long getTimeUs(int segmentNum);
+
+  /**
+   * Returns the duration of a segment.
+   *
+   * @param segmentNum The segment number.
+   * @param periodDurationUs The duration of the enclosing period in microseconds, or
+   *     {@link C#TIME_UNSET} if the period's duration is not yet known.
+   * @return The duration of the segment, in microseconds.
+   */
+  long getDurationUs(int segmentNum, long periodDurationUs);
+
+  /**
+   * Returns a {@link RangedUri} defining the location of a segment.
+   *
+   * @param segmentNum The segment number.
+   * @return The {@link RangedUri} defining the location of the data.
+   */
+  RangedUri getSegmentUrl(int segmentNum);
+
+  /**
+   * Returns the segment number of the first segment.
+   *
+   * @return The segment number of the first segment.
+   */
+  int getFirstSegmentNum();
+
+  /**
+   * Returns the segment number of the last segment, or {@link #INDEX_UNBOUNDED}.
+   * <p>
+   * An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a
+   * SegmentTimeline element, and if the period duration is not yet known. In this case the caller
+   * must manually determine the window of currently available segments.
+   *
+   * @param periodDurationUs The duration of the enclosing period in microseconds, or
+   *     {@link C#TIME_UNSET} if the period's duration is not yet known.
+   * @return The segment number of the last segment, or {@link #INDEX_UNBOUNDED}.
+   */
+  int getLastSegmentNum(long periodDurationUs);
+
+  /**
+   * Returns true if segments are defined explicitly by the index.
+   * <p>
+   * If true is returned, each segment is defined explicitly by the index data, and all of the
+   * listed segments are guaranteed to be available at the time when the index was obtained.
+   * <p>
+   * If false is returned then segment information was derived from properties such as a fixed
+   * segment duration. If the presentation is dynamic, it's possible that only a subset of the
+   * segments are available.
+   *
+   * @return Whether segments are defined explicitly by the index.
+   */
+  boolean isExplicit();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/DashWrappingSegmentIndex.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 com.google.android.exoplayer2.source.dash;
+
+import com.google.android.exoplayer2.extractor.ChunkIndex;
+import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
+
+/**
+ * An implementation of {@link DashSegmentIndex} that wraps a {@link ChunkIndex} parsed from a
+ * media stream.
+ */
+/* package */ final class DashWrappingSegmentIndex implements DashSegmentIndex {
+
+  private final ChunkIndex chunkIndex;
+
+  /**
+   * @param chunkIndex The {@link ChunkIndex} to wrap.
+   */
+  public DashWrappingSegmentIndex(ChunkIndex chunkIndex) {
+    this.chunkIndex = chunkIndex;
+  }
+
+  @Override
+  public int getFirstSegmentNum() {
+    return 0;
+  }
+
+  @Override
+  public int getLastSegmentNum(long periodDurationUs) {
+    return chunkIndex.length - 1;
+  }
+
+  @Override
+  public long getTimeUs(int segmentNum) {
+    return chunkIndex.timesUs[segmentNum];
+  }
+
+  @Override
+  public long getDurationUs(int segmentNum, long periodDurationUs) {
+    return chunkIndex.durationsUs[segmentNum];
+  }
+
+  @Override
+  public RangedUri getSegmentUrl(int segmentNum) {
+    return new RangedUri(null, chunkIndex.offsets[segmentNum], chunkIndex.sizes[segmentNum]);
+  }
+
+  @Override
+  public int getSegmentNum(long timeUs, long periodDurationUs) {
+    return chunkIndex.getChunkIndex(timeUs);
+  }
+
+  @Override
+  public boolean isExplicit() {
+    return true;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java
@@ -0,0 +1,475 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.dash;
+
+import android.net.Uri;
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.ChunkIndex;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
+import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
+import com.google.android.exoplayer2.extractor.rawcc.RawCcExtractor;
+import com.google.android.exoplayer2.source.BehindLiveWindowException;
+import com.google.android.exoplayer2.source.chunk.Chunk;
+import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;
+import com.google.android.exoplayer2.source.chunk.ChunkHolder;
+import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil;
+import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk;
+import com.google.android.exoplayer2.source.chunk.InitializationChunk;
+import com.google.android.exoplayer2.source.chunk.MediaChunk;
+import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk;
+import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
+import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
+import com.google.android.exoplayer2.source.dash.manifest.Representation;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
+import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * A default {@link DashChunkSource} implementation.
+ */
+public class DefaultDashChunkSource implements DashChunkSource {
+
+  public static final class Factory implements DashChunkSource.Factory {
+
+    private final DataSource.Factory dataSourceFactory;
+    private final int maxSegmentsPerLoad;
+
+    public Factory(DataSource.Factory dataSourceFactory) {
+      this(dataSourceFactory, 1);
+    }
+
+    public Factory(DataSource.Factory dataSourceFactory, int maxSegmentsPerLoad) {
+      this.dataSourceFactory = dataSourceFactory;
+      this.maxSegmentsPerLoad = maxSegmentsPerLoad;
+    }
+
+    @Override
+    public DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
+        DashManifest manifest, int periodIndex, int adaptationSetIndex,
+        TrackSelection trackSelection, long elapsedRealtimeOffsetMs) {
+      DataSource dataSource = dataSourceFactory.createDataSource();
+      return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex,
+          adaptationSetIndex, trackSelection, dataSource, elapsedRealtimeOffsetMs,
+          maxSegmentsPerLoad);
+    }
+
+  }
+
+  private final LoaderErrorThrower manifestLoaderErrorThrower;
+  private final int adaptationSetIndex;
+  private final TrackSelection trackSelection;
+  private final RepresentationHolder[] representationHolders;
+  private final DataSource dataSource;
+  private final long elapsedRealtimeOffsetMs;
+  private final int maxSegmentsPerLoad;
+
+  private DashManifest manifest;
+  private int periodIndex;
+
+  private IOException fatalError;
+  private boolean missingLastSegment;
+
+  /**
+   * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
+   * @param manifest The initial manifest.
+   * @param periodIndex The index of the period in the manifest.
+   * @param adaptationSetIndex The index of the adaptation set in the period.
+   * @param trackSelection The track selection.
+   * @param dataSource A {@link DataSource} suitable for loading the media data.
+   * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between
+   *     server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified
+   *     as the server's unix time minus the local elapsed time. If unknown, set to 0.
+   * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request.
+   *     Note that segments will only be combined if their {@link Uri}s are the same and if their
+   *     data ranges are adjacent.
+   */
+  public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
+      DashManifest manifest, int periodIndex, int adaptationSetIndex, TrackSelection trackSelection,
+      DataSource dataSource, long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad) {
+    this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
+    this.manifest = manifest;
+    this.adaptationSetIndex = adaptationSetIndex;
+    this.trackSelection = trackSelection;
+    this.dataSource = dataSource;
+    this.periodIndex = periodIndex;
+    this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs;
+    this.maxSegmentsPerLoad = maxSegmentsPerLoad;
+
+    long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);
+    List<Representation> representations = getRepresentations();
+    representationHolders = new RepresentationHolder[trackSelection.length()];
+    for (int i = 0; i < representationHolders.length; i++) {
+      Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));
+      representationHolders[i] = new RepresentationHolder(periodDurationUs, representation);
+    }
+  }
+
+  @Override
+  public void updateManifest(DashManifest newManifest, int newPeriodIndex) {
+    try {
+      manifest = newManifest;
+      periodIndex = newPeriodIndex;
+      long periodDurationUs = manifest.getPeriodDurationUs(periodIndex);
+      List<Representation> representations = getRepresentations();
+      for (int i = 0; i < representationHolders.length; i++) {
+        Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));
+        representationHolders[i].updateRepresentation(periodDurationUs, representation);
+      }
+    } catch (BehindLiveWindowException e) {
+      fatalError = e;
+    }
+  }
+
+  @Override
+  public void maybeThrowError() throws IOException {
+    if (fatalError != null) {
+      throw fatalError;
+    } else {
+      manifestLoaderErrorThrower.maybeThrowError();
+    }
+  }
+
+  @Override
+  public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
+    if (fatalError != null || trackSelection.length() < 2) {
+      return queue.size();
+    }
+    return trackSelection.evaluateQueueSize(playbackPositionUs, queue);
+  }
+
+  @Override
+  public final void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) {
+    if (fatalError != null) {
+      return;
+    }
+
+    long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0;
+    trackSelection.updateSelectedTrack(bufferedDurationUs);
+
+    RepresentationHolder representationHolder =
+        representationHolders[trackSelection.getSelectedIndex()];
+    Representation selectedRepresentation = representationHolder.representation;
+    DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
+
+    RangedUri pendingInitializationUri = null;
+    RangedUri pendingIndexUri = null;
+    Format sampleFormat = representationHolder.sampleFormat;
+    if (sampleFormat == null) {
+      pendingInitializationUri = selectedRepresentation.getInitializationUri();
+    }
+    if (segmentIndex == null) {
+      pendingIndexUri = selectedRepresentation.getIndexUri();
+    }
+    if (pendingInitializationUri != null || pendingIndexUri != null) {
+      // We have initialization and/or index requests to make.
+      out.chunk = newInitializationChunk(representationHolder, dataSource,
+          trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(),
+          trackSelection.getSelectionData(), pendingInitializationUri, pendingIndexUri);
+      return;
+    }
+
+    long nowUs = getNowUnixTimeUs();
+    int firstAvailableSegmentNum = representationHolder.getFirstSegmentNum();
+    int lastAvailableSegmentNum = representationHolder.getLastSegmentNum();
+    boolean indexUnbounded = lastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED;
+    if (indexUnbounded) {
+      // The index is itself unbounded. We need to use the current time to calculate the range of
+      // available segments.
+      long liveEdgeTimeUs = nowUs - manifest.availabilityStartTime * 1000;
+      long periodStartUs = manifest.getPeriod(periodIndex).startMs * 1000;
+      long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs;
+      if (manifest.timeShiftBufferDepth != C.TIME_UNSET) {
+        long bufferDepthUs = manifest.timeShiftBufferDepth * 1000;
+        firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum,
+            representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs - bufferDepthUs));
+      }
+      // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the
+      // index of the last completed segment.
+      lastAvailableSegmentNum = representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs) - 1;
+    }
+
+    int segmentNum;
+    if (previous == null) {
+      segmentNum = Util.constrainValue(representationHolder.getSegmentNum(playbackPositionUs),
+          firstAvailableSegmentNum, lastAvailableSegmentNum);
+    } else {
+      segmentNum = previous.getNextChunkIndex();
+      if (segmentNum < firstAvailableSegmentNum) {
+        // This is before the first chunk in the current manifest.
+        fatalError = new BehindLiveWindowException();
+        return;
+      }
+    }
+
+    if (segmentNum > lastAvailableSegmentNum
+        || (missingLastSegment && segmentNum >= lastAvailableSegmentNum)) {
+      // This is beyond the last chunk in the current manifest.
+      out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1);
+      return;
+    }
+
+    int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1);
+    out.chunk = newMediaChunk(representationHolder, dataSource, trackSelection.getSelectedFormat(),
+        trackSelection.getSelectionReason(), trackSelection.getSelectionData(), sampleFormat,
+        segmentNum, maxSegmentCount);
+  }
+
+  @Override
+  public void onChunkLoadCompleted(Chunk chunk) {
+    if (chunk instanceof InitializationChunk) {
+      InitializationChunk initializationChunk = (InitializationChunk) chunk;
+      RepresentationHolder representationHolder =
+          representationHolders[trackSelection.indexOf(initializationChunk.trackFormat)];
+      Format sampleFormat = initializationChunk.getSampleFormat();
+      if (sampleFormat != null) {
+        representationHolder.setSampleFormat(sampleFormat);
+      }
+      // The null check avoids overwriting an index obtained from the manifest with one obtained
+      // from the stream. If the manifest defines an index then the stream shouldn't, but in cases
+      // where it does we should ignore it.
+      if (representationHolder.segmentIndex == null) {
+        SeekMap seekMap = initializationChunk.getSeekMap();
+        if (seekMap != null) {
+          representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap);
+        }
+      }
+    }
+  }
+
+  @Override
+  public boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e) {
+    if (!cancelable) {
+      return false;
+    }
+    // Workaround for missing segment at the end of the period
+    if (!manifest.dynamic && chunk instanceof MediaChunk
+        && e instanceof InvalidResponseCodeException
+        && ((InvalidResponseCodeException) e).responseCode == 404) {
+      RepresentationHolder representationHolder =
+          representationHolders[trackSelection.indexOf(chunk.trackFormat)];
+      int lastAvailableSegmentNum = representationHolder.getLastSegmentNum();
+      if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) {
+        missingLastSegment = true;
+        return true;
+      }
+    }
+    // Blacklist if appropriate.
+    return ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection,
+        trackSelection.indexOf(chunk.trackFormat), e);
+  }
+
+  // Private methods.
+
+  private List<Representation> getRepresentations() {
+    return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex).representations;
+  }
+
+  private long getNowUnixTimeUs() {
+    if (elapsedRealtimeOffsetMs != 0) {
+      return (SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs) * 1000;
+    } else {
+      return System.currentTimeMillis() * 1000;
+    }
+  }
+
+  private static Chunk newInitializationChunk(RepresentationHolder representationHolder,
+      DataSource dataSource, Format trackFormat, int trackSelectionReason,
+      Object trackSelectionData, RangedUri initializationUri, RangedUri indexUri) {
+    RangedUri requestUri;
+    String baseUrl = representationHolder.representation.baseUrl;
+    if (initializationUri != null) {
+      // It's common for initialization and index data to be stored adjacently. Attempt to merge
+      // the two requests together to request both at once.
+      requestUri = initializationUri.attemptMerge(indexUri, baseUrl);
+      if (requestUri == null) {
+        requestUri = initializationUri;
+      }
+    } else {
+      requestUri = indexUri;
+    }
+    DataSpec dataSpec = new DataSpec(requestUri.resolveUri(baseUrl), requestUri.start,
+        requestUri.length, representationHolder.representation.getCacheKey());
+    return new InitializationChunk(dataSource, dataSpec, trackFormat,
+        trackSelectionReason, trackSelectionData, representationHolder.extractorWrapper);
+  }
+
+  private static Chunk newMediaChunk(RepresentationHolder representationHolder,
+      DataSource dataSource, Format trackFormat, int trackSelectionReason,
+      Object trackSelectionData, Format sampleFormat, int firstSegmentNum, int maxSegmentCount) {
+    Representation representation = representationHolder.representation;
+    long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum);
+    RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum);
+    String baseUrl = representation.baseUrl;
+    if (representationHolder.extractorWrapper == null) {
+      long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum);
+      DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl),
+          segmentUri.start, segmentUri.length, representation.getCacheKey());
+      return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason,
+          trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, trackFormat);
+    } else {
+      int segmentCount = 1;
+      for (int i = 1; i < maxSegmentCount; i++) {
+        RangedUri nextSegmentUri = representationHolder.getSegmentUrl(firstSegmentNum + i);
+        RangedUri mergedSegmentUri = segmentUri.attemptMerge(nextSegmentUri, baseUrl);
+        if (mergedSegmentUri == null) {
+          // Unable to merge segment fetches because the URIs do not merge.
+          break;
+        }
+        segmentUri = mergedSegmentUri;
+        segmentCount++;
+      }
+      long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum + segmentCount - 1);
+      DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl),
+          segmentUri.start, segmentUri.length, representation.getCacheKey());
+      long sampleOffsetUs = -representation.presentationTimeOffsetUs;
+      return new ContainerMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason,
+          trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, segmentCount,
+          sampleOffsetUs, representationHolder.extractorWrapper, sampleFormat);
+    }
+  }
+
+  // Protected classes.
+
+  protected static final class RepresentationHolder {
+
+    public final ChunkExtractorWrapper extractorWrapper;
+
+    public Representation representation;
+    public DashSegmentIndex segmentIndex;
+    public Format sampleFormat;
+
+    private long periodDurationUs;
+    private int segmentNumShift;
+
+    public RepresentationHolder(long periodDurationUs, Representation representation) {
+      this.periodDurationUs = periodDurationUs;
+      this.representation = representation;
+      String containerMimeType = representation.format.containerMimeType;
+      if (mimeTypeIsRawText(containerMimeType)) {
+        extractorWrapper = null;
+      } else {
+        boolean resendFormatOnInit = false;
+        Extractor extractor;
+        if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {
+          extractor = new RawCcExtractor(representation.format);
+          resendFormatOnInit = true;
+        } else if (mimeTypeIsWebm(containerMimeType)) {
+          extractor = new MatroskaExtractor();
+        } else {
+          extractor = new FragmentedMp4Extractor();
+        }
+        // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream,
+        // as per DASH IF Interoperability Recommendations V3.0, 7.5.3.
+        extractorWrapper = new ChunkExtractorWrapper(extractor,
+            representation.format, true /* preferManifestDrmInitData */,
+            resendFormatOnInit);
+      }
+      segmentIndex = representation.getIndex();
+    }
+
+    public void setSampleFormat(Format sampleFormat) {
+      this.sampleFormat = sampleFormat;
+    }
+
+    public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation)
+        throws BehindLiveWindowException{
+      DashSegmentIndex oldIndex = representation.getIndex();
+      DashSegmentIndex newIndex = newRepresentation.getIndex();
+
+      periodDurationUs = newPeriodDurationUs;
+      representation = newRepresentation;
+      if (oldIndex == null) {
+        // Segment numbers cannot shift if the index isn't defined by the manifest.
+        return;
+      }
+
+      segmentIndex = newIndex;
+      if (!oldIndex.isExplicit()) {
+        // Segment numbers cannot shift if the index isn't explicit.
+        return;
+      }
+
+      int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum(periodDurationUs);
+      long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum)
+          + oldIndex.getDurationUs(oldIndexLastSegmentNum, periodDurationUs);
+      int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum();
+      long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum);
+      if (oldIndexEndTimeUs == newIndexStartTimeUs) {
+        // The new index continues where the old one ended, with no overlap.
+        segmentNumShift += oldIndex.getLastSegmentNum(periodDurationUs) + 1
+            - newIndexFirstSegmentNum;
+      } else if (oldIndexEndTimeUs < newIndexStartTimeUs) {
+        // There's a gap between the old index and the new one which means we've slipped behind the
+        // live window and can't proceed.
+        throw new BehindLiveWindowException();
+      } else {
+        // The new index overlaps with the old one.
+        segmentNumShift += oldIndex.getSegmentNum(newIndexStartTimeUs, periodDurationUs)
+            - newIndexFirstSegmentNum;
+      }
+    }
+
+    public int getFirstSegmentNum() {
+      return segmentIndex.getFirstSegmentNum() + segmentNumShift;
+    }
+
+    public int getLastSegmentNum() {
+      int lastSegmentNum = segmentIndex.getLastSegmentNum(periodDurationUs);
+      if (lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) {
+        return DashSegmentIndex.INDEX_UNBOUNDED;
+      }
+      return lastSegmentNum + segmentNumShift;
+    }
+
+    public long getSegmentStartTimeUs(int segmentNum) {
+      return segmentIndex.getTimeUs(segmentNum - segmentNumShift);
+    }
+
+    public long getSegmentEndTimeUs(int segmentNum) {
+      return getSegmentStartTimeUs(segmentNum)
+          + segmentIndex.getDurationUs(segmentNum - segmentNumShift, periodDurationUs);
+    }
+
+    public int getSegmentNum(long positionUs) {
+      return segmentIndex.getSegmentNum(positionUs, periodDurationUs) + segmentNumShift;
+    }
+
+    public RangedUri getSegmentUrl(int segmentNum) {
+      return segmentIndex.getSegmentUrl(segmentNum - segmentNumShift);
+    }
+
+    private static boolean mimeTypeIsWebm(String mimeType) {
+      return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM)
+          || mimeType.startsWith(MimeTypes.APPLICATION_WEBM);
+    }
+
+    private static boolean mimeTypeIsRawText(String mimeType) {
+      return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType);
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/AdaptationSet.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 com.google.android.exoplayer2.source.dash.manifest;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a set of interchangeable encoded versions of a media content component.
+ */
+public class AdaptationSet {
+
+  /**
+   * Value of {@link #id} indicating no value is set.=
+   */
+  public static final int ID_UNSET = -1;
+
+  /**
+   * A non-negative identifier for the adaptation set that's unique in the scope of its containing
+   * period, or {@link #ID_UNSET} if not specified.
+   */
+  public final int id;
+
+  /**
+   * The type of the adaptation set. One of the {@link com.google.android.exoplayer2.C}
+   * {@code TRACK_TYPE_*} constants.
+   */
+  public final int type;
+
+  /**
+   * The {@link Representation}s in the adaptation set.
+   */
+  public final List<Representation> representations;
+
+  /**
+   * The accessibility descriptors in the adaptation set.
+   */
+  public final List<SchemeValuePair> accessibilityDescriptors;
+
+  /**
+   * @param id A non-negative identifier for the adaptation set that's unique in the scope of its
+   *     containing period, or {@link #ID_UNSET} if not specified.
+   * @param type The type of the adaptation set. One of the {@link com.google.android.exoplayer2.C}
+   *     {@code TRACK_TYPE_*} constants.
+   * @param representations The {@link Representation}s in the adaptation set.
+   * @param accessibilityDescriptors The accessibility descriptors in the adaptation set.
+   */
+  public AdaptationSet(int id, int type, List<Representation> representations,
+      List<SchemeValuePair> accessibilityDescriptors) {
+    this.id = id;
+    this.type = type;
+    this.representations = Collections.unmodifiableList(representations);
+    this.accessibilityDescriptors = accessibilityDescriptors == null
+        ? Collections.<SchemeValuePair>emptyList()
+        : Collections.unmodifiableList(accessibilityDescriptors);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/DashManifest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.dash.manifest;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a DASH media presentation description (mpd).
+ */
+public class DashManifest {
+
+  public final long availabilityStartTime;
+
+  public final long duration;
+
+  public final long minBufferTime;
+
+  public final boolean dynamic;
+
+  public final long minUpdatePeriod;
+
+  public final long timeShiftBufferDepth;
+
+  public final long suggestedPresentationDelay;
+
+  public final UtcTimingElement utcTiming;
+
+  public final Uri location;
+
+  private final List<Period> periods;
+
+  public DashManifest(long availabilityStartTime, long duration, long minBufferTime,
+      boolean dynamic, long minUpdatePeriod, long timeShiftBufferDepth,
+      long suggestedPresentationDelay, UtcTimingElement utcTiming, Uri location,
+      List<Period> periods) {
+    this.availabilityStartTime = availabilityStartTime;
+    this.duration = duration;
+    this.minBufferTime = minBufferTime;
+    this.dynamic = dynamic;
+    this.minUpdatePeriod = minUpdatePeriod;
+    this.timeShiftBufferDepth = timeShiftBufferDepth;
+    this.suggestedPresentationDelay = suggestedPresentationDelay;
+    this.utcTiming = utcTiming;
+    this.location = location;
+    this.periods = periods == null ? Collections.<Period>emptyList() : periods;
+  }
+
+  public final int getPeriodCount() {
+    return periods.size();
+  }
+
+  public final Period getPeriod(int index) {
+    return periods.get(index);
+  }
+
+  public final long getPeriodDurationMs(int index) {
+    return index == periods.size() - 1
+        ? (duration == C.TIME_UNSET ? C.TIME_UNSET : (duration - periods.get(index).startMs))
+        : (periods.get(index + 1).startMs - periods.get(index).startMs);
+  }
+
+  public final long getPeriodDurationUs(int index) {
+    return C.msToUs(getPeriodDurationMs(index));
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java
@@ -0,0 +1,936 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.dash.manifest;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
+import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentList;
+import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTemplate;
+import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTimelineElement;
+import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;
+import com.google.android.exoplayer2.upstream.ParsingLoadable;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.UriUtil;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.util.XmlPullParserUtil;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+/**
+ * A parser of media presentation description files.
+ */
+public class DashManifestParser extends DefaultHandler
+    implements ParsingLoadable.Parser<DashManifest> {
+
+  private static final String TAG = "MpdParser";
+
+  private static final Pattern FRAME_RATE_PATTERN = Pattern.compile("(\\d+)(?:/(\\d+))?");
+
+  private static final Pattern CEA_608_ACCESSIBILITY_PATTERN = Pattern.compile("CC([1-4])=.*");
+  private static final Pattern CEA_708_ACCESSIBILITY_PATTERN =
+      Pattern.compile("([1-9]|[1-5][0-9]|6[0-3])=.*");
+
+  private final String contentId;
+  private final XmlPullParserFactory xmlParserFactory;
+
+  /**
+   * Equivalent to calling {@code new DashManifestParser(null)}.
+   */
+  public DashManifestParser() {
+    this(null);
+  }
+
+  /**
+   * @param contentId An optional content identifier to include in the parsed manifest.
+   */
+  public DashManifestParser(String contentId) {
+    this.contentId = contentId;
+    try {
+      xmlParserFactory = XmlPullParserFactory.newInstance();
+    } catch (XmlPullParserException e) {
+      throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e);
+    }
+  }
+
+  // MPD parsing.
+
+  @Override
+  public DashManifest parse(Uri uri, InputStream inputStream) throws IOException {
+    try {
+      XmlPullParser xpp = xmlParserFactory.newPullParser();
+      xpp.setInput(inputStream, null);
+      int eventType = xpp.next();
+      if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) {
+        throw new ParserException(
+            "inputStream does not contain a valid media presentation description");
+      }
+      return parseMediaPresentationDescription(xpp, uri.toString());
+    } catch (XmlPullParserException e) {
+      throw new ParserException(e);
+    }
+  }
+
+  protected DashManifest parseMediaPresentationDescription(XmlPullParser xpp,
+      String baseUrl) throws XmlPullParserException, IOException {
+    long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", C.TIME_UNSET);
+    long durationMs = parseDuration(xpp, "mediaPresentationDuration", C.TIME_UNSET);
+    long minBufferTimeMs = parseDuration(xpp, "minBufferTime", C.TIME_UNSET);
+    String typeString = xpp.getAttributeValue(null, "type");
+    boolean dynamic = typeString != null && typeString.equals("dynamic");
+    long minUpdateTimeMs = dynamic ? parseDuration(xpp, "minimumUpdatePeriod", C.TIME_UNSET)
+        : C.TIME_UNSET;
+    long timeShiftBufferDepthMs = dynamic
+        ? parseDuration(xpp, "timeShiftBufferDepth", C.TIME_UNSET) : C.TIME_UNSET;
+    long suggestedPresentationDelayMs = dynamic
+        ? parseDuration(xpp, "suggestedPresentationDelay", C.TIME_UNSET) : C.TIME_UNSET;
+    UtcTimingElement utcTiming = null;
+    Uri location = null;
+
+    List<Period> periods = new ArrayList<>();
+    long nextPeriodStartMs = dynamic ? C.TIME_UNSET : 0;
+    boolean seenEarlyAccessPeriod = false;
+    boolean seenFirstBaseUrl = false;
+    do {
+      xpp.next();
+      if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) {
+        if (!seenFirstBaseUrl) {
+          baseUrl = parseBaseUrl(xpp, baseUrl);
+          seenFirstBaseUrl = true;
+        }
+      } else if (XmlPullParserUtil.isStartTag(xpp, "UTCTiming")) {
+        utcTiming = parseUtcTiming(xpp);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "Location")) {
+        location = Uri.parse(xpp.nextText());
+      } else if (XmlPullParserUtil.isStartTag(xpp, "Period") && !seenEarlyAccessPeriod) {
+        Pair<Period, Long> periodWithDurationMs = parsePeriod(xpp, baseUrl, nextPeriodStartMs);
+        Period period = periodWithDurationMs.first;
+        if (period.startMs == C.TIME_UNSET) {
+          if (dynamic) {
+            // This is an early access period. Ignore it. All subsequent periods must also be
+            // early access.
+            seenEarlyAccessPeriod = true;
+          } else {
+            throw new ParserException("Unable to determine start of period " + periods.size());
+          }
+        } else {
+          long periodDurationMs = periodWithDurationMs.second;
+          nextPeriodStartMs = periodDurationMs == C.TIME_UNSET ? C.TIME_UNSET
+              : (period.startMs + periodDurationMs);
+          periods.add(period);
+        }
+      }
+    } while (!XmlPullParserUtil.isEndTag(xpp, "MPD"));
+
+    if (durationMs == C.TIME_UNSET) {
+      if (nextPeriodStartMs != C.TIME_UNSET) {
+        // If we know the end time of the final period, we can use it as the duration.
+        durationMs = nextPeriodStartMs;
+      } else if (!dynamic) {
+        throw new ParserException("Unable to determine duration of static manifest.");
+      }
+    }
+
+    if (periods.isEmpty()) {
+      throw new ParserException("No periods found.");
+    }
+
+    return buildMediaPresentationDescription(availabilityStartTime, durationMs, minBufferTimeMs,
+        dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming,
+        location, periods);
+  }
+
+  protected DashManifest buildMediaPresentationDescription(long availabilityStartTime,
+      long durationMs, long minBufferTimeMs, boolean dynamic, long minUpdateTimeMs,
+      long timeShiftBufferDepthMs, long suggestedPresentationDelayMs, UtcTimingElement utcTiming,
+      Uri location, List<Period> periods) {
+    return new DashManifest(availabilityStartTime, durationMs, minBufferTimeMs,
+        dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming,
+        location, periods);
+  }
+
+  protected UtcTimingElement parseUtcTiming(XmlPullParser xpp) {
+    String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri");
+    String value = xpp.getAttributeValue(null, "value");
+    return buildUtcTimingElement(schemeIdUri, value);
+  }
+
+  protected UtcTimingElement buildUtcTimingElement(String schemeIdUri, String value) {
+    return new UtcTimingElement(schemeIdUri, value);
+  }
+
+  protected Pair<Period, Long> parsePeriod(XmlPullParser xpp, String baseUrl, long defaultStartMs)
+      throws XmlPullParserException, IOException {
+    String id = xpp.getAttributeValue(null, "id");
+    long startMs = parseDuration(xpp, "start", defaultStartMs);
+    long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET);
+    SegmentBase segmentBase = null;
+    List<AdaptationSet> adaptationSets = new ArrayList<>();
+    boolean seenFirstBaseUrl = false;
+    do {
+      xpp.next();
+      if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) {
+        if (!seenFirstBaseUrl) {
+          baseUrl = parseBaseUrl(xpp, baseUrl);
+          seenFirstBaseUrl = true;
+        }
+      } else if (XmlPullParserUtil.isStartTag(xpp, "AdaptationSet")) {
+        adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase));
+      } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) {
+        segmentBase = parseSegmentBase(xpp, null);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) {
+        segmentBase = parseSegmentList(xpp, null);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) {
+        segmentBase = parseSegmentTemplate(xpp, null);
+      }
+    } while (!XmlPullParserUtil.isEndTag(xpp, "Period"));
+
+    return Pair.create(buildPeriod(id, startMs, adaptationSets), durationMs);
+  }
+
+  protected Period buildPeriod(String id, long startMs, List<AdaptationSet> adaptationSets) {
+    return new Period(id, startMs, adaptationSets);
+  }
+
+  // AdaptationSet parsing.
+
+  protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl,
+      SegmentBase segmentBase) throws XmlPullParserException, IOException {
+    int id = parseInt(xpp, "id", AdaptationSet.ID_UNSET);
+    int contentType = parseContentType(xpp);
+
+    String mimeType = xpp.getAttributeValue(null, "mimeType");
+    String codecs = xpp.getAttributeValue(null, "codecs");
+    int width = parseInt(xpp, "width", Format.NO_VALUE);
+    int height = parseInt(xpp, "height", Format.NO_VALUE);
+    float frameRate = parseFrameRate(xpp, Format.NO_VALUE);
+    int audioChannels = Format.NO_VALUE;
+    int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE);
+    String language = xpp.getAttributeValue(null, "lang");
+    ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();
+    ArrayList<SchemeValuePair> inbandEventStreams = new ArrayList<>();
+    ArrayList<SchemeValuePair> accessibilityDescriptors = new ArrayList<>();
+    List<RepresentationInfo> representationInfos = new ArrayList<>();
+    @C.SelectionFlags int selectionFlags = 0;
+
+    boolean seenFirstBaseUrl = false;
+    do {
+      xpp.next();
+      if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) {
+        if (!seenFirstBaseUrl) {
+          baseUrl = parseBaseUrl(xpp, baseUrl);
+          seenFirstBaseUrl = true;
+        }
+      } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) {
+        SchemeData contentProtection = parseContentProtection(xpp);
+        if (contentProtection != null) {
+          drmSchemeDatas.add(contentProtection);
+        }
+      } else if (XmlPullParserUtil.isStartTag(xpp, "ContentComponent")) {
+        language = checkLanguageConsistency(language, xpp.getAttributeValue(null, "lang"));
+        contentType = checkContentTypeConsistency(contentType, parseContentType(xpp));
+      } else if (XmlPullParserUtil.isStartTag(xpp, "Role")) {
+        selectionFlags |= parseRole(xpp);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) {
+        audioChannels = parseAudioChannelConfiguration(xpp);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "Accessibility")) {
+        accessibilityDescriptors.add(parseAccessibility(xpp));
+      } else if (XmlPullParserUtil.isStartTag(xpp, "Representation")) {
+        RepresentationInfo representationInfo = parseRepresentation(xpp, baseUrl, mimeType, codecs,
+            width, height, frameRate, audioChannels, audioSamplingRate, language,
+            selectionFlags, accessibilityDescriptors, segmentBase);
+        contentType = checkContentTypeConsistency(contentType,
+            getContentType(representationInfo.format));
+        representationInfos.add(representationInfo);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) {
+        segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) {
+        segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) {
+        segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) {
+        inbandEventStreams.add(parseInbandEventStream(xpp));
+      } else if (XmlPullParserUtil.isStartTag(xpp)) {
+        parseAdaptationSetChild(xpp);
+      }
+    } while (!XmlPullParserUtil.isEndTag(xpp, "AdaptationSet"));
+
+    // Build the representations.
+    List<Representation> representations = new ArrayList<>(representationInfos.size());
+    for (int i = 0; i < representationInfos.size(); i++) {
+      representations.add(buildRepresentation(representationInfos.get(i), contentId,
+          drmSchemeDatas, inbandEventStreams));
+    }
+
+    return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors);
+  }
+
+  protected AdaptationSet buildAdaptationSet(int id, int contentType,
+      List<Representation> representations, List<SchemeValuePair> accessibilityDescriptors) {
+    return new AdaptationSet(id, contentType, representations, accessibilityDescriptors);
+  }
+
+  protected int parseContentType(XmlPullParser xpp) {
+    String contentType = xpp.getAttributeValue(null, "contentType");
+    return TextUtils.isEmpty(contentType) ? C.TRACK_TYPE_UNKNOWN
+        : MimeTypes.BASE_TYPE_AUDIO.equals(contentType) ? C.TRACK_TYPE_AUDIO
+        : MimeTypes.BASE_TYPE_VIDEO.equals(contentType) ? C.TRACK_TYPE_VIDEO
+        : MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? C.TRACK_TYPE_TEXT
+        : C.TRACK_TYPE_UNKNOWN;
+  }
+
+  protected int getContentType(Format format) {
+    String sampleMimeType = format.sampleMimeType;
+    if (TextUtils.isEmpty(sampleMimeType)) {
+      return C.TRACK_TYPE_UNKNOWN;
+    } else if (MimeTypes.isVideo(sampleMimeType)) {
+      return C.TRACK_TYPE_VIDEO;
+    } else if (MimeTypes.isAudio(sampleMimeType)) {
+      return C.TRACK_TYPE_AUDIO;
+    } else if (mimeTypeIsRawText(sampleMimeType)) {
+      return C.TRACK_TYPE_TEXT;
+    }
+    return C.TRACK_TYPE_UNKNOWN;
+  }
+
+  /**
+   * Parses a ContentProtection element.
+   *
+   * @param xpp The parser from which to read.
+   * @throws XmlPullParserException If an error occurs parsing the element.
+   * @throws IOException If an error occurs reading the element.
+   * @return {@link SchemeData} parsed from the ContentProtection element, or null if the element is
+   *     unsupported.
+   */
+  protected SchemeData parseContentProtection(XmlPullParser xpp) throws XmlPullParserException,
+      IOException {
+    byte[] data = null;
+    UUID uuid = null;
+    boolean seenPsshElement = false;
+    boolean requiresSecureDecoder = false;
+    do {
+      xpp.next();
+      // The cenc:pssh element is defined in 23001-7:2015.
+      if (XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") && xpp.next() == XmlPullParser.TEXT) {
+        seenPsshElement = true;
+        data = Base64.decode(xpp.getText(), Base64.DEFAULT);
+        uuid = PsshAtomUtil.parseUuid(data);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "widevine:license")) {
+        String robustnessLevel = xpp.getAttributeValue(null, "robustness_level");
+        requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW");
+      }
+    } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection"));
+    if (!seenPsshElement) {
+      return null;
+    } else if (uuid != null) {
+      return new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder);
+    } else {
+      Log.w(TAG, "Skipped unsupported ContentProtection element");
+      return null;
+    }
+  }
+
+  /**
+   * Parses an InbandEventStream element.
+   *
+   * @param xpp The parser from which to read.
+   * @throws XmlPullParserException If an error occurs parsing the element.
+   * @throws IOException If an error occurs reading the element.
+   * @return A {@link SchemeValuePair} parsed from the element.
+   */
+  protected SchemeValuePair parseInbandEventStream(XmlPullParser xpp)
+      throws XmlPullParserException, IOException {
+    return parseSchemeValuePair(xpp, "InbandEventStream");
+  }
+
+  /**
+   * Parses an Accessibility element.
+   *
+   * @param xpp The parser from which to read.
+   * @throws XmlPullParserException If an error occurs parsing the element.
+   * @throws IOException If an error occurs reading the element.
+   * @return A {@link SchemeValuePair} parsed from the element.
+   */
+  protected SchemeValuePair parseAccessibility(XmlPullParser xpp)
+      throws XmlPullParserException, IOException {
+    return parseSchemeValuePair(xpp, "Accessibility");
+  }
+
+  /**
+   * Parses a Role element.
+   *
+   * @param xpp The parser from which to read.
+   * @throws XmlPullParserException If an error occurs parsing the element.
+   * @throws IOException If an error occurs reading the element.
+   * @return {@link C.SelectionFlags} parsed from the element.
+   */
+  protected int parseRole(XmlPullParser xpp) throws XmlPullParserException, IOException {
+    String schemeIdUri = parseString(xpp, "schemeIdUri", null);
+    String value = parseString(xpp, "value", null);
+    do {
+      xpp.next();
+    } while (!XmlPullParserUtil.isEndTag(xpp, "Role"));
+    return "urn:mpeg:dash:role:2011".equals(schemeIdUri) && "main".equals(value)
+        ? C.SELECTION_FLAG_DEFAULT : 0;
+  }
+
+  /**
+   * Parses children of AdaptationSet elements not specifically parsed elsewhere.
+   *
+   * @param xpp The XmpPullParser from which the AdaptationSet child should be parsed.
+   * @throws XmlPullParserException If an error occurs parsing the element.
+   * @throws IOException If an error occurs reading the element.
+   */
+  protected void parseAdaptationSetChild(XmlPullParser xpp)
+      throws XmlPullParserException, IOException {
+    // pass
+  }
+
+  // Representation parsing.
+
+  protected RepresentationInfo parseRepresentation(XmlPullParser xpp, String baseUrl,
+      String adaptationSetMimeType, String adaptationSetCodecs, int adaptationSetWidth,
+      int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels,
+      int adaptationSetAudioSamplingRate, String adaptationSetLanguage,
+      @C.SelectionFlags int adaptationSetSelectionFlags,
+      List<SchemeValuePair> adaptationSetAccessibilityDescriptors, SegmentBase segmentBase)
+      throws XmlPullParserException, IOException {
+    String id = xpp.getAttributeValue(null, "id");
+    int bandwidth = parseInt(xpp, "bandwidth", Format.NO_VALUE);
+
+    String mimeType = parseString(xpp, "mimeType", adaptationSetMimeType);
+    String codecs = parseString(xpp, "codecs", adaptationSetCodecs);
+    int width = parseInt(xpp, "width", adaptationSetWidth);
+    int height = parseInt(xpp, "height", adaptationSetHeight);
+    float frameRate = parseFrameRate(xpp, adaptationSetFrameRate);
+    int audioChannels = adaptationSetAudioChannels;
+    int audioSamplingRate = parseInt(xpp, "audioSamplingRate", adaptationSetAudioSamplingRate);
+    ArrayList<SchemeData> drmSchemeDatas = new ArrayList<>();
+    ArrayList<SchemeValuePair> inbandEventStreams = new ArrayList<>();
+
+    boolean seenFirstBaseUrl = false;
+    do {
+      xpp.next();
+      if (XmlPullParserUtil.isStartTag(xpp, "BaseURL")) {
+        if (!seenFirstBaseUrl) {
+          baseUrl = parseBaseUrl(xpp, baseUrl);
+          seenFirstBaseUrl = true;
+        }
+      } else if (XmlPullParserUtil.isStartTag(xpp, "AudioChannelConfiguration")) {
+        audioChannels = parseAudioChannelConfiguration(xpp);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentBase")) {
+        segmentBase = parseSegmentBase(xpp, (SingleSegmentBase) segmentBase);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentList")) {
+        segmentBase = parseSegmentList(xpp, (SegmentList) segmentBase);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) {
+        segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) {
+        SchemeData contentProtection = parseContentProtection(xpp);
+        if (contentProtection != null) {
+          drmSchemeDatas.add(contentProtection);
+        }
+      } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) {
+        inbandEventStreams.add(parseInbandEventStream(xpp));
+      }
+    } while (!XmlPullParserUtil.isEndTag(xpp, "Representation"));
+
+    Format format = buildFormat(id, mimeType, width, height, frameRate, audioChannels,
+        audioSamplingRate, bandwidth, adaptationSetLanguage, adaptationSetSelectionFlags,
+        adaptationSetAccessibilityDescriptors, codecs);
+    segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase();
+
+    return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeDatas, inbandEventStreams);
+  }
+
+  protected Format buildFormat(String id, String containerMimeType, int width, int height,
+      float frameRate, int audioChannels, int audioSamplingRate, int bitrate, String language,
+      @C.SelectionFlags int selectionFlags, List<SchemeValuePair> accessibilityDescriptors,
+      String codecs) {
+    String sampleMimeType = getSampleMimeType(containerMimeType, codecs);
+    if (sampleMimeType != null) {
+      if (MimeTypes.isVideo(sampleMimeType)) {
+        return Format.createVideoContainerFormat(id, containerMimeType, sampleMimeType, codecs,
+            bitrate, width, height, frameRate, null, selectionFlags);
+      } else if (MimeTypes.isAudio(sampleMimeType)) {
+        return Format.createAudioContainerFormat(id, containerMimeType, sampleMimeType, codecs,
+            bitrate, audioChannels, audioSamplingRate, null, selectionFlags, language);
+      } else if (mimeTypeIsRawText(sampleMimeType)) {
+        int accessibilityChannel;
+        if (MimeTypes.APPLICATION_CEA608.equals(sampleMimeType)) {
+          accessibilityChannel = parseCea608AccessibilityChannel(accessibilityDescriptors);
+        } else if (MimeTypes.APPLICATION_CEA708.equals(sampleMimeType)) {
+          accessibilityChannel = parseCea708AccessibilityChannel(accessibilityDescriptors);
+        } else {
+          accessibilityChannel = Format.NO_VALUE;
+        }
+        return Format.createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs,
+            bitrate, selectionFlags, language, accessibilityChannel);
+      }
+    }
+    return Format.createContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate,
+        selectionFlags, language);
+  }
+
+  protected Representation buildRepresentation(RepresentationInfo representationInfo,
+      String contentId, ArrayList<SchemeData> extraDrmSchemeDatas,
+      ArrayList<SchemeValuePair> extraInbandEventStreams) {
+    Format format = representationInfo.format;
+    ArrayList<SchemeData> drmSchemeDatas = representationInfo.drmSchemeDatas;
+    drmSchemeDatas.addAll(extraDrmSchemeDatas);
+    if (!drmSchemeDatas.isEmpty()) {
+      format = format.copyWithDrmInitData(new DrmInitData(drmSchemeDatas));
+    }
+    ArrayList<SchemeValuePair> inbandEventStremas = representationInfo.inbandEventStreams;
+    inbandEventStremas.addAll(extraInbandEventStreams);
+    return Representation.newInstance(contentId, Representation.REVISION_ID_DEFAULT, format,
+        representationInfo.baseUrl, representationInfo.segmentBase, inbandEventStremas);
+  }
+
+  // SegmentBase, SegmentList and SegmentTemplate parsing.
+
+  protected SingleSegmentBase parseSegmentBase(XmlPullParser xpp, SingleSegmentBase parent)
+      throws XmlPullParserException, IOException {
+
+    long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
+    long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
+        parent != null ? parent.presentationTimeOffset : 0);
+
+    long indexStart = parent != null ? parent.indexStart : 0;
+    long indexLength = parent != null ? parent.indexLength : 0;
+    String indexRangeText = xpp.getAttributeValue(null, "indexRange");
+    if (indexRangeText != null) {
+      String[] indexRange = indexRangeText.split("-");
+      indexStart = Long.parseLong(indexRange[0]);
+      indexLength = Long.parseLong(indexRange[1]) - indexStart + 1;
+    }
+
+    RangedUri initialization = parent != null ? parent.initialization : null;
+    do {
+      xpp.next();
+      if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) {
+        initialization = parseInitialization(xpp);
+      }
+    } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentBase"));
+
+    return buildSingleSegmentBase(initialization, timescale, presentationTimeOffset, indexStart,
+        indexLength);
+  }
+
+  protected SingleSegmentBase buildSingleSegmentBase(RangedUri initialization, long timescale,
+      long presentationTimeOffset, long indexStart, long indexLength) {
+    return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, indexStart,
+        indexLength);
+  }
+
+  protected SegmentList parseSegmentList(XmlPullParser xpp, SegmentList parent)
+      throws XmlPullParserException, IOException {
+
+    long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
+    long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
+        parent != null ? parent.presentationTimeOffset : 0);
+    long duration = parseLong(xpp, "duration", parent != null ? parent.duration : C.TIME_UNSET);
+    int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 1);
+
+    RangedUri initialization = null;
+    List<SegmentTimelineElement> timeline = null;
+    List<RangedUri> segments = null;
+
+    do {
+      xpp.next();
+      if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) {
+        initialization = parseInitialization(xpp);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) {
+        timeline = parseSegmentTimeline(xpp);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentURL")) {
+        if (segments == null) {
+          segments = new ArrayList<>();
+        }
+        segments.add(parseSegmentUrl(xpp));
+      }
+    } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentList"));
+
+    if (parent != null) {
+      initialization = initialization != null ? initialization : parent.initialization;
+      timeline = timeline != null ? timeline : parent.segmentTimeline;
+      segments = segments != null ? segments : parent.mediaSegments;
+    }
+
+    return buildSegmentList(initialization, timescale, presentationTimeOffset,
+        startNumber, duration, timeline, segments);
+  }
+
+  protected SegmentList buildSegmentList(RangedUri initialization, long timescale,
+      long presentationTimeOffset, int startNumber, long duration,
+      List<SegmentTimelineElement> timeline, List<RangedUri> segments) {
+    return new SegmentList(initialization, timescale, presentationTimeOffset,
+        startNumber, duration, timeline, segments);
+  }
+
+  protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, SegmentTemplate parent)
+      throws XmlPullParserException, IOException {
+    long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
+    long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
+        parent != null ? parent.presentationTimeOffset : 0);
+    long duration = parseLong(xpp, "duration", parent != null ? parent.duration : C.TIME_UNSET);
+    int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 1);
+    UrlTemplate mediaTemplate = parseUrlTemplate(xpp, "media",
+        parent != null ? parent.mediaTemplate : null);
+    UrlTemplate initializationTemplate = parseUrlTemplate(xpp, "initialization",
+        parent != null ? parent.initializationTemplate : null);
+
+    RangedUri initialization = null;
+    List<SegmentTimelineElement> timeline = null;
+
+    do {
+      xpp.next();
+      if (XmlPullParserUtil.isStartTag(xpp, "Initialization")) {
+        initialization = parseInitialization(xpp);
+      } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTimeline")) {
+        timeline = parseSegmentTimeline(xpp);
+      }
+    } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentTemplate"));
+
+    if (parent != null) {
+      initialization = initialization != null ? initialization : parent.initialization;
+      timeline = timeline != null ? timeline : parent.segmentTimeline;
+    }
+
+    return buildSegmentTemplate(initialization, timescale, presentationTimeOffset,
+        startNumber, duration, timeline, initializationTemplate, mediaTemplate);
+  }
+
+  protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale,
+      long presentationTimeOffset, int startNumber, long duration,
+      List<SegmentTimelineElement> timeline, UrlTemplate initializationTemplate,
+      UrlTemplate mediaTemplate) {
+    return new SegmentTemplate(initialization, timescale, presentationTimeOffset,
+        startNumber, duration, timeline, initializationTemplate, mediaTemplate);
+  }
+
+  protected List<SegmentTimelineElement> parseSegmentTimeline(XmlPullParser xpp)
+      throws XmlPullParserException, IOException {
+    List<SegmentTimelineElement> segmentTimeline = new ArrayList<>();
+    long elapsedTime = 0;
+    do {
+      xpp.next();
+      if (XmlPullParserUtil.isStartTag(xpp, "S")) {
+        elapsedTime = parseLong(xpp, "t", elapsedTime);
+        long duration = parseLong(xpp, "d", C.TIME_UNSET);
+        int count = 1 + parseInt(xpp, "r", 0);
+        for (int i = 0; i < count; i++) {
+          segmentTimeline.add(buildSegmentTimelineElement(elapsedTime, duration));
+          elapsedTime += duration;
+        }
+      }
+    } while (!XmlPullParserUtil.isEndTag(xpp, "SegmentTimeline"));
+    return segmentTimeline;
+  }
+
+  protected SegmentTimelineElement buildSegmentTimelineElement(long elapsedTime, long duration) {
+    return new SegmentTimelineElement(elapsedTime, duration);
+  }
+
+  protected UrlTemplate parseUrlTemplate(XmlPullParser xpp, String name,
+      UrlTemplate defaultValue) {
+    String valueString = xpp.getAttributeValue(null, name);
+    if (valueString != null) {
+      return UrlTemplate.compile(valueString);
+    }
+    return defaultValue;
+  }
+
+  protected RangedUri parseInitialization(XmlPullParser xpp) {
+    return parseRangedUrl(xpp, "sourceURL", "range");
+  }
+
+  protected RangedUri parseSegmentUrl(XmlPullParser xpp) {
+    return parseRangedUrl(xpp, "media", "mediaRange");
+  }
+
+  protected RangedUri parseRangedUrl(XmlPullParser xpp, String urlAttribute,
+      String rangeAttribute) {
+    String urlText = xpp.getAttributeValue(null, urlAttribute);
+    long rangeStart = 0;
+    long rangeLength = C.LENGTH_UNSET;
+    String rangeText = xpp.getAttributeValue(null, rangeAttribute);
+    if (rangeText != null) {
+      String[] rangeTextArray = rangeText.split("-");
+      rangeStart = Long.parseLong(rangeTextArray[0]);
+      if (rangeTextArray.length == 2) {
+        rangeLength = Long.parseLong(rangeTextArray[1]) - rangeStart + 1;
+      }
+    }
+    return buildRangedUri(urlText, rangeStart, rangeLength);
+  }
+
+  protected RangedUri buildRangedUri(String urlText, long rangeStart, long rangeLength) {
+    return new RangedUri(urlText, rangeStart, rangeLength);
+  }
+
+  // AudioChannelConfiguration parsing.
+
+  protected int parseAudioChannelConfiguration(XmlPullParser xpp)
+      throws XmlPullParserException, IOException {
+    String schemeIdUri = parseString(xpp, "schemeIdUri", null);
+    int audioChannels = "urn:mpeg:dash:23003:3:audio_channel_configuration:2011".equals(schemeIdUri)
+        ? parseInt(xpp, "value", Format.NO_VALUE) : Format.NO_VALUE;
+    do {
+      xpp.next();
+    } while (!XmlPullParserUtil.isEndTag(xpp, "AudioChannelConfiguration"));
+    return audioChannels;
+  }
+
+  // Utility methods.
+
+  /**
+   * Derives a sample mimeType from a container mimeType and codecs attribute.
+   *
+   * @param containerMimeType The mimeType of the container.
+   * @param codecs The codecs attribute.
+   * @return The derived sample mimeType, or null if it could not be derived.
+   */
+  private static String getSampleMimeType(String containerMimeType, String codecs) {
+    if (MimeTypes.isAudio(containerMimeType)) {
+      return MimeTypes.getAudioMediaMimeType(codecs);
+    } else if (MimeTypes.isVideo(containerMimeType)) {
+      return MimeTypes.getVideoMediaMimeType(codecs);
+    } else if (mimeTypeIsRawText(containerMimeType)) {
+      return containerMimeType;
+    } else if (MimeTypes.APPLICATION_MP4.equals(containerMimeType)) {
+      if ("stpp".equals(codecs)) {
+        return MimeTypes.APPLICATION_TTML;
+      } else if ("wvtt".equals(codecs)) {
+        return MimeTypes.APPLICATION_MP4VTT;
+      }
+    } else if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) {
+      if (codecs != null) {
+        if (codecs.contains("cea708")) {
+          return MimeTypes.APPLICATION_CEA708;
+        } else if (codecs.contains("eia608") || codecs.contains("cea608")) {
+          return MimeTypes.APPLICATION_CEA608;
+        }
+      }
+      return null;
+    }
+    return null;
+  }
+
+  /**
+   * Returns whether a mimeType is a text sample mimeType.
+   *
+   * @param mimeType The mimeType.
+   * @return Whether the mimeType is a text sample mimeType.
+   */
+  private static boolean mimeTypeIsRawText(String mimeType) {
+    return MimeTypes.isText(mimeType)
+        || MimeTypes.APPLICATION_TTML.equals(mimeType)
+        || MimeTypes.APPLICATION_MP4VTT.equals(mimeType)
+        || MimeTypes.APPLICATION_CEA708.equals(mimeType)
+        || MimeTypes.APPLICATION_CEA608.equals(mimeType);
+  }
+
+  /**
+   * Checks two languages for consistency, returning the consistent language, or throwing an
+   * {@link IllegalStateException} if the languages are inconsistent.
+   * <p>
+   * Two languages are consistent if they are equal, or if one is null.
+   *
+   * @param firstLanguage The first language.
+   * @param secondLanguage The second language.
+   * @return The consistent language.
+   */
+  private static String checkLanguageConsistency(String firstLanguage, String secondLanguage) {
+    if (firstLanguage == null) {
+      return secondLanguage;
+    } else if (secondLanguage == null) {
+      return firstLanguage;
+    } else {
+      Assertions.checkState(firstLanguage.equals(secondLanguage));
+      return firstLanguage;
+    }
+  }
+
+  /**
+   * Checks two adaptation set content types for consistency, returning the consistent type, or
+   * throwing an {@link IllegalStateException} if the types are inconsistent.
+   * <p>
+   * Two types are consistent if they are equal, or if one is {@link C#TRACK_TYPE_UNKNOWN}.
+   * Where one of the types is {@link C#TRACK_TYPE_UNKNOWN}, the other is returned.
+   *
+   * @param firstType The first type.
+   * @param secondType The second type.
+   * @return The consistent type.
+   */
+  private static int checkContentTypeConsistency(int firstType, int secondType) {
+    if (firstType == C.TRACK_TYPE_UNKNOWN) {
+      return secondType;
+    } else if (secondType == C.TRACK_TYPE_UNKNOWN) {
+      return firstType;
+    } else {
+      Assertions.checkState(firstType == secondType);
+      return firstType;
+    }
+  }
+
+  /**
+   * Parses a {@link SchemeValuePair} from an element.
+   *
+   * @param xpp The parser from which to read.
+   * @param tag The tag of the element being parsed.
+   * @throws XmlPullParserException If an error occurs parsing the element.
+   * @throws IOException If an error occurs reading the element.
+   * @return The parsed {@link SchemeValuePair}.
+   */
+  protected static SchemeValuePair parseSchemeValuePair(XmlPullParser xpp, String tag)
+      throws XmlPullParserException, IOException {
+    String schemeIdUri = parseString(xpp, "schemeIdUri", null);
+    String value = parseString(xpp, "value", null);
+    do {
+      xpp.next();
+    } while (!XmlPullParserUtil.isEndTag(xpp, tag));
+    return new SchemeValuePair(schemeIdUri, value);
+  }
+
+  protected static int parseCea608AccessibilityChannel(
+      List<SchemeValuePair> accessibilityDescriptors) {
+    for (int i = 0; i < accessibilityDescriptors.size(); i++) {
+      SchemeValuePair descriptor = accessibilityDescriptors.get(i);
+      if ("urn:scte:dash:cc:cea-608:2015".equals(descriptor.schemeIdUri)
+          && descriptor.value != null) {
+        Matcher accessibilityValueMatcher = CEA_608_ACCESSIBILITY_PATTERN.matcher(descriptor.value);
+        if (accessibilityValueMatcher.matches()) {
+          return Integer.parseInt(accessibilityValueMatcher.group(1));
+        } else {
+          Log.w(TAG, "Unable to parse CEA-608 channel number from: " + descriptor.value);
+        }
+      }
+    }
+    return Format.NO_VALUE;
+  }
+
+  protected static int parseCea708AccessibilityChannel(
+      List<SchemeValuePair> accessibilityDescriptors) {
+    for (int i = 0; i < accessibilityDescriptors.size(); i++) {
+      SchemeValuePair descriptor = accessibilityDescriptors.get(i);
+      if ("urn:scte:dash:cc:cea-708:2015".equals(descriptor.schemeIdUri)
+          && descriptor.value != null) {
+        Matcher accessibilityValueMatcher = CEA_708_ACCESSIBILITY_PATTERN.matcher(descriptor.value);
+        if (accessibilityValueMatcher.matches()) {
+          return Integer.parseInt(accessibilityValueMatcher.group(1));
+        } else {
+          Log.w(TAG, "Unable to parse CEA-708 service block number from: " + descriptor.value);
+        }
+      }
+    }
+    return Format.NO_VALUE;
+  }
+
+  protected static float parseFrameRate(XmlPullParser xpp, float defaultValue) {
+    float frameRate = defaultValue;
+    String frameRateAttribute = xpp.getAttributeValue(null, "frameRate");
+    if (frameRateAttribute != null) {
+      Matcher frameRateMatcher = FRAME_RATE_PATTERN.matcher(frameRateAttribute);
+      if (frameRateMatcher.matches()) {
+        int numerator = Integer.parseInt(frameRateMatcher.group(1));
+        String denominatorString = frameRateMatcher.group(2);
+        if (!TextUtils.isEmpty(denominatorString)) {
+          frameRate = (float) numerator / Integer.parseInt(denominatorString);
+        } else {
+          frameRate = numerator;
+        }
+      }
+    }
+    return frameRate;
+  }
+
+  protected static long parseDuration(XmlPullParser xpp, String name, long defaultValue) {
+    String value = xpp.getAttributeValue(null, name);
+    if (value == null) {
+      return defaultValue;
+    } else {
+      return Util.parseXsDuration(value);
+    }
+  }
+
+  protected static long parseDateTime(XmlPullParser xpp, String name, long defaultValue)
+      throws ParserException {
+    String value = xpp.getAttributeValue(null, name);
+    if (value == null) {
+      return defaultValue;
+    } else {
+      return Util.parseXsDateTime(value);
+    }
+  }
+
+  protected static String parseBaseUrl(XmlPullParser xpp, String parentBaseUrl)
+      throws XmlPullParserException, IOException {
+    xpp.next();
+    return UriUtil.resolve(parentBaseUrl, xpp.getText());
+  }
+
+  protected static int parseInt(XmlPullParser xpp, String name, int defaultValue) {
+    String value = xpp.getAttributeValue(null, name);
+    return value == null ? defaultValue : Integer.parseInt(value);
+  }
+
+  protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) {
+    String value = xpp.getAttributeValue(null, name);
+    return value == null ? defaultValue : Long.parseLong(value);
+  }
+
+  protected static String parseString(XmlPullParser xpp, String name, String defaultValue) {
+    String value = xpp.getAttributeValue(null, name);
+    return value == null ? defaultValue : value;
+  }
+
+  private static final class RepresentationInfo {
+
+    public final Format format;
+    public final String baseUrl;
+    public final SegmentBase segmentBase;
+    public final ArrayList<SchemeData> drmSchemeDatas;
+    public final ArrayList<SchemeValuePair> inbandEventStreams;
+
+    public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase,
+        ArrayList<SchemeData> drmSchemeDatas, ArrayList<SchemeValuePair> inbandEventStreams) {
+      this.format = format;
+      this.baseUrl = baseUrl;
+      this.segmentBase = segmentBase;
+      this.drmSchemeDatas = drmSchemeDatas;
+      this.inbandEventStreams = inbandEventStreams;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/Period.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.dash.manifest;
+
+import com.google.android.exoplayer2.C;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Encapsulates media content components over a contiguous period of time.
+ */
+public class Period {
+
+  /**
+   * The period identifier, if one exists.
+   */
+  public final String id;
+
+  /**
+   * The start time of the period in milliseconds.
+   */
+  public final long startMs;
+
+  /**
+   * The adaptation sets belonging to the period.
+   */
+  public final List<AdaptationSet> adaptationSets;
+
+  /**
+   * @param id The period identifier. May be null.
+   * @param startMs The start time of the period in milliseconds.
+   * @param adaptationSets The adaptation sets belonging to the period.
+   */
+  public Period(String id, long startMs, List<AdaptationSet> adaptationSets) {
+    this.id = id;
+    this.startMs = startMs;
+    this.adaptationSets = Collections.unmodifiableList(adaptationSets);
+  }
+
+  /**
+   * Returns the index of the first adaptation set of a given type, or {@link C#INDEX_UNSET} if no
+   * adaptation set of the specified type exists.
+   *
+   * @param type An adaptation set type.
+   * @return The index of the first adaptation set of the specified type, or {@link C#INDEX_UNSET}.
+   */
+  public int getAdaptationSetIndex(int type) {
+    int adaptationCount = adaptationSets.size();
+    for (int i = 0; i < adaptationCount; i++) {
+      if (adaptationSets.get(i).type == type) {
+        return i;
+      }
+    }
+    return C.INDEX_UNSET;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/RangedUri.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 com.google.android.exoplayer2.source.dash.manifest;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.UriUtil;
+
+/**
+ * Defines a range of data located at a reference uri.
+ */
+public final class RangedUri {
+
+  /**
+   * The (zero based) index of the first byte of the range.
+   */
+  public final long start;
+
+  /**
+   * The length of the range, or {@link C#LENGTH_UNSET} to indicate that the range is unbounded.
+   */
+  public final long length;
+
+  private final String referenceUri;
+
+  private int hashCode;
+
+  /**
+   * Constructs an ranged uri.
+   *
+   * @param referenceUri The reference uri.
+   * @param start The (zero based) index of the first byte of the range.
+   * @param length The length of the range, or {@link C#LENGTH_UNSET} to indicate that the range is
+   *     unbounded.
+   */
+  public RangedUri(String referenceUri, long start, long length) {
+    this.referenceUri = referenceUri == null ? "" : referenceUri;
+    this.start = start;
+    this.length = length;
+  }
+
+  /**
+   * Returns the resolved {@link Uri} represented by the instance.
+   *
+   * @param baseUri The base Uri.
+   * @return The {@link Uri} represented by the instance.
+   */
+  public Uri resolveUri(String baseUri) {
+    return UriUtil.resolveToUri(baseUri, referenceUri);
+  }
+
+  /**
+   * Returns the resolved uri represented by the instance as a string.
+   *
+   * @param baseUri The base Uri.
+   * @return The uri represented by the instance.
+   */
+  public String resolveUriString(String baseUri) {
+    return UriUtil.resolve(baseUri, referenceUri);
+  }
+
+  /**
+   * Attempts to merge this {@link RangedUri} with another and an optional common base uri.
+   * <p>
+   * A merge is successful if both instances define the same {@link Uri} after resolution with the
+   * base uri, and if one starts the byte after the other ends, forming a contiguous region with
+   * no overlap.
+   * <p>
+   * If {@code other} is null then the merge is considered unsuccessful, and null is returned.
+   *
+   * @param other The {@link RangedUri} to merge.
+   * @param baseUri The optional base Uri.
+   * @return The merged {@link RangedUri} if the merge was successful. Null otherwise.
+   */
+  public RangedUri attemptMerge(RangedUri other, String baseUri) {
+    final String resolvedUri = resolveUriString(baseUri);
+    if (other == null || !resolvedUri.equals(other.resolveUriString(baseUri))) {
+      return null;
+    } else if (length != C.LENGTH_UNSET && start + length == other.start) {
+      return new RangedUri(resolvedUri, start,
+          other.length == C.LENGTH_UNSET ? C.LENGTH_UNSET : length + other.length);
+    } else if (other.length != C.LENGTH_UNSET && other.start + other.length == start) {
+      return new RangedUri(resolvedUri, other.start,
+          length == C.LENGTH_UNSET ? C.LENGTH_UNSET : other.length + length);
+    } else {
+      return null;
+    }
+  }
+
+  @Override
+  public int hashCode() {
+    if (hashCode == 0) {
+      int result = 17;
+      result = 31 * result + (int) start;
+      result = 31 * result + (int) length;
+      result = 31 * result + referenceUri.hashCode();
+      hashCode = result;
+    }
+    return hashCode;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null || getClass() != obj.getClass()) {
+      return false;
+    }
+    RangedUri other = (RangedUri) obj;
+    return this.start == other.start
+        && this.length == other.length
+        && referenceUri.equals(other.referenceUri);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/Representation.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 com.google.android.exoplayer2.source.dash.manifest;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.dash.DashSegmentIndex;
+import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.MultiSegmentBase;
+import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A DASH representation.
+ */
+public abstract class Representation {
+
+  /**
+   * A default value for {@link #revisionId}.
+   */
+  public static final long REVISION_ID_DEFAULT = -1;
+
+  /**
+   * Identifies the piece of content to which this {@link Representation} belongs.
+   * <p>
+   * For example, all {@link Representation}s belonging to a video should have the same content
+   * identifier that uniquely identifies that video.
+   */
+  public final String contentId;
+  /**
+   * Identifies the revision of the content.
+   * <p>
+   * If the media for a given ({@link #contentId} can change over time without a change to the
+   * {@link #format}'s {@link Format#id} (e.g. as a result of re-encoding the media with an
+   * updated encoder), then this identifier must uniquely identify the revision of the media. The
+   * timestamp at which the media was encoded is often a suitable.
+   */
+  public final long revisionId;
+  /**
+   * The format of the representation.
+   */
+  public final Format format;
+  /**
+   * The base URL of the representation.
+   */
+  public final String baseUrl;
+  /**
+   * The offset of the presentation timestamps in the media stream relative to media time.
+   */
+  public final long presentationTimeOffsetUs;
+  /**
+   * The in-band event streams in the representation. Never null, but may be empty.
+   */
+  public final List<SchemeValuePair> inbandEventStreams;
+
+  private final RangedUri initializationUri;
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param contentId Identifies the piece of content to which this representation belongs.
+   * @param revisionId Identifies the revision of the content.
+   * @param format The format of the representation.
+   * @param baseUrl The base URL.
+   * @param segmentBase A segment base element for the representation.
+   * @return The constructed instance.
+   */
+  public static Representation newInstance(String contentId, long revisionId, Format format,
+      String baseUrl, SegmentBase segmentBase) {
+    return newInstance(contentId, revisionId, format, baseUrl, segmentBase, null);
+  }
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param contentId Identifies the piece of content to which this representation belongs.
+   * @param revisionId Identifies the revision of the content.
+   * @param format The format of the representation.
+   * @param baseUrl The base URL.
+   * @param segmentBase A segment base element for the representation.
+   * @param inbandEventStreams The in-band event streams in the representation. May be null.
+   * @return The constructed instance.
+   */
+  public static Representation newInstance(String contentId, long revisionId, Format format,
+      String baseUrl, SegmentBase segmentBase, List<SchemeValuePair> inbandEventStreams) {
+    return newInstance(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams,
+        null);
+  }
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param contentId Identifies the piece of content to which this representation belongs.
+   * @param revisionId Identifies the revision of the content.
+   * @param format The format of the representation.
+   * @param baseUrl The base URL of the representation.
+   * @param segmentBase A segment base element for the representation.
+   * @param inbandEventStreams The in-band event streams in the representation. May be null.
+   * @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null. This
+   *     parameter is ignored if {@code segmentBase} consists of multiple segments.
+   * @return The constructed instance.
+   */
+  public static Representation newInstance(String contentId, long revisionId, Format format,
+      String baseUrl, SegmentBase segmentBase, List<SchemeValuePair> inbandEventStreams,
+      String customCacheKey) {
+    if (segmentBase instanceof SingleSegmentBase) {
+      return new SingleSegmentRepresentation(contentId, revisionId, format, baseUrl,
+          (SingleSegmentBase) segmentBase, inbandEventStreams, customCacheKey, C.LENGTH_UNSET);
+    } else if (segmentBase instanceof MultiSegmentBase) {
+      return new MultiSegmentRepresentation(contentId, revisionId, format, baseUrl,
+          (MultiSegmentBase) segmentBase, inbandEventStreams);
+    } else {
+      throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or "
+          + "MultiSegmentBase");
+    }
+  }
+
+  private Representation(String contentId, long revisionId, Format format, String baseUrl,
+      SegmentBase segmentBase, List<SchemeValuePair> inbandEventStreams) {
+    this.contentId = contentId;
+    this.revisionId = revisionId;
+    this.format = format;
+    this.baseUrl = baseUrl;
+    this.inbandEventStreams = inbandEventStreams == null
+        ? Collections.<SchemeValuePair>emptyList()
+        : Collections.unmodifiableList(inbandEventStreams);
+    initializationUri = segmentBase.getInitialization(this);
+    presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs();
+  }
+
+  /**
+   * Returns a {@link RangedUri} defining the location of the representation's initialization data,
+   * or null if no initialization data exists.
+   */
+  public RangedUri getInitializationUri() {
+    return initializationUri;
+  }
+
+  /**
+   * Returns a {@link RangedUri} defining the location of the representation's segment index, or
+   * null if the representation provides an index directly.
+   */
+  public abstract RangedUri getIndexUri();
+
+  /**
+   * Returns an index if the representation provides one directly, or null otherwise.
+   */
+  public abstract DashSegmentIndex getIndex();
+
+  /**
+   * Returns a cache key for the representation if a custom cache key or content id has been
+   * provided and there is only single segment.
+   */
+  public abstract String getCacheKey();
+
+  /**
+   * A DASH representation consisting of a single segment.
+   */
+  public static class SingleSegmentRepresentation extends Representation {
+
+    /**
+     * The uri of the single segment.
+     */
+    public final Uri uri;
+
+    /**
+     * The content length, or {@link C#LENGTH_UNSET} if unknown.
+     */
+    public final long contentLength;
+
+    private final String cacheKey;
+    private final RangedUri indexUri;
+    private final SingleSegmentIndex segmentIndex;
+
+    /**
+     * @param contentId Identifies the piece of content to which this representation belongs.
+     * @param revisionId Identifies the revision of the content.
+     * @param format The format of the representation.
+     * @param uri The uri of the media.
+     * @param initializationStart The offset of the first byte of initialization data.
+     * @param initializationEnd The offset of the last byte of initialization data.
+     * @param indexStart The offset of the first byte of index data.
+     * @param indexEnd The offset of the last byte of index data.
+     * @param inbandEventStreams The in-band event streams in the representation. May be null.
+     * @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null.
+     * @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown.
+     */
+    public static SingleSegmentRepresentation newInstance(String contentId, long revisionId,
+        Format format, String uri, long initializationStart, long initializationEnd,
+        long indexStart, long indexEnd, List<SchemeValuePair> inbandEventStreams,
+        String customCacheKey, long contentLength) {
+      RangedUri rangedUri = new RangedUri(null, initializationStart,
+          initializationEnd - initializationStart + 1);
+      SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, indexStart,
+          indexEnd - indexStart + 1);
+      return new SingleSegmentRepresentation(contentId, revisionId,
+          format, uri, segmentBase, inbandEventStreams, customCacheKey, contentLength);
+    }
+
+    /**
+     * @param contentId Identifies the piece of content to which this representation belongs.
+     * @param revisionId Identifies the revision of the content.
+     * @param format The format of the representation.
+     * @param baseUrl The base URL of the representation.
+     * @param segmentBase The segment base underlying the representation.
+     * @param inbandEventStreams The in-band event streams in the representation. May be null.
+     * @param customCacheKey A custom value to be returned from {@link #getCacheKey()}, or null.
+     * @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown.
+     */
+    public SingleSegmentRepresentation(String contentId, long revisionId, Format format,
+        String baseUrl, SingleSegmentBase segmentBase, List<SchemeValuePair> inbandEventStreams,
+        String customCacheKey, long contentLength) {
+      super(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams);
+      this.uri = Uri.parse(baseUrl);
+      this.indexUri = segmentBase.getIndex();
+      this.cacheKey = customCacheKey != null ? customCacheKey
+          : contentId != null ? contentId + "." + format.id + "." + revisionId : null;
+      this.contentLength = contentLength;
+      // If we have an index uri then the index is defined externally, and we shouldn't return one
+      // directly. If we don't, then we can't do better than an index defining a single segment.
+      segmentIndex = indexUri != null ? null
+          : new SingleSegmentIndex(new RangedUri(null, 0, contentLength));
+    }
+
+    @Override
+    public RangedUri getIndexUri() {
+      return indexUri;
+    }
+
+    @Override
+    public DashSegmentIndex getIndex() {
+      return segmentIndex;
+    }
+
+    @Override
+    public String getCacheKey() {
+      return cacheKey;
+    }
+
+  }
+
+  /**
+   * A DASH representation consisting of multiple segments.
+   */
+  public static class MultiSegmentRepresentation extends Representation
+      implements DashSegmentIndex {
+
+    private final MultiSegmentBase segmentBase;
+
+    /**
+     * @param contentId Identifies the piece of content to which this representation belongs.
+     * @param revisionId Identifies the revision of the content.
+     * @param format The format of the representation.
+     * @param baseUrl The base URL of the representation.
+     * @param segmentBase The segment base underlying the representation.
+     * @param inbandEventStreams The in-band event streams in the representation. May be null.
+     */
+    public MultiSegmentRepresentation(String contentId, long revisionId, Format format,
+        String baseUrl, MultiSegmentBase segmentBase, List<SchemeValuePair> inbandEventStreams) {
+      super(contentId, revisionId, format, baseUrl, segmentBase, inbandEventStreams);
+      this.segmentBase = segmentBase;
+    }
+
+    @Override
+    public RangedUri getIndexUri() {
+      return null;
+    }
+
+    @Override
+    public DashSegmentIndex getIndex() {
+      return this;
+    }
+
+    @Override
+    public String getCacheKey() {
+      return null;
+    }
+
+    // DashSegmentIndex implementation.
+
+    @Override
+    public RangedUri getSegmentUrl(int segmentIndex) {
+      return segmentBase.getSegmentUrl(this, segmentIndex);
+    }
+
+    @Override
+    public int getSegmentNum(long timeUs, long periodDurationUs) {
+      return segmentBase.getSegmentNum(timeUs, periodDurationUs);
+    }
+
+    @Override
+    public long getTimeUs(int segmentIndex) {
+      return segmentBase.getSegmentTimeUs(segmentIndex);
+    }
+
+    @Override
+    public long getDurationUs(int segmentIndex, long periodDurationUs) {
+      return segmentBase.getSegmentDurationUs(segmentIndex, periodDurationUs);
+    }
+
+    @Override
+    public int getFirstSegmentNum() {
+      return segmentBase.getFirstSegmentNum();
+    }
+
+    @Override
+    public int getLastSegmentNum(long periodDurationUs) {
+      return segmentBase.getLastSegmentNum(periodDurationUs);
+    }
+
+    @Override
+    public boolean isExplicit() {
+      return segmentBase.isExplicit();
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/SchemeValuePair.java
@@ -0,0 +1,51 @@
+/*
+ * 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 com.google.android.exoplayer2.source.dash.manifest;
+
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * A pair consisting of a scheme ID and value.
+ */
+public class SchemeValuePair {
+
+  public final String schemeIdUri;
+  public final String value;
+
+  public SchemeValuePair(String schemeIdUri, String value) {
+    this.schemeIdUri = schemeIdUri;
+    this.value = value;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null || getClass() != obj.getClass()) {
+      return false;
+    }
+    SchemeValuePair other = (SchemeValuePair) obj;
+    return Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value);
+  }
+
+  @Override
+  public int hashCode() {
+    return 31 * (schemeIdUri != null ? schemeIdUri.hashCode() : 0)
+        + (value != null ? value.hashCode() : 0);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/SegmentBase.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.dash.manifest;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.source.dash.DashSegmentIndex;
+import com.google.android.exoplayer2.util.Util;
+import java.util.List;
+
+/**
+ * An approximate representation of a SegmentBase manifest element.
+ */
+public abstract class SegmentBase {
+
+  /* package */ final RangedUri initialization;
+  /* package */ final long timescale;
+  /* package */ final long presentationTimeOffset;
+
+  /**
+   * @param initialization A {@link RangedUri} corresponding to initialization data, if such data
+   *     exists.
+   * @param timescale The timescale in units per second.
+   * @param presentationTimeOffset The presentation time offset. The value in seconds is the
+   *     division of this value and {@code timescale}.
+   */
+  public SegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset) {
+    this.initialization = initialization;
+    this.timescale = timescale;
+    this.presentationTimeOffset = presentationTimeOffset;
+  }
+
+  /**
+   * Returns the {@link RangedUri} defining the location of initialization data for a given
+   * representation, or null if no initialization data exists.
+   *
+   * @param representation The {@link Representation} for which initialization data is required.
+   * @return A {@link RangedUri} defining the location of the initialization data, or null.
+   */
+  public RangedUri getInitialization(Representation representation) {
+    return initialization;
+  }
+
+  /**
+   * Returns the presentation time offset, in microseconds.
+   */
+  public long getPresentationTimeOffsetUs() {
+    return Util.scaleLargeTimestamp(presentationTimeOffset, C.MICROS_PER_SECOND, timescale);
+  }
+
+  /**
+   * A {@link SegmentBase} that defines a single segment.
+   */
+  public static class SingleSegmentBase extends SegmentBase {
+
+    /* package */ final long indexStart;
+    /* package */ final long indexLength;
+
+    /**
+     * @param initialization A {@link RangedUri} corresponding to initialization data, if such data
+     *     exists.
+     * @param timescale The timescale in units per second.
+     * @param presentationTimeOffset The presentation time offset. The value in seconds is the
+     *     division of this value and {@code timescale}.
+     * @param indexStart The byte offset of the index data in the segment.
+     * @param indexLength The length of the index data in bytes.
+     */
+    public SingleSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset,
+        long indexStart, long indexLength) {
+      super(initialization, timescale, presentationTimeOffset);
+      this.indexStart = indexStart;
+      this.indexLength = indexLength;
+    }
+
+    public SingleSegmentBase() {
+      this(null, 1, 0, 0, 0);
+    }
+
+    public RangedUri getIndex() {
+      return indexLength <= 0 ? null : new RangedUri(null, indexStart, indexLength);
+    }
+
+  }
+
+  /**
+   * A {@link SegmentBase} that consists of multiple segments.
+   */
+  public abstract static class MultiSegmentBase extends SegmentBase {
+
+    /* package */ final int startNumber;
+    /* package */ final long duration;
+    /* package */ final List<SegmentTimelineElement> segmentTimeline;
+
+    /**
+     * @param initialization A {@link RangedUri} corresponding to initialization data, if such data
+     *     exists.
+     * @param timescale The timescale in units per second.
+     * @param presentationTimeOffset The presentation time offset. The value in seconds is the
+     *     division of this value and {@code timescale}.
+     * @param startNumber The sequence number of the first segment.
+     * @param duration The duration of each segment in the case of fixed duration segments. The
+     *     value in seconds is the division of this value and {@code timescale}. If
+     *     {@code segmentTimeline} is non-null then this parameter is ignored.
+     * @param segmentTimeline A segment timeline corresponding to the segments. If null, then
+     *     segments are assumed to be of fixed duration as specified by the {@code duration}
+     *     parameter.
+     */
+    public MultiSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset,
+        int startNumber, long duration, List<SegmentTimelineElement> segmentTimeline) {
+      super(initialization, timescale, presentationTimeOffset);
+      this.startNumber = startNumber;
+      this.duration = duration;
+      this.segmentTimeline = segmentTimeline;
+    }
+
+    /**
+     * @see DashSegmentIndex#getSegmentNum(long, long)
+     */
+    public int getSegmentNum(long timeUs, long periodDurationUs) {
+      final int firstSegmentNum = getFirstSegmentNum();
+      int lowIndex = firstSegmentNum;
+      int highIndex = getLastSegmentNum(periodDurationUs);
+      if (segmentTimeline == null) {
+        // All segments are of equal duration (with the possible exception of the last one).
+        long durationUs = (duration * C.MICROS_PER_SECOND) / timescale;
+        int segmentNum = startNumber + (int) (timeUs / durationUs);
+        // Ensure we stay within bounds.
+        return segmentNum < lowIndex ? lowIndex
+            : highIndex != DashSegmentIndex.INDEX_UNBOUNDED && segmentNum > highIndex ? highIndex
+            : segmentNum;
+      } else {
+        // The high index cannot be unbounded. Identify the segment using binary search.
+        while (lowIndex <= highIndex) {
+          int midIndex = lowIndex + (highIndex - lowIndex) / 2;
+          long midTimeUs = getSegmentTimeUs(midIndex);
+          if (midTimeUs < timeUs) {
+            lowIndex = midIndex + 1;
+          } else if (midTimeUs > timeUs) {
+            highIndex = midIndex - 1;
+          } else {
+            return midIndex;
+          }
+        }
+        return lowIndex == firstSegmentNum ? lowIndex : highIndex;
+      }
+    }
+
+    /**
+     * @see DashSegmentIndex#getDurationUs(int, long)
+     */
+    public final long getSegmentDurationUs(int sequenceNumber, long periodDurationUs) {
+      if (segmentTimeline != null) {
+        long duration = segmentTimeline.get(sequenceNumber - startNumber).duration;
+        return (duration * C.MICROS_PER_SECOND) / timescale;
+      } else {
+        return sequenceNumber == getLastSegmentNum(periodDurationUs)
+            ? (periodDurationUs - getSegmentTimeUs(sequenceNumber))
+            : ((duration * C.MICROS_PER_SECOND) / timescale);
+      }
+    }
+
+    /**
+     * @see DashSegmentIndex#getTimeUs(int)
+     */
+    public final long getSegmentTimeUs(int sequenceNumber) {
+      long unscaledSegmentTime;
+      if (segmentTimeline != null) {
+        unscaledSegmentTime = segmentTimeline.get(sequenceNumber - startNumber).startTime
+            - presentationTimeOffset;
+      } else {
+        unscaledSegmentTime = (sequenceNumber - startNumber) * duration;
+      }
+      return Util.scaleLargeTimestamp(unscaledSegmentTime, C.MICROS_PER_SECOND, timescale);
+    }
+
+    /**
+     * Returns a {@link RangedUri} defining the location of a segment for the given index in the
+     * given representation.
+     *
+     * @see DashSegmentIndex#getSegmentUrl(int)
+     */
+    public abstract RangedUri getSegmentUrl(Representation representation, int index);
+
+    /**
+     * @see DashSegmentIndex#getFirstSegmentNum()
+     */
+    public int getFirstSegmentNum() {
+      return startNumber;
+    }
+
+    /**
+     * @see DashSegmentIndex#getLastSegmentNum(long)
+     */
+    public abstract int getLastSegmentNum(long periodDurationUs);
+
+    /**
+     * @see DashSegmentIndex#isExplicit()
+     */
+    public boolean isExplicit() {
+      return segmentTimeline != null;
+    }
+
+  }
+
+  /**
+   * A {@link MultiSegmentBase} that uses a SegmentList to define its segments.
+   */
+  public static class SegmentList extends MultiSegmentBase {
+
+    /* package */ final List<RangedUri> mediaSegments;
+
+    /**
+     * @param initialization A {@link RangedUri} corresponding to initialization data, if such data
+     *     exists.
+     * @param timescale The timescale in units per second.
+     * @param presentationTimeOffset The presentation time offset. The value in seconds is the
+     *     division of this value and {@code timescale}.
+     * @param startNumber The sequence number of the first segment.
+     * @param duration The duration of each segment in the case of fixed duration segments. The
+     *     value in seconds is the division of this value and {@code timescale}. If
+     *     {@code segmentTimeline} is non-null then this parameter is ignored.
+     * @param segmentTimeline A segment timeline corresponding to the segments. If null, then
+     *     segments are assumed to be of fixed duration as specified by the {@code duration}
+     *     parameter.
+     * @param mediaSegments A list of {@link RangedUri}s indicating the locations of the segments.
+     */
+    public SegmentList(RangedUri initialization, long timescale, long presentationTimeOffset,
+        int startNumber, long duration, List<SegmentTimelineElement> segmentTimeline,
+        List<RangedUri> mediaSegments) {
+      super(initialization, timescale, presentationTimeOffset, startNumber, duration,
+          segmentTimeline);
+      this.mediaSegments = mediaSegments;
+    }
+
+    @Override
+    public RangedUri getSegmentUrl(Representation representation, int sequenceNumber) {
+      return mediaSegments.get(sequenceNumber - startNumber);
+    }
+
+    @Override
+    public int getLastSegmentNum(long periodDurationUs) {
+      return startNumber + mediaSegments.size() - 1;
+    }
+
+    @Override
+    public boolean isExplicit() {
+      return true;
+    }
+
+  }
+
+  /**
+   * A {@link MultiSegmentBase} that uses a SegmentTemplate to define its segments.
+   */
+  public static class SegmentTemplate extends MultiSegmentBase {
+
+    /* package */ final UrlTemplate initializationTemplate;
+    /* package */ final UrlTemplate mediaTemplate;
+
+    /**
+     * @param initialization A {@link RangedUri} corresponding to initialization data, if such data
+     *     exists. The value of this parameter is ignored if {@code initializationTemplate} is
+     *     non-null.
+     * @param timescale The timescale in units per second.
+     * @param presentationTimeOffset The presentation time offset. The value in seconds is the
+     *     division of this value and {@code timescale}.
+     * @param startNumber The sequence number of the first segment.
+     * @param duration The duration of each segment in the case of fixed duration segments. The
+     *     value in seconds is the division of this value and {@code timescale}. If
+     *     {@code segmentTimeline} is non-null then this parameter is ignored.
+     * @param segmentTimeline A segment timeline corresponding to the segments. If null, then
+     *     segments are assumed to be of fixed duration as specified by the {@code duration}
+     *     parameter.
+     * @param initializationTemplate A template defining the location of initialization data, if
+     *     such data exists. If non-null then the {@code initialization} parameter is ignored. If
+     *     null then {@code initialization} will be used.
+     * @param mediaTemplate A template defining the location of each media segment.
+     */
+    public SegmentTemplate(RangedUri initialization, long timescale, long presentationTimeOffset,
+        int startNumber, long duration, List<SegmentTimelineElement> segmentTimeline,
+        UrlTemplate initializationTemplate, UrlTemplate mediaTemplate) {
+      super(initialization, timescale, presentationTimeOffset, startNumber,
+          duration, segmentTimeline);
+      this.initializationTemplate = initializationTemplate;
+      this.mediaTemplate = mediaTemplate;
+    }
+
+    @Override
+    public RangedUri getInitialization(Representation representation) {
+      if (initializationTemplate != null) {
+        String urlString = initializationTemplate.buildUri(representation.format.id, 0,
+            representation.format.bitrate, 0);
+        return new RangedUri(urlString, 0, C.LENGTH_UNSET);
+      } else {
+        return super.getInitialization(representation);
+      }
+    }
+
+    @Override
+    public RangedUri getSegmentUrl(Representation representation, int sequenceNumber) {
+      long time;
+      if (segmentTimeline != null) {
+        time = segmentTimeline.get(sequenceNumber - startNumber).startTime;
+      } else {
+        time = (sequenceNumber - startNumber) * duration;
+      }
+      String uriString = mediaTemplate.buildUri(representation.format.id, sequenceNumber,
+          representation.format.bitrate, time);
+      return new RangedUri(uriString, 0, C.LENGTH_UNSET);
+    }
+
+    @Override
+    public int getLastSegmentNum(long periodDurationUs) {
+      if (segmentTimeline != null) {
+        return segmentTimeline.size() + startNumber - 1;
+      } else if (periodDurationUs == C.TIME_UNSET) {
+        return DashSegmentIndex.INDEX_UNBOUNDED;
+      } else {
+        long durationUs = (duration * C.MICROS_PER_SECOND) / timescale;
+        return startNumber + (int) Util.ceilDivide(periodDurationUs, durationUs) - 1;
+      }
+    }
+
+  }
+
+  /**
+   * Represents a timeline segment from the MPD's SegmentTimeline list.
+   */
+  public static class SegmentTimelineElement {
+
+    /* package */ final long startTime;
+    /* package */ final long duration;
+
+    /**
+     * @param startTime The start time of the element. The value in seconds is the division of this
+     *     value and the {@code timescale} of the enclosing element.
+     * @param duration The duration of the element. The value in seconds is the division of this
+     *     value and the {@code timescale} of the enclosing element.
+     */
+    public SegmentTimelineElement(long startTime, long duration) {
+      this.startTime = startTime;
+      this.duration = duration;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/SingleSegmentIndex.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 com.google.android.exoplayer2.source.dash.manifest;
+
+import com.google.android.exoplayer2.source.dash.DashSegmentIndex;
+
+/**
+ * A {@link DashSegmentIndex} that defines a single segment.
+ */
+/* package */ final class SingleSegmentIndex implements DashSegmentIndex {
+
+  private final RangedUri uri;
+
+  /**
+   * @param uri A {@link RangedUri} defining the location of the segment data.
+   */
+  public SingleSegmentIndex(RangedUri uri) {
+    this.uri = uri;
+  }
+
+  @Override
+  public int getSegmentNum(long timeUs, long periodDurationUs) {
+    return 0;
+  }
+
+  @Override
+  public long getTimeUs(int segmentNum) {
+    return 0;
+  }
+
+  @Override
+  public long getDurationUs(int segmentNum, long periodDurationUs) {
+    return periodDurationUs;
+  }
+
+  @Override
+  public RangedUri getSegmentUrl(int segmentNum) {
+    return uri;
+  }
+
+  @Override
+  public int getFirstSegmentNum() {
+    return 0;
+  }
+
+  @Override
+  public int getLastSegmentNum(long periodDurationUs) {
+    return 0;
+  }
+
+  @Override
+  public boolean isExplicit() {
+    return true;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/UrlTemplate.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 com.google.android.exoplayer2.source.dash.manifest;
+
+import java.util.Locale;
+
+/**
+ * A template from which URLs can be built.
+ * <p>
+ * URLs are built according to the substitution rules defined in ISO/IEC 23009-1:2014 5.3.9.4.4.
+ */
+public final class UrlTemplate {
+
+  private static final String REPRESENTATION = "RepresentationID";
+  private static final String NUMBER = "Number";
+  private static final String BANDWIDTH = "Bandwidth";
+  private static final String TIME = "Time";
+  private static final String ESCAPED_DOLLAR = "$$";
+  private static final String DEFAULT_FORMAT_TAG = "%01d";
+
+  private static final int REPRESENTATION_ID = 1;
+  private static final int NUMBER_ID = 2;
+  private static final int BANDWIDTH_ID = 3;
+  private static final int TIME_ID = 4;
+
+  private final String[] urlPieces;
+  private final int[] identifiers;
+  private final String[] identifierFormatTags;
+  private final int identifierCount;
+
+  /**
+   * Compile an instance from the provided template string.
+   *
+   * @param template The template.
+   * @return The compiled instance.
+   * @throws IllegalArgumentException If the template string is malformed.
+   */
+  public static UrlTemplate compile(String template) {
+    // These arrays are sizes assuming each of the four possible identifiers will be present at
+    // most once in the template, which seems like a reasonable assumption.
+    String[] urlPieces = new String[5];
+    int[] identifiers = new int[4];
+    String[] identifierFormatTags = new String[4];
+    int identifierCount = parseTemplate(template, urlPieces, identifiers, identifierFormatTags);
+    return new UrlTemplate(urlPieces, identifiers, identifierFormatTags, identifierCount);
+  }
+
+  /**
+   * Internal constructor. Use {@link #compile(String)} to build instances of this class.
+   */
+  private UrlTemplate(String[] urlPieces, int[] identifiers, String[] identifierFormatTags,
+      int identifierCount) {
+    this.urlPieces = urlPieces;
+    this.identifiers = identifiers;
+    this.identifierFormatTags = identifierFormatTags;
+    this.identifierCount = identifierCount;
+  }
+
+  /**
+   * Constructs a Uri from the template, substituting in the provided arguments.
+   * <p>
+   * Arguments whose corresponding identifiers are not present in the template will be ignored.
+   *
+   * @param representationId The representation identifier.
+   * @param segmentNumber The segment number.
+   * @param bandwidth The bandwidth.
+   * @param time The time as specified by the segment timeline.
+   * @return The built Uri.
+   */
+  public String buildUri(String representationId, int segmentNumber, int bandwidth, long time) {
+    StringBuilder builder = new StringBuilder();
+    for (int i = 0; i < identifierCount; i++) {
+      builder.append(urlPieces[i]);
+      if (identifiers[i] == REPRESENTATION_ID) {
+        builder.append(representationId);
+      } else if (identifiers[i] == NUMBER_ID) {
+        builder.append(String.format(Locale.US, identifierFormatTags[i], segmentNumber));
+      } else if (identifiers[i] == BANDWIDTH_ID) {
+        builder.append(String.format(Locale.US, identifierFormatTags[i], bandwidth));
+      } else if (identifiers[i] == TIME_ID) {
+        builder.append(String.format(Locale.US, identifierFormatTags[i], time));
+      }
+    }
+    builder.append(urlPieces[identifierCount]);
+    return builder.toString();
+  }
+
+  /**
+   * Parses {@code template}, placing the decomposed components into the provided arrays.
+   * <p>
+   * If the return value is N, {@code urlPieces} will contain (N+1) strings that must be
+   * interleaved with N arguments in order to construct a url. The N identifiers that correspond to
+   * the required arguments, together with the tags that define their required formatting, are
+   * returned in {@code identifiers} and {@code identifierFormatTags} respectively.
+   *
+   * @param template The template to parse.
+   * @param urlPieces A holder for pieces of url parsed from the template.
+   * @param identifiers A holder for identifiers parsed from the template.
+   * @param identifierFormatTags A holder for format tags corresponding to the parsed identifiers.
+   * @return The number of identifiers in the template url.
+   * @throws IllegalArgumentException If the template string is malformed.
+   */
+  private static int parseTemplate(String template, String[] urlPieces, int[] identifiers,
+      String[] identifierFormatTags) {
+    urlPieces[0] = "";
+    int templateIndex = 0;
+    int identifierCount = 0;
+    while (templateIndex < template.length()) {
+      int dollarIndex = template.indexOf("$", templateIndex);
+      if (dollarIndex == -1) {
+        urlPieces[identifierCount] += template.substring(templateIndex);
+        templateIndex = template.length();
+      } else if (dollarIndex != templateIndex) {
+        urlPieces[identifierCount] += template.substring(templateIndex, dollarIndex);
+        templateIndex = dollarIndex;
+      } else if (template.startsWith(ESCAPED_DOLLAR, templateIndex)) {
+        urlPieces[identifierCount] += "$";
+        templateIndex += 2;
+      } else {
+        int secondIndex = template.indexOf("$", templateIndex + 1);
+        String identifier = template.substring(templateIndex + 1, secondIndex);
+        if (identifier.equals(REPRESENTATION)) {
+          identifiers[identifierCount] = REPRESENTATION_ID;
+        } else {
+          int formatTagIndex = identifier.indexOf("%0");
+          String formatTag = DEFAULT_FORMAT_TAG;
+          if (formatTagIndex != -1) {
+            formatTag = identifier.substring(formatTagIndex);
+            if (!formatTag.endsWith("d")) {
+              formatTag += "d";
+            }
+            identifier = identifier.substring(0, formatTagIndex);
+          }
+          switch (identifier) {
+            case NUMBER:
+              identifiers[identifierCount] = NUMBER_ID;
+              break;
+            case BANDWIDTH:
+              identifiers[identifierCount] = BANDWIDTH_ID;
+              break;
+            case TIME:
+              identifiers[identifierCount] = TIME_ID;
+              break;
+            default:
+              throw new IllegalArgumentException("Invalid template: " + template);
+          }
+          identifierFormatTags[identifierCount] = formatTag;
+        }
+        identifierCount++;
+        urlPieces[identifierCount] = "";
+        templateIndex = secondIndex + 1;
+      }
+    }
+    return identifierCount;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/dash/manifest/UtcTimingElement.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 com.google.android.exoplayer2.source.dash.manifest;
+
+/**
+ * Represents a UTCTiming element.
+ */
+public final class UtcTimingElement {
+
+  public final String schemeIdUri;
+  public final String value;
+
+  public UtcTimingElement(String schemeIdUri, String value) {
+    this.schemeIdUri = schemeIdUri;
+    this.value = value;
+  }
+
+  @Override
+  public String toString() {
+    return schemeIdUri + ", " + value;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/Aes128DataSource.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 com.google.android.exoplayer2.source.hls;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSourceInputStream;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import 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 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 */ final class Aes128DataSource implements DataSource {
+
+  private final DataSource upstream;
+  private final byte[] encryptionKey;
+  private final byte[] encryptionIv;
+
+  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 long open(DataSpec dataSpec) throws IOException {
+    Cipher cipher;
+    try {
+      cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
+    } 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);
+    }
+
+    cipherInputStream = new CipherInputStream(
+        new DataSourceInputStream(upstream, dataSpec), cipher);
+
+    return C.LENGTH_UNSET;
+  }
+
+  @Override
+  public void close() throws IOException {
+    cipherInputStream = null;
+    upstream.close();
+  }
+
+  @Override
+  public int read(byte[] buffer, int offset, int readLength) throws IOException {
+    Assertions.checkState(cipherInputStream != null);
+    int bytesRead = cipherInputStream.read(buffer, offset, readLength);
+    if (bytesRead < 0) {
+      return C.RESULT_END_OF_INPUT;
+    }
+    return bytesRead;
+  }
+
+  @Override
+  public Uri getUri() {
+    return upstream.getUri();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/HlsChunkSource.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.hls;
+
+import android.net.Uri;
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.BehindLiveWindowException;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.chunk.Chunk;
+import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil;
+import com.google.android.exoplayer2.source.chunk.DataChunk;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
+import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
+import com.google.android.exoplayer2.trackselection.BaseTrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import com.google.android.exoplayer2.util.UriUtil;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * 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.
+     */
+    public Chunk chunk;
+
+    /**
+     * Indicates that the end of the stream has been reached.
+     */
+    public boolean endOfStream;
+
+    /**
+     * Indicates that the chunk source is waiting for the referred playlist to be refreshed.
+     */
+    public HlsUrl playlist;
+
+    /**
+     * Clears the holder.
+     */
+    public void clear() {
+      chunk = null;
+      endOfStream = false;
+      playlist = null;
+    }
+
+  }
+
+  private final DataSource dataSource;
+  private final TimestampAdjusterProvider timestampAdjusterProvider;
+  private final HlsUrl[] variants;
+  private final HlsPlaylistTracker playlistTracker;
+  private final TrackGroup trackGroup;
+
+  private boolean isTimestampMaster;
+  private byte[] scratchSpace;
+  private IOException fatalError;
+
+  private Uri encryptionKeyUri;
+  private byte[] encryptionKey;
+  private String encryptionIvString;
+  private byte[] encryptionIv;
+
+  // 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;
+
+  /**
+   * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists.
+   * @param variants The available variants.
+   * @param dataSource A {@link DataSource} suitable for loading the media data.
+   * @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.
+   */
+  public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants,
+      DataSource dataSource, TimestampAdjusterProvider timestampAdjusterProvider) {
+    this.playlistTracker = playlistTracker;
+    this.variants = variants;
+    this.dataSource = dataSource;
+    this.timestampAdjusterProvider = timestampAdjusterProvider;
+
+    Format[] variantFormats = new Format[variants.length];
+    int[] initialTrackSelection = new int[variants.length];
+    for (int i = 0; i < variants.length; i++) {
+      variantFormats[i] = variants[i].format;
+      initialTrackSelection[i] = i;
+    }
+    trackGroup = new TrackGroup(variantFormats);
+    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;
+    }
+  }
+
+  /**
+   * Returns the track group exposed by the source.
+   */
+  public TrackGroup getTrackGroup() {
+    return trackGroup;
+  }
+
+  /**
+   * Selects tracks for use.
+   *
+   * @param trackSelection The track selection.
+   */
+  public void selectTracks(TrackSelection trackSelection) {
+    this.trackSelection = 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#playlist} is set to
+   * contain the {@link HlsUrl} that refers to the playlist that needs refreshing.
+   *
+   * @param previous The most recently loaded media chunk.
+   * @param playbackPositionUs The current playback position. If {@code previous} is null then this
+   *     parameter is the position from which playback is expected to start (or restart) and hence
+   *     should be interpreted as a seek position.
+   * @param out A holder to populate.
+   */
+  public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChunkHolder out) {
+    int oldVariantIndex = previous == null ? C.INDEX_UNSET
+        : trackGroup.indexOf(previous.trackFormat);
+    // Use start time of the previous chunk rather than its end time because switching format will
+    // require downloading overlapping segments.
+    long bufferedDurationUs = previous == null ? 0
+        : Math.max(0, previous.startTimeUs - playbackPositionUs);
+
+    // Select the variant.
+    trackSelection.updateSelectedTrack(bufferedDurationUs);
+    int selectedVariantIndex = trackSelection.getSelectedIndexInTrackGroup();
+
+    boolean switchingVariant = oldVariantIndex != selectedVariantIndex;
+    HlsUrl selectedUrl = variants[selectedVariantIndex];
+    if (!playlistTracker.isSnapshotValid(selectedUrl)) {
+      out.playlist = selectedUrl;
+      // Retry when playlist is refreshed.
+      return;
+    }
+    HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl);
+
+    // Select the chunk.
+    int chunkMediaSequence;
+    if (previous == null || switchingVariant) {
+      long targetPositionUs = previous == null ? playbackPositionUs : previous.startTimeUs;
+      if (!mediaPlaylist.hasEndTag && targetPositionUs > mediaPlaylist.getEndTimeUs()) {
+        // If the playlist is too old to contain the chunk, we need to refresh it.
+        chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size();
+      } else {
+        chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments,
+            targetPositionUs - mediaPlaylist.startTimeUs, true,
+            !playlistTracker.isLive() || previous == null) + mediaPlaylist.mediaSequence;
+        if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null) {
+          // We try getting the next chunk without adapting in case that's the reason for falling
+          // behind the live window.
+          selectedVariantIndex = oldVariantIndex;
+          selectedUrl = variants[selectedVariantIndex];
+          mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl);
+          chunkMediaSequence = previous.getNextChunkIndex();
+        }
+      }
+    } else {
+      chunkMediaSequence = previous.getNextChunkIndex();
+    }
+    if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
+      fatalError = new BehindLiveWindowException();
+      return;
+    }
+
+    int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence;
+    if (chunkIndex >= mediaPlaylist.segments.size()) {
+      if (mediaPlaylist.hasEndTag) {
+        out.endOfStream = true;
+      } else /* Live */ {
+        out.playlist = selectedUrl;
+      }
+      return;
+    }
+
+    // Handle encryption.
+    HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex);
+
+    // Check if encryption is specified.
+    if (segment.isEncrypted) {
+      Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
+      if (!keyUri.equals(encryptionKeyUri)) {
+        // Encryption is specified and the key has changed.
+        out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex,
+            trackSelection.getSelectionReason(), trackSelection.getSelectionData());
+        return;
+      }
+      if (!Util.areEqual(segment.encryptionIV, encryptionIvString)) {
+        setEncryptionData(keyUri, segment.encryptionIV, encryptionKey);
+      }
+    } else {
+      clearEncryptionData();
+    }
+
+    DataSpec initDataSpec = null;
+    Segment initSegment = mediaPlaylist.initializationSegment;
+    if (initSegment != null) {
+      Uri initSegmentUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, initSegment.url);
+      initDataSpec = new DataSpec(initSegmentUri, initSegment.byterangeOffset,
+          initSegment.byterangeLength, null);
+    }
+
+    // Compute start time of the next chunk.
+    long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs;
+    int discontinuitySequence = mediaPlaylist.discontinuitySequence
+        + segment.relativeDiscontinuitySequence;
+    TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(
+        discontinuitySequence, startTimeUs);
+
+    // Configure the data source and spec for the chunk.
+    Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);
+    DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
+        null);
+    out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, selectedUrl,
+        trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
+        startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence,
+        isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv);
+  }
+
+  /**
+   * 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();
+      setEncryptionData(encryptionKeyChunk.dataSpec.uri, encryptionKeyChunk.iv,
+          encryptionKeyChunk.getResult());
+    }
+  }
+
+  /**
+   * Called when the {@link HlsSampleStreamWrapper} encounters an error loading a chunk obtained
+   * from this source.
+   *
+   * @param chunk The chunk whose load encountered the error.
+   * @param cancelable Whether the load can be canceled.
+   * @param error The error.
+   * @return Whether the load should be canceled.
+   */
+  public boolean onChunkLoadError(Chunk chunk, boolean cancelable, IOException error) {
+    return cancelable && ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection,
+        trackSelection.indexOf(trackGroup.indexOf(chunk.trackFormat)), error);
+  }
+
+  /**
+   * Called when a playlist is blacklisted.
+   *
+   * @param url The url that references the blacklisted playlist.
+   * @param blacklistMs The amount of milliseconds for which the playlist was blacklisted.
+   */
+  public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) {
+    int trackGroupIndex = trackGroup.indexOf(url.format);
+    if (trackGroupIndex != C.INDEX_UNSET) {
+      int trackSelectionIndex = trackSelection.indexOf(trackGroupIndex);
+      if (trackSelectionIndex != C.INDEX_UNSET) {
+        trackSelection.blacklist(trackSelectionIndex, blacklistMs);
+      }
+    }
+  }
+
+  // Private methods.
+
+  private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex,
+      int trackSelectionReason, Object trackSelectionData) {
+    DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP);
+    return new EncryptionKeyChunk(dataSource, dataSpec, variants[variantIndex].format,
+        trackSelectionReason, trackSelectionData, scratchSpace, iv);
+  }
+
+  private void setEncryptionData(Uri keyUri, String iv, byte[] secretKey) {
+    String trimmedIv;
+    if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) {
+      trimmedIv = iv.substring(2);
+    } else {
+      trimmedIv = iv;
+    }
+
+    byte[] ivData = new BigInteger(trimmedIv, 16).toByteArray();
+    byte[] ivDataWithPadding = new byte[16];
+    int offset = ivData.length > 16 ? ivData.length - 16 : 0;
+    System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length
+        + offset, ivData.length - offset);
+
+    encryptionKeyUri = keyUri;
+    encryptionKey = secretKey;
+    encryptionIvString = iv;
+    encryptionIv = ivDataWithPadding;
+  }
+
+  private void clearEncryptionData() {
+    encryptionKeyUri = null;
+    encryptionKey = null;
+    encryptionIvString = null;
+    encryptionIv = null;
+  }
+
+  // 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 bufferedDurationUs) {
+      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
+    public Object getSelectionData() {
+      return null;
+    }
+
+  }
+
+  private static final class EncryptionKeyChunk extends DataChunk {
+
+    public final String iv;
+
+    private byte[] result;
+
+    public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
+        int trackSelectionReason, Object trackSelectionData, byte[] scratchSpace, String iv) {
+      super(dataSource, dataSpec, C.DATA_TYPE_DRM, trackFormat, trackSelectionReason,
+          trackSelectionData, scratchSpace);
+      this.iv = iv;
+    }
+
+    @Override
+    protected void consume(byte[] data, int limit) throws IOException {
+      result = Arrays.copyOf(data, limit);
+    }
+
+    public byte[] getResult() {
+      return result;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.hls;
+
+import android.text.TextUtils;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
+import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
+import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
+import com.google.android.exoplayer2.extractor.ts.AdtsExtractor;
+import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory;
+import com.google.android.exoplayer2.extractor.ts.TsExtractor;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
+import com.google.android.exoplayer2.metadata.id3.PrivFrame;
+import com.google.android.exoplayer2.source.chunk.MediaChunk;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * An HLS {@link MediaChunk}.
+ */
+/* package */ final class HlsMediaChunk extends MediaChunk {
+
+  private static final AtomicInteger UID_SOURCE = new AtomicInteger();
+
+  private static final String PRIV_TIMESTAMP_FRAME_OWNER =
+      "com.apple.streaming.transportStreamTimestamp";
+
+  private static final String AAC_FILE_EXTENSION = ".aac";
+  private static final String AC3_FILE_EXTENSION = ".ac3";
+  private static final String EC3_FILE_EXTENSION = ".ec3";
+  private static final String MP3_FILE_EXTENSION = ".mp3";
+  private static final String MP4_FILE_EXTENSION = ".mp4";
+  private static final String VTT_FILE_EXTENSION = ".vtt";
+  private static final String WEBVTT_FILE_EXTENSION = ".webvtt";
+
+  /**
+   * 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 HlsUrl hlsUrl;
+
+  private final DataSource initDataSource;
+  private final DataSpec initDataSpec;
+  private final boolean isEncrypted;
+  private final boolean isMasterTimestampSource;
+  private final TimestampAdjuster timestampAdjuster;
+  private final String lastPathSegment;
+  private final Extractor previousExtractor;
+  private final boolean shouldSpliceIn;
+  private final boolean needNewExtractor;
+
+  private final boolean isPackedAudio;
+  private final Id3Decoder id3Decoder;
+  private final ParsableByteArray id3Data;
+
+  private Extractor extractor;
+  private int initSegmentBytesLoaded;
+  private int bytesLoaded;
+  private boolean initLoadCompleted;
+  private HlsSampleStreamWrapper extractorOutput;
+  private volatile boolean loadCanceled;
+  private volatile boolean loadCompleted;
+
+  /**
+   * @param dataSource The source from which the data should be loaded.
+   * @param dataSpec Defines the data to be loaded.
+   * @param initDataSpec Defines the initialization data to be fed to new extractors. May be null.
+   * @param hlsUrl The url of the playlist from which this chunk was obtained.
+   * @param trackSelectionReason See {@link #trackSelectionReason}.
+   * @param trackSelectionData See {@link #trackSelectionData}.
+   * @param startTimeUs The start time of the chunk in microseconds.
+   * @param endTimeUs The end time of the chunk in microseconds.
+   * @param chunkIndex The media sequence number of the chunk.
+   * @param discontinuitySequenceNumber The discontinuity sequence number of the chunk.
+   * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster.
+   * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number.
+   * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null.
+   * @param encryptionKey For AES encryption chunks, the encryption key.
+   * @param encryptionIv For AES encryption chunks, the encryption initialization vector.
+   */
+  public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec,
+      HlsUrl hlsUrl, int trackSelectionReason, Object trackSelectionData, long startTimeUs,
+      long endTimeUs, int chunkIndex, int discontinuitySequenceNumber,
+      boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster,
+      HlsMediaChunk previousChunk, byte[] encryptionKey, byte[] encryptionIv) {
+    super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format,
+        trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex);
+    this.initDataSpec = initDataSpec;
+    this.hlsUrl = hlsUrl;
+    this.isMasterTimestampSource = isMasterTimestampSource;
+    this.timestampAdjuster = timestampAdjuster;
+    this.discontinuitySequenceNumber = discontinuitySequenceNumber;
+    // Note: this.dataSource and dataSource may be different.
+    this.isEncrypted = this.dataSource instanceof Aes128DataSource;
+    lastPathSegment = dataSpec.uri.getLastPathSegment();
+    isPackedAudio = lastPathSegment.endsWith(AAC_FILE_EXTENSION)
+        || lastPathSegment.endsWith(AC3_FILE_EXTENSION)
+        || lastPathSegment.endsWith(EC3_FILE_EXTENSION)
+        || lastPathSegment.endsWith(MP3_FILE_EXTENSION);
+    if (previousChunk != null) {
+      id3Decoder = previousChunk.id3Decoder;
+      id3Data = previousChunk.id3Data;
+      previousExtractor = previousChunk.extractor;
+      shouldSpliceIn = previousChunk.hlsUrl != hlsUrl;
+      needNewExtractor = previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber
+          || shouldSpliceIn;
+    } else {
+      id3Decoder = isPackedAudio ? new Id3Decoder() : null;
+      id3Data = isPackedAudio ? new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH) : null;
+      previousExtractor = null;
+      shouldSpliceIn = false;
+      needNewExtractor = true;
+    }
+    initDataSource = dataSource;
+    uid = UID_SOURCE.getAndIncrement();
+  }
+
+  /**
+   * 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) {
+    extractorOutput = output;
+    output.init(uid, shouldSpliceIn);
+  }
+
+  @Override
+  public boolean isLoadCompleted() {
+    return loadCompleted;
+  }
+
+  @Override
+  public long bytesLoaded() {
+    return bytesLoaded;
+  }
+
+  // Loadable implementation
+
+  @Override
+  public void cancelLoad() {
+    loadCanceled = true;
+  }
+
+  @Override
+  public boolean isLoadCanceled() {
+    return loadCanceled;
+  }
+
+  @Override
+  public void load() throws IOException, InterruptedException {
+    if (extractor == null && !isPackedAudio) {
+      // See HLS spec, version 20, Section 3.4 for more information on packed audio extraction.
+      extractor = createExtractor();
+    }
+    maybeLoadInitData();
+    if (!loadCanceled) {
+      loadMedia();
+    }
+  }
+
+  // Internal loading methods.
+
+  private void maybeLoadInitData() throws IOException, InterruptedException {
+    if (previousExtractor == extractor || initLoadCompleted || initDataSpec == null) {
+      // According to spec, for packed audio, initDataSpec is expected to be null.
+      return;
+    }
+    DataSpec initSegmentDataSpec = Util.getRemainderDataSpec(initDataSpec, initSegmentBytesLoaded);
+    try {
+      ExtractorInput input = new DefaultExtractorInput(initDataSource,
+          initSegmentDataSpec.absoluteStreamPosition, initDataSource.open(initSegmentDataSpec));
+      try {
+        int result = Extractor.RESULT_CONTINUE;
+        while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+          result = extractor.read(input, null);
+        }
+      } finally {
+        initSegmentBytesLoaded = (int) (input.getPosition() - initDataSpec.absoluteStreamPosition);
+      }
+    } finally {
+      Util.closeQuietly(dataSource);
+    }
+    initLoadCompleted = true;
+  }
+
+  private void loadMedia() throws IOException, InterruptedException {
+    // If we previously fed part of this chunk to the extractor, we need to skip it this time. For
+    // encrypted content we need to skip the data by reading it through the source, so as to ensure
+    // correct decryption of the remainder of the chunk. For clear content, we can request the
+    // remainder of the chunk directly.
+    DataSpec loadDataSpec;
+    boolean skipLoadedBytes;
+    if (isEncrypted) {
+      loadDataSpec = dataSpec;
+      skipLoadedBytes = bytesLoaded != 0;
+    } else {
+      loadDataSpec = Util.getRemainderDataSpec(dataSpec, bytesLoaded);
+      skipLoadedBytes = false;
+    }
+    if (!isMasterTimestampSource) {
+      timestampAdjuster.waitUntilInitialized();
+    }
+    try {
+      ExtractorInput input = new DefaultExtractorInput(dataSource,
+          loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
+      if (extractor == null) {
+        // Media segment format is packed audio.
+        long id3Timestamp = peekId3PrivTimestamp(input);
+        if (id3Timestamp == C.TIME_UNSET) {
+          throw new ParserException("ID3 PRIV timestamp missing.");
+        }
+        extractor = buildPackedAudioExtractor(timestampAdjuster.adjustTsTimestamp(id3Timestamp));
+      }
+      if (skipLoadedBytes) {
+        input.skipFully(bytesLoaded);
+      }
+      try {
+        int result = Extractor.RESULT_CONTINUE;
+        while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
+          result = extractor.read(input, null);
+        }
+      } finally {
+        bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
+      }
+    } finally {
+      Util.closeQuietly(dataSource);
+    }
+    loadCompleted = true;
+  }
+
+  /**
+   * 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();
+    if (!input.peekFully(id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH, true)) {
+      return C.TIME_UNSET;
+    }
+    id3Data.reset(Id3Decoder.ID3_HEADER_LENGTH);
+    int id = id3Data.readUnsignedInt24();
+    if (id != Id3Decoder.ID3_TAG) {
+      return C.TIME_UNSET;
+    }
+    id3Data.skipBytes(3); // version(2), flags(1).
+    int id3Size = id3Data.readSynchSafeInt();
+    int requiredCapacity = id3Size + Id3Decoder.ID3_HEADER_LENGTH;
+    if (requiredCapacity > id3Data.capacity()) {
+      byte[] data = id3Data.data;
+      id3Data.reset(requiredCapacity);
+      System.arraycopy(data, 0, id3Data.data, 0, Id3Decoder.ID3_HEADER_LENGTH);
+    }
+    if (!input.peekFully(id3Data.data, Id3Decoder.ID3_HEADER_LENGTH, id3Size, true)) {
+      return C.TIME_UNSET;
+    }
+    Metadata metadata = id3Decoder.decode(id3Data.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, id3Data.data, 0, 8 /* timestamp size */);
+          id3Data.reset(8);
+          return id3Data.readLong();
+        }
+      }
+    }
+    return C.TIME_UNSET;
+  }
+
+  // Internal factory methods.
+
+  /**
+   * If the content is encrypted, returns an {@link Aes128DataSource} that wraps the original in
+   * order to decrypt the loaded data. Else returns the original.
+   */
+  private static DataSource buildDataSource(DataSource dataSource, byte[] encryptionKey,
+      byte[] encryptionIv) {
+    if (encryptionKey == null || encryptionIv == null) {
+      return dataSource;
+    }
+    return new Aes128DataSource(dataSource, encryptionKey, encryptionIv);
+  }
+
+  private Extractor createExtractor() {
+    // Select the extractor that will read the chunk.
+    Extractor extractor;
+    boolean usingNewExtractor = true;
+    if (MimeTypes.TEXT_VTT.equals(hlsUrl.format.sampleMimeType)
+        || lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION)
+        || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) {
+      extractor = new WebvttExtractor(trackFormat.language, timestampAdjuster);
+    } else if (!needNewExtractor) {
+      // Only reuse TS and fMP4 extractors.
+      usingNewExtractor = false;
+      extractor = previousExtractor;
+    } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION)) {
+      extractor = new FragmentedMp4Extractor(0, timestampAdjuster);
+    } else {
+      // MPEG-2 TS segments, but we need a new extractor.
+      // This flag ensures the change of pid between streams does not affect the sample queues.
+      @DefaultTsPayloadReaderFactory.Flags
+      int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM;
+      String codecs = trackFormat.codecs;
+      if (!TextUtils.isEmpty(codecs)) {
+        // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
+        // exist. If we know from the codec attribute that they don't exist, then we can
+        // explicitly ignore them even if they're declared.
+        if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) {
+          esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM;
+        }
+        if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) {
+          esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM;
+        }
+      }
+      extractor = new TsExtractor(timestampAdjuster,
+          new DefaultTsPayloadReaderFactory(esReaderFactoryFlags), true);
+    }
+    if (usingNewExtractor) {
+      extractor.init(extractorOutput);
+    }
+    return extractor;
+  }
+
+  private Extractor buildPackedAudioExtractor(long startTimeUs) {
+    Extractor extractor;
+    if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) {
+      extractor = new AdtsExtractor(startTimeUs);
+    } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION)
+        || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) {
+      extractor = new Ac3Extractor(startTimeUs);
+    } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) {
+      extractor = new Mp3Extractor(startTimeUs);
+    } else {
+      throw new IllegalArgumentException("Unkown extension for audio file: " + lastPathSegment);
+    }
+    extractor.init(extractorOutput);
+    return extractor;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.hls;
+
+import android.os.Handler;
+import android.text.TextUtils;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.CompositeSequenceableLoader;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
+import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+/**
+ * A {@link MediaPeriod} that loads an HLS stream.
+ */
+public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback,
+    HlsPlaylistTracker.PlaylistEventListener {
+
+  private final HlsPlaylistTracker playlistTracker;
+  private final DataSource.Factory dataSourceFactory;
+  private final int minLoadableRetryCount;
+  private final EventDispatcher eventDispatcher;
+  private final Allocator allocator;
+  private final IdentityHashMap<SampleStream, Integer> streamWrapperIndices;
+  private final TimestampAdjusterProvider timestampAdjusterProvider;
+  private final Handler continueLoadingHandler;
+  private final long preparePositionUs;
+
+  private Callback callback;
+  private int pendingPrepareCount;
+  private boolean seenFirstTrackSelection;
+  private TrackGroupArray trackGroups;
+  private HlsSampleStreamWrapper[] sampleStreamWrappers;
+  private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
+  private CompositeSequenceableLoader sequenceableLoader;
+
+  public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, DataSource.Factory dataSourceFactory,
+      int minLoadableRetryCount, EventDispatcher eventDispatcher, Allocator allocator,
+      long positionUs) {
+    this.playlistTracker = playlistTracker;
+    this.dataSourceFactory = dataSourceFactory;
+    this.minLoadableRetryCount = minLoadableRetryCount;
+    this.eventDispatcher = eventDispatcher;
+    this.allocator = allocator;
+    streamWrapperIndices = new IdentityHashMap<>();
+    timestampAdjusterProvider = new TimestampAdjusterProvider();
+    continueLoadingHandler = new Handler();
+    preparePositionUs = positionUs;
+  }
+
+  public void release() {
+    playlistTracker.removeListener(this);
+    continueLoadingHandler.removeCallbacksAndMessages(null);
+    if (sampleStreamWrappers != null) {
+      for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
+        sampleStreamWrapper.release();
+      }
+    }
+  }
+
+  @Override
+  public void prepare(Callback callback) {
+    playlistTracker.addListener(this);
+    this.callback = callback;
+    buildAndPrepareSampleStreamWrappers();
+  }
+
+  @Override
+  public void maybeThrowPrepareError() throws IOException {
+    if (sampleStreamWrappers != null) {
+      for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
+        sampleStreamWrapper.maybeThrowPrepareError();
+      }
+    }
+  }
+
+  @Override
+  public TrackGroupArray getTrackGroups() {
+    return trackGroups;
+  }
+
+  @Override
+  public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+      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 selectedNewTracks = false;
+    streamWrapperIndices.clear();
+    // Select tracks for each child, copying the resulting streams back into a new streams array.
+    SampleStream[] newStreams = new SampleStream[selections.length];
+    SampleStream[] childStreams = new SampleStream[selections.length];
+    TrackSelection[] childSelections = new TrackSelection[selections.length];
+    ArrayList<HlsSampleStreamWrapper> enabledSampleStreamWrapperList = new ArrayList<>(
+        sampleStreamWrappers.length);
+    for (int i = 0; i < sampleStreamWrappers.length; i++) {
+      for (int j = 0; j < selections.length; j++) {
+        childStreams[j] = streamChildIndices[j] == i ? streams[j] : null;
+        childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null;
+      }
+      selectedNewTracks |= sampleStreamWrappers[i].selectTracks(childSelections,
+          mayRetainStreamFlags, childStreams, streamResetFlags, !seenFirstTrackSelection);
+      boolean wrapperEnabled = false;
+      for (int j = 0; j < selections.length; j++) {
+        if (selectionChildIndices[j] == i) {
+          // Assert that the child provided a stream for the selection.
+          Assertions.checkState(childStreams[j] != null);
+          newStreams[j] = childStreams[j];
+          wrapperEnabled = true;
+          streamWrapperIndices.put(childStreams[j], i);
+        } else if (streamChildIndices[j] == i) {
+          // Assert that the child cleared any previous stream.
+          Assertions.checkState(childStreams[j] == null);
+        }
+      }
+      if (wrapperEnabled) {
+        enabledSampleStreamWrapperList.add(sampleStreamWrappers[i]);
+      }
+    }
+    // Copy the new streams back into the streams array.
+    System.arraycopy(newStreams, 0, streams, 0, newStreams.length);
+    // Update the local state.
+    enabledSampleStreamWrappers = new HlsSampleStreamWrapper[enabledSampleStreamWrapperList.size()];
+    enabledSampleStreamWrapperList.toArray(enabledSampleStreamWrappers);
+
+    // The first enabled sample stream wrapper is responsible for intializing the timestamp
+    // adjuster. This way, if present, variants are responsible. Otherwise, audio renditions are.
+    // If only subtitles are present, then text renditions are used for timestamp adjustment
+    // initialization.
+    if (enabledSampleStreamWrappers.length > 0) {
+      enabledSampleStreamWrappers[0].setIsTimestampMaster(true);
+      for (int i = 1; i < enabledSampleStreamWrappers.length; i++) {
+        enabledSampleStreamWrappers[i].setIsTimestampMaster(false);
+      }
+    }
+
+    sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers);
+    if (seenFirstTrackSelection && selectedNewTracks) {
+      seekToUs(positionUs);
+      // We'll need to reset renderers consuming from all streams due to the seek.
+      for (int i = 0; i < selections.length; i++) {
+        if (streams[i] != null) {
+          streamResetFlags[i] = true;
+        }
+      }
+    }
+    seenFirstTrackSelection = true;
+    return positionUs;
+  }
+
+  @Override
+  public boolean continueLoading(long positionUs) {
+    return sequenceableLoader.continueLoading(positionUs);
+  }
+
+  @Override
+  public long getNextLoadPositionUs() {
+    return sequenceableLoader.getNextLoadPositionUs();
+  }
+
+  @Override
+  public long readDiscontinuity() {
+    return C.TIME_UNSET;
+  }
+
+  @Override
+  public long getBufferedPositionUs() {
+    long bufferedPositionUs = Long.MAX_VALUE;
+    for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) {
+      long rendererBufferedPositionUs = sampleStreamWrapper.getBufferedPositionUs();
+      if (rendererBufferedPositionUs != C.TIME_END_OF_SOURCE) {
+        bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
+      }
+    }
+    return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs;
+  }
+
+  @Override
+  public long seekToUs(long positionUs) {
+    timestampAdjusterProvider.reset();
+    for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) {
+      sampleStreamWrapper.seekTo(positionUs);
+    }
+    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(HlsUrl url) {
+    playlistTracker.refreshPlaylist(url);
+  }
+
+  @Override
+  public void onContinueLoadingRequested(HlsSampleStreamWrapper sampleStreamWrapper) {
+    if (trackGroups == null) {
+      // Still preparing.
+      return;
+    }
+    callback.onContinueLoadingRequested(this);
+  }
+
+  // PlaylistListener implementation.
+
+  @Override
+  public void onPlaylistChanged() {
+    continuePreparingOrLoading();
+  }
+
+  @Override
+  public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) {
+    for (HlsSampleStreamWrapper streamWrapper : sampleStreamWrappers) {
+      streamWrapper.onPlaylistBlacklisted(url, blacklistMs);
+    }
+    continuePreparingOrLoading();
+  }
+
+  // Internal methods.
+
+  private void buildAndPrepareSampleStreamWrappers() {
+    HlsMasterPlaylist masterPlaylist = playlistTracker.getMasterPlaylist();
+    // Build the default stream wrapper.
+    List<HlsUrl> selectedVariants = new ArrayList<>(masterPlaylist.variants);
+    ArrayList<HlsUrl> definiteVideoVariants = new ArrayList<>();
+    ArrayList<HlsUrl> definiteAudioOnlyVariants = new ArrayList<>();
+    for (int i = 0; i < selectedVariants.size(); i++) {
+      HlsUrl variant = selectedVariants.get(i);
+      if (variant.format.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) {
+        definiteVideoVariants.add(variant);
+      } else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) {
+        definiteAudioOnlyVariants.add(variant);
+      }
+    }
+    if (!definiteVideoVariants.isEmpty()) {
+      // We've identified some variants as definitely containing video. Assume variants within the
+      // master playlist are marked consistently, and hence that we have the full set. Filter out
+      // any other variants, which are likely to be audio only.
+      selectedVariants = definiteVideoVariants;
+    } else if (definiteAudioOnlyVariants.size() < selectedVariants.size()) {
+      // We've identified some variants, but not all, as being audio only. Filter them out to leave
+      // the remaining variants, which are likely to contain video.
+      selectedVariants.removeAll(definiteAudioOnlyVariants);
+    } else {
+      // Leave the enabled variants unchanged. They're likely either all video or all audio.
+    }
+    List<HlsUrl> audioRenditions = masterPlaylist.audios;
+    List<HlsUrl> subtitleRenditions = masterPlaylist.subtitles;
+    sampleStreamWrappers = new HlsSampleStreamWrapper[1 /* variants */ + audioRenditions.size()
+        + subtitleRenditions.size()];
+    int currentWrapperIndex = 0;
+    pendingPrepareCount = sampleStreamWrappers.length;
+
+    Assertions.checkArgument(!selectedVariants.isEmpty());
+    HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()];
+    selectedVariants.toArray(variants);
+    HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT,
+        variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat);
+    sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
+    sampleStreamWrapper.setIsTimestampMaster(true);
+    sampleStreamWrapper.continuePreparing();
+
+    // TODO: Build video stream wrappers here.
+
+    // Build audio stream wrappers.
+    for (int i = 0; i < audioRenditions.size(); i++) {
+      sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO,
+          new HlsUrl[] {audioRenditions.get(i)}, null, null);
+      sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
+      sampleStreamWrapper.continuePreparing();
+    }
+
+    // Build subtitle stream wrappers.
+    for (int i = 0; i < subtitleRenditions.size(); i++) {
+      HlsUrl url = subtitleRenditions.get(i);
+      sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null,
+          null);
+      sampleStreamWrapper.prepareSingleTrack(url.format);
+      sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper;
+    }
+  }
+
+  private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants,
+      Format muxedAudioFormat, Format muxedCaptionFormat) {
+    DataSource dataSource = dataSourceFactory.createDataSource();
+    HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, dataSource,
+        timestampAdjusterProvider);
+    return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator,
+        preparePositionUs, muxedAudioFormat, muxedCaptionFormat, minLoadableRetryCount,
+        eventDispatcher);
+  }
+
+  private void continuePreparingOrLoading() {
+    if (trackGroups != null) {
+      callback.onContinueLoadingRequested(this);
+    } else {
+      // Some of the wrappers were waiting for their media playlist to prepare.
+      for (HlsSampleStreamWrapper wrapper : sampleStreamWrappers) {
+        wrapper.continuePreparing();
+      }
+    }
+  }
+
+  private static boolean variantHasExplicitCodecWithPrefix(HlsUrl variant, String prefix) {
+    String codecs = variant.format.codecs;
+    if (TextUtils.isEmpty(codecs)) {
+      return false;
+    }
+    String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)");
+    for (String codec : codecArray) {
+      if (codec.startsWith(prefix)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/HlsMediaSource.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.hls;
+
+import android.net.Uri;
+import android.os.Handler;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.SinglePeriodTimeline;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
+import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * An HLS {@link MediaSource}.
+ */
+public final class HlsMediaSource implements MediaSource,
+    HlsPlaylistTracker.PrimaryPlaylistListener {
+
+  /**
+   * The default minimum number of times to retry loading data prior to failing.
+   */
+  public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
+
+  private final Uri manifestUri;
+  private final DataSource.Factory dataSourceFactory;
+  private final int minLoadableRetryCount;
+  private final EventDispatcher eventDispatcher;
+
+  private HlsPlaylistTracker playlistTracker;
+  private Listener sourceListener;
+
+  public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler,
+      AdaptiveMediaSourceEventListener eventListener) {
+    this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler,
+        eventListener);
+  }
+
+  public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory,
+      int minLoadableRetryCount, Handler eventHandler,
+      AdaptiveMediaSourceEventListener eventListener) {
+    this.manifestUri = manifestUri;
+    this.dataSourceFactory = dataSourceFactory;
+    this.minLoadableRetryCount = minLoadableRetryCount;
+    eventDispatcher = new EventDispatcher(eventHandler, eventListener);
+  }
+
+  @Override
+  public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+    Assertions.checkState(playlistTracker == null);
+    playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher,
+        minLoadableRetryCount, this);
+    sourceListener = listener;
+    playlistTracker.start();
+  }
+
+  @Override
+  public void maybeThrowSourceInfoRefreshError() throws IOException {
+    playlistTracker.maybeThrowPlaylistRefreshError();
+  }
+
+  @Override
+  public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+    Assertions.checkArgument(index == 0);
+    return new HlsMediaPeriod(playlistTracker, dataSourceFactory, minLoadableRetryCount,
+        eventDispatcher, allocator, positionUs);
+  }
+
+  @Override
+  public void releasePeriod(MediaPeriod mediaPeriod) {
+    ((HlsMediaPeriod) mediaPeriod).release();
+  }
+
+  @Override
+  public void releaseSource() {
+    if (playlistTracker != null) {
+      playlistTracker.release();
+      playlistTracker = null;
+    }
+    sourceListener = null;
+  }
+
+  @Override
+  public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) {
+    SinglePeriodTimeline timeline;
+    long windowDefaultStartPositionUs = playlist.startOffsetUs;
+    if (playlistTracker.isLive()) {
+      long periodDurationUs = playlist.hasEndTag ? (playlist.startTimeUs + playlist.durationUs)
+          : C.TIME_UNSET;
+      List<HlsMediaPlaylist.Segment> segments = playlist.segments;
+      if (windowDefaultStartPositionUs == C.TIME_UNSET) {
+        windowDefaultStartPositionUs = segments.isEmpty() ? 0
+            : segments.get(Math.max(0, segments.size() - 3)).relativeStartTimeUs;
+      }
+      timeline = new SinglePeriodTimeline(periodDurationUs, playlist.durationUs,
+          playlist.startTimeUs, windowDefaultStartPositionUs, true, !playlist.hasEndTag);
+    } else /* not live */ {
+      if (windowDefaultStartPositionUs == C.TIME_UNSET) {
+        windowDefaultStartPositionUs = 0;
+      }
+      timeline = new SinglePeriodTimeline(playlist.startTimeUs + playlist.durationUs,
+          playlist.durationUs, playlist.startTimeUs, windowDefaultStartPositionUs, true, false);
+    }
+    sourceListener.onSourceInfoRefreshed(timeline, playlist);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStream.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 com.google.android.exoplayer2.source.hls;
+
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.source.SampleStream;
+import java.io.IOException;
+
+/**
+ * {@link SampleStream} for a particular track group in HLS.
+ */
+/* package */ final class HlsSampleStream implements SampleStream {
+
+  public final int group;
+
+  private final HlsSampleStreamWrapper sampleStreamWrapper;
+
+  public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int group) {
+    this.sampleStreamWrapper = sampleStreamWrapper;
+    this.group = group;
+  }
+
+  @Override
+  public boolean isReady() {
+    return sampleStreamWrapper.isReady(group);
+  }
+
+  @Override
+  public void maybeThrowError() throws IOException {
+    sampleStreamWrapper.maybeThrowError();
+  }
+
+  @Override
+  public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
+    return sampleStreamWrapper.readData(group, formatHolder, buffer);
+  }
+
+  @Override
+  public void skipToKeyframeBefore(long timeUs) {
+    sampleStreamWrapper.skipToKeyframeBefore(group, timeUs);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java
@@ -0,0 +1,675 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.hls;
+
+import android.os.Handler;
+import android.text.TextUtils;
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
+import com.google.android.exoplayer2.extractor.DefaultTrackOutput.UpstreamFormatChangedListener;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.source.SequenceableLoader;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.source.chunk.Chunk;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.Loader;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+import java.io.IOException;
+import java.util.LinkedList;
+
+/**
+ * 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>,
+    SequenceableLoader, ExtractorOutput, UpstreamFormatChangedListener {
+
+  /**
+   * A callback to be notified of events.
+   */
+  public interface Callback extends SequenceableLoader.Callback<HlsSampleStreamWrapper> {
+
+    /**
+     * Called when the wrapper has been prepared.
+     */
+    void onPrepared();
+
+    /**
+     * Called to schedule a {@link #continueLoading(long)} call when the playlist referred by the
+     * given url changes.
+     */
+    void onPlaylistRefreshRequired(HlsMasterPlaylist.HlsUrl playlistUrl);
+
+  }
+
+  private static final int PRIMARY_TYPE_NONE = 0;
+  private static final int PRIMARY_TYPE_TEXT = 1;
+  private static final int PRIMARY_TYPE_AUDIO = 2;
+  private static final int PRIMARY_TYPE_VIDEO = 3;
+
+  private final int trackType;
+  private final Callback callback;
+  private final HlsChunkSource chunkSource;
+  private final Allocator allocator;
+  private final Format muxedAudioFormat;
+  private final Format muxedCaptionFormat;
+  private final int minLoadableRetryCount;
+  private final Loader loader;
+  private final EventDispatcher eventDispatcher;
+  private final HlsChunkSource.HlsChunkHolder nextChunkHolder;
+  private final SparseArray<DefaultTrackOutput> sampleQueues;
+  private final LinkedList<HlsMediaChunk> mediaChunks;
+  private final Runnable maybeFinishPrepareRunnable;
+  private final Handler handler;
+
+  private boolean sampleQueuesBuilt;
+  private boolean prepared;
+  private int enabledTrackCount;
+  private Format downstreamTrackFormat;
+  private int upstreamChunkUid;
+  private boolean released;
+
+  // Tracks are complicated in HLS. See documentation of buildTracks for details.
+  // Indexed by track (as exposed by this source).
+  private TrackGroupArray trackGroups;
+  private int primaryTrackGroupIndex;
+  // Indexed by group.
+  private boolean[] groupEnabledStates;
+
+  private long lastSeekPositionUs;
+  private long pendingResetPositionUs;
+
+  private boolean loadingFinished;
+
+  /**
+   * @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 allocator An {@link Allocator} from which to obtain media buffer allocations.
+   * @param positionUs The position from which to start loading media.
+   * @param muxedAudioFormat If HLS master playlist indicates that the stream contains muxed audio,
+   *     this is the audio {@link Format} as defined by the playlist.
+   * @param muxedCaptionFormat If HLS master playlist indicates that the stream contains muxed
+   *     captions, this is the audio {@link Format} as defined by the playlist.
+   * @param minLoadableRetryCount The minimum number of times that the source should retry a load
+   *     before propagating an error.
+   * @param eventDispatcher A dispatcher to notify of events.
+   */
+  public HlsSampleStreamWrapper(int trackType, Callback callback, HlsChunkSource chunkSource,
+      Allocator allocator, long positionUs, Format muxedAudioFormat, Format muxedCaptionFormat,
+      int minLoadableRetryCount, EventDispatcher eventDispatcher) {
+    this.trackType = trackType;
+    this.callback = callback;
+    this.chunkSource = chunkSource;
+    this.allocator = allocator;
+    this.muxedAudioFormat = muxedAudioFormat;
+    this.muxedCaptionFormat = muxedCaptionFormat;
+    this.minLoadableRetryCount = minLoadableRetryCount;
+    this.eventDispatcher = eventDispatcher;
+    loader = new Loader("Loader:HlsSampleStreamWrapper");
+    nextChunkHolder = new HlsChunkSource.HlsChunkHolder();
+    sampleQueues = new SparseArray<>();
+    mediaChunks = new LinkedList<>();
+    maybeFinishPrepareRunnable = new Runnable() {
+      @Override
+      public void run() {
+        maybeFinishPrepare();
+      }
+    };
+    handler = new Handler();
+    lastSeekPositionUs = positionUs;
+    pendingResetPositionUs = positionUs;
+  }
+
+  public void continuePreparing() {
+    if (!prepared) {
+      continueLoading(lastSeekPositionUs);
+    }
+  }
+
+  /**
+   * Prepares a sample stream wrapper for which the master playlist provides enough information to
+   * prepare.
+   */
+  public void prepareSingleTrack(Format format) {
+    track(0).format(format);
+    sampleQueuesBuilt = true;
+    maybeFinishPrepare();
+  }
+
+  public void maybeThrowPrepareError() throws IOException {
+    maybeThrowError();
+  }
+
+  public TrackGroupArray getTrackGroups() {
+    return trackGroups;
+  }
+
+  public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+      SampleStream[] streams, boolean[] streamResetFlags, boolean isFirstTrackSelection) {
+    Assertions.checkState(prepared);
+    // Disable old tracks.
+    for (int i = 0; i < selections.length; i++) {
+      if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
+        int group = ((HlsSampleStream) streams[i]).group;
+        setTrackGroupEnabledState(group, false);
+        sampleQueues.valueAt(group).disable();
+        streams[i] = null;
+      }
+    }
+    // Enable new tracks.
+    boolean selectedNewTracks = false;
+    for (int i = 0; i < selections.length; i++) {
+      if (streams[i] == null && selections[i] != null) {
+        TrackSelection selection = selections[i];
+        int group = trackGroups.indexOf(selection.getTrackGroup());
+        setTrackGroupEnabledState(group, true);
+        if (group == primaryTrackGroupIndex) {
+          chunkSource.selectTracks(selection);
+        }
+        streams[i] = new HlsSampleStream(this, group);
+        streamResetFlags[i] = true;
+        selectedNewTracks = true;
+      }
+    }
+    if (isFirstTrackSelection) {
+      // At the time of the first track selection all queues will be enabled, so we need to disable
+      // any that are no longer required.
+      int sampleQueueCount = sampleQueues.size();
+      for (int i = 0; i < sampleQueueCount; i++) {
+        if (!groupEnabledStates[i]) {
+          sampleQueues.valueAt(i).disable();
+        }
+      }
+    }
+    // Cancel requests if necessary.
+    if (enabledTrackCount == 0) {
+      chunkSource.reset();
+      downstreamTrackFormat = null;
+      mediaChunks.clear();
+      if (loader.isLoading()) {
+        loader.cancelLoading();
+      }
+    }
+    return selectedNewTracks;
+  }
+
+  public void seekTo(long positionUs) {
+    lastSeekPositionUs = positionUs;
+    pendingResetPositionUs = positionUs;
+    loadingFinished = false;
+    mediaChunks.clear();
+    if (loader.isLoading()) {
+      loader.cancelLoading();
+    } else {
+      int sampleQueueCount = sampleQueues.size();
+      for (int i = 0; i < sampleQueueCount; i++) {
+        sampleQueues.valueAt(i).reset(groupEnabledStates[i]);
+      }
+    }
+  }
+
+  public long getBufferedPositionUs() {
+    if (loadingFinished) {
+      return C.TIME_END_OF_SOURCE;
+    } else if (isPendingReset()) {
+      return pendingResetPositionUs;
+    } else {
+      long bufferedPositionUs = lastSeekPositionUs;
+      HlsMediaChunk lastMediaChunk = mediaChunks.getLast();
+      HlsMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk
+          : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null;
+      if (lastCompletedMediaChunk != null) {
+        bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs);
+      }
+      int sampleQueueCount = sampleQueues.size();
+      for (int i = 0; i < sampleQueueCount; i++) {
+        bufferedPositionUs = Math.max(bufferedPositionUs,
+            sampleQueues.valueAt(i).getLargestQueuedTimestampUs());
+      }
+      return bufferedPositionUs;
+    }
+  }
+
+  public void release() {
+    int sampleQueueCount = sampleQueues.size();
+    for (int i = 0; i < sampleQueueCount; i++) {
+      sampleQueues.valueAt(i).disable();
+    }
+    loader.release();
+    handler.removeCallbacksAndMessages(null);
+    released = true;
+  }
+
+  public long getLargestQueuedTimestampUs() {
+    long largestQueuedTimestampUs = Long.MIN_VALUE;
+    for (int i = 0; i < sampleQueues.size(); i++) {
+      largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs,
+          sampleQueues.valueAt(i).getLargestQueuedTimestampUs());
+    }
+    return largestQueuedTimestampUs;
+  }
+
+  public void setIsTimestampMaster(boolean isTimestampMaster) {
+    chunkSource.setIsTimestampMaster(isTimestampMaster);
+  }
+
+  public void onPlaylistBlacklisted(HlsUrl url, long blacklistMs) {
+    chunkSource.onPlaylistBlacklisted(url, blacklistMs);
+  }
+
+  // SampleStream implementation.
+
+  /* package */ boolean isReady(int group) {
+    return loadingFinished || (!isPendingReset() && !sampleQueues.valueAt(group).isEmpty());
+  }
+
+  /* package */ void maybeThrowError() throws IOException {
+    loader.maybeThrowError();
+    chunkSource.maybeThrowError();
+  }
+
+  /* package */ int readData(int group, FormatHolder formatHolder, DecoderInputBuffer buffer) {
+    if (isPendingReset()) {
+      return C.RESULT_NOTHING_READ;
+    }
+
+    while (mediaChunks.size() > 1 && finishedReadingChunk(mediaChunks.getFirst())) {
+      mediaChunks.removeFirst();
+    }
+    HlsMediaChunk currentChunk = mediaChunks.getFirst();
+    Format trackFormat = currentChunk.trackFormat;
+    if (!trackFormat.equals(downstreamTrackFormat)) {
+      eventDispatcher.downstreamFormatChanged(trackType, trackFormat,
+          currentChunk.trackSelectionReason, currentChunk.trackSelectionData,
+          currentChunk.startTimeUs);
+    }
+    downstreamTrackFormat = trackFormat;
+
+    return sampleQueues.valueAt(group).readData(formatHolder, buffer, loadingFinished,
+        lastSeekPositionUs);
+  }
+
+  /* package */ void skipToKeyframeBefore(int group, long timeUs) {
+    sampleQueues.valueAt(group).skipToKeyframeBefore(timeUs);
+  }
+
+  private boolean finishedReadingChunk(HlsMediaChunk chunk) {
+    int chunkUid = chunk.uid;
+    for (int i = 0; i < sampleQueues.size(); i++) {
+      if (groupEnabledStates[i] && sampleQueues.valueAt(i).peekSourceId() == chunkUid) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  // SequenceableLoader implementation
+
+  @Override
+  public boolean continueLoading(long positionUs) {
+    if (loadingFinished || loader.isLoading()) {
+      return false;
+    }
+
+    chunkSource.getNextChunk(mediaChunks.isEmpty() ? null : mediaChunks.getLast(),
+        pendingResetPositionUs != C.TIME_UNSET ? pendingResetPositionUs : positionUs,
+        nextChunkHolder);
+    boolean endOfStream = nextChunkHolder.endOfStream;
+    Chunk loadable = nextChunkHolder.chunk;
+    HlsMasterPlaylist.HlsUrl playlistToLoad = nextChunkHolder.playlist;
+    nextChunkHolder.clear();
+
+    if (endOfStream) {
+      loadingFinished = true;
+      return true;
+    }
+
+    if (loadable == null) {
+      if (playlistToLoad != null) {
+        callback.onPlaylistRefreshRequired(playlistToLoad);
+      }
+      return false;
+    }
+
+    if (isMediaChunk(loadable)) {
+      pendingResetPositionUs = C.TIME_UNSET;
+      HlsMediaChunk mediaChunk = (HlsMediaChunk) loadable;
+      mediaChunk.init(this);
+      mediaChunks.add(mediaChunk);
+    }
+    long elapsedRealtimeMs = loader.startLoading(loadable, this, minLoadableRetryCount);
+    eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat,
+        loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs,
+        loadable.endTimeUs, elapsedRealtimeMs);
+    return true;
+  }
+
+  @Override
+  public long getNextLoadPositionUs() {
+    if (isPendingReset()) {
+      return pendingResetPositionUs;
+    } else {
+      return loadingFinished ? C.TIME_END_OF_SOURCE : mediaChunks.getLast().endTimeUs;
+    }
+  }
+
+  // Loader.Callback implementation.
+
+  @Override
+  public void onLoadCompleted(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs) {
+    chunkSource.onChunkLoadCompleted(loadable);
+    eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat,
+        loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs,
+        loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded());
+    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.type, trackType, loadable.trackFormat,
+        loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs,
+        loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded());
+    if (!released) {
+      int sampleQueueCount = sampleQueues.size();
+      for (int i = 0; i < sampleQueueCount; i++) {
+        sampleQueues.valueAt(i).reset(groupEnabledStates[i]);
+      }
+      callback.onContinueLoadingRequested(this);
+    }
+  }
+
+  @Override
+  public int onLoadError(Chunk loadable, long elapsedRealtimeMs, long loadDurationMs,
+      IOException error) {
+    long bytesLoaded = loadable.bytesLoaded();
+    boolean isMediaChunk = isMediaChunk(loadable);
+    boolean cancelable = !isMediaChunk || bytesLoaded == 0;
+    boolean canceled = false;
+    if (chunkSource.onChunkLoadError(loadable, cancelable, error)) {
+      if (isMediaChunk) {
+        HlsMediaChunk removed = mediaChunks.removeLast();
+        Assertions.checkState(removed == loadable);
+        if (mediaChunks.isEmpty()) {
+          pendingResetPositionUs = lastSeekPositionUs;
+        }
+      }
+      canceled = true;
+    }
+    eventDispatcher.loadError(loadable.dataSpec, loadable.type, trackType, loadable.trackFormat,
+        loadable.trackSelectionReason, loadable.trackSelectionData, loadable.startTimeUs,
+        loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded(), error,
+        canceled);
+    if (canceled) {
+      if (!prepared) {
+        continueLoading(lastSeekPositionUs);
+      } else {
+        callback.onContinueLoadingRequested(this);
+      }
+      return Loader.DONT_RETRY;
+    } else {
+      return Loader.RETRY;
+    }
+  }
+
+  // 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) {
+    upstreamChunkUid = chunkUid;
+    for (int i = 0; i < sampleQueues.size(); i++) {
+      sampleQueues.valueAt(i).sourceId(chunkUid);
+    }
+    if (shouldSpliceIn) {
+      for (int i = 0; i < sampleQueues.size(); i++) {
+        sampleQueues.valueAt(i).splice();
+      }
+    }
+  }
+
+  // ExtractorOutput implementation. Called by the loading thread.
+
+  @Override
+  public DefaultTrackOutput track(int id) {
+    if (sampleQueues.indexOfKey(id) >= 0) {
+      return sampleQueues.get(id);
+    }
+    DefaultTrackOutput trackOutput = new DefaultTrackOutput(allocator);
+    trackOutput.setUpstreamFormatChangeListener(this);
+    trackOutput.sourceId(upstreamChunkUid);
+    sampleQueues.put(id, trackOutput);
+    return trackOutput;
+  }
+
+  @Override
+  public void endTracks() {
+    sampleQueuesBuilt = true;
+    handler.post(maybeFinishPrepareRunnable);
+  }
+
+  @Override
+  public void seekMap(SeekMap seekMap) {
+    // Do nothing.
+  }
+
+  // UpstreamFormatChangedListener implementation. Called by the loading thread.
+
+  @Override
+  public void onUpstreamFormatChanged(Format format) {
+    handler.post(maybeFinishPrepareRunnable);
+  }
+
+  // Internal methods.
+
+  private void maybeFinishPrepare() {
+    if (released || prepared || !sampleQueuesBuilt) {
+      return;
+    }
+    int sampleQueueCount = sampleQueues.size();
+    for (int i = 0; i < sampleQueueCount; i++) {
+      if (sampleQueues.valueAt(i).getUpstreamFormat() == null) {
+        return;
+      }
+    }
+    buildTracks();
+    prepared = true;
+    callback.onPrepared();
+  }
+
+  /**
+   * 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>
+   * <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>
+   * <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.</li>
+   * </ul>
+   */
+  private void buildTracks() {
+    // Iterate through the extractor tracks to discover the "primary" track type, and the index
+    // of the single track of this type.
+    int primaryExtractorTrackType = PRIMARY_TYPE_NONE;
+    int primaryExtractorTrackIndex = C.INDEX_UNSET;
+    int extractorTrackCount = sampleQueues.size();
+    for (int i = 0; i < extractorTrackCount; i++) {
+      String sampleMimeType = sampleQueues.valueAt(i).getUpstreamFormat().sampleMimeType;
+      int trackType;
+      if (MimeTypes.isVideo(sampleMimeType)) {
+        trackType = PRIMARY_TYPE_VIDEO;
+      } else if (MimeTypes.isAudio(sampleMimeType)) {
+        trackType = PRIMARY_TYPE_AUDIO;
+      } else if (MimeTypes.isText(sampleMimeType)) {
+        trackType = PRIMARY_TYPE_TEXT;
+      } else {
+        trackType = PRIMARY_TYPE_NONE;
+      }
+      if (trackType > 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;
+    groupEnabledStates = new boolean[extractorTrackCount];
+
+    // Construct the set of exposed track groups.
+    TrackGroup[] trackGroups = new TrackGroup[extractorTrackCount];
+    for (int i = 0; i < extractorTrackCount; i++) {
+      Format sampleFormat = sampleQueues.valueAt(i).getUpstreamFormat();
+      if (i == primaryExtractorTrackIndex) {
+        Format[] formats = new Format[chunkSourceTrackCount];
+        for (int j = 0; j < chunkSourceTrackCount; j++) {
+          formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat);
+        }
+        trackGroups[i] = new TrackGroup(formats);
+        primaryTrackGroupIndex = i;
+      } else {
+        Format trackFormat = null;
+        if (primaryExtractorTrackType == PRIMARY_TYPE_VIDEO) {
+          if (MimeTypes.isAudio(sampleFormat.sampleMimeType)) {
+            trackFormat = muxedAudioFormat;
+          } else if (MimeTypes.APPLICATION_CEA608.equals(sampleFormat.sampleMimeType)) {
+            trackFormat = muxedCaptionFormat;
+          }
+        }
+        trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat));
+      }
+    }
+    this.trackGroups = new TrackGroupArray(trackGroups);
+  }
+
+  /**
+   * Enables or disables a specified track group.
+   *
+   * @param group The index of the track group.
+   * @param enabledState True if the group is being enabled, or false if it's being disabled.
+   */
+  private void setTrackGroupEnabledState(int group, boolean enabledState) {
+    Assertions.checkState(groupEnabledStates[group] != enabledState);
+    groupEnabledStates[group] = enabledState;
+    enabledTrackCount = enabledTrackCount + (enabledState ? 1 : -1);
+  }
+
+  /**
+   * Derives a track format corresponding to a given container format, by combining it with sample
+   * level information obtained from the samples.
+   *
+   * @param containerFormat The container format for which the track format should be derived.
+   * @param sampleFormat A sample format from which to obtain sample level information.
+   * @return The derived track format.
+   */
+  private static Format deriveFormat(Format containerFormat, Format sampleFormat) {
+    if (containerFormat == null) {
+      return sampleFormat;
+    }
+    String codecs = null;
+    int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType);
+    if (sampleTrackType == C.TRACK_TYPE_AUDIO) {
+      codecs = getAudioCodecs(containerFormat.codecs);
+    } else if (sampleTrackType == C.TRACK_TYPE_VIDEO) {
+      codecs = getVideoCodecs(containerFormat.codecs);
+    }
+    return sampleFormat.copyWithContainerInfo(containerFormat.id, codecs, containerFormat.bitrate,
+        containerFormat.width, containerFormat.height, containerFormat.selectionFlags,
+        containerFormat.language);
+  }
+
+  private boolean isMediaChunk(Chunk chunk) {
+    return chunk instanceof HlsMediaChunk;
+  }
+
+  private boolean isPendingReset() {
+    return pendingResetPositionUs != C.TIME_UNSET;
+  }
+
+  private static String getAudioCodecs(String codecs) {
+    return getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO);
+  }
+
+  private static String getVideoCodecs(String codecs) {
+    return getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO);
+  }
+
+  private static String getCodecsOfType(String codecs, int trackType) {
+    if (TextUtils.isEmpty(codecs)) {
+      return null;
+    }
+    String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)");
+    StringBuilder builder = new StringBuilder();
+    for (String codec : codecArray) {
+      if (trackType == MimeTypes.getTrackTypeOfCodec(codec)) {
+        if (builder.length() > 0) {
+          builder.append(",");
+        }
+        builder.append(codec);
+      }
+    }
+    return builder.length() > 0 ? builder.toString() : null;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/TimestampAdjusterProvider.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.hls;
+
+import android.util.SparseArray;
+import 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.
+   * @param startTimeUs The chunk's start time.
+   * @return A {@link TimestampAdjuster}.
+   */
+  public TimestampAdjuster getAdjuster(int discontinuitySequence, long startTimeUs) {
+    TimestampAdjuster adjuster = timestampAdjusters.get(discontinuitySequence);
+    if (adjuster == null) {
+      adjuster = new TimestampAdjuster(startTimeUs);
+      timestampAdjusters.put(discontinuitySequence, adjuster);
+    }
+    return adjuster;
+  }
+
+  /**
+   * Resets the provider.
+   */
+  public void reset() {
+    timestampAdjusters.clear();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/WebvttExtractor.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 com.google.android.exoplayer2.source.hls;
+
+import android.text.TextUtils;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.extractor.Extractor;
+import com.google.android.exoplayer2.extractor.ExtractorInput;
+import com.google.android.exoplayer2.extractor.ExtractorOutput;
+import com.google.android.exoplayer2.extractor.PositionHolder;
+import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import com.google.android.exoplayer2.text.webvtt.WebvttParserUtil;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.TimestampAdjuster;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 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.
+ */
+/* package */ 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 final String language;
+  private final TimestampAdjuster timestampAdjuster;
+  private final ParsableByteArray sampleDataWrapper;
+
+  private ExtractorOutput output;
+
+  private byte[] sampleData;
+  private int sampleSize;
+
+  public WebvttExtractor(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 {
+    // This extractor is only used for the HLS use case, which should not call this method.
+    throw new IllegalStateException();
+  }
+
+  @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 {
+    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;
+  }
+
+  private void processSample() throws ParserException {
+    ParsableByteArray webvttData = new ParsableByteArray(sampleData);
+
+    // Validate the first line of the header.
+    try {
+      WebvttParserUtil.validateWebvttHeaderLine(webvttData);
+    } catch (SubtitleDecoderException e) {
+      throw new ParserException(e);
+    }
+
+    // Defaults to use if the header doesn't contain an X-TIMESTAMP-MAP header.
+    long vttTimestampUs = 0;
+    long tsTimestampUs = 0;
+
+    // Parse the remainder of the header looking for X-TIMESTAMP-MAP.
+    String line;
+    while (!TextUtils.isEmpty(line = webvttData.readLine())) {
+      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.adjustSampleTimestamp(
+        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);
+  }
+
+  private TrackOutput buildTrackOutput(long subsampleOffsetUs) {
+    TrackOutput trackOutput = output.track(0);
+    trackOutput.format(Format.createTextSampleFormat(null, MimeTypes.TEXT_VTT, null,
+        Format.NO_VALUE, 0, language, null, subsampleOffsetUs));
+    output.endTracks();
+    return trackOutput;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.hls.playlist;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.MimeTypes;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents an HLS master playlist.
+ */
+public final class HlsMasterPlaylist extends HlsPlaylist {
+
+  /**
+   * Represents a url in an HLS master playlist.
+   */
+  public static final class HlsUrl {
+
+    public final String name;
+    public final String url;
+    public final Format format;
+    public final Format videoFormat;
+    public final Format audioFormat;
+    public final Format[] textFormats;
+
+    public static HlsUrl createMediaPlaylistHlsUrl(String baseUri) {
+      Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, null,
+          Format.NO_VALUE, 0, null);
+      return new HlsUrl(null, baseUri, format, null, null, null);
+    }
+
+    public HlsUrl(String name, String url, Format format, Format videoFormat, Format audioFormat,
+        Format[] textFormats) {
+      this.name = name;
+      this.url = url;
+      this.format = format;
+      this.videoFormat = videoFormat;
+      this.audioFormat = audioFormat;
+      this.textFormats = textFormats;
+    }
+
+  }
+
+  public final List<HlsUrl> variants;
+  public final List<HlsUrl> audios;
+  public final List<HlsUrl> subtitles;
+
+  public final Format muxedAudioFormat;
+  public final Format muxedCaptionFormat;
+
+  public HlsMasterPlaylist(String baseUri, List<HlsUrl> variants, List<HlsUrl> audios,
+      List<HlsUrl> subtitles, Format muxedAudioFormat, Format muxedCaptionFormat) {
+    super(baseUri, HlsPlaylist.TYPE_MASTER);
+    this.variants = Collections.unmodifiableList(variants);
+    this.audios = Collections.unmodifiableList(audios);
+    this.subtitles = Collections.unmodifiableList(subtitles);
+    this.muxedAudioFormat = muxedAudioFormat;
+    this.muxedCaptionFormat = muxedCaptionFormat;
+  }
+
+  public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUri) {
+    List<HlsUrl> variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUri));
+    List<HlsUrl> emptyList = Collections.emptyList();
+    return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, null);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.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 com.google.android.exoplayer2.source.hls.playlist;
+
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.C;
+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.
+   */
+  public static final class Segment implements Comparable<Long> {
+
+    public final String url;
+    public final long durationUs;
+    public final int relativeDiscontinuitySequence;
+    public final long relativeStartTimeUs;
+    public final boolean isEncrypted;
+    public final String encryptionKeyUri;
+    public final String encryptionIV;
+    public final long byterangeOffset;
+    public final long byterangeLength;
+
+    public Segment(String uri, long byterangeOffset, long byterangeLength) {
+      this(uri, 0, -1, C.TIME_UNSET, false, null, null, byterangeOffset, byterangeLength);
+    }
+
+    public Segment(String uri, long durationUs, int relativeDiscontinuitySequence,
+        long relativeStartTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV,
+        long byterangeOffset, long byterangeLength) {
+      this.url = uri;
+      this.durationUs = durationUs;
+      this.relativeDiscontinuitySequence = relativeDiscontinuitySequence;
+      this.relativeStartTimeUs = relativeStartTimeUs;
+      this.isEncrypted = isEncrypted;
+      this.encryptionKeyUri = encryptionKeyUri;
+      this.encryptionIV = encryptionIV;
+      this.byterangeOffset = byterangeOffset;
+      this.byterangeLength = byterangeLength;
+    }
+
+    @Override
+    public int compareTo(Long relativeStartTimeUs) {
+      return this.relativeStartTimeUs > relativeStartTimeUs
+          ? 1 : (this.relativeStartTimeUs < relativeStartTimeUs ? -1 : 0);
+    }
+
+  }
+
+  /**
+   * Type of the playlist as specified by #EXT-X-PLAYLIST-TYPE.
+   */
+  @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;
+
+  @PlaylistType
+  public final int playlistType;
+  public final long startOffsetUs;
+  public final long startTimeUs;
+  public final boolean hasDiscontinuitySequence;
+  public final int discontinuitySequence;
+  public final int mediaSequence;
+  public final int version;
+  public final long targetDurationUs;
+  public final boolean hasEndTag;
+  public final boolean hasProgramDateTime;
+  public final Segment initializationSegment;
+  public final List<Segment> segments;
+  public final long durationUs;
+
+  public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, long startOffsetUs,
+      long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence,
+      int mediaSequence, int version, long targetDurationUs, boolean hasEndTag,
+      boolean hasProgramDateTime, Segment initializationSegment, List<Segment> segments) {
+    super(baseUri, HlsPlaylist.TYPE_MEDIA);
+    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.initializationSegment = initializationSegment;
+    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;
+  }
+
+  /**
+   * 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);
+  }
+
+  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 The playlist.
+   */
+  public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) {
+    return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, true,
+        discontinuitySequence, mediaSequence, version, targetDurationUs, hasEndTag,
+        hasProgramDateTime, initializationSegment, segments);
+  }
+
+  /**
+   * Returns a playlist identical to this one except that an end tag is added. If an end tag is
+   * already present then the playlist will return itself.
+   *
+   * @return The playlist.
+   */
+  public HlsMediaPlaylist copyWithEndTag() {
+    if (this.hasEndTag) {
+      return this;
+    }
+    return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs,
+        hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs,
+        true, hasProgramDateTime, initializationSegment, segments);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylist.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 com.google.android.exoplayer2.source.hls.playlist;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents an HLS playlist.
+ */
+public abstract class HlsPlaylist {
+
+  /**
+   * The type of playlist.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({TYPE_MASTER, TYPE_MEDIA})
+  public @interface Type {}
+  public static final int TYPE_MASTER = 0;
+  public static final int TYPE_MEDIA = 1;
+
+  public final String baseUri;
+  @Type
+  public final int type;
+
+  protected HlsPlaylist(String baseUri, @Type int type) {
+    this.baseUri = baseUri;
+    this.type = type;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java
@@ -0,0 +1,446 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.hls.playlist;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.source.UnrecognizedInputFormatException;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
+import com.google.android.exoplayer2.upstream.ParsingLoadable;
+import com.google.android.exoplayer2.util.MimeTypes;
+import 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.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * HLS playlists parsing logic.
+ */
+public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlaylist> {
+
+  private static final String PLAYLIST_HEADER = "#EXTM3U";
+
+  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_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_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_BYTERANGE = "#EXT-X-BYTERANGE";
+
+  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_AES128 = "AES-128";
+
+  private static final String BOOLEAN_TRUE = "YES";
+  private static final String BOOLEAN_FALSE = "NO";
+
+  private static final Pattern REGEX_BANDWIDTH = Pattern.compile("BANDWIDTH=(\\d+)\\b");
+  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_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_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_AES128 + ")");
+  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_INSTREAM_ID = Pattern.compile("INSTREAM-ID=\"(.+?)\"");
+  private static final Pattern REGEX_AUTOSELECT = compileBooleanAttrPattern("AUTOSELECT");
+  private static final Pattern REGEX_DEFAULT = compileBooleanAttrPattern("DEFAULT");
+  private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED");
+
+  @Override
+  public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException {
+    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
+    Queue<String> extraLines = new LinkedList<>();
+    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(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 {
+    ArrayList<HlsMasterPlaylist.HlsUrl> variants = new ArrayList<>();
+    ArrayList<HlsMasterPlaylist.HlsUrl> audios = new ArrayList<>();
+    ArrayList<HlsMasterPlaylist.HlsUrl> subtitles = new ArrayList<>();
+    Format muxedAudioFormat = null;
+    Format muxedCaptionFormat = null;
+
+    String line;
+    while (iterator.hasNext()) {
+      line = iterator.next();
+      if (line.startsWith(TAG_MEDIA)) {
+        @C.SelectionFlags int selectionFlags = parseSelectionFlags(line);
+        String uri = parseOptionalStringAttr(line, REGEX_URI);
+        String name = parseStringAttr(line, REGEX_NAME);
+        String language = parseOptionalStringAttr(line, REGEX_LANGUAGE);
+        Format format;
+        switch (parseStringAttr(line, REGEX_TYPE)) {
+          case TYPE_AUDIO:
+             format = Format.createAudioContainerFormat(name, MimeTypes.APPLICATION_M3U8,
+                null, null, Format.NO_VALUE, Format.NO_VALUE, Format.NO_VALUE, null, selectionFlags,
+                language);
+            if (uri == null) {
+              muxedAudioFormat = format;
+            } else {
+              audios.add(new HlsMasterPlaylist.HlsUrl(name, uri, format, null, format, null));
+            }
+            break;
+          case TYPE_SUBTITLES:
+            format = Format.createTextContainerFormat(name, MimeTypes.APPLICATION_M3U8,
+                MimeTypes.TEXT_VTT, null, Format.NO_VALUE, selectionFlags, language);
+            subtitles.add(new HlsMasterPlaylist.HlsUrl(name, uri, format, null, format, null));
+            break;
+          case TYPE_CLOSED_CAPTIONS:
+            if ("CC1".equals(parseOptionalStringAttr(line, REGEX_INSTREAM_ID))) {
+              muxedCaptionFormat = Format.createTextContainerFormat(name,
+                  MimeTypes.APPLICATION_M3U8, MimeTypes.APPLICATION_CEA608, null, Format.NO_VALUE,
+                  selectionFlags, language);
+            }
+            break;
+          default:
+            // Do nothing.
+            break;
+        }
+      } else if (line.startsWith(TAG_STREAM_INF)) {
+        int bitrate = parseIntAttr(line, REGEX_BANDWIDTH);
+        String codecs = parseOptionalStringAttr(line, REGEX_CODECS);
+        String resolutionString = parseOptionalStringAttr(line, REGEX_RESOLUTION);
+        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;
+        }
+        line = iterator.next();
+        String name = Integer.toString(variants.size());
+        Format format = Format.createVideoContainerFormat(name, MimeTypes.APPLICATION_M3U8, null,
+            codecs, bitrate, width, height, Format.NO_VALUE, null, 0);
+        variants.add(new HlsMasterPlaylist.HlsUrl(name, line, format, null, null, null));
+      }
+    }
+    return new HlsMasterPlaylist(baseUri, variants, audios, subtitles, muxedAudioFormat,
+        muxedCaptionFormat);
+  }
+
+  @C.SelectionFlags
+  private static int parseSelectionFlags(String line) {
+    return (parseBooleanAttribute(line, REGEX_DEFAULT, false) ? C.SELECTION_FLAG_DEFAULT : 0)
+        | (parseBooleanAttribute(line, REGEX_FORCED, false) ? C.SELECTION_FLAG_FORCED : 0)
+        | (parseBooleanAttribute(line, REGEX_AUTOSELECT, false) ? C.SELECTION_FLAG_AUTOSELECT : 0);
+  }
+
+  private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String baseUri)
+      throws IOException {
+    @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN;
+    long startOffsetUs = C.TIME_UNSET;
+    int mediaSequence = 0;
+    int version = 1; // Default version == 1.
+    long targetDurationUs = C.TIME_UNSET;
+    boolean hasEndTag = false;
+    Segment initializationSegment = null;
+    List<Segment> segments = new ArrayList<>();
+
+    long segmentDurationUs = 0;
+    boolean hasDiscontinuitySequence = false;
+    int playlistDiscontinuitySequence = 0;
+    int relativeDiscontinuitySequence = 0;
+    long playlistStartTimeUs = 0;
+    long segmentStartTimeUs = 0;
+    long segmentByteRangeOffset = 0;
+    long segmentByteRangeLength = C.LENGTH_UNSET;
+    int segmentMediaSequence = 0;
+
+    boolean isEncrypted = false;
+    String encryptionKeyUri = null;
+    String encryptionIV = null;
+
+    String line;
+    while (iterator.hasNext()) {
+      line = iterator.next();
+      if (line.startsWith(TAG_PLAYLIST_TYPE)) {
+        String playlistTypeString = parseStringAttr(line, REGEX_PLAYLIST_TYPE);
+        if ("VOD".equals(playlistTypeString)) {
+          playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_VOD;
+        } else if ("EVENT".equals(playlistTypeString)) {
+          playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_EVENT;
+        } else {
+          throw new ParserException("Illegal playlist type: " + playlistTypeString);
+        }
+      } else if (line.startsWith(TAG_START)) {
+        startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND);
+      } else if (line.startsWith(TAG_INIT_SEGMENT)) {
+        String uri = parseStringAttr(line, REGEX_URI);
+        String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE);
+        if (byteRange != null) {
+          String[] splitByteRange = byteRange.split("@");
+          segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
+          if (splitByteRange.length > 1) {
+            segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
+          }
+        }
+        initializationSegment = new Segment(uri, segmentByteRangeOffset, segmentByteRangeLength);
+        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 = parseIntAttr(line, REGEX_MEDIA_SEQUENCE);
+        segmentMediaSequence = mediaSequence;
+      } else if (line.startsWith(TAG_VERSION)) {
+        version = parseIntAttr(line, REGEX_VERSION);
+      } else if (line.startsWith(TAG_MEDIA_DURATION)) {
+        segmentDurationUs =
+            (long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);
+      } else if (line.startsWith(TAG_KEY)) {
+        String method = parseStringAttr(line, REGEX_METHOD);
+        isEncrypted = METHOD_AES128.equals(method);
+        if (isEncrypted) {
+          encryptionKeyUri = parseStringAttr(line, REGEX_URI);
+          encryptionIV = parseOptionalStringAttr(line, REGEX_IV);
+        } else {
+          encryptionKeyUri = null;
+          encryptionIV = null;
+        }
+      } else if (line.startsWith(TAG_BYTERANGE)) {
+        String byteRange = parseStringAttr(line, REGEX_BYTERANGE);
+        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.startsWith("#")) {
+        String segmentEncryptionIV;
+        if (!isEncrypted) {
+          segmentEncryptionIV = null;
+        } else if (encryptionIV != null) {
+          segmentEncryptionIV = encryptionIV;
+        } else {
+          segmentEncryptionIV = Integer.toHexString(segmentMediaSequence);
+        }
+        segmentMediaSequence++;
+        if (segmentByteRangeLength == C.LENGTH_UNSET) {
+          segmentByteRangeOffset = 0;
+        }
+        segments.add(new Segment(line, segmentDurationUs, relativeDiscontinuitySequence,
+            segmentStartTimeUs, isEncrypted, encryptionKeyUri, segmentEncryptionIV,
+            segmentByteRangeOffset, segmentByteRangeLength));
+        segmentStartTimeUs += segmentDurationUs;
+        segmentDurationUs = 0;
+        if (segmentByteRangeLength != C.LENGTH_UNSET) {
+          segmentByteRangeOffset += segmentByteRangeLength;
+        }
+        segmentByteRangeLength = C.LENGTH_UNSET;
+      } else if (line.equals(TAG_ENDLIST)) {
+        hasEndTag = true;
+      }
+    }
+    return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, playlistStartTimeUs,
+        hasDiscontinuitySequence, playlistDiscontinuitySequence, mediaSequence, version,
+        targetDurationUs, hasEndTag, playlistStartTimeUs != 0, initializationSegment, segments);
+  }
+
+  private static String parseStringAttr(String line, Pattern pattern) throws ParserException {
+    Matcher matcher = pattern.matcher(line);
+    if (matcher.find() && matcher.groupCount() == 1) {
+      return matcher.group(1);
+    }
+    throw new ParserException("Couldn't match " + pattern.pattern() + " in " + line);
+  }
+
+  private static int parseIntAttr(String line, Pattern pattern) throws ParserException {
+    return Integer.parseInt(parseStringAttr(line, pattern));
+  }
+
+  private static double parseDoubleAttr(String line, Pattern pattern) throws ParserException {
+    return Double.parseDouble(parseStringAttr(line, pattern));
+  }
+
+  private static String parseOptionalStringAttr(String line, Pattern pattern) {
+    Matcher matcher = pattern.matcher(line);
+    if (matcher.find()) {
+      return matcher.group(1);
+    }
+    return null;
+  }
+
+  private static boolean parseBooleanAttribute(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;
+
+    private String next;
+
+    public LineIterator(Queue<String> extraLines, BufferedReader reader) {
+      this.extraLines = extraLines;
+      this.reader = reader;
+    }
+
+    public boolean hasNext() throws IOException {
+      if (next != null) {
+        return true;
+      }
+      if (!extraLines.isEmpty()) {
+        next = extraLines.poll();
+        return true;
+      }
+      while ((next = reader.readLine()) != null) {
+        next = next.trim();
+        if (!next.isEmpty()) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    public String next() throws IOException {
+      String result = null;
+      if (hasNext()) {
+        result = next;
+        next = null;
+      }
+      return result;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.java
@@ -0,0 +1,538 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.hls.playlist;
+
+import android.net.Uri;
+import android.os.Handler;
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
+import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.Loader;
+import com.google.android.exoplayer2.upstream.ParsingLoadable;
+import com.google.android.exoplayer2.util.UriUtil;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+/**
+ * Tracks playlists linked to a provided playlist url. The provided url might reference an HLS
+ * master playlist or a media playlist.
+ */
+public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable<HlsPlaylist>> {
+
+  /**
+   * Listener for primary playlist changes.
+   */
+  public interface PrimaryPlaylistListener {
+
+    /**
+     * Called when the primary playlist changes.
+     *
+     * @param mediaPlaylist The primary playlist new snapshot.
+     */
+    void onPrimaryPlaylistRefreshed(HlsMediaPlaylist mediaPlaylist);
+
+  }
+
+  /**
+   * Called on playlist loading events.
+   */
+  public 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 number of milliseconds for which the playlist has been
+     *     blacklisted.
+     */
+    void onPlaylistBlacklisted(HlsUrl url, long blacklistDurationMs);
+
+  }
+
+  /**
+   * The minimum number of milliseconds that a url is kept as primary url, if no
+   * {@link #getPlaylistSnapshot} call is made for that url.
+   */
+  private static final long PRIMARY_URL_KEEPALIVE_MS = 15000;
+
+  private final Uri initialPlaylistUri;
+  private final DataSource.Factory dataSourceFactory;
+  private final HlsPlaylistParser playlistParser;
+  private final int minRetryCount;
+  private final IdentityHashMap<HlsUrl, MediaPlaylistBundle> playlistBundles;
+  private final Handler playlistRefreshHandler;
+  private final PrimaryPlaylistListener primaryPlaylistListener;
+  private final List<PlaylistEventListener> listeners;
+  private final Loader initialPlaylistLoader;
+  private final EventDispatcher eventDispatcher;
+
+  private HlsMasterPlaylist masterPlaylist;
+  private HlsUrl primaryHlsUrl;
+  private HlsMediaPlaylist primaryUrlSnapshot;
+  private boolean isLive;
+
+  /**
+   * @param initialPlaylistUri Uri for the initial playlist of the stream. Can refer a media
+   *     playlist or a master playlist.
+   * @param dataSourceFactory A factory for {@link DataSource} instances.
+   * @param eventDispatcher A dispatcher to notify of events.
+   * @param minRetryCount The minimum number of times the load must be retried before blacklisting a
+   *     playlist.
+   * @param primaryPlaylistListener A callback for the primary playlist change events.
+   */
+  public HlsPlaylistTracker(Uri initialPlaylistUri, DataSource.Factory dataSourceFactory,
+      EventDispatcher eventDispatcher, int minRetryCount,
+      PrimaryPlaylistListener primaryPlaylistListener) {
+    this.initialPlaylistUri = initialPlaylistUri;
+    this.dataSourceFactory = dataSourceFactory;
+    this.eventDispatcher = eventDispatcher;
+    this.minRetryCount = minRetryCount;
+    this.primaryPlaylistListener = primaryPlaylistListener;
+    listeners = new ArrayList<>();
+    initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist");
+    playlistParser = new HlsPlaylistParser();
+    playlistBundles = new IdentityHashMap<>();
+    playlistRefreshHandler = new Handler();
+  }
+
+  /**
+   * Registers a listener to receive events from the playlist tracker.
+   *
+   * @param listener The listener.
+   */
+  public void addListener(PlaylistEventListener listener) {
+    listeners.add(listener);
+  }
+
+  /**
+   * Unregisters a listener.
+   *
+   * @param listener The listener to unregister.
+   */
+  public void removeListener(PlaylistEventListener listener) {
+    listeners.remove(listener);
+  }
+
+  /**
+   * Starts tracking all the playlists related to the provided Uri.
+   */
+  public void start() {
+    ParsingLoadable<HlsPlaylist> masterPlaylistLoadable = new ParsingLoadable<>(
+        dataSourceFactory.createDataSource(), initialPlaylistUri, C.DATA_TYPE_MANIFEST,
+        playlistParser);
+    initialPlaylistLoader.startLoading(masterPlaylistLoadable, this, minRetryCount);
+  }
+
+  /**
+   * Returns the master playlist.
+   *
+   * @return The master playlist. Null if the initial playlist has yet to be loaded.
+   */
+  public HlsMasterPlaylist getMasterPlaylist() {
+    return masterPlaylist;
+  }
+
+  /**
+   * Returns the most recent snapshot available of the playlist referenced by the provided
+   * {@link HlsUrl}.
+   *
+   * @param url The {@link HlsUrl} corresponding to the requested media playlist.
+   * @return The most recent snapshot of the playlist referenced by the provided {@link HlsUrl}. May
+   *     be null if no snapshot has been loaded yet.
+   */
+  public HlsMediaPlaylist getPlaylistSnapshot(HlsUrl url) {
+    HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot();
+    if (snapshot != null) {
+      maybeSetPrimaryUrl(url);
+    }
+    return snapshot;
+  }
+
+  /**
+   * Returns whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is
+   * valid, meaning all the segments referenced by the playlist are expected to be available. If the
+   * playlist is not valid then some of the segments may no longer be available.
+
+   * @param url The {@link HlsUrl}.
+   * @return Whether the snapshot of the playlist referenced by the provided {@link HlsUrl} is
+   *     valid.
+   */
+  public boolean isSnapshotValid(HlsUrl url) {
+    return playlistBundles.get(url).isSnapshotValid();
+  }
+
+  /**
+   * Releases the playlist tracker.
+   */
+  public void release() {
+    initialPlaylistLoader.release();
+    for (MediaPlaylistBundle bundle : playlistBundles.values()) {
+      bundle.release();
+    }
+    playlistRefreshHandler.removeCallbacksAndMessages(null);
+    playlistBundles.clear();
+  }
+
+  /**
+   * If the tracker is having trouble refreshing the primary playlist or loading an irreplaceable
+   * playlist, this method throws the underlying error. Otherwise, does nothing.
+   *
+   * @throws IOException The underlying error.
+   */
+  public void maybeThrowPlaylistRefreshError() throws IOException {
+    initialPlaylistLoader.maybeThrowError();
+    if (primaryHlsUrl != null) {
+      playlistBundles.get(primaryHlsUrl).mediaPlaylistLoader.maybeThrowError();
+    }
+  }
+
+  /**
+   * Triggers a playlist refresh and whitelists it.
+   *
+   * @param url The {@link HlsUrl} of the playlist to be refreshed.
+   */
+  public void refreshPlaylist(HlsUrl url) {
+    playlistBundles.get(url).loadPlaylist();
+  }
+
+  /**
+   * Returns whether this is live content.
+   *
+   * @return True if the content is live. False otherwise.
+   */
+  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;
+    primaryHlsUrl = masterPlaylist.variants.get(0);
+    ArrayList<HlsUrl> urls = new ArrayList<>();
+    urls.addAll(masterPlaylist.variants);
+    urls.addAll(masterPlaylist.audios);
+    urls.addAll(masterPlaylist.subtitles);
+    createBundles(urls);
+    MediaPlaylistBundle primaryBundle = playlistBundles.get(primaryHlsUrl);
+    if (isMediaPlaylist) {
+      // We don't need to load the playlist again. We can use the same result.
+      primaryBundle.processLoadedPlaylist((HlsMediaPlaylist) result);
+    } else {
+      primaryBundle.loadPlaylist();
+    }
+    eventDispatcher.loadCompleted(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
+        loadDurationMs, loadable.bytesLoaded());
+  }
+
+  @Override
+  public void onLoadCanceled(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
+      long loadDurationMs, boolean released) {
+    eventDispatcher.loadCanceled(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
+        loadDurationMs, loadable.bytesLoaded());
+  }
+
+  @Override
+  public int onLoadError(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
+      long loadDurationMs, IOException error) {
+    boolean isFatal = error instanceof ParserException;
+    eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
+        loadDurationMs, loadable.bytesLoaded(), error, isFatal);
+    return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY;
+  }
+
+  // Internal methods.
+
+  private boolean maybeSelectNewPrimaryUrl() {
+    List<HlsUrl> variants = masterPlaylist.variants;
+    int variantsSize = variants.size();
+    long currentTimeMs = SystemClock.elapsedRealtime();
+    for (int i = 0; i < variantsSize; i++) {
+      MediaPlaylistBundle bundle = playlistBundles.get(variants.get(i));
+      if (currentTimeMs > bundle.blacklistUntilMs) {
+        primaryHlsUrl = bundle.playlistUrl;
+        bundle.loadPlaylist();
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private void maybeSetPrimaryUrl(HlsUrl url) {
+    if (!masterPlaylist.variants.contains(url)
+        || (primaryUrlSnapshot != null && primaryUrlSnapshot.hasEndTag)) {
+      // Only allow variant urls to be chosen as primary. Also prevent changing the primary url if
+      // the last primary snapshot contains an end tag.
+      return;
+    }
+    MediaPlaylistBundle currentPrimaryBundle = playlistBundles.get(primaryHlsUrl);
+    long primarySnapshotAccessAgeMs =
+        currentPrimaryBundle.lastSnapshotAccessTimeMs - SystemClock.elapsedRealtime();
+    if (primarySnapshotAccessAgeMs > PRIMARY_URL_KEEPALIVE_MS) {
+      primaryHlsUrl = url;
+      playlistBundles.get(primaryHlsUrl).loadPlaylist();
+    }
+  }
+
+  private void createBundles(List<HlsUrl> urls) {
+    int listSize = urls.size();
+    long currentTimeMs = SystemClock.elapsedRealtime();
+    for (int i = 0; i < listSize; i++) {
+      HlsUrl url = urls.get(i);
+      MediaPlaylistBundle bundle = new MediaPlaylistBundle(url, currentTimeMs);
+      playlistBundles.put(urls.get(i), bundle);
+    }
+  }
+
+  /**
+   * Called by the bundles when a snapshot changes.
+   *
+   * @param url The url of the playlist.
+   * @param newSnapshot The new snapshot.
+   * @return True if a refresh should be scheduled.
+   */
+  private boolean onPlaylistUpdated(HlsUrl url, HlsMediaPlaylist newSnapshot) {
+    if (url == primaryHlsUrl) {
+      if (primaryUrlSnapshot == null) {
+        // This is the first primary url snapshot.
+        isLive = !newSnapshot.hasEndTag;
+      }
+      primaryUrlSnapshot = newSnapshot;
+      primaryPlaylistListener.onPrimaryPlaylistRefreshed(newSnapshot);
+    }
+    int listenersSize = listeners.size();
+    for (int i = 0; i < listenersSize; i++) {
+      listeners.get(i).onPlaylistChanged();
+    }
+    // If the primary playlist is not the final one, we should schedule a refresh.
+    return url == primaryHlsUrl && !newSnapshot.hasEndTag;
+  }
+
+  private void notifyPlaylistBlacklisting(HlsUrl url, long blacklistMs) {
+    int listenersSize = listeners.size();
+    for (int i = 0; i < listenersSize; i++) {
+      listeners.get(i).onPlaylistBlacklisted(url, blacklistMs);
+    }
+  }
+
+  private HlsMediaPlaylist getLatestPlaylistSnapshot(HlsMediaPlaylist oldPlaylist,
+      HlsMediaPlaylist loadedPlaylist) {
+    if (!loadedPlaylist.isNewerThan(oldPlaylist)) {
+      if (loadedPlaylist.hasEndTag) {
+        // If the loaded playlist has an end tag but is not newer than the old playlist then we have
+        // an inconsistent state. This is typically caused by the server incorrectly resetting the
+        // media sequence when appending the end tag. We resolve this case as best we can by
+        // returning the old playlist with the end tag appended.
+        return oldPlaylist.copyWithEndTag();
+      } else {
+        return oldPlaylist;
+      }
+    }
+    long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist);
+    int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist);
+    return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence);
+  }
+
+  private long getLoadedPlaylistStartTimeUs(HlsMediaPlaylist oldPlaylist,
+      HlsMediaPlaylist loadedPlaylist) {
+    if (loadedPlaylist.hasProgramDateTime) {
+      return loadedPlaylist.startTimeUs;
+    }
+    long primarySnapshotStartTimeUs = primaryUrlSnapshot != null
+        ? primaryUrlSnapshot.startTimeUs : 0;
+    if (oldPlaylist == null) {
+      return primarySnapshotStartTimeUs;
+    }
+    int oldPlaylistSize = oldPlaylist.segments.size();
+    Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);
+    if (firstOldOverlappingSegment != null) {
+      return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs;
+    } else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) {
+      return oldPlaylist.getEndTimeUs();
+    } else {
+      // No segments overlap, we assume the new playlist start coincides with the primary playlist.
+      return primarySnapshotStartTimeUs;
+    }
+  }
+
+  private int getLoadedPlaylistDiscontinuitySequence(HlsMediaPlaylist oldPlaylist,
+      HlsMediaPlaylist loadedPlaylist) {
+    if (loadedPlaylist.hasDiscontinuitySequence) {
+      return loadedPlaylist.discontinuitySequence;
+    }
+    // TODO: Improve cross-playlist discontinuity adjustment.
+    int primaryUrlDiscontinuitySequence = primaryUrlSnapshot != null
+        ? primaryUrlSnapshot.discontinuitySequence : 0;
+    if (oldPlaylist == null) {
+      return primaryUrlDiscontinuitySequence;
+    }
+    Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);
+    if (firstOldOverlappingSegment != null) {
+      return oldPlaylist.discontinuitySequence
+          + firstOldOverlappingSegment.relativeDiscontinuitySequence
+          - loadedPlaylist.segments.get(0).relativeDiscontinuitySequence;
+    }
+    return primaryUrlDiscontinuitySequence;
+  }
+
+  private static Segment getFirstOldOverlappingSegment(HlsMediaPlaylist oldPlaylist,
+      HlsMediaPlaylist loadedPlaylist) {
+    int mediaSequenceOffset = loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence;
+    List<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 HlsUrl playlistUrl;
+    private final Loader mediaPlaylistLoader;
+    private final ParsingLoadable<HlsPlaylist> mediaPlaylistLoadable;
+
+    private HlsMediaPlaylist playlistSnapshot;
+    private long lastSnapshotLoadMs;
+    private long lastSnapshotAccessTimeMs;
+    private long blacklistUntilMs;
+
+    public MediaPlaylistBundle(HlsUrl playlistUrl, long initialLastSnapshotAccessTimeMs) {
+      this.playlistUrl = playlistUrl;
+      lastSnapshotAccessTimeMs = initialLastSnapshotAccessTimeMs;
+      mediaPlaylistLoader = new Loader("HlsPlaylistTracker:MediaPlaylist");
+      mediaPlaylistLoadable = new ParsingLoadable<>(dataSourceFactory.createDataSource(),
+          UriUtil.resolveToUri(masterPlaylist.baseUri, playlistUrl.url), C.DATA_TYPE_MANIFEST,
+          playlistParser);
+    }
+
+    public HlsMediaPlaylist getPlaylistSnapshot() {
+      lastSnapshotAccessTimeMs = SystemClock.elapsedRealtime();
+      return playlistSnapshot;
+    }
+
+    public boolean isSnapshotValid() {
+      if (playlistSnapshot == null) {
+        return false;
+      }
+      long currentTimeMs = SystemClock.elapsedRealtime();
+      long snapshotValidityDurationMs = Math.max(30000, C.usToMs(playlistSnapshot.durationUs));
+      return playlistSnapshot.hasEndTag
+          || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_EVENT
+          || playlistSnapshot.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD
+          || lastSnapshotLoadMs + snapshotValidityDurationMs > currentTimeMs;
+    }
+
+    public void release() {
+      mediaPlaylistLoader.release();
+    }
+
+    public void loadPlaylist() {
+      blacklistUntilMs = 0;
+      if (!mediaPlaylistLoader.isLoading()) {
+        mediaPlaylistLoader.startLoading(mediaPlaylistLoadable, this, minRetryCount);
+      }
+    }
+
+    // Loader.Callback implementation.
+
+    @Override
+    public void onLoadCompleted(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
+        long loadDurationMs) {
+      processLoadedPlaylist((HlsMediaPlaylist) loadable.getResult());
+      eventDispatcher.loadCompleted(loadable.dataSpec, 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, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
+          loadDurationMs, loadable.bytesLoaded());
+    }
+
+    @Override
+    public int onLoadError(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
+        long loadDurationMs, IOException error) {
+      boolean isFatal = error instanceof ParserException;
+      eventDispatcher.loadError(loadable.dataSpec, C.DATA_TYPE_MANIFEST, elapsedRealtimeMs,
+          loadDurationMs, loadable.bytesLoaded(), error, isFatal);
+      if (isFatal) {
+        return Loader.DONT_RETRY_FATAL;
+      }
+      boolean shouldRetry = true;
+      if (ChunkedTrackBlacklistUtil.shouldBlacklist(error)) {
+        blacklistUntilMs =
+            SystemClock.elapsedRealtime() + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS;
+        notifyPlaylistBlacklisting(playlistUrl,
+            ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS);
+        shouldRetry = primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl();
+      }
+      return shouldRetry ? Loader.RETRY : Loader.DONT_RETRY;
+    }
+
+    // Runnable implementation.
+
+    @Override
+    public void run() {
+      loadPlaylist();
+    }
+
+    // Internal methods.
+
+    private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) {
+      HlsMediaPlaylist oldPlaylist = playlistSnapshot;
+      lastSnapshotLoadMs = SystemClock.elapsedRealtime();
+      playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist);
+      long refreshDelayUs = C.TIME_UNSET;
+      if (playlistSnapshot != oldPlaylist) {
+        if (onPlaylistUpdated(playlistUrl, playlistSnapshot)) {
+          refreshDelayUs = playlistSnapshot.targetDurationUs;
+        }
+      } else if (!playlistSnapshot.hasEndTag) {
+        refreshDelayUs = playlistSnapshot.targetDurationUs / 2;
+      }
+      if (refreshDelayUs != C.TIME_UNSET) {
+        // See HLS spec v20, section 6.3.4 for more information on media playlist refreshing.
+        playlistRefreshHandler.postDelayed(this, C.usToMs(refreshDelayUs));
+      }
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.smoothstreaming;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
+import com.google.android.exoplayer2.extractor.mp4.Track;
+import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox;
+import com.google.android.exoplayer2.source.BehindLiveWindowException;
+import com.google.android.exoplayer2.source.chunk.Chunk;
+import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;
+import com.google.android.exoplayer2.source.chunk.ChunkHolder;
+import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil;
+import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk;
+import com.google.android.exoplayer2.source.chunk.MediaChunk;
+import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
+import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * A default {@link SsChunkSource} implementation.
+ */
+public class DefaultSsChunkSource implements SsChunkSource {
+
+  public static final class Factory implements SsChunkSource.Factory {
+
+    private final DataSource.Factory dataSourceFactory;
+
+    public Factory(DataSource.Factory dataSourceFactory) {
+      this.dataSourceFactory = dataSourceFactory;
+    }
+
+    @Override
+    public SsChunkSource createChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
+        SsManifest manifest, int elementIndex, TrackSelection trackSelection,
+        TrackEncryptionBox[] trackEncryptionBoxes) {
+      DataSource dataSource = dataSourceFactory.createDataSource();
+      return new DefaultSsChunkSource(manifestLoaderErrorThrower, manifest, elementIndex,
+          trackSelection, dataSource, trackEncryptionBoxes);
+    }
+
+  }
+
+  private final LoaderErrorThrower manifestLoaderErrorThrower;
+  private final int elementIndex;
+  private final TrackSelection trackSelection;
+  private final ChunkExtractorWrapper[] extractorWrappers;
+  private final DataSource dataSource;
+
+  private SsManifest manifest;
+  private int currentManifestChunkOffset;
+
+  private IOException fatalError;
+
+  /**
+   * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
+   * @param manifest The initial manifest.
+   * @param elementIndex The index of the stream element in the manifest.
+   * @param trackSelection The track selection.
+   * @param dataSource A {@link DataSource} suitable for loading the media data.
+   * @param trackEncryptionBoxes Track encryption boxes for the stream.
+   */
+  public DefaultSsChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, SsManifest manifest,
+      int elementIndex, TrackSelection trackSelection, DataSource dataSource,
+      TrackEncryptionBox[] trackEncryptionBoxes) {
+    this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
+    this.manifest = manifest;
+    this.elementIndex = elementIndex;
+    this.trackSelection = trackSelection;
+    this.dataSource = dataSource;
+
+    StreamElement streamElement = manifest.streamElements[elementIndex];
+
+    extractorWrappers = new ChunkExtractorWrapper[trackSelection.length()];
+    for (int i = 0; i < extractorWrappers.length; i++) {
+      int manifestTrackIndex = trackSelection.getIndexInTrackGroup(i);
+      Format format = streamElement.formats[manifestTrackIndex];
+      int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : 0;
+      Track track = new Track(manifestTrackIndex, streamElement.type, streamElement.timescale,
+          C.TIME_UNSET, manifest.durationUs, format, Track.TRANSFORMATION_NONE,
+          trackEncryptionBoxes, nalUnitLengthFieldLength, null, null);
+      FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(
+          FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME
+          | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track, null);
+      extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, false, false);
+    }
+  }
+
+  @Override
+  public void updateManifest(SsManifest newManifest) {
+    StreamElement currentElement = manifest.streamElements[elementIndex];
+    int currentElementChunkCount = currentElement.chunkCount;
+    StreamElement newElement = newManifest.streamElements[elementIndex];
+    if (currentElementChunkCount == 0 || newElement.chunkCount == 0) {
+      // There's no overlap between the old and new elements because at least one is empty.
+      currentManifestChunkOffset += currentElementChunkCount;
+    } else {
+      long currentElementEndTimeUs = currentElement.getStartTimeUs(currentElementChunkCount - 1)
+          + currentElement.getChunkDurationUs(currentElementChunkCount - 1);
+      long newElementStartTimeUs = newElement.getStartTimeUs(0);
+      if (currentElementEndTimeUs <= newElementStartTimeUs) {
+        // There's no overlap between the old and new elements.
+        currentManifestChunkOffset += currentElementChunkCount;
+      } else {
+        // The new element overlaps with the old one.
+        currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs);
+      }
+    }
+    manifest = newManifest;
+  }
+
+  // ChunkSource implementation.
+
+  @Override
+  public void maybeThrowError() throws IOException {
+    if (fatalError != null) {
+      throw fatalError;
+    } else {
+      manifestLoaderErrorThrower.maybeThrowError();
+    }
+  }
+
+  @Override
+  public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
+    if (fatalError != null || trackSelection.length() < 2) {
+      return queue.size();
+    }
+    return trackSelection.evaluateQueueSize(playbackPositionUs, queue);
+  }
+
+  @Override
+  public final void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) {
+    if (fatalError != null) {
+      return;
+    }
+
+    long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0;
+    trackSelection.updateSelectedTrack(bufferedDurationUs);
+
+    StreamElement streamElement = manifest.streamElements[elementIndex];
+    if (streamElement.chunkCount == 0) {
+      // There aren't any chunks for us to load.
+      out.endOfStream = !manifest.isLive;
+      return;
+    }
+
+    int chunkIndex;
+    if (previous == null) {
+      chunkIndex = streamElement.getChunkIndex(playbackPositionUs);
+    } else {
+      chunkIndex = previous.getNextChunkIndex() - currentManifestChunkOffset;
+      if (chunkIndex < 0) {
+        // This is before the first chunk in the current manifest.
+        fatalError = new BehindLiveWindowException();
+        return;
+      }
+    }
+
+    if (chunkIndex >= streamElement.chunkCount) {
+      // This is beyond the last chunk in the current manifest.
+      out.endOfStream = !manifest.isLive;
+      return;
+    }
+
+    long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex);
+    long chunkEndTimeUs = chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex);
+    int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset;
+
+    int trackSelectionIndex = trackSelection.getSelectedIndex();
+    ChunkExtractorWrapper extractorWrapper = extractorWrappers[trackSelectionIndex];
+
+    int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex);
+    Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex);
+
+    out.chunk = newMediaChunk(trackSelection.getSelectedFormat(), dataSource, uri, null,
+        currentAbsoluteChunkIndex, chunkStartTimeUs, chunkEndTimeUs,
+        trackSelection.getSelectionReason(), trackSelection.getSelectionData(), extractorWrapper);
+  }
+
+  @Override
+  public void onChunkLoadCompleted(Chunk chunk) {
+    // Do nothing.
+  }
+
+  @Override
+  public boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e) {
+    return cancelable && ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection,
+        trackSelection.indexOf(chunk.trackFormat), e);
+  }
+
+  // Private methods.
+
+  private static MediaChunk newMediaChunk(Format format, DataSource dataSource, Uri uri,
+      String cacheKey, int chunkIndex, long chunkStartTimeUs, long chunkEndTimeUs,
+      int trackSelectionReason, Object trackSelectionData, ChunkExtractorWrapper extractorWrapper) {
+    DataSpec dataSpec = new DataSpec(uri, 0, C.LENGTH_UNSET, cacheKey);
+    // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk.
+    // To convert them the absolute timestamps, we need to set sampleOffsetUs to chunkStartTimeUs.
+    long sampleOffsetUs = chunkStartTimeUs;
+    return new ContainerMediaChunk(dataSource, dataSpec, format, trackSelectionReason,
+        trackSelectionData, chunkStartTimeUs, chunkEndTimeUs, chunkIndex, 1, sampleOffsetUs,
+        extractorWrapper, format);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.smoothstreaming;
+
+import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox;
+import com.google.android.exoplayer2.source.chunk.ChunkSource;
+import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
+
+/**
+ * A {@link ChunkSource} for SmoothStreaming.
+ */
+public interface SsChunkSource extends ChunkSource {
+
+  interface Factory {
+
+    SsChunkSource createChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
+        SsManifest manifest, int elementIndex, TrackSelection trackSelection,
+        TrackEncryptionBox[] trackEncryptionBoxes);
+
+  }
+
+  void updateManifest(SsManifest newManifest);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.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 com.google.android.exoplayer2.source.smoothstreaming;
+
+import android.util.Base64;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.CompositeSequenceableLoader;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.source.SequenceableLoader;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.source.chunk.ChunkSampleStream;
+import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
+import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * A SmoothStreaming {@link MediaPeriod}.
+ */
+/* package */ final class SsMediaPeriod implements MediaPeriod,
+    SequenceableLoader.Callback<ChunkSampleStream<SsChunkSource>> {
+
+  private static final int INITIALIZATION_VECTOR_SIZE = 8;
+
+  private final SsChunkSource.Factory chunkSourceFactory;
+  private final LoaderErrorThrower manifestLoaderErrorThrower;
+  private final int minLoadableRetryCount;
+  private final EventDispatcher eventDispatcher;
+  private final Allocator allocator;
+  private final TrackGroupArray trackGroups;
+  private final TrackEncryptionBox[] trackEncryptionBoxes;
+
+  private Callback callback;
+  private SsManifest manifest;
+  private ChunkSampleStream<SsChunkSource>[] sampleStreams;
+  private CompositeSequenceableLoader sequenceableLoader;
+
+  public SsMediaPeriod(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory,
+      int minLoadableRetryCount, EventDispatcher eventDispatcher,
+      LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator) {
+    this.chunkSourceFactory = chunkSourceFactory;
+    this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
+    this.minLoadableRetryCount = minLoadableRetryCount;
+    this.eventDispatcher = eventDispatcher;
+    this.allocator = allocator;
+
+    trackGroups = buildTrackGroups(manifest);
+    ProtectionElement protectionElement = manifest.protectionElement;
+    if (protectionElement != null) {
+      byte[] keyId = getProtectionElementKeyId(protectionElement.data);
+      trackEncryptionBoxes = new TrackEncryptionBox[] {
+          new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId)};
+    } else {
+      trackEncryptionBoxes = null;
+    }
+    this.manifest = manifest;
+    sampleStreams = newSampleStreamArray(0);
+    sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
+  }
+
+  public void updateManifest(SsManifest manifest) {
+    this.manifest = manifest;
+    for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {
+      sampleStream.getChunkSource().updateManifest(manifest);
+    }
+    callback.onContinueLoadingRequested(this);
+  }
+
+  public void release() {
+    for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {
+      sampleStream.release();
+    }
+  }
+
+  @Override
+  public void prepare(Callback callback) {
+    this.callback = callback;
+    callback.onPrepared(this);
+  }
+
+  @Override
+  public void maybeThrowPrepareError() throws IOException {
+    manifestLoaderErrorThrower.maybeThrowError();
+  }
+
+  @Override
+  public TrackGroupArray getTrackGroups() {
+    return trackGroups;
+  }
+
+  @Override
+  public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
+      SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
+    ArrayList<ChunkSampleStream<SsChunkSource>> sampleStreamsList = new ArrayList<>();
+    for (int i = 0; i < selections.length; i++) {
+      if (streams[i] != null) {
+        @SuppressWarnings("unchecked")
+        ChunkSampleStream<SsChunkSource> stream = (ChunkSampleStream<SsChunkSource>) streams[i];
+        if (selections[i] == null || !mayRetainStreamFlags[i]) {
+          stream.release();
+          streams[i] = null;
+        } else {
+          sampleStreamsList.add(stream);
+        }
+      }
+      if (streams[i] == null && selections[i] != null) {
+        ChunkSampleStream<SsChunkSource> stream = buildSampleStream(selections[i], positionUs);
+        sampleStreamsList.add(stream);
+        streams[i] = stream;
+        streamResetFlags[i] = true;
+      }
+    }
+    sampleStreams = newSampleStreamArray(sampleStreamsList.size());
+    sampleStreamsList.toArray(sampleStreams);
+    sequenceableLoader = new CompositeSequenceableLoader(sampleStreams);
+    return positionUs;
+  }
+
+  @Override
+  public boolean continueLoading(long positionUs) {
+    return sequenceableLoader.continueLoading(positionUs);
+  }
+
+  @Override
+  public long getNextLoadPositionUs() {
+    return sequenceableLoader.getNextLoadPositionUs();
+  }
+
+  @Override
+  public long readDiscontinuity() {
+    return C.TIME_UNSET;
+  }
+
+  @Override
+  public long getBufferedPositionUs() {
+    long bufferedPositionUs = Long.MAX_VALUE;
+    for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {
+      long rendererBufferedPositionUs = sampleStream.getBufferedPositionUs();
+      if (rendererBufferedPositionUs != C.TIME_END_OF_SOURCE) {
+        bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
+      }
+    }
+    return bufferedPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : bufferedPositionUs;
+  }
+
+  @Override
+  public long seekToUs(long positionUs) {
+    for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {
+      sampleStream.seekToUs(positionUs);
+    }
+    return positionUs;
+  }
+
+  // SequenceableLoader.Callback implementation
+
+  @Override
+  public void onContinueLoadingRequested(ChunkSampleStream<SsChunkSource> sampleStream) {
+    callback.onContinueLoadingRequested(this);
+  }
+
+  // Private methods.
+
+  private ChunkSampleStream<SsChunkSource> buildSampleStream(TrackSelection selection,
+      long positionUs) {
+    int streamElementIndex = trackGroups.indexOf(selection.getTrackGroup());
+    SsChunkSource chunkSource = chunkSourceFactory.createChunkSource(manifestLoaderErrorThrower,
+        manifest, streamElementIndex, selection, trackEncryptionBoxes);
+    return new ChunkSampleStream<>(manifest.streamElements[streamElementIndex].type, chunkSource,
+        this, allocator, positionUs, minLoadableRetryCount, eventDispatcher);
+  }
+
+  private static TrackGroupArray buildTrackGroups(SsManifest manifest) {
+    TrackGroup[] trackGroups = new TrackGroup[manifest.streamElements.length];
+    for (int i = 0; i < manifest.streamElements.length; i++) {
+      trackGroups[i] = new TrackGroup(manifest.streamElements[i].formats);
+    }
+    return new TrackGroupArray(trackGroups);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static ChunkSampleStream<SsChunkSource>[] newSampleStreamArray(int length) {
+    return new ChunkSampleStream[length];
+  }
+
+  private static byte[] getProtectionElementKeyId(byte[] initData) {
+    StringBuilder initDataStringBuilder = new StringBuilder();
+    for (int i = 0; i < initData.length; i += 2) {
+      initDataStringBuilder.append((char) initData[i]);
+    }
+    String initDataString = initDataStringBuilder.toString();
+    String keyIdString = initDataString.substring(
+        initDataString.indexOf("<KID>") + 5, initDataString.indexOf("</KID>"));
+    byte[] keyId = Base64.decode(keyIdString, Base64.DEFAULT);
+    swap(keyId, 0, 3);
+    swap(keyId, 1, 2);
+    swap(keyId, 4, 5);
+    swap(keyId, 6, 7);
+    return keyId;
+  }
+
+  private static void swap(byte[] data, int firstPosition, int secondPosition) {
+    byte temp = data[firstPosition];
+    data[firstPosition] = data[secondPosition];
+    data[secondPosition] = temp;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.source.smoothstreaming;
+
+import android.net.Uri;
+import android.os.Handler;
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
+import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.SinglePeriodTimeline;
+import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest;
+import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;
+import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.Loader;
+import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
+import com.google.android.exoplayer2.upstream.ParsingLoadable;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * A SmoothStreaming {@link MediaSource}.
+ */
+public final class SsMediaSource implements MediaSource,
+    Loader.Callback<ParsingLoadable<SsManifest>> {
+
+  /**
+   * The default minimum number of times to retry loading data prior to failing.
+   */
+  public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
+  /**
+   * The default presentation delay for live streams. The presentation delay is the duration by
+   * which the default start position precedes the end of the live window.
+   */
+  public static final long DEFAULT_LIVE_PRESENTATION_DELAY_MS = 30000;
+
+  /**
+   * The minimum period between manifest refreshes.
+   */
+  private static final int MINIMUM_MANIFEST_REFRESH_PERIOD_MS = 5000;
+  /**
+   * The minimum default start position for live streams, relative to the start of the live window.
+   */
+  private static final long MIN_LIVE_DEFAULT_START_POSITION_US = 5000000;
+
+  private final Uri manifestUri;
+  private final DataSource.Factory manifestDataSourceFactory;
+  private final SsChunkSource.Factory chunkSourceFactory;
+  private final int minLoadableRetryCount;
+  private final long livePresentationDelayMs;
+  private final EventDispatcher eventDispatcher;
+  private final SsManifestParser manifestParser;
+  private final ArrayList<SsMediaPeriod> mediaPeriods;
+
+  private Listener sourceListener;
+  private DataSource manifestDataSource;
+  private Loader manifestLoader;
+  private LoaderErrorThrower manifestLoaderErrorThrower;
+
+  private long manifestLoadStartTimestamp;
+  private SsManifest manifest;
+
+  private Handler manifestRefreshHandler;
+
+  /**
+   * Constructs an instance to play a given {@link SsManifest}, which must not be live.
+   *
+   * @param manifest The manifest. {@link SsManifest#isLive} must be false.
+   * @param chunkSourceFactory A factory for {@link SsChunkSource} instances.
+   * @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.
+   */
+  public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory,
+      Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) {
+    this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT,
+        eventHandler, eventListener);
+  }
+
+  /**
+   * Constructs an instance to play a given {@link SsManifest}, which must not be live.
+   *
+   * @param manifest The manifest. {@link SsManifest#isLive} must be false.
+   * @param chunkSourceFactory A factory for {@link SsChunkSource} instances.
+   * @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.
+   */
+  public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory,
+      int minLoadableRetryCount, Handler eventHandler,
+      AdaptiveMediaSourceEventListener eventListener) {
+    this(manifest, null, null, null, chunkSourceFactory, minLoadableRetryCount,
+        DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, eventListener);
+  }
+
+  /**
+   * Constructs an instance to play the manifest at a given {@link Uri}, which may be live or
+   * on-demand.
+   *
+   * @param manifestUri The manifest {@link Uri}.
+   * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used
+   *     to load (and refresh) the manifest.
+   * @param chunkSourceFactory A factory for {@link SsChunkSource} instances.
+   * @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.
+   */
+  public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory,
+      SsChunkSource.Factory chunkSourceFactory, Handler eventHandler,
+      AdaptiveMediaSourceEventListener eventListener) {
+    this(manifestUri, manifestDataSourceFactory, chunkSourceFactory,
+        DEFAULT_MIN_LOADABLE_RETRY_COUNT, DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler,
+        eventListener);
+  }
+
+  /**
+   * Constructs an instance to play the manifest at a given {@link Uri}, which may be live or
+   * on-demand.
+   *
+   * @param manifestUri The manifest {@link Uri}.
+   * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used
+   *     to load (and refresh) the manifest.
+   * @param chunkSourceFactory A factory for {@link SsChunkSource} instances.
+   * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
+   * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the
+   *     default start position should precede the end of the live window.
+   * @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.
+   */
+  public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory,
+      SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount,
+      long livePresentationDelayMs, Handler eventHandler,
+      AdaptiveMediaSourceEventListener eventListener) {
+    this(manifestUri, manifestDataSourceFactory, new SsManifestParser(), chunkSourceFactory,
+        minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener);
+  }
+
+  /**
+   * Constructs an instance to play the manifest at a given {@link Uri}, which may be live or
+   * on-demand.
+   *
+   * @param manifestUri The manifest {@link Uri}.
+   * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used
+   *     to load (and refresh) the manifest.
+   * @param manifestParser A parser for loaded manifest data.
+   * @param chunkSourceFactory A factory for {@link SsChunkSource} instances.
+   * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
+   * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the
+   *     default start position should precede the end of the live window.
+   * @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.
+   */
+  public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory,
+      SsManifestParser manifestParser, SsChunkSource.Factory chunkSourceFactory,
+      int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler,
+      AdaptiveMediaSourceEventListener eventListener) {
+    this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory,
+        minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener);
+  }
+
+  private SsMediaSource(SsManifest manifest, Uri manifestUri,
+      DataSource.Factory manifestDataSourceFactory, SsManifestParser manifestParser,
+      SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount,
+      long livePresentationDelayMs, Handler eventHandler,
+      AdaptiveMediaSourceEventListener eventListener) {
+    Assertions.checkState(manifest == null || !manifest.isLive);
+    this.manifest = manifest;
+    this.manifestUri = manifestUri == null ? null
+        : Util.toLowerInvariant(manifestUri.getLastPathSegment()).equals("manifest") ? manifestUri
+            : Uri.withAppendedPath(manifestUri, "Manifest");
+    this.manifestDataSourceFactory = manifestDataSourceFactory;
+    this.manifestParser = manifestParser;
+    this.chunkSourceFactory = chunkSourceFactory;
+    this.minLoadableRetryCount = minLoadableRetryCount;
+    this.livePresentationDelayMs = livePresentationDelayMs;
+    this.eventDispatcher = new EventDispatcher(eventHandler, eventListener);
+    mediaPeriods = new ArrayList<>();
+  }
+
+  // MediaSource implementation.
+
+  @Override
+  public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+    sourceListener = listener;
+    if (manifest != null) {
+      manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy();
+      processManifest();
+    } else {
+      manifestDataSource = manifestDataSourceFactory.createDataSource();
+      manifestLoader = new Loader("Loader:Manifest");
+      manifestLoaderErrorThrower = manifestLoader;
+      manifestRefreshHandler = new Handler();
+      startLoadingManifest();
+    }
+  }
+
+  @Override
+  public void maybeThrowSourceInfoRefreshError() throws IOException {
+    manifestLoaderErrorThrower.maybeThrowError();
+  }
+
+  @Override
+  public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
+    Assertions.checkArgument(index == 0);
+    SsMediaPeriod period = new SsMediaPeriod(manifest, chunkSourceFactory, minLoadableRetryCount,
+        eventDispatcher, manifestLoaderErrorThrower, allocator);
+    mediaPeriods.add(period);
+    return period;
+  }
+
+  @Override
+  public void releasePeriod(MediaPeriod period) {
+    ((SsMediaPeriod) period).release();
+    mediaPeriods.remove(period);
+  }
+
+  @Override
+  public void releaseSource() {
+    sourceListener = null;
+    manifest = null;
+    manifestDataSource = null;
+    manifestLoadStartTimestamp = 0;
+    if (manifestLoader != null) {
+      manifestLoader.release();
+      manifestLoader = null;
+    }
+    if (manifestRefreshHandler != null) {
+      manifestRefreshHandler.removeCallbacksAndMessages(null);
+      manifestRefreshHandler = null;
+    }
+  }
+
+  // Loader.Callback implementation
+
+  @Override
+  public void onLoadCompleted(ParsingLoadable<SsManifest> loadable, long elapsedRealtimeMs,
+      long loadDurationMs) {
+    eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs,
+        loadDurationMs, loadable.bytesLoaded());
+    manifest = loadable.getResult();
+    manifestLoadStartTimestamp = elapsedRealtimeMs - loadDurationMs;
+    processManifest();
+    scheduleManifestRefresh();
+  }
+
+  @Override
+  public void onLoadCanceled(ParsingLoadable<SsManifest> loadable, long elapsedRealtimeMs,
+      long loadDurationMs, boolean released) {
+    eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs,
+        loadDurationMs, loadable.bytesLoaded());
+  }
+
+  @Override
+  public int onLoadError(ParsingLoadable<SsManifest> loadable, long elapsedRealtimeMs,
+      long loadDurationMs, IOException error) {
+    boolean isFatal = error instanceof ParserException;
+    eventDispatcher.loadError(loadable.dataSpec, loadable.type, elapsedRealtimeMs, loadDurationMs,
+        loadable.bytesLoaded(), error, isFatal);
+    return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY;
+  }
+
+  // Internal methods
+
+  private void processManifest() {
+    for (int i = 0; i < mediaPeriods.size(); i++) {
+      mediaPeriods.get(i).updateManifest(manifest);
+    }
+    Timeline timeline;
+    if (manifest.isLive) {
+      long startTimeUs = Long.MAX_VALUE;
+      long endTimeUs = Long.MIN_VALUE;
+      for (int i = 0; i < manifest.streamElements.length; i++) {
+        StreamElement element = manifest.streamElements[i];
+        if (element.chunkCount > 0) {
+          startTimeUs = Math.min(startTimeUs, element.getStartTimeUs(0));
+          endTimeUs = Math.max(endTimeUs, element.getStartTimeUs(element.chunkCount - 1)
+              + element.getChunkDurationUs(element.chunkCount - 1));
+        }
+      }
+      if (startTimeUs == Long.MAX_VALUE) {
+        timeline = new SinglePeriodTimeline(C.TIME_UNSET, false);
+      } else {
+        if (manifest.dvrWindowLengthUs != C.TIME_UNSET
+            && manifest.dvrWindowLengthUs > 0) {
+          startTimeUs = Math.max(startTimeUs, endTimeUs - manifest.dvrWindowLengthUs);
+        }
+        long durationUs = endTimeUs - startTimeUs;
+        long defaultStartPositionUs = durationUs - C.msToUs(livePresentationDelayMs);
+        if (defaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) {
+          // The default start position is too close to the start of the live window. Set it to the
+          // minimum default start position provided the window is at least twice as big. Else set
+          // it to the middle of the window.
+          defaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US, durationUs / 2);
+        }
+        timeline = new SinglePeriodTimeline(C.TIME_UNSET, durationUs, startTimeUs,
+            defaultStartPositionUs, true /* isSeekable */, true /* isDynamic */);
+      }
+    } else {
+      boolean isSeekable = manifest.durationUs != C.TIME_UNSET;
+      timeline = new SinglePeriodTimeline(manifest.durationUs, isSeekable);
+    }
+    sourceListener.onSourceInfoRefreshed(timeline, manifest);
+  }
+
+  private void scheduleManifestRefresh() {
+    if (!manifest.isLive) {
+      return;
+    }
+    long nextLoadTimestamp = manifestLoadStartTimestamp + MINIMUM_MANIFEST_REFRESH_PERIOD_MS;
+    long delayUntilNextLoad = Math.max(0, nextLoadTimestamp - SystemClock.elapsedRealtime());
+    manifestRefreshHandler.postDelayed(new Runnable() {
+      @Override
+      public void run() {
+        startLoadingManifest();
+      }
+    }, delayUntilNextLoad);
+  }
+
+  private void startLoadingManifest() {
+    ParsingLoadable<SsManifest> loadable = new ParsingLoadable<>(manifestDataSource,
+        manifestUri, C.DATA_TYPE_MANIFEST, manifestParser);
+    long elapsedRealtimeMs = manifestLoader.startLoading(loadable, this, minLoadableRetryCount);
+    eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.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 com.google.android.exoplayer2.source.smoothstreaming.manifest;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.UriUtil;
+import com.google.android.exoplayer2.util.Util;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Represents a SmoothStreaming manifest.
+ *
+ * @see <a href="http://msdn.microsoft.com/en-us/library/ee673436(v=vs.90).aspx">
+ * IIS Smooth Streaming Client Manifest Format</a>
+ */
+public class SsManifest {
+
+  public static final int UNSET_LOOKAHEAD = -1;
+
+  /**
+   * The client manifest major version.
+   */
+  public final int majorVersion;
+
+  /**
+   * The client manifest minor version.
+   */
+  public final int minorVersion;
+
+  /**
+   * The number of fragments in a lookahead, or {@link #UNSET_LOOKAHEAD} if the lookahead is
+   * unspecified.
+   */
+  public final int lookAheadCount;
+
+  /**
+   * Whether the manifest describes a live presentation still in progress.
+   */
+  public final boolean isLive;
+
+  /**
+   * Content protection information, or null if the content is not protected.
+   */
+  public final ProtectionElement protectionElement;
+
+  /**
+   * The contained stream elements.
+   */
+  public final StreamElement[] streamElements;
+
+  /**
+   * The overall presentation duration of the media in microseconds, or {@link C#TIME_UNSET}
+   * if the duration is unknown.
+   */
+  public final long durationUs;
+
+  /**
+   * The length of the trailing window for a live broadcast in microseconds, or
+   * {@link C#TIME_UNSET} if the stream is not live or if the window length is unspecified.
+   */
+  public final long dvrWindowLengthUs;
+
+  /**
+   * @param majorVersion The client manifest major version.
+   * @param minorVersion The client manifest minor version.
+   * @param timescale The timescale of the media as the number of units that pass in one second.
+   * @param duration The overall presentation duration in units of the timescale attribute, or 0
+   *     if the duration is unknown.
+   * @param dvrWindowLength The length of the trailing window in units of the timescale attribute,
+   *     or 0 if this attribute is unspecified or not applicable.
+   * @param lookAheadCount The number of fragments in a lookahead, or {@link #UNSET_LOOKAHEAD} if
+   *     this attribute is unspecified or not applicable.
+   * @param isLive True if the manifest describes a live presentation still in progress. False
+   *     otherwise.
+   * @param protectionElement Content protection information, or null if the content is not
+   *     protected.
+   * @param streamElements The contained stream elements.
+   */
+  public SsManifest(int majorVersion, int minorVersion, long timescale, long duration,
+      long dvrWindowLength, int lookAheadCount, boolean isLive, ProtectionElement protectionElement,
+      StreamElement[] streamElements) {
+    this.majorVersion = majorVersion;
+    this.minorVersion = minorVersion;
+    this.lookAheadCount = lookAheadCount;
+    this.isLive = isLive;
+    this.protectionElement = protectionElement;
+    this.streamElements = streamElements;
+    dvrWindowLengthUs = dvrWindowLength == 0 ? C.TIME_UNSET
+        : Util.scaleLargeTimestamp(dvrWindowLength, C.MICROS_PER_SECOND, timescale);
+    durationUs = duration == 0 ? C.TIME_UNSET
+        : Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, timescale);
+  }
+
+  /**
+   * Represents a protection element containing a single header.
+   */
+  public static class ProtectionElement {
+
+    public final UUID uuid;
+    public final byte[] data;
+
+    public ProtectionElement(UUID uuid, byte[] data) {
+      this.uuid = uuid;
+      this.data = data;
+    }
+
+  }
+
+  /**
+   * Represents a StreamIndex element.
+   */
+  public static class StreamElement {
+
+    private static final String URL_PLACEHOLDER_START_TIME = "{start time}";
+    private static final String URL_PLACEHOLDER_BITRATE = "{bitrate}";
+
+    public final int type;
+    public final String subType;
+    public final long timescale;
+    public final String name;
+    public final int maxWidth;
+    public final int maxHeight;
+    public final int displayWidth;
+    public final int displayHeight;
+    public final String language;
+    public final Format[] formats;
+    public final int chunkCount;
+
+    private final String baseUri;
+    private final String chunkTemplate;
+
+    private final List<Long> chunkStartTimes;
+    private final long[] chunkStartTimesUs;
+    private final long lastChunkDurationUs;
+
+    public StreamElement(String baseUri, String chunkTemplate, int type, String subType,
+        long timescale, String name, int maxWidth, int maxHeight, int displayWidth,
+        int displayHeight, String language, Format[] formats, List<Long> chunkStartTimes,
+        long lastChunkDuration) {
+      this.baseUri = baseUri;
+      this.chunkTemplate = chunkTemplate;
+      this.type = type;
+      this.subType = subType;
+      this.timescale = timescale;
+      this.name = name;
+      this.maxWidth = maxWidth;
+      this.maxHeight = maxHeight;
+      this.displayWidth = displayWidth;
+      this.displayHeight = displayHeight;
+      this.language = language;
+      this.formats = formats;
+      this.chunkCount = chunkStartTimes.size();
+      this.chunkStartTimes = chunkStartTimes;
+      lastChunkDurationUs =
+          Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale);
+      chunkStartTimesUs =
+          Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale);
+    }
+
+    /**
+     * Returns the index of the chunk that contains the specified time.
+     *
+     * @param timeUs The time in microseconds.
+     * @return The index of the corresponding chunk.
+     */
+    public int getChunkIndex(long timeUs) {
+      return Util.binarySearchFloor(chunkStartTimesUs, timeUs, true, true);
+    }
+
+    /**
+     * Returns the start time of the specified chunk.
+     *
+     * @param chunkIndex The index of the chunk.
+     * @return The start time of the chunk, in microseconds.
+     */
+    public long getStartTimeUs(int chunkIndex) {
+      return chunkStartTimesUs[chunkIndex];
+    }
+
+    /**
+     * Returns the duration of the specified chunk.
+     *
+     * @param chunkIndex The index of the chunk.
+     * @return The duration of the chunk, in microseconds.
+     */
+    public long getChunkDurationUs(int chunkIndex) {
+      return (chunkIndex == chunkCount - 1) ? lastChunkDurationUs
+          : chunkStartTimesUs[chunkIndex + 1] - chunkStartTimesUs[chunkIndex];
+    }
+
+    /**
+     * Builds a uri for requesting the specified chunk of the specified track.
+     *
+     * @param track The index of the track for which to build the URL.
+     * @param chunkIndex The index of the chunk for which to build the URL.
+     * @return The request uri.
+     */
+    public Uri buildRequestUri(int track, int chunkIndex) {
+      Assertions.checkState(formats != null);
+      Assertions.checkState(chunkStartTimes != null);
+      Assertions.checkState(chunkIndex < chunkStartTimes.size());
+      String chunkUrl = chunkTemplate
+          .replace(URL_PLACEHOLDER_BITRATE, Integer.toString(formats[track].bitrate))
+          .replace(URL_PLACEHOLDER_START_TIME, chunkStartTimes.get(chunkIndex).toString());
+      return UriUtil.resolveToUri(baseUri, chunkUrl);
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.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 com.google.android.exoplayer2.source.smoothstreaming.manifest;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
+import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
+import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement;
+import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement;
+import com.google.android.exoplayer2.upstream.ParsingLoadable;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+/**
+ * Parses SmoothStreaming client manifests.
+ *
+ * @see <a href="http://msdn.microsoft.com/en-us/library/ee673436(v=vs.90).aspx">
+ * IIS Smooth Streaming Client Manifest Format</a>
+ */
+public class SsManifestParser implements ParsingLoadable.Parser<SsManifest> {
+
+  private final XmlPullParserFactory xmlParserFactory;
+
+  public SsManifestParser() {
+    try {
+      xmlParserFactory = XmlPullParserFactory.newInstance();
+    } catch (XmlPullParserException e) {
+      throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e);
+    }
+  }
+
+  @Override
+  public SsManifest parse(Uri uri, InputStream inputStream) throws IOException {
+    try {
+      XmlPullParser xmlParser = xmlParserFactory.newPullParser();
+      xmlParser.setInput(inputStream, null);
+      SmoothStreamingMediaParser smoothStreamingMediaParser =
+          new SmoothStreamingMediaParser(null, uri.toString());
+      return (SsManifest) smoothStreamingMediaParser.parse(xmlParser);
+    } catch (XmlPullParserException e) {
+      throw new ParserException(e);
+    }
+  }
+
+  /**
+   * Thrown if a required field is missing.
+   */
+  public static class MissingFieldException extends ParserException {
+
+    public MissingFieldException(String fieldName) {
+      super("Missing required field: " + fieldName);
+    }
+
+  }
+
+  /**
+   * A base class for parsers that parse components of a smooth streaming manifest.
+   */
+  private abstract static class ElementParser {
+
+    private final String baseUri;
+    private final String tag;
+
+    private final ElementParser parent;
+    private final List<Pair<String, Object>> normalizedAttributes;
+
+    public ElementParser(ElementParser parent, String baseUri, String tag) {
+      this.parent = parent;
+      this.baseUri = baseUri;
+      this.tag = tag;
+      this.normalizedAttributes = new LinkedList<>();
+    }
+
+    public final Object parse(XmlPullParser xmlParser) throws XmlPullParserException, IOException {
+      String tagName;
+      boolean foundStartTag = false;
+      int skippingElementDepth = 0;
+      while (true) {
+        int eventType = xmlParser.getEventType();
+        switch (eventType) {
+          case XmlPullParser.START_TAG:
+            tagName = xmlParser.getName();
+            if (tag.equals(tagName)) {
+              foundStartTag = true;
+              parseStartTag(xmlParser);
+            } else if (foundStartTag) {
+              if (skippingElementDepth > 0) {
+                skippingElementDepth++;
+              } else if (handleChildInline(tagName)) {
+                parseStartTag(xmlParser);
+              } else {
+                ElementParser childElementParser = newChildParser(this, tagName, baseUri);
+                if (childElementParser == null) {
+                  skippingElementDepth = 1;
+                } else {
+                  addChild(childElementParser.parse(xmlParser));
+                }
+              }
+            }
+            break;
+          case XmlPullParser.TEXT:
+            if (foundStartTag && skippingElementDepth == 0) {
+              parseText(xmlParser);
+            }
+            break;
+          case XmlPullParser.END_TAG:
+            if (foundStartTag) {
+              if (skippingElementDepth > 0) {
+                skippingElementDepth--;
+              } else {
+                tagName = xmlParser.getName();
+                parseEndTag(xmlParser);
+                if (!handleChildInline(tagName)) {
+                  return build();
+                }
+              }
+            }
+            break;
+          case XmlPullParser.END_DOCUMENT:
+            return null;
+          default:
+            // Do nothing.
+            break;
+        }
+        xmlParser.next();
+      }
+    }
+
+    private ElementParser newChildParser(ElementParser parent, String name, String baseUri) {
+      if (QualityLevelParser.TAG.equals(name)) {
+        return new QualityLevelParser(parent, baseUri);
+      } else if (ProtectionParser.TAG.equals(name)) {
+        return new ProtectionParser(parent, baseUri);
+      } else if (StreamIndexParser.TAG.equals(name)) {
+        return new StreamIndexParser(parent, baseUri);
+      }
+      return null;
+    }
+
+    /**
+     * Stash an attribute that may be normalized at this level. In other words, an attribute that
+     * may have been pulled up from the child elements because its value was the same in all
+     * children.
+     * <p>
+     * Stashing an attribute allows child element parsers to retrieve the values of normalized
+     * attributes using {@link #getNormalizedAttribute(String)}.
+     *
+     * @param key The name of the attribute.
+     * @param value The value of the attribute.
+     */
+    protected final void putNormalizedAttribute(String key, Object value) {
+      normalizedAttributes.add(Pair.create(key, value));
+    }
+
+    /**
+     * Attempt to retrieve a stashed normalized attribute. If there is no stashed attribute with
+     * the provided name, the parent element parser will be queried, and so on up the chain.
+     *
+     * @param key The name of the attribute.
+     * @return The stashed value, or null if the attribute was not be found.
+     */
+    protected final Object getNormalizedAttribute(String key) {
+      for (int i = 0; i < normalizedAttributes.size(); i++) {
+        Pair<String, Object> pair = normalizedAttributes.get(i);
+        if (pair.first.equals(key)) {
+          return pair.second;
+        }
+      }
+      return parent == null ? null : parent.getNormalizedAttribute(key);
+    }
+
+    /**
+     * Whether this {@link ElementParser} parses a child element inline.
+     *
+     * @param tagName The name of the child element.
+     * @return Whether the child is parsed inline.
+     */
+    protected boolean handleChildInline(String tagName) {
+      return false;
+    }
+
+    /**
+     * @param xmlParser The underlying {@link XmlPullParser}
+     * @throws ParserException
+     */
+    protected void parseStartTag(XmlPullParser xmlParser) throws ParserException {
+      // Do nothing.
+    }
+
+    /**
+     * @param xmlParser The underlying {@link XmlPullParser}
+     */
+    protected void parseText(XmlPullParser xmlParser) {
+      // Do nothing.
+    }
+
+    /**
+     * @param xmlParser The underlying {@link XmlPullParser}
+     */
+    protected void parseEndTag(XmlPullParser xmlParser) {
+      // Do nothing.
+    }
+
+    /**
+     * @param parsedChild A parsed child object.
+     */
+    protected void addChild(Object parsedChild) {
+      // Do nothing.
+    }
+
+    protected abstract Object build();
+
+    protected final String parseRequiredString(XmlPullParser parser, String key)
+        throws MissingFieldException {
+      String value = parser.getAttributeValue(null, key);
+      if (value != null) {
+        return value;
+      } else {
+        throw new MissingFieldException(key);
+      }
+    }
+
+    protected final int parseInt(XmlPullParser parser, String key, int defaultValue)
+        throws ParserException {
+      String value = parser.getAttributeValue(null, key);
+      if (value != null) {
+        try {
+          return Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+          throw new ParserException(e);
+        }
+      } else {
+        return defaultValue;
+      }
+    }
+
+    protected final int parseRequiredInt(XmlPullParser parser, String key) throws ParserException {
+      String value = parser.getAttributeValue(null, key);
+      if (value != null) {
+        try {
+          return Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+          throw new ParserException(e);
+        }
+      } else {
+        throw new MissingFieldException(key);
+      }
+    }
+
+    protected final long parseLong(XmlPullParser parser, String key, long defaultValue)
+        throws ParserException {
+      String value = parser.getAttributeValue(null, key);
+      if (value != null) {
+        try {
+          return Long.parseLong(value);
+        } catch (NumberFormatException e) {
+          throw new ParserException(e);
+        }
+      } else {
+        return defaultValue;
+      }
+    }
+
+    protected final long parseRequiredLong(XmlPullParser parser, String key)
+        throws ParserException {
+      String value = parser.getAttributeValue(null, key);
+      if (value != null) {
+        try {
+          return Long.parseLong(value);
+        } catch (NumberFormatException e) {
+          throw new ParserException(e);
+        }
+      } else {
+        throw new MissingFieldException(key);
+      }
+    }
+
+    protected final boolean parseBoolean(XmlPullParser parser, String key, boolean defaultValue) {
+      String value = parser.getAttributeValue(null, key);
+      if (value != null) {
+        return Boolean.parseBoolean(value);
+      } else {
+        return defaultValue;
+      }
+    }
+
+  }
+
+  private static class SmoothStreamingMediaParser extends ElementParser {
+
+    public static final String TAG = "SmoothStreamingMedia";
+
+    private static final String KEY_MAJOR_VERSION = "MajorVersion";
+    private static final String KEY_MINOR_VERSION = "MinorVersion";
+    private static final String KEY_TIME_SCALE = "TimeScale";
+    private static final String KEY_DVR_WINDOW_LENGTH = "DVRWindowLength";
+    private static final String KEY_DURATION = "Duration";
+    private static final String KEY_LOOKAHEAD_COUNT = "LookaheadCount";
+    private static final String KEY_IS_LIVE = "IsLive";
+
+    private final List<StreamElement> streamElements;
+
+    private int majorVersion;
+    private int minorVersion;
+    private long timescale;
+    private long duration;
+    private long dvrWindowLength;
+    private int lookAheadCount;
+    private boolean isLive;
+    private ProtectionElement protectionElement;
+
+    public SmoothStreamingMediaParser(ElementParser parent, String baseUri) {
+      super(parent, baseUri, TAG);
+      lookAheadCount = SsManifest.UNSET_LOOKAHEAD;
+      protectionElement = null;
+      streamElements = new LinkedList<>();
+    }
+
+    @Override
+    public void parseStartTag(XmlPullParser parser) throws ParserException {
+      majorVersion = parseRequiredInt(parser, KEY_MAJOR_VERSION);
+      minorVersion = parseRequiredInt(parser, KEY_MINOR_VERSION);
+      timescale = parseLong(parser, KEY_TIME_SCALE, 10000000L);
+      duration = parseRequiredLong(parser, KEY_DURATION);
+      dvrWindowLength = parseLong(parser, KEY_DVR_WINDOW_LENGTH, 0);
+      lookAheadCount = parseInt(parser, KEY_LOOKAHEAD_COUNT, SsManifest.UNSET_LOOKAHEAD);
+      isLive = parseBoolean(parser, KEY_IS_LIVE, false);
+      putNormalizedAttribute(KEY_TIME_SCALE, timescale);
+    }
+
+    @Override
+    public void addChild(Object child) {
+      if (child instanceof StreamElement) {
+        streamElements.add((StreamElement) child);
+      } else if (child instanceof ProtectionElement) {
+        Assertions.checkState(protectionElement == null);
+        protectionElement = (ProtectionElement) child;
+      }
+    }
+
+    @Override
+    public Object build() {
+      StreamElement[] streamElementArray = new StreamElement[streamElements.size()];
+      streamElements.toArray(streamElementArray);
+      if (protectionElement != null) {
+        DrmInitData drmInitData = new DrmInitData(new SchemeData(protectionElement.uuid,
+            MimeTypes.VIDEO_MP4, protectionElement.data));
+        for (StreamElement streamElement : streamElementArray) {
+          for (int i = 0; i < streamElement.formats.length; i++) {
+            streamElement.formats[i] = streamElement.formats[i].copyWithDrmInitData(drmInitData);
+          }
+        }
+      }
+      return new SsManifest(majorVersion, minorVersion, timescale, duration, dvrWindowLength,
+          lookAheadCount, isLive, protectionElement, streamElementArray);
+    }
+
+  }
+
+  private static class ProtectionParser extends ElementParser {
+
+    public static final String TAG = "Protection";
+    public static final String TAG_PROTECTION_HEADER = "ProtectionHeader";
+
+    public static final String KEY_SYSTEM_ID = "SystemID";
+
+    private boolean inProtectionHeader;
+    private UUID uuid;
+    private byte[] initData;
+
+    public ProtectionParser(ElementParser parent, String baseUri) {
+      super(parent, baseUri, TAG);
+    }
+
+    @Override
+    public boolean handleChildInline(String tag) {
+      return TAG_PROTECTION_HEADER.equals(tag);
+    }
+
+    @Override
+    public void parseStartTag(XmlPullParser parser) {
+      if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
+        inProtectionHeader = true;
+        String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID);
+        uuidString = stripCurlyBraces(uuidString);
+        uuid = UUID.fromString(uuidString);
+      }
+    }
+
+    @Override
+    public void parseText(XmlPullParser parser) {
+      if (inProtectionHeader) {
+        initData = Base64.decode(parser.getText(), Base64.DEFAULT);
+      }
+    }
+
+    @Override
+    public void parseEndTag(XmlPullParser parser) {
+      if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
+        inProtectionHeader = false;
+      }
+    }
+
+    @Override
+    public Object build() {
+      return new ProtectionElement(uuid, PsshAtomUtil.buildPsshAtom(uuid, initData));
+    }
+
+    private static String stripCurlyBraces(String uuidString) {
+      if (uuidString.charAt(0) == '{' && uuidString.charAt(uuidString.length() - 1) == '}') {
+        uuidString = uuidString.substring(1, uuidString.length() - 1);
+      }
+      return uuidString;
+    }
+  }
+
+  private static class StreamIndexParser extends ElementParser {
+
+    public static final String TAG = "StreamIndex";
+    private static final String TAG_STREAM_FRAGMENT = "c";
+
+    private static final String KEY_TYPE = "Type";
+    private static final String KEY_TYPE_AUDIO = "audio";
+    private static final String KEY_TYPE_VIDEO = "video";
+    private static final String KEY_TYPE_TEXT = "text";
+    private static final String KEY_SUB_TYPE = "Subtype";
+    private static final String KEY_NAME = "Name";
+    private static final String KEY_URL = "Url";
+    private static final String KEY_MAX_WIDTH = "MaxWidth";
+    private static final String KEY_MAX_HEIGHT = "MaxHeight";
+    private static final String KEY_DISPLAY_WIDTH = "DisplayWidth";
+    private static final String KEY_DISPLAY_HEIGHT = "DisplayHeight";
+    private static final String KEY_LANGUAGE = "Language";
+    private static final String KEY_TIME_SCALE = "TimeScale";
+
+    private static final String KEY_FRAGMENT_DURATION = "d";
+    private static final String KEY_FRAGMENT_START_TIME = "t";
+    private static final String KEY_FRAGMENT_REPEAT_COUNT = "r";
+
+    private final String baseUri;
+    private final List<Format> formats;
+
+    private int type;
+    private String subType;
+    private long timescale;
+    private String name;
+    private String url;
+    private int maxWidth;
+    private int maxHeight;
+    private int displayWidth;
+    private int displayHeight;
+    private String language;
+    private ArrayList<Long> startTimes;
+
+    private long lastChunkDuration;
+
+    public StreamIndexParser(ElementParser parent, String baseUri) {
+      super(parent, baseUri, TAG);
+      this.baseUri = baseUri;
+      formats = new LinkedList<>();
+    }
+
+    @Override
+    public boolean handleChildInline(String tag) {
+      return TAG_STREAM_FRAGMENT.equals(tag);
+    }
+
+    @Override
+    public void parseStartTag(XmlPullParser parser) throws ParserException {
+      if (TAG_STREAM_FRAGMENT.equals(parser.getName())) {
+        parseStreamFragmentStartTag(parser);
+      } else {
+        parseStreamElementStartTag(parser);
+      }
+    }
+
+    private void parseStreamFragmentStartTag(XmlPullParser parser) throws ParserException {
+      int chunkIndex = startTimes.size();
+      long startTime = parseLong(parser, KEY_FRAGMENT_START_TIME, C.TIME_UNSET);
+      if (startTime == C.TIME_UNSET) {
+        if (chunkIndex == 0) {
+          // Assume the track starts at t = 0.
+          startTime = 0;
+        } else if (lastChunkDuration != C.INDEX_UNSET) {
+          // Infer the start time from the previous chunk's start time and duration.
+          startTime = startTimes.get(chunkIndex - 1) + lastChunkDuration;
+        } else {
+          // We don't have the start time, and we're unable to infer it.
+          throw new ParserException("Unable to infer start time");
+        }
+      }
+      chunkIndex++;
+      startTimes.add(startTime);
+      lastChunkDuration = parseLong(parser, KEY_FRAGMENT_DURATION, C.TIME_UNSET);
+      // Handle repeated chunks.
+      long repeatCount = parseLong(parser, KEY_FRAGMENT_REPEAT_COUNT, 1L);
+      if (repeatCount > 1 && lastChunkDuration == C.TIME_UNSET) {
+        throw new ParserException("Repeated chunk with unspecified duration");
+      }
+      for (int i = 1; i < repeatCount; i++) {
+        chunkIndex++;
+        startTimes.add(startTime + (lastChunkDuration * i));
+      }
+    }
+
+    private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException {
+      type = parseType(parser);
+      putNormalizedAttribute(KEY_TYPE, type);
+      if (type == C.TRACK_TYPE_TEXT) {
+        subType = parseRequiredString(parser, KEY_SUB_TYPE);
+      } else {
+        subType = parser.getAttributeValue(null, KEY_SUB_TYPE);
+      }
+      name = parser.getAttributeValue(null, KEY_NAME);
+      url = parseRequiredString(parser, KEY_URL);
+      maxWidth = parseInt(parser, KEY_MAX_WIDTH, Format.NO_VALUE);
+      maxHeight = parseInt(parser, KEY_MAX_HEIGHT, Format.NO_VALUE);
+      displayWidth = parseInt(parser, KEY_DISPLAY_WIDTH, Format.NO_VALUE);
+      displayHeight = parseInt(parser, KEY_DISPLAY_HEIGHT, Format.NO_VALUE);
+      language = parser.getAttributeValue(null, KEY_LANGUAGE);
+      putNormalizedAttribute(KEY_LANGUAGE, language);
+      timescale = parseInt(parser, KEY_TIME_SCALE, -1);
+      if (timescale == -1) {
+        timescale = (Long) getNormalizedAttribute(KEY_TIME_SCALE);
+      }
+      startTimes = new ArrayList<>();
+    }
+
+    private int parseType(XmlPullParser parser) throws ParserException {
+      String value = parser.getAttributeValue(null, KEY_TYPE);
+      if (value != null) {
+        if (KEY_TYPE_AUDIO.equalsIgnoreCase(value)) {
+          return C.TRACK_TYPE_AUDIO;
+        } else if (KEY_TYPE_VIDEO.equalsIgnoreCase(value)) {
+          return C.TRACK_TYPE_VIDEO;
+        } else if (KEY_TYPE_TEXT.equalsIgnoreCase(value)) {
+          return C.TRACK_TYPE_TEXT;
+        } else {
+          throw new ParserException("Invalid key value[" + value + "]");
+        }
+      }
+      throw new MissingFieldException(KEY_TYPE);
+    }
+
+    @Override
+    public void addChild(Object child) {
+      if (child instanceof Format) {
+        formats.add((Format) child);
+      }
+    }
+
+    @Override
+    public Object build() {
+      Format[] formatArray = new Format[formats.size()];
+      formats.toArray(formatArray);
+      return new StreamElement(baseUri, url, type, subType, timescale, name, maxWidth, maxHeight,
+          displayWidth, displayHeight, language, formatArray, startTimes, lastChunkDuration);
+    }
+
+  }
+
+  private static class QualityLevelParser extends ElementParser {
+
+    public static final String TAG = "QualityLevel";
+
+    private static final String KEY_INDEX = "Index";
+    private static final String KEY_BITRATE = "Bitrate";
+    private static final String KEY_CODEC_PRIVATE_DATA = "CodecPrivateData";
+    private static final String KEY_SAMPLING_RATE = "SamplingRate";
+    private static final String KEY_CHANNELS = "Channels";
+    private static final String KEY_FOUR_CC = "FourCC";
+    private static final String KEY_TYPE = "Type";
+    private static final String KEY_LANGUAGE = "Language";
+    private static final String KEY_MAX_WIDTH = "MaxWidth";
+    private static final String KEY_MAX_HEIGHT = "MaxHeight";
+
+    private Format format;
+
+    public QualityLevelParser(ElementParser parent, String baseUri) {
+      super(parent, baseUri, TAG);
+    }
+
+    @Override
+    public void parseStartTag(XmlPullParser parser) throws ParserException {
+      int type = (Integer) getNormalizedAttribute(KEY_TYPE);
+      String id = parser.getAttributeValue(null, KEY_INDEX);
+      int bitrate = parseRequiredInt(parser, KEY_BITRATE);
+      String sampleMimeType = fourCCToMimeType(parseRequiredString(parser, KEY_FOUR_CC));
+
+      if (type == C.TRACK_TYPE_VIDEO) {
+        int width = parseRequiredInt(parser, KEY_MAX_WIDTH);
+        int height = parseRequiredInt(parser, KEY_MAX_HEIGHT);
+        List<byte[]> codecSpecificData = buildCodecSpecificData(
+            parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA));
+        format = Format.createVideoContainerFormat(id, MimeTypes.VIDEO_MP4, sampleMimeType, null,
+            bitrate, width, height, Format.NO_VALUE, codecSpecificData, 0);
+      } else if (type == C.TRACK_TYPE_AUDIO) {
+        sampleMimeType = sampleMimeType == null ? MimeTypes.AUDIO_AAC : sampleMimeType;
+        int channels = parseRequiredInt(parser, KEY_CHANNELS);
+        int samplingRate = parseRequiredInt(parser, KEY_SAMPLING_RATE);
+        List<byte[]> codecSpecificData = buildCodecSpecificData(
+            parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA));
+        if (codecSpecificData.isEmpty() && MimeTypes.AUDIO_AAC.equals(sampleMimeType)) {
+          codecSpecificData = Collections.singletonList(
+              CodecSpecificDataUtil.buildAacLcAudioSpecificConfig(samplingRate, channels));
+        }
+        String language = (String) getNormalizedAttribute(KEY_LANGUAGE);
+        format = Format.createAudioContainerFormat(id, MimeTypes.AUDIO_MP4, sampleMimeType, null,
+            bitrate, channels, samplingRate, codecSpecificData, 0, language);
+      } else if (type == C.TRACK_TYPE_TEXT) {
+        String language = (String) getNormalizedAttribute(KEY_LANGUAGE);
+        format = Format.createTextContainerFormat(id, MimeTypes.APPLICATION_MP4, sampleMimeType,
+            null, bitrate, 0, language);
+      } else {
+        format = Format.createContainerFormat(id, MimeTypes.APPLICATION_MP4, sampleMimeType, null,
+            bitrate, 0, null);
+      }
+    }
+
+    @Override
+    public Object build() {
+      return format;
+    }
+
+    private static List<byte[]> buildCodecSpecificData(String codecSpecificDataString) {
+      ArrayList<byte[]> csd = new ArrayList<>();
+      if (!TextUtils.isEmpty(codecSpecificDataString)) {
+        byte[] codecPrivateData = Util.getBytesFromHexString(codecSpecificDataString);
+        byte[][] split = CodecSpecificDataUtil.splitNalUnits(codecPrivateData);
+        if (split == null) {
+          csd.add(codecPrivateData);
+        } else {
+          Collections.addAll(csd, split);
+        }
+      }
+      return csd;
+    }
+
+    private static String fourCCToMimeType(String fourCC) {
+      if (fourCC.equalsIgnoreCase("H264") || fourCC.equalsIgnoreCase("X264")
+          || fourCC.equalsIgnoreCase("AVC1") || fourCC.equalsIgnoreCase("DAVC")) {
+        return MimeTypes.VIDEO_H264;
+      } else if (fourCC.equalsIgnoreCase("AAC") || fourCC.equalsIgnoreCase("AACL")
+          || fourCC.equalsIgnoreCase("AACH") || fourCC.equalsIgnoreCase("AACP")) {
+        return MimeTypes.AUDIO_AAC;
+      } else if (fourCC.equalsIgnoreCase("TTML")) {
+        return MimeTypes.APPLICATION_TTML;
+      } else if (fourCC.equalsIgnoreCase("ac-3") || fourCC.equalsIgnoreCase("dac3")) {
+        return MimeTypes.AUDIO_AC3;
+      } else if (fourCC.equalsIgnoreCase("ec-3") || fourCC.equalsIgnoreCase("dec3")) {
+        return MimeTypes.AUDIO_E_AC3;
+      } else if (fourCC.equalsIgnoreCase("dtsc")) {
+        return MimeTypes.AUDIO_DTS;
+      } else if (fourCC.equalsIgnoreCase("dtsh") || fourCC.equalsIgnoreCase("dtsl")) {
+        return MimeTypes.AUDIO_DTS_HD;
+      } else if (fourCC.equalsIgnoreCase("dtse")) {
+        return MimeTypes.AUDIO_DTS_EXPRESS;
+      } else if (fourCC.equalsIgnoreCase("opus")) {
+        return MimeTypes.AUDIO_OPUS;
+      }
+      return null;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/CaptionStyleCompat.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text;
+
+import android.annotation.TargetApi;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.support.annotation.IntDef;
+import android.view.accessibility.CaptioningManager;
+import android.view.accessibility.CaptioningManager.CaptionStyle;
+import com.google.android.exoplayer2.util.Util;
+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.
+   */
+  @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, 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.
+   */
+  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, 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());
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/Cue.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text;
+
+import android.graphics.Color;
+import android.support.annotation.IntDef;
+import android.text.Layout.Alignment;
+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 {
+
+  /**
+   * An unset position or width.
+   */
+  public static final float DIMEN_UNSET = Float.MIN_VALUE;
+
+  /**
+   * The type of anchor, which may be unset.
+   */
+  @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.
+   */
+  @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 cue text. Note the {@link CharSequence} may be decorated with styling spans.
+   */
+  public final CharSequence text;
+
+  /**
+   * The alignment of the cue text within the cue box, or null if the alignment is undefined.
+   */
+  public final Alignment textAlignment;
+
+  /**
+   * The 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.
+   */
+
+  @LineType
+  public final 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.
+   */
+
+  @AnchorType
+  public final 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.
+   */
+  @AnchorType
+  public final 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;
+
+  /**
+   * Specifies whether or not the {@link #windowColor} property is set.
+   */
+  public final boolean windowColorSet;
+
+  /**
+   * The fill color of the window.
+   */
+  public final int windowColor;
+
+  /**
+   * Constructs a 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, null, DIMEN_UNSET, TYPE_UNSET, TYPE_UNSET, DIMEN_UNSET, TYPE_UNSET, DIMEN_UNSET);
+  }
+
+  /**
+   * @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, Alignment textAlignment, float line, @LineType int lineType,
+      @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size) {
+    this(text, textAlignment, line, lineType, lineAnchor, position, positionAnchor, size, false,
+        Color.BLACK);
+  }
+
+  /**
+   * @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, Alignment textAlignment, float line, @LineType int lineType,
+      @AnchorType int lineAnchor, float position, @AnchorType int positionAnchor, float size,
+      boolean windowColorSet, int windowColor) {
+    this.text = text;
+    this.textAlignment = textAlignment;
+    this.line = line;
+    this.lineType = lineType;
+    this.lineAnchor = lineAnchor;
+    this.position = position;
+    this.positionAnchor = positionAnchor;
+    this.size = size;
+    this.windowColorSet = windowColorSet;
+    this.windowColor = windowColor;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.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 com.google.android.exoplayer2.text;
+
+import com.google.android.exoplayer2.decoder.SimpleDecoder;
+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.
+   */
+  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 void releaseOutputBuffer(SubtitleOutputBuffer buffer) {
+    super.releaseOutputBuffer(buffer);
+  }
+
+  @Override
+  protected final SubtitleDecoderException decode(SubtitleInputBuffer inputBuffer,
+      SubtitleOutputBuffer outputBuffer, boolean reset) {
+    try {
+      ByteBuffer inputData = inputBuffer.data;
+      Subtitle subtitle = decode(inputData.array(), inputData.limit());
+      outputBuffer.setContent(inputBuffer.timeUs, subtitle, inputBuffer.subsampleOffsetUs);
+      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.
+   * @return The decoded {@link Subtitle}.
+   * @throws SubtitleDecoderException If a decoding error occurs.
+   */
+  protected abstract Subtitle decode(byte[] data, int size) throws SubtitleDecoderException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 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);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.text;
+
+import 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);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.text;
+
+import 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);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderException.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 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 message The detail message for this exception.
+   * @param cause The cause of this exception.
+   */
+  public SubtitleDecoderException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/SubtitleDecoderFactory.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 com.google.android.exoplayer2.text;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.text.cea.Cea608Decoder;
+import com.google.android.exoplayer2.text.cea.Cea708Decoder;
+import com.google.android.exoplayer2.text.subrip.SubripDecoder;
+import com.google.android.exoplayer2.text.ttml.TtmlDecoder;
+import com.google.android.exoplayer2.text.tx3g.Tx3gDecoder;
+import com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder;
+import com.google.android.exoplayer2.text.webvtt.WebvttDecoder;
+import 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>
+   * <li>WebVTT (MP4) ({@link Mp4WebvttDecoder})</li>
+   * <li>TTML ({@link TtmlDecoder})</li>
+   * <li>SubRip ({@link SubripDecoder})</li>
+   * <li>TX3G ({@link Tx3gDecoder})</li>
+   * <li>Cea608 ({@link Cea608Decoder})</li>
+   * <li>Cea708 ({@link Cea708Decoder})</li>
+   * </ul>
+   */
+  SubtitleDecoderFactory DEFAULT = new SubtitleDecoderFactory() {
+
+    @Override
+    public boolean supportsFormat(Format format) {
+      return getDecoderClass(format.sampleMimeType) != null;
+    }
+
+    @Override
+    public SubtitleDecoder createDecoder(Format format) {
+      try {
+        Class<?> clazz = getDecoderClass(format.sampleMimeType);
+        if (clazz == null) {
+          throw new IllegalArgumentException("Attempted to create decoder for unsupported format");
+        }
+        if (format.sampleMimeType.equals(MimeTypes.APPLICATION_CEA608)
+            || format.sampleMimeType.equals(MimeTypes.APPLICATION_MP4CEA608)) {
+          return clazz.asSubclass(SubtitleDecoder.class).getConstructor(String.class, Integer.TYPE)
+              .newInstance(format.sampleMimeType, format.accessibilityChannel);
+        } else if (format.sampleMimeType.equals(MimeTypes.APPLICATION_CEA708)) {
+          return clazz.asSubclass(SubtitleDecoder.class).getConstructor(Integer.TYPE)
+              .newInstance(format.accessibilityChannel);
+        } else {
+          return clazz.asSubclass(SubtitleDecoder.class).getConstructor().newInstance();
+        }
+      } catch (Exception e) {
+        throw new IllegalStateException("Unexpected error instantiating decoder", e);
+      }
+    }
+
+    private Class<?> getDecoderClass(String mimeType) {
+      if (mimeType == null) {
+        return null;
+      }
+      try {
+        switch (mimeType) {
+          case MimeTypes.TEXT_VTT:
+            return Class.forName("com.google.android.exoplayer2.text.webvtt.WebvttDecoder");
+          case MimeTypes.APPLICATION_TTML:
+            return Class.forName("com.google.android.exoplayer2.text.ttml.TtmlDecoder");
+          case MimeTypes.APPLICATION_MP4VTT:
+            return Class.forName("com.google.android.exoplayer2.text.webvtt.Mp4WebvttDecoder");
+          case MimeTypes.APPLICATION_SUBRIP:
+            return Class.forName("com.google.android.exoplayer2.text.subrip.SubripDecoder");
+          case MimeTypes.APPLICATION_TX3G:
+            return Class.forName("com.google.android.exoplayer2.text.tx3g.Tx3gDecoder");
+          case MimeTypes.APPLICATION_CEA608:
+          case MimeTypes.APPLICATION_MP4CEA608:
+            return Class.forName("com.google.android.exoplayer2.text.cea.Cea608Decoder");
+          case MimeTypes.APPLICATION_CEA708:
+            return Class.forName("com.google.android.exoplayer2.text.cea.Cea708Decoder");
+          default:
+            return null;
+        }
+      } catch (ClassNotFoundException e) {
+        return null;
+      }
+    }
+
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/SubtitleInputBuffer.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 com.google.android.exoplayer2.text;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+
+/**
+ * A {@link DecoderInputBuffer} for a {@link SubtitleDecoder}.
+ */
+public final class SubtitleInputBuffer extends DecoderInputBuffer
+    implements Comparable<SubtitleInputBuffer> {
+
+  /**
+   * 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);
+  }
+
+  @Override
+  public int compareTo(SubtitleInputBuffer other) {
+    long delta = timeUs - other.timeUs;
+    if (delta == 0) {
+      return 0;
+    }
+    return delta > 0 ? 1 : -1;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/SubtitleOutputBuffer.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.decoder.OutputBuffer;
+import java.util.List;
+
+/**
+ * Base class for {@link SubtitleDecoder} output buffers.
+ */
+public abstract class SubtitleOutputBuffer extends OutputBuffer implements Subtitle {
+
+  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 subtitle.getEventTimeCount();
+  }
+
+  @Override
+  public long getEventTime(int index) {
+    return subtitle.getEventTime(index) + subsampleOffsetUs;
+  }
+
+  @Override
+  public int getNextEventTimeIndex(long timeUs) {
+    return subtitle.getNextEventTimeIndex(timeUs - subsampleOffsetUs);
+  }
+
+  @Override
+  public List<Cue> getCues(long timeUs) {
+    return subtitle.getCues(timeUs - subsampleOffsetUs);
+  }
+
+  @Override
+  public abstract void release();
+
+  @Override
+  public void clear() {
+    super.clear();
+    subtitle = null;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/TextRenderer.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 com.google.android.exoplayer2.text;
+
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Looper;
+import android.os.Message;
+import com.google.android.exoplayer2.BaseRenderer;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.MimeTypes;
+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 an {@link Output}.
+ */
+public final class TextRenderer extends BaseRenderer implements Callback {
+
+  /**
+   * Receives output from a {@link TextRenderer}.
+   */
+  public interface Output {
+
+    /**
+     * Called each time there is a change in the {@link Cue}s.
+     *
+     * @param cues The {@link Cue}s.
+     */
+    void onCues(List<Cue> cues);
+
+  }
+
+  private static final int MSG_UPDATE_OUTPUT = 0;
+
+  private final Handler outputHandler;
+  private final Output output;
+  private final SubtitleDecoderFactory decoderFactory;
+  private final FormatHolder formatHolder;
+
+  private boolean inputStreamEnded;
+  private boolean outputStreamEnded;
+  private SubtitleDecoder decoder;
+  private SubtitleInputBuffer nextInputBuffer;
+  private SubtitleOutputBuffer subtitle;
+  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(Output output, 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(Output output, Looper outputLooper, SubtitleDecoderFactory decoderFactory) {
+    super(C.TRACK_TYPE_TEXT);
+    this.output = Assertions.checkNotNull(output);
+    this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this);
+    this.decoderFactory = decoderFactory;
+    formatHolder = new FormatHolder();
+  }
+
+  @Override
+  public int supportsFormat(Format format) {
+    return decoderFactory.supportsFormat(format) ? FORMAT_HANDLED
+        : (MimeTypes.isText(format.sampleMimeType) ? FORMAT_UNSUPPORTED_SUBTYPE
+        : FORMAT_UNSUPPORTED_TYPE);
+  }
+
+  @Override
+  protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+    if (decoder != null) {
+      decoder.release();
+      nextInputBuffer = null;
+    }
+    decoder = decoderFactory.createDecoder(formats[0]);
+  }
+
+  @Override
+  protected void onPositionReset(long positionUs, boolean joining) {
+    clearOutput();
+    resetBuffers();
+    decoder.flush();
+    inputStreamEnded = false;
+    outputStreamEnded = false;
+  }
+
+  @Override
+  public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
+    if (outputStreamEnded) {
+      return;
+    }
+
+    if (nextSubtitle == null) {
+      decoder.setPositionUs(positionUs);
+      try {
+        nextSubtitle = decoder.dequeueOutputBuffer();
+      } catch (SubtitleDecoderException e) {
+        throw ExoPlaybackException.createForRenderer(e, getIndex());
+      }
+    }
+
+    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 (subtitle != null) {
+            subtitle.release();
+            subtitle = null;
+          }
+          nextSubtitle.release();
+          nextSubtitle = null;
+          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));
+    }
+
+    try {
+      while (!inputStreamEnded) {
+        if (nextInputBuffer == null) {
+          nextInputBuffer = decoder.dequeueInputBuffer();
+          if (nextInputBuffer == null) {
+            return;
+          }
+        }
+        // Try and read the next subtitle from the source.
+        int result = readSource(formatHolder, nextInputBuffer);
+        if (result == C.RESULT_BUFFER_READ) {
+          // Clear BUFFER_FLAG_DECODE_ONLY (see [Internal: b/27893809]) and queue the buffer.
+          nextInputBuffer.clearFlag(C.BUFFER_FLAG_DECODE_ONLY);
+          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) {
+          break;
+        }
+      }
+    } catch (SubtitleDecoderException e) {
+      throw ExoPlaybackException.createForRenderer(e, getIndex());
+    }
+  }
+
+  @Override
+  protected void onDisabled() {
+    clearOutput();
+    resetBuffers();
+    decoder.release();
+    decoder = null;
+    super.onDisabled();
+  }
+
+  @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 resetBuffers() {
+    nextInputBuffer = null;
+    nextSubtitleEventIndex = C.INDEX_UNSET;
+    if (subtitle != null) {
+      subtitle.release();
+      subtitle = null;
+    }
+    if (nextSubtitle != null) {
+      nextSubtitle.release();
+      nextSubtitle = null;
+    }
+  }
+
+  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.<Cue>emptyList());
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public boolean handleMessage(Message msg) {
+    switch (msg.what) {
+      case MSG_UPDATE_OUTPUT:
+        invokeUpdateOutputInternal((List<Cue>) msg.obj);
+        return true;
+    }
+    return false;
+  }
+
+  private void invokeUpdateOutputInternal(List<Cue> cues) {
+    output.onCues(cues);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/cea/Cea608Decoder.java
@@ -0,0 +1,785 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.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.CharacterStyle;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.text.SubtitleDecoder;
+import com.google.android.exoplayer2.text.SubtitleInputBuffer;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.LinkedList;
+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 int CC_VALID_FLAG = 0x04;
+  private static final int CC_TYPE_FLAG = 0x02;
+  private static final int CC_FIELD_FLAG = 0x01;
+
+  private static final int NTSC_CC_FIELD_1 = 0x00;
+  private static final int NTSC_CC_FIELD_2 = 0x01;
+  private static final int CC_VALID_608_ID = 0x04;
+
+  private static final int 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[] COLORS = new int[] {
+      Color.WHITE,
+      Color.GREEN,
+      Color.BLUE,
+      Color.CYAN,
+      Color.RED,
+      Color.YELLOW,
+      Color.MAGENTA,
+  };
+
+  // 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;
+  /**
+   * 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;
+  /**
+   * 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;
+
+  private static final byte CTRL_ERASE_DISPLAYED_MEMORY = 0x2C;
+  private static final byte CTRL_CARRIAGE_RETURN = 0x2D;
+  private static final byte CTRL_ERASE_NON_DISPLAYED_MEMORY = 0x2E;
+  private static final byte CTRL_DELETE_TO_END_OF_ROW = 0x24;
+
+  private static final byte CTRL_BACKSPACE = 0x21;
+
+  // 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 final ParsableByteArray ccData;
+  private final int packetLength;
+  private final int selectedField;
+  private final LinkedList<CueBuilder> cueBuilders;
+
+  private CueBuilder currentCueBuilder;
+  private List<Cue> cues;
+  private List<Cue> lastCues;
+
+  private int captionMode;
+  private int captionRowCount;
+
+  private boolean repeatableControlSet;
+  private byte repeatableControlCc1;
+  private byte repeatableControlCc2;
+
+  public Cea608Decoder(String mimeType, int accessibilityChannel) {
+    ccData = new ParsableByteArray();
+    cueBuilders = new LinkedList<>();
+    currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT);
+    packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3;
+    switch (accessibilityChannel) {
+      case 3:
+      case 4:
+        selectedField = 2;
+        break;
+      case 1:
+      case 2:
+      case Format.NO_VALUE:
+      default:
+        selectedField = 1;
+    }
+
+    setCaptionMode(CC_MODE_UNKNOWN);
+    resetCueBuilders();
+  }
+
+  @Override
+  public String getName() {
+    return "Cea608Decoder";
+  }
+
+  @Override
+  public void flush() {
+    super.flush();
+    cues = null;
+    lastCues = null;
+    setCaptionMode(CC_MODE_UNKNOWN);
+    resetCueBuilders();
+    captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT;
+    repeatableControlSet = false;
+    repeatableControlCc1 = 0;
+    repeatableControlCc2 = 0;
+  }
+
+  @Override
+  public void release() {
+    // Do nothing
+  }
+
+  @Override
+  protected boolean isNewSubtitleDataAvailable() {
+    return cues != lastCues;
+  }
+
+  @Override
+  protected Subtitle createSubtitle() {
+    lastCues = cues;
+    return new CeaSubtitle(cues);
+  }
+
+  @Override
+  protected void decode(SubtitleInputBuffer inputBuffer) {
+    ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit());
+    boolean captionDataProcessed = false;
+    boolean isRepeatableControl = false;
+    while (ccData.bytesLeft() >= packetLength) {
+      byte ccDataHeader = packetLength == 2 ? CC_IMPLICIT_DATA_HEADER
+          : (byte) ccData.readUnsignedByte();
+      byte ccData1 = (byte) (ccData.readUnsignedByte() & 0x7F); // strip the parity bit
+      byte ccData2 = (byte) (ccData.readUnsignedByte() & 0x7F); // strip the parity bit
+
+      // Only examine valid CEA-608 packets
+      // TODO: We're currently ignoring the top 5 marker bits, which should all be 1s according
+      // to the CEA-608 specification. We need to determine if the data should be handled
+      // differently when that is not the case.
+      if ((ccDataHeader & (CC_VALID_FLAG | CC_TYPE_FLAG)) != CC_VALID_608_ID) {
+        continue;
+      }
+
+      // Only examine packets within the selected field
+      if ((selectedField == 1 && (ccDataHeader & CC_FIELD_FLAG) != NTSC_CC_FIELD_1)
+          || (selectedField == 2 && (ccDataHeader & CC_FIELD_FLAG) != NTSC_CC_FIELD_2)) {
+        continue;
+      }
+
+      // Ignore empty captions.
+      if (ccData1 == 0 && ccData2 == 0) {
+        continue;
+      }
+
+      // If we've reached this point then there is data to process; flag that work has been done.
+      captionDataProcessed = true;
+
+      // Special North American character set.
+      // ccData1 - 0|0|0|1|C|0|0|1
+      // ccData2 - 0|0|1|1|X|X|X|X
+      if (((ccData1 & 0xF7) == 0x11) && ((ccData2 & 0xF0) == 0x30)) {
+        // TODO: Make use of the channel toggle
+        currentCueBuilder.append(getSpecialChar(ccData2));
+        continue;
+      }
+
+      // Extended Western European character set.
+      // ccData1 - 0|0|0|1|C|0|1|S
+      // ccData2 - 0|0|1|X|X|X|X|X
+      if (((ccData1 & 0xF6) == 0x12) && (ccData2 & 0xE0) == 0x20) {
+        // TODO: Make use of the channel toggle
+        // Remove standard equivalent of the special extended char before appending new one
+        currentCueBuilder.backspace();
+        if ((ccData1 & 0x01) == 0x00) {
+          // Extended Spanish/Miscellaneous and French character set (S = 0).
+          currentCueBuilder.append(getExtendedEsFrChar(ccData2));
+        } else {
+          // Extended Portuguese and German/Danish character set (S = 1).
+          currentCueBuilder.append(getExtendedPtDeChar(ccData2));
+        }
+        continue;
+      }
+
+      // Control character.
+      // ccData1 - 0|0|0|X|X|X|X|X
+      if ((ccData1 & 0xE0) == 0x00) {
+        isRepeatableControl = handleCtrl(ccData1, ccData2);
+        continue;
+      }
+
+      // Basic North American character set.
+      currentCueBuilder.append(getChar(ccData1));
+      if ((ccData2 & 0xE0) != 0x00) {
+        currentCueBuilder.append(getChar(ccData2));
+      }
+    }
+
+    if (captionDataProcessed) {
+      if (!isRepeatableControl) {
+        repeatableControlSet = false;
+      }
+      if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_PAINT_ON) {
+        cues = getDisplayCues();
+      }
+    }
+  }
+
+  private boolean handleCtrl(byte cc1, byte cc2) {
+    boolean isRepeatableControl = isRepeatable(cc1);
+
+    // Most control commands are sent twice in succession to ensure they are received properly.
+    // We don't want to process duplicate commands, so if we see the same repeatable command twice
+    // in a row, ignore the second one.
+    if (isRepeatableControl) {
+      if (repeatableControlSet
+          && repeatableControlCc1 == cc1
+          && repeatableControlCc2 == cc2) {
+        // This is a duplicate. Clear the repeatable control flag and return.
+        repeatableControlSet = false;
+        return true;
+      } else {
+        // This is a repeatable command, but we haven't see it yet, so set the repeabable control
+        // flag (to ensure we ignore the next one should it be a duplicate) and continue processing
+        // the command.
+        repeatableControlSet = true;
+        repeatableControlCc1 = cc1;
+        repeatableControlCc2 = cc2;
+      }
+    }
+
+    if (isMidrowCtrlCode(cc1, cc2)) {
+      handleMidrowCtrl(cc2);
+    } else if (isPreambleAddressCode(cc1, cc2)) {
+      handlePreambleAddressCode(cc1, cc2);
+    } else if (isTabCtrlCode(cc1, cc2)) {
+      currentCueBuilder.tab(cc2 - 0x20);
+    } else if (isMiscCode(cc1, cc2)) {
+      handleMiscCode(cc2);
+    }
+
+    return isRepeatableControl;
+  }
+
+  private void handleMidrowCtrl(byte cc2) {
+    // TODO: support the extended styles (i.e. backgrounds and transparencies)
+
+    // cc2 - 0|0|1|0|ATRBT|U
+    // ATRBT is the 3-byte encoded attribute, and U is the underline toggle
+    boolean isUnderlined = (cc2 & 0x01) == 0x01;
+    currentCueBuilder.setUnderline(isUnderlined);
+
+    int attribute = (cc2 >> 1) & 0x0F;
+    if (attribute == 0x07) {
+      currentCueBuilder.setMidrowStyle(new StyleSpan(Typeface.ITALIC), 2);
+      currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(Color.WHITE), 1);
+    } else {
+      currentCueBuilder.setMidrowStyle(new ForegroundColorSpan(COLORS[attribute]), 1);
+    }
+  }
+
+  private void handlePreambleAddressCode(byte cc1, byte cc2) {
+    // cc1 - 0|0|0|1|C|E|ROW
+    // C is the channel toggle, E is the extended flag, and ROW is the encoded row
+    int row = ROW_INDICES[cc1 & 0x07];
+    // TODO: Make use of the channel toggle
+    // TODO: support the extended address and style
+
+    // cc2 - 0|1|N|ATTRBTE|U
+    // 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.getRow()) {
+      if (captionMode != CC_MODE_ROLL_UP && !currentCueBuilder.isEmpty()) {
+        currentCueBuilder = new CueBuilder(captionMode, captionRowCount);
+        cueBuilders.add(currentCueBuilder);
+      }
+      currentCueBuilder.setRow(row);
+    }
+
+    if ((cc2 & 0x01) == 0x01) {
+      currentCueBuilder.setPreambleStyle(new UnderlineSpan());
+    }
+
+    // cc2 - 0|1|N|0|STYLE|U
+    // cc2 - 0|1|N|1|CURSR|U
+    int attribute = cc2 >> 1 & 0x0F;
+    if (attribute <= 0x07) {
+      if (attribute == 0x07) {
+        currentCueBuilder.setPreambleStyle(new StyleSpan(Typeface.ITALIC));
+        currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(Color.WHITE));
+      } else {
+        currentCueBuilder.setPreambleStyle(new ForegroundColorSpan(COLORS[attribute]));
+      }
+    } else {
+      currentCueBuilder.setIndent(COLUMN_INDICES[attribute & 0x07]);
+    }
+  }
+
+  private void handleMiscCode(byte cc2) {
+    switch (cc2) {
+      case CTRL_ROLL_UP_CAPTIONS_2_ROWS:
+        captionRowCount = 2;
+        setCaptionMode(CC_MODE_ROLL_UP);
+        return;
+      case CTRL_ROLL_UP_CAPTIONS_3_ROWS:
+        captionRowCount = 3;
+        setCaptionMode(CC_MODE_ROLL_UP);
+        return;
+      case CTRL_ROLL_UP_CAPTIONS_4_ROWS:
+        captionRowCount = 4;
+        setCaptionMode(CC_MODE_ROLL_UP);
+        return;
+      case CTRL_RESUME_CAPTION_LOADING:
+        setCaptionMode(CC_MODE_POP_ON);
+        return;
+      case CTRL_RESUME_DIRECT_CAPTIONING:
+        setCaptionMode(CC_MODE_PAINT_ON);
+        return;
+    }
+
+    if (captionMode == CC_MODE_UNKNOWN) {
+      return;
+    }
+
+    switch (cc2) {
+      case CTRL_ERASE_DISPLAYED_MEMORY:
+        cues = null;
+        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;
+    }
+  }
+
+  private List<Cue> getDisplayCues() {
+    List<Cue> displayCues = new ArrayList<>();
+    for (int i = 0; i < cueBuilders.size(); i++) {
+      Cue cue = cueBuilders.get(i).build();
+      if (cue != null) {
+        displayCues.add(cue);
+      }
+    }
+    return displayCues;
+  }
+
+  private void setCaptionMode(int captionMode) {
+    if (this.captionMode == captionMode) {
+      return;
+    }
+
+    this.captionMode = captionMode;
+    // Clear the working memory.
+    resetCueBuilders();
+    if (captionMode == CC_MODE_ROLL_UP || captionMode == CC_MODE_UNKNOWN) {
+      // When switching to roll-up or unknown, we also need to clear the caption.
+      cues = null;
+    }
+  }
+
+  private void resetCueBuilders() {
+    currentCueBuilder.reset(captionMode, captionRowCount);
+    cueBuilders.clear();
+    cueBuilders.add(currentCueBuilder);
+  }
+
+  private static char getChar(byte ccData) {
+    int index = (ccData & 0x7F) - 0x20;
+    return (char) BASIC_CHARACTER_SET[index];
+  }
+
+  private static char getSpecialChar(byte ccData) {
+    int index = ccData & 0x0F;
+    return (char) SPECIAL_CHARACTER_SET[index];
+  }
+
+  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 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|0
+    // cc2 - 0|0|1|0|X|X|X|X
+    return ((cc1 & 0xF7) == 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 class CueBuilder {
+
+    private static final int POSITION_UNSET = -1;
+
+    // 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<CharacterStyle> preambleStyles;
+    private final List<CueStyle> midrowStyles;
+    private final List<SpannableString> rolledUpCaptions;
+    private final SpannableStringBuilder captionStringBuilder;
+
+    private int row;
+    private int indent;
+    private int tabOffset;
+    private int captionMode;
+    private int captionRowCount;
+    private int underlineStartPosition;
+
+    public CueBuilder(int captionMode, int captionRowCount) {
+      preambleStyles = new ArrayList<>();
+      midrowStyles = new ArrayList<>();
+      rolledUpCaptions = new LinkedList<>();
+      captionStringBuilder = new SpannableStringBuilder();
+      reset(captionMode, captionRowCount);
+    }
+
+    public void reset(int captionMode, int captionRowCount) {
+      preambleStyles.clear();
+      midrowStyles.clear();
+      rolledUpCaptions.clear();
+      captionStringBuilder.clear();
+      row = BASE_ROW;
+      indent = 0;
+      tabOffset = 0;
+      this.captionMode = captionMode;
+      this.captionRowCount = captionRowCount;
+      underlineStartPosition = POSITION_UNSET;
+    }
+
+    public boolean isEmpty() {
+      return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty()
+          && captionStringBuilder.length() == 0;
+    }
+
+    public void backspace() {
+      int length = captionStringBuilder.length();
+      if (length > 0) {
+        captionStringBuilder.delete(length - 1, length);
+      }
+    }
+
+    public int getRow() {
+      return row;
+    }
+
+    public void setRow(int row) {
+      this.row = row;
+    }
+
+    public void rollUp() {
+      rolledUpCaptions.add(buildSpannableString());
+      captionStringBuilder.clear();
+      preambleStyles.clear();
+      midrowStyles.clear();
+      underlineStartPosition = POSITION_UNSET;
+
+      int numRows = Math.min(captionRowCount, row);
+      while (rolledUpCaptions.size() >= numRows) {
+        rolledUpCaptions.remove(0);
+      }
+    }
+
+    public void setIndent(int indent) {
+      this.indent = indent;
+    }
+
+    public void tab(int tabs) {
+      tabOffset += tabs;
+    }
+
+    public void setPreambleStyle(CharacterStyle style) {
+      preambleStyles.add(style);
+    }
+
+    public void setMidrowStyle(CharacterStyle style, int nextStyleIncrement) {
+      midrowStyles.add(new CueStyle(style, captionStringBuilder.length(), nextStyleIncrement));
+    }
+
+    public void setUnderline(boolean enabled) {
+      if (enabled) {
+        underlineStartPosition = captionStringBuilder.length();
+      } else if (underlineStartPosition != POSITION_UNSET) {
+        // underline spans won't overlap, so it's safe to modify the builder directly with them
+        captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
+            captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        underlineStartPosition = POSITION_UNSET;
+      }
+    }
+
+    public void append(char text) {
+      captionStringBuilder.append(text);
+    }
+
+    public SpannableString buildSpannableString() {
+      int length = captionStringBuilder.length();
+
+      // preamble styles apply to the entire cue
+      for (int i = 0; i < preambleStyles.size(); i++) {
+        captionStringBuilder.setSpan(preambleStyles.get(i), 0, length,
+            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+      }
+
+      // midrow styles only apply to part of the cue, and after preamble styles
+      for (int i = 0; i < midrowStyles.size(); i++) {
+        CueStyle cueStyle = midrowStyles.get(i);
+        int end = (i < midrowStyles.size() - cueStyle.nextStyleIncrement)
+            ? midrowStyles.get(i + cueStyle.nextStyleIncrement).start
+            : length;
+        captionStringBuilder.setSpan(cueStyle.style, cueStyle.start, end,
+            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+      }
+
+      // special case for midrow underlines that went to the end of the cue
+      if (underlineStartPosition != POSITION_UNSET) {
+        captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition, length,
+            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+      }
+
+      return new SpannableString(captionStringBuilder);
+    }
+
+    public Cue build() {
+      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());
+
+      if (cueString.length() == 0) {
+        // The cue is empty.
+        return null;
+      }
+
+      float position;
+      int positionAnchor;
+      // The number of empty columns before the start of the text, in the range [0-31].
+      int startPadding = indent + tabOffset;
+      // The number of empty columns after the end of the text, in the same range.
+      int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length();
+      int startEndPaddingDelta = startPadding - endPadding;
+      if (captionMode == CC_MODE_POP_ON && Math.abs(startEndPaddingDelta) < 3) {
+        // Treat approximately centered pop-on captions are middle aligned.
+        position = 0.5f;
+        positionAnchor = Cue.ANCHOR_TYPE_MIDDLE;
+      } else if (captionMode == CC_MODE_POP_ON && startEndPaddingDelta > 0) {
+        // Treat pop-on captions with less padding at the end than the start as end aligned.
+        position = (float) (SCREEN_CHARWIDTH - endPadding) / SCREEN_CHARWIDTH;
+        // Adjust the position to fit within the safe area.
+        position = position * 0.8f + 0.1f;
+        positionAnchor = Cue.ANCHOR_TYPE_END;
+      } else {
+        // For all other cases assume start aligned.
+        position = (float) startPadding / SCREEN_CHARWIDTH;
+        // Adjust the position to fit within the safe area.
+        position = position * 0.8f + 0.1f;
+        positionAnchor = Cue.ANCHOR_TYPE_START;
+      }
+
+      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);
+    }
+
+    @Override
+    public String toString() {
+      return captionStringBuilder.toString();
+    }
+
+    private static class CueStyle {
+
+      public final CharacterStyle style;
+      public final int start;
+      public final int nextStyleIncrement;
+
+      public CueStyle(CharacterStyle style, int start, int nextStyleIncrement) {
+        this.style = style;
+        this.start = start;
+        this.nextStyleIncrement = nextStyleIncrement;
+      }
+
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Cue.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 com.google.android.exoplayer2.text.cea;
+
+import android.text.Layout.Alignment;
+import com.google.android.exoplayer2.text.Cue;
+
+/**
+ * A {@link Cue} for CEA-708.
+ */
+/* package */ final class Cea708Cue extends Cue implements Comparable<Cea708Cue> {
+
+  /**
+   * An unset priority.
+   */
+  public static final int PRIORITY_UNSET = -1;
+
+  /**
+   * 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(Cea708Cue other) {
+    if (other.priority < priority) {
+      return -1;
+    } else if (other.priority > priority) {
+      return 1;
+    }
+    return 0;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/cea/Cea708Decoder.java
@@ -0,0 +1,1225 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.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 android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Cue.AnchorType;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.text.SubtitleDecoder;
+import com.google.android.exoplayer2.text.SubtitleInputBuffer;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ParsableBitArray;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A {@link SubtitleDecoder} for CEA-708 (also known as "EIA-708").
+ *
+ * <p>This implementation does not provide full compatibility with the CEA-708 specification. Note
+ * that only the default pen/text and window/cue colors (i.e. text with
+ * {@link CueBuilder#COLOR_SOLID_WHITE} foreground and {@link CueBuilder#COLOR_SOLID_BLACK}
+ * background, and cues with {@link CueBuilder#COLOR_SOLID_BLACK} fill) will be overridden with
+ * device accessibility settings; all others will use the colors and opacity specified by the
+ * caption data.
+ */
+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_DS4 = 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;
+
+  public Cea708Decoder(int accessibilityChannel) {
+    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) {
+    ccData.reset(inputBuffer.data.array(), 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) {
+        finalizeCurrentPacket();
+        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 + ")");
+    }
+
+    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);
+    }
+
+    // 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;
+    }
+
+    while (serviceBlockPacket.bitsLeft() > 0) {
+      int command = serviceBlockPacket.readBits(8);
+      if (command != COMMAND_EXT1) {
+        if (command <= GROUP_C0_END) {
+          handleC0Command(command);
+        } else if (command <= GROUP_G0_END) {
+          handleG0Character(command);
+        } else if (command <= GROUP_C1_END) {
+          handleC1Command(command);
+          // Cues are always updated after a C1 command
+          cues = getDisplayCues();
+        } else if (command <= GROUP_G1_END) {
+          handleG1Character(command);
+        } 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);
+        } else if (command <= GROUP_C3_END) {
+          handleC3Command(command);
+        } else if (command <= GROUP_G3_END) {
+          handleG3Character(command);
+        } else {
+          Log.w(TAG, "Invalid extended command: " + command);
+        }
+      }
+    }
+  }
+
+  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_DS4:
+      case COMMAND_DF5:
+      case COMMAND_DF6:
+      case COMMAND_DF7:
+        window = (command - COMMAND_DF0);
+        handleDefineWindow(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 <= 0x0F) {
+      // 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.<Cue>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;
+
+    public CueBuilder() {
+      rolledUpCaptions = new LinkedList<>();
+      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;
+    }
+
+    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.
+    }
+
+    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));
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/cea/CeaDecoder.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text.cea;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.text.SubtitleDecoder;
+import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import com.google.android.exoplayer2.text.SubtitleInputBuffer;
+import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
+import com.google.android.exoplayer2.util.Assertions;
+import java.util.LinkedList;
+import java.util.TreeSet;
+
+/**
+ * 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 LinkedList<SubtitleInputBuffer> availableInputBuffers;
+  private final LinkedList<SubtitleOutputBuffer> availableOutputBuffers;
+  private final TreeSet<SubtitleInputBuffer> queuedInputBuffers;
+
+  private SubtitleInputBuffer dequeuedInputBuffer;
+  private long playbackPositionUs;
+
+  public CeaDecoder() {
+    availableInputBuffers = new LinkedList<>();
+    for (int i = 0; i < NUM_INPUT_BUFFERS; i++) {
+      availableInputBuffers.add(new SubtitleInputBuffer());
+    }
+    availableOutputBuffers = new LinkedList<>();
+    for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) {
+      availableOutputBuffers.add(new CeaOutputBuffer(this));
+    }
+    queuedInputBuffers = new TreeSet<>();
+  }
+
+  @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 != null);
+    Assertions.checkArgument(inputBuffer == dequeuedInputBuffer);
+    queuedInputBuffers.add(inputBuffer);
+    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.first().timeUs <= playbackPositionUs) {
+      SubtitleInputBuffer inputBuffer = queuedInputBuffers.pollFirst();
+
+      // 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(SubtitleInputBuffer inputBuffer) {
+    inputBuffer.clear();
+    availableInputBuffers.add(inputBuffer);
+  }
+
+  protected void releaseOutputBuffer(SubtitleOutputBuffer outputBuffer) {
+    outputBuffer.clear();
+    availableOutputBuffers.add(outputBuffer);
+  }
+
+  @Override
+  public void flush() {
+    playbackPositionUs = 0;
+    while (!queuedInputBuffers.isEmpty()) {
+      releaseInputBuffer(queuedInputBuffers.pollFirst());
+    }
+    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);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text.cea;
+
+import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
+
+/**
+ * A {@link SubtitleOutputBuffer} for {@link CeaDecoder}s.
+ */
+public final class CeaOutputBuffer extends SubtitleOutputBuffer {
+
+  private final CeaDecoder owner;
+
+  /**
+   * @param owner The decoder that owns this buffer.
+   */
+  public CeaOutputBuffer(CeaDecoder owner) {
+    super();
+    this.owner = owner;
+  }
+
+  @Override
+  public final void release() {
+    owner.releaseOutputBuffer(this);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.text.cea;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import 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.<Cue>emptyList();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/cea/CeaUtil.java
@@ -0,0 +1,106 @@
+/*
+ * 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 com.google.android.exoplayer2.text.cea;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * Utility methods for handling CEA-608/708 messages.
+ */
+public final class CeaUtil {
+
+  private static final int PAYLOAD_TYPE_CC = 4;
+  private static final int COUNTRY_CODE = 0xB5;
+  private static final int PROVIDER_CODE = 0x31;
+  private static final int USER_ID = 0x47413934; // "GA94"
+  private static final int USER_DATA_TYPE_CODE = 0x3;
+
+  /**
+   * Consumes the unescaped content of an SEI NAL unit, writing the content of any CEA-608 messages
+   * as samples to the provided output.
+   *
+   * @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 output The output to which any samples should be written.
+   */
+  public static void consume(long presentationTimeUs, ParsableByteArray seiBuffer,
+      TrackOutput output) {
+    int b;
+    while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) {
+      // Parse payload type.
+      int payloadType = 0;
+      do {
+        b = seiBuffer.readUnsignedByte();
+        payloadType += b;
+      } while (b == 0xFF);
+      // Parse payload size.
+      int payloadSize = 0;
+      do {
+        b = seiBuffer.readUnsignedByte();
+        payloadSize += b;
+      } while (b == 0xFF);
+      // Process the payload.
+      if (isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) {
+        // Ignore country_code (1) + provider_code (2) + user_identifier (4)
+        // + user_data_type_code (1).
+        seiBuffer.skipBytes(8);
+        // Ignore first three bits: reserved (1) + process_cc_data_flag (1) + zero_bit (1).
+        int ccCount = seiBuffer.readUnsignedByte() & 0x1F;
+        // Ignore em_data (1)
+        seiBuffer.skipBytes(1);
+        // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2)
+        // + cc_data_1 (8) + cc_data_2 (8).
+        int sampleLength = ccCount * 3;
+        output.sampleData(seiBuffer, sampleLength);
+        output.sampleMetadata(presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null);
+        // Ignore trailing information in SEI, if any.
+        seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3));
+      } else {
+        seiBuffer.skipBytes(payloadSize);
+      }
+    }
+  }
+
+  /**
+   * Inspects an sei message to determine whether it contains CEA-608.
+   * <p>
+   * The position of {@code payload} is left unchanged.
+   *
+   * @param payloadType The payload type of the message.
+   * @param payloadLength The length of the payload.
+   * @param payload A {@link ParsableByteArray} containing the payload.
+   * @return Whether the sei message contains CEA-608.
+   */
+  private static boolean isSeiMessageCea608(int payloadType, int payloadLength,
+      ParsableByteArray payload) {
+    if (payloadType != PAYLOAD_TYPE_CC || payloadLength < 8) {
+      return false;
+    }
+    int startPosition = payload.getPosition();
+    int countryCode = payload.readUnsignedByte();
+    int providerCode = payload.readUnsignedShort();
+    int userIdentifier = payload.readInt();
+    int userDataTypeCode = payload.readUnsignedByte();
+    payload.setPosition(startPosition);
+    return countryCode == COUNTRY_CODE && providerCode == PROVIDER_CODE
+        && userIdentifier == USER_ID && userDataTypeCode == USER_DATA_TYPE_CODE;
+  }
+
+  private CeaUtil() {}
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/subrip/SubripDecoder.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 com.google.android.exoplayer2.text.subrip;
+
+import android.text.Html;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.util.Log;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
+import com.google.android.exoplayer2.util.LongArray;
+import 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 {
+
+  private static final String TAG = "SubripDecoder";
+
+  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*");
+
+  private final StringBuilder textBuilder;
+
+  public SubripDecoder() {
+    super("SubripDecoder");
+    textBuilder = new StringBuilder();
+  }
+
+  @Override
+  protected SubripSubtitle decode(byte[] bytes, int length) {
+    ArrayList<Cue> cues = new ArrayList<>();
+    LongArray cueTimesUs = new LongArray();
+    ParsableByteArray subripData = new ParsableByteArray(bytes, length);
+    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.
+      boolean haveEndTimecode = false;
+      currentLine = subripData.readLine();
+      Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine);
+      if (matcher.matches()) {
+        cueTimesUs.add(parseTimecode(matcher, 1));
+        if (!TextUtils.isEmpty(matcher.group(6))) {
+          haveEndTimecode = true;
+          cueTimesUs.add(parseTimecode(matcher, 6));
+        }
+      } else {
+        Log.w(TAG, "Skipping invalid timing: " + currentLine);
+        continue;
+      }
+
+      // Read and parse the text.
+      textBuilder.setLength(0);
+      while (!TextUtils.isEmpty(currentLine = subripData.readLine())) {
+        if (textBuilder.length() > 0) {
+          textBuilder.append("<br>");
+        }
+        textBuilder.append(currentLine.trim());
+      }
+
+      Spanned text = Html.fromHtml(textBuilder.toString());
+      cues.add(new Cue(text));
+      if (haveEndTimecode) {
+        cues.add(null);
+      }
+    }
+
+    Cue[] cuesArray = new Cue[cues.size()];
+    cues.toArray(cuesArray);
+    long[] cueTimesUsArray = cueTimesUs.toArray();
+    return new SubripSubtitle(cuesArray, cueTimesUsArray);
+  }
+
+  private static long parseTimecode(Matcher matcher, int groupOffset) {
+    long timestampMs = Long.parseLong(matcher.group(groupOffset + 1)) * 60 * 60 * 1000;
+    timestampMs += Long.parseLong(matcher.group(groupOffset + 2)) * 60 * 1000;
+    timestampMs += Long.parseLong(matcher.group(groupOffset + 3)) * 1000;
+    timestampMs += Long.parseLong(matcher.group(groupOffset + 4));
+    return timestampMs * 1000;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.text.subrip;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.util.Assertions;
+import 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. Null entries may be used to represent empty cues.
+   * @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] == null) {
+      // 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]);
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlDecoder.java
@@ -0,0 +1,546 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text.ttml;
+
+import android.text.Layout;
+import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
+import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import com.google.android.exoplayer2.util.ColorParser;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.util.XmlPullParserUtil;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.LinkedList;
+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
+ * </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 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 int DEFAULT_FRAME_RATE = 30;
+
+  private static final FrameAndTickRate DEFAULT_FRAME_AND_TICK_RATE =
+      new FrameAndTickRate(DEFAULT_FRAME_RATE, 1, 1);
+
+  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 TtmlSubtitle decode(byte[] bytes, int length) throws SubtitleDecoderException {
+    try {
+      XmlPullParser xmlParser = xmlParserFactory.newPullParser();
+      Map<String, TtmlStyle> globalStyles = new HashMap<>();
+      Map<String, TtmlRegion> regionMap = new HashMap<>();
+      regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion());
+      ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length);
+      xmlParser.setInput(inputStream, null);
+      TtmlSubtitle ttmlSubtitle = null;
+      LinkedList<TtmlNode> nodeStack = new LinkedList<>();
+      int unsupportedNodeDepth = 0;
+      int eventType = xmlParser.getEventType();
+      FrameAndTickRate frameAndTickRate = DEFAULT_FRAME_AND_TICK_RATE;
+      while (eventType != XmlPullParser.END_DOCUMENT) {
+        TtmlNode parent = nodeStack.peekLast();
+        if (unsupportedNodeDepth == 0) {
+          String name = xmlParser.getName();
+          if (eventType == XmlPullParser.START_TAG) {
+            if (TtmlNode.TAG_TT.equals(name)) {
+              frameAndTickRate = parseFrameAndTickRates(xmlParser);
+            }
+            if (!isSupportedTag(name)) {
+              Log.i(TAG, "Ignoring unsupported tag: " + xmlParser.getName());
+              unsupportedNodeDepth++;
+            } else if (TtmlNode.TAG_HEAD.equals(name)) {
+              parseHeader(xmlParser, globalStyles, regionMap);
+            } else {
+              try {
+                TtmlNode node = parseNode(xmlParser, parent, regionMap, frameAndTickRate);
+                nodeStack.addLast(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.getLast(), globalStyles, regionMap);
+            }
+            nodeStack.removeLast();
+          }
+        } 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 = frameRateMultiplierString.split(" ");
+      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 Map<String, TtmlStyle> parseHeader(XmlPullParser xmlParser,
+      Map<String, TtmlStyle> globalStyles, Map<String, TtmlRegion> globalRegions)
+      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)) {
+        Pair<String, TtmlRegion> ttmlRegionInfo = parseRegionAttributes(xmlParser);
+        if (ttmlRegionInfo != null) {
+          globalRegions.put(ttmlRegionInfo.first, ttmlRegionInfo.second);
+        }
+      }
+    } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD));
+    return globalStyles;
+  }
+
+  /**
+   * Parses a region declaration. Supports origin and extent definition but only when defined in
+   * terms of percentage of the viewport. Regions that do not correctly declare origin are ignored.
+   */
+  private Pair<String, TtmlRegion> parseRegionAttributes(XmlPullParser xmlParser) {
+    String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID);
+    String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN);
+    String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT);
+    if (regionOrigin == null || regionId == null) {
+      return null;
+    }
+    float position = Cue.DIMEN_UNSET;
+    float line = Cue.DIMEN_UNSET;
+    Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin);
+    if (originMatcher.matches()) {
+      try {
+        position = Float.parseFloat(originMatcher.group(1)) / 100.f;
+        line = Float.parseFloat(originMatcher.group(2)) / 100.f;
+      } catch (NumberFormatException e) {
+        Log.w(TAG, "Ignoring region with malformed origin: '" + regionOrigin + "'", e);
+        position = Cue.DIMEN_UNSET;
+      }
+    }
+    float width = Cue.DIMEN_UNSET;
+    if (regionExtent != null) {
+      Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent);
+      if (extentMatcher.matches()) {
+        try {
+          width = Float.parseFloat(extentMatcher.group(1)) / 100.f;
+        } catch (NumberFormatException e) {
+          Log.w(TAG, "Ignoring malformed region extent: '" + regionExtent + "'", e);
+        }
+      }
+    }
+    return position != Cue.DIMEN_UNSET ? new Pair<>(regionId, new TtmlRegion(position, line,
+        Cue.LINE_TYPE_FRACTION, width)) : null;
+  }
+
+  private String[] parseStyleIds(String parentStyleIds) {
+    return parentStyleIds.split("\\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[] 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;
+        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);
+  }
+
+  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_SMPTE_IMAGE)
+        || tag.equals(TtmlNode.TAG_SMPTE_DATA)
+        || tag.equals(TtmlNode.TAG_SMPTE_INFORMATION);
+  }
+
+  private static void parseFontSize(String expression, TtmlStyle out) throws
+      SubtitleDecoderException {
+    String[] expressions = expression.split("\\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;
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlNode.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text.ttml;
+
+import android.text.SpannableStringBuilder;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import 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_SMPTE_IMAGE = "smpte:image";
+  public static final String TAG_SMPTE_DATA = "smpte:data";
+  public static final String TAG_SMPTE_INFORMATION = "smpte:information";
+
+  public static final String ANONYMOUS_REGION_ID = "";
+  public static final String ATTR_ID = "id";
+  public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor";
+  public static final String ATTR_TTS_EXTENT = "extent";
+  public static final String ATTR_TTS_FONT_STYLE = "fontStyle";
+  public static final String ATTR_TTS_FONT_SIZE = "fontSize";
+  public static final String ATTR_TTS_FONT_FAMILY = "fontFamily";
+  public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight";
+  public static final String ATTR_TTS_COLOR = "color";
+  public static final String ATTR_TTS_ORIGIN = "origin";
+  public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration";
+  public static final String ATTR_TTS_TEXT_ALIGN = "textAlign";
+
+  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";
+
+  public final String tag;
+  public final String text;
+  public final boolean isTextNode;
+  public final long startTimeUs;
+  public final long endTimeUs;
+  public final TtmlStyle style;
+  public final String regionId;
+
+  private final String[] styleIds;
+  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(null, TtmlRenderUtil.applyTextElementSpacePolicy(text), C.TIME_UNSET,
+        C.TIME_UNSET, null, null, ANONYMOUS_REGION_ID);
+  }
+
+  public static TtmlNode buildNode(String tag, long startTimeUs, long endTimeUs,
+      TtmlStyle style, String[] styleIds, String regionId) {
+    return new TtmlNode(tag, null, startTimeUs, endTimeUs, style, styleIds, regionId);
+  }
+
+  private TtmlNode(String tag, String text, long startTimeUs, long endTimeUs,
+      TtmlStyle style, String[] styleIds, String regionId) {
+    this.tag = tag;
+    this.text = text;
+    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);
+    if (descendsPNode || isPNode) {
+      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) {
+    TreeMap<String, SpannableStringBuilder> regionOutputs = new TreeMap<>();
+    traverseForText(timeUs, false, regionId, regionOutputs);
+    traverseForStyle(globalStyles, regionOutputs);
+    List<Cue> cues = new ArrayList<>();
+    for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
+      TtmlRegion region = regionMap.get(entry.getKey());
+      cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType,
+          Cue.TYPE_UNSET, region.position, Cue.TYPE_UNSET, region.width));
+    }
+    return cues;
+  }
+
+  private void traverseForText(long timeUs,  boolean descendsPNode,
+      String inheritedRegion, Map<String, SpannableStringBuilder> regionOutputs) {
+    nodeStartsByRegion.clear();
+    nodeEndsByRegion.clear();
+    String resolvedRegionId = regionId;
+    if (ANONYMOUS_REGION_ID.equals(resolvedRegionId)) {
+      resolvedRegionId = inheritedRegion;
+    }
+    if (isTextNode && descendsPNode) {
+      getRegionOutput(resolvedRegionId, regionOutputs).append(text);
+    } else if (TAG_BR.equals(tag) && descendsPNode) {
+      getRegionOutput(resolvedRegionId, regionOutputs).append('\n');
+    } else if (TAG_METADATA.equals(tag)) {
+      // Do nothing.
+    } else if (isActive(timeUs)) {
+      boolean isPNode = TAG_P.equals(tag);
+      for (Entry<String, SpannableStringBuilder> entry : regionOutputs.entrySet()) {
+        nodeStartsByRegion.put(entry.getKey(), entry.getValue().length());
+      }
+      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(Map<String, TtmlStyle> globalStyles,
+      Map<String, SpannableStringBuilder> regionOutputs) {
+    for (Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) {
+      String regionId = entry.getKey();
+      int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;
+      applyStyleToOutput(globalStyles, regionOutputs.get(regionId), start, entry.getValue());
+      for (int i = 0; i < getChildCount(); ++i) {
+        getChild(i).traverseForStyle(globalStyles, regionOutputs);
+      }
+    }
+  }
+
+  private void applyStyleToOutput(Map<String, TtmlStyle> globalStyles,
+      SpannableStringBuilder regionOutput, int start, int end) {
+    if (start != end) {
+      TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
+      if (resolvedStyle != null) {
+        TtmlRenderUtil.applyStylesToSpan(regionOutput, start, end, resolvedStyle);
+      }
+    }
+  }
+
+  private 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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlRegion.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text.ttml;
+
+import com.google.android.exoplayer2.text.Cue;
+
+/**
+ * Represents a TTML Region.
+ */
+/* package */ final class TtmlRegion {
+
+  public final float position;
+  public final float line;
+  @Cue.LineType
+  public final int lineType;
+  public final float width;
+
+  public TtmlRegion() {
+    this(Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
+  }
+
+  public TtmlRegion(float position, float line, @Cue.LineType int lineType, float width) {
+    this.position = position;
+    this.line = line;
+    this.lineType = lineType;
+    this.width = width;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 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() {}
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlStyle.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text.ttml;
+
+import android.graphics.Typeface;
+import android.support.annotation.IntDef;
+import android.text.Layout;
+import com.google.android.exoplayer2.util.Assertions;
+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;
+
+  @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;
+
+  @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;
+
+  @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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/ttml/TtmlSubtitle.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text.ttml;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import 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;
+
+  public TtmlSubtitle(TtmlNode root, Map<String, TtmlStyle> globalStyles,
+      Map<String, TtmlRegion> regionMap) {
+    this.root = root;
+    this.regionMap = regionMap;
+    this.globalStyles = globalStyles != null
+        ? Collections.unmodifiableMap(globalStyles) : Collections.<String, TtmlStyle>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);
+  }
+
+  /* @VisibleForTesting */
+  /* package */ Map<String, TtmlStyle> getGlobalStyles() {
+    return globalStyles;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/tx3g/Tx3gDecoder.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 com.google.android.exoplayer2.text.tx3g;
+
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+
+/**
+ * A {@link SimpleSubtitleDecoder} for tx3g.
+ * <p>
+ * Currently only supports parsing of a single text track.
+ */
+public final class Tx3gDecoder extends SimpleSubtitleDecoder {
+
+  private final ParsableByteArray parsableByteArray;
+
+  public Tx3gDecoder() {
+    super("Tx3gDecoder");
+    parsableByteArray = new ParsableByteArray();
+  }
+
+  @Override
+  protected Subtitle decode(byte[] bytes, int length) {
+    parsableByteArray.reset(bytes, length);
+    int textLength = parsableByteArray.readUnsignedShort();
+    if (textLength == 0) {
+      return Tx3gSubtitle.EMPTY;
+    }
+    String cueText = parsableByteArray.readString(textLength);
+    return new Tx3gSubtitle(new Cue(cueText));
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.text.tx3g;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import 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.<Cue>emptyList();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/CssParser.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text.webvtt;
+
+import android.text.TextUtils;
+import com.google.android.exoplayer2.util.ColorParser;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Arrays;
+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 BLOCK_START = "{";
+  private static final String BLOCK_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 found. Attempts to parse the
+   * contents of the style block and returns a {@link WebvttCssStyle} instance if successful, or
+   * {@code null} otherwise.
+   *
+   * @param input The input from which the style block should be read.
+   * @return A {@link WebvttCssStyle} that represents the parsed block.
+   */
+  public WebvttCssStyle parseBlock(ParsableByteArray input) {
+    stringBuilder.setLength(0);
+    int initialInputPosition = input.getPosition();
+    skipStyleBlock(input);
+    styleInput.reset(input.data, input.getPosition());
+    styleInput.setPosition(initialInputPosition);
+    String selector = parseSelector(styleInput, stringBuilder);
+    if (selector == null || !BLOCK_START.equals(parseNextToken(styleInput, stringBuilder))) {
+      return null;
+    }
+    WebvttCssStyle style = new WebvttCssStyle();
+    applySelectorToStyle(style, selector);
+    String token = null;
+    boolean blockEndFound = false;
+    while (!blockEndFound) {
+      int position = styleInput.getPosition();
+      token = parseNextToken(styleInput, stringBuilder);
+      blockEndFound = token == null || BLOCK_END.equals(token);
+      if (!blockEndFound) {
+        styleInput.setPosition(position);
+        parseStyleDeclaration(styleInput, style, stringBuilder);
+      }
+    }
+    return BLOCK_END.equals(token) ? style : null; // Check that the style block ended correctly.
+  }
+
+  /**
+   * 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.
+   */
+  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 (BLOCK_START.equals(token)) {
+      input.setPosition(position);
+      return "";
+    }
+    String target = null;
+    if ("(".equals(token)) {
+      target = readCueTarget(input);
+    }
+    token = parseNextToken(input, stringBuilder);
+    if (!")".equals(token) || token == null) {
+      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 (BLOCK_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.
+  /* 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];
+  }
+
+  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 (BLOCK_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 = selector.split("\\.");
+    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(Arrays.copyOfRange(classDivision, 1, classDivision.length));
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoder.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 com.google.android.exoplayer2.text.webvtt;
+
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
+import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import 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.
+ */
+public final class Mp4WebvttDecoder extends SimpleSubtitleDecoder {
+
+  private static final int BOX_HEADER_SIZE = 8;
+
+  private static final int TYPE_payl = Util.getIntegerCodeForString("payl");
+  private static final int TYPE_sttg = Util.getIntegerCodeForString("sttg");
+  private static final int TYPE_vttc = Util.getIntegerCodeForString("vttc");
+
+  private final ParsableByteArray sampleData;
+  private final WebvttCue.Builder builder;
+
+  public Mp4WebvttDecoder() {
+    super("Mp4WebvttDecoder");
+    sampleData = new ParsableByteArray();
+    builder = new WebvttCue.Builder();
+  }
+
+  @Override
+  protected Mp4WebvttSubtitle decode(byte[] bytes, int length) 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 = new String(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.<WebvttCssStyle>emptyList());
+      } else {
+        // Other VTTCueBox children are still not supported and are ignored.
+      }
+    }
+    return builder.build();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.text.webvtt;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import 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.<Cue>emptyList();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text.webvtt;
+
+import android.graphics.Typeface;
+import android.support.annotation.IntDef;
+import android.text.Layout;
+import com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 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>
+ */
+/* package */ final class WebvttCssStyle {
+
+  public static final int UNSPECIFIED = -1;
+
+  @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;
+
+  @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;
+
+  @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.
+  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 Layout.Alignment textAlign;
+
+  public WebvttCssStyle() {
+    reset();
+  }
+
+  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>
+   *
+   * 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(String id, String tag, String[] classes, String voice) {
+    if (targetId.isEmpty() && targetTag.isEmpty() && targetClasses.isEmpty()
+        && targetVoice.isEmpty()) {
+      // The selector is universal. It matches with the minimum score if and only if the given
+      // element is a whole cue.
+      return tag.isEmpty() ? 1 : 0;
+    }
+    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;
+  }
+
+  public String getFontFamily() {
+    return fontFamily;
+  }
+
+  public WebvttCssStyle setFontFamily(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;
+  }
+
+  public Layout.Alignment getTextAlign() {
+    return textAlign;
+  }
+
+  public WebvttCssStyle setTextAlign(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, String actual,
+      int score) {
+    if (target.isEmpty() || currentScore == -1) {
+      return currentScore;
+    }
+    return target.equals(actual) ? currentScore + score : -1;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCue.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 com.google.android.exoplayer2.text.webvtt;
+
+import android.text.Layout.Alignment;
+import android.text.SpannableStringBuilder;
+import android.util.Log;
+import com.google.android.exoplayer2.text.Cue;
+
+/**
+ * A representation of a WebVTT cue.
+ */
+/* package */ final class WebvttCue extends Cue {
+
+  public final long startTime;
+  public final long endTime;
+
+  public WebvttCue(CharSequence text) {
+    this(0, 0, text);
+  }
+
+  public WebvttCue(long startTime, long endTime, CharSequence text) {
+    this(startTime, endTime, text, null, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET,
+        Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
+  }
+
+  public WebvttCue(long startTime, long endTime, CharSequence text, Alignment textAlignment,
+      float line, @Cue.LineType int lineType, @Cue.AnchorType int lineAnchor, float position,
+      @Cue.AnchorType int positionAnchor, float width) {
+    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 == DIMEN_UNSET);
+  }
+
+  /**
+   * Builder for WebVTT cues.
+   */
+  @SuppressWarnings("hiding")
+  public static final class Builder {
+
+    private static final String TAG = "WebvttCueBuilder";
+
+    private long startTime;
+    private long endTime;
+    private SpannableStringBuilder text;
+    private Alignment textAlignment;
+    private float line;
+    private int lineType;
+    private int lineAnchor;
+    private float position;
+    private int positionAnchor;
+    private float width;
+
+    // Initialization methods
+
+    public Builder() {
+      reset();
+    }
+
+    public void reset() {
+      startTime = 0;
+      endTime = 0;
+      text = null;
+      textAlignment = null;
+      line = Cue.DIMEN_UNSET;
+      lineType = Cue.TYPE_UNSET;
+      lineAnchor = Cue.TYPE_UNSET;
+      position = Cue.DIMEN_UNSET;
+      positionAnchor = Cue.TYPE_UNSET;
+      width = Cue.DIMEN_UNSET;
+    }
+
+    // Construction methods.
+
+    public WebvttCue build() {
+      if (position != Cue.DIMEN_UNSET && positionAnchor == Cue.TYPE_UNSET) {
+        derivePositionAnchorFromAlignment();
+      }
+      return new WebvttCue(startTime, endTime, text, 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(SpannableStringBuilder aText) {
+      text = aText;
+      return this;
+    }
+
+    public Builder setTextAlignment(Alignment textAlignment) {
+      this.textAlignment = textAlignment;
+      return this;
+    }
+
+    public Builder setLine(float line) {
+      this.line = line;
+      return this;
+    }
+
+    public Builder setLineType(int lineType) {
+      this.lineType = lineType;
+      return this;
+    }
+
+    public Builder setLineAnchor(int lineAnchor) {
+      this.lineAnchor = lineAnchor;
+      return this;
+    }
+
+    public Builder setPosition(float position) {
+      this.position = position;
+      return this;
+    }
+
+    public Builder setPositionAnchor(int positionAnchor) {
+      this.positionAnchor = positionAnchor;
+      return this;
+    }
+
+    public Builder setWidth(float width) {
+      this.width = width;
+      return this;
+    }
+
+    private Builder derivePositionAnchorFromAlignment() {
+      if (textAlignment == null) {
+        positionAnchor = Cue.TYPE_UNSET;
+      } else {
+        switch (textAlignment) {
+          case ALIGN_NORMAL:
+            positionAnchor = Cue.ANCHOR_TYPE_START;
+            break;
+          case ALIGN_CENTER:
+            positionAnchor = Cue.ANCHOR_TYPE_MIDDLE;
+            break;
+          case ALIGN_OPPOSITE:
+            positionAnchor = Cue.ANCHOR_TYPE_END;
+            break;
+          default:
+            Log.w(TAG, "Unrecognized alignment: " + textAlignment);
+            positionAnchor = Cue.ANCHOR_TYPE_START;
+            break;
+        }
+      }
+      return this;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttCueParser.java
@@ -0,0 +1,531 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text.webvtt;
+
+import android.graphics.Typeface;
+import android.text.Layout.Alignment;
+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 android.util.Log;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Stack;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues)
+ */
+/* package */ final class WebvttCueParser {
+
+  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.
+   * @param styles List of styles defined by the CSS style blocks preceeding the cues.
+   * @return Whether a valid Cue was found.
+   */
+  /* package */ boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder,
+      List<WebvttCssStyle> styles) {
+    String firstLine = webvttData.readLine();
+    Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine);
+    if (cueHeaderMatcher.matches()) {
+      // We have found the timestamps in the first line. No id present.
+      return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles);
+    } else {
+      // The first line is not the timestamps, but could be the cue id.
+      String secondLine = webvttData.readLine();
+      cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine);
+      if (cueHeaderMatcher.matches()) {
+        // We can do the rest of the parsing, including the id.
+        return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder,
+            styles);
+      }
+    }
+    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 preceeding the cues.
+   * @param builder Output builder.
+   */
+  /* package */ static void parseCueText(String id, String markup, WebvttCue.Builder builder,
+      List<WebvttCssStyle> styles) {
+    SpannableStringBuilder spannedText = new SpannableStringBuilder();
+    Stack<StartTag> startTagStack = new Stack<>();
+    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);
+          String tagName = getTagName(fullTagExpression);
+          if (tagName == null || !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(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);
+    String line;
+    while ((line = webvttData.readLine()) != null && !line.isEmpty()) {
+      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)
+      throws NumberFormatException {
+    int commaIndex = s.indexOf(',');
+    if (commaIndex != -1) {
+      builder.setLineAnchor(parsePositionAnchor(s.substring(commaIndex + 1)));
+      s = s.substring(0, commaIndex);
+    } else {
+      builder.setLineAnchor(Cue.TYPE_UNSET);
+    }
+    if (s.endsWith("%")) {
+      builder.setLine(WebvttParserUtil.parsePercentage(s)).setLineType(Cue.LINE_TYPE_FRACTION);
+    } 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)
+      throws NumberFormatException {
+    int commaIndex = s.indexOf(',');
+    if (commaIndex != -1) {
+      builder.setPositionAnchor(parsePositionAnchor(s.substring(commaIndex + 1)));
+      s = s.substring(0, commaIndex);
+    } else {
+      builder.setPositionAnchor(Cue.TYPE_UNSET);
+    }
+    builder.setPosition(WebvttParserUtil.parsePercentage(s));
+  }
+
+  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;
+    }
+  }
+
+  private static Alignment parseTextAlignment(String s) {
+    switch (s) {
+      case "start":
+      case "left":
+        return Alignment.ALIGN_NORMAL;
+      case "center":
+      case "middle":
+        return Alignment.ALIGN_CENTER;
+      case "end":
+      case "right":
+        return Alignment.ALIGN_OPPOSITE;
+      default:
+        Log.w(TAG, "Invalid alignment value: " + s);
+        return null;
+    }
+  }
+
+  /**
+   * 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(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);
+    }
+    if (style.getTextAlign() != null) {
+      spannedText.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), 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();
+    if (tagExpression.isEmpty()) {
+      return null;
+    }
+    return tagExpression.split("[ \\.]")[0];
+  }
+
+  private static void getApplicableStyles(List<WebvttCssStyle> declaredStyles, 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(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();
+      if (fullTagExpression.isEmpty()) {
+        return null;
+      }
+      int voiceStartIndex = fullTagExpression.indexOf(" ");
+      String voice;
+      if (voiceStartIndex == -1) {
+        voice = "";
+      } else {
+        voice = fullTagExpression.substring(voiceStartIndex).trim();
+        fullTagExpression = fullTagExpression.substring(0, voiceStartIndex);
+      }
+      String[] nameAndClasses = fullTagExpression.split("\\.");
+      String name = nameAndClasses[0];
+      String[] classes;
+      if (nameAndClasses.length > 1) {
+        classes = Arrays.copyOfRange(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]);
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttDecoder.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 com.google.android.exoplayer2.text.webvtt;
+
+import android.text.TextUtils;
+import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
+import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import 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 WebvttSubtitle decode(byte[] bytes, int length) 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.
+    WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData);
+    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.
+        WebvttCssStyle styleBlock = cssParser.parseBlock(parsableWebvttData);
+        if (styleBlock != null) {
+          definedStyles.add(styleBlock);
+        }
+      } 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 (COMMENT_START.startsWith(line)) {
+        foundEvent = EVENT_COMMENT;
+      } else {
+        foundEvent = EVENT_CUE;
+      }
+    }
+    parsableWebvttData.setPosition(currentInputPosition);
+    return foundEvent;
+  }
+
+  private static void skipComment(ParsableByteArray parsableWebvttData) {
+    while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {}
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttParserUtil.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 com.google.android.exoplayer2.text.webvtt;
+
+import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+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((\u0020|\u0009).*)?$");
+  private static final Pattern HEADER = Pattern.compile("^\uFEFF?WEBVTT((\u0020|\u0009).*)?$");
+
+  private WebvttParserUtil() {}
+
+  /**
+   * Reads and validates the first line of a WebVTT file.
+   *
+   * @param input The input from which the line should be read.
+   * @throws SubtitleDecoderException If the line isn't the start of a valid WebVTT file.
+   */
+  public static void validateWebvttHeaderLine(ParsableByteArray input)
+      throws SubtitleDecoderException {
+    String line = input.readLine();
+    if (line == null || !HEADER.matcher(line).matches()) {
+      throw new SubtitleDecoderException("Expected WEBVTT. Got " + line);
+    }
+  }
+
+  /**
+   * 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 = timestamp.split("\\.", 2);
+    String[] subparts = parts[0].split(":");
+    for (String subpart : subparts) {
+      value = value * 60 + Long.parseLong(subpart);
+    }
+    return (value * 1000 + Long.parseLong(parts[1])) * 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.
+   */
+  public static Matcher findNextCueHeader(ParsableByteArray input) {
+    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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/text/webvtt/WebvttSubtitle.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.text.webvtt;
+
+import android.text.SpannableStringBuilder;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+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) {
+    ArrayList<Cue> list = null;
+    WebvttCue firstNormalCue = null;
+    SpannableStringBuilder normalCueTextBuilder = null;
+
+    for (int i = 0; i < numCues; i++) {
+      if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) {
+        if (list == null) {
+          list = new ArrayList<>();
+        }
+        WebvttCue cue = cues.get(i);
+        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(firstNormalCue.text).append("\n").append(cue.text);
+          } else {
+            normalCueTextBuilder.append("\n").append(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(normalCueTextBuilder));
+    } else if (firstNormalCue != null) {
+      // there was only a single normal cue, so just add it to the list
+      list.add(firstNormalCue);
+    }
+
+    if (list != null) {
+      return list;
+    } else {
+      return Collections.emptyList();
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveVideoTrackSelection.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.trackselection;
+
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.chunk.MediaChunk;
+import com.google.android.exoplayer2.upstream.BandwidthMeter;
+import java.util.List;
+
+/**
+ * A bandwidth based adaptive {@link TrackSelection} for video, 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 AdaptiveVideoTrackSelection extends BaseTrackSelection {
+
+  /**
+   * Factory for {@link AdaptiveVideoTrackSelection} instances.
+   */
+  public static final class Factory implements TrackSelection.Factory {
+
+    private final BandwidthMeter bandwidthMeter;
+    private final int maxInitialBitrate;
+    private final int minDurationForQualityIncreaseMs;
+    private final int maxDurationForQualityDecreaseMs;
+    private final int minDurationToRetainAfterDiscardMs;
+    private final float bandwidthFraction;
+
+    /**
+     * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
+     */
+    public Factory(BandwidthMeter bandwidthMeter) {
+      this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE,
+          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);
+    }
+
+    /**
+     * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
+     * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed
+     *     when a bandwidth estimate is unavailable.
+     * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for
+     *     the selected track to switch to one of higher quality.
+     * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for
+     *     the selected track to switch to one of lower quality.
+     * @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(BandwidthMeter bandwidthMeter, int maxInitialBitrate,
+        int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs,
+        int minDurationToRetainAfterDiscardMs, float bandwidthFraction) {
+      this.bandwidthMeter = bandwidthMeter;
+      this.maxInitialBitrate = maxInitialBitrate;
+      this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs;
+      this.maxDurationForQualityDecreaseMs = maxDurationForQualityDecreaseMs;
+      this.minDurationToRetainAfterDiscardMs = minDurationToRetainAfterDiscardMs;
+      this.bandwidthFraction = bandwidthFraction;
+    }
+
+    @Override
+    public AdaptiveVideoTrackSelection createTrackSelection(TrackGroup group, int... tracks) {
+      return new AdaptiveVideoTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate,
+          minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs,
+          minDurationToRetainAfterDiscardMs, bandwidthFraction);
+    }
+
+  }
+
+  public static final int DEFAULT_MAX_INITIAL_BITRATE = 800000;
+  public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;
+  public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000;
+  public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000;
+  public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f;
+
+  private final BandwidthMeter bandwidthMeter;
+  private final int maxInitialBitrate;
+  private final long minDurationForQualityIncreaseUs;
+  private final long maxDurationForQualityDecreaseUs;
+  private final long minDurationToRetainAfterDiscardUs;
+  private final float bandwidthFraction;
+
+  private int selectedIndex;
+  private int reason;
+
+  /**
+   * @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 bandwidthMeter Provides an estimate of the currently available bandwidth.
+   */
+  public AdaptiveVideoTrackSelection(TrackGroup group, int[] tracks,
+      BandwidthMeter bandwidthMeter) {
+    this (group, tracks, bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE,
+        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);
+  }
+
+  /**
+   * @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 bandwidthMeter Provides an estimate of the currently available bandwidth.
+   * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed when a
+   *     bandwidth estimate is unavailable.
+   * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the
+   *     selected track to switch to one of higher quality.
+   * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
+   *     selected track to switch to one of lower quality.
+   * @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 AdaptiveVideoTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter,
+      int maxInitialBitrate, long minDurationForQualityIncreaseMs,
+      long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs,
+      float bandwidthFraction) {
+    super(group, tracks);
+    this.bandwidthMeter = bandwidthMeter;
+    this.maxInitialBitrate = maxInitialBitrate;
+    this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
+    this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
+    this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
+    this.bandwidthFraction = bandwidthFraction;
+    selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE);
+    reason = C.SELECTION_REASON_INITIAL;
+  }
+
+  @Override
+  public void updateSelectedTrack(long bufferedDurationUs) {
+    long nowMs = SystemClock.elapsedRealtime();
+    // Get the current and ideal selections.
+    int currentSelectedIndex = selectedIndex;
+    Format currentFormat = getSelectedFormat();
+    int idealSelectedIndex = determineIdealSelectedIndex(nowMs);
+    Format idealFormat = getFormat(idealSelectedIndex);
+    // Assume we can switch to the ideal selection.
+    selectedIndex = idealSelectedIndex;
+    // Revert back to the current selection if conditions are not suitable for switching.
+    if (currentFormat != null && !isBlacklisted(selectedIndex, nowMs)) {
+      if (idealFormat.bitrate > currentFormat.bitrate
+          && bufferedDurationUs < minDurationForQualityIncreaseUs) {
+        // The ideal track is a higher quality, but we have insufficient buffer to safely switch
+        // up. Defer switching up for now.
+        selectedIndex = currentSelectedIndex;
+      } else if (idealFormat.bitrate < currentFormat.bitrate
+          && bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
+        // The ideal 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
+  public Object getSelectionData() {
+    return null;
+  }
+
+  @Override
+  public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
+    if (queue.isEmpty()) {
+      return 0;
+    }
+    int queueSize = queue.size();
+    long bufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs;
+    if (bufferedDurationUs < minDurationToRetainAfterDiscardUs) {
+      return queueSize;
+    }
+    int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime());
+    Format idealFormat = getFormat(idealSelectedIndex);
+    // 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);
+      long durationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs;
+      if (durationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs
+          && chunk.trackFormat.bitrate < idealFormat.bitrate
+          && chunk.trackFormat.height < idealFormat.height
+          && chunk.trackFormat.height < 720 && chunk.trackFormat.width < 1280) {
+        return i;
+      }
+    }
+    return queueSize;
+  }
+
+  /**
+   * Computes the ideal selected index ignoring buffer health.
+   *
+   * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}, or
+   *     {@link Long#MIN_VALUE} to ignore blacklisting.
+   */
+  private int determineIdealSelectedIndex(long nowMs) {
+    long bitrateEstimate = bandwidthMeter.getBitrateEstimate();
+    long effectiveBitrate = bitrateEstimate == BandwidthMeter.NO_ESTIMATE
+        ? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction);
+    int lowestBitrateNonBlacklistedIndex = 0;
+    for (int i = 0; i < length; i++) {
+      if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) {
+        Format format = getFormat(i);
+        if (format.bitrate <= effectiveBitrate) {
+          return i;
+        } else {
+          lowestBitrateNonBlacklistedIndex = i;
+        }
+      }
+    }
+    return lowestBitrateNonBlacklistedIndex;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.trackselection;
+
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.chunk.MediaChunk;
+import com.google.android.exoplayer2.util.Assertions;
+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 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
+  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 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], nowMs + blacklistDurationMs);
+    return true;
+  }
+
+  /**
+   * Returns whether the track at the specified index in the selection is blaclisted.
+   *
+   * @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;
+  }
+
+  @Override
+  public boolean equals(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;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
@@ -0,0 +1,834 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.trackselection;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.text.TextUtils;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.RendererCapabilities;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A {@link MappingTrackSelector} that allows configuration of common parameters. It is safe to call
+ * the methods of this class from the application thread. See {@link Parameters#Parameters()} for
+ * default selection parameters.
+ */
+public class DefaultTrackSelector extends MappingTrackSelector {
+
+  /**
+   * Holder for available configurations for the {@link DefaultTrackSelector}.
+   */
+  public static final class Parameters {
+
+    // Audio.
+    public final String preferredAudioLanguage;
+
+    // Text.
+    public final String preferredTextLanguage;
+
+    // Video.
+    public final boolean allowMixedMimeAdaptiveness;
+    public final boolean allowNonSeamlessAdaptiveness;
+    public final int maxVideoWidth;
+    public final int maxVideoHeight;
+    public final boolean exceedVideoConstraintsIfNecessary;
+    public final boolean exceedRendererCapabilitiesIfNecessary;
+    public final int viewportWidth;
+    public final int viewportHeight;
+    public final boolean orientationMayChange;
+
+    /**
+     * Constructor with default selection parameters:
+     * <ul>
+     *   <li>No preferred audio language is set.</li>
+     *   <li>No preferred text language is set.</li>
+     *   <li>Adaptation between different mime types is not allowed.</li>
+     *   <li>Non seamless adaptation is allowed.</li>
+     *   <li>No max limit for video width/height.</li>
+     *   <li>Video constraints are exceeded if no supported selection can be made otherwise.</li>
+     *   <li>Renderer capabilities are exceeded if no supported selection can be made.</li>
+     *   <li>No viewport width/height constraints are set.</li>
+     * </ul>
+     */
+    public Parameters() {
+      this(null, null, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true, true,
+          Integer.MAX_VALUE, Integer.MAX_VALUE, true);
+    }
+
+    /**
+     * @param preferredAudioLanguage The preferred language for audio, as well as for forced text
+     *     tracks as defined by RFC 5646. {@code null} to select the default track, or first track
+     *     if there's no default.
+     * @param preferredTextLanguage The preferred language for text tracks as defined by RFC 5646.
+     *     {@code null} to select the default track, or first track if there's no default.
+     * @param allowMixedMimeAdaptiveness Whether to allow selections to contain mixed mime types.
+     * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed.
+     * @param maxVideoWidth Maximum allowed video width.
+     * @param maxVideoHeight Maximum allowed video height.
+     * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no
+     *     selection can be made otherwise.
+     * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no
+     *     selection can be made otherwise.
+     * @param viewportWidth Viewport width in pixels.
+     * @param viewportHeight Viewport height in pixels.
+     * @param orientationMayChange Whether orientation may change during playback.
+     */
+    public Parameters(String preferredAudioLanguage, String preferredTextLanguage,
+        boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness,
+        int maxVideoWidth, int maxVideoHeight, boolean exceedVideoConstraintsIfNecessary,
+        boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight,
+        boolean orientationMayChange) {
+      this.preferredAudioLanguage = preferredAudioLanguage;
+      this.preferredTextLanguage = preferredTextLanguage;
+      this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness;
+      this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness;
+      this.maxVideoWidth = maxVideoWidth;
+      this.maxVideoHeight = maxVideoHeight;
+      this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary;
+      this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary;
+      this.viewportWidth = viewportWidth;
+      this.viewportHeight = viewportHeight;
+      this.orientationMayChange = orientationMayChange;
+    }
+
+    /**
+     * Returns a {@link Parameters} instance with the provided preferred language for audio and
+     * forced text tracks.
+     *
+     * @param preferredAudioLanguage The preferred language as defined by RFC 5646. {@code null} to
+     *     select the default track, or first track if there's no default.
+     * @return A {@link Parameters} instance with the provided preferred language for audio and
+     *     forced text tracks.
+     */
+    public Parameters withPreferredAudioLanguage(String preferredAudioLanguage) {
+      preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage);
+      if (TextUtils.equals(preferredAudioLanguage, this.preferredAudioLanguage)) {
+        return this;
+      }
+      return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+          allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+          exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth,
+          viewportHeight, orientationMayChange);
+    }
+
+    /**
+     * Returns a {@link Parameters} instance with the provided preferred language for text tracks.
+     *
+     * @param preferredTextLanguage The preferred language as defined by RFC 5646. {@code null} to
+     *     select the default track, or no track if there's no default.
+     * @return A {@link Parameters} instance with the provided preferred language for text tracks.
+     */
+    public Parameters withPreferredTextLanguage(String preferredTextLanguage) {
+      preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage);
+      if (TextUtils.equals(preferredTextLanguage, this.preferredTextLanguage)) {
+        return this;
+      }
+      return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+          allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+          exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth,
+          viewportHeight, orientationMayChange);
+    }
+
+    /**
+     * Returns a {@link Parameters} instance with the provided mixed mime adaptiveness allowance.
+     *
+     * @param allowMixedMimeAdaptiveness Whether to allow selections to contain mixed mime types.
+     * @return A {@link Parameters} instance with the provided mixed mime adaptiveness allowance.
+     */
+    public Parameters withAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) {
+      if (allowMixedMimeAdaptiveness == this.allowMixedMimeAdaptiveness) {
+        return this;
+      }
+      return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+          allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+          exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth,
+          viewportHeight, orientationMayChange);
+    }
+
+    /**
+     * Returns a {@link Parameters} instance with the provided seamless adaptiveness allowance.
+     *
+     * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed.
+     * @return A {@link Parameters} instance with the provided seamless adaptiveness allowance.
+     */
+    public Parameters withAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) {
+      if (allowNonSeamlessAdaptiveness == this.allowNonSeamlessAdaptiveness) {
+        return this;
+      }
+      return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+          allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+          exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth,
+          viewportHeight, orientationMayChange);
+    }
+
+    /**
+     * Returns a {@link Parameters} instance with the provided max video size.
+     *
+     * @param maxVideoWidth The max video width.
+     * @param maxVideoHeight The max video width.
+     * @return A {@link Parameters} instance with the provided max video size.
+     */
+    public Parameters withMaxVideoSize(int maxVideoWidth, int maxVideoHeight) {
+      if (maxVideoWidth == this.maxVideoWidth && maxVideoHeight == this.maxVideoHeight) {
+        return this;
+      }
+      return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+          allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+          exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth,
+          viewportHeight, orientationMayChange);
+    }
+
+    /**
+     * Equivalent to {@code withMaxVideoSize(1279, 719)}.
+     *
+     * @return A {@link Parameters} instance with maximum standard definition as maximum video size.
+     */
+    public Parameters withMaxVideoSizeSd() {
+      return withMaxVideoSize(1279, 719);
+    }
+
+    /**
+     * Equivalent to {@code withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE)}.
+     *
+     * @return A {@link Parameters} instance without video size constraints.
+     */
+    public Parameters withoutVideoSizeConstraints() {
+      return withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE);
+    }
+
+    /**
+     * Returns a {@link Parameters} instance with the provided
+     * {@code exceedVideoConstraintsIfNecessary} value.
+     *
+     * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no
+     *     selection can be made otherwise.
+     * @return A {@link Parameters} instance with the provided
+     *     {@code exceedVideoConstraintsIfNecessary} value.
+     */
+    public Parameters withExceedVideoConstraintsIfNecessary(
+        boolean exceedVideoConstraintsIfNecessary) {
+      if (exceedVideoConstraintsIfNecessary == this.exceedVideoConstraintsIfNecessary) {
+        return this;
+      }
+      return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+          allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+          exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth,
+          viewportHeight, orientationMayChange);
+    }
+
+    /**
+     * Returns a {@link Parameters} instance with the provided
+     * {@code exceedRendererCapabilitiesIfNecessary} value.
+     *
+     * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no
+     *     selection can be made otherwise.
+     * @return A {@link Parameters} instance with the provided
+     *     {@code exceedRendererCapabilitiesIfNecessary} value.
+     */
+    public Parameters withExceedRendererCapabilitiesIfNecessary(
+        boolean exceedRendererCapabilitiesIfNecessary) {
+      if (exceedRendererCapabilitiesIfNecessary == this.exceedRendererCapabilitiesIfNecessary) {
+        return this;
+      }
+      return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+          allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+          exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth,
+          viewportHeight, orientationMayChange);
+    }
+
+    /**
+     * Returns a {@link Parameters} instance with the provided viewport size.
+     *
+     * @param viewportWidth Viewport width in pixels.
+     * @param viewportHeight Viewport height in pixels.
+     * @param orientationMayChange Whether orientation may change during playback.
+     * @return A {@link Parameters} instance with the provided viewport size.
+     */
+    public Parameters withViewportSize(int viewportWidth, int viewportHeight,
+        boolean orientationMayChange) {
+      if (viewportWidth == this.viewportWidth && viewportHeight == this.viewportHeight
+          && orientationMayChange == this.orientationMayChange) {
+        return this;
+      }
+      return new Parameters(preferredAudioLanguage, preferredTextLanguage,
+          allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight,
+          exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth,
+          viewportHeight, orientationMayChange);
+    }
+
+    /**
+     * Returns a {@link Parameters} instance where the viewport size is obtained from the provided
+     * {@link Context}.
+     *
+     * @param context The context to obtain the viewport size from.
+     * @param orientationMayChange Whether orientation may change during playback.
+     * @return A {@link Parameters} instance where the viewport size is obtained from the provided
+     *     {@link Context}.
+     */
+    public Parameters withViewportSizeFromContext(Context context, boolean orientationMayChange) {
+      // Assume the viewport is fullscreen.
+      Point viewportSize = Util.getPhysicalDisplaySize(context);
+      return withViewportSize(viewportSize.x, viewportSize.y, orientationMayChange);
+    }
+
+    /**
+     * Equivalent to {@code withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true)}.
+     *
+     * @return A {@link Parameters} instance without viewport size constraints.
+     */
+    public Parameters withoutViewportSizeConstraints() {
+      return withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (obj == null || getClass() != obj.getClass()) {
+        return false;
+      }
+      Parameters other = (Parameters) obj;
+      return allowMixedMimeAdaptiveness == other.allowMixedMimeAdaptiveness
+          && allowNonSeamlessAdaptiveness == other.allowNonSeamlessAdaptiveness
+          && maxVideoWidth == other.maxVideoWidth && maxVideoHeight == other.maxVideoHeight
+          && exceedVideoConstraintsIfNecessary == other.exceedVideoConstraintsIfNecessary
+          && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary
+          && orientationMayChange == other.orientationMayChange
+          && viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight
+          && TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage)
+          && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage);
+    }
+
+    @Override
+    public int hashCode() {
+      int result = preferredAudioLanguage.hashCode();
+      result = 31 * result + preferredTextLanguage.hashCode();
+      result = 31 * result + (allowMixedMimeAdaptiveness ? 1 : 0);
+      result = 31 * result + (allowNonSeamlessAdaptiveness ? 1 : 0);
+      result = 31 * result + maxVideoWidth;
+      result = 31 * result + maxVideoHeight;
+      result = 31 * result + (exceedVideoConstraintsIfNecessary ? 1 : 0);
+      result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0);
+      result = 31 * result + (orientationMayChange ? 1 : 0);
+      result = 31 * result + viewportWidth;
+      result = 31 * result + viewportHeight;
+      return result;
+    }
+
+  }
+
+  /**
+   * 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 adaptiveVideoTrackSelectionFactory;
+  private final AtomicReference<Parameters> paramsReference;
+
+  /**
+   * Constructs an instance that does not support adaptive video.
+   */
+  public DefaultTrackSelector() {
+    this(null);
+  }
+
+  /**
+   * Constructs an instance that uses a factory to create adaptive video track selections.
+   *
+   * @param adaptiveVideoTrackSelectionFactory A factory for adaptive video {@link TrackSelection}s,
+   *     or null if the selector should not support adaptive video.
+   */
+  public DefaultTrackSelector(TrackSelection.Factory adaptiveVideoTrackSelectionFactory) {
+    this.adaptiveVideoTrackSelectionFactory = adaptiveVideoTrackSelectionFactory;
+    paramsReference = new AtomicReference<>(new Parameters());
+  }
+
+  /**
+   * Atomically sets the provided parameters for track selection.
+   *
+   * @param params The parameters for track selection.
+   */
+  public void setParameters(Parameters params) {
+    Assertions.checkNotNull(params);
+    if (!paramsReference.getAndSet(params).equals(params)) {
+      invalidate();
+    }
+  }
+
+  /**
+   * Gets the current selection parameters.
+   *
+   * @return The current selection parameters.
+   */
+  public Parameters getParameters() {
+    return paramsReference.get();
+  }
+
+  // MappingTrackSelector implementation.
+
+  @Override
+  protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities,
+      TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports)
+      throws ExoPlaybackException {
+    // Make a track selection for each renderer.
+    TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCapabilities.length];
+    Parameters params = paramsReference.get();
+    for (int i = 0; i < rendererCapabilities.length; i++) {
+      switch (rendererCapabilities[i].getTrackType()) {
+        case C.TRACK_TYPE_VIDEO:
+          rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i],
+              rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth,
+              params.maxVideoHeight, params.allowNonSeamlessAdaptiveness,
+              params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight,
+              params.orientationMayChange, adaptiveVideoTrackSelectionFactory,
+              params.exceedVideoConstraintsIfNecessary,
+              params.exceedRendererCapabilitiesIfNecessary);
+          break;
+        case C.TRACK_TYPE_AUDIO:
+          rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i],
+              rendererFormatSupports[i], params.preferredAudioLanguage,
+              params.exceedRendererCapabilitiesIfNecessary);
+          break;
+        case C.TRACK_TYPE_TEXT:
+          rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i],
+              rendererFormatSupports[i], params.preferredTextLanguage,
+              params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary);
+          break;
+        default:
+          rendererTrackSelections[i] = selectOtherTrack(rendererCapabilities[i].getTrackType(),
+              rendererTrackGroupArrays[i], rendererFormatSupports[i],
+              params.exceedRendererCapabilitiesIfNecessary);
+          break;
+      }
+    }
+    return rendererTrackSelections;
+  }
+
+  // Video track selection implementation.
+
+  protected TrackSelection selectVideoTrack(RendererCapabilities rendererCapabilities,
+      TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight,
+      boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, int viewportWidth,
+      int viewportHeight, boolean orientationMayChange,
+      TrackSelection.Factory adaptiveVideoTrackSelectionFactory,
+      boolean exceedConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary)
+      throws ExoPlaybackException {
+    TrackSelection selection = null;
+    if (adaptiveVideoTrackSelectionFactory != null) {
+      selection = selectAdaptiveVideoTrack(rendererCapabilities, groups, formatSupport,
+          maxVideoWidth, maxVideoHeight, allowNonSeamlessAdaptiveness,
+          allowMixedMimeAdaptiveness, viewportWidth, viewportHeight,
+          orientationMayChange, adaptiveVideoTrackSelectionFactory);
+    }
+    if (selection == null) {
+      selection = selectFixedVideoTrack(groups, formatSupport, maxVideoWidth, maxVideoHeight,
+          viewportWidth, viewportHeight, orientationMayChange, exceedConstraintsIfNecessary,
+          exceedRendererCapabilitiesIfNecessary);
+    }
+    return selection;
+  }
+
+  private static TrackSelection selectAdaptiveVideoTrack(RendererCapabilities rendererCapabilities,
+      TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight,
+      boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, int viewportWidth,
+      int viewportHeight, boolean orientationMayChange,
+      TrackSelection.Factory adaptiveVideoTrackSelectionFactory) throws ExoPlaybackException {
+    int requiredAdaptiveSupport = allowNonSeamlessAdaptiveness
+        ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS)
+        : RendererCapabilities.ADAPTIVE_SEAMLESS;
+    boolean allowMixedMimeTypes = allowMixedMimeAdaptiveness
+        && (rendererCapabilities.supportsMixedMimeTypeAdaptation() & requiredAdaptiveSupport) != 0;
+    for (int i = 0; i < groups.length; i++) {
+      TrackGroup group = groups.get(i);
+      int[] adaptiveTracks = getAdaptiveTracksForGroup(group, formatSupport[i],
+          allowMixedMimeTypes, requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight,
+          viewportWidth, viewportHeight, orientationMayChange);
+      if (adaptiveTracks.length > 0) {
+        return adaptiveVideoTrackSelectionFactory.createTrackSelection(group, adaptiveTracks);
+      }
+    }
+    return null;
+  }
+
+  private static int[] getAdaptiveTracksForGroup(TrackGroup group, int[] formatSupport,
+      boolean allowMixedMimeTypes, int requiredAdaptiveSupport, int maxVideoWidth,
+      int maxVideoHeight, int viewportWidth, int viewportHeight, boolean orientationMayChange) {
+    if (group.length < 2) {
+      return NO_TRACKS;
+    }
+
+    List<Integer> selectedTrackIndices = getViewportFilteredTrackIndices(group, viewportWidth,
+        viewportHeight, orientationMayChange);
+    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<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.contains(sampleMimeType)) {
+          seenMimeTypes.add(sampleMimeType);
+          int countForMimeType = getAdaptiveTrackCountForMimeType(group, formatSupport,
+              requiredAdaptiveSupport, sampleMimeType, maxVideoWidth, maxVideoHeight,
+              selectedTrackIndices);
+          if (countForMimeType > selectedMimeTypeTrackCount) {
+            selectedMimeType = sampleMimeType;
+            selectedMimeTypeTrackCount = countForMimeType;
+          }
+        }
+      }
+    }
+
+    // Filter by the selected mime type.
+    filterAdaptiveTrackCountForMimeType(group, formatSupport, requiredAdaptiveSupport,
+        selectedMimeType, maxVideoWidth, maxVideoHeight, selectedTrackIndices);
+
+    return selectedTrackIndices.size() < 2 ? NO_TRACKS : Util.toArray(selectedTrackIndices);
+  }
+
+  private static int getAdaptiveTrackCountForMimeType(TrackGroup group, int[] formatSupport,
+      int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight,
+      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)) {
+        adaptiveTrackCount++;
+      }
+    }
+    return adaptiveTrackCount;
+  }
+
+  private static void filterAdaptiveTrackCountForMimeType(TrackGroup group, int[] formatSupport,
+      int requiredAdaptiveSupport, String mimeType, int maxVideoWidth, int maxVideoHeight,
+      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)) {
+        selectedTrackIndices.remove(i);
+      }
+    }
+  }
+
+  private static boolean isSupportedAdaptiveVideoTrack(Format format, String mimeType,
+      int formatSupport, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight) {
+    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);
+  }
+
+  private static TrackSelection selectFixedVideoTrack(TrackGroupArray groups,
+      int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, int viewportWidth,
+      int viewportHeight, boolean orientationMayChange, boolean exceedConstraintsIfNecessary,
+      boolean exceedRendererCapabilitiesIfNecessary) {
+    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,
+          viewportWidth, viewportHeight, orientationMayChange);
+      int[] trackFormatSupport = formatSupport[groupIndex];
+      for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+        if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) {
+          Format format = trackGroup.getFormat(trackIndex);
+          boolean isWithinConstraints = selectedTrackIndices.contains(trackIndex)
+              && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth)
+              && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight);
+          if (!isWithinConstraints && !exceedConstraintsIfNecessary) {
+            // Track should not be selected.
+            continue;
+          }
+          int trackScore = isWithinConstraints ? 2 : 1;
+          if (isSupported(trackFormatSupport[trackIndex], false)) {
+            trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
+          }
+          boolean selectTrack = trackScore > selectedTrackScore;
+          if (trackScore == selectedTrackScore) {
+            // Use the pixel count as a tie breaker (or bitrate if pixel counts are tied). If we're
+            // within constraints prefer a higher pixel count (or bitrate), else prefer a lower
+            // count (or bitrate). If still tied then prefer the first track (i.e. the one that's
+            // already selected).
+            int comparisonResult;
+            int formatPixelCount = format.getPixelCount();
+            if (formatPixelCount != selectedPixelCount) {
+              comparisonResult = compareFormatValues(format.getPixelCount(), selectedPixelCount);
+            } else {
+              comparisonResult = compareFormatValues(format.bitrate, selectedBitrate);
+            }
+            selectTrack = isWithinConstraints ? comparisonResult > 0 : comparisonResult < 0;
+          }
+          if (selectTrack) {
+            selectedGroup = trackGroup;
+            selectedTrackIndex = trackIndex;
+            selectedTrackScore = trackScore;
+            selectedBitrate = format.bitrate;
+            selectedPixelCount = format.getPixelCount();
+          }
+        }
+      }
+    }
+    return selectedGroup == null ? null
+        : new FixedTrackSelection(selectedGroup, selectedTrackIndex);
+  }
+
+  /**
+   * Compares two format values for order. A known value is considered greater than
+   * {@link Format#NO_VALUE}.
+   *
+   * @param first The first value.
+   * @param second The second value.
+   * @return A negative integer if the first value is less than the second. Zero if they are equal.
+   *     A positive integer if the first value is greater than the second.
+   */
+  private static int compareFormatValues(int first, int second) {
+    return first == Format.NO_VALUE ? (second == Format.NO_VALUE ? 0 : -1)
+        : (second == Format.NO_VALUE ? 1 : (first - second));
+  }
+
+  // Audio track selection implementation.
+
+  protected TrackSelection selectAudioTrack(TrackGroupArray groups, int[][] formatSupport,
+      String preferredAudioLanguage, boolean exceedRendererCapabilitiesIfNecessary) {
+    TrackGroup selectedGroup = null;
+    int selectedTrackIndex = 0;
+    int selectedTrackScore = 0;
+    for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
+      TrackGroup trackGroup = groups.get(groupIndex);
+      int[] trackFormatSupport = formatSupport[groupIndex];
+      for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+        if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) {
+          Format format = trackGroup.getFormat(trackIndex);
+          boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
+          int trackScore;
+          if (formatHasLanguage(format, preferredAudioLanguage)) {
+            if (isDefault) {
+              trackScore = 4;
+            } else {
+              trackScore = 3;
+            }
+          } else if (isDefault) {
+            trackScore = 2;
+          } else {
+            trackScore = 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 FixedTrackSelection(selectedGroup, selectedTrackIndex);
+  }
+
+  // Text track selection implementation.
+
+  protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport,
+      String preferredTextLanguage, String preferredAudioLanguage,
+      boolean exceedRendererCapabilitiesIfNecessary) {
+    TrackGroup selectedGroup = null;
+    int selectedTrackIndex = 0;
+    int selectedTrackScore = 0;
+    for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
+      TrackGroup trackGroup = groups.get(groupIndex);
+      int[] trackFormatSupport = formatSupport[groupIndex];
+      for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+        if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) {
+          Format format = trackGroup.getFormat(trackIndex);
+          boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
+          boolean isForced = (format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0;
+          int trackScore;
+          if (formatHasLanguage(format, preferredTextLanguage)) {
+            if (isDefault) {
+              trackScore = 6;
+            } else if (!isForced) {
+              // Prefer non-forced to forced if a preferred text language has been specified. Where
+              // both are provided the non-forced track will usually contain the forced subtitles as
+              // a subset.
+              trackScore = 5;
+            } else {
+              trackScore = 4;
+            }
+          } else if (isDefault) {
+            trackScore = 3;
+          } else if (isForced) {
+            if (formatHasLanguage(format, preferredAudioLanguage)) {
+              trackScore = 2;
+            } else {
+              trackScore = 1;
+            }
+          } else {
+            // Track should not be selected.
+            continue;
+          }
+          if (isSupported(trackFormatSupport[trackIndex], false)) {
+            trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
+          }
+          if (trackScore > selectedTrackScore) {
+            selectedGroup = trackGroup;
+            selectedTrackIndex = trackIndex;
+            selectedTrackScore = trackScore;
+          }
+        }
+      }
+    }
+    return selectedGroup == null ? null
+        : new FixedTrackSelection(selectedGroup, selectedTrackIndex);
+  }
+
+  // General track selection methods.
+
+  protected TrackSelection selectOtherTrack(int trackType, TrackGroupArray groups,
+      int[][] formatSupport, boolean exceedRendererCapabilitiesIfNecessary) {
+    TrackGroup selectedGroup = null;
+    int selectedTrackIndex = 0;
+    int selectedTrackScore = 0;
+    for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
+      TrackGroup trackGroup = groups.get(groupIndex);
+      int[] trackFormatSupport = formatSupport[groupIndex];
+      for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+        if (isSupported(trackFormatSupport[trackIndex], 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 FixedTrackSelection(selectedGroup, selectedTrackIndex);
+  }
+
+  protected static boolean isSupported(int formatSupport, boolean allowExceedsCapabilities) {
+    int maskedSupport = formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK;
+    return maskedSupport == RendererCapabilities.FORMAT_HANDLED || (allowExceedsCapabilities
+        && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES);
+  }
+
+  protected static boolean formatHasLanguage(Format format, String language) {
+    return language != null && language.equals(Util.normalizeLanguageCode(format.language));
+  }
+
+  // Viewport size util methods.
+
+  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);
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/FixedTrackSelection.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 com.google.android.exoplayer2.trackselection;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * A {@link TrackSelection} consisting of a single track.
+ */
+public final class FixedTrackSelection extends BaseTrackSelection {
+
+  /**
+   * Factory for {@link FixedTrackSelection} instances.
+   */
+  public static final class Factory implements TrackSelection.Factory {
+
+    private final int reason;
+    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, Object data) {
+      this.reason = reason;
+      this.data = data;
+    }
+
+    @Override
+    public FixedTrackSelection createTrackSelection(TrackGroup group, int... tracks) {
+      Assertions.checkArgument(tracks.length == 1);
+      return new FixedTrackSelection(group, tracks[0], reason, data);
+    }
+
+  }
+
+  private final int reason;
+  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, Object data) {
+    super(group, track);
+    this.reason = reason;
+    this.data = data;
+  }
+
+  @Override
+  public void updateSelectedTrack(long bufferedDurationUs) {
+    // Do nothing.
+  }
+
+  @Override
+  public int getSelectedIndex() {
+    return 0;
+  }
+
+  @Override
+  public int getSelectionReason() {
+    return reason;
+  }
+
+  @Override
+  public Object getSelectionData() {
+    return data;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java
@@ -0,0 +1,733 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.trackselection;
+
+import android.content.Context;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.RendererCapabilities;
+import com.google.android.exoplayer2.RendererConfiguration;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s
+ * and renderers, and then from that mapping create a {@link TrackSelection} for each renderer.
+ */
+public abstract class MappingTrackSelector extends TrackSelector {
+
+  /**
+   * A track selection override.
+   */
+  public static final class SelectionOverride {
+
+    public final TrackSelection.Factory factory;
+    public final int groupIndex;
+    public final int[] tracks;
+    public final int length;
+
+    /**
+     * @param factory A factory for creating selections from this override.
+     * @param groupIndex The overriding group index.
+     * @param tracks The overriding track indices within the group.
+     */
+    public SelectionOverride(TrackSelection.Factory factory, int groupIndex, int... tracks) {
+      this.factory = factory;
+      this.groupIndex = groupIndex;
+      this.tracks = tracks;
+      this.length = tracks.length;
+    }
+
+    /**
+     * Creates an selection from this override.
+     *
+     * @param groups The groups whose selection is being overridden.
+     * @return The selection.
+     */
+    public TrackSelection createTrackSelection(TrackGroupArray groups) {
+      return factory.createTrackSelection(groups.get(groupIndex), tracks);
+    }
+
+    /**
+     * 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;
+    }
+
+  }
+
+  private final SparseArray<Map<TrackGroupArray, SelectionOverride>> selectionOverrides;
+  private final SparseBooleanArray rendererDisabledFlags;
+  private int tunnelingAudioSessionId;
+
+  private MappedTrackInfo currentMappedTrackInfo;
+
+  public MappingTrackSelector() {
+    selectionOverrides = new SparseArray<>();
+    rendererDisabledFlags = new SparseBooleanArray();
+    tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET;
+  }
+
+  /**
+   * Returns the mapping information associated with the current track selections, or null if no
+   * selection is currently active.
+   */
+  public final MappedTrackInfo getCurrentMappedTrackInfo() {
+    return currentMappedTrackInfo;
+  }
+
+  /**
+   * Sets whether the renderer at the specified index is disabled.
+   *
+   * @param rendererIndex The renderer index.
+   * @param disabled Whether the renderer is disabled.
+   */
+  public final void setRendererDisabled(int rendererIndex, boolean disabled) {
+    if (rendererDisabledFlags.get(rendererIndex) == disabled) {
+      // The disabled flag is unchanged.
+      return;
+    }
+    rendererDisabledFlags.put(rendererIndex, disabled);
+    invalidate();
+  }
+
+  /**
+   * Returns whether the renderer is disabled.
+   *
+   * @param rendererIndex The renderer index.
+   * @return Whether the renderer is disabled.
+   */
+  public final boolean getRendererDisabled(int rendererIndex) {
+    return rendererDisabledFlags.get(rendererIndex);
+  }
+
+  /**
+   * Overrides the track selection for the renderer at a specified index.
+   * <p>
+   * When the {@link TrackGroupArray} available to the renderer at the specified index matches the
+   * one provided, the override is applied. When the {@link TrackGroupArray} does not match, the
+   * override has no effect. The override replaces any previous override for the renderer and the
+   * provided {@link TrackGroupArray}.
+   * <p>
+   * Passing a {@code null} override will explicitly disable the renderer. To remove overrides use
+   * {@link #clearSelectionOverride(int, TrackGroupArray)}, {@link #clearSelectionOverrides(int)}
+   * or {@link #clearSelectionOverrides()}.
+   *
+   * @param rendererIndex The renderer index.
+   * @param groups The {@link TrackGroupArray} for which the override should be applied.
+   * @param override The override.
+   */
+  public final void setSelectionOverride(int rendererIndex, TrackGroupArray groups,
+      SelectionOverride override) {
+    Map<TrackGroupArray, 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;
+    }
+    overrides.put(groups, override);
+    invalidate();
+  }
+
+  /**
+   * Returns whether there is an override for the specified renderer and {@link TrackGroupArray}.
+   *
+   * @param rendererIndex The renderer index.
+   * @param groups The {@link TrackGroupArray}.
+   * @return Whether there is an override.
+   */
+  public final boolean hasSelectionOverride(int rendererIndex, TrackGroupArray groups) {
+    Map<TrackGroupArray, 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.
+   */
+  public final SelectionOverride getSelectionOverride(int rendererIndex, TrackGroupArray groups) {
+    Map<TrackGroupArray, SelectionOverride> overrides = selectionOverrides.get(rendererIndex);
+    return overrides != null ? overrides.get(groups) : null;
+  }
+
+  /**
+   * Clears a track selection override for the specified renderer and {@link TrackGroupArray}.
+   *
+   * @param rendererIndex The renderer index.
+   * @param groups The {@link TrackGroupArray} for which the override should be cleared.
+   */
+  public final void clearSelectionOverride(int rendererIndex, TrackGroupArray groups) {
+    Map<TrackGroupArray, SelectionOverride> overrides = selectionOverrides.get(rendererIndex);
+    if (overrides == null || !overrides.containsKey(groups)) {
+      // Nothing to clear.
+      return;
+    }
+    overrides.remove(groups);
+    if (overrides.isEmpty()) {
+      selectionOverrides.remove(rendererIndex);
+    }
+    invalidate();
+  }
+
+  /**
+   * Clears all track selection override for the specified renderer.
+   *
+   * @param rendererIndex The renderer index.
+   */
+  public final void clearSelectionOverrides(int rendererIndex) {
+    Map<TrackGroupArray, ?> overrides = selectionOverrides.get(rendererIndex);
+    if (overrides == null || overrides.isEmpty()) {
+      // Nothing to clear.
+      return;
+    }
+    selectionOverrides.remove(rendererIndex);
+    invalidate();
+  }
+
+  /**
+   * Clears all track selection overrides.
+   */
+  public final void clearSelectionOverrides() {
+    if (selectionOverrides.size() == 0) {
+      // Nothing to clear.
+      return;
+    }
+    selectionOverrides.clear();
+    invalidate();
+  }
+
+  /**
+   * Enables or disables tunneling. To enable tunneling, pass an audio session id to use when in
+   * tunneling mode. Session ids can be generated using
+   * {@link C#generateAudioSessionIdV21(Context)}. To disable tunneling pass
+   * {@link C#AUDIO_SESSION_ID_UNSET}. Tunneling will only be activated if it's both enabled and
+   * supported by the audio and video renderers for the selected tracks.
+   *
+   * @param tunnelingAudioSessionId The audio session id to use when tunneling, or
+   *     {@link C#AUDIO_SESSION_ID_UNSET} to disable tunneling.
+   */
+  public void setTunnelingAudioSessionId(int tunnelingAudioSessionId) {
+    if (this.tunnelingAudioSessionId != tunnelingAudioSessionId) {
+      this.tunnelingAudioSessionId = tunnelingAudioSessionId;
+      invalidate();
+    }
+  }
+
+  // TrackSelector implementation.
+
+  @Override
+  public final TrackSelectorResult selectTracks(RendererCapabilities[] rendererCapabilities,
+      TrackGroupArray trackGroups) throws ExoPlaybackException {
+    // Structures into which data will be written during the selection. The extra item at the end
+    // of each array is to store data associated with track groups that cannot be associated with
+    // any renderer.
+    int[] rendererTrackGroupCounts = new int[rendererCapabilities.length + 1];
+    TrackGroup[][] rendererTrackGroups = new TrackGroup[rendererCapabilities.length + 1][];
+    int[][][] rendererFormatSupports = new int[rendererCapabilities.length + 1][][];
+    for (int i = 0; i < rendererTrackGroups.length; i++) {
+      rendererTrackGroups[i] = new TrackGroup[trackGroups.length];
+      rendererFormatSupports[i] = new int[trackGroups.length][];
+    }
+
+    // Determine the extent to which each renderer supports mixed mimeType adaptation.
+    int[] mixedMimeTypeAdaptationSupport = getMixedMimeTypeAdaptationSupport(rendererCapabilities);
+
+    // Associate each track group to a preferred renderer, and evaluate the support that the
+    // renderer provides for each track in the group.
+    for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
+      TrackGroup group = trackGroups.get(groupIndex);
+      // Associate the group to a preferred renderer.
+      int rendererIndex = findRenderer(rendererCapabilities, group);
+      // Evaluate the support that the renderer provides for each track in the group.
+      int[] rendererFormatSupport = rendererIndex == rendererCapabilities.length
+          ? new int[group.length] : getFormatSupport(rendererCapabilities[rendererIndex], group);
+      // 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(
+          Arrays.copyOf(rendererTrackGroups[i], rendererTrackGroupCount));
+      rendererFormatSupports[i] = Arrays.copyOf(rendererFormatSupports[i], rendererTrackGroupCount);
+      rendererTrackTypes[i] = rendererCapabilities[i].getTrackType();
+    }
+
+    // Create a track group array for track groups not associated with a renderer.
+    int unassociatedTrackGroupCount = rendererTrackGroupCounts[rendererCapabilities.length];
+    TrackGroupArray unassociatedTrackGroupArray = new TrackGroupArray(Arrays.copyOf(
+        rendererTrackGroups[rendererCapabilities.length], unassociatedTrackGroupCount));
+
+    TrackSelection[] trackSelections = selectTracks(rendererCapabilities, rendererTrackGroupArrays,
+        rendererFormatSupports);
+
+    // Apply track disabling and overriding.
+    for (int i = 0; i < rendererCapabilities.length; i++) {
+      if (rendererDisabledFlags.get(i)) {
+        trackSelections[i] = null;
+      } else {
+        TrackGroupArray rendererTrackGroup = rendererTrackGroupArrays[i];
+        Map<TrackGroupArray, SelectionOverride> overrides = selectionOverrides.get(i);
+        SelectionOverride override = overrides == null ? null : overrides.get(rendererTrackGroup);
+        if (override != null) {
+          trackSelections[i] = override.createTrackSelection(rendererTrackGroup);
+        }
+      }
+    }
+
+    // Package up the track information and selections.
+    MappedTrackInfo mappedTrackInfo = new MappedTrackInfo(rendererTrackTypes,
+        rendererTrackGroupArrays, mixedMimeTypeAdaptationSupport, rendererFormatSupports,
+        unassociatedTrackGroupArray);
+
+    // Initialize the renderer configurations to the default configuration for all renderers with
+    // selections, and null otherwise.
+    RendererConfiguration[] rendererConfigurations =
+        new RendererConfiguration[rendererCapabilities.length];
+    for (int i = 0; i < rendererCapabilities.length; i++) {
+      rendererConfigurations[i] = trackSelections[i] != null ? RendererConfiguration.DEFAULT : null;
+    }
+    // Configure audio and video renderers to use tunneling if appropriate.
+    maybeConfigureRenderersForTunneling(rendererCapabilities, rendererTrackGroupArrays,
+        rendererFormatSupports, rendererConfigurations, trackSelections, tunnelingAudioSessionId);
+
+    return new TrackSelectorResult(trackGroups, new TrackSelectionArray(trackSelections),
+        mappedTrackInfo, rendererConfigurations);
+  }
+
+  @Override
+  public final void onSelectionActivated(Object info) {
+    currentMappedTrackInfo = (MappedTrackInfo) info;
+  }
+
+  /**
+   * Given an array of renderers and a set of {@link TrackGroup}s mapped to each of them, provides a
+   * {@link TrackSelection} per renderer.
+   *
+   * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which
+   *     {@link TrackSelection}s are to be generated.
+   * @param rendererTrackGroupArrays An array of {@link TrackGroupArray}s where each entry
+   *     corresponds to the renderer of equal index in {@code renderers}.
+   * @param rendererFormatSupports Maps every available track to a specific level of support as
+   *     defined by the renderer {@code FORMAT_*} constants.
+   * @throws ExoPlaybackException If an error occurs while selecting the tracks.
+   */
+  protected abstract TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities,
+      TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports)
+      throws ExoPlaybackException;
+
+  /**
+   * Finds the renderer to which the provided {@link TrackGroup} should be associated.
+   * <p>
+   * A {@link TrackGroup} is associated to a renderer that reports
+   * {@link RendererCapabilities#FORMAT_HANDLED} support for one or more of the tracks in the group,
+   * or {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} if no such renderer exists, or
+   * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} if again no such renderer exists. In
+   * the case that two or more renderers report the same level of support, the renderer with the
+   * lowest index is associated.
+   * <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 no association
+   * was made.
+   *
+   * @param rendererCapabilities The {@link RendererCapabilities} of the renderers.
+   * @param group The {@link TrackGroup} whose associated renderer is to be found.
+   * @return The index of the associated renderer, or {@code renderers.length} if no
+   *     association was made.
+   * @throws ExoPlaybackException If an error occurs finding a renderer.
+   */
+  private static int findRenderer(RendererCapabilities[] rendererCapabilities, TrackGroup group)
+      throws ExoPlaybackException {
+    int bestRendererIndex = rendererCapabilities.length;
+    int bestFormatSupportLevel = RendererCapabilities.FORMAT_UNSUPPORTED_TYPE;
+    for (int rendererIndex = 0; rendererIndex < rendererCapabilities.length; rendererIndex++) {
+      RendererCapabilities rendererCapability = rendererCapabilities[rendererIndex];
+      for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
+        int formatSupportLevel = rendererCapability.supportsFormat(group.getFormat(trackIndex))
+            & RendererCapabilities.FORMAT_SUPPORT_MASK;
+        if (formatSupportLevel > bestFormatSupportLevel) {
+          bestRendererIndex = rendererIndex;
+          bestFormatSupportLevel = formatSupportLevel;
+          if (bestFormatSupportLevel == RendererCapabilities.FORMAT_HANDLED) {
+            // We can't do better.
+            return bestRendererIndex;
+          }
+        }
+      }
+    }
+    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 {@link TrackGroup} to evaluate.
+   * @return An array containing the result of calling
+   *     {@link RendererCapabilities#supportsFormat} for each track in the group.
+   * @throws ExoPlaybackException If an error occurs determining the format support.
+   */
+  private static int[] getFormatSupport(RendererCapabilities rendererCapabilities, TrackGroup group)
+      throws ExoPlaybackException {
+    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 result of calling
+   *     {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer.
+   * @throws ExoPlaybackException If an error occurs determining the adaptation support.
+   */
+  private static int[] getMixedMimeTypeAdaptationSupport(
+      RendererCapabilities[] rendererCapabilities) throws ExoPlaybackException {
+    int[] mixedMimeTypeAdaptationSupport = new int[rendererCapabilities.length];
+    for (int i = 0; i < mixedMimeTypeAdaptationSupport.length; i++) {
+      mixedMimeTypeAdaptationSupport[i] = rendererCapabilities[i].supportsMixedMimeTypeAdaptation();
+    }
+    return mixedMimeTypeAdaptationSupport;
+  }
+
+  /**
+   * Determines whether tunneling should be enabled, replacing {@link RendererConfiguration}s in
+   * {@code rendererConfigurations} with configurations that enable tunneling on the appropriate
+   * renderers if so.
+   *
+   * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which
+   *     {@link TrackSelection}s are to be generated.
+   * @param rendererTrackGroupArrays An array of {@link TrackGroupArray}s where each entry
+   *     corresponds to the renderer of equal index in {@code renderers}.
+   * @param rendererFormatSupports Maps every available track to a specific level of support as
+   *     defined by the renderer {@code FORMAT_*} constants.
+   * @param rendererConfigurations The renderer configurations. Configurations may be replaced with
+   *     ones that enable tunneling as a result of this call.
+   * @param trackSelections The renderer track selections.
+   * @param tunnelingAudioSessionId The audio session id to use when tunneling, or
+   *     {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled.
+   */
+  private static void maybeConfigureRenderersForTunneling(
+      RendererCapabilities[] rendererCapabilities, TrackGroupArray[] rendererTrackGroupArrays,
+      int[][][] rendererFormatSupports, RendererConfiguration[] rendererConfigurations,
+      TrackSelection[] trackSelections, int tunnelingAudioSessionId) {
+    if (tunnelingAudioSessionId == C.AUDIO_SESSION_ID_UNSET) {
+      return;
+    }
+    // Check whether we can enable tunneling. To enable tunneling we require exactly one audio and
+    // one video renderer to support tunneling and have a selection.
+    int tunnelingAudioRendererIndex = -1;
+    int tunnelingVideoRendererIndex = -1;
+    boolean enableTunneling = true;
+    for (int i = 0; i < rendererCapabilities.length; i++) {
+      int rendererType = rendererCapabilities[i].getTrackType();
+      TrackSelection trackSelection = trackSelections[i];
+      if ((rendererType == C.TRACK_TYPE_AUDIO || rendererType == C.TRACK_TYPE_VIDEO)
+          && trackSelection != null) {
+        if (rendererSupportsTunneling(rendererFormatSupports[i], rendererTrackGroupArrays[i],
+            trackSelection)) {
+          if (rendererType == C.TRACK_TYPE_AUDIO) {
+            if (tunnelingAudioRendererIndex != -1) {
+              enableTunneling = false;
+              break;
+            } else {
+              tunnelingAudioRendererIndex = i;
+            }
+          } else {
+            if (tunnelingVideoRendererIndex != -1) {
+              enableTunneling = false;
+              break;
+            } else {
+              tunnelingVideoRendererIndex = i;
+            }
+          }
+        }
+      }
+    }
+    enableTunneling &= tunnelingAudioRendererIndex != -1 && tunnelingVideoRendererIndex != -1;
+    if (enableTunneling) {
+      RendererConfiguration tunnelingRendererConfiguration =
+          new RendererConfiguration(tunnelingAudioSessionId);
+      rendererConfigurations[tunnelingAudioRendererIndex] = tunnelingRendererConfiguration;
+      rendererConfigurations[tunnelingVideoRendererIndex] = tunnelingRendererConfiguration;
+    }
+  }
+
+  /**
+   * Returns whether a renderer supports tunneling for a {@link TrackSelection}.
+   *
+   * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each
+   *     track, indexed by group index and track index (in that order).
+   * @param trackGroups The {@link TrackGroupArray}s for the renderer.
+   * @param selection The track selection.
+   * @return Whether the renderer supports tunneling for the {@link TrackSelection}.
+   */
+  private static boolean rendererSupportsTunneling(int[][] formatSupport,
+      TrackGroupArray trackGroups, TrackSelection selection) {
+    if (selection == null) {
+      return false;
+    }
+    int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup());
+    for (int i = 0; i < selection.length(); i++) {
+      int trackFormatSupport = formatSupport[trackGroupIndex][selection.getIndexInTrackGroup(i)];
+      if ((trackFormatSupport & RendererCapabilities.TUNNELING_SUPPORT_MASK)
+          != RendererCapabilities.TUNNELING_SUPPORTED) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Provides track information for each renderer.
+   */
+  public static final class MappedTrackInfo {
+
+    /**
+     * The renderer does not have any associated tracks.
+     */
+    public static final int RENDERER_SUPPORT_NO_TRACKS = 0;
+    /**
+     * The renderer has associated tracks, but all are of unsupported types.
+     */
+    public static final int RENDERER_SUPPORT_UNSUPPORTED_TRACKS = 1;
+    /**
+     * The renderer has associated tracks and at least one is of a supported type, but all of the
+     * tracks whose types are supported exceed the renderer's capabilities.
+     */
+    public static final int RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS = 2;
+    /**
+     * The renderer has associated tracks and can play at least one of them.
+     */
+    public static final int RENDERER_SUPPORT_PLAYABLE_TRACKS = 3;
+
+    /**
+     * The number of renderers to which tracks are mapped.
+     */
+    public final int length;
+
+    private final int[] rendererTrackTypes;
+    private final TrackGroupArray[] trackGroups;
+    private final int[] mixedMimeTypeAdaptiveSupport;
+    private final int[][][] formatSupport;
+    private final TrackGroupArray unassociatedTrackGroups;
+
+    /**
+     * @param rendererTrackTypes The track type supported by each renderer.
+     * @param trackGroups The {@link TrackGroupArray}s for each renderer.
+     * @param mixedMimeTypeAdaptiveSupport The result of
+     *     {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer.
+     * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each
+     *     track, indexed by renderer index, group index and track index (in that order).
+     * @param unassociatedTrackGroups Contains {@link TrackGroup}s not associated with any renderer.
+     */
+    /* package */ MappedTrackInfo(int[] rendererTrackTypes,
+        TrackGroupArray[] trackGroups, int[] mixedMimeTypeAdaptiveSupport,
+        int[][][] formatSupport, TrackGroupArray unassociatedTrackGroups) {
+      this.rendererTrackTypes = rendererTrackTypes;
+      this.trackGroups = trackGroups;
+      this.formatSupport = formatSupport;
+      this.mixedMimeTypeAdaptiveSupport = mixedMimeTypeAdaptiveSupport;
+      this.unassociatedTrackGroups = unassociatedTrackGroups;
+      this.length = trackGroups.length;
+    }
+
+    /**
+     * Returns the array of {@link TrackGroup}s associated to the renderer at a specified index.
+     *
+     * @param rendererIndex The renderer index.
+     * @return The corresponding {@link TrackGroup}s.
+     */
+    public TrackGroupArray getTrackGroups(int rendererIndex) {
+      return trackGroups[rendererIndex];
+    }
+
+    /**
+     * Returns the extent to which a renderer can support playback of the tracks associated to it.
+     *
+     * @param rendererIndex The renderer index.
+     * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS},
+     *     {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS},
+     *     {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}.
+     */
+    public int getRendererSupport(int rendererIndex) {
+      int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS;
+      int[][] rendererFormatSupport = formatSupport[rendererIndex];
+      for (int i = 0; i < rendererFormatSupport.length; i++) {
+        for (int j = 0; j < rendererFormatSupport[i].length; j++) {
+          int trackRendererSupport;
+          switch (rendererFormatSupport[i][j] & RendererCapabilities.FORMAT_SUPPORT_MASK) {
+            case RendererCapabilities.FORMAT_HANDLED:
+              return RENDERER_SUPPORT_PLAYABLE_TRACKS;
+            case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES:
+              trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS;
+              break;
+            default:
+              trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS;
+              break;
+          }
+          bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport);
+        }
+      }
+      return bestRendererSupport;
+    }
+
+    /**
+     * Returns the best level of support obtained from {@link #getRendererSupport(int)} for all
+     * renderers of the specified track type. If no renderers exist for the specified type then
+     * {@link #RENDERER_SUPPORT_NO_TRACKS} is returned.
+     *
+     * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants.
+     * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS},
+     *     {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS},
+     *     {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}.
+     */
+    public int getTrackTypeRendererSupport(int trackType) {
+      int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS;
+      for (int i = 0; i < length; i++) {
+        if (rendererTrackTypes[i] == trackType) {
+          bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i));
+        }
+      }
+      return bestRendererSupport;
+    }
+
+    /**
+     * Returns the extent to which the format of an individual track is supported by the renderer.
+     *
+     * @param rendererIndex The renderer index.
+     * @param groupIndex The index of the group to which the track belongs.
+     * @param trackIndex The index of the track within the group.
+     * @return One of {@link RendererCapabilities#FORMAT_HANDLED},
+     *     {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES},
+     *     {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and
+     *     {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}.
+     */
+    public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) {
+      return formatSupport[rendererIndex][groupIndex][trackIndex]
+          & RendererCapabilities.FORMAT_SUPPORT_MASK;
+    }
+
+    /**
+     * Returns the extent to which the renderer supports adaptation between supported tracks in a
+     * specified {@link TrackGroup}.
+     * <p>
+     * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns
+     * {@link RendererCapabilities#FORMAT_HANDLED} are always considered.
+     * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns
+     * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or
+     * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered.
+     * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns
+     * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} are considered only if
+     * {@code includeCapabilitiesExceededTracks} is set to {@code true}.
+     *
+     * @param rendererIndex The renderer index.
+     * @param groupIndex The index of the group.
+     * @param includeCapabilitiesExceededTracks True if formats that exceed the capabilities of the
+     *     renderer should be included when determining support. False otherwise.
+     * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS},
+     *     {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and
+     *     {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}.
+     */
+    public int getAdaptiveSupport(int rendererIndex, int groupIndex,
+        boolean includeCapabilitiesExceededTracks) {
+      int trackCount = trackGroups[rendererIndex].get(groupIndex).length;
+      // Iterate over the tracks in the group, recording the indices of those to consider.
+      int[] trackIndices = new int[trackCount];
+      int trackIndexCount = 0;
+      for (int i = 0; i < trackCount; i++) {
+        int fixedSupport = getTrackFormatSupport(rendererIndex, groupIndex, i);
+        if (fixedSupport == RendererCapabilities.FORMAT_HANDLED
+            || (includeCapabilitiesExceededTracks
+            && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) {
+          trackIndices[trackIndexCount++] = i;
+        }
+      }
+      trackIndices = Arrays.copyOf(trackIndices, trackIndexCount);
+      return getAdaptiveSupport(rendererIndex, groupIndex, trackIndices);
+    }
+
+    /**
+     * Returns the extent to which the renderer supports adaptation between specified tracks within
+     * a {@link TrackGroup}.
+     *
+     * @param rendererIndex The renderer index.
+     * @param groupIndex The index of the group.
+     * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS},
+     *     {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and
+     *     {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}.
+     */
+    public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) {
+      int handledTrackCount = 0;
+      int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS;
+      boolean multipleMimeTypes = false;
+      String firstSampleMimeType = null;
+      for (int i = 0; i < trackIndices.length; i++) {
+        int trackIndex = trackIndices[i];
+        String sampleMimeType = trackGroups[rendererIndex].get(groupIndex).getFormat(trackIndex)
+            .sampleMimeType;
+        if (handledTrackCount++ == 0) {
+          firstSampleMimeType = sampleMimeType;
+        } else {
+          multipleMimeTypes |= !Util.areEqual(firstSampleMimeType, sampleMimeType);
+        }
+        adaptiveSupport = Math.min(adaptiveSupport, formatSupport[rendererIndex][groupIndex][i]
+            & RendererCapabilities.ADAPTIVE_SUPPORT_MASK);
+      }
+      return multipleMimeTypes
+          ? Math.min(adaptiveSupport, mixedMimeTypeAdaptiveSupport[rendererIndex])
+          : adaptiveSupport;
+    }
+
+    /**
+     * Returns the {@link TrackGroup}s not associated with any renderer.
+     */
+    public TrackGroupArray getUnassociatedTrackGroups() {
+      return unassociatedTrackGroups;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/RandomTrackSelection.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 com.google.android.exoplayer2.trackselection;
+
+import android.os.SystemClock;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.source.TrackGroup;
+import java.util.Random;
+
+/**
+ * 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 RandomTrackSelection createTrackSelection(TrackGroup group, int... tracks) {
+      return new RandomTrackSelection(group, 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 bufferedDurationUs) {
+    // 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
+  public Object getSelectionData() {
+    return null;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelection.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.trackselection;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.chunk.MediaChunk;
+import java.util.List;
+
+/**
+ * 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 as a result of calling {@link #updateSelectedTrack(long)}.
+ */
+public interface TrackSelection {
+
+  /**
+   * Factory for {@link TrackSelection} instances.
+   */
+  interface Factory {
+
+    /**
+     * Creates a new selection.
+     *
+     * @param group The {@link TrackGroup}. Must not be null.
+     * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
+     *     null or empty. May be in any order.
+     * @return The created selection.
+     */
+    TrackSelection createTrackSelection(TrackGroup group, int... tracks);
+
+  }
+
+  /**
+   * 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.
+   *
+   * @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.
+   */
+  Object getSelectionData();
+
+  // Adaptation.
+
+  /**
+   * Updates the selected track.
+   *
+   * @param bufferedDurationUs The duration of media currently buffered in microseconds.
+   */
+  void updateSelectedTrack(long bufferedDurationUs);
+
+  /**
+   * 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.
+   *
+   * @param playbackPositionUs The current playback position in microseconds.
+   * @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)} 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)}.
+   *
+   * @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);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectionArray.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.trackselection;
+
+import java.util.Arrays;
+
+/**
+ * The result of a {@link TrackSelector} operation.
+ */
+public final class TrackSelectionArray {
+
+  /**
+   * The number of selections in the result. Greater than or equal to zero.
+   */
+  public final int length;
+
+  private final TrackSelection[] trackSelections;
+
+  // Lazily initialized hashcode.
+  private int hashCode;
+
+  /**
+   * @param trackSelections The selections. Must not be null, but may contain null elements.
+   */
+  public TrackSelectionArray(TrackSelection... trackSelections) {
+    this.trackSelections = trackSelections;
+    this.length = trackSelections.length;
+  }
+
+  /**
+   * Returns the selection at a given index.
+   *
+   * @param index The index of the selection.
+   * @return The selection.
+   */
+  public TrackSelection get(int index) {
+    return trackSelections[index];
+  }
+
+  /**
+   * Returns the selections in a newly allocated array.
+   */
+  public 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(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);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelector.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 com.google.android.exoplayer2.trackselection;
+
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.RendererCapabilities;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+
+/** Selects tracks to be consumed by available renderers. */
+public abstract class TrackSelector {
+
+  /**
+   * Notified when previous selections by a {@link TrackSelector} are no longer valid.
+   */
+  public interface InvalidationListener {
+
+    /**
+     * Called by a {@link TrackSelector} when previous selections are no longer valid.
+     */
+    void onTrackSelectionsInvalidated();
+
+  }
+
+  private InvalidationListener listener;
+
+  /**
+   * Initializes the selector.
+   *
+   * @param listener A listener for the selector.
+   */
+  public final void init(InvalidationListener listener) {
+    this.listener = listener;
+  }
+
+  /**
+   * Performs a track selection for renderers.
+   *
+   * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks
+   *     are to be selected.
+   * @param trackGroups The available track groups.
+   * @return A {@link TrackSelectorResult} describing the track selections.
+   * @throws ExoPlaybackException If an error occurs selecting tracks.
+   */
+  public abstract TrackSelectorResult selectTracks(RendererCapabilities[] rendererCapabilities,
+      TrackGroupArray trackGroups) throws ExoPlaybackException;
+
+  /**
+   * Called when a {@link TrackSelectorResult} previously generated by
+   * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)} is activated.
+   *
+   * @param info The value of {@link TrackSelectorResult#info} in the activated result.
+   */
+  public abstract void onSelectionActivated(Object info);
+
+  /**
+   * Invalidates all previously generated track selections.
+   */
+  protected final void invalidate() {
+    if (listener != null) {
+      listener.onTrackSelectionsInvalidated();
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java
@@ -0,0 +1,98 @@
+/*
+ * 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 com.google.android.exoplayer2.trackselection;
+
+import com.google.android.exoplayer2.RendererConfiguration;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.util.Util;
+
+/**
+ * The result of a {@link TrackSelector} operation.
+ */
+public final class TrackSelectorResult {
+
+  /**
+   * The groups provided to the {@link TrackSelector}.
+   */
+  public final TrackGroupArray groups;
+  /**
+   * A {@link TrackSelectionArray} containing the 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;
+  /**
+   * A {@link RendererConfiguration} for each renderer, to be used with the selections.
+   */
+  public final RendererConfiguration[] rendererConfigurations;
+
+  /**
+   * @param groups The groups provided to the {@link TrackSelector}.
+   * @param selections A {@link TrackSelectionArray} containing the selection for each renderer.
+   * @param info An opaque object that will be returned to
+   *     {@link TrackSelector#onSelectionActivated(Object)} should the selections be activated.
+   * @param rendererConfigurations A {@link RendererConfiguration} for each renderer, to be used
+   *     with the selections.
+   */
+  public TrackSelectorResult(TrackGroupArray groups, TrackSelectionArray selections, Object info,
+      RendererConfiguration[] rendererConfigurations) {
+    this.groups = groups;
+    this.selections = selections;
+    this.info = info;
+    this.rendererConfigurations = rendererConfigurations;
+  }
+
+  /**
+   * Returns whether this result is equivalent to {@code other} for all renderers.
+   *
+   * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false}
+   *     will be returned in all cases.
+   * @return Whether this result is equivalent to {@code other} for all renderers.
+   */
+  public boolean isEquivalent(TrackSelectorResult other) {
+    if (other == null) {
+      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 in all cases.
+   * @param index The renderer index to check for equivalence.
+   * @return Whether this result is equivalent to {@code other} for all renderers.
+   */
+  public boolean isEquivalent(TrackSelectorResult other, int index) {
+    if (other == null) {
+      return false;
+    }
+    return Util.areEqual(selections.get(index), other.selections.get(index))
+        && Util.areEqual(rendererConfigurations[index], other.rendererConfigurations[index]);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/Allocation.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.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 #translateOffset(int)} method must be used when indexing into it.
+   */
+  public final byte[] data;
+
+  private final int offset;
+
+  /**
+   * @param data The array containing the allocated space.
+   * @param offset The offset of the allocated space within the array.
+   */
+  public Allocation(byte[] data, int offset) {
+    this.data = data;
+    this.offset = offset;
+  }
+
+  /**
+   * Translates a zero-based offset into the allocation to the corresponding {@link #data} offset.
+   *
+   * @param offset The zero-based offset to translate.
+   * @return The corresponding offset in {@link #data}.
+   */
+  public int translateOffset(int offset) {
+    return this.offset + offset;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 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();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/AssetDataSource.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A {@link DataSource} for reading from a local asset.
+ */
+public final class AssetDataSource implements DataSource {
+
+  /**
+   * 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;
+  private final TransferListener<? super AssetDataSource> listener;
+
+  private Uri uri;
+  private InputStream inputStream;
+  private long bytesRemaining;
+  private boolean opened;
+
+  /**
+   * @param context A context.
+   */
+  public AssetDataSource(Context context) {
+    this(context, null);
+  }
+
+  /**
+   * @param context A context.
+   * @param listener An optional listener.
+   */
+  public AssetDataSource(Context context, TransferListener<? super AssetDataSource> listener) {
+    this.assetManager = context.getAssets();
+    this.listener = listener;
+  }
+
+  @Override
+  public long open(DataSpec dataSpec) throws AssetDataSourceException {
+    try {
+      uri = dataSpec.uri;
+      String path = uri.getPath();
+      if (path.startsWith("/android_asset/")) {
+        path = path.substring(15);
+      } else if (path.startsWith("/")) {
+        path = path.substring(1);
+      }
+      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;
+    if (listener != null) {
+      listener.onTransferStart(this, 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 = 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;
+    }
+    if (listener != null) {
+      listener.onBytesTransferred(this, bytesRead);
+    }
+    return bytesRead;
+  }
+
+  @Override
+  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;
+        if (listener != null) {
+          listener.onTransferEnd(this);
+        }
+      }
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/BandwidthMeter.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+/**
+ * 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.
+     * <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 the bytes, in milliseconds.
+     * @param bytes The number of bytes transferred.
+     * @param bitrate The estimated bitrate in bits/sec, or {@link #NO_ESTIMATE} if an estimate is
+     *     not available.
+     */
+    void onBandwidthSample(int elapsedMs, long bytes, long bitrate);
+
+  }
+
+  /**
+   * Indicates no bandwidth estimate is available.
+   */
+  long NO_ESTIMATE = -1;
+
+  /**
+   * Returns the estimated bandwidth in bits/sec, or {@link #NO_ESTIMATE} if an estimate is not
+   * available.
+   */
+  long getBitrateEstimate();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSink.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * A {@link DataSink} for writing to a byte array.
+ */
+public final class ByteArrayDataSink implements DataSink {
+
+  private ByteArrayOutputStream stream;
+
+  @Override
+  public void open(DataSpec dataSpec) throws IOException {
+    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 {
+    stream.close();
+  }
+
+  @Override
+  public void write(byte[] buffer, int offset, int length) throws IOException {
+    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.
+   */
+  public byte[] getData() {
+    return stream == null ? null : stream.toByteArray();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/ByteArrayDataSource.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 com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * A {@link DataSource} for reading from a byte array.
+ */
+public final class ByteArrayDataSource implements DataSource {
+
+  private final byte[] data;
+
+  private Uri uri;
+  private int readPosition;
+  private int bytesRemaining;
+
+  /**
+   * @param data The data to be read.
+   */
+  public ByteArrayDataSource(byte[] data) {
+    Assertions.checkNotNull(data);
+    Assertions.checkArgument(data.length > 0);
+    this.data = data;
+  }
+
+  @Override
+  public long open(DataSpec dataSpec) throws IOException {
+    uri = dataSpec.uri;
+    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);
+    }
+    return bytesRemaining;
+  }
+
+  @Override
+  public int read(byte[] buffer, int offset, int readLength) throws IOException {
+    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;
+    return readLength;
+  }
+
+  @Override
+  public Uri getUri() {
+    return uri;
+  }
+
+  @Override
+  public void close() throws IOException {
+    uri = null;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/ContentDataSource.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 com.google.android.exoplayer2.upstream;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import java.io.EOFException;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A {@link DataSource} for reading from a content URI.
+ */
+public final class ContentDataSource implements DataSource {
+
+  /**
+   * 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;
+  private final TransferListener<? super ContentDataSource> listener;
+
+  private Uri uri;
+  private AssetFileDescriptor assetFileDescriptor;
+  private InputStream inputStream;
+  private long bytesRemaining;
+  private boolean opened;
+
+  /**
+   * @param context A context.
+   */
+  public ContentDataSource(Context context) {
+    this(context, null);
+  }
+
+  /**
+   * @param context A context.
+   * @param listener An optional listener.
+   */
+  public ContentDataSource(Context context, TransferListener<? super ContentDataSource> listener) {
+    this.resolver = context.getContentResolver();
+    this.listener = listener;
+  }
+
+  @Override
+  public long open(DataSpec dataSpec) throws ContentDataSourceException {
+    try {
+      uri = dataSpec.uri;
+      assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r");
+      inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());
+      long skipped = inputStream.skip(dataSpec.position);
+      if (skipped < dataSpec.position) {
+        // 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 {
+        bytesRemaining = inputStream.available();
+        if (bytesRemaining == 0) {
+          // FileInputStream.available() returns 0 if the remaining length cannot be determined, or
+          // if it's greater than Integer.MAX_VALUE. We don't know the true length in either case,
+          // so treat as unbounded.
+          bytesRemaining = C.LENGTH_UNSET;
+        }
+      }
+    } catch (IOException e) {
+      throw new ContentDataSourceException(e);
+    }
+
+    opened = true;
+    if (listener != null) {
+      listener.onTransferStart(this, 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 = 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;
+    }
+    if (listener != null) {
+      listener.onBytesTransferred(this, bytesRead);
+    }
+    return bytesRead;
+  }
+
+  @Override
+  public Uri getUri() {
+    return uri;
+  }
+
+  @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;
+          if (listener != null) {
+            listener.onTransferEnd(this);
+          }
+        }
+      }
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DataSink.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 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.
+   *
+   * @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.
+   *
+   * @throws IOException If an error occurs closing the sink.
+   */
+  void close() throws IOException;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DataSource.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 com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import java.io.IOException;
+
+/**
+ * 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();
+
+  }
+
+  /**
+   * 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 length} bytes of data and stores them into {@code buffer}, starting at
+   * index {@code offset}.
+   * <p>
+   * If {@code length} is zero then 0 is returned. Otherwise, if no data is available because the
+   * end of the opened range has been reached, then {@link C#RESULT_END_OF_INPUT} is returned.
+   * 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.
+   */
+  Uri getUri();
+
+  /**
+   * 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;
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DataSourceInputStream.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 com.google.android.exoplayer2.upstream;
+
+import com.google.android.exoplayer2.C;
+import 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(byte[] buffer) throws IOException {
+    return read(buffer, 0, buffer.length);
+  }
+
+  @Override
+  public int read(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;
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DataSpec.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * Defines a region of data.
+ */
+public final class DataSpec {
+
+  /**
+   * The flags that apply to any request for data.
+   */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef(flag = true, value = {FLAG_ALLOW_GZIP, FLAG_ALLOW_CACHING_UNKNOWN_LENGTH})
+  public @interface Flags {}
+  /**
+   * Permits 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 << 0;
+
+  /** Permits content to be cached even if its length can not be resolved. */
+  public static final int FLAG_ALLOW_CACHING_UNKNOWN_LENGTH = 1 << 1;
+
+  /**
+   * The source from which data should be read.
+   */
+  public final Uri uri;
+  /**
+   * Body for a POST request, null otherwise.
+   */
+  public final byte[] postBody;
+  /**
+   * 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 underyling 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
+   * {@link DataSpec} is not intended to be used in conjunction with a cache.
+   */
+  public final String key;
+  /**
+   * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and
+   * {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags.
+   */
+  @Flags
+  public final int flags;
+
+  /**
+   * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null.
+   *
+   * @param uri {@link #uri}.
+   */
+  public DataSpec(Uri uri) {
+    this(uri, 0);
+  }
+
+  /**
+   * Construct a {@link DataSpec} 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 {@link DataSpec} where {@link #position} equals {@link #absoluteStreamPosition}.
+   *
+   * @param uri {@link #uri}.
+   * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}.
+   * @param length {@link #length}.
+   * @param key {@link #key}.
+   */
+  public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key) {
+    this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, 0);
+  }
+
+  /**
+   * Construct a {@link DataSpec} 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, String key, @Flags int flags) {
+    this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags);
+  }
+
+  /**
+   * Construct a {@link DataSpec} 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, String key,
+      @Flags int flags) {
+    this(uri, null, absoluteStreamPosition, position, length, key, flags);
+  }
+
+  /**
+   * Construct a {@link DataSpec} where {@link #position} may differ from
+   * {@link #absoluteStreamPosition}.
+   *
+   * @param uri {@link #uri}.
+   * @param postBody {@link #postBody}.
+   * @param absoluteStreamPosition {@link #absoluteStreamPosition}.
+   * @param position {@link #position}.
+   * @param length {@link #length}.
+   * @param key {@link #key}.
+   * @param flags {@link #flags}.
+   */
+  public DataSpec(Uri uri, byte[] postBody, long absoluteStreamPosition, long position, long length,
+      String key, @Flags int flags) {
+    Assertions.checkArgument(absoluteStreamPosition >= 0);
+    Assertions.checkArgument(position >= 0);
+    Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET);
+    this.uri = uri;
+    this.postBody = postBody;
+    this.absoluteStreamPosition = absoluteStreamPosition;
+    this.position = position;
+    this.length = length;
+    this.key = key;
+    this.flags = flags;
+  }
+
+  /**
+   * 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[" + uri + ", " + Arrays.toString(postBody) + ", " + absoluteStreamPosition
+        + ", "  + position + ", " + length + ", " + key + ", " + flags + "]";
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DefaultAllocator.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import com.google.android.exoplayer2.util.Assertions;
+import 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) {
+      // Weak sanity check that the allocation probably originated from this pool.
+      Assertions.checkArgument(allocation.data == initialAllocationBlock
+          || allocation.data.length == individualAllocationSize);
+      availableAllocations[availableCount++] = allocation;
+    }
+    allocatedCount -= allocations.length;
+    // 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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DefaultBandwidthMeter.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 com.google.android.exoplayer2.upstream;
+
+import android.os.Handler;
+import android.os.SystemClock;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.SlidingPercentile;
+
+/**
+ * Estimates bandwidth by listening to data transfers. The bandwidth estimate is calculated using
+ * a {@link SlidingPercentile} and is updated each time a transfer ends.
+ */
+public final class DefaultBandwidthMeter implements BandwidthMeter, TransferListener<Object> {
+
+  /**
+   * The default maximum weight for the sliding window.
+   */
+  public static final int DEFAULT_MAX_WEIGHT = 2000;
+
+  private static final int ELAPSED_MILLIS_FOR_ESTIMATE = 2000;
+  private static final int BYTES_TRANSFERRED_FOR_ESTIMATE = 512 * 1024;
+
+  private final Handler eventHandler;
+  private final EventListener eventListener;
+  private final SlidingPercentile slidingPercentile;
+
+  private int streamCount;
+  private long sampleStartTimeMs;
+  private long sampleBytesTransferred;
+
+  private long totalElapsedTimeMs;
+  private long totalBytesTransferred;
+  private long bitrateEstimate;
+
+  public DefaultBandwidthMeter() {
+    this(null, null);
+  }
+
+  public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener) {
+    this(eventHandler, eventListener, DEFAULT_MAX_WEIGHT);
+  }
+
+  public DefaultBandwidthMeter(Handler eventHandler, EventListener eventListener, int maxWeight) {
+    this.eventHandler = eventHandler;
+    this.eventListener = eventListener;
+    this.slidingPercentile = new SlidingPercentile(maxWeight);
+    bitrateEstimate = NO_ESTIMATE;
+  }
+
+  @Override
+  public synchronized long getBitrateEstimate() {
+    return bitrateEstimate;
+  }
+
+  @Override
+  public synchronized void onTransferStart(Object source, DataSpec dataSpec) {
+    if (streamCount == 0) {
+      sampleStartTimeMs = SystemClock.elapsedRealtime();
+    }
+    streamCount++;
+  }
+
+  @Override
+  public synchronized void onBytesTransferred(Object source, int bytes) {
+    sampleBytesTransferred += bytes;
+  }
+
+  @Override
+  public synchronized void onTransferEnd(Object source) {
+    Assertions.checkState(streamCount > 0);
+    long nowMs = SystemClock.elapsedRealtime();
+    int sampleElapsedTimeMs = (int) (nowMs - sampleStartTimeMs);
+    totalElapsedTimeMs += sampleElapsedTimeMs;
+    totalBytesTransferred += sampleBytesTransferred;
+    if (sampleElapsedTimeMs > 0) {
+      float bitsPerSecond = (sampleBytesTransferred * 8000) / sampleElapsedTimeMs;
+      slidingPercentile.addSample((int) Math.sqrt(sampleBytesTransferred), bitsPerSecond);
+      if (totalElapsedTimeMs >= ELAPSED_MILLIS_FOR_ESTIMATE
+          || totalBytesTransferred >= BYTES_TRANSFERRED_FOR_ESTIMATE) {
+        float bitrateEstimateFloat = slidingPercentile.getPercentile(0.5f);
+        bitrateEstimate = Float.isNaN(bitrateEstimateFloat) ? NO_ESTIMATE
+            : (long) bitrateEstimateFloat;
+      }
+    }
+    notifyBandwidthSample(sampleElapsedTimeMs, sampleBytesTransferred, bitrateEstimate);
+    if (--streamCount > 0) {
+      sampleStartTimeMs = nowMs;
+    }
+    sampleBytesTransferred = 0;
+  }
+
+  private void notifyBandwidthSample(final int elapsedMs, final long bytes, final long bitrate) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable()  {
+        @Override
+        public void run() {
+          eventListener.onBandwidthSample(elapsedMs, bytes, bitrate);
+        }
+      });
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSource.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 com.google.android.exoplayer2.upstream;
+
+import android.content.Context;
+import android.net.Uri;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+
+/**
+ * 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>content: For fetching data from a content URI (e.g. content://authority/path/123).
+ * <li>http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4), if
+ *     constructed using {@link #DefaultDataSource(Context, TransferListener, String, boolean)}, or
+ *     any other schemes supported by a base data source if constructed using
+ *     {@link #DefaultDataSource(Context, TransferListener, DataSource)}.
+ * </ul>
+ */
+public final class DefaultDataSource implements DataSource {
+
+  private static final String SCHEME_ASSET = "asset";
+  private static final String SCHEME_CONTENT = "content";
+
+  private final DataSource baseDataSource;
+  private final DataSource fileDataSource;
+  private final DataSource assetDataSource;
+  private final DataSource contentDataSource;
+
+  private DataSource dataSource;
+
+  /**
+   * Constructs a new instance, optionally configured to follow cross-protocol redirects.
+   *
+   * @param context A context.
+   * @param listener An optional listener.
+   * @param userAgent The User-Agent string that should be used when requesting remote data.
+   * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
+   *     to HTTPS and vice versa) are enabled when fetching remote data.
+   */
+  public DefaultDataSource(Context context, TransferListener<? super DataSource> listener,
+      String userAgent, boolean allowCrossProtocolRedirects) {
+    this(context, listener, userAgent, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
+        DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects);
+  }
+
+  /**
+   * Constructs a new instance, optionally configured to follow cross-protocol redirects.
+   *
+   * @param context A context.
+   * @param listener An optional listener.
+   * @param userAgent The User-Agent string that should be used when requesting remote data.
+   * @param 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, TransferListener<? super DataSource> listener,
+      String userAgent, int connectTimeoutMillis, int readTimeoutMillis,
+      boolean allowCrossProtocolRedirects) {
+    this(context, listener,
+        new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis,
+            readTimeoutMillis, allowCrossProtocolRedirects));
+  }
+
+  /**
+   * 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 listener An optional listener.
+   * @param baseDataSource A {@link DataSource} to use for URI schemes other than file, asset and
+   *     content. This {@link DataSource} should normally support at least http(s).
+   */
+  public DefaultDataSource(Context context, TransferListener<? super DataSource> listener,
+      DataSource baseDataSource) {
+    this.baseDataSource = Assertions.checkNotNull(baseDataSource);
+    this.fileDataSource = new FileDataSource(listener);
+    this.assetDataSource = new AssetDataSource(context, listener);
+    this.contentDataSource = new ContentDataSource(context, listener);
+  }
+
+  @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)) {
+      if (dataSpec.uri.getPath().startsWith("/android_asset/")) {
+        dataSource = assetDataSource;
+      } else {
+        dataSource = fileDataSource;
+      }
+    } else if (SCHEME_ASSET.equals(scheme)) {
+      dataSource = assetDataSource;
+    } else if (SCHEME_CONTENT.equals(scheme)) {
+      dataSource = contentDataSource;
+    } 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 dataSource.read(buffer, offset, readLength);
+  }
+
+  @Override
+  public Uri getUri() {
+    return dataSource == null ? null : dataSource.getUri();
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (dataSource != null) {
+      try {
+        dataSource.close();
+      } finally {
+        dataSource = null;
+      }
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.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 com.google.android.exoplayer2.upstream;
+
+import android.content.Context;
+import 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;
+  private final TransferListener<? super DataSource> 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, 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,
+      TransferListener<? super DataSource> listener) {
+    this(context, listener, new DefaultHttpDataSourceFactory(userAgent, listener));
+  }
+
+  /**
+   * @param context A context.
+   * @param listener An optional listener.
+   * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource}
+   *     for {@link DefaultDataSource}.
+   * @see DefaultDataSource#DefaultDataSource(Context, TransferListener, DataSource)
+   */
+  public DefaultDataSourceFactory(Context context, TransferListener<? super DataSource> listener,
+      DataSource.Factory baseDataSourceFactory) {
+    this.context = context.getApplicationContext();
+    this.listener = listener;
+    this.baseDataSourceFactory = baseDataSourceFactory;
+  }
+
+  @Override
+  public DefaultDataSource createDataSource() {
+    return new DefaultDataSource(context, listener, baseDataSourceFactory.createDataSource());
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java
@@ -0,0 +1,644 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Predicate;
+import 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.URL;
+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;
+
+/**
+ * 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, Predicate, TransferListener, int, int, boolean)}
+ * constructor and passing {@code true} as the final argument.
+ */
+public class DefaultHttpDataSource 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 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;
+  private final Predicate<String> contentTypePredicate;
+  private final HashMap<String, String> requestProperties;
+  private final TransferListener<? super DefaultHttpDataSource> listener;
+
+  private DataSpec dataSpec;
+  private HttpURLConnection connection;
+  private InputStream inputStream;
+  private boolean opened;
+
+  private long bytesToSkip;
+  private long bytesToRead;
+
+  private long bytesSkipped;
+  private long bytesRead;
+
+  /**
+   * @param userAgent The User-Agent string that should be used.
+   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+   *     predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from
+   *     {@link #open(DataSpec)}.
+   */
+  public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate) {
+    this(userAgent, contentTypePredicate, null);
+  }
+
+  /**
+   * @param userAgent The User-Agent string that should be used.
+   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+   *     predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from
+   *     {@link #open(DataSpec)}.
+   * @param listener An optional listener.
+   */
+  public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
+      TransferListener<? super DefaultHttpDataSource> listener) {
+    this(userAgent, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS,
+        DEFAULT_READ_TIMEOUT_MILLIS);
+  }
+
+  /**
+   * @param userAgent The User-Agent string that should be used.
+   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+   *     predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from
+   *     {@link #open(DataSpec)}.
+   * @param listener An optional listener.
+   * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
+   *     interpreted as an infinite timeout.
+   * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted
+   *     as an infinite timeout.
+   */
+  public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
+      TransferListener<? super DefaultHttpDataSource> listener, int connectTimeoutMillis,
+      int readTimeoutMillis) {
+    this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, false);
+  }
+
+  /**
+   * @param userAgent The User-Agent string that should be used.
+   * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
+   *     predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from
+   *     {@link #open(DataSpec)}.
+   * @param listener An optional listener.
+   * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
+   *     interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use
+   *     the default value.
+   * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted
+   *     as an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value.
+   * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
+   *     to HTTPS and vice versa) are enabled.
+   */
+  public DefaultHttpDataSource(String userAgent, Predicate<String> contentTypePredicate,
+      TransferListener<? super DefaultHttpDataSource> listener, int connectTimeoutMillis,
+      int readTimeoutMillis, boolean allowCrossProtocolRedirects) {
+    this.userAgent = Assertions.checkNotEmpty(userAgent);
+    this.contentTypePredicate = contentTypePredicate;
+    this.listener = listener;
+    this.requestProperties = new HashMap<>();
+    this.connectTimeoutMillis = connectTimeoutMillis;
+    this.readTimeoutMillis = readTimeoutMillis;
+    this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
+  }
+
+  @Override
+  public Uri getUri() {
+    return connection == null ? null : Uri.parse(connection.getURL().toString());
+  }
+
+  @Override
+  public Map<String, List<String>> getResponseHeaders() {
+    return connection == null ? null : connection.getHeaderFields();
+  }
+
+  @Override
+  public void setRequestProperty(String name, String value) {
+    Assertions.checkNotNull(name);
+    Assertions.checkNotNull(value);
+    synchronized (requestProperties) {
+      requestProperties.put(name, value);
+    }
+  }
+
+  @Override
+  public void clearRequestProperty(String name) {
+    Assertions.checkNotNull(name);
+    synchronized (requestProperties) {
+      requestProperties.remove(name);
+    }
+  }
+
+  @Override
+  public void clearAllRequestProperties() {
+    synchronized (requestProperties) {
+      requestProperties.clear();
+    }
+  }
+
+  @Override
+  public long open(DataSpec dataSpec) throws HttpDataSourceException {
+    this.dataSpec = dataSpec;
+    this.bytesRead = 0;
+    this.bytesSkipped = 0;
+    try {
+      connection = makeConnection(dataSpec);
+    } catch (IOException e) {
+      throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
+          dataSpec, HttpDataSourceException.TYPE_OPEN);
+    }
+
+    int responseCode;
+    try {
+      responseCode = connection.getResponseCode();
+    } catch (IOException e) {
+      closeConnectionQuietly();
+      throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), 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, 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.
+    if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
+      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. Furthermore, there isn't a
+      // reliable way to determine whether the gzip was used or not. Always use the dataSpec length
+      // in this case.
+      bytesToRead = dataSpec.length;
+    }
+
+    try {
+      inputStream = connection.getInputStream();
+    } catch (IOException e) {
+      closeConnectionQuietly();
+      throw new HttpDataSourceException(e, dataSpec, HttpDataSourceException.TYPE_OPEN);
+    }
+
+    opened = true;
+    if (listener != null) {
+      listener.onTransferStart(this, 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;
+        if (listener != null) {
+          listener.onTransferEnd(this);
+        }
+      }
+    }
+  }
+
+  /**
+   * Returns the current connection, or null if the source is not currently opened.
+   *
+   * @return The current open connection, or null.
+   */
+  protected final 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 {
+    URL url = new URL(dataSpec.uri.toString());
+    byte[] postBody = dataSpec.postBody;
+    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, postBody, position, length, allowGzip, true /* followRedirects */);
+    }
+
+    // We need to handle redirects ourselves to allow cross-protocol redirects.
+    int redirectCount = 0;
+    while (redirectCount++ <= MAX_REDIRECTS) {
+      HttpURLConnection connection = makeConnection(
+          url, postBody, position, length, allowGzip, false /* followRedirects */);
+      int responseCode = connection.getResponseCode();
+      if (responseCode == HttpURLConnection.HTTP_MULT_CHOICE
+          || responseCode == HttpURLConnection.HTTP_MOVED_PERM
+          || responseCode == HttpURLConnection.HTTP_MOVED_TEMP
+          || responseCode == HttpURLConnection.HTTP_SEE_OTHER
+          || (postBody == null
+              && (responseCode == 307 /* HTTP_TEMP_REDIRECT */
+                  || responseCode == 308 /* HTTP_PERM_REDIRECT */))) {
+        // For 300, 301, 302, and 303 POST requests follow the redirect and are transformed into
+        // GET requests. For 307 and 308 POST requests are not redirected.
+        postBody = null;
+        String location = connection.getHeaderField("Location");
+        connection.disconnect();
+        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);
+  }
+
+  /**
+   * Configures a connection and opens it.
+   *
+   * @param url The url to connect to.
+   * @param postBody The body data for a POST request.
+   * @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.
+   */
+  private HttpURLConnection makeConnection(URL url, byte[] postBody, long position,
+      long length, boolean allowGzip, boolean followRedirects) throws IOException {
+    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+    connection.setConnectTimeout(connectTimeoutMillis);
+    connection.setReadTimeout(readTimeoutMillis);
+    synchronized (requestProperties) {
+      for (Map.Entry<String, String> property : requestProperties.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);
+    if (!allowGzip) {
+      connection.setRequestProperty("Accept-Encoding", "identity");
+    }
+    connection.setInstanceFollowRedirects(followRedirects);
+    connection.setDoOutput(postBody != null);
+    if (postBody != null) {
+      connection.setRequestMethod("POST");
+      if (postBody.length == 0) {
+        connection.connect();
+      } else  {
+        connection.setFixedLengthStreamingMode(postBody.length);
+        connection.connect();
+        OutputStream os = connection.getOutputStream();
+        os.write(postBody);
+        os.close();
+      }
+    } else {
+      connection.connect();
+    }
+    return connection;
+  }
+
+  /**
+   * 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.interrupted()) {
+        throw new InterruptedIOException();
+      }
+      if (read == -1) {
+        throw new EOFException();
+      }
+      bytesSkipped += read;
+      if (listener != null) {
+        listener.onBytesTransferred(this, 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;
+    if (listener != null) {
+      listener.onBytesTransferred(this, 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 (className.equals("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream")
+          || className.equals(
+          "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream")) {
+        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;
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.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 com.google.android.exoplayer2.upstream;
+
+import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
+import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
+
+/** A {@link Factory} that produces {@link DefaultHttpDataSource} instances. */
+public final class DefaultHttpDataSourceFactory extends BaseFactory {
+
+  private final String userAgent;
+  private final TransferListener<? super DataSource> 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, TransferListener<? super DataSource> 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 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,
+      TransferListener<? super DataSource> listener, int connectTimeoutMillis,
+      int readTimeoutMillis, boolean allowCrossProtocolRedirects) {
+    this.userAgent = userAgent;
+    this.listener = listener;
+    this.connectTimeoutMillis = connectTimeoutMillis;
+    this.readTimeoutMillis = readTimeoutMillis;
+    this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
+  }
+
+  @Override
+  protected DefaultHttpDataSource createDataSourceInternal() {
+    return new DefaultHttpDataSource(userAgent, null, listener, connectTimeoutMillis,
+        readTimeoutMillis, allowCrossProtocolRedirects);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/FileDataSource.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 com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+/**
+ * A {@link DataSource} for reading local files.
+ */
+public final class FileDataSource implements DataSource {
+
+  /**
+   * Thrown when IOException is encountered during local file read operation.
+   */
+  public static class FileDataSourceException extends IOException {
+
+    public FileDataSourceException(IOException cause) {
+      super(cause);
+    }
+
+  }
+
+  private final TransferListener<? super FileDataSource> listener;
+
+  private RandomAccessFile file;
+  private Uri uri;
+  private long bytesRemaining;
+  private boolean opened;
+
+  public FileDataSource() {
+    this(null);
+  }
+
+  /**
+   * @param listener An optional listener.
+   */
+  public FileDataSource(TransferListener<? super FileDataSource> listener) {
+    this.listener = listener;
+  }
+
+  @Override
+  public long open(DataSpec dataSpec) throws FileDataSourceException {
+    try {
+      uri = dataSpec.uri;
+      file = new RandomAccessFile(dataSpec.uri.getPath(), "r");
+      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;
+    if (listener != null) {
+      listener.onTransferStart(this, dataSpec);
+    }
+
+    return bytesRemaining;
+  }
+
+  @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 = file.read(buffer, offset, (int) Math.min(bytesRemaining, readLength));
+      } catch (IOException e) {
+        throw new FileDataSourceException(e);
+      }
+
+      if (bytesRead > 0) {
+        bytesRemaining -= bytesRead;
+        if (listener != null) {
+          listener.onBytesTransferred(this, bytesRead);
+        }
+      }
+
+      return bytesRead;
+    }
+  }
+
+  @Override
+  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;
+        if (listener != null) {
+          listener.onTransferEnd(this);
+        }
+      }
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.upstream;
+
+/**
+ * A {@link DataSource.Factory} that produces {@link FileDataSource}.
+ */
+public final class FileDataSourceFactory implements DataSource.Factory {
+
+  private final TransferListener<? super FileDataSource> listener;
+
+  public FileDataSourceFactory() {
+    this(null);
+  }
+
+  public FileDataSourceFactory(TransferListener<? super FileDataSource> listener) {
+    this.listener = listener;
+  }
+
+  @Override
+  public DataSource createDataSource() {
+    return new FileDataSource(listener);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/HttpDataSource.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.support.annotation.IntDef;
+import android.text.TextUtils;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Predicate;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+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();
+
+    /**
+     * Sets a default request header field for {@link HttpDataSource} instances subsequently
+     * created by the factory. Previously created instances are not affected.
+     *
+     * @param name The name of the header field.
+     * @param value The value of the field.
+     */
+    void setDefaultRequestProperty(String name, String value);
+
+    /**
+     * Clears a default request header field for {@link HttpDataSource} instances subsequently
+     * created by the factory. Previously created instances are not affected.
+     *
+     * @param name The name of the header field.
+     */
+    void clearDefaultRequestProperty(String name);
+
+    /**
+     * Clears all default request header fields for all {@link HttpDataSource} instances
+     * subsequently created by the factory.  Previously created instances are not affected.
+     */
+    void clearAllDefaultRequestProperties();
+
+  }
+
+  /**
+   * Base implementation of {@link Factory} that sets default request properties.
+   */
+  abstract class BaseFactory implements Factory {
+
+    private final HashMap<String, String> requestProperties;
+
+    public BaseFactory() {
+      requestProperties = new HashMap<>();
+    }
+
+    @Override
+    public final HttpDataSource createDataSource() {
+      HttpDataSource dataSource = createDataSourceInternal();
+      synchronized (requestProperties) {
+        for (Map.Entry<String, String> property : requestProperties.entrySet()) {
+          dataSource.setRequestProperty(property.getKey(), property.getValue());
+        }
+      }
+      return dataSource;
+    }
+
+    @Override
+    public final void setDefaultRequestProperty(String name, String value) {
+      Assertions.checkNotNull(name);
+      Assertions.checkNotNull(value);
+      synchronized (requestProperties) {
+        requestProperties.put(name, value);
+      }
+    }
+
+    @Override
+    public final void clearDefaultRequestProperty(String name) {
+      Assertions.checkNotNull(name);
+      synchronized (requestProperties) {
+        requestProperties.remove(name);
+      }
+    }
+
+    @Override
+    public final void clearAllDefaultRequestProperties() {
+      synchronized (requestProperties) {
+        requestProperties.clear();
+      }
+    }
+
+    /**
+     * Called by {@link #createDataSource()} to create a {@link HttpDataSource} instance without
+     * default request properties set. Default request properties will be set by
+     * {@link #createDataSource()} before the instance is returned.
+     *
+     * @return A {@link HttpDataSource} instance without default request properties set.
+     */
+    protected abstract HttpDataSource createDataSourceInternal();
+
+  }
+
+  /**
+   * A {@link Predicate} that rejects content types often used for pay-walls.
+   */
+  Predicate<String> REJECT_PAYWALL_TYPES = new Predicate<String>() {
+
+    @Override
+    public boolean evaluate(String contentType) {
+      contentType = Util.toLowerInvariant(contentType);
+      return !TextUtils.isEmpty(contentType)
+          && (!contentType.contains("text") || contentType.contains("text/vtt"))
+          && !contentType.contains("html") && !contentType.contains("xml");
+    }
+
+  };
+
+  /**
+   * Thrown when an error is encountered when trying to read from a {@link HttpDataSource}.
+   */
+  class HttpDataSourceException extends IOException {
+
+    @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;
+
+    /**
+     * An unmodifiable map of the response header fields and values.
+     */
+    public final Map<String, List<String>> headerFields;
+
+    public InvalidResponseCodeException(int responseCode, Map<String, List<String>> headerFields,
+        DataSpec dataSpec) {
+      super("Response code: " + responseCode, dataSpec, TYPE_OPEN);
+      this.responseCode = responseCode;
+      this.headerFields = headerFields;
+    }
+
+  }
+
+  @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 field. The value will be used for subsequent connections
+   * established by the source.
+   *
+   * @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 field. 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 header fields that were set by {@link #setRequestProperty(String, String)}.
+   */
+  void clearAllRequestProperties();
+
+  /**
+   * Returns the headers provided in the response, or {@code null} if response headers are
+   * unavailable.
+   */
+  Map<String, List<String>> getResponseHeaders();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/Loader.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.TraceUtil;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Manages the background loading of {@link Loadable}s.
+ */
+public final class Loader implements LoaderErrorThrower {
+
+  /**
+   * Thrown when an unexpected exception is encountered during loading.
+   */
+  public static final class UnexpectedLoaderException extends IOException {
+
+    public UnexpectedLoaderException(Exception 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();
+
+    /**
+     * Returns whether the load has been canceled.
+     */
+    boolean isLoadCanceled();
+
+    /**
+     * Performs the load, returning on completion or cancellation.
+     *
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    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 of the load.
+     */
+    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 of the load 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 of the load up to the point at which the error occurred.
+     * @param error The load error.
+     * @return The desired retry action. One of {@link Loader#RETRY},
+     *     {@link Loader#RETRY_RESET_ERROR_COUNT}, {@link Loader#DONT_RETRY} and
+     *     {@link Loader#DONT_RETRY_FATAL}.
+     */
+    int onLoadError(T loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error);
+
+  }
+
+  public static final int RETRY = 0;
+  public static final int RETRY_RESET_ERROR_COUNT = 1;
+  public static final int DONT_RETRY = 2;
+  public static final int DONT_RETRY_FATAL = 3;
+
+  private static final int MSG_START = 0;
+  private static final int MSG_CANCEL = 1;
+  private static final int MSG_END_OF_SOURCE = 2;
+  private static final int MSG_IO_EXCEPTION = 3;
+  private static final int MSG_FATAL_ERROR = 4;
+
+  private final ExecutorService downloadExecutorService;
+
+  private LoadTask<? extends Loadable> currentTask;
+  private IOException fatalError;
+
+  /**
+   * @param threadName A name for the loader's thread.
+   */
+  public Loader(String threadName) {
+    this.downloadExecutorService = Util.newSingleThreadExecutor(threadName);
+  }
+
+  /**
+   * 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 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 = Looper.myLooper();
+    Assertions.checkState(looper != null);
+    long startTimeMs = SystemClock.elapsedRealtime();
+    new LoadTask<>(looper, loadable, callback, defaultMinRetryCount, startTimeMs).start(0);
+    return startTimeMs;
+  }
+
+  /**
+   * Returns whether the {@link Loader} is currently loading a {@link Loadable}.
+   */
+  public boolean isLoading() {
+    return currentTask != null;
+  }
+
+  /**
+   * Cancels the current load. This method should only be called when a load is in progress.
+   */
+  public void cancelLoading() {
+    currentTask.cancel(false);
+  }
+
+  /**
+   * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer
+   * required.
+   */
+  public void release() {
+    release(null);
+  }
+
+  /**
+   * Releases the {@link Loader}, running {@code postLoadAction} on its thread. This method should
+   * be called when the {@link Loader} is no longer required.
+   *
+   * @param postLoadAction A {@link Runnable} to run on the loader's thread when
+   *     {@link Loadable#load()} is no longer running.
+   */
+  public void release(Runnable postLoadAction) {
+    if (currentTask != null) {
+      currentTask.cancel(true);
+    }
+    if (postLoadAction != null) {
+      downloadExecutorService.submit(postLoadAction);
+    }
+    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 final T loadable;
+    private final Loader.Callback<T> callback;
+    public final int defaultMinRetryCount;
+    private final long startTimeMs;
+
+    private IOException currentError;
+    private int errorCount;
+
+    private volatile Thread executorThread;
+    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 {
+        submitToExecutor();
+      }
+    }
+
+    public void cancel(boolean released) {
+      this.released = released;
+      currentError = null;
+      if (hasMessages(MSG_START)) {
+        removeMessages(MSG_START);
+        if (!released) {
+          sendEmptyMessage(MSG_CANCEL);
+        }
+      } else {
+        loadable.cancelLoad();
+        if (executorThread != null) {
+          executorThread.interrupt();
+        }
+      }
+      if (released) {
+        finish();
+        long nowMs = SystemClock.elapsedRealtime();
+        callback.onLoadCanceled(loadable, nowMs, nowMs - startTimeMs, true);
+      }
+    }
+
+    @Override
+    public void run() {
+      try {
+        executorThread = Thread.currentThread();
+        if (!loadable.isLoadCanceled()) {
+          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(loadable.isLoadCanceled());
+        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 (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) {
+        submitToExecutor();
+        return;
+      }
+      if (msg.what == MSG_FATAL_ERROR) {
+        throw (Error) msg.obj;
+      }
+      finish();
+      long nowMs = SystemClock.elapsedRealtime();
+      long durationMs = nowMs - startTimeMs;
+      if (loadable.isLoadCanceled()) {
+        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:
+          callback.onLoadCompleted(loadable, nowMs, durationMs);
+          break;
+        case MSG_IO_EXCEPTION:
+          currentError = (IOException) msg.obj;
+          int retryAction = callback.onLoadError(loadable, nowMs, durationMs, currentError);
+          if (retryAction == DONT_RETRY_FATAL) {
+            fatalError = currentError;
+          } else if (retryAction != DONT_RETRY) {
+            errorCount = retryAction == RETRY_RESET_ERROR_COUNT ? 1 : errorCount + 1;
+            start(getRetryDelayMillis());
+          }
+          break;
+      }
+    }
+
+    private void submitToExecutor() {
+      currentError = null;
+      downloadExecutorService.submit(currentTask);
+    }
+
+    private void finish() {
+      currentTask = null;
+    }
+
+    private long getRetryDelayMillis() {
+      return Math.min((errorCount - 1) * 1000, 5000);
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.upstream;
+
+import 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.
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/ParsingLoadable.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.upstream.Loader.Loadable;
+import com.google.android.exoplayer2.util.Util;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * 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;
+
+  }
+
+  /**
+   * 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 DataSource dataSource;
+  private final Parser<T> parser;
+
+  private volatile T result;
+  private volatile boolean isCanceled;
+  private volatile long bytesLoaded;
+
+  /**
+   * @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<T> parser) {
+    this.dataSource = dataSource;
+    this.dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP);
+    this.type = type;
+    this.parser = parser;
+  }
+
+  /**
+   * Returns the loaded object, or null if an object has not been loaded.
+   */
+  public final 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.
+   *
+   * @return The number of bytes loaded.
+   */
+  public long bytesLoaded() {
+    return bytesLoaded;
+  }
+
+  @Override
+  public final void cancelLoad() {
+    // We don't actually cancel anything, but we need to record the cancellation so that
+    // isLoadCanceled can return the correct value.
+    isCanceled = true;
+  }
+
+  @Override
+  public final boolean isLoadCanceled() {
+    return isCanceled;
+  }
+
+  @Override
+  public final void load() throws IOException, InterruptedException {
+    DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec);
+    try {
+      inputStream.open();
+      result = parser.parse(dataSource.getUri(), inputStream);
+    } finally {
+      bytesLoaded = inputStream.bytesRead();
+      Util.closeQuietly(inputStream);
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/PriorityDataSource.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.PriorityTaskManager;
+import java.io.IOException;
+
+/**
+ * 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 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
+  public Uri getUri() {
+    return upstream.getUri();
+  }
+
+  @Override
+  public void close() throws IOException {
+    upstream.close();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/RawResourceDataSource.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.text.TextUtils;
+import com.google.android.exoplayer2.C;
+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 implements DataSource {
+
+  /**
+   * 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);
+  }
+
+  private static final String RAW_RESOURCE_SCHEME = "rawresource";
+
+  private final Resources resources;
+  private final TransferListener<? super RawResourceDataSource> listener;
+
+  private Uri uri;
+  private AssetFileDescriptor assetFileDescriptor;
+  private InputStream inputStream;
+  private long bytesRemaining;
+  private boolean opened;
+
+  /**
+   * @param context A context.
+   */
+  public RawResourceDataSource(Context context) {
+    this(context, null);
+  }
+
+  /**
+   * @param context A context.
+   * @param listener An optional listener.
+   */
+  public RawResourceDataSource(Context context,
+      TransferListener<? super RawResourceDataSource> listener) {
+    this.resources = context.getResources();
+    this.listener = listener;
+  }
+
+  @Override
+  public long open(DataSpec dataSpec) throws RawResourceDataSourceException {
+    try {
+      uri = dataSpec.uri;
+      if (!TextUtils.equals(RAW_RESOURCE_SCHEME, uri.getScheme())) {
+        throw new RawResourceDataSourceException("URI must use scheme " + RAW_RESOURCE_SCHEME);
+      }
+
+      int resourceId;
+      try {
+        resourceId = Integer.parseInt(uri.getLastPathSegment());
+      } catch (NumberFormatException e) {
+        throw new RawResourceDataSourceException("Resource identifier must be an integer.");
+      }
+
+      assetFileDescriptor = resources.openRawResourceFd(resourceId);
+      inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor());
+      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;
+    if (listener != null) {
+      listener.onTransferStart(this, 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 = 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;
+    }
+    if (listener != null) {
+      listener.onBytesTransferred(this, bytesRead);
+    }
+    return bytesRead;
+  }
+
+  @Override
+  public Uri getUri() {
+    return uri;
+  }
+
+  @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;
+          if (listener != null) {
+            listener.onTransferEnd(this);
+          }
+        }
+      }
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/TeeDataSource.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+
+/**
+ * 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;
+
+  /**
+   * @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 long open(DataSpec dataSpec) throws IOException {
+    long dataLength = upstream.open(dataSpec);
+    if (dataSpec.length == C.LENGTH_UNSET && dataLength != C.LENGTH_UNSET) {
+      // Reconstruct dataSpec in order to provide the resolved length to the sink.
+      dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataSpec.position,
+          dataLength, dataSpec.key, dataSpec.flags);
+    }
+    dataSink.open(dataSpec);
+    return dataLength;
+  }
+
+  @Override
+  public int read(byte[] buffer, int offset, int max) throws IOException {
+    int num = upstream.read(buffer, offset, max);
+    if (num > 0) {
+      // TODO: Consider continuing even if disk writes fail.
+      dataSink.write(buffer, offset, num);
+    }
+    return num;
+  }
+
+  @Override
+  public Uri getUri() {
+    return upstream.getUri();
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      upstream.close();
+    } finally {
+      dataSink.close();
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/TransferListener.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 com.google.android.exoplayer2.upstream;
+
+/**
+ * A listener of data transfer events.
+ */
+public interface TransferListener<S> {
+
+  /**
+   * Called when a transfer starts.
+   *
+   * @param source The source performing the transfer.
+   * @param dataSpec Describes the data being transferred.
+   */
+  void onTransferStart(S source, DataSpec dataSpec);
+
+  /**
+   * Called incrementally during a transfer.
+   *
+   * @param source The source performing the transfer.
+   * @param bytesTransferred The number of bytes transferred since the previous call to this
+   *     method (or if the first call, since the transfer was started).
+   */
+  void onBytesTransferred(S source, int bytesTransferred);
+
+  /**
+   * Called when a transfer ends.
+   *
+   * @param source The source performing the transfer.
+   */
+  void onTransferEnd(S source);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/UdpDataSource.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream;
+
+import android.net.Uri;
+import 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 implements DataSource {
+
+  /**
+   * 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 DEAFULT_SOCKET_TIMEOUT_MILLIS = 8 * 1000;
+
+  private final TransferListener<? super UdpDataSource> listener;
+  private final int socketTimeoutMillis;
+  private final byte[] packetBuffer;
+  private final DatagramPacket packet;
+
+  private Uri uri;
+  private DatagramSocket socket;
+  private MulticastSocket multicastSocket;
+  private InetAddress address;
+  private InetSocketAddress socketAddress;
+  private boolean opened;
+
+  private int packetRemaining;
+
+  /**
+   * @param listener An optional listener.
+   */
+  public UdpDataSource(TransferListener<? super UdpDataSource> listener) {
+    this(listener, DEFAULT_MAX_PACKET_SIZE);
+  }
+
+  /**
+   * @param listener An optional listener.
+   * @param maxPacketSize The maximum datagram packet size, in bytes.
+   */
+  public UdpDataSource(TransferListener<? super UdpDataSource> listener, int maxPacketSize) {
+    this(listener, maxPacketSize, DEAFULT_SOCKET_TIMEOUT_MILLIS);
+  }
+
+  /**
+   * @param listener An optional listener.
+   * @param maxPacketSize The maximum datagram packet size, in bytes.
+   * @param socketTimeoutMillis The socket timeout in milliseconds. A timeout of zero is interpreted
+   *     as an infinite timeout.
+   */
+  public UdpDataSource(TransferListener<? super UdpDataSource> listener, int maxPacketSize,
+      int socketTimeoutMillis) {
+    this.listener = listener;
+    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();
+
+    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;
+    if (listener != null) {
+      listener.onTransferStart(this, 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();
+      if (listener != null) {
+        listener.onBytesTransferred(this, packetRemaining);
+      }
+    }
+
+    int packetOffset = packet.getLength() - packetRemaining;
+    int bytesToRead = Math.min(packetRemaining, readLength);
+    System.arraycopy(packetBuffer, packetOffset, buffer, offset, bytesToRead);
+    packetRemaining -= bytesToRead;
+    return bytesToRead;
+  }
+
+  @Override
+  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;
+      if (listener != null) {
+        listener.onTransferEnd(this);
+      }
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/Cache.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 com.google.android.exoplayer2.upstream.cache;
+
+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 accessed, causing it to be replaced. The new
+     * {@link CacheSpan} is guaranteed to represent the same data as the one it replaces, however
+     * {@link CacheSpan#file} and {@link CacheSpan#lastAccessTimestamp} may have changed.
+     * <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(IOException cause) {
+      super(cause);
+    }
+
+  }
+
+  /**
+   * 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. May be null if there are no such spans.
+   */
+  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)}. When the caller has finished writing, it must release
+   * the lock by calling {@link #releaseHoleSpan}.
+   *
+   * @param key The key of the data being requested.
+   * @param position The position of the data being requested.
+   * @return The {@link CacheSpan}.
+   * @throws InterruptedException
+   */
+  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}.
+   *
+   * @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.
+   */
+  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)}.
+   *
+   * @param key The cache key for the data.
+   * @param position The starting position of the data.
+   * @param maxLength The maximum length of the data to be written. Used only to ensure that there
+   *     is enough space in the cache.
+   * @return The file into which data should be written.
+   */
+  File startFile(String key, long position, long maxLength) 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)}
+   *
+   * @param file A newly written cache file.
+   */
+  void commitFile(File file) 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.
+   *
+   * @param span The {@link CacheSpan} to remove.
+   */
+  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);
+
+  /**
+   * Sets the content length for the given key.
+   *
+   * @param key The cache key for the data.
+   * @param length The length of the data.
+   */
+  void setContentLength(String key, long length) throws CacheException;
+
+  /**
+   * Returns the content length for the given key if one set, or {@link
+   * com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise.
+   *
+   * @param key The cache key for the data.
+   */
+  long getContentLength(String key);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream.cache;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.DataSink;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ReusableBufferedOutputStream;
+import 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.
+ */
+public final class CacheDataSink implements DataSink {
+
+  private final Cache cache;
+  private final long maxCacheFileSize;
+  private final int bufferSize;
+
+  private DataSpec dataSpec;
+  private File file;
+  private OutputStream outputStream;
+  private FileOutputStream underlyingFileOutputStream;
+  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);
+    }
+
+  }
+
+  /**
+   * @param cache The cache into which data should be written.
+   * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for
+   *    a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into
+   *    multiple cache files.
+   */
+  public CacheDataSink(Cache cache, long maxCacheFileSize) {
+    this(cache, maxCacheFileSize, 0);
+  }
+
+  /**
+   * @param cache The cache into which data should be written.
+   * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the sink is opened for
+   *    a {@link DataSpec} whose size exceeds this value, then the data will be fragmented into
+   *    multiple cache files.
+   * @param bufferSize The buffer size in bytes for writing to a cache file. A zero or negative
+   *    value disables buffering.
+   */
+  public CacheDataSink(Cache cache, long maxCacheFileSize, int bufferSize) {
+    this.cache = Assertions.checkNotNull(cache);
+    this.maxCacheFileSize = maxCacheFileSize;
+    this.bufferSize = bufferSize;
+  }
+
+  @Override
+  public void open(DataSpec dataSpec) throws CacheDataSinkException {
+    if (dataSpec.length == C.LENGTH_UNSET
+        && !dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)) {
+      this.dataSpec = null;
+      return;
+    }
+    this.dataSpec = dataSpec;
+    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 == maxCacheFileSize) {
+          closeCurrentOutputStream();
+          openNextOutputStream();
+        }
+        int bytesToWrite = (int) Math.min(length - bytesWritten,
+            maxCacheFileSize - 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 maxLength = dataSpec.length == C.LENGTH_UNSET ? maxCacheFileSize
+        : Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize);
+    file = cache.startFile(dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten,
+        maxLength);
+    underlyingFileOutputStream = new FileOutputStream(file);
+    if (bufferSize > 0) {
+      if (bufferedOutputStream == null) {
+        bufferedOutputStream = new ReusableBufferedOutputStream(underlyingFileOutputStream,
+            bufferSize);
+      } else {
+        bufferedOutputStream.reset(underlyingFileOutputStream);
+      }
+      outputStream = bufferedOutputStream;
+    } else {
+      outputStream = underlyingFileOutputStream;
+    }
+    outputStreamBytesWritten = 0;
+  }
+
+  @SuppressWarnings("ThrowFromFinallyBlock")
+  private void closeCurrentOutputStream() throws IOException {
+    if (outputStream == null) {
+      return;
+    }
+
+    boolean success = false;
+    try {
+      outputStream.flush();
+      underlyingFileOutputStream.getFD().sync();
+      success = true;
+    } finally {
+      Util.closeQuietly(outputStream);
+      outputStream = null;
+      File fileToCommit = file;
+      file = null;
+      if (success) {
+        cache.commitFile(fileToCommit);
+      } else {
+        fileToCommit.delete();
+      }
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.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 com.google.android.exoplayer2.upstream.cache;
+
+import 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 maxCacheFileSize;
+
+  /**
+   * @see CacheDataSink#CacheDataSink(Cache, long)
+   */
+  public CacheDataSinkFactory(Cache cache, long maxCacheFileSize) {
+    this.cache = cache;
+    this.maxCacheFileSize = maxCacheFileSize;
+  }
+
+  @Override
+  public DataSink createDataSink() {
+    return new CacheDataSink(cache, maxCacheFileSize);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream.cache;
+
+import android.net.Uri;
+import android.support.annotation.IntDef;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.DataSink;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSourceException;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.FileDataSource;
+import com.google.android.exoplayer2.upstream.TeeDataSource;
+import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * 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 {
+
+  /**
+   * Default maximum single cache file size.
+   *
+   * @see #CacheDataSource(Cache, DataSource, int)
+   * @see #CacheDataSource(Cache, DataSource, int, long)
+   */
+  public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024;
+
+  /**
+   * Flags controlling the cache's behavior.
+   */
+  @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 this flag is
+   * set, then we will read from upstream if the cache key is locked.
+   */
+  public static final int FLAG_BLOCK_ON_CACHE = 1 << 0;
+
+  /**
+   * 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;
+
+  /**
+   * A flag indicating that the cache should be bypassed for requests whose lengths are unset.
+   */
+  public static final int FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS = 1 << 2;
+
+  /**
+   * 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);
+
+  }
+
+  private final Cache cache;
+  private final DataSource cacheReadDataSource;
+  private final DataSource cacheWriteDataSource;
+  private final DataSource upstreamDataSource;
+  private final EventListener eventListener;
+
+  private final boolean blockOnCache;
+  private final boolean ignoreCacheOnError;
+  private final boolean ignoreCacheForUnsetLengthRequests;
+
+  private DataSource currentDataSource;
+  private boolean currentRequestUnbounded;
+  private Uri uri;
+  private int flags;
+  private String key;
+  private long readPosition;
+  private long bytesRemaining;
+  private CacheSpan lockedSpan;
+  private boolean seenCacheError;
+  private boolean currentRequestIgnoresCache;
+  private long totalCachedBytesRead;
+
+  /**
+   * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
+   * reading and writing the cache and with {@link #DEFAULT_MAX_CACHE_FILE_SIZE}.
+   */
+  public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags) {
+    this(cache, upstream, flags, DEFAULT_MAX_CACHE_FILE_SIZE);
+  }
+
+  /**
+   * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
+   * reading and writing the cache. The sink is configured to fragment data such that no single
+   * cache file is greater than maxCacheFileSize bytes.
+   *
+   * @param cache The cache.
+   * @param upstream A {@link DataSource} for reading data not in the cache.
+   * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link
+   *     #FLAG_IGNORE_CACHE_ON_ERROR} or 0.
+   * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the cached data size
+   *     exceeds this value, then the data will be fragmented into multiple cache files. The
+   *     finer-grained this is the finer-grained the eviction policy can be.
+   */
+  public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags,
+      long maxCacheFileSize) {
+    this(cache, upstream, new FileDataSource(), new CacheDataSink(cache, maxCacheFileSize),
+        flags, 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.
+   * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link
+   *     #FLAG_IGNORE_CACHE_ON_ERROR} or 0.
+   * @param eventListener An optional {@link EventListener} to receive events.
+   */
+  public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource,
+      DataSink cacheWriteDataSink, @Flags int flags, EventListener eventListener) {
+    this.cache = cache;
+    this.cacheReadDataSource = cacheReadDataSource;
+    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 long open(DataSpec dataSpec) throws IOException {
+    try {
+      uri = dataSpec.uri;
+      flags = dataSpec.flags;
+      key = dataSpec.key != null ? dataSpec.key : uri.toString();
+      readPosition = dataSpec.position;
+      currentRequestIgnoresCache = (ignoreCacheOnError && seenCacheError)
+          || (dataSpec.length == C.LENGTH_UNSET && ignoreCacheForUnsetLengthRequests);
+      if (dataSpec.length != C.LENGTH_UNSET || currentRequestIgnoresCache) {
+        bytesRemaining = dataSpec.length;
+      } else {
+        bytesRemaining = cache.getContentLength(key);
+        if (bytesRemaining != C.LENGTH_UNSET) {
+          bytesRemaining -= dataSpec.position;
+        }
+      }
+      openNextSource(true);
+      return bytesRemaining;
+    } catch (IOException 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 {
+      int bytesRead = currentDataSource.read(buffer, offset, readLength);
+      if (bytesRead >= 0) {
+        if (currentDataSource == cacheReadDataSource) {
+          totalCachedBytesRead += bytesRead;
+        }
+        readPosition += bytesRead;
+        if (bytesRemaining != C.LENGTH_UNSET) {
+          bytesRemaining -= bytesRead;
+        }
+      } else {
+        if (currentRequestUnbounded) {
+          // We only do unbounded requests to upstream and only when we don't know the actual stream
+          // length. So we reached the end of stream.
+          setContentLength(readPosition);
+          bytesRemaining = 0;
+        }
+        closeCurrentSource();
+        if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
+          if (openNextSource(false)) {
+            return read(buffer, offset, readLength);
+          }
+        }
+      }
+      return bytesRead;
+    } catch (IOException e) {
+      handleBeforeThrow(e);
+      throw e;
+    }
+  }
+
+  @Override
+  public Uri getUri() {
+    return currentDataSource == upstreamDataSource ? currentDataSource.getUri() : uri;
+  }
+
+  @Override
+  public void close() throws IOException {
+    uri = null;
+    notifyBytesRead();
+    try {
+      closeCurrentSource();
+    } catch (IOException 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.
+   * @param initial Whether it is the initial open call.
+   */
+  private boolean openNextSource(boolean initial) throws IOException {
+    DataSpec dataSpec;
+    CacheSpan span;
+    if (currentRequestIgnoresCache) {
+      span = null;
+    } else if (blockOnCache) {
+      try {
+        span = cache.startReadWrite(key, readPosition);
+      } catch (InterruptedException e) {
+        throw new InterruptedIOException();
+      }
+    } else {
+      span = cache.startReadWriteNonBlocking(key, readPosition);
+    }
+
+    if (span == null) {
+      // The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read
+      // from upstream.
+      currentDataSource = upstreamDataSource;
+      dataSpec = new DataSpec(uri, readPosition, bytesRemaining, key, flags);
+    } else if (span.isCached) {
+      // Data is cached, read from cache.
+      Uri fileUri = Uri.fromFile(span.file);
+      long filePosition = readPosition - span.position;
+      long length = span.length - filePosition;
+      if (bytesRemaining != C.LENGTH_UNSET) {
+        length = Math.min(length, bytesRemaining);
+      }
+      dataSpec = new DataSpec(fileUri, readPosition, filePosition, length, key, flags);
+      currentDataSource = cacheReadDataSource;
+    } else {
+      // Data is not cached, and data is not locked, read from upstream with cache backing.
+      lockedSpan = span;
+      long length;
+      if (span.isOpenEnded()) {
+        length = bytesRemaining;
+      } else {
+        length = span.length;
+        if (bytesRemaining != C.LENGTH_UNSET) {
+          length = Math.min(length, bytesRemaining);
+        }
+      }
+      dataSpec = new DataSpec(uri, readPosition, length, key, flags);
+      currentDataSource = cacheWriteDataSource != null ? cacheWriteDataSource
+          : upstreamDataSource;
+    }
+
+    currentRequestUnbounded = dataSpec.length == C.LENGTH_UNSET;
+    boolean successful = false;
+    long currentBytesRemaining = 0;
+    try {
+      currentBytesRemaining = currentDataSource.open(dataSpec);
+      successful = true;
+    } catch (IOException e) {
+      // if this isn't the initial open call (we had read some bytes) and an unbounded range request
+      // failed because of POSITION_OUT_OF_RANGE then mute the exception. We are trying to find the
+      // end of the stream.
+      if (!initial && currentRequestUnbounded) {
+        Throwable cause = e;
+        while (cause != null) {
+          if (cause instanceof DataSourceException) {
+            int reason = ((DataSourceException) cause).reason;
+            if (reason == DataSourceException.POSITION_OUT_OF_RANGE) {
+              e = null;
+              break;
+            }
+          }
+          cause = cause.getCause();
+        }
+      }
+      if (e != null) {
+        throw e;
+      }
+    }
+
+    // If we did an unbounded request (which means it's to upstream and
+    // bytesRemaining == C.LENGTH_UNSET) and got a resolved length from open() request
+    if (currentRequestUnbounded && currentBytesRemaining != C.LENGTH_UNSET) {
+      bytesRemaining = currentBytesRemaining;
+      setContentLength(dataSpec.position + bytesRemaining);
+    }
+    return successful;
+  }
+
+  private void setContentLength(long length) throws IOException {
+    // If writing into cache
+    if (currentDataSource == cacheWriteDataSource) {
+      cache.setContentLength(key, length);
+    }
+  }
+
+  private void closeCurrentSource() throws IOException {
+    if (currentDataSource == null) {
+      return;
+    }
+    try {
+      currentDataSource.close();
+      currentDataSource = null;
+      currentRequestUnbounded = false;
+    } finally {
+      if (lockedSpan != null) {
+        cache.releaseHoleSpan(lockedSpan);
+        lockedSpan = null;
+      }
+    }
+  }
+
+  private void handleBeforeThrow(IOException exception) {
+    if (currentDataSource == cacheReadDataSource || exception instanceof CacheException) {
+      seenCacheError = true;
+    }
+  }
+
+  private void notifyBytesRead() {
+    if (eventListener != null && totalCachedBytesRead > 0) {
+      eventListener.onCachedBytesRead(cache.getCacheSpace(), totalCachedBytesRead);
+      totalCachedBytesRead = 0;
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream.cache;
+
+import com.google.android.exoplayer2.upstream.DataSink;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSource.Factory;
+import com.google.android.exoplayer2.upstream.FileDataSourceFactory;
+import com.google.android.exoplayer2.upstream.cache.CacheDataSource.EventListener;
+
+/**
+ * A {@link DataSource.Factory} that produces {@link CacheDataSource}.
+ */
+public final class CacheDataSourceFactory implements DataSource.Factory {
+
+  private final Cache cache;
+  private final DataSource.Factory upstreamFactory;
+  private final DataSource.Factory cacheReadDataSourceFactory;
+  private final DataSink.Factory cacheWriteDataSinkFactory;
+  private final int flags;
+  private final EventListener eventListener;
+
+  /**
+   * @see CacheDataSource#CacheDataSource(Cache, DataSource, int)
+   */
+  public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, int flags) {
+    this(cache, upstreamFactory, flags, CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE);
+  }
+
+  /**
+   * @see CacheDataSource#CacheDataSource(Cache, DataSource, int, long)
+   */
+  public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, int flags,
+      long maxCacheFileSize) {
+    this(cache, upstreamFactory, new FileDataSourceFactory(),
+        new CacheDataSinkFactory(cache, maxCacheFileSize), flags, null);
+  }
+
+  /**
+   * @see CacheDataSource#CacheDataSource(Cache, DataSource, DataSource, DataSink, int,
+   *     EventListener)
+   */
+  public CacheDataSourceFactory(Cache cache, Factory upstreamFactory,
+      Factory cacheReadDataSourceFactory,
+      DataSink.Factory cacheWriteDataSinkFactory, int flags, EventListener eventListener) {
+    this.cache = cache;
+    this.upstreamFactory = upstreamFactory;
+    this.cacheReadDataSourceFactory = cacheReadDataSourceFactory;
+    this.cacheWriteDataSinkFactory = cacheWriteDataSinkFactory;
+    this.flags = flags;
+    this.eventListener = eventListener;
+  }
+
+  @Override
+  public DataSource createDataSource() {
+    return new CacheDataSource(cache, upstreamFactory.createDataSource(),
+        cacheReadDataSourceFactory.createDataSource(),
+        cacheWriteDataSinkFactory.createDataSink(), flags, eventListener);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CacheEvictor.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream.cache;
+
+/**
+ * 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 {
+
+  /**
+   * 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 maxLength The maximum length of the data being written.
+   */
+  void onStartFile(Cache cache, String key, long position, long maxLength);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.upstream.cache;
+
+import 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.
+   */
+  public final File file;
+  /**
+   * The last access timestamp, or {@link C#TIME_UNSET} if {@link #isCached} is false.
+   */
+  public final long lastAccessTimestamp;
+
+  /**
+   * Creates a hole CacheSpan which isn't cached, has no last access time 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 lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if
+   *     {@link #isCached} is false.
+   * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole.
+   */
+  public CacheSpan(String key, long position, long length, long lastAccessTimestamp, File file) {
+    this.key = key;
+    this.position = position;
+    this.length = length;
+    this.isCached = file != null;
+    this.file = file;
+    this.lastAccessTimestamp = lastAccessTimestamp;
+  }
+
+  /**
+   * 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(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);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContent.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 com.google.android.exoplayer2.upstream.cache;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.TreeSet;
+
+/**
+ * Defines the cached content for a single stream.
+ */
+/*package*/ final class 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;
+  /**
+   * The length of the original stream, or {@link C#LENGTH_UNSET} if the length is unknown.
+   */
+  private long length;
+
+  /**
+   * Reads an instance from a {@link DataInputStream}.
+   *
+   * @param input Input stream containing values needed to initialize CachedContent instance.
+   * @throws IOException If an error occurs during reading values.
+   */
+  public CachedContent(DataInputStream input) throws IOException {
+    this(input.readInt(), input.readUTF(), input.readLong());
+  }
+
+  /**
+   * Creates a CachedContent.
+   *
+   * @param id The cache file id.
+   * @param key The cache stream key.
+   * @param length The length of the original stream.
+   */
+  public CachedContent(int id, String key, long length) {
+    this.id = id;
+    this.key = key;
+    this.length = length;
+    this.cachedSpans = new TreeSet<>();
+  }
+
+  /**
+   * Writes the instance to a {@link DataOutputStream}.
+   *
+   * @param output Output stream to store the values.
+   * @throws IOException If an error occurs during writing values to output.
+   */
+  public void writeToStream(DataOutputStream output) throws IOException {
+    output.writeInt(id);
+    output.writeUTF(key);
+    output.writeLong(length);
+  }
+
+  /** Returns the length of the content. */
+  public long getLength() {
+    return length;
+  }
+
+  /** Sets the length of the content. */
+  public void setLength(long length) {
+    this.length = length;
+  }
+
+  /** 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 span = getSpanInternal(position);
+    if (!span.isCached) {
+      SimpleCacheSpan ceilEntry = cachedSpans.ceiling(span);
+      return ceilEntry == null ? SimpleCacheSpan.createOpenHole(key, position)
+          : SimpleCacheSpan.createClosedHole(key, position, ceilEntry.position - position);
+    }
+    return span;
+  }
+
+  /** Queries if a range is entirely available in the cache. */
+  public boolean isCached(long position, long length) {
+    SimpleCacheSpan floorSpan = getSpanInternal(position);
+    if (!floorSpan.isCached) {
+      // We don't have a span covering the start of the queried region.
+      return false;
+    }
+    long queryEndPosition = position + length;
+    long currentEndPosition = floorSpan.position + floorSpan.length;
+    if (currentEndPosition >= queryEndPosition) {
+      // floorSpan covers the queried region.
+      return true;
+    }
+    for (SimpleCacheSpan next : cachedSpans.tailSet(floorSpan, false)) {
+      if (next.position > currentEndPosition) {
+        // There's a hole in the cache within the queried region.
+        return false;
+      }
+      // 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.
+        return true;
+      }
+    }
+    // We ran out of spans before covering the queried region.
+    return false;
+  }
+
+  /**
+   * Copies the given span with an updated last access time. Passed span becomes invalid after this
+   * call.
+   *
+   * @param cacheSpan Span to be copied and updated.
+   * @return a span with the updated last access time.
+   * @throws CacheException If renaming of the underlying span file failed.
+   */
+  public SimpleCacheSpan touch(SimpleCacheSpan cacheSpan) throws CacheException {
+    // Remove the old span from the in-memory representation.
+    Assertions.checkState(cachedSpans.remove(cacheSpan));
+    // Obtain a new span with updated last access timestamp.
+    SimpleCacheSpan newCacheSpan = cacheSpan.copyWithUpdatedLastAccessTime(id);
+    // Rename the cache file
+    if (!cacheSpan.file.renameTo(newCacheSpan.file)) {
+      throw new CacheException("Renaming of " + cacheSpan.file + " to " + newCacheSpan.file
+          + " failed.");
+    }
+    // Add the updated span back into the in-memory representation.
+    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;
+  }
+
+  /** Calculates a hash code for the header of this {@code CachedContent}. */
+  public int headerHashCode() {
+    int result = id;
+    result = 31 * result + key.hashCode();
+    result = 31 * result + (int) (length ^ (length >>> 32));
+    return result;
+  }
+
+  /**
+   * Returns the span containing the position. If there isn't one, it returns the lookup span it
+   * used for searching.
+   */
+  private SimpleCacheSpan getSpanInternal(long position) {
+    SimpleCacheSpan lookupSpan = SimpleCacheSpan.createLookup(key, position);
+    SimpleCacheSpan floorSpan = cachedSpans.floor(lookupSpan);
+    return floorSpan == null || floorSpan.position + floorSpan.length <= position ? lookupSpan
+        : floorSpan;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream.cache;
+
+import android.util.Log;
+import android.util.SparseArray;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.AtomicFile;
+import com.google.android.exoplayer2.util.ReusableBufferedOutputStream;
+import com.google.android.exoplayer2.util.Util;
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+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;
+
+/**
+ * This class maintains the index of cached content.
+ */
+/*package*/ final class CachedContentIndex {
+
+  public static final String FILE_NAME = "cached_content_index.exi";
+
+  private static final int VERSION = 1;
+
+  private static final int FLAG_ENCRYPTED_INDEX = 1;
+
+  private static final String TAG = "CachedContentIndex";
+
+  private final HashMap<String, CachedContent> keyToContent;
+  private final SparseArray<String> idToKey;
+  private final AtomicFile atomicFile;
+  private final Cipher cipher;
+  private final SecretKeySpec secretKeySpec;
+  private boolean changed;
+  private ReusableBufferedOutputStream bufferedOutputStream;
+
+  /**
+   * Creates a CachedContentIndex which works on the index file in the given cacheDir.
+   *
+   * @param cacheDir Directory where the index file is kept.
+   */
+  public CachedContentIndex(File cacheDir) {
+    this(cacheDir, null);
+  }
+
+  /**
+   * Creates a CachedContentIndex which works on the index file in the given cacheDir.
+   *
+   * @param cacheDir Directory where the index file is kept.
+   * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC.
+   *     The key must be 16 bytes long.
+   */
+  public CachedContentIndex(File cacheDir, byte[] secretKey) {
+    if (secretKey != null) {
+      Assertions.checkArgument(secretKey.length == 16);
+      try {
+        cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
+        secretKeySpec = new SecretKeySpec(secretKey, "AES");
+      } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+        throw new IllegalStateException(e); // Should never happen.
+      }
+    } else {
+      cipher = null;
+      secretKeySpec = null;
+    }
+    keyToContent = new HashMap<>();
+    idToKey = new SparseArray<>();
+    atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME));
+  }
+
+  /** Loads the index file. */
+  public void load() {
+    Assertions.checkState(!changed);
+    if (!readFile()) {
+      atomicFile.delete();
+      keyToContent.clear();
+      idToKey.clear();
+    }
+  }
+
+  /** Stores the index data to index file if there is a change. */
+  public void store() throws CacheException {
+    if (!changed) {
+      return;
+    }
+    writeFile();
+    changed = false;
+  }
+
+  /**
+   * 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 add(String key) {
+    CachedContent cachedContent = keyToContent.get(key);
+    if (cachedContent == null) {
+      cachedContent = addNew(key, C.LENGTH_UNSET);
+    }
+    return 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 add(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. It shouldn't contain any spans.
+   *
+   * @throws IllegalStateException If {@link CachedContent} isn't empty.
+   */
+  public void removeEmpty(String key) {
+    CachedContent cachedContent = keyToContent.remove(key);
+    if (cachedContent != null) {
+      Assertions.checkState(cachedContent.isEmpty());
+      idToKey.remove(cachedContent.id);
+      changed = true;
+    }
+  }
+
+  /** Removes empty {@link CachedContent} instances from index. */
+  public void removeEmpty() {
+    LinkedList<String> cachedContentToBeRemoved = new LinkedList<>();
+    for (CachedContent cachedContent : keyToContent.values()) {
+      if (cachedContent.isEmpty()) {
+        cachedContentToBeRemoved.add(cachedContent.key);
+      }
+    }
+    for (String key : cachedContentToBeRemoved) {
+      removeEmpty(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();
+  }
+
+  /**
+   * Sets the content length for the given key. A new {@link CachedContent} is added if there isn't
+   * one already with the given key.
+   */
+  public void setContentLength(String key, long length) {
+    CachedContent cachedContent = get(key);
+    if (cachedContent != null) {
+      if (cachedContent.getLength() != length) {
+        cachedContent.setLength(length);
+        changed = true;
+      }
+    } else {
+      addNew(key, length);
+    }
+  }
+
+  /**
+   * Returns the content length for the given key if one set, or {@link
+   * com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise.
+   */
+  public long getContentLength(String key) {
+    CachedContent cachedContent = get(key);
+    return cachedContent == null ? C.LENGTH_UNSET : cachedContent.getLength();
+  }
+
+  private boolean readFile() {
+    DataInputStream input = null;
+    try {
+      InputStream inputStream = new BufferedInputStream(atomicFile.openRead());
+      input = new DataInputStream(inputStream);
+      int version = input.readInt();
+      if (version != VERSION) {
+        // Currently there is no other version
+        return false;
+      }
+
+      int flags = input.readInt();
+      if ((flags & FLAG_ENCRYPTED_INDEX) != 0) {
+        if (cipher == null) {
+          return false;
+        }
+        byte[] initializationVector = new byte[16];
+        input.readFully(initializationVector);
+        IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
+        try {
+          cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
+        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+          throw new IllegalStateException(e);
+        }
+        input = new DataInputStream(new CipherInputStream(inputStream, cipher));
+      } else {
+        if (cipher != null) {
+          changed = true; // Force index to be rewritten encrypted after read.
+        }
+      }
+
+      int count = input.readInt();
+      int hashCode = 0;
+      for (int i = 0; i < count; i++) {
+        CachedContent cachedContent = new CachedContent(input);
+        add(cachedContent);
+        hashCode += cachedContent.headerHashCode();
+      }
+      if (input.readInt() != hashCode) {
+        return false;
+      }
+    } catch (FileNotFoundException e) {
+      return false;
+    } catch (IOException e) {
+      Log.e(TAG, "Error reading cache content index file.", e);
+      return false;
+    } finally {
+      if (input != null) {
+        Util.closeQuietly(input);
+      }
+    }
+    return true;
+  }
+
+  private void writeFile() throws CacheException {
+    DataOutputStream output = null;
+    try {
+      OutputStream outputStream = atomicFile.startWrite();
+      if (bufferedOutputStream == null) {
+        bufferedOutputStream = new ReusableBufferedOutputStream(outputStream);
+      } else {
+        bufferedOutputStream.reset(outputStream);
+      }
+      output = new DataOutputStream(bufferedOutputStream);
+      output.writeInt(VERSION);
+
+      int flags = cipher != null ? FLAG_ENCRYPTED_INDEX : 0;
+      output.writeInt(flags);
+
+      if (cipher != null) {
+        byte[] initializationVector = new byte[16];
+        new Random().nextBytes(initializationVector);
+        output.write(initializationVector);
+        IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
+        try {
+          cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
+        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+          throw new IllegalStateException(e); // Should never happen.
+        }
+        output.flush();
+        output = new DataOutputStream(new CipherOutputStream(bufferedOutputStream, cipher));
+      }
+
+      output.writeInt(keyToContent.size());
+      int hashCode = 0;
+      for (CachedContent cachedContent : keyToContent.values()) {
+        cachedContent.writeToStream(output);
+        hashCode += cachedContent.headerHashCode();
+      }
+      output.writeInt(hashCode);
+      atomicFile.endWrite(output);
+      // Avoid calling close twice. Duplicate CipherOutputStream.close calls did
+      // not used to be no-ops: https://android-review.googlesource.com/#/c/272799/
+      output = null;
+    } catch (IOException e) {
+      throw new CacheException(e);
+    } finally {
+      Util.closeQuietly(output);
+    }
+  }
+
+  private void add(CachedContent cachedContent) {
+    keyToContent.put(cachedContent.key, cachedContent);
+    idToKey.put(cachedContent.id, cachedContent.key);
+  }
+
+  /** Adds the given CachedContent to the index. */
+  /*package*/ void addNew(CachedContent cachedContent) {
+    add(cachedContent);
+    changed = true;
+  }
+
+  private CachedContent addNew(String key, long length) {
+    int id = getNewId(idToKey);
+    CachedContent cachedContent = new CachedContent(id, key, length);
+    addNew(cachedContent);
+    return cachedContent;
+  }
+
+  /**
+   * Returns an id which isn't used in the given array. If the maximum id in the array is smaller
+   * than {@link java.lang.Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it
+   * returns the smallest unused non-negative integer.
+   */
+  //@VisibleForTesting
+  public static int getNewId(SparseArray<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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.upstream.cache;
+
+import android.util.Log;
+import com.google.android.exoplayer2.extractor.ChunkIndex;
+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);
+      if (cacheSpans != null) {
+        // Merge the spans into regions. mergeSpan is more efficient when merging from high to low,
+        // which is why a descending iterator is used here.
+        Iterator<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(Region another) {
+      return startOffset < another.startOffset ? -1
+          : startOffset == another.startOffset ? 0 : 1;
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.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 com.google.android.exoplayer2.upstream.cache;
+
+import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
+import java.util.Comparator;
+import java.util.TreeSet;
+
+/**
+ * Evicts least recently used cache files first.
+ */
+public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Comparator<CacheSpan> {
+
+  private final long maxBytes;
+  private final TreeSet<CacheSpan> leastRecentlyUsed;
+
+  private long currentSize;
+
+  public LeastRecentlyUsedCacheEvictor(long maxBytes) {
+    this.maxBytes = maxBytes;
+    this.leastRecentlyUsed = new TreeSet<>(this);
+  }
+
+  @Override
+  public void onCacheInitialized() {
+    // Do nothing.
+  }
+
+  @Override
+  public void onStartFile(Cache cache, String key, long position, long maxLength) {
+    evictCache(cache, maxLength);
+  }
+
+  @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);
+  }
+
+  @Override
+  public int compare(CacheSpan lhs, CacheSpan rhs) {
+    long lastAccessTimestampDelta = lhs.lastAccessTimestamp - rhs.lastAccessTimestamp;
+    if (lastAccessTimestampDelta == 0) {
+      // Use the standard compareTo method as a tie-break.
+      return lhs.compareTo(rhs);
+    }
+    return lhs.lastAccessTimestamp < rhs.lastAccessTimestamp ? -1 : 1;
+  }
+
+  private void evictCache(Cache cache, long requiredSpace) {
+    while (currentSize + requiredSpace > maxBytes) {
+      try {
+        cache.removeSpan(leastRecentlyUsed.first());
+      } catch (CacheException e) {
+        // do nothing.
+      }
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/NoOpCacheEvictor.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.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 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.
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCache.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 com.google.android.exoplayer2.upstream.cache;
+
+import android.os.ConditionVariable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.NavigableSet;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * A {@link Cache} implementation that maintains an in-memory representation.
+ */
+public final class SimpleCache implements Cache {
+
+  private final File cacheDir;
+  private final CacheEvictor evictor;
+  private final HashMap<String, CacheSpan> lockedSpans;
+  private final CachedContentIndex index;
+  private final HashMap<String, ArrayList<Listener>> listeners;
+  private long totalSpace = 0;
+  private CacheException initializationException;
+
+  /**
+   * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence
+   * the directory cannot be used to store other files.
+   *
+   * @param cacheDir A dedicated cache directory.
+   * @param evictor The evictor to be used.
+   */
+  public SimpleCache(File cacheDir, CacheEvictor evictor) {
+    this(cacheDir, evictor, 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.
+   * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC.
+   *     The key must be 16 bytes long.
+   */
+  public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey) {
+    this.cacheDir = cacheDir;
+    this.evictor = evictor;
+    this.lockedSpans = new HashMap<>();
+    this.index = new CachedContentIndex(cacheDir, secretKey);
+    this.listeners = new HashMap<>();
+    // Start cache initialization.
+    final ConditionVariable conditionVariable = new ConditionVariable();
+    new Thread("SimpleCache.initialize()") {
+      @Override
+      public void run() {
+        synchronized (SimpleCache.this) {
+          conditionVariable.open();
+          try {
+            initialize();
+          } catch (CacheException e) {
+            initializationException = e;
+          }
+          SimpleCache.this.evictor.onCacheInitialized();
+        }
+      }
+    }.start();
+    conditionVariable.block();
+  }
+
+  @Override
+  public synchronized NavigableSet<CacheSpan> addListener(String key, Listener listener) {
+    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) {
+    ArrayList<Listener> listenersForKey = listeners.get(key);
+    if (listenersForKey != null) {
+      listenersForKey.remove(listener);
+      if (listenersForKey.isEmpty()) {
+        listeners.remove(key);
+      }
+    }
+  }
+
+  @Override
+  public synchronized NavigableSet<CacheSpan> getCachedSpans(String key) {
+    CachedContent cachedContent = index.get(key);
+    return cachedContent == null ? null : new TreeSet<CacheSpan>(cachedContent.getSpans());
+  }
+
+  @Override
+  public synchronized Set<String> getKeys() {
+    return new HashSet<>(index.getKeys());
+  }
+
+  @Override
+  public synchronized long getCacheSpace() {
+    return totalSpace;
+  }
+
+  @Override
+  public synchronized SimpleCacheSpan startReadWrite(String key, long position)
+      throws InterruptedException, CacheException {
+    while (true) {
+      SimpleCacheSpan span = startReadWriteNonBlocking(key, position);
+      if (span != null) {
+        return span;
+      } else {
+        // Write case, lock not available. We'll be woken up when a locked span is released (if the
+        // released lock is for the requested key then we'll be able to make progress) or when a
+        // span is added to the cache (if the span is for the requested key and covers the requested
+        // position, then we'll become a read and be able to make progress).
+        wait();
+      }
+    }
+  }
+
+  @Override
+  public synchronized SimpleCacheSpan startReadWriteNonBlocking(String key, long position)
+      throws CacheException {
+    if (initializationException != null) {
+      throw initializationException;
+    }
+
+    SimpleCacheSpan cacheSpan = getSpan(key, position);
+
+    // Read case.
+    if (cacheSpan.isCached) {
+      // Obtain a new span with updated last access timestamp.
+      SimpleCacheSpan newCacheSpan = index.get(key).touch(cacheSpan);
+      notifySpanTouched(cacheSpan, newCacheSpan);
+      return newCacheSpan;
+    }
+
+    // Write case, lock available.
+    if (!lockedSpans.containsKey(key)) {
+      lockedSpans.put(key, cacheSpan);
+      return cacheSpan;
+    }
+
+    // Write case, lock not available.
+    return null;
+  }
+
+  @Override
+  public synchronized File startFile(String key, long position, long maxLength)
+      throws CacheException {
+    Assertions.checkState(lockedSpans.containsKey(key));
+    if (!cacheDir.exists()) {
+      // For some reason the cache directory doesn't exist. Make a best effort to create it.
+      removeStaleSpansAndCachedContents();
+      cacheDir.mkdirs();
+    }
+    evictor.onStartFile(this, key, position, maxLength);
+    return SimpleCacheSpan.getCacheFile(cacheDir, index.assignIdForKey(key), position,
+        System.currentTimeMillis());
+  }
+
+  @Override
+  public synchronized void commitFile(File file) throws CacheException {
+    SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, index);
+    Assertions.checkState(span != null);
+    Assertions.checkState(lockedSpans.containsKey(span.key));
+    // If the file doesn't exist, don't add it to the in-memory representation.
+    if (!file.exists()) {
+      return;
+    }
+    // If the file has length 0, delete it and don't add it to the in-memory representation.
+    if (file.length() == 0) {
+      file.delete();
+      return;
+    }
+    // Check if the span conflicts with the set content length
+    Long length = getContentLength(span.key);
+    if (length != C.LENGTH_UNSET) {
+      Assertions.checkState((span.position + span.length) <= length);
+    }
+    addSpan(span);
+    index.store();
+    notifyAll();
+  }
+
+  @Override
+  public synchronized void releaseHoleSpan(CacheSpan holeSpan) {
+    Assertions.checkState(holeSpan == lockedSpans.remove(holeSpan.key));
+    notifyAll();
+  }
+
+  /**
+   * Returns the cache {@link SimpleCacheSpan} corresponding to the provided lookup {@link
+   * SimpleCacheSpan}.
+   *
+   * <p>If the lookup position is contained by an existing entry in the cache, then the returned
+   * {@link SimpleCacheSpan} defines the file in which the data is stored. If the lookup position is
+   * not contained by an existing entry, then the returned {@link SimpleCacheSpan} defines the
+   * maximum extents of the hole in the cache.
+   *
+   * @param key The key of the span being requested.
+   * @param position The position of the span being requested.
+   * @return The corresponding cache {@link SimpleCacheSpan}.
+   */
+  private SimpleCacheSpan getSpan(String key, long position) throws CacheException {
+    CachedContent cachedContent = index.get(key);
+    if (cachedContent == null) {
+      return SimpleCacheSpan.createOpenHole(key, position);
+    }
+    while (true) {
+      SimpleCacheSpan span = cachedContent.getSpan(position);
+      if (span.isCached && !span.file.exists()) {
+        // The file has been deleted from under us. It's likely that other files will have been
+        // deleted too, so scan the whole in-memory representation.
+        removeStaleSpansAndCachedContents();
+        continue;
+      }
+      return span;
+    }
+  }
+
+  /**
+   * Ensures that the cache's in-memory representation has been initialized.
+   */
+  private void initialize() throws CacheException {
+    if (!cacheDir.exists()) {
+      cacheDir.mkdirs();
+      return;
+    }
+
+    index.load();
+
+    File[] files = cacheDir.listFiles();
+    if (files == null) {
+      return;
+    }
+    for (File file : files) {
+      if (file.getName().equals(CachedContentIndex.FILE_NAME)) {
+        continue;
+      }
+      SimpleCacheSpan span = file.length() > 0
+          ? SimpleCacheSpan.createCacheEntry(file, index) : null;
+      if (span != null) {
+        addSpan(span);
+      } else {
+        file.delete();
+      }
+    }
+
+    index.removeEmpty();
+    index.store();
+  }
+
+  /**
+   * Adds a cached span to the in-memory representation.
+   *
+   * @param span The span to be added.
+   */
+  private void addSpan(SimpleCacheSpan span) {
+    index.add(span.key).addSpan(span);
+    totalSpace += span.length;
+    notifySpanAdded(span);
+  }
+
+  private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException {
+    CachedContent cachedContent = index.get(span.key);
+    Assertions.checkState(cachedContent.removeSpan(span));
+    totalSpace -= span.length;
+    if (removeEmptyCachedContent && cachedContent.isEmpty()) {
+      index.removeEmpty(cachedContent.key);
+      index.store();
+    }
+    notifySpanRemoved(span);
+  }
+
+  @Override
+  public synchronized void removeSpan(CacheSpan span) throws CacheException {
+    removeSpan(span, true);
+  }
+
+  /**
+   * Scans all of the cached spans in the in-memory representation, removing any for which files
+   * no longer exist.
+   */
+  private void removeStaleSpansAndCachedContents() throws CacheException {
+    LinkedList<CacheSpan> spansToBeRemoved = new LinkedList<>();
+    for (CachedContent cachedContent : index.getAll()) {
+      for (CacheSpan span : cachedContent.getSpans()) {
+        if (!span.file.exists()) {
+          spansToBeRemoved.add(span);
+        }
+      }
+    }
+    for (CacheSpan span : spansToBeRemoved) {
+      // Remove span but not CachedContent to prevent multiple index.store() calls.
+      removeSpan(span, false);
+    }
+    index.removeEmpty();
+    index.store();
+  }
+
+  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);
+  }
+
+  @Override
+  public synchronized boolean isCached(String key, long position, long length) {
+    CachedContent cachedContent = index.get(key);
+    return cachedContent != null && cachedContent.isCached(position, length);
+  }
+
+  @Override
+  public synchronized void setContentLength(String key, long length) throws CacheException {
+    index.setContentLength(key, length);
+    index.store();
+  }
+
+  @Override
+  public synchronized long getContentLength(String key) {
+    return index.getContentLength(key);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.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 com.google.android.exoplayer2.upstream.cache;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import 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 {
+
+  private static final String SUFFIX = ".v3.exo";
+  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);
+
+  public static File getCacheFile(File cacheDir, int id, long position,
+      long lastAccessTimestamp) {
+    return new File(cacheDir, id + "." + position + "." + lastAccessTimestamp + SUFFIX);
+  }
+
+  public static SimpleCacheSpan createLookup(String key, long position) {
+    return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null);
+  }
+
+  public static SimpleCacheSpan createOpenHole(String key, long position) {
+    return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null);
+  }
+
+  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 index Cached content index.
+   * @return The span, or null if the file name is not correctly formatted, or if the id is not
+   *     present in the content index.
+   */
+  public static SimpleCacheSpan createCacheEntry(File file, CachedContentIndex index) {
+    String name = file.getName();
+    if (!name.endsWith(SUFFIX)) {
+      file = upgradeFile(file, index);
+      if (file == null) {
+        return null;
+      }
+      name = file.getName();
+    }
+
+    Matcher matcher = CACHE_FILE_PATTERN_V3.matcher(name);
+    if (!matcher.matches()) {
+      return null;
+    }
+    long length = file.length();
+    int id = Integer.parseInt(matcher.group(1));
+    String key = index.getKeyForId(id);
+    return key == null ? null : new SimpleCacheSpan(key, Long.parseLong(matcher.group(2)), length,
+        Long.parseLong(matcher.group(3)), file);
+  }
+
+  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(file.getParentFile(), index.assignIdForKey(key),
+        Long.parseLong(matcher.group(2)), Long.parseLong(matcher.group(3)));
+    if (!file.renameTo(newCacheFile)) {
+      return null;
+    }
+    return newCacheFile;
+  }
+
+  private SimpleCacheSpan(String key, long position, long length, long lastAccessTimestamp,
+      File file) {
+    super(key, position, length, lastAccessTimestamp, file);
+  }
+
+  /**
+   * Returns a copy of this CacheSpan whose last access time stamp is set to current time. This
+   * doesn't copy or change the underlying cache file.
+   *
+   * @param id The cache file id.
+   * @return A {@link SimpleCacheSpan} with updated last access time stamp.
+   * @throws IllegalStateException If called on a non-cached span (i.e. {@link #isCached} is false).
+   */
+  public SimpleCacheSpan copyWithUpdatedLastAccessTime(int id) {
+    Assertions.checkState(isCached);
+    long now = System.currentTimeMillis();
+    File newCacheFile = getCacheFile(file.getParentFile(), id, position, now);
+    return new SimpleCacheSpan(key, position, length, now, newCacheFile);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSink.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 com.google.android.exoplayer2.upstream.crypto;
+
+import com.google.android.exoplayer2.upstream.DataSink;
+import 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;
+  private final byte[] scratch;
+
+  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 decrypted 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.
+   */
+  public AesCipherDataSink(byte[] secretKey, DataSink wrappedDataSink, 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.
+      cipher.updateInPlace(data, offset, length);
+      wrappedDataSink.write(data, offset, length);
+    } else {
+      // Use scratch space. The original data remains intact.
+      int bytesProcessed = 0;
+      while (bytesProcessed < length) {
+        int bytesToProcess = Math.min(length - bytesProcessed, scratch.length);
+        cipher.update(data, offset + bytesProcessed, bytesToProcess, scratch, 0);
+        wrappedDataSink.write(scratch, 0, bytesToProcess);
+        bytesProcessed += bytesToProcess;
+      }
+    }
+  }
+
+  @Override
+  public void close() throws IOException {
+    cipher = null;
+    wrappedDataSink.close();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesCipherDataSource.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 com.google.android.exoplayer2.upstream.crypto;
+
+import android.net.Uri;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import java.io.IOException;
+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;
+
+  private AesFlushingCipher cipher;
+
+  public AesCipherDataSource(byte[] secretKey, DataSource upstream) {
+    this.upstream = upstream;
+    this.secretKey = secretKey;
+  }
+
+  @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;
+    }
+    cipher.updateInPlace(data, offset, read);
+    return read;
+  }
+
+  @Override
+  public void close() throws IOException {
+    cipher = null;
+    upstream.close();
+  }
+
+  @Override
+  public Uri getUri() {
+    return upstream.getUri();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipher.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 com.google.android.exoplayer2.upstream.crypto;
+
+import com.google.android.exoplayer2.util.Assertions;
+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, cipher.getAlgorithm().split("/")[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();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/upstream/crypto/CryptoUtil.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 com.google.android.exoplayer2.upstream.crypto;
+
+/**
+ * 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(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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/Assertions.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 com.google.android.exoplayer2.util;
+
+import android.os.Looper;
+import android.text.TextUtils;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+
+/**
+ * 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 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.
+   */
+  public static <T> T checkNotNull(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.
+   */
+  public static <T> T checkNotNull(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.
+   */
+  public static String checkNotEmpty(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.
+   */
+  public static String checkNotEmpty(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");
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/AtomicFile.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 com.google.android.exoplayer2.util;
+
+import android.util.Log;
+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
+ * sync'd 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");
+  }
+
+  /** 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.mkdirs()) {
+        throw new IOException("Couldn't create directory " + baseName);
+      }
+      try {
+        str = new AtomicFileOutputStream(baseName);
+      } catch (FileNotFoundException e2) {
+        throw new IOException("Couldn't create " + baseName);
+      }
+    }
+    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);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/Clock.java
@@ -0,0 +1,31 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+/**
+ * An interface through which system clocks can be read. The {@link SystemClock} implementation
+ * must be used for all non-test cases.
+ */
+public interface Clock {
+
+  /**
+   * Returns {@link android.os.SystemClock#elapsedRealtime}.
+   *
+   * @return Elapsed milliseconds since boot.
+   */
+  long elapsedRealtime();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+import android.util.Pair;
+import com.google.android.exoplayer2.C;
+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;
+
+  private CodecSpecificDataUtil() {}
+
+  /**
+   * Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
+   *
+   * @param audioSpecificConfig The AudioSpecificConfig to parse.
+   * @return A pair consisting of the sample rate in Hz and the channel count.
+   */
+  public static Pair<Integer, Integer> parseAacAudioSpecificConfig(byte[] audioSpecificConfig) {
+    ParsableBitArray bitArray = new ParsableBitArray(audioSpecificConfig);
+    int audioObjectType = bitArray.readBits(5);
+    int frequencyIndex = bitArray.readBits(4);
+    int sampleRate;
+    if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) {
+      sampleRate = bitArray.readBits(24);
+    } else {
+      Assertions.checkArgument(frequencyIndex < 13);
+      sampleRate = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex];
+    }
+    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.
+      frequencyIndex = bitArray.readBits(4);
+      if (frequencyIndex == AUDIO_SPECIFIC_CONFIG_FREQUENCY_INDEX_ARBITRARY) {
+        sampleRate = bitArray.readBits(24);
+      } else {
+        Assertions.checkArgument(frequencyIndex < 13);
+        sampleRate = AUDIO_SPECIFIC_CONFIG_SAMPLING_RATE_TABLE[frequencyIndex];
+      }
+      audioObjectType = bitArray.readBits(5);
+      if (audioObjectType == AUDIO_OBJECT_TYPE_ER_BSAC) {
+        // Use the extensionChannelConfiguration.
+        channelConfiguration = bitArray.readBits(4);
+      }
+    }
+    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 numChannels The number of channels.
+   * @return The AudioSpecificConfig.
+   */
+  public static byte[] buildAacLcAudioSpecificConfig(int sampleRate, int numChannels) {
+    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 (numChannels == AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[i]) {
+        channelConfig = i;
+      }
+    }
+    if (sampleRate == C.INDEX_UNSET || channelConfig == C.INDEX_UNSET) {
+      throw new IllegalArgumentException("Invalid sample rate or number of channels: "
+          + sampleRate + ", " + numChannels);
+    }
+    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;
+  }
+
+  /**
+   * 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 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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/ColorParser.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.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);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/ConditionVariable.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 com.google.android.exoplayer2.util;
+
+/**
+ * A 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();
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/FlacStreamInfo.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+/**
+ * Holder for FLAC stream info.
+ */
+public final class FlacStreamInfo {
+
+  public final int minBlockSize;
+  public final int maxBlockSize;
+  public final int minFrameSize;
+  public final int maxFrameSize;
+  public final int sampleRate;
+  public final int channels;
+  public final int bitsPerSample;
+  public final long totalSamples;
+
+  /**
+   * Constructs a FlacStreamInfo parsing the given binary FLAC stream info metadata structure.
+   *
+   * @param data An array holding FLAC stream info metadata structure
+   * @param offset Offset of the structure in the array
+   * @see <a href="https://xiph.org/flac/format.html#metadata_block_streaminfo">FLAC format
+   *     METADATA_BLOCK_STREAMINFO</a>
+   */
+  public FlacStreamInfo(byte[] data, int offset) {
+    ParsableBitArray scratch = new ParsableBitArray(data);
+    scratch.setPosition(offset * 8);
+    this.minBlockSize = scratch.readBits(16);
+    this.maxBlockSize = scratch.readBits(16);
+    this.minFrameSize = scratch.readBits(24);
+    this.maxFrameSize = scratch.readBits(24);
+    this.sampleRate = scratch.readBits(20);
+    this.channels = scratch.readBits(3) + 1;
+    this.bitsPerSample = scratch.readBits(5) + 1;
+    this.totalSamples = scratch.readBits(36);
+    // Remaining 16 bytes is md5 value
+  }
+
+  public FlacStreamInfo(int minBlockSize, int maxBlockSize, int minFrameSize, int maxFrameSize,
+      int sampleRate, int channels, int bitsPerSample, long totalSamples) {
+    this.minBlockSize = minBlockSize;
+    this.maxBlockSize = maxBlockSize;
+    this.minFrameSize = minFrameSize;
+    this.maxFrameSize = maxFrameSize;
+    this.sampleRate = sampleRate;
+    this.channels = channels;
+    this.bitsPerSample = bitsPerSample;
+    this.totalSamples = totalSamples;
+  }
+
+  public int maxDecodedFrameSize() {
+    return maxBlockSize * channels * 2;
+  }
+
+  public int bitRate() {
+    return bitsPerSample * sampleRate;
+  }
+
+  public long durationUs() {
+    return (totalSamples * 1000000L) / sampleRate;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/LibraryLoader.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 com.google.android.exoplayer2.util;
+
+/**
+ * Configurable loader for native libraries.
+ */
+public final class 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) {
+      // Do nothing.
+    }
+    return isAvailable;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 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);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/MediaClock.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 com.google.android.exoplayer2.util;
+
+/**
+ * Tracks the progression of media time.
+ */
+public interface MediaClock {
+
+  /**
+   * Returns the current media position in microseconds.
+   */
+  long getPositionUs();
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/MimeTypes.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+import android.text.TextUtils;
+import com.google.android.exoplayer2.C;
+
+/**
+ * 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_MP4V = BASE_TYPE_VIDEO + "/mp4v-es";
+  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_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_ULAW = 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_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 + "/x-flac";
+  public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac";
+
+  public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt";
+
+  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_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL";
+  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";
+
+  private MimeTypes() {}
+
+  /**
+   * Whether the top-level type of {@code mimeType} is audio.
+   *
+   * @param mimeType The mimeType to test.
+   * @return Whether the top level type is audio.
+   */
+  public static boolean isAudio(String mimeType) {
+    return BASE_TYPE_AUDIO.equals(getTopLevelType(mimeType));
+  }
+
+  /**
+   * Whether the top-level type of {@code mimeType} is video.
+   *
+   * @param mimeType The mimeType to test.
+   * @return Whether the top level type is video.
+   */
+  public static boolean isVideo(String mimeType) {
+    return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType));
+  }
+
+  /**
+   * Whether the top-level type of {@code mimeType} is text.
+   *
+   * @param mimeType The mimeType to test.
+   * @return Whether the top level type is text.
+   */
+  public static boolean isText(String mimeType) {
+    return BASE_TYPE_TEXT.equals(getTopLevelType(mimeType));
+  }
+
+  /**
+   * Whether the top-level type of {@code mimeType} is application.
+   *
+   * @param mimeType The mimeType to test.
+   * @return Whether the top level type is application.
+   */
+  public static boolean isApplication(String mimeType) {
+    return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType));
+  }
+
+
+  /**
+   * 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.
+   */
+  public static String getVideoMediaMimeType(String codecs) {
+    if (codecs == null) {
+      return null;
+    }
+    String[] codecList = codecs.split(",");
+    for (String codec : codecList) {
+      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.
+   */
+  public static String getAudioMediaMimeType(String codecs) {
+    if (codecs == null) {
+      return null;
+    }
+    String[] codecList = codecs.split(",");
+    for (String codec : codecList) {
+      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.
+   */
+  public static String getMediaMimeType(String codec) {
+    if (codec == null) {
+      return null;
+    }
+    codec = codec.trim();
+    if (codec.startsWith("avc1") || codec.startsWith("avc3")) {
+      return MimeTypes.VIDEO_H264;
+    } else if (codec.startsWith("hev1") || codec.startsWith("hvc1")) {
+      return MimeTypes.VIDEO_H265;
+    } else if (codec.startsWith("vp9")) {
+      return MimeTypes.VIDEO_VP9;
+    } else if (codec.startsWith("vp8")) {
+      return MimeTypes.VIDEO_VP8;
+    } else if (codec.startsWith("mp4a")) {
+      return MimeTypes.AUDIO_AAC;
+    } 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("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;
+    }
+    return null;
+  }
+
+  /**
+   * Returns the {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified mime type.
+   * {@link C#TRACK_TYPE_UNKNOWN} if the mime type is not known or the mapping cannot be
+   * established.
+   *
+   * @param mimeType The mimeType.
+   * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified mime type.
+   */
+  public static int getTrackType(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)) {
+      return C.TRACK_TYPE_TEXT;
+    } else if (APPLICATION_ID3.equals(mimeType)
+        || APPLICATION_EMSG.equals(mimeType)
+        || APPLICATION_SCTE35.equals(mimeType)
+        || APPLICATION_CAMERA_MOTION.equals(mimeType)) {
+      return C.TRACK_TYPE_METADATA;
+    } else {
+      return C.TRACK_TYPE_UNKNOWN;
+    }
+  }
+
+  /**
+   * 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}.
+   *
+   * @param mimeType The mimeType whose top-level type is required.
+   * @return The top-level type, or null if the mimeType is null.
+   */
+  private static String getTopLevelType(String mimeType) {
+    if (mimeType == null) {
+      return null;
+    }
+    int indexOfSlash = mimeType.indexOf('/');
+    if (indexOfSlash == -1) {
+      throw new IllegalArgumentException("Invalid mime type: " + mimeType);
+    }
+    return mimeType.substring(0, indexOfSlash);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/NalUnitUtil.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 com.google.android.exoplayer2.util;
+
+import android.util.Log;
+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 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 seqParameterSetId, int width, int height, float pixelWidthAspectRatio,
+        boolean separateColorPlaneFlag, boolean frameMbsOnlyFlag, int frameNumLength,
+        int picOrderCountType, int picOrderCntLsbLength, boolean deltaPicOrderAlwaysZeroFlag) {
+      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 NAL_UNIT_TYPE_SPS = 7;
+
+  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) == 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 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);
+    data.skipBits(16); // constraint bits (6), reserved (2) and level_idc (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.skipBits(1); // 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.skipBits(1); // gaps_in_frame_num_value_allowed_flag
+
+    int picWidthInMbs = data.readUnsignedExpGolombCodedInt() + 1;
+    int picHeightInMapUnits = data.readUnsignedExpGolombCodedInt() + 1;
+    boolean frameMbsOnlyFlag = data.readBit();
+    int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits;
+    if (!frameMbsOnlyFlag) {
+      data.skipBits(1); // mb_adaptive_frame_field_flag
+    }
+
+    data.skipBits(1); // 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, 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(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.skipBits(1); // 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.
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/ParsableBitArray.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.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() {}
+
+  /**
+   * 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);
+  }
+
+  /**
+   * 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;
+  }
+
+  /**
+   * 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 bits and moves current reading position forward.
+   *
+   * @param n The number of bits to skip.
+   */
+  public void skipBits(int n) {
+    byteOffset += (n / 8);
+    bitOffset += (n % 8);
+    if (bitOffset > 7) {
+      byteOffset++;
+      bitOffset -= 8;
+    }
+    assertValidOffset();
+  }
+
+  /**
+   * Reads a single bit.
+   *
+   * @return Whether the bit is set.
+   */
+  public boolean readBit() {
+    return readBits(1) == 1;
+  }
+
+  /**
+   * 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) {
+    if (numBits == 0) {
+      return 0;
+    }
+
+    int returnValue = 0;
+
+    // Read as many whole bytes as we can.
+    int wholeBytes = (numBits / 8);
+    for (int i = 0; i < wholeBytes; i++) {
+      int byteValue;
+      if (bitOffset != 0) {
+        byteValue = ((data[byteOffset] & 0xFF) << bitOffset)
+            | ((data[byteOffset + 1] & 0xFF) >>> (8 - bitOffset));
+      } else {
+        byteValue = data[byteOffset];
+      }
+      numBits -= 8;
+      returnValue |= (byteValue & 0xFF) << numBits;
+      byteOffset++;
+    }
+
+    // Read any remaining bits.
+    if (numBits > 0) {
+      int nextBit = bitOffset + numBits;
+      byte writeMask = (byte) (0xFF >> (8 - numBits));
+
+      if (nextBit > 8) {
+        // Combine bits from current byte and next byte.
+        returnValue |= ((((data[byteOffset] & 0xFF) << (nextBit - 8)
+            | ((data[byteOffset + 1] & 0xFF) >> (16 - nextBit))) & writeMask));
+        byteOffset++;
+      } else {
+        // Bits to be read only within current byte.
+        returnValue |= (((data[byteOffset] & 0xFF) >> (8 - nextBit)) & writeMask);
+        if (nextBit == 8) {
+          byteOffset++;
+        }
+      }
+
+      bitOffset = nextBit % 8;
+    }
+
+    assertValidOffset();
+    return returnValue;
+  }
+
+  private void assertValidOffset() {
+    // It is fine for position to be at the end of the array, but no further.
+    Assertions.checkState(byteOffset >= 0
+        && (bitOffset >= 0 && bitOffset < 8)
+        && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0)));
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/ParsableByteArray.java
@@ -0,0 +1,557 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+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() {}
+
+  /**
+   * 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;
+  }
+
+  /**
+   * 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.
+   *
+   * @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;
+  }
+
+  /**
+   * Sets the position and limit to zero.
+   */
+  public void reset() {
+    position = 0;
+    limit = 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 == null ? 0 : 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);
+  }
+
+  /**
+   * 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 an 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 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 an 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.defaultCharset());
+  }
+
+  /**
+   * 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 = new String(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.
+   */
+  public String readNullTerminatedString() {
+    if (bytesLeft() == 0) {
+      return null;
+    }
+    int stringLimit = position;
+    while (stringLimit < limit && data[stringLimit] != 0) {
+      stringLimit++;
+    }
+    String string = new String(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.
+   *
+   * @return The line not including any line-termination characters, or null if the end of the data
+   *     has already been reached.
+   */
+  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 byte order mark at the start of the line. Discard it.
+      position += 3;
+    }
+    String line = new String(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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.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 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}.
+   */
+  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 bits and moves current reading position forward.
+   *
+   * @param n The number of bits to skip.
+   */
+  public void skipBits(int n) {
+    int oldByteOffset = byteOffset;
+    byteOffset += (n / 8);
+    bitOffset += (n % 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 n The number of bits.
+   * @return Whether it is possible to read {@code n} bits.
+   */
+  public boolean canReadBits(int n) {
+    int oldByteOffset = byteOffset;
+    int newByteOffset = byteOffset + (n / 8);
+    int newBitOffset = bitOffset + (n % 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() {
+    return readBits(1) == 1;
+  }
+
+  /**
+   * 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) {
+    if (numBits == 0) {
+      return 0;
+    }
+
+    int returnValue = 0;
+
+    // Read as many whole bytes as we can.
+    int wholeBytes = (numBits / 8);
+    for (int i = 0; i < wholeBytes; i++) {
+      int nextByteOffset = shouldSkipByte(byteOffset + 1) ? byteOffset + 2 : byteOffset + 1;
+      int byteValue;
+      if (bitOffset != 0) {
+        byteValue = ((data[byteOffset] & 0xFF) << bitOffset)
+            | ((data[nextByteOffset] & 0xFF) >>> (8 - bitOffset));
+      } else {
+        byteValue = data[byteOffset];
+      }
+      numBits -= 8;
+      returnValue |= (byteValue & 0xFF) << numBits;
+      byteOffset = nextByteOffset;
+    }
+
+    // Read any remaining bits.
+    if (numBits > 0) {
+      int nextBit = bitOffset + numBits;
+      byte writeMask = (byte) (0xFF >> (8 - numBits));
+      int nextByteOffset = shouldSkipByte(byteOffset + 1) ? byteOffset + 2 : byteOffset + 1;
+
+      if (nextBit > 8) {
+        // Combine bits from current byte and next byte.
+        returnValue |= ((((data[byteOffset] & 0xFF) << (nextBit - 8)
+            | ((data[nextByteOffset] & 0xFF) >> (16 - nextBit))) & writeMask));
+        byteOffset = nextByteOffset;
+      } else {
+        // Bits to be read only within current byte.
+        returnValue |= (((data[byteOffset] & 0xFF) >> (8 - nextBit)) & writeMask);
+        if (nextBit == 8) {
+          byteOffset = nextByteOffset;
+        }
+      }
+
+      bitOffset = nextBit % 8;
+    }
+
+    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
+        && (bitOffset >= 0 && bitOffset < 8)
+        && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0)));
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.util;
+
+/**
+ * Determines a true of 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);
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/PriorityHandlerThread.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 com.google.android.exoplayer2.util;
+
+import android.os.HandlerThread;
+import android.os.Process;
+
+/**
+ * A {@link HandlerThread} with a specified process priority.
+ */
+public final class PriorityHandlerThread extends HandlerThread {
+
+  private final int priority;
+
+  /**
+   * @param name The name of the thread.
+   * @param priority The priority level. See {@link Process#setThreadPriority(int)} for details.
+   */
+  public PriorityHandlerThread(String name, int priority) {
+    super(name);
+    this.priority = priority;
+  }
+
+  @Override
+  public void run() {
+    Process.setThreadPriority(priority);
+    super.run();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 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.
+   */
+  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 : queue.peek();
+      lock.notifyAll();
+    }
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 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;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/SlidingPercentile.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.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 = new Comparator<Sample>() {
+    @Override
+    public int compare(Sample a, Sample b) {
+      return a.index - b.index;
+    }
+  };
+
+  private static final Comparator<Sample> VALUE_COMPARATOR = new Comparator<Sample>() {
+    @Override
+    public int compare(Sample a, Sample b) {
+      return a.value < b.value ? -1 : b.value < a.value ? 1 : 0;
+    }
+  };
+
+  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;
+  }
+
+  /**
+   * 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;
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/StandaloneMediaClock.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+import android.os.SystemClock;
+
+/**
+ * A standalone {@link MediaClock}. The clock can be started, stopped and its time can be set and
+ * retrieved. When started, this clock is based on {@link SystemClock#elapsedRealtime()}.
+ */
+public final class StandaloneMediaClock implements MediaClock {
+
+  private boolean started;
+
+  /**
+   * The media time when the clock was last set or stopped.
+   */
+  private long positionUs;
+
+  /**
+   * The difference between {@link SystemClock#elapsedRealtime()} and {@link #positionUs}
+   * when the clock was last set or started.
+   */
+  private long deltaUs;
+
+  /**
+   * Starts the clock. Does nothing if the clock is already started.
+   */
+  public void start() {
+    if (!started) {
+      started = true;
+      deltaUs = elapsedRealtimeMinus(positionUs);
+    }
+  }
+
+  /**
+   * Stops the clock. Does nothing if the clock is already stopped.
+   */
+  public void stop() {
+    if (started) {
+      positionUs = elapsedRealtimeMinus(deltaUs);
+      started = false;
+    }
+  }
+
+  /**
+   * @param timeUs The position to set in microseconds.
+   */
+  public void setPositionUs(long timeUs) {
+    this.positionUs = timeUs;
+    deltaUs = elapsedRealtimeMinus(timeUs);
+  }
+
+  @Override
+  public long getPositionUs() {
+    return started ? elapsedRealtimeMinus(deltaUs) : positionUs;
+  }
+
+  private long elapsedRealtimeMinus(long toSubtractUs) {
+    return SystemClock.elapsedRealtime() * 1000 - toSubtractUs;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/SystemClock.java
@@ -0,0 +1,28 @@
+/*
+ * 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 com.google.android.exoplayer2.util;
+
+/**
+ * The standard implementation of {@link Clock}.
+ */
+public final class SystemClock implements Clock {
+
+  @Override
+  public long elapsedRealtime() {
+    return android.os.SystemClock.elapsedRealtime();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/TimestampAdjuster.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 com.google.android.exoplayer2.util;
+
+import 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 presentation timestamp.
+   */
+  private static final long MAX_PTS_PLUS_ONE = 0x200000000L;
+
+  private final long firstSampleTimestampUs;
+
+  private long timestampOffsetUs;
+
+  // Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp.
+  private volatile long lastSampleTimestamp;
+
+  /**
+   * @param firstSampleTimestampUs The desired result of the first call to
+   *     {@link #adjustSampleTimestamp(long)}, or {@link #DO_NOT_OFFSET} if presentation timestamps
+   *     should not be offset.
+   */
+  public TimestampAdjuster(long firstSampleTimestampUs) {
+    this.firstSampleTimestampUs = firstSampleTimestampUs;
+    lastSampleTimestamp = C.TIME_UNSET;
+  }
+
+  /**
+   * Returns the last adjusted timestamp. If no timestamp has been adjusted, returns
+   * {@code firstSampleTimestampUs} as provided to the constructor. If this value is
+   * {@link #DO_NOT_OFFSET}, returns {@link C#TIME_UNSET}.
+   *
+   * @return The last adjusted timestamp. If not present, {@code firstSampleTimestampUs} is
+   *     returned unless equal to {@link #DO_NOT_OFFSET}, in which case {@link C#TIME_UNSET} is
+   *     returned.
+   */
+  public long getLastAdjustedTimestampUs() {
+    return lastSampleTimestamp != C.TIME_UNSET ? lastSampleTimestamp
+        : 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
+        : lastSampleTimestamp == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
+  }
+
+  /**
+   * Resets the instance to its initial state.
+   */
+  public void reset() {
+    lastSampleTimestamp = C.TIME_UNSET;
+  }
+
+  /**
+   * Scales and offsets an MPEG-2 TS presentation timestamp considering wraparound.
+   *
+   * @param pts The MPEG-2 TS presentation timestamp.
+   * @return The adjusted timestamp in microseconds.
+   */
+  public long adjustTsTimestamp(long pts) {
+    if (pts == C.TIME_UNSET) {
+      return C.TIME_UNSET;
+    }
+    if (lastSampleTimestamp != C.TIME_UNSET) {
+      // The wrap count for the current PTS may be closestWrapCount or (closestWrapCount - 1),
+      // and we need to snap to the one closest to lastSampleTimestamp.
+      long lastPts = usToPts(lastSampleTimestamp);
+      long closestWrapCount = (lastPts + (MAX_PTS_PLUS_ONE / 2)) / MAX_PTS_PLUS_ONE;
+      long ptsWrapBelow = pts + (MAX_PTS_PLUS_ONE * (closestWrapCount - 1));
+      long ptsWrapAbove = pts + (MAX_PTS_PLUS_ONE * closestWrapCount);
+      pts = Math.abs(ptsWrapBelow - lastPts) < Math.abs(ptsWrapAbove - lastPts)
+          ? ptsWrapBelow : ptsWrapAbove;
+    }
+    return adjustSampleTimestamp(ptsToUs(pts));
+  }
+
+  /**
+   * Offsets a sample timestamp in microseconds.
+   *
+   * @param timeUs The timestamp of a sample to adjust.
+   * @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 (lastSampleTimestamp != C.TIME_UNSET) {
+      lastSampleTimestamp = timeUs;
+    } else {
+      if (firstSampleTimestampUs != DO_NOT_OFFSET) {
+        // Calculate the timestamp offset.
+        timestampOffsetUs = firstSampleTimestampUs - timeUs;
+      }
+      synchronized (this) {
+        lastSampleTimestamp = 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 (lastSampleTimestamp == C.TIME_UNSET) {
+      wait();
+    }
+  }
+
+  /**
+   * Converts a value in MPEG-2 timestamp units to the corresponding value in microseconds.
+   *
+   * @param pts A value in MPEG-2 timestamp units.
+   * @return The corresponding value in microseconds.
+   */
+  public static long ptsToUs(long pts) {
+    return (pts * C.MICROS_PER_SECOND) / 90000;
+  }
+
+  /**
+   * Converts a value in microseconds to the corresponding values in MPEG-2 timestamp units.
+   *
+   * @param us A value in microseconds.
+   * @return The corresponding value in MPEG-2 timestamp units.
+   */
+  public static long usToPts(long us) {
+    return (us * 90000) / C.MICROS_PER_SECOND;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.util;
+
+import android.annotation.TargetApi;
+import 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();
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/UriUtil.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+/**
+ * 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(String baseUri, 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(String baseUri, 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 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;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/Util.java
@@ -0,0 +1,1079 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.util;
+
+import android.Manifest.permission;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.Point;
+import android.net.Uri;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Display;
+import android.view.WindowManager;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.upstream.DataSource;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+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.GregorianCalendar;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 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 == 25 && Build.VERSION.CODENAME.charAt(0) == 'O') ? 26
+      : 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;
+
+  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})");
+
+  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();
+  }
+
+  /**
+   * 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 (Util.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 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) || scheme.equals("file");
+  }
+
+  /**
+   * 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(Object o1, 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(Object[] items, Object item) {
+    for (Object arrayItem : items) {
+      if (Util.areEqual(arrayItem, item)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * 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(new ThreadFactory() {
+      @Override
+      public Thread newThread(Runnable r) {
+        return new Thread(r, threadName);
+      }
+    });
+  }
+
+  /**
+   * Closes a {@link DataSource}, suppressing any {@link IOException} that may occur.
+   *
+   * @param dataSource The {@link DataSource} to close.
+   */
+  public static void closeQuietly(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(Closeable closeable) {
+    try {
+      if (closeable != null) {
+        closeable.close();
+      }
+    } catch (IOException e) {
+      // Ignore.
+    }
+  }
+
+  /**
+   * Returns a normalized RFC 5646 language code.
+   *
+   * @param language A possibly non-normalized RFC 5646 language code.
+   * @return The normalized code, or null if the input was null.
+   */
+  public static String normalizeLanguageCode(String language) {
+    return language == null ? null : new Locale(language).getLanguage();
+  }
+
+  /**
+   * 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.defaultCharset()); // UTF-8 is the default on Android.
+  }
+
+  /**
+   * 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 String toLowerInvariant(String text) {
+    return text == null ? null : text.toLowerCase(Locale.US);
+  }
+
+  /**
+   * 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));
+  }
+
+  /**
+   * 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 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 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> 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 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> 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;
+  }
+
+  /**
+   * 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 (matcher.group(11).equals("-")) {
+        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);
+      }
+    }
+  }
+
+  /**
+   * Converts a list of integers to a primitive array.
+   *
+   * @param list A list of integers.
+   * @return The list in array form, or null if the input list was null.
+   */
+  public static int[] toArray(List<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;
+  }
+
+  /**
+   * Given a {@link DataSpec} and a number of bytes already loaded, returns a {@link DataSpec}
+   * that represents the remainder of the data.
+   *
+   * @param dataSpec The original {@link DataSpec}.
+   * @param bytesLoaded The number of bytes already loaded.
+   * @return A {@link DataSpec} that represents the remainder of the data.
+   */
+  public static DataSpec getRemainderDataSpec(DataSpec dataSpec, int bytesLoaded) {
+    if (bytesLoaded == 0) {
+      return dataSpec;
+    } else {
+      long remainingLength = dataSpec.length == C.LENGTH_UNSET ? C.LENGTH_UNSET
+          : dataSpec.length - bytesLoaded;
+      return new DataSpec(dataSpec.uri, dataSpec.position + bytesLoaded, remainingLength,
+          dataSpec.key, dataSpec.flags);
+    }
+  }
+
+  /**
+   * Returns the integer equal to the big-endian concatenation of the characters in {@code string}
+   * as bytes. The string must be no more than four characters long.
+   *
+   * @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;
+  }
+
+  /**
+   * 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
+        + ") " + "ExoPlayerLib/" + ExoPlayerLibraryInfo.VERSION;
+  }
+
+  /**
+   * 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;
+    }
+  }
+
+  /**
+   * 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) {
+    if (fileName == null) {
+      return C.TYPE_OTHER;
+    } else if (fileName.endsWith(".mpd")) {
+      return C.TYPE_DASH;
+    } else if (fileName.endsWith(".ism") || fileName.endsWith(".isml")) {
+      return C.TYPE_SS;
+    } else if (fileName.endsWith(".m3u8")) {
+      return C.TYPE_HLS;
+    } else {
+      return C.TYPE_OTHER;
+    }
+  }
+
+  /**
+   * Maps a {@link C} {@code TRACK_TYPE_*} constant to the corresponding {@link C}
+   * {@code DEFAULT_*_BUFFER_SIZE} constant.
+   *
+   * @param trackType The track type.
+   * @return The corresponding default buffer size in bytes.
+   */
+  public static int getDefaultBufferSize(int trackType) {
+    switch (trackType) {
+      case C.TRACK_TYPE_DEFAULT:
+        return C.DEFAULT_MUXED_BUFFER_SIZE;
+      case C.TRACK_TYPE_AUDIO:
+        return C.DEFAULT_AUDIO_BUFFER_SIZE;
+      case C.TRACK_TYPE_VIDEO:
+        return C.DEFAULT_VIDEO_BUFFER_SIZE;
+      case C.TRACK_TYPE_TEXT:
+        return C.DEFAULT_TEXT_BUFFER_SIZE;
+      case C.TRACK_TYPE_METADATA:
+        return C.DEFAULT_METADATA_BUFFER_SIZE;
+      default:
+        throw new IllegalStateException();
+    }
+  }
+
+  /**
+   * Escapes a string so that it's safe for use as a file or directory name on at least FAT32
+   * filesystems. FAT32 is the most restrictive of all filesystems still commonly used today.
+   *
+   * <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 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 endOfLastMatch = 0;
+    while (percentCharacterCount > 0 && matcher.find()) {
+      char unescapedCharacter = (char) Integer.parseInt(matcher.group(1), 16);
+      builder.append(fileName, endOfLastMatch, matcher.start()).append(unescapedCharacter);
+      endOfLastMatch = matcher.end();
+      percentCharacterCount--;
+    }
+    if (endOfLastMatch < length) {
+      builder.append(fileName, endOfLastMatch, 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) {
+    Util.<RuntimeException>sneakyThrowInternal(t);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <T extends Throwable> void sneakyThrowInternal(Throwable t) throws T {
+    throw (T) t;
+  }
+
+  /**
+   * Returns the result of updating a CRC 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 crc(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;
+  }
+
+  /**
+   * Gets the physical size of the default display, in pixels.
+   *
+   * @param context Any context.
+   * @return The physical display size, in pixels.
+   */
+  public static Point getPhysicalDisplaySize(Context context) {
+    WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+    return getPhysicalDisplaySize(context, windowManager.getDefaultDisplay());
+  }
+
+  /**
+   * Gets the physical size of the specified display, in pixels.
+   *
+   * @param context Any context.
+   * @param display The display whose size is to be returned.
+   * @return The physical display size, in pixels.
+   */
+  public static Point getPhysicalDisplaySize(Context context, Display display) {
+    if (Util.SDK_INT < 25 && display.getDisplayId() == Display.DEFAULT_DISPLAY) {
+      // Before API 25 the Display object does not provide a working way to identify Android TVs
+      // that can show 4k resolution in a SurfaceView, so check for supported devices here.
+      if ("Sony".equals(Util.MANUFACTURER) && Util.MODEL.startsWith("BRAVIA")
+          && context.getPackageManager().hasSystemFeature("com.sony.dtv.hardware.panel.qfhd")) {
+        return new Point(3840, 2160);
+      } else if ("NVIDIA".equals(Util.MANUFACTURER) && Util.MODEL.contains("SHIELD")) {
+        // Attempt to read sys.display-size.
+        String sysDisplaySize = null;
+        try {
+          Class<?> systemProperties = Class.forName("android.os.SystemProperties");
+          Method getMethod = systemProperties.getMethod("get", String.class);
+          sysDisplaySize = (String) getMethod.invoke(systemProperties, "sys.display-size");
+        } catch (Exception e) {
+          Log.e(TAG, "Failed to read sys.display-size", e);
+        }
+        // If we managed to read sys.display-size, attempt to parse it.
+        if (!TextUtils.isEmpty(sysDisplaySize)) {
+          try {
+            String[] sysDisplaySizeParts = sysDisplaySize.trim().split("x");
+            if (sysDisplaySizeParts.length == 2) {
+              int width = Integer.parseInt(sysDisplaySizeParts[0]);
+              int height = Integer.parseInt(sysDisplaySizeParts[1]);
+              if (width > 0 && height > 0) {
+                return new Point(width, height);
+              }
+            }
+          } catch (NumberFormatException e) {
+            // Do nothing.
+          }
+          Log.e(TAG, "Invalid sys.display-size: " + sysDisplaySize);
+        }
+      }
+    }
+
+    Point displaySize = new Point();
+    if (Util.SDK_INT >= 23) {
+      getDisplaySizeV23(display, displaySize);
+    } else if (Util.SDK_INT >= 17) {
+      getDisplaySizeV17(display, displaySize);
+    } else if (Util.SDK_INT >= 16) {
+      getDisplaySizeV16(display, displaySize);
+    } else {
+      getDisplaySizeV9(display, displaySize);
+    }
+    return displaySize;
+  }
+
+  @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);
+  }
+
+  @TargetApi(16)
+  private static void getDisplaySizeV16(Display display, Point outSize) {
+    display.getSize(outSize);
+  }
+
+  @SuppressWarnings("deprecation")
+  private static void getDisplaySizeV9(Display display, Point outSize) {
+    outSize.x = display.getWidth();
+    outSize.y = display.getHeight();
+  }
+
+  /**
+   * Allows the CRC calculation to be done byte by byte instead of bit per bit being 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
+  };
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/util/XmlPullParserUtil.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 com.google.android.exoplayer2.util;
+
+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 the value of an attribute of the current start tag.
+   *
+   * @param xpp The {@link XmlPullParser} to query.
+   * @param attributeName The name of the attribute.
+   * @return The value of the attribute, or null if the current event is not a start tag or if no
+   *     no such attribute was found.
+   */
+  public static String getAttributeValue(XmlPullParser xpp, String attributeName) {
+    int attributeCount = xpp.getAttributeCount();
+    for (int i = 0; i < attributeCount; i++) {
+      if (attributeName.equals(xpp.getAttributeName(i))) {
+        return xpp.getAttributeValue(i);
+      }
+    }
+    return null;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/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 com.google.android.exoplayer2.video;
+
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.NalUnitUtil.SpsData;
+import 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);
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/video/HevcConfig.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.video;
+
+import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.util.NalUnitUtil;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * HEVC configuration data.
+ */
+public final class HevcConfig {
+
+  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(List<byte[]> initializationData, int nalUnitLengthFieldLength) {
+    this.initializationData = initializationData;
+    this.nalUnitLengthFieldLength = nalUnitLengthFieldLength;
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java
@@ -0,0 +1,826 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.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.MediaCrypto;
+import android.media.MediaFormat;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.Surface;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.drm.DrmInitData;
+import com.google.android.exoplayer2.drm.DrmSessionManager;
+import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
+import com.google.android.exoplayer2.mediacodec.MediaCodecInfo;
+import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer;
+import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
+import com.google.android.exoplayer2.mediacodec.MediaCodecUtil;
+import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
+import com.google.android.exoplayer2.util.MimeTypes;
+import com.google.android.exoplayer2.util.TraceUtil;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
+import java.nio.ByteBuffer;
+
+/**
+ * Decodes and renders video using {@link MediaCodec}.
+ */
+@TargetApi(16)
+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};
+
+  private final VideoFrameReleaseTimeHelper frameReleaseTimeHelper;
+  private final EventDispatcher eventDispatcher;
+  private final long allowedJoiningTimeMs;
+  private final int maxDroppedFramesToNotify;
+  private final boolean deviceNeedsAutoFrcWorkaround;
+
+  private Format[] streamFormats;
+  private CodecMaxValues codecMaxValues;
+
+  private Surface surface;
+  @C.VideoScalingMode
+  private int scalingMode;
+  private boolean renderedFirstFrame;
+  private long joiningDeadlineMs;
+  private long droppedFrameAccumulationStartTimeMs;
+  private int droppedFrames;
+  private int consecutiveDroppedFrameCount;
+
+  private int pendingRotationDegrees;
+  private float pendingPixelWidthHeightRatio;
+  private int currentWidth;
+  private int currentHeight;
+  private int currentUnappliedRotationDegrees;
+  private float currentPixelWidthHeightRatio;
+  private int lastReportedWidth;
+  private int lastReportedHeight;
+  private int lastReportedUnappliedRotationDegrees;
+  private float lastReportedPixelWidthHeightRatio;
+
+  private boolean tunneling;
+  private int tunnelingAudioSessionId;
+  /* package */ OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener;
+
+  /**
+   * @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, null, null, -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 maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between
+   *     invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
+   */
+  public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector,
+      long allowedJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener,
+      int maxDroppedFrameCountToNotify) {
+    this(context, mediaCodecSelector, allowedJoiningTimeMs, null, false, eventHandler,
+        eventListener, maxDroppedFrameCountToNotify);
+  }
+
+  /**
+   * @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)}.
+   */
+  public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector,
+      long allowedJoiningTimeMs, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
+      boolean playClearSamplesWithoutKeys, Handler eventHandler,
+      VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) {
+    super(C.TRACK_TYPE_VIDEO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
+    this.allowedJoiningTimeMs = allowedJoiningTimeMs;
+    this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
+    frameReleaseTimeHelper = new VideoFrameReleaseTimeHelper(context);
+    eventDispatcher = new EventDispatcher(eventHandler, eventListener);
+    deviceNeedsAutoFrcWorkaround = deviceNeedsAutoFrcWorkaround();
+    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;
+    clearLastReportedVideoSize();
+  }
+
+  @Override
+  protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format)
+      throws DecoderQueryException {
+    String mimeType = format.sampleMimeType;
+    if (!MimeTypes.isVideo(mimeType)) {
+      return FORMAT_UNSUPPORTED_TYPE;
+    }
+    boolean requiresSecureDecryption = false;
+    DrmInitData drmInitData = format.drmInitData;
+    if (drmInitData != null) {
+      for (int i = 0; i < drmInitData.schemeDataCount; i++) {
+        requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption;
+      }
+    }
+    MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType,
+        requiresSecureDecryption);
+    if (decoderInfo == null) {
+      return FORMAT_UNSUPPORTED_SUBTYPE;
+    }
+
+    boolean decoderCapable = decoderInfo.isCodecSupported(format.codecs);
+    if (decoderCapable && format.width > 0 && format.height > 0) {
+      if (Util.SDK_INT >= 21) {
+        decoderCapable = decoderInfo.isVideoSizeAndRateSupportedV21(format.width, format.height,
+            format.frameRate);
+      } else {
+        decoderCapable = format.width * format.height <= MediaCodecUtil.maxH264DecodableFrameSize();
+        if (!decoderCapable) {
+          Log.d(TAG, "FalseCheck [legacyFrameSize, " + format.width + "x" + format.height + "] ["
+              + Util.DEVICE_DEBUG_INFO + "]");
+        }
+      }
+    }
+
+    int adaptiveSupport = decoderInfo.adaptive ? ADAPTIVE_SEAMLESS : ADAPTIVE_NOT_SEAMLESS;
+    int tunnelingSupport = decoderInfo.tunneling ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
+    int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;
+    return adaptiveSupport | tunnelingSupport | formatSupport;
+  }
+
+  @Override
+  protected void onEnabled(boolean joining) throws ExoPlaybackException {
+    super.onEnabled(joining);
+    tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
+    tunneling = tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET;
+    eventDispatcher.enabled(decoderCounters);
+    frameReleaseTimeHelper.enable();
+  }
+
+  @Override
+  protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
+    streamFormats = formats;
+    super.onStreamChanged(formats);
+  }
+
+  @Override
+  protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+    super.onPositionReset(positionUs, joining);
+    clearRenderedFirstFrame();
+    consecutiveDroppedFrameCount = 0;
+    joiningDeadlineMs = joining && allowedJoiningTimeMs > 0
+        ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET;
+  }
+
+  @Override
+  public boolean isReady() {
+    if ((renderedFirstFrame || super.shouldInitCodec()) && super.isReady()) {
+      // 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();
+  }
+
+  @Override
+  protected void onStopped() {
+    joiningDeadlineMs = C.TIME_UNSET;
+    maybeNotifyDroppedFrames();
+    super.onStopped();
+  }
+
+  @Override
+  protected void onDisabled() {
+    currentWidth = Format.NO_VALUE;
+    currentHeight = Format.NO_VALUE;
+    currentPixelWidthHeightRatio = Format.NO_VALUE;
+    pendingPixelWidthHeightRatio = Format.NO_VALUE;
+    clearLastReportedVideoSize();
+    frameReleaseTimeHelper.disable();
+    tunnelingOnFrameRenderedListener = null;
+    try {
+      super.onDisabled();
+    } finally {
+      decoderCounters.ensureUpdated();
+      eventDispatcher.disabled(decoderCounters);
+    }
+  }
+
+  @Override
+  public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+    if (messageType == C.MSG_SET_SURFACE) {
+      setSurface((Surface) message);
+    } else if (messageType == C.MSG_SET_SCALING_MODE) {
+      scalingMode = (Integer) message;
+      MediaCodec codec = getCodec();
+      if (codec != null) {
+        setVideoScalingMode(codec, scalingMode);
+      }
+    } else {
+      super.handleMessage(messageType, message);
+    }
+  }
+
+  private void setSurface(Surface surface) throws ExoPlaybackException {
+    // We only need to release and reinitialize the codec if the surface has changed.
+    if (this.surface != surface) {
+      this.surface = surface;
+      int state = getState();
+      if (state == STATE_ENABLED || state == STATE_STARTED) {
+        releaseCodec();
+        maybeInitCodec();
+      }
+    }
+    // Clear state so that we always call the event listener with the video size and when a frame
+    // is rendered, even if the surface hasn't changed.
+    clearRenderedFirstFrame();
+    clearLastReportedVideoSize();
+  }
+
+  @Override
+  protected boolean shouldInitCodec() {
+    return super.shouldInitCodec() && surface != null && surface.isValid();
+  }
+
+  @Override
+  protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
+      MediaCrypto crypto) throws DecoderQueryException {
+    codecMaxValues = getCodecMaxValues(codecInfo, format, streamFormats);
+    MediaFormat mediaFormat = getMediaFormat(format, codecMaxValues, deviceNeedsAutoFrcWorkaround,
+        tunnelingAudioSessionId);
+    codec.configure(mediaFormat, surface, crypto, 0);
+    if (Util.SDK_INT >= 23 && tunneling) {
+      tunnelingOnFrameRenderedListener = new OnFrameRenderedListenerV23(codec);
+    }
+  }
+
+  @Override
+  protected void onCodecInitialized(String name, long initializedTimestampMs,
+      long initializationDurationMs) {
+    eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs);
+  }
+
+  @Override
+  protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
+    super.onInputFormatChanged(newFormat);
+    eventDispatcher.inputFormatChanged(newFormat);
+    pendingPixelWidthHeightRatio = getPixelWidthHeightRatio(newFormat);
+    pendingRotationDegrees = getRotationDegrees(newFormat);
+  }
+
+  @Override
+  protected void onQueueInputBuffer(DecoderInputBuffer buffer) {
+    if (Util.SDK_INT < 23 && tunneling) {
+      maybeNotifyRenderedFirstFrame();
+    }
+  }
+
+  @Override
+  protected void onOutputFormatChanged(MediaCodec codec, android.media.MediaFormat outputFormat) {
+    boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT)
+        && outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM)
+        && outputFormat.containsKey(KEY_CROP_TOP);
+    currentWidth = hasCrop
+        ? outputFormat.getInteger(KEY_CROP_RIGHT) - outputFormat.getInteger(KEY_CROP_LEFT) + 1
+        : outputFormat.getInteger(MediaFormat.KEY_WIDTH);
+    currentHeight = hasCrop
+        ? outputFormat.getInteger(KEY_CROP_BOTTOM) - outputFormat.getInteger(KEY_CROP_TOP) + 1
+        : outputFormat.getInteger(MediaFormat.KEY_HEIGHT);
+    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 format changes.
+    setVideoScalingMode(codec, scalingMode);
+  }
+
+  @Override
+  protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
+      Format oldFormat, Format newFormat) {
+    return areAdaptationCompatible(oldFormat, newFormat)
+        && newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height
+        && newFormat.maxInputSize <= codecMaxValues.inputSize
+        && (codecIsAdaptive
+        || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height));
+  }
+
+  @Override
+  protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
+      ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs,
+      boolean shouldSkip) {
+    if (shouldSkip) {
+      skipOutputBuffer(codec, bufferIndex);
+      return true;
+    }
+
+    if (!renderedFirstFrame) {
+      if (Util.SDK_INT >= 21) {
+        renderOutputBufferV21(codec, bufferIndex, System.nanoTime());
+      } else {
+        renderOutputBuffer(codec, bufferIndex);
+      }
+      return true;
+    }
+
+    if (getState() != STATE_STARTED) {
+      return false;
+    }
+
+    // Compute how many microseconds it is until the buffer's presentation time.
+    long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs;
+    long earlyUs = bufferPresentationTimeUs - positionUs - elapsedSinceStartOfLoopUs;
+
+    // Compute the buffer's desired release time in nanoseconds.
+    long systemTimeNs = System.nanoTime();
+    long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000);
+
+    // Apply a timestamp adjustment, if there is one.
+    long adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime(
+        bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs);
+    earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000;
+
+    if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) {
+      // We're more than 30ms late rendering the frame.
+      dropOutputBuffer(codec, bufferIndex);
+      return true;
+    }
+
+    if (Util.SDK_INT >= 21) {
+      // Let the underlying framework time the release.
+      if (earlyUs < 50000) {
+        renderOutputBufferV21(codec, bufferIndex, adjustedReleaseTimeNs);
+        return true;
+      }
+    } else {
+      // We need to time the release ourselves.
+      if (earlyUs < 30000) {
+        if (earlyUs > 11000) {
+          // We're a little too early to render the frame. Sleep until the frame can be rendered.
+          // Note: The 11ms threshold was chosen fairly arbitrarily.
+          try {
+            // Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms.
+            Thread.sleep((earlyUs - 10000) / 1000);
+          } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+          }
+        }
+        renderOutputBuffer(codec, bufferIndex);
+        return true;
+      }
+    }
+
+    // We're either not playing, or it's not time to render the frame yet.
+    return false;
+  }
+
+  /**
+   * 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) {
+    // Drop the frame if we're more than 30ms late rendering the frame.
+    return earlyUs < -30000;
+  }
+
+  private void skipOutputBuffer(MediaCodec codec, int bufferIndex) {
+    TraceUtil.beginSection("skipVideoBuffer");
+    codec.releaseOutputBuffer(bufferIndex, false);
+    TraceUtil.endSection();
+    decoderCounters.skippedOutputBufferCount++;
+  }
+
+  private void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
+    TraceUtil.beginSection("dropVideoBuffer");
+    codec.releaseOutputBuffer(bufferIndex, false);
+    TraceUtil.endSection();
+    decoderCounters.droppedOutputBufferCount++;
+    droppedFrames++;
+    consecutiveDroppedFrameCount++;
+    decoderCounters.maxConsecutiveDroppedOutputBufferCount = Math.max(consecutiveDroppedFrameCount,
+        decoderCounters.maxConsecutiveDroppedOutputBufferCount);
+    if (droppedFrames == maxDroppedFramesToNotify) {
+      maybeNotifyDroppedFrames();
+    }
+  }
+
+  private void renderOutputBuffer(MediaCodec codec, int bufferIndex) {
+    maybeNotifyVideoSizeChanged();
+    TraceUtil.beginSection("releaseOutputBuffer");
+    codec.releaseOutputBuffer(bufferIndex, true);
+    TraceUtil.endSection();
+    decoderCounters.renderedOutputBufferCount++;
+    consecutiveDroppedFrameCount = 0;
+    maybeNotifyRenderedFirstFrame();
+  }
+
+  @TargetApi(21)
+  private void renderOutputBufferV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) {
+    maybeNotifyVideoSizeChanged();
+    TraceUtil.beginSection("releaseOutputBuffer");
+    codec.releaseOutputBuffer(bufferIndex, releaseTimeNs);
+    TraceUtil.endSection();
+    decoderCounters.renderedOutputBufferCount++;
+    consecutiveDroppedFrameCount = 0;
+    maybeNotifyRenderedFirstFrame();
+  }
+
+  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 clearLastReportedVideoSize() {
+    lastReportedWidth = Format.NO_VALUE;
+    lastReportedHeight = Format.NO_VALUE;
+    lastReportedPixelWidthHeightRatio = Format.NO_VALUE;
+    lastReportedUnappliedRotationDegrees = Format.NO_VALUE;
+  }
+
+  private void maybeNotifyVideoSizeChanged() {
+    if (lastReportedWidth != currentWidth || lastReportedHeight != currentHeight
+        || lastReportedUnappliedRotationDegrees != currentUnappliedRotationDegrees
+        || lastReportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) {
+      eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees,
+          currentPixelWidthHeightRatio);
+      lastReportedWidth = currentWidth;
+      lastReportedHeight = currentHeight;
+      lastReportedUnappliedRotationDegrees = currentUnappliedRotationDegrees;
+      lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio;
+    }
+  }
+
+  private void maybeNotifyDroppedFrames() {
+    if (droppedFrames > 0) {
+      long now = SystemClock.elapsedRealtime();
+      long elapsedMs = now - droppedFrameAccumulationStartTimeMs;
+      eventDispatcher.droppedFrames(droppedFrames, elapsedMs);
+      droppedFrames = 0;
+      droppedFrameAccumulationStartTimeMs = now;
+    }
+  }
+
+  @SuppressLint("InlinedApi")
+  private static MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues,
+      boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) {
+    MediaFormat frameworkMediaFormat = format.getFrameworkMediaFormatV16();
+    // Set the maximum adaptive video dimensions.
+    frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width);
+    frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height);
+    // Set the maximum input size.
+    if (codecMaxValues.inputSize != Format.NO_VALUE) {
+      frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize);
+    }
+    // Set FRC workaround.
+    if (deviceNeedsAutoFrcWorkaround) {
+      frameworkMediaFormat.setInteger("auto-frc", 0);
+    }
+    // Configure tunneling if enabled.
+    if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
+      configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId);
+    }
+    return frameworkMediaFormat;
+  }
+
+  @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 {@link CodecMaxValues} suitable for configuring a codec for {@code format} in a way
+   * that will allow possible adaptation to other compatible formats in {@code streamFormats}.
+   *
+   * @param codecInfo Information about the {@link MediaCodec} being configured.
+   * @param format The format for which the codec is being configured.
+   * @param streamFormats The possible stream formats.
+   * @return Suitable {@link CodecMaxValues}.
+   * @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
+   */
+  private static CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format,
+      Format[] streamFormats) throws DecoderQueryException {
+    int maxWidth = format.width;
+    int maxHeight = format.height;
+    int maxInputSize = getMaxInputSize(format);
+    if (streamFormats.length == 1) {
+      // The single entry in streamFormats must correspond to the format for which the codec is
+      // being configured.
+      return new CodecMaxValues(maxWidth, maxHeight, maxInputSize);
+    }
+    boolean haveUnknownDimensions = false;
+    for (Format streamFormat : streamFormats) {
+      if (areAdaptationCompatible(format, streamFormat)) {
+        haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE
+            || streamFormat.height == Format.NO_VALUE);
+        maxWidth = Math.max(maxWidth, streamFormat.width);
+        maxHeight = Math.max(maxHeight, streamFormat.height);
+        maxInputSize = Math.max(maxInputSize, getMaxInputSize(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,
+            getMaxInputSize(format.sampleMimeType, maxWidth, maxHeight));
+        Log.w(TAG, "Codec max resolution adjusted to: " + maxWidth + "x" + maxHeight);
+      }
+    }
+    return new CodecMaxValues(maxWidth, maxHeight, maxInputSize);
+  }
+
+  /**
+   * Returns a maximum video size to use when configuring a codec for {@code format} in a way
+   * that will allow possible adaptation to other compatible formats that are expected to have the
+   * same aspect ratio, but whose sizes are unknown.
+   *
+   * @param codecInfo Information about the {@link MediaCodec} being configured.
+   * @param format The format for which the codec is being configured.
+   * @return The maximum video size to use, or null if the size of {@code format} should be used.
+   * @throws DecoderQueryException If an error occurs querying {@code codecInfo}.
+   */
+  private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format)
+      throws DecoderQueryException {
+    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 {
+        // 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);
+        }
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Returns a maximum input size for a given format.
+   *
+   * @param format The format.
+   * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be
+   *     determined.
+   */
+  private static int getMaxInputSize(Format format) {
+    if (format.maxInputSize != Format.NO_VALUE) {
+      // The format defines an explicit maximum input size.
+      return format.maxInputSize;
+    }
+    return getMaxInputSize(format.sampleMimeType, format.width, format.height);
+  }
+
+  /**
+   * Returns a maximum input size for a given mime type, width and height.
+   *
+   * @param sampleMimeType The format mime type.
+   * @param width The width in pixels.
+   * @param height The height in pixels.
+   * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be
+   *     determined.
+   */
+  private static int getMaxInputSize(String sampleMimeType, int width, int height) {
+    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)) {
+          // The Sony BRAVIA 4k TV has input buffers that are too small for the calculated 4k video
+          // maximum input size, so use the default value.
+          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);
+  }
+
+  private static void setVideoScalingMode(MediaCodec codec, int scalingMode) {
+    codec.setVideoScalingMode(scalingMode);
+  }
+
+  /**
+   * Returns whether the device is known to enable frame-rate conversion logic that negatively
+   * impacts ExoPlayer.
+   * <p>
+   * If true is returned then we explicitly disable the feature.
+   *
+   * @return True if the device is known to enable frame-rate conversion logic that negatively
+   *     impacts ExoPlayer. False otherwise.
+   */
+  private static boolean deviceNeedsAutoFrcWorkaround() {
+    // nVidia Shield prior to M tries to adjust the playback rate to better map the frame-rate of
+    // content to the refresh rate of the display. For example playback of 23.976fps content is
+    // adjusted to play at 1.001x speed when the output display is 60Hz. Unfortunately the
+    // implementation causes ExoPlayer's reported playback position to drift out of sync. Captions
+    // also lose sync [Internal: b/26453592].
+    return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER);
+  }
+
+  /**
+   * Returns whether an adaptive codec with suitable {@link CodecMaxValues} will support adaptation
+   * between two {@link Format}s.
+   *
+   * @param first The first format.
+   * @param second The second format.
+   * @return Whether an adaptive codec with suitable {@link CodecMaxValues} will support adaptation
+   *     between two {@link Format}s.
+   */
+  private static boolean areAdaptationCompatible(Format first, Format second) {
+    return first.sampleMimeType.equals(second.sampleMimeType)
+        && getRotationDegrees(first) == getRotationDegrees(second);
+  }
+
+  private static float getPixelWidthHeightRatio(Format format) {
+    return format.pixelWidthHeightRatio == Format.NO_VALUE ? 1 : format.pixelWidthHeightRatio;
+  }
+
+  private static int getRotationDegrees(Format format) {
+    return format.rotationDegrees == Format.NO_VALUE ? 0 : format.rotationDegrees;
+  }
+
+  private 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 {
+
+    private OnFrameRenderedListenerV23(MediaCodec codec) {
+      codec.setOnFrameRenderedListener(this, new Handler());
+    }
+
+    @Override
+    public void onFrameRendered(MediaCodec codec, long presentationTimeUs, long nanoTime) {
+      if (this != tunnelingOnFrameRenderedListener) {
+        // Stale event.
+        return;
+      }
+      maybeNotifyRenderedFirstFrame();
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.video;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.view.Choreographer;
+import android.view.Choreographer.FrameCallback;
+import android.view.WindowManager;
+import com.google.android.exoplayer2.C;
+
+/**
+ * Makes a best effort to adjust frame release timestamps for a smoother visual result.
+ */
+@TargetApi(16)
+public final class VideoFrameReleaseTimeHelper {
+
+  private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500;
+  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 VSyncSampler vsyncSampler;
+  private final boolean useDefaultDisplayVsync;
+  private final long vsyncDurationNs;
+  private final 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 smoothes frame release timestamps but does not align them with
+   * the default display's vsync signal.
+   */
+  public VideoFrameReleaseTimeHelper() {
+    this(-1 /* Value unused */, false);
+  }
+
+  /**
+   * Constructs an instance that smoothes frame release timestamps and aligns them with the default
+   * display's vsync signal.
+   *
+   * @param context A context from which information about the default display can be retrieved.
+   */
+  public VideoFrameReleaseTimeHelper(Context context) {
+    this(getDefaultDisplayRefreshRate(context), true);
+  }
+
+  private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate,
+      boolean useDefaultDisplayVsync) {
+    this.useDefaultDisplayVsync = useDefaultDisplayVsync;
+    if (useDefaultDisplayVsync) {
+      vsyncSampler = VSyncSampler.getInstance();
+      vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate);
+      vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100;
+    } else {
+      vsyncSampler = null;
+      vsyncDurationNs = -1; // Value unused.
+      vsyncOffsetNs = -1; // Value unused.
+    }
+  }
+
+  /**
+   * Enables the helper.
+   */
+  public void enable() {
+    haveSync = false;
+    if (useDefaultDisplayVsync) {
+      vsyncSampler.addObserver();
+    }
+  }
+
+  /**
+   * Disables the helper.
+   */
+  public void disable() {
+    if (useDefaultDisplayVsync) {
+      vsyncSampler.removeObserver();
+    }
+  }
+
+  /**
+   * Adjusts a frame release timestamp.
+   *
+   * @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;
+      onSynced();
+    }
+
+    lastFramePresentationTimeUs = framePresentationTimeUs;
+    pendingAdjustedFrameTimeNs = adjustedFrameTimeNs;
+
+    if (vsyncSampler == null || vsyncSampler.sampledVsyncTimeNs == 0) {
+      return adjustedReleaseTimeNs;
+    }
+
+    // Find the timestamp of the closest vsync. This is the vsync that we're targeting.
+    long snappedTimeNs = closestVsync(adjustedReleaseTimeNs,
+        vsyncSampler.sampledVsyncTimeNs, vsyncDurationNs);
+    // Apply an offset so that we release before the target vsync, but after the previous one.
+    return snappedTimeNs - vsyncOffsetNs;
+  }
+
+  protected void onSynced() {
+    // Do nothing.
+  }
+
+  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;
+  }
+
+  private static float getDefaultDisplayRefreshRate(Context context) {
+    WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+    return manager.getDefaultDisplay().getRefreshRate();
+  }
+
+  /**
+   * 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() {
+      choreographerOwnerThread = new HandlerThread("ChoreographerOwner:Handler");
+      choreographerOwnerThread.start();
+      handler = new Handler(choreographerOwnerThread.getLooper(), 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 = 0;
+      }
+    }
+
+  }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/google/android/exoplayer2/video/VideoRendererEventListener.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer2.video;
+
+import android.os.Handler;
+import android.os.SystemClock;
+import android.view.Surface;
+import android.view.TextureView;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.Renderer;
+import com.google.android.exoplayer2.decoder.DecoderCounters;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * Listener of video {@link Renderer} events.
+ */
+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.
+   */
+  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.
+   */
+  void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs,
+      long initializationDurationMs);
+
+  /**
+   * Called when the format of the media being consumed by the renderer changes.
+   *
+   * @param format The new format.
+   */
+  void onVideoInputFormatChanged(Format format);
+
+  /**
+   * 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.
+   */
+  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.
+   */
+  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}.
+   */
+  void onRenderedFirstFrame(Surface surface);
+
+  /**
+   * Called when the renderer is disabled.
+   *
+   * @param counters {@link DecoderCounters} that were updated by the renderer.
+   */
+  void onVideoDisabled(DecoderCounters counters);
+
+  /**
+   * Dispatches events to a {@link VideoRendererEventListener}.
+   */
+  final class EventDispatcher {
+
+    private final Handler handler;
+    private final VideoRendererEventListener listener;
+
+    /**
+     * @param handler A handler for dispatching events, or null if creating a dummy instance.
+     * @param listener The listener to which events should be dispatched, or null if creating a
+     *     dummy instance.
+     */
+    public EventDispatcher(Handler handler, VideoRendererEventListener listener) {
+      this.handler = listener != null ? Assertions.checkNotNull(handler) : null;
+      this.listener = listener;
+    }
+
+    /**
+     * Invokes {@link VideoRendererEventListener#onVideoEnabled(DecoderCounters)}.
+     */
+    public void enabled(final DecoderCounters decoderCounters) {
+      if (listener != null) {
+        handler.post(new Runnable() {
+          @Override
+          public void run() {
+            listener.onVideoEnabled(decoderCounters);
+          }
+        });
+      }
+    }
+
+    /**
+     * Invokes {@link VideoRendererEventListener#onVideoDecoderInitialized(String, long, long)}.
+     */
+    public void decoderInitialized(final String decoderName,
+        final long initializedTimestampMs, final long initializationDurationMs) {
+      if (listener != null) {
+        handler.post(new Runnable() {
+          @Override
+          public void run() {
+            listener.onVideoDecoderInitialized(decoderName, initializedTimestampMs,
+                initializationDurationMs);
+          }
+        });
+      }
+    }
+
+    /**
+     * Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format)}.
+     */
+    public void inputFormatChanged(final Format format) {
+      if (listener != null) {
+        handler.post(new Runnable() {
+          @Override
+          public void run() {
+            listener.onVideoInputFormatChanged(format);
+          }
+        });
+      }
+    }
+
+    /**
+     * Invokes {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
+     */
+    public void droppedFrames(final int droppedFrameCount, final long elapsedMs) {
+      if (listener != null) {
+        handler.post(new Runnable()  {
+          @Override
+          public void run() {
+            listener.onDroppedFrames(droppedFrameCount, elapsedMs);
+          }
+        });
+      }
+    }
+
+    /**
+     * Invokes {@link VideoRendererEventListener#onVideoSizeChanged(int, int, int, float)}.
+     */
+    public void videoSizeChanged(final int width, final int height,
+        final int unappliedRotationDegrees, final float pixelWidthHeightRatio) {
+      if (listener != null) {
+        handler.post(new Runnable()  {
+          @Override
+          public void run() {
+            listener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
+                pixelWidthHeightRatio);
+          }
+        });
+      }
+    }
+
+    /**
+     * Invokes {@link VideoRendererEventListener#onRenderedFirstFrame(Surface)}.
+     */
+    public void renderedFirstFrame(final Surface surface) {
+      if (listener != null) {
+        handler.post(new Runnable()  {
+          @Override
+          public void run() {
+            listener.onRenderedFirstFrame(surface);
+          }
+        });
+      }
+    }
+
+    /**
+     * Invokes {@link VideoRendererEventListener#onVideoDisabled(DecoderCounters)}.
+     */
+    public void disabled(final DecoderCounters counters) {
+      if (listener != null) {
+        handler.post(new Runnable() {
+          @Override
+          public void run() {
+            counters.ensureUpdated();
+            listener.onVideoDisabled(counters);
+          }
+        });
+      }
+    }
+
+  }
+
+}