Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 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)로 하드웨어 가속기를 이용한 비디오 디코딩 예제(hw_decode.c)

지난 글에 이어,

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

이번에는 하드웨어 가속기를 이용한 디코딩 예제를 변환해 보겠습니다.

hw_decode.c
; https://ffmpeg.org/doxygen/trunk/hw_decode_8c-example.html

변환한 C# 코드의 전문은 다음과 같습니다.

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

namespace FFmpegApp1
{
    internal unsafe class Program
    {
        static AVPixelFormat _hw_pix_fmt = AVPixelFormat.AV_PIX_FMT_NONE;
        static AVBufferRef* _hw_device_ctx = null;

        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()}");
#endif
            Console.WriteLine();

            Console.WriteLine($"LIBAVFORMAT Version: {ffmpeg.LIBAVFORMAT_VERSION_MAJOR}.{ffmpeg.LIBAVFORMAT_VERSION_MINOR}");

            string outputPath = @"c:\temp\output";
            try
            {
                Directory.Delete(outputPath, true);
                Directory.CreateDirectory(outputPath);
            }
            catch { }

            string outputFilePath = Path.Combine(outputPath, "test_cs2.dat");

            /* hwdevice type: cuda, dxva2, d3d11va, opencl */
            video_decode_example("cuda", @"D:\video_sample\theade-i-was-young.mp4", outputFilePath);
        }

        static unsafe void video_decode_example(string hwdeviceName, string filename, string outputFileName)
        {
            AVFormatContext* input_ctx = null;
            AVStream* video = null;
            AVCodecContext* decoder_ctx = null;
            AVCodec* decoder = null;
            AVPacket* packet = null;

            // https://ffmpeg.org/doxygen/trunk/codec_8h_source.html#l00420
            int AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX = 0x01;
            int ret = 0;

            AVHWDeviceType type = AVHWDeviceType.AV_HWDEVICE_TYPE_NONE;

            Console.WriteLine("Available device types:");
            while ((type = ffmpeg.av_hwdevice_iterate_types(type)) != AVHWDeviceType.AV_HWDEVICE_TYPE_NONE)
            {
                Console.WriteLine($"\t{ffmpeg.av_hwdevice_get_type_name(type)}");
            }

            type = ffmpeg.av_hwdevice_find_type_by_name(hwdeviceName);
            if (type == AVHWDeviceType.AV_HWDEVICE_TYPE_NONE)
            {
                Console.WriteLine($"Device type {hwdeviceNae} is not supported.");
                return;
            }

            do
            {
                packet = ffmpeg.av_packet_alloc();
                if (packet == null)
                {
                    Console.WriteLine("Failed to allocate AVPacket");
                    break;
                }

                if (ffmpeg.avformat_open_input(&input_ctx, filename, null, null) < 0)
                {
                    Console.WriteLine($"Cannot open input file: {filename}");
                    break;
                }

                if (ffmpeg.avformat_find_stream_info(input_ctx, null) < 0)
                {
                    Console.WriteLine("Cannot find input stream information.");
                    break;
                }

                /* find the video stream information */
                ret = ffmpeg.av_find_best_stream(input_ctx, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, &decoder, 0);
                if (ret < 0)
                {
                    Console.WriteLine("Cannot find a video stream in the input file\n");
                    break;
                }

                int video_stream = ret;

                for (int i = 0; ; i++)
                {
                    AVCodecHWConfig* config = ffmpeg.avcodec_get_hw_config(decoder, i);
                    if (config == null)
                    {
                        string decoderName = Marshal.PtrToStringUTF8(new IntPtr(decoder->name));
                        Console.WriteLine($"Decoder {decoderName} does not support device type {ffmpeg.av_hwdevice_get_type_name(type)}");
                        break;
                    }

                    if ((config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) == AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX &&
                            config->device_type == type)
                    {
                        _hw_pix_fmt = config->pix_fmt;
                        break;
                    }
                }

                if (_hw_pix_fmt == AVPixelFormat.AV_PIX_FMT_NONE)
                {
                    break;
                }

                decoder_ctx = ffmpeg.avcodec_alloc_context3(decoder);
                if (decoder_ctx == null)
                {
                    Console.WriteLine("Could not allocate video codec context");
                    break;
                }

                video = input_ctx->streams[video_stream];
                if (ffmpeg.avcodec_parameters_to_context(decoder_ctx, video->codecpar) < 0)
                {
                    break;
                }

                decoder_ctx->get_format = (AVCodecContext_get_format_func)get_hw_format;

                if (hw_decoder_init(decoder_ctx, type) < 0)
                {
                    break;
                }

                if ((ret = ffmpeg.avcodec_open2(decoder_ctx, decoder, null)) < 0)
                {
                    Console.WriteLine($"Failed to open codec for stream {video_stream}");
                    break;
                }

                using FileStream output_file = File.OpenWrite(outputFileName);

                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);
                    }
                }

                ffmpeg.av_packet_unref(packet);

                /* flush the decoder */
                ret = decode_write(decoder_ctx, null, output_file);

            } while (false);

            if (packet != null)
            {
                ffmpeg.av_packet_free(&packet);
            }

            if (decoder_ctx != null)
            {
                ffmpeg.avcodec_free_context(&decoder_ctx);
            }

            if (input_ctx != null)
            {
                ffmpeg.avformat_close_input(&input_ctx);
            }

            if (_hw_device_ctx != null)
            {
                fixed (AVBufferRef** ppRef = &_hw_device_ctx)
                {
                    ffmpeg.av_buffer_unref(ppRef);
                }
            }
        }

        static unsafe int decode_write(AVCodecContext* avctx, AVPacket* packet, FileStream output_file)
        {
            AVFrame* frame = null;
            AVFrame* sw_frame = null;
            AVFrame* tmp_frame = null;
            byte* buffer = null;
            int size = 0;
            int ret = 0;

            ret = ffmpeg.avcodec_send_packet(avctx, packet);
            if (ret < 0)
            {
                Console.WriteLine("Error during decoding");
                return ret;
            }

            while (true)
            {
                if ((frame = ffmpeg.av_frame_alloc()) == null || (sw_frame = ffmpeg.av_frame_alloc()) == null)
                {
                    Console.WriteLine("Can not alloc frame");
                    ret = ffmpeg.AVERROR(ffmpeg.ENOMEM);
                    goto fail;
                }

                ret = ffmpeg.avcodec_receive_frame(avctx, frame);
                if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN) || ret == ffmpeg.AVERROR_EOF)
                {
                    ffmpeg.av_frame_free(&frame);
                    ffmpeg.av_frame_free(&sw_frame);
                    return 0;
                }
                else if (ret < 0)
                {
                    Console.WriteLine("Error while decoding");
                    goto fail;
                }

                if (frame->format == (int)_hw_pix_fmt)
                {
                    /* retrieve data from GPU to CPU */
                    if ((ret = ffmpeg.av_hwframe_transfer_data(sw_frame, frame, 0)) < 0)
                    {
                        Console.WriteLine("Error transferring the data to system memory");
                        goto fail;
                    }

                    tmp_frame = sw_frame;
                }
                else
                {
                    tmp_frame = frame;
                }

                size = ffmpeg.av_image_get_buffer_size((AVPixelFormat)tmp_frame->format, tmp_frame->width,
                                                tmp_frame->height, 1);
                buffer = (byte *)ffmpeg.av_malloc((ulong)size);
                if (buffer == null)
                {
                    Console.WriteLine("Can not alloc buffer");
                    ret = ffmpeg.AVERROR(ffmpeg.ENOMEM);
                    goto fail;
                }

                FFmpeg.AutoGen.byte_ptrArray4* ptrFrameData = (FFmpeg.AutoGen.byte_ptrArray4*)&tmp_frame->data;
                FFmpeg.AutoGen.int_array4* ptrLineSize = (FFmpeg.AutoGen.int_array4*)&tmp_frame->linesize;

                ret = ffmpeg.av_image_copy_to_buffer(buffer, size,
                                              *ptrFrameData, *ptrLineSize,
                                      (AVPixelFormat)tmp_frame->format,
                                      tmp_frame->width, tmp_frame->height, 1);
                if (ret < 0)
                {
                    Console.WriteLine("Can not copy image to buffer");
                    goto fail;
                }

                ReadOnlySpan<byte> contents = new Span<byte>(buffer, size);
                output_file.Write(contents);

            fail:
                ffmpeg.av_frame_free(&frame);
                ffmpeg.av_frame_free(&sw_frame);
                ffmpeg.av_freep(&buffer);
                if (ret < 0)
                {
                    return ret;
                }
            }
        }

        static unsafe int hw_decoder_init(AVCodecContext* ctx, AVHWDeviceType type)
        {
            int err = 0;

            fixed (AVBufferRef** ptr = &_hw_device_ctx)
            {
                if ((err = ffmpeg.av_hwdevice_ctx_create(ptr, type, null, null, 0)) < 0)
                {
                    Console.WriteLine("Failed to create specified HW device.");
                    return err;
                }

                ctx->hw_device_ctx = ffmpeg.av_buffer_ref(*ptr);
            }

            return err;
        }

        static unsafe AVPixelFormat get_hw_format(AVCodecContext* ctx, AVPixelFormat* pix_fmts)
        {
            AVPixelFormat* p;

            for (p = pix_fmts; *p != AVPixelFormat.AV_PIX_FMT_NONE; p++)
            {
                if (*p == _hw_pix_fmt)
                {
                    return *p;
                }
            }

            Console.WriteLine("Failed to get HW surface format.");
            return AVPixelFormat.AV_PIX_FMT_NONE;
        }
    }
}

