狠狠色丁香婷婷综合尤物/久久精品综合一区二区三区/中国有色金属学报/国产日韩欧美在线观看 - 国产一区二区三区四区五区tv

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

前端生成視頻縮略圖

freeflydom
2024年8月7日 8:42 本文熱度 1071

前言

接到一個需求,需要前端生成獲取視頻的縮略圖,并且需要把多張圖片拼接在一起,類似于剪輯軟件時間軸的效果:

 

在服務(wù)端使用ffmpeg生成其實比較簡單,但是別問為啥要前端來實現(xiàn),問就是沒空!

總體思路

首先想到的就是在瀏覽器端引入ffmpeg.wasm,但是這樣會增大應(yīng)用體積,如果沒有其他視頻處理的需求,還是盡量避免這個方案。

然后想到的是WebCodecs API,WebCodecs API是瀏覽器提供處理音視頻的原生接口,但是只支持視頻的編解碼,不支持解封裝,需要搭配mp4box.js使用,而且mp4box.js只支持mp4、mov格式的視頻,所有也不考慮這個方案。

最后的方案是張鑫旭大神在使用JS快速獲取video視頻任意位置的縮略圖提到的Video標(biāo)簽獲取截圖的方案,接下來詳細了解一下

第一步、獲取視頻截圖

代碼實現(xiàn)

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

    if (typeof url != 'string') {

        return;

    }

    // 默認參數(shù)

    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

    });


    // 繪制縮略圖的標(biāo)志量

    let isTimeUpdated = false;

    // 幾個視頻事件

    // 1. 獲取視頻尺寸

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

        canvas.width = video.videoWidth;

        canvas.height = video.videoHeight;


        // 開始執(zhí)行繪制

        draw();

    });

    // 2. 觸發(fā)繪制監(jiān)控

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

        isTimeUpdated = true;

    });


    // 獲取視頻數(shù)據(jù)

    params.onLoading();

    // 請求視頻地址,如果是本地文件,直接執(zhí)行

    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;

            }

            // 監(jiān)控狀態(tài)

            requestAnimationFrame(loop);

        }


        // 逐步繪制,因為currentTime修改生效是異步的

        const step = () => {

            isTimeUpdated = false;

            video.currentTime = seekTime;


            loop();

        }


        step();

    }

};

代碼解析

handleGetVideoThumb函數(shù)是實現(xiàn)視頻截圖功能的核心。它接受兩個參數(shù):視頻的URL和可選的配置對象options

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

    if (typeof url != 'string') {

        return;

    }

    // 默認參數(shù)

    const defaults = {

        onLoading: () => {},

        onLoaded: () => {},

        onFinish: (arr) => {}

    };


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

    // ...

};

在這段代碼中,首先檢查url是否為字符串類型,確保輸入有效。然后,定義了默認的回調(diào)函數(shù),并通過Object.assign合并用戶定義的選項。

視頻元素與Canvas初始化

接下來,代碼創(chuàng)建了視頻元素video和Canvas元素canvas,用于加載視頻和繪制縮略圖:

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

video.muted = true; // 靜音視頻


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

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

    willReadFrequently: true

});

這里,video元素被設(shè)置為靜音,以防止自動播放時產(chǎn)生聲音。canvasgetContext方法設(shè)置了willReadFrequently選項,這有助于提高繪制性能。

視頻加載與尺寸設(shè)置

視頻加載過程中,通過監(jiān)聽loadedmetadata事件來獲取視頻的尺寸,并設(shè)置canvas的大小:

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

    canvas.width = video.videoWidth;

    canvas.height = video.videoHeight;

    // 開始執(zhí)行繪制

    draw();

});

縮略圖繪制邏輯

draw函數(shù)是繪制縮略圖的核心,它定義了如何從視頻中捕獲幀并生成縮略圖:

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函數(shù)中,使用requestAnimationFrame創(chuàng)建了一個循環(huán),該循環(huán)在視頻的timeupdate事件觸發(fā)時執(zhí)行。每次循環(huán)都會清除畫布并重新繪制當(dāng)前視頻幀,然后生成縮略圖的blob,并將其轉(zhuǎn)換為URL。

視頻數(shù)據(jù)獲取

