C# - ffmpeg(FFmpeg.AutoGen) - decoding 과정
지난 글에서 동영상 파일의 스트림에 대한 decoder까지 열었으니,
C# - ffmpeg(FFmpeg.AutoGen) - Codec 정보 열람 및 사용 준비
; https://www.sysnet.pe.kr/2/0/12949
C 언어로 작성된 FFmpeg Examples의 C# 포팅 전체 소스 코드
; https://www.sysnet.pe.kr/2/0/13026
이제 decoder를 이용해 영상을 재생하든, 오디오를 재생하든 할 수 있습니다. 지금까지 배운 단계로 보면, 동영상을 재생한다고 할 때 보통은 Video와 Audio를 재생할 것이므로 다음과 같은 식으로 뼈대 코드를 작성할 수 있습니다.
using FFmpeg.AutoGen;
using FFmpeg.AutoGen.Example;
using System;
using System.IO;
namespace ffmpeg_basic_3
{
internal unsafe class Program
{
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()}");
Console.WriteLine($"LIBAVFORMAT Version: {ffmpeg.LIBAVFORMAT_VERSION_MAJOR}.{ffmpeg.LIBAVFORMAT_VERSION_MINOR}");
Console.WriteLine();
#endif
AVFormatContext* av_context = null;
// ffmpeg.exe - 기존 동영상 컨테이너에 다중 스트림을 추가하는 방법
// https://www.sysnet.pe.kr/2/0/12947
string filePath = @"D:\media_sample\output2.mp4";
int ret = ffmpeg.avformat_open_input(&av_context, filePath, null, null);
if (ret != 0)
{
return;
}
ffmpeg.avformat_find_stream_info(av_context, null);
// https://www.sysnet.pe.kr/2/0/12951#get_decoder
AVCodec* videoDecoder = null;
AVCodec* audioDecoder = null;
int videoStreamIndex = ffmpeg.av_find_best_stream(av_context, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, &videoDecoder, 0);
int audioStreamIndex = ffmpeg.av_find_best_stream(av_context, AVMediaType.AVMEDIA_TYPE_AUDIO, -1, videoStreamIndex, &audioDecoder, 0);
AVCodecContext* videoContext = null;
AVCodecContext* audioContext = null;
AVStream* videoStream = null;
AVStream* audioStream = null;
do
{
if (videoDecoder == null || audioDecoder == null)
{
break;
}
// C# - ffmpeg(FFmpeg.AutoGen) - Codec 정보 열람 및 사용 준비
// https://www.sysnet.pe.kr/2/0/12949
{
videoStream = av_context->streams[videoStreamIndex];
videoContext = ffmpeg.avcodec_alloc_context3(videoDecoder);
ret = ffmpeg.avcodec_parameters_to_context(videoContext, videoStream->codecpar);
ret = ffmpeg.avcodec_open2(videoContext, videoDecoder, null);
}
{
audioStream = av_context->streams[audioStreamIndex];
audioContext = ffmpeg.avcodec_alloc_context3(audioDecoder);
ret = ffmpeg.avcodec_parameters_to_context(audioContext, audioStream->codecpar);
ret = ffmpeg.avcodec_open2(audioContext, audioDecoder, null);
}
// ...[패킷 및 프레임 처리]...
} while (false);
if (videoFrame != null)
{
ffmpeg.av_frame_unref(videoFrame);
}
if (audioFrame != null)
{
ffmpeg.av_frame_unref(audioFrame);
}
if (videoContext != null)
{
ffmpeg.avcodec_free_context(&videoContext);
}
if (audioContext != null)
{
ffmpeg.avcodec_free_context(&audioContext);
}
ffmpeg.avformat_close_input(&av_context);
}
}
}
이제 남은 것은 디코딩 부분인데요, 이게 제가 예상했던 것과는 좀 다릅니다. 우선, packet과 frame이라는 용어가 나오는데요, packet은 압축되어 있는 상태의 데이터를 가리키며 그것을 코덱으로 압축을 풀어 나오는 것이 frame이라고 합니다.
일단, packet은 av_read_frame 함수로 읽고 av_packet_unref로 정리할 수 있습니다.
// 패킷 및 프레임 처리
AVPacket* packet = ffmpeg.av_packet_alloc();
videoFrame = ffmpeg.av_frame_alloc();
audioFrame = ffmpeg.av_frame_alloc();
// 이름은 frame이지만, packet을 읽어오는!
while (ffmpeg.av_read_frame(av_context, packet) == 0)
{
Console.Write(packet->stream_index);
ffmpeg.av_packet_unref(packet);
}
if (packet != null)
{
ffmpeg.av_packet_free(&packet);
}
그런데, 저렇게 packet을 읽어오는 코드를
다중 스트림이 있는 동영상에 적용해 보면, 루프에서 stream_index를 출력했을 때 원래라면 0과 3의 스트림만 출력되는 것을 기대했었는데요,
기대하는 출력: 0303000303030303030303030333030303030303003
그런데 실제로는 다음과 같은 결과를 얻을 수 있습니다.
021230121302132101230123....[생략]...1213201230121302123102130
그러니까, 동영상 파일을 읽어올 때 특정 Stream의 데이터만 읽어오는 것이 아니라 전체 데이터에 대해 개별 스트림의 데이터를 처음부터 읽어서 반환해 주는 것입니다.
참 희한한 방식입니다. ^^;
지금 와서 보니까 과거의 글에서,
C# - ffmpeg(FFmpeg.AutoGen)로 하드웨어 가속기를 이용한 비디오 디코딩 예제(hw_decode.c)
; https://www.sysnet.pe.kr/2/0/12932
봤던 while 루프에서의 stream_index 비교 코드가 눈에 들어옵니다.
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);
}
}
그러니까, 개별 av_read_frame으로 읽은 packet마다 그것이 속한 stream에 해당하는 디코딩을 한 것입니다. 따라서, while 루프를 이번에는 비디오와 오디오를 처리해야 하므로 2개로 나눌 수 있습니다.
bool process = true;
while (process == true && ffmpeg.av_read_frame(av_context, packet) == 0)
{
if (packet->stream_index == videoStreamIndex)
{
process = decodePacket(videoContext, packet, videoFrame, AVMediaType.AVMEDIA_TYPE_VIDEO);
}
else if (packet->stream_index == audioStreamIndex)
{
process = decodePacket(audioContext, packet, audioFrame, AVMediaType.AVMEDIA_TYPE_AUDIO);
}
ffmpeg.av_packet_unref(packet);
}
그리고 packet 처리하는 함수 내부가 재미있는데요, 이전의 av_read_frame은 압축 데이터를 담은 packet을 반환했고, CodecContext를 이용해 디코딩을 avcodec_send_packet으로 한 후, 압축 해제된 프레임 데이터를 avcodec_receive_frame 호출로 받아옵니다.
private static bool decodePacket(AVCodecContext* codecContext, AVPacket* packet, AVFrame* frame, AVMediaType mediaType)
{
int ret = ffmpeg.avcodec_send_packet(pCodecContext, pkt);
if (ret < 0)
{
return false;
}
ffmpeg.avcodec_receive_frame(pCodecContext, frame);
switch (mediaType)
{
case AVMediaType.AVMEDIA_TYPE_VIDEO:
// 비디오 frame 처리
break;
case AVMediaType.AVMEDIA_TYPE_AUDIO:
// 오디오 frame 처리
break;
}
}
위에서는 packet 하나와 frame 하나가 대응하는 것으로 가정하는데요, 예전에 포팅했던 예제 코드들은 모두 packet 하나에 frame이 여러 개 있는 것처럼 디코딩을 하고 있습니다.
// C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c)
// ; https://www.sysnet.pe.kr/2/0/12924
int ret = ffmpeg.avcodec_send_packet(pCodecContext, pkt);
if (ret < 0)
{
Console.WriteLine("Error sending a packet for decoding");
return false;
}
while (ret >= 0)
{
ret = ffmpeg.avcodec_receive_frame(pCodecContext, frame);
if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN) || ret == ffmpeg.AVERROR_EOF)
{
return true;
}
else if (ret < 0)
{
Console.WriteLine("Error during decoding");
return false;
}
...[생략]...
}
// C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP2 오디오 파일 디코딩 예제(decode_audio.c)
// ; https://www.sysnet.pe.kr/2/0/12933
ret = ffmpeg.avcodec_send_packet(dec_ctx, packet);
if (ret < 0)
{
Console.WriteLine("Error submitting the packet to the decoder");
return false;
}
while (ret >= 0)
{
ret = ffmpeg.avcodec_receive_frame(dec_ctx, frame);
if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN) || ret == ffmpeg.AVERROR_EOF)
{
return true;
}
else if (ret < 0)
{
Console.WriteLine("Error during decoding");
return false;
}
...[생략]...
}
검색해 보면, packet:frame이 1:1, N:1, 1:N일 수도 있으므로 저렇게 처리하는 것이 맞습니다. 따라서 decodePacket을 대략 다음과 같은 식으로 구성할 수 있습니다.
private static bool decodePacket(AVCodecContext* codecContext, AVPacket* packet, AVFrame* frame, AVMediaType mediaType)
{
int ret = ffmpeg.avcodec_send_packet(codecContext, packet);
if (ret < 0)
{
return false;
}
while (true)
{
ret = ffmpeg.avcodec_receive_frame(codecContext, frame);
if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN))
{
return true;
}
else if (ret == ffmpeg.AVERROR_EOF)
{
ffmpeg.avcodec_flush_buffers(codecContext);
return false;
}
else if (ret < 0)
{
return false;
}
switch (mediaType)
{
case AVMediaType.AVMEDIA_TYPE_VIDEO:
// 비디오 frame 처리
break;
case AVMediaType.AVMEDIA_TYPE_AUDIO:
// 오디오 frame 처리
break;
}
}
}
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
(2022-02-07 업데이트) 이승준님의 덧글에 따라 다음과 같이 discard 설정을 하는 경우,
if (videoDecoder == null || audioDecoder == null)
{
break;
}
for (int i = 0; i < av_context->nb_streams; i++)
{
if (i != videoStreamIndex && i != audioStreamIndex)
{
av_context->streams[i]->discard = AVDiscard.AVDISCARD_ALL;
}
}
이후 실행했을 때, 초기에 1, 3번 인덱스가 한 번 나온 것을 제외하고는 이후 0, 2번 스트림만 읽혀졌습니다. ^^
02123020220202....[생략]...2202020220202202022020202202022020220202202020220202202022
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]