Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

(시리즈 글이 24개 있습니다.)
.NET Framework: 1129. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 인코딩 예제(encode_video.c)
; https://www.sysnet.pe.kr/2/0/12898

.NET Framework: 1134. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c)
; https://www.sysnet.pe.kr/2/0/12924

.NET Framework: 1135. C# - ffmpeg(FFmpeg.AutoGen)로 하드웨어 가속기를 이용한 비디오 디코딩 예제(hw_decode.c)
; https://www.sysnet.pe.kr/2/0/12932

.NET Framework: 1136. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 MP2 오디오 파일 디코딩 예제(decode_audio.c)
; https://www.sysnet.pe.kr/2/0/12933

.NET Framework: 1137. ffmpeg의 파일 해시 예제(ffhash.c)를 C#으로 포팅
; https://www.sysnet.pe.kr/2/0/12935

.NET Framework: 1138. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 멀티미디어 파일의 메타데이터를 보여주는 예제(metadata.c)
; https://www.sysnet.pe.kr/2/0/12936

.NET Framework: 1139. C# - ffmpeg(FFmpeg.AutoGen)를 이용해 오디오(mp2) 인코딩하는 예제(encode_audio.c)
; https://www.sysnet.pe.kr/2/0/12937

.NET Framework: 1147. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터링 예제(filtering_audio.c)
; https://www.sysnet.pe.kr/2/0/12951

.NET Framework: 1148. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 오디오 필터 예제(filter_audio.c)
; https://www.sysnet.pe.kr/2/0/12952

.NET Framework: 1150. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/12959

개발 환경 구성: 637. ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) - 세 번째 이야기
; https://www.sysnet.pe.kr/2/0/12960

.NET Framework: 1151. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 프레임의 크기 및 포맷 변경 예제(scaling_video.c)
; https://www.sysnet.pe.kr/2/0/12961

.NET Framework: 1153. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 avio_reading.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/12964

.NET Framework: 1157. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 muxing.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/12971

.NET Framework: 1159. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 qsvdec.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/12975

.NET Framework: 1161. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 resampling_audio.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/12978

.NET Framework: 1166. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 filtering_video.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/12984

.NET Framework: 1169. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 demuxing_decoding.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/12987

.NET Framework: 1170. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 transcode_aac.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/12991

.NET Framework: 1178. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 http_multiclient.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/13002

.NET Framework: 1180. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 remuxing.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/13006

.NET Framework: 1188. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 transcoding.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/13023

.NET Framework: 1190. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 vaapi_encode.c, vaapi_transcode.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/13025

.NET Framework: 1191. C 언어로 작성된 FFmpeg Examples의 C# 포팅 전체 소스 코드
; https://www.sysnet.pe.kr/2/0/13026




C# - ffmpeg(FFmpeg.AutoGen)를 이용한 demuxing_decoding.c 예제 포팅

지난 예제에 이어,

C# - ffmpeg(FFmpeg.AutoGen)를 이용한 filtering_video.c 예제 포팅
; https://www.sysnet.pe.kr/2/0/12984

이번에는 ffmpeg 예제 중 "demuxing_decoding.c" 파일을 FFmpeg.AutoGen으로 포팅하겠습니다.

using FFmpeg.AutoGen;
using FFmpeg.AutoGen.Example;
using System;
using System.IO;

namespace encode_video
{
    internal unsafe class Program
    {
        static AVFormatContext* fmt_ctx = null;
        static AVCodecContext* video_dec_ctx = null;
        static AVCodecContext* audio_dec_ctx = null;
        static int width, height;
        static AVPixelFormat pix_fmt;
        static AVStream* video_stream;
        static AVStream* audio_stream;
        static string src_filename = "";
        static string video_dst_filename = "";
        static string audio_dst_filename = "";
        static FileStream? video_dst_file;
        static FileStream? audio_dst_file;

        static byte_ptrArray4 video_dst_data;
        static int_array4 video_dst_linesize;
        static int video_dst_bufsize;

        static int video_stream_idx = -1;
        static int audio_stream_idx = -1;
        static AVFrame* frame = null;
        static AVPacket* pkt = null;
        static int video_Frame_count = 0;
        static int audio_frame_count = 0;

