Мультимедиа

Низкоуровневая обработка через FFmpeg в связке с Qt

Введение

Qt Multimedia предоставляет высокоуровневый, удобный API для типичных задач воспроизведения и записи — но для более специфичных, продвинутых задач (произвольное транскодирование, статья 473; сложная, кастомная обработка видеокадров; поддержка форматов, не покрытых стандартным API) часто используется FFmpeg — мощная, широко распространённая библиотека (и одноимённая консольная утилита) для обработки мультимедиа, либо как внешний процесс, либо через прямую интеграцию её библиотек (libavcodec, libavformat) в код приложения.

Концепция

Использование FFmpeg как внешнего процесса (через QProcess) — простой, но менее интегрированный подход, где приложение запускает консольную утилиту ffmpeg с нужными параметрами командной строки для выполнения конкретной задачи (транскодирование, извлечение кадров, наложение фильтров) и обрабатывает результат после завершения процесса. Прямая интеграция библиотек FFmpeg (libavcodec для кодеков, libavformat для контейнерных форматов) в код C++/Qt-приложения даёт значительно более глубокий, программный контроль (доступ к отдельным декодированным кадрам, кастомная обработка на уровне отдельных пакетов данных), но требует существенно большей сложности кода и понимания низкоуровневого API самого FFmpeg.

Пример кода

// Использование FFmpeg как внешнего процесса для транскодирования — простой, но эффективный подход
#include <QProcess>

void transcodeVideo(const QString &inputPath, const QString &outputPath,
                      std::function<void(bool success)> onFinished)
{
    auto *process = new QProcess();
    QStringList arguments = {
        "-i", inputPath,
        "-c:v", "libx264",      // видеокодек H.264
        "-preset", "medium",     // баланс скорости кодирования и сжатия
        "-c:a", "aac",           // аудиокодек AAC
        "-y",                    // перезаписать выходной файл без запроса подтверждения
        outputPath
    };

    QObject::connect(process, &QProcess::finished,
                      [process, onFinished](int exitCode, QProcess::ExitStatus) {
        onFinished(exitCode == 0);
        process->deleteLater();
    });

    process->start("ffmpeg", arguments);
}
// Извлечение прогресса транскодирования из вывода FFmpeg (парсинг stderr, куда ffmpeg пишет прогресс)
void transcodeWithProgress(QProcess *process, std::function<void(int percent)> onProgress, qint64 totalDurationMs)
{
    QObject::connect(process, &QProcess::readyReadStandardError, [=]() {
        QString output = QString::fromUtf8(process->readAllStandardError());

        QRegularExpression timeRegex(R"(time=(d+):(d+):(d+).(d+))");
        QRegularExpressionMatch match = timeRegex.match(output);
        if (match.hasMatch()) {
            qint64 currentMs = match.captured(1).toInt() * 3600000
                              + match.captured(2).toInt() * 60000
                              + match.captured(3).toInt() * 1000;
            int percent = static_cast<int>((currentMs * 100) / totalDurationMs);
            onProgress(percent);
        }
    });
}

Пояснения к коду

transcodeVideo показывает простой, но практичный подход — формирование аргументов командной строки ffmpeg, эквивалентных тому, что выполнил бы пользователь вручную в терминале, и запуск через QProcess с обработкой завершения через сигнал finished, что не требует прямой интеграции сложных библиотек FFmpeg, но достаточно для большинства задач пакетного транскодирования. transcodeWithProgress демонстрирует менее очевидный, но практически важный приём — FFmpeg выводит прогресс выполнения в stderr (а не stdout) в текстовом, парсируемом формате, включающем текущее обработанное время (time=HH:MM:SS.ms), и регулярное выражение позволяет извлечь эту информацию для построения индикатора прогресса длительной операции транскодирования, не предоставляемого напрямую самим FFmpeg в более удобной, структурированной форме.

Подводные камни

  • Зависимость от наличия установленного во внешней системе исполняемого файла FFmpeg, не входящего в состав самого Qt и требующего отдельной установки или включения в дистрибутив приложения — приложение, использующее FFmpeg как внешний процесс, должно либо явно проверять его наличие и версию перед использованием, либо включать собственную, статически слинкованную или поставляемую вместе с приложением копию исполняемого файла, без чего попытка запуска ffmpeg на системе без его установки завершится ошибкой запуска процесса.
  • Парсинг текстового вывода FFmpeg (как в примере с прогрессом) без учёта возможных изменений точного формата этого вывода между разными версиями FFmpeg — формат текстового вывода консольной утилиты не является строго стабильным, документированным API, и обновление версии FFmpeg теоретически может изменить точный формат строк прогресса, что нарушит парсинг через жёстко заданное регулярное выражение; для критичных к стабильности случаев интеграции стоит рассмотреть более стабильные способы получения прогресса (если доступны) или быть готовым к необходимости адаптации парсинга при обновлении используемой версии FFmpeg.
  • Игнорирование лицензионных особенностей FFmpeg и отдельных входящих в его состав кодеков для коммерческого распространения приложения — сам FFmpeg распространяется под лицензиями LGPL/GPL (в зависимости от конкретной сборки и включённых компонентов), и отдельные кодеки внутри него могут иметь дополнительные патентные ограничения (аналогично подводному камню статьи 473 про H.264) — перед включением FFmpeg в коммерческий продукт стоит внимательно изучить применимые лицензионные условия конкретной используемой сборки.
  • Прямая интеграция низкоуровневых библиотек FFmpeg (libavcodec/libavformat) без достаточного понимания их сложного, низкоуровневого C-API, спроектированного без учёта удобства использования из современного C++ — этот API требует ручного управления памятью пакетов и кадров, явной обработки множества предельных случаев формата данных, и ошибки в этой интеграции (утечки памяти, некорректное освобождение ресурсов) особенно вероятны для разработчиков без специфичного опыта именно с этим конкретным, нетривиальным API; для большинства практических задач использование FFmpeg как внешнего процесса (первый пример статьи) — значительно более простой и менее подверженный ошибкам путь.