using FFMpegCore; using FFMpegCore.Enums; using Standard; using System; using System.IO; using System.Threading.Tasks; using VideoConcat.Common.Tools; using static System.Windows.Forms.DataFormats; namespace VideoConcat.Services.Video { public class VideoProcess { /// /// 在前30%、中间30%、后30%三个位置各随机删除一帧,确保通过广告平台相似度检查 /// /// 输入文件路径 /// 输出文件路径 /// 操作是否成功 public static async Task RemoveFrameRandomeAsync(string inputPath, string outputPath) { if (!File.Exists(inputPath)) { LogUtils.Error($"输入文件不存在:{inputPath}"); return false; } // 创建临时目录 string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); try { Directory.CreateDirectory(tempDir); // 1. 获取视频信息 IMediaAnalysis mediaInfo = await FFProbe.AnalyseAsync(inputPath); var videoStream = mediaInfo.PrimaryVideoStream; if (videoStream == null) { LogUtils.Error("没有找到视频流"); return false; } // 视频总时长(秒) double totalDuration = mediaInfo.Duration.TotalSeconds; double frameRate = videoStream.FrameRate; // 确保视频时长足够(至少20秒) if (totalDuration < 20) { LogUtils.Error($"视频时长太短({totalDuration:F2}秒),无法抽帧(需要至少20秒)"); return false; } // 计算总帧数 int totalFrames = (int)(totalDuration * frameRate); if (totalFrames <= 0) { LogUtils.Error($"无法计算视频总帧数,帧率={frameRate}"); return false; } // 如果是 HEVC 编码,先转换为 H.264(select 过滤器在某些编码格式下可能不稳定) bool isHevc = videoStream.CodecName == "hevc"; string workingInputPath = inputPath; if (isHevc) { try { string videoConvert = Path.Combine(tempDir, $"{Guid.NewGuid()}.mp4"); LogUtils.Info($"检测到 HEVC 编码,先转换为 H.264:{inputPath} -> {videoConvert}"); await FFMpegArguments.FromFileInput(inputPath) .OutputToFile(videoConvert, true, opt => opt.WithVideoCodec("libx264") .WithAudioCodec("copy") // 复制音频流,不重新编码 ).ProcessAsynchronously(); // 重新分析转换后的视频 mediaInfo = await FFProbe.AnalyseAsync(videoConvert); videoStream = mediaInfo.PrimaryVideoStream; if (videoStream == null) { LogUtils.Error("转换后没有找到视频流"); return false; } totalDuration = mediaInfo.Duration.TotalSeconds; frameRate = videoStream.FrameRate; totalFrames = (int)(totalDuration * frameRate); workingInputPath = videoConvert; LogUtils.Info($"HEVC 转换完成,新帧率={frameRate}, 总帧数={totalFrames}"); } catch (Exception ex) { LogUtils.Error("HEVC 转换失败", ex); return false; } } // 2. 计算三个区域的范围(前30%、中间30%、后30%) // 前30%:0% 到 30% int front30StartFrame = 0; int front30EndFrame = (int)(totalFrames * 0.30); // 中间30%:35% 到 65%(避开边界) int middle30StartFrame = (int)(totalFrames * 0.35); int middle30EndFrame = (int)(totalFrames * 0.65); // 后30%:70% 到 100% int back30StartFrame = (int)(totalFrames * 0.70); int back30EndFrame = totalFrames - 1; // 最后一帧 // 确保范围有效 if (front30EndFrame <= front30StartFrame) front30EndFrame = front30StartFrame + 1; if (middle30EndFrame <= middle30StartFrame) middle30EndFrame = middle30StartFrame + 1; if (back30EndFrame <= back30StartFrame) back30EndFrame = back30StartFrame + 1; // 使用时间戳和文件路径作为随机种子,确保每次处理都不同 var random = new Random((int)(DateTime.Now.Ticks % int.MaxValue) + inputPath.GetHashCode()); // 3. 在每个区域随机选择一帧删除 int frame1 = random.Next(front30StartFrame, front30EndFrame + 1); // 前30% int frame2 = random.Next(middle30StartFrame, middle30EndFrame + 1); // 中间30% int frame3 = random.Next(back30StartFrame, back30EndFrame + 1); // 后30% // 确保三帧不重复(如果重复,调整其中一个) if (frame2 == frame1) frame2 = (frame2 + 1) % totalFrames; if (frame3 == frame1 || frame3 == frame2) { frame3 = (frame3 + 1) % totalFrames; if (frame3 == frame1 || frame3 == frame2) frame3 = (frame3 + 1) % totalFrames; } double time1 = frame1 / frameRate; double time2 = frame2 / frameRate; double time3 = frame3 / frameRate; LogUtils.Info($"开始精确抽帧:输入={inputPath}, 输出={outputPath}, 总帧数={totalFrames}"); LogUtils.Info($"删除帧1(前30%):帧编号={frame1}, 时间≈{time1:F2}秒"); LogUtils.Info($"删除帧2(中间30%):帧编号={frame2}, 时间≈{time2:F2}秒"); LogUtils.Info($"删除帧3(后30%):帧编号={frame3}, 时间≈{time3:F2}秒"); // 4. 只删除视频帧,音频直接复制,使用最快编码设置以达到1秒内完成 // 注意:使用 select 过滤器删除帧时,视频需要重新编码,但使用最快设置 LogUtils.Info("使用最快模式删除帧(仅处理视频,音频直接复制)..."); var ffmpegArgs = FFMpegArguments .FromFileInput(workingInputPath) .OutputToFile(outputPath, true, options => { // 使用 select 过滤器删除三帧(只删除视频帧) options.WithCustomArgument($"-vf select='not(eq(n\\,{frame1})+eq(n\\,{frame2})+eq(n\\,{frame3}))'"); // 视频编码:使用最快设置 options.WithVideoCodec("libx264"); options.WithCustomArgument("-preset ultrafast"); // 最快编码预设 options.WithCustomArgument("-tune zerolatency"); // 零延迟调优 options.WithConstantRateFactor(28); // 质量设置(28比23快) options.WithCustomArgument("-g 30"); // 减少关键帧间隔 options.WithCustomArgument("-threads 0"); // 使用所有CPU核心 options.WithCustomArgument("-vsync cfr"); // 恒定帧率 // 音频:直接复制,不重新编码(最快,不处理) options.WithAudioCodec("copy"); // 直接复制音频流,零处理时间 }); bool success = ffmpegArgs.ProcessSynchronously(); // 验证输出文件是否存在且有效 if (success && File.Exists(outputPath)) { FileInfo fileInfo = new FileInfo(outputPath); if (fileInfo.Length > 0) { // 验证输出视频的帧数是否正确(应该比原视频少3帧) try { var outputMediaInfo = await FFProbe.AnalyseAsync(outputPath); var outputVideoStream = outputMediaInfo.PrimaryVideoStream; if (outputVideoStream != null) { int expectedFrames = totalFrames - 3; double outputDuration = outputMediaInfo.Duration.TotalSeconds; int actualFrames = (int)(outputDuration * outputVideoStream.FrameRate); LogUtils.Info($"抽帧完成:输出文件={outputPath}, 大小={fileInfo.Length / 1024 / 1024:F2}MB, 时长={outputDuration:F2}秒, 帧数≈{actualFrames} (预期少3帧)"); // 验证时长是否合理(应该略小于原视频) if (outputDuration >= totalDuration) { LogUtils.Warn($"警告:输出视频时长({outputDuration:F2}秒) >= 原视频时长({totalDuration:F2}秒),可能未成功删除帧"); } } } catch (Exception ex) { LogUtils.Warn($"验证输出视频信息时出错(不影响结果):{ex.Message}"); } return true; } else { LogUtils.Error($"输出文件存在但大小为0:{outputPath}"); return false; } } else { LogUtils.Error($"抽帧失败:success={success}, 文件存在={File.Exists(outputPath)}, 输出路径={outputPath}"); return false; } } catch (Exception ex) { LogUtils.Error("抽帧过程发生异常", ex); return false; } finally { // 清理临时目录 try { if (Directory.Exists(tempDir)) { string[] files = Directory.GetFiles(tempDir); foreach (string file in files) { try { File.Delete(file); } catch { // 忽略删除失败 } } try { Directory.Delete(tempDir); } catch { // 忽略删除失败 } } } catch { // 忽略清理错误 } } } /// /// 通过修改视频元数据(添加注释)改变MD5 /// public static bool ModifyByMetadata(string inputPath, string outputPath, string comment = "JSY") { // 添加或修改视频元数据中的注释信息 return FFMpegArguments .FromFileInput(inputPath) .OutputToFile(outputPath, true, options => options //.WithVideoCodec("copy") // 直接复制视频流,不重新编码 //.WithAudioCodec("copy") // 直接复制音频流 .CopyChannel() .WithCustomArgument($"-metadata comment=\"{comment}_{Guid.NewGuid()}\"") // 添加唯一注释 ) .ProcessSynchronously(); } public static bool SubVideo(string inputPath, string outputPath, Double startSec, Double endSec) { try { // 计算时长 double duration = endSec - startSec; if (duration <= 0) { LogUtils.Error($"SubVideo失败:时长无效,开始={startSec}秒, 结束={endSec}秒"); return false; } LogUtils.Info($"SubVideo:输入={inputPath}, 输出={outputPath}, 开始={startSec}秒, 结束={endSec}秒, 时长={duration}秒"); // 使用 -ss 和 -t 参数进行裁剪 bool result = FFMpegArguments .FromFileInput(inputPath, true, options => options .Seek(TimeSpan.FromSeconds(startSec)) .WithCustomArgument($"-t {duration}")) // 指定时长而不是结束时间 .OutputToFile(outputPath, true, options => options .CopyChannel()) // 复制流,不重新编码 .ProcessSynchronously(); // 验证输出文件是否存在 if (result && File.Exists(outputPath)) { FileInfo fileInfo = new FileInfo(outputPath); LogUtils.Info($"SubVideo成功:{outputPath}, 文件大小={fileInfo.Length / 1024 / 1024}MB"); return true; } else { LogUtils.Error($"SubVideo失败:输入={inputPath}, 输出={outputPath}, 开始={startSec}秒, 结束={endSec}秒, 结果={result}, 文件存在={File.Exists(outputPath)}"); return false; } } catch (Exception ex) { LogUtils.Error($"SubVideo异常:输入={inputPath}, 输出={outputPath}", ex); return false; } } public static bool SubAudio(string inputPath, string outputPath, Double startSec, Double endSec) { return FFMpegArguments .FromFileInput(inputPath, true, options => options.Seek(TimeSpan.FromSeconds(startSec)).EndSeek(TimeSpan.FromSeconds(endSec))) .OutputToFile(outputPath, true, options => options.CopyChannel()) .ProcessSynchronously(); } public static bool JoinVideo(string outPutPath, string[] videoParts) { try { // 验证所有输入文件是否存在 foreach (var part in videoParts) { if (!File.Exists(part)) { LogUtils.Error($"JoinVideo失败:输入文件不存在 - {part}"); return false; } } bool result = FFMpeg.Join(outPutPath, videoParts); // 验证输出文件是否存在 if (result && File.Exists(outPutPath)) { LogUtils.Info($"JoinVideo成功:{outPutPath}"); return true; } else { LogUtils.Error($"JoinVideo失败:输出文件不存在 - {outPutPath}"); return false; } } catch (Exception ex) { LogUtils.Error($"JoinVideo异常:输出={outPutPath}", ex); return false; } } public async Task ProcessVideo(string inputPath, string outputPath, string tempDir) { // 1. 获取视频信息 IMediaAnalysis mediaInfo = await FFProbe.AnalyseAsync(inputPath); var videoStream = mediaInfo.PrimaryVideoStream; if (videoStream == null) { Console.WriteLine("没有找到视频流"); return false; } // 视频总时长(秒) var totalDuration = mediaInfo.Duration.TotalSeconds; // 计算帧时间参数 double frameRate = videoStream.FrameRate; double frameDuration = Math.Round(1.0 / frameRate, 6); // 一帧时长(秒) int totalFram = (int)(totalDuration * frameRate); // 2. 随机生成要删除的帧的时间点(避开最后一帧,防止越界) var random = new Random(); var randomFrame = random.Next(20, totalFram - 10); double frameTime = Math.Round((randomFrame - 1) * frameDuration, 6); // 目标帧开始时间 double nextFrameTime = Math.Round((randomFrame + 1) * frameDuration, 6); // 下一帧开始时间 // 临时文件路径 string videoPart1 = Path.Combine(tempDir, $"{Guid.NewGuid()}.mp4"); string videoPart2 = Path.Combine(tempDir, $"{Guid.NewGuid()}.mp4"); string audioPath = Path.Combine(tempDir, $"{Guid.NewGuid()}.aac"); string videoNoaudioPath = Path.Combine(tempDir, $"{Guid.NewGuid()}.mp4"); string finalyPath = Path.Combine(tempDir, $"{Guid.NewGuid()}.mp4"); string audioPart1 = Path.Combine(tempDir, $"{Guid.NewGuid()}.aac"); string audioPart2 = Path.Combine(tempDir, $"{Guid.NewGuid()}.aac"); // 分离视频流(不含音频) await FFMpegArguments .FromFileInput(inputPath) .OutputToFile(videoNoaudioPath, true, opt => opt.WithCustomArgument("-an -c:v copy")) .ProcessAsynchronously(); if (!File.Exists(videoNoaudioPath)) return false; // 分离音频流(不含视频)(如有音频) if (mediaInfo.PrimaryAudioStream != null) { await FFMpegArguments .FromFileInput(inputPath) .OutputToFile(audioPath, true, opt => opt.WithCustomArgument("-vn -c:a copy")) .ProcessAsynchronously(); if (!File.Exists(audioPath)) return false; } inputPath = videoNoaudioPath; bool hasSubVideo1 = SubVideo(inputPath, videoPart1, 0, frameTime); bool hasSubVideo2 = SubVideo(inputPath, videoPart2, nextFrameTime, totalDuration); if (!hasSubVideo1 || !hasSubVideo2) { return false; } bool isJoinSuccess = JoinVideo(finalyPath, [videoPart1, videoPart2]); if (!isJoinSuccess) { return false; } return await FFMpegArguments.FromFileInput(finalyPath) .AddFileInput(audioPath) .OutputToFile(outputPath, true, opt => opt.WithCustomArgument("-c copy -shortest -y")) .ProcessAsynchronously(); } public static bool RemoveVideoFrame(string inputPath, string outputPath, int frameToRemove) { // 使用FFMpegArguments构建转换流程 return FFMpegArguments .FromFileInput(inputPath) .OutputToFile(outputPath, true, options => options // 使用select过滤器排除指定帧(帧序号从0开始) .WithCustomArgument($"select=not(eq(n\\,{frameToRemove}))") // $"not(eq(n\\,{frameToRemove}))"); // 注意:在C#中需要转义反斜杠 //// 保持其他编码参数 .WithVideoCodec("libx264") .WithAudioCodec("copy") // 如果不需要处理音频,直接复制 ) .ProcessSynchronously(); } } }