Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

C# - ffmpeg(FFmpeg.AutoGen)로 하드웨어 가속기를 이용한 비디오 디코딩 예제(hw_decode.c)

지난 글에 이어,

C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c)
; https://www.sysnet.pe.kr/2/0/12924

이번에는 하드웨어 가속기를 이용한 디코딩 예제를 변환해 보겠습니다.

hw_decode.c
; https://ffmpeg.org/doxygen/trunk/hw_decode_8c-example.html

변환한 C# 코드의 전문은 다음과 같습니다.

using FFmpeg.AutoGen;
using FFmpeg.AutoGen.Example;
using System;
using System.IO;
using System.Runtime.InteropServices;

namespace FFmpegApp1
{
    internal unsafe class Program
    {
        static AVPixelFormat _hw_pix_fmt = AVPixelFormat.AV_PIX_FMT_NONE;
        static AVBufferRef* _hw_device_ctx = null;

        static void Main(string[] args)
        {
            FFmpegBinariesHelper.RegisterFFmpegBinaries();

#if DEBUG
            Console.WriteLine("Current directory: " + Environment.CurrentDirectory);
            Console.WriteLine("Running in {0}-bit mode.", Environment.Is64BitProcess ? "64" : "32");
            Console.WriteLine($"FFmpeg version info: {ffmpeg.av_version_info()}");
#endif
            Console.WriteLine();

            Console.WriteLine($"LIBAVFORMAT Version: {ffmpeg.LIBAVFORMAT_VERSION_MAJOR}.{ffmpeg.LIBAVFORMAT_VERSION_MINOR}");

            string outputPath = @"c:\temp\output";
            try
            {
                Directory.Delete(outputPath, true);
                Directory.CreateDirectory(outputPath);
            }
            catch { }

            string outputFilePath = Path.Combine(outputPath, "test_cs2.dat");

            /* hwdevice type: cuda, dxva2, d3d11va, opencl */
            video_decode_example("cuda", @"D:\video_sample\theade-i-was-young.mp4", outputFilePath);
        }

