Compare commits

...

3 Commits

Author SHA1 Message Date
a28d3cac8e update 2025-08-13 22:43:32 +08:00
da5f023390 update 2025-08-12 23:11:03 +08:00
e2a75a64bd update 2025-08-12 22:36:44 +08:00
5 changed files with 302 additions and 61 deletions

View File

@ -26,7 +26,8 @@ namespace VideoConcat.Models
private string _folderPath = ""; private string _folderPath = "";
private string _helpInfo = ""; private string _helpInfo = "";
private bool _canStart = false; private bool _canExtractFrame = false;
private bool _canModify = false;
private bool _isCanOperate = false; private bool _isCanOperate = false;
private bool _isStart = false; private bool _isStart = false;
private string[] _videos = []; private string[] _videos = [];
@ -102,18 +103,28 @@ namespace VideoConcat.Models
} }
} }
public bool CanStart public bool CanExtractFrame
{ {
get { return _canStart; } get { return _canExtractFrame; }
set set
{ {
_canStart = value; _canExtractFrame = value;
OnPropertyChanged(nameof(CanStart)); OnPropertyChanged(nameof(CanExtractFrame));
}
}
public bool CanModify
{
get { return _canModify; }
set
{
_canModify = value;
OnPropertyChanged(nameof(CanModify));
} }
} }
public ICommand? BtnOpenFolderCommand { get; set; } public ICommand? BtnOpenFolderCommand { get; set; }
public ICommand? BtnStartVideoConcatCommand { get; set; } public ICommand? BtnStartVideoConcatCommand { get; set; }
public ICommand? BtnStartVideoModifyCommand { get; set; }
public ICommand? BtnChooseAuditImageCommand { get; set; } public ICommand? BtnChooseAuditImageCommand { get; set; }
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
@ -132,13 +143,14 @@ namespace VideoConcat.Models
public void SetCanStart() public void SetCanStart()
{ {
CanExtractFrame = false;
if (videos.Length > 0) if (videos.Length > 0)
{ {
CanStart = true; CanModify = true;
} }
else else
{ {
CanStart = false; CanModify = false;
} }
} }
} }

View File

