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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
//! A film's timing: which of its pictures belongs on screen now.
//!
//! Shared by everything that plays video — [`Viewer`](crate::window::Viewer)
//! for one film in one place, the [`Compositor`](crate::Compositor) for many
//! in layers. Each picture goes up when its own timestamp says, against a
//! clock that starts once the first one is actually on screen; late pictures
//! are dropped rather than shown late, so a stall costs a few frames instead
//! of permanent lag.
use std::time::Duration;
use crate::clock::{Action, Follower, MasterClock, Pacing, SharedClock};
use crate::error::{Error, Result};
use crate::frame::Frame;
use crate::video_source::VideoSource;
/// A film while it plays.
pub(crate) struct Playback {
source: VideoSource,
/// The film's position, on whichever timeline it was given — the engine's,
/// which is atome's when there is audio.
clock: Follower,
pacing: Pacing,
/// Start again at the end, or finish.
pub(crate) looping: bool,
/// Whether the clock has started. Unless the film was given a start on the
/// timeline, it waits for the first picture to be on screen, so opening
/// the GPU, filling the queue, and a window appearing are not counted
/// against the film's first second.
started: bool,
draws_before_start: u8,
/// The first picture's timestamp. An MP4 with B-frames starts a frame or
/// two in, and the clock starts at zero.
origin: Option<Duration>,
/// When the picture on screen is due, relative to `origin`.
showing: Option<Duration>,
/// How long the last picture stays up before the film ends or loops.
frame_duration: Duration,
loops: u32,
}
/// What the next refresh should do.
pub(crate) enum Tick {
/// This picture is due: upload it and draw it.
Show(Frame),
/// Keep showing what is already up.
Hold,
/// The film is over and does not loop.
Ended,
}
impl Playback {
/// A film on `timeline`, starting once its first picture is on screen —
/// or, with `start_at`, exactly when the timeline reaches that point, and
/// showing nothing until then. That is how a picture lines up with a sound
/// scheduled in atome for the same moment.
pub(crate) fn new(
source: VideoSource,
looping: bool,
timeline: SharedClock,
start_at: Option<Duration>,
) -> Self {
let rate = source.info().video().and_then(|track| track.frame_rate);
let clock = match start_at {
Some(at) => Follower::starting_at(timeline, at),
None => Follower::new(timeline),
};
Playback {
source,
clock,
pacing: Pacing::for_frame_rate(rate.map_or(25.0, |rate| rate.as_f64())),
looping,
started: start_at.is_some(),
draws_before_start: 0,
origin: None,
showing: None,
frame_duration: rate.map_or(Duration::from_millis(40), |rate| rate.frame_duration()),
loops: 0,
}
}
pub(crate) fn source(&self) -> &VideoSource {
&self.source
}
pub(crate) fn source_mut(&mut self) -> &mut VideoSource {
&mut self.source
}
/// The picture's size as stored, known before anything decodes. Not as
/// rotated: the renderer draws the planes as they are.
// Only `Viewer` reports on a single film.
#[cfg_attr(not(all(feature = "window", not(any(target_os = "ios", target_os = "android")))), allow(dead_code))]
pub(crate) fn size(&self) -> (u32, u32) {
self.source
.info()
.video()
.map_or((1, 1), |track| (track.width, track.height))
}
/// Presented, dropped, and repeated, as [`Pacing`] counts them.
// Only `Viewer` reports on a single film.
#[cfg_attr(not(all(feature = "window", not(any(target_os = "ios", target_os = "android")))), allow(dead_code))]
pub(crate) fn counts(&self) -> (u64, u64, u64) {
self.pacing.counts()
}
/// How many times the film has started again from the top.
// Only `Viewer` reports on a single film.
#[cfg_attr(not(all(feature = "window", not(any(target_os = "ios", target_os = "android")))), allow(dead_code))]
pub(crate) fn loops(&self) -> u32 {
self.loops
}
/// Where the film is, on its own clock.
pub(crate) fn position(&self) -> Duration {
self.clock.position()
}
/// Decodes ahead and decides what this refresh shows.
pub(crate) fn tick(&mut self) -> Result<Tick> {
self.source.advance(Duration::ZERO)?;
// Given a start on the timeline that is still ahead: decode ready,
// show nothing.
if !self.clock.is_due() {
return Ok(Tick::Hold);
}
let origin = *self
.origin
.get_or_insert_with(|| self.source.current_frame().map_or(Duration::ZERO, Frame::pts));
let now = self.clock.position();
if self.source.is_finished() {
let Some(last) = self.showing else {
return Err(Error::Decode {
encoding: self.source.decoder().encoding(),
reason: "the file decoded to no pictures at all".to_string(),
});
};
// The last picture gets its full time on screen before the end.
if now < last + self.frame_duration {
return Ok(Tick::Hold);
}
if !self.looping {
return Ok(Tick::Ended);
}
self.source.rewind()?;
self.origin = None;
self.showing = None;
self.clock.seek(Duration::ZERO);
self.loops += 1;
// Once: after a rewind the queue refills, and a file with nothing
// in it has already been refused above.
return self.tick();
}
// Pacing decides for the picture at the head of the queue: show it,
// throw it away as too late, or leave the current one up a while.
while let Some(next) = self.source.current_frame() {
match self.pacing.decide(next.pts().saturating_sub(origin), now) {
Action::Present => {
if let Some(frame) = self.source.pop_frame() {
self.showing = Some(frame.pts().saturating_sub(origin));
return Ok(Tick::Show(frame));
}
}
Action::Drop => {
self.source.pop_frame();
}
Action::Wait(_) => break,
}
}
if self.showing.is_some() {
self.pacing.note_repeat();
}
Ok(Tick::Hold)
}
/// Called after every presentation. The second one starts the clock.
///
/// Not the first: drawing into a brand-new window returns at once, and it
/// is the *next* refresh that waits while the window server puts the
/// window on screen — about 200 ms on macOS. A clock already running
/// through that wait drops the film's first handful of frames.
pub(crate) fn presented(&mut self) {
if self.started || self.showing.is_none() {
return;
}
self.draws_before_start += 1;
if self.draws_before_start >= 2 {
self.started = true;
self.clock.play();
}
}
// Only `Viewer` reports on a single film.
#[cfg_attr(not(all(feature = "window", not(any(target_os = "ios", target_os = "android")))), allow(dead_code))]
pub(crate) fn toggle_pause(&mut self) {
if !self.started {
return;
}
if self.clock.is_playing() {
self.clock.pause();
} else {
self.clock.play();
}
}
}