Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen) - decoding 과정 [링크 복사], [링크+제목 복사],
조회: 7176
글쓴 사람
정성태 (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분
@이승준 님 감사합니다. 본문을 수정했고 실제로 테스트해 보니 원하는 대로 결과가 나왔습니다. ^^
정성태

... 31  32  33  [34]  35  36  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12796정성태8/21/20218657.NET Framework: 1099. C# 10 - (4) 상수 문자열에 포맷 식 사용 가능파일 다운로드1
12795정성태8/20/20219322.NET Framework: 1098. .NET 6에 포함된 신규 BCL API - 스레드 관련
12794정성태8/20/20218771스크립트: 23. 파이썬 - WSGI를 만족하는 최소한의 구현 코드 및 PyCharm에서의 디버깅 방법 [1]
12793정성태8/20/20219457.NET Framework: 1097. C# 10 - (3) 개선된 변수 초기화 판정파일 다운로드1
12792정성태8/19/20219895.NET Framework: 1096. C# 10 - (2) 전역 네임스페이스 선언파일 다운로드1
12791정성태8/19/20218234.NET Framework: 1095. C# COM 개체를 C++에서 사용하는 예제 [3]파일 다운로드1
12790정성태8/18/202110500.NET Framework: 1094. C# 10 - (1) 구조체를 생성하는 record struct파일 다운로드1
12789정성태8/18/20219622개발 환경 구성: 597. PyCharm - 윈도우 환경에서 WSL을 이용해 파이썬 앱 개발/디버깅하는 방법
12788정성태8/17/20218063.NET Framework: 1093. C# - 인터페이스의 메서드가 다형성을 제공할까요? (virtual일까요?)파일 다운로드1
12787정성태8/17/20218286.NET Framework: 1092. (책 내용 수정) "4.5.1.4 인터페이스"의 "인터페이스와 다형성"
12786정성태8/16/20219864.NET Framework: 1091. C# - Python range 함수 구현 (2) INumber<T>를 이용한 개선 [1]파일 다운로드1
12785정성태8/16/20218139.NET Framework: 1090. .NET 6 Preview 7에 추가된 숫자 형식에 대한 제네릭 연산 지원 [1]파일 다운로드1
12784정성태8/15/20217578오류 유형: 757. 구글 메일 - 아웃룩에서 메일 전송 시 Sending' reported error (0x800CCC0F, 0x800CCC92)
12783정성태8/15/20217059.NET Framework: 1089. C# - Indexer에 Range 및 람다 식을 이용한 필터 구현 [1]파일 다운로드1
12782정성태8/14/20216864오류 유형: 756. 파이썬 - 윈도우 환경에서 pytagcloud의 한글 출력 방법
12781정성태8/14/20219087오류 유형: 755. 파이썬 - konlpy 사용 시 JVM과 jpype1 관련 오류
12780정성태8/13/20217448.NET Framework: 1088. C# - 버스 노선 및 위치 정보 조회 API 사용을 위한 기초 라이브러리 [2]
12779정성태8/13/20219310개발 환경 구성: 596. 공공 데이터 포털에서 버스 노선 및 위치 정보 조회 API 사용법
12778정성태8/12/20216517오류 유형: 755. PyCharm - "Manage Repositories"의 목록이 나오지 않는 문제
12777정성태8/12/20218086오류 유형: 754. Visual Studio - Input or output cannot be redirected because the specified file is invalid.
12776정성태8/12/20217413오류 유형: 753. gunicorn과 uwsgi 함께 사용 시 ERR_CONNECTION_REFUSED
12775정성태8/12/202118711스크립트: 22. 파이썬 - 윈도우 환경에서 개발한 Django 앱을 WSL 환경의 gunicorn을 이용해 실행
12774정성태8/11/20219115.NET Framework: 1087. C# - Collection 개체의 다중 스레드 접근 시 "Operations that change non-concurrent collections must have exclusive access" 예외 발생
12773정성태8/11/20218303개발 환경 구성: 595. PyCharm - WSL과 연동해 Django App을 윈도우에서 리눅스 대상으로 개발
12772정성태8/11/20219741스크립트: 21. 파이썬 - 윈도우 환경에서 개발한 Django 앱을 WSL 환경의 uwsgi를 이용해 실행 [1]
12771정성태8/11/20218202Windows: 196. "Microsoft Windows Subsystem for Linux Background Host" / "Vmmem"을 종료하는 방법
... 31  32  33  [34]  35  36  37  38  39  40  41  42  43  44  45  ...