@matjash/pixi-native-win32-x64 0.2.1 → 0.2.3
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.
|
@@ -228,15 +228,51 @@ impl Voice {
|
|
|
228
228
|
self.timeline_updated_at = now;
|
|
229
229
|
return;
|
|
230
230
|
}
|
|
231
|
-
self.timeline_seconds
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
231
|
+
self.timeline_seconds = advance_timeline(
|
|
232
|
+
self.timeline_seconds,
|
|
233
|
+
now.duration_since(self.timeline_updated_at).as_secs_f64(),
|
|
234
|
+
self.playback_rate,
|
|
235
|
+
self.duration_seconds,
|
|
236
|
+
self.looped,
|
|
237
|
+
);
|
|
238
238
|
self.timeline_updated_at = now;
|
|
239
239
|
}
|
|
240
|
+
|
|
241
|
+
fn store_snapshot_time(&self, elapsed_seconds: f64) {
|
|
242
|
+
if let Some(time) = finite_absolute_time(self.offset_seconds, elapsed_seconds) {
|
|
243
|
+
self.snapshot
|
|
244
|
+
.time_bits
|
|
245
|
+
.store(time.to_bits(), Ordering::Release);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fn advance_timeline(
|
|
251
|
+
current_seconds: f64,
|
|
252
|
+
elapsed_seconds: f64,
|
|
253
|
+
playback_rate: f64,
|
|
254
|
+
duration_seconds: Option<f64>,
|
|
255
|
+
looped: bool,
|
|
256
|
+
) -> f64 {
|
|
257
|
+
let current_seconds = if current_seconds.is_finite() {
|
|
258
|
+
current_seconds.max(0.0)
|
|
259
|
+
} else {
|
|
260
|
+
0.0
|
|
261
|
+
};
|
|
262
|
+
let advanced_seconds = current_seconds + elapsed_seconds * playback_rate;
|
|
263
|
+
if !advanced_seconds.is_finite() {
|
|
264
|
+
return current_seconds;
|
|
265
|
+
}
|
|
266
|
+
match duration_seconds {
|
|
267
|
+
Some(duration) if looped => advanced_seconds % duration,
|
|
268
|
+
Some(duration) => advanced_seconds.min(duration),
|
|
269
|
+
None => advanced_seconds,
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn finite_absolute_time(offset_seconds: f64, elapsed_seconds: f64) -> Option<f64> {
|
|
274
|
+
let time = offset_seconds + elapsed_seconds;
|
|
275
|
+
time.is_finite().then_some(time)
|
|
240
276
|
}
|
|
241
277
|
|
|
242
278
|
enum EngineCommand {
|
|
@@ -507,7 +543,8 @@ impl NativeAudioEngine {
|
|
|
507
543
|
#[napi]
|
|
508
544
|
pub fn current_time(&self, id: u32) -> Option<f64> {
|
|
509
545
|
let snapshot = self.state.snapshots.lock().ok()?.get(&id).cloned()?;
|
|
510
|
-
|
|
546
|
+
let time = f64::from_bits(snapshot.time_bits.load(Ordering::Acquire));
|
|
547
|
+
time.is_finite().then_some(time)
|
|
511
548
|
}
|
|
512
549
|
|
|
513
550
|
#[napi]
|
|
@@ -917,11 +954,7 @@ impl EngineRuntime {
|
|
|
917
954
|
} else {
|
|
918
955
|
voice.timeline_seconds
|
|
919
956
|
};
|
|
920
|
-
|
|
921
|
-
voice
|
|
922
|
-
.snapshot
|
|
923
|
-
.time_bits
|
|
924
|
-
.store(time.to_bits(), Ordering::Release);
|
|
957
|
+
voice.store_snapshot_time(elapsed_seconds);
|
|
925
958
|
let current_volume = if voice.fade.is_some() {
|
|
926
959
|
sound.current_fade_volume()
|
|
927
960
|
} else {
|
|
@@ -1345,4 +1378,27 @@ mod tests {
|
|
|
1345
1378
|
"https://***:***@example.test/audio.mp3"
|
|
1346
1379
|
);
|
|
1347
1380
|
}
|
|
1381
|
+
|
|
1382
|
+
#[test]
|
|
1383
|
+
fn static_timeline_stays_finite_and_within_sprite_duration() {
|
|
1384
|
+
assert_eq!(advance_timeline(0.2, 0.05, 1.0, Some(0.8), false), 0.25);
|
|
1385
|
+
assert_eq!(advance_timeline(0.7, 0.2, 1.0, Some(0.8), false), 0.8);
|
|
1386
|
+
let looped = advance_timeline(0.7, 0.2, 1.0, Some(0.8), true);
|
|
1387
|
+
assert!((looped - 0.1).abs() < f64::EPSILON);
|
|
1388
|
+
assert_eq!(
|
|
1389
|
+
advance_timeline(f64::INFINITY, 0.05, 1.0, Some(0.8), false),
|
|
1390
|
+
0.05
|
|
1391
|
+
);
|
|
1392
|
+
assert_eq!(
|
|
1393
|
+
advance_timeline(0.2, f64::INFINITY, 1.0, Some(0.8), false),
|
|
1394
|
+
0.2
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
#[test]
|
|
1399
|
+
fn snapshot_time_rejects_non_finite_values() {
|
|
1400
|
+
assert_eq!(finite_absolute_time(1.0, 0.2), Some(1.2));
|
|
1401
|
+
assert_eq!(finite_absolute_time(0.0, f64::INFINITY), None);
|
|
1402
|
+
assert_eq!(finite_absolute_time(f64::INFINITY, 0.0), None);
|
|
1403
|
+
}
|
|
1348
1404
|
}
|
|
Binary file
|
|
@@ -19,11 +19,27 @@ export interface VideoFrame {
|
|
|
19
19
|
data: Uint8Array;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export interface VideoFrameInfo {
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
timestampUs: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface VideoShutdownDiagnostics {
|
|
29
|
+
activeDecoderWorkers: number;
|
|
30
|
+
pendingDecoderShutdowns: number;
|
|
31
|
+
completedDecoderShutdowns: number;
|
|
32
|
+
maxDecoderShutdownMs: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
export class NativeVideoDecoder {
|
|
23
36
|
public constructor(options: DecoderOptions);
|
|
24
37
|
public open(source: string): void;
|
|
25
38
|
public pollLatest(): VideoFrame | null;
|
|
26
39
|
public pollNext(): VideoFrame | null;
|
|
40
|
+
public supportsFrameBufferReuse(): boolean;
|
|
41
|
+
public pollLatestInto(target: Buffer): VideoFrameInfo | null;
|
|
42
|
+
public pollNextInto(target: Buffer): VideoFrameInfo | null;
|
|
27
43
|
public queuedFrames(): number;
|
|
28
44
|
public catchUpTo(timestampUs: number): void;
|
|
29
45
|
public pollError(): string | null;
|
|
@@ -31,6 +47,11 @@ export class NativeVideoDecoder {
|
|
|
31
47
|
public decodedFrames(): number;
|
|
32
48
|
public droppedFrames(): number;
|
|
33
49
|
public skippedFrames(): number;
|
|
50
|
+
public frameBufferAllocations(): number;
|
|
51
|
+
public frameBufferReuses(): number;
|
|
52
|
+
public recycledFrameBuffers(): number;
|
|
34
53
|
public isFinished(): boolean;
|
|
35
54
|
public close(): void;
|
|
36
55
|
}
|
|
56
|
+
|
|
57
|
+
export function videoShutdownDiagnostics(): VideoShutdownDiagnostics;
|
|
@@ -21,6 +21,18 @@ class NativeVideoDecoder {
|
|
|
21
21
|
return this.decoder.pollNext();
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
supportsFrameBufferReuse() {
|
|
25
|
+
return typeof this.decoder.pollNextInto === "function";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pollLatestInto(target) {
|
|
29
|
+
return this.pollIntoFallback("pollLatest", "pollLatestInto", target);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pollNextInto(target) {
|
|
33
|
+
return this.pollIntoFallback("pollNext", "pollNextInto", target);
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
queuedFrames() {
|
|
25
37
|
return this.decoder.queuedFrames();
|
|
26
38
|
}
|
|
@@ -49,6 +61,18 @@ class NativeVideoDecoder {
|
|
|
49
61
|
return this.decoder.skippedFrames();
|
|
50
62
|
}
|
|
51
63
|
|
|
64
|
+
frameBufferAllocations() {
|
|
65
|
+
return this.decoder.frameBufferAllocations?.() ?? 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
frameBufferReuses() {
|
|
69
|
+
return this.decoder.frameBufferReuses?.() ?? 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
recycledFrameBuffers() {
|
|
73
|
+
return this.decoder.recycledFrameBuffers?.() ?? 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
52
76
|
isFinished() {
|
|
53
77
|
return this.decoder.isFinished();
|
|
54
78
|
}
|
|
@@ -56,6 +80,34 @@ class NativeVideoDecoder {
|
|
|
56
80
|
close() {
|
|
57
81
|
this.decoder.close();
|
|
58
82
|
}
|
|
83
|
+
|
|
84
|
+
pollIntoFallback(pollName, pollIntoName, target) {
|
|
85
|
+
if (typeof this.decoder[pollIntoName] === "function") {
|
|
86
|
+
return this.decoder[pollIntoName](target);
|
|
87
|
+
}
|
|
88
|
+
const frame = this.decoder[pollName]();
|
|
89
|
+
if (!frame) return null;
|
|
90
|
+
if (target.byteLength !== frame.data.byteLength) {
|
|
91
|
+
throw new RangeError(
|
|
92
|
+
`NV12 target has ${target.byteLength} bytes; expected ${frame.data.byteLength}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
target.set(frame.data);
|
|
96
|
+
return {
|
|
97
|
+
width: frame.width,
|
|
98
|
+
height: frame.height,
|
|
99
|
+
timestampUs: frame.timestampUs,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function videoShutdownDiagnostics() {
|
|
105
|
+
return native.videoShutdownDiagnostics?.() ?? {
|
|
106
|
+
activeDecoderWorkers: 0,
|
|
107
|
+
pendingDecoderShutdowns: 0,
|
|
108
|
+
completedDecoderShutdowns: 0,
|
|
109
|
+
maxDecoderShutdownMs: 0,
|
|
110
|
+
};
|
|
59
111
|
}
|
|
60
112
|
|
|
61
|
-
module.exports = { NativeVideoDecoder };
|
|
113
|
+
module.exports = { NativeVideoDecoder, videoShutdownDiagnostics };
|
package/native/video/src/lib.rs
CHANGED
|
@@ -6,7 +6,7 @@ use std::process::{Child, ChildStdout, Command, Stdio};
|
|
|
6
6
|
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
|
|
7
7
|
use std::sync::{Arc, Condvar, Mutex};
|
|
8
8
|
use std::thread;
|
|
9
|
-
use std::time::Duration;
|
|
9
|
+
use std::time::{Duration, Instant};
|
|
10
10
|
|
|
11
11
|
use napi::bindgen_prelude::*;
|
|
12
12
|
use napi_derive::napi;
|
|
@@ -15,6 +15,11 @@ const FRAME_QUEUE_CAPACITY: usize = 4;
|
|
|
15
15
|
const HARDWARE_DECODE_ATTEMPTS: usize = 5;
|
|
16
16
|
const HARDWARE_RETRY_DELAY: Duration = Duration::from_millis(500);
|
|
17
17
|
|
|
18
|
+
static ACTIVE_DECODER_WORKERS: AtomicU64 = AtomicU64::new(0);
|
|
19
|
+
static PENDING_DECODER_SHUTDOWNS: AtomicU64 = AtomicU64::new(0);
|
|
20
|
+
static COMPLETED_DECODER_SHUTDOWNS: AtomicU64 = AtomicU64::new(0);
|
|
21
|
+
static MAX_DECODER_SHUTDOWN_MS: AtomicU64 = AtomicU64::new(0);
|
|
22
|
+
|
|
18
23
|
#[napi(object)]
|
|
19
24
|
pub struct DecoderOptions {
|
|
20
25
|
pub width: i64,
|
|
@@ -38,6 +43,31 @@ pub struct VideoFrame {
|
|
|
38
43
|
pub data: Buffer,
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
#[napi(object)]
|
|
47
|
+
pub struct VideoFrameInfo {
|
|
48
|
+
pub width: i64,
|
|
49
|
+
pub height: i64,
|
|
50
|
+
pub timestamp_us: i64,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#[napi(object)]
|
|
54
|
+
pub struct VideoShutdownDiagnostics {
|
|
55
|
+
pub active_decoder_workers: i64,
|
|
56
|
+
pub pending_decoder_shutdowns: i64,
|
|
57
|
+
pub completed_decoder_shutdowns: i64,
|
|
58
|
+
pub max_decoder_shutdown_ms: i64,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[napi]
|
|
62
|
+
pub fn video_shutdown_diagnostics() -> VideoShutdownDiagnostics {
|
|
63
|
+
VideoShutdownDiagnostics {
|
|
64
|
+
active_decoder_workers: atomic_i64(&ACTIVE_DECODER_WORKERS),
|
|
65
|
+
pending_decoder_shutdowns: atomic_i64(&PENDING_DECODER_SHUTDOWNS),
|
|
66
|
+
completed_decoder_shutdowns: atomic_i64(&COMPLETED_DECODER_SHUTDOWNS),
|
|
67
|
+
max_decoder_shutdown_ms: atomic_i64(&MAX_DECODER_SHUTDOWN_MS),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
41
71
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
42
72
|
enum DecoderBackend {
|
|
43
73
|
D3d11va,
|
|
@@ -84,7 +114,10 @@ struct DecoderState {
|
|
|
84
114
|
decoded_frames: Arc<AtomicU64>,
|
|
85
115
|
dropped_frames: Arc<AtomicU64>,
|
|
86
116
|
skipped_frames: Arc<AtomicU64>,
|
|
117
|
+
frame_buffer_allocations: Arc<AtomicU64>,
|
|
118
|
+
frame_buffer_reuses: Arc<AtomicU64>,
|
|
87
119
|
backend: Arc<Mutex<String>>,
|
|
120
|
+
stderr_workers: Arc<Mutex<Vec<thread::JoinHandle<()>>>>,
|
|
88
121
|
}
|
|
89
122
|
|
|
90
123
|
struct PendingFrame {
|
|
@@ -95,6 +128,7 @@ struct PendingFrame {
|
|
|
95
128
|
#[derive(Default)]
|
|
96
129
|
struct FrameQueue {
|
|
97
130
|
frames: VecDeque<PendingFrame>,
|
|
131
|
+
recycled: Vec<Vec<u8>>,
|
|
98
132
|
}
|
|
99
133
|
|
|
100
134
|
impl FrameQueue {
|
|
@@ -108,6 +142,19 @@ impl FrameQueue {
|
|
|
108
142
|
recycled
|
|
109
143
|
}
|
|
110
144
|
|
|
145
|
+
fn take_recycled(&mut self, frame_bytes: usize) -> Option<Vec<u8>> {
|
|
146
|
+
while let Some(buffer) = self.recycled.pop() {
|
|
147
|
+
if buffer.len() == frame_bytes {
|
|
148
|
+
return Some(buffer);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
None
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn recycle(&mut self, buffer: Vec<u8>) {
|
|
155
|
+
self.recycled.push(buffer);
|
|
156
|
+
}
|
|
157
|
+
|
|
111
158
|
fn push_back(&mut self, frame: PendingFrame) {
|
|
112
159
|
self.frames.push_back(frame);
|
|
113
160
|
}
|
|
@@ -119,7 +166,9 @@ impl FrameQueue {
|
|
|
119
166
|
fn pop_latest(&mut self) -> (Option<PendingFrame>, usize) {
|
|
120
167
|
let latest = self.frames.pop_back();
|
|
121
168
|
let skipped = self.frames.len();
|
|
122
|
-
self.frames.
|
|
169
|
+
while let Some(frame) = self.frames.pop_front() {
|
|
170
|
+
self.recycle(frame.data);
|
|
171
|
+
}
|
|
123
172
|
(latest, skipped)
|
|
124
173
|
}
|
|
125
174
|
|
|
@@ -127,14 +176,22 @@ impl FrameQueue {
|
|
|
127
176
|
self.frames.len()
|
|
128
177
|
}
|
|
129
178
|
|
|
179
|
+
fn recycle_queued(&mut self) {
|
|
180
|
+
while let Some(frame) = self.frames.pop_front() {
|
|
181
|
+
self.recycle(frame.data);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
130
185
|
fn clear(&mut self) {
|
|
131
186
|
self.frames.clear();
|
|
187
|
+
self.recycled.clear();
|
|
132
188
|
}
|
|
133
189
|
}
|
|
134
190
|
|
|
135
191
|
struct SpawnedFfmpeg {
|
|
136
192
|
child: Child,
|
|
137
193
|
stdout: ChildStdout,
|
|
194
|
+
stderr_worker: Option<thread::JoinHandle<()>>,
|
|
138
195
|
}
|
|
139
196
|
|
|
140
197
|
#[derive(Clone)]
|
|
@@ -155,6 +212,8 @@ struct FfmpegRequest {
|
|
|
155
212
|
pub struct NativeVideoDecoder {
|
|
156
213
|
options: DecoderOptions,
|
|
157
214
|
state: DecoderState,
|
|
215
|
+
worker: Option<thread::JoinHandle<()>>,
|
|
216
|
+
retired: bool,
|
|
158
217
|
}
|
|
159
218
|
|
|
160
219
|
#[napi]
|
|
@@ -171,6 +230,8 @@ impl NativeVideoDecoder {
|
|
|
171
230
|
|
|
172
231
|
Ok(Self {
|
|
173
232
|
options,
|
|
233
|
+
worker: None,
|
|
234
|
+
retired: false,
|
|
174
235
|
state: DecoderState {
|
|
175
236
|
closed: Arc::new(AtomicBool::new(true)),
|
|
176
237
|
finished: Arc::new(AtomicBool::new(false)),
|
|
@@ -182,13 +243,24 @@ impl NativeVideoDecoder {
|
|
|
182
243
|
decoded_frames: Arc::new(AtomicU64::new(0)),
|
|
183
244
|
dropped_frames: Arc::new(AtomicU64::new(0)),
|
|
184
245
|
skipped_frames: Arc::new(AtomicU64::new(0)),
|
|
246
|
+
frame_buffer_allocations: Arc::new(AtomicU64::new(0)),
|
|
247
|
+
frame_buffer_reuses: Arc::new(AtomicU64::new(0)),
|
|
185
248
|
backend: Arc::new(Mutex::new(backend)),
|
|
249
|
+
stderr_workers: Arc::new(Mutex::new(Vec::new())),
|
|
186
250
|
},
|
|
187
251
|
})
|
|
188
252
|
}
|
|
189
253
|
|
|
190
254
|
#[napi]
|
|
191
255
|
pub fn open(&mut self, source: String) -> Result<()> {
|
|
256
|
+
if self.retired {
|
|
257
|
+
return Err(Error::from_reason(
|
|
258
|
+
"Video decoder cannot reopen after shutdown",
|
|
259
|
+
));
|
|
260
|
+
}
|
|
261
|
+
if self.state.closed.load(Ordering::SeqCst) {
|
|
262
|
+
self.join_worker();
|
|
263
|
+
}
|
|
192
264
|
if !self.state.closed.swap(false, Ordering::SeqCst) {
|
|
193
265
|
return Err(Error::from_reason("Video decoder is already open"));
|
|
194
266
|
}
|
|
@@ -197,6 +269,10 @@ impl NativeVideoDecoder {
|
|
|
197
269
|
self.state.decoded_frames.store(0, Ordering::SeqCst);
|
|
198
270
|
self.state.dropped_frames.store(0, Ordering::SeqCst);
|
|
199
271
|
self.state.skipped_frames.store(0, Ordering::SeqCst);
|
|
272
|
+
self.state
|
|
273
|
+
.frame_buffer_allocations
|
|
274
|
+
.store(0, Ordering::SeqCst);
|
|
275
|
+
self.state.frame_buffer_reuses.store(0, Ordering::SeqCst);
|
|
200
276
|
|
|
201
277
|
if let Ok(mut error) = self.state.error.lock() {
|
|
202
278
|
*error = None;
|
|
@@ -268,7 +344,9 @@ impl NativeVideoDecoder {
|
|
|
268
344
|
let initial_stdout = install_child(&state, spawned)
|
|
269
345
|
.map_err(|error| Error::from_reason(error.to_string()))?;
|
|
270
346
|
|
|
271
|
-
|
|
347
|
+
ACTIVE_DECODER_WORKERS.fetch_add(1, Ordering::SeqCst);
|
|
348
|
+
self.worker = Some(thread::spawn(move || {
|
|
349
|
+
let _active_worker = ActiveDecoderWorker;
|
|
272
350
|
let mut result =
|
|
273
351
|
consume_decoder_attempt(initial_stdout, width, height, fps, start_time, &state);
|
|
274
352
|
|
|
@@ -334,7 +412,7 @@ impl NativeVideoDecoder {
|
|
|
334
412
|
state.finished.store(true, Ordering::SeqCst);
|
|
335
413
|
}
|
|
336
414
|
state.closed.store(true, Ordering::SeqCst);
|
|
337
|
-
});
|
|
415
|
+
}));
|
|
338
416
|
|
|
339
417
|
Ok(())
|
|
340
418
|
}
|
|
@@ -360,6 +438,41 @@ impl NativeVideoDecoder {
|
|
|
360
438
|
self.to_video_frame(pending)
|
|
361
439
|
}
|
|
362
440
|
|
|
441
|
+
#[napi]
|
|
442
|
+
pub fn poll_latest_into(&self, mut target: BufferSlice) -> Result<Option<VideoFrameInfo>> {
|
|
443
|
+
let (frames, available) = &*self.state.frames;
|
|
444
|
+
let (pending, skipped) = frames
|
|
445
|
+
.lock()
|
|
446
|
+
.map_err(|_| Error::from_reason("Video frame queue lock poisoned"))?
|
|
447
|
+
.pop_latest();
|
|
448
|
+
if skipped > 0 {
|
|
449
|
+
self.state
|
|
450
|
+
.skipped_frames
|
|
451
|
+
.fetch_add(skipped as u64, Ordering::SeqCst);
|
|
452
|
+
}
|
|
453
|
+
let result = pending
|
|
454
|
+
.map(|pending| self.copy_video_frame_into(pending, &mut target))
|
|
455
|
+
.transpose();
|
|
456
|
+
available.notify_all();
|
|
457
|
+
result
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
#[napi]
|
|
461
|
+
pub fn poll_next_into(&self, mut target: BufferSlice) -> Result<Option<VideoFrameInfo>> {
|
|
462
|
+
let (frames, available) = &*self.state.frames;
|
|
463
|
+
let pending = frames
|
|
464
|
+
.lock()
|
|
465
|
+
.map_err(|_| Error::from_reason("Video frame queue lock poisoned"))?
|
|
466
|
+
.pop_next();
|
|
467
|
+
let result = pending
|
|
468
|
+
.map(|pending| self.copy_video_frame_into(pending, &mut target))
|
|
469
|
+
.transpose();
|
|
470
|
+
if result.as_ref().is_ok_and(Option::is_some) {
|
|
471
|
+
available.notify_one();
|
|
472
|
+
}
|
|
473
|
+
result
|
|
474
|
+
}
|
|
475
|
+
|
|
363
476
|
#[napi]
|
|
364
477
|
pub fn queued_frames(&self) -> i64 {
|
|
365
478
|
let (frames, _) = &*self.state.frames;
|
|
@@ -383,7 +496,7 @@ impl NativeVideoDecoder {
|
|
|
383
496
|
let (frames, available) = &*self.state.frames;
|
|
384
497
|
if let Ok(mut frames) = frames.lock() {
|
|
385
498
|
let skipped = frames.len();
|
|
386
|
-
frames.
|
|
499
|
+
frames.recycle_queued();
|
|
387
500
|
self.state
|
|
388
501
|
.skipped_frames
|
|
389
502
|
.fetch_add(skipped as u64, Ordering::SeqCst);
|
|
@@ -430,6 +543,26 @@ impl NativeVideoDecoder {
|
|
|
430
543
|
i64::try_from(self.state.skipped_frames.load(Ordering::SeqCst)).unwrap_or(i64::MAX)
|
|
431
544
|
}
|
|
432
545
|
|
|
546
|
+
#[napi]
|
|
547
|
+
pub fn frame_buffer_allocations(&self) -> i64 {
|
|
548
|
+
i64::try_from(self.state.frame_buffer_allocations.load(Ordering::SeqCst))
|
|
549
|
+
.unwrap_or(i64::MAX)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
#[napi]
|
|
553
|
+
pub fn frame_buffer_reuses(&self) -> i64 {
|
|
554
|
+
i64::try_from(self.state.frame_buffer_reuses.load(Ordering::SeqCst)).unwrap_or(i64::MAX)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
#[napi]
|
|
558
|
+
pub fn recycled_frame_buffers(&self) -> i64 {
|
|
559
|
+
let (frames, _) = &*self.state.frames;
|
|
560
|
+
frames
|
|
561
|
+
.lock()
|
|
562
|
+
.map(|frames| i64::try_from(frames.recycled.len()).unwrap_or(i64::MAX))
|
|
563
|
+
.unwrap_or(0)
|
|
564
|
+
}
|
|
565
|
+
|
|
433
566
|
#[napi]
|
|
434
567
|
pub fn is_finished(&self) -> bool {
|
|
435
568
|
self.state.finished.load(Ordering::SeqCst)
|
|
@@ -437,13 +570,54 @@ impl NativeVideoDecoder {
|
|
|
437
570
|
|
|
438
571
|
#[napi]
|
|
439
572
|
pub fn close(&mut self) {
|
|
440
|
-
|
|
573
|
+
self.begin_shutdown();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
fn begin_shutdown(&mut self) {
|
|
577
|
+
if self.retired {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
self.retired = true;
|
|
581
|
+
let child = request_close_state(&self.state);
|
|
582
|
+
retire_decoder_cleanup(self.worker.take(), child);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
fn join_worker(&mut self) {
|
|
586
|
+
if let Some(worker) = self.worker.take() {
|
|
587
|
+
let _ = worker.join();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
fn copy_video_frame_into(
|
|
592
|
+
&self,
|
|
593
|
+
pending: PendingFrame,
|
|
594
|
+
target: &mut [u8],
|
|
595
|
+
) -> Result<VideoFrameInfo> {
|
|
596
|
+
if target.len() != pending.data.len() {
|
|
597
|
+
let actual = target.len();
|
|
598
|
+
let expected = pending.data.len();
|
|
599
|
+
if let Ok(mut frames) = self.state.frames.0.lock() {
|
|
600
|
+
frames.recycle(pending.data);
|
|
601
|
+
}
|
|
602
|
+
return Err(Error::from_reason(format!(
|
|
603
|
+
"NV12 target has {actual} bytes; expected {expected}"
|
|
604
|
+
)));
|
|
605
|
+
}
|
|
606
|
+
target.copy_from_slice(&pending.data);
|
|
607
|
+
if let Ok(mut frames) = self.state.frames.0.lock() {
|
|
608
|
+
frames.recycle(pending.data);
|
|
609
|
+
}
|
|
610
|
+
Ok(VideoFrameInfo {
|
|
611
|
+
width: self.options.width,
|
|
612
|
+
height: self.options.height,
|
|
613
|
+
timestamp_us: pending.timestamp_us,
|
|
614
|
+
})
|
|
441
615
|
}
|
|
442
616
|
}
|
|
443
617
|
|
|
444
618
|
impl Drop for NativeVideoDecoder {
|
|
445
619
|
fn drop(&mut self) {
|
|
446
|
-
|
|
620
|
+
self.begin_shutdown();
|
|
447
621
|
}
|
|
448
622
|
}
|
|
449
623
|
|
|
@@ -582,14 +756,18 @@ fn spawn_ffmpeg(request: &FfmpegRequest, backend: DecoderBackend) -> io::Result<
|
|
|
582
756
|
.stdout
|
|
583
757
|
.take()
|
|
584
758
|
.ok_or_else(|| io::Error::other("FFmpeg stdout unavailable"))?;
|
|
585
|
-
|
|
759
|
+
let stderr_worker = child.stderr.take().map(|stderr| {
|
|
586
760
|
thread::spawn(move || {
|
|
587
761
|
for line in BufReader::new(stderr).lines().map_while(|line| line.ok()) {
|
|
588
762
|
eprintln!("{}", redact_url_credentials(&line));
|
|
589
763
|
}
|
|
590
|
-
})
|
|
591
|
-
}
|
|
592
|
-
Ok(SpawnedFfmpeg {
|
|
764
|
+
})
|
|
765
|
+
});
|
|
766
|
+
Ok(SpawnedFfmpeg {
|
|
767
|
+
child,
|
|
768
|
+
stdout,
|
|
769
|
+
stderr_worker,
|
|
770
|
+
})
|
|
593
771
|
}
|
|
594
772
|
|
|
595
773
|
fn redact_url_credentials(value: &str) -> String {
|
|
@@ -612,11 +790,32 @@ fn redact_url_credentials(value: &str) -> String {
|
|
|
612
790
|
result
|
|
613
791
|
}
|
|
614
792
|
|
|
615
|
-
fn install_child(state: &DecoderState, spawned: SpawnedFfmpeg) -> io::Result<ChildStdout> {
|
|
616
|
-
|
|
793
|
+
fn install_child(state: &DecoderState, mut spawned: SpawnedFfmpeg) -> io::Result<ChildStdout> {
|
|
794
|
+
let mut child = state
|
|
617
795
|
.child
|
|
618
796
|
.lock()
|
|
619
|
-
.map_err(|_| io::Error::other("FFmpeg process lock poisoned"))
|
|
797
|
+
.map_err(|_| io::Error::other("FFmpeg process lock poisoned"))?;
|
|
798
|
+
if state.closed.load(Ordering::SeqCst) {
|
|
799
|
+
drop(child);
|
|
800
|
+
let _ = spawned.child.kill();
|
|
801
|
+
let _ = spawned.child.wait();
|
|
802
|
+
if let Some(worker) = spawned.stderr_worker {
|
|
803
|
+
let _ = worker.join();
|
|
804
|
+
}
|
|
805
|
+
return Err(io::Error::new(
|
|
806
|
+
io::ErrorKind::Interrupted,
|
|
807
|
+
"Video decoder closed before FFmpeg startup completed",
|
|
808
|
+
));
|
|
809
|
+
}
|
|
810
|
+
*child = Some(spawned.child);
|
|
811
|
+
drop(child);
|
|
812
|
+
if let Some(worker) = spawned.stderr_worker {
|
|
813
|
+
state
|
|
814
|
+
.stderr_workers
|
|
815
|
+
.lock()
|
|
816
|
+
.map_err(|_| io::Error::other("FFmpeg stderr worker lock poisoned"))?
|
|
817
|
+
.push(worker);
|
|
818
|
+
}
|
|
620
819
|
Ok(spawned.stdout)
|
|
621
820
|
}
|
|
622
821
|
|
|
@@ -631,7 +830,7 @@ fn consume_ffmpeg_output(
|
|
|
631
830
|
let frame_bytes = nv12_frame_bytes(width, height)?;
|
|
632
831
|
let start_timestamp_us = (start_time * 1_000_000.0).round() as i64;
|
|
633
832
|
let mut frame_index = 0_i64;
|
|
634
|
-
let mut data =
|
|
833
|
+
let mut data = acquire_frame_buffer(frame_bytes, state)?;
|
|
635
834
|
let read_result = loop {
|
|
636
835
|
if state.closed.load(Ordering::SeqCst) {
|
|
637
836
|
break Ok(());
|
|
@@ -644,20 +843,16 @@ fn consume_ffmpeg_output(
|
|
|
644
843
|
frame_index += 1;
|
|
645
844
|
state.decoded_frames.fetch_add(1, Ordering::SeqCst);
|
|
646
845
|
|
|
647
|
-
|
|
846
|
+
enqueue_frame(PendingFrame { timestamp_us, data }, state)?;
|
|
847
|
+
data = acquire_frame_buffer(frame_bytes, state)?;
|
|
648
848
|
}
|
|
649
849
|
Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => break Ok(()),
|
|
650
850
|
Err(error) => break Err(error),
|
|
651
851
|
}
|
|
652
852
|
};
|
|
653
853
|
|
|
654
|
-
let status = state
|
|
655
|
-
|
|
656
|
-
.lock()
|
|
657
|
-
.map_err(|_| io::Error::other("FFmpeg process lock poisoned"))?
|
|
658
|
-
.take()
|
|
659
|
-
.map(|mut process| process.wait())
|
|
660
|
-
.transpose()?;
|
|
854
|
+
let status = wait_for_child_exit(state)?;
|
|
855
|
+
join_stderr_workers(state);
|
|
661
856
|
|
|
662
857
|
read_result?;
|
|
663
858
|
if !state.closed.load(Ordering::SeqCst) && status.is_some_and(|status| !status.success()) {
|
|
@@ -709,15 +904,16 @@ fn wait_for_hardware_retry(state: &DecoderState) -> io::Result<bool> {
|
|
|
709
904
|
Ok(!state.closed.load(Ordering::SeqCst))
|
|
710
905
|
}
|
|
711
906
|
|
|
712
|
-
fn enqueue_frame(
|
|
713
|
-
frame: PendingFrame,
|
|
714
|
-
frame_bytes: usize,
|
|
715
|
-
state: &DecoderState,
|
|
716
|
-
) -> io::Result<Vec<u8>> {
|
|
907
|
+
fn enqueue_frame(frame: PendingFrame, state: &DecoderState) -> io::Result<()> {
|
|
717
908
|
let catch_up_timestamp_us = state.catch_up_timestamp_us.load(Ordering::SeqCst);
|
|
718
909
|
if catch_up_timestamp_us >= 0 && frame.timestamp_us < catch_up_timestamp_us {
|
|
719
910
|
state.skipped_frames.fetch_add(1, Ordering::SeqCst);
|
|
720
|
-
|
|
911
|
+
let (frames, _) = &*state.frames;
|
|
912
|
+
frames
|
|
913
|
+
.lock()
|
|
914
|
+
.map_err(|_| io::Error::other("Video frame queue lock poisoned"))?
|
|
915
|
+
.recycle(frame.data);
|
|
916
|
+
return Ok(());
|
|
721
917
|
}
|
|
722
918
|
if catch_up_timestamp_us >= 0 {
|
|
723
919
|
state.catch_up_timestamp_us.store(-1, Ordering::SeqCst);
|
|
@@ -731,7 +927,7 @@ fn enqueue_frame(
|
|
|
731
927
|
if state.source_paced {
|
|
732
928
|
if let Some(recycled) = frames.push_latest(frame) {
|
|
733
929
|
state.dropped_frames.fetch_add(1, Ordering::SeqCst);
|
|
734
|
-
|
|
930
|
+
frames.recycle(recycled);
|
|
735
931
|
}
|
|
736
932
|
} else {
|
|
737
933
|
while frames.len() >= FRAME_QUEUE_CAPACITY
|
|
@@ -743,12 +939,14 @@ fn enqueue_frame(
|
|
|
743
939
|
.map_err(|_| io::Error::other("Video frame queue lock poisoned"))?;
|
|
744
940
|
}
|
|
745
941
|
if state.closed.load(Ordering::SeqCst) {
|
|
746
|
-
|
|
942
|
+
frames.recycle(frame.data);
|
|
943
|
+
return Ok(());
|
|
747
944
|
}
|
|
748
945
|
let target = state.catch_up_timestamp_us.load(Ordering::SeqCst);
|
|
749
946
|
if target >= 0 && frame.timestamp_us < target {
|
|
750
947
|
state.skipped_frames.fetch_add(1, Ordering::SeqCst);
|
|
751
|
-
|
|
948
|
+
frames.recycle(frame.data);
|
|
949
|
+
return Ok(());
|
|
752
950
|
}
|
|
753
951
|
if target >= 0 {
|
|
754
952
|
state.catch_up_timestamp_us.store(-1, Ordering::SeqCst);
|
|
@@ -756,9 +954,36 @@ fn enqueue_frame(
|
|
|
756
954
|
frames.push_back(frame);
|
|
757
955
|
}
|
|
758
956
|
|
|
957
|
+
Ok(())
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
fn acquire_frame_buffer(frame_bytes: usize, state: &DecoderState) -> io::Result<Vec<u8>> {
|
|
961
|
+
let (frames, _) = &*state.frames;
|
|
962
|
+
if let Some(buffer) = frames
|
|
963
|
+
.lock()
|
|
964
|
+
.map_err(|_| io::Error::other("Video frame queue lock poisoned"))?
|
|
965
|
+
.take_recycled(frame_bytes)
|
|
966
|
+
{
|
|
967
|
+
state.frame_buffer_reuses.fetch_add(1, Ordering::SeqCst);
|
|
968
|
+
return Ok(buffer);
|
|
969
|
+
}
|
|
970
|
+
state
|
|
971
|
+
.frame_buffer_allocations
|
|
972
|
+
.fetch_add(1, Ordering::SeqCst);
|
|
759
973
|
Ok(vec![0_u8; frame_bytes])
|
|
760
974
|
}
|
|
761
975
|
|
|
976
|
+
fn join_stderr_workers(state: &DecoderState) {
|
|
977
|
+
let workers = state
|
|
978
|
+
.stderr_workers
|
|
979
|
+
.lock()
|
|
980
|
+
.map(|mut workers| workers.drain(..).collect::<Vec<_>>())
|
|
981
|
+
.unwrap_or_default();
|
|
982
|
+
for worker in workers {
|
|
983
|
+
let _ = worker.join();
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
762
987
|
fn set_backend_name(state: &DecoderState, name: &str) {
|
|
763
988
|
if let Ok(mut backend) = state.backend.lock() {
|
|
764
989
|
*backend = name.to_string();
|
|
@@ -772,18 +997,78 @@ fn store_error(state: &DecoderState, message: String) {
|
|
|
772
997
|
}
|
|
773
998
|
}
|
|
774
999
|
|
|
775
|
-
|
|
1000
|
+
/** Polls without holding the child mutex while the FFmpeg process exits. */
|
|
1001
|
+
fn wait_for_child_exit(state: &DecoderState) -> io::Result<Option<std::process::ExitStatus>> {
|
|
1002
|
+
loop {
|
|
1003
|
+
let status = {
|
|
1004
|
+
let mut child = state
|
|
1005
|
+
.child
|
|
1006
|
+
.lock()
|
|
1007
|
+
.map_err(|_| io::Error::other("FFmpeg process lock poisoned"))?;
|
|
1008
|
+
match child.as_mut() {
|
|
1009
|
+
Some(process) => process.try_wait()?,
|
|
1010
|
+
None => return Ok(None),
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
if let Some(status) = status {
|
|
1014
|
+
if let Ok(mut child) = state.child.lock() {
|
|
1015
|
+
child.take();
|
|
1016
|
+
}
|
|
1017
|
+
return Ok(Some(status));
|
|
1018
|
+
}
|
|
1019
|
+
if state.closed.load(Ordering::SeqCst) {
|
|
1020
|
+
return Ok(None);
|
|
1021
|
+
}
|
|
1022
|
+
thread::sleep(Duration::from_millis(10));
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** Signals decoder shutdown without waiting on FFmpeg or worker threads. */
|
|
1027
|
+
fn request_close_state(state: &DecoderState) -> Option<Child> {
|
|
776
1028
|
state.closed.store(true, Ordering::SeqCst);
|
|
777
1029
|
let (frames, available) = &*state.frames;
|
|
778
1030
|
available.notify_all();
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
1031
|
+
let mut child = state.child.lock().ok().and_then(|mut child| child.take());
|
|
1032
|
+
if let Some(process) = child.as_mut() {
|
|
1033
|
+
let _ = process.kill();
|
|
783
1034
|
}
|
|
784
1035
|
if let Ok(mut frames) = frames.lock() {
|
|
785
1036
|
frames.clear();
|
|
786
1037
|
}
|
|
1038
|
+
child
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/** Reaps the killed process and joins workers away from the N-API thread. */
|
|
1042
|
+
fn retire_decoder_cleanup(worker: Option<thread::JoinHandle<()>>, child: Option<Child>) {
|
|
1043
|
+
if worker.is_none() && child.is_none() {
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
PENDING_DECODER_SHUTDOWNS.fetch_add(1, Ordering::SeqCst);
|
|
1047
|
+
thread::spawn(move || {
|
|
1048
|
+
let started = Instant::now();
|
|
1049
|
+
if let Some(mut process) = child {
|
|
1050
|
+
let _ = process.wait();
|
|
1051
|
+
}
|
|
1052
|
+
if let Some(worker) = worker {
|
|
1053
|
+
let _ = worker.join();
|
|
1054
|
+
}
|
|
1055
|
+
let elapsed_ms = u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX);
|
|
1056
|
+
MAX_DECODER_SHUTDOWN_MS.fetch_max(elapsed_ms, Ordering::SeqCst);
|
|
1057
|
+
COMPLETED_DECODER_SHUTDOWNS.fetch_add(1, Ordering::SeqCst);
|
|
1058
|
+
PENDING_DECODER_SHUTDOWNS.fetch_sub(1, Ordering::SeqCst);
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
fn atomic_i64(value: &AtomicU64) -> i64 {
|
|
1063
|
+
i64::try_from(value.load(Ordering::SeqCst)).unwrap_or(i64::MAX)
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
struct ActiveDecoderWorker;
|
|
1067
|
+
|
|
1068
|
+
impl Drop for ActiveDecoderWorker {
|
|
1069
|
+
fn drop(&mut self) {
|
|
1070
|
+
ACTIVE_DECODER_WORKERS.fetch_sub(1, Ordering::SeqCst);
|
|
1071
|
+
}
|
|
787
1072
|
}
|
|
788
1073
|
|
|
789
1074
|
#[cfg(test)]
|
|
@@ -906,13 +1191,13 @@ mod tests {
|
|
|
906
1191
|
fn file_queue_applies_backpressure_until_a_frame_is_consumed() {
|
|
907
1192
|
let state = decoder_state(false);
|
|
908
1193
|
for timestamp_us in 0..FRAME_QUEUE_CAPACITY as i64 {
|
|
909
|
-
enqueue_frame(pending(timestamp_us),
|
|
1194
|
+
enqueue_frame(pending(timestamp_us), &state).unwrap();
|
|
910
1195
|
}
|
|
911
1196
|
|
|
912
1197
|
let producer_state = state.clone();
|
|
913
1198
|
let (sent, received) = mpsc::channel();
|
|
914
1199
|
thread::spawn(move || {
|
|
915
|
-
let result = enqueue_frame(pending(99),
|
|
1200
|
+
let result = enqueue_frame(pending(99), &producer_state);
|
|
916
1201
|
sent.send(result.is_ok()).unwrap();
|
|
917
1202
|
});
|
|
918
1203
|
|
|
@@ -928,14 +1213,110 @@ mod tests {
|
|
|
928
1213
|
let state = decoder_state(false);
|
|
929
1214
|
state.catch_up_timestamp_us.store(30, Ordering::SeqCst);
|
|
930
1215
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
enqueue_frame(pending(30),
|
|
1216
|
+
enqueue_frame(pending(10), &state).unwrap();
|
|
1217
|
+
enqueue_frame(pending(20), &state).unwrap();
|
|
1218
|
+
enqueue_frame(pending(30), &state).unwrap();
|
|
934
1219
|
|
|
935
1220
|
assert_eq!(state.skipped_frames.load(Ordering::SeqCst), 2);
|
|
936
1221
|
let (frames, _) = &*state.frames;
|
|
937
1222
|
let mut frames = frames.lock().unwrap();
|
|
938
1223
|
assert_eq!(frames.pop_next().unwrap().timestamp_us, 30);
|
|
1224
|
+
assert_eq!(frames.recycled.len(), 2);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
#[test]
|
|
1228
|
+
fn copies_frame_into_caller_buffer_and_recycles_native_storage() {
|
|
1229
|
+
let decoder = NativeVideoDecoder::new(DecoderOptions {
|
|
1230
|
+
width: 2,
|
|
1231
|
+
height: 2,
|
|
1232
|
+
fps: Some(30.0),
|
|
1233
|
+
start_time: None,
|
|
1234
|
+
ffmpeg_path: None,
|
|
1235
|
+
vaapi_device: None,
|
|
1236
|
+
playback_rate: None,
|
|
1237
|
+
end_time: None,
|
|
1238
|
+
source_paced: None,
|
|
1239
|
+
input_args: None,
|
|
1240
|
+
output_args: None,
|
|
1241
|
+
})
|
|
1242
|
+
.unwrap();
|
|
1243
|
+
let mut target = vec![0; 6];
|
|
1244
|
+
let info = decoder
|
|
1245
|
+
.copy_video_frame_into(
|
|
1246
|
+
PendingFrame {
|
|
1247
|
+
timestamp_us: 42,
|
|
1248
|
+
data: vec![1, 2, 3, 4, 5, 6],
|
|
1249
|
+
},
|
|
1250
|
+
&mut target,
|
|
1251
|
+
)
|
|
1252
|
+
.unwrap();
|
|
1253
|
+
|
|
1254
|
+
assert_eq!(target, vec![1, 2, 3, 4, 5, 6]);
|
|
1255
|
+
assert_eq!(info.timestamp_us, 42);
|
|
1256
|
+
assert_eq!(decoder.state.frames.0.lock().unwrap().recycled.len(), 1);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
#[test]
|
|
1260
|
+
fn rejects_wrong_caller_buffer_size_without_losing_native_storage() {
|
|
1261
|
+
let decoder = NativeVideoDecoder::new(DecoderOptions {
|
|
1262
|
+
width: 2,
|
|
1263
|
+
height: 2,
|
|
1264
|
+
fps: Some(30.0),
|
|
1265
|
+
start_time: None,
|
|
1266
|
+
ffmpeg_path: None,
|
|
1267
|
+
vaapi_device: None,
|
|
1268
|
+
playback_rate: None,
|
|
1269
|
+
end_time: None,
|
|
1270
|
+
source_paced: None,
|
|
1271
|
+
input_args: None,
|
|
1272
|
+
output_args: None,
|
|
1273
|
+
})
|
|
1274
|
+
.unwrap();
|
|
1275
|
+
let mut target = vec![0; 5];
|
|
1276
|
+
let result = decoder.copy_video_frame_into(
|
|
1277
|
+
PendingFrame {
|
|
1278
|
+
timestamp_us: 42,
|
|
1279
|
+
data: vec![1, 2, 3, 4, 5, 6],
|
|
1280
|
+
},
|
|
1281
|
+
&mut target,
|
|
1282
|
+
);
|
|
1283
|
+
|
|
1284
|
+
assert!(result.is_err());
|
|
1285
|
+
assert_eq!(decoder.state.frames.0.lock().unwrap().recycled.len(), 1);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
#[test]
|
|
1289
|
+
fn close_retires_a_blocked_worker_without_waiting_for_it() {
|
|
1290
|
+
let mut decoder = test_decoder();
|
|
1291
|
+
let (started_tx, started_rx) = mpsc::channel();
|
|
1292
|
+
let (release_tx, release_rx) = mpsc::channel();
|
|
1293
|
+
decoder.worker = Some(thread::spawn(move || {
|
|
1294
|
+
started_tx.send(()).unwrap();
|
|
1295
|
+
release_rx.recv().unwrap();
|
|
1296
|
+
}));
|
|
1297
|
+
started_rx.recv_timeout(Duration::from_secs(1)).unwrap();
|
|
1298
|
+
let completed_before = COMPLETED_DECODER_SHUTDOWNS.load(Ordering::SeqCst);
|
|
1299
|
+
|
|
1300
|
+
let started = Instant::now();
|
|
1301
|
+
decoder.close();
|
|
1302
|
+
assert!(started.elapsed() < Duration::from_millis(100));
|
|
1303
|
+
assert!(PENDING_DECODER_SHUTDOWNS.load(Ordering::SeqCst) >= 1);
|
|
1304
|
+
|
|
1305
|
+
release_tx.send(()).unwrap();
|
|
1306
|
+
let deadline = Instant::now() + Duration::from_secs(1);
|
|
1307
|
+
while COMPLETED_DECODER_SHUTDOWNS.load(Ordering::SeqCst) == completed_before {
|
|
1308
|
+
assert!(Instant::now() < deadline);
|
|
1309
|
+
thread::sleep(Duration::from_millis(5));
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
#[test]
|
|
1314
|
+
fn explicit_close_makes_the_decoder_terminal_without_blocking_reopen() {
|
|
1315
|
+
let mut decoder = test_decoder();
|
|
1316
|
+
decoder.close();
|
|
1317
|
+
|
|
1318
|
+
let error = decoder.open("unused.mp4".to_string()).unwrap_err();
|
|
1319
|
+
assert!(error.to_string().contains("cannot reopen after shutdown"));
|
|
939
1320
|
}
|
|
940
1321
|
|
|
941
1322
|
#[test]
|
|
@@ -961,6 +1342,23 @@ mod tests {
|
|
|
961
1342
|
}
|
|
962
1343
|
}
|
|
963
1344
|
|
|
1345
|
+
fn test_decoder() -> NativeVideoDecoder {
|
|
1346
|
+
NativeVideoDecoder::new(DecoderOptions {
|
|
1347
|
+
width: 2,
|
|
1348
|
+
height: 2,
|
|
1349
|
+
fps: Some(30.0),
|
|
1350
|
+
start_time: None,
|
|
1351
|
+
ffmpeg_path: None,
|
|
1352
|
+
vaapi_device: None,
|
|
1353
|
+
playback_rate: None,
|
|
1354
|
+
end_time: None,
|
|
1355
|
+
source_paced: None,
|
|
1356
|
+
input_args: None,
|
|
1357
|
+
output_args: None,
|
|
1358
|
+
})
|
|
1359
|
+
.unwrap()
|
|
1360
|
+
}
|
|
1361
|
+
|
|
964
1362
|
fn decoder_state(source_paced: bool) -> DecoderState {
|
|
965
1363
|
DecoderState {
|
|
966
1364
|
closed: Arc::new(AtomicBool::new(false)),
|
|
@@ -973,7 +1371,10 @@ mod tests {
|
|
|
973
1371
|
decoded_frames: Arc::new(AtomicU64::new(0)),
|
|
974
1372
|
dropped_frames: Arc::new(AtomicU64::new(0)),
|
|
975
1373
|
skipped_frames: Arc::new(AtomicU64::new(0)),
|
|
1374
|
+
frame_buffer_allocations: Arc::new(AtomicU64::new(0)),
|
|
1375
|
+
frame_buffer_reuses: Arc::new(AtomicU64::new(0)),
|
|
976
1376
|
backend: Arc::new(Mutex::new("test".to_string())),
|
|
1377
|
+
stderr_workers: Arc::new(Mutex::new(Vec::new())),
|
|
977
1378
|
}
|
|
978
1379
|
}
|
|
979
1380
|
|