@ -1,5 +1,6 @@
using FFMpegCore; using FFMpegCore;
using FFMpegCore.Enums; using FFMpegCore.Enums;
using Standard;
using System; using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -21,11 +22,12 @@ namespace VideoConcat.Services.Video
{ {
if (!File.Exists(inputPath)) if (!File.Exists(inputPath))
return false; return false;
// 创建临时目录
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
try try
{ {
// 1. 获取视频信息 // 1. 获取视频信息
var 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)
{ {
@ -35,8 +37,6 @@ namespace VideoConcat.Services.Video
bool isHevc = videoStream.CodecName == "hevc"; bool isHevc = videoStream.CodecName == "hevc";
// 2. 创建临时目录
string tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDir); Directory.CreateDirectory(tempDir);
if (isHevc) if (isHevc)
@ -45,47 +45,59 @@ namespace VideoConcat.Services.Video
{ {
// 临时文件路径 // 临时文件路径
string videoConvert = Path.Combine(tempDir, "convert.mp4"); string videoConvert = Path.Combine(tempDir, $"{Guid.NewGuid()}.mp4");
await FFMpegArguments.FromFileInput(inputPath) await FFMpegArguments.FromFileInput(inputPath)
.OutputToFile(videoConvert, true, opt => // 设置输出格式 .OutputToFile(videoConvert, true, opt => // 设置输出格式
opt.WithVideoCodec("libx264") opt.WithVideoCodec("libx264")
).ProcessAsynchronously(); ).ProcessAsynchronously();
mediaInfo = await FFProbe.AnalyseAsync(videoConvert); mediaInfo = await FFProbe.AnalyseAsync(videoConvert);
inputPath = videoConvert; inputPath = videoConvert;
} }
catch (Exception) catch (Exception)
{ {
File.Delete(outputPath); throw new Exception("转换失败!");
} }
} }
// 1. 获取视频信息
// 视频总时长(秒) mediaInfo = await FFProbe.AnalyseAsync(inputPath);
var totalDuration = mediaInfo.Duration.TotalSeconds; videoStream = mediaInfo.PrimaryVideoStream;
// 计算帧时间参数 if (videoStream == null)
double frameRate = videoStream.FrameRate;
double frameDuration = 1.0 / frameRate; // 一帧时长(秒)
int totalFram = (int)(totalDuration * frameRate);
// 2. 随机生成要删除的帧的时间点(避开最后一帧,防止越界)
var random = new Random();
var randomFrame = random.Next(20, totalFram - 10);
double frameTime = (randomFrame - 1) * frameDuration; // 目标帧开始时间
double nextFrameTime = frameTime + frameDuration; // 下一帧开始时间
// 临时文件路径
string videoPart1 = Path.Combine(tempDir, "video_part1.mp4");
string videoPart2 = Path.Combine(tempDir, "video_part2.mp4");
bool hasSubVideo1 = SubVideo(inputPath, videoPart1, 0, frameTime);
bool hasSubVideo2 = SubVideo(inputPath, videoPart2, nextFrameTime, totalDuration);
if (hasSubVideo1 && hasSubVideo2)
{ {
return JoinVideo(outputPath, [videoPart1, videoPart2]); 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);
var random = new Random();
var randomFrame = random.Next(20, (int)totalDuration);
//return RemoveVideoFrame(inputPath, outputPath, randomFrame);
string videoPart1 = Path.Combine(tempDir, $"{Guid.NewGuid()}.mp4");
string videoPart2 = Path.Combine(tempDir, $"{Guid.NewGuid()}.mp4");
bool hasSubVideo1 = SubVideo(inputPath, videoPart1, 0, randomFrame - 0.016);
bool hasSubVideo2 = SubVideo(inputPath, videoPart2, randomFrame, totalDuration);
if (!hasSubVideo1 || !hasSubVideo2)
{
return false;
}
bool isJoinSuccess = JoinVideo(outputPath, [videoPart1, videoPart2]);
if (!isJoinSuccess)
{
return false;
}
return false; return false;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -93,11 +105,53 @@ namespace VideoConcat.Services.Video
Console.WriteLine($"操作失败: {ex.Message}"); Console.WriteLine($"操作失败: {ex.Message}");
return false; return false;
} }
finally
{
string[] files = Directory.GetFiles(tempDir);
foreach (string file in files)
{
File.Delete(file);
}
File.Delete(tempDir);
}
} }
/// <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) public static bool SubVideo(string inputPath, string outputPath, Double startSec, Double endSec)
{ {
return FFMpeg.SubVideo(inputPath, outputPath, TimeSpan.FromSeconds(startSec), TimeSpan.FromSeconds(endSec)); return FFMpegArguments
.FromFileInput(inputPath, true, options => options.Seek(TimeSpan.FromSeconds(startSec)).EndSeek(TimeSpan.FromSeconds(endSec)))
.OutputToFile(outputPath, true, options => options.CopyChannel()) //.WithCustomArgument("-an") 去掉音频
.ProcessSynchronously();
}
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) public static bool JoinVideo(string outPutPath, string[] videoParts)
@ -105,5 +159,105 @@ namespace VideoConcat.Services.Video
return FFMpeg.Join(outPutPath, videoParts); return FFMpeg.Join(outPutPath, videoParts);
} }
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();
}
} }
} }

View File

