1
//! Video-playback widget — a "dumb widget" identical in architecture to the
2
//! [`CameraWidget`](super::camera) / [`ScreenCaptureWidget`](super::screencap),
3
//! only the source differs (a video URL/file decoded via vk-video).
4
//! SUPER_PLAN_2 §4 P6, widget pivot.
5
//!
6
//! `VideoWidget::create(config).dom()` → an `<img>` a background decode thread
7
//! keeps fed; each frame goes through [`super::capture_common::present_frame`]
8
//! (GL-texture install-once / re-upload + recomposite). Shared core in
9
//! `capture_common`; this widget is its config + worker. Test-pattern worker
10
//! (scrolling SMPTE colour bars) stands in for the real vk-video decode worker.
11

            
12
use alloc::vec::Vec;
13

            
14
use azul_core::callbacks::Update;
15
use azul_core::dom::{ComponentEventFilter, DatasetMergeCallbackType, Dom, EventFilter};
16
use azul_core::refany::{OptionRefAny, RefAny};
17
use azul_core::resources::{ImageRef, RawImageFormat};
18
use azul_core::task::{ThreadId, ThreadReceiver};
19
use azul_core::video::{VideoConfig, VideoFrame};
20

            
21
use super::capture_common::{
22
    invoke_on_frame, present_frame, OnVideoFrame, OnVideoFrameCallback, OptionOnVideoFrame,
23
};
24
use crate::callbacks::{Callback, CallbackInfo, CallbackType};
25
use crate::thread::{
26
    Thread, ThreadCallback, ThreadReceiveMsg, ThreadSender, ThreadWriteBackMsg, WriteBackCallback,
27
};
28

            
29
/// Default decode size for the test pattern (the real decoder reports the
30
/// stream's actual size).
31
const DEFAULT_W: u32 = 1280;
32
const DEFAULT_H: u32 = 720;
33

            
34
/// Live state for one video widget, carried across relayout by
35
/// [`merge_video_state`].
36
pub struct VideoWidgetState {
37
    /// The requested playback configuration (source + autoplay/loop).
38
    pub config: VideoConfig,
39
    /// `true` once the decode thread has been started.
40
    pub started: bool,
41
    /// The stable external GL texture id once installed.
42
    pub gl_texture_id: Option<u32>,
43
    /// Optional user hook invoked with each decoded frame (effects / save /
44
    /// send). Re-set on every fresh build (see [`merge_video_state`]).
45
    pub on_frame: OptionOnVideoFrame,
46
}
47

            
48
/// A video-playback widget. `create(config).dom()` yields an `<img>` the
49
/// decode thread keeps fed.
50
#[repr(C)]
51
pub struct VideoWidget {
52
    /// Source URL + autoplay/loop + format.
53
    pub config: VideoConfig,
54
    /// Optional per-frame user hook (effects / save / send - azul-meet).
55
    pub on_frame: OptionOnVideoFrame,
56
}
57

            
58
impl VideoWidget {
59
    /// Create a video widget for the given config.
60
    pub fn create(config: VideoConfig) -> Self {
61
        Self {
62
            config,
63
            on_frame: OptionOnVideoFrame::None,
64
        }
65
    }
66

            
67
    /// Set a hook invoked with every decoded frame - for live effects, saving
68
    /// frames into your data model, or sending them over the network
69
    /// (azul-meet). The backreference DI pattern (see `architecture.md`).
70
    pub fn set_on_frame<C: Into<OnVideoFrameCallback>>(&mut self, data: RefAny, on_frame: C) {
71
        self.on_frame = Some(OnVideoFrame {
72
            refany: data,
73
            callback: on_frame.into(),
74
        })
75
        .into();
76
    }
77

            
78
    /// Builder form of [`set_on_frame`](Self::set_on_frame).
79
    pub fn with_on_frame<C: Into<OnVideoFrameCallback>>(
80
        mut self,
81
        data: RefAny,
82
        on_frame: C,
83
    ) -> Self {
84
        self.set_on_frame(data, on_frame);
85
        self
86
    }
87

            
88
    /// Build the widget's DOM: a single `<img>` node, fed by a background
89
    /// decode thread started on mount.
90
    pub fn dom(self) -> Dom {
91
        let state = VideoWidgetState {
92
            config: self.config,
93
            started: false,
94
            gl_texture_id: None,
95
            on_frame: self.on_frame,
96
        };
97
        let dataset = RefAny::new(state);
98

            
99
        let placeholder = ImageRef::null_image(
100
            DEFAULT_W as usize,
101
            DEFAULT_H as usize,
102
            RawImageFormat::BGRA8,
103
            b"azul-video-placeholder".to_vec(),
104
        );
105

            
106
        Dom::create_image(placeholder)
107
            .with_dataset(OptionRefAny::Some(dataset.clone()))
108
            .with_merge_callback(merge_video_state as DatasetMergeCallbackType)
109
            .with_callback(
110
                EventFilter::Component(ComponentEventFilter::AfterMount),
111
                dataset,
112
                Callback::from(video_on_after_mount as CallbackType),
113
            )
114
    }
115
}
116

            
117
/// AfterMount: start the background decode thread exactly once.
118
extern "C" fn video_on_after_mount(mut data: RefAny, mut info: CallbackInfo) -> Update {
119
    {
120
        let mut s = match data.downcast_mut::<VideoWidgetState>() {
121
            Some(s) => s,
122
            None => return Update::DoNothing,
123
        };
124
        if s.started {
125
            return Update::DoNothing;
126
        }
127
        s.started = true;
128
    }
129
    info.add_thread(
130
        ThreadId::unique(),
131
        Thread::create(
132
            RefAny::new(()),
133
            data.clone(),
134
            ThreadCallback::new(video_test_worker),
135
        ),
136
    );
137
    Update::DoNothing
138
}
139

            
140
/// Background worker (test pattern): SMPTE-style colour bars scrolling
141
/// horizontally ~30×/s. Replaced by the real vk-video decode worker later.
142
extern "C" fn video_test_worker(_init: RefAny, mut sender: ThreadSender, _recv: ThreadReceiver) {
143
    const BARS: [[u8; 3]; 7] = [
144
        [235, 235, 235],
145
        [235, 235, 16],
146
        [16, 235, 235],
147
        [16, 235, 16],
148
        [235, 16, 235],
149
        [235, 16, 16],
150
        [16, 16, 235],
151
    ];
152
    let (w, h) = (DEFAULT_W as usize, DEFAULT_H as usize);
153
    let mut tick: u32 = 0;
154
    loop {
155
        let shift = (tick as usize / 4) % 7;
156
        let mut bytes = Vec::with_capacity(w * h * 4);
157
        for _y in 0..h {
158
            for x in 0..w {
159
                let c = BARS[((x * 7 / w) + shift) % 7];
160
                bytes.extend_from_slice(&[c[0], c[1], c[2], 255]);
161
            }
162
        }
163
        let frame = VideoFrame {
164
            width: w as u32,
165
            height: h as u32,
166
            bytes: bytes.into(),
167
        };
168
        let sent = sender.send(ThreadReceiveMsg::WriteBack(ThreadWriteBackMsg::new(
169
            WriteBackCallback::new(video_writeback),
170
            RefAny::new(frame),
171
        )));
172
        if !sent {
173
            break;
174
        }
175
        std::thread::sleep(std::time::Duration::from_millis(33));
176
        tick = tick.wrapping_add(2);
177
    }
178
}
179

            
180
/// Writeback (main thread): hand the decoded frame to the shared GL presenter
181
/// and store the (stable) texture id.
182
extern "C" fn video_writeback(
183
    mut writeback_data: RefAny,
184
    mut frame_data: RefAny,
185
    mut info: CallbackInfo,
186
) -> Update {
187
    let (current, hook) = match writeback_data.downcast_ref::<VideoWidgetState>() {
188
        Some(s) => (s.gl_texture_id, s.on_frame.clone()),
189
        None => (None, OptionOnVideoFrame::None),
190
    };
191
    let mut user_update = Update::DoNothing;
192
    let new_id = match frame_data.downcast_ref::<VideoFrame>() {
193
        Some(frame) => {
194
            let id = present_frame(&mut info, writeback_data.clone(), current, &frame);
195
            user_update = invoke_on_frame(&hook, &mut info, &frame);
196
            id
197
        }
198
        None => return Update::DoNothing,
199
    };
200
    if let Some(mut s) = writeback_data.downcast_mut::<VideoWidgetState>() {
201
        s.gl_texture_id = new_id;
202
    }
203
    user_update
204
}
205

            
206
/// Carry live state forward across relayout.
207
extern "C" fn merge_video_state(mut new_data: RefAny, mut old_data: RefAny) -> RefAny {
208
    {
209
        let new_guard = new_data.downcast_mut::<VideoWidgetState>();
210
        let old_guard = old_data.downcast_ref::<VideoWidgetState>();
211
        if let (Some(mut new_g), Some(old_g)) = (new_guard, old_guard) {
212
            new_g.started = old_g.started;
213
            new_g.gl_texture_id = old_g.gl_texture_id;
214
        }
215
    }
216
    new_data
217
}