Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen) - decoding 과정 [링크 복사], [링크+제목 복사]
조회: 997
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

C# - ffmpeg(FFmpeg.AutoGen) - decoding 과정

지난 글에서 동영상 파일의 스트림에 대한 decoder까지 열었으니,

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

이제 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




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 2/7/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만 설정 합니다.
제 경우는 전부 버리게 설정한 후 원하는 스트림만 따로 열어줍니다.
[손님]
2022-02-07 10시39분
@이승준 님 감사합니다. 본문을 수정했고 실제로 테스트해 보니 원하는 대로 결과가 나왔습니다. ^^
정성태

1  2  3  4  5  [6]  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12991정성태3/2/2022971.NET Framework: 1170. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 transcode_aac.c 예제 포팅
12990정성태3/2/2022980오류 유형: 797. msbuild - The BaseOutputPath/OutputPath property is not set for project '[...].vcxproj'
12989정성태3/2/2022947오류 유형: 796. mstest.exe - System.IO.FileNotFoundException: Could not load file or assembly 'Microsoft.VisualStudio.QualityTools.Tips.WebLoadTest.Tip
12988정성태3/2/2022783오류 유형: 795. CI 환경에서 Docker build 시 csproj의 Link 파일에 대한 빌드 오류
12987정성태3/1/2022946.NET Framework: 1169. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 demuxing_decoding.c 예제 포팅
12986정성태2/28/2022961.NET Framework: 1168. C# -IIncrementalGenerator를 적용한 Version 2 Source Generator 실습
12985정성태2/28/20221037.NET Framework: 1167. C# -Version 1 Source Generator 실습
12984정성태2/24/2022900.NET Framework: 1166. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 filtering_video.c 예제 포팅
12983정성태2/24/2022885.NET Framework: 1165. .NET Core/5+ 빌드 시 runtimeconfig.json에 설정을 반영하는 방법
12982정성태2/24/2022863.NET Framework: 1164. HTTP Error 500.31 - ANCM Failed to Find Native Dependencies
12981정성태2/23/2022915VC++: 154. C/C++ 언어의 문자열 Literal에 인덱스 적용하는 구문 [1]
12980정성태2/23/20221141.NET Framework: 1163. C# - 윈도우 환경에서 usleep을 호출하는 방법 [2]
12979정성태2/22/20221394.NET Framework: 1162. C# - 인텔 CPU의 P-Core와 E-Core를 구분하는 방법파일 다운로드1
12978정성태2/21/2022876.NET Framework: 1161. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 resampling_audio.c 예제 포팅
12977정성태2/21/20222161.NET Framework: 1160. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 qsv 디코딩
12976정성태2/21/20221324VS.NET IDE: 174. Visual C++ - "External Dependencies" 노드 비활성화하는 방법
12975정성태2/20/20221203.NET Framework: 1159. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 qsvdec.c 예제 포팅파일 다운로드1
12974정성태2/20/2022878.NET Framework: 1158. C# - SqlConnection의 최소 Pooling 수를 초과한 DB 연결은 언제 해제될까요?
12973정성태2/16/20221114개발 환경 구성: 639. ffmpeg.exe - Intel Quick Sync Video(qsv)를 이용한 인코딩 [3]
12972정성태2/16/2022932Windows: 200. Intel CPU의 내장 그래픽 GPU가 작업 관리자에 없다면? [4]
12971정성태2/15/20221866.NET Framework: 1157. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 muxing.c 예제 포팅 [7]파일 다운로드1
12970정성태2/15/20221086.NET Framework: 1156. C# - ffmpeg(FFmpeg.AutoGen): Bitmap으로부터 h264 형식의 파일로 쓰기 [1]파일 다운로드1
12969정성태2/14/2022896개발 환경 구성: 638. Visual Studio의 Connection Manager 기능(Remote SSH 관리)을 위한 명령행 도구 - 두 번째 이야기파일 다운로드1
12968정성태2/14/2022661오류 유형: 794. msbuild 에러 - error NETSDK1005: Assets file '...\project.assets.json' doesn't have a target for '...'.
12967정성태2/14/2022873VC++: 153. Visual C++ - C99 표준의 Compund Literals 빌드 방법 [4]
12966정성태2/13/2022882.NET Framework: 1155. C# - ffmpeg(FFmpeg.AutoGen): Bitmap으로부터 yuv420p + rawvideo 형식의 파일로 쓰기파일 다운로드1
1  2  3  4  5  [6]  7  8  9  10  11  12  13  14  15  ...