        static unsafe int output_video_frame(AVFrame* frame)
        {
            if (frame->width != width || frame->height != height || frame->format != (int)pix_fmt)
            {
                Console.WriteLine("Error: Width, height and pixel format have to be constant in an rawvideo file, but the width, height or pixel format of the input video changed:\n" +
                    $"old: width = {width}, height = {height}, format = {ffmpeg.av_get_pix_fmt_name(pix_fmt)}\n" +
                    $"new: width = {frame->width}, height = {frame->height}, format = {ffmpeg.av_get_pix_fmt_name((AVPixelFormat)frame->format)}");
                return -1;
            }

            Console.WriteLine($"video_frame n: {video_Frame_count++} coded: {frame->coded_picture_number}\n");

            byte_ptrArray4 tempData = new byte_ptrArray4();
            tempData.UpdateFrom(frame->data);

            int_array4 tempLinesize = new int_array4();
            tempLinesize.UpdateFrom(frame->linesize);
            ffmpeg.av_image_copy(ref video_dst_data, ref video_dst_linesize, ref tempData, tempLinesize, pix_fmt, width, height);

            video_dst_file?.Write(new ReadOnlySpan<byte>(video_dst_data[0], video_dst_bufsize));
            return 0;
        }

        static int output_audio_frame(AVFrame* frame)
        {
            int unpadded_linesize = frame->nb_samples * ffmpeg.av_get_bytes_per_sample((AVSampleFormat)frame->format);
            Console.WriteLine($"audio_frame n: {audio_frame_count++} nb_samples: {frame->nb_samples} pts: {FFmpegHelper.av_ts2timestr(frame->pts, in audio_dec_ctx->time_base)}");

            audio_dst_file?.Write(new ReadOnlySpan<byte>(frame->extended_data[0], unpadded_linesize));

            return 0;
        }

        static unsafe int decode_packet(AVCodecContext* dec, AVPacket* pkt)
        {
            int ret = ffmpeg.avcodec_send_packet(dec, pkt);
            if (ret < 0)
            {
                Console.WriteLine($"Error submitting a packet for decoding {ret}");
                return ret;
            }

            while (ret >= 0)
            {
                ret = ffmpeg.avcodec_receive_frame(dec, frame);
                if (ret < 0)
                {
                    if (ret == ffmpeg.AVERROR(ffmpeg.AVERROR_EOF) || ret == ffmpeg.AVERROR(ffmpeg.EAGAIN))
                    {
                        return 0;
                    }

                    Console.WriteLine($"Error during decoding ({FFmpegHelper.av_err2str(ret)})");
                    return ret;
                }

                if (dec->codec->type == AVMediaType.AVMEDIA_TYPE_VIDEO)
                {
                    ret = output_video_frame(frame);
                }
                else
                {
                    ret = output_audio_frame(frame);
                }

                ffmpeg.av_frame_unref(frame);

                if (ret < 0)
                {
                    return ret;
                }
            }

            return 0;
        }

        static unsafe int open_codec_context(int* stream_idx, AVCodecContext** dec_ctx, AVFormatContext* fmt_ctx, AVMediaType type)
        {
            int ret, stream_index;
            AVStream* st;
            AVCodec* dec = null;

            ret = ffmpeg.av_find_best_stream(fmt_ctx, type, -1, -1, null, 0);
            if (ret < 0)
            {
                Console.WriteLine($"Could not find {ffmpeg.av_get_media_type_string(type)} stream in input file '{src_filename}'");
                return ret;
            }
            else
            {
                stream_index = ret;
                st = fmt_ctx->streams[stream_index];

                dec = ffmpeg.avcodec_find_decoder(st->codecpar->codec_id);
                if (dec == null)
                {
                    Console.WriteLine($"Failed to find {ffmpeg.av_get_media_type_string(type)} codec");
                    return ffmpeg.AVERROR(ffmpeg.EINVAL);
                }

                *dec_ctx = ffmpeg.avcodec_alloc_context3(dec);
                if (*dec_ctx == null)
                {
                    Console.WriteLine($"Failed to allocate the {ffmpeg.av_get_media_type_string(type)} codec context");
                    return ffmpeg.AVERROR(ffmpeg.ENOMEM);
                }

                if ((ret = ffmpeg.avcodec_parameters_to_context(*dec_ctx, st->codecpar)) < 0)
                {
                    Console.WriteLine($"Failed to copy {ffmpeg.av_get_media_type_string(type)} codec parameters to decoder context");
                    return ret;
                }

                if ((ret = ffmpeg.avcodec_open2(*dec_ctx, dec, null)) < 0)
                {
                    Console.WriteLine($"Failed to open {ffmpeg.av_get_media_type_string(type)} codec");
                    return ret;
                }

                *stream_idx = stream_index;
            }

            return 0;
        }

