private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException { type = parseType(parser); putNormalizedAttribute(KEY_TYPE, type); if (type == StreamElement.TYPE_TEXT) { subType = parseRequiredString(parser, KEY_SUB_TYPE); } else { subType = parser.getAttributeValue(null, KEY_SUB_TYPE); } name = parser.getAttributeValue(null, KEY_NAME); qualityLevels = parseInt(parser, KEY_QUALITY_LEVELS, -1); url = parseRequiredString(parser, KEY_URL); maxWidth = parseInt(parser, KEY_MAX_WIDTH, -1); maxHeight = parseInt(parser, KEY_MAX_HEIGHT, -1); displayWidth = parseInt(parser, KEY_DISPLAY_WIDTH, -1); displayHeight = parseInt(parser, KEY_DISPLAY_HEIGHT, -1); language = parser.getAttributeValue(null, KEY_LANGUAGE); timescale = parseInt(parser, KEY_TIME_SCALE, -1); if (timescale == -1) { timescale = (Long) getNormalizedAttribute(KEY_TIME_SCALE); } startTimes = new ArrayList<>(); }
private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException { type = parseType(parser); putNormalizedAttribute(KEY_TYPE, type); if (type == StreamElement.TYPE_TEXT) { subType = parseRequiredString(parser, KEY_SUB_TYPE); } else { subType = parser.getAttributeValue(null, KEY_SUB_TYPE); } name = parser.getAttributeValue(null, KEY_NAME); qualityLevels = parseInt(parser, KEY_QUALITY_LEVELS, -1); url = parseRequiredString(parser, KEY_URL); maxWidth = parseInt(parser, KEY_MAX_WIDTH, -1); maxHeight = parseInt(parser, KEY_MAX_HEIGHT, -1); displayWidth = parseInt(parser, KEY_DISPLAY_WIDTH, -1); displayHeight = parseInt(parser, KEY_DISPLAY_HEIGHT, -1); language = parser.getAttributeValue(null, KEY_LANGUAGE); putNormalizedAttribute(KEY_LANGUAGE, language); timescale = parseInt(parser, KEY_TIME_SCALE, -1); if (timescale == -1) { timescale = (Long) getNormalizedAttribute(KEY_TIME_SCALE); } startTimes = new ArrayList<>(); }
@Override public void adaptiveTrack(SmoothStreamingManifest manifest, int element, int[] trackIndices) { if (adaptiveFormatEvaluator == null) { // Do nothing. return; } MediaFormat maxHeightMediaFormat = null; StreamElement streamElement = manifest.streamElements[element]; int maxWidth = -1; int maxHeight = -1; Format[] formats = new Format[trackIndices.length]; for (int i = 0; i < formats.length; i++) { int manifestTrackIndex = trackIndices[i]; formats[i] = streamElement.tracks[manifestTrackIndex].format; MediaFormat mediaFormat = initManifestTrack(manifest, element, manifestTrackIndex); if (maxHeightMediaFormat == null || mediaFormat.height > maxHeight) { maxHeightMediaFormat = mediaFormat; } maxWidth = Math.max(maxWidth, mediaFormat.width); maxHeight = Math.max(maxHeight, mediaFormat.height); } Arrays.sort(formats, new DecreasingBandwidthComparator()); MediaFormat adaptiveMediaFormat = maxHeightMediaFormat.copyAsAdaptive(null); tracks.add(new ExposedTrack(adaptiveMediaFormat, element, formats, maxWidth, maxHeight)); }
private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException { type = parseType(parser); putNormalizedAttribute(KEY_TYPE, type); if (type == StreamElement.TYPE_TEXT) { subType = parseRequiredString(parser, KEY_SUB_TYPE); } else { subType = parser.getAttributeValue(null, KEY_SUB_TYPE); } name = parser.getAttributeValue(null, KEY_NAME); qualityLevels = parseInt(parser, KEY_QUALITY_LEVELS, -1); url = parseRequiredString(parser, KEY_URL); maxWidth = parseInt(parser, KEY_MAX_WIDTH, -1); maxHeight = parseInt(parser, KEY_MAX_HEIGHT, -1); displayWidth = parseInt(parser, KEY_DISPLAY_WIDTH, -1); displayHeight = parseInt(parser, KEY_DISPLAY_HEIGHT, -1); language = parser.getAttributeValue(null, KEY_LANGUAGE); timescale = parseInt(parser, KEY_TIME_SCALE, -1); if (timescale == -1) { timescale = (Long) getNormalizedAttribute(KEY_TIME_SCALE); } startTimes = new ArrayList<Long>(); }
@Override public void continueBuffering(long playbackPositionUs) { if (manifestFetcher == null || !currentManifest.isLive || fatalError != null) { return; } SmoothStreamingManifest newManifest = manifestFetcher.getManifest(); if (currentManifest != newManifest && newManifest != null) { StreamElement currentElement = getElement(currentManifest); StreamElement newElement = getElement(newManifest); if (newElement.chunkCount == 0) { currentManifestChunkOffset += currentElement.chunkCount; } else if (currentElement.chunkCount > 0) { currentManifestChunkOffset += currentElement.getChunkIndex(newElement.getStartTimeUs(0)); } currentManifest = newManifest; finishedCurrentManifest = false; } if (finishedCurrentManifest && (SystemClock.elapsedRealtime() > manifestFetcher.getManifestLoadTimestamp() + MINIMUM_MANIFEST_REFRESH_PERIOD_MS)) { manifestFetcher.requestRefresh(); } }
private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException { type = parseType(parser); putNormalizedAttribute(KEY_TYPE, type); if (type == StreamElement.TYPE_TEXT) { subType = parseRequiredString(parser, KEY_SUB_TYPE); } else { subType = parser.getAttributeValue(null, KEY_SUB_TYPE); } name = parser.getAttributeValue(null, KEY_NAME); qualityLevels = parseInt(parser, KEY_QUALITY_LEVELS, -1); url = parseRequiredString(parser, KEY_URL); maxWidth = parseInt(parser, KEY_MAX_WIDTH, -1); maxHeight = parseInt(parser, KEY_MAX_HEIGHT, -1); displayWidth = parseInt(parser, KEY_DISPLAY_WIDTH, -1); displayHeight = parseInt(parser, KEY_DISPLAY_HEIGHT, -1); language = parser.getAttributeValue(null, KEY_LANGUAGE); timeScale = parseInt(parser, KEY_TIME_SCALE, -1); if (timeScale == -1) { timeScale = (Long) getNormalizedAttribute(KEY_TIME_SCALE); } startTimes = new long[parseRequiredInt(parser, KEY_CHUNKS)]; }
@Override public Object build() { StreamElement[] streamElementArray = new StreamElement[streamElements.size()]; streamElements.toArray(streamElementArray); return new SmoothStreamingManifest(majorVersion, minorVersion, timescale, duration, dvrWindowLength, lookAheadCount, isLive, protectionElement, streamElementArray); }
private int parseType(XmlPullParser parser) throws ParserException { String value = parser.getAttributeValue(null, KEY_TYPE); if (value != null) { if (KEY_TYPE_AUDIO.equalsIgnoreCase(value)) { return StreamElement.TYPE_AUDIO; } else if (KEY_TYPE_VIDEO.equalsIgnoreCase(value)) { return StreamElement.TYPE_VIDEO; } else if (KEY_TYPE_TEXT.equalsIgnoreCase(value)) { return StreamElement.TYPE_TEXT; } else { throw new ParserException("Invalid key value[" + value + "]"); } } throw new MissingFieldException(KEY_TYPE); }
@Override public Object build() { TrackElement[] trackElements = new TrackElement[tracks.size()]; tracks.toArray(trackElements); return new StreamElement(baseUri, url, type, subType, timescale, name, qualityLevels, maxWidth, maxHeight, displayWidth, displayHeight, language, trackElements, startTimes, lastChunkDuration); }
@Override public void continueBuffering(long playbackPositionUs) { if (manifestFetcher == null || !currentManifest.isLive || fatalError != null) { return; } SmoothStreamingManifest newManifest = manifestFetcher.getManifest(); if (currentManifest != newManifest && newManifest != null) { StreamElement currentElement = getElement(currentManifest); int currentElementChunkCount = currentElement.chunkCount; StreamElement newElement = getElement(newManifest); if (currentElementChunkCount == 0 || newElement.chunkCount == 0) { // There's no overlap between the old and new elements because at least one is empty. currentManifestChunkOffset += currentElementChunkCount; } else { long currentElementEndTimeUs = currentElement.getStartTimeUs(currentElementChunkCount - 1) + currentElement.getChunkDurationUs(currentElementChunkCount - 1); long newElementStartTimeUs = newElement.getStartTimeUs(0); if (currentElementEndTimeUs <= newElementStartTimeUs) { // There's no overlap between the old and new elements. currentManifestChunkOffset += currentElementChunkCount; } else { // The new element overlaps with the old one. currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs); } } currentManifest = newManifest; finishedCurrentManifest = false; } if (finishedCurrentManifest && (SystemClock.elapsedRealtime() > manifestFetcher.getManifestLoadStartTimestamp() + MINIMUM_MANIFEST_REFRESH_PERIOD_MS)) { manifestFetcher.requestRefresh(); } }
/** * For live playbacks, determines the seek position that snaps playback to be * {@link #liveEdgeLatencyUs} behind the live edge of the current manifest * * @return The seek position in microseconds. */ private long getLiveSeekPosition() { long liveEdgeTimestampUs = Long.MIN_VALUE; for (int i = 0; i < currentManifest.streamElements.length; i++) { StreamElement streamElement = currentManifest.streamElements[i]; if (streamElement.chunkCount > 0) { long elementLiveEdgeTimestampUs = streamElement.getStartTimeUs(streamElement.chunkCount - 1) + streamElement.getChunkDurationUs(streamElement.chunkCount - 1); liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, elementLiveEdgeTimestampUs); } } return liveEdgeTimestampUs - liveEdgeLatencyUs; }
@Override public void selectTracks(SmoothStreamingManifest manifest, Output output) throws IOException { for (int i = 0; i < manifest.streamElements.length; i++) { TrackElement[] tracks = manifest.streamElements[i].tracks; if (manifest.streamElements[i].type == streamElementType) { if (streamElementType == StreamElement.TYPE_VIDEO) { int[] trackIndices; if (filterVideoRepresentations) { trackIndices = VideoFormatSelectorUtil.selectVideoFormatsForDefaultDisplay( context, Arrays.asList(tracks), null, filterProtectedHdContent && manifest.protectionElement != null); } else { trackIndices = Util.firstIntegersArray(tracks.length); } int trackCount = trackIndices.length; if (trackCount > 1) { output.adaptiveTrack(manifest, i, trackIndices); } for (int j = 0; j < trackCount; j++) { output.fixedTrack(manifest, i, trackIndices[j]); } } else { for (int j = 0; j < tracks.length; j++) { output.fixedTrack(manifest, i, j); } } } } }
@Override public void continueBuffering(long playbackPositionUs) { if (manifestFetcher == null || !currentManifest.isLive || fatalError != null) { return; } SmoothStreamingManifest newManifest = manifestFetcher.getManifest(); if (currentManifest != newManifest && newManifest != null) { StreamElement currentElement = currentManifest.streamElements[enabledTrack.elementIndex]; int currentElementChunkCount = currentElement.chunkCount; StreamElement newElement = newManifest.streamElements[enabledTrack.elementIndex]; if (currentElementChunkCount == 0 || newElement.chunkCount == 0) { // There's no overlap between the old and new elements because at least one is empty. currentManifestChunkOffset += currentElementChunkCount; } else { long currentElementEndTimeUs = currentElement.getStartTimeUs(currentElementChunkCount - 1) + currentElement.getChunkDurationUs(currentElementChunkCount - 1); long newElementStartTimeUs = newElement.getStartTimeUs(0); if (currentElementEndTimeUs <= newElementStartTimeUs) { // There's no overlap between the old and new elements. currentManifestChunkOffset += currentElementChunkCount; } else { // The new element overlaps with the old one. currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs); } } currentManifest = newManifest; needManifestRefresh = false; } if (needManifestRefresh && (SystemClock.elapsedRealtime() > manifestFetcher.getManifestLoadStartTimestamp() + MINIMUM_MANIFEST_REFRESH_PERIOD_MS)) { manifestFetcher.requestRefresh(); } }
/** * For live playbacks, determines the seek position that snaps playback to be * {@code liveEdgeLatencyUs} behind the live edge of the provided manifest. * * @param manifest The manifest. * @param liveEdgeLatencyUs The live edge latency, in microseconds. * @return The seek position in microseconds. */ private static long getLiveSeekPosition(SmoothStreamingManifest manifest, long liveEdgeLatencyUs) { long liveEdgeTimestampUs = Long.MIN_VALUE; for (int i = 0; i < manifest.streamElements.length; i++) { StreamElement streamElement = manifest.streamElements[i]; if (streamElement.chunkCount > 0) { long elementLiveEdgeTimestampUs = streamElement.getStartTimeUs(streamElement.chunkCount - 1) + streamElement.getChunkDurationUs(streamElement.chunkCount - 1); liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, elementLiveEdgeTimestampUs); } } return liveEdgeTimestampUs - liveEdgeLatencyUs; }
private static int getManifestTrackIndex(StreamElement element, Format format) { TrackElement[] tracks = element.tracks; for (int i = 0; i < tracks.length; i++) { if (tracks[i].format.equals(format)) { return i; } } // Should never happen. throw new IllegalStateException("Invalid format: " + format); }
@Override public Object build() { StreamElement[] streamElementArray = new StreamElement[streamElements.size()]; streamElements.toArray(streamElementArray); return new SmoothStreamingManifest(majorVersion, minorVersion, timeScale, duration, lookAheadCount, protectionElement, streamElementArray); }
@Override public Object build() { TrackElement[] trackElements = new TrackElement[tracks.size()]; tracks.toArray(trackElements); return new StreamElement(type, subType, timeScale, name, qualityLevels, url, maxWidth, maxHeight, displayWidth, displayHeight, language, trackElements, startTimes); }
@Override public void parseStartTag(XmlPullParser parser) throws ParserException { int type = (Integer) getNormalizedAttribute(KEY_TYPE); String value; index = parseInt(parser, KEY_INDEX, -1); bitrate = parseRequiredInt(parser, KEY_BITRATE); if (type == StreamElement.TYPE_VIDEO) { maxHeight = parseRequiredInt(parser, KEY_MAX_HEIGHT); maxWidth = parseRequiredInt(parser, KEY_MAX_WIDTH); mimeType = fourCCToMimeType(parseRequiredString(parser, KEY_FOUR_CC)); } else { maxHeight = -1; maxWidth = -1; String fourCC = parser.getAttributeValue(null, KEY_FOUR_CC); // If fourCC is missing and the stream type is audio, we assume AAC. mimeType = fourCC != null ? fourCCToMimeType(fourCC) : type == StreamElement.TYPE_AUDIO ? MimeTypes.AUDIO_AAC : null; } if (type == StreamElement.TYPE_AUDIO) { samplingRate = parseRequiredInt(parser, KEY_SAMPLING_RATE); channels = parseRequiredInt(parser, KEY_CHANNELS); } else { samplingRate = -1; channels = -1; } value = parser.getAttributeValue(null, KEY_CODEC_PRIVATE_DATA); if (value != null && value.length() > 0) { byte[] codecPrivateData = hexStringToByteArray(value); byte[][] split = CodecSpecificDataUtil.splitNalUnits(codecPrivateData); if (split == null) { csd.add(codecPrivateData); } else { for (int i = 0; i < split.length; i++) { csd.add(split[i]); } } } }
private SmoothStreamingChunkSource(ManifestFetcher<SmoothStreamingManifest> manifestFetcher, SmoothStreamingManifest initialManifest, int streamElementIndex, int[] trackIndices, DataSource dataSource, FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) { this.manifestFetcher = manifestFetcher; this.streamElementIndex = streamElementIndex; this.currentManifest = initialManifest; this.dataSource = dataSource; this.formatEvaluator = formatEvaluator; this.liveEdgeLatencyUs = liveEdgeLatencyMs * 1000; StreamElement streamElement = getElement(initialManifest); trackInfo = new TrackInfo(streamElement.tracks[0].format.mimeType, initialManifest.durationUs); evaluation = new Evaluation(); TrackEncryptionBox[] trackEncryptionBoxes = null; ProtectionElement protectionElement = initialManifest.protectionElement; if (protectionElement != null) { byte[] keyId = getKeyId(protectionElement.data); trackEncryptionBoxes = new TrackEncryptionBox[1]; trackEncryptionBoxes[0] = new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId); DrmInitData.Mapped drmInitData = new DrmInitData.Mapped(MimeTypes.VIDEO_MP4); drmInitData.put(protectionElement.uuid, protectionElement.data); this.drmInitData = drmInitData; } else { drmInitData = null; } int trackCount = trackIndices != null ? trackIndices.length : streamElement.tracks.length; formats = new Format[trackCount]; extractorWrappers = new SparseArray<>(); mediaFormats = new SparseArray<>(); int maxWidth = 0; int maxHeight = 0; for (int i = 0; i < trackCount; i++) { int trackIndex = trackIndices != null ? trackIndices[i] : i; formats[i] = streamElement.tracks[trackIndex].format; maxWidth = Math.max(maxWidth, formats[i].width); maxHeight = Math.max(maxHeight, formats[i].height); MediaFormat mediaFormat = getMediaFormat(streamElement, trackIndex); int trackType = streamElement.type == StreamElement.TYPE_VIDEO ? Track.TYPE_VIDEO : Track.TYPE_AUDIO; FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( FragmentedMp4Extractor.WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME); extractor.setTrack(new Track(trackIndex, trackType, streamElement.timescale, initialManifest.durationUs, mediaFormat, trackEncryptionBoxes, trackType == Track.TYPE_VIDEO ? 4 : -1)); extractorWrappers.put(trackIndex, new ChunkExtractorWrapper(extractor)); mediaFormats.put(trackIndex, mediaFormat); } this.maxWidth = maxWidth; this.maxHeight = maxHeight; Arrays.sort(formats, new DecreasingBandwidthComparator()); }
private StreamElement getElement(SmoothStreamingManifest manifest) { return manifest.streamElements[streamElementIndex]; }