Merge pull request #469 from google/dev

dev -> dev-webm-vp9-opus
This commit is contained in:
ojw28 2015-05-19 14:57:00 +01:00
commit e3a7fc4da7
26 changed files with 527 additions and 181 deletions

View File

@ -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
View 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.

View File

@ -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"/>

View File

@ -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);

View File

@ -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>();

View File

@ -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,

View File

@ -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,

View File

@ -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++) {

View File

@ -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'
}

View File

@ -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}

View File

@ -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;

View File

@ -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.

View File

@ -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) {

View File

@ -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();

View File

@ -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;

View File

@ -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) {

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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();
}
}
}
}
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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) {