diff --git a/README.md b/README.md index c6935075ac..f1499b23dd 100644 --- a/README.md +++ b/README.md @@ -16,18 +16,16 @@ Read news, hints and tips on the [news][] page. [news]: https://google.github.io/ExoPlayer/news.html -## Developer guide ## +## Documentation ## -The [developer guide][] provides a wealth of information to help you get +* The [developer guide][] provides a wealth of information to help you get started. +* The [class reference][] documents the ExoPlayer library classes. +* The [release notes][] document the major changes in each release. [developer guide]: https://google.github.io/ExoPlayer/guide.html - -## Reference documentation ## - -The [class reference][] documents the ExoPlayer library classes. - [class reference]: https://google.github.io/ExoPlayer/doc/reference +[release notes]: https://github.com/google/ExoPlayer/blob/dev/RELEASENOTES.md ## Project branches ## diff --git a/RELEASENOTES.md b/RELEASENOTES.md new file mode 100644 index 0000000000..1dfc9ac394 --- /dev/null +++ b/RELEASENOTES.md @@ -0,0 +1,14 @@ +# Release notes # + +### r1.3.2 ### + +* DataSource improvements: `DefaultUriDataSource` now handles http://, https://, file://, asset:// + and content:// URIs automatically. It also handles file:///android_asset/* URIs, and file paths + like /path/to/media.mp4 where the scheme is omitted. +* HLS: Fix for some ID3 events being dropped. +* HLS: Correctly handle 0x0 and floating point RESOLUTION tags. +* Mp3Extractor: robustness improvements. + +### r1.3.1 ### + +* No notes provided. diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index f774c2aacc..2c0158c23f 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index e824ed0115..5dd1eb8d32 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -233,19 +233,19 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, audioCapabilities); case DemoUtil.TYPE_M4A: // There are no file format differences between M4A and MP4. case DemoUtil.TYPE_MP4: - return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView, + return new ExtractorRendererBuilder(this, userAgent, contentUri, debugTextView, new Mp4Extractor()); case DemoUtil.TYPE_MP3: - return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView, + return new ExtractorRendererBuilder(this, userAgent, contentUri, debugTextView, new Mp3Extractor()); case DemoUtil.TYPE_TS: - return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView, + return new ExtractorRendererBuilder(this, userAgent, contentUri, debugTextView, new TsExtractor(0, audioCapabilities)); case DemoUtil.TYPE_AAC: - return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView, + return new ExtractorRendererBuilder(this, userAgent, contentUri, debugTextView, new AdtsExtractor()); case DemoUtil.TYPE_WEBM: - return new ExtractorRendererBuilder(userAgent, contentUri, debugTextView, + return new ExtractorRendererBuilder(this, userAgent, contentUri, debugTextView, new WebmExtractor()); default: throw new IllegalStateException("Unsupported type: " + contentType); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java index 2b0de68c3d..bde74928a0 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java @@ -130,7 +130,7 @@ public class DashRendererBuilder implements RendererBuilder, this.player = player; this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); - manifestDataSource = new DefaultUriDataSource(userAgent, null); + manifestDataSource = new DefaultUriDataSource(context, userAgent); manifestFetcher = new ManifestFetcher(url, manifestDataSource, parser); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); @@ -232,10 +232,10 @@ public class DashRendererBuilder implements RendererBuilder, videoRenderer = null; debugRenderer = null; } else { - DataSource videoDataSource = new DefaultUriDataSource(userAgent, bandwidthMeter); - ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, videoAdaptationSetIndex, - videoRepresentationIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter), - LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset); + DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); + ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, + videoAdaptationSetIndex, videoRepresentationIndices, videoDataSource, + new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset); ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_VIDEO); @@ -249,7 +249,7 @@ public class DashRendererBuilder implements RendererBuilder, List audioChunkSourceList = new ArrayList(); List audioTrackNameList = new ArrayList(); if (audioAdaptationSet != null) { - DataSource audioDataSource = new DefaultUriDataSource(userAgent, bandwidthMeter); + DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); List audioRepresentations = audioAdaptationSet.representations; List codecs = new ArrayList(); @@ -304,7 +304,7 @@ public class DashRendererBuilder implements RendererBuilder, } // Build the text chunk sources. - DataSource textDataSource = new DefaultUriDataSource(userAgent, bandwidthMeter); + DataSource textDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); FormatEvaluator textEvaluator = new FormatEvaluator.FixedEvaluator(); List textChunkSourceList = new ArrayList(); List textTrackNameList = new ArrayList(); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java index c9cff47697..3437678e04 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/ExtractorRendererBuilder.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer.extractor.ExtractorSampleSource; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DefaultUriDataSource; +import android.content.Context; import android.media.MediaCodec; import android.net.Uri; import android.widget.TextView; @@ -36,13 +37,15 @@ public class ExtractorRendererBuilder implements RendererBuilder { private static final int BUFFER_SIZE = 10 * 1024 * 1024; + private final Context context; private final String userAgent; private final Uri uri; private final TextView debugTextView; private final Extractor extractor; - public ExtractorRendererBuilder(String userAgent, Uri uri, TextView debugTextView, - Extractor extractor) { + public ExtractorRendererBuilder(Context context, String userAgent, Uri uri, + TextView debugTextView, Extractor extractor) { + this.context = context; this.userAgent = userAgent; this.uri = uri; this.debugTextView = debugTextView; @@ -52,7 +55,7 @@ public class ExtractorRendererBuilder implements RendererBuilder { @Override public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { // Build the video and audio renderers. - DataSource dataSource = new DefaultUriDataSource(userAgent, null); + DataSource dataSource = new DefaultUriDataSource(context, userAgent); ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, extractor, 2, BUFFER_SIZE); MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java index 171433c0d8..8eb762e218 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/HlsRendererBuilder.java @@ -76,8 +76,8 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback playlistFetcher = - new ManifestFetcher(url, new DefaultUriDataSource(userAgent, null), parser); + ManifestFetcher playlistFetcher = new ManifestFetcher(url, + new DefaultUriDataSource(context, userAgent), parser); playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this); } @@ -103,7 +103,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback(url + "/Manifest", + manifestFetcher = new ManifestFetcher(manifestUrl, new DefaultHttpDataSource(userAgent, null), parser); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); } @@ -160,7 +164,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, videoRenderer = null; debugRenderer = null; } else { - DataSource videoDataSource = new DefaultUriDataSource(userAgent, bandwidthMeter); + DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); ChunkSource videoChunkSource = new SmoothStreamingChunkSource(manifestFetcher, videoStreamElementIndex, videoTrackIndices, videoDataSource, new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS); @@ -184,7 +188,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, } else { audioTrackNames = new String[audioStreamElementCount]; ChunkSource[] audioChunkSources = new ChunkSource[audioStreamElementCount]; - DataSource audioDataSource = new DefaultUriDataSource(userAgent, bandwidthMeter); + DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); FormatEvaluator audioFormatEvaluator = new FormatEvaluator.FixedEvaluator(); audioStreamElementCount = 0; for (int i = 0; i < manifest.streamElements.length; i++) { @@ -215,7 +219,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, } else { textTrackNames = new String[textStreamElementCount]; ChunkSource[] textChunkSources = new ChunkSource[textStreamElementCount]; - DataSource ttmlDataSource = new DefaultUriDataSource(userAgent, bandwidthMeter); + DataSource ttmlDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); FormatEvaluator ttmlFormatEvaluator = new FormatEvaluator.FixedEvaluator(); textStreamElementCount = 0; for (int i = 0; i < manifest.streamElements.length; i++) { diff --git a/library/build.gradle b/library/build.gradle index da79b37a4c..674bf50de1 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -74,7 +74,7 @@ publish { userOrg = 'google' groupId = 'com.google.android.exoplayer' artifactId = 'exoplayer' - version = 'r1.3.1' + version = 'r1.3.2' description = 'The ExoPlayer library.' website = 'https://github.com/google/ExoPlayer' } diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java index 5b1eb77536..af7decdbd8 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java @@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo { /** * The version of the library, expressed as a string. */ - public static final String VERSION = "1.3.1"; + public static final String VERSION = "1.3.2"; /** * The version of the library, expressed as an integer. @@ -34,7 +34,7 @@ public class ExoPlayerLibraryInfo { * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the * corresponding integer version 001002003. */ - public static final int VERSION_INT = 001003001; + public static final int VERSION_INT = 001003002; /** * Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions} diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java index 96271a2774..a5bc408751 100644 --- a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java @@ -574,6 +574,7 @@ public final class AudioTrack { submittedBytes = 0; temporaryBufferSize = 0; startMediaTimeUs = START_NOT_SET; + latencyUs = 0; resetSyncParams(); int playState = audioTrack.getPlayState(); if (playState == android.media.AudioTrack.PLAYSTATE_PLAYING) { @@ -647,9 +648,10 @@ public final class AudioTrack { } } - if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) { - // Don't use AudioTrack.getTimestamp() on AC-3 tracks, as it gives an incorrect timestamp. - audioTimestampSet = !isAc3 && audioTrackUtil.updateTimestamp(); + // Don't sample the timestamp and latency if this is an AC-3 passthrough AudioTrack, as the + // returned values cause audio/video synchronization to be incorrect. + if (!isAc3 && systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) { + audioTimestampSet = audioTrackUtil.updateTimestamp(); if (audioTimestampSet) { // Perform sanity checks on the timestamp. long audioTimestampUs = audioTrackUtil.getTimestampNanoTime() / 1000; diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/VideoFormatSelectorUtil.java b/library/src/main/java/com/google/android/exoplayer/chunk/VideoFormatSelectorUtil.java index de156ea0db..03adf45819 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/VideoFormatSelectorUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/VideoFormatSelectorUtil.java @@ -108,7 +108,7 @@ public final class VideoFormatSelectorUtil { // 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 in a second pass. - if (format.width != -1 && format.height != -1) { + if (format.width > 0 && format.height > 0) { Point maxVideoSizeInViewport = getMaxVideoSizeInViewport(orientationMayChange, viewportWidth, viewportHeight, format.width, format.height); int videoPixels = format.width * format.height; @@ -126,7 +126,7 @@ public final class VideoFormatSelectorUtil { // viewport. for (int i = selectedIndexList.size() - 1; i >= 0; i--) { Format format = formatWrappers.get(i).getFormat(); - if (format.width != -1 && format.height != -1 + if (format.width > 0 && format.height > 0 && format.width * format.height > maxVideoPixelsToRetain) { selectedIndexList.remove(i); } @@ -150,7 +150,7 @@ public final class VideoFormatSelectorUtil { // Filtering format because it's HD. return false; } - if (format.width != -1 && format.height != -1) { + if (format.width > 0 && format.height > 0) { // TODO: Use MediaCodecUtil.isSizeAndRateSupportedV21 on API levels >= 21 if we know the // mimeType of the media samples within the container. Remove the assumption that we're // dealing with H.264. diff --git a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java index 2855678b5f..3187aa4788 100644 --- a/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java +++ b/library/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java @@ -213,6 +213,18 @@ public class StreamingDrmSessionManager implements DrmSessionManager { return mediaDrm.getPropertyString(key); } + /** + * Provides access to {@link MediaDrm#setPropertyString(String, String)}. + *

