commit
e3a7fc4da7
12
README.md
12
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 ##
|
||||
|
||||
|
14
RELEASENOTES.md
Normal file
14
RELEASENOTES.md
Normal file
@ -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.
|
@ -16,8 +16,8 @@
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer.demo"
|
||||
android:versionCode="1301"
|
||||
android:versionName="1.3.1"
|
||||
android:versionCode="1302"
|
||||
android:versionName="1.3.2"
|
||||
android:theme="@style/RootTheme">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
@ -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);
|
||||
|
@ -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<MediaPresentationDescription>(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<ChunkSource> audioChunkSourceList = new ArrayList<ChunkSource>();
|
||||
List<String> audioTrackNameList = new ArrayList<String>();
|
||||
if (audioAdaptationSet != null) {
|
||||
DataSource audioDataSource = new DefaultUriDataSource(userAgent, bandwidthMeter);
|
||||
DataSource audioDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
|
||||
List<Representation> audioRepresentations = audioAdaptationSet.representations;
|
||||
List<String> codecs = new ArrayList<String>();
|
||||
@ -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<ChunkSource> textChunkSourceList = new ArrayList<ChunkSource>();
|
||||
List<String> textTrackNameList = new ArrayList<String>();
|
||||
|
@ -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,
|
||||
|
@ -76,8 +76,8 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
|
||||
this.player = player;
|
||||
this.callback = callback;
|
||||
HlsPlaylistParser parser = new HlsPlaylistParser();
|
||||
ManifestFetcher<HlsPlaylist> playlistFetcher =
|
||||
new ManifestFetcher<HlsPlaylist>(url, new DefaultUriDataSource(userAgent, null), parser);
|
||||
ManifestFetcher<HlsPlaylist> playlistFetcher = new ManifestFetcher<HlsPlaylist>(url,
|
||||
new DefaultUriDataSource(context, userAgent), parser);
|
||||
playlistFetcher.singleLoad(player.getMainHandler().getLooper(), this);
|
||||
}
|
||||
|
||||
@ -103,7 +103,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
|
||||
}
|
||||
}
|
||||
|
||||
DataSource dataSource = new DefaultUriDataSource(userAgent, bandwidthMeter);
|
||||
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
|
||||
HlsChunkSource chunkSource = new HlsChunkSource(dataSource, url, manifest, bandwidthMeter,
|
||||
variantIndices, HlsChunkSource.ADAPTIVE_MODE_SPLICE, audioCapabilities);
|
||||
HlsSampleSource sampleSource = new HlsSampleSource(chunkSource, true, 3, REQUESTED_BUFFER_SIZE,
|
||||
|
@ -92,8 +92,12 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
|
||||
public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
|
||||
this.player = player;
|
||||
this.callback = callback;
|
||||
String manifestUrl = url;
|
||||
if (!manifestUrl.endsWith("/Manifest")) {
|
||||
manifestUrl += "/Manifest";
|
||||
}
|
||||
SmoothStreamingManifestParser parser = new SmoothStreamingManifestParser();
|
||||
manifestFetcher = new ManifestFetcher<SmoothStreamingManifest>(url + "/Manifest",
|
||||
manifestFetcher = new ManifestFetcher<SmoothStreamingManifest>(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++) {
|
||||
|
@ -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'
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -213,6 +213,18 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
|
||||
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>
|
||||
@ -225,6 +237,18 @@ public class StreamingDrmSessionManager implements DrmSessionManager {
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open(DrmInitData drmInitData) {
|
||||
if (++openCount != 1) {
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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<TsPayloadReader> 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<TsPayloadReader> 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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -72,7 +72,7 @@ public final class HlsPlaylistParser implements UriLoadable.Parser<HlsPlaylist>
|
||||
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<HlsPlaylist>
|
||||
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;
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4).
|
||||
* <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).
|
||||
* </ul>
|
||||
*/
|
||||
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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user