История одной фичи в Qt Multimedia

Что это? *Что это? *

Несмотря на рекомендации мейнтейнеров, этот модуль каждый раз возвращается в Qt Essentials Modules.

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

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

Модуль, который хейтили много поколейний разработчиков.

Речь пойдет про Qt Multimedia.

Qt Multimedia

Дополнительно к плееру QMediaPlayer и проигрыванию аудио QSoundEffect, киллер фичей модуля является доступ к камере QCamera.

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

Осторожно, спойлер

это не всегда получалось

Если вы столкнулись с проблемой в Qt Multimedia, то скорее всего это будет либо шоустопером, либо потребует немалых усилий, чтобы разобраться. Не каждому хочется становиться экспертом в GStreamer или, не дай бог, в DirectShow.

А все из-за чрезмерной и бессмысленной сложности в реализации, ну и чуточку потому что Qt Multimedia — это надстройка, API для платформозависимого кода, который базируется на медиа фреймворках: GStreamer, DirectShow, AVFoundation, PulseAudio, WindowsAudio и т. д.

Плагины

Все начиналось с гениальной идеи времен когда Qt был под Nokia и мультимедия была потенциальной и востребованной задачей. У каждого была нокия, и можно было и музыку проигрывать, а вдруг и видео, и может даже с камеры стрим сделать? Кто ж знал…

Поэтому можно представить такой диалог:

— Давайте напишем и релизним абстрактные интерфейсы и позволим сторонним разработчикам реализовывать их под нужные им платформы…

— Гениально, а если пользователи будут жаловаться, то предложим им самим все написать. Все равно это опенсорс, и денег с них не просят. А пока время идет, будем дописывать недостающий функционал.

Осторожно, спойлер

не дописали.

И никого не смутило, что громадное количество функционала либо не реализована вообще, либо работает не так, как ожидается.

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

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

В основе лежит QMediaServiceProviderPlugin, который возвращает QMediaService, который возвращает QMediaControl.

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

service->requestControl(QMediaPlayerControl_iid), ничего не напоминает?

Если же такой реализации интерфейса нет, то и фича не реализована, все просто.

По сути, доступ к какому-то функционалу предоставляли реализации абстрактных интерфейсов QMediaControl, таких как QMediaPlayerControl или QCameraControl. Можно было работать напрямую с ними и радоваться (нет).

Надо больше API

Но решили придумать еще один слой, чтобы работать с этими интерфейсами. Например, QMediaPlayer требует реализации QMediaPlayerControl, а QCamera — реализацию QCameraControl. ИлиQVideoProbe — потенциально очень восстребованный API, но не везде реализован и еще почему оно такое все сложное? А QSoundEffect — это вообще другая история, со своими костылями. И так далее.

А как вам идея, придумать API чтобы проигрывать плейлист — QMediaPlaylistControl? Как много сторонних пользователей реализовали этот интерфейс?

Но разработчики пошли дальше и внедрили публичный API слоем выше, который тоже требовал реализации под платформу, но этому не суждено было сбыться: например, поддержка перечислений, таких как QAudio::Role или QMediaPlayer::Flag.

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

QMediaPlayer::availability, QMediaPlayer::hasSupport, QMediaObject::isMetaDataAvailable, QCamera::isCaptureModeSupported, QCameraExposure::isFlashModeSupported, QCamera::supportedLocks и т.д.

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

Бекенды

Так появился список фич под платформу: https://wiki.qt.io/Qt_5.13_Multimedia_Backends

Представляю как пользователь хочет просто эффективно зарендерить стрим с камеры, открывает эту страницу и убеждается, что рендеринг в окно реализован только в GStreamer и то, только для виджетов… (Как много таких, кто так делал?) А потом обнаруживает, что на некоторых платформах fps иногда проседает с 22 до 10. Но почему об этом не написно? Как это пофиксить? А что такое рендеринг в окно? Хотя оно как-то работает, и на этом спасибо.

И как много функционала, который работает не так, как пользователь ожидает? Как часто мы говорили, что вы неправильно понимаете как оно работает?

И да, вы скорее всего неправильно понимаете что такое Qt Multimedia и что такое Qt вообще.

Это вам не конструктор

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

Как вы уже поняли, это не про Qt. Хотя пока неочевидно почему.

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

Но Qt — не консруктор, и не библиотека, а фреймворк и, своего рода, среда разработки с идеологией или философией, это даже не совсем С++. И не призван решать редкие задачи или корнеркейсы. Если у вас получилось решить что-то экзотическое для специфической платформы, то вам повезло, пишите блог, выступайте на конференциях, а если знаете как заставить такое работать — контрибутьте, спасибо вам, удачи вам.

Например, QString не должен и не планировался быть эффективнее и лучше, чем std::string. QList не должен заменить std::vector и вообще, использовать Qt контейнеры без QObject и QCoreApplication бессмысленно.

Qt — это решение обычных задач обычным способом без особых требований. Он по определению уступает нативным, платформозависимым решениям.

А Qt Multimedia не является и не должен быть мультимедия фреймворком, это всего лишь небольшая часть большой среды разработки. Это монолит, с плотной связностью и кучей зависимостей внутри модуля и с другими частями системы. Например, рендеринг видео до недавнего времени был сильно инкапсулирован. Считалось, что рендеринг — это детали реализации, и не стоит пользователей посвещать в это: посмотритеQMediaPlayer::setVideoOutput или попробуйте понять как ускорить рендеринг используя OpenGL.

Qt Multimedia — не для вас

Qt Multimedia призван решать до 80% возможных задач с которыми может столкнуться пользователь.

