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

前端生成视频缩略图

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

前言

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

 

在服务端使用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-2025 ClickSun All Rights Reserved