欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JavaScript實(shí)現(xiàn)視頻轉(zhuǎn)GIF的示例代碼

 更新時(shí)間:2024年03月06日 08:37:06   作者:可樂(lè)雞翅kele  
這篇文章主要介紹了JavaScript實(shí)現(xiàn)視頻轉(zhuǎn)GIF,本文一共會(huì)按照以下三步去實(shí)現(xiàn)一個(gè)視頻轉(zhuǎn)?GIF?功能,解封裝視頻,從視頻文件中獲取視頻幀,解碼視頻幀,獲取幀圖像信息,拼裝幀圖像信息,生成?GIF,需要的朋友可以參考下

前言

之前使用過(guò)FFMpeg來(lái)做視頻轉(zhuǎn)GIF,但是FFMpeg的體積還是太大了,前端加載一般要10M左右。后面發(fā)現(xiàn)了 Webcodecs 這個(gè)新的 Web API ,它提供了解碼視頻的能力。所以就沿著這個(gè)方向去使勁,也是實(shí)現(xiàn)了一個(gè)純前端的在線的視頻轉(zhuǎn) GIF 功能。

本文一共會(huì)按照以下三步去實(shí)現(xiàn)一個(gè)視頻轉(zhuǎn) GIF 功能:

  • 解封裝視頻,從視頻文件中獲取視頻幀
  • 解碼視頻幀,獲取幀圖像信息
  • 拼裝幀圖像信息,生成 GIF

視頻解封裝

視頻解封裝是從一個(gè)包含多種媒體數(shù)據(jù)的容器中提取出特定類型的媒體數(shù)據(jù)的過(guò)程。通過(guò)解封裝,可以從容器中分離出視頻軌道、音頻軌道等各種媒體數(shù)據(jù)。

它的主要目的是獲取原始的音頻、視頻等媒體數(shù)據(jù),以便進(jìn)行后續(xù)的處理,比如播放、編輯或者轉(zhuǎn)碼。解封裝后的數(shù)據(jù)可以根據(jù)需要被送入相應(yīng)的解碼器進(jìn)行解碼。

這里使用到的是 mp4box.js 這個(gè)庫(kù)去解碼上傳的視頻文件,以獲取視頻軌道信息。首先定義一個(gè)獲取文件 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);

以下大概是一個(gè)視頻軌道的字段:

這里如果我們想獲取視頻的時(shí)長(zhǎng),幀率等信息,需要做一些小小的轉(zhuǎn)換。nb_samples是視頻總幀數(shù); movie_timescale 我理解是視頻的一個(gè)采樣單位,拿 movie_duration/movie_timescale 才是我們視頻的長(zhǎng)度,這里大概是 18.2 秒。幀率就是總幀數(shù)/視頻時(shí)長(zhǎng),這里大概是 15FPS

獲取視頻幀

獲取視頻幀這里用到的是一個(gè)較新的 Web API , VideoDecoderEncodedVideoChunk ,它們的API兼容性如下:

  • VideoDecoder是一個(gè)較新的API,它可以讓我們通過(guò)JS在瀏覽器中解碼視頻
  • EncodedVideoChunk是指表示視頻編碼數(shù)據(jù)塊對(duì)象,用于表示已經(jīng)編碼的視頻數(shù)據(jù),這些數(shù)據(jù)可以通過(guò)網(wǎng)絡(luò)傳輸并在接收端進(jìn)行解碼。

我們利用VideoDecodermp4box解封裝后得到的軌道信息進(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錯(cuò)誤:", 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中我們初始化了一個(gè)VideoDecoder,它接收到數(shù)據(jù)之后就會(huì)響應(yīng)output回調(diào),在output回調(diào)中我們把videoFrame轉(zhuǎn)成了一個(gè)ImageBitmap對(duì)象(即幀圖像信息),然后收集起來(lái)。
  • 然后我們實(shí)現(xiàn)了一個(gè)getChunkList函數(shù)來(lái)收集解封裝后的視頻數(shù)據(jù),把所有的chunk收集起來(lái)供decoder調(diào)用
  • 兩者配合起來(lái),我們就可以拿到這段視頻軌道的所有視頻幀圖像

合成GIF

當(dāng)所有的視頻幀處理完成之后,docoder會(huì)觸發(fā)一個(gè)flush方法,我們可以在這里進(jìn)行GIF的合成。這里我GIF合成使用的庫(kù)是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();
});

簡(jiǎn)單解釋一下上面的代碼:

  • 由于生成的imageBitmap并不能直接喂給gif.addFrame調(diào)用,所以這里使用了一個(gè)離屏Canvas去轉(zhuǎn)換一下
  • gif.addFrame(imageData, { delay: 1000 / fps });這里的delay參數(shù)就是每一幀圖片持續(xù)的時(shí)長(zhǎng),默認(rèn)是500ms,我們用1秒除于幀率,來(lái)?yè)Q算出實(shí)際的時(shí)長(zhǎng)
  • 合成完畢之后,通過(guò)一個(gè)a標(biāo)簽把GIF下載下來(lái)

通過(guò)這樣的方式,一個(gè)1M多MP4生成出來(lái)的GIF居然有30M,我滴媽呀。雖然質(zhì)量跟流暢度還是挺好的,但這個(gè)體積也太嚇人了。

