qidao123.com技术社区-IT企服评测·应用市场

标题: Rust 赋能前端: 视频抽帧 [打印本页]

作者: 曹旭辉    时间: 2024-9-16 21:19
标题: 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;">   ❝   
此篇文章所涉及到的技术有

      
因为,行笔墨数所限,有些概念可能会一带而过亦或者提供对应的学习资料。请各人酌情观看。

  
  前言

  
老粉都知道,我之前接手了一个内容审读的开发需求。它是个啥呢,它需要对各种文档资源举行解析和展示。

  
在上周呢,我们写了一篇Rust 赋能前端:PDF 分页/关键词标注/转图片/抽取文本/抽取图片/翻转...,在内里先容怎样在前端环境中(React/Vue)中使用Mupdf,用于实行各种PDF的操纵。

  
在我们体系中,有一个需求就是视频抽帧。也就是对一个视频资源基于某些特征将其关键帧抽离成图片信息。然后对其举行OCR识别,而且基于关键字标注处理。

  
我们本日就来讲讲怎样使用WebAssembly对视频资源举行抽帧处理。对于OCR部分我们会单开一篇。

  效果展示

     
   
可以看到,我们将一个时长快5分多的视频,仅用时8秒(颠簸在5-9秒之间)就将其抽成69个关键帧。

  
  
好了,天不早了,干点正事哇。

     
    我们能所学到的知识点

  <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;">   ❝      
  1. 项目初始化

  
还是一样的套路,我们还是基于f_cli_f[1]来构建的前端Vite+React+TS项目。

  
当我们通过yarn/npm安装好对应的包时。我们就可以在pages新建一个Video2Img的目次,然后直接构建一个index.tsx即可。

  
随后,我们在src目次下构建一个wasm目次来存放在前端项目中要用到的各种wasm。针对这个例子,我们构建一个video2Img的目次,用于存放Rust编译后的文件。

  
  2. 技术选择的初衷

  
实在呢,针对视频抽帧的需求,我们可以不消WebAssembly来处理。直接使用浏览器原生API就可以实现。

  用原生API实现视频抽帧

  1. 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.play() 后停息并逐帧处理,但没有等待视频元数据[3](如时长、帧率、宽高等)加载完成。假如不等元数据加载,视频可能还没有完全预备好,导致一些延迟。

  
通过监听视频的 loadedmetadata 变乱[4],可以确保在开始处理之前,全部必要的元数据(如视频时长、宽度、高度)都已加载完成。这能制止因元数据未加载完而导致的时间跳转缓慢或帧提取卡顿,从而提升整体处理效率。

  选择使用Rust+WebAssembly的原因

  
各人从上面的代码核心点可知。在处理过程中,出现了几种数据范例。

  
  
还记得之前我们写过宝贝,带上WebAssembly,换个姿势来优化你的前端应用其中有一节就是讲到,假如在前端绘制内容比较复杂的图片资源时,可以往Rust+WebAssembly上靠。

  
还记得这两张图吗?

     
   
上面的示例,可能在有些同砚眼中有点牵强。然后,我们继续来说别的一个我选择使用Rust+WebAssembly处理视频抽帧的。

  
假如各人写过Rust+WebAssembly的步伐的话,想必肯定听说过大名鼎鼎的 - wasm-bindgen[5]。假如想在Rust使用浏览器的一些API例如(Document/Window等)还离不开它 - web-sys[6]

  
然后,不知道各人注意过这个例子不 - Parallel Raytracing[7]。它是使用Rust的多线程[8]操纵同一份数据并将这些数据信息绘制到一个canvas中。(强烈发起各人在浏览器实操一下)

  
下面是它的效果图

     
       单线程绘制,耗时1.2秒         
       多线程,耗时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中有如下的安装信息。

  1. [package]
  2. name = "audio2img"
  3. version = "0.1.0"
  4. edition = "2021"
  5. [dependencies]
  6. futures = "0.3.30"
  7. js-sys = "0.3.69"
  8. serde-wasm-bindgen = "0.6.5"
  9. wasm-bindgen = "0.2.92"
  10. wasm-bindgen-futures = "0.4.42"
  11. [lib]
  12. crate-type = ["cdylib"]
  13. [dependencies.web-sys]
  14. version = "0.3"
  15. features = [
  16.     'Window',
  17.     'Document',
  18.     'Element',
  19.     'HtmlCanvasElement',
  20.     'CanvasRenderingContext2d',
  21.     'HtmlVideoElement',
  22.     'Event'
  23.     ]
复制代码
实现JS版本的平替版本(简单版本)

  
在上一节中我们先容过,我们完全可以用原生js来实现抽帧处理。那么,我们所要做的就是,实现一个最简单版本的Rust视频抽帧版本。

  
话不多说,我们直接上代码。在src/lib.rs中直接操纵。

  1. 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 />#[wasm_bindgen]<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审读能力。我们就对这段代码做一下比较具体的讲解。

  重要逻辑总结

    
可以看到,我们除了第5步和之前的JS逻辑差别,其他的代码思绪都和之前的出奇的一致。

  代码逻辑

  
然后,我们再对Rust的代码做一次较为具体的解读。

    
  涉及的重要 Rust 概念

    
有些概念,例如Closure::wrap和Rc 和 RefCell在我们之前的Rust学习条记中都有涉猎。

  运行效果

     
   
上面的效果就是我们把编译好的Rust代码在前端环境实行的效果。能达到抽帧的效果,但是有几点瑕疵。

    
所以接下来,我们来优化上面的代码

  
  新增callback

  
首先,我们来解决上面的第一个题目,我们不要在视频处理完后才向前端返回信息,我们要的是没处理完一批数据,前端就可以先渲染。也就是在文章刚开始的那个效果。

  
话不多说,我们直接上代码。

  1. use futures::channel::oneshot;
  2. use wasm_bindgen::prelude::*;
  3. use wasm_bindgen_futures::JsFuture;
  4. use web_sys::{ window, HtmlVideoElement, HtmlCanvasElement, CanvasRenderingContext2d };
  5. use js_sys::{ Function, Promise };
  6. use std::rc::Rc;
  7. use std::cell::RefCell;
  8. #[wasm_bindgen]
  9. pub async fn extract_frames_from_url(
  10.     video_url: &str,
  11.     frame_rate: f64,
  12.     callback: &JsValue
  13. ) -> Result<(), JsValue> {
  14.     let document = window().unwrap().document().unwrap();
  15.     let video_element = document.create_element("video")?.dyn_into::<HtmlVideoElement>()?;
  16.     video_element.set_src(video_url);
  17.     video_element.set_cross_origin(Some("anonymous"));
  18.     let loaded_metadata_promise = Promise::new(
  19.         &mut (|resolve, _| {
  20.             let resolve = Rc::new(RefCell::new(Some(resolve)));
  21.             let onloadedmetadata_closure = Closure::wrap(
  22.                 Box::new(move || {
  23.                     if let Some(resolve) = resolve.borrow_mut().take() {
  24.                         resolve.call0(&JsValue::NULL).unwrap();
  25.                     }
  26.                 }) as Box<dyn FnMut()>
  27.             );
  28.             video_element.set_onloadedmetadata(
  29.                 Some(onloadedmetadata_closure.as_ref().unchecked_ref())
  30.             );
  31.             onloadedmetadata_closure.forget();
  32.         })
  33.     );
  34.     JsFuture::from(loaded_metadata_promise).await?;
  35.     video_element.pause()?;<br />
  36.     let canvas_element = document.create_element("canvas")?.dyn_into::<HtmlCanvasElement>()?;
  37.     canvas_element.set_width(video_element.video_width() as u32);
  38.     canvas_element.set_height(video_element.video_height() as u32);
  39.     let ctx = canvas_element.get_context("2d")?.unwrap().dyn_into::<CanvasRenderingContext2d>()?;<br />
  40.     let total_frames = (video_element.duration() * frame_rate) as i32;
  41.     let onseeked_closure = Rc::new(RefCell::new(None));
  42.     let video_element_clone = video_element.clone();
  43.     // 将 JsValue 转换为 JavaScript 的回调函数
  44.     let callback_function = callback.dyn_ref::<Function>().ok_or("callback function is not valid")?;
  45.     for i in 0..total_frames {
  46.         let (sender, receiver) = oneshot::channel();
  47.         let sender = Rc::new(RefCell::new(Some(sender)));
  48.         *onseeked_closure.borrow_mut() = Some(
  49.             Closure::wrap(
  50.                 Box::new({
  51.                     let sender = sender.clone();
  52.                     move || {
  53.                         if let Some(sender) = sender.borrow_mut().take() {
  54.                             let _ = sender.send(());
  55.                         }
  56.                     }
  57.                 }) as Box<dyn FnMut()>
  58.             )
  59.         );
  60.         video_element_clone.set_onseeked(
  61.             Some(onseeked_closure.borrow().as_ref().unwrap().as_ref().unchecked_ref())
  62.         );
  63.         video_element_clone.set_current_time((i as f64) / frame_rate);
  64.         receiver.await.unwrap();
  65.         ctx.draw_image_with_html_video_element(&video_element_clone, 0.0, 0.0).unwrap();
  66.         let frame_data = canvas_element.to_data_url().unwrap();
  67.         // 每次生成frame_data后,立即调用回调函数,将frame_data传递给前端
  68.         let js_frame_data = JsValue::from_str(&frame_data);
  69.         callback_function.call1(&JsValue::NULL, &js_frame_data)?;
  70.     }
  71.     video_element.set_onseeked(None);
  72.     Ok(())
  73. }
复制代码
对比之前代码的改变

    
  优点

    效果展示

     
   
可以看到,我们通过加入了callback后,不仅在页面交互上有所优化,在渲染速度上也有一定的提升。

  
也就是说,我们通过上面的改动,都解决了上面的两个顽疾。

  
  使用多线程

  
实在吧,在颠末优化后,上面的代码已经可以大概告竣我们的需求了。之前文章也写了,我们之所以选用Rust来处理视频抽帧,是看中了它的多线程能力。

  
然后,我们就尝试往这边靠。具体实现思绪呢,还是和raytrace-parallel[9]一致。

  
所幸,它也有完整的代码实现。然后,咱也照猫画虎实现一遍,然后假如有性能不可靠的地方,我们在见招拆招。

  <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[10]来初始化多个线程,而且实例化多个video实例,每个video处理一部分视频的解析处理,从而达到收缩处理时间的目标,但是呢由于document/video的实例无法在多线程中共享数据,然后达不到这种效果,后来不了了之。

   
部分代码如下所示:(这是一个不成功的案例,但是我们的思绪就是分而治之)

  1. 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 />#[wasm_bindgen]<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下来的,但是也是一个完成的代码片断,以后假如有需要,我们可以直接使用。

  1. // 关闭编译器警告:当目标架构不是 wasm 时,忽略 Work.func 和 child_entry_point 未使用的警告<br />#![cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]<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 />#[wasm_bindgen]<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 />#[wasm_bindgen]<br />impl WorkerPool {<br />    /// 创建一个新的 `WorkerPool`,该池立即创建 `initial` 个 worker。<br />    ///<br />    /// 该池可以长期使用,并且会初始填充 `initial` 个 worker。<br />    /// 目前,除非整个池被销毁,否则不会释放或回收 worker。<br />    ///<br />    /// # 错误<br />    ///<br />    /// 如果在创建 JS web worker 或发送消息时发生任何错误,将返回该错误。<br />    #[wasm_bindgen(constructor)]<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 />#[wasm_bindgen(js_name = childEntryPoint)]<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 />
复制代码

  后记

  
分享是一种态度

  
全文完,既然看到这里了,假如觉得不错,顺手点个赞和“在看”吧。

     
       Reference       [1]
f_cli_f: https://www.npmjs.com/package/f_cli_f

    [2]
onseeked 变乱: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/seeked_event

    [3]
视频元数据: https://wistia.com/learn/marketing/video-metadata

    [4]
loadedmetadata 变乱: loadedmetadata_event

    [5]
wasm-bindgen: https://rustwasm.github.io/wasm-bindgen/introduction.html

    [6]
web-sys: https://rustwasm.github.io/wasm-bindgen/examples/dom.html

    [7]
Parallel Raytracing: https://rustwasm.github.io/wasm-bindgen/examples/raytrace.html

    [8]
Rust的多线程: https://docs.rs/threadpool/latest/threadpool/

    [9]
raytrace-parallel: https://wasm-bindgen.netlify.app/exbuild/raytrace-parallel/

    [10]
rayon: https://github.com/rayon-rs/rayon

     本文由 mdnice 多平台发布

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 qidao123.com技术社区-IT企服评测·应用市场 (https://dis.qidao123.com/) Powered by Discuz! X3.4