성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 그냥 RSS Reader 기능과 약간의 UI 편의성 때문에 사용...
[이종효] 오래된 소프트웨어는 보안 위협이 되기도 합니다. 혹시 어떤 기능...
[정성태] @Keystroke IEEE의 문서를 소개해 주시다니... +_...
[손민수 (Keystroke)] 괜히 듀얼채널 구성할 때 한번에 같은 제품 사라고 하는 것이 아...
[정성태] 전각(Full-width)/반각(Half-width) 기능을 토...
[정성태] Vector에 대한 내용은 없습니다. Vector가 닷넷 BCL...
[orion] 글 읽고 찾아보니 디자인 타임에는 InitializeCompon...
[orion] 연휴 전에 재현 프로젝트 올리자 생각해 놓고 여의치 않아서 못 ...
[정성태] 아래의 글에 정리했으니 참고하세요. C# - Typed D...
[정성태] 간단한 재현 프로젝트라도 있을까요? 저런 식으로 설명만 해...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - ffmpeg(FFmpeg.AutoGen)로 하드웨어 가속기를 이용한 비디오 디코딩 예제(hw_decode.c)</h1> <p> 지난 글에 이어,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12924'>https://www.sysnet.pe.kr/2/0/12924</a> </pre> <br /> 이번에는 하드웨어 가속기를 이용한 디코딩 예제를 변환해 보겠습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > hw_decode.c ; <a target='tab' href='https://ffmpeg.org/doxygen/trunk/hw_decode_8c-example.html'>https://ffmpeg.org/doxygen/trunk/hw_decode_8c-example.html</a> </pre> <br /> 변환한 C# 코드의 전문은 다음과 같습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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; } } } </pre> <br /> C#의 unsafe 구문이 없었으면 꿈도 못 꿀 변환입니다. ^^;<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그나저나, <a target='tab' href='https://www.sysnet.pe.kr/2/0/12924'>지난번 decode_video.c 사례</a>에서는 정상적인 이미지 파일로 변환을 못했는데요, 이번에는 어떨까요? ^^<br /> <br /> decode_write 함수에 av_image_copy_to_buffer의 buffer에 담긴 데이터가, <br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ret = ffmpeg.av_image_copy_to_buffer(<span style='color: blue; font-weight: bold'>buffer</span>, size, *ptrFrameData, *ptrLineSize, (AVPixelFormat)tmp_frame->format, tmp_frame->width, tmp_frame->height, 1); </pre> <br /> 이거저거 시도해 보니 해석이 됩니다. 가령, 다음과 같이 프레임 하나를 건져 format을 보니 AV_PIX_FMT_NV12로 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > if (avctx->frame_number == 310) { if (<span style='color: blue; font-weight: bold'>tmp_frame->format == (int)AVPixelFormat.AV_PIX_FMT_NV12</span>) { // ...[생략]... } } </pre> <br /> AV_PIX_FMT_NV12에 대한 도움말을 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // // 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, </pre> <br /> 12bit per plane인데, 다행히 이에 대한 해석을 stackoverflow에서 얻을 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > How to Convert NV12 to BGR(AVFrame to cv::Mat) ; <a target='tab' href='https://stackoverflow.com/questions/53994735/how-to-convert-nv12-to-bgravframe-to-cvmat'>https://stackoverflow.com/questions/53994735/how-to-convert-nv12-to-bgravframe-to-cvmat</a> </pre> <br /> 따라서, 다음과 같이 코딩을 완성할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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(<span style='color: blue; font-weight: bold'>tmp_frame->height * 3 / 2</span>, tmp_frame->width, OpenCvSharp.MatType.CV_8UC1, new IntPtr(buffer)); OpenCvSharp.Cv2.CvtColor(nvMat, rgbMat, OpenCvSharp.ColorConversionCodes.<span style='color: blue; font-weight: bold'>YUV2BGR_NV12</span>); OpenCvSharp.Cv2.ImShow("test", rgbMat); return ret; } } </pre> <br /> 8UC1이라 8bit * 3 / 2 = 12bit가 왠지 절묘하게 맞는 것도 같습니다. ^^ 어쨌든, 저렇게 해서 변환된 rgbMat 변수를 OpenCvSharp.Cv2.ImShow로 보면 이미지가 정상적으로 잘 나옵니다. 실제로 아래의 화면에서 곰 플레이어로 본 영상(위)과 OpenCvSharp.Cv2.ImShow로 영상(아래)을 함께 캡처한 것입니다.<br /> <br /> (사진은 <a target='tab' href='https://www.youtube.com/watch?v=f-sxdm4QIvc'>유튜브 영상 "디에이드"의 "안다은" 님</a>이고 사용을 허락받고 올립니다.)<br /> <img alt='hw_video_decode_1.png' src='/SysWebRes/bbs/hw_video_decode_1.png' /><br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1884&boardid=331301885'>첨부 파일은 hw_decode.c 파일의 C#과 Visual C++로 포팅한 프로젝트 파일</a>을 포함합니다.)<br /> (<a target='tab' href='https://github.com/stjeong/ffmpeg_autogen_cs/tree/master/hw_decode'>이 글의 소스 코드는 github에 올려</a>져 있습니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> 그나저나, 정말 영상 압축의 힘이 대단한 것 같습니다. ^^ 13MB 짜리 h264 인코딩된 mp4 파일인데, 이것을 위의 hw_decode.c / decode_write 함수 내에서 해석한 raw 이미지들을 파일로 쓰면 그 용량이 거의 1.8GB에 달합니다.<br /> <br /> 그리고, AVPixelFormat에 정의된 enum 상수들은 보는 것만으로도 질리게 만듭니다. ^^; 게다가 비슷한 정의들도 있는데, 이번 글에서 언급한 AV_PIX_FMT_NV12 포맷과 AV_PIX_FMT_YUV420P의 포맷은 어떤 차이일까요? ^^ (혹시 아시는 분은 덧글 부탁드립니다.)<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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) </pre> <br /> 아마도 AVPixelFormat의 상수를 모두 설명하는 것만으로 책 한 권 분량이 나오지 않을까 싶습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> 이 예제에서 출력한 test.dat 파일은 다음의 명령어로 재생해 볼 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ffplay -autoexit -f rawvideo -pixel_format nv12 -video_size 1920x1080 test.dat </pre> <br /> 참고로, 테스트를 위해 (repo의 Samples 디렉터리에 포함시킨 5.00초 분량의) file_example_MP4_1920_18MG.mp4 파일을 출력한 test.dat 파일을 ffplay로 재생해 보면 5.97초로 재생이 됩니다. 왜냐하면 ffplay는 기본적으로 25fps로 재생하기 때문인데요, 따라서 옵션에 30fps를 지정하면 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > ffplay -autoexit -f rawvideo -framerate 30 -pixel_format nv12 -video_size 1920x1080 test.dat </pre> <br /> 그럼 ffplay에 찍히는 재생 시간이 대략 4.96 ~ 4.98초 정도로 나옵니다.<br /> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
8095
(왼쪽의 숫자를 입력해야 합니다.)