Merge pull request #43 from google/dev

Merge 1.0.13 to master
This commit is contained in:
ojw28 2014-09-08 11:41:56 +01:00
commit 280ccb1630
33 changed files with 945 additions and 1092 deletions

View File

@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer.demo"
android:versionCode="1012"
android:versionName="1.0.12"
android:versionCode="1013"
android:versionName="1.0.13"
android:theme="@style/RootTheme">
<uses-permission android:name="android.permission.INTERNET"/>

View File

@ -134,6 +134,8 @@ package com.google.android.exoplayer.demo;
public static final Sample[] MISC = new Sample[] {
new Sample("Dizzy", "uid:misc:dizzy", "http://html5demos.com/assets/dizzy.mp4",
DemoUtil.TYPE_OTHER, false, true),
new Sample("Dizzy (https->http redirect)", "uid:misc:dizzy2", "https://goo.gl/MtUDEj",
DemoUtil.TYPE_OTHER, false, true),
};
private Samples() {}

View File

@ -28,8 +28,7 @@ import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
import com.google.android.exoplayer.dash.DashMp4ChunkSource;
import com.google.android.exoplayer.dash.DashWebmChunkSource;
import com.google.android.exoplayer.dash.DashChunkSource;
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher;
@ -163,14 +162,8 @@ public class DashVodRendererBuilder implements RendererBuilder,
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource videoChunkSource;
String mimeType = videoRepresentations[0].format.mimeType;
if (mimeType.equals(MimeTypes.VIDEO_MP4)) {
videoChunkSource = new DashMp4ChunkSource(videoDataSource,
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
} else if (mimeType.equals(MimeTypes.VIDEO_WEBM)) {
// TODO: Figure out how to query supported vpX resolutions. For now, restrict to standard
// definition streams.
videoRepresentations = getSdRepresentations(videoRepresentations);
videoChunkSource = new DashWebmChunkSource(videoDataSource,
if (mimeType.equals(MimeTypes.VIDEO_MP4) || mimeType.equals(MimeTypes.VIDEO_WEBM)) {
videoChunkSource = new DashChunkSource(videoDataSource,
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
} else {
throw new IllegalStateException("Unexpected mime type: " + mimeType);
@ -200,7 +193,7 @@ public class DashVodRendererBuilder implements RendererBuilder,
Format format = representation.format;
audioTrackNames[i] = format.id + " (" + format.numChannels + "ch, " +
format.audioSamplingRate + "Hz)";
audioChunkSources[i] = new DashMp4ChunkSource(audioDataSource,
audioChunkSources[i] = new DashChunkSource(audioDataSource,
audioEvaluator, representation);
}
audioChunkSource = new MultiTrackChunkSource(audioChunkSources);

View File

@ -26,7 +26,7 @@ import com.google.android.exoplayer.chunk.ChunkSource;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
import com.google.android.exoplayer.dash.DashMp4ChunkSource;
import com.google.android.exoplayer.dash.DashChunkSource;
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescriptionFetcher;
@ -116,7 +116,7 @@ import java.util.ArrayList;
// Build the video renderer.
DataSource videoDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource videoChunkSource = new DashMp4ChunkSource(videoDataSource,
ChunkSource videoChunkSource = new DashChunkSource(videoDataSource,
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);
@ -125,7 +125,7 @@ import java.util.ArrayList;
// Build the audio renderer.
DataSource audioDataSource = new HttpDataSource(userAgent, null, bandwidthMeter);
ChunkSource audioChunkSource = new DashMp4ChunkSource(audioDataSource,
ChunkSource audioChunkSource = new DashChunkSource(audioDataSource,
new FormatEvaluator.FixedEvaluator(), audioRepresentation);
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true);

View File

@ -21,6 +21,12 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!--
Important: ExoPlayerLib specifies a minSdkVersion of 9 because various components provided by
the library may be of use on older devices. However, please note that the core video playback
functionality provided by the library requires API level 16 or greater.
-->
<uses-sdk android:minSdkVersion="9" android:targetSdkVersion="19"/>
</manifest>

View File

@ -26,7 +26,7 @@ public class ExoPlayerLibraryInfo {
/**
* The version of the library, expressed as a string.
*/
public static final String VERSION = "1.0.12";
public static final String VERSION = "1.0.13";
/**
* 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 1002003.
*/
public static final int VERSION_INT = 1000012;
public static final int VERSION_INT = 1000013;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions}

View File

@ -112,7 +112,7 @@ public final class FrameworkSampleSource implements SampleSource {
}
@Override
public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) {
Assertions.checkState(prepared);
Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED);

View File

@ -27,6 +27,7 @@ import android.media.AudioTimestamp;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.media.audiofx.Virtualizer;
import android.os.ConditionVariable;
import android.os.Handler;
import android.util.Log;
@ -91,6 +92,13 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
private static final long MICROS_PER_SECOND = 1000000L;
/**
* AudioTrack timestamps are deemed spurious if they are offset from the system clock by more
* than this amount. This is a fail safe that should not be required on correctly functioning
* devices.
*/
private static final long MAX_AUDIO_TIMSTAMP_OFFSET_US = 10 * MICROS_PER_SECOND;
private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10;
private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;
private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000;
@ -358,9 +366,9 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
* subsequently re-enabled.
* <p>
* The default implementation is a no-op. One reason for overriding this method would be to
* instantiate and enable a {@link android.media.audiofx.Virtualizer} in order to spatialize the
* audio channels. For this use case, any {@link android.media.audiofx.Virtualizer} instances
* should be released in {@link #onDisabled()} (if not before).
* instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For
* this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()}
* (if not before).
*
* @param audioSessionId The audio session id.
*/
@ -425,7 +433,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
@Override
protected boolean isReady() {
return super.isReady() || getPendingFrameCount() > 0;
return getPendingFrameCount() > 0
|| (super.isReady() && getSourceState() == SOURCE_STATE_READY_READ_MAY_FAIL);
}
/**
@ -500,11 +509,18 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) {
audioTimestampSet = audioTimestampCompat.initTimestamp(audioTrack);
if (audioTimestampSet
&& (audioTimestampCompat.getNanoTime() / 1000) < audioTrackResumeSystemTimeUs) {
// The timestamp was set, but it corresponds to a time before the track was most recently
// resumed.
audioTimestampSet = false;
if (audioTimestampSet) {
// Perform sanity checks on the timestamp.
long audioTimestampUs = audioTimestampCompat.getNanoTime() / 1000;
if (audioTimestampUs < audioTrackResumeSystemTimeUs) {
// The timestamp corresponds to a time before the track was most recently resumed.
audioTimestampSet = false;
} else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMSTAMP_OFFSET_US) {
// The timestamp time base is probably wrong.
audioTimestampSet = false;
Log.w(TAG, "Spurious audio timestamp: " + audioTimestampCompat.getFramePosition() + ", "
+ audioTimestampUs + ", " + systemClockUs);
}
}
if (audioTrackGetLatencyMethod != null) {
try {

View File

@ -78,6 +78,22 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
}
/**
* Value of {@link #sourceState} when the source is not ready.
*/
protected static final int SOURCE_STATE_NOT_READY = 0;
/**
* Value of {@link #sourceState} when the source is ready and we're able to read from it.
*/
protected static final int SOURCE_STATE_READY = 1;
/**
* Value of {@link #sourceState} when the source is ready but we might not be able to read from
* it. We transition to this state when an attempt to read a sample fails despite the source
* reporting that samples are available. This can occur when the next sample to be provided by
* the source is for another renderer.
*/
protected static final int SOURCE_STATE_READY_READ_MAY_FAIL = 2;
/**
* If the {@link MediaCodec} is hotswapped (i.e. replaced during playback), this is the period of
* time during which {@link #isReady()} will report true regardless of whether the new codec has
@ -108,7 +124,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
private final boolean playClearSamplesWithoutKeys;
private final SampleSource source;
private final SampleHolder sampleHolder;
private final FormatHolder formatHolder;
private final MediaFormatHolder formatHolder;
private final HashSet<Long> decodeOnlyPresentationTimestamps;
private final MediaCodec.BufferInfo outputBufferInfo;
private final EventListener eventListener;
@ -128,7 +144,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
private int codecReconfigurationState;
private int trackIndex;
private boolean sourceIsReady;
private int sourceState;
private boolean inputStreamEnded;
private boolean outputStreamEnded;
private boolean waitingForKeys;
@ -158,7 +174,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
this.eventListener = eventListener;
codecCounters = new CodecCounters();
sampleHolder = new SampleHolder(false);
formatHolder = new FormatHolder();
formatHolder = new MediaFormatHolder();
decodeOnlyPresentationTimestamps = new HashSet<Long>();
outputBufferInfo = new MediaCodec.BufferInfo();
}
@ -202,7 +218,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
@Override
protected void onEnabled(long timeUs, boolean joining) {
source.enable(trackIndex, timeUs);
sourceIsReady = false;
sourceState = SOURCE_STATE_NOT_READY;
inputStreamEnded = false;
outputStreamEnded = false;
waitingForKeys = false;
@ -267,9 +283,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
}
codecHotswapTimeMs = getState() == TrackRenderer.STATE_STARTED ?
SystemClock.elapsedRealtime() : -1;
inputIndex = -1;
outputIndex = -1;
waitingForFirstSyncFrame = true;
inputIndex = -1;
outputIndex = -1;
waitingForFirstSyncFrame = true;
codecCounters.codecInitCount++;
}
@ -353,7 +369,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
protected void seekTo(long timeUs) throws ExoPlaybackException {
currentPositionUs = timeUs;
source.seekToUs(timeUs);
sourceIsReady = false;
sourceState = SOURCE_STATE_NOT_READY;
inputStreamEnded = false;
outputStreamEnded = false;
waitingForKeys = false;
@ -372,7 +388,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
@Override
protected void doSomeWork(long timeUs) throws ExoPlaybackException {
try {
sourceIsReady = source.continueBuffering(timeUs);
sourceState = source.continueBuffering(timeUs)
? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState)
: SOURCE_STATE_NOT_READY;
checkForDiscontinuity();
if (format == null) {
readFormat();
@ -384,7 +402,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
}
if (codec != null) {
while (drainOutputBuffer(timeUs)) {}
while (feedInputBuffer()) {}
if (feedInputBuffer(true)) {
while (feedInputBuffer(false)) {}
}
}
}
codecCounters.ensureUpdated();
@ -429,6 +449,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
codecHotswapTimeMs = -1;
inputIndex = -1;
outputIndex = -1;
waitingForFirstSyncFrame = true;
decodeOnlyPresentationTimestamps.clear();
// Workaround for framework bugs.
// See [redacted], [redacted], [redacted].
@ -446,11 +467,13 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
}
/**
* @param firstFeed True if this is the first call to this method from the current invocation of
* {@link #doSomeWork(long)}. False otherwise.
* @return True if it may be possible to feed more input data. False otherwise.
* @throws IOException If an error occurs reading data from the upstream source.
* @throws ExoPlaybackException If an error occurs feeding the input buffer.
*/
private boolean feedInputBuffer() throws IOException, ExoPlaybackException {
private boolean feedInputBuffer(boolean firstFeed) throws IOException, ExoPlaybackException {
if (inputStreamEnded) {
return false;
}
@ -478,6 +501,9 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;
}
result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
if (firstFeed && sourceState == SOURCE_STATE_READY && result == SampleSource.NOTHING_READ) {
sourceState = SOURCE_STATE_READY_READ_MAY_FAIL;
}
}
if (result == SampleSource.NOTHING_READ) {
@ -594,7 +620,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
* @param formatHolder Holds the new format.
* @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}.
*/
private void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {
private void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException {
MediaFormat oldFormat = format;
format = formatHolder.format;
drmInitData = formatHolder.drmInitData;
@ -646,7 +672,17 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer {
@Override
protected boolean isReady() {
return format != null && !waitingForKeys
&& (sourceIsReady || outputIndex >= 0 || isWithinHotswapPeriod());
&& sourceState != SOURCE_STATE_NOT_READY || outputIndex >= 0 || isWithinHotswapPeriod();
}
/**
* Gets the source state.
*
* @return One of {@link #SOURCE_STATE_NOT_READY}, {@link #SOURCE_STATE_READY} and
* {@link #SOURCE_STATE_READY_READ_MAY_FAIL}.
*/
protected final int getSourceState() {
return sourceState;
}
private boolean isWithinHotswapPeriod() {

View File

@ -235,7 +235,8 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
@Override
protected boolean isReady() {
if (super.isReady()) {
if (super.isReady() && (renderedFirstFrame || !codecInitialized()
|| getSourceState() == SOURCE_STATE_READY_READ_MAY_FAIL)) {
// Ready. If we were joining then we've now joined, so clear the joining deadline.
joiningDeadlineUs = -1;
return true;

View File

@ -27,7 +27,7 @@ import java.util.Collections;
import java.util.List;
/**
* Encapsulates the information describing the format of media data, be it audio or video.
* Defines the format of an elementary media stream.
*/
public class MediaFormat {

View File

@ -21,7 +21,7 @@ import java.util.UUID;
/**
* Holds a {@link MediaFormat} and corresponding drm scheme initialization data.
*/
public final class FormatHolder {
public final class MediaFormatHolder {
/**
* The format of the media.

View File

@ -80,7 +80,7 @@ public interface SampleSource {
/**
* Enable the specified track. This allows the track's format and samples to be read from
* {@link #readData(int, long, FormatHolder, SampleHolder, boolean)}.
* {@link #readData(int, long, MediaFormatHolder, SampleHolder, boolean)}.
* <p>
* This method should not be called until after the source has been successfully prepared.
*
@ -119,7 +119,7 @@ public interface SampleSource {
*
* @param track The track from which to read.
* @param playbackPositionUs The current playback position.
* @param formatHolder A {@link FormatHolder} object to populate in the case of a new format.
* @param formatHolder A {@link MediaFormatHolder} object to populate in the case of a new format.
* @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample. If
* the caller requires the sample data then it must ensure that {@link SampleHolder#data}
* references a valid output buffer.
@ -129,7 +129,7 @@ public interface SampleSource {
* {@link #DISCONTINUITY_READ}, {@link #NOTHING_READ} or {@link #END_OF_STREAM}.
* @throws IOException If an error occurred reading from the source.
*/
public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException;
/**

View File

@ -16,9 +16,9 @@
package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.FormatHolder;
import com.google.android.exoplayer.LoadControl;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackInfo;
@ -267,7 +267,7 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
}
@Override
public int readData(int track, long playbackPositionUs, FormatHolder formatHolder,
public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
Assertions.checkState(state == STATE_ENABLED);
Assertions.checkState(track == 0);
@ -318,6 +318,9 @@ public class ChunkSampleSource implements SampleSource, Loader.Listener {
}
if (!mediaChunk.prepare()) {
if (currentLoadableException != null) {
throw currentLoadableException;
}
return NOTHING_READ;
}

View File

@ -20,7 +20,7 @@ import com.google.android.exoplayer.util.Assertions;
import java.util.Comparator;
/**
* A format definition for streams.
* Defines the high level format of a media stream.
*/
public class Format {

View File

@ -18,7 +18,7 @@ package com.google.android.exoplayer.chunk;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.parser.Extractor;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
@ -32,7 +32,7 @@ import java.util.UUID;
*/
public final class Mp4MediaChunk extends MediaChunk {
private final FragmentedMp4Extractor extractor;
private final Extractor extractor;
private final boolean maybeSelfContained;
private final long sampleOffsetUs;
@ -57,7 +57,7 @@ public final class Mp4MediaChunk extends MediaChunk {
*/
public Mp4MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex,
FragmentedMp4Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) {
Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) {
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
this.extractor = extractor;
this.maybeSelfContained = maybeSelfContained;
@ -89,7 +89,7 @@ public final class Mp4MediaChunk extends MediaChunk {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null);
int result = extractor.read(inputStream, null);
prepared = (result & FragmentedMp4Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
prepared = (result & Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
} else {
// We know there isn't a moov atom. The extractor must have parsed one from a separate
// initialization chunk.
@ -107,7 +107,7 @@ public final class Mp4MediaChunk extends MediaChunk {
public boolean sampleAvailable() throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
int result = extractor.read(inputStream, null);
return (result & FragmentedMp4Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
return (result & Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
}
@Override
@ -115,7 +115,7 @@ public final class Mp4MediaChunk extends MediaChunk {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null);
int result = extractor.read(inputStream, holder);
boolean sampleRead = (result & FragmentedMp4Extractor.RESULT_READ_SAMPLE) != 0;
boolean sampleRead = (result & Extractor.RESULT_READ_SAMPLE) != 0;
if (sampleRead) {
holder.timeUs -= sampleOffsetUs;
}

View File

@ -1,99 +0,0 @@
/*
* 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.chunk;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.parser.webm.WebmExtractor;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import java.util.Map;
import java.util.UUID;
/**
* A WebM {@link MediaChunk}.
*/
public final class WebmMediaChunk extends MediaChunk {
private final WebmExtractor extractor;
/**
* @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded.
* @param format The format of the stream to which this chunk belongs.
* @param extractor The extractor that will be used to extract the samples.
* @param trigger The reason for this chunk being selected.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
*/
public WebmMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
int trigger, WebmExtractor extractor, long startTimeUs, long endTimeUs,
int nextChunkIndex) {
super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
this.extractor = extractor;
}
@Override
public void seekToStart() {
seekTo(0, false);
}
@Override
public boolean seekTo(long positionUs, boolean allowNoop) {
boolean isDiscontinuous = extractor.seekTo(positionUs, allowNoop);
if (isDiscontinuous) {
resetReadPosition();
}
return isDiscontinuous;
}
@Override
public boolean prepare() {
return true;
}
@Override
public boolean sampleAvailable() throws ParserException {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
int result = extractor.read(inputStream, null);
return (result & WebmExtractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
}
@Override
public boolean read(SampleHolder holder) {
NonBlockingInputStream inputStream = getNonBlockingInputStream();
Assertions.checkState(inputStream != null);
int result = extractor.read(inputStream, holder);
return (result & WebmExtractor.RESULT_READ_SAMPLE) != 0;
}
@Override
public MediaFormat getMediaFormat() {
return extractor.getFormat();
}
@Override
public Map<UUID, byte[]> getPsshInfo() {
// TODO: Add support for Pssh to WebmExtractor
return null;
}
}

View File

@ -29,10 +29,13 @@ import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.chunk.Mp4MediaChunk;
import com.google.android.exoplayer.dash.mpd.RangedUri;
import com.google.android.exoplayer.dash.mpd.Representation;
import com.google.android.exoplayer.parser.Extractor;
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.parser.webm.WebmExtractor;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.MimeTypes;
import android.net.Uri;
@ -42,9 +45,11 @@ import java.util.HashMap;
import java.util.List;
/**
* An {@link ChunkSource} for Mp4 DASH streams.
* An {@link ChunkSource} for DASH streams.
* <p>
* This implementation currently supports fMP4 and webm.
*/
public class DashMp4ChunkSource implements ChunkSource {
public class DashChunkSource implements ChunkSource {
private final TrackInfo trackInfo;
private final DataSource dataSource;
@ -55,7 +60,7 @@ public class DashMp4ChunkSource implements ChunkSource {
private final Format[] formats;
private final HashMap<String, Representation> representations;
private final HashMap<String, FragmentedMp4Extractor> extractors;
private final HashMap<String, Extractor> extractors;
private final HashMap<String, DashSegmentIndex> segmentIndexes;
private boolean lastChunkWasInitialization;
@ -65,12 +70,12 @@ public class DashMp4ChunkSource implements ChunkSource {
* @param evaluator Selects from the available formats.
* @param representations The representations to be considered by the source.
*/
public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
public DashChunkSource(DataSource dataSource, FormatEvaluator evaluator,
Representation... representations) {
this.dataSource = dataSource;
this.evaluator = evaluator;
this.formats = new Format[representations.length];
this.extractors = new HashMap<String, FragmentedMp4Extractor>();
this.extractors = new HashMap<String, Extractor>();
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
this.representations = new HashMap<String, Representation>();
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
@ -82,7 +87,9 @@ public class DashMp4ChunkSource implements ChunkSource {
formats[i] = representations[i].format;
maxWidth = Math.max(formats[i].width, maxWidth);
maxHeight = Math.max(formats[i].height, maxHeight);
extractors.put(formats[i].id, new FragmentedMp4Extractor());
Extractor extractor = formats[i].mimeType.startsWith(MimeTypes.VIDEO_WEBM)
? new WebmExtractor() : new FragmentedMp4Extractor();
extractors.put(formats[i].id, extractor);
this.representations.put(formats[i].id, representations[i]);
DashSegmentIndex segmentIndex = representations[i].getIndex();
if (segmentIndex != null) {
@ -142,7 +149,7 @@ public class DashMp4ChunkSource implements ChunkSource {
}
Representation selectedRepresentation = representations.get(selectedFormat.id);
FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id);
Extractor extractor = extractors.get(selectedRepresentation.format.id);
RangedUri pendingInitializationUri = null;
RangedUri pendingIndexUri = null;
@ -191,35 +198,39 @@ public class DashMp4ChunkSource implements ChunkSource {
}
private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
Representation representation, FragmentedMp4Extractor extractor, DataSource dataSource,
Representation representation, Extractor extractor, DataSource dataSource,
int trigger) {
int expectedExtractorResult = FragmentedMp4Extractor.RESULT_END_OF_STREAM;
int expectedExtractorResult = Extractor.RESULT_END_OF_STREAM;
long indexAnchor = 0;
RangedUri requestUri;
if (initializationUri != null) {
// It's common for initialization and index data to be stored adjacently. Attempt to merge
// the two requests together to request both at once.
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INIT;
expectedExtractorResult |= Extractor.RESULT_READ_INIT;
requestUri = initializationUri.attemptMerge(indexUri);
if (requestUri != null) {
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX;
indexAnchor = indexUri.start + indexUri.length;
expectedExtractorResult |= Extractor.RESULT_READ_INDEX;
if (extractor.hasRelativeIndexOffsets()) {
indexAnchor = indexUri.start + indexUri.length;
}
} else {
requestUri = initializationUri;
}
} else {
requestUri = indexUri;
indexAnchor = indexUri.start + indexUri.length;
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_INDEX;
if (extractor.hasRelativeIndexOffsets()) {
indexAnchor = indexUri.start + indexUri.length;
}
expectedExtractorResult |= Extractor.RESULT_READ_INDEX;
}
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
representation.getCacheKey());
return new InitializationMp4Loadable(dataSource, dataSpec, trigger, representation.format,
return new InitializationLoadable(dataSource, dataSpec, trigger, representation.format,
extractor, expectedExtractorResult, indexAnchor);
}
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
FragmentedMp4Extractor extractor, DataSource dataSource, int segmentNum, int trigger) {
Extractor extractor, DataSource dataSource, int segmentNum, int trigger) {
int lastSegmentNum = segmentIndex.getLastSegmentNum();
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
long startTimeUs = segmentIndex.getTimeUs(segmentNum);
@ -232,15 +243,15 @@ public class DashMp4ChunkSource implements ChunkSource {
endTimeUs, nextSegmentNum, extractor, false, 0);
}
private class InitializationMp4Loadable extends Chunk {
private class InitializationLoadable extends Chunk {
private final FragmentedMp4Extractor extractor;
private final Extractor extractor;
private final int expectedExtractorResult;
private final long indexAnchor;
private final Uri uri;
public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger,
Format format, FragmentedMp4Extractor extractor, int expectedExtractorResult,
public InitializationLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
Format format, Extractor extractor, int expectedExtractorResult,
long indexAnchor) {
super(dataSource, dataSpec, format, trigger);
this.extractor = extractor;
@ -256,7 +267,7 @@ public class DashMp4ChunkSource implements ChunkSource {
throw new ParserException("Invalid extractor result. Expected "
+ expectedExtractorResult + ", got " + result);
}
if ((result & FragmentedMp4Extractor.RESULT_READ_INDEX) != 0) {
if ((result & Extractor.RESULT_READ_INDEX) != 0) {
segmentIndexes.put(format.id,
new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor));
}

View File

@ -1,261 +0,0 @@
/*
* 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.dash;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.TrackInfo;
import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.ChunkOperationHolder;
import com.google.android.exoplayer.chunk.ChunkSource;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.chunk.Format.DecreasingBandwidthComparator;
import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.chunk.WebmMediaChunk;
import com.google.android.exoplayer.dash.mpd.RangedUri;
import com.google.android.exoplayer.dash.mpd.Representation;
import com.google.android.exoplayer.parser.webm.DefaultWebmExtractor;
import com.google.android.exoplayer.parser.webm.WebmExtractor;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import android.net.Uri;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
/**
* An {@link ChunkSource} for WebM DASH streams.
*/
public class DashWebmChunkSource implements ChunkSource {
private final TrackInfo trackInfo;
private final DataSource dataSource;
private final FormatEvaluator evaluator;
private final Evaluation evaluation;
private final int maxWidth;
private final int maxHeight;
private final Format[] formats;
private final HashMap<String, Representation> representations;
private final HashMap<String, DefaultWebmExtractor> extractors;
private final HashMap<String, DashSegmentIndex> segmentIndexes;
private boolean lastChunkWasInitialization;
/**
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param evaluator Selects from the available formats.
* @param representations The representations to be considered by the source.
*/
public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator,
Representation... representations) {
this.dataSource = dataSource;
this.evaluator = evaluator;
this.formats = new Format[representations.length];
this.extractors = new HashMap<String, DefaultWebmExtractor>();
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
this.representations = new HashMap<String, Representation>();
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
representations[0].periodDurationMs * 1000);
this.evaluation = new Evaluation();
int maxWidth = 0;
int maxHeight = 0;
for (int i = 0; i < representations.length; i++) {
formats[i] = representations[i].format;
maxWidth = Math.max(formats[i].width, maxWidth);
maxHeight = Math.max(formats[i].height, maxHeight);
extractors.put(formats[i].id, new DefaultWebmExtractor());
this.representations.put(formats[i].id, representations[i]);
DashSegmentIndex segmentIndex = representations[i].getIndex();
if (segmentIndex != null) {
segmentIndexes.put(formats[i].id, segmentIndex);
}
}
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
Arrays.sort(formats, new DecreasingBandwidthComparator());
}
@Override
public final void getMaxVideoDimensions(MediaFormat out) {
if (trackInfo.mimeType.startsWith("video")) {
out.setMaxVideoDimensions(maxWidth, maxHeight);
}
}
@Override
public final TrackInfo getTrackInfo() {
return trackInfo;
}
@Override
public void enable() {
evaluator.enable();
}
@Override
public void disable(List<? extends MediaChunk> queue) {
evaluator.disable();
}
@Override
public void continueBuffering(long playbackPositionUs) {
// Do nothing
}
@Override
public final void getChunkOperation(List<? extends MediaChunk> queue, long seekPositionUs,
long playbackPositionUs, ChunkOperationHolder out) {
evaluation.queueSize = queue.size();
if (evaluation.format == null || !lastChunkWasInitialization) {
evaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
}
Format selectedFormat = evaluation.format;
out.queueSize = evaluation.queueSize;
if (selectedFormat == null) {
out.chunk = null;
return;
} else if (out.queueSize == queue.size() && out.chunk != null
&& out.chunk.format.id.equals(selectedFormat.id)) {
// We already have a chunk, and the evaluation hasn't changed either the format or the size
// of the queue. Leave unchanged.
return;
}
Representation selectedRepresentation = representations.get(selectedFormat.id);
WebmExtractor extractor = extractors.get(selectedRepresentation.format.id);
RangedUri pendingInitializationUri = null;
RangedUri pendingIndexUri = null;
if (extractor.getFormat() == null) {
pendingInitializationUri = selectedRepresentation.getInitializationUri();
}
if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) {
pendingIndexUri = selectedRepresentation.getIndexUri();
}
if (pendingInitializationUri != null || pendingIndexUri != null) {
// We have initialization and/or index requests to make.
Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri,
selectedRepresentation, extractor, dataSource, evaluation.trigger);
lastChunkWasInitialization = true;
out.chunk = initializationChunk;
return;
}
int nextSegmentNum;
DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id);
if (queue.isEmpty()) {
nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs);
} else {
nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex;
}
if (nextSegmentNum == -1) {
out.chunk = null;
return;
}
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor,
dataSource, nextSegmentNum, evaluation.trigger);
lastChunkWasInitialization = false;
out.chunk = nextMediaChunk;
}
@Override
public IOException getError() {
return null;
}
@Override
public void onChunkLoadError(Chunk chunk, Exception e) {
// Do nothing.
}
private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
Representation representation, WebmExtractor extractor, DataSource dataSource,
int trigger) {
int expectedExtractorResult = WebmExtractor.RESULT_END_OF_STREAM;
RangedUri requestUri;
if (initializationUri != null) {
// It's common for initialization and index data to be stored adjacently. Attempt to merge
// the two requests together to request both at once.
expectedExtractorResult |= WebmExtractor.RESULT_READ_INIT;
requestUri = initializationUri.attemptMerge(indexUri);
if (requestUri != null) {
expectedExtractorResult |= WebmExtractor.RESULT_READ_INDEX;
} else {
requestUri = initializationUri;
}
} else {
requestUri = indexUri;
expectedExtractorResult |= WebmExtractor.RESULT_READ_INDEX;
}
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
representation.getCacheKey());
return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format,
extractor, expectedExtractorResult);
}
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
WebmExtractor extractor, DataSource dataSource, int segmentNum, int trigger) {
int lastSegmentNum = segmentIndex.getLastSegmentNum();
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
long startTimeUs = segmentIndex.getTimeUs(segmentNum);
long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1)
: startTimeUs + segmentIndex.getDurationUs(segmentNum);
RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum);
DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length,
representation.getCacheKey());
return new WebmMediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
startTimeUs, endTimeUs, nextSegmentNum);
}
private class InitializationWebmLoadable extends Chunk {
private final WebmExtractor extractor;
private final int expectedExtractorResult;
private final Uri uri;
public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
Format format, WebmExtractor extractor, int expectedExtractorResult) {
super(dataSource, dataSpec, format, trigger);
this.extractor = extractor;
this.expectedExtractorResult = expectedExtractorResult;
this.uri = dataSpec.uri;
}
@Override
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
int result = extractor.read(stream, null);
if (result != expectedExtractorResult) {
throw new ParserException("Invalid extractor result. Expected "
+ expectedExtractorResult + ", got " + result);
}
if ((result & WebmExtractor.RESULT_READ_INDEX) != 0) {
segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getIndex(), uri, 0));
}
}
}
}

View File

@ -23,6 +23,8 @@ import java.util.List;
*/
public final class MediaPresentationDescription {
public final long availabilityStartTime;
public final long duration;
public final long minBufferTime;
@ -31,14 +33,22 @@ public final class MediaPresentationDescription {
public final long minUpdatePeriod;
public final long timeShiftBufferDepth;
public final List<Period> periods;
public MediaPresentationDescription(long duration, long minBufferTime, boolean dynamic,
long minUpdatePeriod, List<Period> periods) {
public final UtcTimingElement utcTiming;
public MediaPresentationDescription(long availabilityStartTime, long duration, long minBufferTime,
boolean dynamic, long minUpdatePeriod, long timeShiftBufferDepth, UtcTimingElement utcTiming,
List<Period> periods) {
this.availabilityStartTime = availabilityStartTime;
this.duration = duration;
this.minBufferTime = minBufferTime;
this.dynamic = dynamic;
this.minUpdatePeriod = minUpdatePeriod;
this.timeShiftBufferDepth = timeShiftBufferDepth;
this.utcTiming = utcTiming;
this.periods = Collections.unmodifiableList(periods);
}

View File

@ -20,8 +20,6 @@ import com.google.android.exoplayer.util.ManifestFetcher;
import android.net.Uri;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
@ -60,11 +58,7 @@ public final class MediaPresentationDescriptionFetcher extends
@Override
protected MediaPresentationDescription parse(InputStream stream, String inputEncoding,
String contentId, Uri baseUrl) throws IOException, ParserException {
try {
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl);
} catch (XmlPullParserException e) {
throw new ParserException(e);
}
return parser.parseMediaPresentationDescription(stream, inputEncoding, contentId, baseUrl);
}
}

View File

@ -34,8 +34,13 @@ import org.xmlpull.v1.XmlPullParserFactory;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -48,6 +53,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
private static final Pattern DURATION =
Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$");
private static final Pattern DATE_TIME_PATTERN =
Pattern.compile("(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]"
+ "(\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?"
+ "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?");
private final XmlPullParserFactory xmlParserFactory;
public MediaPresentationDescriptionParser() {
@ -69,42 +79,57 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
* @param baseUrl The url that any relative urls defined within the manifest are relative to.
* @return The parsed manifest.
* @throws IOException If a problem occurred reading from the stream.
* @throws XmlPullParserException If a problem occurred parsing the stream as xml.
* @throws ParserException If a problem occurred parsing the xml as a DASH mpd.
*/
public MediaPresentationDescription parseMediaPresentationDescription(InputStream inputStream,
String inputEncoding, String contentId, Uri baseUrl) throws XmlPullParserException,
IOException, ParserException {
XmlPullParser xpp = xmlParserFactory.newPullParser();
xpp.setInput(inputStream, inputEncoding);
int eventType = xpp.next();
if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) {
throw new ParserException(
"inputStream does not contain a valid media presentation description");
String inputEncoding, String contentId, Uri baseUrl) throws IOException, ParserException {
try {
XmlPullParser xpp = xmlParserFactory.newPullParser();
xpp.setInput(inputStream, inputEncoding);
int eventType = xpp.next();
if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) {
throw new ParserException(
"inputStream does not contain a valid media presentation description");
}
return parseMediaPresentationDescription(xpp, contentId, baseUrl);
} catch (XmlPullParserException e) {
throw new ParserException(e);
} catch (ParseException e) {
throw new ParserException(e);
}
return parseMediaPresentationDescription(xpp, contentId, baseUrl);
}
private MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
String contentId, Uri baseUrl) throws XmlPullParserException, IOException {
String contentId, Uri baseUrl) throws XmlPullParserException, IOException, ParseException {
long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1);
long durationMs = parseDurationMs(xpp, "mediaPresentationDuration");
long minBufferTimeMs = parseDurationMs(xpp, "minBufferTime");
String typeString = xpp.getAttributeValue(null, "type");
boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false;
long minUpdateTimeMs = (dynamic) ? parseDurationMs(xpp, "minimumUpdatePeriod", -1) : -1;
long timeShiftBufferDepthMs = (dynamic) ? parseDurationMs(xpp, "timeShiftBufferDepth", -1) : -1;
UtcTimingElement utcTiming = null;
List<Period> periods = new ArrayList<Period>();
do {
xpp.next();
if (isStartTag(xpp, "BaseURL")) {
baseUrl = parseBaseUrl(xpp, baseUrl);
} else if (isStartTag(xpp, "UTCTiming")) {
utcTiming = parseUtcTiming(xpp);
} else if (isStartTag(xpp, "Period")) {
periods.add(parsePeriod(xpp, contentId, baseUrl, durationMs));
}
} while (!isEndTag(xpp, "MPD"));
return new MediaPresentationDescription(durationMs, minBufferTimeMs, dynamic, minUpdateTimeMs,
periods);
return new MediaPresentationDescription(availabilityStartTime, durationMs, minBufferTimeMs,
dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, utcTiming, periods);
}
private UtcTimingElement parseUtcTiming(XmlPullParser xpp) {
String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri");
String value = xpp.getAttributeValue(null, "value");
return new UtcTimingElement(schemeIdUri, value);
}
private Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs)
@ -429,6 +454,62 @@ public class MediaPresentationDescriptionParser extends DefaultHandler {
return parseDurationMs(xpp, name, -1);
}
private static long parseDateTime(XmlPullParser xpp, String name, long defaultValue)
throws ParseException {
String value = xpp.getAttributeValue(null, name);
if (value == null) {
return defaultValue;
} else {
return parseDateTime(value);
}
}
// VisibleForTesting
static long parseDateTime(String value) throws ParseException {
Matcher matcher = DATE_TIME_PATTERN.matcher(value);
if (!matcher.matches()) {
throw new ParseException("Invalid date/time format: " + value, 0);
}
int timezoneShift;
if (matcher.group(9) == null) {
// No time zone specified.
timezoneShift = 0;
} else if (matcher.group(9).equalsIgnoreCase("Z")) {
timezoneShift = 0;
} else {
timezoneShift = ((Integer.valueOf(matcher.group(12)) * 60
+ Integer.valueOf(matcher.group(13))));
if (matcher.group(11).equals("-")) {
timezoneShift *= -1;
}
}
Calendar dateTime = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
dateTime.clear();
// Note: The month value is 0-based, hence the -1 on group(2)
dateTime.set(Integer.valueOf(matcher.group(1)),
Integer.valueOf(matcher.group(2)) - 1,
Integer.valueOf(matcher.group(3)),
Integer.valueOf(matcher.group(4)),
Integer.valueOf(matcher.group(5)),
Integer.valueOf(matcher.group(6)));
if (!TextUtils.isEmpty(matcher.group(8))) {
final BigDecimal bd = new BigDecimal("0." + matcher.group(8));
// we care only for milliseconds, so movePointRight(3)
dateTime.set(Calendar.MILLISECOND, bd.movePointRight(3).intValue());
}
long time = dateTime.getTimeInMillis();
if (timezoneShift != 0) {
time -= timezoneShift * 60000;
}
return time;
}
private static long parseDurationMs(XmlPullParser xpp, String name, long defaultValue) {
String value = xpp.getAttributeValue(null, name);
if (value != null) {

View File

@ -0,0 +1,31 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.dash.mpd;
/**
* Represents a UTCTiming element.
*/
public class UtcTimingElement {
public final String schemeIdUri;
public final String value;
public UtcTimingElement(String schemeIdUri, String value) {
this.schemeIdUri = schemeIdUri;
this.value = value;
}
}

View File

@ -0,0 +1,115 @@
/*
* 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.parser;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import java.util.Map;
import java.util.UUID;
/**
* Facilitates extraction of media samples from a container format.
*/
public interface Extractor {
/**
* An attempt to read from the input stream returned insufficient data.
*/
public static final int RESULT_NEED_MORE_DATA = 1;
/**
* The end of the input stream was reached.
*/
public static final int RESULT_END_OF_STREAM = 2;
/**
* A media sample was read.
*/
public static final int RESULT_READ_SAMPLE = 4;
/**
* Initialization data was read. The parsed data can be read using {@link #getFormat()} and
* {@link #getPsshInfo}.
*/
public static final int RESULT_READ_INIT = 8;
/**
* A sidx atom was read. The parsed data can be read using {@link #getIndex()}.
*/
public static final int RESULT_READ_INDEX = 16;
/**
* The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
*/
public static final int RESULT_NEED_SAMPLE_HOLDER = 32;
/**
* Returns the segment index parsed from the stream.
*
* @return The segment index, or null if a SIDX atom has yet to be parsed.
*/
public SegmentIndex getIndex();
/**
* Returns true if the offsets in the index returned by {@link #getIndex()} are relative to the
* first byte following the initialization data, or false if they are absolute (i.e. relative to
* the first byte of the stream).
*
* @return True if the offsets are relative to the first byte following the initialization data.
* False otherwise.
*/
public boolean hasRelativeIndexOffsets();
/**
* Returns the format of the samples contained within the media stream.
*
* @return The sample media format, or null if the format has yet to be parsed.
*/
public MediaFormat getFormat();
/**
* Returns the pssh information parsed from the stream.
*
* @return The pssh information. May be null if pssh data has yet to be parsed, or if the stream
* does not contain any pssh data.
*/
public Map<UUID, byte[]> getPsshInfo();
/**
* Consumes data from a {@link NonBlockingInputStream}.
* <p>
* The read terminates if the end of the input stream is reached, if an attempt to read from the
* input stream returned 0 bytes of data, or if a sample is read. The returned flags indicate
* both the reason for termination and data that was parsed during the read.
*
* @param inputStream The input stream from which data should be read.
* @param out A {@link SampleHolder} into which the next sample should be read. If null then
* {@link #RESULT_NEED_SAMPLE_HOLDER} will be returned once a sample has been reached.
* @return One or more of the {@code RESULT_*} flags defined in this class.
* @throws ParserException If an error occurs parsing the media data.
*/
public int read(NonBlockingInputStream inputStream, SampleHolder out) throws ParserException;
/**
* Seeks to a position before or equal to the requested time.
*
* @param seekTimeUs The desired seek time in microseconds.
* @param allowNoop Allow the seek operation to do nothing if the seek time is in the current
* fragment run, is equal to or greater than the time of the current sample, and if there
* does not exist a sync frame between these two times.
* @return True if the operation resulted in a change of state. False if it was a no-op.
*/
public boolean seekTo(long seekTimeUs, boolean allowNoop);
}

View File

@ -18,11 +18,13 @@ package com.google.android.exoplayer.parser.mp4;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.parser.Extractor;
import com.google.android.exoplayer.parser.SegmentIndex;
import com.google.android.exoplayer.parser.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer.parser.mp4.Atom.LeafAtom;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import com.google.android.exoplayer.util.MimeTypes;
import android.annotation.SuppressLint;
@ -47,7 +49,7 @@ import java.util.UUID;
* <p>
* This implementation only supports de-muxed (i.e. single track) streams.
*/
public final class FragmentedMp4Extractor {
public final class FragmentedMp4Extractor implements Extractor {
/**
* Flag to work around an issue in some video streams where every frame is marked as a sync frame.
@ -58,32 +60,6 @@ public final class FragmentedMp4Extractor {
*/
public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
/**
* An attempt to read from the input stream returned insufficient data.
*/
public static final int RESULT_NEED_MORE_DATA = 1;
/**
* The end of the input stream was reached.
*/
public static final int RESULT_END_OF_STREAM = 2;
/**
* A media sample was read.
*/
public static final int RESULT_READ_SAMPLE = 4;
/**
* A moov atom was read. The parsed data can be read using {@link #getFormat()} and
* {@link #getPsshInfo}.
*/
public static final int RESULT_READ_INIT = 8;
/**
* A sidx atom was read. The parsed data can be read using {@link #getIndex()}.
*/
public static final int RESULT_READ_INDEX = 16;
/**
* The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
*/
public static final int RESULT_NEED_SAMPLE_HOLDER = 32;
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
| RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
private static final byte[] NAL_START_CODE = new byte[] {0, 0, 0, 1};
@ -196,22 +172,13 @@ public final class FragmentedMp4Extractor {
}
/**
* Returns the segment index parsed from the stream.
* Sideloads track information into the extractor.
*
* @return The segment index, or null if a SIDX atom has yet to be parsed.
* @param track The track to sideload.
*/
public SegmentIndex getIndex() {
return segmentIndex;
}
/**
* Returns the pssh information parsed from the stream.
*
* @return The pssh information. May be null if the MOOV atom has yet to be parsed of if it did
* not contain any pssh information.
*/
public Map<UUID, byte[]> getPsshInfo() {
return psshData.isEmpty() ? null : psshData;
public void setTrack(Track track) {
this.extendsDefaults = new DefaultSampleValues(0, 0, 0, 0);
this.track = track;
}
/**
@ -228,38 +195,27 @@ public final class FragmentedMp4Extractor {
psshData.put(uuid, data);
}
/**
* Returns the format of the samples contained within the media stream.
*
* @return The sample media format, or null if a MOOV atom has yet to be parsed.
*/
@Override
public Map<UUID, byte[]> getPsshInfo() {
return psshData.isEmpty() ? null : psshData;
}
@Override
public SegmentIndex getIndex() {
return segmentIndex;
}
@Override
public boolean hasRelativeIndexOffsets() {
return true;
}
@Override
public MediaFormat getFormat() {
return track == null ? null : track.mediaFormat;
}
/**
* Sideloads track information into the extractor.
*
* @param track The track to sideload.
*/
public void setTrack(Track track) {
this.extendsDefaults = new DefaultSampleValues(0, 0, 0, 0);
this.track = track;
}
/**
* Consumes data from a {@link NonBlockingInputStream}.
* <p>
* The read terminates if the end of the input stream is reached, if an attempt to read from the
* input stream returned 0 bytes of data, or if a sample is read. The returned flags indicate
* both the reason for termination and data that was parsed during the read.
*
* @param inputStream The input stream from which data should be read.
* @param out A {@link SampleHolder} into which the next sample should be read. If null then
* {@link #RESULT_NEED_SAMPLE_HOLDER} will be returned once a sample has been reached.
* @return One or more of the {@code RESULT_*} flags defined in this class.
* @throws ParserException If an error occurs parsing the media data.
*/
@Override
public int read(NonBlockingInputStream inputStream, SampleHolder out)
throws ParserException {
try {
@ -286,15 +242,7 @@ public final class FragmentedMp4Extractor {
}
}
/**
* Seeks to a position before or equal to the requested time.
*
* @param seekTimeUs The desired seek time in microseconds.
* @param allowNoop Allow the seek operation to do nothing if the seek time is in the current
* fragment run, is equal to or greater than the time of the current sample, and if there
* does not exist a sync frame between these two times.
* @return True if the operation resulted in a change of state. False if it was a no-op.
*/
@Override
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
pendingSeekTimeMs = (int) (seekTimeUs / 1000);
if (allowNoop && fragmentRun != null
@ -780,10 +728,10 @@ public final class FragmentedMp4Extractor {
LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.data, out);
TrackEncryptionBox trackEncryptionBox =
track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex];
LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
if (saiz != null) {
TrackEncryptionBox trackEncryptionBox =
track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex];
parseSaiz(trackEncryptionBox, saiz.data, out);
}

View File

@ -1,462 +0,0 @@
/*
* 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.parser.webm;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.parser.SegmentIndex;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.LongArray;
import com.google.android.exoplayer.util.MimeTypes;
import android.annotation.TargetApi;
import android.media.MediaExtractor;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* Default version of an extractor to facilitate data retrieval from the WebM container format.
*
* <p>WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
* More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>.
*/
@TargetApi(16)
public final class DefaultWebmExtractor implements WebmExtractor {
private static final String DOC_TYPE_WEBM = "webm";
private static final String CODEC_ID_VP9 = "V_VP9";
private static final int UNKNOWN = -1;
// Element IDs
private static final int ID_EBML = 0x1A45DFA3;
private static final int ID_EBML_READ_VERSION = 0x42F7;
private static final int ID_DOC_TYPE = 0x4282;
private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
private static final int ID_SEGMENT = 0x18538067;
private static final int ID_INFO = 0x1549A966;
private static final int ID_TIMECODE_SCALE = 0x2AD7B1;
private static final int ID_DURATION = 0x4489;
private static final int ID_CLUSTER = 0x1F43B675;
private static final int ID_TIME_CODE = 0xE7;
private static final int ID_SIMPLE_BLOCK = 0xA3;
private static final int ID_TRACKS = 0x1654AE6B;
private static final int ID_TRACK_ENTRY = 0xAE;
private static final int ID_CODEC_ID = 0x86;
private static final int ID_VIDEO = 0xE0;
private static final int ID_PIXEL_WIDTH = 0xB0;
private static final int ID_PIXEL_HEIGHT = 0xBA;
private static final int ID_CUES = 0x1C53BB6B;
private static final int ID_CUE_POINT = 0xBB;
private static final int ID_CUE_TIME = 0xB3;
private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
// SimpleBlock Lacing Values
private static final int LACING_NONE = 0;
private static final int LACING_XIPH = 1;
private static final int LACING_FIXED = 2;
private static final int LACING_EBML = 3;
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
| RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
private final EbmlReader reader;
private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
private SampleHolder sampleHolder;
private int readResults;
private long segmentStartOffsetBytes = UNKNOWN;
private long segmentEndOffsetBytes = UNKNOWN;
private long timecodeScale = 1000000L;
private long durationUs = UNKNOWN;
private int pixelWidth = UNKNOWN;
private int pixelHeight = UNKNOWN;
private long cuesSizeBytes = UNKNOWN;
private long clusterTimecodeUs = UNKNOWN;
private long simpleBlockTimecodeUs = UNKNOWN;
private MediaFormat format;
private SegmentIndex cues;
private LongArray cueTimesUs;
private LongArray cueClusterPositions;
public DefaultWebmExtractor() {
this(new DefaultEbmlReader());
}
/* package */ DefaultWebmExtractor(EbmlReader reader) {
this.reader = reader;
this.reader.setEventHandler(new InnerEbmlEventHandler());
}
@Override
public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
this.sampleHolder = sampleHolder;
this.readResults = 0;
while ((readResults & READ_TERMINATING_RESULTS) == 0) {
int ebmlReadResult = reader.read(inputStream);
if (ebmlReadResult == EbmlReader.READ_RESULT_NEED_MORE_DATA) {
readResults |= WebmExtractor.RESULT_NEED_MORE_DATA;
} else if (ebmlReadResult == EbmlReader.READ_RESULT_END_OF_STREAM) {
readResults |= WebmExtractor.RESULT_END_OF_STREAM;
}
}
this.sampleHolder = null;
return readResults;
}
@Override
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
if (allowNoop
&& cues != null
&& clusterTimecodeUs != UNKNOWN
&& simpleBlockTimecodeUs != UNKNOWN
&& seekTimeUs >= simpleBlockTimecodeUs) {
int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) {
return false;
}
}
clusterTimecodeUs = UNKNOWN;
simpleBlockTimecodeUs = UNKNOWN;
reader.reset();
return true;
}
@Override
public SegmentIndex getIndex() {
return cues;
}
@Override
public MediaFormat getFormat() {
return format;
}
/* package */ int getElementType(int id) {
switch (id) {
case ID_EBML:
case ID_SEGMENT:
case ID_INFO:
case ID_CLUSTER:
case ID_TRACKS:
case ID_TRACK_ENTRY:
case ID_VIDEO:
case ID_CUES:
case ID_CUE_POINT:
case ID_CUE_TRACK_POSITIONS:
return EbmlReader.TYPE_MASTER;
case ID_EBML_READ_VERSION:
case ID_DOC_TYPE_READ_VERSION:
case ID_TIMECODE_SCALE:
case ID_TIME_CODE:
case ID_PIXEL_WIDTH:
case ID_PIXEL_HEIGHT:
case ID_CUE_TIME:
case ID_CUE_CLUSTER_POSITION:
return EbmlReader.TYPE_UNSIGNED_INT;
case ID_DOC_TYPE:
case ID_CODEC_ID:
return EbmlReader.TYPE_STRING;
case ID_SIMPLE_BLOCK:
return EbmlReader.TYPE_BINARY;
case ID_DURATION:
return EbmlReader.TYPE_FLOAT;
default:
return EbmlReader.TYPE_UNKNOWN;
}
}
/* package */ boolean onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
switch (id) {
case ID_SEGMENT:
if (segmentStartOffsetBytes != UNKNOWN || segmentEndOffsetBytes != UNKNOWN) {
throw new IllegalStateException("Multiple Segment elements not supported");
}
segmentStartOffsetBytes = elementOffsetBytes + headerSizeBytes;
segmentEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
break;
case ID_CUES:
cuesSizeBytes = headerSizeBytes + contentsSizeBytes;
cueTimesUs = new LongArray();
cueClusterPositions = new LongArray();
break;
default:
// pass
}
return true;
}
/* package */ boolean onMasterElementEnd(int id) {
switch (id) {
case ID_CUES:
buildCues();
return false;
case ID_VIDEO:
buildFormat();
return true;
default:
return true;
}
}
/* package */ boolean onIntegerElement(int id, long value) {
switch (id) {
case ID_EBML_READ_VERSION:
// Validate that EBMLReadVersion is supported. This extractor only supports v1.
if (value != 1) {
throw new IllegalArgumentException("EBMLReadVersion " + value + " not supported");
}
break;
case ID_DOC_TYPE_READ_VERSION:
// Validate that DocTypeReadVersion is supported. This extractor only supports up to v2.
if (value < 1 || value > 2) {
throw new IllegalArgumentException("DocTypeReadVersion " + value + " not supported");
}
break;
case ID_TIMECODE_SCALE:
timecodeScale = value;
break;
case ID_PIXEL_WIDTH:
pixelWidth = (int) value;
break;
case ID_PIXEL_HEIGHT:
pixelHeight = (int) value;
break;
case ID_CUE_TIME:
cueTimesUs.add(scaleTimecodeToUs(value));
break;
case ID_CUE_CLUSTER_POSITION:
cueClusterPositions.add(value);
break;
case ID_TIME_CODE:
clusterTimecodeUs = scaleTimecodeToUs(value);
break;
default:
// pass
}
return true;
}
/* package */ boolean onFloatElement(int id, double value) {
if (id == ID_DURATION) {
durationUs = scaleTimecodeToUs((long) value);
}
return true;
}
/* package */ boolean onStringElement(int id, String value) {
switch (id) {
case ID_DOC_TYPE:
// Validate that DocType is supported. This extractor only supports "webm".
if (!DOC_TYPE_WEBM.equals(value)) {
throw new IllegalArgumentException("DocType " + value + " not supported");
}
break;
case ID_CODEC_ID:
// Validate that CodecID is supported. This extractor only supports "V_VP9".
if (!CODEC_ID_VP9.equals(value)) {
throw new IllegalArgumentException("CodecID " + value + " not supported");
}
break;
default:
// pass
}
return true;
}
/* package */ boolean onBinaryElement(
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
NonBlockingInputStream inputStream) {
if (id == ID_SIMPLE_BLOCK) {
// Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
// for info about how data is organized in a SimpleBlock element.
// If we don't have a sample holder then don't consume the data.
if (sampleHolder == null) {
readResults |= RESULT_NEED_SAMPLE_HOLDER;
return false;
}
// Value of trackNumber is not used but needs to be read.
reader.readVarint(inputStream);
// Next three bytes have timecode and flags.
reader.readBytes(inputStream, simpleBlockTimecodeAndFlags, 3);
// First two bytes of the three are the relative timecode.
int timecode =
(simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff);
long timecodeUs = scaleTimecodeToUs(timecode);
// Last byte of the three has some flags and the lacing value.
boolean keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80;
boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
// Validate lacing and set info into sample holder.
switch (lacing) {
case LACING_NONE:
long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
sampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
sampleHolder.decodeOnly = invisible;
sampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
break;
case LACING_EBML:
case LACING_FIXED:
case LACING_XIPH:
default:
throw new IllegalStateException("Lacing mode " + lacing + " not supported");
}
ByteBuffer outputData = sampleHolder.data;
if (sampleHolder.allowDataBufferReplacement
&& (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size)) {
outputData = ByteBuffer.allocate(sampleHolder.size);
sampleHolder.data = outputData;
}
if (outputData == null) {
reader.skipBytes(inputStream, sampleHolder.size);
sampleHolder.size = 0;
} else {
reader.readBytes(inputStream, outputData, sampleHolder.size);
}
readResults |= RESULT_READ_SAMPLE;
}
return true;
}
private long scaleTimecodeToUs(long unscaledTimecode) {
return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
}
/**
* Build a video {@link MediaFormat} containing recently gathered Video information, if needed.
*
* <p>Replaces the previous {@link #format} only if video width/height have changed.
* {@link #format} is guaranteed to not be null after calling this method. In
* the event that it can't be built, an {@link IllegalStateException} will be thrown.
*/
private void buildFormat() {
if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN
&& (format == null || format.width != pixelWidth || format.height != pixelHeight)) {
format = MediaFormat.createVideoFormat(
MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
readResults |= RESULT_READ_INIT;
} else if (format == null) {
throw new IllegalStateException("Unable to build format");
}
}
/**
* Build a {@link SegmentIndex} containing recently gathered Cues information.
*
* <p>{@link #cues} is guaranteed to not be null after calling this method. In
* the event that it can't be built, an {@link IllegalStateException} will be thrown.
*/
private void buildCues() {
if (segmentStartOffsetBytes == UNKNOWN) {
throw new IllegalStateException("Segment start/end offsets unknown");
} else if (durationUs == UNKNOWN) {
throw new IllegalStateException("Duration unknown");
} else if (cuesSizeBytes == UNKNOWN) {
throw new IllegalStateException("Cues size unknown");
} else if (cueTimesUs == null || cueClusterPositions == null
|| cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
throw new IllegalStateException("Invalid/missing cue points");
}
int cuePointsSize = cueTimesUs.size();
int[] sizes = new int[cuePointsSize];
long[] offsets = new long[cuePointsSize];
long[] durationsUs = new long[cuePointsSize];
long[] timesUs = new long[cuePointsSize];
for (int i = 0; i < cuePointsSize; i++) {
timesUs[i] = cueTimesUs.get(i);
offsets[i] = segmentStartOffsetBytes + cueClusterPositions.get(i);
}
for (int i = 0; i < cuePointsSize - 1; i++) {
sizes[i] = (int) (offsets[i + 1] - offsets[i]);
durationsUs[i] = timesUs[i + 1] - timesUs[i];
}
sizes[cuePointsSize - 1] = (int) (segmentEndOffsetBytes - offsets[cuePointsSize - 1]);
durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
cues = new SegmentIndex((int) cuesSizeBytes, sizes, offsets, durationsUs, timesUs);
cueTimesUs = null;
cueClusterPositions = null;
readResults |= RESULT_READ_INDEX;
}
/**
* Passes events through to {@link DefaultWebmExtractor} as
* callbacks from {@link EbmlReader} are received.
*/
private final class InnerEbmlEventHandler implements EbmlEventHandler {
@Override
public int getElementType(int id) {
return DefaultWebmExtractor.this.getElementType(id);
}
@Override
public void onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
DefaultWebmExtractor.this.onMasterElementStart(
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
}
@Override
public void onMasterElementEnd(int id) {
DefaultWebmExtractor.this.onMasterElementEnd(id);
}
@Override
public void onIntegerElement(int id, long value) {
DefaultWebmExtractor.this.onIntegerElement(id, value);
}
@Override
public void onFloatElement(int id, double value) {
DefaultWebmExtractor.this.onFloatElement(id, value);
}
@Override
public void onStringElement(int id, String value) {
DefaultWebmExtractor.this.onStringElement(id, value);
}
@Override
public boolean onBinaryElement(
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
NonBlockingInputStream inputStream) {
return DefaultWebmExtractor.this.onBinaryElement(
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes, inputStream);
}
}
}

View File

@ -17,76 +17,460 @@ package com.google.android.exoplayer.parser.webm;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.parser.Extractor;
import com.google.android.exoplayer.parser.SegmentIndex;
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
import com.google.android.exoplayer.util.LongArray;
import com.google.android.exoplayer.util.MimeTypes;
import android.annotation.TargetApi;
import android.media.MediaExtractor;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Extractor to facilitate data retrieval from the WebM container format.
* An extractor to facilitate data retrieval from the WebM container format.
*
* <p>WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
* Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
* More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>.
*/
public interface WebmExtractor {
@TargetApi(16)
public final class WebmExtractor implements Extractor {
private static final String DOC_TYPE_WEBM = "webm";
private static final String CODEC_ID_VP9 = "V_VP9";
private static final int UNKNOWN = -1;
// Element IDs
private static final int ID_EBML = 0x1A45DFA3;
private static final int ID_EBML_READ_VERSION = 0x42F7;
private static final int ID_DOC_TYPE = 0x4282;
private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
private static final int ID_SEGMENT = 0x18538067;
private static final int ID_INFO = 0x1549A966;
private static final int ID_TIMECODE_SCALE = 0x2AD7B1;
private static final int ID_DURATION = 0x4489;
private static final int ID_CLUSTER = 0x1F43B675;
private static final int ID_TIME_CODE = 0xE7;
private static final int ID_SIMPLE_BLOCK = 0xA3;
private static final int ID_TRACKS = 0x1654AE6B;
private static final int ID_TRACK_ENTRY = 0xAE;
private static final int ID_CODEC_ID = 0x86;
private static final int ID_VIDEO = 0xE0;
private static final int ID_PIXEL_WIDTH = 0xB0;
private static final int ID_PIXEL_HEIGHT = 0xBA;
private static final int ID_CUES = 0x1C53BB6B;
private static final int ID_CUE_POINT = 0xBB;
private static final int ID_CUE_TIME = 0xB3;
private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
// SimpleBlock Lacing Values
private static final int LACING_NONE = 0;
private static final int LACING_XIPH = 1;
private static final int LACING_FIXED = 2;
private static final int LACING_EBML = 3;
private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
| RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
private final EbmlReader reader;
private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
private SampleHolder sampleHolder;
private int readResults;
private long segmentStartOffsetBytes = UNKNOWN;
private long segmentEndOffsetBytes = UNKNOWN;
private long timecodeScale = 1000000L;
private long durationUs = UNKNOWN;
private int pixelWidth = UNKNOWN;
private int pixelHeight = UNKNOWN;
private long cuesSizeBytes = UNKNOWN;
private long clusterTimecodeUs = UNKNOWN;
private long simpleBlockTimecodeUs = UNKNOWN;
private MediaFormat format;
private SegmentIndex cues;
private LongArray cueTimesUs;
private LongArray cueClusterPositions;
public WebmExtractor() {
this(new DefaultEbmlReader());
}
/* package */ WebmExtractor(EbmlReader reader) {
this.reader = reader;
this.reader.setEventHandler(new InnerEbmlEventHandler());
}
@Override
public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder) {
this.sampleHolder = sampleHolder;
this.readResults = 0;
while ((readResults & READ_TERMINATING_RESULTS) == 0) {
int ebmlReadResult = reader.read(inputStream);
if (ebmlReadResult == EbmlReader.READ_RESULT_NEED_MORE_DATA) {
readResults |= WebmExtractor.RESULT_NEED_MORE_DATA;
} else if (ebmlReadResult == EbmlReader.READ_RESULT_END_OF_STREAM) {
readResults |= WebmExtractor.RESULT_END_OF_STREAM;
}
}
this.sampleHolder = null;
return readResults;
}
@Override
public boolean seekTo(long seekTimeUs, boolean allowNoop) {
if (allowNoop
&& cues != null
&& clusterTimecodeUs != UNKNOWN
&& simpleBlockTimecodeUs != UNKNOWN
&& seekTimeUs >= simpleBlockTimecodeUs) {
int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) {
return false;
}
}
clusterTimecodeUs = UNKNOWN;
simpleBlockTimecodeUs = UNKNOWN;
reader.reset();
return true;
}
@Override
public SegmentIndex getIndex() {
return cues;
}
@Override
public boolean hasRelativeIndexOffsets() {
return false;
}
@Override
public MediaFormat getFormat() {
return format;
}
@Override
public Map<UUID, byte[]> getPsshInfo() {
// TODO: Parse pssh data from Webm streams.
return null;
}
/* package */ int getElementType(int id) {
switch (id) {
case ID_EBML:
case ID_SEGMENT:
case ID_INFO:
case ID_CLUSTER:
case ID_TRACKS:
case ID_TRACK_ENTRY:
case ID_VIDEO:
case ID_CUES:
case ID_CUE_POINT:
case ID_CUE_TRACK_POSITIONS:
return EbmlReader.TYPE_MASTER;
case ID_EBML_READ_VERSION:
case ID_DOC_TYPE_READ_VERSION:
case ID_TIMECODE_SCALE:
case ID_TIME_CODE:
case ID_PIXEL_WIDTH:
case ID_PIXEL_HEIGHT:
case ID_CUE_TIME:
case ID_CUE_CLUSTER_POSITION:
return EbmlReader.TYPE_UNSIGNED_INT;
case ID_DOC_TYPE:
case ID_CODEC_ID:
return EbmlReader.TYPE_STRING;
case ID_SIMPLE_BLOCK:
return EbmlReader.TYPE_BINARY;
case ID_DURATION:
return EbmlReader.TYPE_FLOAT;
default:
return EbmlReader.TYPE_UNKNOWN;
}
}
/* package */ boolean onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
switch (id) {
case ID_SEGMENT:
if (segmentStartOffsetBytes != UNKNOWN || segmentEndOffsetBytes != UNKNOWN) {
throw new IllegalStateException("Multiple Segment elements not supported");
}
segmentStartOffsetBytes = elementOffsetBytes + headerSizeBytes;
segmentEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
break;
case ID_CUES:
cuesSizeBytes = headerSizeBytes + contentsSizeBytes;
cueTimesUs = new LongArray();
cueClusterPositions = new LongArray();
break;
default:
// pass
}
return true;
}
/* package */ boolean onMasterElementEnd(int id) {
switch (id) {
case ID_CUES:
buildCues();
return false;
case ID_VIDEO:
buildFormat();
return true;
default:
return true;
}
}
/* package */ boolean onIntegerElement(int id, long value) {
switch (id) {
case ID_EBML_READ_VERSION:
// Validate that EBMLReadVersion is supported. This extractor only supports v1.
if (value != 1) {
throw new IllegalArgumentException("EBMLReadVersion " + value + " not supported");
}
break;
case ID_DOC_TYPE_READ_VERSION:
// Validate that DocTypeReadVersion is supported. This extractor only supports up to v2.
if (value < 1 || value > 2) {
throw new IllegalArgumentException("DocTypeReadVersion " + value + " not supported");
}
break;
case ID_TIMECODE_SCALE:
timecodeScale = value;
break;
case ID_PIXEL_WIDTH:
pixelWidth = (int) value;
break;
case ID_PIXEL_HEIGHT:
pixelHeight = (int) value;
break;
case ID_CUE_TIME:
cueTimesUs.add(scaleTimecodeToUs(value));
break;
case ID_CUE_CLUSTER_POSITION:
cueClusterPositions.add(value);
break;
case ID_TIME_CODE:
clusterTimecodeUs = scaleTimecodeToUs(value);
break;
default:
// pass
}
return true;
}
/* package */ boolean onFloatElement(int id, double value) {
if (id == ID_DURATION) {
durationUs = scaleTimecodeToUs((long) value);
}
return true;
}
/* package */ boolean onStringElement(int id, String value) {
switch (id) {
case ID_DOC_TYPE:
// Validate that DocType is supported. This extractor only supports "webm".
if (!DOC_TYPE_WEBM.equals(value)) {
throw new IllegalArgumentException("DocType " + value + " not supported");
}
break;
case ID_CODEC_ID:
// Validate that CodecID is supported. This extractor only supports "V_VP9".
if (!CODEC_ID_VP9.equals(value)) {
throw new IllegalArgumentException("CodecID " + value + " not supported");
}
break;
default:
// pass
}
return true;
}
/* package */ boolean onBinaryElement(
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
NonBlockingInputStream inputStream) {
if (id == ID_SIMPLE_BLOCK) {
// Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
// for info about how data is organized in a SimpleBlock element.
// If we don't have a sample holder then don't consume the data.
if (sampleHolder == null) {
readResults |= RESULT_NEED_SAMPLE_HOLDER;
return false;
}
// Value of trackNumber is not used but needs to be read.
reader.readVarint(inputStream);
// Next three bytes have timecode and flags.
reader.readBytes(inputStream, simpleBlockTimecodeAndFlags, 3);
// First two bytes of the three are the relative timecode.
int timecode =
(simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff);
long timecodeUs = scaleTimecodeToUs(timecode);
// Last byte of the three has some flags and the lacing value.
boolean keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80;
boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
// Validate lacing and set info into sample holder.
switch (lacing) {
case LACING_NONE:
long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
sampleHolder.flags = keyframe ? MediaExtractor.SAMPLE_FLAG_SYNC : 0;
sampleHolder.decodeOnly = invisible;
sampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
break;
case LACING_EBML:
case LACING_FIXED:
case LACING_XIPH:
default:
throw new IllegalStateException("Lacing mode " + lacing + " not supported");
}
ByteBuffer outputData = sampleHolder.data;
if (sampleHolder.allowDataBufferReplacement
&& (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size)) {
outputData = ByteBuffer.allocate(sampleHolder.size);
sampleHolder.data = outputData;
}
if (outputData == null) {
reader.skipBytes(inputStream, sampleHolder.size);
sampleHolder.size = 0;
} else {
reader.readBytes(inputStream, outputData, sampleHolder.size);
}
readResults |= RESULT_READ_SAMPLE;
}
return true;
}
private long scaleTimecodeToUs(long unscaledTimecode) {
return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
}
/**
* An attempt to read from the input stream returned insufficient data.
*/
public static final int RESULT_NEED_MORE_DATA = 1;
/**
* The end of the input stream was reached.
*/
public static final int RESULT_END_OF_STREAM = 2;
/**
* A media sample was read.
*/
public static final int RESULT_READ_SAMPLE = 4;
/**
* Initialization data was read. The parsed data can be read using {@link #getFormat()}.
*/
public static final int RESULT_READ_INIT = 8;
/**
* A sidx atom was read. The parsed data can be read using {@link #getIndex()}.
*/
public static final int RESULT_READ_INDEX = 16;
/**
* The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
*/
public static final int RESULT_NEED_SAMPLE_HOLDER = 32;
/**
* Consumes data from a {@link NonBlockingInputStream}.
* Build a video {@link MediaFormat} containing recently gathered Video information, if needed.
*
* @param inputStream The input stream from which data should be read
* @param sampleHolder A {@link SampleHolder} into which the sample should be read
* @return One or more of the {@code RESULT_*} flags defined in this class.
* <p>Replaces the previous {@link #format} only if video width/height have changed.
* {@link #format} is guaranteed to not be null after calling this method. In
* the event that it can't be built, an {@link IllegalStateException} will be thrown.
*/
public int read(NonBlockingInputStream inputStream, SampleHolder sampleHolder);
private void buildFormat() {
if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN
&& (format == null || format.width != pixelWidth || format.height != pixelHeight)) {
format = MediaFormat.createVideoFormat(
MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
readResults |= RESULT_READ_INIT;
} else if (format == null) {
throw new IllegalStateException("Unable to build format");
}
}
/**
* Seeks to a position before or equal to the requested time.
* Build a {@link SegmentIndex} containing recently gathered Cues information.
*
* @param seekTimeUs The desired seek time in microseconds
* @param allowNoop Allow the seek operation to do nothing if the seek time is in the current
* segment, is equal to or greater than the time of the current sample, and if there does not
* exist a sync frame between these two times
* @return True if the operation resulted in a change of state. False if it was a no-op
* <p>{@link #cues} is guaranteed to not be null after calling this method. In
* the event that it can't be built, an {@link IllegalStateException} will be thrown.
*/
public boolean seekTo(long seekTimeUs, boolean allowNoop);
private void buildCues() {
if (segmentStartOffsetBytes == UNKNOWN) {
throw new IllegalStateException("Segment start/end offsets unknown");
} else if (durationUs == UNKNOWN) {
throw new IllegalStateException("Duration unknown");
} else if (cuesSizeBytes == UNKNOWN) {
throw new IllegalStateException("Cues size unknown");
} else if (cueTimesUs == null || cueClusterPositions == null
|| cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
throw new IllegalStateException("Invalid/missing cue points");
}
int cuePointsSize = cueTimesUs.size();
int[] sizes = new int[cuePointsSize];
long[] offsets = new long[cuePointsSize];
long[] durationsUs = new long[cuePointsSize];
long[] timesUs = new long[cuePointsSize];
for (int i = 0; i < cuePointsSize; i++) {
timesUs[i] = cueTimesUs.get(i);
offsets[i] = segmentStartOffsetBytes + cueClusterPositions.get(i);
}
for (int i = 0; i < cuePointsSize - 1; i++) {
sizes[i] = (int) (offsets[i + 1] - offsets[i]);
durationsUs[i] = timesUs[i + 1] - timesUs[i];
}
sizes[cuePointsSize - 1] = (int) (segmentEndOffsetBytes - offsets[cuePointsSize - 1]);
durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
cues = new SegmentIndex((int) cuesSizeBytes, sizes, offsets, durationsUs, timesUs);
cueTimesUs = null;
cueClusterPositions = null;
readResults |= RESULT_READ_INDEX;
}
/**
* Returns the cues for the media stream.
*
* @return The cues in the form of a {@link SegmentIndex}, or null if the extractor is not yet
* prepared
* Passes events through to {@link WebmExtractor} as
* callbacks from {@link EbmlReader} are received.
*/
public SegmentIndex getIndex();
private final class InnerEbmlEventHandler implements EbmlEventHandler {
/**
* Returns the format of the samples contained within the media stream.
*
* @return The sample media format, or null if the extracted is not yet prepared
*/
public MediaFormat getFormat();
@Override
public int getElementType(int id) {
return WebmExtractor.this.getElementType(id);
}
@Override
public void onMasterElementStart(
int id, long elementOffsetBytes, int headerSizeBytes, long contentsSizeBytes) {
WebmExtractor.this.onMasterElementStart(
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
}
@Override
public void onMasterElementEnd(int id) {
WebmExtractor.this.onMasterElementEnd(id);
}
@Override
public void onIntegerElement(int id, long value) {
WebmExtractor.this.onIntegerElement(id, value);
}
@Override
public void onFloatElement(int id, double value) {
WebmExtractor.this.onFloatElement(id, value);
}
@Override
public void onStringElement(int id, String value) {
WebmExtractor.this.onStringElement(id, value);
}
@Override
public boolean onBinaryElement(
int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
NonBlockingInputStream inputStream) {
return WebmExtractor.this.onBinaryElement(
id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes, inputStream);
}
}
}

View File

@ -26,7 +26,7 @@ import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.chunk.Mp4MediaChunk;
import com.google.android.exoplayer.parser.mp4.CodecSpecificDataUtil;
import com.google.android.exoplayer.parser.Extractor;
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer.parser.mp4.Track;
import com.google.android.exoplayer.parser.mp4.TrackEncryptionBox;
@ -35,6 +35,7 @@ import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.Stre
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import android.net.Uri;
import android.util.Base64;
@ -227,7 +228,7 @@ public class SmoothStreamingChunkSource implements ChunkSource {
}
private static MediaChunk newMediaChunk(Format formatInfo, Uri uri, String cacheKey,
FragmentedMp4Extractor extractor, DataSource dataSource, int chunkIndex,
Extractor extractor, DataSource dataSource, int chunkIndex,
boolean isLast, long chunkStartTimeUs, long nextChunkStartTimeUs, int trigger) {
int nextChunkIndex = isLast ? -1 : chunkIndex + 1;
long nextStartTimeUs = isLast ? -1 : nextChunkStartTimeUs;

View File

@ -20,8 +20,6 @@ import com.google.android.exoplayer.util.ManifestFetcher;
import android.net.Uri;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
@ -59,11 +57,7 @@ public final class SmoothStreamingManifestFetcher extends ManifestFetcher<Smooth
@Override
protected SmoothStreamingManifest parse(InputStream stream, String inputEncoding,
String contentId, Uri baseUrl) throws IOException, ParserException {
try {
return parser.parse(stream, inputEncoding);
} catch (XmlPullParserException e) {
throw new ParserException(e);
}
return parser.parse(stream, inputEncoding);
}
}

View File

@ -16,11 +16,11 @@
package com.google.android.exoplayer.smoothstreaming;
import com.google.android.exoplayer.ParserException;
import com.google.android.exoplayer.parser.mp4.CodecSpecificDataUtil;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.StreamElement;
import com.google.android.exoplayer.smoothstreaming.SmoothStreamingManifest.TrackElement;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.CodecSpecificDataUtil;
import android.util.Base64;
import android.util.Pair;
@ -60,15 +60,18 @@ public class SmoothStreamingManifestParser {
* @param inputEncoding The encoding of the input.
* @return The parsed manifest.
* @throws IOException If a problem occurred reading from the stream.
* @throws XmlPullParserException If a problem occurred parsing the stream as xml.
* @throws ParserException If a problem occurred parsing the xml as a smooth streaming manifest.
*/
public SmoothStreamingManifest parse(InputStream inputStream, String inputEncoding) throws
XmlPullParserException, IOException, ParserException {
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
xmlParser.setInput(inputStream, inputEncoding);
SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null);
return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
IOException, ParserException {
try {
XmlPullParser xmlParser = xmlParserFactory.newPullParser();
xmlParser.setInput(inputStream, inputEncoding);
SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null);
return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser);
} catch (XmlPullParserException e) {
throw new ParserException(e);
}
}
/**
@ -102,6 +105,7 @@ public class SmoothStreamingManifestParser {
ParserException {
String tagName;
boolean foundStartTag = false;
int skippingElementDepth = 0;
while (true) {
int eventType = xmlParser.getEventType();
switch (eventType) {
@ -111,24 +115,35 @@ public class SmoothStreamingManifestParser {
foundStartTag = true;
parseStartTag(xmlParser);
} else if (foundStartTag) {
if (handleChildInline(tagName)) {
if (skippingElementDepth > 0) {
skippingElementDepth++;
} else if (handleChildInline(tagName)) {
parseStartTag(xmlParser);
} else {
addChild(newChildParser(this, tagName).parse(xmlParser));
ElementParser childElementParser = newChildParser(this, tagName);
if (childElementParser == null) {
skippingElementDepth = 1;
} else {
addChild(childElementParser.parse(xmlParser));
}
}
}
break;
case XmlPullParser.TEXT:
if (foundStartTag) {
if (foundStartTag && skippingElementDepth == 0) {
parseText(xmlParser);
}
break;
case XmlPullParser.END_TAG:
if (foundStartTag) {
tagName = xmlParser.getName();
parseEndTag(xmlParser);
if (!handleChildInline(tagName)) {
return build();
if (skippingElementDepth > 0) {
skippingElementDepth--;
} else {
tagName = xmlParser.getName();
parseEndTag(xmlParser);
if (!handleChildInline(tagName)) {
return build();
}
}
}
break;
@ -357,6 +372,7 @@ public class SmoothStreamingManifestParser {
public static final String KEY_SYSTEM_ID = "SystemID";
private boolean inProtectionHeader;
private UUID uuid;
private byte[] initData;
@ -371,16 +387,25 @@ public class SmoothStreamingManifestParser {
@Override
public void parseStartTag(XmlPullParser parser) {
if (!TAG_PROTECTION_HEADER.equals(parser.getName())) {
return;
if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
inProtectionHeader = true;
String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID);
uuid = UUID.fromString(uuidString);
}
String uuidString = parser.getAttributeValue(null, KEY_SYSTEM_ID);
uuid = UUID.fromString(uuidString);
}
@Override
public void parseText(XmlPullParser parser) {
initData = Base64.decode(parser.getText(), Base64.DEFAULT);
if (inProtectionHeader) {
initData = Base64.decode(parser.getText(), Base64.DEFAULT);
}
}
@Override
public void parseEndTag(XmlPullParser parser) {
if (TAG_PROTECTION_HEADER.equals(parser.getName())) {
inProtectionHeader = false;
}
}
@Override
@ -579,9 +604,11 @@ public class SmoothStreamingManifestParser {
if (type == StreamElement.TYPE_VIDEO) {
maxHeight = parseRequiredInt(parser, KEY_MAX_HEIGHT);
maxWidth = parseRequiredInt(parser, KEY_MAX_WIDTH);
fourCC = parseRequiredString(parser, KEY_FOUR_CC);
} else {
maxHeight = -1;
maxWidth = -1;
fourCC = parser.getAttributeValue(null, KEY_FOUR_CC);
}
if (type == StreamElement.TYPE_AUDIO) {
@ -590,14 +617,12 @@ public class SmoothStreamingManifestParser {
bitPerSample = parseRequiredInt(parser, KEY_BITS_PER_SAMPLE);
packetSize = parseRequiredInt(parser, KEY_PACKET_SIZE);
audioTag = parseRequiredInt(parser, KEY_AUDIO_TAG);
fourCC = parseRequiredString(parser, KEY_FOUR_CC);
} else {
samplingRate = -1;
channels = -1;
bitPerSample = -1;
packetSize = -1;
audioTag = -1;
fourCC = parser.getAttributeValue(null, KEY_FOUR_CC);
}
value = parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA);

View File

@ -16,7 +16,7 @@
package com.google.android.exoplayer.text;
import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.FormatHolder;
import com.google.android.exoplayer.MediaFormatHolder;
import com.google.android.exoplayer.SampleHolder;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackRenderer;
@ -64,7 +64,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
private final TextRenderer textRenderer;
private final SampleSource source;
private final SampleHolder sampleHolder;
private final FormatHolder formatHolder;
private final MediaFormatHolder formatHolder;
private final SubtitleParser subtitleParser;
private int trackIndex;
@ -93,7 +93,7 @@ public class TextTrackRenderer extends TrackRenderer implements Callback {
this.textRenderer = Assertions.checkNotNull(textRenderer);
this.textRendererHandler = textRendererLooper == null ? null : new Handler(textRendererLooper,
this);
formatHolder = new FormatHolder();
formatHolder = new MediaFormatHolder();
sampleHolder = new SampleHolder(true);
}

View File

@ -41,10 +41,10 @@ public final class CacheDataSource implements DataSource {
public interface EventListener {
/**
* Invoked when bytes have been read from {@link #cache} since the last invocation.
* Invoked when bytes have been read from the cache.
*
* @param cacheSizeBytes Current cache size in bytes.
* @param cachedBytesRead Total bytes read from {@link #cache} since last report.
* @param cachedBytesRead Total bytes read from the cache since this method was last invoked.
*/
void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead);

View File

@ -13,9 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.parser.mp4;
import com.google.android.exoplayer.util.Assertions;
package com.google.android.exoplayer.util;
import android.annotation.SuppressLint;
import android.media.MediaCodecInfo.CodecProfileLevel;
@ -35,6 +33,10 @@ public final class CodecSpecificDataUtil {
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350
};
private static final int[] AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE = new int[] {
0, 1, 2, 3, 4, 5, 6, 8
};
private static final int SPS_NAL_UNIT_TYPE = 7;
private CodecSpecificDataUtil() {}
@ -42,7 +44,7 @@ public final class CodecSpecificDataUtil {
/**
* Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
*
* @param audioSpecificConfig
* @param audioSpecificConfig The AudioSpecificConfig to parse.
* @return A pair consisting of the sample rate in Hz and the channel count.
*/
public static Pair<Integer, Integer> parseAudioSpecificConfig(byte[] audioSpecificConfig) {
@ -56,11 +58,27 @@ public final class CodecSpecificDataUtil {
return Pair.create(sampleRate, channelCount);
}
/**
* Builds a simple AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
*
* @param audioObjectType The audio object type.
* @param sampleRateIndex The sample rate index.
* @param channelConfig The channel configuration.
* @return The AudioSpecificConfig.
*/
public static byte[] buildAudioSpecificConfig(int audioObjectType, int sampleRateIndex,
int channelConfig) {
byte[] audioSpecificConfig = new byte[2];
audioSpecificConfig[0] = (byte) ((audioObjectType << 3) & 0xF8 | (sampleRateIndex >> 1) & 0x07);
audioSpecificConfig[1] = (byte) ((sampleRateIndex << 7) & 0x80 | (channelConfig << 3) & 0x78);
return audioSpecificConfig;
}
/**
* Builds a simple HE-AAC LC AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
*
* @param sampleRate The sample rate in Hz.
* @param numChannels The number of channels
* @param numChannels The number of channels.
* @return The AudioSpecificConfig.
*/
public static byte[] buildAudioSpecificConfig(int sampleRate, int numChannels) {
@ -70,10 +88,16 @@ public final class CodecSpecificDataUtil {
sampleRateIndex = i;
}
}
int channelConfig = -1;
for (int i = 0; i < AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE.length; ++i) {
if (numChannels == AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[i]) {
channelConfig = i;
}
}
// The full specification for AudioSpecificConfig is stated in ISO 14496-3 Section 1.6.2.1
byte[] csd = new byte[2];
csd[0] = (byte) ((2 /* AAC LC */ << 3) | (sampleRateIndex >> 1));
csd[1] = (byte) (((sampleRateIndex & 0x1) << 7) | (numChannels << 3));
csd[1] = (byte) (((sampleRateIndex & 0x1) << 7) | (channelConfig << 3));
return csd;
}