update
This commit is contained in:
parent
cf99275df8
commit
598d51eba4
@ -12,132 +12,210 @@ namespace VideoConcat.Services.Video
|
|||||||
public class VideoProcess
|
public class VideoProcess
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 同步删除视频指定帧和对应音频片段
|
/// 在前30%、中间30%、后30%三个位置各随机删除一帧,确保通过广告平台相似度检查
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="inputPath">输入文件路径</param>
|
/// <param name="inputPath">输入文件路径</param>
|
||||||
/// <param name="outputPath">输出文件路径</param>
|
/// <param name="outputPath">输出文件路径</param>
|
||||||
/// <param name="frameNumber">要删除的帧编号(从1开始)</param>
|
|
||||||
/// <returns>操作是否成功</returns>
|
/// <returns>操作是否成功</returns>
|
||||||
public static async Task<bool> RemoveFrameRandomeAsync(string inputPath, string outputPath)
|
public static async Task<bool> RemoveFrameRandomeAsync(string inputPath, string outputPath)
|
||||||
{
|
{
|
||||||
if (!File.Exists(inputPath))
|
if (!File.Exists(inputPath))
|
||||||
|
{
|
||||||
|
LogUtils.Error($"输入文件不存在:{inputPath}");
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// 创建临时目录
|
// 创建临时目录
|
||||||
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
|
||||||
// 1. 获取视频信息
|
// 1. 获取视频信息
|
||||||
IMediaAnalysis mediaInfo = await FFProbe.AnalyseAsync(inputPath);
|
IMediaAnalysis mediaInfo = await FFProbe.AnalyseAsync(inputPath);
|
||||||
var videoStream = mediaInfo.PrimaryVideoStream;
|
var videoStream = mediaInfo.PrimaryVideoStream;
|
||||||
if (videoStream == null)
|
if (videoStream == null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("没有找到视频流");
|
LogUtils.Error("没有找到视频流");
|
||||||
return false;
|
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";
|
bool isHevc = videoStream.CodecName == "hevc";
|
||||||
|
string workingInputPath = inputPath;
|
||||||
Directory.CreateDirectory(tempDir);
|
|
||||||
|
|
||||||
if (isHevc)
|
if (isHevc)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
||||||
// 临时文件路径
|
|
||||||
string videoConvert = Path.Combine(tempDir, $"{Guid.NewGuid()}.mp4");
|
string videoConvert = Path.Combine(tempDir, $"{Guid.NewGuid()}.mp4");
|
||||||
|
LogUtils.Info($"检测到 HEVC 编码,先转换为 H.264:{inputPath} -> {videoConvert}");
|
||||||
|
|
||||||
await FFMpegArguments.FromFileInput(inputPath)
|
await FFMpegArguments.FromFileInput(inputPath)
|
||||||
.OutputToFile(videoConvert, true, opt => // 设置输出格式
|
.OutputToFile(videoConvert, true, opt =>
|
||||||
opt.WithVideoCodec("libx264")
|
opt.WithVideoCodec("libx264")
|
||||||
|
.WithAudioCodec("copy") // 复制音频流,不重新编码
|
||||||
).ProcessAsynchronously();
|
).ProcessAsynchronously();
|
||||||
|
|
||||||
|
// 重新分析转换后的视频
|
||||||
mediaInfo = await FFProbe.AnalyseAsync(videoConvert);
|
mediaInfo = await FFProbe.AnalyseAsync(videoConvert);
|
||||||
|
videoStream = mediaInfo.PrimaryVideoStream;
|
||||||
inputPath = videoConvert;
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
throw new Exception("转换失败!");
|
LogUtils.Error("HEVC 转换失败", ex);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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秒)
|
// 2. 计算三个区域的范围(前30%、中间30%、后30%)
|
||||||
if (totalDuration < 20)
|
// 前30%:0% 到 30%
|
||||||
{
|
int front30StartFrame = 0;
|
||||||
LogUtils.Error($"视频时长太短({totalDuration}秒),无法抽帧");
|
int front30EndFrame = (int)(totalFrames * 0.30);
|
||||||
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}秒");
|
// 中间30%:35% 到 65%(避开边界)
|
||||||
|
int middle30StartFrame = (int)(totalFrames * 0.35);
|
||||||
|
int middle30EndFrame = (int)(totalFrames * 0.65);
|
||||||
|
|
||||||
bool hasSubVideo1 = SubVideo(inputPath, videoPart1, 0, randomFrame - 0.016);
|
// 后30%:70% 到 100%
|
||||||
if (!hasSubVideo1)
|
int back30StartFrame = (int)(totalFrames * 0.70);
|
||||||
{
|
int back30EndFrame = totalFrames - 1; // 最后一帧
|
||||||
LogUtils.Error($"裁剪第一部分视频失败:{videoPart1}");
|
|
||||||
return false;
|
// 确保范围有效
|
||||||
}
|
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());
|
||||||
|
|
||||||
bool hasSubVideo2 = SubVideo(inputPath, videoPart2, randomFrame, totalDuration);
|
// 3. 在每个区域随机选择一帧删除
|
||||||
if (!hasSubVideo2)
|
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)
|
||||||
{
|
{
|
||||||
LogUtils.Error($"裁剪第二部分视频失败:{videoPart2}");
|
frame3 = (frame3 + 1) % totalFrames;
|
||||||
return false;
|
if (frame3 == frame1 || frame3 == frame2) frame3 = (frame3 + 1) % totalFrames;
|
||||||
}
|
}
|
||||||
|
|
||||||
LogUtils.Info($"视频裁剪成功,开始合并:{videoPart1} + {videoPart2} -> {outputPath}");
|
double time1 = frame1 / frameRate;
|
||||||
bool isJoinSuccess = JoinVideo(outputPath, [videoPart1, videoPart2]);
|
double time2 = frame2 / frameRate;
|
||||||
if (!isJoinSuccess)
|
double time3 = frame3 / frameRate;
|
||||||
{
|
|
||||||
LogUtils.Error($"合并视频失败:{outputPath}");
|
LogUtils.Info($"开始精确抽帧:输入={inputPath}, 输出={outputPath}, 总帧数={totalFrames}");
|
||||||
return false;
|
LogUtils.Info($"删除帧1(前30%):帧编号={frame1}, 时间≈{time1:F2}秒");
|
||||||
}
|
LogUtils.Info($"删除帧2(中间30%):帧编号={frame2}, 时间≈{time2:F2}秒");
|
||||||
|
LogUtils.Info($"删除帧3(后30%):帧编号={frame3}, 时间≈{time3:F2}秒");
|
||||||
|
|
||||||
// 验证输出文件是否存在
|
// 4. 只删除视频帧,音频直接复制,使用最快编码设置以达到1秒内完成
|
||||||
if (File.Exists(outputPath))
|
// 注意:使用 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))
|
||||||
{
|
{
|
||||||
LogUtils.Info($"抽帧成功:{outputPath}");
|
FileInfo fileInfo = new FileInfo(outputPath);
|
||||||
return true;
|
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
|
else
|
||||||
{
|
{
|
||||||
LogUtils.Error($"抽帧失败:输出文件不存在 - {outputPath}");
|
LogUtils.Error($"抽帧失败:success={success}, 文件存在={File.Exists(outputPath)}, 输出路径={outputPath}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LogUtils.Error("抽帧失败", ex);
|
LogUtils.Error("抽帧过程发生异常", ex);
|
||||||
Console.WriteLine($"操作失败: {ex.Message}");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
1
wails/assets/assets/index-BDrFF8AO.css
Normal file
1
wails/assets/assets/index-BDrFF8AO.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,9 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>视频拼接工具</title>
|
<title>视频拼接工具</title>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -19,12 +19,12 @@
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="./assets/index-IwiMqFON.js"></script>
|
<script type="module" crossorigin src="./assets/index-DSuQhuGl.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-BaD48VVT.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-BDrFF8AO.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user