Это не инструмент для создания мультимедийных приложений, это небольшой модуль чтобы добавить немного мультимедии в приложение, смекаете разницу?

Если вам потребуется что-то более специфическое, то Qt Multimedia — не для вас.

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

И это всегда решалось тем, что если что-то не работает, то we don’t give a shit мы посмотрим насколько тяжело это починить.

Осторожно, спойлер

скорее всего тяжело

gst-pipeline:

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

Наверное, наиболее востребованной фичей была возможность создавать произвольные пайплайны (custom pipelines). Так как это специфика GStreamer, то создавать отдельный публичный, а, значит, неумираемый API, не было никакой возможности.

Многие годы в коридорах летала идея оставить только один бекенд GStreamer, некоторые робкие попытки даже были сделаны. Так как удалять систему плагинов было нельзя, то попытались дать пользователям возможность выбирать какой бекенд надо подгружать, и собирать/деплоить сразу несколько. Так в винде появилось аж три бекенда: DirectShow, WMF и GStreamer.

Так же придумали передавать пайплайны в плеер через урл с кастомной схемой:

player->setMedia(QUrl("gst-pipeline: videotestsrc ! qtvideosink");

Используя qtvideosink элемент в пайплайне можно было рендерить буфера в QML или Widget.

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

Если вам захотелось использовать нестандартный пайплайн, то Qt Multimedia вам не подходит.

Это все еще детали реализации

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

Первым делом было понятно, что любой бекенд будет возвращать видео буфера — придумали QVideoFrame.

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

На свет появился QVideoRendererControl, который принимает в себя еще одно произведение искусства — QAbstractVideoSurface.

Абстрактный QAbstractVideoSurface требуется реализовать, чтобы принимать туда видео фреймы.

Далее подсмотрев, что можно передавать хендлер окна в бекенд, то можно добиться copy-free рендеринга. Придумали QVideoWindowControl и QVideoWidgetControl. ПотомQVideoWidgetControl оказался мало востребован, плохо реализуем, и вообще дублировал функционал.

Хотя все это публичный API, это все еще детали реализации.

Бунтарство

Но важное для рендеринга открытие, хоть через плечо, но смогли придумать — QAbstractVideoSurface.QVideoRendererControl удалось реализовать под все платформы.

Даже много лет назад для QML VideoOutput кто-то добавил скрытую возможность рисовать произвольные видео фреймы, без медиа плеера. Чем не попытка расколоть монолит? Чистейшее бунтарство…

class Source : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QAbstractVideoSurface *videoSurface READ videoSurface WRITE setVideoSurface)
public:
    explicit Source(QObject *parent = 0) : QObject(parent) { }
    virtual ~Source() { }

    QAbstractVideoSurface* videoSurface() const { return m_surface; }
    void setVideoSurface(QAbstractVideoSurface *surface)
    {
        m_surface = surface;
    }

    QAbstractVideoSurface *m_surface = nullptr;
};

Source src;
auto vo = rootObject->findChild("videoOutput");
vo->setSource(&src);
src.m_surface->present(videoFrame);

А знаете как рисовать прямо в QVideoWidget?

class VideoRenderer : public QVideoRendererControl
{
public:
    QAbstractVideoSurface *surface() const override
    {
        return m_surface;
    }

    void setSurface(QAbstractVideoSurface *surface) override
    {
        m_surface = surface;
    }

    QAbstractVideoSurface *m_surface = nullptr;
};

class MediaObject;
class MediaService : public QMediaService
{
public:
    MediaService(VideoRenderer *vr, QObject* parent = nullptr)
        : QMediaService(parent)
        , m_renderer(vr)
    {
    }

    QMediaControl* requestControl(const char *name) override
    {
        if (qstrcmp(name, QVideoRendererControl_iid) == 0)
            return m_renderer;

        return nullptr;
    }

    void releaseControl(QMediaControl *) override
    {
    }

    VideoRenderer *m_renderer = nullptr;
};

class MediaObject : public QMediaObject
{
public:
    MediaObject(VideoRenderer *vr, QObject* parent = nullptr)
        : QMediaObject(parent, new MediaService(vr, parent))
    {
    }
};

class VideoWidget : public QVideoWidget
{
public:
    bool setMediaObject(QMediaObject *object) override
    {
        return QVideoWidget::setMediaObject(object);
    }
};

VideoRenderer vr;

VideoWidget w;
w.show();

MediaObject mo(&vr);
w.setMediaObject(&mo);
vr.m_surface->present(videoFrame);

И потом пришло время дать доступ к внутренним реализациям QAbstractVideoSurface: так появились QVideoWidget::videoSurface, QGraphicsVideoItem::videoSurface и QDeclarativeVideoOutput::videoSurface

Это воспринималось довольно скептически. Считалось, что есть более эффективные способы рисования фреймов, чем QVideoWidget или QML VideoOutput.

Но это дало возможность немного, но убрать зависимость плеера от рендеринга.

Все должно работать из коробки

Как результат, эту концепцию перенесли даже в Qt Multimedia 6, переименовав QAbstractVideoSurface в QVideoSink, что дает возможность использовать рендеры из Qt Multimedia, а не копировать/реализовывать их самим.

Не знаю какие аргументы звучали в пользу этого, но уверен, что решение далось без энтузиазма, (ведь все должно работать из коробки и как один большой кусок …).

Хотя надо просто знать тонкости настройки, тогда не больно (нет).

P.S. * Заглавная картинка — это пример с QRadioDataControl, и вряд ли найдется тот, кто когда-либо это использовал.

Если вы еще тут, и вдруг интересен свободный и кроссплатформенный плеер на основе FFmpeg?

© Habrahabr.ru