Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 3개 있습니다.)

C# - FFmpeg.AutoGen 라이브러리를 이용한 기본 프로젝트 구성

이와 관련한 동영상 강의를 마침 2022년 5월 6일에 진행된 ".NET Conf Mini"에서 했으니 참고하세요. ^^

C# 개발자를 위한 ffmpeg 라이브러리 사용법
; https://youtu.be/ERU9tXdtHLg




원래의 네이티브 ffmpeg 라이브러리를 단순히 API만 PInvoke 수준에 가깝게 래핑해서 제공하는 라이브러리가 있습니다.

Ruslan-B/FFmpeg.AutoGen
; https://github.com/Ruslan-B/FFmpeg.AutoGen

그렇다면 당연히 C# 라이브러리와 함께 네이티브 모듈들이 있어야 정상 동작할 텐데요, 윈도우용에 한해서는 친절하게 x64 DLL들을 빌드시켜 github repo에 포함하고 있으니 그것을 사용하면 됩니다.

FFmpeg.AutoGen/FFmpeg/bin/
; https://github.com/Ruslan-B/FFmpeg.AutoGen/tree/master/FFmpeg/bin/x64

혹은, vcpkg가 있으니,

오픈 소스 라이브러리를 쉽게 빌드해 주는 "C++ Package Manager for Windows: vcpkg"
; https://www.sysnet.pe.kr/2/0/11409

다음의 명령으로 빌드해,

c:\vcpkg> vcpkg install ffmpeg:x64-windows

.\vcpkg\packages\ffmpeg_x64-windows\bin 디렉터리에 빌드 완료된 DLL들을 사용해도 됩니다.

avcodec-58.dll
avdevice-58.dll
avfilter-7.dll
avformat-58.dll
avutil-56.dll
swresample-3.dll
swscale-5.dll

이를 기반으로 FFmpeg.AutoGen을 사용하기 위해서는 "https://github.com/Ruslan-B/FFmpeg.AutoGen/tree/master/FFmpeg.AutoGen.Example" 예제 프로젝트의 소스 코드를 참고하면 됩니다.

그럼, 한번 구성해 볼까요? ^^

우선, 콘솔 프로젝트를 생성하고 FFmpeg.AutoGen 참조를 추가합니다.

// Install-Package FFmpeg.AutoGen -Version 4.4.1.1
// Install-Package FFmpeg.AutoGen -Version 5.0.0 

Install-Package FFmpeg.AutoGen

그다음, "예제 프로젝트"에서 FFmpegHelper.cs, FFmpegBinariesHelper.cs 2개의 파일을 복사해 콘솔 프로젝트에 "FFmpeg" 디렉터리를 만들어 추가한 후, Program.cs에 FFmpegBinariesHelper.cs의 메서드를 하나 호출합니다.

using FFmpeg.AutoGen.Example;
using System;

namespace ConsoleApp1
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Current directory: " + Environment.CurrentDirectory);
            Console.WriteLine("Running in {0}-bit mode.", Environment.Is64BitProcess ? "64" : "32");

            FFmpegBinariesHelper.RegisterFFmpegBinaries();

            Console.WriteLine($"FFmpeg version info: {ffmpeg.av_version_info()}");
        }
    }
}

그러니까, RegisterFFmpegBinaries 메서드의 역할은 (위에서 vcpkg로 빌드한) 네이티브 라이브러리를 찾는 경로를 지정합니다. 코드를 한번 볼까요?

using System;
using System.IO;
using System.Runtime.InteropServices;

namespace FFmpeg.AutoGen.Example
{
    public class FFmpegBinariesHelper
    {
        internal static void RegisterFFmpegBinaries()
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                var current = Environment.CurrentDirectory;
                var probe = Path.Combine("FFmpeg", "bin", Environment.Is64BitProcess ? "x64" : "x86");

                while (current != null)
                {
                    var ffmpegBinaryPath = Path.Combine(current, probe);

                    if (Directory.Exists(ffmpegBinaryPath))
                    {
                        Console.WriteLine($"FFmpeg binaries found in: {ffmpegBinaryPath}");
                        ffmpeg.RootPath = ffmpegBinaryPath;
                        return;
                    }

                    current = Directory.GetParent(current)?.FullName;
                }
            } 
            else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                ffmpeg.RootPath = "/lib/x86_64-linux-gnu/";
            } 
            else
            {
                throw new NotSupportedException(); // fell free add support for platform of your choose
            }
        }
    }
}

보는 바와 같이 윈도우를 대상으로 한다면, 실행 파일 위치를 기준으로 ./FFmpeg/bin/ 하위의 플랫폼에 따른 x64/x86 디렉터리에 ffmpeg 네이티브 라이브러리가 있다는 것을 가정하고 있습니다.

