Pull to refresh

C# и обработка медиафайлов средствами FFmpeg, Pandoc и ImageMagick

Level of difficultyMedium
Reading time13 min
Views11K

Приветствую читатели, в этой статье я бы хотел рассказать о написанной мной OpenSource библиотеке MediaFileProcessor под платформу .NET (.netstandart 2.0).

dotnet add package MediaFileProcessor --version 1.0.0

Исходный код доступен на GitHub

Данная библиотека является универсальной оболочкой для исполняемых процессов в операционной системе (Windows/Linux).Библиотека позволяет файлам взаимодействовать с процессами через именованные каналы, потоки, массивы байтов и пути в директориях. Так же имеет некоторые полезные функции, такие как возможность декодирования потока на лету и получения из него набора файлов по их сигнатурам.

В данной версии(1.0) в библиотеки реализованы оболочки над такими процессами как FFmpeg, ImageMagick и Pandoc.Эту библиотеку так же можно использовать для взаимодействия с сторонними процессами.

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

В процессе работы над финтех проектом мне пришлось интегрироваться с такими сервисами Amazon как Rekognition (фото и видео распознавание), Kinesis (передача видеопотока с устройств) и Transcribe(распознавание речи).

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

Обработку медиафайлов приходилось выполнять программно получая данные из сторонних сервисов. Для этого я использовал такие процессы как FFmpeg и ImageMagick. Для тех кто не в курсе - FFmpeg и ImageMagick это OpenSource проекты для работы с видеофайлами и изображениями, использовать их инструменты приходится через их исполняемые файлы ffmpeg.exe и convert.exe. Конкретно в моем случае мне приходилось запускать эти процессы программно передавая туда аргументы.

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

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

К примеру простейшая команда ffmpeg по конвертации видеофайла из одного формата в другой с получением входного файла через входной поток и выдачу результат в выходной поток.

ffmpeg -i - -f avi -  

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

Но как быть в том случае когда нам надо передать через потоки 2 входных файла?

Пример добавления аудио файла в видеофайл

ffmpeg -i - -i - -c:v copy -c:a aac -strict experimental -map 0:v:0 -map 1:a:0 -f avi -

Или к примеру если нам надо извлечь набор кадров в формате jpg из видео в виде потока?

ffmpeg -i - -f imagepipe - 

В первом случае у нас указано 2 входных аргумента -i - -i - . Но входной поток у нас один, и мы не можем передать 2 потока с данными в один входной поток.

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

Чтобы решить эти и подобные проблемы я и написал библиотеку MediaFileProcessor.

Эта библиотека позволяет передавать данные в любом виде в процессы и обрабатывать их результат.

В первом случае, при использовании этой библиотеки потоки файлов будут переданы в входные аргументы в виде поток через именованные каналы (named pipes).

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

Ниже представления инструкция по использованию данной библиотеки и ее более подробное описание.

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

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

var fromPath = new MediaFile(@"C:\fileTest.avi", MediaFileInputType.Path);

var fromNamedPipe = new MediaFile(@"fileTestPipeName", MediaFileInputType.NamedPipe);

var namingTemplate = new MediaFile(@"C:\fileTest%003d.avi", MediaFileInputType.Template);

var fs = @"C:\fileTest.avi".ToStream();
var fromStream = new MediaFile(fs);

var bytes = @"C:\fileTest.avi".ToBytes();
var fromBytes = new MediaFile(bytes);

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

Инструкция FFmpeg

Для обработки видеофайлов средствами FFmpeg необходимо иметь его исполняемый файл ffmpeg.exe.Если вы не хотите скачивать его собственноручно то можете использовать следующий код: await VideoFileProcessor.DownloadExecutableFiles();

Данный код скачает архив по адресу https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip и распокует необходимый ffmpeg.exe в корневую директорию.

Пример обработки файла

Ниже представлен пример получения кадра из видео.

За обработку видеофайлов средствами ffmpeg отвечает класс VideoFileProcessor. Следует создать его экземпляр:

var videoFileProcessor = new VideoFileProcessor();

Создание через конструктор без параметров подразумевает что исполняемые файлы ffmpeg.exe и ffprobe.exe находятся в корневой папке.

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

var videoFileProcessor = new VideoFileProcessor("pathToFFmpeg.exe", "pathToFFprobe.exe");

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

var settings = new VideoProcessingSettings();

var mediaFile = new MediaFile(@"pathToOutputFile", MediaFileInputType.Path);

