VideoConcat/Services/Video/VideoProcess.cs
2026-01-01 15:39:54 +08:00

389 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
{
/// <summary>
/// 同步删除视频指定帧和对应音频片段
/// </summary>
/// <param name="inputPath">输入文件路径</param>
/// <param name="outputPath">输出文件路径</param>
/// <param name="frameNumber">要删除的帧编号从1开始</param>
/// <returns>操作是否成功</returns>
public static async Task<bool> RemoveFrameRandomeAsync(string inputPath, string outputPath)
{
if (!File.Exists(inputPath))
return false;
// 创建临时目录
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
try
{
// 1. 获取视频信息
IMediaAnalysis mediaInfo = await FFProbe.AnalyseAsync(inputPath);
var videoStream = mediaInfo.PrimaryVideoStream;
if (videoStream == null)
{
Console.WriteLine("没有找到视频流");
return false;
}
bool isHevc = videoStream.CodecName == "hevc";
Directory.CreateDirectory(tempDir);
if (isHevc)
{
try
{
// 临时文件路径
string videoConvert = Path.Combine(tempDir, $"{Guid.NewGuid()}.mp4");
await FFMpegArguments.FromFileInput(inputPath)
.OutputToFile(videoConvert, true, opt => // 设置输出格式
opt.WithVideoCodec("libx264")
).ProcessAsynchronously();
mediaInfo = await FFProbe.AnalyseAsync(videoConvert);
inputPath = videoConvert;
}
catch (Exception)
{
throw new Exception("转换失败!");
}
}
// 1. 获取视频信息
mediaInfo = await FFProbe.AnalyseAsync(inputPath);
videoStream = mediaInfo.PrimaryVideoStream;
if (videoStream == null)
{
Console.WriteLine("没有找到视频流");
return false;
}
// 视频总时长(秒)
double totalDuration = mediaInfo.Duration.TotalSeconds;
double frameRate = videoStream.FrameRate;
double frameDuration = Math.Round(1.0 / frameRate, 6); // 一帧时长(秒)
int totalFram = (int)(totalDuration * frameRate);
// 确保视频时长足够至少20秒
if (totalDuration < 20)
{
LogUtils.Error($"视频时长太短({totalDuration}秒),无法抽帧");
return false;
}
var random = new Random();
// 随机选择要删除的帧时间点(避开开头和结尾)
int maxFrameTime = (int)totalDuration - 5; // 确保有足够的空间
int minFrameTime = 20; // 从20秒开始
if (maxFrameTime <= minFrameTime)
{
maxFrameTime = minFrameTime + 1;
}
var randomFrame = random.Next(minFrameTime, maxFrameTime);
//return RemoveVideoFrame(inputPath, outputPath, randomFrame);
string videoPart1 = Path.Combine(tempDir, $"{Guid.NewGuid()}.mp4");
string videoPart2 = Path.Combine(tempDir, $"{Guid.NewGuid()}.mp4");
LogUtils.Info($"开始抽帧:输入={inputPath}, 输出={outputPath}, 删除帧时间={randomFrame}秒");
bool hasSubVideo1 = SubVideo(inputPath, videoPart1, 0, randomFrame - 0.016);
if (!hasSubVideo1)
{
LogUtils.Error($"裁剪第一部分视频失败:{videoPart1}");
return false;
}
bool hasSubVideo2 = SubVideo(inputPath, videoPart2, randomFrame, totalDuration);
if (!hasSubVideo2)
{
LogUtils.Error($"裁剪第二部分视频失败:{videoPart2}");
return false;
}
LogUtils.Info($"视频裁剪成功,开始合并:{videoPart1} + {videoPart2} -> {outputPath}");
bool isJoinSuccess = JoinVideo(outputPath, [videoPart1, videoPart2]);
if (!isJoinSuccess)
{
LogUtils.Error($"合并视频失败:{outputPath}");
return false;
}
// 验证输出文件是否存在
if (File.Exists(outputPath))
{
LogUtils.Info($"抽帧成功:{outputPath}");
return true;
}
else
{
LogUtils.Error($"抽帧失败:输出文件不存在 - {outputPath}");
return false;
}
}
catch (Exception ex)
{
LogUtils.Error("抽帧失败", ex);
Console.WriteLine($"操作失败: {ex.Message}");
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
{
// 忽略清理错误
}
}
}
/// <summary>
/// 通过修改视频元数据添加注释改变MD5
/// </summary>
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<bool> 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();
}
}
}