@ -1,15 +1,16 @@
using System.Windows.Input; using FFMpegCore;
using VideoConcat.Models;
using MessageBox = System.Windows.MessageBox;
using VideoConcat.Common.Tools;
using System.IO;
using Microsoft.Expression.Drawing.Core;
using FFMpegCore;
using FFMpegCore.Enums; using FFMpegCore.Enums;
using static VideoConcat.Models.VideoModel; using Microsoft.Expression.Drawing.Core;
using System.Windows.Threading; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Forms;
using System.Windows.Input;
using System.Windows.Threading;
using VideoConcat.Common.Tools;
using VideoConcat.Models;
using VideoConcat.Services.Video; using VideoConcat.Services.Video;
using static VideoConcat.Models.VideoModel;
using MessageBox = System.Windows.MessageBox;
namespace VideoConcat.ViewModels namespace VideoConcat.ViewModels
{ {
@ -34,7 +35,8 @@ namespace VideoConcat.ViewModels
{ {
ExtractWindowModel = new ExtractWindowModel ExtractWindowModel = new ExtractWindowModel
{ {
CanStart = false, CanExtractFrame = false,
CanModify = false,
IsStart = false, IsStart = false,
IsCanOperate = true, IsCanOperate = true,
@ -58,20 +60,92 @@ namespace VideoConcat.ViewModels
DoExcue = obj => DoExcue = obj =>
{ {
ExtractWindowModel.HelpInfo = ""; ExtractWindowModel.HelpInfo = "";
Task.Run(action: () =>
SemaphoreSlim semaphore = new(10); // Limit to 3 threads
List<Task> _tasks = [];
ExtractWindowModel.videos.ForEach(async (video) =>
{ {
ExtractWindowModel.videos.ForEach(async (video) => await semaphore.WaitAsync(); // Wait when more than 3 threads are running
var _task = Task.Run(async () =>
{ {
// 实例化并调用 try
var remover = new VideoProcess(); {
// 删除4秒处的帧需根据实际帧位置调整 // 实例化并调用
string _tmpPath = Path.GetDirectoryName(video) ?? ""; var remover = new VideoProcess();
string _tmpFileName = $"抽帧视频-{Path.GetFileName(video)}"; // 删除4秒处的帧需根据实际帧位置调整
await VideoProcess.RemoveFrameRandomeAsync(video, $"{_tmpPath}\\{_tmpFileName}"); string _tmpPath = Path.GetDirectoryName(video) ?? "";
string _tmpFileName = $"{(new Random()).Next(10000, 99999)}{Path.GetFileName(video)}";
string outPath = Path.Combine(_tmpPath, "out");
if (!Path.Exists(outPath))
{
Directory.CreateDirectory(outPath);
}
await VideoProcess.RemoveFrameRandomeAsync(video, $"{_tmpPath}\\out\\{_tmpFileName}");
}
finally
{
semaphore.Release(); // Work is done, signal to semaphore that more work is possible
}
}); });
_tasks.Add(_task);
}); });
ExtractWindowModel.HelpInfo = "全部完成!"; Task.WhenAll(_tasks).ContinueWith((task) =>
{
ExtractWindowModel.HelpInfo = "全部完成!";
});
}
},
BtnStartVideoModifyCommand = new Command()
{
DoExcue = obj =>
{
ExtractWindowModel.HelpInfo = "";
SemaphoreSlim semaphore = new(10); // Limit to 3 threads
List<Task> _tasks = [];
ExtractWindowModel.videos.ForEach(async (video) =>
{
await semaphore.WaitAsync(); // Wait when more than 3 threads are running
var _task = Task.Run(async () =>
{
try
{
// 实例化并调用
var remover = new VideoProcess();
// 删除4秒处的帧需根据实际帧位置调整
string _tmpPath = Path.GetDirectoryName(video) ?? "";
string _tmpFileName = $"{(new Random()).Next(10000, 99999)}{Path.GetFileName(video)}";
string outPath = Path.Combine(_tmpPath, "out");
if (!Path.Exists(outPath))
{
Directory.CreateDirectory(outPath);
}
VideoProcess.ModifyByMetadata(video, $"{_tmpPath}\\out\\modify{_tmpFileName}");
}
finally
{
semaphore.Release(); // Work is done, signal to semaphore that more work is possible
}
});
_tasks.Add(_task);
});
Task.WhenAll(_tasks).ContinueWith((task) =>
{
ExtractWindowModel.HelpInfo = "全部完成!";
});
} }
} }
}; };

View File

@ -29,9 +29,10 @@
<Border> <Border>
<WrapPanel> <WrapPanel>
<Label VerticalAlignment="Center" Width="100" FontSize="16" Margin="5,2" BorderBrush="#7F7F7F" BorderThickness="2" Style="{StaticResource MaterialDesignLabel}" VerticalContentAlignment="Stretch">视频文件夹:</Label> <Label VerticalAlignment="Center" Width="100" FontSize="16" Margin="5,2" BorderBrush="#7F7F7F" BorderThickness="2" Style="{StaticResource MaterialDesignLabel}" VerticalContentAlignment="Stretch">视频文件夹:</Label>
<TextBox Grid.Column="1" Width="650" Name="FoldPath" Text="{Binding ExtractWindowModel.FolderPath,Mode=TwoWay}" IsReadOnly="True" Margin="5,2" BorderBrush="#7F7F7F" BorderThickness="2" FontSize="16" VerticalContentAlignment="Center" materialDesign:HintAssist.Hint="选择包含需要处理的视频文件夹"/> <TextBox Grid.Column="1" Width="600" Name="FoldPath" Text="{Binding ExtractWindowModel.FolderPath,Mode=TwoWay}" IsReadOnly="True" Margin="5,2" BorderBrush="#7F7F7F" BorderThickness="2" FontSize="16" VerticalContentAlignment="Center" materialDesign:HintAssist.Hint="选择包含需要处理的视频文件夹"/>
<Button Grid.Column="2" Width="100" Content="选择" FontSize="16" Command="{Binding ExtractWindowModel.BtnOpenFolderCommand}" Background="#40568D" BorderBrush="#7F7F7F" BorderThickness="2" ToolTip="选择含有视频的主文件夹" Style="{StaticResource MaterialDesignRaisedButton}" Margin="5,2" IsEnabled="{Binding ExtractWindowModel.IsCanOperate}"/> <Button Grid.Column="2" Width="80" Content="选择" FontSize="16" Command="{Binding ExtractWindowModel.BtnOpenFolderCommand}" Background="#40568D" BorderBrush="#7F7F7F" BorderThickness="2" ToolTip="选择含有视频的主文件夹" Style="{StaticResource MaterialDesignRaisedButton}" Margin="5,2" IsEnabled="{Binding ExtractWindowModel.IsCanOperate}"/>
<Button Grid.Column="3" Width="100" Content="处理" FontSize="16" Command="{Binding ExtractWindowModel.BtnStartVideoConcatCommand}" Background="#3B94FE" BorderBrush="#7F7F7F" BorderThickness="2" ToolTip="开始处理视频" Style="{StaticResource MaterialDesignRaisedButton}" Margin="5,2,0,2" IsEnabled="{Binding ExtractWindowModel.CanStart}"/> <Button Grid.Column="3" Width="80" Content="抽帧" FontSize="16" Command="{Binding ExtractWindowModel.BtnStartVideoConcatCommand}" Background="#3B94FE" BorderBrush="#7F7F7F" BorderThickness="2" ToolTip="开始处理视频" Style="{StaticResource MaterialDesignRaisedButton}" Margin="5,2,0,2" IsEnabled="{Binding ExtractWindowModel.CanExtractFrame}"/>
<Button Grid.Column="4" Width="80" Content="修改" FontSize="16" Command="{Binding ExtractWindowModel.BtnStartVideoConcatCommand}" Background="#3B94FE" BorderBrush="#7F7F7F" BorderThickness="2" ToolTip="开始处理视频" Style="{StaticResource MaterialDesignRaisedButton}" Margin="5,2,0,2" IsEnabled="{Binding ExtractWindowModel.CanModify}"/>
</WrapPanel> </WrapPanel>
</Border> </Border>

View File

@ -11,7 +11,7 @@
mc:Ignorable="d" mc:Ignorable="d"
Width="1100" Height="800" WindowStartupLocation="CenterScreen" Width="1100" Height="800" WindowStartupLocation="CenterScreen"
ResizeMode="NoResize" ResizeMode="CanMinimize"
Title="工具"> Title="工具">
<Window.Resources> <Window.Resources>
<Style x:Key="BottomButton" TargetType="Button"> <Style x:Key="BottomButton" TargetType="Button">