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

... [151]  152  153  154  155  156  157  158  159  160  161  162  163  164  165  ...
NoWriterDateCnt.TitleFile(s)
1277정성태5/8/201231235오류 유형: 152. cmd.exe - The system cannot write to the specified device. [2]
1276정성태4/28/201223095Phone: 5. 모든 Marketplace에 윈폰 앱을 등록하는 방법 [1]
1275정성태4/28/201226941개발 환경 구성: 150. 프로세스 실행으로 잠긴 파일이지만, 이름은 변경가능하다는 사실! 아셨나요? [7]
1274정성태4/17/201221484Phone: 4. "Holiday Calendar" 윈폰 응용 프로그램 등록
1273정성태4/6/201224696Phone: 3. 윈도우 폰을 위한 Holiyday Calendar 앱 개발파일 다운로드1
1272정성태4/5/201226255오류 유형: 151. ASP.NET - EcbGetUnicodeServerVariables 코드에서 System.AccessViolationException 예외 발생
1271정성태4/3/201228936Math: 6. 동전을 여러 더미로 나누는 경우의 수 세기 [1]
1270정성태3/29/201222868오류 유형: 150. Visual Studio 2010 원격 디버깅 오류 - Kerberos authentication failed
1269정성태3/27/201236702오류 유형: 149. ODP.NET 오류 - The provider is not compatible with the version of Oracle client
1268정성태3/27/201233209오류 유형: 148. WCF svc 호출 시 HTTP Error 404.17 - Not Found [1]
1267정성태3/16/201231172.NET Framework: 314. C++의 inline asm 사용을 .NET으로 포팅하는 방법 [1]파일 다운로드1
1266정성태3/14/201234396개발 환경 구성: 149. RAID 1 구성 시 하드 디스크 장애 발생 해결에 대한 경험담
1265정성태3/13/201224696VC++: 61. 아이태니엄(IA64: Itanium) 에서 겪은 C++ 포인터 연산 문제 [2]
1264정성태3/10/201244102.NET Framework: 313. WELL512 난수 발생 알고리즘 - C# [5]파일 다운로드1
1263정성태3/9/201222898개발 환경 구성: 148. tinyget 사용법
1262정성태3/8/201243763개발 환경 구성: 147. .keystore 파일에 저장된 개인키 추출 방법과 인증기관으로부터 온 공개키를 합친 pfx 파일 만드는 방법 [1]
1261정성태3/7/201224472Phone: 2. 개발자용 윈도우 폰 7 기기 등록하는 방법
1260정성태3/6/201224310Phone: 1. 윈도폰 7 개발자 (회사) 등록하는 방법 [3]
1259정성태3/4/201235814Windows: 57. 새로 추가된 네트워크 커널 디버깅 및 PowerShell 3.0 [1]
1258정성태3/3/201237440개발 환경 구성: 146. SQL Server 2012에 포함된 LocalDB 기능 소개 [3]파일 다운로드1
1257정성태3/3/201225640.NET Framework: 312. Native 스레드와 Managed 스레드 개체의 상태 관계 [1]파일 다운로드1
1256정성태3/3/201229207Math: 5. Euler's totient function - C#파일 다운로드1
1255정성태3/3/201231569Math: 4. 소수 판정 및 소인수 분해 소스 코드 - C# [1]파일 다운로드1
1254정성태3/1/201226534Windows: 56. Windows 8 Consumer Preview를 사용해 보고... [1]
1253정성태3/1/201227998VS.NET IDE: 71. Visual Studio 11 Ultimate 베타 설치 [3]
1252정성태3/1/201225347Windows: 55. 윈도우 8 베타 설치 과정 [1]
... [151]  152  153  154  155  156  157  158  159  160  161  162  163  164  165  ...