C#의 unsafe 구문이 없었으면 꿈도 못 꿀 변환입니다. ^^;




그나저나, 지난번 decode_video.c 사례에서는 정상적인 이미지 파일로 변환을 못했는데요, 이번에는 어떨까요? ^^

decode_write 함수에 av_image_copy_to_buffer의 buffer에 담긴 데이터가,

ret = ffmpeg.av_image_copy_to_buffer(buffer, size,
                                *ptrFrameData, *ptrLineSize,
                        (AVPixelFormat)tmp_frame->format,
                        tmp_frame->width, tmp_frame->height, 1);

이거저거 시도해 보니 해석이 됩니다. 가령, 다음과 같이 프레임 하나를 건져 format을 보니 AV_PIX_FMT_NV12로 나옵니다.

if (avctx->frame_number == 310)
{
    if (tmp_frame->format == (int)AVPixelFormat.AV_PIX_FMT_NV12)
    {
        // ...[생략]...
    }
}

AV_PIX_FMT_NV12에 대한 도움말을 보면,

//
// Summary:
//     planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which
//     are interleaved (first byte U and the following byte V)
AV_PIX_FMT_NV12 = 23,

12bit per plane인데, 다행히 이에 대한 해석을 stackoverflow에서 얻을 수 있습니다.

