Rust 赋能前端: 视频抽帧
<blockquote class="multiquote-1" style="margin-top: 20px; margin-bottom: 20px; margin-left: 0px; margin-right: 0px; padding-top: 10px; padding-bottom: 10px; padding-left: 20px; padding-right: 10px; border-top-style: solid; border-bottom-style: solid; border-left-style: solid; border-right-style: solid; border-top-width: 1px; border-bottom-width: 1px; border-left-width: 1px; border-right-width: 1px; border-top-color: rgba(222, 198, 251, 0.4); border-bottom-color: rgba(222, 198, 251, 0.4); border-left-color: rgba(222, 198, 251, 0.4); border-right-color: rgba(222, 198, 251, 0.4); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-attachment: scroll; background-clip: border-box; background-color: rgb(246, 238, 255); background-image: none; background-origin: padding-box; background-position-x: 0%; background-position-y: 0%; background-repeat: no-repeat; background-size: auto; width: auto; height: auto; box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px; display: block; overflow-x: auto; overflow-y: auto;"> ❝ 假如你能想得到,就能做得到各人好,我是柒八九。一个专注于前端开发技术/Rust及AI应用知识分享的Coder
<blockquote class="multiquote-1" style="margin-top: 20px; margin-bottom: 20px; margin-left: 0px; margin-right: 0px; padding-top: 10px; padding-bottom: 10px; padding-left: 20px; padding-right: 10px; border-top-style: solid; border-bottom-style: solid; border-left-style: solid; border-right-style: solid; border-top-width: 1px; border-bottom-width: 1px; border-left-width: 1px; border-right-width: 1px; border-top-color: rgba(222, 198, 251, 0.4); border-bottom-color: rgba(222, 198, 251, 0.4); border-left-color: rgba(222, 198, 251, 0.4); border-right-color: rgba(222, 198, 251, 0.4); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-attachment: scroll; background-clip: border-box; background-color: rgb(246, 238, 255); background-image: none; background-origin: padding-box; background-position-x: 0%; background-position-y: 0%; background-repeat: no-repeat; background-size: auto; width: auto; height: auto; box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px; display: block; overflow-x: auto; overflow-y: auto;"> ❝ 此篇文章所涉及到的技术有
[*] WebAssembly
[*] Rust
[*] wasm-bindgen
[*] 线程池
[*] Vite+React/Vue(下面的内容,在各种前端框架中都用)
因为,行笔墨数所限,有些概念可能会一带而过亦或者提供对应的学习资料。请各人酌情观看。
前言
老粉都知道,我之前接手了一个内容审读的开发需求。它是个啥呢,它需要对各种文档资源举行解析和展示。
在上周呢,我们写了一篇Rust 赋能前端:PDF 分页/关键词标注/转图片/抽取文本/抽取图片/翻转...,在内里先容怎样在前端环境中(React/Vue)中使用Mupdf,用于实行各种PDF的操纵。
在我们体系中,有一个需求就是视频抽帧。也就是对一个视频资源基于某些特征将其关键帧抽离成图片信息。然后对其举行OCR识别,而且基于关键字标注处理。
我们本日就来讲讲怎样使用WebAssembly对视频资源举行抽帧处理。对于OCR部分我们会单开一篇。
效果展示
https://img-blog.csdnimg.cn/img_convert/6b792c74db8f179c89a0501817f16b14.gif 可以看到,我们将一个时长快5分多的视频,仅用时8秒(颠簸在5-9秒之间)就将其抽成69个关键帧。
好了,天不早了,干点正事哇。
https://img-blog.csdnimg.cn/img_convert/c1a503460dc51cbddcbe6e8a93c5de9e.gif 我们能所学到的知识点
<blockquote class="multiquote-1" style="margin-top: 20px; margin-bottom: 20px; margin-left: 0px; margin-right: 0px; padding-top: 10px; padding-bottom: 10px; padding-left: 20px; padding-right: 10px; border-top-style: solid; border-bottom-style: solid; border-left-style: solid; border-right-style: solid; border-top-width: 1px; border-bottom-width: 1px; border-left-width: 1px; border-right-width: 1px; border-top-color: rgba(222, 198, 251, 0.4); border-bottom-color: rgba(222, 198, 251, 0.4); border-left-color: rgba(222, 198, 251, 0.4); border-right-color: rgba(222, 198, 251, 0.4); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-attachment: scroll; background-clip: border-box; background-color: rgb(246, 238, 255); background-image: none; background-origin: padding-box; background-position-x: 0%; background-position-y: 0%; background-repeat: no-repeat; background-size: auto; width: auto; height: auto; box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px; display: block; overflow-x: auto; overflow-y: auto;"> ❝
[*] 项目初始化
[*] 技术选择的初衷
[*] Rust + WebAssembly 抽帧处理
1. 项目初始化
还是一样的套路,我们还是基于f_cli_f来构建的前端Vite+React+TS项目。
当我们通过yarn/npm安装好对应的包时。我们就可以在pages新建一个Video2Img的目次,然后直接构建一个index.tsx即可。
随后,我们在src目次下构建一个wasm目次来存放在前端项目中要用到的各种wasm。针对这个例子,我们构建一个video2Img的目次,用于存放Rust编译后的文件。
2. 技术选择的初衷
实在呢,针对视频抽帧的需求,我们可以不消WebAssembly来处理。直接使用浏览器原生API就可以实现。
用原生API实现视频抽帧
const extractFrames = async (file: File) => {<br /> const video = document.createElement('video');<br /> const src = URL.createObjectURL(file);<br /> video.src = src;<br /> await video.play();<br /> video.pause(); // 暂停播放以进行逐帧处理<br /><br /> const canvas = document.createElement('canvas');<br /> const ctx = canvas.getContext('2d');<br /><br /> if (!ctx) return;<br /><br /> canvas.width = video.videoWidth;<br /> canvas.height = video.videoHeight;<br /><br /> const frameArray: string[] = [];<br /> const totalFrames = Math.floor(video.duration * frameRate); <br /><br /> for (let i = 0; i < totalFrames; i++) {<br /> video.currentTime = i / frameRate; // 设置视频当前时间<br /><br /> await new Promise<void>((resolve) => {<br /> video.onseeked = () => {<br /> ctx.drawImage(video, 0, 0, canvas.width, canvas.height);<br /> frameArray.push(canvas.toDataURL('image/jpeg'));<br /> resolve();<br /> };<br /> });<br /> }<br /> <br /> };<br />上面的代码就是使用原生API实现视频抽帧的简单版本。
它的重要核心点就是
[*] 创建视频和画布元素:使用 <video> 元素加载视频文件,并创建 <canvas> 用于捕捉视频帧。
[*] 逐帧提取图像:
[*] 通过 video.currentTime 设置视频的播放时间。
[*] 使用 onseeked 变乱 在视频跳转到特定时间后,捕捉当前帧。
[*] 渲染帧到画布:将视频帧绘制到画布中,然后使用 canvas.toDataURL 将帧转换为 ( "Base64 编码")的 JPEG 图像。
[*] 异步处理:使用 await 和 Promise 确保每帧的提取和处理按顺序举行。
当然,上面的代码只是一个简单版本,实在还有很多的优化空间。
例如:上面的代码在直接调用 video.play() 后停息并逐帧处理,但没有等待视频元数据(如时长、帧率、宽高等)加载完成。假如不等元数据加载,视频可能还没有完全预备好,导致一些延迟。
通过监听视频的 loadedmetadata 变乱,可以确保在开始处理之前,全部必要的元数据(如视频时长、宽度、高度)都已加载完成。这能制止因元数据未加载完而导致的时间跳转缓慢或帧提取卡顿,从而提升整体处理效率。
选择使用Rust+WebAssembly的原因
各人从上面的代码核心点可知。在处理过程中,出现了几种数据范例。
[*] 视频资源
[*] video元素
[*] canvas
[*] image
还记得之前我们写过宝贝,带上WebAssembly,换个姿势来优化你的前端应用其中有一节就是讲到,假如在前端绘制内容比较复杂的图片资源时,可以往Rust+WebAssembly上靠。
还记得这两张图吗? https://img-blog.csdnimg.cn/img_convert/2d3fe584eda7820c82d153aae74625e1.png
https://img-blog.csdnimg.cn/img_convert/c2f42013666f79d4408d0e38fac73e62.png 上面的示例,可能在有些同砚眼中有点牵强。然后,我们继续来说别的一个我选择使用Rust+WebAssembly处理视频抽帧的。
假如各人写过Rust+WebAssembly的步伐的话,想必肯定听说过大名鼎鼎的 - wasm-bindgen。假如想在Rust使用浏览器的一些API例如(Document/Window等)还离不开它 - web-sys
然后,不知道各人注意过这个例子不 - Parallel Raytracing。它是使用Rust的多线程操纵同一份数据并将这些数据信息绘制到一个canvas中。(强烈发起各人在浏览器实操一下)
下面是它的效果图
https://img-blog.csdnimg.cn/img_convert/479f78bd24ce3f3ed4e034649c8c84a7.png 单线程绘制,耗时1.2秒 https://img-blog.csdnimg.cn/img_convert/cc31b1418f639cf67fd55c5155616e8d.png 多线程,耗时0.8秒 上面的绘制时间,实在远远不是这个比例。
看到上面的两个的示例,这就在心底埋下了种子 - 使用Rust+WebAssembly+多线程 举行视频抽帧。
<blockquote class="multiquote-1" style="margin-top: 20px; margin-bottom: 20px; margin-left: 0px; margin-right: 0px; padding-top: 10px; padding-bottom: 10px; padding-left: 20px; padding-right: 10px; border-top-style: solid; border-bottom-style: solid; border-left-style: solid; border-right-style: solid; border-top-width: 1px; border-bottom-width: 1px; border-left-width: 1px; border-right-width: 1px; border-top-color: rgba(222, 198, 251, 0.4); border-bottom-color: rgba(222, 198, 251, 0.4); border-left-color: rgba(222, 198, 251, 0.4); border-right-color: rgba(222, 198, 251, 0.4); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-attachment: scroll; background-clip: border-box; background-color: rgb(246, 238, 255); background-image: none; background-origin: padding-box; background-position-x: 0%; background-position-y: 0%; background-repeat: no-repeat; background-size: auto; width: auto; height: auto; box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px; display: block; overflow-x: auto; overflow-y: auto;"> ❝ 至于是否能成功,你不试试咋知道成不成功呢。
下面,我们来尝试用Rust+WebAssembly实现抽帧的逻辑。
3. Rust + WebAssembly 抽帧处理
Rust项目初始化
使用cargo new --lib audio2img的Rust的项目。
然后,使用cargo install 安装对应的wasm-bindgen/js-sys/web-sys等包。
因为,我们在Rust中需要用到Document/HtmlCanvasElement等信息,我们还需要对web-sys做一些配置。
最后在Cargo.toml中有如下的安装信息。
name = "audio2img"
version = "0.1.0"
edition = "2021"
futures = "0.3.30"
js-sys = "0.3.69"
serde-wasm-bindgen = "0.6.5"
wasm-bindgen = "0.2.92"
wasm-bindgen-futures = "0.4.42"
crate-type = ["cdylib"]
version = "0.3"
features = [
'Window',
'Document',
'Element',
'HtmlCanvasElement',
'CanvasRenderingContext2d',
'HtmlVideoElement',
'Event'
]
实现JS版本的平替版本(简单版本)
在上一节中我们先容过,我们完全可以用原生js来实现抽帧处理。那么,我们所要做的就是,实现一个最简单版本的Rust视频抽帧版本。
话不多说,我们直接上代码。在src/lib.rs中直接操纵。
use futures::channel::oneshot;<br />use wasm_bindgen::prelude::*;<br />use wasm_bindgen_futures::JsFuture;<br />use web_sys::{ window, HtmlVideoElement, HtmlCanvasElement, CanvasRenderingContext2d };<br />use js_sys::Promise;<br />use serde_wasm_bindgen::to_value;<br />use std::rc::Rc;<br />use std::cell::RefCell;<br /><br />#<br />pub async fn extract_frames_from_url(video_url: &str, frame_rate: f64) -> Result<JsValue, JsValue> {<br /> // 创建 video 元素<br /> let document = window().unwrap().document().unwrap();<br /> let video_element = document.create_element("video")?.dyn_into::<HtmlVideoElement>()?;<br /><br /> // 设置视频来源<br /> video_element.set_src(video_url);<br /> video_element.set_cross_origin(Some("anonymous"));<br /><br /> // 加载视频元数据<br /> let loaded_metadata_promise = Promise::new(<br /> &mut (|resolve, _| {<br /> let resolve = Rc::new(RefCell::new(Some(resolve)));<br /> let onloadedmetadata_closure = Closure::wrap(<br /> Box::new(move || {<br /> if let Some(resolve) = resolve.borrow_mut().take() {<br /> resolve.call0(&JsValue::NULL).unwrap();<br /> }<br /> }) as Box<dyn FnMut()><br /> );<br /> video_element.set_onloadedmetadata(<br /> Some(onloadedmetadata_closure.as_ref().unchecked_ref())<br /> );<br /> onloadedmetadata_closure.forget();<br /> })<br /> );<br /> JsFuture::from(loaded_metadata_promise).await?;<br /><br /> // 暂停视频<br /> video_element.pause()?;<br /><br /> // 创建 canvas 元素<br /> let canvas_element = document.create_element("canvas")?.dyn_into::<HtmlCanvasElement>()?;<br /> canvas_element.set_width(video_element.video_width() as u32);<br /> canvas_element.set_height(video_element.video_height() as u32);<br /><br /> // 获取 canvas 上下文<br /> let ctx = canvas_element.get_context("2d")?.unwrap().dyn_into::<CanvasRenderingContext2d>()?;<br /><br /> let mut frame_array = Vec::new();<br /> let total_frames = (video_element.duration() * frame_rate) as i32;<br /><br /> for i in 0..total_frames {<br /> video_element.set_current_time((i as f64) / frame_rate);<br /><br /> // 通过监听 onseeked 事件同步获取帧<br /> let (sender, receiver) = oneshot::channel();<br /> let sender = Rc::new(RefCell::new(Some(sender)));<br /> let onseeked_closure = Closure::wrap(<br /> Box::new(move || {<br /> if let Some(sender) = sender.borrow_mut().take() {<br /> let _ = sender.send(());<br /> }<br /> }) as Box<dyn FnMut()><br /> );<br /> video_element.set_onseeked(Some(onseeked_closure.as_ref().unchecked_ref()));<br /> onseeked_closure.forget();<br /><br /> // 等待 onseeked 事件触发<br /> receiver.await.unwrap();<br /><br /> ctx.draw_image_with_html_video_element(&video_element, 0.0, 0.0).unwrap();<br /> let frame_data = canvas_element.to_data_url().unwrap();<br /> frame_array.push(frame_data);<br /> }<br /><br /> Ok(to_value(&frame_array).unwrap())<br />}<br />由于这段代码比较简单,同时呢为了兼顾差别同砚的Rust审读能力。我们就对这段代码做一下比较具体的讲解。
重要逻辑总结
[*] 创建并配置 HTML 元素:创建 video 和 canvas 元素,设置视频源并调整 canvas 尺寸。
[*] 等待视频元数据加载:通过 onloadedmetadata 确保视频元数据加载完成,制止提前操纵视频。
[*] 逐帧跳转并捕捉帧:
[*] 使用 set_current_time 来逐帧调整视频的当前时间。
[*] 通过监听 onseeked 变乱确保每次时间跳转后处理帧。
[*] 绘制帧到 canvas:将每一帧绘制到 canvas,然后转换为 Base64 格式的图像数据。
[*] 返回帧数据:将帧数据数组通过 WebAssembly 和 Rust 返回给 JavaScript。
可以看到,我们除了第5步和之前的JS逻辑差别,其他的代码思绪都和之前的出奇的一致。
代码逻辑
然后,我们再对Rust的代码做一次较为具体的解读。
[*] 创建 video 元素:
let document = window().unwrap().document().unwrap();<br />let video_element = document.create_element("video")?.dyn_into::<HtmlVideoElement>()?;<br />
[*] 通过 window() 和 document(),在浏览器环境下创建一个 HTML video 元素,用于加载和播放视频。
[*] 设置视频来源:
video_element.set_src(video_url);<br />video_element.set_cross_origin(Some("anonymous"));<br />
[*] 设置视频的 src,从传入的 video_url 加载视频。
[*] 设置 cross_origin 为 "anonymous",用于跨域加载资源,确保视频可以被绘制到 canvas 中。
[*] 等待视频元数据加载完成:
let loaded_metadata_promise = Promise::new(<br /> &mut (|resolve, _| {<br /> let resolve = Rc::new(RefCell::new(Some(resolve)));<br /> let onloadedmetadata_closure = Closure::wrap(<br /> Box::new(move || {<br /> if let Some(resolve) = resolve.borrow_mut().take() {<br /> resolve.call0(&JsValue::NULL).unwrap();<br /> }<br /> }) as Box<dyn FnMut()><br /> );<br /> video_element.set_onloadedmetadata(<br /> Some(onloadedmetadata_closure.as_ref().unchecked_ref())<br /> );<br /> onloadedmetadata_closure.forget();<br /> })<br />);<br />JsFuture::from(loaded_metadata_promise).await?;<br />
[*] 创建一个 JavaScript Promise,并监听 video 元素的 onloadedmetadata 变乱。
[*] 只有当视频元数据(例如时长、尺寸等)加载完成后,才气开始进一步操纵。
[*] Promise 是异步的,比及 onloadedmetadata 变乱触发后继续代码实行。
[*] 停息视频播放:
video_element.pause()?;<br />
[*] 停息视频,防止主动播放,确保可以逐帧处理视频。
[*] 创建 canvas 元素并设置巨细:
let canvas_element = document.create_element("canvas")?.dyn_into::<HtmlCanvasElement>()?;<br />canvas_element.set_width(video_element.video_width() as u32);<br />canvas_element.set_height(video_element.video_height() as u32);<br />
[*] 创建一个 HTML canvas 元素,并设置其宽高与视频的宽高一致,以便绘制视频帧。
[*] 获取 canvas 的绘图上下文:
let ctx = canvas_element.get_context("2d")?.unwrap().dyn_into::<CanvasRenderingContext2d>()?;<br />
[*] 获取 canvas 的 2D 绘图上下文 ( CanvasRenderingContext2d),用于在 canvas 上绘制视频帧。
[*] 逐帧处理视频:
let total_frames = (video_element.duration() * frame_rate) as i32;<br /><br />for i in 0..total_frames {<br /> video_element.set_current_time((i as f64) / frame_rate);<br /> <br /> let (sender, receiver) = oneshot::channel();<br /> let sender = Rc::new(RefCell::new(Some(sender)));<br /> let onseeked_closure = Closure::wrap(<br /> Box::new(move || {<br /> if let Some(sender) = sender.borrow_mut().take() {<br /> let _ = sender.send(());<br /> }<br /> }) as Box<dyn FnMut()><br /> );<br /> video_element.set_onseeked(Some(onseeked_closure.as_ref().unchecked_ref()));<br /> onseeked_closure.forget();<br /> receiver.await.unwrap();<br /><br /> ctx.draw_image_with_html_video_element(&video_element, 0.0, 0.0).unwrap();<br /> let frame_data = canvas_element.to_data_url().unwrap();<br /> frame_array.push(frame_data);<br />}<br />
[*] 计算总帧数 total_frames。
[*] 每次循环设置 video_element.set_current_time 调整视频的当前时间。
[*] 使用 oneshot::channel() 实现同步等待 onseeked 变乱触发,确保在时间跳转完毕后再举行下一步操纵。
[*] 使用 ctx.draw_image_with_html_video_element 绘制视频帧到 canvas。
[*] 使用 canvas_element.to_data_url() 将当前帧转换为 Base64 编码的图像数据,生存到 frame_array 数组中。
[*] 返回帧数组:
Ok(to_value(&frame_array).unwrap())<br />
[*] 使用 serde_wasm_bindgen::to_value 将 frame_array 转换为 JavaScript 值 JsValue,并返回帧数据数组。
涉及的重要 Rust 概念
[*] wasm_bindgen:
[*] 宏和工具,用于在 Rust 中与 JavaScript 交互,特别是在 WebAssembly 中调用 JavaScript API。
[*] 如 # 标记的函数可以被 JavaScript 调用。
[*] futures::channel::oneshot:
[*] Rust 的异步工具,提供了一个简单的 oneshot 通道,用于线程间或异步任务之间举行一次性消息传递。在这里用于同步等待 onseeked 变乱触发。
[*] Closure::wrap:
[*] 将 Rust 的闭包转换为 JavaScript 的闭包,并传递给 DOM 变乱处理器。在处理 onloadedmetadata 和 onseeked 时使用。
[*] Rc 和 RefCell:
[*] Rc( Reference Counted)是引用计数智能指针,用于在多个地方共享数据。
[*] RefCell 允许在运行时举行可变借用,共同 Rc 使用,解决共享状态下的可变性题目。
[*] JsFuture::from(Promise):
[*] 将 JavaScript 的 Promise 转换为 Rust 的 Future,以便在异步代码中使用 await。
有些概念,例如Closure::wrap和Rc 和 RefCell在我们之前的Rust学习条记中都有涉猎。
运行效果
https://img-blog.csdnimg.cn/img_convert/4d6cda68376d3bfb6f058eb6e3c8f8cc.gif 上面的效果就是我们把编译好的Rust代码在前端环境实行的效果。能达到抽帧的效果,但是有几点瑕疵。
[*] 只有在视频解析完成后,我们才会拿到最后的数据信息,也就是我们页面中有很长的一段空窗期。这很不好,我们要那种抽离出一个页面就像前端返回,要有及时性
[*] 处理的时间过长,对比文章刚开始,相同的视频,抽离69张图片,需要耗时13秒。这也是我们不能容忍的。
所以接下来,我们来优化上面的代码
新增callback
首先,我们来解决上面的第一个题目,我们不要在视频处理完后才向前端返回信息,我们要的是没处理完一批数据,前端就可以先渲染。也就是在文章刚开始的那个效果。
话不多说,我们直接上代码。
use futures::channel::oneshot;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{ window, HtmlVideoElement, HtmlCanvasElement, CanvasRenderingContext2d };
use js_sys::{ Function, Promise };
use std::rc::Rc;
use std::cell::RefCell;
#
pub async fn extract_frames_from_url(
video_url: &str,
frame_rate: f64,
callback: &JsValue
) -> Result<(), JsValue> {
let document = window().unwrap().document().unwrap();
let video_element = document.create_element("video")?.dyn_into::<HtmlVideoElement>()?;
video_element.set_src(video_url);
video_element.set_cross_origin(Some("anonymous"));
let loaded_metadata_promise = Promise::new(
&mut (|resolve, _| {
let resolve = Rc::new(RefCell::new(Some(resolve)));
let onloadedmetadata_closure = Closure::wrap(
Box::new(move || {
if let Some(resolve) = resolve.borrow_mut().take() {
resolve.call0(&JsValue::NULL).unwrap();
}
}) as Box<dyn FnMut()>
);
video_element.set_onloadedmetadata(
Some(onloadedmetadata_closure.as_ref().unchecked_ref())
);
onloadedmetadata_closure.forget();
})
);
JsFuture::from(loaded_metadata_promise).await?;
video_element.pause()?;<br />
let canvas_element = document.create_element("canvas")?.dyn_into::<HtmlCanvasElement>()?;
canvas_element.set_width(video_element.video_width() as u32);
canvas_element.set_height(video_element.video_height() as u32);
let ctx = canvas_element.get_context("2d")?.unwrap().dyn_into::<CanvasRenderingContext2d>()?;<br />
let total_frames = (video_element.duration() * frame_rate) as i32;
let onseeked_closure = Rc::new(RefCell::new(None));
let video_element_clone = video_element.clone();
// 将 JsValue 转换为 JavaScript 的回调函数
let callback_function = callback.dyn_ref::<Function>().ok_or("callback function is not valid")?;
for i in 0..total_frames {
let (sender, receiver) = oneshot::channel();
let sender = Rc::new(RefCell::new(Some(sender)));
*onseeked_closure.borrow_mut() = Some(
Closure::wrap(
Box::new({
let sender = sender.clone();
move || {
if let Some(sender) = sender.borrow_mut().take() {
let _ = sender.send(());
}
}
}) as Box<dyn FnMut()>
)
);
video_element_clone.set_onseeked(
Some(onseeked_closure.borrow().as_ref().unwrap().as_ref().unchecked_ref())
);
video_element_clone.set_current_time((i as f64) / frame_rate);
receiver.await.unwrap();
ctx.draw_image_with_html_video_element(&video_element_clone, 0.0, 0.0).unwrap();
let frame_data = canvas_element.to_data_url().unwrap();
// 每次生成frame_data后,立即调用回调函数,将frame_data传递给前端
let js_frame_data = JsValue::from_str(&frame_data);
callback_function.call1(&JsValue::NULL, &js_frame_data)?;
}
video_element.set_onseeked(None);
Ok(())
}
对比之前代码的改变
[*] 添加了 callback 参数:
[*] 新增了 callback: &JsValue 参数,允许调用 JavaScript 回调函数将每一帧的图像数据实时传递给前端,而不是像之前那样返回整个帧数组。
[*] 通过 callback_function.call1() 方法,在每一帧生成后立即将帧数据传递给回调函数。
[*] 去除了 frame_array:
[*] 之前的代码将全部帧数据生存到一个数组 frame_array 中,最后一次性返回整个数组。
[*] 新代码去掉了 frame_array,改为每次生成帧数据后立即调用回调函数,因此不需要在 Rust 端生存全部帧数据。
[*] 优化 onseeked 变乱处理:
[*] 原代码每次生成帧时都创建一个新的 Closure 监听 onseeked 变乱。
[*] 优化后, onseeked_closure 是通过 Rc<RefCell<>> 来缓存的,因此可以在循环中复用它,制止每次都生成新的闭包对象。
[*] 这一优化减少了闭包的创建和内存分配,提升了代码的性能。
[*] JsValue 转 Function:
[*] 在新代码中,通过 callback.dyn_ref::<Function>() 将传入的 JsValue 转换为 JavaScript 回调函数 ( Function),并在每一帧处理完后调用该回调。
[*] 这一点加强了 Rust 端与 JavaScript 端的交互,提供了实时处理的能力。
优点
[*] 实时传递帧数据:
[*] 提升了内存效率:不再需要将全部帧数据存入一个大数组,而是每处理一帧立即发送给前端。如许减少了内存占用,尤其是对于处理长视频时,帧数据不再需要在 Rust 端缓存。
[*] 更流通的用户体验:帧数据可以实时传递给前端,这意味着视频的帧捕捉和渲染可以并行举行,不必等待全部帧处理完毕后再返回结果。用户可以立即看到处理中的帧。
[*] 复用闭包,减少性能开销:
[*] 通过 Rc<RefCell<>> 复用 onseeked_closure,减少了闭包的创建和销毁,低落了闭包生成的开销,进步了代码的性能。
[*] 灵活性加强:
[*] 新的 callback 参数使得这段代码更加灵活,用户可以自界说怎样处理帧数据(例如显示在页面上,存储到服务器,或者举行其他操纵)。通过回调机制,处理每帧数据的逻辑可以完全在前端控制。
[*] 更简洁的结构:
[*] 由于去掉了帧数据数组 frame_array,整个代码逻辑更加清晰,减少了无用的变量管理,提升了可读性。
效果展示
https://img-blog.csdnimg.cn/img_convert/90e54f8a60cb388949b710fa1d61b847.gif 可以看到,我们通过加入了callback后,不仅在页面交互上有所优化,在渲染速度上也有一定的提升。
也就是说,我们通过上面的改动,都解决了上面的两个顽疾。
使用多线程
实在吧,在颠末优化后,上面的代码已经可以大概告竣我们的需求了。之前文章也写了,我们之所以选用Rust来处理视频抽帧,是看中了它的多线程能力。
然后,我们就尝试往这边靠。具体实现思绪呢,还是和raytrace-parallel一致。
所幸,它也有完整的代码实现。然后,咱也照猫画虎实现一遍,然后假如有性能不可靠的地方,我们在见招拆招。
<blockquote class="multiquote-1" style="margin-top: 20px; margin-bottom: 20px; margin-left: 0px; margin-right: 0px; padding-top: 10px; padding-bottom: 10px; padding-left: 20px; padding-right: 10px; border-top-style: solid; border-bottom-style: solid; border-left-style: solid; border-right-style: solid; border-top-width: 1px; border-bottom-width: 1px; border-left-width: 1px; border-right-width: 1px; border-top-color: rgba(222, 198, 251, 0.4); border-bottom-color: rgba(222, 198, 251, 0.4); border-left-color: rgba(222, 198, 251, 0.4); border-right-color: rgba(222, 198, 251, 0.4); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-attachment: scroll; background-clip: border-box; background-color: rgb(246, 238, 255); background-image: none; background-origin: padding-box; background-position-x: 0%; background-position-y: 0%; background-repeat: no-repeat; background-size: auto; width: auto; height: auto; box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px; display: block; overflow-x: auto; overflow-y: auto;"> ❝ 先说结果,本来我们想使用rayon来初始化多个线程,而且实例化多个video实例,每个video处理一部分视频的解析处理,从而达到收缩处理时间的目标,但是呢由于document/video的实例无法在多线程中共享数据,然后达不到这种效果,后来不了了之。
部分代码如下所示:(这是一个不成功的案例,但是我们的思绪就是分而治之)
use futures::channel::oneshot;<br />use wasm_bindgen::prelude::*;<br />use wasm_bindgen_futures::JsFuture;<br />use web_sys::{window, HtmlVideoElement, HtmlCanvasElement, CanvasRenderingContext2d};<br />use js_sys::{Function, Promise};<br />use std::rc::Rc;<br />use std::cell::RefCell;<br />use rayon::prelude::*;<br />use std::sync::{Arc, Mutex};<br />use wasm_bindgen_rayon::init_thread_pool;<br /><br />#<br />pub async fn extract_frames_from_url(<br /> video_url: &str,<br /> frame_rate: f64,<br /> callback: &JsValue<br />) -> Result<(), JsValue> {<br /> init_thread_pool(8).expect("Failed to initialize thread pool");<br /><br /> let document = Arc::new(window().unwrap().document().unwrap());<br /><br /> let video_element1 = document.create_element("video")?.dyn_into::<HtmlVideoElement>()?;<br /> let video_element2 = document.create_element("video")?.dyn_into::<HtmlVideoElement>()?;<br /><br /> video_element1.set_src(video_url);<br /> video_element2.set_src(video_url);<br /><br /> video_element1.set_cross_origin(Some("anonymous"));<br /> video_element2.set_cross_origin(Some("anonymous"));<br /><br /> // 省略部分代码<br /><br /> video_element1.pause()?;<br /> video_element2.pause()?;<br /><br /> let total_frames = (video_element1.duration() * frame_rate) as i32;<br /> let midpoint = total_frames / 2;<br /><br /> let document_clone1 = Arc::clone(&document);<br /> let document_clone2 = Arc::clone(&document);<br /><br /> let process_frames = move |<br /> video_element: HtmlVideoElement,<br /> start_frame: i32,<br /> end_frame: i32,<br /> document: Arc<web_sys::Document><br /> | {<br /> let video_element_arc = Arc::new(Mutex::new(video_element));<br /> let ctx = {<br /> let canvas_element = document.create_element("canvas").unwrap().dyn_into::<HtmlCanvasElement>().unwrap();<br /> canvas_element.set_width(video_element_arc.lock().unwrap().video_width() as u32);<br /> canvas_element.set_height(video_element_arc.lock().unwrap().video_height() as u32);<br /> canvas_element.get_context("2d").unwrap().unwrap().dyn_into::<CanvasRenderingContext2d>().unwrap()<br /> };<br /><br /> let callback_function = Arc::new(callback.dyn_ref::<Function>().unwrap().clone());<br /><br /> (start_frame..end_frame).into_par_iter().for_each(|i| {<br /> let video_element = Arc::clone(&video_element_arc);<br /> let callback_function = Arc::clone(&callback_function);<br /><br /> let mut video_element = video_element.lock().unwrap();<br /><br /> let (sender, receiver) = oneshot::channel();<br /> let sender = Rc::new(RefCell::new(Some(sender)));<br /><br /> let onseeked_closure = Closure::wrap(<br /> Box::new({<br /> let sender = sender.clone();<br /> move || {<br /> if let Some(sender) = sender.borrow_mut().take() {<br /> let _ = sender.send(());<br /> web_sys::console::log_1(&format!("Frame {} processed", i).into());<br /> }<br /> }<br /> }) as Box<dyn FnMut()><br /> );<br /><br /> video_element.set_onseeked(Some(onseeked_closure.as_ref().unchecked_ref()));<br /> video_element.set_current_time((i as f64) / frame_rate);<br /><br /> receiver.await.unwrap();<br /><br /> ctx.draw_image_with_html_video_element(&*video_element, 0.0, 0.0).unwrap();<br /> let frame_data = ctx.canvas().unwrap().to_data_url().unwrap();<br /><br /> let js_frame_data = JsValue::from_str(&frame_data);<br /> callback_function.call1(&JsValue::NULL, &js_frame_data).unwrap();<br /> });<br /> };<br /><br /> let task1 = process_frames(video_element1, 0, midpoint, document_clone1);<br /> let task2 = process_frames(video_element2, midpoint, total_frames, document_clone2);<br /><br /> futures::join!(task1, task2);<br /><br /> Ok(())<br />}<br /><br />虽然,结果很无奈,但是实在也是给了我们很多思绪,就像raytrace-parallel使用多线程实现对一批数据实现多线程处理,而且将结果返回的思绪。
当背面我们碰到类似的功能,我们也是可以往这边靠拢的。
创建一个 web worker 池
下面的代码,我们是从raytrace-parallel中cv下来的,但是也是一个完成的代码片断,以后假如有需要,我们可以直接使用。
// 关闭编译器警告:当目标架构不是 wasm 时,忽略 Work.func 和 child_entry_point 未使用的警告<br />#!<br /><br />// 演示了如何创建一个 web worker 池,以便执行类似于 `rayon` 的任务。<br /><br />use std::cell::RefCell;<br />use std::rc::Rc;<br />use wasm_bindgen::prelude::*;<br />use web_sys::{ DedicatedWorkerGlobalScope, MessageEvent, WorkerOptions };<br />use web_sys::{ ErrorEvent, Event, Worker, WorkerType };<br /><br />#<br />pub struct WorkerPool {<br /> state: Rc<PoolState>,<br /> worker_path: String,<br />}<br /><br />struct PoolState {<br /> workers: RefCell<Vec<Worker>>,<br /> callback: Closure<dyn FnMut(Event)>,<br />}<br /><br />struct Work {<br /> func: Box<dyn FnOnce() + Send>,<br />}<br /><br />#<br />impl WorkerPool {<br /> /// 创建一个新的 `WorkerPool`,该池立即创建 `initial` 个 worker。<br /> ///<br /> /// 该池可以长期使用,并且会初始填充 `initial` 个 worker。<br /> /// 目前,除非整个池被销毁,否则不会释放或回收 worker。<br /> ///<br /> /// # 错误<br /> ///<br /> /// 如果在创建 JS web worker 或发送消息时发生任何错误,将返回该错误。<br /> #<br /> pub fn new(initial: usize, worker_path: String) -> Result<WorkerPool, JsValue> {<br /> let pool = WorkerPool {<br /> state: Rc::new(PoolState {<br /> workers: RefCell::new(Vec::with_capacity(initial)),<br /> callback: Closure::new(|event: Event| {<br /> console_log!("未处理的事件: {}", event.type_());<br /> crate::logv(&event);<br /> }),<br /> }),<br /> worker_path: worker_path.to_string(),<br /> };<br /> for _ in 0..initial {<br /> let worker = pool.spawn(worker_path.clone())?;<br /> pool.state.push(worker);<br /> }<br /><br /> Ok(pool)<br /> }<br /><br /> /// 无条件地生成一个新的 worker<br /> ///<br /> /// 该 worker 不会被注册到此 `WorkerPool`,但能够为此 wasm 模块执行任务。<br /> ///<br /> /// # 错误<br /> ///<br /> /// 如果在创建 JS web worker 或发送消息时发生任何错误,将返回该错误。<br /> fn spawn(&self, worker_path: String) -> Result<Worker, JsValue> {<br /> console_log!("生成新 worker");<br /> /// 通过在调用处,传人worker_path,来构建一个module 类型的worker<br /> let mut opts = WorkerOptions::new();<br /> opts.set_type(WorkerType::Module);<br /> // 创建 Worker<br /> let worker = Worker::new_with_options(&worker_path, &opts).map_err(|err|<br /> JsValue::from(err)<br /> )?;<br /><br /> // 在生成 worker 后,发送模块/内存以便它可以开始实例化 wasm 模块。<br /> // 稍后,它可能会收到有关在 wasm 模块上运行代码的进一步消息。<br /> let array = js_sys::Array::new();<br /> array.push(&wasm_bindgen::module());<br /> array.push(&wasm_bindgen::memory());<br /> worker.post_message(&array)?;<br /><br /> Ok(worker)<br /> }<br /><br /> /// 从此池中获取一个 worker,必要时生成一个新的。<br /> ///<br /> /// 如果有可用的已生成的 web worker,这将尝试从缓存中提取一个,否则将生成一个新的 worker 并返回新生成的 worker。<br /> ///<br /> /// # 错误<br /> ///<br /> /// 如果在创建 JS web worker 或发送消息时发生任何错误,将返回该错误。<br /> fn worker(&self) -> Result<Worker, JsValue> {<br /> match self.state.workers.borrow_mut().pop() {<br /> Some(worker) => Ok(worker),<br /> None => self.spawn(self.worker_path.clone()),<br /> }<br /> }<br /><br /> /// 在 web worker 中执行任务 `f`,必要时生成 web worker。<br /> ///<br /> /// 这将获取一个 web worker,然后将闭包 `f` 发送给 worker 执行。在 `f` 执行期间,该 worker 将不可用于其他任务,且不会为 worker 完成时注册回调。<br /> ///<br /> /// # 错误<br /> ///<br /> /// 如果在创建 JS web worker 或发送消息时发生任何错误,将返回该错误。<br /> fn execute(&self, f: impl FnOnce() + Send + 'static) -> Result<Worker, JsValue> {<br /> let worker = self.worker()?;<br /> let work = Box::new(Work { func: Box::new(f) });<br /> let ptr = Box::into_raw(work);<br /> match worker.post_message(&JsValue::from(ptr as u32)) {<br /> Ok(()) => Ok(worker),<br /> Err(e) => {<br /> unsafe {<br /> drop(Box::from_raw(ptr));<br /> }<br /> Err(e)<br /> }<br /> }<br /> }<br /><br /> /// 为指定的 `worker` 配置一个 `onmessage` 回调,以便在收到消息时回收并重新插入此池。<br /> ///<br /> /// 当前,此 `WorkerPool` 抽象用于执行一次性任务,其中任务本身不会发送任何通知,任务完成后 worker 准备执行更多任务。<br /> /// 此方法用于所有生成的 worker,以确保任务完成后 worker 被回收到此池中。<br /> fn reclaim_on_message(&self, worker: Worker) {<br /> let state = Rc::downgrade(&self.state);<br /> let worker2 = worker.clone();<br /> let reclaim_slot = Rc::new(RefCell::new(None));<br /> let slot2 = reclaim_slot.clone();<br /> let reclaim = Closure::<dyn FnMut(_)>::new(move |event: Event| {<br /> if let Some(error) = event.dyn_ref::<ErrorEvent>() {<br /> console_log!("worker 中的错误: {}", error.message());<br /> // TODO: 这可能会导致内存泄漏?目前尚不清楚如何处理 worker 中的错误。<br /> return;<br /> }<br /><br /> // 如果这是一个完成事件,可以通过清空 `slot2` 来释放自己的回调,其中包含我们的闭包。<br /> if let Some(_msg) = event.dyn_ref::<MessageEvent>() {<br /> if let Some(state) = state.upgrade() {<br /> state.push(worker2.clone());<br /> }<br /> *slot2.borrow_mut() = None;<br /> return;<br /> }<br /><br /> console_log!("未处理的事件: {}", event.type_());<br /> crate::logv(&event);<br /> // TODO: 与上面类似,这里可能也存在内存泄漏?<br /> });<br /> worker.set_onmessage(Some(reclaim.as_ref().unchecked_ref()));<br /> *reclaim_slot.borrow_mut() = Some(reclaim);<br /> }<br />}<br /><br />impl WorkerPool {<br /> /// 在 web worker 中执行 `f`。<br /> ///<br /> /// 此池管理一组可供使用的 web worker,如果 worker 空闲,`f` 将被快速分配给一个 worker。如果没有空闲的 worker 可用,则会生成一个新的 web worker。<br /> ///<br /> /// 一旦 `f` 返回,分配给 `f` 的 worker 将自动被此 `WorkerPool` 回收。此方法不提供了解 `f` 何时完成的方法,对于此类需求,你需要使用 `run_notify`。<br /> ///<br /> /// # 错误<br /> ///<br /> /// 如果在生成 web worker 或向其发送消息时发生错误,将返回该错误。<br /> pub fn run(&self, f: impl FnOnce() + Send + 'static) -> Result<(), JsValue> {<br /> let worker = self.execute(f)?;<br /> self.reclaim_on_message(worker);<br /> Ok(())<br /> }<br />}<br /><br />impl PoolState {<br /> fn push(&self, worker: Worker) {<br /> worker.set_onmessage(Some(self.callback.as_ref().unchecked_ref()));<br /> worker.set_onerror(Some(self.callback.as_ref().unchecked_ref()));<br /> let mut workers = self.workers.borrow_mut();<br /> for prev in workers.iter() {<br /> let prev: &JsValue = prev;<br /> let worker: &JsValue = &worker;<br /> assert!(prev != worker);<br /> }<br /> workers.push(worker);<br /> }<br />}<br /><br />/// 由 `worker.js` 调用的入口点<br />#<br />pub fn child_entry_point(ptr: u32) -> Result<(), JsValue> {<br /> let ptr = unsafe { Box::from_raw(ptr as *mut Work) };<br /> let global = js_sys::global().unchecked_into::<DedicatedWorkerGlobalScope>();<br /> (ptr.func)();<br /> global.post_message(&JsValue::undefined())?;<br /> Ok(())<br />}<br />后记
分享是一种态度。
全文完,既然看到这里了,假如觉得不错,顺手点个赞和“在看”吧。
https://img-blog.csdnimg.cn/img_convert/ee25342ff4dff4a5d7058f33f43b2b23.gif Reference f_cli_f: https://www.npmjs.com/package/f_cli_f
onseeked 变乱: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeked_event
视频元数据: https://wistia.com/learn/marketing/video-metadata
loadedmetadata 变乱: loadedmetadata_event
wasm-bindgen: https://rustwasm.github.io/wasm-bindgen/introduction.html
web-sys: https://rustwasm.github.io/wasm-bindgen/examples/dom.html
Parallel Raytracing: https://rustwasm.github.io/wasm-bindgen/examples/raytrace.html
Rust的多线程: https://docs.rs/threadpool/latest/threadpool/
raytrace-parallel: https://wasm-bindgen.netlify.app/exbuild/raytrace-parallel/
rayon: https://github.com/rayon-rs/rayon
本文由 mdnice 多平台发布
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]