From 1a557a06c1a25989d9fc644c64762c4561a9a218 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 27 Nov 2014 18:11:43 +0000 Subject: [PATCH 1/4] Support SmoothStreaming repeated chunk tags. --- .../SmoothStreamingManifestParser.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java index 1114c1e4d0..20aea8ad32 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java @@ -450,6 +450,7 @@ public class SmoothStreamingManifestParser implements ManifestParser tracks; @@ -504,9 +505,18 @@ public class SmoothStreamingManifestParser implements ManifestParser 1 && lastChunkDuration == -1L) { + throw new ParserException("Repeated chunk with unspecified duration"); + } + for (int i = 1; i < repeatCount; i++) { + chunkIndex++; + startTimes.add(startTime + (lastChunkDuration * i)); + } } private void parseStreamElementStartTag(XmlPullParser parser) throws ParserException { From c5342630328fcda01eeec01784d63d43b0c0d352 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 27 Nov 2014 18:12:46 +0000 Subject: [PATCH 2/4] Enhance parsing of xs:duration to support year/month/day. --- .../google/android/exoplayer/util/Util.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index 78c2d267d6..4c08c5a528 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -55,7 +55,8 @@ public final class Util { + "([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?"); 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() {} @@ -274,11 +275,19 @@ public final class Util { public static long parseXsDuration(String value) { Matcher matcher = XS_DURATION_PATTERN.matcher(value); if (matcher.matches()) { - String hours = matcher.group(2); - double durationSeconds = (hours != null) ? Double.parseDouble(hours) * 3600 : 0; - String minutes = matcher.group(4); + // Durations containing years and months aren't completely defined. We assume there are + // 30.4368 days in a month, and 365.242 days in a year. + 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; - String seconds = matcher.group(6); + String seconds = matcher.group(13); durationSeconds += (seconds != null) ? Double.parseDouble(seconds) : 0; return (long) (durationSeconds * 1000); } else { From 2969bba60fd502bbac2d11cdab399fdd3b438ea3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 27 Nov 2014 18:14:19 +0000 Subject: [PATCH 3/4] Fix timestamp rollover issue for DASH live. The timestamp scaling in SegmentBase.getSegmentTimeUs was overflowing for some streams. Apply a similar trick to that applied in the SmoothStreaming case to fix it. --- .../exoplayer/dash/mpd/SegmentBase.java | 4 +- .../SmoothStreamingManifest.java | 39 +++----------- .../google/android/exoplayer/util/Util.java | 53 +++++++++++++++++++ 3 files changed, 62 insertions(+), 34 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java index 89a9dd49be..df92d029bc 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer.dash.mpd; +import com.google.android.exoplayer.util.Util; + import android.net.Uri; import java.util.List; @@ -155,7 +157,7 @@ public abstract class SegmentBase { } else { unscaledSegmentTime = (sequenceNumber - startNumber) * duration; } - return (unscaledSegmentTime * 1000000) / timescale; + return Util.scaleLargeTimestamp(unscaledSegmentTime, 1000000, timescale); } public abstract RangedUri getSegmentUrl(Representation representation, int index); diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java index a26ca6a48e..7b45aed9cc 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java @@ -53,19 +53,8 @@ public class SmoothStreamingManifest { this.isLive = isLive; this.protectionElement = protectionElement; this.streamElements = streamElements; - if (timescale >= MICROS_PER_SECOND && (timescale % MICROS_PER_SECOND) == 0) { - long divisionFactor = timescale / MICROS_PER_SECOND; - 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); - } + dvrWindowLengthUs = Util.scaleLargeTimestamp(dvrWindowLength, MICROS_PER_SECOND, timescale); + durationUs = Util.scaleLargeTimestamp(duration, MICROS_PER_SECOND, timescale); } /** @@ -186,26 +175,10 @@ public class SmoothStreamingManifest { this.tracks = tracks; this.chunkCount = chunkStartTimes.size(); this.chunkStartTimes = chunkStartTimes; - chunkStartTimesUs = new long[chunkStartTimes.size()]; - if (timescale >= MICROS_PER_SECOND && (timescale % MICROS_PER_SECOND) == 0) { - long divisionFactor = timescale / MICROS_PER_SECOND; - for (int i = 0; i < chunkStartTimesUs.length; i++) { - 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); - } + lastChunkDurationUs = + Util.scaleLargeTimestamp(lastChunkDuration, MICROS_PER_SECOND, timescale); + chunkStartTimesUs = + Util.scaleLargeTimestamps(chunkStartTimes, MICROS_PER_SECOND, timescale); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index 4c08c5a528..b8cd40215d 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -346,4 +346,57 @@ public final class Util { return time; } + /** + * Scales a large timestamp. + *

+ * 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 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; + } + } From 165562d8808cc46578d1732f76112c732c53662e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 27 Nov 2014 18:15:16 +0000 Subject: [PATCH 4/4] Add VSYNC aligning smooth frame release helper. --- .../SmoothFrameReleaseTimeHelper.java | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 library/src/main/java/com/google/android/exoplayer/SmoothFrameReleaseTimeHelper.java diff --git a/library/src/main/java/com/google/android/exoplayer/SmoothFrameReleaseTimeHelper.java b/library/src/main/java/com/google/android/exoplayer/SmoothFrameReleaseTimeHelper.java new file mode 100644 index 0000000000..7248f1cdeb --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/SmoothFrameReleaseTimeHelper.java @@ -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; + } + +}