        static unsafe int Main(string[] args)
        {
            FFmpegBinariesHelper.RegisterFFmpegBinaries();

            int ret = 0;

            // https://samplelib.com/sample-mp4.html
            string dirPath = Path.GetDirectoryName(typeof(Program).Assembly.Location) ?? "";
            src_filename = Path.Combine(dirPath, "..", "..", "..", "Samples", "sample-10s.mp4");

            video_dst_filename = Path.Combine(dirPath, "video.out");
            audio_dst_filename = Path.Combine(dirPath, "audio.out");

            fixed (AVFormatContext** pfmt_ctx = &fmt_ctx)
            fixed (AVCodecContext** pvideo_dec_ctx = &video_dec_ctx)
            fixed (AVCodecContext** paudio_dec_ctx = &audio_dec_ctx)
            fixed (int* pvideo_stream_index = &video_stream_idx)
            fixed (int* paudio_stream_index = &audio_stream_idx)
            fixed (AVPacket** ppkt = &pkt)
            fixed (AVFrame** pframe = &frame)
            {
                if (ffmpeg.avformat_open_input(pfmt_ctx, src_filename, null, null) < 0)
                {
                    Console.WriteLine($"Could not open source file {src_filename}");
                    return 1;
                }

                if (ffmpeg.avformat_find_stream_info(fmt_ctx, null) < 0)
                {
                    Console.WriteLine($"Could not find stream information");
                    return 1;
                }

                if (open_codec_context(pvideo_stream_index, pvideo_dec_ctx, fmt_ctx, AVMediaType.AVMEDIA_TYPE_VIDEO) >= 0)
                {
                    video_stream = fmt_ctx->streams[video_stream_idx];

                    video_dst_file = File.OpenWrite(video_dst_filename);
                    if (video_dst_file == null)
                    {
                        Console.WriteLine($"Could not open destination file {video_dst_filename}");
                        ret = 1;
                        goto end;
                    }

                    width = video_dec_ctx->width;
                    height = video_dec_ctx->height;
                    pix_fmt = video_dec_ctx->pix_fmt;
                    ret = ffmpeg.av_image_alloc(ref video_dst_data, ref video_dst_linesize, width, height, pix_fmt, 1);
                    if (ret < 0)
                    {
                        Console.WriteLine("Could not allocate raw video buffer");
                        goto end;
                    }

                    video_dst_bufsize = ret;
                }

                if (open_codec_context(paudio_stream_index, paudio_dec_ctx, fmt_ctx, AVMediaType.AVMEDIA_TYPE_AUDIO) >= 0)
                {
                    audio_stream = fmt_ctx->streams[audio_stream_idx];
                    audio_dst_file = File.OpenWrite(audio_dst_filename);
                    if (audio_dst_file == null)
                    {
                        Console.WriteLine($"Could not open destination file {audio_dst_filename}");
                        ret = 1;
                        goto end;
                    }
                }

                ffmpeg.av_dump_format(fmt_ctx, 0, src_filename, 0);

                if (audio_stream == null && video_stream == null)
                {
                    Console.WriteLine("Could not find audio or video stream in the input, aborting");
                    ret = 1;
                    goto end;
                }

                frame = ffmpeg.av_frame_alloc();
                if (frame == null)
                {
                    Console.WriteLine("Could not allocate frame");
                    ret = ffmpeg.AVERROR(ffmpeg.ENOMEM);
                    goto end;
                }

                pkt = ffmpeg.av_packet_alloc();
                if (pkt == null)
                {
                    Console.WriteLine("Could not allocate packet");
                    ret = ffmpeg.AVERROR(ffmpeg.ENOMEM);
                    goto end;
                }

                if (video_stream != null)
                {
                    Console.WriteLine($"Demuxing video from file '{src_filename}' into '{video_dst_filename}'");
                }

                if (audio_stream != null)
                {
                    Console.WriteLine($"Demuxing audio from file '{src_filename}' into '{audio_dst_filename}'");
                }

                while (ffmpeg.av_read_frame(fmt_ctx, pkt) >= 0)
                {
                    if (pkt->stream_index == video_stream_idx)
                    {
                        ret = decode_packet(video_dec_ctx, pkt);
                    }
                    else if (pkt->stream_index == audio_stream_idx)
                    {
                        ret = decode_packet(audio_dec_ctx, pkt);
                    }

                    ffmpeg.av_packet_unref(pkt);
                    if (ret < 0)
                    {
                        break;
                    }
                }

                if (video_dec_ctx != null)
                {
                    decode_packet(video_dec_ctx, null);
                }

                if (audio_dec_ctx != null)
                {
                    decode_packet(audio_dec_ctx, null);
                }

                Console.WriteLine("Demuxing succeeded.");

                if (video_stream != null)
                {
                    Console.WriteLine("Play the output video file with the command: \n" +
                        $"ffplay -autoexit -f rawvideo -pix_fmt {ffmpeg.av_get_pix_fmt_name(pix_fmt)} -video_size {width}x{height} {video_dst_filename}");
                }

                if (audio_stream != null)
                {
                    AVSampleFormat sfmt = audio_dec_ctx->sample_fmt;
                    int n_channels = audio_dec_ctx->channels;
                    string fmt;

                    if (ffmpeg.av_sample_fmt_is_planar(sfmt) == 1)
                    {
                        string packed = ffmpeg.av_get_sample_fmt_name(sfmt);
                        Console.WriteLine("Warning: the sample format the decoder produced is planar " +
                            $"({(packed != null ? packed : "?")}). This example will output the first channel only.");
                        sfmt = ffmpeg.av_get_packed_sample_fmt(sfmt);
                        n_channels = 1;
                    }

                    if ((ret = FFmpegHelper.get_format_from_sample_fmt(out fmt, sfmt)) < 0)
                    {
                        goto end;
                    }

                    Console.WriteLine("Play the output audio file with the command:\n" +
                        $"ffplay -autoexit -f {fmt} -ac {n_channels} -ar {audio_dec_ctx->sample_rate} {audio_dst_filename}");
                }

            end:

                ffmpeg.avcodec_free_context(pvideo_dec_ctx);
                ffmpeg.avcodec_free_context(paudio_dec_ctx);
                ffmpeg.avformat_close_input(pfmt_ctx);

                if (video_dst_file != null)
                {
                    video_dst_file.Close();
                }

                if (audio_dst_file != null)
                {
                    audio_dst_file.Close();
                }

                ffmpeg.av_packet_free(ppkt);
                ffmpeg.av_frame_free(pframe);
                ffmpeg.av_free(video_dst_data[0]);

            }

            return 0;
        }
    }
}

