Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen) - decoding 과정 [링크 복사], [링크+제목 복사],
조회: 19543
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 8개 있습니다.)
.NET Framework: 1140. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP3 오디오 파일 인코딩/디코딩하는 예제
; https://www.sysnet.pe.kr/2/0/12939

.NET Framework: 1144. C# - ffmpeg(FFmpeg.AutoGen) AVFormatContext를 이용해 ffprobe처럼 정보 출력
; https://www.sysnet.pe.kr/2/0/12948

.NET Framework: 1145. C# - ffmpeg(FFmpeg.AutoGen) - Codec 정보 열람 및 사용 준비
; https://www.sysnet.pe.kr/2/0/12949

.NET Framework: 1148.  C# - ffmpeg(FFmpeg.AutoGen) - decoding 과정
; https://www.sysnet.pe.kr/2/0/12956

.NET Framework: 1149. C# - ffmpeg(FFmpeg.AutoGen) - 비디오 프레임 디코딩
; https://www.sysnet.pe.kr/2/0/12958

.NET Framework: 1155. C# - ffmpeg(FFmpeg.AutoGen): Bitmap으로부터 yuv420p + rawvideo 형식의 파일로 쓰기
; https://www.sysnet.pe.kr/2/0/12966

.NET Framework: 1156. C# - ffmpeg(FFmpeg.AutoGen): Bitmap으로부터 h264 형식의 파일로 쓰기
; https://www.sysnet.pe.kr/2/0/12970

.NET Framework: 1160. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 qsv 디코딩
; https://www.sysnet.pe.kr/2/0/12977




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




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 12/22/2022]

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

비밀번호

댓글 작성자
 



2022-02-07 10시03분
[이승준] 멀티 스트림에서 원하는 스트림만 읽고 싶을 경우
fmt_ctx->streams[sel]->discard 값을 바꿔서 해결할 수 있습니다.
AVDISCARD_ALL : 전부 버리다
AVDISCARD_DEFAULT : 기본값으로 전부 받다 입니다.
몇가지 값이 더 있는데. 보통 ALL과 DEFAULT만 설정 합니다.
제 경우는 전부 버리게 설정한 후 원하는 스트림만 따로 열어줍니다.
[guest]
2022-02-07 10시39분
@이승준 님 감사합니다. 본문을 수정했고 실제로 테스트해 보니 원하는 대로 결과가 나왔습니다. ^^
정성태

... 61  62  63  64  65  66  67  68  69  70  71  72  73  74  [75]  ...
NoWriterDateCnt.TitleFile(s)
12153정성태2/23/202024447.NET Framework: 898. Trampoline을 이용한 후킹의 한계파일 다운로드1
12152정성태2/23/202021443.NET Framework: 897. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 세 번째 이야기(Trampoline 후킹)파일 다운로드1
12151정성태2/22/202024075.NET Framework: 896. C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법 - 두 번째 이야기 (원본 함수 호출)파일 다운로드1
12150정성태2/21/202024185.NET Framework: 895. C# - Win32 API를 Trampoline 기법을 이용해 C# 메서드로 가로채는 방법 [1]파일 다운로드1
12149정성태2/20/202021082.NET Framework: 894. eBEST C# XingAPI 래퍼 - 연속 조회 처리 방법 [1]
12148정성태2/19/202025773디버깅 기술: 163. x64 환경에서 구현하는 다양한 Trampoline 기법 [1]
12147정성태2/19/202021066디버깅 기술: 162. x86/x64의 기계어 코드 최대 길이
12146정성태2/18/202022263.NET Framework: 893. eBEST C# XingAPI 래퍼 - 로그인 처리파일 다운로드1
12145정성태2/18/202023871.NET Framework: 892. eBEST C# XingAPI 래퍼 - Sqlite 지원 추가파일 다운로드1
12144정성태2/13/202024057.NET Framework: 891. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 두 번째 이야기파일 다운로드1
12143정성태2/13/202018478.NET Framework: 890. 상황별 GetFunctionPointer 반환값 정리 - x64파일 다운로드1
12142정성태2/12/202022427.NET Framework: 889. C# 코드로 접근하는 MethodDesc, MethodTable파일 다운로드1
12141정성태2/10/202021406.NET Framework: 888. C# - ASP.NET Core 웹 응용 프로그램의 출력 가로채기 [2]파일 다운로드1
12140정성태2/10/202022744.NET Framework: 887. C# - ASP.NET 웹 응용 프로그램의 출력 가로채기파일 다운로드1
12139정성태2/9/202022433.NET Framework: 886. C# - Console 응용 프로그램에서 UI 스레드 구현 방법
12138정성태2/9/202028641.NET Framework: 885. C# - 닷넷 응용 프로그램에서 SQLite 사용 [6]파일 다운로드1
12137정성태2/9/202020304오류 유형: 592. [AhnLab] 경고 - 디버거 실행을 탐지했습니다.
12136정성태2/6/202021965Windows: 168. Windows + S(또는 Q)로 뜨는 작업 표시줄의 검색 바가 동작하지 않는 경우
12135정성태2/6/202027738개발 환경 구성: 468. Nuget 패키지의 로컬 보관 폴더를 옮기는 방법 [2]
12134정성태2/5/202024985.NET Framework: 884. eBEST XingAPI의 C# 래퍼 버전 - XingAPINet Nuget 패키지 [5]파일 다운로드1
12133정성태2/5/202022772디버깅 기술: 161. Windbg 환경에서 확인해 본 .NET 메서드 JIT 컴파일 전과 후 - 두 번째 이야기
12132정성태1/28/202025859.NET Framework: 883. C#으로 구현하는 Win32 API 후킹(예: Sleep 호출 가로채기) [1]파일 다운로드1
12131정성태1/27/202024509개발 환경 구성: 467. LocaleEmulator를 이용해 유니코드를 지원하지 않는(한글이 깨지는) 프로그램을 실행하는 방법 [1]
12130정성태1/26/202022056VS.NET IDE: 142. Visual Studio에서 windbg의 "Open Executable..."처럼 EXE를 직접 열어 디버깅을 시작하는 방법
12129정성태1/26/202029076.NET Framework: 882. C# - 키움 Open API+ 사용 시 Registry 등록 없이 KHOpenAPI.ocx 사용하는 방법 [3]
12128정성태1/26/202023199오류 유형: 591. The code execution cannot proceed because mfc100.dll was not found. Reinstalling the program may fix this problem.
... 61  62  63  64  65  66  67  68  69  70  71  72  73  74  [75]  ...