使用 Rust 和 WebAssembly 处理视频源中的像素
什么是 Web Assembly?
如何处理来自远程视频源的像素?
在Streem,我们的使命是让世界更容易获得专业知识。我们创建引导工具来引导讨论,并确保第一时间获得准确理解。我们正在为 Web 开发的引导工具之一是可在远程视频中定位的 3D 光标。为了实现这一点,我们需要处理每帧大量的原始像素数据和 AR 数据。
在 AR 中定位远程对象需要在动画帧之间进行大量计算。计算量如此之大,一篇文章根本无法涵盖。在本文中,我将讨论如何使用 Rust 从视频帧中访问原始像素数据。
如果您想直接跳到代码,请跳转到此处并给此 repo 一个⭐
什么是 Web Assembly?
WebAssembly (wasm) 是一种可以在 Web 浏览器和移动设备上运行的代码。wasm 的设计目标是成为 C、C++ 和 Rust 等低级语言的编译目标。借助 wasm,Web 浏览器和移动设备现在可以利用常见的硬件功能,以接近原生的速度运行多种语言编写的代码。
Wasm 被引入到所有现代 Web 浏览器中,以帮助扩展 JavaScript 的功能。由于 JavaScript 完全控制着 WebAssembly 代码的下载、编译和运行方式,因此 JavaScript 开发者可以将 wasm 视为高效创建高性能函数的功能。
在这个演示中,我们使用 WebAssembly 从远程视频源中提取原始像素数据。本指南将详细介绍 WebAssembly,但不涵盖如何设置 WebAssembly 项目。我们提供一些工具和教程,帮助您开始下一个 WebAssembly 项目。如果您是 Rust 新手,那么您应该观看 Tensor Programming 的“Rust 入门”播放列表。
如何处理来自远程视频源的像素?
为了处理视频每一帧的原始像素数据,我们使用了MediaStream对象中的视频轨道,并使用该轨道创建了 HtmlVideoElement。之后,该视频元素可以作为画布的源,用于绘制图像。当图像以 60fps 的速度绘制到画布上时,我们可以使用CanvasRenderingContext2D.getImageData()访问原始底层像素数据。
下面是一个高级图表,演示了如何将单个视频帧绘制到画布元素上。将视频帧绘制到画布元素上后,您就可以访问原始像素数据了。
在了解如何从帧中访问原始像素数据后,我们引入了 Rust 和 wasm。我们希望 JavaScript 和 Rust 之间的接口简洁,因此我们负责RenderingEngine
两件事:
- 为我们处理的视频帧注册目标画布以进行渲染
- 处理视频源的每一帧
注册目标画布
目标画布是我们处理过的视频帧将要呈现的地方。
动态加载 wasm 后,我们可以调用它add_target_canvas
来注册渲染目标RenderingEngine
const renderingEngine = new wasm.RenderingEngine();
renderingEngine.add_target_canvas(canvas)
这RenderingEngine
是一个占用三个私有字段的结构
canvas
用于解析 LightShow 数据的缓冲区画布render_targets
用于渲染最终帧的画布元素向量cancel
停止在画布上渲染帧的信号
pub struct RenderingEngine {
canvas: Rc<RenderingEngineCanvas>,
render_targets: Rc<RefCell<Vec<RenderingEngineCanvas>>>,
cancel: Rc<RefCell<bool>>,
}
这些字段中的每一个都包装在 Rust 的引用计数器(Rc) 中。sRc
启用数据的共享所有权。Rc
当我们需要同时对一个不可变值进行多个引用时,使用A。Rc
指针与 Rust 通常的引用不同,虽然它们是在堆上分配的,但克隆Rc
指针不会导致新的堆分配。相反,内部的计数器Rc
会递增。我们将看到如何在动画循环中使用它。这是必需的,因为我们无法在 wasm_bindgen 中使用生命周期。请参阅此问题。
我们内部Rc
有一个RefCell
,当存在对数据的不可变引用时,它为我们提供了一种修改数据的方法。我们需要在应用程序运行时添加 manyrender_targets
并修改我们的cancel
标志。简而言之,RefCell
让你获取&mut
内容的引用。当我们使用 时Rc<RefCell<T>>
,我们表示我们在应用程序中拥有共享的、可变的数据所有权。
在 Rust 中,add_target_canvas
是一个通过 公开的公共方法wasm_bindgen
。需要注意的是,此方法使用&mut self
。此引用类型允许您self
在不获取其所有权的情况下进行修改。
#[derive(Debug)]
struct RenderingEngineCanvas {
element: HtmlCanvasElement,
context_2d: CanvasRenderingContext2d,
}
#[wasm_bindgen]
#[derive(Debug)]
pub struct RenderingEngine {
canvas: Rc<RenderingEngineCanvas>,
render_targets: Rc<RefCell<Vec<RenderingEngineCanvas>>>,
cancel: Rc<RefCell<bool>>,
}
#[wasm_bindgen]
impl RenderingEngine {
#[wasm_bindgen(constructor)]
pub fn new() -> RenderingEngine {
let canvas = Rc::new(RenderingEngine::create_buffer_canvas());
let render_targets = Rc::new(RefCell::new(Vec::new()));
let cancel = Rc::new(RefCell::new(false));
RenderingEngine {
canvas,
render_targets,
cancel,
}
}
#[wasm_bindgen(method)]
pub fn add_target_canvas(&mut self, canvas: HtmlCanvasElement) {
// Obtain 2D context from canvas
let context = canvas
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()
.expect("failed to obtain 2d rendering context for target <canvas>");
// Create a struct
let container = RenderingEngineCanvas {
element: canvas,
context_2d: context,
};
// Update instance of rendering engine
let mut render_targets = self.render_targets.borrow_mut();
render_targets.push(container);
}
}
处理视频源的每一帧
处理视频源中的每一帧更为复杂。我将省略许多细节,但您可以探索GitHub 仓库,查看完整的代码示例。
在 JavaScript 中,我们可以通过一个方法调用动画循环start
。它唯一的参数是通过请求用户媒体MediaStream
获取的对象。
const renderingEngine = new wasm.RenderingEngine();
renderingEngine.add_target_canvas(canvas)
const userMedia = await navigator.mediaDevices.getUserMedia(someContraints);
renderingEngine.start(userMedia);
在 Rust 中,我们创建一个 HTMLVideoElement 并启动动画循环。使用start_animation_loop
,我们克隆动画循环中将要用到的值。
video
是必需的,这样我们就可以从中获得它的尺寸和框架。canvas
是我们的缓冲画布,因此我们可以处理像素数据cancel
是我们可以用来触发动画循环停止的信号render_targets
是 JS 上所有需要渲染最终图像的目标画布。
还有两个新的常量f
和。我们希望在视频结束前每一帧都g
调用它。视频源结束后,我们希望清理所有资源。我们将使用它来存储我们想要在每一帧执行的闭包,并启动它。requestAnimationFrame
f
g
我们创建的闭包存储g
在第一帧中。我们调用borrow_mut
来获取内部值的可变引用RefCell::new(None)
。
我们从rustwasm 的这个 PR 中学到了很多关于这方面的知识,以及如何在匿名函数中捕获环境
#[wasm_bindgen(method)]
pub fn start(&self, media_stream: &MediaStream) {
let video = RenderingEngine::create_video_element(media_stream);
&self.start_animation_loop(&video);
}
fn start_animation_loop(&self, video: &Rc<HtmlVideoElement>) {
let video = video.clone();
let canvas = self.canvas.clone();
let cancel = self.cancel.clone();
let render_targets = self.render_targets.clone();
let f = Rc::new(RefCell::new(None));
let g = f.clone();
*g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
// clean up f when cancel is set to true
if *cancel.borrow() == true {
let _ = f.borrow_mut().take();
return;
}
// continuously animate with the value of f.
RenderingEngine::request_animation_frame(
f.borrow().as_ref().unwrap()
}) as Box<dyn FnMut()>));
// start the animation loop here for 1 frame, drop g.
RenderingEngine::request_animation_frame(g.borrow().as_ref().unwrap());
}
// Note this method call, which uses `as_ref()` to get a `JsValue`
// from our `Closure` which is then converted to a `&Function`
// using the `JsCast::unchecked_ref` function.
fn request_animation_frame(n: &Closure<dyn FnMut()>) {
RenderingEngine::get_window()
.request_animation_frame(n.as_ref().unchecked_ref())
.expect("should register `requestAnimationFrame` OK");
}
通过一个用闭包包裹的JavaScript 函数来执行,我们可以处理视频帧的像素数据。下面的代码示例会比较简单,不过您可以在这里找到原始代码。
// inside our animation loop
// obtain video dimensions
let video_dimensions = Dimensions {
width: video.video_width() as f64,
height: video.video_height() as f64,
};
// draw frame onto buffer canvas
// perform any pixel manipulation you need on this canvas
canvas.element.set_width(video_dimensions.width as u32);
canvas.element.set_height(video_dimensions.height as u32);
canvas.context_2d.draw_image_with_html_video_element(&video, 0.0, 0.0).expect("failed to draw video frame to <canvas> element");
// render resulting image onto target canvas
for target in render_targets.borrow().iter() {
// Use scrollWidth/scrollHeight so we fill the canvas element.
let target_dimensions = Dimensions {
width: target.element.scroll_width() as f64,
height: target.element.scroll_height() as f64,
};
let scaled_dimensions = RenderingEngine::get_scaled_video_size(
&video_dimensions,
&target_dimensions,
);
let offset = Dimensions {
width: (target_dimensions.width - scaled_dimensions.width) / 2.0,
height: (target_dimensions.height - scaled_dimensions.height) / 2.0,
};
// Ensure the target canvas has a set width/height, otherwise rendering breaks. target.element.set_width(target_dimensions.width as u32);
target.element.set_height(target_dimensions.height as u32);
target.context_2d.draw_image_with_html_canvas_element_and_dw_and_dh(
&canvas.element,
offset.width,
offset.height,
scaled_dimensions.width,
scaled_dimensions.height,
).expect("failed to draw buffer <canvas> to target <canvas>");
}
如果你喜欢这个例子,并且想了解更多关于 Rust、WebAssembly 和 TypeScript 的知识,欢迎告诉我!欢迎在这里留言或在 Twitter 上关注我。
文章来源:https://dev.to/fallenstedt/using-rust-and-web assembly-to-process-pixels-from-a-video-feed-4hhg