/* * 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.Representation; import com.google.android.exoplayer.parser.SegmentIndex; 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.util.Log; import android.util.SparseArray; import java.io.IOException; import java.util.Arrays; import java.util.List; /** * An {@link ChunkSource} for WebM DASH streams. */ public class DashWebmChunkSource implements ChunkSource { private static final String TAG = "DashWebmChunkSource"; 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 int numSegmentsPerChunk; private final Format[] formats; private final SparseArray<Representation> representations; private final SparseArray<WebmExtractor> extractors; private boolean lastChunkWasInitialization; public DashWebmChunkSource( DataSource dataSource, FormatEvaluator evaluator, Representation... representations) { this(dataSource, evaluator, 1, representations); } public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator, int numSegmentsPerChunk, Representation... representations) { this.dataSource = dataSource; this.evaluator = evaluator; this.numSegmentsPerChunk = numSegmentsPerChunk; this.formats = new Format[representations.length]; this.extractors = new SparseArray<WebmExtractor>(); this.representations = new SparseArray<Representation>(); this.trackInfo = new TrackInfo( representations[0].format.mimeType, representations[0].periodDuration * 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.append(formats[i].id, new WebmExtractor()); this.representations.put(formats[i].id, representations[i]); } 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<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 == 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); if (!extractor.isPrepared()) { Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor, dataSource, evaluation.trigger); lastChunkWasInitialization = true; out.chunk = initializationChunk; return; } int nextIndex; if (queue.isEmpty()) { nextIndex = Arrays.binarySearch(extractor.getCues().timesUs, seekPositionUs); nextIndex = nextIndex < 0 ? -nextIndex - 2 : nextIndex; } else { nextIndex = queue.get(out.queueSize - 1).nextChunkIndex; } if (nextIndex == -1) { out.chunk = null; return; } Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource, extractor.getCues(), nextIndex, evaluation.trigger, numSegmentsPerChunk); lastChunkWasInitialization = false; out.chunk = nextMediaChunk; } @Override public IOException getError() { return null; } private static Chunk newInitializationChunk(Representation representation, WebmExtractor extractor, DataSource dataSource, int trigger) { DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1, representation.getCacheKey()); return new InitializationWebmLoadable(dataSource, dataSpec, trigger, extractor, representation); } private static Chunk newMediaChunk(Representation representation, WebmExtractor extractor, DataSource dataSource, SegmentIndex cues, int index, int trigger, int numSegmentsPerChunk) { // Computes the segments to included in the next fetch. int numSegmentsToFetch = Math.min(numSegmentsPerChunk, cues.length - index); int lastSegmentInChunk = index + numSegmentsToFetch - 1; int nextIndex = lastSegmentInChunk == cues.length - 1 ? -1 : lastSegmentInChunk + 1; long startTimeUs = cues.timesUs[index]; // Compute the end time, prefer to use next segment start time if there is a next segment. long endTimeUs = nextIndex == -1 ? cues.timesUs[lastSegmentInChunk] + cues.durationsUs[lastSegmentInChunk] : cues.timesUs[nextIndex]; long offset = cues.offsets[index]; // Compute combined segments byte length. long size = 0; for (int i = index; i <= lastSegmentInChunk; i++) { size += cues.sizes[i]; } DataSpec dataSpec = new DataSpec(representation.uri, offset, size, representation.getCacheKey()); return new WebmMediaChunk(dataSource, dataSpec, representation.format, trigger, extractor, startTimeUs, endTimeUs, nextIndex); } private static class InitializationWebmLoadable extends Chunk { private final Representation representation; private final WebmExtractor extractor; public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger, WebmExtractor extractor, Representation representation) { super(dataSource, dataSpec, representation.format, trigger); this.extractor = extractor; this.representation = representation; } @Override protected void consumeStream(NonBlockingInputStream stream) throws IOException { extractor.read(stream, null); if (!extractor.isPrepared()) { throw new ParserException("Invalid initialization data"); } validateCues(extractor.getCues()); } private void validateCues(SegmentIndex cues) { long expectedSizeBytes = representation.indexEnd - representation.indexStart + 1; if (cues.sizeBytes != expectedSizeBytes) { Log.w(TAG, "Cues length mismatch: got " + cues.sizeBytes + " but expected " + expectedSizeBytes); } long expectedContentLength = cues.offsets[cues.length - 1] + cues.sizes[cues.length - 1] + representation.indexEnd + 1; if (representation.contentLength > 0 && expectedContentLength != representation.contentLength) { Log.w(TAG, "ContentLength mismatch: got " + expectedContentLength + " but expected " + representation.contentLength); } } } }