Как написать свой видеоплеер

Время на прочтение
9 мин

Количество просмотров 171K

Ранее мы уже рассмотрели общие вопросы использования HTML5 Audio и Video и начали погружаться в детали, начав с задачи определения поддержки браузером нужного кодека. Сегодня мы рассмотрим задачу создания собственного видео-плеера на HTML5 Video.


Напомню, что video-элемент сам по себе уже обеспечивает необходимый набор контролов для управления проигрыванием. Чтобы была видна панель управления воспроизведением, достаточно указать атрибут controls.

<video src="trailer_480p.mp4" width="480" height="270" poster="poster.gif" controls />

Однако, как я отмечал в вводной статье, со стандартными контролами есть проблема, которая заключается как раз в том, что выглядят они нестандартно. Другими словами, в каждом браузере они выглядят по-своему (проверить, как выглядят контролы в разных браузерах, можно на примере Video Format Support на ietestdrive.com — просто откройте его в двух-трех различных браузерах).

API для управления воспроизведением

Стандарт HTML5 для работы с видео вводит в DOM новый интерфейс — HTMLVideoElement, наследующий в свою очередь интерфейс HTMLMediaElement.

Интерфейс HTMLMediaElement

Это общий интерфейс для обоих медиа-элементов (аудио и видео), описывающий доступ к базовым возможностями работы с медиа-контентом: контроль источника контента, управление воспроизведением, изменение уровня звука и обработка ошибок. Основные свойства и методы, которые нам понадобятся:

Состояние сети и готовность к работе
src — ссылка (url) на воспроизводимый контент
buffered — буферизованные куски видео

Воспроизведение и контролы
currentTime — текущий момент проигрывания (с.)
duration — длительность медиа-контента (с.)
paused — находится ли воспроизведение на паузе
ended — закончилось ли проигрывание
muted — включение/выключение звука
volume — уровень звука [0, 1]
play() — начать проигрывание
pause() — поставить на паузу

События
oncanplay — можно начать проигрывание
ontimeupdate — изменена позиция проигрывания
onplay — запущено проигрыв
onpause — нажата пауза
onended — воспроизведение закончилось

Важно: это далеко не все методы и свойства, выставляемые через интерфейс HTMLMediaElement.

Интерфейс HTMLVideoElement

Видео отличается от аудио несколькими дополнительными свойствами:
width и height — ширина и высота контейнера для проигрывания видео;
videoWidth и videoHeight — внутреннее значение ширины и высоты видео, если размеры не известны, равны 0;
poster — ссылка на картинку, которую можно показывать, пока видео недоступно (обычно это один
из первых непустых кадров).

Разница между width/height и videoWidth/videoHeight в том, что последние — это собственные характеристики видео, в частности, с учетом соотношения сторон и других характеристик, в то время как контейнер для видео может быть любых размеров (больше, меньше, с другой пропорцией).

Play & Pause

Создание нашего собственного видео-плеера мы начнем с простой задачи: научимся запускать видео на проигрывание и останавливать воспроизведение. Для этого нам понадобятся методы play() и pause() и несколько свойств, описывающих текущее состояние видео-потока (мы также будем использовать библиотеку jQuery, не забудьте ее подключить).

Первым делом нам необходим video-элемент, которым мы хотим управлять, и элемент на который можно нажимать для управления текущим состоянием:

<div>
    <video id="myvideo" width="480" height="270" poster="poster.gif" >
        <source src="trailer_480p.mp4" type='video/mp4;codecs="avc1.42E01E, mp4a.40.2"' />
        <source src="trailer_480p.webm" type='video/webm; codecs="vorbis,vp8"'/> 
    </video>
</div>
<div id="controls">
    <span id="playpause" class="paused" >Play</span>
</div>
#controls span {
    display:inline-block;
}
        
#playpause {
    background:#eee;
    color:#333;
    padding:0 5px;
    font-size:12pt;
    text-transform:uppercase;
    width:50px;
}

Обратите внимание на инвертирование состояния кнопки (paused) и действия (play).

Теперь надо добавить немного js-кода, чтобы нажатие на кнопку play переключало ее состояние и соответственно запускало видео-ролик или ставило его на паузу:

$(document).ready(function(){
    var controls = {
        video: $("#myvideo"),
        playpause: $("#playpause")                 
    };
                
    var video = controls.video[0];
               
    controls.playpause.click(function(){
        if (video.paused) {
            video.play();
            $(this).text("Pause");    
        } else {
            video.pause();
            $(this).text("Play");
        }
                
        $(this).toggleClass("paused"); 
    });
}); 

При желании можно сразу добавить несколько css-стилей для кнопок управления и их различных состояний и…

… казалось бы, все уже замечательно работает, но не тут-то было! Есть несколько мелочей, которые нам также нужно учесть.

Проигрывание сначала

Во-первых, нам нужно правильно обработать окончание проигрывания видео-ролика (если, конечно, оно не зациклено), и в этот момент нужно переключить кнопки управления так, чтобы вместо состояния «pause» было состояние «play»:

video.addEventListener("ended", function() {
    video.pause();
    controls.playpause.text("Play");
    controls.playpause.toggleClass("paused");
});

Контекстное меню

Во-вторых, браузеры обычно добавляют возможность управлять воспроизведением через контекстное меню. Это означает, что пользователь, вообще говоря, может что-то изменить в обход наших элементов управления. Этот момент нужно также отловить и внести необходимые изменения во внешний вид контролов. Для этого достаточно подписаться на события onplay и onpause.

video.addEventListener("play", function() {
    controls.playpause.text("Pause");
    controls.playpause.toggleClass("paused");
});
                
video.addEventListener("pause", function() {
    controls.playpause.text("Play");
    controls.playpause.toggleClass("paused");
});

Так как у нас становится многовато мест, где меняется внешний вид, самое время попутно произвести небольшой рефакторинг, убрав из изначального переключения режимов теперь уже дублирующую смену внешнего состояния:

var controls = {
    ...  
    togglePlayback: function() {
        (video.paused) ? video.play() : video.pause();
    }
    ...
};
                
controls.playpause.click(function(){
    controls.togglePlayback();
});

Кликабельное видео

Наконец, наверняка, нам захочется, чтобы проигрывание и пауза переключались по нажатию на само видео, поэтому нужно добавить еще несколько строчек:

controls.video.click(function() {
    controls.togglePlayback();
});

Текущий результат:

Прогресс

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

<span id="progress">
    <span id="total">
        <span id="buffered"><span id="current">​</span></span>
    </span>
</span>
<span id="time">
    <span id="currenttime">00:00</span> / 
    <span id="duration">00:00</span>
</span>

И соответствующие стили:

#progress {
    width:290px;
}
            
#total {
    width:100%;                
    background:#999;
}
            
#buffered {
    background:#ccc;
}
            
#current {
    background:#eee;
    line-height:0;
    height:10px;
}
            
#time {
    color:#999;
    font-size:12pt;
}

И несколько ссылок на соответствующие элементы для быстрого доступа в объект controls:

var controls = {
    ...
    total: $("#total"),
    buffered: $("#buffered"),
    progress: $("#current"),
    duration: $("#duration"),
    currentTime: $("#currenttime"),
    hasHours: false,
    ...
};

Первым делом, нам нужно понять, какова длительность ролика — для этого у video-элемента есть свойство duration. Отследить это значение можно, например, в момент готовности ролика к проигрыванию — по событию oncanplay:

video.addEventListener("canplay", function() {
    controls.hasHours = (video.duration / 3600) >= 1.0;                    
    controls.duration.text(formatTime(video.duration, controls.hasHours));
    controls.currentTime.text(formatTime(0),controls.hasHours);
}, false);

В данном случае, мы попутно определяем, нужно ли отображать количество часов в видео-плеере (кстати, вообще говоря, спецификация предполагает, что длительность ролика может изменяться — в этот момент срабатывает событие ondurationchange, и к тому же быть бесконечной — например, при стриминге радио).

Также мы используем специальную функцию formatTime для перевода секунд в формат HH:mm:ss или mm:ss:

function formatTime(time, hours) {
    if (hours) {
        var h = Math.floor(time / 3600);
        time = time - h * 3600;
                    
        var m = Math.floor(time / 60);
        var s = Math.floor(time % 60);
                    
        return h.lead0(2)  + ":" + m.lead0(2) + ":" + s.lead0(2);
    } else {
        var m = Math.floor(time / 60);
        var s = Math.floor(time % 60);
                    
        return m.lead0(2) + ":" + s.lead0(2);
    }
}
            
Number.prototype.lead0 = function(n) {
    var nz = "" + this;
    while (nz.length < n) {
        nz = "0" + nz;
    }
    return nz;
};

Для отображения процесса проигрывания нам понадобится событие ontimeupdate, срабатывающее при изменении текущего момента:

video.addEventListener("timeupdate", function() {
    controls.currentTime.text(formatTime(video.currentTime, controls.hasHours));
                    
    var progress = Math.floor(video.currentTime) / Math.floor(video.duration);
    controls.progress[0].style.width = Math.floor(progress * controls.total.width()) + "px";
}, false);

Свойство currentTime выдает в секундах текущее время. Его же можно использовать, чтобы изменить время проигрывания:

controls.total.click(function(e) {
    var x = (e.pageX - this.offsetLeft)/$(this).width();
    video.currentTime = x * video.duration;
});

Также будет полезным показывать буферизацию видео, для этого можно отталкиваться от события onprogress, срабатывающего при загрузке новых порций видео:

video.addEventListener("progress", function() {
    var buffered = Math.floor(video.buffered.end(0)) / Math.floor(video.duration);
    controls.buffered[0].style.width =  Math.floor(buffered * controls.total.width()) + "px";
}, false);

Важный нюанс относительно свойства buffered, который нужно иметь в виду, заключается в том, что он предоставляет не просто время в секундах, а промежутки времени в виде объекта TimaRanges. В большинстве случаев это будет только один промежуток с индексом 0, и начинающийся с отметки 0c. Однако, если браузер использует HTTP range запросы к серверу, например, в ответ на попытки перейти к другим фрагментам видео-потока, промежутков может быть несколько. Также надо учитывать, что в зависимости от реализации браузер может удалять из буфера памяти уже проигранные куски видео.

Промежуточный результат:

Звук

Наконец, давайте добавим еще небольшой штрих к нашем видео-плееру — возможность включать и выключать звук. Для этого добавим небольшой контрол с динамиком (SVG-иконка взята с сайта The Noun Project):

<span id="volume">
    <svg id="dynamic" version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
	 width="16px" height="16px" viewBox="0 0 95.465 95.465">
        <g >
            <polygon points="39.323,20.517 22.705,37.134 0,37.134 0,62.865 22.705,62.865 39.323,79.486 "/>
            <path d="M52.287,77.218c14.751-15.316,14.751-39.116,0-54.436c-2.909-3.02-7.493,1.577-4.59,4.59
                        c12.285,12.757,12.285,32.498,0,45.254C44.794,75.645,49.378,80.241,52.287,77.218L52.287,77.218z"/>
            <path d="M62.619,89.682c21.551-22.103,21.551-57.258,0-79.36c-2.927-3.001-7.515,1.592-4.592,4.59
                        c19.08,19.57,19.08,50.608,0,70.179C55.104,88.089,59.692,92.683,62.619,89.682L62.619,89.682z"/>
            <path d="M75.48,99.025c26.646-27.192,26.646-70.855,0-98.051c-2.936-2.996-7.524,1.601-4.592,4.59
                        c24.174,24.674,24.174,64.2,0,88.871C67.956,97.428,72.545,102.021,75.48,99.025L75.48,99.025z"/>
        </g>
        </svg>
