using System.IO; using System.Text; using System.Text.RegularExpressions; using Paca.App.Browser.Features; using Paca.Browser; using Paca.Core; using Paca.Core.Detection; using Paca.Core.Net; using Paca.Core.Platform; using Xunit; namespace Paca.Tests.Core; /// /// 미디어 엔진 & 고도화 프로세싱 메가 파이프라인 (Muxer/Codec, 자막, 오디오 노멀라이징, 챕터 분할, HDR 톤매핑, 스마트 트림 등). /// /// /// 22차: 고성능 미디어 리먹스 & 코덱 호환성 & 자막/오디오 트랙 추출 파이프라인 검증. /// public class Milestone22MuxerAndCodecTests { private sealed class RecordingProcessRunner : IProcessRunner { public ProcessSpec? LastSpec { get; private set; } public Task RunAsync(ProcessSpec spec, Action? onLine, CancellationToken ct) { LastSpec = spec; return Task.FromResult(new ProcessResult(0, "OK", "")); } } [Fact] public async Task 미디어_오디오_트랙_추출_명령_파이프라인() { var runner = new RecordingProcessRunner(); var muxer = new DesktopMediaMuxer(runner); await muxer.ExtractAudioAsync("ffmpeg.exe", "input.mp4", "output.mp3", "mp3", CancellationToken.None); Xunit.Assert.NotNull(runner.LastSpec); Xunit.Assert.Equal("ffmpeg.exe", runner.LastSpec.ExePath); Xunit.Assert.Contains("-vn", runner.LastSpec.Args); Xunit.Assert.Contains("output.mp3", runner.LastSpec.Args); } [Fact] public async Task 미디어_자막_병합_명령_파이프라인() { var runner = new RecordingProcessRunner(); var muxer = new DesktopMediaMuxer(runner); await muxer.MergeSubtitlesAsync("ffmpeg.exe", "input.mp4", "sub.vtt", "output.mp4", CancellationToken.None); Xunit.Assert.NotNull(runner.LastSpec); Xunit.Assert.Equal("ffmpeg.exe", runner.LastSpec.ExePath); Xunit.Assert.Contains("sub.vtt", runner.LastSpec.Args); Xunit.Assert.Contains("-c", runner.LastSpec.Args); Xunit.Assert.Contains("mov_text", runner.LastSpec.Args); } [Fact] public async Task 미디어_썸네일_커버_임베딩_명령_파이프라인() { var runner = new RecordingProcessRunner(); var muxer = new DesktopMediaMuxer(runner); await muxer.EmbedThumbnailAsync("ffmpeg.exe", "input.mp4", "cover.jpg", "output.mp4", CancellationToken.None); Xunit.Assert.NotNull(runner.LastSpec); Xunit.Assert.Equal("ffmpeg.exe", runner.LastSpec.ExePath); Xunit.Assert.Contains("cover.jpg", runner.LastSpec.Args); Xunit.Assert.Contains("-disposition:v:1", runner.LastSpec.Args); } [Fact] public void 미디어_컨버전_포맷_해결_유효성_검증() { var audioFormats = new[] { "mp3", "m4a", "aac", "wav", "flac" }; foreach (var format in audioFormats) { Xunit.Assert.True(DesktopMediaMuxer.IsSupportedAudioFormat(format)); } } } /// /// 27차: 미디어 분석 인텔리전스 & 스트림 메타데이터 엔진 검증. /// public class Milestone27MediaAnalysisIntelligenceTests { [Fact] public void FFprobe_JSON_출력_스트림_메타데이터_정밀_파싱() { var sampleFfprobeJson = @"{ ""streams"": [ { ""codec_type"": ""video"", ""codec_name"": ""h264"", ""width"": 1920, ""height"": 1080, ""r_frame_rate"": ""60/1"", ""bit_rate"": ""4500000"" }, { ""codec_type"": ""audio"", ""codec_name"": ""aac"", ""sample_rate"": ""48000"", ""channels"": 2, ""bit_rate"": ""192000"" } ], ""format"": { ""filename"": ""sample.mp4"", ""duration"": ""124.5"", ""size"": ""72000000"", ""bit_rate"": ""4692000"" } }"; var report = MediaMetadataExtractor.ParseFfprobeOutput(sampleFfprobeJson); Xunit.Assert.NotNull(report); Xunit.Assert.Equal(1920, report.Width); Xunit.Assert.Equal(1080, report.Height); Xunit.Assert.Equal("1080p", report.ResolutionLabel); Xunit.Assert.Equal("h264", report.VideoCodec); Xunit.Assert.Equal("aac", report.AudioCodec); Xunit.Assert.Equal(60.0, report.Fps); Xunit.Assert.Equal(124.5, report.DurationSec); Xunit.Assert.Equal(2, report.AudioChannels); Xunit.Assert.True(report.IsHd); } [Fact] public void 비트레이트_기반_화질_등급_평가_검증() { var qualityHigh = MediaMetadataExtractor.EvaluateQualityGrade(8_000_000, 1080); Xunit.Assert.Equal("초고화질 (Very High)", qualityHigh); var qualityNormal = MediaMetadataExtractor.EvaluateQualityGrade(3_000_000, 1080); Xunit.Assert.Equal("표준 고화질 (Standard HD)", qualityNormal); var qualitySd = MediaMetadataExtractor.EvaluateQualityGrade(800_000, 480); Xunit.Assert.Equal("일반 화질 (SD)", qualitySd); } [Fact] public void 챕터_타임스탬프_인덱싱_검증() { var rawChapters = new[] { new ChapterInfo(1, "인트로", 0.0, 15.0), new ChapterInfo(2, "본문", 15.0, 100.0), new ChapterInfo(3, "아웃트로", 100.0, 120.0) }; var indexed = MediaMetadataExtractor.IndexChapters(rawChapters); Xunit.Assert.Equal(3, indexed.Count); Xunit.Assert.Equal("인트로", indexed[0].Title); Xunit.Assert.Equal(15.0, indexed[0].Duration); Xunit.Assert.Equal(85.0, indexed[1].Duration); } [Fact] public void 손상_스트림_진단_및_복구_가능_여부_판정() { var corruptedJson = @"{ ""streams"": [], ""format"": {} }"; var report = MediaMetadataExtractor.ParseFfprobeOutput(corruptedJson); Xunit.Assert.NotNull(report); Xunit.Assert.False(report.IsValid); Xunit.Assert.True(report.HasWarnings); } } /// /// 28차: 스마트 챕터 분할 및 메타데이터 태깅 파이프라인 검증. /// public class Milestone28ChapterSplitterTests { private sealed class RecordingProcessRunner : IProcessRunner { public List Specs { get; } = new(); public Task RunAsync(ProcessSpec spec, Action? onLine, CancellationToken ct) { Specs.Add(spec); return Task.FromResult(new ProcessResult(0, "OK", "")); } } [Fact] public async Task 챕터별_개별_파일_분할_인자_생성_검증() { var runner = new RecordingProcessRunner(); var splitter = new ChapterSplitter(runner); var chapters = new[] { new ChapterInfo(1, "01 Intro", 0.0, 30.0), new ChapterInfo(2, "02 Main", 30.0, 150.0) }; var outputDir = @"C:\output"; var outputs = await splitter.SplitByChaptersAsync("ffmpeg.exe", "video.mp4", chapters, outputDir, CancellationToken.None); Xunit.Assert.Equal(2, outputs.Count); Xunit.Assert.Equal(2, runner.Specs.Count); Xunit.Assert.Contains("-ss", runner.Specs[0].Args); Xunit.Assert.Contains("-to", runner.Specs[0].Args); Xunit.Assert.Contains("-c", runner.Specs[0].Args); Xunit.Assert.Contains("copy", runner.Specs[0].Args); } [Fact] public void 챕터_파일명_안전화_검증() { var sanitized = ChapterSplitter.GetChapterFileName(1, "Intro: Special? / Part 1", "mp4"); Xunit.Assert.DoesNotContain(":", sanitized); Xunit.Assert.DoesNotContain("?", sanitized); Xunit.Assert.DoesNotContain("/", sanitized); Xunit.Assert.EndsWith(".mp4", sanitized); } [Fact] public void 단일_챕터_또는_빈_챕터_시_예외_없이_원본_유지() { var runner = new RecordingProcessRunner(); var splitter = new ChapterSplitter(runner); var emptyChapters = Array.Empty(); var outputs = splitter.PlanSplits("video.mp4", emptyChapters, @"C:\output"); Xunit.Assert.Single(outputs); Xunit.Assert.Equal("video.mp4", outputs[0]); } [Fact] public void 챕터_FFmetadata_텍스트_생성_검증() { var chapters = new[] { new ChapterInfo(1, "Intro", 0.0, 10.0), new ChapterInfo(2, "Outro", 10.0, 20.0) }; var metadataText = ChapterSplitter.GenerateFfmetadata(chapters, "Sample Title"); Xunit.Assert.Contains(";FFMETADATA1", metadataText); Xunit.Assert.Contains("title=Intro", metadataText); Xunit.Assert.Contains("START=0", metadataText); Xunit.Assert.Contains("END=10000", metadataText); } } /// /// 29차: EBU R128 오디오 라우드니스 노멀라이제이션 & 무음 제거 파이프라인 검증. /// public class Milestone29AudioNormalizerTests { private sealed class RecordingProcessRunner : IProcessRunner { public List Specs { get; } = new(); public Task RunAsync(ProcessSpec spec, Action? onLine, CancellationToken ct) { Specs.Add(spec); var sampleStdout = "{\n" + " \"input_i\": \"-24.50\",\n" + " \"input_tp\": \"-1.20\",\n" + " \"input_lra\": \"8.50\",\n" + " \"input_thresh\": \"-35.00\"\n" + "}"; return Task.FromResult(new ProcessResult(0, sampleStdout, "")); } } [Fact] public async Task Loudnorm_1차_측정_및_2차_적용_명령_파이프라인() { var runner = new RecordingProcessRunner(); var normalizer = new AudioNormalizer(runner); var spec = new LoudnessSpec(TargetLufs: -14.0, TruePeak: -1.0, LoudnessRange: 11.0); var res = await normalizer.NormalizeAudioAsync("ffmpeg.exe", "input.mp4", "output.mp4", spec, CancellationToken.None); Xunit.Assert.Equal("output.mp4", res); Xunit.Assert.Equal(2, runner.Specs.Count); // 1-pass measure, 2-pass apply Xunit.Assert.Contains("-af", runner.Specs[1].Args); var filterArg = runner.Specs[1].Args.First(a => a.StartsWith("loudnorm=")); Xunit.Assert.Contains("I=-14", filterArg); Xunit.Assert.Contains("TP=-1", filterArg); } [Fact] public void Loudnorm_측정_JSON_파싱_검증() { var json = @"{ ""input_i"": ""-28.30"", ""input_tp"": ""-4.50"", ""input_lra"": ""12.10"", ""input_thresh"": ""-39.20"" }"; var measured = AudioNormalizer.ParseLoudnessStats(json); Xunit.Assert.NotNull(measured); Xunit.Assert.Equal(-28.30, measured.MeasuredI); Xunit.Assert.Equal(-4.50, measured.MeasuredTp); Xunit.Assert.Equal(12.10, measured.MeasuredLra); Xunit.Assert.Equal(-39.20, measured.MeasuredThresh); } [Fact] public void 무음_제거_필터_문자열_구성_검증() { var filter = AudioNormalizer.BuildSilenceRemoveFilter(thresholdDb: -50.0, minDurationSec: 0.5); Xunit.Assert.Contains("silenceremove=", filter); Xunit.Assert.Contains("-50dB", filter); Xunit.Assert.Contains("0.5", filter); } [Fact] public void 표준_프리셋_값_유효성_검증() { var broadcast = LoudnessSpec.EbuR128; Xunit.Assert.Equal(-23.0, broadcast.TargetLufs); var web = LoudnessSpec.WebStandard; Xunit.Assert.Equal(-14.0, web.TargetLufs); } } /// /// 31차: 브라우저 실시간 웹 번역 & Live Caption 자막 오버레이 엔진 검증. /// public class Milestone31LiveCaptionEngineTests { [Fact] public void WebVTT_실시간_자막_파싱_및_타임코드_정렬_검증() { var sampleVtt = @"WEBVTT 00:00:01.000 --> 00:00:04.000 Hello and welcome to the live stream. 00:00:04.500 --> 00:00:08.000 Today we are discussing system architecture. "; var engine = new LiveCaptionEngine(); engine.LoadWebVtt(sampleVtt); Xunit.Assert.Equal(2, engine.CueCount); var cueAt2Sec = engine.GetActiveCue(2.5); Xunit.Assert.NotNull(cueAt2Sec); Xunit.Assert.Equal("Hello and welcome to the live stream.", cueAt2Sec.Text); var cueAt5Sec = engine.GetActiveCue(5.0); Xunit.Assert.NotNull(cueAt5Sec); Xunit.Assert.Equal("Today we are discussing system architecture.", cueAt5Sec.Text); var cueAt10Sec = engine.GetActiveCue(10.0); Xunit.Assert.Null(cueAt10Sec); } [Fact] public void 타임코드_초단위_변환_정밀도_검증() { var sec1 = LiveCaptionEngine.ParseTimeCode("00:01:23.456"); Xunit.Assert.Equal(83.456, sec1, 3); var sec2 = LiveCaptionEngine.ParseTimeCode("02:00.500"); Xunit.Assert.Equal(120.500, sec2, 3); } [Fact] public void 자막_오버레이_스타일_및_위치_설정_검증() { var style = new CaptionStyle(FontSize: 18, TextColor: "#FFFFFF", BackgroundColor: "#CC000000", Position: CaptionPosition.BottomCenter); Xunit.Assert.Equal(18, style.FontSize); Xunit.Assert.Equal(CaptionPosition.BottomCenter, style.Position); } [Fact] public void 번역_텍스트_병합_및_이중_언어_표시_포맷_검증() { var formatted = LiveCaptionEngine.FormatBilingual("Hello world", "안녕하세요 세계"); Xunit.Assert.Equal("Hello world\n안녕하세요 세계", formatted); } } /// /// 32차: HDR10 / Dolby Vision 10-bit 색공간 보존 및 SDR 톤매핑 파이프라인 검증. /// public class Milestone32HdrToneMapperTests { private sealed class RecordingProcessRunner : IProcessRunner { public List Specs { get; } = new(); public Task RunAsync(ProcessSpec spec, Action? onLine, CancellationToken ct) { Specs.Add(spec); return Task.FromResult(new ProcessResult(0, "OK", "")); } } [Fact] public void HDR_색공간_특성_판별_검증() { var isHdr1 = HdrToneMapper.IsHdrContent("bt2020nc", "smpte2084", "arib-std-b67"); Xunit.Assert.True(isHdr1); var isHdr2 = HdrToneMapper.IsHdrContent("bt709", "bt709", "bt709"); Xunit.Assert.False(isHdr2); } [Fact] public void SDR_톤매핑_필터_문자열_생성_검증() { var filter = HdrToneMapper.BuildToneMapFilter(ToneMapAlgorithm.Mobius, targetNits: 100); Xunit.Assert.Contains("zscale=", filter); Xunit.Assert.Contains("tonemap=mobius", filter); Xunit.Assert.Contains("bt709", filter); } [Fact] public void HDR_10비트_무손실_패스스루_인자_검증() { var args = HdrToneMapper.Build10BitPassthroughArgs("input.mkv", "output.mp4"); Xunit.Assert.Contains("-c:v", args); Xunit.Assert.Contains("copy", args); Xunit.Assert.Contains("-c:a", args); } [Fact] public async Task 톤매핑_변환_프로세스_실행_검증() { var runner = new RecordingProcessRunner(); var mapper = new HdrToneMapper(runner); var outPath = await mapper.ConvertToSdrAsync("ffmpeg.exe", "hdr_sample.mkv", "sdr_sample.mp4", ToneMapAlgorithm.Hable, CancellationToken.None); Xunit.Assert.Equal("sdr_sample.mp4", outPath); Xunit.Assert.Single(runner.Specs); Xunit.Assert.Contains("-vf", runner.Specs[0].Args); Xunit.Assert.Contains("tonemap=hable", runner.Specs[0].Args.First(a => a.Contains("tonemap="))); } } /// /// 33차: 퍼셉추얼 비디오 해시(pHash) 기반 중복 미디어 검출 파이프라인 검증. /// public class Milestone33VideoDeduplicatorTests { [Fact] public void 해밍_거리_비트_차이_계산_검증() { ulong hash1 = 0b10101010_10101010; ulong hash2 = 0b10101010_10101011; // 1비트 차이 ulong hash3 = 0b01010101_01010101; // 전체 반전 (16비트 차이) Xunit.Assert.Equal(1, VideoDeduplicator.ComputeHammingDistance(hash1, hash2)); Xunit.Assert.Equal(16, VideoDeduplicator.ComputeHammingDistance(hash1, hash3)); } [Fact] public void 유사도_임계치_중복_판정_검증() { ulong original = 0xAAAA_BBBB_CCCC_DDDD; ulong nearDuplicate = 0xAAAA_BBBB_CCCC_DDDC; // 1비트 차이 (1101 vs 1100) ulong totallyDifferent = 0x1111_2222_3333_4444; var deduplicator = new VideoDeduplicator(maxHammingThreshold: 5); deduplicator.RegisterVideo("video_original.mp4", original); var match1 = deduplicator.FindDuplicate(nearDuplicate); Xunit.Assert.NotNull(match1); Xunit.Assert.Equal("video_original.mp4", match1.FilePath); Xunit.Assert.Equal(1, match1.HammingDistance); var match2 = deduplicator.FindDuplicate(totallyDifferent); Xunit.Assert.Null(match2); } [Fact] public void 그레이스케일_배열_기반_64비트_pHash_생성_검증() { // 8x8 그레이스케일 픽셀 밝기 배열 (64개) byte[] pixels = new byte[64]; for (int i = 0; i < 32; i++) pixels[i] = 200; // 상단 밝음 for (int i = 32; i < 64; i++) pixels[i] = 50; // 하단 어두움 var hash = VideoDeduplicator.ComputeHashFromPixels(pixels); Xunit.Assert.NotEqual(0UL, hash); // 동일 픽셀이면 동일 해시 var hash2 = VideoDeduplicator.ComputeHashFromPixels(pixels); Xunit.Assert.Equal(hash, hash2); } [Fact] public void 등록된_미디어_라이브러리_클리어_및_카운트_검증() { var deduplicator = new VideoDeduplicator(); deduplicator.RegisterVideo("v1.mp4", 0x1234); deduplicator.RegisterVideo("v2.mp4", 0x5678); Xunit.Assert.Equal(2, deduplicator.RegisteredCount); deduplicator.Clear(); Xunit.Assert.Equal(0, deduplicator.RegisteredCount); } } /// /// 36차: 실시간 라이브 스트림 DVR 녹화 & 롤링 타임머신 버퍼 엔진 검증. /// public class Milestone36LiveDvrRecorderTests { [Fact] public void 세그먼트_순차_추가_및_롤링_버퍼_초과_삭제_검증() { var recorder = new LiveDvrRecorder(maxBufferDurationSec: 30.0); recorder.AddSegment(new LiveSegment(Sequence: 1, DurationSec: 10.0, FilePath: "chunk1.ts")); recorder.AddSegment(new LiveSegment(Sequence: 2, DurationSec: 10.0, FilePath: "chunk2.ts")); recorder.AddSegment(new LiveSegment(Sequence: 3, DurationSec: 10.0, FilePath: "chunk3.ts")); recorder.AddSegment(new LiveSegment(Sequence: 4, DurationSec: 10.0, FilePath: "chunk4.ts")); // 40초 (초과 -> chunk1 삭제) Xunit.Assert.Equal(3, recorder.SegmentCount); Xunit.Assert.Equal(30.0, recorder.TotalDurationSec); Xunit.Assert.Equal(2, recorder.FirstSequence); Xunit.Assert.Equal(4, recorder.LastSequence); } [Fact] public void 타임머신_시점_추출_및_세그먼트_목록_슬라이스_검증() { var recorder = new LiveDvrRecorder(maxBufferDurationSec: 60.0); for (int i = 1; i <= 6; i++) { recorder.AddSegment(new LiveSegment(Sequence: i, DurationSec: 10.0, FilePath: $"chunk{i}.ts")); } // 마지막 20초 (chunk 5, 6) var recent20Sec = recorder.GetSegmentsForTimeWindow(targetDurationSec: 20.0); Xunit.Assert.Equal(2, recent20Sec.Count); Xunit.Assert.Equal(5, recent20Sec[0].Sequence); Xunit.Assert.Equal(6, recent20Sec[1].Sequence); } [Fact] public void 중복_세그먼트_수신_시_무시_검증() { var recorder = new LiveDvrRecorder(); recorder.AddSegment(new LiveSegment(Sequence: 10, DurationSec: 5.0, FilePath: "chunk10.ts")); recorder.AddSegment(new LiveSegment(Sequence: 10, DurationSec: 5.0, FilePath: "chunk10_dup.ts")); Xunit.Assert.Equal(1, recorder.SegmentCount); } [Fact] public void 녹화_상태_토글_및_통계_리포트_검증() { var recorder = new LiveDvrRecorder(); Xunit.Assert.False(recorder.IsRecording); recorder.StartRecording(); Xunit.Assert.True(recorder.IsRecording); recorder.AddSegment(new LiveSegment(Sequence: 1, DurationSec: 6.0, FilePath: "chunk1.ts")); var stats = recorder.GetStats(); Xunit.Assert.True(stats.IsRecording); Xunit.Assert.Equal(6.0, stats.DurationSec); Xunit.Assert.Equal(1, stats.SegmentCount); recorder.StopRecording(); Xunit.Assert.False(recorder.IsRecording); } } /// /// 38차: 플레이어 듀얼 자막 & 다중 오디오 트랙 스위처 파이프라인 검증. /// public class Milestone38DualSubtitleEngineTests { [Fact] public void 주_자막과_보조_자막_동시_매칭_검증() { var engine = new DualSubtitleEngine(); engine.LoadPrimaryVtt("00:00:01.000 --> 00:00:05.000\nHello World"); engine.LoadSecondaryVtt("00:00:01.000 --> 00:00:05.000\n안녕하세요 세계"); var pair = engine.GetDualCues(3.0); Xunit.Assert.NotNull(pair); Xunit.Assert.Equal("Hello World", pair.PrimaryText); Xunit.Assert.Equal("안녕하세요 세계", pair.SecondaryText); } [Fact] public void 보조_자막만_존재하거나_비동기_타임라인_시_개별_매칭() { var engine = new DualSubtitleEngine(); engine.LoadPrimaryVtt("00:00:01.000 --> 00:00:03.000\nFirst Part"); engine.LoadSecondaryVtt("00:00:02.000 --> 00:00:04.000\n두번째 파트"); var pairAt2_5 = engine.GetDualCues(2.5); Xunit.Assert.NotNull(pairAt2_5); Xunit.Assert.Equal("First Part", pairAt2_5.PrimaryText); Xunit.Assert.Equal("두번째 파트", pairAt2_5.SecondaryText); var pairAt3_5 = engine.GetDualCues(3.5); Xunit.Assert.NotNull(pairAt3_5); Xunit.Assert.Null(pairAt3_5.PrimaryText); Xunit.Assert.Equal("두번째 파트", pairAt3_5.SecondaryText); } [Fact] public void 다중_오디오_트랙_목록_선택_및_전환_검증() { var engine = new DualSubtitleEngine(); engine.RegisterAudioTrack(new AudioTrackInfo(1, "ko", "한국어 (원본)", IsDefault: true)); engine.RegisterAudioTrack(new AudioTrackInfo(2, "en", "English Commentary", IsDefault: false)); Xunit.Assert.Equal(2, engine.AudioTracks.Count); Xunit.Assert.Equal(1, engine.SelectedAudioTrackId); engine.SelectAudioTrack(2); Xunit.Assert.Equal(2, engine.SelectedAudioTrackId); } [Fact] public void 듀얼_자막_레이아웃_설정_검증() { var config = new DualSubtitleLayoutConfig(PrimaryColor: "#FFFFFF", SecondaryColor: "#FFD700", PrimaryFontSize: 20, SecondaryFontSize: 16); Xunit.Assert.Equal("#FFD700", config.SecondaryColor); Xunit.Assert.Equal(20, config.PrimaryFontSize); Xunit.Assert.Equal(16, config.SecondaryFontSize); } } /// /// 39차: GPU 하드웨어 가속 인코더 감지 및 인코딩 파라미터 최적화 파이프라인 검증. /// public class Milestone39HardwareEncoderDetectorTests { private sealed class MockEncoderRunner : IProcessRunner { private readonly string _encodersOutput; public MockEncoderRunner(string encodersOutput) { _encodersOutput = encodersOutput; } public Task RunAsync(ProcessSpec spec, Action? onLine, CancellationToken ct) { return Task.FromResult(new ProcessResult(0, _encodersOutput, "")); } } [Fact] public async Task NVIDIA_NVENC_하드웨어_인코더_감지_및_우선순위_선정() { var sampleEncoders = "Encoders:\n" + " V..... h264_nvenc NVIDIA NVENC H.264 encoder (codec h264)\n" + " V..... hevc_nvenc NVIDIA NVENC hevc encoder (codec hevc)\n" + " V..... libx264 libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (codec h264)\n"; var runner = new MockEncoderRunner(sampleEncoders); var detector = new HardwareEncoderDetector(runner); var capabilities = await detector.DetectCapabilitiesAsync("ffmpeg.exe", CancellationToken.None); Xunit.Assert.True(capabilities.HasNvidiaNvenc); Xunit.Assert.False(capabilities.HasIntelQsv); Xunit.Assert.False(capabilities.HasAmdAmf); var bestH264 = detector.GetBestH264Encoder(capabilities); Xunit.Assert.Equal("h264_nvenc", bestH264); var bestHevc = detector.GetBestHevcEncoder(capabilities); Xunit.Assert.Equal("hevc_nvenc", bestHevc); } [Fact] public async Task 하드웨어_인코더_부재_시_소프트웨어_libx264_폴백_선정() { var sampleEncoders = "Encoders:\n" + " V..... libx264 libx264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (codec h264)\n" + " V..... libx265 libx265 H.265 / HEVC (codec hevc)\n"; var runner = new MockEncoderRunner(sampleEncoders); var detector = new HardwareEncoderDetector(runner); var capabilities = await detector.DetectCapabilitiesAsync("ffmpeg.exe", CancellationToken.None); Xunit.Assert.False(capabilities.HasNvidiaNvenc); var bestH264 = detector.GetBestH264Encoder(capabilities); Xunit.Assert.Equal("libx264", bestH264); } [Fact] public void 인코더별_프리셋_및_품질_인자_구성_검증() { var nvencArgs = HardwareEncoderDetector.BuildEncodingArgs("h264_nvenc", crfOrCqp: 19, preset: "p4"); Xunit.Assert.Contains("-c:v", nvencArgs); Xunit.Assert.Contains("h264_nvenc", nvencArgs); Xunit.Assert.Contains("-cq", nvencArgs); var libx264Args = HardwareEncoderDetector.BuildEncodingArgs("libx264", crfOrCqp: 19, preset: "fast"); Xunit.Assert.Contains("-crf", libx264Args); Xunit.Assert.Contains("-preset", libx264Args); } [Fact] public void Intel_QSV_AMD_AMF_파싱_검증() { var sample = " V..... h264_qsv H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (Intel Quick Sync Video acceleration) (codec h264)\n" + " V..... h264_amf AMD AMF H.264 Encoder (codec h264)\n"; var caps = HardwareEncoderDetector.ParseCapabilities(sample); Xunit.Assert.True(caps.HasIntelQsv); Xunit.Assert.True(caps.HasAmdAmf); } } /// /// 41차: 스마트 AI 비디오 하이라이트 & 숏폼 자동 클립퍼 파이프라인 검증. /// public class Milestone41HighlightShortsClipperTests { private sealed class RecordingProcessRunner : IProcessRunner { public List Specs { get; } = new(); public Task RunAsync(ProcessSpec spec, Action? onLine, CancellationToken ct) { Specs.Add(spec); return Task.FromResult(new ProcessResult(0, "OK", "")); } } [Fact] public void 세로_숏폼_9대16_크롭_필터_문자열_생성_검증() { var filter = HighlightShortsClipper.BuildVerticalCropFilter(inputWidth: 1920, inputHeight: 1080); Xunit.Assert.Contains("crop=", filter); Xunit.Assert.Contains("ih*9/16", filter); } [Fact] public void 볼륨_피크_지점_기반_하이라이트_구간_추출_검증() { // 10초 단위 볼륨 샘플들 (-dB) var volumeSamples = new[] { new VolumeSample(TimeSec: 10.0, VolumeDb: -25.0), new VolumeSample(TimeSec: 20.0, VolumeDb: -12.0), // Peak! new VolumeSample(TimeSec: 30.0, VolumeDb: -28.0), new VolumeSample(TimeSec: 40.0, VolumeDb: -10.0), // Peak! new VolumeSample(TimeSec: 50.0, VolumeDb: -30.0) }; var highlights = HighlightShortsClipper.DetectHighlightWindows(volumeSamples, windowDurationSec: 15.0, topCount: 2); Xunit.Assert.Equal(2, highlights.Count); Xunit.Assert.Equal(40.0, highlights[0].CenterTimeSec); Xunit.Assert.Equal(20.0, highlights[1].CenterTimeSec); } [Fact] public async Task 숏폼_클립_추출_및_인코딩_프로세스_실행_검증() { var runner = new RecordingProcessRunner(); var clipper = new HighlightShortsClipper(runner); var clip = new HighlightClip(StartSec: 35.0, DurationSec: 15.0, Title: "Best Moment"); var outPath = await clipper.ExportShortsClipAsync("ffmpeg.exe", "video.mp4", "shorts_out.mp4", clip, isVerticalShorts: true, CancellationToken.None); Xunit.Assert.Equal("shorts_out.mp4", outPath); Xunit.Assert.Single(runner.Specs); Xunit.Assert.Contains("-ss", runner.Specs[0].Args); Xunit.Assert.Contains("-vf", runner.Specs[0].Args); Xunit.Assert.Contains("crop=", runner.Specs[0].Args.First(a => a.Contains("crop="))); } [Fact] public void 유효하지_않은_클립_지정_시_예외_검증() { var runner = new RecordingProcessRunner(); var clipper = new HighlightShortsClipper(runner); var invalidClip = new HighlightClip(StartSec: -5.0, DurationSec: 0, Title: "Invalid"); Xunit.Assert.False(invalidClip.IsValid()); } } /// /// 42차: 분산 멀티소스 세그먼트 고속 가속기 및 미러 헬스 가디언 검증. /// public class Milestone42MultiSourceSegmentAcceleratorTests { [Fact] public void 다중_미러_등록_및_청크_라운드로빈_분배_검증() { var accelerator = new MultiSourceSegmentAccelerator(); accelerator.AddMirror("https://cdn1.example.com/stream"); accelerator.AddMirror("https://cdn2.example.com/stream"); var mirror1 = accelerator.SelectBestMirror(chunkIndex: 0); var mirror2 = accelerator.SelectBestMirror(chunkIndex: 1); var mirror3 = accelerator.SelectBestMirror(chunkIndex: 2); Xunit.Assert.Equal("https://cdn1.example.com/stream", mirror1); Xunit.Assert.Equal("https://cdn2.example.com/stream", mirror2); Xunit.Assert.Equal("https://cdn1.example.com/stream", mirror3); } [Fact] public void 응답_실패_시_불량_미러_점수_하락_및_자동_제외() { var accelerator = new MultiSourceSegmentAccelerator(maxFailuresBeforeDrop: 2); accelerator.AddMirror("https://fast-cdn.com"); accelerator.AddMirror("https://slow-failing-cdn.com"); // 불량 미러에 2회 실패 기록 accelerator.RecordFailure("https://slow-failing-cdn.com"); accelerator.RecordFailure("https://slow-failing-cdn.com"); // 정상 미러만 선택되어야 함 var best = accelerator.SelectBestMirror(chunkIndex: 0); Xunit.Assert.Equal("https://fast-cdn.com", best); var best2 = accelerator.SelectBestMirror(chunkIndex: 1); Xunit.Assert.Equal("https://fast-cdn.com", best2); } [Fact] public void 미러별_응답속도_측정_및_가장_빠른_미러_우선순위() { var accelerator = new MultiSourceSegmentAccelerator(); accelerator.AddMirror("https://cdn-100ms.com"); accelerator.AddMirror("https://cdn-10ms.com"); accelerator.RecordLatency("https://cdn-100ms.com", TimeSpan.FromMilliseconds(100)); accelerator.RecordLatency("https://cdn-10ms.com", TimeSpan.FromMilliseconds(10)); var fastest = accelerator.GetFastestMirror(); Xunit.Assert.Equal("https://cdn-10ms.com", fastest); } [Fact] public void 모든_미러_탈락_시_기본_미러_폴백_유지() { var accelerator = new MultiSourceSegmentAccelerator(maxFailuresBeforeDrop: 1); accelerator.AddMirror("https://cdn.only.com"); accelerator.RecordFailure("https://cdn.only.com"); var fallback = accelerator.SelectBestMirror(chunkIndex: 0); Xunit.Assert.Equal("https://cdn.only.com", fallback); // 마지막 하나는 폴백으로 유지 } } /// /// 43차: 무손실 스마트 트림 및 I-프레임 키프레임 정렬 컷팅기 검증. /// public class Milestone43LosslessSmartTrimmerTests { private sealed class RecordingProcessRunner : IProcessRunner { public List Specs { get; } = new(); public Task RunAsync(ProcessSpec spec, Action? onLine, CancellationToken ct) { Specs.Add(spec); return Task.FromResult(new ProcessResult(0, "OK", "")); } } [Fact] public void 가장_가까운_키프레임_스냅_계산_검증() { // 0s, 5s, 10s, 15s, 20s 에 키프레임(I-프레임) 존재 var keyframes = new[] { 0.0, 5.0, 10.0, 15.0, 20.0 }; var snappedStart = LosslessSmartTrimmer.FindNearestKeyframe(keyframes, targetSec: 4.8); Xunit.Assert.Equal(5.0, snappedStart); var snappedStart2 = LosslessSmartTrimmer.FindNearestKeyframe(keyframes, targetSec: 1.2); Xunit.Assert.Equal(0.0, snappedStart2); } [Fact] public async Task 무손실_스트림카피_트리밍_명령어_생성_검증() { var runner = new RecordingProcessRunner(); var trimmer = new LosslessSmartTrimmer(runner); var outPath = await trimmer.TrimLosslessAsync("ffmpeg.exe", "source.mp4", "trimmed.mp4", startSec: 10.0, endSec: 25.0, CancellationToken.None); Xunit.Assert.Equal("trimmed.mp4", outPath); Xunit.Assert.Single(runner.Specs); Xunit.Assert.Contains("-ss", runner.Specs[0].Args); Xunit.Assert.Contains("-to", runner.Specs[0].Args); Xunit.Assert.Contains("-c", runner.Specs[0].Args); Xunit.Assert.Contains("copy", runner.Specs[0].Args); } [Fact] public async Task 시작시간이_종료시간보다_크면_예외() { var runner = new RecordingProcessRunner(); var trimmer = new LosslessSmartTrimmer(runner); await Xunit.Assert.ThrowsAsync(() => trimmer.TrimLosslessAsync("ffmpeg.exe", "source.mp4", "trimmed.mp4", startSec: 30.0, endSec: 10.0, CancellationToken.None)); } [Fact] public void 키프레임_목록이_비어있으면_목표시간_그대로_반환() { var result = LosslessSmartTrimmer.FindNearestKeyframe(Array.Empty(), targetSec: 12.34); Xunit.Assert.Equal(12.34, result); } } /// /// 45차: VAD 음성 활동 감지 및 AI 자막 생성 어댑터 파이프라인 검증. /// public class Milestone45VoiceActivitySubtitleAdapterTests { [Fact] public void 오디오_에너지_기반_음성_발화구간_VAD_검출_검증() { // 1초 단위 오디오 RMS 에너지 (0.0 ~ 1.0) var energyProfile = new[] { 0.01, 0.02, // 무음 0.45, 0.55, 0.60, 0.40, // 발화 1 (2s ~ 5s) 0.02, 0.01, // 무음 0.70, 0.80, 0.65 // 발화 2 (8s ~ 10s) }; var segments = VoiceActivitySubtitleAdapter.DetectSpeechSegments(energyProfile, speechThreshold: 0.1, minDurationSec: 2.0); Xunit.Assert.Equal(2, segments.Count); Xunit.Assert.Equal(2.0, segments[0].StartSec); Xunit.Assert.Equal(6.0, segments[0].EndSec); Xunit.Assert.Equal(8.0, segments[1].StartSec); } [Fact] public void VAD_구간_기반_WebVTT_템플릿_자동_생성_검증() { var segments = new[] { new SpeechSegment(StartSec: 1.5, EndSec: 4.2), new SpeechSegment(StartSec: 5.0, EndSec: 8.8) }; var vtt = VoiceActivitySubtitleAdapter.GenerateVttSkeleton(segments); Xunit.Assert.StartsWith("WEBVTT", vtt); Xunit.Assert.Contains("00:00:01.500 --> 00:00:04.200", vtt); Xunit.Assert.Contains("00:00:05.000 --> 00:00:08.800", vtt); } [Fact] public void 타임코드_밀리초_포맷터_정밀도_검증() { var tc = VoiceActivitySubtitleAdapter.FormatTimecode(3661.123); Xunit.Assert.Equal("01:01:01.123", tc); } [Fact] public void 빈_에너지_프로필_처리_검증() { var empty = VoiceActivitySubtitleAdapter.DetectSpeechSegments(Array.Empty(), speechThreshold: 0.1); Xunit.Assert.Empty(empty); } } /// /// 46차: 지능형 대역폭 예측 및 동적 청크 프리패칭 윈도우 엔진 검증. /// public class Milestone46PredictiveChunkPrefetcherTests { [Fact] public void 다운로드_속도_이동평균_대역폭_예측_검증() { var prefetcher = new PredictiveChunkPrefetcher(initialWindowSize: 3, maxWindowSize: 10); // 속도 샘플 추가 (bytes/sec) prefetcher.RecordChunkDownload(chunkSizeBytes: 1_000_000, duration: TimeSpan.FromMilliseconds(500)); // 2MB/s prefetcher.RecordChunkDownload(chunkSizeBytes: 1_000_000, duration: TimeSpan.FromMilliseconds(250)); // 4MB/s var estimatedSpeed = prefetcher.GetEstimatedSpeedBytesPerSec(); Xunit.Assert.True(estimatedSpeed >= 2_000_000 && estimatedSpeed <= 4_000_000); } [Fact] public void 고속_네트워크_감지_시_프리패치_윈도우_자동_확장() { var prefetcher = new PredictiveChunkPrefetcher(initialWindowSize: 2, maxWindowSize: 8); // 초고속 다운로드 (10MB/s 이상) for (int i = 0; i < 5; i++) { prefetcher.RecordChunkDownload(chunkSizeBytes: 5_000_000, duration: TimeSpan.FromMilliseconds(100)); } var windowSize = prefetcher.CalculateOptimalPrefetchWindow(); Xunit.Assert.True(windowSize >= 5); // 윈도우 확장 } [Fact] public void 네트워크_속도_급락_시_윈도우_크기_축소_방어() { var prefetcher = new PredictiveChunkPrefetcher(initialWindowSize: 6, maxWindowSize: 10); // 저속 다운로드 (100KB/s) for (int i = 0; i < 5; i++) { prefetcher.RecordChunkDownload(chunkSizeBytes: 100_000, duration: TimeSpan.FromMilliseconds(1000)); } var windowSize = prefetcher.CalculateOptimalPrefetchWindow(); Xunit.Assert.True(windowSize <= 2); // 윈도우 축소로 버퍼 낭비 방지 } [Fact] public void 윈도우_경계값_범위_초과_방어_검증() { var prefetcher = new PredictiveChunkPrefetcher(minWindowSize: 1, maxWindowSize: 5); var window = prefetcher.CalculateOptimalPrefetchWindow(); Xunit.Assert.InRange(window, 1, 5); } } /// /// 47차: 비디오 워터마크 & 타임스탬프 번인 엔진 검증. /// public class Milestone47VideoWatermarkBurnerTests { private sealed class RecordingProcessRunner : IProcessRunner { public List Specs { get; } = new(); public Task RunAsync(ProcessSpec spec, Action? onLine, CancellationToken ct) { Specs.Add(spec); return Task.FromResult(new ProcessResult(0, "OK", "")); } } [Fact] public void 우하단_로고_워터마크_필터_문자열_생성_검증() { var filter = VideoWatermarkBurner.BuildOverlayFilter(WatermarkPosition.BottomRight, margin: 20, opacity: 0.8); Xunit.Assert.Contains("overlay=W-w-20:H-h-20", filter); Xunit.Assert.Contains("format=auto", filter); } [Fact] public void 텍스트_타임스탬프_번인_필터_생성_검증() { var filter = VideoWatermarkBurner.BuildDrawTextFilter("VideoDownloader Archival", fontSize: 24, fontColor: "white"); Xunit.Assert.Contains("drawtext=text='VideoDownloader Archival'", filter); Xunit.Assert.Contains("fontsize=24", filter); Xunit.Assert.Contains("fontcolor=white", filter); } [Fact] public async Task 워터마크_인코딩_명령어_파이프라인_실행_검증() { var runner = new RecordingProcessRunner(); var burner = new VideoWatermarkBurner(runner); var spec = new WatermarkSpec( WatermarkImagePath: "logo.png", Position: WatermarkPosition.TopLeft, Margin: 15, Opacity: 0.7 ); var outPath = await burner.ApplyWatermarkAsync("ffmpeg.exe", "input.mp4", "watermarked.mp4", spec, CancellationToken.None); Xunit.Assert.Equal("watermarked.mp4", outPath); Xunit.Assert.Single(runner.Specs); Xunit.Assert.Contains("-filter_complex", runner.Specs[0].Args); Xunit.Assert.Contains("overlay=15:15", runner.Specs[0].Args.First(a => a.Contains("overlay="))); } [Fact] public void 유효하지_않은_투명도_설정_예외_검증() { Xunit.Assert.Throws(() => VideoWatermarkBurner.BuildOverlayFilter(WatermarkPosition.Center, margin: 10, opacity: 1.5)); } } /// /// 48차: 360도 VR 및 Ambisonics 공간 오디오(Spatial Audio) 메타데이터 보존 엔진 검증. /// public class Milestone48VrSpatialAudioEngineTests { private sealed class RecordingProcessRunner : IProcessRunner { public List Specs { get; } = new(); public Task RunAsync(ProcessSpec spec, Action? onLine, CancellationToken ct) { Specs.Add(spec); return Task.FromResult(new ProcessResult(0, "OK", "")); } } [Fact] public void VR360_Equirectangular_투영_메타데이터_판별_검증() { var isVr = VrSpatialAudioEngine.IsVr360Content(projectionType: "equirectangular", stereoMode: "left-right"); Xunit.Assert.True(isVr); var isNotVr = VrSpatialAudioEngine.IsVr360Content(projectionType: "flat", stereoMode: "mono"); Xunit.Assert.False(isNotVr); } [Fact] public void Ambisonics_4채널_공간음향_채널레이아웃_판별_검증() { var isSpatial = VrSpatialAudioEngine.IsSpatialAudio(audioChannels: 4, channelLayout: "ambisonic/quad"); Xunit.Assert.True(isSpatial); var isStereo = VrSpatialAudioEngine.IsSpatialAudio(audioChannels: 2, channelLayout: "stereo"); Xunit.Assert.False(isStereo); } [Fact] public async Task VR_공간메타데이터_보존_MP4_인젝션_명령어_검증() { var runner = new RecordingProcessRunner(); var engine = new VrSpatialAudioEngine(runner); var vrMeta = new VrSpatialMetadata( Projection: "equirectangular", StereoMode: "mono", AmbisonicOrder: 1 ); var outPath = await engine.InjectVrMetadataAsync("ffmpeg.exe", "source_vr.mp4", "injected_vr.mp4", vrMeta, CancellationToken.None); Xunit.Assert.Equal("injected_vr.mp4", outPath); Xunit.Assert.Single(runner.Specs); Xunit.Assert.Contains("-c", runner.Specs[0].Args); Xunit.Assert.Contains("copy", runner.Specs[0].Args); } [Fact] public void 투영방식이_비어있으면_일반_영상으로_판정() { var isVr = VrSpatialAudioEngine.IsVr360Content(null, null); Xunit.Assert.False(isVr); } } /// /// 49차: 비디오 노이즈 제거 및 AI 업스케일러 어댑터 파이프라인 검증. /// public class Milestone49VideoDenoiseUpscalerTests { private sealed class RecordingProcessRunner : IProcessRunner { public List Specs { get; } = new(); public Task RunAsync(ProcessSpec spec, Action? onLine, CancellationToken ct) { Specs.Add(spec); return Task.FromResult(new ProcessResult(0, "OK", "")); } } [Fact] public void 디노이즈_강도별_FFmpeg_hqdn3d_필터_생성_검증() { var filterLight = VideoDenoiseUpscaler.BuildDenoiseFilter(DenoiseStrength.Light); Xunit.Assert.Equal("hqdn3d=2.0:1.5:2.0:1.5", filterLight); var filterHeavy = VideoDenoiseUpscaler.BuildDenoiseFilter(DenoiseStrength.Heavy); Xunit.Assert.Equal("hqdn3d=6.0:4.5:6.0:4.5", filterHeavy); } [Fact] public void AI_초해상도_Lanczos_업스케일_필터_생성_검증() { var filter = VideoDenoiseUpscaler.BuildUpscaleFilter(targetWidth: 1920, targetHeight: 1080, useUnsharp: true); Xunit.Assert.Contains("scale=1920:1080:flags=lanczos", filter); Xunit.Assert.Contains("unsharp=5:5:1.0:5:5:0.0", filter); } [Fact] public async Task 디노이즈_업스케일_통합_인코딩_프로세스_실행_검증() { var runner = new RecordingProcessRunner(); var upscaler = new VideoDenoiseUpscaler(runner); var spec = new DenoiseUpscaleSpec( Denoise: DenoiseStrength.Medium, TargetWidth: 1920, TargetHeight: 1080, ApplyUnsharpMask: true ); var outPath = await upscaler.EnhanceVideoAsync("ffmpeg.exe", "low_res.mp4", "enhanced.mp4", spec, CancellationToken.None); Xunit.Assert.Equal("enhanced.mp4", outPath); Xunit.Assert.Single(runner.Specs); Xunit.Assert.Contains("-vf", runner.Specs[0].Args); Xunit.Assert.Contains("hqdn3d", runner.Specs[0].Args.First(a => a.Contains("hqdn3d"))); Xunit.Assert.Contains("scale=1920:1080", runner.Specs[0].Args.First(a => a.Contains("scale="))); } [Fact] public void 디노이즈_없음_지정_시_스케일만_적용() { var spec = new DenoiseUpscaleSpec(Denoise: DenoiseStrength.None, TargetWidth: 1280, TargetHeight: 720); var combinedFilter = VideoDenoiseUpscaler.BuildCombinedFilter(spec); Xunit.Assert.DoesNotContain("hqdn3d", combinedFilter); Xunit.Assert.Contains("scale=1280:720", combinedFilter); } } /// /// Phase 61: P2P Swarm & Multi-CDN Mesh Downloader 전방위 검증 테스트 스위트. /// WebTorrent/BitTorrent Merkle 트리 피스 검증, 비트필드 조립, 다중 CDN 세그먼트 레이싱을 검증합니다. /// public class P2pSwarmAndMultiCdnPhase61Tests { [Fact] public void P2pSwarm_매니페스트_생성_및_Merkle_루트_해시_계산_검증() { byte[] payload = new byte[1024 * 1024 + 500]; // 약 1MB + 500바이트 new Random(42).NextBytes(payload); var manifest = P2pSwarmEngine.CreateManifestFromBytes("sample_video.mp4", payload, pieceLength: 256 * 1024); Xunit.Assert.Equal("sample_video.mp4", manifest.Name); Xunit.Assert.Equal(payload.Length, manifest.TotalSize); Xunit.Assert.Equal(5, manifest.PieceHashes.Count); // 5개 피스 Xunit.Assert.False(string.IsNullOrEmpty(manifest.MerkleRoot)); Xunit.Assert.False(string.IsNullOrEmpty(manifest.InfoHash)); } [Fact] public void P2pSwarm_피스_주입_해시_무결성_검증_및_손상_데이터_거부() { byte[] payload = new byte[512 * 1024]; new Random(99).NextBytes(payload); var manifest = P2pSwarmEngine.CreateManifestFromBytes("chunk.ts", payload, pieceLength: 256 * 1024); var engine = new P2pSwarmEngine(); engine.InitializeSession(manifest); Xunit.Assert.Equal(0, engine.VerifiedPieceCount); Xunit.Assert.Equal(2, engine.GetMissingPieceIndices().Count); // 1. 유효 피스 0 주입 byte[] piece0 = new byte[256 * 1024]; Buffer.BlockCopy(payload, 0, piece0, 0, piece0.Length); bool ok0 = engine.IngestPieceData(0, piece0); Xunit.Assert.True(ok0); Xunit.Assert.Equal(1, engine.VerifiedPieceCount); Xunit.Assert.Equal(50.0, engine.ProgressPercentage); // 2. 손상된 피스 1 주입 (거부되어야 함) byte[] corruptPiece1 = new byte[256 * 1024]; Buffer.BlockCopy(payload, 256 * 1024, corruptPiece1, 0, corruptPiece1.Length); corruptPiece1[100] ^= 0xFF; // 비트 플립 bool corruptResult = engine.IngestPieceData(1, corruptPiece1); Xunit.Assert.False(corruptResult); Xunit.Assert.Equal(1, engine.VerifiedPieceCount); // 3. 정상 피스 1 주입 및 전체 페이로드 조립 byte[] validPiece1 = new byte[256 * 1024]; Buffer.BlockCopy(payload, 256 * 1024, validPiece1, 0, validPiece1.Length); bool ok1 = engine.IngestPieceData(1, validPiece1); Xunit.Assert.True(ok1); Xunit.Assert.Equal(2, engine.VerifiedPieceCount); Xunit.Assert.Equal(100.0, engine.ProgressPercentage); byte[]? assembled = engine.AssemblePayload(); Xunit.Assert.NotNull(assembled); Xunit.Assert.Equal(payload, assembled); } [Fact] public async Task MultiCdn_다중_미러_세그먼트_레이싱_및_승자_선택_검증() { var handler = new MockHttpMessageHandler(); var httpClient = new HttpClient(handler); var downloader = new MultiCdnMeshDownloader(httpClient); var mirrorFast = new Uri("https://cdn1.fast.com/seg001.ts"); var mirrorSlow = new Uri("https://cdn2.slow.com/seg001.ts"); handler.RegisterResponse(mirrorFast, new byte[] { 0x47, 0x01, 0x02 }, delayMs: 10); handler.RegisterResponse(mirrorSlow, new byte[] { 0x47, 0x01, 0x02 }, delayMs: 200); downloader.RegisterMirror(new Uri("https://cdn1.fast.com")); downloader.RegisterMirror(new Uri("https://cdn2.slow.com")); var result = await downloader.RaceDownloadSegmentAsync(new[] { mirrorFast, mirrorSlow }, staggerDelayMs: 20); Xunit.Assert.True(result.IsSuccess); Xunit.Assert.Equal(mirrorFast, result.WinningMirror); Xunit.Assert.Equal(3, result.Data.Length); Xunit.Assert.Equal(0x47, result.Data[0]); var ranked = downloader.GetRankedMirrors(); Xunit.Assert.Equal(2, ranked.Count); Xunit.Assert.True(ranked[0].HealthScore >= ranked[1].HealthScore); } private sealed class MockHttpMessageHandler : HttpMessageHandler { private readonly Dictionary _responses = new(); public void RegisterResponse(Uri uri, byte[] data, int delayMs = 0, int statusCode = 200) { _responses[uri.ToString()] = (data, delayMs, statusCode); } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var url = request.RequestUri?.ToString() ?? ""; if (_responses.TryGetValue(url, out var item)) { if (item.DelayMs > 0) { await Task.Delay(item.DelayMs, cancellationToken); } var resp = new HttpResponseMessage((System.Net.HttpStatusCode)item.StatusCode); resp.Content = new ByteArrayContent(item.Data); return resp; } return new HttpResponseMessage(System.Net.HttpStatusCode.NotFound); } } } /// /// Phase 62: On-Device AI Neural Media Lab 전방위 검증 테스트 스위트. /// Whisper 자막 생성, SRT/VTT 포맷팅, AI 숏폼 하이라이트 감지, 신경망 셰이더 업스케일러를 검증합니다. /// public class OnDeviceAiNeuralMediaLabPhase62Tests { [Fact] public void NeuralSubtitle_음성인식_단어별_타임스탬프_및_SRT_VTT_변환_검증() { var text = "안녕하세요. 파카 미디어 브라우저입니다. 차세대 확장 기능을 테스트합니다."; var result = NeuralSubtitleEngine.CreateMockTranscription(text, totalDurationSeconds: 12.0, language: "ko"); Xunit.Assert.Equal("ko", result.Language); Xunit.Assert.Equal(12.0, result.DurationSeconds); Xunit.Assert.Equal(3, result.Segments.Count); var seg1 = result.Segments[0]; Xunit.Assert.Equal("안녕하세요", seg1.Text); Xunit.Assert.Equal(0.0, seg1.StartSeconds); Xunit.Assert.Equal(4.0, seg1.EndSeconds); Xunit.Assert.Single(seg1.Words); // SRT Export var srt = result.ToSrt(); Xunit.Assert.Contains("00:00:00,000 --> 00:00:04,000", srt); Xunit.Assert.Contains("안녕하세요", srt); // VTT Export var vtt = result.ToVtt(); Xunit.Assert.StartsWith("WEBVTT", vtt); Xunit.Assert.Contains("00:00:00.000 --> 00:00:04.000", vtt); } [Fact] public void AiHighlightClipper_하이라이트_구간_점수_계산_및_랭킹_검증() { // 120초 비디오, 60초 지점에서 오디오 에너지와 씬 전환이 가장 높음 var highlights = AiHighlightClipper.DetectHighlights( totalDurationSeconds: 120.0, audioEnergyProvider: sec => sec >= 50 && sec <= 80 ? 0.95 : 0.2, sceneChangeProvider: sec => sec >= 50 && sec <= 80 ? 0.85 : 0.1, windowSeconds: 30.0 ); Xunit.Assert.Equal(4, highlights.Count); // 1등 하이라이트가 60~90초 또는 30~60초 구간이어야 함 var best = highlights[0]; Xunit.Assert.True(best.TotalScore > 0.6); Xunit.Assert.Contains("하이라이트", best.SuggestedTitle); } [Fact] public void AiHighlightClipper_9대16_수직_쇼츠_FFmpeg_인자_배열_검증() { var spec = new ShortsClipSpec { StartSeconds = 45.5, DurationSeconds = 55.0, VerticalCrop9x16 = true, Title = "Viral Shorts" }; var args = AiHighlightClipper.BuildFfmpegShortsCommand("input_1080p.mp4", "shorts_9x16.mp4", spec); Xunit.Assert.Contains("-ss", args); Xunit.Assert.Contains("45.50", args); Xunit.Assert.Contains("-t", args); Xunit.Assert.Contains("55.00", args); Xunit.Assert.Contains("-vf", args); Xunit.Assert.Contains(args, a => a.Contains("crop=ih*9/16")); Xunit.Assert.Equal("shorts_9x16.mp4", args.Last()); } [Fact] public void NeuralShaderUpscaler_HLSL_셰이더_코드_생성_검증() { var config = new ShaderUpscaleConfig { Algorithm = UpscalerAlgorithm.Anime4K_Ultra, TargetWidth = 3840, TargetHeight = 2160, Sharpness = 1.2 }; var hlsl = NeuralShaderUpscaler.GenerateHlslShaderCode(config); Xunit.Assert.Contains("Anime4K_Ultra", hlsl); Xunit.Assert.Contains("3840x2160", hlsl); Xunit.Assert.Contains("PS_NeuralUpscale", hlsl); Xunit.Assert.Contains("Texture2D", hlsl); } } /// /// Phase 65: Ultra-Low Latency Casting & Dynamic Transcoder Studio 전방위 검증 테스트 스위트. /// public class UltraLowLatencyCastingAndTranscoderPhase65Tests { [Fact] public void WebRtcCastingEngine_세션_생성_오퍼_SDP_발행_및_종료_검증() { var engine = new Paca.Core.Media.WebRtcCastingEngine(); var session = engine.CreateCastSession("LG OLED 4K TV"); Assert.NotNull(session.SessionId); Assert.Equal("LG OLED 4K TV", session.TargetDeviceName); Assert.True(session.IsConnected); Assert.True(session.EstimatedLatencyMs < 50.0); var offer = engine.GenerateOffer(session.SessionId, 1920, 1080, 60); Assert.Equal("offer", offer.Type); Assert.Contains("v=0", offer.Sdp); Assert.Contains("H264/90000", offer.Sdp); bool terminated = engine.TerminateSession(session.SessionId); Assert.True(terminated); } [Fact] public void HardwareTranscoderStudio_NVENC_AV1_10Bit_HDR_FFmpeg_인자_배열_검증() { var spec = new Paca.Core.Media.TranscodePresetSpec { Backend = Paca.Core.Media.HardwareEncoderBackend.NvidiaNvenc, Codec = Paca.Core.Media.VideoCodecTarget.AV1, TargetBitrateKbps = 8000, Enable10BitHdr = true, AudioCodec = "libopus", AudioBitrateKbps = 256 }; var args = Paca.Core.Media.HardwareTranscoderStudio.BuildFfmpegTranscodeCommand("raw_4k.mkv", "out_av1_hdr.mp4", spec); Assert.Contains("-i", args); Assert.Contains("av1_nvenc", args); Assert.Contains("yuv420p10le", args); Assert.Contains("bt2020", args); Assert.Contains("8000k", args); Assert.Contains("libopus", args); Assert.Equal("out_av1_hdr.mp4", args.Last()); } [Fact] public void FftAudioWaveformEngine_10밴드_이퀄라이저_프리셋_및_FFmpeg_필터_문자열_검증() { var bassBoost = Paca.Core.Media.FftAudioWaveformEngine.GetPresetBands(Paca.Core.Media.EqualizerPreset.BassBoost); Assert.Equal(10, bassBoost.Count); Assert.Equal(6.0, bassBoost[0].GainDb); // 32Hz +6dB var filter = Paca.Core.Media.FftAudioWaveformEngine.BuildFfmpegEqualizerFilter(bassBoost); Assert.Contains("equalizer=f=32:t=q:w=1:g=6.0", filter); Assert.Contains("equalizer=f=16000:t=q:w=1:g=0.0", filter); } }