+ * 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)}. *

@@ -225,6 +237,18 @@ public class StreamingDrmSessionManager implements DrmSessionManager { return mediaDrm.getPropertyByteArray(key); } + /** + * Provides access to {@link MediaDrm#setPropertyByteArray(String, byte[])}. + *

+ * This method may be called when the manager is in any state. + * + * @param key The property to write. + * @param value The value to write. + */ + public final void setPropertyByteArray(String key, byte[] value) { + mediaDrm.setPropertyByteArray(key, value); + } + @Override public void open(DrmInitData drmInitData) { if (++openCount != 1) { diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java index 5842b19ce3..0b82bede91 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/Mp3Extractor.java @@ -45,6 +45,9 @@ public final class Mp3Extractor implements Extractor { private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); 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 XING_HEADER = Util.getIntegerCodeForString("Xing"); + private static final int INFO_HEADER = Util.getIntegerCodeForString("Info"); + private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI"); /** * Theoretical maximum frame size for an MPEG audio stream, which occurs when playing a Layer 2 @@ -172,6 +175,15 @@ public final class Mp3Extractor implements Extractor { } private long synchronize(ExtractorInput extractorInput) throws IOException, InterruptedException { + if (extractorInput.getPosition() == 0) { + // Before preparation completes, retrying loads from the start, so clear any buffered data. + inputBuffer.reset(); + } else { + // After preparation completes, retrying resumes loading from the old position, so return to + // the start of buffered data to parse it again. + inputBuffer.returnToMark(); + } + long startPosition = getPosition(extractorInput, inputBuffer); // Skip any ID3 header at the start of the file. @@ -195,6 +207,7 @@ public final class Mp3Extractor implements Extractor { inputBuffer.mark(); long headerPosition = startPosition; int validFrameCount = 0; + int candidateSynchronizedHeaderData = 0; while (true) { if (headerPosition - startPosition >= MAX_BYTES_TO_SEARCH) { throw new ParserException("Searched too many bytes while resynchronizing."); @@ -207,11 +220,11 @@ public final class Mp3Extractor implements Extractor { scratch.setPosition(0); int headerData = scratch.readInt(); int frameSize; - if ((synchronizedHeaderData != 0 - && (headerData & HEADER_MASK) != (synchronizedHeaderData & HEADER_MASK)) + if ((candidateSynchronizedHeaderData != 0 + && (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK)) || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == -1) { validFrameCount = 0; - synchronizedHeaderData = 0; + candidateSynchronizedHeaderData = 0; // Try reading a header starting at the next byte. inputBuffer.returnToMark(); @@ -223,7 +236,7 @@ public final class Mp3Extractor implements Extractor { if (validFrameCount == 0) { MpegAudioHeader.populateHeader(headerData, synchronizedHeader); - synchronizedHeaderData = headerData; + candidateSynchronizedHeaderData = headerData; } // The header was valid and matching (if appropriate). Check another or end synchronization. @@ -238,22 +251,9 @@ public final class Mp3Extractor implements Extractor { // The input buffer read position is now synchronized. inputBuffer.returnToMark(); + synchronizedHeaderData = candidateSynchronizedHeaderData; if (seeker == null) { - ParsableByteArray frame = - inputBuffer.getParsableByteArray(extractorInput, synchronizedHeader.frameSize); - seeker = XingSeeker.create(synchronizedHeader, frame, headerPosition, - extractorInput.getLength()); - if (seeker == null) { - seeker = VbriSeeker.create(synchronizedHeader, frame, headerPosition); - } - if (seeker == null) { - inputBuffer.returnToMark(); - seeker = new ConstantBitrateSeeker(headerPosition, synchronizedHeader.bitrate * 1000, - extractorInput.getLength()); - } else { - // Discard the frame that was parsed for seeking metadata. - inputBuffer.mark(); - } + setupSeeker(extractorInput, headerPosition); extractorOutput.seekMap(seeker); trackOutput.format(MediaFormat.createAudioFormat( MIME_TYPE_BY_LAYER[synchronizedHeader.layerIndex], MAX_FRAME_SIZE_BYTES, @@ -264,6 +264,93 @@ public final class Mp3Extractor implements Extractor { return headerPosition; } + /** + * Sets {@link #seeker} to seek using metadata from {@link #inputBuffer}, which should have its + * position set to the start of the first frame in the stream. On returning, + * {@link #inputBuffer}'s position and mark will be set to the start of the first frame of audio. + * + * @param extractorInput Source of data for {@link #inputBuffer}. + * @param headerPosition Position (byte offset) of the synchronized header in the stream. + * @throws IOException Thrown if there was an error reading from the stream. Not expected if the + * next two frames were already read during synchronization. + * @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if + * the next two frames were already read during synchronization. + */ + private void setupSeeker(ExtractorInput extractorInput, long headerPosition) + throws IOException, InterruptedException { + // Try to set up seeking based on a XING or VBRI header. + if (parseSeekerFrame(extractorInput, headerPosition, extractorInput.getLength())) { + // Discard the parsed header so we start reading from the first audio frame. + inputBuffer.mark(); + if (seeker != null) { + return; + } + + // If there was a header but it was not usable, synchronize to the next frame so we don't + // use an invalid bitrate for CBR seeking. This read is guaranteed to succeed if the frame was + // already read during synchronization. + inputBuffer.read(extractorInput, scratch.data, 0, 4); + scratch.setPosition(0); + headerPosition += synchronizedHeader.frameSize; + MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); + } + + inputBuffer.returnToMark(); + seeker = new ConstantBitrateSeeker(headerPosition, synchronizedHeader.bitrate * 1000, + extractorInput.getLength()); + } + + /** + * Consumes the frame at {@link #inputBuffer}'s current position, advancing it to the next frame. + * The mark is not modified. {@link #seeker} will be assigned based on seeking metadata in the + * frame. If there is no seeking metadata, returns {@code false} and sets {@link #seeker} to null. + * If seeking metadata is present and unusable, returns {@code true} and sets {@link #seeker} to + * null. Otherwise, returns {@code true} and assigns {@link #seeker}. + */ + private boolean parseSeekerFrame(ExtractorInput extractorInput, long headerPosition, + long inputLength) throws IOException, InterruptedException { + // Read the first frame so it can be parsed for seeking metadata. + inputBuffer.mark(); + seeker = null; + ParsableByteArray frame = + inputBuffer.getParsableByteArray(extractorInput, synchronizedHeader.frameSize); + + // Check if there is a XING header. + int xingBase; + if ((synchronizedHeader.version & 1) == 1) { + // MPEG 1. + if (synchronizedHeader.channels != 1) { + xingBase = 32; + } else { + xingBase = 17; + } + } else { + // MPEG 2 or 2.5. + if (synchronizedHeader.channels != 1) { + xingBase = 17; + } else { + xingBase = 9; + } + } + frame.setPosition(4 + xingBase); + int headerData = frame.readInt(); + if (headerData == XING_HEADER || headerData == INFO_HEADER) { + seeker = XingSeeker.create(synchronizedHeader, frame, headerPosition, inputLength); + return true; + } + + // 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, headerPosition); + return true; + } + + // Neither header is present. + return false; + } + /** Returns the reading position of {@code bufferingInput} relative to the extractor's stream. */ private static long getPosition(ExtractorInput extractorInput, BufferingInput bufferingInput) { return extractorInput.getPosition() - bufferingInput.getAvailableByteCount(); diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/VbriSeeker.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/VbriSeeker.java index 4413edaaa7..9d096607cb 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/VbriSeeker.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/VbriSeeker.java @@ -23,23 +23,20 @@ import com.google.android.exoplayer.util.Util; */ /* package */ final class VbriSeeker implements Mp3Extractor.Seeker { - private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI"); - /** - * If {@code frame} contains a VBRI header and it is usable for seeking, returns a - * {@link VbriSeeker} for seeking in the containing stream. Otherwise, returns {@code null}, which - * indicates that the information in the frame was not a VBRI header, or was unusable for seeking. + * 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. + * @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 basePosition = position + mpegAudioHeader.frameSize; - - // Read the VBRI header. - frame.skipBytes(32); - int headerData = frame.readInt(); - if (headerData != VBRI_HEADER) { - return null; - } + public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, + long position) { frame.skipBytes(10); int numFrames = frame.readInt(); if (numFrames <= 0) { @@ -83,7 +80,7 @@ import com.google.android.exoplayer.util.Util; segmentIndex++; } - return new VbriSeeker(timesUs, offsets, basePosition, durationUs); + return new VbriSeeker(timesUs, offsets, position + mpegAudioHeader.frameSize, durationUs); } private final long[] timesUs; diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java index bbc3ae4c8a..7ddb006705 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/mp3/XingSeeker.java @@ -24,13 +24,18 @@ import com.google.android.exoplayer.util.Util; */ /* package */ final class XingSeeker implements Mp3Extractor.Seeker { - private static final int XING_HEADER = Util.getIntegerCodeForString("Xing"); - private static final int INFO_HEADER = Util.getIntegerCodeForString("Info"); - /** - * If {@code frame} contains a XING header and it is usable for seeking, returns a - * {@link XingSeeker} for seeking in the containing stream. Otherwise, returns {@code null}, which - * indicates that the information in the frame was not a XING header, or was unusable for seeking. + * 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) { @@ -38,29 +43,6 @@ import com.google.android.exoplayer.util.Util; int sampleRate = mpegAudioHeader.sampleRate; long firstFramePosition = position + mpegAudioHeader.frameSize; - // Skip to the XING header. - int xingBase; - if ((mpegAudioHeader.version & 1) == 1) { - // MPEG 1. - if (mpegAudioHeader.channels != 1) { - xingBase = 32; - } else { - xingBase = 17; - } - } else { - // MPEG 2 or 2.5. - if (mpegAudioHeader.channels != 1) { - xingBase = 17; - } else { - xingBase = 9; - } - } - frame.skipBytes(4 + xingBase); - int headerData = frame.readInt(); - if (headerData != XING_HEADER && headerData != INFO_HEADER) { - return null; - } - int flags = frame.readInt(); // Frame count, size and table of contents are required to use this header. if ((flags & 0x07) != 0x07) { diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java index 670f3ea8ec..684ada760f 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ts/TsExtractor.java @@ -52,16 +52,17 @@ public final class TsExtractor implements Extractor, SeekMap { private static final long MAX_PTS = 0x1FFFFFFFFL; private final ParsableByteArray tsPacketBuffer; - private final SparseBooleanArray streamTypes; - private final SparseBooleanArray allowedPassthroughStreamTypes; - private final SparseArray tsPayloadReaders; // Indexed by pid - private final long firstSampleTimestampUs; private final ParsableBitArray tsScratch; + private final long firstSampleTimestampUs; + /* package */ final SparseBooleanArray streamTypes; + /* package */ final SparseBooleanArray allowedPassthroughStreamTypes; + /* package */ final SparseArray tsPayloadReaders; // Indexed by pid // Accessed only by the loading thread. private ExtractorOutput output; private long timestampOffsetUs; private long lastPts; + /* package */ Id3Reader id3Reader; public TsExtractor() { this(0, null); @@ -307,9 +308,11 @@ public final class TsExtractor implements Extractor, SeekMap { // Skip the descriptors. data.skipBytes(programInfoLength); - // Setup an ID3 track regardless of whether there's a corresponding entry, in case one - // appears intermittently during playback. See b/20261500. - Id3Reader id3Reader = new Id3Reader(output.track(TS_STREAM_TYPE_ID3)); + if (id3Reader == null) { + // Setup an ID3 track regardless of whether there's a corresponding entry, in case one + // appears intermittently during playback. See b/20261500. + id3Reader = new Id3Reader(output.track(TS_STREAM_TYPE_ID3)); + } int entriesSize = sectionLength - 9 /* Size of the rest of the fields before descriptors */ - programInfoLength - 4 /* CRC size */; @@ -532,11 +535,11 @@ public final class TsExtractor implements Extractor, SeekMap { timeUs = 0; if (ptsFlag) { pesScratch.skipBits(4); // '0010' - long pts = pesScratch.readBitsLong(3) << 30; + long pts = (long) pesScratch.readBits(3) << 30; pesScratch.skipBits(1); // marker_bit - pts |= pesScratch.readBitsLong(15) << 15; + pts |= pesScratch.readBits(15) << 15; pesScratch.skipBits(1); // marker_bit - pts |= pesScratch.readBitsLong(15); + pts |= pesScratch.readBits(15); pesScratch.skipBits(1); // marker_bit timeUs = ptsToTimeUs(pts); } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsParserUtil.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsParserUtil.java index 7e5dd64367..2ddfdd081b 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsParserUtil.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsParserUtil.java @@ -51,7 +51,7 @@ import java.util.regex.Pattern; public static String parseOptionalStringAttr(String line, Pattern pattern) { Matcher matcher = pattern.matcher(line); - if (matcher.find() && matcher.groupCount() == 1) { + if (matcher.find()) { return matcher.group(1); } return null; @@ -59,7 +59,7 @@ import java.util.regex.Pattern; public static boolean parseOptionalBooleanAttr(String line, Pattern pattern) { Matcher matcher = pattern.matcher(line); - if (matcher.find() && matcher.groupCount() == 1) { + if (matcher.find()) { return BOOLEAN_YES.equals(matcher.group(1)); } return false; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java index 316f41a44d..4d0b163cb0 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java @@ -72,7 +72,7 @@ public final class HlsPlaylistParser implements UriLoadable.Parser private static final Pattern CODECS_ATTR_REGEX = Pattern.compile(CODECS_ATTR + "=\"(.+?)\""); private static final Pattern RESOLUTION_ATTR_REGEX = - Pattern.compile(RESOLUTION_ATTR + "=(\\d+x\\d+)"); + Pattern.compile(RESOLUTION_ATTR + "=(\\d+(\\.\\d+)?x\\d+(\\.\\d+)?)"); private static final Pattern MEDIA_DURATION_REGEX = Pattern.compile(MEDIA_DURATION_TAG + ":([\\d.]+),"); private static final Pattern MEDIA_SEQUENCE_REGEX = @@ -168,8 +168,16 @@ public final class HlsPlaylistParser implements UriLoadable.Parser RESOLUTION_ATTR_REGEX); if (resolutionString != null) { String[] widthAndHeight = resolutionString.split("x"); - width = Integer.parseInt(widthAndHeight[0]); - height = Integer.parseInt(widthAndHeight[1]); + width = Math.round(Float.parseFloat(widthAndHeight[0])); + if (width <= 0) { + // Width was invalid. + width = -1; + } + height = Math.round(Float.parseFloat(widthAndHeight[1])); + if (height <= 0) { + // Height was invalid. + height = -1; + } } else { width = -1; height = -1; diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/AssetDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/AssetDataSource.java index 77bff359e1..84642df5a0 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/AssetDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/AssetDataSource.java @@ -16,7 +16,9 @@ package com.google.android.exoplayer.upstream; import com.google.android.exoplayer.C; +import com.google.android.exoplayer.util.Assertions; +import android.content.Context; import android.content.res.AssetManager; import java.io.EOFException; @@ -24,14 +26,14 @@ import java.io.IOException; import java.io.InputStream; /** - * A local asset {@link DataSource}. + * A local asset {@link UriDataSource}. */ -public final class AssetDataSource implements DataSource { +public final class AssetDataSource implements UriDataSource { /** - * Thrown when IOException is encountered during local asset read operation. + * Thrown when an {@link IOException} is encountered reading a local asset. */ - public static class AssetDataSourceException extends IOException { + public static final class AssetDataSourceException extends IOException { public AssetDataSourceException(IOException cause) { super(cause); @@ -42,15 +44,16 @@ public final class AssetDataSource implements DataSource { private final AssetManager assetManager; private final TransferListener listener; - private InputStream assetInputStream; + private String uriString; + private InputStream inputStream; private long bytesRemaining; private boolean opened; /** * Constructs a new {@link DataSource} that retrieves data from a local asset. */ - public AssetDataSource(AssetManager assetManager) { - this(assetManager, null); + public AssetDataSource(Context context) { + this(context, null); } /** @@ -58,19 +61,26 @@ public final class AssetDataSource implements DataSource { * * @param listener An optional listener. Specify {@code null} for no listener. */ - public AssetDataSource(AssetManager assetManager, TransferListener listener) { - this.assetManager = assetManager; + public AssetDataSource(Context context, TransferListener listener) { + this.assetManager = context.getAssets(); this.listener = listener; } @Override public long open(DataSpec dataSpec) throws AssetDataSourceException { try { - // Lose the '/' prefix in the path or else AssetManager won't find our file - assetInputStream = assetManager.open(dataSpec.uri.getPath().substring(1), - AssetManager.ACCESS_RANDOM); - assetInputStream.skip(dataSpec.position); - bytesRemaining = dataSpec.length == C.LENGTH_UNBOUNDED ? assetInputStream.available() + uriString = dataSpec.uri.toString(); + String path = dataSpec.uri.getPath(); + if (path.startsWith("/android_asset/")) { + path = path.substring(15); + } else if (path.startsWith("/")) { + path = path.substring(1); + } + uriString = dataSpec.uri.toString(); + inputStream = assetManager.open(path, AssetManager.ACCESS_RANDOM); + long skipped = inputStream.skip(dataSpec.position); + Assertions.checkState(skipped == dataSpec.position); + bytesRemaining = dataSpec.length == C.LENGTH_UNBOUNDED ? inputStream.available() : dataSpec.length; if (bytesRemaining < 0) { throw new EOFException(); @@ -93,8 +103,7 @@ public final class AssetDataSource implements DataSource { } else { int bytesRead = 0; try { - bytesRead = assetInputStream.read(buffer, offset, - (int) Math.min(bytesRemaining, readLength)); + bytesRead = inputStream.read(buffer, offset, (int) Math.min(bytesRemaining, readLength)); } catch (IOException e) { throw new AssetDataSourceException(e); } @@ -110,15 +119,21 @@ public final class AssetDataSource implements DataSource { } } + @Override + public String getUri() { + return uriString; + } + @Override public void close() throws AssetDataSourceException { - if (assetInputStream != null) { + uriString = null; + if (inputStream != null) { try { - assetInputStream.close(); + inputStream.close(); } catch (IOException e) { throw new AssetDataSourceException(e); } finally { - assetInputStream = null; + inputStream = null; if (opened) { opened = false; if (listener != null) { diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/ContentDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/ContentDataSource.java new file mode 100644 index 0000000000..6102bc2b2c --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/upstream/ContentDataSource.java @@ -0,0 +1,144 @@ +/* + * 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.exoplayer.upstream; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.util.Assertions; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.AssetFileDescriptor; + +import java.io.EOFException; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * A content URI {@link UriDataSource}. + */ +public final class ContentDataSource implements UriDataSource { + + /** + * 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 listener; + + private InputStream inputStream; + private String uriString; + private long bytesRemaining; + private boolean opened; + + /** + * Constructs a new {@link DataSource} that retrieves data from a content provider. + */ + public ContentDataSource(Context context) { + this(context, null); + } + + /** + * Constructs a new {@link DataSource} that retrieves data from a content provider. + * + * @param listener An optional listener. Specify {@code null} for no listener. + */ + public ContentDataSource(Context context, TransferListener listener) { + this.resolver = context.getContentResolver(); + this.listener = listener; + } + + @Override + public long open(DataSpec dataSpec) throws ContentDataSourceException { + try { + uriString = dataSpec.uri.toString(); + AssetFileDescriptor assetFd = resolver.openAssetFileDescriptor(dataSpec.uri, "r"); + inputStream = new FileInputStream(assetFd.getFileDescriptor()); + long skipped = inputStream.skip(dataSpec.position); + Assertions.checkState(skipped == dataSpec.position); + bytesRemaining = dataSpec.length == C.LENGTH_UNBOUNDED ? inputStream.available() + : dataSpec.length; + if (bytesRemaining < 0) { + throw new EOFException(); + } + } catch (IOException e) { + throw new ContentDataSourceException(e); + } + + opened = true; + if (listener != null) { + listener.onTransferStart(); + } + + return bytesRemaining; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws ContentDataSourceException { + if (bytesRemaining == 0) { + return -1; + } else { + int bytesRead = 0; + try { + bytesRead = inputStream.read(buffer, offset, (int) Math.min(bytesRemaining, readLength)); + } catch (IOException e) { + throw new ContentDataSourceException(e); + } + + if (bytesRead > 0) { + bytesRemaining -= bytesRead; + if (listener != null) { + listener.onBytesTransferred(bytesRead); + } + } + + return bytesRead; + } + } + + @Override + public String getUri() { + return uriString; + } + + @Override + public void close() throws ContentDataSourceException { + uriString = null; + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + throw new ContentDataSourceException(e); + } finally { + inputStream = null; + if (opened) { + opened = false; + if (listener != null) { + listener.onTransferEnd(); + } + } + } + } + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java index f2c7ec49b1..8af7bf9250 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultBandwidthMeter.java @@ -28,7 +28,7 @@ import android.os.Handler; */ public class DefaultBandwidthMeter implements BandwidthMeter { - private static final int DEFAULT_MAX_WEIGHT = 2000; + public static final int DEFAULT_MAX_WEIGHT = 2000; private final Handler eventHandler; private final EventListener eventListener; diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultUriDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultUriDataSource.java index fa225bf266..e3cbb7b84d 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultUriDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultUriDataSource.java @@ -17,17 +17,55 @@ package com.google.android.exoplayer.upstream; import com.google.android.exoplayer.util.Assertions; +import android.content.Context; +import android.text.TextUtils; + import java.io.IOException; /** - * A data source that fetches data from a local or remote {@link DataSpec}. + * A {@link UriDataSource} that supports multiple URI schemes. The supported schemes are: + * + *

    + *
  • http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4). + *
  • file: For fetching data from a local file (e.g. file:///path/to/media/media.mp4, or just + * /path/to/media/media.mp4 because the implementation assumes that a URI without a scheme is a + * local file URI). + *
  • asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4). + *
  • content: For fetching data from a content URI (e.g. content://authority/path/123). + *
*/ public final class DefaultUriDataSource implements UriDataSource { - private static final String FILE_URI_SCHEME = "file"; + /** + * Thrown when a {@link DefaultUriDataSource} is opened for a URI with an unsupported scheme. + */ + public static final class UnsupportedSchemeException extends IOException { + + /** + * The unsupported scheme. + */ + public final String scheme; + + /** + * @param scheme The unsupported scheme. + */ + public UnsupportedSchemeException(String scheme) { + super("Unsupported URI scheme: " + scheme); + this.scheme = scheme; + } + + } + + private static final String SCHEME_HTTP = "http"; + private static final String SCHEME_HTTPS = "https"; + private static final String SCHEME_FILE = "file"; + private static final String SCHEME_ASSET = "asset"; + private static final String SCHEME_CONTENT = "content"; - private final UriDataSource fileDataSource; private final UriDataSource httpDataSource; + private final UriDataSource fileDataSource; + private final UriDataSource assetDataSource; + private final UriDataSource contentDataSource; /** * {@code null} if no data source is open. Otherwise, equal to {@link #fileDataSource} if the open @@ -36,54 +74,89 @@ public final class DefaultUriDataSource implements UriDataSource { private UriDataSource dataSource; /** - * Constructs a new data source that delegates to a {@link FileDataSource} for file URIs and a - * {@link DefaultHttpDataSource} for other URIs. + * Constructs a new instance. *

* The constructed instance will not follow cross-protocol redirects (i.e. redirects from HTTP to * HTTPS or vice versa) when fetching remote data. Cross-protocol redirects can be enabled by - * using the {@link #DefaultUriDataSource(String, TransferListener, boolean)} constructor and - * passing {@code true} as the final argument. + * using {@link #DefaultUriDataSource(Context, TransferListener, String, boolean)} and passing + * {@code true} as the final argument. * + * @param context A context. * @param userAgent The User-Agent string that should be used when requesting remote data. - * @param transferListener An optional listener. */ - public DefaultUriDataSource(String userAgent, TransferListener transferListener) { - this(userAgent, transferListener, false); + public DefaultUriDataSource(Context context, String userAgent) { + this(context, null, userAgent, false); } /** - * Constructs a new data source that delegates to a {@link FileDataSource} for file URIs and a - * {@link DefaultHttpDataSource} for other URIs. + * Constructs a new instance. + *

+ * The constructed instance will not follow cross-protocol redirects (i.e. redirects from HTTP to + * HTTPS or vice versa) when fetching remote data. Cross-protocol redirects can be enabled by + * using {@link #DefaultUriDataSource(Context, TransferListener, String, boolean)} and passing + * {@code true} as the final argument. * + * @param context A context. + * @param listener An optional {@link TransferListener}. + * @param userAgent The User-Agent string that should be used when requesting remote data. + */ + public DefaultUriDataSource(Context context, TransferListener listener, String userAgent) { + this(context, listener, userAgent, false); + } + + /** + * Constructs a new instance, optionally configured to follow cross-protocol redirects. + * + * @param context A context. + * @param listener An optional {@link TransferListener}. * @param userAgent The User-Agent string that should be used when requesting remote data. - * @param transferListener An optional listener. * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP * to HTTPS and vice versa) are enabled when fetching remote data.. */ - public DefaultUriDataSource(String userAgent, TransferListener transferListener, + public DefaultUriDataSource(Context context, TransferListener listener, String userAgent, boolean allowCrossProtocolRedirects) { - this(new FileDataSource(transferListener), - new DefaultHttpDataSource(userAgent, null, transferListener, + this(context, listener, + new DefaultHttpDataSource(userAgent, null, listener, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, allowCrossProtocolRedirects)); } /** - * Constructs a new data source using {@code fileDataSource} for file URIs, and - * {@code httpDataSource} for non-file URIs. + * Constructs a new instance, using a provided {@link HttpDataSource} for fetching remote data. * - * @param fileDataSource {@link UriDataSource} to use for file URIs. + * @param context A context. + * @param listener An optional {@link TransferListener}. * @param httpDataSource {@link UriDataSource} to use for non-file URIs. */ - public DefaultUriDataSource(UriDataSource fileDataSource, UriDataSource httpDataSource) { - this.fileDataSource = Assertions.checkNotNull(fileDataSource); + public DefaultUriDataSource(Context context, TransferListener listener, + UriDataSource httpDataSource) { this.httpDataSource = Assertions.checkNotNull(httpDataSource); + 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); - dataSource = FILE_URI_SCHEME.equals(dataSpec.uri.getScheme()) ? fileDataSource : httpDataSource; + // Choose the correct source for the scheme. + String scheme = dataSpec.uri.getScheme(); + if (SCHEME_HTTP.equals(scheme) || SCHEME_HTTPS.equals(scheme)) { + dataSource = httpDataSource; + } else if (SCHEME_FILE.equals(scheme) || TextUtils.isEmpty(scheme)) { + 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 { + throw new UnsupportedSchemeException(scheme); + } + // Open the source and return. return dataSource.open(dataSpec); } @@ -100,8 +173,11 @@ public final class DefaultUriDataSource implements UriDataSource { @Override public void close() throws IOException { if (dataSource != null) { - dataSource.close(); - dataSource = null; + try { + dataSource.close(); + } finally { + dataSource = null; + } } } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java index bd890b38d2..784914a7ef 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/FileDataSource.java @@ -22,7 +22,7 @@ import java.io.IOException; import java.io.RandomAccessFile; /** - * A local file {@link DataSource}. + * A local file {@link UriDataSource}. */ public final class FileDataSource implements UriDataSource { @@ -40,7 +40,7 @@ public final class FileDataSource implements UriDataSource { private final TransferListener listener; private RandomAccessFile file; - private String uri; + private String uriString; private long bytesRemaining; private boolean opened; @@ -63,7 +63,7 @@ public final class FileDataSource implements UriDataSource { @Override public long open(DataSpec dataSpec) throws FileDataSourceException { try { - uri = dataSpec.uri.toString(); + uriString = dataSpec.uri.toString(); file = new RandomAccessFile(dataSpec.uri.getPath(), "r"); file.seek(dataSpec.position); bytesRemaining = dataSpec.length == C.LENGTH_UNBOUNDED ? file.length() - dataSpec.position @@ -108,11 +108,12 @@ public final class FileDataSource implements UriDataSource { @Override public String getUri() { - return uri; + return uriString; } @Override public void close() throws FileDataSourceException { + uriString = null; if (file != null) { try { file.close(); @@ -120,8 +121,6 @@ public final class FileDataSource implements UriDataSource { throw new FileDataSourceException(e); } finally { file = null; - uri = null; - if (opened) { opened = false; if (listener != null) { diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java index 2944616a09..6009a837a5 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/HttpDataSource.java @@ -25,7 +25,7 @@ import java.util.List; import java.util.Map; /** - * An HTTP specific extension to {@link DataSource}. + * An HTTP specific extension to {@link UriDataSource}. */ public interface HttpDataSource extends UriDataSource { diff --git a/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java b/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java index 98e4cc7160..b8dd63228a 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java +++ b/library/src/main/java/com/google/android/exoplayer/util/ParsableBitArray.java @@ -99,21 +99,11 @@ public final class ParsableBitArray { * @return An integer whose bottom n bits hold the read data. */ public int readBits(int n) { - return (int) readBitsLong(n); - } - - /** - * Reads up to 64 bits. - * - * @param n The number of bits to read. - * @return A long whose bottom n bits hold the read data. - */ - public long readBitsLong(int n) { if (n == 0) { return 0; } - long retval = 0; + int retval = 0; // While n >= 8, read whole bytes. while (n >= 8) {