mirror of
https://github.com/androidx/media.git
synced 2025-05-15 19:49:50 +08:00
commit
b9f3253924
@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer.FrameReleaseTimeHelper;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.view.Choreographer;
|
||||||
|
import android.view.Choreographer.FrameCallback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a best effort to adjust frame release timestamps for a smoother visual result.
|
||||||
|
*/
|
||||||
|
@TargetApi(16)
|
||||||
|
public class SmoothFrameReleaseTimeHelper implements FrameReleaseTimeHelper, FrameCallback {
|
||||||
|
|
||||||
|
private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500;
|
||||||
|
private static final long MAX_ALLOWED_DRIFT_NS = 20000000;
|
||||||
|
|
||||||
|
private static final long VSYNC_OFFSET_PERCENTAGE = 80;
|
||||||
|
private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6;
|
||||||
|
|
||||||
|
private final boolean usePrimaryDisplayVsync;
|
||||||
|
private final long vsyncDurationNs;
|
||||||
|
private final long vsyncOffsetNs;
|
||||||
|
|
||||||
|
private Choreographer choreographer;
|
||||||
|
private long sampledVsyncTimeNs;
|
||||||
|
|
||||||
|
private long lastUnadjustedFrameTimeUs;
|
||||||
|
private long adjustedLastFrameTimeNs;
|
||||||
|
private long pendingAdjustedFrameTimeNs;
|
||||||
|
|
||||||
|
private boolean haveSync;
|
||||||
|
private long syncReleaseTimeNs;
|
||||||
|
private long syncFrameTimeNs;
|
||||||
|
private int frameCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param primaryDisplayRefreshRate The refresh rate of the default display.
|
||||||
|
* @param usePrimaryDisplayVsync Whether to snap to the primary display vsync. May not be
|
||||||
|
* suitable when rendering to secondary displays.
|
||||||
|
*/
|
||||||
|
public SmoothFrameReleaseTimeHelper(
|
||||||
|
float primaryDisplayRefreshRate, boolean usePrimaryDisplayVsync) {
|
||||||
|
this.usePrimaryDisplayVsync = usePrimaryDisplayVsync;
|
||||||
|
if (usePrimaryDisplayVsync) {
|
||||||
|
vsyncDurationNs = (long) (1000000000d / primaryDisplayRefreshRate);
|
||||||
|
vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100;
|
||||||
|
} else {
|
||||||
|
vsyncDurationNs = -1;
|
||||||
|
vsyncOffsetNs = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void enable() {
|
||||||
|
haveSync = false;
|
||||||
|
if (usePrimaryDisplayVsync) {
|
||||||
|
sampledVsyncTimeNs = 0;
|
||||||
|
choreographer = Choreographer.getInstance();
|
||||||
|
choreographer.postFrameCallback(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disable() {
|
||||||
|
if (usePrimaryDisplayVsync) {
|
||||||
|
choreographer.removeFrameCallback(this);
|
||||||
|
choreographer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFrame(long vsyncTimeNs) {
|
||||||
|
sampledVsyncTimeNs = vsyncTimeNs;
|
||||||
|
choreographer.postFrameCallbackDelayed(this, CHOREOGRAPHER_SAMPLE_DELAY_MILLIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long adjustReleaseTime(long unadjustedFrameTimeUs, long unadjustedReleaseTimeNs) {
|
||||||
|
long unadjustedFrameTimeNs = unadjustedFrameTimeUs * 1000;
|
||||||
|
|
||||||
|
// Until we know better, the adjustment will be a no-op.
|
||||||
|
long adjustedFrameTimeNs = unadjustedFrameTimeNs;
|
||||||
|
long adjustedReleaseTimeNs = unadjustedReleaseTimeNs;
|
||||||
|
|
||||||
|
if (haveSync) {
|
||||||
|
// See if we've advanced to the next frame.
|
||||||
|
if (unadjustedFrameTimeUs != lastUnadjustedFrameTimeUs) {
|
||||||
|
frameCount++;
|
||||||
|
adjustedLastFrameTimeNs = pendingAdjustedFrameTimeNs;
|
||||||
|
}
|
||||||
|
if (frameCount >= MIN_FRAMES_FOR_ADJUSTMENT) {
|
||||||
|
// We're synced and have waited the required number of frames to apply an adjustment.
|
||||||
|
// Calculate the average frame time across all the frames we've seen since the last sync.
|
||||||
|
// This will typically give us a framerate at a finer granularity than the frame times
|
||||||
|
// themselves (which often only have millisecond granularity).
|
||||||
|
long averageFrameTimeNs = (unadjustedFrameTimeNs - syncFrameTimeNs) / frameCount;
|
||||||
|
// Project the adjusted frame time forward using the average.
|
||||||
|
long candidateAdjustedFrameTimeNs = adjustedLastFrameTimeNs + averageFrameTimeNs;
|
||||||
|
|
||||||
|
if (isDriftTooLarge(candidateAdjustedFrameTimeNs, unadjustedReleaseTimeNs)) {
|
||||||
|
haveSync = false;
|
||||||
|
} else {
|
||||||
|
adjustedFrameTimeNs = candidateAdjustedFrameTimeNs;
|
||||||
|
adjustedReleaseTimeNs = syncReleaseTimeNs + adjustedFrameTimeNs - syncFrameTimeNs;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We're synced but haven't waited the required number of frames to apply an adjustment.
|
||||||
|
// Check drift anyway.
|
||||||
|
if (isDriftTooLarge(unadjustedFrameTimeNs, unadjustedReleaseTimeNs)) {
|
||||||
|
haveSync = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we need to sync, do so now.
|
||||||
|
if (!haveSync) {
|
||||||
|
syncFrameTimeNs = unadjustedFrameTimeNs;
|
||||||
|
syncReleaseTimeNs = unadjustedReleaseTimeNs;
|
||||||
|
frameCount = 0;
|
||||||
|
haveSync = true;
|
||||||
|
onSynced();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUnadjustedFrameTimeUs = unadjustedFrameTimeUs;
|
||||||
|
pendingAdjustedFrameTimeNs = adjustedFrameTimeNs;
|
||||||
|
|
||||||
|
if (sampledVsyncTimeNs == 0) {
|
||||||
|
return adjustedReleaseTimeNs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the timestamp of the closest vsync. This is the vsync that we're targeting.
|
||||||
|
long snappedTimeNs = closestVsync(adjustedReleaseTimeNs, sampledVsyncTimeNs, vsyncDurationNs);
|
||||||
|
// Apply an offset so that we release before the target vsync, but after the previous one.
|
||||||
|
return snappedTimeNs - vsyncOffsetNs;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onSynced() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDriftTooLarge(long frameTimeNs, long releaseTimeNs) {
|
||||||
|
long elapsedFrameTimeNs = frameTimeNs - syncFrameTimeNs;
|
||||||
|
long elapsedReleaseTimeNs = releaseTimeNs - syncReleaseTimeNs;
|
||||||
|
return Math.abs(elapsedReleaseTimeNs - elapsedFrameTimeNs) > MAX_ALLOWED_DRIFT_NS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long closestVsync(long releaseTime, long sampledVsyncTime, long vsyncDuration) {
|
||||||
|
long vsyncCount = (releaseTime - sampledVsyncTime) / vsyncDuration;
|
||||||
|
long snappedTimeNs = sampledVsyncTime + (vsyncDuration * vsyncCount);
|
||||||
|
long snappedBeforeNs;
|
||||||
|
long snappedAfterNs;
|
||||||
|
if (releaseTime <= snappedTimeNs) {
|
||||||
|
snappedBeforeNs = snappedTimeNs - vsyncDuration;
|
||||||
|
snappedAfterNs = snappedTimeNs;
|
||||||
|
} else {
|
||||||
|
snappedBeforeNs = snappedTimeNs;
|
||||||
|
snappedAfterNs = snappedTimeNs + vsyncDuration;
|
||||||
|
}
|
||||||
|
long snappedAfterDiff = snappedAfterNs - releaseTime;
|
||||||
|
long snappedBeforeDiff = releaseTime - snappedBeforeNs;
|
||||||
|
return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.exoplayer.dash.mpd;
|
package com.google.android.exoplayer.dash.mpd;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer.util.Util;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -155,7 +157,7 @@ public abstract class SegmentBase {
|
|||||||
} else {
|
} else {
|
||||||
unscaledSegmentTime = (sequenceNumber - startNumber) * duration;
|
unscaledSegmentTime = (sequenceNumber - startNumber) * duration;
|
||||||
}
|
}
|
||||||
return (unscaledSegmentTime * 1000000) / timescale;
|
return Util.scaleLargeTimestamp(unscaledSegmentTime, 1000000, timescale);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract RangedUri getSegmentUrl(Representation representation, int index);
|
public abstract RangedUri getSegmentUrl(Representation representation, int index);
|
||||||
|
@ -53,19 +53,8 @@ public class SmoothStreamingManifest {
|
|||||||
this.isLive = isLive;
|
this.isLive = isLive;
|
||||||
this.protectionElement = protectionElement;
|
this.protectionElement = protectionElement;
|
||||||
this.streamElements = streamElements;
|
this.streamElements = streamElements;
|
||||||
if (timescale >= MICROS_PER_SECOND && (timescale % MICROS_PER_SECOND) == 0) {
|
dvrWindowLengthUs = Util.scaleLargeTimestamp(dvrWindowLength, MICROS_PER_SECOND, timescale);
|
||||||
long divisionFactor = timescale / MICROS_PER_SECOND;
|
durationUs = Util.scaleLargeTimestamp(duration, MICROS_PER_SECOND, timescale);
|
||||||
dvrWindowLengthUs = dvrWindowLength / divisionFactor;
|
|
||||||
durationUs = duration / divisionFactor;
|
|
||||||
} else if (timescale < MICROS_PER_SECOND && (MICROS_PER_SECOND % timescale) == 0) {
|
|
||||||
long multiplicationFactor = MICROS_PER_SECOND / timescale;
|
|
||||||
dvrWindowLengthUs = dvrWindowLength * multiplicationFactor;
|
|
||||||
durationUs = duration * multiplicationFactor;
|
|
||||||
} else {
|
|
||||||
double multiplicationFactor = (double) MICROS_PER_SECOND / timescale;
|
|
||||||
dvrWindowLengthUs = (long) (dvrWindowLength * multiplicationFactor);
|
|
||||||
durationUs = (long) (duration * multiplicationFactor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -186,26 +175,10 @@ public class SmoothStreamingManifest {
|
|||||||
this.tracks = tracks;
|
this.tracks = tracks;
|
||||||
this.chunkCount = chunkStartTimes.size();
|
this.chunkCount = chunkStartTimes.size();
|
||||||
this.chunkStartTimes = chunkStartTimes;
|
this.chunkStartTimes = chunkStartTimes;
|
||||||
chunkStartTimesUs = new long[chunkStartTimes.size()];
|
lastChunkDurationUs =
|
||||||
if (timescale >= MICROS_PER_SECOND && (timescale % MICROS_PER_SECOND) == 0) {
|
Util.scaleLargeTimestamp(lastChunkDuration, MICROS_PER_SECOND, timescale);
|
||||||
long divisionFactor = timescale / MICROS_PER_SECOND;
|
chunkStartTimesUs =
|
||||||
for (int i = 0; i < chunkStartTimesUs.length; i++) {
|
Util.scaleLargeTimestamps(chunkStartTimes, MICROS_PER_SECOND, timescale);
|
||||||
chunkStartTimesUs[i] = chunkStartTimes.get(i) / divisionFactor;
|
|
||||||
}
|
|
||||||
lastChunkDurationUs = lastChunkDuration / divisionFactor;
|
|
||||||
} else if (timescale < MICROS_PER_SECOND && (MICROS_PER_SECOND % timescale) == 0) {
|
|
||||||
long multiplicationFactor = MICROS_PER_SECOND / timescale;
|
|
||||||
for (int i = 0; i < chunkStartTimesUs.length; i++) {
|
|
||||||
chunkStartTimesUs[i] = chunkStartTimes.get(i) * multiplicationFactor;
|
|
||||||
}
|
|
||||||
lastChunkDurationUs = lastChunkDuration * multiplicationFactor;
|
|
||||||
} else {
|
|
||||||
double multiplicationFactor = (double) MICROS_PER_SECOND / timescale;
|
|
||||||
for (int i = 0; i < chunkStartTimesUs.length; i++) {
|
|
||||||
chunkStartTimesUs[i] = (long) (chunkStartTimes.get(i) * multiplicationFactor);
|
|
||||||
}
|
|
||||||
lastChunkDurationUs = (long) (lastChunkDuration * multiplicationFactor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -450,6 +450,7 @@ public class SmoothStreamingManifestParser implements ManifestParser<SmoothStrea
|
|||||||
|
|
||||||
private static final String KEY_FRAGMENT_DURATION = "d";
|
private static final String KEY_FRAGMENT_DURATION = "d";
|
||||||
private static final String KEY_FRAGMENT_START_TIME = "t";
|
private static final String KEY_FRAGMENT_START_TIME = "t";
|
||||||
|
private static final String KEY_FRAGMENT_REPEAT_COUNT = "r";
|
||||||
|
|
||||||
private final Uri baseUri;
|
private final Uri baseUri;
|
||||||
private final List<TrackElement> tracks;
|
private final List<TrackElement> tracks;
|
||||||
@ -504,9 +505,18 @@ public class SmoothStreamingManifestParser implements ManifestParser<SmoothStrea
|
|||||||
throw new ParserException("Unable to infer start time");
|
throw new ParserException("Unable to infer start time");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
chunkIndex++;
|
||||||
startTimes.add(startTime);
|
startTimes.add(startTime);
|
||||||
lastChunkDuration = parseLong(parser, KEY_FRAGMENT_DURATION, -1L);
|
lastChunkDuration = parseLong(parser, KEY_FRAGMENT_DURATION, -1L);
|
||||||
|
// Handle repeated chunks.
|
||||||
|
long repeatCount = parseLong(parser, KEY_FRAGMENT_REPEAT_COUNT, 1L);
|
||||||
|
if (repeatCount > 1 && lastChunkDuration == -1L) {
|
||||||
|
throw new ParserException("Repeated chunk with unspecified duration");
|
||||||
|
}
|
||||||
|
for (int i = 1; i < repeatCount; i++) {
|
||||||
chunkIndex++;
|
chunkIndex++;
|
||||||
|
startTimes.add(startTime + (lastChunkDuration * i));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException {
|
private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException {
|
||||||
|
@ -55,7 +55,8 @@ public final class Util {
|
|||||||
+ "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?");
|
+ "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?");
|
||||||
|
|
||||||
private static final Pattern XS_DURATION_PATTERN =
|
private static final Pattern XS_DURATION_PATTERN =
|
||||||
Pattern.compile("^PT(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?$");
|
Pattern.compile("^P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?"
|
||||||
|
+ "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$");
|
||||||
|
|
||||||
private Util() {}
|
private Util() {}
|
||||||
|
|
||||||
@ -274,11 +275,19 @@ public final class Util {
|
|||||||
public static long parseXsDuration(String value) {
|
public static long parseXsDuration(String value) {
|
||||||
Matcher matcher = XS_DURATION_PATTERN.matcher(value);
|
Matcher matcher = XS_DURATION_PATTERN.matcher(value);
|
||||||
if (matcher.matches()) {
|
if (matcher.matches()) {
|
||||||
String hours = matcher.group(2);
|
// Durations containing years and months aren't completely defined. We assume there are
|
||||||
double durationSeconds = (hours != null) ? Double.parseDouble(hours) * 3600 : 0;
|
// 30.4368 days in a month, and 365.242 days in a year.
|
||||||
String minutes = matcher.group(4);
|
String years = matcher.group(2);
|
||||||
|
double durationSeconds = (years != null) ? Double.parseDouble(years) * 31556908 : 0;
|
||||||
|
String months = matcher.group(4);
|
||||||
|
durationSeconds += (months != null) ? Double.parseDouble(months) * 2629739 : 0;
|
||||||
|
String days = matcher.group(6);
|
||||||
|
durationSeconds += (days != null) ? Double.parseDouble(days) * 86400 : 0;
|
||||||
|
String hours = matcher.group(9);
|
||||||
|
durationSeconds += (hours != null) ? Double.parseDouble(hours) * 3600 : 0;
|
||||||
|
String minutes = matcher.group(11);
|
||||||
durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0;
|
durationSeconds += (minutes != null) ? Double.parseDouble(minutes) * 60 : 0;
|
||||||
String seconds = matcher.group(6);
|
String seconds = matcher.group(13);
|
||||||
durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0;
|
durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0;
|
||||||
return (long) (durationSeconds * 1000);
|
return (long) (durationSeconds * 1000);
|
||||||
} else {
|
} else {
|
||||||
@ -337,4 +346,57 @@ public final class Util {
|
|||||||
return time;
|
return time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scales a large timestamp.
|
||||||
|
* <p>
|
||||||
|
* Logically, scaling consists of a multiplication followed by a division. The actual operations
|
||||||
|
* performed are designed to minimize the probability of overflow.
|
||||||
|
*
|
||||||
|
* @param timestamp The timestamp to scale.
|
||||||
|
* @param multiplier The multiplier.
|
||||||
|
* @param divisor The divisor.
|
||||||
|
* @return The scaled timestamp.
|
||||||
|
*/
|
||||||
|
public static long scaleLargeTimestamp(long timestamp, long multiplier, long divisor) {
|
||||||
|
if (divisor >= multiplier && (divisor % multiplier) == 0) {
|
||||||
|
long divisionFactor = divisor / multiplier;
|
||||||
|
return timestamp / divisionFactor;
|
||||||
|
} else if (divisor < multiplier && (multiplier % divisor) == 0) {
|
||||||
|
long multiplicationFactor = multiplier / divisor;
|
||||||
|
return timestamp * multiplicationFactor;
|
||||||
|
} else {
|
||||||
|
double multiplicationFactor = (double) multiplier / divisor;
|
||||||
|
return (long) (timestamp * multiplicationFactor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies {@link #scaleLargeTimestamp(long, long, long)} to a list of unscaled timestamps.
|
||||||
|
*
|
||||||
|
* @param timestamps The timestamps to scale.
|
||||||
|
* @param multiplier The multiplier.
|
||||||
|
* @param divisor The divisor.
|
||||||
|
* @return The scaled timestamps.
|
||||||
|
*/
|
||||||
|
public static long[] scaleLargeTimestamps(List<Long> timestamps, long multiplier, long divisor) {
|
||||||
|
long[] scaledTimestamps = new long[timestamps.size()];
|
||||||
|
if (divisor >= multiplier && (divisor % multiplier) == 0) {
|
||||||
|
long divisionFactor = divisor / multiplier;
|
||||||
|
for (int i = 0; i < scaledTimestamps.length; i++) {
|
||||||
|
scaledTimestamps[i] = timestamps.get(i) / divisionFactor;
|
||||||
|
}
|
||||||
|
} else if (divisor < multiplier && (multiplier % divisor) == 0) {
|
||||||
|
long multiplicationFactor = multiplier / divisor;
|
||||||
|
for (int i = 0; i < scaledTimestamps.length; i++) {
|
||||||
|
scaledTimestamps[i] = timestamps.get(i) * multiplicationFactor;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
double multiplicationFactor = (double) multiplier / divisor;
|
||||||
|
for (int i = 0; i < scaledTimestamps.length; i++) {
|
||||||
|
scaledTimestamps[i] = (long) (timestamps.get(i) * multiplicationFactor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return scaledTimestamps;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user