視頻數(shù)據(jù)的獲取處理了本地和遠程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,直接設(shè)置為視頻源。對于遠程URL,使用fetch請求視頻數(shù)據(jù),并將其轉(zhuǎn)換為blob對象,然后創(chuàng)建一個對象URL。

第二步、方案優(yōu)化

如果直接使用這個方法截圖會存在一些性能問題,比如:

  1. 視頻的縮略圖列表需要使用多個img標(biāo)簽展示,如果列表比較長或者需要同時展示多個縮略圖列表,會使用到很多的img標(biāo)簽,造成性能問題

  2. canvas.toBlob比較耗時,而且需要等待圖片生成完成,才進行下一張截圖的生成,我們可以直接使用canvas展示圖片內(nèi)容,避免調(diào)用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');


        // 繪制縮略圖的標(biāo)志量

        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;


            // 開始執(zhí)行繪制

            draw(interval, drawW, drawH, seekTime);

        });

        // 2. 觸發(fā)繪制監(jiān)控

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

            isTimeUpdated = true;

        });


        // 請求視頻地址,如果是本地文件,直接執(zhí)行

        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) {

                        // 執(zhí)行完畢

                        resolve(offscreenCanvas.transferToImageBitmap());

                        return;

                    }


                    step();

                    return;

                }

                // 監(jiān)


控狀態(tài)

                requestAnimationFrame(loop);

            }


            // 逐步繪制,因為currentTime修改生效是異步的

            const step = () => {

                isTimeUpdated = false;

                video.currentTime = currentTime;

                loop();

            }


            step();

        }

    });

}

代碼解析

初始化和創(chuàng)建視頻元素

在視頻URL之外還會接收一個參數(shù),用來接收容器的尺寸,后面我們需要根據(jù)視頻尺寸判斷需要繪制多少張縮略圖

創(chuàng)建離屏畫布元素

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

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

創(chuàng)建一個OffscreenCanvas元素,并獲取其2D繪圖上下文;OffscreenCanvas用于在后臺線程繪制圖形,可以提高性能。

加載視頻并獲取元數(shù)據(jù)

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事件中獲取視頻的寬度和高度,然后根據(jù)容器的尺寸計算縮略圖的數(shù)量和每個縮略圖的尺寸;再根據(jù)縮略圖數(shù)量和視頻時長計算每次截取視頻幀的時間間隔。

seekTIme設(shè)置為0.1是因為很多視頻首幀沒有內(nèi)容,所有從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函數(shù)的整體結(jié)構(gòu)沒有改變,主要修改是為:

  • 根據(jù)count將截取到的視頻幀其繪制到同一個畫布上的相應(yīng)位置。

  • 不再生成圖片,而是通過offscreenCanvas.transferToImageBitmap方法將離屏畫布內(nèi)容轉(zhuǎn)換為ImageBitmap對象并返回。

總結(jié)

實際測試下來,生成耗時有些許提升,對于這個方案耗時影響比較大的因素,還是視頻加載。

測試Demo地址:生成視頻縮略圖

相對于后端生成,前端生成縮略圖的方案,在處理本地視頻文件場景時還是比較合適,不需要將視頻上傳到服務(wù)器就可以獲取到,因為視頻資源就在本地,所以不需要把時間消耗在資源加載上;但是如果是網(wǎng)絡(luò)資源,就會受到用戶網(wǎng)絡(luò)的限制,而且如果視頻資源無法使用Video標(biāo)簽播放或者不允許跨域,我們也沒有辦法獲取到縮略圖。



該文章在 2024/8/7 10:48:56 編輯過
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點晴ERP是一款針對中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國內(nèi)大量中小企業(yè)的青睞。
點晴PMS碼頭管理系統(tǒng)主要針對港口碼頭集裝箱與散貨日常運作、調(diào)度、堆場、車隊、財務(wù)費用、相關(guān)報表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點,圍繞調(diào)度、堆場作業(yè)而開發(fā)的。集技術(shù)的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點晴WMS倉儲管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質(zhì)期管理,貨位管理,庫位管理,生產(chǎn)管理,WMS管理系統(tǒng),標(biāo)簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務(wù)都免費,不限功能、不限時間、不限用戶的免費OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved