Частота кадров = FPS. Получить FPS видео и указать FPS для GIF. Включает полную реализацию исходного кода.

Обоснование побочного проекта

Ранее в этом году, когда я искал упрощенную реализацию для преобразования видеоклипа в файл GIF (желательно с клиентским JavaScript), большинство предложений, которые я нашел в Интернете, как правило, требовали Node.js (т. е. реализации на стороне сервера). Поскольку легкодоступные плагины с указанной функциональностью, как правило, объединяются в виде узловых модулей, подробностей о создании реализации только для браузера мало. Таким образом, это привело меня к созданию собственного прототипа:



Будучи преисполнен решимости отрегулировать приведенную выше реализацию, я решил больше изучить анимированные GIF-файлы и заметил, что пренебрег важнейшей настройкой — частотой кадров (FPS), также известной как частота кадров. Поэтому я решил включить в эту итерацию возможность для пользователей выбирать предпочтительное значение частоты кадров в секунду.

Кадров в секунду (FPS)

Прежде чем углубляться в детали реализации, важно понять значение FPS.

Между временем начала и прошедшим временем любого анимационного мультимедиа определенная нет. обновлений изображения были внесены в представление. Каждое обновленное изображение называется ❝Frame❞.

Общая продолжительность исходного видеоклипа: ≈ 6 сек.

Общее количество кадров: 34

∴ Количество кадров в секунду = 34 ÷ 6 ≈ 5,6 кадров в секунду

— Предположим, что вместо этого я хочу настроитьзначение частоты кадров на 3,5 кадра в секунду:

Нет. кадров в секунду = 3,5 кадров в секунду = (5,6−𝓍) кадров в секунду

𝓍 = 5.6−3.5 = 2.1 s

Примечание: 𝓍относится к нет. секунд для задержки перед рендерингом следующего кадра.

  • Определение наилучшего значения частоты кадров для анимированного мультимедиа

❝[…]Время задержки — если не 0, в этом поле указывается число сотых (1/100) секунды, которое необходимо подождать перед продолжением обработки потока данных.

Источник: https://www.w3.org/Graphics/GIF/spec-gif89a.txt

Ключевые моменты из вышеизложенного заключаются в следующем:

  • Единицей измерения 𝓍 является ¹⁄₁₀₀ секунды.
  • Показатели конверсии: 1 единица =¹⁄₁₀₀ секунды.
  • Предполагая, что 𝓍 › 0, минимальное время задержки должно быть 1 единица (для 100 кадров в секунду).

¹⁄₁₀₀ с рендеринг 1 кадр = 0,01 с рендеринг 1 кадр

(0,01 ÷ 0,01) с отображает (1 ÷ 0,01) кадр= 1 с рендерит 100 кадров, что преобразуется в 100 кадров в секунду.

Поскольку время задержки измеряется в количестве единиц (¹⁄₁₀₀ с), на следующем графике показана взаимосвязь между FPS и временем задержки:

Примечание. Полная таблица данных для диаграммы доступна на моем GitHub — FPS.csv

  • Хотя 100 кадров в секунду теоретически возможно, большинство устройств на это не способны. Таким образом, это может быть опущено для выбора позже в приложении.

Наконец, в следующем разделе будет рассмотрен технический обзор реализации Video to GIF Maker с подключаемым модулем JavaScript — GIFEncoder.js.

Техническая реализация — всего 4 шага

Условие: Включите в 3 файла(ов) ниже + b64.js следующим образом:

К сведению:эти плагины изначально были получены из репозитория GitHub jsgif пользователем GitHub Kevin Kwok (создателем).

  • Затем перейдите к импорту вышеуказанных файлов в файл разметки HTML следующим образом:
<script type="text/javascript" src="LZWEncoder.js"></script>
<script type="text/javascript" src="NeuQuant.js"></script>
<script type="text/javascript" src="GIFEncoder.js"></script><script type="text/javascript" src="b64.js"></script>

Шаг (1): загружается видеоклип (≤ 30 секунд).

Создайте простой пользовательский интерфейс ввода с элементом ввода файла HTML:

<input id='inputVideoClipFile' type='file' multiple='false' accept='.mp4,.webm,.avi,.mpeg,.flv,.mov,.3gp' />

Пометьте прослушиватель событий (Event: change) выше и приступайте к инициализации экземпляра FileReader в JavaScript:

function readFileAsDataURL(file) {
    return new Promise((resolve,reject) => {
        let fileredr = new FileReader();
        fileredr.onload = () => resolve(fileredr.result);
        fileredr.onerror = () => reject(fileredr);
        fileredr.readAsDataURL(file);
    });
}
inputVideoClipFile.addEventListener('change', async(evt) => {
    let file = evt.target.files[0];
    if(!file) return;
    let fileName=file.name;
    let fileType=file.type;
    let fileSize=(file.size/1024).toFixed(2);
    let b64Str = await readFileAsDataURL(file);
    /* TO DO CODE HERE */
});
  • Обратите внимание, что экземпляр new FileReader() вызывает readAsDataURL, поэтому содержимое видеофайла, назначенное b64Str, читается как строка Base64.
  • Информация о видеофайле извлекается из объекта file для последующего отображения

Шаг (2): Обработайте двоичные данные видео и извлеките кадры. Есть 2 основные части, которые следует учитывать —

Часть I.Предварительный просмотр видеоконтента путем создания элемента <video></video> DOM в JavaScript.

// rendered as <video></video> in HTML code
const loadVideo = (url) => new Promise((resolve, reject) => {
    var vid = document.createElement('video');
    vid.addEventListener('canplay', () => resolve(vid));
    vid.addEventListener('error', (err) => reject(err));
    vid.src = url;
});
inputVideoClipFile.addEventListener('change', async(evt) => {
    let file = evt.target.files[0];
    if(!file) return;
    let fileName=file.name;
    let fileType=file.type;
    let fileSize=(file.size/1024).toFixed(2);
    let b64Str = await readFileAsDataURL(file);
    /* TO DO CODE HERE */
    let videoObj=await loadVideo(b64Str);
    videoObj.autoplay=false;
    videoObj.muted=true;
    videoObj.loop=false;
    let vidDuration=parseInt(videoObj.duration);
    let vidHeight=videoObj.videoHeight; // 720
    let vidWidth=videoObj.videoWidth; // 1280
    videoObj.height=vidHeight;
    videoObj.width=vidWidth;
    videoObj['style']['height']=`${vidHeight}px`;
    videoObj['style']['width']=`${vidWidth}px`;
    document.getElementById('inputVideoPreview').appendChild(videoObj);
});
  • Обратите внимание, что b64Str — это данные видеофайла, прочитанные FileReader() на предыдущем шаге.
  • KIV: настройки видео для autoplay,muted,loop установлены как false
  • Код разметки HTML должен содержать<div id='inputVideoPreview'></div>

Часть II. Извлечение кадра.Каждый видеокадр относится к моментальному снимку изображения клипа с уникальными временными метками.

Поскольку файл GIF создается путем слияния набора последовательных изображений, для каждого хронологического графического обновления видео должен быть извлечен кадр со встроенными данными изображения для последующего процесса создания GIF.

Хотя необходимые данные изображения для каждого видеокадра нельзя напрямую извлечь из элемента DOM <video></video>, предварительное содержимое в <video></video> вместо этого можно рендерить в<canvas></canvas> элемент. strong> для извлечения данных изображения кадра.

Таким образом, создайте элемент <canvas></canvas> (похожий на <video></video>) в JavaScript, а затем соответственно масштабируйте <video></video> и <canvas></canvas> для отображения:

const byteToKBScale = 0.0009765625;
const displayedSize=500;
const scale = window.devicePixelRatio;
function scaleCanvas(_CANVAS, videoObj, vidHeight, vidWidth, scale){
    _CANVAS['style']['height'] = `${vidHeight}px`;
    _CANVAS['style']['width'] = `${vidWidth}px`;
    let cWidth=vidWidth*scale;
    let cHeight=vidHeight*scale;
    _CANVAS.width=cWidth;
    _CANVAS.height=cHeight;
    _CANVAS.getContext('2d').scale(scale, scale);
}
inputVideoClipFile.addEventListener('change', async(evt) => {
    /* THIS SEGMENT IS OMMITTED FOR VISUAL CONVENIENCE */
    /* ACTUAL CODE REFERS TO THE ABOVE IMPLEMENTED */
    let _CANVAS = document.createElement('canvas');
    scaleCanvas(_CANVAS, videoObj, vidHeight, vidWidth, scale);
    
    document.getElementById('hiddenCanvas').appendChild(_CANVAS)
    let sizeBenchmark=vidHeight;
    if(vidWidth>vidHeight) {
        sizeBenchmark=vidWidth;
    }
    let scaleRatio=parseFloat(displayedSize/sizeBenchmark);
    let displayedHeight=scaleRatio*vidHeight;
    let displayedWidth=scaleRatio*vidWidth;
    videoObj['style']['height']=`${displayedHeight}px`;
    videoObj['style']['width']=`${displayedWidth}px`;
    scaleCanvas(_CANVAS, videoObj, displayedHeight, displayedWidth, scale);
});
  • Обратите внимание, что vidWidth и vidHeight извлекаются из <video></video>. (Это исходные размеры клипа.)
  • Так как каждый кадр рендерится на элемент <canvas></canvas>, необходимо масштабировать его на основе плотности пикселей, чтобы сохранить исходное разрешение видео и соотношение сторон (реализовано в утилите function scaleCanvas())
  • Код разметки HTML должен содержать<div id='hiddenCanvas'></div>

