@matjash/pixi-native-win32-x64 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/THIRD_PARTY_NOTICES.md +83 -0
- package/index.cjs +25 -0
- package/index.d.ts +11 -0
- package/native/audio/binding-path.js +16 -0
- package/native/audio/dist/win32-x64/native_audio.node +0 -0
- package/native/audio/package.json +7 -0
- package/native/audio/src/index.d.ts +63 -0
- package/native/audio/src/index.js +5 -0
- package/native/audio/src/lib.rs +1520 -0
- package/native/gpu/dist/win32-x64/d3dcompiler_47.dll +0 -0
- package/native/gpu/dist/win32-x64/pixi_native_gpu.node +0 -0
- package/native/gpu/package.json +24 -0
- package/native/gpu/src/binding-path.js +16 -0
- package/native/gpu/src/index.d.ts +2847 -0
- package/native/gpu/src/index.js +48 -0
- package/native/video/dist/win32-x64/FFMPEG_BUILD_INFO.txt +46 -0
- package/native/video/dist/win32-x64/FFMPEG_LICENSE.txt +502 -0
- package/native/video/dist/win32-x64/FFMPEG_SHA256SUMS +4 -0
- package/native/video/dist/win32-x64/ffmpeg.exe +0 -0
- package/native/video/dist/win32-x64/ffprobe.exe +0 -0
- package/native/video/dist/win32-x64/native_video.node +0 -0
- package/native/video/package.json +7 -0
- package/native/video/src/binding-path.js +26 -0
- package/native/video/src/index.d.ts +36 -0
- package/native/video/src/index.js +61 -0
- package/native/video/src/lib.rs +994 -0
- package/native/window/binding-path.js +11 -0
- package/native/window/dist/win32-x64/native_window.node +0 -0
- package/native/window/package.json +7 -0
- package/native/window/src/index.d.ts +17 -0
- package/native/window/src/index.js +32 -0
- package/native/window/src/lib.rs +308 -0
- package/package.json +41 -0
- package/third_party/ffmpeg-source-8.0-140fd653ae.tar.gz +0 -0
- package/third_party/ffmpeg-source-8.0-140fd653ae.tar.gz.sha256 +1 -0
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
#![deny(clippy::all)]
|
|
2
|
+
|
|
3
|
+
use std::collections::VecDeque;
|
|
4
|
+
use std::io::{self, BufRead, BufReader, Read};
|
|
5
|
+
use std::process::{Child, ChildStdout, Command, Stdio};
|
|
6
|
+
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
|
|
7
|
+
use std::sync::{Arc, Condvar, Mutex};
|
|
8
|
+
use std::thread;
|
|
9
|
+
use std::time::Duration;
|
|
10
|
+
|
|
11
|
+
use napi::bindgen_prelude::*;
|
|
12
|
+
use napi_derive::napi;
|
|
13
|
+
|
|
14
|
+
const FRAME_QUEUE_CAPACITY: usize = 4;
|
|
15
|
+
const HARDWARE_DECODE_ATTEMPTS: usize = 5;
|
|
16
|
+
const HARDWARE_RETRY_DELAY: Duration = Duration::from_millis(500);
|
|
17
|
+
|
|
18
|
+
#[napi(object)]
|
|
19
|
+
pub struct DecoderOptions {
|
|
20
|
+
pub width: i64,
|
|
21
|
+
pub height: i64,
|
|
22
|
+
pub fps: Option<f64>,
|
|
23
|
+
pub start_time: Option<f64>,
|
|
24
|
+
pub ffmpeg_path: Option<String>,
|
|
25
|
+
pub vaapi_device: Option<String>,
|
|
26
|
+
pub playback_rate: Option<f64>,
|
|
27
|
+
pub end_time: Option<f64>,
|
|
28
|
+
pub source_paced: Option<bool>,
|
|
29
|
+
pub input_args: Option<Vec<String>>,
|
|
30
|
+
pub output_args: Option<Vec<String>>,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[napi(object)]
|
|
34
|
+
pub struct VideoFrame {
|
|
35
|
+
pub width: i64,
|
|
36
|
+
pub height: i64,
|
|
37
|
+
pub timestamp_us: i64,
|
|
38
|
+
pub data: Buffer,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
42
|
+
enum DecoderBackend {
|
|
43
|
+
D3d11va,
|
|
44
|
+
Vaapi,
|
|
45
|
+
Cpu,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl DecoderBackend {
|
|
49
|
+
fn name(self) -> &'static str {
|
|
50
|
+
match self {
|
|
51
|
+
Self::D3d11va => "D3D11VA",
|
|
52
|
+
Self::Vaapi => "VA-API",
|
|
53
|
+
Self::Cpu => "CPU",
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn hardware() -> Option<Self> {
|
|
58
|
+
#[cfg(target_os = "windows")]
|
|
59
|
+
{
|
|
60
|
+
Some(Self::D3d11va)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[cfg(target_os = "linux")]
|
|
64
|
+
{
|
|
65
|
+
Some(Self::Vaapi)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
|
69
|
+
{
|
|
70
|
+
None
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[derive(Clone)]
|
|
76
|
+
struct DecoderState {
|
|
77
|
+
closed: Arc<AtomicBool>,
|
|
78
|
+
finished: Arc<AtomicBool>,
|
|
79
|
+
child: Arc<Mutex<Option<Child>>>,
|
|
80
|
+
frames: Arc<(Mutex<FrameQueue>, Condvar)>,
|
|
81
|
+
catch_up_timestamp_us: Arc<AtomicI64>,
|
|
82
|
+
source_paced: bool,
|
|
83
|
+
error: Arc<Mutex<Option<String>>>,
|
|
84
|
+
decoded_frames: Arc<AtomicU64>,
|
|
85
|
+
dropped_frames: Arc<AtomicU64>,
|
|
86
|
+
skipped_frames: Arc<AtomicU64>,
|
|
87
|
+
backend: Arc<Mutex<String>>,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
struct PendingFrame {
|
|
91
|
+
timestamp_us: i64,
|
|
92
|
+
data: Vec<u8>,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[derive(Default)]
|
|
96
|
+
struct FrameQueue {
|
|
97
|
+
frames: VecDeque<PendingFrame>,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl FrameQueue {
|
|
101
|
+
fn push_latest(&mut self, frame: PendingFrame) -> Option<Vec<u8>> {
|
|
102
|
+
let recycled = if self.frames.len() >= FRAME_QUEUE_CAPACITY {
|
|
103
|
+
self.frames.pop_front().map(|dropped| dropped.data)
|
|
104
|
+
} else {
|
|
105
|
+
None
|
|
106
|
+
};
|
|
107
|
+
self.frames.push_back(frame);
|
|
108
|
+
recycled
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fn push_back(&mut self, frame: PendingFrame) {
|
|
112
|
+
self.frames.push_back(frame);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn pop_next(&mut self) -> Option<PendingFrame> {
|
|
116
|
+
self.frames.pop_front()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn pop_latest(&mut self) -> (Option<PendingFrame>, usize) {
|
|
120
|
+
let latest = self.frames.pop_back();
|
|
121
|
+
let skipped = self.frames.len();
|
|
122
|
+
self.frames.clear();
|
|
123
|
+
(latest, skipped)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn len(&self) -> usize {
|
|
127
|
+
self.frames.len()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fn clear(&mut self) {
|
|
131
|
+
self.frames.clear();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
struct SpawnedFfmpeg {
|
|
136
|
+
child: Child,
|
|
137
|
+
stdout: ChildStdout,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[derive(Clone)]
|
|
141
|
+
struct FfmpegRequest {
|
|
142
|
+
ffmpeg_path: String,
|
|
143
|
+
source: String,
|
|
144
|
+
vaapi_device: String,
|
|
145
|
+
width: usize,
|
|
146
|
+
height: usize,
|
|
147
|
+
fps: f64,
|
|
148
|
+
start_time: f64,
|
|
149
|
+
end_time: Option<f64>,
|
|
150
|
+
input_args: Vec<String>,
|
|
151
|
+
output_args: Vec<String>,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#[napi]
|
|
155
|
+
pub struct NativeVideoDecoder {
|
|
156
|
+
options: DecoderOptions,
|
|
157
|
+
state: DecoderState,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#[napi]
|
|
161
|
+
impl NativeVideoDecoder {
|
|
162
|
+
#[napi(constructor)]
|
|
163
|
+
pub fn new(options: DecoderOptions) -> Result<Self> {
|
|
164
|
+
validate_options(&options)?;
|
|
165
|
+
let source_paced = options.source_paced.unwrap_or(false);
|
|
166
|
+
|
|
167
|
+
let backend = DecoderBackend::hardware()
|
|
168
|
+
.map(DecoderBackend::name)
|
|
169
|
+
.unwrap_or("CPU")
|
|
170
|
+
.to_string();
|
|
171
|
+
|
|
172
|
+
Ok(Self {
|
|
173
|
+
options,
|
|
174
|
+
state: DecoderState {
|
|
175
|
+
closed: Arc::new(AtomicBool::new(true)),
|
|
176
|
+
finished: Arc::new(AtomicBool::new(false)),
|
|
177
|
+
child: Arc::new(Mutex::new(None)),
|
|
178
|
+
frames: Arc::new((Mutex::new(FrameQueue::default()), Condvar::new())),
|
|
179
|
+
catch_up_timestamp_us: Arc::new(AtomicI64::new(-1)),
|
|
180
|
+
source_paced,
|
|
181
|
+
error: Arc::new(Mutex::new(None)),
|
|
182
|
+
decoded_frames: Arc::new(AtomicU64::new(0)),
|
|
183
|
+
dropped_frames: Arc::new(AtomicU64::new(0)),
|
|
184
|
+
skipped_frames: Arc::new(AtomicU64::new(0)),
|
|
185
|
+
backend: Arc::new(Mutex::new(backend)),
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#[napi]
|
|
191
|
+
pub fn open(&mut self, source: String) -> Result<()> {
|
|
192
|
+
if !self.state.closed.swap(false, Ordering::SeqCst) {
|
|
193
|
+
return Err(Error::from_reason("Video decoder is already open"));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
self.state.finished.store(false, Ordering::SeqCst);
|
|
197
|
+
self.state.decoded_frames.store(0, Ordering::SeqCst);
|
|
198
|
+
self.state.dropped_frames.store(0, Ordering::SeqCst);
|
|
199
|
+
self.state.skipped_frames.store(0, Ordering::SeqCst);
|
|
200
|
+
|
|
201
|
+
if let Ok(mut error) = self.state.error.lock() {
|
|
202
|
+
*error = None;
|
|
203
|
+
}
|
|
204
|
+
let (frames, _) = &*self.state.frames;
|
|
205
|
+
if let Ok(mut frames) = frames.lock() {
|
|
206
|
+
frames.clear();
|
|
207
|
+
}
|
|
208
|
+
self.state.catch_up_timestamp_us.store(-1, Ordering::SeqCst);
|
|
209
|
+
|
|
210
|
+
let width = usize::try_from(self.options.width)
|
|
211
|
+
.map_err(|_| Error::from_reason("Invalid video width"))?;
|
|
212
|
+
let height = usize::try_from(self.options.height)
|
|
213
|
+
.map_err(|_| Error::from_reason("Invalid video height"))?;
|
|
214
|
+
let fps = self.options.fps.unwrap_or(30.0);
|
|
215
|
+
let start_time = self.options.start_time.unwrap_or(0.0);
|
|
216
|
+
let ffmpeg_path = self
|
|
217
|
+
.options
|
|
218
|
+
.ffmpeg_path
|
|
219
|
+
.clone()
|
|
220
|
+
.unwrap_or_else(|| "ffmpeg".to_string());
|
|
221
|
+
let vaapi_device = self
|
|
222
|
+
.options
|
|
223
|
+
.vaapi_device
|
|
224
|
+
.clone()
|
|
225
|
+
.or_else(|| std::env::var("FFMPEG_VAAPI_DEVICE").ok())
|
|
226
|
+
.unwrap_or_else(|| "/dev/dri/renderD128".to_string());
|
|
227
|
+
|
|
228
|
+
let requested_backend = DecoderBackend::hardware().unwrap_or(DecoderBackend::Cpu);
|
|
229
|
+
let request = FfmpegRequest {
|
|
230
|
+
ffmpeg_path,
|
|
231
|
+
source,
|
|
232
|
+
vaapi_device,
|
|
233
|
+
width,
|
|
234
|
+
height,
|
|
235
|
+
fps,
|
|
236
|
+
start_time,
|
|
237
|
+
end_time: self.options.end_time,
|
|
238
|
+
input_args: self.options.input_args.clone().unwrap_or_default(),
|
|
239
|
+
output_args: self.options.output_args.clone().unwrap_or_default(),
|
|
240
|
+
};
|
|
241
|
+
let (spawned, active_backend) = match spawn_ffmpeg(&request, requested_backend) {
|
|
242
|
+
Ok(spawned) => (spawned, requested_backend),
|
|
243
|
+
Err(hardware_error) if requested_backend != DecoderBackend::Cpu => {
|
|
244
|
+
eprintln!(
|
|
245
|
+
"FFmpeg {} process failed to start; retrying with CPU decoder: {hardware_error}",
|
|
246
|
+
requested_backend.name()
|
|
247
|
+
);
|
|
248
|
+
let spawned = spawn_ffmpeg(&request, DecoderBackend::Cpu)
|
|
249
|
+
.map_err(|error| Error::from_reason(error.to_string()))?;
|
|
250
|
+
(spawned, DecoderBackend::Cpu)
|
|
251
|
+
}
|
|
252
|
+
Err(error) => {
|
|
253
|
+
self.state.closed.store(true, Ordering::SeqCst);
|
|
254
|
+
return Err(Error::from_reason(error.to_string()));
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
set_backend_name(
|
|
259
|
+
&self.state,
|
|
260
|
+
if active_backend == DecoderBackend::Cpu && requested_backend != DecoderBackend::Cpu {
|
|
261
|
+
"CPU fallback"
|
|
262
|
+
} else {
|
|
263
|
+
active_backend.name()
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
let state = self.state.clone();
|
|
268
|
+
let initial_stdout = install_child(&state, spawned)
|
|
269
|
+
.map_err(|error| Error::from_reason(error.to_string()))?;
|
|
270
|
+
|
|
271
|
+
thread::spawn(move || {
|
|
272
|
+
let mut result =
|
|
273
|
+
consume_decoder_attempt(initial_stdout, width, height, fps, start_time, &state);
|
|
274
|
+
|
|
275
|
+
if active_backend != DecoderBackend::Cpu {
|
|
276
|
+
for attempt in 2..=HARDWARE_DECODE_ATTEMPTS {
|
|
277
|
+
if result.is_ok() || !should_retry_hardware(attempt - 1, &state) {
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
let error = result
|
|
281
|
+
.as_ref()
|
|
282
|
+
.expect_err("failed hardware attempt checked above");
|
|
283
|
+
eprintln!(
|
|
284
|
+
"FFmpeg {} decoder attempt {}/{} failed: {error}; retrying in {} ms",
|
|
285
|
+
active_backend.name(),
|
|
286
|
+
attempt - 1,
|
|
287
|
+
HARDWARE_DECODE_ATTEMPTS,
|
|
288
|
+
HARDWARE_RETRY_DELAY.as_millis(),
|
|
289
|
+
);
|
|
290
|
+
match wait_for_hardware_retry(&state) {
|
|
291
|
+
Ok(true) => {}
|
|
292
|
+
Ok(false) => break,
|
|
293
|
+
Err(wait_error) => {
|
|
294
|
+
result = Err(wait_error);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
result = spawn_ffmpeg(&request, active_backend)
|
|
299
|
+
.and_then(|spawned| install_child(&state, spawned))
|
|
300
|
+
.and_then(|stdout| {
|
|
301
|
+
consume_decoder_attempt(stdout, width, height, fps, start_time, &state)
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if let Err(error) = result {
|
|
307
|
+
let has_frames = state.decoded_frames.load(Ordering::SeqCst) > 0;
|
|
308
|
+
if active_backend != DecoderBackend::Cpu
|
|
309
|
+
&& !has_frames
|
|
310
|
+
&& !state.closed.load(Ordering::SeqCst)
|
|
311
|
+
{
|
|
312
|
+
eprintln!(
|
|
313
|
+
"FFmpeg {} decoder failed after {} attempts; retrying with CPU decoder: {error}",
|
|
314
|
+
active_backend.name(),
|
|
315
|
+
HARDWARE_DECODE_ATTEMPTS,
|
|
316
|
+
);
|
|
317
|
+
set_backend_name(&state, "CPU fallback");
|
|
318
|
+
|
|
319
|
+
let fallback_result = spawn_ffmpeg(&request, DecoderBackend::Cpu)
|
|
320
|
+
.and_then(|spawned| install_child(&state, spawned))
|
|
321
|
+
.and_then(|stdout| {
|
|
322
|
+
consume_decoder_attempt(stdout, width, height, fps, start_time, &state)
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
if let Err(fallback_error) = fallback_result {
|
|
326
|
+
store_error(&state, fallback_error.to_string());
|
|
327
|
+
}
|
|
328
|
+
} else if !state.closed.load(Ordering::SeqCst) {
|
|
329
|
+
store_error(&state, error.to_string());
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if !state.closed.load(Ordering::SeqCst) {
|
|
334
|
+
state.finished.store(true, Ordering::SeqCst);
|
|
335
|
+
}
|
|
336
|
+
state.closed.store(true, Ordering::SeqCst);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
Ok(())
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
#[napi]
|
|
343
|
+
pub fn poll_latest(&self) -> Option<VideoFrame> {
|
|
344
|
+
let (frames, available) = &*self.state.frames;
|
|
345
|
+
let (pending, skipped) = frames.lock().ok()?.pop_latest();
|
|
346
|
+
available.notify_all();
|
|
347
|
+
if skipped > 0 {
|
|
348
|
+
self.state
|
|
349
|
+
.skipped_frames
|
|
350
|
+
.fetch_add(skipped as u64, Ordering::SeqCst);
|
|
351
|
+
}
|
|
352
|
+
self.to_video_frame(pending?)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#[napi]
|
|
356
|
+
pub fn poll_next(&self) -> Option<VideoFrame> {
|
|
357
|
+
let (frames, available) = &*self.state.frames;
|
|
358
|
+
let pending = frames.lock().ok()?.pop_next()?;
|
|
359
|
+
available.notify_one();
|
|
360
|
+
self.to_video_frame(pending)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
#[napi]
|
|
364
|
+
pub fn queued_frames(&self) -> i64 {
|
|
365
|
+
let (frames, _) = &*self.state.frames;
|
|
366
|
+
frames
|
|
367
|
+
.lock()
|
|
368
|
+
.map(|frames| i64::try_from(frames.len()).unwrap_or(i64::MAX))
|
|
369
|
+
.unwrap_or(0)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
#[napi]
|
|
373
|
+
pub fn catch_up_to(&self, timestamp_us: i64) -> Result<()> {
|
|
374
|
+
if timestamp_us < 0 {
|
|
375
|
+
return Err(Error::from_reason(
|
|
376
|
+
"Catch-up timestamp must be non-negative",
|
|
377
|
+
));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
self.state
|
|
381
|
+
.catch_up_timestamp_us
|
|
382
|
+
.store(timestamp_us, Ordering::SeqCst);
|
|
383
|
+
let (frames, available) = &*self.state.frames;
|
|
384
|
+
if let Ok(mut frames) = frames.lock() {
|
|
385
|
+
let skipped = frames.len();
|
|
386
|
+
frames.clear();
|
|
387
|
+
self.state
|
|
388
|
+
.skipped_frames
|
|
389
|
+
.fetch_add(skipped as u64, Ordering::SeqCst);
|
|
390
|
+
}
|
|
391
|
+
available.notify_all();
|
|
392
|
+
Ok(())
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
fn to_video_frame(&self, pending: PendingFrame) -> Option<VideoFrame> {
|
|
396
|
+
Some(VideoFrame {
|
|
397
|
+
width: self.options.width,
|
|
398
|
+
height: self.options.height,
|
|
399
|
+
timestamp_us: pending.timestamp_us,
|
|
400
|
+
data: Buffer::from(pending.data),
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
#[napi]
|
|
405
|
+
pub fn poll_error(&self) -> Option<String> {
|
|
406
|
+
self.state.error.lock().ok()?.take()
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
#[napi]
|
|
410
|
+
pub fn backend(&self) -> String {
|
|
411
|
+
self.state
|
|
412
|
+
.backend
|
|
413
|
+
.lock()
|
|
414
|
+
.map(|backend| backend.clone())
|
|
415
|
+
.unwrap_or_else(|_| "unknown".to_string())
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
#[napi]
|
|
419
|
+
pub fn decoded_frames(&self) -> i64 {
|
|
420
|
+
i64::try_from(self.state.decoded_frames.load(Ordering::SeqCst)).unwrap_or(i64::MAX)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
#[napi]
|
|
424
|
+
pub fn dropped_frames(&self) -> i64 {
|
|
425
|
+
i64::try_from(self.state.dropped_frames.load(Ordering::SeqCst)).unwrap_or(i64::MAX)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#[napi]
|
|
429
|
+
pub fn skipped_frames(&self) -> i64 {
|
|
430
|
+
i64::try_from(self.state.skipped_frames.load(Ordering::SeqCst)).unwrap_or(i64::MAX)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
#[napi]
|
|
434
|
+
pub fn is_finished(&self) -> bool {
|
|
435
|
+
self.state.finished.load(Ordering::SeqCst)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#[napi]
|
|
439
|
+
pub fn close(&mut self) {
|
|
440
|
+
close_state(&self.state);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
impl Drop for NativeVideoDecoder {
|
|
445
|
+
fn drop(&mut self) {
|
|
446
|
+
close_state(&self.state);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
fn validate_options(options: &DecoderOptions) -> Result<()> {
|
|
451
|
+
if options.width <= 0 || options.height <= 0 {
|
|
452
|
+
return Err(Error::from_reason("Video dimensions must be positive"));
|
|
453
|
+
}
|
|
454
|
+
if options.width % 2 != 0 || options.height % 2 != 0 {
|
|
455
|
+
return Err(Error::from_reason("NV12 video dimensions must be even"));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
let fps = options.fps.unwrap_or(30.0);
|
|
459
|
+
if !fps.is_finite() || fps <= 0.0 {
|
|
460
|
+
return Err(Error::from_reason("Video FPS must be positive and finite"));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
let start_time = options.start_time.unwrap_or(0.0);
|
|
464
|
+
if !start_time.is_finite() || start_time < 0.0 {
|
|
465
|
+
return Err(Error::from_reason(
|
|
466
|
+
"Video start time must be non-negative and finite",
|
|
467
|
+
));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
let playback_rate = options.playback_rate.unwrap_or(1.0);
|
|
471
|
+
if !playback_rate.is_finite() || playback_rate <= 0.0 {
|
|
472
|
+
return Err(Error::from_reason(
|
|
473
|
+
"Video playback rate must be positive and finite",
|
|
474
|
+
));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if let Some(end_time) = options.end_time {
|
|
478
|
+
if !end_time.is_finite() || end_time <= start_time {
|
|
479
|
+
return Err(Error::from_reason(
|
|
480
|
+
"Video end time must be finite and greater than start time",
|
|
481
|
+
));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
Ok(())
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
fn nv12_frame_bytes(width: usize, height: usize) -> io::Result<usize> {
|
|
488
|
+
let y_bytes = width
|
|
489
|
+
.checked_mul(height)
|
|
490
|
+
.ok_or_else(|| io::Error::other("Video frame size overflow"))?;
|
|
491
|
+
y_bytes
|
|
492
|
+
.checked_add(y_bytes / 2)
|
|
493
|
+
.ok_or_else(|| io::Error::other("Video frame size overflow"))
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
fn ffmpeg_args(request: &FfmpegRequest, backend: DecoderBackend) -> Vec<String> {
|
|
497
|
+
let mut args = vec![
|
|
498
|
+
"-hide_banner".to_string(),
|
|
499
|
+
"-loglevel".to_string(),
|
|
500
|
+
"error".to_string(),
|
|
501
|
+
"-nostdin".to_string(),
|
|
502
|
+
];
|
|
503
|
+
|
|
504
|
+
match backend {
|
|
505
|
+
DecoderBackend::D3d11va => {
|
|
506
|
+
args.extend(["-hwaccel".to_string(), "d3d11va".to_string()]);
|
|
507
|
+
}
|
|
508
|
+
DecoderBackend::Vaapi => {
|
|
509
|
+
args.extend([
|
|
510
|
+
"-hwaccel".to_string(),
|
|
511
|
+
"vaapi".to_string(),
|
|
512
|
+
"-hwaccel_device".to_string(),
|
|
513
|
+
request.vaapi_device.clone(),
|
|
514
|
+
"-hwaccel_output_format".to_string(),
|
|
515
|
+
"vaapi".to_string(),
|
|
516
|
+
]);
|
|
517
|
+
}
|
|
518
|
+
DecoderBackend::Cpu => {}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if request.start_time > 0.0 {
|
|
522
|
+
args.extend(["-ss".to_string(), request.start_time.to_string()]);
|
|
523
|
+
}
|
|
524
|
+
args.extend(request.input_args.iter().cloned());
|
|
525
|
+
args.extend(["-i".to_string(), request.source.clone(), "-an".to_string()]);
|
|
526
|
+
if let Some(end_time) = request.end_time {
|
|
527
|
+
args.extend([
|
|
528
|
+
"-t".to_string(),
|
|
529
|
+
(end_time - request.start_time).to_string(),
|
|
530
|
+
]);
|
|
531
|
+
}
|
|
532
|
+
args.extend(request.output_args.iter().cloned());
|
|
533
|
+
|
|
534
|
+
let scale = format!(
|
|
535
|
+
"fps={},scale={}:{}:flags=fast_bilinear:in_range=auto:out_range=tv:in_color_matrix=auto:out_color_matrix=bt709,format=nv12",
|
|
536
|
+
request.fps, request.width, request.height
|
|
537
|
+
);
|
|
538
|
+
let filter = match backend {
|
|
539
|
+
DecoderBackend::Vaapi => format!("hwdownload,format=nv12,{scale}"),
|
|
540
|
+
DecoderBackend::D3d11va => scale,
|
|
541
|
+
// Keep the CPU fallback independent from the color-negotiation path
|
|
542
|
+
// used after VA-API download. Some FFmpeg builds fail to initialize
|
|
543
|
+
// auto_scale when that path is reused for software yuv420p frames.
|
|
544
|
+
DecoderBackend::Cpu => format!(
|
|
545
|
+
"fps={},scale={}:{}:flags=fast_bilinear,format=nv12",
|
|
546
|
+
request.fps, request.width, request.height
|
|
547
|
+
),
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
if backend == DecoderBackend::Cpu {
|
|
551
|
+
// The bundled/minimal FFmpeg builds can fail filter negotiation when
|
|
552
|
+
// multiple filter workers initialize the software graph concurrently.
|
|
553
|
+
args.extend([
|
|
554
|
+
"-filter_threads".to_string(),
|
|
555
|
+
"1".to_string(),
|
|
556
|
+
"-filter_complex_threads".to_string(),
|
|
557
|
+
"1".to_string(),
|
|
558
|
+
]);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
args.extend([
|
|
562
|
+
"-vf".to_string(),
|
|
563
|
+
filter,
|
|
564
|
+
"-f".to_string(),
|
|
565
|
+
"rawvideo".to_string(),
|
|
566
|
+
"-pix_fmt".to_string(),
|
|
567
|
+
"nv12".to_string(),
|
|
568
|
+
"pipe:1".to_string(),
|
|
569
|
+
]);
|
|
570
|
+
args
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
fn spawn_ffmpeg(request: &FfmpegRequest, backend: DecoderBackend) -> io::Result<SpawnedFfmpeg> {
|
|
574
|
+
let mut child = Command::new(&request.ffmpeg_path)
|
|
575
|
+
.args(ffmpeg_args(request, backend))
|
|
576
|
+
.stdin(Stdio::null())
|
|
577
|
+
.stdout(Stdio::piped())
|
|
578
|
+
.stderr(Stdio::piped())
|
|
579
|
+
.spawn()?;
|
|
580
|
+
|
|
581
|
+
let stdout = child
|
|
582
|
+
.stdout
|
|
583
|
+
.take()
|
|
584
|
+
.ok_or_else(|| io::Error::other("FFmpeg stdout unavailable"))?;
|
|
585
|
+
if let Some(stderr) = child.stderr.take() {
|
|
586
|
+
thread::spawn(move || {
|
|
587
|
+
for line in BufReader::new(stderr).lines().map_while(|line| line.ok()) {
|
|
588
|
+
eprintln!("{}", redact_url_credentials(&line));
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
Ok(SpawnedFfmpeg { child, stdout })
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
fn redact_url_credentials(value: &str) -> String {
|
|
596
|
+
let mut result = value.to_string();
|
|
597
|
+
let mut search_from = 0;
|
|
598
|
+
while let Some(relative_scheme) = result[search_from..].find("://") {
|
|
599
|
+
let authority_start = search_from + relative_scheme + 3;
|
|
600
|
+
let authority_end = result[authority_start..]
|
|
601
|
+
.find(|character: char| character == '/' || character.is_whitespace())
|
|
602
|
+
.map(|offset| authority_start + offset)
|
|
603
|
+
.unwrap_or(result.len());
|
|
604
|
+
let Some(relative_at) = result[authority_start..authority_end].find('@') else {
|
|
605
|
+
search_from = authority_end.min(result.len());
|
|
606
|
+
continue;
|
|
607
|
+
};
|
|
608
|
+
let at = authority_start + relative_at;
|
|
609
|
+
result.replace_range(authority_start..at, "***:***");
|
|
610
|
+
search_from = authority_start + "***:***@".len();
|
|
611
|
+
}
|
|
612
|
+
result
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
fn install_child(state: &DecoderState, spawned: SpawnedFfmpeg) -> io::Result<ChildStdout> {
|
|
616
|
+
*state
|
|
617
|
+
.child
|
|
618
|
+
.lock()
|
|
619
|
+
.map_err(|_| io::Error::other("FFmpeg process lock poisoned"))? = Some(spawned.child);
|
|
620
|
+
Ok(spawned.stdout)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
fn consume_ffmpeg_output(
|
|
624
|
+
mut stdout: ChildStdout,
|
|
625
|
+
width: usize,
|
|
626
|
+
height: usize,
|
|
627
|
+
fps: f64,
|
|
628
|
+
start_time: f64,
|
|
629
|
+
state: &DecoderState,
|
|
630
|
+
) -> io::Result<()> {
|
|
631
|
+
let frame_bytes = nv12_frame_bytes(width, height)?;
|
|
632
|
+
let start_timestamp_us = (start_time * 1_000_000.0).round() as i64;
|
|
633
|
+
let mut frame_index = 0_i64;
|
|
634
|
+
let mut data = vec![0_u8; frame_bytes];
|
|
635
|
+
let read_result = loop {
|
|
636
|
+
if state.closed.load(Ordering::SeqCst) {
|
|
637
|
+
break Ok(());
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
match stdout.read_exact(&mut data) {
|
|
641
|
+
Ok(()) => {
|
|
642
|
+
let timestamp_us =
|
|
643
|
+
start_timestamp_us + (frame_index as f64 * 1_000_000.0 / fps).round() as i64;
|
|
644
|
+
frame_index += 1;
|
|
645
|
+
state.decoded_frames.fetch_add(1, Ordering::SeqCst);
|
|
646
|
+
|
|
647
|
+
data = enqueue_frame(PendingFrame { timestamp_us, data }, frame_bytes, state)?;
|
|
648
|
+
}
|
|
649
|
+
Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => break Ok(()),
|
|
650
|
+
Err(error) => break Err(error),
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
let status = state
|
|
655
|
+
.child
|
|
656
|
+
.lock()
|
|
657
|
+
.map_err(|_| io::Error::other("FFmpeg process lock poisoned"))?
|
|
658
|
+
.take()
|
|
659
|
+
.map(|mut process| process.wait())
|
|
660
|
+
.transpose()?;
|
|
661
|
+
|
|
662
|
+
read_result?;
|
|
663
|
+
if !state.closed.load(Ordering::SeqCst) && status.is_some_and(|status| !status.success()) {
|
|
664
|
+
return Err(io::Error::other(format!(
|
|
665
|
+
"FFmpeg exited with status {}",
|
|
666
|
+
status.expect("status checked above")
|
|
667
|
+
)));
|
|
668
|
+
}
|
|
669
|
+
Ok(())
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
fn consume_decoder_attempt(
|
|
673
|
+
stdout: ChildStdout,
|
|
674
|
+
width: usize,
|
|
675
|
+
height: usize,
|
|
676
|
+
fps: f64,
|
|
677
|
+
start_time: f64,
|
|
678
|
+
state: &DecoderState,
|
|
679
|
+
) -> io::Result<()> {
|
|
680
|
+
let result = consume_ffmpeg_output(stdout, width, height, fps, start_time, state);
|
|
681
|
+
result?;
|
|
682
|
+
if !state.closed.load(Ordering::SeqCst) && state.decoded_frames.load(Ordering::SeqCst) == 0 {
|
|
683
|
+
return Err(io::Error::other(
|
|
684
|
+
"FFmpeg ended before producing its first video frame",
|
|
685
|
+
));
|
|
686
|
+
}
|
|
687
|
+
Ok(())
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
fn should_retry_hardware(completed_attempts: usize, state: &DecoderState) -> bool {
|
|
691
|
+
completed_attempts < HARDWARE_DECODE_ATTEMPTS
|
|
692
|
+
&& state.decoded_frames.load(Ordering::SeqCst) == 0
|
|
693
|
+
&& !state.closed.load(Ordering::SeqCst)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
fn wait_for_hardware_retry(state: &DecoderState) -> io::Result<bool> {
|
|
697
|
+
if state.closed.load(Ordering::SeqCst) {
|
|
698
|
+
return Ok(false);
|
|
699
|
+
}
|
|
700
|
+
let (frames, available) = &*state.frames;
|
|
701
|
+
let frames = frames
|
|
702
|
+
.lock()
|
|
703
|
+
.map_err(|_| io::Error::other("Video frame queue lock poisoned"))?;
|
|
704
|
+
let (_frames, _) = available
|
|
705
|
+
.wait_timeout_while(frames, HARDWARE_RETRY_DELAY, |_| {
|
|
706
|
+
!state.closed.load(Ordering::SeqCst)
|
|
707
|
+
})
|
|
708
|
+
.map_err(|_| io::Error::other("Video frame queue lock poisoned"))?;
|
|
709
|
+
Ok(!state.closed.load(Ordering::SeqCst))
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
fn enqueue_frame(
|
|
713
|
+
frame: PendingFrame,
|
|
714
|
+
frame_bytes: usize,
|
|
715
|
+
state: &DecoderState,
|
|
716
|
+
) -> io::Result<Vec<u8>> {
|
|
717
|
+
let catch_up_timestamp_us = state.catch_up_timestamp_us.load(Ordering::SeqCst);
|
|
718
|
+
if catch_up_timestamp_us >= 0 && frame.timestamp_us < catch_up_timestamp_us {
|
|
719
|
+
state.skipped_frames.fetch_add(1, Ordering::SeqCst);
|
|
720
|
+
return Ok(frame.data);
|
|
721
|
+
}
|
|
722
|
+
if catch_up_timestamp_us >= 0 {
|
|
723
|
+
state.catch_up_timestamp_us.store(-1, Ordering::SeqCst);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
let (frames, available) = &*state.frames;
|
|
727
|
+
let mut frames = frames
|
|
728
|
+
.lock()
|
|
729
|
+
.map_err(|_| io::Error::other("Video frame queue lock poisoned"))?;
|
|
730
|
+
|
|
731
|
+
if state.source_paced {
|
|
732
|
+
if let Some(recycled) = frames.push_latest(frame) {
|
|
733
|
+
state.dropped_frames.fetch_add(1, Ordering::SeqCst);
|
|
734
|
+
return Ok(recycled);
|
|
735
|
+
}
|
|
736
|
+
} else {
|
|
737
|
+
while frames.len() >= FRAME_QUEUE_CAPACITY
|
|
738
|
+
&& !state.closed.load(Ordering::SeqCst)
|
|
739
|
+
&& state.catch_up_timestamp_us.load(Ordering::SeqCst) < 0
|
|
740
|
+
{
|
|
741
|
+
frames = available
|
|
742
|
+
.wait(frames)
|
|
743
|
+
.map_err(|_| io::Error::other("Video frame queue lock poisoned"))?;
|
|
744
|
+
}
|
|
745
|
+
if state.closed.load(Ordering::SeqCst) {
|
|
746
|
+
return Ok(frame.data);
|
|
747
|
+
}
|
|
748
|
+
let target = state.catch_up_timestamp_us.load(Ordering::SeqCst);
|
|
749
|
+
if target >= 0 && frame.timestamp_us < target {
|
|
750
|
+
state.skipped_frames.fetch_add(1, Ordering::SeqCst);
|
|
751
|
+
return Ok(frame.data);
|
|
752
|
+
}
|
|
753
|
+
if target >= 0 {
|
|
754
|
+
state.catch_up_timestamp_us.store(-1, Ordering::SeqCst);
|
|
755
|
+
}
|
|
756
|
+
frames.push_back(frame);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
Ok(vec![0_u8; frame_bytes])
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
fn set_backend_name(state: &DecoderState, name: &str) {
|
|
763
|
+
if let Ok(mut backend) = state.backend.lock() {
|
|
764
|
+
*backend = name.to_string();
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
fn store_error(state: &DecoderState, message: String) {
|
|
769
|
+
eprintln!("FFmpeg decoder failed: {message}");
|
|
770
|
+
if let Ok(mut error) = state.error.lock() {
|
|
771
|
+
*error = Some(message);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
fn close_state(state: &DecoderState) {
|
|
776
|
+
state.closed.store(true, Ordering::SeqCst);
|
|
777
|
+
let (frames, available) = &*state.frames;
|
|
778
|
+
available.notify_all();
|
|
779
|
+
if let Ok(mut child) = state.child.lock() {
|
|
780
|
+
if let Some(mut process) = child.take() {
|
|
781
|
+
let _ = process.kill();
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if let Ok(mut frames) = frames.lock() {
|
|
785
|
+
frames.clear();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
#[cfg(test)]
|
|
790
|
+
mod tests {
|
|
791
|
+
use super::*;
|
|
792
|
+
use std::sync::mpsc;
|
|
793
|
+
use std::time::Duration;
|
|
794
|
+
|
|
795
|
+
#[test]
|
|
796
|
+
fn calculates_nv12_frame_size() {
|
|
797
|
+
assert_eq!(nv12_frame_bytes(1280, 720).unwrap(), 1_382_400);
|
|
798
|
+
assert_eq!(nv12_frame_bytes(1920, 1080).unwrap(), 3_110_400);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
#[test]
|
|
802
|
+
fn builds_windows_nv12_seek_args() {
|
|
803
|
+
let args = ffmpeg_args(&request(24.0, 12.5), DecoderBackend::D3d11va);
|
|
804
|
+
assert!(args.windows(2).any(|pair| pair == ["-hwaccel", "d3d11va"]));
|
|
805
|
+
assert!(args.windows(2).any(|pair| pair == ["-ss", "12.5"]));
|
|
806
|
+
assert!(args
|
|
807
|
+
.iter()
|
|
808
|
+
.any(|arg| arg.contains("out_color_matrix=bt709")));
|
|
809
|
+
assert_eq!(args[args.len() - 3..], ["-pix_fmt", "nv12", "pipe:1"]);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
#[test]
|
|
813
|
+
fn builds_linux_vaapi_download_filter() {
|
|
814
|
+
let args = ffmpeg_args(&request(30.0, 0.0), DecoderBackend::Vaapi);
|
|
815
|
+
assert!(args.iter().any(|arg| arg == "/dev/dri/test"));
|
|
816
|
+
assert!(args
|
|
817
|
+
.iter()
|
|
818
|
+
.any(|arg| arg.starts_with("hwdownload,format=nv12,fps=")));
|
|
819
|
+
assert!(!args.iter().any(|arg| arg == "-ss"));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
#[test]
|
|
823
|
+
fn builds_custom_arguments_without_readrate() {
|
|
824
|
+
let mut request = request(25.0, 0.0);
|
|
825
|
+
request.input_args = vec!["-fflags".to_string(), "nobuffer".to_string()];
|
|
826
|
+
request.output_args = vec!["-threads".to_string(), "1".to_string()];
|
|
827
|
+
let args = ffmpeg_args(&request, DecoderBackend::Cpu);
|
|
828
|
+
assert!(!args.iter().any(|arg| arg == "-readrate" || arg == "-re"));
|
|
829
|
+
let input_index = args.iter().position(|arg| arg == "-i").unwrap();
|
|
830
|
+
let fflags_index = args.iter().position(|arg| arg == "-fflags").unwrap();
|
|
831
|
+
let threads_index = args.iter().position(|arg| arg == "-threads").unwrap();
|
|
832
|
+
let pipe_index = args.iter().position(|arg| arg == "pipe:1").unwrap();
|
|
833
|
+
assert!(fflags_index < input_index);
|
|
834
|
+
assert!(threads_index > input_index && threads_index < pipe_index);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
#[test]
|
|
838
|
+
fn builds_serial_software_fallback_filter() {
|
|
839
|
+
let args = ffmpeg_args(&request(30.0, 0.0), DecoderBackend::Cpu);
|
|
840
|
+
assert!(args.windows(2).any(|pair| pair == ["-filter_threads", "1"]));
|
|
841
|
+
assert!(args
|
|
842
|
+
.windows(2)
|
|
843
|
+
.any(|pair| pair == ["-filter_complex_threads", "1"]));
|
|
844
|
+
assert!(args
|
|
845
|
+
.iter()
|
|
846
|
+
.any(|arg| arg == "fps=30,scale=1280:720:flags=fast_bilinear,format=nv12"));
|
|
847
|
+
assert!(!args.iter().any(|arg| arg.contains("in_color_matrix=auto")));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
#[test]
|
|
851
|
+
fn redacts_url_credentials_from_ffmpeg_errors() {
|
|
852
|
+
assert_eq!(
|
|
853
|
+
redact_url_credentials("failed http://root:secret@10.1.2.3/live.sdp"),
|
|
854
|
+
"failed http://***:***@10.1.2.3/live.sdp"
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
#[test]
|
|
859
|
+
fn rejects_odd_nv12_dimensions() {
|
|
860
|
+
let result = validate_options(&DecoderOptions {
|
|
861
|
+
width: 1279,
|
|
862
|
+
height: 720,
|
|
863
|
+
fps: Some(24.0),
|
|
864
|
+
start_time: None,
|
|
865
|
+
ffmpeg_path: None,
|
|
866
|
+
vaapi_device: None,
|
|
867
|
+
playback_rate: None,
|
|
868
|
+
end_time: None,
|
|
869
|
+
source_paced: None,
|
|
870
|
+
input_args: None,
|
|
871
|
+
output_args: None,
|
|
872
|
+
});
|
|
873
|
+
assert!(result.is_err());
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
#[test]
|
|
877
|
+
fn frame_queue_preserves_order_and_drops_oldest_on_overflow() {
|
|
878
|
+
let mut queue = FrameQueue::default();
|
|
879
|
+
for timestamp_us in 0..FRAME_QUEUE_CAPACITY as i64 {
|
|
880
|
+
assert!(queue.push_latest(pending(timestamp_us)).is_none());
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
let recycled = queue
|
|
884
|
+
.push_latest(pending(FRAME_QUEUE_CAPACITY as i64))
|
|
885
|
+
.expect("oldest frame should be recycled");
|
|
886
|
+
assert_eq!(recycled, vec![0]);
|
|
887
|
+
assert_eq!(queue.len(), FRAME_QUEUE_CAPACITY);
|
|
888
|
+
assert_eq!(queue.pop_next().unwrap().timestamp_us, 1);
|
|
889
|
+
assert_eq!(queue.pop_next().unwrap().timestamp_us, 2);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
#[test]
|
|
893
|
+
fn frame_queue_can_take_latest_and_reports_skipped_frames() {
|
|
894
|
+
let mut queue = FrameQueue::default();
|
|
895
|
+
queue.push_latest(pending(10));
|
|
896
|
+
queue.push_latest(pending(20));
|
|
897
|
+
queue.push_latest(pending(30));
|
|
898
|
+
|
|
899
|
+
let (latest, skipped) = queue.pop_latest();
|
|
900
|
+
assert_eq!(latest.unwrap().timestamp_us, 30);
|
|
901
|
+
assert_eq!(skipped, 2);
|
|
902
|
+
assert_eq!(queue.len(), 0);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
#[test]
|
|
906
|
+
fn file_queue_applies_backpressure_until_a_frame_is_consumed() {
|
|
907
|
+
let state = decoder_state(false);
|
|
908
|
+
for timestamp_us in 0..FRAME_QUEUE_CAPACITY as i64 {
|
|
909
|
+
enqueue_frame(pending(timestamp_us), 1, &state).unwrap();
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
let producer_state = state.clone();
|
|
913
|
+
let (sent, received) = mpsc::channel();
|
|
914
|
+
thread::spawn(move || {
|
|
915
|
+
let result = enqueue_frame(pending(99), 1, &producer_state);
|
|
916
|
+
sent.send(result.is_ok()).unwrap();
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
assert!(received.recv_timeout(Duration::from_millis(25)).is_err());
|
|
920
|
+
let (frames, available) = &*state.frames;
|
|
921
|
+
frames.lock().unwrap().pop_next();
|
|
922
|
+
available.notify_one();
|
|
923
|
+
assert!(received.recv_timeout(Duration::from_secs(1)).unwrap());
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
#[test]
|
|
927
|
+
fn catch_up_discards_obsolete_file_frames_before_rebuffering() {
|
|
928
|
+
let state = decoder_state(false);
|
|
929
|
+
state.catch_up_timestamp_us.store(30, Ordering::SeqCst);
|
|
930
|
+
|
|
931
|
+
assert_eq!(enqueue_frame(pending(10), 1, &state).unwrap(), vec![10]);
|
|
932
|
+
assert_eq!(enqueue_frame(pending(20), 1, &state).unwrap(), vec![20]);
|
|
933
|
+
enqueue_frame(pending(30), 1, &state).unwrap();
|
|
934
|
+
|
|
935
|
+
assert_eq!(state.skipped_frames.load(Ordering::SeqCst), 2);
|
|
936
|
+
let (frames, _) = &*state.frames;
|
|
937
|
+
let mut frames = frames.lock().unwrap();
|
|
938
|
+
assert_eq!(frames.pop_next().unwrap().timestamp_us, 30);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
#[test]
|
|
942
|
+
fn hardware_retry_stops_after_five_attempts_or_first_frame() {
|
|
943
|
+
let state = decoder_state(false);
|
|
944
|
+
for completed_attempts in 1..HARDWARE_DECODE_ATTEMPTS {
|
|
945
|
+
assert!(should_retry_hardware(completed_attempts, &state));
|
|
946
|
+
}
|
|
947
|
+
assert!(!should_retry_hardware(HARDWARE_DECODE_ATTEMPTS, &state));
|
|
948
|
+
|
|
949
|
+
state.decoded_frames.store(1, Ordering::SeqCst);
|
|
950
|
+
assert!(!should_retry_hardware(1, &state));
|
|
951
|
+
|
|
952
|
+
state.decoded_frames.store(0, Ordering::SeqCst);
|
|
953
|
+
state.closed.store(true, Ordering::SeqCst);
|
|
954
|
+
assert!(!should_retry_hardware(1, &state));
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
fn pending(timestamp_us: i64) -> PendingFrame {
|
|
958
|
+
PendingFrame {
|
|
959
|
+
timestamp_us,
|
|
960
|
+
data: vec![timestamp_us as u8],
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
fn decoder_state(source_paced: bool) -> DecoderState {
|
|
965
|
+
DecoderState {
|
|
966
|
+
closed: Arc::new(AtomicBool::new(false)),
|
|
967
|
+
finished: Arc::new(AtomicBool::new(false)),
|
|
968
|
+
child: Arc::new(Mutex::new(None)),
|
|
969
|
+
frames: Arc::new((Mutex::new(FrameQueue::default()), Condvar::new())),
|
|
970
|
+
catch_up_timestamp_us: Arc::new(AtomicI64::new(-1)),
|
|
971
|
+
source_paced,
|
|
972
|
+
error: Arc::new(Mutex::new(None)),
|
|
973
|
+
decoded_frames: Arc::new(AtomicU64::new(0)),
|
|
974
|
+
dropped_frames: Arc::new(AtomicU64::new(0)),
|
|
975
|
+
skipped_frames: Arc::new(AtomicU64::new(0)),
|
|
976
|
+
backend: Arc::new(Mutex::new("test".to_string())),
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
fn request(fps: f64, start_time: f64) -> FfmpegRequest {
|
|
981
|
+
FfmpegRequest {
|
|
982
|
+
ffmpeg_path: "ffmpeg".to_string(),
|
|
983
|
+
source: "video.mp4".to_string(),
|
|
984
|
+
vaapi_device: "/dev/dri/test".to_string(),
|
|
985
|
+
width: 1280,
|
|
986
|
+
height: 720,
|
|
987
|
+
fps,
|
|
988
|
+
start_time,
|
|
989
|
+
end_time: None,
|
|
990
|
+
input_args: Vec::new(),
|
|
991
|
+
output_args: Vec::new(),
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|