How to Convert NV12 to BGR(AVFrame to cv::Mat)
; https://stackoverflow.com/questions/53994735/how-to-convert-nv12-to-bgravframe-to-cvmat

따라서, 다음과 같이 코딩을 완성할 수 있습니다.

if (avctx->frame_number == 310)
{
    if (tmp_frame->format == (int)AVPixelFormat.AV_PIX_FMT_NV12)
    {
        OpenCvSharp.Mat rgbMat = new OpenCvSharp.Mat();
        OpenCvSharp.Mat nvMat = new OpenCvSharp.Mat(tmp_frame->height * 3 / 2, tmp_frame->width,
            OpenCvSharp.MatType.CV_8UC1, new IntPtr(buffer));

        OpenCvSharp.Cv2.CvtColor(nvMat, rgbMat, OpenCvSharp.ColorConversionCodes.YUV2BGR_NV12);
        OpenCvSharp.Cv2.ImShow("test", rgbMat);
        return ret;
    }
}

8UC1이라 8bit * 3 / 2 = 12bit가 왠지 절묘하게 맞는 것도 같습니다. ^^ 어쨌든, 저렇게 해서 변환된 rgbMat 변수를 OpenCvSharp.Cv2.ImShow로 보면 이미지가 정상적으로 잘 나옵니다. 실제로 아래의 화면에서 곰 플레이어로 본 영상(위)과 OpenCvSharp.Cv2.ImShow로 영상(아래)을 함께 캡처한 것입니다.

(사진은 유튜브 영상 "디에이드"의 "안다은" 님이고 사용을 허락받고 올립니다.)
hw_video_decode_1.png

(첨부 파일은 hw_decode.c 파일의 C#과 Visual C++로 포팅한 프로젝트 파일을 포함합니다.)
(이 글의 소스 코드는 github에 올려져 있습니다.)




그나저나, 정말 영상 압축의 힘이 대단한 것 같습니다. ^^ 13MB 짜리 h264 인코딩된 mp4 파일인데, 이것을 위의 hw_decode.c / decode_write 함수 내에서 해석한 raw 이미지들을 파일로 쓰면 그 용량이 거의 1.8GB에 달합니다.

그리고, AVPixelFormat에 정의된 enum 상수들은 보는 것만으로도 질리게 만듭니다. ^^; 게다가 비슷한 정의들도 있는데, 이번 글에서 언급한 AV_PIX_FMT_NV12 포맷과 AV_PIX_FMT_YUV420P의 포맷은 어떤 차이일까요? ^^ (혹시 아시는 분은 덧글 부탁드립니다.)

AV_PIX_FMT_YUV420P 	
planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)

AV_PIX_FMT_NV12 
planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V)

아마도 AVPixelFormat의 상수를 모두 설명하는 것만으로 책 한 권 분량이 나오지 않을까 싶습니다.




이 예제에서 출력한 test.dat 파일은 다음의 명령어로 재생해 볼 수 있습니다.

ffplay -autoexit -f rawvideo -pixel_format nv12 -video_size 1920x1080 test.dat

참고로, 테스트를 위해 (repo의 Samples 디렉터리에 포함시킨 5.00초 분량의) file_example_MP4_1920_18MG.mp4 파일을 출력한 test.dat 파일을 ffplay로 재생해 보면 5.97초로 재생이 됩니다. 왜냐하면 ffplay는 기본적으로 25fps로 재생하기 때문인데요, 따라서 옵션에 30fps를 지정하면 됩니다.

