성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 그냥 RSS Reader 기능과 약간의 UI 편의성 때문에 사용...
[이종효] 오래된 소프트웨어는 보안 위협이 되기도 합니다. 혹시 어떤 기능...
[정성태] @Keystroke IEEE의 문서를 소개해 주시다니... +_...
[손민수 (Keystroke)] 괜히 듀얼채널 구성할 때 한번에 같은 제품 사라고 하는 것이 아...
[정성태] 전각(Full-width)/반각(Half-width) 기능을 토...
[정성태] Vector에 대한 내용은 없습니다. Vector가 닷넷 BCL...
[orion] 글 읽고 찾아보니 디자인 타임에는 InitializeCompon...
[orion] 연휴 전에 재현 프로젝트 올리자 생각해 놓고 여의치 않아서 못 ...
[정성태] 아래의 글에 정리했으니 참고하세요. C# - Typed D...
[정성태] 간단한 재현 프로젝트라도 있을까요? 저런 식으로 설명만 해...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>C# - ffmpeg(FFmpeg.AutoGen) - decoding 과정</h1> <p> 지난 글에서 동영상 파일의 스트림에 대한 decoder까지 열었으니,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - ffmpeg(FFmpeg.AutoGen) - Codec 정보 열람 및 사용 준비 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12949'>https://www.sysnet.pe.kr/2/0/12949</a> C 언어로 작성된 FFmpeg Examples의 C# 포팅 전체 소스 코드 ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/13026'>https://www.sysnet.pe.kr/2/0/13026</a> </pre> <br /> 이제 decoder를 이용해 영상을 재생하든, 오디오를 재생하든 할 수 있습니다. 지금까지 배운 단계로 보면, 동영상을 재생한다고 할 때 보통은 Video와 Audio를 재생할 것이므로 다음과 같은 식으로 뼈대 코드를 작성할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 - 기존 동영상 컨테이너에 다중 스트림을 추가하는 방법 // <a target='tab' href='https://www.sysnet.pe.kr/2/0/12947'>https://www.sysnet.pe.kr/2/0/12947</a> 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); // <a target='tab' href='https://www.sysnet.pe.kr/2/0/12951#get_decoder'>https://www.sysnet.pe.kr/2/0/12951#get_decoder</a> 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 정보 열람 및 사용 준비 // <a target='tab' href='https://www.sysnet.pe.kr/2/0/12949'>https://www.sysnet.pe.kr/2/0/12949</a> { 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); } } } </pre> <br /> 이제 남은 것은 디코딩 부분인데요, 이게 제가 예상했던 것과는 좀 다릅니다. 우선, packet과 frame이라는 용어가 나오는데요, packet은 압축되어 있는 상태의 데이터를 가리키며 그것을 코덱으로 압축을 풀어 나오는 것이 frame이라고 합니다.<br /> <br /> 일단, packet은 av_read_frame 함수로 읽고 av_packet_unref로 정리할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // 패킷 및 프레임 처리 AVPacket* packet = ffmpeg.<span style='color: blue; font-weight: bold'>av_packet_alloc</span>(); videoFrame = ffmpeg.av_frame_alloc(); audioFrame = ffmpeg.av_frame_alloc(); // 이름은 frame이지만, packet을 읽어오는! while (ffmpeg.<span style='color: blue; font-weight: bold'>av_read_frame(av_context, packet)</span> == 0) { Console.Write(packet->stream_index); <span style='color: blue; font-weight: bold'>ffmpeg.av_packet_unref(packet);</span> } if (packet != null) { ffmpeg.<span style='color: blue; font-weight: bold'>av_packet_free</span>(&packet); } </pre> <br /> 그런데, 저렇게 packet을 읽어오는 코드를 <a target='tab' href='https://www.sysnet.pe.kr/2/0/12947'>다중 스트림이 있는 동영상에 적용해 보면</a>, 루프에서 stream_index를 출력했을 때 원래라면 0과 3의 스트림만 출력되는 것을 기대했었는데요,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 기대하는 출력: 0303000303030303030303030333030303030303003 </pre> <br /> 그런데 실제로는 다음과 같은 결과를 얻을 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 021230121302132101230123....[생략]...1213201230121302123102130 </pre> <br /> 그러니까, 동영상 파일을 읽어올 때 특정 Stream의 데이터만 읽어오는 것이 아니라 전체 데이터에 대해 개별 스트림의 데이터를 처음부터 읽어서 반환해 주는 것입니다. <strike>참 희한한 방식입니다. ^^;</strike><br /> <br /> 지금 와서 보니까 과거의 글에서,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > C# - ffmpeg(FFmpeg.AutoGen)로 하드웨어 가속기를 이용한 비디오 디코딩 예제(hw_decode.c) ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12932'>https://www.sysnet.pe.kr/2/0/12932</a> </pre> <br /> 봤던 while 루프에서의 stream_index 비교 코드가 눈에 들어옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > while (ret >= 0) { if ((ret = ffmpeg.av_read_frame(input_ctx, packet)) < 0) { break; } if (<span style='color: blue; font-weight: bold'>video_stream == packet->stream_index</span>) { ret = decode_write(decoder_ctx, packet, output_file); } } </pre> <br /> 그러니까, 개별 av_read_frame으로 읽은 packet마다 그것이 속한 stream에 해당하는 디코딩을 한 것입니다. 따라서, while 루프를 이번에는 비디오와 오디오를 처리해야 하므로 2개로 나눌 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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); } </pre> <br /> 그리고 packet 처리하는 함수 내부가 재미있는데요, 이전의 av_read_frame은 압축 데이터를 담은 packet을 반환했고, CodecContext를 이용해 디코딩을 avcodec_send_packet으로 한 후, 압축 해제된 프레임 데이터를 avcodec_receive_frame 호출로 받아옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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; } } </pre> <br /> 위에서는 packet 하나와 frame 하나가 대응하는 것으로 가정하는데요, 예전에 포팅했던 예제 코드들은 모두 packet 하나에 frame이 여러 개 있는 것처럼 디코딩을 하고 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) // ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12924'>https://www.sysnet.pe.kr/2/0/12924</a> int ret = ffmpeg.<span style='color: blue; font-weight: bold'>avcodec_send_packet</span>(pCodecContext, pkt); if (ret < 0) { Console.WriteLine("Error sending a packet for decoding"); return false; } <span style='color: blue; font-weight: bold'>while (ret >= 0)</span> { ret = ffmpeg.<span style='color: blue; font-weight: bold'>avcodec_receive_frame</span>(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; } ...[생략]... } </pre> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > // C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP2 오디오 파일 디코딩 예제(decode_audio.c) // ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/12933'>https://www.sysnet.pe.kr/2/0/12933</a> ret = ffmpeg.<span style='color: blue; font-weight: bold'>avcodec_send_packet</span>(dec_ctx, packet); if (ret < 0) { Console.WriteLine("Error submitting the packet to the decoder"); return false; } <span style='color: blue; font-weight: bold'>while (ret >= 0)</span> { ret = ffmpeg.<span style='color: blue; font-weight: bold'>avcodec_receive_frame</span>(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; } ...[생략]... } </pre> <br /> 검색해 보면, packet:frame이 1:1, N:1, 1:N일 수도 있으므로 저렇게 처리하는 것이 맞습니다. 따라서 decodePacket을 대략 다음과 같은 식으로 구성할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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; } } } </pre> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1896&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> <br /> <hr style='width: 50%' /><br /> <br /> (2022-02-07 업데이트) 이승준님의 덧글에 따라 다음과 같이 discard 설정을 하는 경우,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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; } } </pre> <br /> 이후 실행했을 때, 초기에 1, 3번 인덱스가 한 번 나온 것을 제외하고는 이후 0, 2번 스트림만 읽혀졌습니다. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 02123020220202....[생략]...2202020220202202022020202202022020220202202020220202202022 </pre> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1779
(왼쪽의 숫자를 입력해야 합니다.)