1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
//! POD types for the video-playback surface
//! (SUPER_PLAN_2 ยง4 Priority 6 + research).
//!
//! Same "dumb widget" architecture as camera/screencap
//! (`azul_layout::widgets::video::VideoWidget`): a background thread decodes
//! the source (vk-video - GPU decode + HTTP-range fetch) and its writeback
//! uploads each frame into the shared GL-texture `ImageRef` + recomposites.
//! Defined here in `azul-core` so the config crosses the FFI without
//! `azul-layout` (or vk-video) as a dependency.
//!
//! Unlike the camera/screencap configs this carries a `source` string, so
//! it's `Clone` but not `Copy`.
use crate::resources::RawImageFormat;
use crate::url::Url;
use azul_css::{AzString, U8Vec};
#[allow(variant_size_differences)]
// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
/// Where a video widget pulls its H.264/MP4 data from โ strongly typed so the
/// decode worker matches on it directly (no `RefAny` downcast). Mirrors
/// [`crate::screencap::ScreenCaptureSource`].
#[repr(C, u8)]
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(clippy::large_enum_variant)] // #[repr(C,u8)] FFI enum: boxing a variant changes the C ABI/api.json
pub enum VideoSource {
/// An HTTP(S) URL, fetched on the decode thread via an HTTP range request.
Url(Url),
/// A local filesystem path.
File(AzString),
/// Raw MP4 bytes already in memory.
Bytes(U8Vec),
}
impl Default for VideoSource {
fn default() -> Self {
Self::Url(Url::default())
}
}
/// Requested video-playback configuration.
#[repr(C)]
#[derive(Debug, Clone, PartialEq)]
pub struct VideoConfig {
/// Where to load the video from (URL / file path / in-memory bytes).
pub source: VideoSource,
/// Seek / scrub position in seconds. Changing it across a relayout makes the
/// widget's merge callback tell the decode worker to seek (scrubbing
/// timeline) โ the decoder survives relayout like the map's tile cache.
pub timestamp: f32,
/// Start playing automatically on mount.
pub autoplay: bool,
/// Restart from the beginning when the stream ends.
pub looping: bool,
/// Texture format the decoder delivers. `BGRA8` is the portable default;
/// `Nv12` (a later `RawImageFormat` addition) is the zero-copy path.
pub output_format: RawImageFormat,
}
impl Default for VideoConfig {
fn default() -> Self {
Self {
source: VideoSource::default(),
timestamp: 0.0,
autoplay: true,
looping: false,
output_format: RawImageFormat::BGRA8,
}
}
}
impl VideoConfig {
/// A default config playing `source` (autoplay on, no loop, BGRA8, t=0).
#[must_use]
pub fn new(source: VideoSource) -> Self {
Self {
source,
..Self::default()
}
}
}
/// One captured or decoded frame - tightly-packed RGBA8 pixels
/// (`width * height * 4`).
///
/// The unit a capture/decode worker produces, the
/// `set_on_frame` hook hands to user code (effects / save / send), and (P8)
/// azul-meet sends over UDP. Defined here (like [`crate::audio::AudioFrame`])
/// so it crosses the FFI without `azul-layout` as a dependency.
#[repr(C)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VideoFrame {
/// Frame width in px.
pub width: u32,
/// Frame height in px.
pub height: u32,
/// Tightly-packed RGBA8 pixel bytes (`width * height * 4`).
pub bytes: U8Vec,
}
impl VideoFrame {
/// A frame wrapping `bytes` (tightly-packed RGBA8, `width * height * 4`).
#[must_use]
pub const fn new(width: u32, height: u32, bytes: U8Vec) -> Self {
Self {
width,
height,
bytes,
}
}
}
// FFI Option wrapper for a frame-pull hook / accessor. `copy = false` (U8Vec).
impl_option!(VideoFrame, OptionVideoFrame, copy = false, [Clone, Debug]);
/// One CONSUMER of a capture source's frames: a requested output size.
///
/// A camera or a screen share has many consumers of the same captured
/// frame at once โ the on-screen preview tile (sized by layout), a remote
/// participant who asked for 500x200, a recorder at full size. The device
/// captures ONCE, at the smallest size that covers every consumer
/// (`azul_layout::image_scale::covering_size`), and every consumer gets its
/// own resample of that one frame (`azul_layout::image_scale::fan_out`): the
/// camera never captures more than the largest consumer needs, and nothing
/// is ever sent bigger than the consumer asked for. Registered on a capture
/// widget with `CameraWidget::with_consumer` /
/// `ScreenCaptureWidget::with_consumer`; each cut frame is handed to the
/// widget's `on_consumer_frame` hook as a [`ConsumerFrame`].
///
/// `id` is caller-chosen and handed back with every cut frame so one hook can
/// serve many consumers ("client Bob" = 7, "the recorder" = 8). Id 0
/// ([`FrameConsumer::PREVIEW_ID`]) is reserved for the widget's own on-screen
/// tile, whose size follows layout.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct FrameConsumer {
/// Caller-chosen id, handed back with every cut frame. 0 is the preview.
pub id: u32,
/// Requested output width in px (0 = invalid, the consumer is skipped).
pub width: u32,
/// Requested output height in px (0 = invalid, the consumer is skipped).
pub height: u32,
}
impl FrameConsumer {
/// The id of the widget's own on-screen preview: its size follows the
/// laid-out node (device pixels), never the caller.
pub const PREVIEW_ID: u32 = 0;
/// A consumer `id` that wants `width` x `height` frames.
#[must_use]
pub const fn new(id: u32, width: u32, height: u32) -> Self {
Self { id, width, height }
}
/// `false` for a zero-sized request (skipped by the fan-out) or the
/// reserved preview id.
#[must_use]
pub const fn is_valid(&self) -> bool {
self.id != Self::PREVIEW_ID && self.width > 0 && self.height > 0
}
}
impl_vec!(
FrameConsumer,
FrameConsumerVec,
FrameConsumerVecDestructor,
FrameConsumerVecDestructorType,
FrameConsumerVecSlice,
OptionFrameConsumer
);
impl_vec_debug!(FrameConsumer, FrameConsumerVec);
impl_vec_clone!(FrameConsumer, FrameConsumerVec, FrameConsumerVecDestructor);
impl_vec_partialeq!(FrameConsumer, FrameConsumerVec);
impl_vec_eq!(FrameConsumer, FrameConsumerVec);
impl_vec_partialord!(FrameConsumer, FrameConsumerVec);
impl_vec_ord!(FrameConsumer, FrameConsumerVec);
impl_vec_hash!(FrameConsumer, FrameConsumerVec);
impl_option!(
FrameConsumer,
OptionFrameConsumer,
[Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
/// A frame cut to one consumer's requested size: the [`FrameConsumer`] it
/// was cut for (so one hook can route by `consumer.id`) and the resampled
/// RGBA8 pixels (`consumer.width * consumer.height * 4`).
#[repr(C)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsumerFrame {
/// Which consumer this frame was cut for.
pub consumer: FrameConsumer,
/// The frame at the consumer's size.
pub frame: VideoFrame,
}
impl ConsumerFrame {
/// Pair a cut frame with the consumer it was cut for.
#[must_use]
pub const fn new(consumer: FrameConsumer, frame: VideoFrame) -> Self {
Self { consumer, frame }
}
}
impl_option!(
ConsumerFrame,
OptionConsumerFrame,
copy = false,
[Clone, Debug]
);
// FFI `Vec<VideoFrame>` wrapper โ the list a batch decode (`DecodedVideo`,
// `dll::desktop::extra::video_codec::pipeline`) hands back across the C ABI.
// `VideoFrame` derives Debug + Clone + PartialEq, so mirror exactly those Vec
// trait impls (no PartialOrd: `VideoFrame` isn't `PartialOrd`).
impl_vec!(
VideoFrame,
VideoFrameVec,
VideoFrameVecDestructor,
VideoFrameVecDestructorType,
VideoFrameVecSlice,
OptionVideoFrame
);
impl_vec_debug!(VideoFrame, VideoFrameVec);
impl_vec_clone!(VideoFrame, VideoFrameVec, VideoFrameVecDestructor);
impl_vec_partialeq!(VideoFrame, VideoFrameVec);
#[cfg(test)]
#[path = "video_test.rs"]
mod video_test;