물론, 저 경로를 바꿔도 되겠지만 그냥 맞춰줘도 무방합니다. 혹은, 아예 저 코드를 통째로 날리고 다음과 같이 한 줄로 자신이 포함한 네이티브 라이브러리의 위치를 지정하면 됩니다.

ffmpeg.RootPath = Path.Combine(Environment.CurrentDirectory, "FFmpeg", "bin", "x64");

자, 그럼 위의 경로를 맞춰주기 위해 vcpkg로 빌드한(또는, github repo에서 다운로드한) DLL들을 콘솔 프로젝트의 "FFmpeg/bin/x64" 디렉터리에 복사합니다. 여기까지의 솔루션 구성은 다음과 같습니다.

ffmpeg_prj_1.png

당연히 이렇게만 하면 안 되죠? ^^ 빌드 시에 위에서 포함한 DLL들이 복사가 되도록 각각의 DLL 파일에 대한 "Copy to Output Directory" 값을 "Copy if newer"로 바꿔줍니다. (그리고 잊지 말아야 할 것은, 위와 같이 x64용 DLL을 사용하는 경우라면 콘솔 프로젝트의 속성 창에서 "Prefer 32-bit" 옵션을 꺼야 합니다.)

여기까지가 준비 과정의 끝입니다. 이후의 작업은 ffmpeg 라이브러리를 사용한 경험이 있다면 그에 따라 FFmpeg.AutoGen의 "ffempg" 정적 속성에서 제공하는 래퍼 API를 호출하는 식으로 작업을 진행하면 됩니다.




기왕에 하는 거, 그럼 기본 예제 프로젝트만이라도 포팅을 해볼까요? ^^ 해당 프로젝트에서 로그 설정과 Hardware Decoder를 조회하는 코드를 복사해 옵니다.

static void Main(string[] args)
{
    Console.WriteLine("Current directory: " + Environment.CurrentDirectory);
    Console.WriteLine("Running in {0}-bit mode.", Environment.Is64BitProcess ? "64" : "32");

    FFmpegBinariesHelper.RegisterFFmpegBinaries();

    Console.WriteLine($"FFmpeg version info: {ffmpeg.av_version_info()}");

    SetupLogging();

    ConfigureHWDecoder(out var deviceType);
}

private static void ConfigureHWDecoder(out AVHWDeviceType HWtype)
{
    HWtype = AVHWDeviceType.AV_HWDEVICE_TYPE_NONE;
    Console.WriteLine("Use hardware acceleration for decoding?[n]");
    var key = Console.ReadLine();
    var availableHWDecoders = new Dictionary<int, AVHWDeviceType>();

    if (key == "y")
    {
        Console.WriteLine("Select hardware decoder:");
        var type = AVHWDeviceType.AV_HWDEVICE_TYPE_NONE;
        var number = 0;

        while ((type = ffmpeg.av_hwdevice_iterate_types(type)) != AVHWDeviceType.AV_HWDEVICE_TYPE_NONE)
        {
            Console.WriteLine($"{++number}. {type}");
            availableHWDecoders.Add(number, type);
        }

        if (availableHWDecoders.Count == 0)
        {
            Console.WriteLine("Your system have no hardware decoders.");
            HWtype = AVHWDeviceType.AV_HWDEVICE_TYPE_NONE;
            return;
        }

        var decoderNumber = availableHWDecoders
            .SingleOrDefault(t => t.Value == AVHWDeviceType.AV_HWDEVICE_TYPE_DXVA2).Key;
        if (decoderNumber == 0)
            decoderNumber = availableHWDecoders.First().Key;
        Console.WriteLine($"Selected [{decoderNumber}]");
        int.TryParse(Console.ReadLine(), out var inputDecoderNumber);
        availableHWDecoders.TryGetValue(inputDecoderNumber == 0 ? decoderNumber : inputDecoderNumber,
            out HWtype);
    }
}

private static unsafe void SetupLogging()
{
    ffmpeg.av_log_set_level(ffmpeg.AV_LOG_VERBOSE);

    // do not convert to local function
    av_log_set_callback_callback logCallback = (p0, level, format, vl) =>
    {
        if (level > ffmpeg.av_log_get_level()) return;

        var lineSize = 1024;
        var lineBuffer = stackalloc byte[lineSize];
        var printPrefix = 1;
        ffmpeg.av_log_format_line(p0, level, format, vl, lineBuffer, lineSize, &printPrefix);
        var line = Marshal.PtrToStringAnsi((IntPtr)lineBuffer);
        Console.ForegroundColor = ConsoleColor.Yellow;
        Console.Write(line);
        Console.ResetColor();
    };

    ffmpeg.av_log_set_callback(logCallback);
}

