@matjash/pixi-native-win32-x64 0.2.0 → 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.
- package/native/audio/dist/win32-x64/native_audio.node +0 -0
- package/native/audio/src/audio_impl.rs +195 -40
- package/native/video/dist/win32-x64/native_video.node +0 -0
- package/native/video/src/index.d.ts +12 -0
- package/native/video/src/index.js +43 -0
- package/native/video/src/lib.rs +269 -24
- package/package.json +1 -1
|
Binary file
|
|
@@ -7,10 +7,13 @@ use std::thread;
|
|
|
7
7
|
use std::time::{Duration, Instant};
|
|
8
8
|
|
|
9
9
|
use maudio::audio::sample_rate::SampleRate;
|
|
10
|
-
use maudio::data_source::
|
|
10
|
+
use maudio::data_source::data_source_builder::DataSourceBuilder;
|
|
11
|
+
use maudio::data_source::pcm_source::PcmSource;
|
|
11
12
|
use maudio::data_source::sources::pcm_ring_buffer::{PcmRbRecv, PcmRbSend, PcmRingBuffer};
|
|
13
|
+
use maudio::data_source::{DataSource, SourceContext};
|
|
12
14
|
use maudio::engine::{engine_builder::EngineBuilder, Engine};
|
|
13
15
|
use maudio::sound::{notifier::EndNotifier, sound_builder::SoundBuilder, Sound};
|
|
16
|
+
use maudio::{ErrorKinds, MaResult, MaudioError};
|
|
14
17
|
use napi::bindgen_prelude::*;
|
|
15
18
|
use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
|
|
16
19
|
use napi_derive::napi;
|
|
@@ -112,9 +115,64 @@ impl StreamingControl {
|
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
|
|
118
|
+
struct SharedPcmSource {
|
|
119
|
+
samples: Arc<Vec<f32>>,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
impl SharedPcmSource {
|
|
123
|
+
fn new(samples: Arc<Vec<f32>>) -> Self {
|
|
124
|
+
Self { samples }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
impl PcmSource<f32> for SharedPcmSource {
|
|
129
|
+
fn fill_pcm_frames(
|
|
130
|
+
&mut self,
|
|
131
|
+
output: &mut [f32],
|
|
132
|
+
context: &mut SourceContext,
|
|
133
|
+
) -> MaResult<usize> {
|
|
134
|
+
let channels = context.data_format.channels as usize;
|
|
135
|
+
let frame_count = self.samples.len() / channels;
|
|
136
|
+
let cursor = usize::try_from(context.cursor).unwrap_or(usize::MAX);
|
|
137
|
+
if cursor >= frame_count {
|
|
138
|
+
return Ok(0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let output_frames = output.len() / channels;
|
|
142
|
+
let frames_to_copy = output_frames.min(frame_count - cursor);
|
|
143
|
+
let sample_start = cursor * channels;
|
|
144
|
+
let samples_to_copy = frames_to_copy * channels;
|
|
145
|
+
output[..samples_to_copy]
|
|
146
|
+
.copy_from_slice(&self.samples[sample_start..sample_start + samples_to_copy]);
|
|
147
|
+
context.cursor += frames_to_copy as u64;
|
|
148
|
+
Ok(frames_to_copy)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fn seek_to_pcm_frame(&mut self, frame_index: u64, context: &mut SourceContext) -> MaResult<()> {
|
|
152
|
+
let frame_count = self.samples.len() / context.data_format.channels as usize;
|
|
153
|
+
if frame_index > frame_count as u64 {
|
|
154
|
+
return Err(MaudioError::new_ma_error(ErrorKinds::InvalidOperation(
|
|
155
|
+
"Audio seek is outside the cached PCM source",
|
|
156
|
+
)));
|
|
157
|
+
}
|
|
158
|
+
context.cursor = frame_index;
|
|
159
|
+
Ok(())
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fn cursor_in_pcm_frames(&self, context: &SourceContext) -> MaResult<u64> {
|
|
163
|
+
Ok(context.cursor)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn length_in_pcm_frames(&self, context: &SourceContext) -> MaResult<u64> {
|
|
167
|
+
Ok((self.samples.len() / context.data_format.channels as usize) as u64)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
type StaticDataSource = DataSource<f32, SharedPcmSource>;
|
|
172
|
+
|
|
115
173
|
enum VoiceSource {
|
|
116
174
|
Loading,
|
|
117
|
-
Static(Box<
|
|
175
|
+
Static(Box<StaticDataSource>),
|
|
118
176
|
Streaming {
|
|
119
177
|
receiver: Box<PcmRbRecv<f32>>,
|
|
120
178
|
control: Arc<StreamingControl>,
|
|
@@ -170,15 +228,51 @@ impl Voice {
|
|
|
170
228
|
self.timeline_updated_at = now;
|
|
171
229
|
return;
|
|
172
230
|
}
|
|
173
|
-
self.timeline_seconds
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
);
|
|
180
238
|
self.timeline_updated_at = now;
|
|
181
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)
|
|
182
276
|
}
|
|
183
277
|
|
|
184
278
|
enum EngineCommand {
|
|
@@ -449,7 +543,8 @@ impl NativeAudioEngine {
|
|
|
449
543
|
#[napi]
|
|
450
544
|
pub fn current_time(&self, id: u32) -> Option<f64> {
|
|
451
545
|
let snapshot = self.state.snapshots.lock().ok()?.get(&id).cloned()?;
|
|
452
|
-
|
|
546
|
+
let time = f64::from_bits(snapshot.time_bits.load(Ordering::Acquire));
|
|
547
|
+
time.is_finite().then_some(time)
|
|
453
548
|
}
|
|
454
549
|
|
|
455
550
|
#[napi]
|
|
@@ -643,8 +738,8 @@ impl EngineRuntime {
|
|
|
643
738
|
let Some(mut voice) = self.voices.remove(&id) else {
|
|
644
739
|
continue;
|
|
645
740
|
};
|
|
646
|
-
let result =
|
|
647
|
-
.map_err(|error| format!("Cannot create miniaudio
|
|
741
|
+
let result = build_static_data_source(samples)
|
|
742
|
+
.map_err(|error| format!("Cannot create miniaudio data source: {error}"))
|
|
648
743
|
.and_then(|buffer| {
|
|
649
744
|
voice.source = VoiceSource::Static(Box::new(buffer));
|
|
650
745
|
self.attach_sound(&mut voice)
|
|
@@ -830,11 +925,12 @@ impl EngineRuntime {
|
|
|
830
925
|
.snapshot
|
|
831
926
|
.queued_frames
|
|
832
927
|
.store(queued, Ordering::Release);
|
|
833
|
-
if control.ready.load(Ordering::Acquire)
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
928
|
+
if control.ready.load(Ordering::Acquire)
|
|
929
|
+
&& !voice.play_announced
|
|
930
|
+
&& sound.play_sound().is_ok()
|
|
931
|
+
{
|
|
932
|
+
voice.play_announced = true;
|
|
933
|
+
state.queue_event(voice.owner_id, "play", Some(*id), None, None);
|
|
838
934
|
}
|
|
839
935
|
let starved = voice.play_announced
|
|
840
936
|
&& voice.playing
|
|
@@ -858,11 +954,7 @@ impl EngineRuntime {
|
|
|
858
954
|
} else {
|
|
859
955
|
voice.timeline_seconds
|
|
860
956
|
};
|
|
861
|
-
|
|
862
|
-
voice
|
|
863
|
-
.snapshot
|
|
864
|
-
.time_bits
|
|
865
|
-
.store(time.to_bits(), Ordering::Release);
|
|
957
|
+
voice.store_snapshot_time(elapsed_seconds);
|
|
866
958
|
let current_volume = if voice.fade.is_some() {
|
|
867
959
|
sound.current_fade_volume()
|
|
868
960
|
} else {
|
|
@@ -913,30 +1005,25 @@ fn start_static_decode(
|
|
|
913
1005
|
preload: bool,
|
|
914
1006
|
request_id: Option<u32>,
|
|
915
1007
|
) {
|
|
1008
|
+
let key = cache_key(&options);
|
|
1009
|
+
if let Some(samples) = state
|
|
1010
|
+
.cache
|
|
1011
|
+
.lock()
|
|
1012
|
+
.ok()
|
|
1013
|
+
.and_then(|cache| cache.get(&key).cloned())
|
|
1014
|
+
{
|
|
1015
|
+
complete_static_decode(&state, &options, preload, request_id, samples);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
916
1019
|
thread::spawn(move || {
|
|
917
|
-
let
|
|
918
|
-
let decoded = state
|
|
919
|
-
.cache
|
|
920
|
-
.lock()
|
|
921
|
-
.ok()
|
|
922
|
-
.and_then(|cache| cache.get(&key).cloned())
|
|
923
|
-
.map_or_else(
|
|
924
|
-
|| decode_static(&options, state.sample_rate).map(Arc::new),
|
|
925
|
-
Ok,
|
|
926
|
-
);
|
|
1020
|
+
let decoded = decode_static(&options, state.sample_rate).map(Arc::new);
|
|
927
1021
|
match decoded {
|
|
928
1022
|
Ok(samples) => {
|
|
929
1023
|
if let Ok(mut cache) = state.cache.lock() {
|
|
930
1024
|
cache.insert(key, Arc::clone(&samples));
|
|
931
1025
|
}
|
|
932
|
-
|
|
933
|
-
state.queue_event(options.owner_id, "load", request_id, None, None);
|
|
934
|
-
} else {
|
|
935
|
-
state.push_command(EngineCommand::AttachStatic {
|
|
936
|
-
id: options.id,
|
|
937
|
-
samples,
|
|
938
|
-
});
|
|
939
|
-
}
|
|
1026
|
+
complete_static_decode(&state, &options, preload, request_id, samples);
|
|
940
1027
|
}
|
|
941
1028
|
Err(message) => {
|
|
942
1029
|
if !preload {
|
|
@@ -954,6 +1041,28 @@ fn start_static_decode(
|
|
|
954
1041
|
});
|
|
955
1042
|
}
|
|
956
1043
|
|
|
1044
|
+
fn complete_static_decode(
|
|
1045
|
+
state: &SharedState,
|
|
1046
|
+
options: &NativeVoiceOptions,
|
|
1047
|
+
preload: bool,
|
|
1048
|
+
request_id: Option<u32>,
|
|
1049
|
+
samples: Arc<Vec<f32>>,
|
|
1050
|
+
) {
|
|
1051
|
+
if preload {
|
|
1052
|
+
state.queue_event(options.owner_id, "load", request_id, None, None);
|
|
1053
|
+
} else {
|
|
1054
|
+
state.push_command(EngineCommand::AttachStatic {
|
|
1055
|
+
id: options.id,
|
|
1056
|
+
samples,
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
fn build_static_data_source(samples: Arc<Vec<f32>>) -> MaResult<StaticDataSource> {
|
|
1062
|
+
DataSourceBuilder::new(CHANNELS as u32, SampleRate::Sr48000)
|
|
1063
|
+
.build_f32(SharedPcmSource::new(samples))
|
|
1064
|
+
}
|
|
1065
|
+
|
|
957
1066
|
fn start_streaming_decode(
|
|
958
1067
|
state: Arc<SharedState>,
|
|
959
1068
|
options: NativeVoiceOptions,
|
|
@@ -1233,6 +1342,29 @@ fn redact_credentials(message: &str) -> String {
|
|
|
1233
1342
|
mod tests {
|
|
1234
1343
|
use super::*;
|
|
1235
1344
|
|
|
1345
|
+
#[test]
|
|
1346
|
+
fn static_voices_share_pcm_with_independent_cursors() {
|
|
1347
|
+
let samples = Arc::new(vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6]);
|
|
1348
|
+
let mut first = build_static_data_source(Arc::clone(&samples)).unwrap();
|
|
1349
|
+
let second = build_static_data_source(Arc::clone(&samples)).unwrap();
|
|
1350
|
+
|
|
1351
|
+
assert_eq!(Arc::strong_count(&samples), 3);
|
|
1352
|
+
assert_eq!(first.cursor_in_pcm_frames().unwrap(), 0);
|
|
1353
|
+
assert_eq!(second.cursor_in_pcm_frames().unwrap(), 0);
|
|
1354
|
+
|
|
1355
|
+
assert_eq!(first.read_pcm_frames(1).unwrap().frames(), 1);
|
|
1356
|
+
assert_eq!(first.cursor_in_pcm_frames().unwrap(), 1);
|
|
1357
|
+
assert_eq!(second.cursor_in_pcm_frames().unwrap(), 0);
|
|
1358
|
+
|
|
1359
|
+
first.seek_to_pcm_frame(0).unwrap();
|
|
1360
|
+
assert_eq!(first.cursor_in_pcm_frames().unwrap(), 0);
|
|
1361
|
+
assert!(first.seek_to_pcm_frame(4).is_err());
|
|
1362
|
+
|
|
1363
|
+
drop(first);
|
|
1364
|
+
drop(second);
|
|
1365
|
+
assert_eq!(Arc::strong_count(&samples), 1);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1236
1368
|
#[test]
|
|
1237
1369
|
fn atempo_decomposes_extreme_rates() {
|
|
1238
1370
|
assert_eq!(build_atempo_filter(4.0).unwrap(), "atempo=2,atempo=2");
|
|
@@ -1246,4 +1378,27 @@ mod tests {
|
|
|
1246
1378
|
"https://***:***@example.test/audio.mp3"
|
|
1247
1379
|
);
|
|
1248
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
|
+
}
|
|
1249
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
|
|