ffplay -autoexit -f rawvideo -framerate 30 -pixel_format nv12 -video_size 1920x1080 test.dat

그럼 ffplay에 찍히는 재생 시간이 대략 4.96 ~ 4.98초 정도로 나옵니다.




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

[연관 글]






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

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

비밀번호

댓글 작성자
 



2022-01-21 10시14분
[이승준] https://learn.microsoft.com/ko-kr/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering
요기서 yv12랑 nv12를 보시면 됩니다. yv12가 yuv420p 입니다.
[guest]
2022-01-21 09시29분
@이승준 링크 정보 감사합니다. Microsoft 자료에 그렇게 있으리라고는 상상도 못했습니다. ^^ 암튼, 전문 분야는 아니지만 알아가는 재미가 있어서 좋은 것 같습니다.
정성태

1  2  [3]  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13557정성태2/18/20241902Windows: 258. Task Scheduler의 Author 속성 값을 변경하는 방법
13556정성태2/17/20241947Windows: 257. Windows - Symbolic (hard/soft) Link 및 Junction 차이점
13555정성태2/15/20242101닷넷: 2216. C# - SemaphoreSlim 사용 시 주의점
13554정성태2/15/20241849VS.NET IDE: 189. Visual Studio - 닷넷 소스코드 디컴파일 찾기가 안 될 때
13553정성태2/14/20241932닷넷: 2215. windbg - thin/fat lock 없이 동작하는 Monitor.Wait + Pulse
13552정성태2/13/20241880닷넷: 2214. windbg - Monitor.Enter의 thin lock과 fat lock
13551정성태2/12/20242077닷넷: 2213. ASP.NET/Core 웹 응용 프로그램 - 2차 스레드의 예외로 인한 비정상 종료
13550정성태2/11/20242162Windows: 256. C# - Server socket이 닫히면 Accept 시켰던 자식 소켓이 닫힐까요?
13549정성태2/3/20242487개발 환경 구성: 706. C# - 컨테이너에서 실행하기 위한 (소켓) 콘솔 프로젝트 구성
13548정성태2/1/20242319개발 환경 구성: 705. "Docker Desktop for Windows" - ASP.NET Core 응용 프로그램의 소켓 주소 바인딩(IPv4/IPv6 loopback, Any)
13547정성태1/31/20242066개발 환경 구성: 704. Visual Studio - .NET 8 프로젝트부터 dockerfile에 추가된 "USER app" 설정
13546정성태1/30/20241922Windows: 255. (디버거의 영향 등으로) 대상 프로세스가 멈추면 Socket KeepAlive로 연결이 끊길까요?
13545정성태1/30/20241841닷넷: 2212. ASP.NET Core - 우선순위에 따른 HTTP/HTTPS 호스트:포트 바인딩 방법
13544정성태1/30/20241854오류 유형: 894. Microsoft.Data.SqlClient - Could not load file or assembly 'System.Security.Permissions, ...'
13543정성태1/30/20241850Windows: 254. Windows - 기본 사용 중인 5357 포트 비활성화는 방법
13542정성태1/30/20241884오류 유형: 893. Visual Studio - Web Application을 실행하지 못하는 IISExpress - 두 번째 이야기
13541정성태1/29/20241929VS.NET IDE: 188. launchSettings.json의 useSSL 옵션
13540정성태1/29/20242058Linux: 69. 리눅스 - "Docker Desktop for Windows" Container 환경에서 IPv6 Loopback Address 바인딩 오류
13539정성태1/26/20242155개발 환경 구성: 703. Visual Studio - launchSettings.json을 이용한 HTTP/HTTPS 포트 바인딩
13538정성태1/25/20242236닷넷: 2211. C# - NonGC(FOH) 영역에 .NET 개체를 생성파일 다운로드1
13537정성태1/24/20242294닷넷: 2210. C# - Native 메모리에 .NET 개체를 생성파일 다운로드1
13536정성태1/23/20242458닷넷: 2209. .NET 8 - NonGC Heap / FOH (Frozen Object Heap) [1]
13535정성태1/22/20242271닷넷: 2208. C# - GCHandle 구조체의 메모리 분석
13534정성태1/21/20242089닷넷: 2207. C# - SQL Server DB를 bacpac으로 Export/Import파일 다운로드1
13533정성태1/18/20242288닷넷: 2206. C# - TCP KeepAlive의 서버 측 구현파일 다운로드1
13532정성태1/17/20242189닷넷: 2205. C# - SuperSimpleTcp 사용 시 주의할 점파일 다운로드1
1  2  [3]  4  5  6  7  8  9  10  11  12  13  14  15  ...