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_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초 정도로 나옵니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]