        static unsafe void video_decode_example(string hwdeviceName, string filename, string outputFileName)
        {
            AVFormatContext* input_ctx = null;
            AVStream* video = null;
            AVCodecContext* decoder_ctx = null;
            AVCodec* decoder = null;
            AVPacket* packet = null;

            // https://ffmpeg.org/doxygen/trunk/codec_8h_source.html#l00420
            int AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX = 0x01;
            int ret = 0;

            AVHWDeviceType type = AVHWDeviceType.AV_HWDEVICE_TYPE_NONE;

            Console.WriteLine("Available device types:");
            while ((type = ffmpeg.av_hwdevice_iterate_types(type)) != AVHWDeviceType.AV_HWDEVICE_TYPE_NONE)
            {
                Console.WriteLine($"\t{ffmpeg.av_hwdevice_get_type_name(type)}");
            }

            type = ffmpeg.av_hwdevice_find_type_by_name(hwdeviceName);
            if (type == AVHWDeviceType.AV_HWDEVICE_TYPE_NONE)
            {
                Console.WriteLine($"Device type {hwdeviceNae} is not supported.");
                return;
            }

            do
            {
                packet = ffmpeg.av_packet_alloc();
                if (packet == null)
                {
                    Console.WriteLine("Failed to allocate AVPacket");
                    break;
                }

                if (ffmpeg.avformat_open_input(&input_ctx, filename, null, null) < 0)
                {
                    Console.WriteLine($"Cannot open input file: {filename}");
                    break;
                }

                if (ffmpeg.avformat_find_stream_info(input_ctx, null) < 0)
                {
                    Console.WriteLine("Cannot find input stream information.");
                    break;
                }

                /* find the video stream information */
                ret = ffmpeg.av_find_best_stream(input_ctx, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, &decoder, 0);
                if (ret < 0)
                {
                    Console.WriteLine("Cannot find a video stream in the input file\n");
                    break;
                }

                int video_stream = ret;

                for (int i = 0; ; i++)
                {
                    AVCodecHWConfig* config = ffmpeg.avcodec_get_hw_config(decoder, i);
                    if (config == null)
                    {
                        string decoderName = Marshal.PtrToStringUTF8(new IntPtr(decoder->name));
                        Console.WriteLine($"Decoder {decoderName} does not support device type {ffmpeg.av_hwdevice_get_type_name(type)}");
                        break;
                    }

                    if ((config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) == AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX &&
                            config->device_type == type)
                    {
                        _hw_pix_fmt = config->pix_fmt;
                        break;
                    }
                }

                if (_hw_pix_fmt == AVPixelFormat.AV_PIX_FMT_NONE)
                {
                    break;
                }

                decoder_ctx = ffmpeg.avcodec_alloc_context3(decoder);
                if (decoder_ctx == null)
                {
                    Console.WriteLine("Could not allocate video codec context");
                    break;
                }

                video = input_ctx->streams[video_stream];
                if (ffmpeg.avcodec_parameters_to_context(decoder_ctx, video->codecpar) < 0)
                {
                    break;
                }

                decoder_ctx->get_format = (AVCodecContext_get_format_func)get_hw_format;

                if (hw_decoder_init(decoder_ctx, type) < 0)
                {
                    break;
                }

                if ((ret = ffmpeg.avcodec_open2(decoder_ctx, decoder, null)) < 0)
                {
                    Console.WriteLine($"Failed to open codec for stream {video_stream}");
                    break;
                }

                using FileStream output_file = File.OpenWrite(outputFileName);

                while (ret >= 0)
                {
                    if ((ret = ffmpeg.av_read_frame(input_ctx, packet)) < 0)
                    {
                        break;
                    }

                    if (video_stream == packet->stream_index)
                    {
                        ret = decode_write(decoder_ctx, packet, output_file);
                    }
                }

                ffmpeg.av_packet_unref(packet);

                /* flush the decoder */
                ret = decode_write(decoder_ctx, null, output_file);

            } while (false);

            if (packet != null)
            {
                ffmpeg.av_packet_free(&packet);
            }

            if (decoder_ctx != null)
            {
                ffmpeg.avcodec_free_context(&decoder_ctx);
            }

            if (input_ctx != null)
            {
                ffmpeg.avformat_close_input(&input_ctx);
            }

            if (_hw_device_ctx != null)
            {
                fixed (AVBufferRef** ppRef = &_hw_device_ctx)
                {
                    ffmpeg.av_buffer_unref(ppRef);
                }
            }
        }

        static unsafe int decode_write(AVCodecContext* avctx, AVPacket* packet, FileStream output_file)
        {
            AVFrame* frame = null;
            AVFrame* sw_frame = null;
            AVFrame* tmp_frame = null;
            byte* buffer = null;
            int size = 0;
            int ret = 0;

            ret = ffmpeg.avcodec_send_packet(avctx, packet);
            if (ret < 0)
            {
                Console.WriteLine("Error during decoding");
                return ret;
            }

            while (true)
            {
                if ((frame = ffmpeg.av_frame_alloc()) == null || (sw_frame = ffmpeg.av_frame_alloc()) == null)
                {
                    Console.WriteLine("Can not alloc frame");
                    ret = ffmpeg.AVERROR(ffmpeg.ENOMEM);
                    goto fail;
                }

                ret = ffmpeg.avcodec_receive_frame(avctx, frame);
                if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN) || ret == ffmpeg.AVERROR_EOF)
                {
                    ffmpeg.av_frame_free(&frame);
                    ffmpeg.av_frame_free(&sw_frame);
                    return 0;
                }
                else if (ret < 0)
                {
                    Console.WriteLine("Error while decoding");
                    goto fail;
                }

                if (frame->format == (int)_hw_pix_fmt)
                {
                    /* retrieve data from GPU to CPU */
                    if ((ret = ffmpeg.av_hwframe_transfer_data(sw_frame, frame, 0)) < 0)
                    {
                        Console.WriteLine("Error transferring the data to system memory");
                        goto fail;
                    }

                    tmp_frame = sw_frame;
                }
                else
                {
                    tmp_frame = frame;
                }

