private static DrmInitData getDrmInitData(MediaPresentationDescription manifest, int adaptationSetIndex) { AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex); String drmInitMimeType = mimeTypeIsWebm(adaptationSet.representations.get(0).format.mimeType) ? MimeTypes.VIDEO_WEBM : MimeTypes.VIDEO_MP4; if (adaptationSet.contentProtections.isEmpty()) { return null; } else { DrmInitData.Mapped drmInitData = null; for (ContentProtection contentProtection : adaptationSet.contentProtections) { if (contentProtection.uuid != null && contentProtection.data != null) { if (drmInitData == null) { drmInitData = new DrmInitData.Mapped(drmInitMimeType); } drmInitData.put(contentProtection.uuid, contentProtection.data); } } return drmInitData; } }
private void testDashPlayback(HostActivity activity, String streamName, ActionSchedule actionSchedule, boolean fullPlaybackNoSeeking, String manifestFileName, String audioFormat, boolean canIncludeAdditionalVideoFormats, String... videoFormats) throws IOException { MediaPresentationDescription mpd = TestUtil.loadManifest(activity, TAG, MANIFEST_URL_PREFIX + manifestFileName, new MediaPresentationDescriptionParser()); MetricsLogger metricsLogger = MetricsLogger.Factory.createDefault(getInstrumentation(), TAG, REPORT_NAME, streamName); DashHostedTest test = new DashHostedTest(streamName, mpd, metricsLogger, fullPlaybackNoSeeking, audioFormat, canIncludeAdditionalVideoFormats, false, actionSchedule, videoFormats); activity.runTest(test, mpd.duration + MAX_ADDITIONAL_TIME_MS); // Retry test exactly once if adaptive test fails due to excessive dropped buffers when playing // non-CDD required formats (b/28220076). if (test.needsCddLimitedRetry) { metricsLogger = MetricsLogger.Factory.createDefault(getInstrumentation(), TAG, REPORT_NAME, streamName + "_cdd_limited_retry"); test = new DashHostedTest(streamName, mpd, metricsLogger, fullPlaybackNoSeeking, audioFormat, false, true, actionSchedule, videoFormats); activity.runTest(test, mpd.duration + MAX_ADDITIONAL_TIME_MS); } }
/** * @param streamName The name of the test stream for metric logging. * @param mpd The manifest. * @param metricsLogger Logger to log metrics from the test. * @param fullPlaybackNoSeeking True if the test will play the entire source with no seeking. * False otherwise. * @param audioFormat The audio format. * @param canIncludeAdditionalVideoFormats Whether to use video formats in addition to * those listed in the videoFormats argument, if the device is capable of playing them. * @param isCddLimitedRetry Whether this is a CDD limited retry following a previous failure. * @param videoFormats The video formats. */ public DashHostedTest(String streamName, MediaPresentationDescription mpd, MetricsLogger metricsLogger, boolean fullPlaybackNoSeeking, String audioFormat, boolean canIncludeAdditionalVideoFormats, boolean isCddLimitedRetry, ActionSchedule actionSchedule, String... videoFormats) { super(RENDERER_COUNT); Assertions.checkArgument(!(isCddLimitedRetry && canIncludeAdditionalVideoFormats)); this.streamName = streamName; this.mpd = Assertions.checkNotNull(mpd); this.metricsLogger = metricsLogger; this.fullPlaybackNoSeeking = fullPlaybackNoSeeking; this.audioFormats = new String[] {audioFormat}; this.canIncludeAdditionalVideoFormats = canIncludeAdditionalVideoFormats; this.videoFormats = videoFormats; if (actionSchedule != null) { setSchedule(actionSchedule); } }
@Override public void selectTracks(MediaPresentationDescription manifest, int periodIndex, Output output) throws IOException { Period period = manifest.getPeriod(periodIndex); int adaptationSetIndex = period.getAdaptationSetIndex(adaptationSetType); AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex); int[] representationIndices = getRepresentationIndices(adaptationSet, representationIds, canIncludeAdditionalVideoRepresentations); if (representationIndices.length > representationIds.length) { includedAdditionalVideoRepresentations = true; } if (adaptationSetType == AdaptationSet.TYPE_VIDEO) { output.adaptiveTrack(manifest, periodIndex, adaptationSetIndex, representationIndices); } for (int i = 0; i < representationIndices.length; i++) { output.fixedTrack(manifest, periodIndex, adaptationSetIndex, representationIndices[i]); } }
DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher, MediaPresentationDescription initialManifest, DashTrackSelector trackSelector, DataSource dataSource, FormatEvaluator adaptiveFormatEvaluator, Clock systemClock, long liveEdgeLatencyUs, long elapsedRealtimeOffsetUs, boolean startAtLiveEdge, Handler eventHandler, EventListener eventListener, int eventSourceId) { this.manifestFetcher = manifestFetcher; this.currentManifest = initialManifest; this.trackSelector = trackSelector; this.dataSource = dataSource; this.adaptiveFormatEvaluator = adaptiveFormatEvaluator; this.systemClock = systemClock; this.liveEdgeLatencyUs = liveEdgeLatencyUs; this.elapsedRealtimeOffsetUs = elapsedRealtimeOffsetUs; this.startAtLiveEdge = startAtLiveEdge; this.eventHandler = eventHandler; this.eventListener = eventListener; this.eventSourceId = eventSourceId; this.evaluation = new Evaluation(); this.availableRangeValues = new long[2]; periodHolders = new SparseArray<>(); tracks = new ArrayList<>(); live = initialManifest.dynamic; }
@Override public void fixedTrack(MediaPresentationDescription manifest, int periodIndex, int adaptationSetIndex, int representationIndex) { List<AdaptationSet> adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; AdaptationSet adaptationSet = adaptationSets.get(adaptationSetIndex); Format representationFormat = adaptationSet.representations.get(representationIndex).format; String mediaMimeType = getMediaMimeType(representationFormat); if (mediaMimeType == null) { Log.w(TAG, "Skipped track " + representationFormat.id + " (unknown media mime type)"); return; } MediaFormat trackFormat = getTrackFormat(adaptationSet.type, representationFormat, mediaMimeType, manifest.dynamic ? C.UNKNOWN_TIME_US : manifest.duration * 1000); if (trackFormat == null) { Log.w(TAG, "Skipped track " + representationFormat.id + " (unknown media format)"); return; } tracks.add(new ExposedTrack(trackFormat, adaptationSetIndex, representationFormat)); }
@Override public void onSingleManifest(MediaPresentationDescription manifest) { if (canceled) { return; } this.manifest = manifest; if (manifest.dynamic && manifest.utcTiming != null) { UtcTimingElementResolver.resolveTimingElement(manifestDataSource, manifest.utcTiming, manifestFetcher.getManifestLoadCompleteTimestamp(), this); } else { buildRenderers(); } }
private static MediaPresentationDescription buildManifest(List<Representation> representations) { Representation firstRepresentation = representations.get(0); AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations); Period period = new Period(null, firstRepresentation.periodStartMs, firstRepresentation.periodDurationMs, Collections.singletonList(adaptationSet)); long duration = firstRepresentation.periodDurationMs - firstRepresentation.periodStartMs; return new MediaPresentationDescription(-1, duration, -1, false, -1, -1, null, null, Collections.singletonList(period)); }
@Override public void selectTracks(MediaPresentationDescription manifest, int periodIndex, Output output) throws IOException { Period period = manifest.getPeriod(periodIndex); for (int i = 0; i < period.adaptationSets.size(); i++) { AdaptationSet adaptationSet = period.adaptationSets.get(i); if (adaptationSet.type == adaptationSetType) { if (adaptationSetType == AdaptationSet.TYPE_VIDEO) { int[] representations; if (filterVideoRepresentations) { representations = VideoFormatSelectorUtil.selectVideoFormatsForDefaultDisplay( context, adaptationSet.representations, null, filterProtectedHdContent && adaptationSet.hasContentProtection()); } else { representations = Util.firstIntegersArray(adaptationSet.representations.size()); } int representationCount = representations.length; if (representationCount > 1) { output.adaptiveTrack(manifest, periodIndex, i, representations); } for (int j = 0; j < representationCount; j++) { output.fixedTrack(manifest, periodIndex, i, representations[j]); } } else { for (int j = 0; j < adaptationSet.representations.size(); j++) { output.fixedTrack(manifest, periodIndex, i, j); } } } } }
@Override public void continueBuffering(long playbackPositionUs) { if (manifestFetcher == null || !currentManifest.dynamic || fatalError != null) { return; } MediaPresentationDescription newManifest = manifestFetcher.getManifest(); if (newManifest != null && newManifest != processedManifest) { processManifest(newManifest); // Manifests may be rejected, so the new manifest may not become the next currentManifest. // Track a manifest has been processed to avoid processing twice when it was discarded. processedManifest = newManifest; } // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where // minUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is explicit // signaling in the stream, according to: // http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/ long minUpdatePeriod = currentManifest.minUpdatePeriod; if (minUpdatePeriod == 0) { minUpdatePeriod = 5000; } if (android.os.SystemClock.elapsedRealtime() > manifestFetcher.getManifestLoadStartTimestamp() + minUpdatePeriod) { manifestFetcher.requestRefresh(); } }
@Override public void adaptiveTrack(MediaPresentationDescription manifest, int periodIndex, int adaptationSetIndex, int[] representationIndices) { if (adaptiveFormatEvaluator == null) { Log.w(TAG, "Skipping adaptive track (missing format evaluator)"); return; } AdaptationSet adaptationSet = manifest.getPeriod(periodIndex).adaptationSets.get( adaptationSetIndex); int maxWidth = 0; int maxHeight = 0; Format maxHeightRepresentationFormat = null; Format[] representationFormats = new Format[representationIndices.length]; for (int i = 0; i < representationFormats.length; i++) { Format format = adaptationSet.representations.get(representationIndices[i]).format; if (maxHeightRepresentationFormat == null || format.height > maxHeight) { maxHeightRepresentationFormat = format; } maxWidth = Math.max(maxWidth, format.width); maxHeight = Math.max(maxHeight, format.height); representationFormats[i] = format; } Arrays.sort(representationFormats, new DecreasingBandwidthComparator()); long trackDurationUs = live ? C.UNKNOWN_TIME_US : manifest.duration * 1000; String mediaMimeType = getMediaMimeType(maxHeightRepresentationFormat); if (mediaMimeType == null) { Log.w(TAG, "Skipped adaptive track (unknown media mime type)"); return; } MediaFormat trackFormat = getTrackFormat(adaptationSet.type, maxHeightRepresentationFormat, mediaMimeType, trackDurationUs); if (trackFormat == null) { Log.w(TAG, "Skipped adaptive track (unknown media format)"); return; } tracks.add(new ExposedTrack(trackFormat.copyAsAdaptive(null), adaptationSetIndex, representationFormats, maxWidth, maxHeight)); }
private static MediaPresentationDescription buildManifest(long durationMs, int adaptationSetType, List<Representation> representations) { AdaptationSet adaptationSet = new AdaptationSet(0, adaptationSetType, representations); Period period = new Period(null, 0, Collections.singletonList(adaptationSet)); return new MediaPresentationDescription(-1, durationMs, -1, false, -1, -1, null, null, Collections.singletonList(period)); }
public PeriodHolder(int localIndex, MediaPresentationDescription manifest, int manifestIndex, ExposedTrack selectedTrack) { this.localIndex = localIndex; Period period = manifest.getPeriod(manifestIndex); long periodDurationUs = getPeriodDurationUs(manifest, manifestIndex); AdaptationSet adaptationSet = period.adaptationSets.get(selectedTrack.adaptationSetIndex); List<Representation> representations = adaptationSet.representations; startTimeUs = period.startMs * 1000; drmInitData = getDrmInitData(adaptationSet); if (!selectedTrack.isAdaptive()) { representationIndices = new int[] { getRepresentationIndex(representations, selectedTrack.fixedFormat.id)}; } else { representationIndices = new int[selectedTrack.adaptiveFormats.length]; for (int j = 0; j < selectedTrack.adaptiveFormats.length; j++) { representationIndices[j] = getRepresentationIndex( representations, selectedTrack.adaptiveFormats[j].id); } } representationHolders = new HashMap<>(); for (int i = 0; i < representationIndices.length; i++) { Representation representation = representations.get(representationIndices[i]); RepresentationHolder representationHolder = new RepresentationHolder(startTimeUs, periodDurationUs, representation); representationHolders.put(representation.format.id, representationHolder); } updateRepresentationIndependentProperties(periodDurationUs, representations.get(representationIndices[0])); }
public void updatePeriod(MediaPresentationDescription manifest, int manifestIndex, ExposedTrack selectedTrack) throws BehindLiveWindowException { Period period = manifest.getPeriod(manifestIndex); long periodDurationUs = getPeriodDurationUs(manifest, manifestIndex); List<Representation> representations = period.adaptationSets .get(selectedTrack.adaptationSetIndex).representations; for (int j = 0; j < representationIndices.length; j++) { Representation representation = representations.get(representationIndices[j]); representationHolders.get(representation.format.id).updateRepresentation(periodDurationUs, representation); } updateRepresentationIndependentProperties(periodDurationUs, representations.get(representationIndices[0])); }
private static long getPeriodDurationUs(MediaPresentationDescription manifest, int index) { long durationMs = manifest.getPeriodDuration(index); if (durationMs == -1) { return C.UNKNOWN_TIME_US; } else { return durationMs * 1000; } }
private static MediaPresentationDescription buildMpd(long durationMs, List<Representation> representations, boolean live, boolean limitTimeshiftBuffer) { AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_VIDEO, representations); Period period = new Period(null, 0, Collections.singletonList(adaptationSet)); return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, durationMs, -1, live, -1, (limitTimeshiftBuffer) ? LIVE_TIMESHIFT_BUFFER_DEPTH_MS : -1, null, null, Collections.singletonList(period)); }
private static MediaPresentationDescription buildMultiPeriodVodMpd() { List<Period> periods = new ArrayList<>(); long timeMs = 0; long periodDurationMs = VOD_DURATION_MS; for (int i = 0; i < 2; i++) { Representation representation = buildVodRepresentation(REGULAR_VIDEO); AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_VIDEO, Collections.singletonList(representation)); Period period = new Period(null, timeMs, Collections.singletonList(adaptationSet)); periods.add(period); timeMs += periodDurationMs; } return buildMultiPeriodMpd(timeMs, periods, false, false); }
private static MediaPresentationDescription buildMultiPeriodLiveMpdWithTimeline() { List<Period> periods = new ArrayList<>(); long periodStartTimeMs = 0; long periodDurationMs = LIVE_DURATION_MS; for (int i = 0; i < MULTI_PERIOD_COUNT; i++) { Representation representation = buildSegmentTimelineRepresentation(LIVE_DURATION_MS, 0); AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_VIDEO, Collections.singletonList(representation)); Period period = new Period(null, periodStartTimeMs, Collections.singletonList(adaptationSet)); periods.add(period); periodStartTimeMs += periodDurationMs; } return buildMultiPeriodMpd(periodDurationMs, periods, true, false); }
private static MediaPresentationDescription buildMultiPeriodLiveMpdWithTemplate() { List<Period> periods = new ArrayList<>(); long periodStartTimeMs = 0; long periodDurationMs = LIVE_DURATION_MS; for (int i = 0; i < MULTI_PERIOD_COUNT; i++) { Representation representation = buildSegmentTemplateRepresentation(); AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_VIDEO, Collections.singletonList(representation)); Period period = new Period(null, periodStartTimeMs, Collections.singletonList(adaptationSet)); periods.add(period); periodStartTimeMs += periodDurationMs; } return buildMultiPeriodMpd(MULTI_PERIOD_LIVE_DURATION_MS, periods, true, false); }
private static DashChunkSource buildDashChunkSource(MediaPresentationDescription mpd, boolean startAtLiveEdge, long liveEdgeLatencyMs) { @SuppressWarnings("unchecked") ManifestFetcher<MediaPresentationDescription> manifestFetcher = mock(ManifestFetcher.class); when(manifestFetcher.getManifest()).thenReturn(mpd); DashChunkSource chunkSource = new DashChunkSource(manifestFetcher, mpd, DefaultDashTrackSelector.newVideoInstance(null, false, false), mock(DataSource.class), null, new FakeClock(mpd.availabilityStartTime + mpd.duration - ELAPSED_REALTIME_OFFSET_MS), liveEdgeLatencyMs * 1000, ELAPSED_REALTIME_OFFSET_MS * 1000, startAtLiveEdge, null, null, 0); chunkSource.prepare(); chunkSource.enable(0); return chunkSource; }
private static void checkLiveEdgeConsistencyWithTimeline(long durationMs, long timelineStartMs, long liveEdgeLatencyMs, long seekPositionMs, long availableRangeStartMs, long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) { MediaPresentationDescription mpd = buildLiveMpdWithTimeline(durationMs, timelineStartMs); checkLiveEdgeConsistency(mpd, liveEdgeLatencyMs, seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); }
private static void checkLiveEdgeConsistencyWithTemplateAndUnlimitedTimeshift(long durationMs, long liveEdgeLatencyMs, long availablePositionMs, long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) { MediaPresentationDescription mpd = buildLiveMpdWithTemplate(durationMs, false); checkLiveEdgeConsistency(mpd, liveEdgeLatencyMs, availablePositionMs, 0, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); }
private static void checkLiveEdgeConsistencyWithTemplateAndLimitedTimeshift(long durationMs, long liveEdgeLatencyMs, long seekPositionMs, long availableRangeStartMs, long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) { MediaPresentationDescription mpd = buildLiveMpdWithTemplate(durationMs, true); checkLiveEdgeConsistency(mpd, liveEdgeLatencyMs, seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); }
private static void checkLiveEdgeConsistency(MediaPresentationDescription mpd, long liveEdgeLatencyMs, long seekPositionMs, long availableRangeStartMs, long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) { DashChunkSource chunkSource = buildDashChunkSource(mpd, true, liveEdgeLatencyMs); List<MediaChunk> queue = new ArrayList<>(); ChunkOperationHolder out = new ChunkOperationHolder(); checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); }
@Override public void onSingleManifest(MediaPresentationDescription manifest) { if (canceled) { return; } this.currentManifest = manifest; if (manifest.dynamic && manifest.utcTiming != null) { UtcTimingElementResolver.resolveTimingElement(manifestDataSource, manifest.utcTiming, manifestFetcher.getManifestLoadCompleteTimestamp(), this); } else { buildRenderers(); } }
@Override public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { this.player = player; this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); manifestFetcher = new ManifestFetcher<MediaPresentationDescription>(parser, contentId, url, userAgent); manifestFetcher.singleLoad(player.getMainHandler().getLooper(), this); }
@Override public void buildRenderers(RendererBuilderCallback callback) { this.callback = callback; MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); manifestFetcher = new ManifestFetcher<MediaPresentationDescription>(parser, contentId, url, userAgent); manifestFetcher.singleLoad(playerActivity.getMainLooper(), this); }