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