                size = ffmpeg.av_image_get_buffer_size((AVPixelFormat)tmp_frame->format, tmp_frame->width,
                                                tmp_frame->height, 1);
                buffer = (byte *)ffmpeg.av_malloc((ulong)size);
                if (buffer == null)
                {
                    Console.WriteLine("Can not alloc buffer");
                    ret = ffmpeg.AVERROR(ffmpeg.ENOMEM);
                    goto fail;
                }

                FFmpeg.AutoGen.byte_ptrArray4* ptrFrameData = (FFmpeg.AutoGen.byte_ptrArray4*)&tmp_frame->data;
                FFmpeg.AutoGen.int_array4* ptrLineSize = (FFmpeg.AutoGen.int_array4*)&tmp_frame->linesize;

                ret = ffmpeg.av_image_copy_to_buffer(buffer, size,
                                              *ptrFrameData, *ptrLineSize,
                                      (AVPixelFormat)tmp_frame->format,
                                      tmp_frame->width, tmp_frame->height, 1);
                if (ret < 0)
                {
                    Console.WriteLine("Can not copy image to buffer");
                    goto fail;
                }

                ReadOnlySpan<byte> contents = new Span<byte>(buffer, size);
                output_file.Write(contents);

            fail:
                ffmpeg.av_frame_free(&frame);
                ffmpeg.av_frame_free(&sw_frame);
                ffmpeg.av_freep(&buffer);
                if (ret < 0)
                {
                    return ret;
                }
            }
        }

        static unsafe int hw_decoder_init(AVCodecContext* ctx, AVHWDeviceType type)
        {
            int err = 0;

            fixed (AVBufferRef** ptr = &_hw_device_ctx)
            {
                if ((err = ffmpeg.av_hwdevice_ctx_create(ptr, type, null, null, 0)) < 0)
                {
                    Console.WriteLine("Failed to create specified HW device.");
                    return err;
                }

                ctx->hw_device_ctx = ffmpeg.av_buffer_ref(*ptr);
            }

            return err;
        }

        static unsafe AVPixelFormat get_hw_format(AVCodecContext* ctx, AVPixelFormat* pix_fmts)
        {
            AVPixelFormat* p;

            for (p = pix_fmts; *p != AVPixelFormat.AV_PIX_FMT_NONE; p++)
            {
                if (*p == _hw_pix_fmt)
                {
                    return *p;
                }
            }

            Console.WriteLine("Failed to get HW surface format.");
            return AVPixelFormat.AV_PIX_FMT_NONE;
        }
    }
}

C#의 unsafe 구문이 없었으면 꿈도 못 꿀 변환입니다. ^^;




그나저나, 지난번 decode_video.c 사례에서는 정상적인 이미지 파일로 변환을 못했는데요, 이번에는 어떨까요? ^^

decode_write 함수에 av_image_copy_to_buffer의 buffer에 담긴 데이터가,

ret = ffmpeg.av_image_copy_to_buffer(buffer, size,
                                *ptrFrameData, *ptrLineSize,
                        (AVPixelFormat)tmp_frame->format,
                        tmp_frame->width, tmp_frame->height, 1);

이거저거 시도해 보니 해석이 됩니다. 가령, 다음과 같이 프레임 하나를 건져 format을 보니 AV_PIX_FMT_NV12로 나옵니다.

if (avctx->frame_number == 310)
{
    if (tmp_frame->format == (int)AVPixelFormat.AV_PIX_FMT_NV12)
    {
        // ...[생략]...
    }
}

AV_PIX_FMT_NV12에 대한 도움말을 보면,

//
// Summary:
//     planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which
//     are interleaved (first byte U and the following byte V)
AV_PIX_FMT_NV12 = 23,

12bit per plane인데, 다행히 이에 대한 해석을 stackoverflow에서 얻을 수 있습니다.

How to Convert NV12 to BGR(AVFrame to cv::Mat)
; https://stackoverflow.com/questions/53994735/how-to-convert-nv12-to-bgravframe-to-cvmat

따라서, 다음과 같이 코딩을 완성할 수 있습니다.