settings.ReplaceIfExist()                          //Перезаписывать выходные файлы без запроса.
        .Seek(TimeSpan.FromMilliseconds(47500))    //Кадр, с которого нужно начать поиск.
        .SetInputFiles(mediaFile)                  //Установить входные файлы
        .FramesNumber(1)                           //Количество видеокадров для вывода
        .Format(FileFormatType.JPG)                //Форсировать формат входного или выходного файла.
        .SetOutputArguments(@"pathToInputFile");   //Настройка выходных аргументов

Далее надо лишь передать конфигурацию в метод ExecuteAsync:

var result = await videoFileProcessor.ExecuteAsync(settings, new CancellationToken());

Указанные методы конфигурации выдадут нам следующие аргументы для запуска процесса ffmpeg: -y -ss 00:00:47.500 -i pathToOutputFile -frames:v 1 -f image2 pathToInputFile. Необходимо СОБЛЮДАТЬ ПОРЯДОК конфигуарций, т.к. некоторые аргументы должны быть заданы до указания входного аргумента и некоторые после.

Внимание

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

Вам следует просто создать экземпляры этого класса из данных представленных в любом виде(путь, поток, байты, каналы, шаблоны) и передать в метод ```SetInputFiles```.

Метод SetOutputArguments отвечает за установку аргумента выходного файла. Через этот метод можно установить путь выходного файла, адрес rtp сервера для трансляции и т.д.

Если этот метод не вызывать то это значит что результат обработки будет выдан в StandardOutput в виде потока. И метод ExecuteAsync вернет результат в потоке.

Если же вы установили свой выходной аргумент то StandardOutput будет пустой и ExecuteAsync вернет null.

Если вам нужно установить аргумент которого нету в методах конфигурации то вы можете задать кастомные аргументы с помощью метода CustomArguments.

Полный код:

var mediaFile = new MediaFile(@"pathToOutputFile", MediaFileInputType.Path);

var videoFileProcessor = new VideoFileProcessor();

var settings = new VideoProcessingSettings();

settings.ReplaceIfExist()                        //Overwrite output files without asking.
        .Seek(TimeSpan.FromMilliseconds(47500))  //The frame to begin seeking from.
        .SetInputFiles(mediaFile)                //Set input files
        .FramesNumber(1)                         //Number of video frames to output
        .Format(FileFormatType.JPG)              //Force input or output file format.
        .SetOutputArguments(@"pathToInputFile"); //Setting Output Arguments

var result = await videoFileProcessor.ExecuteAsync(settings, new CancellationToken());

В текущей версии библиотеки уже реализованы некоторые варианты обработки видеофайлов с помощью ffmpeg:

  • Извлечь кадр из видео

  • Обрезать видео

  • Конвертировать видео в набор изображений покадрово

  • Конвертировать изображения в видео

  • Извлечь аудиодорожку из видеофайла

  • Конвертировать в другой формат

  • Добавить Вотермарку

  • Удалить звук из видео

  • Добавить аудиофайл в видеофайл

  • Конвертировать видео в Gif анимацию

  • Сжать видео

  • Сжать изображение

  • Соединить набор видеофайлов в единый видеофайл

  • Добавить субтитры

  • Получить подробную информацию по метаданным видеофайла

Пример "Извлечь кадр из видео"

Ниже представлен пример применения извлечения кадра из видеофайла на определенном тайминге при условии что файл существует ФИЗИЧЕСКИ в директории

var videoFileProcessor = new VideoFileProcessor();
 //Test block with physical paths to input and output files
 await videoFileProcessor.GetFrameFromVideoAsync(TimeSpan.FromMilliseconds(47500),
                                                 new MediaFile(@"C:\inputFile.avi", MediaFileInputType.Path),
                                                 @"C:\resultPath.jpg",
                                                 FileFormatType.JPG);

Ниже представлен пример применения извлечения кадра из видеофайла на определенном тайминге при условии если у нас файл в видео массива байтов

//Block for testing file processing as bytes without specifying physical paths
 var bytes = await File.ReadAllBytesAsync(@"C:\inputFile.avi");
 var resultBytes = await videoProcessor.GetFrameFromVideoAsBytesAsync(TimeSpan.FromMilliseconds(47500), new MediaFile(bytes), FileFormatType.JPG);
 await using (var output = new FileStream(@"C:\resultPath.jpg", FileMode.Create))
     output.Write(resultBytes);

