LXF125:GStreamer

Материал из Linuxformat.

Перейти к: навигация, поиск
GStreamer Надежный каркас для ваших мультимедиа-приложений

Содержание

GStreamer: Ваш видеоплейер

Пусть Linux и не испытывает недостатка в таких настольных приложениях, как проигрыватели мультимедиа – Дмитрий Мусаев все равно покажет вам,

как написать еще один, свой собственный.

Не возникало ли у вас когда-нибудь желания написать свой собственный медиа-плейер? Дело не только в том, чтобы почить на лаврах Totem и Kaffeine – это еще и прекрасный повод познакомиться с мультимедиа-каркасом GStreamer (http://gstreamer.freedesktop.org). Он написан на С и имеет интерфейсы для многих других языков программирования, таких как С++, Python и C#. На данный момент для него существует более 150 модулей расширения (plugin), позволяющих декодировать практически все аудио- и видеоформаты (полный список доступен по адресу http://gstreamer.freedesktop.org/documentation/plugins.html). С помощью этих модулей можно не только просматривать или прослушивать аудио- и видеофайлы, но и перекодировать их; например, вы сможете легко написать скрипт для конвертации фильма в формат, понимаемый вашим сотовым телефоном, если последний вообще умеет воспроизводить видео. Можно получать и отправлять медиа-потоки через сеть – в списке модулей вы найдете реализацию нескольких протоколов. Например, чтобы получить видео с web-камеры, достаточно набрать в терминале команду gst-launch v4l2src! xvimagesink.

Впечатляет? Тогда давайте перейдем от слов к делу.

Немного теории

Рис. 1. Рис. 1. Архитектура каркаса GStreamer: как видно, он имеет три уровня.

Общая архитектура каркаса GStreamer представлена на рис. 1. Как можно видеть, он состоит из базового ядра, утилит и подключаемых модулей. Давайте введем некоторые понятия.

Элемент [element] – наиболее важный компонент в GStreamer. Мы будем создавать цепочки связанных между собой элементов и направлять через них поток данных. Элементы соединяются коннекторами [pads]; это не вполне точный перевод, но мне кажется, он удобнее, чем «пады» или «подушки». Коннекторы бывают входными [sink pad] и выходными [source pad]. Элемент может иметь различное количество коннекторов: одни присутствуют всегда, другие создаются в зависимости от типа медиа-данных, которые проходят через элемент. Посмотреть, какие именно коннекторы доступны у данного элемента, можно при помощи утилиты gst-inspect. Выполните команду gst-inspect decodebin, и вы получите много интересной информации.

Pad Templates:
 SRC template: 'src%d'
  Availability: Sometimes
  Capabilities:
   ANY
 SINK template: 'sink'
  Availability: Always
  Capabilities:
   ANY

Мы видим, что у элемента есть постоянный входной коннектор “sink”, совместимый с любым типом медиа-данных, и иногда выходные коннекторы “src%d”, количество которых зависит от типа входных данных. Отметим также, что элемент посылает три сигнала (на них мы остановимся позже):

Element Signals:
 “new-decoded-pad” : void user_function (GstElement* object,
                          GstPad* arg0,
                          gboolean arg1,
                          gpointer user_data);
 “removed-decoded-pad” : void user_function (GstElement*object,
                            GstPad* arg0,
                            gpointer user_data);
 “unknown-type” : void user_function (GstElement* object,
                        GstPad* arg0,
                        GstCaps* arg1,
                        gpointer user_data);
   Контейнер [bin] – это объект для управления набором элемен-

Контейнер [bin] – это объект для управления набором элементов. Контейнер позволяет объединять несколько связанных элементов в один логический. Все, что справедливо для элементов, справедливо и для контейнеров. Например, с помощью контейнеров можно заранее подготовить в программе наборы элементов для кодирования или декодирования различных форматов входных данных и потом, в зависимости от типа последних, использовать тот или иной контейнер.

Конвейер [pipeline] – это специальный подтип контейнера, который позволяет управлять всеми дочерними контейнерами и элементами. Конвейер должен быть контейнером самого верхнего уровня; он присутствует во всех приложения работающих с каркасом.

Элементы GStreamer могут находиться в одном из четырех состояний:

  • GST_STATE_NULL Состояние по умолчанию. В этом состоянии элемент освобождает все ресурсы, которые он занимал.
  • GST_STATE_READY В этом состоянии элемент размещает глобальные ресурсы. Поток данных закрыт, и позиция в нем выставлена в начало.
  • GST_STATE_PAUSED В этом состоянии элемент открывает поток, данные подготавливаются для обработки. Элемент позволяет менять позицию в потоке.
  • GST_STATE_PLAYING В этом состоянии запускается обработка данных.

GStreamer является многопоточным каркасом, и чтобы упростить взаимодействие приложения и конвейера, в нем введена простая система передачи сообщений Bus (шина сообщений). Каждый конвейер автоматически создает шину сообщений – приложению остается только назначить обработчики сигналов и реагировать на интересующие его события. Обработчики могут быть синхронными и асинхронными; мы будем использовать оба типа. Асинхронный обработчик сигналов вызывается в контексте главного цикла Gtk-приложения.

Рис. 2. Рис. 2. Схема разрабатываемого нами медиа-плейера.

GStreamer предоставляет два высокоуровневых контейнера – это Playbin и Decodebin. Они обеспечивают всю рутинную работу по определению типа медиа-данных и их декодированию. Playbin – готовый медиа-плейер, которому нужно указать лишь источник данных и дескриптор окна (что, впрочем, не обязательно – он может создать и свое собственное), куда будет выводиться видео. Но мы будем использовать Decodebin, так как он поддается более тонкой настройке.

Исходя из вышесказанного, давайте составим схему нашего будущего медиа-плейера. Графически она представлена на рис. 2, а в текстовом виде может выглядеть так:

gst-launch filesrc location=bubble_dancer.wmv !decodebin
name=decoder decoder. !queue !videoscale !xvimagesink
decoder. ! queue !audioconvert !alsasink

Gst-launch – одна из ключевых вспомогательных утилит каркаса. Она позволяет размещать элементы на конвейере, восклицательный знак служит их разделителем: SRCELEMENT.PAD1!SINKELEMENT.PAD1. Если коннекторы не указаны, то перебираются все коннекторы и соединяются подходящие. Свойство name используется для задания имени элемента, что дает нам возможность обратиться к нему. При использовании имени элемента точка в конце обязательна – таков синтаксис команды.

Время кодировать

Перейдем к написанию кода. Я буду использовать C++ и среду разработки Anjuta; при желании, вы можете обойтись простым текстовым редактором. Итак, запускаем Anjuta и создаем новый проект; на вкладке C++ выбираем GTKmm.

Введите имя проекта, например, Playermm, и выберите его местоположение на жестком диске. Готово: мы имеем функцию main() и Glade-форму нашего будущего приложения. Нажимаем F7 и соглашаемся с тем, что нам надо создать проект; после завершения компиляции нажимаем F3. После запуска приложения вы должны увидеть пустое окно с заголовком "Hello world!". Теперь внесем небольшие изменения в main.cc (выделены жирным шрифтом)

Рис. 3. Рис. 3. Для написания медиа-плейера подойдет Anjuta или любая другая среда, ориентированная на GNOME.

int main (int argc, char *argv[]) {
 Gtk::Main kit(argc, argv);
 //Загру зить Glade-файл и соз дать его вид жеты:
 Glib::RefPtr<Gnome::Glade::Xml> refXml;
 try{
   refXml = Gnome::Glade::Xml::create(GLADE_FILE);
 } catch (const Gnome::Glade::XmlError& ex) {
   std::cerr << ex.what() << std::endl;
   return 1;
  }
  PlayerWindow* main_win = 0;
  refXml->get_widget_derived(“main_window”, main_win);
  if (main_win){
    kit.run(*main_win);
  }
  delete main_win;
  return 0;
}


Откройте файл playermm.glade двойным щелчком мыши и разместите на форме вертикальный контейнер, состоящий из трех элементов. В самый верхний добавьте меню, в следующий элемент добавьте еще один вертикальный контейнер, состоящий из четырех элементов, и назовите его ‘playerbox’; в самый нижний элемент поместите строку состояния. В первый элемент контейнера playerbox поместите Gtk::DrawingArea – в эту область будет выводиться наше видео. Во второй элемент добавьте метку – Gtk::Label, далее разместите горизонтальную шкалу Gtk::HScale. В самом последнем элементе располагается горизонтальная группа кнопок Gtk::HButtonBox: добавьте туда шесть штук и назовите их btn_play, btn_pause, btn_stop, btn_rewind, btn_forward и btn_open. В результате должен получиться интерфейс, показанный на рис. 4.

Рис. 4.Рис. 4. Рис. 4. Иерархия виджетов и внешний вид окна плейера.

Создадим новый класс (Файл > Новый > Класс С++), введем название – PlayerWindow, базовый класс – Gtk::Window. Далее, добавим в заголовочный файл объявления всех виджетов, что мы будем использовать согласно рис. 4 (подробности ищите на прилагаемом DVD).

Теперь, глядя на рис. 2, создадим для каждого его элемента объявление:

Glib::RefPtr<Gst::DecodeBin> m_decode_bin;
Glib::RefPtr<Gst::VideoScale> m_scale;
Glib::RefPtr<Gst::XvImageSink> m_video_sink;
Glib::RefPtr<Gst::AudioConvert> m_conv;
Glib::RefPtr<Gst::AlsaSink> m_audio_sink;
Glib::RefPtr<Gst::Pipeline> m_pipeline;
Glib::RefPtr<Gst::FileSrc> m_src;
Glib::RefPtr<Gst::Queue> m_queuev;
Glib::RefPtr<Gst::Queue> m_queuea;

Поясним, что означает каждая из этих переменных:

  • FileSrc Источник данных, то есть файл.
  • DecodeBin Элемент, который будет раскодировать наши медиа-данные. В зависимости от типа данных у него будут создаваться выходы для видео- и аудиопотоков, каждый из которых мы будем передавать в элемент Queue [очередь].
  • Queue Элемент, который используется в GStreamer для создания многопоточности; наши аудио и видео будут обрабатываться в разных потоках.
  • AudioConvert Конвертирует буфер «сырого» звука [‘raw audio’] между различными возможными форматами. Строго говоря, в нашем случае он не нужен и добавлен «для массовости», чтобы вы могли лучше представить себе структуру типового приложения GStreamer. Нужно помнить, что разные входные коннекторы могут принимать разные форматы, и даже одни и те же коннекторы могут принимать разные форматы на разных машинах. Поэтому лучше добавлять в цепочки для обработки данных конвертирующие элементы, такие как audioconvert и audioresample для звука или ffmpegcolorspace для видео. Об этом, по крайней мере, предупреждает документация.
  • VideoScale Изменяет размер изображения. По умолчанию используется билинейный алгоритм, что дает при масштабировании более приятную картинку. В нашем случае элемент также необязательный, поскольку протокол Xv пытается привлечь для масштабирования графическую карту.
  • AlsaSink В качестве аудиовыхода используется ALSA (Advanced Linux Sound Architecture).
  • XvImageSink Элемент транслирует видеофреймы в нечто, пригодное к выводу на локальный дисплей с использованием видеоконтроллера для преобразования цветов и размера изображения. Также может использоваться для преобразования яркости, контрастности и оттенка цветов. XImageSink для всех этих операций применяет другие элементы, например, VideoScale.
  • Pipeline Конвейер, в который помещаются все остальные элементы и который осуществляет управление ими.

Кроме того, в нашем заголовочном файле присутствуют конструктор, деструктор и обработчики сигналов.

Вы, наверное, обратили внимание на использование интеллектуального указателя с подсчетом ссылок – Glib::RefPtr< t_CppObject>. Это стандартная практика управления ресурсами glibmm и gtkmm. Суффикс «mm» в конце имени библиотеки, кстати, говорит о том, что это С++-обертка.

Свет, камера, мотор!

Заголовочный файл готов; приступим к реализации. Конструктор настраивает все виджеты, связывает виджеты с обработчиками сигналов и выставляет кнопки в начальное состояние, то есть блокирует все, кроме кнопки Открыть. Настройку GStreamer вынесем в отдельную функцию, которая будет вызываться из нашего конструктора.

В методе GstreamInit() происходит инициализация GStreamer и настройка всех нужных нам элементов:

void PlayerWindow::GstreamInit() {
  // инициа лизация GStreamer
  Gst::init();
  //соз даем конвейер
  m_pipeline = Gst::Pipeline::create(“pipeline”);
  //шина сообщений
  Glib::RefPtr<Gst::Bus> bus = m_pipeline->get_bus();
  // Разрешить син хронное извлечение сообщений
  bus->enable_sync_message_emission();
  // На значить син хронный обработ чик сообщений
  bus->signal_sync_message().connect( sigc::mem_fun(*this,
    &PlayerWindow::on_bus_message_sync));
  // На значить асин хронный обработ чик сообщений
  m_watch_id = bus->add_watch(sigc::mem_fun(*this,
    &PlayerWindow::on_bus_message) );
  //ис точник данных
  m_src = Gst::FileSrc::create(“source”);

Теперь создадим наш декодер. Выше мы рассматривали вывод команды gst-inspect decodebin и видели, что он посылает три сигнала:

  • new-decoded-pad Создан новый коннектор. В этом обработчике мы будем связывать аудио- и видеоцепочки с выходными данными декодера, исходя из типа создаваемого коннектора (см. рис. 2).
  • removed-decoded-pad Коннектор удален.
  • unknown-type Неизвестный тип медиа-данных.

Из всех этих событий нам нужно обрабатывать только создание новых коннекторов.

m_decode_bin = Gst::DecodeBin::create(“decodebin”);
m_decode_bin->signal_new_decoded_pad().
connect(sigc::mem_fun(*this,&PlayerWindow::on_new_decoded_pad));

Следующим шагом создаются оставшиеся элементы:

m_conv = Gst::AudioConvert::create();
m_audio_sink = Gst::AlsaSink::create();
m_scale = Gst::VideoScale::create();
m_video_sink = Gst::XvImageSink::create(“ximagesink”);
m_video_sink->set_property(“force-aspect-ratio”, true);
m_queuea = Gst::Queue::create();
m_queuev = Gst::Queue::create();

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

m_pipeline->add(m_src)->add(m_decode_bin)->add(m_queuea)-
>add(m_conv)-> add(m_audio_sink)->add(m_queuev)->add(m_scale)-
>add(m_video_sink);
m_src->link(m_decode_bin);
// аудиоветвь
m_queuea->link(m_conv)->link(m_audio_sink);
// видеоветвь
m_queuev->link(m_scale)->link(m_video_sink);
m_pipeline->set_state(Gst::STATE_NULL);
}

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

void PlayerWindow::on_new_decoded_pad(const
 Glib::RefPtr<Gst::Pad>& pad,
                           bool arg1) {
   if (pad->get_caps()->get_structure(0).get_name().find(“video”)
       != Glib::ustring::npos) {
     Glib::RefPtr<Gst::Pad> sinkPad = m_queuev-
 >get_static_pad(“sink”);
     // связать только один раз
     if (!sinkPad->is_linked()) {
       Gst::PadLinkReturn ret = pad->link(sinkPad);
       if (ret != Gst::PAD_LINK_OK
           && ret != Gst::PAD_LINK_WAS_LINKED) {
         std::cerr << “Невозмож но на значить видеовы ход” <<
 std::endl;
       }
     }
   }
   //... подключение аудиопотока осу щест в ляется точно так же
 }

Каждый обработчик сигналов принимает одинаковые последовательности сообщений: первым их получает синхронный обработчик, далее – асинхронный. Рассмотрим синхронный обработчик шины сообщений:

void PlayerWindow::on_bus_message_sync( const
 Glib::RefPtr<Gst::Message>& message) {
   // игнорировать все события кроме ‘prepare-xwindow-id’
   if(message->get_message_type() != Gst::MESSAGE_ELEMENT
      && !message->get_structure().has_name(“prepare-xwindow-id”))
     return;
   Glib::RefPtr<Gst::Element> element =
     Glib::RefPtr<Gst::Element>::cast_dynamic(message->get_source());
   Glib::RefPtr< Gst::ElementInterfaced<Gst::XOverlay> > xoverlay =
     Gst::Interface::cast <Gst::XOverlay>(element);
   if(xoverlay){
     const gulong xWindowId =
      GDK_WINDOW_XID(m_video_area->get_window()->gobj());
     xoverlay->set_xwindow_id(xWindowId);
   }
 }

Задача этого обработчика – получить контекст окна, в которое будет выводится видео. Наш асинхронный обработчик, on_bus_message(), обрабатывает только два сигнала: это конец потока данных и ошибка. В любом случае обработчик вызывает метод on_button_stop(), который переводит конвейер в состояние STATE_NULL.

Вот почти и все: осталось только добавить обработчики нажатия кнопок, и приложение можно запускать. Все управление воспроизведением сводится к изменению состояния контейнера посредством шести кнопок. Также в обработчиках кнопок разместим управление таймером. Он нужен нам для обновления прогресса воспроизведения и отслеживания прошедшего времени. Например, обработчик кнопки «Play» может выглядеть так:

void PlayerWindow::on_button_play() {
  //изменить состояние кнопок
  m_progress_scale->set_sensitive();
  m_play_button->set_sensitive(false);
  m_pause_button->set_sensitive();
  m_stop_button->set_sensitive();
  m_rewind_button->set_sensitive();
  m_forward_button->set_sensitive();
  m_open_button->set_sensitive(false);
  m_play_button->hide();
  m_pause_button->show();
  // вызывать функ цию on_timeout ка ж дые 200 мс
  // для регулярного обнов ления по зиции в потоке
  m_timeout_connection = Glib::signal_timeout().connect(
    sigc::mem_fun(*this, &PlayerWindow::on_timeout), 200);
  // Включить режим воспроизведения
  m_pipeline->set_state(Gst::STATE_PLAYING);
 }

Рис. 5. Рис. 5. Результат наших трудов: не слишком развитый, но вполне работоспособный видеоплейер.

Соответственно, постановка на паузу будет выглядеть так:

void PlayerWindow::on_button_pause() {
   m_play_button->set_sensitive();
   m_pause_button->set_sensitive(false);
   m_pause_button->hide();
   m_play_button->show();
   // Ос тановить таймер
   m_timeout_connection.disconnect();
   // Пау за
   m_pipeline->set_state(Gst::STATE_PAUSED);
 }

Полный текст приложения имеется на прилагающемся к журналу диске. Кроме того, пакеты GStreamer и GStreamermm содержат примеры, которые помогают понять все тонкости использования данного каркаса. Проект активно развивается, расширяется документация (http://gstreamer.freedesktop.org/documentation/), в планах есть интеграция с KDE, что упростит обработку событий; об этом вы можете подробнее прочитать на сайте проекта.

Ну и, прежде чем закончить статью, давайте посмотрим, что у нас получилось! Для запуска приложения скопируйте его в свою рабочую папку, запустите Anjuta, откройте диалог Настроить проект (Сборка > Конфигурация проекта...) отметьте галочку Пересоздать проект и нажмите Выполнить. Теперь Playermm можно запустить, нажав F3.

LXF

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