if (avctx->frame_number == 310)
{
    if (tmp_frame->format == (int)AVPixelFormat.AV_PIX_FMT_NV12)
    {
        OpenCvSharp.Mat rgbMat = new OpenCvSharp.Mat();
        OpenCvSharp.Mat nvMat = new OpenCvSharp.Mat(tmp_frame->height * 3 / 2, tmp_frame->width,
            OpenCvSharp.MatType.CV_8UC1, new IntPtr(buffer));

        OpenCvSharp.Cv2.CvtColor(nvMat, rgbMat, OpenCvSharp.ColorConversionCodes.YUV2BGR_NV12);
        OpenCvSharp.Cv2.ImShow("test", rgbMat);
        return ret;
    }
}

8UC1이라 8bit * 3 / 2 = 12bit가 왠지 절묘하게 맞는 것도 같습니다. ^^ 어쨌든, 저렇게 해서 변환된 rgbMat 변수를 OpenCvSharp.Cv2.ImShow로 보면 이미지가 정상적으로 잘 나옵니다. 실제로 아래의 화면에서 곰 플레이어로 본 영상(위)과 OpenCvSharp.Cv2.ImShow로 영상(아래)을 함께 캡처한 것입니다.

(사진은 유튜브 영상 "디에이드"의 "안다은" 님이고 사용을 허락받고 올립니다.)
hw_video_decode_1.png

(첨부 파일은 hw_decode.c 파일의 C#과 Visual C++로 포팅한 프로젝트 파일을 포함합니다.)
(이 글의 소스 코드는 github에 올려져 있습니다.)




그나저나, 정말 영상 압축의 힘이 대단한 것 같습니다. ^^ 13MB 짜리 h264 인코딩된 mp4 파일인데, 이것을 위의 hw_decode.c / decode_write 함수 내에서 해석한 raw 이미지들을 파일로 쓰면 그 용량이 거의 1.8GB에 달합니다.

그리고, AVPixelFormat에 정의된 enum 상수들은 보는 것만으로도 질리게 만듭니다. ^^; 게다가 비슷한 정의들도 있는데, 이번 글에서 언급한 AV_PIX_FMT_NV12 포맷과 AV_PIX_FMT_YUV420P의 포맷은 어떤 차이일까요? ^^ (혹시 아시는 분은 덧글 부탁드립니다.)

AV_PIX_FMT_YUV420P 	
planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)

AV_PIX_FMT_NV12 
planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V)

아마도 AVPixelFormat의 상수를 모두 설명하는 것만으로 책 한 권 분량이 나오지 않을까 싶습니다.




이 예제에서 출력한 test.dat 파일은 다음의 명령어로 재생해 볼 수 있습니다.

ffplay -autoexit -f rawvideo -pixel_format nv12 -video_size 1920x1080 test.dat

참고로, 테스트를 위해 (repo의 Samples 디렉터리에 포함시킨 5.00초 분량의) file_example_MP4_1920_18MG.mp4 파일을 출력한 test.dat 파일을 ffplay로 재생해 보면 5.97초로 재생이 됩니다. 왜냐하면 ffplay는 기본적으로 25fps로 재생하기 때문인데요, 따라서 옵션에 30fps를 지정하면 됩니다.

ffplay -autoexit -f rawvideo -framerate 30 -pixel_format nv12 -video_size 1920x1080 test.dat

그럼 ffplay에 찍히는 재생 시간이 대략 4.96 ~ 4.98초 정도로 나옵니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/14/2022]

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

비밀번호

댓글 작성자
 



2022-01-21 10시14분
[이승준] https://docs.microsoft.com/ko-kr/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering
요기서 yv12랑 nv12를 보시면 됩니다. yv12가 yuv420p 입니다.
[손님]
2022-01-21 09시29분
@이승준 링크 정보 감사합니다. Microsoft 자료에 그렇게 있으리라고는 상상도 못했습니다. ^^ 암튼, 전문 분야는 아니지만 알아가는 재미가 있어서 좋은 것 같습니다.
정성태

