@matjash/pixi-native-win32-x64 0.2.1 → 0.2.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.
|
@@ -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,20 @@ 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
|
+
|
|
22
28
|
export class NativeVideoDecoder {
|
|
23
29
|
public constructor(options: DecoderOptions);
|
|
24
30
|
public open(source: string): void;
|
|
25
31
|
public pollLatest(): VideoFrame | null;
|
|
26
32
|
public pollNext(): VideoFrame | null;
|
|
33
|
+
public supportsFrameBufferReuse(): boolean;
|
|
34
|
+
public pollLatestInto(target: Buffer): VideoFrameInfo | null;
|
|
35
|
+
public pollNextInto(target: Buffer): VideoFrameInfo | null;
|
|
27
36
|
public queuedFrames(): number;
|
|
28
37
|
public catchUpTo(timestampUs: number): void;
|
|
29
38
|
public pollError(): string | null;
|
|
@@ -31,6 +40,9 @@ export class NativeVideoDecoder {
|
|
|
31
40
|
public decodedFrames(): number;
|
|
32
41
|
public droppedFrames(): number;
|
|
33
42
|
public skippedFrames(): number;
|
|
43
|
+
public frameBufferAllocations(): number;
|
|
44
|
+
public frameBufferReuses(): number;
|
|
45
|
+
public recycledFrameBuffers(): number;
|
|
34
46
|
public isFinished(): boolean;
|
|
35
47
|
public close(): void;
|
|
36
48
|
}
|
|
@@ -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,25 @@ 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
|
+
}
|
|
59
102
|
}
|
|
60
103
|
|
|
61
104
|
module.exports = { NativeVideoDecoder };
|
package/native/video/src/lib.rs
CHANGED
|
@@ -38,6 +38,13 @@ pub struct VideoFrame {
|
|
|
38
38
|
pub data: Buffer,
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
#[napi(object)]
|
|
42
|
+
pub struct VideoFrameInfo {
|
|
43
|
+
pub width: i64,
|
|
44
|
+
pub height: i64,
|
|
45
|
+
pub timestamp_us: i64,
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
42
49
|
enum DecoderBackend {
|
|
43
50
|
D3d11va,
|
|
@@ -84,7 +91,10 @@ struct DecoderState {
|
|
|
84
91
|
decoded_frames: Arc<AtomicU64>,
|
|
85
92
|
dropped_frames: Arc<AtomicU64>,
|
|
86
93
|
skipped_frames: Arc<AtomicU64>,
|
|
94
|
+
frame_buffer_allocations: Arc<AtomicU64>,
|
|
95
|
+
frame_buffer_reuses: Arc<AtomicU64>,
|
|
87
96
|
backend: Arc<Mutex<String>>,
|
|
97
|
+
stderr_workers: Arc<Mutex<Vec<thread::JoinHandle<()>>>>,
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
struct PendingFrame {
|
|
@@ -95,6 +105,7 @@ struct PendingFrame {
|
|
|
95
105
|
#[derive(Default)]
|
|
96
106
|
struct FrameQueue {
|
|
97
107
|
frames: VecDeque<PendingFrame>,
|
|
108
|
+
recycled: Vec<Vec<u8>>,
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
impl FrameQueue {
|
|
@@ -108,6 +119,19 @@ impl FrameQueue {
|
|
|
108
119
|
recycled
|
|
109
120
|
}
|
|
110
121
|
|
|
122
|
+
fn take_recycled(&mut self, frame_bytes: usize) -> Option<Vec<u8>> {
|
|
123
|
+
while let Some(buffer) = self.recycled.pop() {
|
|
124
|
+
if buffer.len() == frame_bytes {
|
|
125
|
+
return Some(buffer);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
None
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fn recycle(&mut self, buffer: Vec<u8>) {
|
|
132
|
+
self.recycled.push(buffer);
|
|
133
|
+
}
|
|
134
|
+
|
|
111
135
|
fn push_back(&mut self, frame: PendingFrame) {
|
|
112
136
|
self.frames.push_back(frame);
|
|
113
137
|
}
|
|
@@ -119,7 +143,9 @@ impl FrameQueue {
|
|
|
119
143
|
fn pop_latest(&mut self) -> (Option<PendingFrame>, usize) {
|
|
120
144
|
let latest = self.frames.pop_back();
|
|
121
145
|
let skipped = self.frames.len();
|
|
122
|
-
self.frames.
|
|
146
|
+
while let Some(frame) = self.frames.pop_front() {
|
|
147
|
+
self.recycle(frame.data);
|
|
148
|
+
}
|
|
123
149
|
(latest, skipped)
|
|
124
150
|
}
|
|
125
151
|
|
|
@@ -127,14 +153,22 @@ impl FrameQueue {
|
|
|
127
153
|
self.frames.len()
|
|
128
154
|
}
|
|
129
155
|
|
|
156
|
+
fn recycle_queued(&mut self) {
|
|
157
|
+
while let Some(frame) = self.frames.pop_front() {
|
|
158
|
+
self.recycle(frame.data);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
130
162
|
fn clear(&mut self) {
|
|
131
163
|
self.frames.clear();
|
|
164
|
+
self.recycled.clear();
|
|
132
165
|
}
|
|
133
166
|
}
|
|
134
167
|
|
|
135
168
|
struct SpawnedFfmpeg {
|
|
136
169
|
child: Child,
|
|
137
170
|
stdout: ChildStdout,
|
|
171
|
+
stderr_worker: Option<thread::JoinHandle<()>>,
|
|
138
172
|
}
|
|
139
173
|
|
|
140
174
|
#[derive(Clone)]
|
|
@@ -155,6 +189,7 @@ struct FfmpegRequest {
|
|
|
155
189
|
pub struct NativeVideoDecoder {
|
|
156
190
|
options: DecoderOptions,
|
|
157
191
|
state: DecoderState,
|
|
192
|
+
worker: Option<thread::JoinHandle<()>>,
|
|
158
193
|
}
|
|
159
194
|
|
|
160
195
|
#[napi]
|
|
@@ -171,6 +206,7 @@ impl NativeVideoDecoder {
|
|
|
171
206
|
|
|
172
207
|
Ok(Self {
|
|
173
208
|
options,
|
|
209
|
+
worker: None,
|
|
174
210
|
state: DecoderState {
|
|
175
211
|
closed: Arc::new(AtomicBool::new(true)),
|
|
176
212
|
finished: Arc::new(AtomicBool::new(false)),
|
|
@@ -182,13 +218,19 @@ impl NativeVideoDecoder {
|
|
|
182
218
|
decoded_frames: Arc::new(AtomicU64::new(0)),
|
|
183
219
|
dropped_frames: Arc::new(AtomicU64::new(0)),
|
|
184
220
|
skipped_frames: Arc::new(AtomicU64::new(0)),
|
|
221
|
+
frame_buffer_allocations: Arc::new(AtomicU64::new(0)),
|
|
222
|
+
frame_buffer_reuses: Arc::new(AtomicU64::new(0)),
|
|
185
223
|
backend: Arc::new(Mutex::new(backend)),
|
|
224
|
+
stderr_workers: Arc::new(Mutex::new(Vec::new())),
|
|
186
225
|
},
|
|
187
226
|
})
|
|
188
227
|
}
|
|
189
228
|
|
|
190
229
|
#[napi]
|
|
191
230
|
pub fn open(&mut self, source: String) -> Result<()> {
|
|
231
|
+
if self.state.closed.load(Ordering::SeqCst) {
|
|
232
|
+
self.join_worker();
|
|
233
|
+
}
|
|
192
234
|
if !self.state.closed.swap(false, Ordering::SeqCst) {
|
|
193
235
|
return Err(Error::from_reason("Video decoder is already open"));
|
|
194
236
|
}
|
|
@@ -197,6 +239,10 @@ impl NativeVideoDecoder {
|
|
|
197
239
|
self.state.decoded_frames.store(0, Ordering::SeqCst);
|
|
198
240
|
self.state.dropped_frames.store(0, Ordering::SeqCst);
|
|
199
241
|
self.state.skipped_frames.store(0, Ordering::SeqCst);
|
|
242
|
+
self.state
|
|
243
|
+
.frame_buffer_allocations
|
|
244
|
+
.store(0, Ordering::SeqCst);
|
|
245
|
+
self.state.frame_buffer_reuses.store(0, Ordering::SeqCst);
|
|
200
246
|
|
|
201
247
|
if let Ok(mut error) = self.state.error.lock() {
|
|
202
248
|
*error = None;
|
|
@@ -268,7 +314,7 @@ impl NativeVideoDecoder {
|
|
|
268
314
|
let initial_stdout = install_child(&state, spawned)
|
|
269
315
|
.map_err(|error| Error::from_reason(error.to_string()))?;
|
|
270
316
|
|
|
271
|
-
thread::spawn(move || {
|
|
317
|
+
self.worker = Some(thread::spawn(move || {
|
|
272
318
|
let mut result =
|
|
273
319
|
consume_decoder_attempt(initial_stdout, width, height, fps, start_time, &state);
|
|
274
320
|
|
|
@@ -334,7 +380,7 @@ impl NativeVideoDecoder {
|
|
|
334
380
|
state.finished.store(true, Ordering::SeqCst);
|
|
335
381
|
}
|
|
336
382
|
state.closed.store(true, Ordering::SeqCst);
|
|
337
|
-
});
|
|
383
|
+
}));
|
|
338
384
|
|
|
339
385
|
Ok(())
|
|
340
386
|
}
|
|
@@ -360,6 +406,41 @@ impl NativeVideoDecoder {
|
|
|
360
406
|
self.to_video_frame(pending)
|
|
361
407
|
}
|
|
362
408
|
|
|
409
|
+
#[napi]
|
|
410
|
+
pub fn poll_latest_into(&self, mut target: BufferSlice) -> Result<Option<VideoFrameInfo>> {
|
|
411
|
+
let (frames, available) = &*self.state.frames;
|
|
412
|
+
let (pending, skipped) = frames
|
|
413
|
+
.lock()
|
|
414
|
+
.map_err(|_| Error::from_reason("Video frame queue lock poisoned"))?
|
|
415
|
+
.pop_latest();
|
|
416
|
+
if skipped > 0 {
|
|
417
|
+
self.state
|
|
418
|
+
.skipped_frames
|
|
419
|
+
.fetch_add(skipped as u64, Ordering::SeqCst);
|
|
420
|
+
}
|
|
421
|
+
let result = pending
|
|
422
|
+
.map(|pending| self.copy_video_frame_into(pending, &mut target))
|
|
423
|
+
.transpose();
|
|
424
|
+
available.notify_all();
|
|
425
|
+
result
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#[napi]
|
|
429
|
+
pub fn poll_next_into(&self, mut target: BufferSlice) -> Result<Option<VideoFrameInfo>> {
|
|
430
|
+
let (frames, available) = &*self.state.frames;
|
|
431
|
+
let pending = frames
|
|
432
|
+
.lock()
|
|
433
|
+
.map_err(|_| Error::from_reason("Video frame queue lock poisoned"))?
|
|
434
|
+
.pop_next();
|
|
435
|
+
let result = pending
|
|
436
|
+
.map(|pending| self.copy_video_frame_into(pending, &mut target))
|
|
437
|
+
.transpose();
|
|
438
|
+
if result.as_ref().is_ok_and(Option::is_some) {
|
|
439
|
+
available.notify_one();
|
|
440
|
+
}
|
|
441
|
+
result
|
|
442
|
+
}
|
|
443
|
+
|
|
363
444
|
#[napi]
|
|
364
445
|
pub fn queued_frames(&self) -> i64 {
|
|
365
446
|
let (frames, _) = &*self.state.frames;
|
|
@@ -383,7 +464,7 @@ impl NativeVideoDecoder {
|
|
|
383
464
|
let (frames, available) = &*self.state.frames;
|
|
384
465
|
if let Ok(mut frames) = frames.lock() {
|
|
385
466
|
let skipped = frames.len();
|
|
386
|
-
frames.
|
|
467
|
+
frames.recycle_queued();
|
|
387
468
|
self.state
|
|
388
469
|
.skipped_frames
|
|
389
470
|
.fetch_add(skipped as u64, Ordering::SeqCst);
|
|
@@ -430,6 +511,26 @@ impl NativeVideoDecoder {
|
|
|
430
511
|
i64::try_from(self.state.skipped_frames.load(Ordering::SeqCst)).unwrap_or(i64::MAX)
|
|
431
512
|
}
|
|
432
513
|
|
|
514
|
+
#[napi]
|
|
515
|
+
pub fn frame_buffer_allocations(&self) -> i64 {
|
|
516
|
+
i64::try_from(self.state.frame_buffer_allocations.load(Ordering::SeqCst))
|
|
517
|
+
.unwrap_or(i64::MAX)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
#[napi]
|
|
521
|
+
pub fn frame_buffer_reuses(&self) -> i64 {
|
|
522
|
+
i64::try_from(self.state.frame_buffer_reuses.load(Ordering::SeqCst)).unwrap_or(i64::MAX)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#[napi]
|
|
526
|
+
pub fn recycled_frame_buffers(&self) -> i64 {
|
|
527
|
+
let (frames, _) = &*self.state.frames;
|
|
528
|
+
frames
|
|
529
|
+
.lock()
|
|
530
|
+
.map(|frames| i64::try_from(frames.recycled.len()).unwrap_or(i64::MAX))
|
|
531
|
+
.unwrap_or(0)
|
|
532
|
+
}
|
|
533
|
+
|
|
433
534
|
#[napi]
|
|
434
535
|
pub fn is_finished(&self) -> bool {
|
|
435
536
|
self.state.finished.load(Ordering::SeqCst)
|
|
@@ -438,12 +539,46 @@ impl NativeVideoDecoder {
|
|
|
438
539
|
#[napi]
|
|
439
540
|
pub fn close(&mut self) {
|
|
440
541
|
close_state(&self.state);
|
|
542
|
+
self.join_worker();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
fn join_worker(&mut self) {
|
|
546
|
+
if let Some(worker) = self.worker.take() {
|
|
547
|
+
let _ = worker.join();
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
fn copy_video_frame_into(
|
|
552
|
+
&self,
|
|
553
|
+
pending: PendingFrame,
|
|
554
|
+
target: &mut [u8],
|
|
555
|
+
) -> Result<VideoFrameInfo> {
|
|
556
|
+
if target.len() != pending.data.len() {
|
|
557
|
+
let actual = target.len();
|
|
558
|
+
let expected = pending.data.len();
|
|
559
|
+
if let Ok(mut frames) = self.state.frames.0.lock() {
|
|
560
|
+
frames.recycle(pending.data);
|
|
561
|
+
}
|
|
562
|
+
return Err(Error::from_reason(format!(
|
|
563
|
+
"NV12 target has {actual} bytes; expected {expected}"
|
|
564
|
+
)));
|
|
565
|
+
}
|
|
566
|
+
target.copy_from_slice(&pending.data);
|
|
567
|
+
if let Ok(mut frames) = self.state.frames.0.lock() {
|
|
568
|
+
frames.recycle(pending.data);
|
|
569
|
+
}
|
|
570
|
+
Ok(VideoFrameInfo {
|
|
571
|
+
width: self.options.width,
|
|
572
|
+
height: self.options.height,
|
|
573
|
+
timestamp_us: pending.timestamp_us,
|
|
574
|
+
})
|
|
441
575
|
}
|
|
442
576
|
}
|
|
443
577
|
|
|
444
578
|
impl Drop for NativeVideoDecoder {
|
|
445
579
|
fn drop(&mut self) {
|
|
446
580
|
close_state(&self.state);
|
|
581
|
+
self.join_worker();
|
|
447
582
|
}
|
|
448
583
|
}
|
|
449
584
|
|
|
@@ -582,14 +717,18 @@ fn spawn_ffmpeg(request: &FfmpegRequest, backend: DecoderBackend) -> io::Result<
|
|
|
582
717
|
.stdout
|
|
583
718
|
.take()
|
|
584
719
|
.ok_or_else(|| io::Error::other("FFmpeg stdout unavailable"))?;
|
|
585
|
-
|
|
720
|
+
let stderr_worker = child.stderr.take().map(|stderr| {
|
|
586
721
|
thread::spawn(move || {
|
|
587
722
|
for line in BufReader::new(stderr).lines().map_while(|line| line.ok()) {
|
|
588
723
|
eprintln!("{}", redact_url_credentials(&line));
|
|
589
724
|
}
|
|
590
|
-
})
|
|
591
|
-
}
|
|
592
|
-
Ok(SpawnedFfmpeg {
|
|
725
|
+
})
|
|
726
|
+
});
|
|
727
|
+
Ok(SpawnedFfmpeg {
|
|
728
|
+
child,
|
|
729
|
+
stdout,
|
|
730
|
+
stderr_worker,
|
|
731
|
+
})
|
|
593
732
|
}
|
|
594
733
|
|
|
595
734
|
fn redact_url_credentials(value: &str) -> String {
|
|
@@ -613,6 +752,13 @@ fn redact_url_credentials(value: &str) -> String {
|
|
|
613
752
|
}
|
|
614
753
|
|
|
615
754
|
fn install_child(state: &DecoderState, spawned: SpawnedFfmpeg) -> io::Result<ChildStdout> {
|
|
755
|
+
if let Some(worker) = spawned.stderr_worker {
|
|
756
|
+
state
|
|
757
|
+
.stderr_workers
|
|
758
|
+
.lock()
|
|
759
|
+
.map_err(|_| io::Error::other("FFmpeg stderr worker lock poisoned"))?
|
|
760
|
+
.push(worker);
|
|
761
|
+
}
|
|
616
762
|
*state
|
|
617
763
|
.child
|
|
618
764
|
.lock()
|
|
@@ -631,7 +777,7 @@ fn consume_ffmpeg_output(
|
|
|
631
777
|
let frame_bytes = nv12_frame_bytes(width, height)?;
|
|
632
778
|
let start_timestamp_us = (start_time * 1_000_000.0).round() as i64;
|
|
633
779
|
let mut frame_index = 0_i64;
|
|
634
|
-
let mut data =
|
|
780
|
+
let mut data = acquire_frame_buffer(frame_bytes, state)?;
|
|
635
781
|
let read_result = loop {
|
|
636
782
|
if state.closed.load(Ordering::SeqCst) {
|
|
637
783
|
break Ok(());
|
|
@@ -644,7 +790,8 @@ fn consume_ffmpeg_output(
|
|
|
644
790
|
frame_index += 1;
|
|
645
791
|
state.decoded_frames.fetch_add(1, Ordering::SeqCst);
|
|
646
792
|
|
|
647
|
-
|
|
793
|
+
enqueue_frame(PendingFrame { timestamp_us, data }, state)?;
|
|
794
|
+
data = acquire_frame_buffer(frame_bytes, state)?;
|
|
648
795
|
}
|
|
649
796
|
Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => break Ok(()),
|
|
650
797
|
Err(error) => break Err(error),
|
|
@@ -658,6 +805,7 @@ fn consume_ffmpeg_output(
|
|
|
658
805
|
.take()
|
|
659
806
|
.map(|mut process| process.wait())
|
|
660
807
|
.transpose()?;
|
|
808
|
+
join_stderr_workers(state);
|
|
661
809
|
|
|
662
810
|
read_result?;
|
|
663
811
|
if !state.closed.load(Ordering::SeqCst) && status.is_some_and(|status| !status.success()) {
|
|
@@ -709,15 +857,16 @@ fn wait_for_hardware_retry(state: &DecoderState) -> io::Result<bool> {
|
|
|
709
857
|
Ok(!state.closed.load(Ordering::SeqCst))
|
|
710
858
|
}
|
|
711
859
|
|
|
712
|
-
fn enqueue_frame(
|
|
713
|
-
frame: PendingFrame,
|
|
714
|
-
frame_bytes: usize,
|
|
715
|
-
state: &DecoderState,
|
|
716
|
-
) -> io::Result<Vec<u8>> {
|
|
860
|
+
fn enqueue_frame(frame: PendingFrame, state: &DecoderState) -> io::Result<()> {
|
|
717
861
|
let catch_up_timestamp_us = state.catch_up_timestamp_us.load(Ordering::SeqCst);
|
|
718
862
|
if catch_up_timestamp_us >= 0 && frame.timestamp_us < catch_up_timestamp_us {
|
|
719
863
|
state.skipped_frames.fetch_add(1, Ordering::SeqCst);
|
|
720
|
-
|
|
864
|
+
let (frames, _) = &*state.frames;
|
|
865
|
+
frames
|
|
866
|
+
.lock()
|
|
867
|
+
.map_err(|_| io::Error::other("Video frame queue lock poisoned"))?
|
|
868
|
+
.recycle(frame.data);
|
|
869
|
+
return Ok(());
|
|
721
870
|
}
|
|
722
871
|
if catch_up_timestamp_us >= 0 {
|
|
723
872
|
state.catch_up_timestamp_us.store(-1, Ordering::SeqCst);
|
|
@@ -731,7 +880,7 @@ fn enqueue_frame(
|
|
|
731
880
|
if state.source_paced {
|
|
732
881
|
if let Some(recycled) = frames.push_latest(frame) {
|
|
733
882
|
state.dropped_frames.fetch_add(1, Ordering::SeqCst);
|
|
734
|
-
|
|
883
|
+
frames.recycle(recycled);
|
|
735
884
|
}
|
|
736
885
|
} else {
|
|
737
886
|
while frames.len() >= FRAME_QUEUE_CAPACITY
|
|
@@ -743,12 +892,14 @@ fn enqueue_frame(
|
|
|
743
892
|
.map_err(|_| io::Error::other("Video frame queue lock poisoned"))?;
|
|
744
893
|
}
|
|
745
894
|
if state.closed.load(Ordering::SeqCst) {
|
|
746
|
-
|
|
895
|
+
frames.recycle(frame.data);
|
|
896
|
+
return Ok(());
|
|
747
897
|
}
|
|
748
898
|
let target = state.catch_up_timestamp_us.load(Ordering::SeqCst);
|
|
749
899
|
if target >= 0 && frame.timestamp_us < target {
|
|
750
900
|
state.skipped_frames.fetch_add(1, Ordering::SeqCst);
|
|
751
|
-
|
|
901
|
+
frames.recycle(frame.data);
|
|
902
|
+
return Ok(());
|
|
752
903
|
}
|
|
753
904
|
if target >= 0 {
|
|
754
905
|
state.catch_up_timestamp_us.store(-1, Ordering::SeqCst);
|
|
@@ -756,9 +907,36 @@ fn enqueue_frame(
|
|
|
756
907
|
frames.push_back(frame);
|
|
757
908
|
}
|
|
758
909
|
|
|
910
|
+
Ok(())
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
fn acquire_frame_buffer(frame_bytes: usize, state: &DecoderState) -> io::Result<Vec<u8>> {
|
|
914
|
+
let (frames, _) = &*state.frames;
|
|
915
|
+
if let Some(buffer) = frames
|
|
916
|
+
.lock()
|
|
917
|
+
.map_err(|_| io::Error::other("Video frame queue lock poisoned"))?
|
|
918
|
+
.take_recycled(frame_bytes)
|
|
919
|
+
{
|
|
920
|
+
state.frame_buffer_reuses.fetch_add(1, Ordering::SeqCst);
|
|
921
|
+
return Ok(buffer);
|
|
922
|
+
}
|
|
923
|
+
state
|
|
924
|
+
.frame_buffer_allocations
|
|
925
|
+
.fetch_add(1, Ordering::SeqCst);
|
|
759
926
|
Ok(vec![0_u8; frame_bytes])
|
|
760
927
|
}
|
|
761
928
|
|
|
929
|
+
fn join_stderr_workers(state: &DecoderState) {
|
|
930
|
+
let workers = state
|
|
931
|
+
.stderr_workers
|
|
932
|
+
.lock()
|
|
933
|
+
.map(|mut workers| workers.drain(..).collect::<Vec<_>>())
|
|
934
|
+
.unwrap_or_default();
|
|
935
|
+
for worker in workers {
|
|
936
|
+
let _ = worker.join();
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
762
940
|
fn set_backend_name(state: &DecoderState, name: &str) {
|
|
763
941
|
if let Ok(mut backend) = state.backend.lock() {
|
|
764
942
|
*backend = name.to_string();
|
|
@@ -779,11 +957,13 @@ fn close_state(state: &DecoderState) {
|
|
|
779
957
|
if let Ok(mut child) = state.child.lock() {
|
|
780
958
|
if let Some(mut process) = child.take() {
|
|
781
959
|
let _ = process.kill();
|
|
960
|
+
let _ = process.wait();
|
|
782
961
|
}
|
|
783
962
|
}
|
|
784
963
|
if let Ok(mut frames) = frames.lock() {
|
|
785
964
|
frames.clear();
|
|
786
965
|
}
|
|
966
|
+
join_stderr_workers(state);
|
|
787
967
|
}
|
|
788
968
|
|
|
789
969
|
#[cfg(test)]
|
|
@@ -906,13 +1086,13 @@ mod tests {
|
|
|
906
1086
|
fn file_queue_applies_backpressure_until_a_frame_is_consumed() {
|
|
907
1087
|
let state = decoder_state(false);
|
|
908
1088
|
for timestamp_us in 0..FRAME_QUEUE_CAPACITY as i64 {
|
|
909
|
-
enqueue_frame(pending(timestamp_us),
|
|
1089
|
+
enqueue_frame(pending(timestamp_us), &state).unwrap();
|
|
910
1090
|
}
|
|
911
1091
|
|
|
912
1092
|
let producer_state = state.clone();
|
|
913
1093
|
let (sent, received) = mpsc::channel();
|
|
914
1094
|
thread::spawn(move || {
|
|
915
|
-
let result = enqueue_frame(pending(99),
|
|
1095
|
+
let result = enqueue_frame(pending(99), &producer_state);
|
|
916
1096
|
sent.send(result.is_ok()).unwrap();
|
|
917
1097
|
});
|
|
918
1098
|
|
|
@@ -928,14 +1108,76 @@ mod tests {
|
|
|
928
1108
|
let state = decoder_state(false);
|
|
929
1109
|
state.catch_up_timestamp_us.store(30, Ordering::SeqCst);
|
|
930
1110
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
enqueue_frame(pending(30),
|
|
1111
|
+
enqueue_frame(pending(10), &state).unwrap();
|
|
1112
|
+
enqueue_frame(pending(20), &state).unwrap();
|
|
1113
|
+
enqueue_frame(pending(30), &state).unwrap();
|
|
934
1114
|
|
|
935
1115
|
assert_eq!(state.skipped_frames.load(Ordering::SeqCst), 2);
|
|
936
1116
|
let (frames, _) = &*state.frames;
|
|
937
1117
|
let mut frames = frames.lock().unwrap();
|
|
938
1118
|
assert_eq!(frames.pop_next().unwrap().timestamp_us, 30);
|
|
1119
|
+
assert_eq!(frames.recycled.len(), 2);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
#[test]
|
|
1123
|
+
fn copies_frame_into_caller_buffer_and_recycles_native_storage() {
|
|
1124
|
+
let decoder = NativeVideoDecoder::new(DecoderOptions {
|
|
1125
|
+
width: 2,
|
|
1126
|
+
height: 2,
|
|
1127
|
+
fps: Some(30.0),
|
|
1128
|
+
start_time: None,
|
|
1129
|
+
ffmpeg_path: None,
|
|
1130
|
+
vaapi_device: None,
|
|
1131
|
+
playback_rate: None,
|
|
1132
|
+
end_time: None,
|
|
1133
|
+
source_paced: None,
|
|
1134
|
+
input_args: None,
|
|
1135
|
+
output_args: None,
|
|
1136
|
+
})
|
|
1137
|
+
.unwrap();
|
|
1138
|
+
let mut target = vec![0; 6];
|
|
1139
|
+
let info = decoder
|
|
1140
|
+
.copy_video_frame_into(
|
|
1141
|
+
PendingFrame {
|
|
1142
|
+
timestamp_us: 42,
|
|
1143
|
+
data: vec![1, 2, 3, 4, 5, 6],
|
|
1144
|
+
},
|
|
1145
|
+
&mut target,
|
|
1146
|
+
)
|
|
1147
|
+
.unwrap();
|
|
1148
|
+
|
|
1149
|
+
assert_eq!(target, vec![1, 2, 3, 4, 5, 6]);
|
|
1150
|
+
assert_eq!(info.timestamp_us, 42);
|
|
1151
|
+
assert_eq!(decoder.state.frames.0.lock().unwrap().recycled.len(), 1);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
#[test]
|
|
1155
|
+
fn rejects_wrong_caller_buffer_size_without_losing_native_storage() {
|
|
1156
|
+
let decoder = NativeVideoDecoder::new(DecoderOptions {
|
|
1157
|
+
width: 2,
|
|
1158
|
+
height: 2,
|
|
1159
|
+
fps: Some(30.0),
|
|
1160
|
+
start_time: None,
|
|
1161
|
+
ffmpeg_path: None,
|
|
1162
|
+
vaapi_device: None,
|
|
1163
|
+
playback_rate: None,
|
|
1164
|
+
end_time: None,
|
|
1165
|
+
source_paced: None,
|
|
1166
|
+
input_args: None,
|
|
1167
|
+
output_args: None,
|
|
1168
|
+
})
|
|
1169
|
+
.unwrap();
|
|
1170
|
+
let mut target = vec![0; 5];
|
|
1171
|
+
let result = decoder.copy_video_frame_into(
|
|
1172
|
+
PendingFrame {
|
|
1173
|
+
timestamp_us: 42,
|
|
1174
|
+
data: vec![1, 2, 3, 4, 5, 6],
|
|
1175
|
+
},
|
|
1176
|
+
&mut target,
|
|
1177
|
+
);
|
|
1178
|
+
|
|
1179
|
+
assert!(result.is_err());
|
|
1180
|
+
assert_eq!(decoder.state.frames.0.lock().unwrap().recycled.len(), 1);
|
|
939
1181
|
}
|
|
940
1182
|
|
|
941
1183
|
#[test]
|
|
@@ -973,7 +1215,10 @@ mod tests {
|
|
|
973
1215
|
decoded_frames: Arc::new(AtomicU64::new(0)),
|
|
974
1216
|
dropped_frames: Arc::new(AtomicU64::new(0)),
|
|
975
1217
|
skipped_frames: Arc::new(AtomicU64::new(0)),
|
|
1218
|
+
frame_buffer_allocations: Arc::new(AtomicU64::new(0)),
|
|
1219
|
+
frame_buffer_reuses: Arc::new(AtomicU64::new(0)),
|
|
976
1220
|
backend: Arc::new(Mutex::new("test".to_string())),
|
|
1221
|
+
stderr_workers: Arc::new(Mutex::new(Vec::new())),
|
|
977
1222
|
}
|
|
978
1223
|
}
|
|
979
1224
|
|