Теперь, когда оба элемента <video></video> и <canvas></canvas> инициализированы и масштабированы, должно продолжаться фактическое извлечение кадра. Чтобы это произошло, необходимо последовательно выполнить следующее для извлечения каждого кадра:

a)Данные изображения видеокадра рисуются на <canvas></canvas>

b)A GIFEncoder() требуется для добавления вывода a) в качестве кадра для последующего кодирования.

c)Расчет нескорректированного количества кадров в секунду и времени задержки для рендеринга следующего кадра.

  • В HTML-код включите раскрывающийся список со всеми 18 параметрами (кроме 100 кадров в секунду) для выбора.
<select id='fpsDropdownList'></select>
  • Экземпляр GIFEncoder() инициализируется следующим образом
var encoder = new GIFEncoder(vidWidth, vidHeight);
encoder.setRepeat(0);
encoder.setDelay(0);
encoder.setQuality(10); // default value
  • Затем создается служебная функция step() для инкапсуляции a),b)&c)
var startTime=0;
var frameIndex=0;
var requiredFPSDelay=0;
var FPS=0; // used to capture current fps
const step = async() => {
  if(startTime == 0) { startTime=(Date.now()); } // in ms
  let _CANVAS_CTX=_CANVAS.getContext('2d');
  _CANVAS_CTX.drawImage(videoObj, 0, 0, displayedWidth, displayedHeight);
  encoder.addFrame(_CANVAS_CTX);
  if(FPS==0) {
    let elapsed = ((Date.now()) - startTime) / 1000.0;
    FPS=(frameIndex / elapsed)*1000.0;
    let fpsDropdownList=document.getElementById('fpsDropdownList');
    let requiredFPS=parseInt(fpsDropdownList.value);
    let requiredFPSDelay=FPS-(requiredFPS*1000);
    if(requiredFPSDelay<0){ requiredFPSDelay=0; }
  }
  await new Promise((resolve, reject) => setTimeout(resolve, 0));
  videoObj.requestVideoFrameCallback(step);
};
videoObj.addEventListener('play', (vEvt) => {
   encoder.start();
   videoObj.requestVideoFrameCallback(step);
}, false);
videoObj.addEventListener('ended', (vEvt) => {
   encoder.finish();
}, false);
videoObj.play();
  • Чтобы определить необходимое время задержки, после рендеринга первого кадра переменная FPS фиксирует текущее значение частоты кадров на основе истекшего времени(обратите внимание, что время задержки не может быть меньше 0)
  • Так как autoplay=false, play() должен быть вызван для <video></video> в то время как videoObj.requestVideoFrameCallback(step) запрос для последующего кадра и принимает функцию обратного вызова (т.е. step()) для обработки данных изображения каждого кадра
  • _CANVAS_CTX.drawImage() переходит к рендерингу каждого снимка изображения на _CANVAS для GIFEncoder() для захвата кадра с помощью addFrame()
  • Когда генерируются события play и ended, GIFEncoder() вызывает start() и finish() соответственно.

Шаг (4): Создание GIF с помощью GIFEncoder

Чтобы извлечь из encoder объединенную версию всех кадров (т. е. вывод GIF), необходимо реализовать следующий фрагмент кода JavaScript:

var fileType='image/gif';
var readableStream=encoder.stream();
var binary_gif=readableStream.getData();
var b64Str='data:'+fileType+';base64,'+encode64(binary_gif);
  • encode64() — это метод, присутствующий в b64.js, для преобразования потоковых данных, захваченных GIFEncoder(), в формат Base64.
  • b64Str относится к данным, закодированным для файла GIF путем слияния всех кадров, присутствующих в GIFEncoder(). Следовательно, в коде HTML включите: <img id='outputGif' src=’${b64Str}’ alt=’${fileName}’ /> для предварительного просмотра выходного файла GIF.

Наконец, ссылка для загрузки файла GIF создается следующим образом:

let dwnlnk = document.createElement('a');
dwnlnk.download = fileName;
dwnlnk.innerHTML = `💾 <small>Save</small>`;
dwnlnk.className = 'btn btn-outline-dark';
dwnlnk.href = b64Str;

К вашему сведению: полная реализация кода находится на моем GitHub: video-to-GIF (не стесняйтесь ★ или 🔱fork!) или попробуйте демо!

И вот оно! Большое спасибо за упорство до конца этой статьи! ❤ Надеюсь, вы нашли это руководство полезным, и не стесняйтесь следовать за мной на Medium, если вы хотите больше контента, связанного с ГИС, аналитикой данных и веб-приложениями. Буду очень признателен — 😀

— 🌮 Пожалуйста, купи мне тако ξ(🎀˶❛◡❛)



Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter и LinkedIn. Присоединяйтесь к нашему сообществу Discord.