Ниже представлен пример применения извлечения кадра из видеофайла на определенном тайминге при условии если у нас файл в видео потока

//Block for testing file processing as streams without specifying physical paths
await using var stream = new FileStream(@"C:\inputFile.avi", FileMode.Open);
var resultStream = await videoProcessor.GetFrameFromVideoAsStreamAsync(TimeSpan.FromMilliseconds(47500), new MediaFile(stream), FileFormatType.JPG);
await using (var output = new FileStream(@"C:\resultPath.jpg", FileMode.Create))
     resultStream.WriteTo(output);

Все остальные методы работают точно также. Вы можете передать файлы в процесс в любом виде и получить в любом видео.

Инструкция ImageMagick

Для обработки изображений применяется ImageMagick его класс ImageFileProcessor и его исполняемый файл convert.exe

Для загрузки его исполняемого файла можете вызвать следующий код

await ImageFileProcessor.DownloadExecutableFiles();

Данный код скачать исполняемый файл в корневую директорию с адреса https://imagemagick.org/archive/binaries/ImageMagick-7.1.0-61-portable-Q16-x64.zip

Вся инструкция которая относилась к ffmpeg, так же относится и к ImageMagick. Обработчиком ImageMagick является класс ImageFileProcessor

var i = new ImageFileProcessor();
var j = new ImageFileProcessor("pathToConvert.exe");

В текущей версии библиотеки уже реализованы некоторые варианты обработки изображений с помощью ImageMagick:

Сжать изображение -Конвертировать изображение в другой формат -Изменить размер изображения -Преобразовать набор изображений в Gif анимацию

Пример сжатия изображения в трех вариантах (путь в директории, поток, массив байтов)

//Test block with physical paths to input and output files
await processor.CompressImageAsync(new MediaFile(_image, MediaFileInputType.Path), ImageFormat.JPG, 60, FilterType.Lanczos, "x1080", @"С:\result.jpg", ImageFormat.JPG);

//Block for testing file processing as streams without specifying physical paths
await using var stream = new FileStream(_image, FileMode.Open);
var resultStream = await processor.CompressImageAsStreamAsync(new MediaFile(stream), ImageFormat.JPG, 60, FilterType.Lanczos, "x1080", ImageFormat.JPG);
await using (var output = new FileStream(@"С:\result.jpg", FileMode.Create))
     resultStream.WriteTo(output);

//Block for testing file processing as bytes without specifying physical paths
var bytes = await File.ReadAllBytesAsync(_image);
var resultBytes = await processor.CompressImageAsBytesAsync(new MediaFile(bytes), ImageFormat.JPG, 60, FilterType.Lanczos, "x1080", ImageFormat.JPG);
await using (var output = new FileStream(@"С:\result.jpg", FileMode.Create))
    output.Write(resultBytes);

Инструкция Pandoc

Для обработки документов применяется процесс pandoc.exe, его процессор DocumentFileProcessor.

В текущей версии библиотеки уже реализованы некоторые варианты обработки документов с помощью pandoc:

-конвертирование файла .docx в .pdf

var file = new MediaFile(@"C:\inputFile.docx", MediaFileInputType.Path);
var processor = new DocumentFileProcessor();
await processor.ConvertDocxToPdf(file, "test.pdf");

Полезные функции

MultiStream

Класс MultiStream предназначен для работы с набором потоков как с единым целлым.

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

var stream = new MultiStream();
stream.AddStream(new FileStream(@"С:\inputfile1.jpg", FileMode.Open, FileAccess.Read, FileShare.Read));
stream.AddStream(new FileStream(@"С:\inputfile2.jpg", FileMode.Open, FileAccess.Read, FileShare.Read));
stream.AddStream(new FileStream(@"С:\inputfile3.jpg", FileMode.Open, FileAccess.Read, FileShare.Read));
stream.AddStream(new FileStream(@"С:\inputfile4.jpg", FileMode.Open, FileAccess.Read, FileShare.Read));
stream.AddStream(new FileStream(@"С:\inputfile5.jpg", FileMode.Open, FileAccess.Read, FileShare.Read));

Здесь мы создаем экземпляр класса MultiStream и через метод AddStream добавляем в этот потом несколько потоков с различными файлами. Теперь мы может эти потоки передать в процесс одним потоком в один входной поток

Пример использования MultiStream

