/* * 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.Mp4MediaChunk; import com.google.android.exoplayer.dash.mpd.Representation; import com.google.android.exoplayer.parser.SegmentIndex; import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor; 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 Mp4 DASH streams. */ public class DashMp4ChunkSource implements ChunkSource { public static final int DEFAULT_NUM_SEGMENTS_PER_CHUNK = 1; private static final int EXPECTED_INITIALIZATION_RESULT = FragmentedMp4Extractor.RESULT_END_OF_STREAM | FragmentedMp4Extractor.RESULT_READ_MOOV | FragmentedMp4Extractor.RESULT_READ_SIDX; private static final String TAG = "DashMp4ChunkSource"; 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<FragmentedMp4Extractor> extractors; 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 DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator, Representation... representations) { this(dataSource, evaluator, DEFAULT_NUM_SEGMENTS_PER_CHUNK, representations); } /** * @param dataSource A {@link DataSource} suitable for loading the media data. * @param evaluator Selects from the available formats. * @param numSegmentsPerChunk The number of segments (as defined in the stream's segment index) * that should be grouped into a single chunk. * @param representations The representations to be considered by the source. */ public DashMp4ChunkSource(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<FragmentedMp4Extractor>(); 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 FragmentedMp4Extractor()); 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); FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id); if (extractor.getTrack() == null) { Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor, dataSource, evaluation.trigger); lastChunkWasInitialization = true; out.chunk = initializationChunk; return; } int nextIndex; if (queue.isEmpty()) { nextIndex = Arrays.binarySearch(extractor.getSegmentIndex().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.getSegmentIndex(), nextIndex, evaluation.trigger, numSegmentsPerChunk); lastChunkWasInitialization = false; out.chunk = nextMediaChunk; } @Override public IOException getError() { return null; } private static Chunk newInitializationChunk(Representation representation, FragmentedMp4Extractor extractor, DataSource dataSource, int trigger) { DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1, representation.getCacheKey()); return new InitializationMp4Loadable(dataSource, dataSpec, trigger, extractor, representation); } private static Chunk newMediaChunk(Representation representation, FragmentedMp4Extractor extractor, DataSource dataSource, SegmentIndex sidx, int index, int trigger, int numSegmentsPerChunk) { // Computes the segments to included in the next fetch. int numSegmentsToFetch = Math.min(numSegmentsPerChunk, sidx.length - index); int lastSegmentInChunk = index + numSegmentsToFetch - 1; int nextIndex = lastSegmentInChunk == sidx.length - 1 ? -1 : lastSegmentInChunk + 1; long startTimeUs = sidx.timesUs[index]; // Compute the end time, prefer to use next segment start time if there is a next segment. long endTimeUs = nextIndex == -1 ? sidx.timesUs[lastSegmentInChunk] + sidx.durationsUs[lastSegmentInChunk] : sidx.timesUs[nextIndex]; long offset = (int) representation.indexEnd + 1 + sidx.offsets[index]; // Compute combined segments byte length. long size = 0; for (int i = index; i <= lastSegmentInChunk; i++) { size += sidx.sizes[i]; } DataSpec dataSpec = new DataSpec(representation.uri, offset, size, representation.getCacheKey()); return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, extractor, startTimeUs, endTimeUs, 0, nextIndex); } private static class InitializationMp4Loadable extends Chunk { private final Representation representation; private final FragmentedMp4Extractor extractor; public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger, FragmentedMp4Extractor extractor, Representation representation) { super(dataSource, dataSpec, representation.format, trigger); this.extractor = extractor; this.representation = representation; } @Override protected void consumeStream(NonBlockingInputStream stream) throws IOException { int result = extractor.read(stream, null); if (result != EXPECTED_INITIALIZATION_RESULT) { throw new ParserException("Invalid initialization data"); } validateSegmentIndex(extractor.getSegmentIndex()); } private void validateSegmentIndex(SegmentIndex segmentIndex) { long expectedIndexLen = representation.indexEnd - representation.indexStart + 1; if (segmentIndex.sizeBytes != expectedIndexLen) { Log.w(TAG, "Sidx length mismatch: sidxLen = " + segmentIndex.sizeBytes + ", ExpectedLen = " + expectedIndexLen); } long sidxContentLength = segmentIndex.offsets[segmentIndex.length - 1] + segmentIndex.sizes[segmentIndex.length - 1] + representation.indexEnd + 1; if (sidxContentLength != representation.contentLength) { Log.w(TAG, "ContentLength mismatch: Actual = " + sidxContentLength + ", Expected = " + representation.contentLength); } } } }