</span>

С соответствующими стилями для включенного и выключенного состояний:

#dynamic {
    fill:#333;
    padding:0 5px;
}
            
#dynamic.off {
    fill:#ccc;
}

Для переключения состояния динамика нам понадобится свойство mute:

controls.dynamic.click(function() {
    var classes = this.getAttribute("class");

    if (new RegExp('\boff\b').test(classes)) {
        classes = classes.replace(" off", "");
    } else {
        classes = classes + " off";
    }

    this.setAttribute("class", classes);
                    
    video.muted = !video.muted;
});

(Стандартные методы jQuery для переключения css-классов не работают с SVG-элементами.)
Если вы хотите также менять уровень громкости, то вам поможет свойство volume, принимающее значения в диапазоне [0, 1].

Финальный результат:

Что еще…

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

  • проверка поддержки браузером HMTL5 Video,
  • программное определение и переключение поддерживаемых кодеков,
  • поддержка субтитров, в том числе для обеспечния accessibility.

Также не забудьте, что привязку событий к элементам управления нужно делать после того, как стало понятно, что видео доступно для проигрывания (oncanplay):

video.addEventListener("canplay", function() {
    ...
}, false);

Либо нужно делать соответствующие проверки или отлавливать возможные исключения. Исключения вообще надо отлавливать, например, событие onerror, возникающее при ошибке загрузки видео-потока :)

Из дополнительных опций, которые могут вам понадобиться: изменение скорости проигрывания. Для этого есть свойство playbackRate и соответствующее событие onratechange.

Готовые плееры

Думаю, вам не составит труда найти готовые решения для использования HTML5 Video со всеми полагающимися плюшками, вплоть до удобной кастомизации внешнего вида через CSS. Вот несколько полезных ссылок:

  • VideoJS
  • MediaElementJS
  • Kaltura
  • JW Player
  • LeanBack Player
  • Табличка со сравнениями плееров

Наконец, HTML5 Video в спецификации.

В этом уроке мы рассмотрим исходный код утилиты, позволяющей проигрывать ролики с видеохостинга Vimeo. На предыдущем открытом уроке, состоявшемся в рамках онлайн-курса «Программист С» была создана программа, аналогичная известному опенсорсному продукту youtube-dl, который занимается скачиванием файлов с различных видеохостингов. Youtube-dl принимает на вход ссылку на страницу с видео и скачивает видеофайл для последующего локального просмотра любимым плеером. Мы используем часть кода с прошлого занятия, которая получает адрес видеофайла с конкретной страницы Vimeo. На этот раз мы увидим окно плеера с видео (а в идеале — и с аудио), и для этого мы будем использовать фреймворк GStreamer.

В случаях, когда в программе нужна обработка мультимедийных данных, будь то видео или аудио, в независимости от используемого языка (C, C++ или что-то другое) есть два наиболее распространённых варианта для добавления такой функциональности: GStreamer и FFMpeg.

И то, и другое — опенсорсные библиотеки, у обоих достаточно пермиссивные лицензии, то есть их можно использовать в любых коммерческих приложениях. Безусловно, есть ряд других решений, в том числе библиотека libav, которая является форком FFMpeg, но две вышеперечисленных библиотеки можно назвать наиболее популярными. В чём же различия между ними?

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

Резюмируя, если нужно сделать что-то простое, FFMpeg подойдёт, но для более сложных задач имеет смысл обратить взгляд на фреймворк GStreamer. Это не просто библиотека, а целый программный каркас, на который можно насаживать свои элементы, которые занимаются обработкой видео, аудио и прочей мультимедийной информации. По своему внутреннему устройству GStreamer внешне похож на механизм фильтров DirectShow, с которым можно столкнуться при программировании под Windows.

DirectShow — это системная библиотека Windows для обработки видео. В частности, всякий раз, когда вы проигрываете видео на компьютере с данной ОС, под капотом у используемого вами видеоплеера создаётся так называемый pipeline (конвейер) из вышеупомянутых фильтров, и именно этот конвейер позволяет считывать, декодировать и отображать видео. GStreamer также предоставляет различные мультимедийные элементы и возможность комбинировать их в конвейер обработки.

Сам по себе GStreamer базируется на такой библиотеке, как GLib. Я люблю называть её «стандартной библиотекой на стероидах». Язык C разрабатывался в 70-х — 80-х годах прошлого века, и тогдашние взгляды на стандартную библиотеку языка были гораздо более минималистичными, буквально на уровне «можно открывать файлы — уже круто». Поэтому так сложилось, что в языке C стандартная библиотека, будем до конца честны, феноменально бедная, если сравнивать с более поздними языками — такими, как Python. В стандартной библиотеке последнего есть множество функций на любой случай жизни — можно скачать из интернета файл, закодировать данные в один из распространённых форматов вроде CSV или JSON, легко разобрать аргументы командной строки, и всё это — в стандартной поставке языка. В C же в подавляющем большинстве случаев придётся устанавливать сторонние библиотеки, чтобы сделать хоть что-либо.

Возвращаясь к GLib, это сторонняя библиотека, которая распространяется по пермиссивной лицензии LGPL. Она предоставляет множество различных подсистем, как то: отладочное журналирование, обработка ошибок, сетевые взаимодействия, контейнерные типы данных (хэш-таблица, красно-чёрное дерево и прочее, которых, конечно, нет в стандартной библиотеке C) и множество других.

Среди всего прочего, одна из подсистем GLib, называемая GObject, добавляет возможность использовать ООП в чистом C. Эта подсистема добавляет ООП-шные возможности не на уровне языка, а на уровне библиотеки, то есть для их использования всё-таки придётся написать некоторое количество бойлерплейта — вспомогательного кода, который не несёт никакой логики, но требуется для работы GObject. Для этого в библиотеке есть набор макросов, которые генерируют код для поддержки объектов. В GObject поддерживается даже наследование (кроме множественного).

Для более подробного ознакомления с ООП на основе GLib рекомендуется обратиться к статье «GObject: инкапсуляция, инстанциация, интроспекция», являющейся первой из цикла статей на Хабр, подробно описывающего простой пример с наследованием GObject’ов.

ООП в GLib строится на базе обычных C-шных структур, но в этих структурах также хранятся указатели на функции, которые используются в качестве виртуальных методов, то есть таких методов, которые могут быть переопределены в потомках класса при наследовании. Для каждого класса есть две структуры. Одна соответствует самому классу, и с ней происходит работа, когда речь идёт именно про класс всех возможных объектов и его поведение. Другая структура соответствует инстансу класса, то есть конкретному экземпляру. Для наследования используется интересный C-шный трюк. Рассмотрим его на примере из вышеупомянутой статьи:

struct _AnimalCatClass
{
    GObjectClass parent_class; /* родительская классовая структура */
    void (*say_meow) (AnimalCat*); /* виртуальный метод */
    gpointer padding[10]; /* массив указателей */
};

Мы определяем структуру, соответствующую нашему классу кошки _AnimalCatClass, и первым полем в ней мы определяем parent_class, имеющий тип GObjectClass, который также является структурой. Теперь за счёт этого поля и за счёт того, что оно идёт первым в определении нашей структуры, мы можем передавать её в те функции, которые ожидают в качестве параметра GObjectClass. Это работает благодаря расположению структуры в памяти: первые sizeof(GObjectClass) байт в нашей структуре полностью совпадают с GObjectClass, а всё, что идёт после них — это поля, специфичные для нашей структуры. В частности, padding — это резерв для последующих потомков нашей структуры, которые (возможно) будут переопределять виртуальные методы. Для того, чтобы их размер полностью совпадал с размером предка, у потомков размер поля padding нужно будет уменьшать за счёт добавления новых полей.

Далее мы определяем класс тигра _AnimalTiger:

struct _AnimalTiger
{
    AnimalCat parent; /* обязательно первым полем должен идти экземпляр родительского объекта */
    int speed; /* приватные данные */
};

И снова используем вышеупомянутый трюк: самым первым полем в структуре идёт структура AnimalCat, и за счёт этого в любую функцию, ожидающую кота, мы сможем передать тигра 🐯

Вернёмся к GStreamer. Когда мы проигрываем некий видео- или аудиофайл с помощью GStreamer, под капотом у приложения, чем бы оно ни было — видеоплеером, аудиоплеером или нашим C-шным приложением, каждый раз создаётся граф декодирования. Он может выглядеть, например, следующим образом:

Центральная часть фреймворка GStreamer, представляемая этим графом — так называемый pipeline, конвейер. Отдельными ступенями этого конвейера являются экземпляры класса GstElement, в терминологии фреймворка — элементы. Каждый элемент может быть одного из трёх типов.

  1. Это может быть элемент, порождающий медиаинформацию, такой элемент называется source, источник. В примере на картинке выше это file-source, считывающий информацию из файла на диске.
  2. Это может быть так называемый sink (дословно «сток»), являющийся конечной точкой конвейера. Он либо отображает пользователю медиа-информацию, либо утилизирует её другим образом — например, раздаёт по сети в качестве сервера. В примере на картинке у нас в конвейере сразу два стока (да, так тоже можно было), один — audio-sink, воспроизводящий аудио, другой — video-sink, отображающий видео.
  3. Могут быть промежуточные элементы, называемые фильтрами. В примере у нас три разных фильтра: ogg-demuxer, vorbis-decoder и theora-decoder. Суть фильтров в том, что они принимают на вход медиаинформацию, преобразуют её тем или иным образом и отдают дальше по конвейеру. Так, ogg-demuxer не занимается обработкой самой аудио или видеоинформации, он попросту разбирает формат контейнера OGG и достаёт из него эту информацию. В качестве других подобных примеров можно упомянуть другие demuxer’ы: для распространённого формата MP4, различных потоковых форматов, таких, как RTMP и RTSP, и так далее — все эти форматы поддерживаются GStreamer. Также в примере есть фильтры vorbis-decoder и theora-decoder, которые занимаются декодированием медиаинформации: в подавляющем большинстве случаев она хранится в сжатом виде, так как без сжатия даже небольшие по таймингу фрагменты могут занимать огромные объёмы памяти вплоть до десятков и сотен гигабайт. Фильтры-декодеры занимаются тем, что разжимают эту информацию для её дальнейшей обработки.

У каждого элемента есть способ соедиенения с другими элементами, называемый pads. Трудно подобрать адекватный перевод этого термина; отличительная особенность как GLib, так и GStreamer в том, что эти библиотеки широко используют локализацию и интернационализацию, то есть они по максимуму пытаются общаться с пользователем на его родном языке. Так вот, в отладочных журналах GStreamer при запуске программы с русскоязычной локалью можно встретить перевод термина pad как «контактное гнездо» — за неимением лучшего будем далее использовать этот вариант.

Изначально в конвейере элементы, как правило, не связаны друг с другом, если только программист не связал их эксплицитно в момент написания кода. Кроме того, каждый элемент, как правило, имеет различные контактные гнёзда, как входные, так и выходные, и каждый раз, когда GStreamer автоматически строит конвейер, он обнаруживает, каким оптимальным способом можно соединить друг с другом различные элементы. Этот процесс называется caps negotiation, что можно перевести как «переговоры о возможностях». Например, у ogg-demuxer есть два выходных контактных гнезда: для видео и для аудио, и мы не можем, например, взять его аудиовыход и соединить со входом декодера видео. Для работы механизма caps negotiation каждый элемент должен реализовать ряд виртуальных методов, вызываемых фреймворком во время построения конвейера для получения информации о контактных гнёздах элемента и поддерживаемых им типах контента, называемых caps (по-видимому, сокращение от capabilities, «возможности»), например, "video/x-h264" для видео, пожатого кодеком H.264. Формат, в котором задаются возможности, сильно похож на MIME-типы, которые можно часто встретить в web-программировании. Итак, GStreamer, в свою очередь, вызывает вышеуказанные методы и согласует между собой элементы в конвейере; если процесс согласования не удастся, фреймворк сообщит об ошибке.