var stream = new MultiStream();
var files = new List<string>();
for (var i = 1; i <= 1000; i++)
{
    files.Add($@"C:\image{i:000}.jpg");
}
foreach (var file in files)
{
    stream.AddStream(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read));
}

//Block for testing file processing as streams without specifying physical paths
stream.Seek(0, SeekOrigin.Begin);
var resultStream = await videoProcessor.ConvertImagesToVideoAsStreamAsync(new MediaFile(stream), 24, "yuv420p", FileFormatType.AVI);
await using (var output = new FileStream(@"C:\mfptest\results\ConvertImagesToVideoTest\resultStream.avi", FileMode.Create))
{
   resultStream.WriteTo(output);
}

Собираем тысячу изображений в один MultiStream и передаем в процесс У класса MultiStream есть метод ReadAsDataArray чтобы получить содержащиеся потоки в виде массивов байтов, и ReadAsStreamArray чтобы получить содержащиеся потоки в виде массива потоков.

Декодирование потока на лету

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

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

GetMultiStreamBySignature(this Stream stream, byte[] fileSignature)

Этот следует вызвать на потоке который следует декодировать и передать в этот метод в качестве аргумента - сигнатуру извлекаемых файлов. Результатом этого метода будет MultiStream содержащий в себе массив потоков файлов. 1 поток для 1 файла. И уже используя его методы ReadAsDataArray или ReadAsStreamArray мы можем получить эти файлы в виде массива байтов или потоков.

Чтобы подробнее изучить процесс декодирования я советую изучить исходный код.

Наглядный пример декодирования потока:

//Block for testing file processing as streams without specifying physical paths
await using var stream = new FileStream(@"C:\inputFile.avi", FileMode.Open);
var resultMultiStream = await videoProcessor.ConvertVideoToImagesAsStreamAsync(new MediaFile(stream), FileFormatType.JPG);
var count = 1;
var data = resultMultiStream.ReadAsDataArray();

foreach (var bytes in data)
{
   await using (var output = new FileStream(@$"C:\result{count++}.jpg", FileMode.Create))
       output.Write(bytes, 0, bytes.Length);
}

Для получения сигнатуры определенного формата файла есть метод расширения

public static byte[] GetSignature(this FileFormatType outputFormatType)

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

FileDownloadProcessor

Если вам необходимо скачать файл то можете использовать статичный метод DownloadFile класса FileDownloadProcessor. Этот метод использует для скачивания не устаревщий WebClient а HttpClient и позволяет в процентах отслеживать прогресс скачивания.

ZipFileProcessor

Для работы с zip архивами представлен класс ZipFileProcessor.

Применения для распаковки скачанного архива ffmpeg и извлечение исполняемых файлов

// Open an existing zip file for reading
            using(var zip = ZipFileProcessor.Open(fileName, FileAccess.Read))
            {
                // Read the central directory collection
                var dir = zip.ReadCentralDir();

                // Look for the desired file
                foreach (var entry in dir)
                {
                    if (Path.GetFileName(entry.FilenameInZip) == "ffmpeg.exe")
                    {
                        zip.ExtractFile(entry, $@"ffmpeg.exe"); // File found, extract it
                    }

                    if (Path.GetFileName(entry.FilenameInZip) == "ffmpeg.exe")
                    {
                        zip.ExtractFile(entry, $@"ffprobe.exe"); // File found, extract it
                    }
                }
            }

MediaFileProcess

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

При создании его экземпляра следует задать ему путь/имя исполняемого процесса, аргументы процесса, ProcessingSettings, входные потоки и наименования входных именованных каналов.

Примечание по входным потокам и именованным каналам:

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

var inputStreamFile = @"C:\inputFile.txt".ToStream();

var settings = new ProcessingSettings
{
    CreateNoWindow = true,
    UseShellExecute = false,
    EnableRaisingEvents = false,
    WindowStyle = ProcessWindowStyle.Normal,
    ProcessOnExitedHandler = null,
    IsStandartOutputRedirect = true,
    OutputDataReceivedEventHandler = null,
    ErrorDataReceivedHandler = null
};

var process = new MediaFileProcess("program.exe", "-arg1 value1 -arg2 value2 -arg3 value3", settings, new Stream[] { inputStreamFile } );

var result = await process.ExecuteAsync(new CancellationToken());

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

Спасибо за внимание.

Tags:
Hubs:
Total votes 9: ↑8 and ↓1+9
Comments10

Articles