Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
SDK
/
exoplayer
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
d0165438
authored
Dec 09, 2020
by
olly
Committed by
Ian Baker
Dec 14, 2020
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
VideoFrameReleaseTimeHelper: Split out frame-rate estimation
PiperOrigin-RevId: 346554044
parent
f18d81f8
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
545 additions
and
50 deletions
library/core/src/main/java/com/google/android/exoplayer2/video/FixedFrameRateEstimator.java
library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java
library/core/src/test/java/com/google/android/exoplayer2/video/FixedFrameRateEstimatorTest.java
library/core/src/main/java/com/google/android/exoplayer2/video/FixedFrameRateEstimator.java
0 → 100644
View file @
d0165438
/*
* Copyright (C) 2020 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
.
exoplayer2
.
video
;
import
androidx.annotation.VisibleForTesting
;
import
com.google.android.exoplayer2.C
;
import
com.google.android.exoplayer2.Format
;
import
java.util.Arrays
;
/**
* Attempts to detect and refine a fixed frame rate estimate based on frame presentation timestamps.
*/
/* package */
final
class
FixedFrameRateEstimator
{
/**
* The number of consecutive matching frame durations for the tracker to be considered in sync.
*/
@VisibleForTesting
static
final
int
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
=
10
;
/**
* The maximum amount frame durations can differ for them to be considered matching, in
* nanoseconds.
*
* <p>This constant is set to 1ms to account for container formats that only represent frame
* presentation timestamps to the nearest millisecond. In such cases, frame durations need to
* switch between values that are 1ms apart to achieve common fixed frame rates (e.g., 30fps
* content will need frames that are 33ms and 34ms).
*/
@VisibleForTesting
static
final
long
MAX_MATCHING_FRAME_DIFFERENCE_NS
=
1_000_000
;
private
Matcher
currentMatcher
;
private
Matcher
candidateMatcher
;
private
boolean
candidateMatcherActive
;
private
boolean
switchToCandidateMatcherWhenSynced
;
private
float
formatFrameRate
;
private
long
lastFramePresentationTimeNs
;
public
FixedFrameRateEstimator
()
{
currentMatcher
=
new
Matcher
();
candidateMatcher
=
new
Matcher
();
formatFrameRate
=
Format
.
NO_VALUE
;
lastFramePresentationTimeNs
=
C
.
TIME_UNSET
;
}
/** Resets the estimator. */
public
void
reset
()
{
currentMatcher
.
reset
();
candidateMatcher
.
reset
();
candidateMatcherActive
=
false
;
formatFrameRate
=
Format
.
NO_VALUE
;
lastFramePresentationTimeNs
=
C
.
TIME_UNSET
;
}
/**
* Called when the renderer's output format changes.
*
* @param formatFrameRate The format's frame rate, or {@link Format#NO_VALUE} if unknown.
*/
public
void
onFormatChanged
(
float
formatFrameRate
)
{
// The format frame rate is only used to determine to what extent the estimator should be reset.
// Frame rate estimates are always calculated directly from frame presentation timestamps.
if
(
this
.
formatFrameRate
!=
formatFrameRate
)
{
reset
();
}
else
{
// Keep the current matcher, but prefer to switch to a new matcher once synced even if the
// current one does not lose sync. This avoids an issue where the current matcher would
// continue to be used if a frame rate change has occurred that's too small to trigger sync
// loss (e.g., a change from 30fps to 29.97fps) and which is not represented in the format
// frame rates (e.g., because they're unset or only have integer precision).
switchToCandidateMatcherWhenSynced
=
true
;
}
this
.
formatFrameRate
=
formatFrameRate
;
}
/**
* Called with each frame presentation timestamp.
*
* @param framePresentationTimeNs The frame presentation timestamp, in nanoseconds.
*/
public
void
onNextFrame
(
long
framePresentationTimeNs
)
{
currentMatcher
.
onNextFrame
(
framePresentationTimeNs
);
if
(
currentMatcher
.
isSynced
()
&&
!
switchToCandidateMatcherWhenSynced
)
{
candidateMatcherActive
=
false
;
}
else
if
(
lastFramePresentationTimeNs
!=
C
.
TIME_UNSET
)
{
if
(!
candidateMatcherActive
||
candidateMatcher
.
isLastFrameOutlier
())
{
// Reset the candidate with the last and current frame presentation timestamps, so that it
// will try and match against the duration of the previous frame.
candidateMatcher
.
reset
();
candidateMatcher
.
onNextFrame
(
lastFramePresentationTimeNs
);
}
candidateMatcherActive
=
true
;
candidateMatcher
.
onNextFrame
(
framePresentationTimeNs
);
}
if
(
candidateMatcherActive
&&
candidateMatcher
.
isSynced
())
{
// The candidate matcher should be promoted to be the current matcher. The current matcher
// can be re-used as the next candidate matcher.
Matcher
previousMatcher
=
currentMatcher
;
currentMatcher
=
candidateMatcher
;
candidateMatcher
=
previousMatcher
;
candidateMatcherActive
=
false
;
switchToCandidateMatcherWhenSynced
=
false
;
}
lastFramePresentationTimeNs
=
framePresentationTimeNs
;
}
/** Returns whether the estimator has detected a fixed frame rate. */
public
boolean
isSynced
()
{
return
currentMatcher
.
isSynced
();
}
/**
* The currently detected fixed frame duration estimate in nanoseconds, or {@link C#TIME_UNSET} if
* {@link #isSynced()} is {@code false}. Whilst synced, the estimate is refined each time {@link
* #onNextFrame} is called with a new frame presentation timestamp.
*/
public
long
getFrameDurationNs
()
{
return
isSynced
()
?
currentMatcher
.
getFrameDurationNs
()
:
C
.
TIME_UNSET
;
}
/**
* The currently detected fixed frame rate estimate, or {@link Format#NO_VALUE} if {@link
* #isSynced()} is {@code false}. Whilst synced, the estimate is refined each time {@link
* #onNextFrame} is called with a new frame presentation timestamp.
*/
public
double
getFrameRate
()
{
return
isSynced
()
?
(
double
)
C
.
NANOS_PER_SECOND
/
currentMatcher
.
getFrameDurationNs
()
:
Format
.
NO_VALUE
;
}
/** Tries to match frame durations against the duration of the first frame it receives. */
private
static
final
class
Matcher
{
private
long
firstFramePresentationTimeNs
;
private
long
firstFrameDurationNs
;
private
long
lastFramePresentationTimeNs
;
private
long
frameCount
;
/** The total number of frames that have matched the frame duration being tracked. */
private
long
matchingFrameCount
;
/** The sum of the frame durations of all matching frames. */
private
long
matchingFrameDurationSumNs
;
/** Cyclic buffer of flags indicating whether the most recent frame durations were outliers. */
private
final
boolean
[]
recentFrameOutlierFlags
;
/**
* The number of recent frame durations that were outliers. Equal to the number of {@code true}
* values in {@link #recentFrameOutlierFlags}.
*/
private
int
recentFrameOutlierCount
;
public
Matcher
()
{
recentFrameOutlierFlags
=
new
boolean
[
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
];
}
public
void
reset
()
{
frameCount
=
0
;
matchingFrameCount
=
0
;
matchingFrameDurationSumNs
=
0
;
recentFrameOutlierCount
=
0
;
Arrays
.
fill
(
recentFrameOutlierFlags
,
false
);
}
public
boolean
isSynced
()
{
return
frameCount
>
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
&&
recentFrameOutlierCount
==
0
;
}
public
boolean
isLastFrameOutlier
()
{
if
(
frameCount
==
0
)
{
return
false
;
}
return
recentFrameOutlierFlags
[
getRecentFrameOutlierIndex
(
frameCount
-
1
)];
}
public
long
getFrameDurationNs
()
{
return
matchingFrameCount
==
0
?
0
:
(
matchingFrameDurationSumNs
/
matchingFrameCount
);
}
public
void
onNextFrame
(
long
framePresentationTimeNs
)
{
if
(
frameCount
==
0
)
{
firstFramePresentationTimeNs
=
framePresentationTimeNs
;
}
else
if
(
frameCount
==
1
)
{
// This is the frame duration that the tracker will match against.
firstFrameDurationNs
=
framePresentationTimeNs
-
firstFramePresentationTimeNs
;
matchingFrameDurationSumNs
=
firstFrameDurationNs
;
matchingFrameCount
=
1
;
}
else
{
long
lastFrameDurationNs
=
framePresentationTimeNs
-
lastFramePresentationTimeNs
;
int
recentFrameOutlierIndex
=
getRecentFrameOutlierIndex
(
frameCount
);
if
(
Math
.
abs
(
lastFrameDurationNs
-
firstFrameDurationNs
)
<=
MAX_MATCHING_FRAME_DIFFERENCE_NS
)
{
matchingFrameCount
++;
matchingFrameDurationSumNs
+=
lastFrameDurationNs
;
if
(
recentFrameOutlierFlags
[
recentFrameOutlierIndex
])
{
recentFrameOutlierFlags
[
recentFrameOutlierIndex
]
=
false
;
recentFrameOutlierCount
--;
}
}
else
{
if
(!
recentFrameOutlierFlags
[
recentFrameOutlierIndex
])
{
recentFrameOutlierFlags
[
recentFrameOutlierIndex
]
=
true
;
recentFrameOutlierCount
++;
}
}
}
frameCount
++;
lastFramePresentationTimeNs
=
framePresentationTimeNs
;
}
private
static
int
getRecentFrameOutlierIndex
(
long
frameCount
)
{
return
(
int
)
(
frameCount
%
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
);
}
}
}
library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java
View file @
d0165438
...
@@ -55,24 +55,18 @@ public final class VideoFrameReleaseTimeHelper {
...
@@ -55,24 +55,18 @@ public final class VideoFrameReleaseTimeHelper {
*/
*/
private
static
final
long
VSYNC_OFFSET_PERCENTAGE
=
80
;
private
static
final
long
VSYNC_OFFSET_PERCENTAGE
=
80
;
private
static
final
int
MIN_FRAMES_FOR_ADJUSTMENT
=
6
;
private
final
FixedFrameRateEstimator
fixedFrameRateEstimator
;
@Nullable
private
final
WindowManager
windowManager
;
@Nullable
private
final
WindowManager
windowManager
;
@Nullable
private
final
VSyncSampler
vsyncSampler
;
@Nullable
private
final
VSyncSampler
vsyncSampler
;
@Nullable
private
final
DefaultDisplayListener
displayListener
;
@Nullable
private
final
DefaultDisplayListener
displayListener
;
private
float
formatFrameRate
;
private
float
formatFrameRate
;
private
double
playbackSpeed
;
private
double
playbackSpeed
;
private
long
nextFramePresentationTimeUs
;
private
long
vsyncDurationNs
;
private
long
vsyncDurationNs
;
private
long
vsyncOffsetNs
;
private
long
vsyncOffsetNs
;
private
boolean
haveSync
;
private
long
frameIndex
;
private
long
syncReleaseTimeNs
;
private
long
syncFramePresentationTimeNs
;
private
long
frameCount
;
private
long
pendingLastAdjustedFrameIndex
;
private
long
pendingLastAdjustedFrameIndex
;
private
long
pendingLastAdjustedReleaseTimeNs
;
private
long
pendingLastAdjustedReleaseTimeNs
;
private
long
lastAdjustedFrameIndex
;
private
long
lastAdjustedFrameIndex
;
...
@@ -93,6 +87,7 @@ public final class VideoFrameReleaseTimeHelper {
...
@@ -93,6 +87,7 @@ public final class VideoFrameReleaseTimeHelper {
* @param context A context from which information about the default display can be retrieved.
* @param context A context from which information about the default display can be retrieved.
*/
*/
public
VideoFrameReleaseTimeHelper
(
@Nullable
Context
context
)
{
public
VideoFrameReleaseTimeHelper
(
@Nullable
Context
context
)
{
fixedFrameRateEstimator
=
new
FixedFrameRateEstimator
();
if
(
context
!=
null
)
{
if
(
context
!=
null
)
{
context
=
context
.
getApplicationContext
();
context
=
context
.
getApplicationContext
();
windowManager
=
(
WindowManager
)
context
.
getSystemService
(
Context
.
WINDOW_SERVICE
);
windowManager
=
(
WindowManager
)
context
.
getSystemService
(
Context
.
WINDOW_SERVICE
);
...
@@ -115,7 +110,7 @@ public final class VideoFrameReleaseTimeHelper {
...
@@ -115,7 +110,7 @@ public final class VideoFrameReleaseTimeHelper {
/** Called when the renderer is enabled. */
/** Called when the renderer is enabled. */
@TargetApi
(
17
)
// displayListener is null if Util.SDK_INT < 17.
@TargetApi
(
17
)
// displayListener is null if Util.SDK_INT < 17.
public
void
onEnabled
()
{
public
void
onEnabled
()
{
haveSync
=
false
;
fixedFrameRateEstimator
.
reset
()
;
if
(
windowManager
!=
null
)
{
if
(
windowManager
!=
null
)
{
vsyncSampler
.
addObserver
();
vsyncSampler
.
addObserver
();
if
(
displayListener
!=
null
)
{
if
(
displayListener
!=
null
)
{
...
@@ -138,12 +133,12 @@ public final class VideoFrameReleaseTimeHelper {
...
@@ -138,12 +133,12 @@ public final class VideoFrameReleaseTimeHelper {
/** Called when the renderer is started. */
/** Called when the renderer is started. */
public
void
onStarted
()
{
public
void
onStarted
()
{
haveSync
=
false
;
resetAdjustment
()
;
}
}
/** Called when the renderer's position is reset. */
/** Called when the renderer's position is reset. */
public
void
onPositionReset
()
{
public
void
onPositionReset
()
{
haveSync
=
false
;
resetAdjustment
()
;
}
}
/**
/**
...
@@ -154,7 +149,7 @@ public final class VideoFrameReleaseTimeHelper {
...
@@ -154,7 +149,7 @@ public final class VideoFrameReleaseTimeHelper {
*/
*/
public
void
onPlaybackSpeed
(
double
playbackSpeed
)
{
public
void
onPlaybackSpeed
(
double
playbackSpeed
)
{
this
.
playbackSpeed
=
playbackSpeed
;
this
.
playbackSpeed
=
playbackSpeed
;
haveSync
=
false
;
resetAdjustment
()
;
}
}
/**
/**
...
@@ -164,6 +159,7 @@ public final class VideoFrameReleaseTimeHelper {
...
@@ -164,6 +159,7 @@ public final class VideoFrameReleaseTimeHelper {
*/
*/
public
void
onFormatChanged
(
float
formatFrameRate
)
{
public
void
onFormatChanged
(
float
formatFrameRate
)
{
this
.
formatFrameRate
=
formatFrameRate
;
this
.
formatFrameRate
=
formatFrameRate
;
fixedFrameRateEstimator
.
onFormatChanged
(
formatFrameRate
);
}
}
/**
/**
...
@@ -172,14 +168,17 @@ public final class VideoFrameReleaseTimeHelper {
...
@@ -172,14 +168,17 @@ public final class VideoFrameReleaseTimeHelper {
* @param framePresentationTimeUs The frame presentation timestamp, in microseconds.
* @param framePresentationTimeUs The frame presentation timestamp, in microseconds.
*/
*/
public
void
onNextFrame
(
long
framePresentationTimeUs
)
{
public
void
onNextFrame
(
long
framePresentationTimeUs
)
{
if
(
pendingLastAdjustedFrameIndex
!=
C
.
INDEX_UNSET
)
{
lastAdjustedFrameIndex
=
pendingLastAdjustedFrameIndex
;
lastAdjustedFrameIndex
=
pendingLastAdjustedFrameIndex
;
lastAdjustedReleaseTimeNs
=
pendingLastAdjustedReleaseTimeNs
;
lastAdjustedReleaseTimeNs
=
pendingLastAdjustedReleaseTimeNs
;
nextFramePresentationTimeUs
=
framePresentationTimeUs
;
}
frameCount
++;
fixedFrameRateEstimator
.
onNextFrame
(
framePresentationTimeUs
*
1000
);
frameIndex
++;
}
}
/** Returns the estimated playback frame rate, or {@link C#RATE_UNSET} if unknown. */
/** Returns the estimated playback frame rate, or {@link C#RATE_UNSET} if unknown. */
public
float
getPlaybackFrameRate
()
{
public
float
getPlaybackFrameRate
()
{
// TODO: Hook up fixedFrameRateEstimator.
return
formatFrameRate
==
Format
.
NO_VALUE
return
formatFrameRate
==
Format
.
NO_VALUE
?
C
.
RATE_UNSET
?
C
.
RATE_UNSET
:
(
float
)
(
formatFrameRate
*
playbackSpeed
);
:
(
float
)
(
formatFrameRate
*
playbackSpeed
);
...
@@ -200,51 +199,21 @@ public final class VideoFrameReleaseTimeHelper {
...
@@ -200,51 +199,21 @@ public final class VideoFrameReleaseTimeHelper {
* {@link System#nanoTime()}.
* {@link System#nanoTime()}.
*/
*/
public
long
adjustReleaseTime
(
long
releaseTimeNs
)
{
public
long
adjustReleaseTime
(
long
releaseTimeNs
)
{
long
framePresentationTimeNs
=
nextFramePresentationTimeUs
*
1000
;
// Until we know better, the adjustment will be a no-op.
// Until we know better, the adjustment will be a no-op.
long
adjustedReleaseTimeNs
=
releaseTimeNs
;
long
adjustedReleaseTimeNs
=
releaseTimeNs
;
if
(
haveSync
)
{
if
(
lastAdjustedFrameIndex
!=
C
.
INDEX_UNSET
&&
fixedFrameRateEstimator
.
isSynced
())
{
if
(
frameCount
>=
MIN_FRAMES_FOR_ADJUSTMENT
)
{
long
frameDurationNs
=
fixedFrameRateEstimator
.
getFrameDurationNs
();
// 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 frame rate at a finer granularity than the frame times
// themselves (which often only have millisecond granularity).
long
averageFrameDurationNs
=
(
framePresentationTimeNs
-
syncFramePresentationTimeNs
)
/
frameCount
;
// Project the adjusted frame time forward using the average.
long
candidateAdjustedReleaseTimeNs
=
long
candidateAdjustedReleaseTimeNs
=
lastAdjustedReleaseTimeNs
lastAdjustedReleaseTimeNs
+
getPlayoutDuration
(
+
getPlayoutDuration
(
frameDurationNs
*
(
frameIndex
-
lastAdjustedFrameIndex
));
averageFrameDurationNs
*
(
frameCount
-
lastAdjustedFrameIndex
));
if
(
adjustmentAllowed
(
releaseTimeNs
,
candidateAdjustedReleaseTimeNs
))
{
if
(
adjustmentAllowed
(
releaseTimeNs
,
candidateAdjustedReleaseTimeNs
))
{
adjustedReleaseTimeNs
=
candidateAdjustedReleaseTimeNs
;
adjustedReleaseTimeNs
=
candidateAdjustedReleaseTimeNs
;
}
else
{
}
else
{
haveSync
=
false
;
resetAdjustment
();
}
}
else
{
// We're synced but haven't waited the required number of frames to apply an adjustment.
// Check for drift between the proposed and projected frame release timestamps.
long
projectedReleaseTimeNs
=
syncReleaseTimeNs
+
getPlayoutDuration
(
framePresentationTimeNs
-
syncFramePresentationTimeNs
);
if
(!
adjustmentAllowed
(
releaseTimeNs
,
projectedReleaseTimeNs
))
{
haveSync
=
false
;
}
}
}
}
}
pendingLastAdjustedFrameIndex
=
frameIndex
;
// If we need to sync, do so now.
if
(!
haveSync
)
{
syncFramePresentationTimeNs
=
framePresentationTimeNs
;
syncReleaseTimeNs
=
releaseTimeNs
;
frameCount
=
0
;
haveSync
=
true
;
}
pendingLastAdjustedFrameIndex
=
frameCount
;
pendingLastAdjustedReleaseTimeNs
=
adjustedReleaseTimeNs
;
pendingLastAdjustedReleaseTimeNs
=
adjustedReleaseTimeNs
;
if
(
vsyncSampler
==
null
||
vsyncDurationNs
==
C
.
TIME_UNSET
)
{
if
(
vsyncSampler
==
null
||
vsyncDurationNs
==
C
.
TIME_UNSET
)
{
...
@@ -254,13 +223,18 @@ public final class VideoFrameReleaseTimeHelper {
...
@@ -254,13 +223,18 @@ public final class VideoFrameReleaseTimeHelper {
if
(
sampledVsyncTimeNs
==
C
.
TIME_UNSET
)
{
if
(
sampledVsyncTimeNs
==
C
.
TIME_UNSET
)
{
return
adjustedReleaseTimeNs
;
return
adjustedReleaseTimeNs
;
}
}
// Find the timestamp of the closest vsync. This is the vsync that we're targeting.
// Find the timestamp of the closest vsync. This is the vsync that we're targeting.
long
snappedTimeNs
=
closestVsync
(
adjustedReleaseTimeNs
,
sampledVsyncTimeNs
,
vsyncDurationNs
);
long
snappedTimeNs
=
closestVsync
(
adjustedReleaseTimeNs
,
sampledVsyncTimeNs
,
vsyncDurationNs
);
// Apply an offset so that we release before the target vsync, but after the previous one.
// Apply an offset so that we release before the target vsync, but after the previous one.
return
snappedTimeNs
-
vsyncOffsetNs
;
return
snappedTimeNs
-
vsyncOffsetNs
;
}
}
private
void
resetAdjustment
()
{
frameIndex
=
0
;
lastAdjustedFrameIndex
=
C
.
INDEX_UNSET
;
pendingLastAdjustedFrameIndex
=
C
.
INDEX_UNSET
;
}
@RequiresApi
(
17
)
@RequiresApi
(
17
)
private
DefaultDisplayListener
maybeBuildDefaultDisplayListenerV17
(
Context
context
)
{
private
DefaultDisplayListener
maybeBuildDefaultDisplayListenerV17
(
Context
context
)
{
DisplayManager
manager
=
(
DisplayManager
)
context
.
getSystemService
(
Context
.
DISPLAY_SERVICE
);
DisplayManager
manager
=
(
DisplayManager
)
context
.
getSystemService
(
Context
.
DISPLAY_SERVICE
);
...
...
library/core/src/test/java/com/google/android/exoplayer2/video/FixedFrameRateEstimatorTest.java
0 → 100644
View file @
d0165438
/*
* Copyright (C) 2020 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
.
exoplayer2
.
video
;
import
static
com
.
google
.
android
.
exoplayer2
.
video
.
FixedFrameRateEstimator
.
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
;
import
static
com
.
google
.
android
.
exoplayer2
.
video
.
FixedFrameRateEstimator
.
MAX_MATCHING_FRAME_DIFFERENCE_NS
;
import
static
com
.
google
.
common
.
truth
.
Truth
.
assertThat
;
import
androidx.test.ext.junit.runners.AndroidJUnit4
;
import
com.google.android.exoplayer2.C
;
import
org.junit.Test
;
import
org.junit.runner.RunWith
;
/** Unit test for {@link FixedFrameRateEstimator}. */
@RunWith
(
AndroidJUnit4
.
class
)
public
final
class
FixedFrameRateEstimatorTest
{
@Test
public
void
fixedFrameRate_withSingleOutlier_syncsAndResyncs
()
{
long
frameDurationNs
=
33_333_333
;
FixedFrameRateEstimator
estimator
=
new
FixedFrameRateEstimator
();
// Initial frame.
long
framePresentationTimestampNs
=
0
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
assertThat
(
estimator
.
isSynced
()).
isFalse
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
C
.
TIME_UNSET
);
// Frames with consistent durations, working toward establishing sync.
for
(
int
i
=
0
;
i
<
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
-
1
;
i
++)
{
framePresentationTimestampNs
+=
frameDurationNs
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
assertThat
(
estimator
.
isSynced
()).
isFalse
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
C
.
TIME_UNSET
);
}
// This frame should establish sync.
framePresentationTimestampNs
+=
frameDurationNs
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
assertThat
(
estimator
.
isSynced
()).
isTrue
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
frameDurationNs
);
framePresentationTimestampNs
+=
frameDurationNs
;
// Make the frame duration just shorter enough to lose sync.
framePresentationTimestampNs
-=
MAX_MATCHING_FRAME_DIFFERENCE_NS
+
1
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
assertThat
(
estimator
.
isSynced
()).
isFalse
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
C
.
TIME_UNSET
);
// Frames with consistent durations, working toward re-establishing sync.
for
(
int
i
=
0
;
i
<
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
-
1
;
i
++)
{
framePresentationTimestampNs
+=
frameDurationNs
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
assertThat
(
estimator
.
isSynced
()).
isFalse
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
C
.
TIME_UNSET
);
}
// This frame should re-establish sync.
framePresentationTimestampNs
+=
frameDurationNs
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
assertThat
(
estimator
.
isSynced
()).
isTrue
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
frameDurationNs
);
}
@Test
public
void
fixedFrameRate_withOutlierFirstFrameDuration_syncs
()
{
long
frameDurationNs
=
33_333_333
;
FixedFrameRateEstimator
estimator
=
new
FixedFrameRateEstimator
();
// Initial frame with double duration.
long
framePresentationTimestampNs
=
0
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
framePresentationTimestampNs
+=
frameDurationNs
*
2
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
assertThat
(
estimator
.
isSynced
()).
isFalse
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
C
.
TIME_UNSET
);
// Frames with consistent durations, working toward establishing sync.
for
(
int
i
=
0
;
i
<
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
-
1
;
i
++)
{
framePresentationTimestampNs
+=
frameDurationNs
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
assertThat
(
estimator
.
isSynced
()).
isFalse
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
C
.
TIME_UNSET
);
}
// This frame should establish sync.
framePresentationTimestampNs
+=
frameDurationNs
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
}
@Test
public
void
newFixedFrameRate_resyncs
()
{
long
frameDurationNs
=
33_333_333
;
FixedFrameRateEstimator
estimator
=
new
FixedFrameRateEstimator
();
long
framePresentationTimestampNs
=
0
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
for
(
int
i
=
0
;
i
<
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
;
i
++)
{
framePresentationTimestampNs
+=
frameDurationNs
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
}
assertThat
(
estimator
.
isSynced
()).
isTrue
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
frameDurationNs
);
// Frames durations are halved from this point.
long
halfFrameRateDuration
=
frameDurationNs
/
2
;
// Frames with consistent durations, working toward establishing new sync.
for
(
int
i
=
0
;
i
<
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
-
1
;
i
++)
{
framePresentationTimestampNs
+=
halfFrameRateDuration
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
assertThat
(
estimator
.
isSynced
()).
isFalse
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
C
.
TIME_UNSET
);
}
// This frame should establish sync.
framePresentationTimestampNs
+=
halfFrameRateDuration
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
assertThat
(
estimator
.
isSynced
()).
isTrue
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
halfFrameRateDuration
);
}
@Test
public
void
fixedFrameRate_withMillisecondPrecision_syncs
()
{
long
frameDurationNs
=
33_333_333
;
FixedFrameRateEstimator
estimator
=
new
FixedFrameRateEstimator
();
// Initial frame.
long
framePresentationTimestampNs
=
0
;
estimator
.
onNextFrame
(
getNsWithMsPrecision
(
framePresentationTimestampNs
));
assertThat
(
estimator
.
isSynced
()).
isFalse
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
C
.
TIME_UNSET
);
// Frames with consistent durations, working toward establishing sync.
for
(
int
i
=
0
;
i
<
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
-
1
;
i
++)
{
framePresentationTimestampNs
+=
frameDurationNs
;
estimator
.
onNextFrame
(
getNsWithMsPrecision
(
framePresentationTimestampNs
));
assertThat
(
estimator
.
isSynced
()).
isFalse
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
C
.
TIME_UNSET
);
}
// This frame should establish sync.
framePresentationTimestampNs
+=
frameDurationNs
;
estimator
.
onNextFrame
(
getNsWithMsPrecision
(
framePresentationTimestampNs
));
assertThat
(
estimator
.
isSynced
()).
isTrue
();
// The estimated frame duration should be strictly better than millisecond precision.
long
estimatedFrameDurationNs
=
estimator
.
getFrameDurationNs
();
long
estimatedFrameDurationErrorNs
=
Math
.
abs
(
estimatedFrameDurationNs
-
frameDurationNs
);
assertThat
(
estimatedFrameDurationErrorNs
).
isLessThan
(
1000000
);
}
@Test
public
void
variableFrameRate_doesNotSync
()
{
long
frameDurationNs
=
33_333_333
;
FixedFrameRateEstimator
estimator
=
new
FixedFrameRateEstimator
();
// Initial frame.
long
framePresentationTimestampNs
=
0
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
assertThat
(
estimator
.
isSynced
()).
isFalse
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
C
.
TIME_UNSET
);
for
(
int
i
=
0
;
i
<
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
*
10
;
i
++)
{
framePresentationTimestampNs
+=
frameDurationNs
;
// Adjust a frame that's just different enough, just often enough to prevent sync.
if
((
i
%
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
)
==
0
)
{
framePresentationTimestampNs
+=
MAX_MATCHING_FRAME_DIFFERENCE_NS
+
1
;
}
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
assertThat
(
estimator
.
isSynced
()).
isFalse
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
C
.
TIME_UNSET
);
}
}
@Test
public
void
newFixedFrameRate_withFormatFrameRateChange_resyncs
()
{
long
frameDurationNs
=
33_333_333
;
float
frameRate
=
(
float
)
C
.
NANOS_PER_SECOND
/
frameDurationNs
;
FixedFrameRateEstimator
estimator
=
new
FixedFrameRateEstimator
();
estimator
.
onFormatChanged
(
frameRate
);
long
framePresentationTimestampNs
=
0
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
for
(
int
i
=
0
;
i
<
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
;
i
++)
{
framePresentationTimestampNs
+=
frameDurationNs
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
}
assertThat
(
estimator
.
isSynced
()).
isTrue
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
frameDurationNs
);
// Frames durations are halved from this point.
long
halfFrameDuration
=
frameDurationNs
*
2
;
float
doubleFrameRate
=
(
float
)
C
.
NANOS_PER_SECOND
/
halfFrameDuration
;
estimator
.
onFormatChanged
(
doubleFrameRate
);
// Format frame rate change should cause immediate sync loss.
assertThat
(
estimator
.
isSynced
()).
isFalse
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
C
.
TIME_UNSET
);
// Frames with consistent durations, working toward establishing new sync.
for
(
int
i
=
0
;
i
<
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
;
i
++)
{
framePresentationTimestampNs
+=
halfFrameDuration
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
assertThat
(
estimator
.
isSynced
()).
isFalse
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
C
.
TIME_UNSET
);
}
// This frame should establish sync.
framePresentationTimestampNs
+=
halfFrameDuration
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
assertThat
(
estimator
.
isSynced
()).
isTrue
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
halfFrameDuration
);
}
@Test
public
void
smallFrameRateChange_withoutFormatFrameRateChange_keepsSyncAndAdjustsEstimate
()
{
long
frameDurationNs
=
33_333_333
;
// 30 fps
float
roundedFrameRate
=
30
;
FixedFrameRateEstimator
estimator
=
new
FixedFrameRateEstimator
();
estimator
.
onFormatChanged
(
roundedFrameRate
);
long
framePresentationTimestampNs
=
0
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
for
(
int
i
=
0
;
i
<
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
;
i
++)
{
framePresentationTimestampNs
+=
frameDurationNs
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
}
assertThat
(
estimator
.
isSynced
()).
isTrue
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
frameDurationNs
);
long
newFrameDurationNs
=
33_366_667
;
// 30 * (1000/1001) = 29.97 fps
estimator
.
onFormatChanged
(
roundedFrameRate
);
// Format frame rate is unchanged.
// Previous estimate should remain valid for now because neither format specified a duration.
assertThat
(
estimator
.
isSynced
()).
isTrue
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
frameDurationNs
);
// The estimate should start moving toward the new frame duration. If should not lose sync
// because the change in frame rate is very small.
for
(
int
i
=
0
;
i
<
CONSECUTIVE_MATCHING_FRAME_DURATIONS_FOR_SYNC
-
1
;
i
++)
{
framePresentationTimestampNs
+=
newFrameDurationNs
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
assertThat
(
estimator
.
isSynced
()).
isTrue
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isGreaterThan
(
frameDurationNs
);
assertThat
(
estimator
.
getFrameDurationNs
()).
isLessThan
(
newFrameDurationNs
);
}
framePresentationTimestampNs
+=
newFrameDurationNs
;
estimator
.
onNextFrame
(
framePresentationTimestampNs
);
// Frames with the previous frame duration should now be excluded from the estimate, so the
// estimate should become exact.
assertThat
(
estimator
.
isSynced
()).
isTrue
();
assertThat
(
estimator
.
getFrameDurationNs
()).
isEqualTo
(
newFrameDurationNs
);
}
private
static
final
long
getNsWithMsPrecision
(
long
presentationTimeNs
)
{
return
(
presentationTimeNs
/
1000000
)
*
1000000
;
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment