Rust 赋能前端: 视频抽帧

打印 上一主题 下一主题

主题 1857|帖子 1857|积分 5571

<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部分我们会单开一篇。

  效果展示

     
   
可以看到,我们将一个时长快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;">   ❝   

  •            项目初始化
  •            技术选择的初衷
  •            Rust + WebAssembly 抽帧处理
   
  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> 元素加载视频文件,并创建 <canvas> 用于捕捉视频帧。

  •          
    逐帧提取图像


    •                通过        video.currentTime 设置视频的播放时间。
    •                使用        onseeked 变乱        [2]在视频跳转到特定时间后,捕捉当前帧。
       
  •          
    渲染帧到画布:将视频帧绘制到画布中,然后使用 canvas.toDataURL 将帧转换为 [Base64 编码]( "Base64 编码")的 JPEG 图像。

  •          
    异步处理:使用 await 和 Promise 确保每帧的提取和处理按顺序举行。

  
当然,上面的代码只是一个简单版本,实在还有很多的优化空间。

  
例如:上面的代码在直接调用 video.play() 后停息并逐帧处理,但没有等待视频元数据[3](如时长、帧率、宽高等)加载完成。假如不等元数据加载,视频可能还没有完全预备好,导致一些延迟。

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

  选择使用Rust+WebAssembly的原因

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

  

  •          视频资源
  •          video元素
  •          canvas
  •          image
  
还记得之前我们写过宝贝,带上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审读能力。我们就对这段代码做一下比较具体的讲解。

  重要逻辑总结

  

  •          创建并配置 HTML 元素:创建     video 和     canvas 元素,设置视频源并调整     canvas 尺寸。
  •          等待视频元数据加载:通过     onloadedmetadata 确保视频元数据加载完成,制止提前操纵视频。
  •          逐帧跳转并捕捉帧

    •                使用        set_current_time 来逐帧调整视频的当前时间。
    •                通过监听        onseeked 变乱确保每次时间跳转后处理帧。
       
  •          绘制帧到 canvas:将每一帧绘制到     canvas,然后转换为 Base64 格式的图像数据。
  •          返回帧数据:将帧数据数组通过     WebAssembly 和     Rust 返回给     JavaScript。
  
可以看到,我们除了第5步和之前的JS逻辑差别,其他的代码思绪都和之前的出奇的一致。

  代码逻辑

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

  

  •          
    创建 video 元素

       
    1. let document = window().unwrap().document().unwrap();<br />let video_element = document.create_element("video")?.dyn_into::<HtmlVideoElement>()?;<br />
    复制代码

    •                通过        window() 和        document(),在浏览器环境下创建一个 HTML        video 元素,用于加载和播放视频。
       
  •          
    设置视频来源

       
    1. video_element.set_src(video_url);<br />video_element.set_cross_origin(Some("anonymous"));<br />
    复制代码

    •                设置视频的        src,从传入的        video_url 加载视频。
    •                设置        cross_origin 为        "anonymous",用于跨域加载资源,确保视频可以被绘制到 canvas 中。
       
  •          
    等待视频元数据加载完成

       
    1. 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 变乱触发后继续代码实行。
       
  •          
    停息视频播放

       
    1. video_element.pause()?;<br />
    复制代码

    •                停息视频,防止主动播放,确保可以逐帧处理视频。
       
  •          
    创建 canvas 元素并设置巨细

       
    1. 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 的绘图上下文

       
    1. let ctx = canvas_element.get_context("2d")?.unwrap().dyn_into::<CanvasRenderingContext2d>()?;<br />
    复制代码

    •                获取        canvas 的 2D 绘图上下文 (        CanvasRenderingContext2d),用于在 canvas 上绘制视频帧。
       
  •          
    逐帧处理视频

       
    1. 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 数组中。
       
  •          
    返回帧数组

       
    1. Ok(to_value(&frame_array).unwrap())<br />
    复制代码

    •                使用        serde_wasm_bindgen::to_value 将        frame_array 转换为 JavaScript 值        JsValue,并返回帧数据数组。
       
  
  涉及的重要 Rust 概念

  

  •          
    wasm_bindgen


    •                宏和工具,用于在        Rust 中与        JavaScript 交互,特别是在        WebAssembly 中调用        JavaScript API。
    •                如        #[wasm_bindgen] 标记的函数可以被        JavaScript 调用。
       
  •          
    futures::channel:neshot


    •                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学习条记中都有涉猎。

  运行效果

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

  

  •          只有在视频解析完成后,我们才会拿到最后的数据信息,也就是我们页面中有很长的一段空窗期。这很不好,我们要那种抽离出一个页面就像前端返回,要有及时性
  •          处理的时间过长,对比文章刚开始,相同的视频,抽离69张图片,需要耗时13秒。这也是我们不能容忍的。
  
所以接下来,我们来优化上面的代码

  
  新增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 参数


    •                新增了        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,整个代码逻辑更加清晰,减少了无用的变量管理,提升了可读性。
       
  效果展示

     
   
可以看到,我们通过加入了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企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

曹旭辉

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表