Для написания программы с использованием GStreamer, например, плеера, либо плагина со своими кастомными элементами, есть следующие ресурсы.

  • Официальная документация GStreamer, к сожалению, по большей части довольно спартанская — одни сухие описания классов и функций, ничего лишнего.
  • Официальное руководство по созданию своих плагинов.

Также в GLib существует механизм подсчёта ссылок, что несколько упрощает написание кода, в том числе с использованием GStreamer — не нужно беспокоиться о том, что буферы памяти, через которые передаётся медиаинформация в конвейере, не были удалены в нужный момент времени и таким образом создают утечку свободной памяти. По сути, GLib предоставляет некий рудиментарный lifetime management для GObject’ов, чуть более гибкий, чем C-шная модель с указателями и эксплицитным указанием всего и вся.

Хорошим стартом для нового проекта с использованием GStreamer будет заранее созданный авторами фреймворка бойлерплейт, упоминаемый в официальном руководстве. Есть два варианта получения этого бойлерплейта. Первый — склонировать себе готовый репозиторий:

git clone https://gitlab.freedesktop.org/gstreamer/gst-template.git

Этот вариант включает в себя довольно много кода, который можно счесть лишним — там есть C-шное приложение, использующее GStreamer, и шаблон простенького элемента.

Второй способ — использовать специальную утилиту gst-element-maker, входящую в пакет gst-plugins-bad. Однако у меня этот способ не заработал под несколькими разными дистрибутивами GNU/Linux — ни в одном из них мейнтейнеры не включили эту утилиту в готовый пакет, поэтому мне пришлось скомпилировать эту утилиту из исходников самому.

Итак, мы собираемся создать не полноразмерный плеер, а один небольшой source element конвейера GStreamer, который будет получать видео из ссылки на страницу на Vimeo и отдавать его для дальнейшей обработки.

Существуют два разных способа того, как наш source element будет отдавать свои данные в конвейер: pull-режим и push-режим, тяни или толкай 😃
В push-режиме источник сам генерирует события о том, что у него есть новые данные. В pull-режиме эти данные из него забираются явным образом по мере необходимости соединёнными с ним элементами. Pull модель лучше подходит для file-source, так как файл мы можем в произвольный момент времени проматывать и получать из него байты из произвольного места. Push-интерфейс больше подходит для источников, которые работают в live-режиме, например, элемент, который забирает аудиосигнал с микрофона, видео с вебкамеры, или сетевой поток по протоколу RTMP — во всех этих случаях мы не можем по запросу фреймворка «промотать» источник данных.

В нашем коде используется push-режим, так как он чуть проще в программировании. Можно было реализовать и в pull-режиме, добавить проматывание на произвольное место, синхронизацию для предотвращения «захлёбывания» и прочие необходимые для этого вещи, но код стал бы сложнее для восприятия.

Для источников, работающих в push-режиме, есть целый отдельный класс GstPushSrc, наследуемый (как и все прочие классы GStreamer) от GObject. Кроме того, в его цепочке наследования есть класс GstElement, представляющий собой элементы конвейера, и GstBaseSrc, являющийся базовым для всех элементов-источников. GstPushSrc — специальный класс источника, работающий только в push-режиме; в принципе, push-источник можно написать, отнаследовавшись от GstBaseSrc, но для этого понадобится чуть больше бойлерплейта.

Перейдём к коду нашего элемента; его можно найти в данном репозитории. Рассмотрим ключевые фрагменты различных файлов.

В корне репозитория есть несколько вспомогательных файлов, в том числе meson.build. Это файл с инструкциями для системы сборки Meson, написанной на Python и функционально похожую на более распространённую систему сборки CMake. Мы могли бы ограничиться классическим Makefile, но стартовый проект из официального руководства включает в себя сборку через Meson, а нам это только сыграет на руку, так как наш плагин будет зависеть от большого количества библиотек, и зависимости от этих библиотек будет проще прописать с помощью продвинутой системы сборки навроде Meson, нежели с помощью классической утилиты make.

Файл meson.build содержит на некотором псевдо-языке (DSL, Domain Specific Language) описание того, каким образом нужно собирать наш плагин. Meson принимает на вход это описание и генерирует входные файлы для более примитивной и низкоуровневой утилиты для сборки Ninja, которая и будет непосредственно заниматься сборкой. Ninja была разработана как альтернатива классическому make с повышенной скоростью работы.

Среди библиотек, от которых зависит наш плагин, понятное дело, будет GStreamer, последней версии 1.x:

gst_dep = dependency('gstreamer-1.0', version : '>=1.0',
    required : true, fallback : ['gstreamer', 'gst_dep'])

Также плагин будет зависеть от libcurl, поскольку как именно с помощью этой библиотеки мы будем качать из сети те байтики, которые представляют собой видео с Vimeo:

curl_dep = dependency('libcurl', version : '>= 7.66.0', required : true)

Мы используем libxml2 для парсинга той HTML-странички, которую нам будет отдавать Vimeo:

libxml2_dep = dependency('libxml2', version : '>= 2.9.3', required : true)

Кроме того, мы используем библиотеку Parson для разбора JSON, но эта библиотека подключена в виде своих исходников непосредственно в нашем репозитории, в каталоге contrib (от англ. contributed), через git submodule, поэтому мы попросту включаем единственный исходный файл этой библиотеки в список исходных файлов нашего плагина:

plugin_sources = [
  'src/vimeosource.c',
  'src/config.c',
  'src/http.c',
  'contrib/parson/parson.c'
  ]

И, наконец, финальным аккордом собираем всю информацию о сборке плагина в одном месте:

gstpluginexample = library('vimeosource',
  plugin_sources,
  c_args: plugin_c_args,
  dependencies : [gst_dep, gstvideo_dep, curl_dep, libxml2_dep],
  include_directories : include_directories('contrib/parson'),
  install : true,
  install_dir : plugins_install_dir,
)

Для запуска нашего плагина в репозитории есть шелл-скрипт launch.sh. Первым делом в нём определяется директория, в которой будет находиться собранный плагин:

plugin_path=$(realpath "$(dirname "$0")")/bin

Далее используется утилита, которая входит в базовую поставку самого GStreamer и называется gst-launch. Предназначение этой утилиты состоит в том, чтобы запускать произвольные контейнеры, переданные ей в текстовом виде:

gst-launch-1.0 -v -m --gst-plugin-path="$plugin_path" 
               vimeosource location=https://vimeo.com/59785024 ! decodebin name=dmux 
               dmux. ! queue ! audioconvert ! autoaudiosink 
               dmux. ! queue ! autovideoconvert ! autovideosink

В скрипте мы указываем каталог, в котором gst-launch надлежит искать дополнительные плагины, через аргумент командной строки --gst-plugin-path. Кроме того, мы включаем чуть более болтливые логи с помощью аргументов -v и -m. Затем мы передаём конвейер, который хотим запустить.

Первый элемент в конвейере — тот самый, который мы создаём; мы назвали его vimeosource. У данного элемента есть свойство location, в которое мы и передаём ссылку на страничку с видео. Восклицательный знак здесь — это аналог символа | в шелл-скриптах, он просто указывает, что мы соединяем два элемента друг с другом с помощью их контактных гнёзд. Далее в конвейере мы используем стандартный элемент decodebin, который умеет автоматически у себя под капотом создавать правильный элемент для демультиплексирования данных, которые в него приходят. По сути, decodebin — контейнерный элемент, его задача — содержать другие элементы и автоматически разбираться с тем, какого типа элементы требуется создавать. В нашем случае decodebin, скорее всего, создаст под капотом элемент h264parse, так как видео от Vimeo приходит в качестве H.264 потока.

Итак, мы будем передавать байты, полученные по сети, в элемент decodebin, который не занимается декодированием, а просто демультиплексирует входной поток данных. Впрочем, если бы мы соединили decodebin с элементом типа autovideosink, который воспроизводит видео в GUI-окне (то есть если бы мы пропустили промежуточные шаги), такой вариант по-прежнему работал бы, так как decodebin «понял» бы, что от него ожидают уже готовое к воспроизведению несжатое видео, и создал бы внутри себя дополнительные элементы для декодирования (скорее всего, avdec_h264).

В нашем случае мы поступаем чуть хитрее: задаём имя dmux элементу decodebin, и на этом данная часть конвейера заканчивается. Затем мы обращаемся к этому элементу по имени (dmux.) и соединяем его выход сразу с двумя элементами:

  1. Первое контактное гнездо соединяется с элементом queue, который занимается буферизацией, и соединяется затем с элементом audioconvert, который опять-таки автомагически декодирует аудио (учитывая специфику Vimeo, скорее всего с использованием кодека AAC) в обычный сырой звук, готовый для воспроизведения, которым, в свою очередь, занимается элемент autoaudiosink.
  2. Второе контактное гнездо decodebin тоже сначала соединяется с queue для буферизациии данных, затем с элементом autovideoconvert, который декодирует видео и передает готовое для воспроизведения видео в autovideosink.

Такой усложнённый конвейер с двумя элементами queue в нашем случае необходим для синхронизации аудиодорожки с видео.

Далее рассмотрим самый главный файл с исходным кодом src/vimeosource.c и соответствующий ему заголовочный файл src/vimeosource.h.

В src/vimeosource.h мы наследуемся от вышеупомянутого GstPushSrc. Структура, которая будет соответствовать объекту нашего элемента vimeosource_VimeoSource; каждый раз, когда в конвейере будет создаваться данный элемент, фреймворк будет создавать данную структуру.

struct _VimeoSource
{
    GstPushSrc base_vimeosource;
    gchar* location;
    gchar* file_location;

    CURLM* curlm;
    CURL* curl;
    GstBuffer* current_buffer;
};

В этой структуре у нас снова используется вышеописанный трюк: первым полем идёт структура GstPushSrc, а за ним — поля с некоторой информацией, которая необходима для работы нашего класса. На самом деле, если от класса планируется в дальнейшем наследоваться, то так делать не рекомендуется, вместо этого принято использовать специальный механизм под названием private data. Однако у нас дальнейшего наследования не предполагается, поэтому мы просто сваливаем все нужные поля в нашу структуру.

Далее мы определяем структуру, соответствующую классу — она будет создаваться лишь в единственном экземпляре на всё приложение:

struct _VimeoSourceClass
{
    GstPushSrcClass base_vimeosource_class;
};

В файле src/vimeosource.c располагается бизнес-логика. Первая функция, определяемая в файле — _vimeosource_class_init. Эта функция будет вызываться GLib единожды для инициализации класса нашего элемента. Тут можно усмотреть параллель с так называемыми метаклассами в языке Python, которые управляют созданием других классов. Объявление функции _vimeosource_class_init происходит автоматически с помощью макроса G_DEFINE_TYPE_WITH_CODE, который вызывается чуть выше:

G_DEFINE_TYPE_WITH_CODE(
    VimeoSource, _vimeosource, GST_TYPE_PUSH_SRC,
    GST_DEBUG_CATEGORY_INIT(_vimeosource_debug_category, "vimeosource", 0,
                            "debug category for vimeosource element"));

Данный макрос генерирует для нас довольно много кода, и, среди всего прочего, объявляет ряд функций, среди которых — функция инициализации класса.

