JavaScript實(shí)現(xiàn)視頻轉(zhuǎn)GIF的示例代碼
前言
之前使用過FFMpeg
來做視頻轉(zhuǎn)GIF
,但是FFMpeg
的體積還是太大了,前端加載一般要10M
左右。后面發(fā)現(xiàn)了 Webcodecs
這個新的 Web API
,它提供了解碼視頻的能力。所以就沿著這個方向去使勁,也是實(shí)現(xiàn)了一個純前端的在線的視頻轉(zhuǎn) GIF
功能。
本文一共會按照以下三步去實(shí)現(xiàn)一個視頻轉(zhuǎn) GIF
功能:
- 解封裝視頻,從視頻文件中獲取視頻幀
- 解碼視頻幀,獲取幀圖像信息
- 拼裝幀圖像信息,生成
GIF
視頻解封裝
視頻解封裝是從一個包含多種媒體數(shù)據(jù)的容器中提取出特定類型的媒體數(shù)據(jù)的過程。通過解封裝,可以從容器中分離出視頻軌道、音頻軌道等各種媒體數(shù)據(jù)。
它的主要目的是獲取原始的音頻、視頻等媒體數(shù)據(jù),以便進(jìn)行后續(xù)的處理,比如播放、編輯或者轉(zhuǎn)碼。解封裝后的數(shù)據(jù)可以根據(jù)需要被送入相應(yīng)的解碼器進(jìn)行解碼。
這里使用到的是 mp4box.js
這個庫去解碼上傳的視頻文件,以獲取視頻軌道信息。首先定義一個獲取文件 Buffer
的方法,我這里是上傳文件然后去獲取 ArrayBuffer
:
const getFileArrayBuffer = (file) => { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (e) => { resolve(e.target.result); }; reader.readAsArrayBuffer(file); }); };
然后調(diào)用 mp4box
去解封裝視頻的軌道信息
const data = await getFileArrayBuffer(file); data.fileStart = 0; const getVideoInfo = async (data) => { return new Promise((resolve, rejcet) => { const mp4boxfile = MP4Box.createFile(); mp4boxfile.onError = function (e) { console.log("e", e); rejcet(e); }; mp4boxfile.onReady = (info) => { resolve({ mp4boxfile, info, }); }; mp4boxfile.appendBuffer(data); mp4boxfile.flush(); }); }; const { mp4boxfile, info } = await getVideoInfo(data); console.log(info); const videoTrack = info.tracks.find((track) => track.type === "video"); const timescale = videoTrack.timescale; const duration = videoTrack.duration / timescale; const nbSamples = videoTrack.nb_samples; const fps = Math.round(nbSamples / duration);
以下大概是一個視頻軌道的字段:
這里如果我們想獲取視頻的時長,幀率等信息,需要做一些小小的轉(zhuǎn)換。nb_samples
是視頻總幀數(shù); movie_timescale
我理解是視頻的一個采樣單位,拿 movie_duration/movie_timescale
才是我們視頻的長度,這里大概是 18.2
秒。幀率就是總幀數(shù)/視頻時長,這里大概是 15FPS
。
獲取視頻幀
獲取視頻幀這里用到的是一個較新的 Web API
, VideoDecoder
和 EncodedVideoChunk
,它們的API兼容性如下:
VideoDecoder
是一個較新的API
,它可以讓我們通過JS
在瀏覽器中解碼視頻EncodedVideoChunk
是指表示視頻編碼數(shù)據(jù)塊對象,用于表示已經(jīng)編碼的視頻數(shù)據(jù),這些數(shù)據(jù)可以通過網(wǎng)絡(luò)傳輸并在接收端進(jìn)行解碼。
我們利用VideoDecoder
將mp4box
解封裝后得到的軌道信息進(jìn)一步解析成一幀一幀的圖片,為我們后續(xù)的合成GIF
做準(zhǔn)備。
const videoFrames = []; const initDecoder = () => { const getExtradata = () => { // 生成VideoDecoder.configure需要的description信息 const entry = mp4boxfile.moov.traks[0].mdia.minf.stbl.stsd.entries[0]; const box = entry.avcC ?? entry.hvcC ?? entry.vpcC; if (box != null) { const stream = new MP4Box.DataStream( undefined, 0, MP4Box.DataStream.BIG_ENDIAN ); box.write(stream); // slice()方法的作用是移除moov box的header信息 return new Uint8Array(stream.buffer.slice(8)); } }; // 初始化 VideoDecoder const decoder = new VideoDecoder({ output: (videoFrame) => { createImageBitmap(videoFrame).then((img) => { videoFrames.push({ img, duration: videoFrame.duration, timestamp: videoFrame.timestamp, }); videoFrame.close(); if (videoFrames.length === nbSamples) { const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); const img = videoFrames[0].img; console.log(img); ctx.drawImage(img, 0, 0, img.width, img.height); } }); }, error: (err) => { console.error("videoDecoder錯誤:", err); }, }); const config = { codec: videoTrack.codec, codedWidth: videoTrack.video.width, codedHeight: videoTrack.video.height, description: getExtradata(), }; decoder.configure(config); return decoder; }; let decoder = initDecoder(); const getChunkList = () => { const track = mp4boxfile.getTrackById(videoTrack.id); console.log(track.samples.length); const chunkList = track.samples.map((_, index) => { const sample = mp4boxfile.getSample(track, index); const type = sample.is_sync ? "key" : "delta"; const chunk = new EncodedVideoChunk({ type, timestamp: sample.cts, duration: sample.duration, data: sample.data, }); return chunk; }); return chunkList; }; const chunkList = getChunkList(); chunkList.forEach((chunk) => decoder.decode(chunk));
大概解釋一下上面的代碼:
initDecoder
中我們初始化了一個VideoDecoder
,它接收到數(shù)據(jù)之后就會響應(yīng)output
回調(diào),在output
回調(diào)中我們把videoFrame
轉(zhuǎn)成了一個ImageBitmap
對象(即幀圖像信息),然后收集起來。- 然后我們實(shí)現(xiàn)了一個
getChunkList
函數(shù)來收集解封裝后的視頻數(shù)據(jù),把所有的chunk
收集起來供decoder
調(diào)用 - 兩者配合起來,我們就可以拿到這段視頻軌道的所有視頻幀圖像
合成GIF
當(dāng)所有的視頻幀處理完成之后,docoder
會觸發(fā)一個flush
方法,我們可以在這里進(jìn)行GIF
的合成。這里我GIF
合成使用的庫是gif.js
。實(shí)現(xiàn)代碼如下:
decoder.flush().then(() => { const width = videoFrames[0].img.width; const height = videoFrames[0].img.height; var gif = new GIF({ workers: 4, quality: 10, width, height, }); console.log("開始"); videoFrames .map((frame) => frame.img) .forEach((imageBitmap) => { var offscreenCanvas = new OffscreenCanvas( imageBitmap.width, imageBitmap.height ); var offscreenContext = offscreenCanvas.getContext("2d"); offscreenContext.drawImage(imageBitmap, 0, 0); var imageData = offscreenContext.getImageData( 0, 0, imageBitmap.width, imageBitmap.height ); gif.addFrame(imageData, { delay: 1000 / fps }); }); gif.on("finished", function (blob) { var link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = "animated.gif"; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); // 開始生成 GIF gif.render(); });
簡單解釋一下上面的代碼:
- 由于生成的
imageBitmap
并不能直接喂給gif.addFrame
調(diào)用,所以這里使用了一個離屏Canvas
去轉(zhuǎn)換一下 gif.addFrame(imageData, { delay: 1000 / fps });
這里的delay
參數(shù)就是每一幀圖片持續(xù)的時長,默認(rèn)是500ms
,我們用1秒
除于幀率,來換算出實(shí)際的時長- 合成完畢之后,通過一個
a
標(biāo)簽把GIF
下載下來
通過這樣的方式,一個1M多
的MP4
生成出來的GIF
居然有30M
,我滴媽呀。雖然質(zhì)量跟流暢度還是挺好的,但這個體積也太嚇人了。
所以我們最好對GIF
進(jìn)行一個壓縮,這個場景下壓縮主要是減少合成GIF
的幀圖像以及壓縮每一幀圖像的體積。
所以接下來我們會做如下操作:
new GIF
的畫布寬高縮小一半- 逢兩幀抽取一幀,每一幀的延時變成原來的
2
倍 - 對每一幀進(jìn)行壓縮
完整代碼如下:
decoder.flush().then(() => { const width = videoFrames[0].img.width / 2; const height = videoFrames[0].img.height / 2; const gif = new GIF({ workers: 4, quality: 10, width, height, }); const halfFrames = videoFrames.filter((frame, index) => index % 2 === 0); halfFrames .map((frame) => frame.img) .forEach((imageBitmap) => { const originalWidth = imageBitmap.width; const originalHeight = imageBitmap.height; var offscreenCanvas = new OffscreenCanvas( imageBitmap.width / 2, imageBitmap.height / 2 ); var offscreenContext = offscreenCanvas.getContext("2d"); // 在新Canvas上繪制原始ImageBitmap,并縮小一半 offscreenContext.drawImage( imageBitmap, 0, 0, originalWidth, originalHeight, 0, 0, offscreenCanvas.width, offscreenCanvas.height ); const compressedImageData = offscreenContext.getImageData( 0, 0, offscreenCanvas.width, offscreenCanvas.height ); gif.addFrame(compressedImageData, { delay: (1000 / fps) * 2 }); }); gif.on("finished", function (blob) { // 創(chuàng)建一個虛擬的下載鏈接并觸發(fā)點(diǎn)擊 const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = "animated.gif"; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); // 開始生成 GIF gif.render(); });
下面是生成的GIF
圖,大小在5M
左右
最后
decoder.configure(config);
中有一個description
字段,搞了好久都沒搞定,最后還是拜讀了張鑫旭大佬的文章,才把這個demo
跑通。
跑通這個demo
的時候是十分開心的,前端能做的事情越來越多了,而且Webcodecs
解碼的速度非??欤M鹊剿油晟坪?,會鋪開更多的使用場景。
參考
以上就是JavaScript實(shí)現(xiàn)視頻轉(zhuǎn)GIF的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于JavaScript視頻轉(zhuǎn)GIF的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript的創(chuàng)建多行字符串的7種方法
多行字符串的作用是用來提高源代碼的可讀性.尤其是當(dāng)你處理預(yù)定義好的較長字符串時,把這種字符串分成多行書寫更有助于提高代碼的可讀性和可維護(hù)性.在一些語言中,多行字符串還可以用來做代碼注釋. 大部分動態(tài)腳本語言都支持多行字符串,比如Python, Ruby, PHP. 但Javascript呢?2014-04-04js實(shí)現(xiàn)按鈕控制帶有停頓效果的圖片滾動
這篇文章主要介紹了js實(shí)現(xiàn)按鈕控制帶有停頓效果的圖片滾動,,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-08-08Openlayers實(shí)現(xiàn)角度測量的方法
在Openlayers中,雖然沒有直接的角度測量API,但可以通過自定義方法實(shí)現(xiàn),首先,選取三個頂點(diǎn),利用這些點(diǎn)的坐標(biāo)計算夾角度數(shù),接著,用SVG或canvas繪制代表角度的圓弧,并通過Overlay添加到地圖上,本文給大家介紹Openlayers實(shí)現(xiàn)角度測量的方法,感興趣的朋友一起看看吧2024-11-11JS實(shí)現(xiàn)AES加密并與PHP互通的方法分析
這篇文章主要介紹了JS實(shí)現(xiàn)AES加密并與PHP互通的方法,結(jié)合實(shí)例形式分析了javascript與php的加密、解密算法相關(guān)操作技巧,需要的朋友可以參考下2017-04-04