LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

前端生成视频缩略图

freeflydom
2024年8月7日 8:42 本文热度 1000

前言

接到一个需求,需要前端生成获取视频的缩略图,并且需要把多张图片拼接在一起,类似于剪辑软件时间轴的效果:

 

在服务端使用ffmpeg生成其实比较简单,但是别问为啥要前端来实现,问就是没空!

总体思路

首先想到的就是在浏览器端引入ffmpeg.wasm,但是这样会增大应用体积,如果没有其他视频处理的需求,还是尽量避免这个方案。

然后想到的是WebCodecs API,WebCodecs API是浏览器提供处理音视频的原生接口,但是只支持视频的编解码,不支持解封装,需要搭配mp4box.js使用,而且mp4box.js只支持mp4、mov格式的视频,所有也不考虑这个方案。

最后的方案是张鑫旭大神在使用JS快速获取video视频任意位置的缩略图提到的Video标签获取截图的方案,接下来详细了解一下

第一步、获取视频截图

代码实现

const handleGetVideoThumb = function (url, options = {}) {

    if (typeof url != 'string') {

        return;

    }

    // 默认参数

    const defaults = {

        onLoading: () => {},

        onLoaded: () => {},

        onFinish: (arr) => {}

    };


    const params = Object.assign({}, defaults, options);


    // 基于视频元素绘制缩略图,而非解码视频

    const video = document.createElement('video');

    // 静音

    video.muted = true;


    // 绘制缩略图的canvas画布元素

    const canvas = document.createElement('canvas');

    const context = canvas.getContext('2d', {

        willReadFrequently: true

    });


    // 绘制缩略图的标志量

    let isTimeUpdated = false;

    // 几个视频事件

    // 1. 获取视频尺寸

    video.addEventListener('loadedmetadata', () => {

        canvas.width = video.videoWidth;

        canvas.height = video.videoHeight;


        // 开始执行绘制

        draw();

    });

    // 2. 触发绘制监控

    video.addEventListener('timeupdate', () => {

        isTimeUpdated = true;

    });


    // 获取视频数据

    params.onLoading();

    // 请求视频地址,如果是本地文件,直接执行

    if (/^blob:|base64,/i.test(url)) {

        video.src = url;

    } else {

        fetch(url).then(res => res.blob()).then(blob => {

            params.onLoaded();

            // 赋予视频

            video.src = URL.createObjectURL(blob);

        });

    }


    // 绘制方法

    const draw = () => {

        const arrThumb = [];

        const duration = video.duration;

        let seekTime = 0.1;


        const loop = () => {

            if (isTimeUpdated) {

                context.clearRect(0, 0, canvas.width, canvas.height);

                context.drawImage(video, 0, 0, canvas.width, canvas.height);


                canvas.toBlob(blob => {

                    arrThumb.push(URL.createObjectURL(blob));


                    seekTime += 1;


                    if (seekTime > duration) {

                        params.onFinish(arrThumb);


                        return;

                    }


                    step();

                }, 'image/jpeg');


                return;

            }

            // 监控状态

            requestAnimationFrame(loop);

        }


        // 逐步绘制,因为currentTime修改生效是异步的

        const step = () => {

            isTimeUpdated = false;

            video.currentTime = seekTime;


            loop();

        }


        step();

    }

};

代码解析

handleGetVideoThumb函数是实现视频截图功能的核心。它接受两个参数:视频的URL和可选的配置对象options

const handleGetVideoThumb = function (url, options = {}) {

    if (typeof url != 'string') {

        return;

    }

    // 默认参数

    const defaults = {

        onLoading: () => {},

        onLoaded: () => {},

        onFinish: (arr) => {}

    };


    const params = Object.assign({}, defaults, options);

    // ...

};

在这段代码中,首先检查url是否为字符串类型,确保输入有效。然后,定义了默认的回调函数,并通过Object.assign合并用户定义的选项。

