@matjash/pixi-native-linux-x64 0.2.2 → 0.2.4
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/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -22,6 +22,14 @@ Pixi Native uses these projects through direct packages or native integration:
|
|
|
22
22
|
The installed packages and their upstream repositories contain their copyright
|
|
23
23
|
and license terms. Those terms continue to apply to each dependency.
|
|
24
24
|
|
|
25
|
+
## Source Sans 3 demo font
|
|
26
|
+
|
|
27
|
+
The PixiJS 7 WebGL text demo includes Source Sans 3 Regular from
|
|
28
|
+
[Adobe Fonts](https://github.com/adobe-fonts/source-sans). Copyright 2010-2024
|
|
29
|
+
Adobe, with Reserved Font Name "Source". The font is distributed under the SIL
|
|
30
|
+
Open Font License 1.1; the complete license is stored beside the font at
|
|
31
|
+
`src/demo/assets/fonts/SourceSans3-LICENSE.md`.
|
|
32
|
+
|
|
25
33
|
## Dawn and Tint
|
|
26
34
|
|
|
27
35
|
The packaged `pixi_native_gpu.node` binaries contain Dawn and Tint code.
|
|
Binary file
|
|
@@ -25,6 +25,13 @@ export interface VideoFrameInfo {
|
|
|
25
25
|
timestampUs: number;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export interface VideoShutdownDiagnostics {
|
|
29
|
+
activeDecoderWorkers: number;
|
|
30
|
+
pendingDecoderShutdowns: number;
|
|
31
|
+
completedDecoderShutdowns: number;
|
|
32
|
+
maxDecoderShutdownMs: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
export class NativeVideoDecoder {
|
|
29
36
|
public constructor(options: DecoderOptions);
|
|
30
37
|
public open(source: string): void;
|
|
@@ -46,3 +53,5 @@ export class NativeVideoDecoder {
|
|
|
46
53
|
public isFinished(): boolean;
|
|
47
54
|
public close(): void;
|
|
48
55
|
}
|
|
56
|
+
|
|
57
|
+
export function videoShutdownDiagnostics(): VideoShutdownDiagnostics;
|
|
@@ -101,4 +101,13 @@ class NativeVideoDecoder {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
function videoShutdownDiagnostics() {
|
|
105
|
+
return native.videoShutdownDiagnostics?.() ?? {
|
|
106
|
+
activeDecoderWorkers: 0,
|
|
107
|
+
pendingDecoderShutdowns: 0,
|
|
108
|
+
completedDecoderShutdowns: 0,
|
|
109
|
+
maxDecoderShutdownMs: 0,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
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,
|
|
@@ -45,6 +50,24 @@ pub struct VideoFrameInfo {
|
|
|
45
50
|
pub timestamp_us: i64,
|
|
46
51
|
}
|
|
47
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
|
+
|
|
48
71
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
49
72
|
enum DecoderBackend {
|
|
50
73
|
D3d11va,
|
|
@@ -190,6 +213,7 @@ pub struct NativeVideoDecoder {
|
|
|
190
213
|
options: DecoderOptions,
|
|
191
214
|
state: DecoderState,
|
|
192
215
|
worker: Option<thread::JoinHandle<()>>,
|
|
216
|
+
retired: bool,
|
|
193
217
|
}
|
|
194
218
|
|
|
195
219
|
#[napi]
|
|
@@ -207,6 +231,7 @@ impl NativeVideoDecoder {
|
|
|
207
231
|
Ok(Self {
|
|
208
232
|
options,
|
|
209
233
|
worker: None,
|
|
234
|
+
retired: false,
|
|
210
235
|
state: DecoderState {
|
|
211
236
|
closed: Arc::new(AtomicBool::new(true)),
|
|
212
237
|
finished: Arc::new(AtomicBool::new(false)),
|
|
@@ -228,6 +253,11 @@ impl NativeVideoDecoder {
|
|
|
228
253
|
|
|
229
254
|
#[napi]
|
|
230
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
|
+
}
|
|
231
261
|
if self.state.closed.load(Ordering::SeqCst) {
|
|
232
262
|
self.join_worker();
|
|
233
263
|
}
|
|
@@ -314,7 +344,9 @@ impl NativeVideoDecoder {
|
|
|
314
344
|
let initial_stdout = install_child(&state, spawned)
|
|
315
345
|
.map_err(|error| Error::from_reason(error.to_string()))?;
|
|
316
346
|
|
|
347
|
+
ACTIVE_DECODER_WORKERS.fetch_add(1, Ordering::SeqCst);
|
|
317
348
|
self.worker = Some(thread::spawn(move || {
|
|
349
|
+
let _active_worker = ActiveDecoderWorker;
|
|
318
350
|
let mut result =
|
|
319
351
|
consume_decoder_attempt(initial_stdout, width, height, fps, start_time, &state);
|
|
320
352
|
|
|
@@ -538,8 +570,16 @@ impl NativeVideoDecoder {
|
|
|
538
570
|
|
|
539
571
|
#[napi]
|
|
540
572
|
pub fn close(&mut self) {
|
|
541
|
-
|
|
542
|
-
|
|
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);
|
|
543
583
|
}
|
|
544
584
|
|
|
545
585
|
fn join_worker(&mut self) {
|
|
@@ -577,8 +617,7 @@ impl NativeVideoDecoder {
|
|
|
577
617
|
|
|
578
618
|
impl Drop for NativeVideoDecoder {
|
|
579
619
|
fn drop(&mut self) {
|
|
580
|
-
|
|
581
|
-
self.join_worker();
|
|
620
|
+
self.begin_shutdown();
|
|
582
621
|
}
|
|
583
622
|
}
|
|
584
623
|
|
|
@@ -751,7 +790,25 @@ fn redact_url_credentials(value: &str) -> String {
|
|
|
751
790
|
result
|
|
752
791
|
}
|
|
753
792
|
|
|
754
|
-
fn install_child(state: &DecoderState, spawned: SpawnedFfmpeg) -> io::Result<ChildStdout> {
|
|
793
|
+
fn install_child(state: &DecoderState, mut spawned: SpawnedFfmpeg) -> io::Result<ChildStdout> {
|
|
794
|
+
let mut child = state
|
|
795
|
+
.child
|
|
796
|
+
.lock()
|
|
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);
|
|
755
812
|
if let Some(worker) = spawned.stderr_worker {
|
|
756
813
|
state
|
|
757
814
|
.stderr_workers
|
|
@@ -759,10 +816,6 @@ fn install_child(state: &DecoderState, spawned: SpawnedFfmpeg) -> io::Result<Chi
|
|
|
759
816
|
.map_err(|_| io::Error::other("FFmpeg stderr worker lock poisoned"))?
|
|
760
817
|
.push(worker);
|
|
761
818
|
}
|
|
762
|
-
*state
|
|
763
|
-
.child
|
|
764
|
-
.lock()
|
|
765
|
-
.map_err(|_| io::Error::other("FFmpeg process lock poisoned"))? = Some(spawned.child);
|
|
766
819
|
Ok(spawned.stdout)
|
|
767
820
|
}
|
|
768
821
|
|
|
@@ -798,13 +851,7 @@ fn consume_ffmpeg_output(
|
|
|
798
851
|
}
|
|
799
852
|
};
|
|
800
853
|
|
|
801
|
-
let status = state
|
|
802
|
-
.child
|
|
803
|
-
.lock()
|
|
804
|
-
.map_err(|_| io::Error::other("FFmpeg process lock poisoned"))?
|
|
805
|
-
.take()
|
|
806
|
-
.map(|mut process| process.wait())
|
|
807
|
-
.transpose()?;
|
|
854
|
+
let status = wait_for_child_exit(state)?;
|
|
808
855
|
join_stderr_workers(state);
|
|
809
856
|
|
|
810
857
|
read_result?;
|
|
@@ -950,20 +997,78 @@ fn store_error(state: &DecoderState, message: String) {
|
|
|
950
997
|
}
|
|
951
998
|
}
|
|
952
999
|
|
|
953
|
-
|
|
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> {
|
|
954
1028
|
state.closed.store(true, Ordering::SeqCst);
|
|
955
1029
|
let (frames, available) = &*state.frames;
|
|
956
1030
|
available.notify_all();
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
let _ = process.wait();
|
|
961
|
-
}
|
|
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();
|
|
962
1034
|
}
|
|
963
1035
|
if let Ok(mut frames) = frames.lock() {
|
|
964
1036
|
frames.clear();
|
|
965
1037
|
}
|
|
966
|
-
|
|
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
|
+
}
|
|
967
1072
|
}
|
|
968
1073
|
|
|
969
1074
|
#[cfg(test)]
|
|
@@ -1180,6 +1285,40 @@ mod tests {
|
|
|
1180
1285
|
assert_eq!(decoder.state.frames.0.lock().unwrap().recycled.len(), 1);
|
|
1181
1286
|
}
|
|
1182
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"));
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1183
1322
|
#[test]
|
|
1184
1323
|
fn hardware_retry_stops_after_five_attempts_or_first_frame() {
|
|
1185
1324
|
let state = decoder_state(false);
|
|
@@ -1203,6 +1342,23 @@ mod tests {
|
|
|
1203
1342
|
}
|
|
1204
1343
|
}
|
|
1205
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
|
+
|
|
1206
1362
|
fn decoder_state(source_paced: bool) -> DecoderState {
|
|
1207
1363
|
DecoderState {
|
|
1208
1364
|
closed: Arc::new(AtomicBool::new(false)),
|