제 컴퓨터에서 실행해 보면, "AV_HWDEVICE_TYPE_DXVA2", "AV_HWDEVICE_TYPE_D3D11VA" 2개의 decoder가 탐색되었는데요, 소스 코드를 보면 Console.ReadLine으로 묻는 과정이 있으므로 이를 생략해 그냥 적절하게 옵션 처리하거나 대중적으로 보이는 AV_HWDEVICE_TYPE_DXVA2 옵션을 기본값으로 시작하는 정도로 코드를 살짝 바꿔 응용하면 되겠습니다.




Hardware Decoder 유형이 선택되었으면, 이것으로 당연히 프레임을 디코딩할 수 있습니다.

Console.WriteLine("Decoding...");
DecodeAllFramesToImages(deviceType);

private static unsafe void DecodeAllFramesToImages(AVHWDeviceType HWDevice)
{
    // decode all frames from url, please not it might local resorce, e.g. string url = "../../sample_mpeg4.mp4";
    var url = "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"; // be advised this file holds 1440 frames
    using var vsd = new VideoStreamDecoder(url, HWDevice);

    Console.WriteLine($"codec name: {vsd.CodecName}");

    var info = vsd.GetContextInfo();
    info.ToList().ForEach(x => Console.WriteLine($"{x.Key} = {x.Value}"));

    var sourceSize = vsd.FrameSize;
    var sourcePixelFormat = HWDevice == AVHWDeviceType.AV_HWDEVICE_TYPE_NONE
        ? vsd.PixelFormat
        : GetHWPixelFormat(HWDevice);
    var destinationSize = sourceSize;
    var destinationPixelFormat = AVPixelFormat.AV_PIX_FMT_BGR24;
    using var vfc =
        new VideoFrameConverter(sourceSize, sourcePixelFormat, destinationSize, destinationPixelFormat);

    var frameNumber = 0;

    while (vsd.TryDecodeNextFrame(out var frame))
    {
        var convertedFrame = vfc.Convert(frame);

        using (var bitmap = new Bitmap(convertedFrame.width,
            convertedFrame.height,
            convertedFrame.linesize[0],
            PixelFormat.Format24bppRgb,
            (IntPtr) convertedFrame.data[0]))
            bitmap.Save($"frame.{frameNumber:D8}.jpg", ImageFormat.Jpeg);

        Console.WriteLine($"frame: {frameNumber}");
        frameNumber++;
    }
}

위에 사용한 VideoStreamDecoder는 예제 프로젝트에 포함돼 있으니 그걸 그대로 추가하면 됩니다. 그러니까, 결국 위의 소스 코드는 mp4 파일을 프레임 별로 Bitmap으로 변환 후 JPG 파일로 저장하는 역할을 합니다.

그리고 그다음 나오는 EncodeImagesToH264 메서드는, 위에서 저장했던 JPG 파일을 다시 "out.h264" 파일로 인코딩시킵니다. (해당 파일은 곰 플레이어나, VLC를 이용해 재생할 수 있습니다.) 오호~~~ 간단한 예제 프로젝트 하나에서 동영상 디코딩/인코딩 과정을 모두 다루고 있었군요. ^^

Console.WriteLine("Encoding...");
EncodeImagesToH264();

private static unsafe void EncodeImagesToH264()
{
    var frameFiles = Directory.GetFiles(".", "frame.*.jpg").OrderBy(x => x).ToArray();
    var fistFrameImage = Image.FromFile(frameFiles.First());

    var outputFileName = "out.h264";
    var fps = 25;
    var sourceSize = fistFrameImage.Size;
    var sourcePixelFormat = AVPixelFormat.AV_PIX_FMT_BGR24;
    var destinationSize = sourceSize;
    var destinationPixelFormat = AVPixelFormat.AV_PIX_FMT_YUV420P;
    using var vfc =
        new VideoFrameConverter(sourceSize, sourcePixelFormat, destinationSize, destinationPixelFormat);

    using var fs = File.Open(outputFileName, FileMode.Create);

    using var vse = new H264VideoStreamEncoder(fs, fps, destinationSize);

    var frameNumber = 0;

    foreach (var frameFile in frameFiles)
    {
        byte[] bitmapData;

        using (var frameImage = Image.FromFile(frameFile))
        using (var frameBitmap = frameImage is Bitmap bitmap ? bitmap : new Bitmap(frameImage))
            bitmapData = GetBitmapData(frameBitmap);

        fixed (byte* pBitmapData = bitmapData)
        {
            var data = new byte_ptrArray8 { [0] = pBitmapData };
            var linesize = new int_array8 { [0] = bitmapData.Length / sourceSize.Height };
            var frame = new AVFrame
            {
                data = data,
                linesize = linesize,
                height = sourceSize.Height
            };
            var convertedFrame = vfc.Convert(frame);
            convertedFrame.pts = frameNumber * fps;
            vse.Encode(convertedFrame);
        }

        Console.WriteLine($"frame: {frameNumber}");
        frameNumber++;
    }
}

