曹旭辉 发表于 2024-9-16 21:19:28

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]
查看完整版本: Rust 赋能前端: 视频抽帧