Частота кадров = 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) секунды, которое необходимо подождать перед продолжением обработки потока данных.❞
Ключевые моменты из вышеизложенного заключаются в следующем:
- Единицей измерения 𝓍 является ¹⁄₁₀₀ секунды.
- Показатели конверсии: 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.