실행하면,

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '...[생략]...\demuxing_decoding\bin\Debug\..\..\..\Samples\sample-10s.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf58.44.100
  Duration: 00:00:10.24, start: 0.000000, bitrate: 4285 kb/s
  Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 4207 kb/s, 29.97 fps, 29.97 tbr, 30k tbn (default)
    Metadata:
      handler_name    : VideoHandler
      vendor_id       : [0][0][0][0]
  Stream #0:1[0x2](eng): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 121 kb/s (default)
    Metadata:
      handler_name    : SoundHandler
      vendor_id       : [0][0][0][0]
Demuxing video from file 'E:\git_clone\ffmpeg_autogen_cs\ffmpeg_autogen_cs\demuxing_decoding\bin\Debug\..\..\..\Samples\sample-10s.mp4' into '...[생략]...\demuxing_decoding\bin\Debug\video.out'
Demuxing audio from file 'E:\git_clone\ffmpeg_autogen_cs\ffmpeg_autogen_cs\demuxing_decoding\bin\Debug\..\..\..\Samples\sample-10s.mp4' into '...[생략]...\demuxing_decoding\bin\Debug\audio.out'
video_frame n: 0 coded: 0

audio_frame n: 0 nb_samples: 1024 pts: 0
audio_frame n: 1 nb_samples: 1024 pts: 0.023219954648526078
video_frame n: 1 coded: 3

audio_frame n: 2 nb_samples: 1024 pts: 0.046439909297052155
video_frame n: 2 coded: 2

...[생략]...
audio_frame n: 437 nb_samples: 1024 pts: 10.147120181405896
audio_frame n: 438 nb_samples: 1024 pts: 10.170340136054422
audio_frame n: 439 nb_samples: 1024 pts: 10.193560090702949
video_frame n: 301 coded: 302

video_frame n: 302 coded: 299

Error during decoding (End of file)
Error during decoding (End of file)
Demuxing succeeded.
Play the output video file with the command:
ffplay -autoexit -f rawvideo -pix_fmt yuv420p -video_size 1920x1080 ...[생략]...\demuxing_decoding\bin\Debug\video.out
Warning: the sample format the decoder produced is planar (fltp). This example will output the first channel only.
Play the output audio file with the command:
ffplay -autoexit -f f32le -ac 1 -ar 44100 ...[생략]...\demuxing_decoding\bin\Debug\audio.out