1  2  3  4  5  6  [7]  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12966정성태2/13/2022878.NET Framework: 1155. C# - ffmpeg(FFmpeg.AutoGen): Bitmap으로부터 yuv420p + rawvideo 형식의 파일로 쓰기파일 다운로드1
12965정성태2/13/20221051.NET Framework: 1154. "Hanja Hangul Project v1.01 (파이썬)"의 C# 버전
12964정성태2/11/2022895.NET Framework: 1153. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 avio_reading.c 예제 포팅파일 다운로드1
12963정성태2/11/2022982.NET Framework: 1152. C# - 화면 캡처한 이미지를 ffmpeg(FFmpeg.AutoGen)로 동영상 처리 (저해상도 현상 해결)파일 다운로드1
12962정성태2/9/2022827오류 유형: 793. 마이크로소프트 스토어 - 제품이 존재하지 않습니다. 재고가 없는 것일 수 있습니다.
12961정성태2/8/20221038.NET Framework: 1151. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 프레임의 크기 및 포맷 변경 예제(scaling_video.c) [7]파일 다운로드1
12960정성태2/8/2022970개발 환경 구성: 637. ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 세 번째 이야기
12959정성태2/7/20221180.NET Framework: 1150. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 두 번째 이야기 [2]파일 다운로드1
12958정성태2/6/20221070.NET Framework: 1149. C# - ffmpeg(FFmpeg.AutoGen) - 비디오 프레임 디코딩 [2]파일 다운로드1
12957정성태2/6/20221017개발 환경 구성: 636. ffmpeg.exe를 이용해 planar 포맷의 데이터를 packed 형식으로 변환하는 방법? [2]
12956정성태2/4/2022993.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen) - decoding 과정 [2]파일 다운로드1
12955정성태2/4/2022899개발 환경 구성: 635. 비주얼 스튜디오에서 실행하던 ASP.NET Core (.NET Framework) 응용 프로그램을 명령행에서 실행하는 방법 (2)
12954정성태2/4/2022633VS.NET IDE: 173. 비주얼 스튜디오 - Output 창에 색상이 지정된 출력 결과가 "[39m[22m" 식의 문자로 나오는 문제
12953정성태2/2/2022851Linux: 48. Windows 11 + WSL 우분투 GUI 환경에서 한글 출력
12952정성태2/2/2022761.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터 예제(filter_audio.c)파일 다운로드1
12951정성태2/2/2022835.NET Framework: 1147. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터링 예제(filtering_audio.c)파일 다운로드1
12950정성태2/1/2022934.NET Framework: 1146. .NET 6에 추가되지 않은 Generic Math (예: INumber<T>)
12949정성태2/1/2022704.NET Framework: 1145. C# - ffmpeg(FFmpeg.AutoGen) - Codec 정보 열람 및 사용 준비파일 다운로드1
12948정성태1/30/2022891.NET Framework: 1144. C# - ffmpeg(FFmpeg.AutoGen) AVFormatContext를 이용해 ffprobe처럼 정보 출력파일 다운로드1
12947정성태1/30/20221051개발 환경 구성: 634. ffmpeg.exe - 기존 동영상 컨테이너에 다중 스트림을 추가하는 방법
12946정성태1/28/2022827오류 유형: 792. .NET Core - 로컬 개발 중에 docker 호스팅으로 바꾸는 경우 SQL 서버 접근 방법
12945정성태1/28/2022862오류 유형: 791. SQL 서버 로그인 시 localhost는 되고, 127.0.0.1로는 안 되는 문제
12944정성태1/28/20221277.NET Framework: 1143. C# - Entity Framework Core 6 개요
12943정성태1/27/2022949.NET Framework: 1142. .NET 5+로 포팅 시 플랫폼 호환성 경고 메시지(SYSLIB0006, SYSLIB0011, CA1416)파일 다운로드1
12942정성태1/27/2022950.NET Framework: 1141. XmlSerializer와 Dictionary 타입파일 다운로드1
12941정성태1/26/2022760오류 유형: 790. AKS/k8s - pod 상태가 Pending으로 지속되는 경우
1  2  3  4  5  6  [7]  8  9  10  11  12  13  14  15  ...