browser_video_capture/
lib.rs

1#[macro_use]
2mod macros;
3mod utils;
4
5#[cfg(feature = "2d")]
6mod d2;
7#[cfg(feature = "gl")]
8mod gl;
9
10use web_sys::{js_sys, HtmlVideoElement};
11
12#[allow(unused_macros)]
13macro_rules! impl_enum_from {
14    ($from:ty => $typ:ty:$name:tt) => {
15        impl From<$from> for $typ {
16            fn from(value: $from) -> Self {
17                Self::$name(value)
18            }
19        }
20    };
21}
22
23macro_rules! enum_method {
24    ($name:tt ($( $arg:tt: $typ:ty ),*) => $ret:ty) => {
25        fn $name(&self, $($arg: $typ),*) -> $ret {
26            match self {
27                #[cfg(feature = "html-2d")]
28                Self::Html2D(c) => c.$name($($arg),*),
29                #[cfg(feature = "offscreen-2d")]
30                Self::Offscreen2D(c) => c.$name($($arg),*),
31                #[cfg(all(feature = "html", feature = "webgl"))]
32                Self::HtmlGL(c) => c.$name($($arg),*),
33                #[cfg(all(feature = "html", feature = "webgl2"))]
34                Self::HtmlGL2(c) => c.$name($($arg),*),
35                #[cfg(all(feature = "offscreen", feature = "webgl"))]
36                Self::OffscreenGL(c) => c.$name($($arg),*),
37                #[cfg(all(feature = "offscreen", feature = "webgl2"))]
38                Self::OffscreenGL2(c) => c.$name($($arg),*),
39                #[allow(unreachable_patterns)]
40                _ => panic!("Unsupported variant. Please enable any features."),
41            }
42        }
43    };
44}
45
46#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
47#[non_exhaustive]
48pub enum CaptureMode {
49    /// Put the video frame at `(x, y)` on the capture area
50    /// with original size ignoring capture size.
51    Put(i32, i32),
52    /// Fill the capture area with the entire video frame.
53    /// Same as `object-fit: fill` CSS property.
54    Fill,
55    /// Resize the capture area to fit the entire video frame.
56    /// This is the default mode.
57    #[default]
58    Adjust,
59    /// Put and scale the video frame to cover the capture area
60    /// matching centers.
61    Pinhole,
62}
63
64impl CaptureMode {
65    pub const fn put_top_left() -> Self {
66        Self::Put(0, 0)
67    }
68}
69
70#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
71pub enum CaptureColor {
72    /// Output data as RGBA.
73    #[default]
74    RGBA,
75    /// Output data as RGBA but Alpha channel is luminosity.
76    RGBL,
77    /// Output data as grayscale RGBA.
78    LLLA,
79}
80
81pub trait CaptureArea {
82    /// Get the width of the available capture area in pixels.
83    fn capture_width(&self) -> u32;
84
85    /// Get the height of the available capture area in pixels.
86    fn capture_height(&self) -> u32;
87
88    /// Get the size of the available capture area in pixels.
89    fn capture_size(&self) -> (u32, u32) {
90        (self.capture_width(), self.capture_height())
91    }
92
93    /// Get the capture area in pixels.
94    fn capture_area(&self) -> u32 {
95        self.capture_width() * self.capture_height()
96    }
97
98    /// Set the width for the capture area.
99    fn set_capture_width(&self, width: u32);
100
101    /// Set the height for the capture area.
102    fn set_capture_height(&self, height: u32);
103
104    /// Set the size for the capture area.
105    fn set_capture_size(&self, width: u32, height: u32) {
106        self.set_capture_width(width);
107        self.set_capture_height(height);
108    }
109}
110
111pub trait BrowserVideoCapture: CaptureArea {
112    /// Get the number of channels in the capture buffer.
113    fn channels_count(&self) -> u32 {
114        4
115    }
116
117    #[cfg(feature = "image")]
118    fn color_type(&self) -> image::ColorType {
119        match self.channels_count() {
120            1 => image::ColorType::L8,
121            2 => image::ColorType::La8,
122            3 => image::ColorType::Rgb8,
123            4 => image::ColorType::Rgba8,
124            _ => panic!("Unsupported channels count"),
125        }
126    }
127
128    /// Get the size of the capture buffer in bytes.
129    fn buffer_size(&self) -> usize {
130        (self.capture_area() * self.channels_count()) as usize
131    }
132
133    /// Capture a frame from the video element.
134    fn capture(&self, source: &HtmlVideoElement, mode: CaptureMode) -> (u32, u32);
135
136    /// Retrieve the grabbed frame raw data into the buffer.
137    fn retrieve(&self, buffer: &mut [u8]);
138
139    /// Get the raw data from the captured frame.
140    fn data(&self) -> Vec<u8> {
141        let mut buffer = vec![0; self.buffer_size()];
142        self.retrieve(&mut buffer);
143        buffer
144    }
145
146    #[cfg(feature = "image")]
147    fn image(&self) -> Option<image::DynamicImage> {
148        let (width, height) = self.capture_size();
149        Some(match self.channels_count() {
150            1 => image::DynamicImage::ImageLuma8(image::GrayImage::from_raw(
151                width,
152                height,
153                self.data(),
154            )?),
155            2 => image::DynamicImage::ImageLumaA8(image::GrayAlphaImage::from_raw(
156                width,
157                height,
158                self.data(),
159            )?),
160            3 => image::DynamicImage::ImageRgb8(image::RgbImage::from_raw(
161                width,
162                height,
163                self.data(),
164            )?),
165            4 => image::DynamicImage::ImageRgba8(image::RgbaImage::from_raw(
166                width,
167                height,
168                self.data(),
169            )?),
170            _ => panic!("Unsupported channels count"),
171        })
172    }
173
174    /// Read the raw data from the video element.
175    fn read(&self, source: &HtmlVideoElement, mode: CaptureMode) -> Vec<u8> {
176        let (width, height) = self.capture(source, mode);
177
178        let buffer_size = (width * height * self.channels_count()) as usize;
179
180        if buffer_size > 0 {
181            let mut buffer = vec![0; buffer_size];
182            self.retrieve(&mut buffer);
183            buffer
184        } else {
185            return Vec::new();
186        }
187    }
188
189    /// Clear the capture area.
190    fn clear(&self);
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub enum SupportedCanvas {
195    #[cfg(feature = "html")]
196    Html(web_sys::HtmlCanvasElement),
197    #[cfg(feature = "offscreen")]
198    Offscreen(web_sys::OffscreenCanvas),
199}
200
201#[cfg(feature = "html")]
202impl_enum_from!(web_sys::HtmlCanvasElement => SupportedCanvas:Html);
203#[cfg(feature = "offscreen")]
204impl_enum_from!(web_sys::OffscreenCanvas => SupportedCanvas:Offscreen);
205
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub enum SupportedContext {
208    #[cfg(feature = "html-2d")]
209    Html2D(web_sys::CanvasRenderingContext2d),
210    #[cfg(feature = "offscreen-2d")]
211    Ofscreen2D(web_sys::OffscreenCanvasRenderingContext2d),
212    #[cfg(feature = "webgl")]
213    WebGL(web_sys::WebGlRenderingContext),
214    #[cfg(feature = "webgl2")]
215    WebGL2(web_sys::WebGl2RenderingContext),
216}
217
218#[cfg(feature = "html-2d")]
219impl_enum_from!(web_sys::CanvasRenderingContext2d => SupportedContext:Html2D);
220#[cfg(feature = "offscreen-2d")]
221impl_enum_from!(web_sys::OffscreenCanvasRenderingContext2d => SupportedContext:Ofscreen2D);
222#[cfg(feature = "webgl")]
223impl_enum_from!(web_sys::WebGlRenderingContext => SupportedContext:WebGL);
224#[cfg(feature = "webgl2")]
225impl_enum_from!(web_sys::WebGl2RenderingContext => SupportedContext:WebGL2);
226
227#[derive(Default, Debug, Clone, PartialEq, Eq)]
228pub struct BrowserCaptureBuilder {
229    pub context: Option<SupportedContext>,
230    pub canvas: Option<SupportedCanvas>,
231    pub color: Option<CaptureColor>,
232    pub options: Option<SupportedOptions>,
233}
234
235impl BrowserCaptureBuilder {
236    pub fn color(mut self, color: CaptureColor) -> Self {
237        self.color = Some(color);
238        self
239    }
240
241    pub fn canvas(mut self, canvas: SupportedCanvas) -> Self {
242        self.canvas = Some(canvas);
243        self
244    }
245
246    pub fn context(mut self, context: SupportedContext) -> Self {
247        self.context = Some(context);
248        self
249    }
250
251    pub fn options(mut self, options: SupportedOptions) -> Self {
252        self.options = Some(options);
253        self
254    }
255
256    #[cfg(feature = "html")]
257    pub fn html_canvas(self, canvas: web_sys::HtmlCanvasElement) -> Self {
258        self.canvas(SupportedCanvas::Html(canvas))
259    }
260
261    #[cfg(feature = "offscreen")]
262    pub fn offscreen_canvas(self, canvas: web_sys::OffscreenCanvas) -> Self {
263        self.canvas(SupportedCanvas::Offscreen(canvas))
264    }
265
266    #[cfg(feature = "html-2d")]
267    pub fn html_2d(self, context: web_sys::CanvasRenderingContext2d) -> Self {
268        self.context(SupportedContext::Html2D(context))
269    }
270
271    #[cfg(feature = "offscreen-2d")]
272    pub fn offscreen_2d(self, context: web_sys::OffscreenCanvasRenderingContext2d) -> Self {
273        self.context(SupportedContext::Ofscreen2D(context))
274    }
275
276    #[cfg(feature = "webgl")]
277    pub fn webgl(self, context: web_sys::WebGlRenderingContext) -> Self {
278        self.context(SupportedContext::WebGL(context))
279    }
280
281    #[cfg(feature = "webgl2")]
282    pub fn webgl2(self, context: web_sys::WebGl2RenderingContext) -> Self {
283        self.context(SupportedContext::WebGL2(context))
284    }
285
286    pub fn build(self) -> Option<Result<BrowserCapture, js_sys::Error>> {
287        match (self.canvas, self.context, self.options) {
288            #[cfg(feature = "html-2d")]
289            (Some(SupportedCanvas::Html(canvas)), Some(SupportedContext::Html2D(context)), _) => {
290                Some(Ok(HtmlCapture2D::new(
291                    canvas,
292                    context,
293                    self.color.unwrap_or_default(),
294                )
295                .into()))
296            }
297            #[cfg(feature = "html-2d")]
298            (
299                Some(SupportedCanvas::Html(canvas)),
300                None,
301                Some(SupportedOptions::Html2D(options)),
302            ) => Some(
303                HtmlCapture2D::from_canvas_with_options(
304                    canvas,
305                    self.color.unwrap_or_default(),
306                    options,
307                )
308                .transpose()?
309                .map(Into::into),
310            ),
311            #[cfg(feature = "offscreen-2d")]
312            (
313                Some(SupportedCanvas::Offscreen(canvas)),
314                Some(SupportedContext::Ofscreen2D(context)),
315                _,
316            ) => Some(Ok(OffscreenCapture2D::new(
317                canvas,
318                context,
319                self.color.unwrap_or_default(),
320            )
321            .into())),
322            #[cfg(feature = "offscreen-2d")]
323            (
324                Some(SupportedCanvas::Offscreen(canvas)),
325                None,
326                Some(SupportedOptions::Offscreen2D(options)),
327            ) => Some(
328                OffscreenCapture2D::from_canvas_with_options(
329                    canvas,
330                    self.color.unwrap_or_default(),
331                    options,
332                )
333                .transpose()?
334                .map(Into::into),
335            ),
336            #[cfg(all(feature = "html", feature = "webgl"))]
337            (Some(SupportedCanvas::Html(canvas)), Some(SupportedContext::WebGL(context)), _) => {
338                Some(Ok(HtmlCaptureGL::new(
339                    canvas,
340                    context,
341                    self.color.unwrap_or_default(),
342                )
343                .into()))
344            }
345            #[cfg(all(feature = "html", feature = "webgl2"))]
346            (Some(SupportedCanvas::Html(canvas)), Some(SupportedContext::WebGL2(context)), _) => {
347                Some(Ok(HtmlCaptureGL2::new(
348                    canvas,
349                    context,
350                    self.color.unwrap_or_default(),
351                )
352                .into()))
353            }
354            #[cfg(all(feature = "html", feature = "webgl"))]
355            (
356                Some(SupportedCanvas::Html(canvas)),
357                None,
358                Some(SupportedOptions::HtmlGL(options)),
359            ) if matches!(options.version, GLVersion::WebGL) => Some(
360                HtmlCaptureGL::from_canvas_with_options(
361                    canvas,
362                    self.color.unwrap_or_default(),
363                    options,
364                )
365                .transpose()?
366                .map(|c| c.validate().ok())
367                .transpose()?
368                .map(Into::into),
369            ),
370            #[cfg(all(feature = "html", feature = "webgl2"))]
371            (
372                Some(SupportedCanvas::Html(canvas)),
373                None,
374                Some(SupportedOptions::HtmlGL(options)),
375            ) if matches!(options.version, GLVersion::WebGL2) => Some(
376                HtmlCaptureGL2::from_canvas_with_options(
377                    canvas,
378                    self.color.unwrap_or_default(),
379                    options,
380                )
381                .transpose()?
382                .map(|c| c.validate().ok())
383                .transpose()?
384                .map(Into::into),
385            ),
386            #[cfg(all(feature = "offscreen", feature = "webgl"))]
387            (
388                Some(SupportedCanvas::Offscreen(canvas)),
389                Some(SupportedContext::WebGL(context)),
390                _,
391            ) => Some(Ok(OffscreenCaptureGL::new(
392                canvas,
393                context,
394                self.color.unwrap_or_default(),
395            )
396            .into())),
397            #[cfg(all(feature = "offscreen", feature = "webgl2"))]
398            (
399                Some(SupportedCanvas::Offscreen(canvas)),
400                Some(SupportedContext::WebGL2(context)),
401                _,
402            ) => Some(Ok(OffscreenCaptureGL2::new(
403                canvas,
404                context,
405                self.color.unwrap_or_default(),
406            )
407            .into())),
408            #[cfg(all(feature = "offscreen", feature = "webgl"))]
409            (
410                Some(SupportedCanvas::Offscreen(canvas)),
411                None,
412                Some(SupportedOptions::OffscreenGL(options)),
413            ) if matches!(options.version, GLVersion::WebGL) => Some(
414                OffscreenCaptureGL::from_canvas_with_options(
415                    canvas,
416                    self.color.unwrap_or_default(),
417                    options,
418                )
419                .transpose()?
420                .map(|c| c.validate().ok())
421                .transpose()?
422                .map(Into::into),
423            ),
424            #[cfg(all(feature = "offscreen", feature = "webgl"))]
425            (
426                Some(SupportedCanvas::Offscreen(canvas)),
427                None,
428                Some(SupportedOptions::OffscreenGL(options)),
429            ) if matches!(options.version, GLVersion::WebGL2) => Some(
430                OffscreenCaptureGL2::from_canvas_with_options(
431                    canvas,
432                    self.color.unwrap_or_default(),
433                    options,
434                )
435                .transpose()?
436                .map(|c| c.validate().ok())
437                .transpose()?
438                .map(Into::into),
439            ),
440            _ => None,
441        }
442    }
443}
444
445pub use utils::video_size;
446
447#[cfg(all(feature = "html", feature = "2d"))]
448pub use d2::html::ColorSpaceType;
449#[cfg(feature = "html-2d")]
450pub use d2::html::{HtmlCapture2D, HtmlContextOptions2D};
451#[cfg(all(feature = "offscreen", feature = "2d"))]
452pub use d2::offscreen::OffscreenStorageType;
453#[cfg(feature = "offscreen-2d")]
454pub use d2::offscreen::{OffscreenCapture2D, OffscreenContextOptions2D};
455
456#[cfg(feature = "gl")]
457pub use gl::GLVersion;
458#[cfg(all(feature = "html", feature = "gl"))]
459pub use gl::html::{PowerPreference, HtmlContextOptionsGL};
460#[cfg(all(feature = "offscreen", feature = "gl"))]
461pub use gl::offscreen::OffscreenContextOptionsGL;
462
463#[cfg(all(feature = "html", feature = "webgl"))]
464pub use gl::html::HtmlCaptureGL;
465#[cfg(all(feature = "html", feature = "webgl2"))]
466pub use gl::html::HtmlCaptureGL2;
467#[cfg(all(feature = "offscreen", feature = "webgl"))]
468pub use gl::offscreen::OffscreenCaptureGL;
469#[cfg(all(feature = "offscreen", feature = "webgl2"))]
470pub use gl::offscreen::OffscreenCaptureGL2;
471
472#[derive(Debug, Clone, PartialEq, Eq)]
473pub enum BrowserCapture {
474    #[cfg(feature = "html-2d")]
475    Html2D(HtmlCapture2D),
476    #[cfg(feature = "offscreen-2d")]
477    Offscreen2D(OffscreenCapture2D),
478    #[cfg(all(feature = "html", feature = "webgl"))]
479    HtmlGL(HtmlCaptureGL),
480    #[cfg(all(feature = "html", feature = "webgl2"))]
481    HtmlGL2(HtmlCaptureGL2),
482    #[cfg(all(feature = "offscreen", feature = "webgl"))]
483    OffscreenGL(OffscreenCaptureGL),
484    #[cfg(all(feature = "offscreen", feature = "webgl2"))]
485    OffscreenGL2(OffscreenCaptureGL2),
486}
487
488#[cfg(feature = "html-2d")]
489impl_enum_from!(HtmlCapture2D => BrowserCapture:Html2D);
490#[cfg(feature = "offscreen-2d")]
491impl_enum_from!(OffscreenCapture2D => BrowserCapture:Offscreen2D);
492#[cfg(all(feature = "html", feature = "webgl"))]
493impl_enum_from!(HtmlCaptureGL => BrowserCapture:HtmlGL);
494#[cfg(all(feature = "offscreen", feature = "webgl"))]
495impl_enum_from!(OffscreenCaptureGL => BrowserCapture:OffscreenGL);
496#[cfg(all(feature = "html", feature = "webgl2"))]
497impl_enum_from!(HtmlCaptureGL2 => BrowserCapture:HtmlGL2);
498#[cfg(all(feature = "offscreen", feature = "webgl2"))]
499impl_enum_from!(OffscreenCaptureGL2 => BrowserCapture:OffscreenGL2);
500
501#[derive(Debug, Clone, Copy, PartialEq, Eq)]
502pub enum SupportedOptions {
503    #[cfg(feature = "html-2d")]
504    Html2D(HtmlContextOptions2D),
505    #[cfg(feature = "offscreen-2d")]
506    Offscreen2D(OffscreenContextOptions2D),
507    #[cfg(all(feature = "html", feature = "gl"))]
508    HtmlGL(HtmlContextOptionsGL),
509    #[cfg(all(feature = "offscreen", feature = "gl"))]
510    OffscreenGL(OffscreenContextOptionsGL),
511}
512
513#[cfg(feature = "html-2d")]
514impl_enum_from!(HtmlContextOptions2D => SupportedOptions:Html2D);
515#[cfg(feature = "offscreen-2d")]
516impl_enum_from!(OffscreenContextOptions2D => SupportedOptions:Offscreen2D);
517#[cfg(all(feature = "html", feature = "gl"))]
518impl_enum_from!(HtmlContextOptionsGL => SupportedOptions:HtmlGL);
519#[cfg(all(feature = "offscreen", feature = "gl"))]
520impl_enum_from!(OffscreenContextOptionsGL => SupportedOptions:OffscreenGL);
521
522impl From<BrowserCapture> for Box<dyn BrowserVideoCapture> {
523    fn from(value: BrowserCapture) -> Self {
524        match value {
525            #[cfg(feature = "html-2d")]
526            BrowserCapture::Html2D(c) => Box::new(c),
527            #[cfg(feature = "offscreen-2d")]
528            BrowserCapture::Offscreen2D(c) => Box::new(c),
529            #[cfg(all(feature = "html", feature = "webgl"))]
530            BrowserCapture::HtmlGL(c) => Box::new(c),
531            #[cfg(all(feature = "html", feature = "webgl2"))]
532            BrowserCapture::HtmlGL2(c) => Box::new(c),
533            #[cfg(all(feature = "offscreen", feature = "webgl"))]
534            BrowserCapture::OffscreenGL(c) => Box::new(c),
535            #[cfg(all(feature = "offscreen", feature = "webgl2"))]
536            BrowserCapture::OffscreenGL2(c) => Box::new(c),
537        }
538    }
539}
540
541#[allow(unused_variables)]
542impl CaptureArea for BrowserCapture {
543    enum_method!(capture_width () => u32);
544    enum_method!(capture_height () => u32);
545    enum_method!(set_capture_width (width: u32) => ());
546    enum_method!(set_capture_height (height: u32) => ());
547}
548
549#[allow(unused_variables)]
550impl BrowserVideoCapture for BrowserCapture {
551    enum_method!(channels_count () => u32);
552    enum_method!(buffer_size () => usize);
553    enum_method!(capture (source: &HtmlVideoElement, mode: CaptureMode) => (u32, u32));
554    enum_method!(retrieve (buffer: &mut [u8]) => ());
555    enum_method!(data () => Vec<u8>);
556    #[cfg(feature = "image")]
557    enum_method!(image () => Option<image::DynamicImage>);
558    enum_method!(read (source: &HtmlVideoElement, mode: CaptureMode) => Vec<u8>);
559    enum_method!(clear () => ());
560}