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

... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1756정성태9/23/201427486기타: 48. NVidia 제품의 과다한 디스크 사용 [2]
1755정성태9/22/201434280오류 유형: 241. Unity Web Player를 설치해도 여전히 설치하라는 화면이 나오는 경우 [4]
1754정성태9/22/201424657VC++: 80. 내 컴퓨터에서 C++ AMP 코드가 실행이 될까요? [1]
1753정성태9/22/201420611오류 유형: 240. Lync로 세미나 참여 시 소리만 들리지 않는 경우 [1]
1752정성태9/21/201441071Windows: 100. 윈도우 8 - RDP 연결을 이용해 VNC처럼 사용자 로그온 화면을 공유하는 방법 [5]
1751정성태9/20/201438950.NET Framework: 464. 프로세스 간 통신 시 소켓 필요 없이 간단하게 Pipe를 열어 통신하는 방법 [1]파일 다운로드1
1750정성태9/20/201423832.NET Framework: 463. PInvoke 호출을 이용한 비동기 파일 작업파일 다운로드1
1749정성태9/20/201423732.NET Framework: 462. 커널 객체를 위한 null DACL 생성 방법파일 다운로드1
1748정성태9/19/201425385개발 환경 구성: 238. [Synergy] 여러 컴퓨터에서 키보드, 마우스 공유
1747정성태9/19/201428486오류 유형: 239. psexec 실행 오류 - The system cannot find the file specified.
1746정성태9/18/201426106.NET Framework: 461. .NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요? - 두 번째 이야기 [6]파일 다운로드1
1745정성태9/17/201423037개발 환경 구성: 237. 리눅스 Integration Services 버전 업그레이드 하는 방법 [1]
1744정성태9/17/201431064.NET Framework: 460. GetTickCount / GetTickCount64와 0x7FFE0000 주솟값 [4]파일 다운로드1
1743정성태9/16/201420985오류 유형: 238. 설치 오류 - Failed to get size of pseudo bundle
1742정성태8/27/201426973개발 환경 구성: 236. Hyper-V에 설치한 리눅스 VM의 VHD 크기 늘리는 방법 [2]
1741정성태8/26/201421334.NET Framework: 459. GetModuleHandleEx로 알아보는 .NET 메서드의 DLL 모듈 관계파일 다운로드1
1740정성태8/25/201432522.NET Framework: 458. 닷넷 GC가 순환 참조를 해제할 수 있을까요? [2]파일 다운로드1
1739정성태8/24/201426551.NET Framework: 457. 교착상태(Dead-lock) 해결 방법 - Lock Leveling [2]파일 다운로드1
1738정성태8/23/201422049.NET Framework: 456. C# - CAS를 이용한 Lock 래퍼 클래스파일 다운로드1
1737정성태8/20/201419765VS.NET IDE: 93. Visual Studio 2013 동기화 문제
1736정성태8/19/201425579VC++: 79. [부연] CAS Lock 알고리즘은 과연 빠른가? [2]파일 다운로드1
1735정성태8/19/201418222.NET Framework: 455. 닷넷 사용자 정의 예외 클래스의 최소 구현 코드 - 두 번째 이야기
1734정성태8/13/201419879오류 유형: 237. Windows Media Player cannot access the file. The file might be in use, you might not have access to the computer where the file is stored, or your proxy settings might not be correct.
1733정성태8/13/201426363.NET Framework: 454. EmptyWorkingSet Win32 API를 사용하는 C# 예제파일 다운로드1
1732정성태8/13/201434480Windows: 99. INetCache 폴더가 다르게 보이는 이유
1731정성태8/11/201427084개발 환경 구성: 235. 점(.)으로 시작하는 파일명을 탐색기에서 만드는 방법
... 121  122  123  124  125  126  127  128  129  130  131  [132]  133  134  135  ...