입력 파일의 동영상 파일을 각각 영상과 음성 파일로 분리해 별도의 파일로 저장합니다. 그리고 마지막 콘솔 출력 부분을 보면, 해당 파일들을 ffplay로 어떻게 재생할 수 있는지 명령행 옵션과 함께 보여줍니다.

(이 글의 소스 코드는 github에 올려져 있습니다.)




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







[최초 등록일: ]
[최종 수정일: 3/1/2022]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




1  2  3  4  5  6  7  8  9  10  [11]  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13355정성태5/12/20233817.NET Framework: 2123. C# - Semantic Kernel의 ChatGPT 대화 구현 [1]파일 다운로드1
13354정성태5/12/20234074.NET Framework: 2122. C# - "Use Unicode UTF-8 for worldwide language support" 설정을 한 경우, 한글 입력이 '\0' 문자로 처리
13352정성태5/12/20233686.NET Framework: 2121. C# - Semantic Kernel의 대화 문맥 유지파일 다운로드1
13351정성태5/11/20234174VS.NET IDE: 185. Visual Studio - 원격 Docker container 내에 실행 중인 응용 프로그램에 대한 디버깅 [1]
13350정성태5/11/20233463오류 유형: 859. Windows Date and Time - Unable to continue. You do not have permission to perform this task
13349정성태5/11/20233771.NET Framework: 2120. C# - Semantic Kernel의 Skill과 Function 사용 예제파일 다운로드1
13348정성태5/10/20233674.NET Framework: 2119. C# - Semantic Kernel의 "Basic Loading of the Kernel" 예제
13347정성태5/10/20234052.NET Framework: 2118. C# - Semantic Kernel의 Prompt chaining 예제파일 다운로드1
13346정성태5/10/20233892오류 유형: 858. RDP 원격 환경과 로컬 PC 간의 Ctrl+C, Ctrl+V 복사가 안 되는 문제
13345정성태5/9/20235268.NET Framework: 2117. C# - (OpenAI 기반의) Microsoft Semantic Kernel을 이용한 자연어 처리 [1]파일 다운로드1
13344정성태5/9/20236475.NET Framework: 2116. C# - OpenAI API 사용 - 지원 모델 목록 [1]파일 다운로드1
13343정성태5/9/20234332디버깅 기술: 192. Windbg - Hyper-V VM으로 이더넷 원격 디버깅 연결하는 방법
13342정성태5/8/20234250.NET Framework: 2115. System.Text.Json의 역직렬화 시 필드/속성 주의
13341정성태5/8/20233999닷넷: 2114. C# 12 - 모든 형식의 별칭(Using aliases for any type)
13340정성태5/8/20234071오류 유형: 857. Microsoft.Data.SqlClient.SqlException - 0x80131904
13339정성태5/6/20234730닷넷: 2113. C# 12 - 기본 생성자(Primary Constructors)
13338정성태5/6/20234242닷넷: 2112. C# 12 - 기본 람다 매개 변수파일 다운로드1
13337정성태5/5/20234751Linux: 59. dockerfile - docker exec로 container에 접속 시 자동으로 실행되는 코드 적용
13336정성태5/4/20234557.NET Framework: 2111. C# - 바이너리 출력 디렉터리와 연관된 csproj 설정
13335정성태4/30/20234662.NET Framework: 2110. C# - FFmpeg.AutoGen 라이브러리를 이용한 기본 프로젝트 구성 - Windows Forms파일 다운로드1
13334정성태4/29/20234288Windows: 250. Win32 C/C++ - Modal 메시지 루프 내에서 SetWindowsHookEx를 이용한 Thread 메시지 처리 방법
13333정성태4/28/20233739Windows: 249. Win32 C/C++ - 대화창 템플릿을 런타임에 코딩해서 사용파일 다운로드1
13332정성태4/27/20233838Windows: 248. Win32 C/C++ - 대화창을 위한 메시지 루프 사용자 정의파일 다운로드1
13331정성태4/27/20233846오류 유형: 856. dockerfile - 구 버전의 .NET Core 이미지 사용 시 apt update 오류
13330정성태4/26/20233483Windows: 247. Win32 C/C++ - CS_GLOBALCLASS 설명
13329정성태4/24/20233706Windows: 246. Win32 C/C++ - 직접 띄운 대화창 템플릿을 위한 Modal 메시지 루프 생성파일 다운로드1
1  2  3  4  5  6  7  8  9  10  [11]  12  13  14  15  ...