Merge fMP4/H264 and WebM/VP9 DASH implementations.
This commit is contained in:
parent
1ddd5c6e16
commit
52a300f1fd
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
@ -18,6 +18,7 @@ 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;
|
||||
@ -48,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.
|
||||
@ -59,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};
|
||||
@ -197,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -229,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 {
|
||||
@ -287,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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -26,6 +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.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;
|
||||
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user