Самое важное, что мы делаем в функции инициализации класса, — это установка в структуре класса указателей на функции, которые соответствуют переопределённым в нашем классе виртуальным методам:

static void _vimeosource_class_init(VimeoSourceClass* klass)
{
    GObjectClass* gobject_class = G_OBJECT_CLASS(klass);
    GstBaseSrcClass* base_src_class = GST_BASE_SRC_CLASS(klass);

    /* Код пропущен для ясности */

    gobject_class->set_property = _vimeosource_set_property;
    gobject_class->get_property = _vimeosource_get_property;
    gobject_class->finalize = _vimeosource_finalize;
    /* ... */

Прежде всего, мы переопределяем ряд виртуальных методов самого GObject, а именно — set_property, get_property и finalize. Последний занимается финализацией экземпляров класса, то есть примерно соответствует деструкторам C++, а set_property и get_property нам нужны для поддержки кастомного свойства location — того самого, которое мы задаём нашему элементу в конвейере. Механизм свойств не специфичен для GStreamer, он входит в GLib-овскую ООП-машинерию.

Далее мы переопределяем ряд виртуальных методов, определённых в предках нашего класса — GstElement, GstBaseSrc и GstPushSrc:

    base_src_class->negotiate = GST_DEBUG_FUNCPTR(_vimeosource_negotiate);
    base_src_class->start = GST_DEBUG_FUNCPTR(_vimeosource_start);
    base_src_class->stop = GST_DEBUG_FUNCPTR(_vimeosource_stop);
    base_src_class->query = GST_DEBUG_FUNCPTR(_vimeosource_query);
    base_src_class->create = GST_DEBUG_FUNCPTR(_vimeosource_create);

Метод negotiate является частью вышеописанного механизма caps negotiation — он сообщает фреймворку о том, подходят ли элементу заданные ему типы выходных данных. Наша реализация этого метода тривиальна — мы всегда возвращаем значение TRUE:

static gboolean _vimeosource_negotiate(GstBaseSrc* src)
{
    VimeoSource* vimeosource = _VIMEOSOURCE(src);

    GST_DEBUG_OBJECT(vimeosource, "negotiate");

    return TRUE;
}

то есть соглашаемся со всеми типами, которые от нас ожидает фреймворк (множество таких типов в любом случае ограничивается video/x-h264, указанным нами выше при создании контактного гнезда с помощью макроса GST_STATIC_PAD_TEMPLATE). Без переопределения этого метода, к сожалению, элемент не заработает, так как caps negotiation для него будет завершаться неудачей.

Метод query вызывается фреймворком для запрашивания некоторой мета-информации о текущем состоянии нашего элемента. В его реализации мы сначала делаем отладочный вывод — добавляем в журнал сообщение о том, что этот метод был вызван с определённым запросом:

static gboolean _vimeosource_query(GstBaseSrc* src, GstQuery* query)
{
    VimeoSource* vimeosource = _VIMEOSOURCE(src);

    GST_DEBUG_OBJECT(vimeosource, "query %s",
                     gst_query_type_get_name(GST_QUERY_TYPE(query)));

Далее, по сути, все запросы о состоянии элемента мы перенаправляем родительским классам:

    if(!ret)
        ret = GST_BASE_SRC_CLASS(_vimeosource_parent_class)->query(src, query);

    return ret;

Макрос GST_BASE_SRC_CLASS возвращает базовый класс, мы обращаемся через -> к полю его структуры, которое будет указателем на функцию-метод, и, наконец, вызываем эту функцию с теми аргументами, которые мы получили от фреймворка.

Однако запрос типа GST_QUERY_URI мы обрабатываем самостоятельно:

    switch(GST_QUERY_TYPE(query))
    {
    case GST_QUERY_URI:
        gst_query_set_uri(query, vimeosource->location);
        ret = TRUE;
        break;

В ответ на такой запрос мы возвращаем текущий location, выставленный для нашего элемента. Здесь vimeosource — это экземпляр структуры _VimeoSource, и для получения необходимой информации мы попросту обращаемся к её полю location. Затем мы сохраняем полученную строку с помощью вызова функции gst_query_set_uri в полученный аргументом объект типа GstQuery, который является многофункциональным объектом, способным также хранить произвольную информацию, полученную в качестве ответа на запрос от фреймворка.

Мы могли бы и не переопределять метод query, но всё-таки делаем это из следующих соображений. Дело в том, что в GStreamer есть элементы, аналогичные вышерассмотренному decodebin, которые автоматически создают дочерние элементы, и подобный запрос URL (точнее, URI — Universal Resource Identifier) может быть отправлен такими элементами для определения того, какого типа дочерний элемент должен быть создан для заданного этим элементам URI. Мы не планируем пользоваться этим механизмом, но для порядка реализуем 👌

Методы start и stop отвечают за начало и конец работы нашего элемента в рамках конвейра. В start мы должны инициализировать ресурсы, необходимые для работы, а в stop — финализировать их. В нашей реализации метода start довольно много кода, но он достаточно простой, как и практически любой код на C 😊

Прежде всего, мы записываем в журнал тот факт, что метод был вызван:

static gboolean _vimeosource_start(GstBaseSrc* src)
{
    VimeoSource* vimeosource = _VIMEOSOURCE(src);

    GST_DEBUG_OBJECT(vimeosource, "start");

Затем получаем непосредственный URL видеофайла по URL страницы на Vimeo, который нам передали в свойстве location. Для этого мы используем код из предыдущего открытого урока, а именно — функцию get_file_url, объявленную в src/config.h и определённую в src/config.c. Функция работает с libcurl для загрузки страницы видео, ищет на странице JSON-объект, в котором прописан специальный config URL, затем обращается на этот URL, получает в ответ ещё порцию JSON’а, и уже из него получает ссылки на медиафайлы. После чего функция запускает цикл, который проходится по всем медиафайлам и ищет тот, у которого наибольшее разрешение, а найдя, возвращает его. В предыдущем открытом уроке мы подробнее рассматривали все эти механизмы.

Мы записываем полученный URL видеофайла в поле file_location нашей структуры VimeoSource:

    setlocale(LC_NUMERIC, "C"); // see https://git.io/Jte2C
    vimeosource->file_location = get_file_url(vimeosource->location);
    setlocale(LC_NUMERIC, "");

Здесь сразу виден большой подводный камень, про который я сам знал, но забыл и при подготовке этого кода угробил почти целый день, пытаясь понять, что же пошло не так. Суть проблемы в том, что библиотека для разбора JSON Parson разбирает числовые поля с помощью функции strtod, которая конвертирует строки в числа с плавающей запятой, но эта функция зависит от текущей локали. В русскоязычной локали разделителем десятичных разрядов является запятая, а не точка, как это принято в стандартной английской локали. Таким образом, при запуске кода с русскоязычной локалью запятая, которая идёт после поля, считается частью числа, и поэтому парсинг заканчивается неудачей. Что интересно, автору библиотеки неоднократно писали про эту проблему в багтрекер, но он эту особенность исправлять отказался, мол, локали не во власти моей библиотеки. Чтобы исправить эту проблему, на время получения URL на видеофайл через get_file_url, у которой под капотом и происходит разбор JSON через Parson, мы временно переключаем аспект LC_NUMERIC локали, отвечающий за форматирование чисел, на стандартную, так называемую C locale.

Далее мы журналируем полученный на предыдущем шаге URL видеофайла:

    GST_DEBUG_OBJECT(vimeosource, "location=%s", vimeosource->file_location);

Затем мы производим манипуляции, связанные с инициализацией libcurl:

    vimeosource->curlm = curl_multi_init();
    g_assert(vimeosource->curlm);
    if(!vimeosource->curlm)
        return GST_FLOW_ERROR;

В libcurl, среди всего прочего, есть механизм под названием multi interface, который позволяет скомбинировать несколько закачек в одну в асинхронном режиме; предполагается, что он будет использоваться вместе с каким-либо механизмом мультиплескирования ввода-вывода вроде select или poll, либо со своей родной функцией для ожидания curl_multi_poll. Мы будем использовать этот механизм по следующим соображениям. Каждый раз, когда фреймворк будет вызывать метод create у нашего элемента, он должен будет возвращать очередной буфер с накопившимися на данный момент данными, такова суть работы источника в push-режиме. Если бы мы использовали простой curl easy interface, у нас бы не получилось выстроить такую схему, т.к. при вызове curl_easy_perform управление не возвращается из этой функции до тех пор, пока файл не будет скачан до конца; нас это не удовлетворяет, т.к. нам нужно скачать файл не за раз целиком, а скармливать фреймворку маленькими кусочками.

Мы создаём обычный curl easy handle, устанавливаем для него все нужные нам параметры с помощью функции curl_easy_setopt и добавляем его в multi handle через функцию curl_multi_add_handle:

    vimeosource->curl = curl_easy_init();
    g_assert(vimeosource->curl);
    if(!vimeosource->curl)
        return GST_FLOW_ERROR;

    curl_easy_setopt(vimeosource->curl, CURLOPT_URL,
                     vimeosource->file_location);
    curl_easy_setopt(vimeosource->curl, CURLOPT_USERAGENT, useragent);
    curl_easy_setopt(vimeosource->curl, CURLOPT_WRITEDATA, src);
    curl_easy_setopt(vimeosource->curl, CURLOPT_WRITEFUNCTION, &curl_callback);

    ret = curl_multi_add_handle(vimeosource->curlm, vimeosource->curl);
    g_assert(ret == CURLM_OK);
    if(ret != CURLM_OK)
        return GST_FLOW_ERROR;

В качестве callback’а (функции обратного вызова) для easy handle мы выставляем нашу функцию curl_callback, которая по сути создаёт те буферы с данными, которые от нас ожидает фреймворк, и возвращает их. Через аргумент callback’а WRITEDATA мы передаём сам экземпляр нашего класса VimeoSource. В нём у нас есть поле current_buffer, в которое в callback’е мы будем прост класть очередной буфер, который удалось считать из сети.

Далее мы вызываем функцию curl_multi_perform, которая не блокирует управление, а возвращает его после начала работы переданного ей multi handle:

    int dummy;
    ret = curl_multi_perform(vimeosource->curlm, &dummy);
    g_assert(ret == CURLM_OK);
    if(ret != CURLM_OK)
        return GST_FLOW_ERROR;

    return TRUE;
}

Метод stop — злой брат-близнец метода start, он просто уничтожает в обратном созданию порядке все ресурсы, инициализированные в start. Мы уничтожаем созданный multi handle, если он есть:

static gboolean _vimeosource_stop(GstBaseSrc* src)
{
    VimeoSource* vimeosource = _VIMEOSOURCE(src);
    if(vimeosource->curlm)
    {
        CURLMcode ret
            = curl_multi_remove_handle(vimeosource->curlm, vimeosource->curl);
        g_assert(ret == CURLM_OK);
        if(ret != CURLM_OK)
            return FALSE;

        ret = curl_multi_cleanup(vimeosource->curlm);
        g_assert(ret == CURLM_OK);
        if(ret != CURLM_OK)
            return FALSE;

        vimeosource->curlm = NULL;
    }

так же поступаем с easy handle:

    if(vimeosource->curl)
    {
        curl_easy_cleanup(vimeosource->curl);
        vimeosource->curl = NULL;
    }

и освобождаем память, отведённую под строки, хранившие URL страницы на Vimeo и URL видеофайла:

    g_free(vimeosource->file_location);
    vimeosource->file_location = NULL;

    g_free(vimeosource->location);
    vimeosource->location = NULL;

    return TRUE;
}

Наконец, сердце нашего элемента — метод create. Вопреки названию, он не связан с созданием элемента; напротив, он вызывается фреймворком в тот момент, когда GStreamer от нас нужен очередной буфер с данными. Под названием create подразумевается, что мы создаём этот самый буфер. GStreamer передаёт нам аргументом двойной указатель на этот буфер buf, изначально указывающий на нулевой указатель, и мы по этому указателю должны положить вместо NULL новосозданный буфер, содержащий очередной фрагмент данных. Мы делаем это под конец функции — просто кладём туда то самое поле current_buffer из структуры нашего элемента и возвращаем значение GST_FLOW_OK, сигнализирующее о том, что мы удачно создали буфер:

static GstFlowReturn _vimeosource_create(GstBaseSrc* src, guint64 offset,
                                         guint size, GstBuffer** buf)
{
    VimeoSource* vimeosource = _VIMEOSOURCE(src);

    /* ... */

    *buf = vimeosource->current_buffer;
    return GST_FLOW_OK;
}

Вся соль нашей реализации в том, что мы в цикле вызываем функцию curl_multi_poll, которая похожа на обычный системный вызов poll — она постоянно опрашивает (англ. to poll) дескрипторы, которые есть под капотом у переданного ей curl multi handle до тех пор, пока на каком-то из этих дескрипторов не появятся новые данные. Как только данные появляются, мы на этой же итерации цикла вызываем функцию curl_multi_perform. Последняя функция довольно хитрая: она может вызвать наш callback с прочитанными данными, а может в ряде случаев (например, при обработке HTTP-заголовков) и не вызвать, и при этом вернуть значение CURLM_OK, сигнализирующее о том, что всё прошло успешно. Именно поэтому мы в цикле while, пока не получим из callback’а новый буфер, вызываем эти функции: сначала с помощью curl_mutli_poll ждём появления новых данных, затем через curl_mutli_perform обрабатываем эти новые данные с помощью callback’а, и, когда в результате обработки мы получаем непустой current_buffer, мы его и возвращаем:

    vimeosource->current_buffer = NULL;

    while(!vimeosource->current_buffer)
    {
        gint numfds = 0;
        ret = curl_multi_poll(vimeosource->curlm, NULL, 0, 0, &numfds);
        g_assert(ret == CURLM_OK);
        if(ret != CURLM_OK)
            return GST_FLOW_ERROR;

        gint running;
        ret = curl_multi_perform(vimeosource->curlm, &running);
        g_assert(ret == CURLM_OK);
        if(ret != CURLM_OK)
            return GST_FLOW_ERROR;
        if(!running)
            break;
    }

    *buf = vimeosource->current_buffer;
    return GST_FLOW_OK;
}

Теперь выходит на сцену вышеупомянутый механизм подсчёта ссылок из GLib. В callback’е мы выделяем кусок памяти под сами данные:

static size_t curl_callback(void* contents, size_t size, size_t nmemb,
                            void* userp)
{
    GstBaseSrc* src = userp;
    VimeoSource* vimeosource = _VIMEOSOURCE(src);
    gsize realsize = size * nmemb;

    vimeosource->current_buffer = gst_buffer_new();

    gchar* data = g_malloc(realsize);
    g_assert(data);
    if(!data)
        return 0;
    memcpy(data, contents, realsize);

затем создаём регион памяти GstMemory, являющийся тонкой обёрткой над этим куском памяти, через функцию gst_memory_new_wrapped; последним аргументом эта функция принимает указатель на функцию, которая будет вызвана для удаления этого региона памяти: мы попросту передаём в качестве такой функции g_free, которая соответствует предыдущему вызову g_malloc:

    GstMemory* memory
        = gst_memory_new_wrapped(0, data, realsize, 0, realsize, data, g_free);
    g_assert(memory);
    if(!memory)
        return 0;

Далее, в начале функции мы создаём новый буфер в current_buffer через вызов gst_buffer_new, а под конец добавляем в буфер наш регион памяти:

    vimeosource->current_buffer = gst_buffer_new();

    /* ... */

    gst_buffer_insert_memory(vimeosource->current_buffer, -1, memory);

    return realsize;
}

Возвращаясь к подсчёту ссылок, мы создали буфер, динамический объект, через функцию gst_buffer_new, но при этом мы нигде в нашем коде не вызываем функцию для его удаления. Несмотря на это, у нас не будет происходить утечка памяти (я проверял 😂). Не происходит она потому, что буфер создаётся со счётчиком ссылок, равным 1. Мы его отправляем во фреймворк через аргумент buf в методе create, фреймворк этот буфер нужным ему образом обрабатывает, и, когда буфер становится ему ненужным, уменьшает счётчик ссылок на 1, в результате чего он становится равным нулю, и внутренняя машинерия GLib автоматически удаляет этот динамический объект.

Далее, у нас есть метод finalize, в котором мы, помимо журналирования и передачи управления в метод finalize родительского класса, вызваем функцию curl_global_cleanup, которая корректным образом финализирует некое глобальное внутреннее состояние библиотеки libcurl.

Наконец, для того, чтобы запустить наш код, нужно в корневом каталоге проекта создать каталог с именем bin, затем в этом каталоге вызвать команду meson .., чтобы Meson нам сгенерировал файлы для сборки через Ninja (а именно, файл build.ninja):

$ mkdir -p bin
$ cd bin
$ meson ..
The Meson build system
Version: 0.43.0
Source dir: /home/andrew/Progs/otus-video-player
Build dir: /home/andrew/Progs/otus-video-player/bin
Build type: native build
Project name: otus-video-player
Native C compiler: cc (gcc 5.4.0)
Build machine cpu family: x86_64
Build machine cpu: x86_64
Found pkg-config: /usr/bin/pkg-config (0.29.1)
Native dependency gstreamer-1.0 found: YES 1.8.3
Native dependency libcurl found: YES 7.76.1
Native dependency libxml2 found: YES 2.9.3
Configuring config.h using configuration
Native dependency gstreamer-video-1.0 found: YES 1.8.3
Build targets in project: 1
Found ninja-1.9.0 at /usr/bin/ninja

При этом Meson сообщит нам инфорацию о собственной версии, версии компилятора и о версиях запрошенных нами через файл meson.build библиотек. Далее, для непосредственной сборки нашего плагина, мы просто без аргументов вызываем ninja.
Результат сборки — файл libvimeosource.so, это разделяемая библиотека (SO — shared object). Мы можем заглянуть ей под капот и посмотреть на функции, которые экспортируются и импортируются в неё, с помощью команды nm -D libvimeosource.so:

$ nm -D libvimeosource.so
000000000020f4d8 B __bss_start
                 U __ctype_b_loc
                 U curl_easy_cleanup
                 U curl_easy_init
                 U curl_easy_perform
                 U curl_easy_setopt
                 U curl_global_cleanup
                 U curl_global_init
                 U curl_multi_add_handle
                 U curl_multi_cleanup
                 U curl_multi_init
                 U curl_multi_perform
                 U curl_multi_poll
                 U curl_multi_remove_handle
                 w __cxa_finalize
0000000000005b7b T do_request
000000000020f4d8 D _edata
000000000020f500 B _end
                 U __errno_location
                 U fclose
                 U ferror
000000000000bcdc T _fini
                 U fopen64
                 U fputs
                 U fread
                 U free
                 U fseek
                 U ftell
                 U g_assertion_message_expr
00000000000053ab T get_config_url
00000000000056ac T get_file_url
                 U g_free
                 U g_intern_static_string
                 U g_log
                 U g_malloc
                 w __gmon_start__
                 U g_object_class_install_property
                 U g_once_init_enter
                 U g_once_init_leave
                 U g_param_spec_string
                 U g_realloc
                 U gst_base_src_get_type
                 U gst_buffer_insert_memory
                 U gst_buffer_new
                 U _gst_debug_category_new
                 U gst_debug_log
                 U _gst_debug_min
                 U _gst_debug_register_funcptr
                 U gst_element_class_add_static_pad_template
                 U gst_element_class_set_static_metadata
                 U gst_element_get_type
                 U gst_element_register
                 U gst_memory_new_wrapped
000000000020f440 D gst_plugin_desc
                 U gst_push_src_get_type
                 U gst_query_set_uri
                 U gst_query_type_get_name
                 U g_strdup
                 U g_strndup
                 U g_type_check_class_cast
                 U g_type_check_instance_cast
                 U g_type_class_adjust_private_offset
                 U g_type_class_peek_parent
                 U g_type_name
                 U g_type_register_static_simple
                 U g_value_get_string
                 U g_value_set_string
                 U htmlCreateMemoryParserCtxt
                 U htmlCtxtUseOptions
                 U htmlParseDocument
00000000000037d0 T _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
000000000000bc11 T json_array
000000000000ad9d T json_array_append_boolean
000000000000adfa T json_array_append_null
000000000000ad36 T json_array_append_number
000000000000ac6b T json_array_append_string
000000000000accb T json_array_append_string_with_len
000000000000ac25 T json_array_append_value
000000000000abb4 T json_array_clear
0000000000009ae7 T json_array_get_array
0000000000009b14 T json_array_get_boolean
0000000000009b41 T json_array_get_count
0000000000009a7f T json_array_get_number
0000000000009aba T json_array_get_object
0000000000009a25 T json_array_get_string
0000000000009a52 T json_array_get_string_len
00000000000099dd T json_array_get_value
0000000000009b61 T json_array_get_wrapping_value
000000000000a847 T json_array_remove
000000000000aaf2 T json_array_replace_boolean
000000000000ab57 T json_array_replace_null
000000000000aa83 T json_array_replace_number
000000000000a9a8 T json_array_replace_string
000000000000aa10 T json_array_replace_string_with_len
000000000000a90f T json_array_replace_value
000000000000bc87 T json_boolean
000000000000a828 T json_free_serialized_string
000000000000bc5f T json_number
000000000000bbf7 T json_object
000000000000b544 T json_object_clear
00000000000097c6 T json_object_dotget_array
00000000000097f3 T json_object_dotget_boolean
000000000000975e T json_object_dotget_number
0000000000009799 T json_object_dotget_object
0000000000009704 T json_object_dotget_string
0000000000009731 T json_object_dotget_string_len
000000000000967a T json_object_dotget_value
000000000000995f T json_object_dothas_value
000000000000998d T json_object_dothas_value_of_type
000000000000b51a T json_object_dotremove
000000000000b42e T json_object_dotset_boolean
000000000000b493 T json_object_dotset_null
000000000000b3bf T json_object_dotset_number
000000000000b2e4 T json_object_dotset_string
000000000000b34c T json_object_dotset_string_with_len
000000000000b10b T json_object_dotset_value
0000000000009620 T json_object_get_array
000000000000964d T json_object_get_boolean
0000000000009820 T json_object_get_count
0000000000009840 T json_object_get_name
00000000000095b8 T json_object_get_number
00000000000095f3 T json_object_get_object
000000000000955e T json_object_get_string
000000000000958b T json_object_get_string_len
0000000000009515 T json_object_get_value
0000000000009888 T json_object_get_value_at
00000000000098d0 T json_object_get_wrapping_value
00000000000098e1 T json_object_has_value
000000000000990f T json_object_has_value_of_type
000000000000b4f0 T json_object_remove
000000000000b06f T json_object_set_boolean
000000000000b0c1 T json_object_set_null
000000000000b013 T json_object_set_number
000000000000af5e T json_object_set_string
000000000000afb3 T json_object_set_string_with_len
000000000000ae4f T json_object_set_value
0000000000009335 T json_parse_file
000000000000938d T json_parse_file_with_comments
00000000000093e5 T json_parse_string
0000000000009449 T json_parse_string_with_comments
000000000000a3c8 T json_serialization_size
000000000000a5f8 T json_serialization_size_pretty
000000000000a434 T json_serialize_to_buffer
000000000000a664 T json_serialize_to_buffer_pretty
000000000000a4ae T json_serialize_to_file
000000000000a6de T json_serialize_to_file_pretty
000000000000a564 T json_serialize_to_string
000000000000a794 T json_serialize_to_string_pretty
000000000000bca1 T json_set_allocation_functions
000000000000bcc6 T json_set_escape_slashes
000000000000bc2b T json_string
000000000000bc45 T json_string_len
000000000000bbdd T json_type
000000000000b5da T json_validate
000000000000a077 T json_value_deep_copy
000000000000b877 T json_value_equals
0000000000009cfc T json_value_free
0000000000009bbf T json_value_get_array
0000000000009cb0 T json_value_get_boolean
0000000000009c82 T json_value_get_number
0000000000009b91 T json_value_get_object
0000000000009cdd T json_value_get_parent
0000000000009c1b T json_value_get_string
0000000000009c4e T json_value_get_string_len
0000000000009b72 T json_value_get_type
0000000000009df0 T json_value_init_array
0000000000009fdb T json_value_init_boolean
000000000000a033 T json_value_init_null
0000000000009f46 T json_value_init_number
0000000000009d71 T json_value_init_object
0000000000009e6f T json_value_init_string
0000000000009ea9 T json_value_init_string_with_len
                 w _Jv_RegisterClasses
                 U malloc
                 U memcmp
                 U memcpy
                 U memmove
                 U rewind
                 U setlocale
                 U sprintf
                 U __stack_chk_fail
                 U strchr
                 U strcmp
                 U strlen
                 U strncmp
                 U strncpy
                 U strstr
                 U strtod
000000000020f4b8 D useragent
00000000000041cd T _vimeosource_get_type
                 U xmlStrlen
                 U xmlStrstr

Мы можем увидеть внутренние функции для разбора JSON, которые мы использовали — у них префикс json_, а также импорты стандартных библиотечных функций, как то: malloc, memcmp и так далее, а также импорты функций из библиотеки libcurl и из самого GStreamer.

Так как это библиотека, запустить напрямую мы её не можем, но зато у нас есть скрипт для запуска launch.sh, который умеет её подгружать в конвейер GStreamer. Мы можем убедиться, что GStreamer подргужает именно наш код следующим образом:

$ rm libvimeosource.so
$ ../launch.sh
ПРЕДУПРЕЖДЕНИЕ: ошибочный конвейер: элемент «vimeosource» не найден

Почему элемент не найден? Да потому что я его удалил.

Скомпилируем библиотеку заново через вызов ninja и запустим ../launch.sh. В качестве примера видео для воспроизведения в скрипте прописана ссылка на мультфильм под названием Sintel, и мы можем убедиться, что он действительно воспроизводится и даже проигрывает звук 🎉

Sintel — коротенький мультфильм с душераздирающим сюжетом, созданный для рекламы опенсорсного пакета 3D-моделирования Blender, который, кстати, тоже написан на чистом C.

Подводя итог, мы написали библиотеку, которая содержит внутри себя элемент конвейра GStreamer, и мы успешно использовали этот элемент для воспроизведения видео с Vimeo.

Важная особенность состоит в том, что наша библиотека универсальна, мы можем использовать написанный нами элемент в произвольных конвейерах, в том числе теоретически можно взять произвольное приложение, использующее GStreamer, например, какой-нибудь третьесторонний плеер, и заставить его использовать наш элемент и воспроизводить видео с Vimeo. Таким образом, мы на конкретном примере наблюдаем гибкость данного фреймворка.

Уже прошли те времена, когда для воспроизведения аудио или видео на сайте нужно было подключать сторонний плеер на Flash — в Adobe решили больше не поддерживать эту технологию, а значит, мы можем вздохнуть с облегчением, потому что HTML5 позволяет создавать плееры с помощью тегов <audio> и <video>.

Чтобы создать плеер, достаточно такого кода для аудио:

<audio controls>
<source src="music.mp3" type="audio/mpeg">
<source src="music.ogg" type="audio/ogg">
</audio>

И такого — для видео:

<video src="video.mp4" poster="poster.jpg" controls></video>

Атрибут controls используется для того, чтобы отобразить элементы управления. Если его не указать, никакого интерфейса не будет: аудиоплеер не будет отображаться, а в видеоплеере просто будет показан кадр из видео или постер.

Обратите внимание, как указан источник. Для видео достаточно прописать атрибут src, а для аудио нужно прописывать дополнительные элементы source, чтобы указать пути и формат файлов. В коде выше добавлено два одинаковых файла, но в разных расширениях — это нужно для того, чтобы прослушать запись можно было на всех устройствах и во всех браузерах, потому что не все из них поддерживают mp3 или какой-то другой формат.

Также внутри плеера можно прописать какой-нибудь текст — его увидят те, у кого не поддерживается HTML5. Правилом хорошего тона считается указание ссылки на скачивание современного браузера.

Вот список атрибутов, которые можно указать для плеера:

  • controls — панель управления;
  • autoplay — автовоспроизведение;
  • loop — цикличность;
  • muted — выключение звука;
  • poster — обложка видео. Если не указать, будет выбран случайный кадр;
  • preload — предварительная загрузка. Существует 3 значения: auto (полностью), metadata (небольшую часть, чтобы определить основные метаданные) и none (без загрузки);
  • src — ссылка на файл.

Также можно указать высоту и ширину.

Существует элемент <track>, который размещается внутри плеера, — в нем указывается путь к текстовым файлам: субтитрам или метаданным. Для них прописываются следующие атрибуты:

  • default — указывает на дорожку, которая используется по умолчанию;
  • kind — тип файла, можно указать следующие значения:
  • subtitles — субтитры (стоит по умолчанию),
  • captions — субтитры для глухонемых,
  • chapters — название глав и их временные рамки,
  • descriptions — звуковое описание происходящего для слепых,
  • metadata — метаданные;
  • label — название дорожки;
  • src — путь к файлу;
  • srclang — язык дорожки.

Всего этого достаточно, чтобы вставить простой плеер на сайт, но некоторых функций у него все-таки нет:

  • можно убрать звук, но нельзя регулировать громкость;
  • нельзя менять скорость воспроизведения;
  • нельзя поставить на повтор и так далее.

Поэтому мы подключаем JS и пишем свой интерфейс.

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

<div class='video-container'>
<video src="video.mp4" poster='preview.jpg' class='video-player' id='video-player' preload='metadata'></video>
<div class='video-hud'>
<div class='video-hud__element video-hud__action video-hud__action_play' id='video-hud__action'></div>
<div class='video-hud__element video-hud__curr-time' id='video-hud__curr-time'>00:00</div>
<progress value='0' max='100' class='video-hud__element video-hud__progress-bar' id='video-hud__progress-bar'></progress>
<div class='video-hud__element video-hud__duration' id='video-hud__duration'>00:00</div>
<div class='video-hud__element video-hud__mute video-hud__mute_false' id='video-hud__mute'></div>
<input type='range' value='100' max='100' title='Громкость' class='video-hud__element video-hud__volume' id='video-hud__volume'>
<select title='Скорость' class='video-hud__element video-hud__speed' id='video-hud__speed'>
<option value='25'>x0.25</option>
<option value='50'>x0.50</option>
<option value='75'>x0.75</option>
<option value='100' selected>x1.00</option>
<option value='125'>x1.25</option>
<option value='150'>x1.50</option>
<option value='175'>x1.75</option>
<option value='200'>x2.00</option>
</select>
<a class='video-hud__element video-hud__download' title='Скачать' href='video.mp4' download></a>
</div>
</div>

И задать ему стили:

.video-container {
background:#000;
width:80%;
color:#fff;
}
.video-player {
width:100%;
margin:0;
}
.video-hud {
margin:0;
padding:1px;
}
.video-hud__element {
cursor:pointer;
display:inline-block;
vertical-align:middle;
height:30px;
}
.video-hud__action {
width:30px;
}
.video-hud__action_play {
background:#ccc;
border-radius:0 100px 100px 0;
}
.video-hud__action_pause {
background:#c00;
}
.video-hud__mute {
width:30px;
border-radius:100px 100px 100px 100px;
}
.video-hud__mute_true {
background:#c00;
}
.video-hud__mute_false {
background:#ccc;
}
.video-hud__download {
background:#ccc;
width:30px;
border-radius:0 0 100px 100px;
}

Выглядит это вот так:

Нас пока не интересует красивое оформление, но в этом варианте есть всё необходимое:

  • кнопка старта и паузы;
  • текущее время (в том числе и на прогресс-баре);
  • общая длительность;
  • кнопка отключения звука;
  • шкала громкости;
  • выбор скорости;
  • кнопка скачивания.

Теперь нужно написать функции, которые будут отдавать команды плееру. Начнем с получения объектов, запуска и паузы:

//Получаем объекты
//Плеер
var videoPlayer = document.getElementById('video-player');
//Время
var progressBar = document.getElementById('video-hud__progress-bar');
var currTime = document.getElementById('video-hud__curr-time');
var durationTime = document.getElementById('video-hud__duration');
//Кнопки
var actionButton = document.getElementById('video-hud__action');
var muteButton = document.getElementById('video-hud__mute');
var volumeScale = document.getElementById('video-hud__volume');
var speedSelect = document.getElementById('video-hud__speed');
function videoAct() { //Запускаем или ставим на паузу
if(videoPlayer.paused) {
videoPlayer.play();
actionButton.setAttribute('class','video-hud__element video-hud__action video-hud__action_play');
} else {
videoPlayer.pause();
actionButton.setAttribute('class','video-hud__element video-hud__action video-hud__action_pause');
}
if(durationTime.innerHTML == '00:00') {
durationTime.innerHTML = videoTime(videoPlayer.duration); //Об этой функции чуть ниже
}
}

Сначала идет проверка, стоит ли видео на паузе — информация об этом содержится в переменной paused объекта videoPlayer (плеер). Затем используются функции play и pause, чтобы запустить и остановить видео соответственно. Для кнопки указываются классы, чтобы было понятно, в каком состоянии находится ролик. Также длительность ролика записывается в специальное поле.

Чтобы функция работала, нужно перехватывать события нажатий на кнопку и на сам ролик:

//Запуск, пауза
actionButton.addEventListener('click',videoAct);
videoPlayer.addEventListener('click',videoAct);

Теперь, когда ролик можно запустить, пора настроить прогресс-бар. Для этого понадобятся 3 функции: перевод секунд в формат «ММ: СС», отображение текущего времени и перемотка.

function videoTime(time) { //Рассчитываем время в секундах и минутах
time = Math.floor(time);
var minutes = Math.floor(time / 60);
var seconds = Math.floor(time - minutes * 60);
var minutesVal = minutes;
var secondsVal = seconds;
if(minutes < 10) {
minutesVal = '0' + minutes;
}
if(seconds < 10) {
secondsVal = '0' + seconds;
}
return minutesVal + ':' + secondsVal;
}
function videoProgress() { //Отображаем время воспроизведения
progress = (Math.floor(videoPlayer.currentTime) / (Math.floor(videoPlayer.duration) / 100));
progressBar.value = progress;
currTime.innerHTML = videoTime(videoPlayer.currentTime);
}
function videoChangeTime(e) { //Перематываем
var mouseX = Math.floor(e.pageX - progressBar.offsetLeft);
var progress = mouseX / (progressBar.offsetWidth / 100);
videoPlayer.currentTime = videoPlayer.duration * (progress / 100);
}

Первая функция получает секунды и возвращает их в формате «ММ: СС». Вторая функция высчитывает текущее время в процентах от общей длительности и двигает полосу прогресса, а также показывает прогресс в виде минут и секунд. При необходимости можно изменить функцию, чтобы она показывала оставшееся время, — для этого нужно отнять текущий момент от длительности и преобразовать с помощью videoTime. А третья функция на основе положения мыши на полосе рассчитывает, куда нужно перемотать видео, и перематывает его.

Перехватываем события:

//Отображение времени
videoPlayer.addEventListener('timeupdate',videoProgress);
//Перемотка
progressBar.addEventListener('click',videoChangeTime);

На очереди — работа со звуком и скоростью:

function videoChangeVolume() { //Меняем громкость
var volume = volumeScale.value / 100;
videoPlayer.volume = volume;
if(videoPlayer.volume == 0) {
muteButton.setAttribute('class','video-hud__element video-hud__mute video-hud__mute_true');
} else {
muteButton.setAttribute('class','video-hud__element video-hud__mute video-hud__mute_false');
}
}
function videoMute() { //Убираем звук
if(videoPlayer.volume == 0) {
videoPlayer.volume = volumeScale.value / 100;
muteButton.setAttribute('class','video-hud__element video-hud__mute video-hud__mute_false');
} else {
videoPlayer.volume = 0;
muteButton.setAttribute('class','video-hud__element video-hud__mute video-hud__mute_true');
}
}
function videoChangeSpeed() { //Меняем скорость
var speed = speedSelect.value / 100;
videoPlayer.playbackRate = speed;
}

Звук хранится в переменной volume, а скорость — в playbackRate. Меняем их значения в зависимости от выбора пользователя.

//Звук
muteButton.addEventListener('click',videoMute);
volumeScale.addEventListener('change',videoChangeVolume);
//Работа со скоростью
speedSelect.addEventListener('change',videoChangeSpeed);

Это необходимый минимум для работы с плеером, но еще можно добавить полноэкранный режим, окно в окне, выбор субтитров и дополнительных дорожек — зная принцип взаимодействия с тегами <audio> и <video>, вы можете разобраться с этим, изучив необходимые события и функции в мануалах.

Кроме воспроизведения видео, плеер можно использовать и для добавления звуковых эффектов в интерфейсы:

  • звук щелчка при нажатии на кнопку;
  • звук перелистывания во время свайпа;
  • звук комкания бумаги при удалении записи из базы данных и так далее.

Для этого нужно создать элемент <audio> без атрибута controls, задать ему id и запускать воспроизведение при каком-нибудь событии.

var buttonA = document.getElementsByid('button');
var clickSound = document.getElementById('click-sound');
function buttonClick() {
clickSound.currTime = 0;
clickSound.play();
}
buttonA.addEventListener('click',buttonClick);

Так можно вызывать звук в любое время — главное, чтобы он был коротким, иначе могут быть проблемы, если пользователь быстро кликает по кнопкам. И еще продолжительные звуки сбивают с толку.

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

Также можно сделать удобный плеер для гифок:

<video src='file.gif' preload='none' id='gif-player' class='gif-player gif-player_pause' loop></video>

Немного стилей:

.gif-player {
cursor:pointer;
}
.gif-player_pause {
opacity:0.8;
}

И сам скрипт:

var gifPlayer = document.getElementById('gif-player');
function gifAct() {
if(gifPlayer.paused) {
gifPlayer.play();
gifPlayer.setAttribute('class','gif-player gif-player_play');
} else {
gifPlayer.pause();
gifPlayer.currentTime = 0;
gifPlayer.setAttribute('class','gif-player gif-player_pause');
}
}
gifPlayer.addEventListener('click',gifAct);

Такую гифку можно поставить на паузу, поэтому она не загружает страницу. Также пользователь может вообще не запускать и даже не загружать ее.

Встроенный плеер на HTML5 — это лучшее решение, которое доступно современному веб-разработчику. Больше не нужно мучиться с Flash и другими подключаемыми плеерами, потому что есть более удобный вариант, который еще и хорошо оптимизирован и не требует скачивания сторонних приложений.

В этой статье вы прочитаете как сделать на чистом javascript видеоплеер, или на HTML.

Также у нас на сайте есть статья о том, как сделать аудиоплеер на JavaScript и HTML, если вам это интересно, то посмотрите.

Делаем видеоплеер на JavaScript:

Для начала разберём как будем делать видеоплеер на JS и что мы реализуем, с начала мы возьмём элемент <video> и будем брать данные о видео, менять их и выводить при необходимости.

А реализуем мы кнопки перемотки и пауза/плай, также видео дорожку и перемотку на нужную часть видео под средством нажатия на неё.

HTML:

Для начала конечно нужно в HTML объявить элемент <video>, с атрибутом controls, он нужен что бы уже какие то кнопки были для управления видео.

<video id=«video» src=«./video/video.mp4» controls></video>

Вот результат.

видеоплеер для сайта html

Как видите в браузере уже, какой то плеер, но опять же это только базовый, поэтому мы уберём атрибут controls, и у нас будет просто видео.

Дальше уже буду показывать как сделать плеер с нуля, что бы вы поняли как можно работать с видео в JavaScript, поэтому сейчас изменю HTML.

<div id=«videoPlayer»>

    <video id=«video» src=«./video/video.mp4»></video>

    <div id=«controls»>

        <div class=«video-track»>

            <div class=«timeline»></div>

        </div>

        <div class=«buttons»>

            <button class=«play»>Play</button>

            <button class=«pause»>Pause</button>

            <button class=«rewind»><rewind</button>

            <button class=«forward»>forward></button>

        </div>

    </div>

</div>

Тут в целом всё понятно, единственное скажу, что элемент с классом video-track, это видео дорожка.

Если вам тут что то не понятно или плохо знаете HTML, то посмотрите наш учебник по HTML.

CSS:

Теперь перейдём к CSS, не много изменим вид, вот вёрстка.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

#videoPlayer {

    width: 800px;

}

#video {

    width: 100%;

}

