欧美成人精品手机在线观看_69视频国产_动漫精品第一页_日韩中文字幕网 - 日本欧美一区二区

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

前端生成視頻縮略圖

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

前言

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

 

在服務端使用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