视频元素与Canvas初始化

接下来,代码创建了视频元素video和Canvas元素canvas,用于加载视频和绘制缩略图:

const video = document.createElement('video');

video.muted = true; // 静音视频


const canvas = document.createElement('canvas');

const context = canvas.getContext('2d', {

    willReadFrequently: true

});

这里,video元素被设置为静音,以防止自动播放时产生声音。canvasgetContext方法设置了willReadFrequently选项,这有助于提高绘制性能。

视频加载与尺寸设置

视频加载过程中,通过监听loadedmetadata事件来获取视频的尺寸,并设置canvas的大小:

video.addEventListener('loadedmetadata', () => {

    canvas.width = video.videoWidth;

    canvas.height = video.videoHeight;

    // 开始执行绘制

    draw();

});

缩略图绘制逻辑

draw函数是绘制缩略图的核心,它定义了如何从视频中捕获帧并生成缩略图:

const draw = () => {

    // ...

    const loop = () => {

        if (isTimeUpdated) {

            context.clearRect(0, 0, canvas.width, canvas.height);

            context.drawImage(video, 0, 0, canvas.width, canvas.height);

            // ...

        }

        requestAnimationFrame(loop);

    };


    const step = () => {

        // ...

        video.currentTime = seekTime;

        loop();

    };


    step();

}

draw函数中,使用requestAnimationFrame创建了一个循环,该循环在视频的timeupdate事件触发时执行。每次循环都会清除画布并重新绘制当前视频帧,然后生成缩略图的blob,并将其转换为URL。

视频数据获取

视频数据的获取处理了本地和远程URL的情况:

if (/^blob:|base64,/i.test(url)) {

    video.src = url;

} else {

    fetch(url).then(res => res.blob()).then(blob => {

        params.onLoaded();

        video.src = URL.createObjectURL(blob);

    });

}

如果URL是本地文件或base64编码的URL,直接设置为视频源。对于远程URL,使用fetch请求视频数据,并将其转换为blob对象,然后创建一个对象URL。

第二步、方案优化

如果直接使用这个方法截图会存在一些性能问题,比如:

  1. 视频的缩略图列表需要使用多个img标签展示,如果列表比较长或者需要同时展示多个缩略图列表,会使用到很多的img标签,造成性能问题

  2. canvas.toBlob比较耗时,而且需要等待图片生成完成,才进行下一张截图的生成,我们可以直接使用canvas展示图片内容,避免调用canvas.toBlob 和等待图片生成的耗时。

export const generateThumbnails = async (url: string, container: { width: number, height: number }): Promise<ImageBitmap> => {

    return new Promise((resolve) => {

        // 基于视频元素绘制缩略图,而非解码视频

        const video = document.createElement('video');

        // 静音

        video.muted = true;


        // 绘制缩略图的canvas画布元素

        const offscreenCanvas = new OffscreenCanvas(container.width, container.height);

        const ctx = offscreenCanvas.getContext('2d');


        // 绘制缩略图的标志量

        let isTimeUpdated = false;

        // 几个视频事件

        // 1. 获取视频尺寸

        video.addEventListener('loadedmetadata', () => {

            // 使用视频尺寸计算,缩略图的尺寸,确定需要几张图片和step的值

            const scale = container.height / video.videoHeight;

            const total = Math.ceil(container.width / (video.videoWidth * scale));


            const drawH = video.videoHeight * scale;

            const drawW = video.videoWidth * scale;


            let seekTime = 0.1;

            const interval = (video.duration - seekTime) / total;


            // 开始执行绘制

            draw(interval, drawW, drawH, seekTime);

        });

        // 2. 触发绘制监控

        video.addEventListener('timeupdate', () => {

            isTimeUpdated = true;

        });


        // 请求视频地址,如果是本地文件,直接执行

        if (/^blob:|base64,/i.test(url)) {

            video.src = url;

        } else {

            fetch(url).then(res => res.blob()).then(blob => {

                // 赋予视频

                video.src = URL.createObjectURL(blob);

            });

        }


        // 绘制方法

        const draw = (interval: number, drawW: number, drawH: number, seekTime: number) => {

            const duration = video.duration;

            let count = 0;

            let currentTime = seekTime + interval * count;


            const loop = () => {

                if (isTimeUpdated && ctx) {

                    // 绘制到指定的位置

                    ctx.drawImage(video, count * drawW, 0, drawW, drawH);

                    currentTime = seekTime + interval * count;

                    count++;


                    if (currentTime > duration) {

                        // 执行完毕

                        resolve(offscreenCanvas.transferToImageBitmap());

                        return;

                    }


                    step();

                    return;

                }

                // 监


控状态

                requestAnimationFrame(loop);

            }


            // 逐步绘制,因为currentTime修改生效是异步的

            const step = () => {

                isTimeUpdated = false;

                video.currentTime = currentTime;

                loop();

            }


            step();

        }

    });

}

代码解析

初始化和创建视频元素

在视频URL之外还会接收一个参数,用来接收容器的尺寸,后面我们需要根据视频尺寸判断需要绘制多少张缩略图

创建离屏画布元素

const offscreenCanvas = new OffscreenCanvas(container.width, container.height);

const ctx = offscreenCanvas.getContext('2d');

创建一个OffscreenCanvas元素,并获取其2D绘图上下文;OffscreenCanvas用于在后台线程绘制图形,可以提高性能。

加载视频并获取元数据

video.addEventListener('loadedmetadata', () => {

    const scale = container.height / video.videoHeight;

    const total = Math.ceil(container.width / (video.videoWidth * scale));

    const drawH = video.videoHeight * scale;

    const drawW = video.videoWidth * scale;

    let seekTime = 0.1;

    const interval = (video.duration - seekTime) / total;

    draw(interval, drawW, drawH, seekTime);

});

loadedmetadata事件中获取视频的宽度和高度,然后根据容器的尺寸计算缩略图的数量和每个缩略图的尺寸;再根据缩略图数量和视频时长计算每次截取视频帧的时间间隔。

seekTIme设置为0.1是因为很多视频首帧没有内容,所有从0.1s开始进行截屏 。

绘制视频缩略图

const draw = (interval: number, drawW: number, drawH: number, seekTime: number) => {

    const duration = video.duration;

    let count = 0;

    let currentTime = seekTime + interval * count;


    const loop = () => {

        if (isTimeUpdated && ctx) {

            ctx.drawImage(video, count * drawW, 0, drawW, drawH);

            currentTime = seekTime + interval * count;

            count++;

            if (currentTime > duration) {

                resolve(offscreenCanvas.transferToImageBitmap());

                return;

            }

            step();

            return;

        }

        requestAnimationFrame(loop);

    }


    const step = () => {

        isTimeUpdated = false;

        video.currentTime = currentTime;

        loop();

    }


    step();

}

draw函数的整体结构没有改变,主要修改是为:

  • 根据count将截取到的视频帧其绘制到同一个画布上的相应位置。

  • 不再生成图片,而是通过offscreenCanvas.transferToImageBitmap方法将离屏画布内容转换为ImageBitmap对象并返回。

总结

实际测试下来,生成耗时有些许提升,对于这个方案耗时影响比较大的因素,还是视频加载。

测试Demo地址:生成视频缩略图

相对于后端生成,前端生成缩略图的方案,在处理本地视频文件场景时还是比较合适,不需要将视频上传到服务器就可以获取到,因为视频资源就在本地,所以不需要把时间消耗在资源加载上;但是如果是网络资源,就会受到用户网络的限制,而且如果视频资源无法使用Video标签播放或者不允许跨域,我们也没有办法获取到缩略图。



该文章在 2024/8/7 10:48:56 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2024 ClickSun All Rights Reserved