所以我們最好對(duì)GIF進(jìn)行一個(gè)壓縮,這個(gè)場(chǎng)景下壓縮主要是減少合成GIF的幀圖像以及壓縮每一幀圖像的體積。

所以接下來(lái)我們會(huì)做如下操作:

  • new GIF的畫布寬高縮小一半
  • 逢兩幀抽取一幀,每一幀的延時(shí)變成原來(lái)的2
  • 對(duì)每一幀進(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)建一個(gè)虛擬的下載鏈接并觸發(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);中有一個(gè)description字段,搞了好久都沒(méi)搞定,最后還是拜讀了張?chǎng)涡翊罄械奈恼?,才把這個(gè)demo跑通。

跑通這個(gè)demo的時(shí)候是十分開心的,前端能做的事情越來(lái)越多了,而且Webcodecs解碼的速度非常快,希望等到它更加完善后,會(huì)鋪開更多的使用場(chǎng)景。

參考

以上就是JavaScript實(shí)現(xiàn)視頻轉(zhuǎn)GIF的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于JavaScript視頻轉(zhuǎn)GIF的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • javascript的創(chuàng)建多行字符串的7種方法

    javascript的創(chuàng)建多行字符串的7種方法

    多行字符串的作用是用來(lái)提高源代碼的可讀性.尤其是當(dāng)你處理預(yù)定義好的較長(zhǎng)字符串時(shí),把這種字符串分成多行書寫更有助于提高代碼的可讀性和可維護(hù)性.在一些語(yǔ)言中,多行字符串還可以用來(lái)做代碼注釋. 大部分動(dòng)態(tài)腳本語(yǔ)言都支持多行字符串,比如Python, Ruby, PHP. 但Javascript呢?
    2014-04-04
  • js實(shí)現(xiàn)按鈕控制帶有停頓效果的圖片滾動(dòng)

    js實(shí)現(xiàn)按鈕控制帶有停頓效果的圖片滾動(dòng)

    這篇文章主要介紹了js實(shí)現(xiàn)按鈕控制帶有停頓效果的圖片滾動(dòng),,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2016-08-08
  • 淺談javascript實(shí)現(xiàn)八大排序

    淺談javascript實(shí)現(xiàn)八大排序

    排序有內(nèi)部排序和外部排序,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進(jìn)行排序,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部的排序記錄,在排序過(guò)程中需要訪問(wèn)外存。我們這里說(shuō)說(shuō)八大排序就是內(nèi)部排序。
    2015-04-04
  • element-ui上傳一張圖片后隱藏上傳按鈕功能

    element-ui上傳一張圖片后隱藏上傳按鈕功能

    這篇文章主要介紹了element-ui上傳一張圖片后隱藏上傳按鈕功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下
    2019-05-05
  • JavaScript函數(shù)中this指向問(wèn)題詳解

    JavaScript函數(shù)中this指向問(wèn)題詳解

    這篇文章主要給大家介紹了關(guān)于JavaScript函數(shù)中this指向問(wèn)題的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2021-05-05
  • swiper自定義分頁(yè)器的樣式

    swiper自定義分頁(yè)器的樣式

    這篇文章主要為大家詳細(xì)介紹了swiper自定義分頁(yè)器的樣式,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-09-09
  • Openlayers實(shí)現(xiàn)角度測(cè)量的方法

    Openlayers實(shí)現(xiàn)角度測(cè)量的方法

    在Openlayers中,雖然沒(méi)有直接的角度測(cè)量API,但可以通過(guò)自定義方法實(shí)現(xiàn),首先,選取三個(gè)頂點(diǎn),利用這些點(diǎn)的坐標(biāo)計(jì)算夾角度數(shù),接著,用SVG或canvas繪制代表角度的圓弧,并通過(guò)Overlay添加到地圖上,本文給大家介紹Openlayers實(shí)現(xiàn)角度測(cè)量的方法,感興趣的朋友一起看看吧
    2024-11-11
  • JS實(shí)現(xiàn)AES加密并與PHP互通的方法分析

    JS實(shí)現(xiàn)AES加密并與PHP互通的方法分析

    這篇文章主要介紹了JS實(shí)現(xiàn)AES加密并與PHP互通的方法,結(jié)合實(shí)例形式分析了javascript與php的加密、解密算法相關(guān)操作技巧,需要的朋友可以參考下
    2017-04-04
  • JS中如何比較兩個(gè)Json對(duì)象是否相等實(shí)例代碼

    JS中如何比較兩個(gè)Json對(duì)象是否相等實(shí)例代碼

    這篇文章主要介紹了JS中如何比較兩個(gè)Json對(duì)象是否相等實(shí)例代碼的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下
    2016-07-07
  • 一步步教你用js簡(jiǎn)單實(shí)現(xiàn)新年倒計(jì)時(shí)

    一步步教你用js簡(jiǎn)單實(shí)現(xiàn)新年倒計(jì)時(shí)

    一轉(zhuǎn)眼已經(jīng)臘月了,相信小伙伴們一定想知道我們距離2023新年還有多少天,下面這篇文章主要給大家介紹了關(guān)于如何一步步教你用js簡(jiǎn)單實(shí)現(xiàn)新年倒計(jì)時(shí)的相關(guān)資料,需要的朋友可以參考下
    2022-12-12

最新評(píng)論