Merge pull request #176 from google/dev

dev -> dev-hls
This commit is contained in:
ojw28 2014-11-27 18:18:02 +00:00
commit b9f3253924
5 changed files with 267 additions and 40 deletions

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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);
}
} }
/** /**

View File

@ -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 {

View File

@ -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;
}
} }