.video-track {

    height: 5px;

    width: 100%;

    background-color: #b6b6b6;

}

.timeline {

    height: 5px;

    width: 0;

    background-color: #58b4ff

}

.buttons {

    padding: 5px 0;

}

Вот результат.

В целом тут всё просто, но если не понятно, то посмотрите наш учебник по CSS.

JavaScript:

Теперь перейдём к самому главному, это к созданию кода на JavaScript, и логики, сначала посмотрим какие элементы нужно подключить.

let video = document.getElementById(«video»);            // Получаем элемент video

let videoTrack = document.querySelector(«.video-track»); // Получаем элемент Видеодорожки

let time = document.querySelector(«.timeline»);          // Получаем элемент времени видео

let btnPlay = document.querySelector(«.play»);           // Получаем кнопку проигрывания

let btnPause = document.querySelector(«.pause»);         // Получаем кнопку паузы

let btnRewind = document.querySelector(«.rewind»);       // Получаем кнопки перемотки назад

let btnForward = document.querySelector(«.forward»);     // Получаем кнопку перемотки вперёд

Тут мы берём элемент <video> по id остальное берём по селектору.

Дальше будем делать событие кнопок, сначала посмотрим работу с запуском видео.

btnPlay.addEventListener(«click», function() {

    video.play(); // Запуск проигрывания

    // Запуск интервала

    videoPlay = setInterval(function() {

        // Создаём переменную времени видел

        let videoTime = Math.round(video.currentTime)

        // Создаём переменную всего времени видео

        let videoLength = Math.round(video.duration)

        // Вычисляем длину дорожки

        time.style.width = (videoTime * 100) / videoLength + ‘%’;

    }, 10)

});

