/
SpeedControlCallback.java
145 lines (133 loc) · 6.51 KB
/
SpeedControlCallback.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/*
* Copyright 2013 Google Inc. All rights reserved.
*
* 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.android.grafika;
import android.util.Log;
/**
* Movie player callback.
* <p>
* The goal here is to play back frames at the original rate. This is done by introducing
* a pause before the frame is submitted to the renderer.
* <p>
* This is not coordinated with VSYNC. Since we can't control the display's refresh rate, and
* the source material has time stamps that specify when each frame should be presented,
* we will have to drop or repeat frames occasionally.
* <p>
* Thread restrictions are noted in the method descriptions. The FrameCallback overrides should
* only be called from the MoviePlayer.
*/
public class SpeedControlCallback implements MoviePlayer.FrameCallback {
private static final String TAG = MainActivity.TAG;
private static final boolean CHECK_SLEEP_TIME = false;
private static final long ONE_MILLION = 1000000L;
private long mPrevPresentUsec;
private long mPrevMonoUsec;
private long mFixedFrameDurationUsec;
private boolean mLoopReset;
/**
* Sets a fixed playback rate. If set, this will ignore the presentation time stamp
* in the video file. Must be called before playback thread starts.
*/
public void setFixedPlaybackRate(int fps) {
mFixedFrameDurationUsec = ONE_MILLION / fps;
}
// runs on decode thread
@Override
public void preRender(long presentationTimeUsec) {
// For the first frame, we grab the presentation time from the video
// and the current monotonic clock time. For subsequent frames, we
// sleep for a bit to try to ensure that we're rendering frames at the
// pace dictated by the video stream.
//
// If the frame rate is faster than vsync we should be dropping frames. On
// Android 4.4 this may not be happening.
if (mPrevMonoUsec == 0) {
// Latch current values, then return immediately.
mPrevMonoUsec = System.nanoTime() / 1000;
mPrevPresentUsec = presentationTimeUsec;
} else {
// Compute the desired time delta between the previous frame and this frame.
long frameDelta;
if (mLoopReset) {
// We don't get an indication of how long the last frame should appear
// on-screen, so we just throw a reasonable value in. We could probably
// do better by using a previous frame duration or some sort of average;
// for now we just use 30fps.
mPrevPresentUsec = presentationTimeUsec - ONE_MILLION / 30;
mLoopReset = false;
}
if (mFixedFrameDurationUsec != 0) {
// Caller requested a fixed frame rate. Ignore PTS.
frameDelta = mFixedFrameDurationUsec;
} else {
frameDelta = presentationTimeUsec - mPrevPresentUsec;
}
if (frameDelta < 0) {
Log.w(TAG, "Weird, video times went backward");
frameDelta = 0;
} else if (frameDelta == 0) {
// This suggests a possible bug in movie generation.
Log.i(TAG, "Warning: current frame and previous frame had same timestamp");
} else if (frameDelta > 10 * ONE_MILLION) {
// Inter-frame times could be arbitrarily long. For this player, we want
// to alert the developer that their movie might have issues (maybe they
// accidentally output timestamps in nsec rather than usec).
Log.i(TAG, "Inter-frame pause was " + (frameDelta / ONE_MILLION) +
"sec, capping at 5 sec");
frameDelta = 5 * ONE_MILLION;
}
long desiredUsec = mPrevMonoUsec + frameDelta; // when we want to wake up
long nowUsec = System.nanoTime() / 1000;
while (nowUsec < (desiredUsec - 100) /*&& mState == RUNNING*/) {
// Sleep until it's time to wake up. To be responsive to "stop" commands
// we're going to wake up every half a second even if the sleep is supposed
// to be longer (which should be rare). The alternative would be
// to interrupt the thread, but that requires more work.
//
// The precision of the sleep call varies widely from one device to another;
// we may wake early or late. Different devices will have a minimum possible
// sleep time. If we're within 100us of the target time, we'll probably
// overshoot if we try to sleep, so just go ahead and continue on.
long sleepTimeUsec = desiredUsec - nowUsec;
if (sleepTimeUsec > 500000) {
sleepTimeUsec = 500000;
}
try {
if (CHECK_SLEEP_TIME) {
long startNsec = System.nanoTime();
Thread.sleep(sleepTimeUsec / 1000, (int) (sleepTimeUsec % 1000) * 1000);
long actualSleepNsec = System.nanoTime() - startNsec;
Log.d(TAG, "sleep=" + sleepTimeUsec + " actual=" + (actualSleepNsec/1000) +
" diff=" + (Math.abs(actualSleepNsec / 1000 - sleepTimeUsec)) +
" (usec)");
} else {
Thread.sleep(sleepTimeUsec / 1000, (int) (sleepTimeUsec % 1000) * 1000);
}
} catch (InterruptedException ie) {}
nowUsec = System.nanoTime() / 1000;
}
// Advance times using calculated time values, not the post-sleep monotonic
// clock time, to avoid drifting.
mPrevMonoUsec += frameDelta;
mPrevPresentUsec += frameDelta;
}
}
// runs on decode thread
@Override public void postRender() {}
@Override
public void loopReset() {
mLoopReset = true;
}
}