처음엔 막막했는데, 초보인 저도 예제 프로젝트 하나만으로 대충 ffmpeg를 사용할 수 있을 것만 같은 자신감이 생기는군요. ^^

(첨부 파일은 이 글의 예제 코드를 포함합니다. 단지, 압축 파일 크기를 줄이기 위해 ./FFmpeg/bin/x64 하위의 DLL은 제거했습니다.)





[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 4/20/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 16  17  18  [19]  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13148정성태10/26/20225657오류 유형: 824. msbuild 에러 - error NETSDK1005: Assets file '...\project.assets.json' doesn't have a target for 'net5.0'. Ensure that restore has run and that you have included 'net5.0' in the TargetFramew
13147정성태10/25/20224767오류 유형: 823. Visual Studio 2022 - Unable to attach to CoreCLR. The debugger's protocol is incompatible with the debuggee.
13146정성태10/24/20225612.NET Framework: 2060. C# - Java의 Xmx와 유사한 힙 메모리 최댓값 제어 옵션 HeapHardLimit
13145정성태10/21/20225877오류 유형: 822. db2 - Password validation for user db2inst1 failed with rc = -2146500508
13144정성태10/20/20225715.NET Framework: 2059. ClrMD를 이용해 윈도우 환경의 메모리 덤프로부터 닷넷 모듈을 추출하는 방법파일 다운로드1
13143정성태10/19/20226227오류 유형: 821. windbg/sos - Error code - 0x000021BE
13142정성태10/18/20224967도서: 시작하세요! C# 12 프로그래밍
13141정성태10/17/20226712.NET Framework: 2058. [in,out] 배열을 C#에서 C/C++로 넘기는 방법 - 세 번째 이야기파일 다운로드1
13140정성태10/11/20226096C/C++: 159. C/C++ - 리눅스 환경에서 u16string 문자열을 출력하는 방법 [2]
13139정성태10/9/20225926.NET Framework: 2057. 리눅스 환경의 .NET Core 3/5+ 메모리 덤프로부터 모든 닷넷 모듈을 추출하는 방법파일 다운로드1
13138정성태10/8/20227216.NET Framework: 2056. C# - await 비동기 호출을 기대한 메서드가 동기로 호출되었을 때의 부작용 [1]
13137정성태10/8/20225603.NET Framework: 2055. 리눅스 환경의 .NET Core 3/5+ 메모리 덤프로부터 닷넷 모듈을 추출하는 방법
13136정성태10/7/20226176.NET Framework: 2054. .NET Core/5+ SDK 설치 없이 dotnet-dump 사용하는 방법
13135정성태10/5/20226405.NET Framework: 2053. 리눅스 환경의 .NET Core 3/5+ 메모리 덤프를 분석하는 방법 - 두 번째 이야기
13134정성태10/4/20225137오류 유형: 820. There is a problem with AMD Radeon RX 5600 XT device. For more information, search for 'graphics device driver error code 31'
13133정성태10/4/20225455Windows: 211. Windows - (commit이 아닌) reserved 메모리 사용량 확인 방법 [1]
13132정성태10/3/20225331스크립트: 42. 파이썬 - latexify-py 패키지 소개 - 함수를 mathjax 식으로 표현
13131정성태10/3/20227994.NET Framework: 2052. C# - Windows Forms의 데이터 바인딩 지원(DataBinding, DataSource) [2]파일 다운로드1
13130정성태9/28/20225096.NET Framework: 2051. .NET Core/5+ - 에러 로깅을 위한 Middleware가 동작하지 않는 경우파일 다운로드1
13129정성태9/27/20225391.NET Framework: 2050. .NET Core를 IIS에서 호스팅하는 경우 .NET Framework CLR이 함께 로드되는 환경
13128정성태9/23/20227974C/C++: 158. Visual C++ - IDL 구문 중 "unsigned long"을 인식하지 못하는 #import파일 다운로드1
13127정성태9/22/20226423Windows: 210. WSL에 systemd 도입
13126정성태9/15/20227031.NET Framework: 2049. C# 11 - 정적 메서드에 대한 delegate 처리 시 cache 적용
13125정성태9/14/20227241.NET Framework: 2048. C# 11 - 구조체 필드의 자동 초기화(auto-default structs)
13124정성태9/13/20226981.NET Framework: 2047. Golang, Python, C#에서의 CRC32 사용
13123정성태9/8/20227418.NET Framework: 2046. C# 11 - 멤버(속성/필드)에 지정할 수 있는 required 예약어 추가
... 16  17  18  [19]  20  21  22  23  24  25  26  27  28  29  30  ...