Давайте разберём этот код, в начале через video.play() запускаем проигрывание видео, а потом запускаем интервал, он нам нужен для отображение сколько прошло времени от начала видео, на видео дорожке.

Внутри него создаём две переменных, videoTime отвечает за то, сколько уже секунд проигрывается видео, videoLength содержит сколько всего секунд длится видео, потом по формуле определяем сколько процентов видео прошло, и отображаем это на видео дорожке с помощью элемента с классом timeline, назначив ему этот процент в качестве ширины.

Теперь посмотрите на код нажатия на кнопку паузы.

btnPause.addEventListener(«click», function() {

    video.pause(); // Останавливает воспроизведение

    clearInterval(videoPlay) // убирает работу интервала

});

Как видите с помощью video.pause(), останавливаем воспроизведение и потом удаляем работу интервала.

Дальше идёт код для перемотки видио, но он очень простой.

// Нажимаем на кнопку перемотать назад

btnRewind.addEventListener(«click», function() {

    video.currentTime -= 5; // Уменьшаем время на пять секунд

});

// Нажимаем на кнопку перемотать вперёд

btnForward.addEventListener(«click», function() {

    video.currentTime += 5; // Увеличиваем время на пять секунд

});

Тут всё так просто, что даже не буду говорить что тут написано, так как всё из комментариев должно быть понятно.

Последнее что осталось сделать, так это что бы при нажатие на видео дорожку автоматически переключалось время.

videoTrack.addEventListener(«click», function(e) {

    let posX = e.clientX 8; // Вычисляем позицию нажатия

    let timePos = (posX * 100) / 800; // Вычисляем процент перемотки

    time.style.width = timePos + ‘%’; // Присваиваем процент перемотки

    video.currentTime = (timePos * Math.round(video.duration)) / 100 // Перематываем

});

На мой взгляд эта самая интересная часть программы, сначала мы присваиваем переменной posX, позицию клика по «X», и вычитаем из него восемь, это нужно, так как левый отступ у нас равен восьми, а вычисляет e.clientX от размеров всего экрана.

Дальше определяем по формуле, на какой процент записи мы хотим переключится, и присваиваем это значение ширине линии таймлайна, потом точно также вычисляем сколько на какую секунду видео надо переключится, вот результат.

видеоплеер на js

Также ниже можете скачать файлы этого плеера, что бы проверить как он работает.

Вывод:

В этой статье вы прочитали как сделать на javascript видеоплеер, и на HTML тоже, думаю вам понравилось.

Но если вы заметили, там есть много не доработок, например, видео не делается на полный экран или не показывается сколько времени осталось до окончания.

Если вы хотите это исправить, то скачайте файлы и используя сайты по этим ссылкам (сайт 1, сайт 2), они вам помогут.

Подписываетесь на соц-сети:

Оценка:

Загрузка…

Также рекомендую:

Сегодня мы заглянем за кулисы HTML5, и вы узнаете, как сделать проигрыватель видео  с нуля. Цель этого урока заключается в том, чтобы объяснить код таким образом, чтобы любой мог создать свой собственный видеоплеер:

  • Создание документа
  • Вставка видео на веб-страницу
  • Позиционирование видеоплеера с помощью CSS
  • Стили видеоплеера
  • Создание функциональности с помощью JavaScript

Перед началом проекта нужно подготовить исходные материалы. Вам понадобятся:

  • Пустой html документ;
  • Два видеоролика (вы можете загрузить примеры видеороликов с бесплатных онлайн-источников, таких как PixaBay.com или Videezy.com). Убедитесь, что они оба формата .mp4;
  • Обложка (изображение для презентации видео). Для этого можно скачать соответствующее изображение с Pexels.com или FreeImages.com;
  • Иконки для управления плеером (можно воспользоваться такими сайтами, как FlatIcon.com или IconArchive.com).

Результат должен выглядеть примерно так:

Создание документа

В своем уроке я буду использовать:

  • Изображение Белка;
  • Архив контурных медиа кнопок;
  • Шрифт Awesome стилизованный под видеоплеер;
  • Бесплатный редактор кода Brackets, меня привлекла в нем удобная кнопка «Live Preview» (Предварительный просмотр), расположенная в правом верхнем углу, которая показывает результат работы на веб-странице после того, как вы сохранили отредактированный html-файл.

Теперь, когда мы выбрали и собрали все необходимые материалы, можно приступить к работе над кодом плеера для сайта HTML5.

Вставка видео на веб-страницу

Это базовый скелет HTML5 будущего видеоплеера. Он использует 10 основных строк кода, которые позволят видео отображаться на любой веб-странице с основными кнопками управления.

Начнем с разметки HTML, в ней используется универсальное объявление doctype <! DOCTYPE html>. Это первое, с чего начинается любой HTML-документ. Оно нужно для того, чтобы браузер был в курсе, какой документ вы используете.

Теперь перейдем к элементам, которые нужно включить в HTML: <head> и <body>. Сейчас мы должны сосредоточиться на том, что происходит в body. Вы не сможете создать видео без тега <video>. Внутри <head> вставляем <video>.

Теперь в теге <video> нужно указать, какие размеры должен иметь плеер (рекомендуется установить размеры плеера, чтобы избежать мерцания). Источник видео, которое вы хотите воспроизвести в плеере, и изображение обложки. Это будет презентацией видео, которое зрители увидят, прежде чем нажмут кнопку «Play».

Теперь рассмотрим доступные атрибуты и посмотрим, как они работают.

Атрибут poster — он нужен для создания изображения-презентации вашего видео. В нем необходимо указать папку с изображением (в данном случае «Images») и название файла. Затем нужно выбрать ширину и высоту плеера. Я решил выбрать симметричную форму.

Чтобы собрать плеер для сайта, важно вставить атрибут «controls». Без него вы можете управлять своим видео только правой кнопкой мыши, а затем выбрать «Воспроизвести» или другие основные функции. Тег <controls> отображает основной массив элементов управления: кнопки «Воспроизвести», «Пауза», «Громкость» и кнопку полноэкранного режима для более удобного использования функций.

Далее идет тег <source>, в котором необходимо указать атрибут src с источником видео. Поскольку вы уже создали папку для видеоплеера, источник видео будет легко распознаваться кодом, достаточно просто указать имя конкретного видеофайла.

Поскольку тег <video> поддерживает три формата видео (MP4, WebM и Ogg) необходимо указать в атрибуте type, какой из них используется. Для удобства пользователей рекомендуется использовать как можно больше версий видео. Поэтому, если у вас есть .ogg-версия видео, нужно открыть еще один тег <source>. Например: <source src = «videoexample.ogg» type = video / ogg>.

Теперь, если вы нажмете кнопку «Video Preview» (Предварительный просмотр видео), то увидите базовый видеоплеер с обложкой, кнопками управления и видео, которое корректно воспроизводится в пределах выбранного размера.

Создаваемый плеер для сайта будет находиться в <div>, который в свою очередь будет содержать два других <div>:

Позиционирование видеоплеера с помощью CSS

Затем мы собираемся построить площадку для CSS-кода. Для этого я создал три идентификатора внутри большого тега div с именем video-player, поскольку — это цель нашего проекта.

Первый div-контейнер отвечает за скелет видео. Сюда нужно перенести первоначальные строки тега <video>, который мы создали на втором этапе данного руководства. Второй div-контейнер содержит индикатор просмотра, а третий — кнопки видеоплеера. Помните, что каждый тег <div> должен иметь уникальный идентификатор:

Позиционирование видеоплеера с помощью CSS - 2

Далее я задаю каждому <div> необходимые атрибуты. Таким образом, у div video-tree есть video теги.

<Div> progress-tree отвечает за индикатор выполнения, поэтому имеет идентификатор «progress».

<Div> button-tree требует больше вашего внимания. Я вставил три кнопки: play (воспроизвести), back (назад) и next (вперед). Таким образом, каждая кнопка заключена в свой собственный тег <div>, имеет собственный идентификатор («play-button», «backward-button» и «forward-button») и размеры (100 на 100 пикселей для каждой кнопки).

У кнопки воспроизведения есть своя временная шкала, которую я вставил в <div> с идентификатором «time-factor». Не забудьте также использовать ограничения времени «0: 00/0: 00», которые представляют собой время начала и момент времени, которого достигло видео.

После всего этого ваш «Live Preview» (Предварительный просмотр) должен выглядеть так:

Как видите, кнопки плеера с плейлистом для сайта находятся в неправильном порядке, но мы исправим это с помощью CSS.

Сохраните файл html и откройте новый файл с именем «video-player.css». Не забудьте сохранить файл css в той же папке, где html.

Теперь вернитесь в файл html и добавьте в тег <head> атрибуты, которые свяжут файл html с css-файлом: <link rel = «stylesheet» type = «text / CSS» href = «video -player.css»>.

Независимо от структуры, которую вы хотите использовать в файле css, просто указываете элемент с id, который отметили в html-файле, указав в начале #. Так вы сообщите редактору кода, какую часть необходимо стилизовать первой:

Стили видеоплеера

Выше показан скриншот файла css. Я показал основной CSS, но с помощью этого языка можно сконструировать видеоплеер более сложными способами. Когда понятны основы, можно исследовать более сложные стили самостоятельно.

Я последовательно настроил все элементы создаваемого плеера в файле css.

Для цветовой палитры видеоплеера я выбрал различные оттенки синего, чтобы различать его основные элементы.

У видеоплеера синий фон, он ограничен размерами дисплея плеера, так как функция display имеет значение inline-block. Поэтому веб-страница не станет полностью синей, так как синий фон будет ограничен размерами видеоплеера.

Следующий элемент проектирования — это video-tree, для которого я выбрал нужные размеры, и указал, чтобы видео выводилось на весь экран.

Для progress-tree я выбрал только цвет, и больше сосредоточился на ветке «progress», определяющей индикатор просмотра. Вам нужно выбрать для прогресс-бара цвет отличный от progress-tree, чтобы пользователи могли видеть, сколько осталось видео.

Для button-tree я создал две разные записи. Первая запись фокусируется только на ширине кнопок. Вторая запись управляет кнопками при горизонтальной перестройке с помощью команды «display: inline-block» и центрируется атрибутом «vertical-align: middle».

Этот CSS позволяет настроить плеер для сайта на ваше усмотрение.

На этом этапе вы должны снова сохранить проект, создать новый файл и назвать его «video-player.js». Сохраните файл в той папке, которую используете для этого проекта.

Затем нужно связать файл JavaScript с исходным файлом HTML5 строкой между тегом <link> и закрывающим тегом <head>. Например: <script type = «text / javascript» src = «video-player.js»> </ script>:

Создание функциональности с помощью JavaScript

В приведенных выше строках JavaScript-кода я сосредоточился только на кнопке воспроизведения.

Сначала мы вводим идентификатор элемента, с которым хотим работать в первую очередь. В нашем случае это идентификатор «play-button». Затем необходимо прописать форму кнопке через GetElementbyID.

Далее, когда зритель нажимает на кнопку воспроизведения, мы обрабатываем «Click» с помощью метода addEventListener. Функция «playOrPause» заставляет кнопку «Воспроизвести» работать, как обычную кнопку воспроизведения, а также как кнопку «Пауза».

Затем в коде создания плеера для сайта вы описываете функцию playOrPause. Если видео приостановлено, нажатие кнопки активирует воспроизведение. Если не приостановлено (блок «else»), нажатие кнопки «Воспроизвести» остановит воспроизведение.

Вы можете поделиться своим опытом и мыслями относительно создания видеопроигрывателя в комментариях!

package com.example.videoapp_demo;

import android.content.DialogInterface;

import android.media.MediaPlayer;

import android.net.Uri;

import android.support.v7.app.AlertDialog;

import android.support.v7.app.AppCompatActivity;

import android.os.Bundle;

import android.widget.MediaController;

import android.widget.VideoView;

import java.util.ArrayList;

public class MainActivity

    extends AppCompatActivity

    implements MediaPlayer.OnCompletionListener {

    VideoView vw;

    ArrayList<Integer> videolist = new ArrayList<>();

    int currvideo = 0;

    @Override

    protected void onCreate(Bundle savedInstanceState)

    {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        vw = (VideoView)findViewById(R.id.vidvw);

        vw.setMediaController(new MediaController(this));

        vw.setOnCompletionListener(this);

        videolist.add(R.raw.middle);

        videolist.add(R.raw.faded);

        videolist.add(R.raw.aeroplane);

        setVideo(videolist.get(0));

    }

    public void setVideo(int id)

    {

        String uriPath

              + getPackageName() + "/" + id;

        Uri uri = Uri.parse(uriPath);

        vw.setVideoURI(uri);

        vw.start();

    }

    public void onCompletion(MediaPlayer mediapalyer)

    {

        AlertDialog.Builder obj = new AlertDialog.Builder(this);

        obj.setTitle("Playback Finished!");

        obj.setIcon(R.mipmap.ic_launcher);

        MyListener m = new MyListener();

        obj.setPositiveButton("Replay", m);

        obj.setNegativeButton("Next", m);

        obj.setMessage("Want to replay or play next video?");

        obj.show();

    }

    class MyListener implements DialogInterface.OnClickListener {

        public void onClick(DialogInterface dialog, int which)

        {

            if (which == -1) {

                vw.seekTo(0);

                vw.start();

            }

            else {

                ++currvideo;

                if (currvideo == videolist.size())

                    currvideo = 0;

                setVideo(videolist.get(currvideo));

            }

        }

    }

}

package com.example.videoapp_demo;

import android.content.DialogInterface;

import android.media.MediaPlayer;

import android.net.Uri;

import android.support.v7.app.AlertDialog;

import android.support.v7.app.AppCompatActivity;

import android.os.Bundle;

import android.widget.MediaController;

import android.widget.VideoView;

import java.util.ArrayList;

public class MainActivity

    extends AppCompatActivity

    implements MediaPlayer.OnCompletionListener {

    VideoView vw;

    ArrayList<Integer> videolist = new ArrayList<>();

    int currvideo = 0;

    @Override

    protected void onCreate(Bundle savedInstanceState)

    {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        vw = (VideoView)findViewById(R.id.vidvw);

        vw.setMediaController(new MediaController(this));

        vw.setOnCompletionListener(this);

        videolist.add(R.raw.middle);

        videolist.add(R.raw.faded);

        videolist.add(R.raw.aeroplane);

        setVideo(videolist.get(0));

    }

    public void setVideo(int id)

    {

        String uriPath

              + getPackageName() + "/" + id;

        Uri uri = Uri.parse(uriPath);

        vw.setVideoURI(uri);

        vw.start();

    }

    public void onCompletion(MediaPlayer mediapalyer)

    {

        AlertDialog.Builder obj = new AlertDialog.Builder(this);

        obj.setTitle("Playback Finished!");

        obj.setIcon(R.mipmap.ic_launcher);

        MyListener m = new MyListener();

        obj.setPositiveButton("Replay", m);

        obj.setNegativeButton("Next", m);

        obj.setMessage("Want to replay or play next video?");

        obj.show();

    }

    class MyListener implements DialogInterface.OnClickListener {

        public void onClick(DialogInterface dialog, int which)

        {

            if (which == -1) {

                vw.seekTo(0);

                vw.start();

            }

            else {

                ++currvideo;

                if (currvideo == videolist.size())

                    currvideo = 0;

                setVideo(videolist.get(currvideo));

            }

        }

    }

}

Понравилась статья? Поделить с друзьями:
  • Как написать свой веб сервер
  • Как написать свой брут
  • Как написать свой браузер на python
  • Как написать свой браузер на java
  • Как написать свой блокнот