秩序存到根目录的D:\software_work\Back\back_branch2\tools\ffmpeg\bin中
这个是FFMPEGHelper.cs
using NP.PE.WorkSpace.Model;
using NP.PE.WorkSpace.Model.ResponseModel;
using Pinyin4net;
using Pinyin4net.Format;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace NP.PE.WorkSpace.Helper
{
public class FFMPEGHelper
{
// FFmpeg 路径,优先从环境变量或配置文件读取,否则使用默认路径
private static readonly string _ffmpeg_path = GetFFmpegPath();
private static string GetFFmpegPath()
{
// 1. 尝试从环境变量读取
var envPath = Environment.GetEnvironmentVariable("FFMPEG_PATH");
if (!string.IsNullOrWhiteSpace(envPath) && File.Exists(envPath))
{
return envPath;
}
// 2. 尝试从项目根目录查找(相对于 Helper 项目目录)
try
{
// 获取 Helper 项目所在的目录
var helperDir = Path.GetDirectoryName(typeof(FFMPEGHelper).Assembly.Location);
if (string.IsNullOrEmpty(helperDir))
{
helperDir = AppDomain.CurrentDomain.BaseDirectory;
}
// 向上查找项目根目录(找到 .sln 文件或特定标记)
var currentDir = new DirectoryInfo(helperDir);
while (currentDir != null && currentDir.Parent != null)
{
// 检查是否有 .sln 文件(项目根目录标记)
if (currentDir.GetFiles("*.sln").Length > 0)
{
// 在项目根目录下查找 FFmpeg
var projectFfmpegPaths = new[]
{
Path.Combine(currentDir.FullName, "tools", "ffmpeg", "bin", "ffmpeg.exe"),
Path.Combine(currentDir.FullName, "tools", "ffmpeg", "ffmpeg.exe"), // 当前仓库实际路径
Path.Combine(currentDir.FullName, "ffmpeg", "bin", "ffmpeg.exe"),
Path.Combine(currentDir.FullName, "ffmpeg.exe"),
Path.Combine(currentDir.FullName, "bin", "ffmpeg.exe")
};
foreach (var path in projectFfmpegPaths)
{
if (File.Exists(path))
{
return path;
}
}
break;
}
currentDir = currentDir.Parent;
}
}
catch
{
// 忽略查找错误
}
// 3. 尝试从配置文件读取(如果项目使用了配置系统)
try
{
var configPath = AppDomain.CurrentDomain.BaseDirectory;
var configFile = Path.Combine(configPath, "appsettings.json");
if (File.Exists(configFile))
{
// 简单读取配置(可以使用 JSON 解析库,这里先简单处理)
var content = File.ReadAllText(configFile);
// 如果配置中有 FFmpeg 路径,可以在这里解析
}
}
catch
{
// 忽略配置读取错误
}
// 4. 尝试常见的系统安装路径
var commonPaths = new[]
{
@"D:\Program Files\ffmpeg-7.0.2-essentials_build\bin\ffmpeg.exe",
@"C:\Program Files\ffmpeg\bin\ffmpeg.exe",
@"C:\ffmpeg\bin\ffmpeg.exe",
@"D:\Program Files\ffmpeg\bin\ffmpeg.exe"
};
foreach (var path in commonPaths)
{
if (File.Exists(path))
{
return path;
}
}
// 5. 最后尝试直接使用 ffmpeg(如果已在系统 PATH 中)
return "ffmpeg";
}
private static void ValidateFFmpegPath()
{
if (string.IsNullOrWhiteSpace(_ffmpeg_path))
{
throw new FileNotFoundException("FFmpeg 路径未配置。请设置环境变量 FFMPEG_PATH 或在配置文件中指定 FFmpeg 路径。");
}
// 如果不是 "ffmpeg"(系统 PATH 中的情况),检查文件是否存在
if (_ffmpeg_path != "ffmpeg" && !File.Exists(_ffmpeg_path))
{
throw new FileNotFoundException(
$"FFmpeg 可执行文件不存在: {_ffmpeg_path}\n" +
$"请检查:\n" +
$"1. FFmpeg 是否已正确安装\n" +
$"2. 路径是否正确\n" +
$"3. 可以设置环境变量 FFMPEG_PATH 指向 FFmpeg 的完整路径");
}
}
public static ResponseContent<VideoCodecInfo> GetVideoInfo(string path)
{
try
{
ValidateFFmpegPath();
}
catch (FileNotFoundException ex)
{
return ResponseContent<VideoCodecInfo>.Error(ErrorCode.OPERATION_FAILED, ex.Message);
}
Process process = new Process();
process.StartInfo.FileName = _ffmpeg_path;
process.StartInfo.Arguments = $"-i \"{path}\"";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
process.Start();
string output = process.StandardError.ReadToEnd();
process.WaitForExit();
// 解析输出获取视频编码格式
var pattern = @"Video:\s(?<codec>\w+)\s*(?:\((?<profile>[^)]+)\))?\s*\((?<tag>\w+)\s*/\s*(?<fourcc>0x[0-9a-fA-F]+)\)[^,]*,\s*(?<pix_fmt>\w+)[^,]*,\s*(?<width>\d+)x(?<height>\d+)(?:\s*\[SAR\s*(?<sar>[^]]+)\sDAR\s*(?<dar>[^]]+)\])?[^,]*,\s*(?<bitrate>\d+)\s*kb/s";
var match = Regex.Match(output, pattern);
if (match.Success)
{
var info = new VideoCodecInfo();
info.CodecName = match.Groups["codec"].Value;
info.Profile = match.Groups["profile"].Success ? match.Groups["profile"].Value : null;
info.CodecTag = match.Groups["tag"].Value;
info.FourCC = match.Groups["fourcc"].Value;
info.PixelFormat = match.Groups["pix_fmt"].Value;
info.Width = int.Parse(match.Groups["width"].Value);
info.Height = int.Parse(match.Groups["height"].Value);
info.SAR = match.Groups["sar"].Success ? match.Groups["sar"].Value : null;
info.DAR = match.Groups["dar"].Success ? match.Groups["dar"].Value : null;
info.Bitrate = int.Parse(match.Groups["bitrate"].Value);
return ResponseContent<VideoCodecInfo>.OK(info);
}
return ResponseContent<VideoCodecInfo>.Error(ErrorCode.OPERATION_FAILED);
}
public static ResponseContent ConvertToAvcCode(string videoPath, string savePath)
{
try
{
ValidateFFmpegPath();
}
catch (FileNotFoundException ex)
{
return ResponseContent.Error(ErrorCode.OPERATION_FAILED, ex.Message);
}
Process process = new Process();
process.StartInfo.FileName = _ffmpeg_path;
process.StartInfo.Arguments = $"-i \"{videoPath}\" -c:v libx264 -preset fast -crf 23 -c:a aac \"{savePath}\"";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.CreateNoWindow = true;
process.Start();
process.WaitForExit();
if (process.ExitCode != 0)
{
return ResponseContent.Error(ErrorCode.OPERATION_FAILED);
}
return ResponseContent.OK();
}
public static ResponseContent SavePoster(string videoPath, string savePath)
{
try
{
ValidateFFmpegPath();
}
catch (FileNotFoundException ex)
{
return ResponseContent.Error(ErrorCode.OPERATION_FAILED, ex.Message);
}
Process process = new Process();
process.StartInfo.FileName = _ffmpeg_path;
process.StartInfo.Arguments = $"-i \"{videoPath}\" -ss 00:00:02 -vframes 1 -q:v 2 \"{savePath}\"";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.CreateNoWindow = true;
process.Start();
process.WaitForExit();
if (process.ExitCode != 0)
{
return ResponseContent.Error(ErrorCode.OPERATION_FAILED);
}
return ResponseContent.OK();
}
/// <summary>
/// 按指定时间点生成视频封面图(可选缩放和质量)
/// </summary>
public static ResponseContent GenerateThumbnail(
string videoPath,
string savePath,
double frameTimeSeconds = 1d,
int? width = null,
int? height = null,
int quality = 80)
{
try
{
ValidateFFmpegPath();
}
catch (FileNotFoundException ex)
{
return ResponseContent.Error(ErrorCode.OPERATION_FAILED, ex.Message);
}
if (string.IsNullOrWhiteSpace(videoPath) || !File.Exists(videoPath))
{
return ResponseContent.Error(ErrorCode.FILE_NOT_EXISTS);
}
var dir = Path.GetDirectoryName(savePath);
if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
var timestamp = TimeSpan.FromSeconds(frameTimeSeconds < 0 ? 0 : frameTimeSeconds)
.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
// qscale 1~31,值越小质量越高;把 1-100 映射到 1-31
var qValue = Math.Min(Math.Max(quality, 1), 100);
qValue = 31 - (int)Math.Round((qValue / 100d) * 30d);
qValue = Math.Min(Math.Max(qValue, 1), 31);
string vf = string.Empty;
if (width.HasValue || height.HasValue)
{
var w = width.HasValue && width.Value > 0 ? width.Value.ToString() : "-1";
var h = height.HasValue && height.Value > 0 ? height.Value.ToString() : "-1";
vf = $" -vf scale={w}:{h}";
}
// -y 自动覆盖已存在文件,避免 FFmpeg 等待用户输入导致进程卡住
var arguments = $"-y -ss {timestamp} -i \"{videoPath}\" -frames:v 1 -q:v {qValue}{vf} \"{savePath}\"";
var process = new Process();
process.StartInfo.FileName = _ffmpeg_path;
process.StartInfo.Arguments = arguments;
process.StartInfo.UseShellExecute = false;
// 不重定向输出,避免缓冲区写满造成潜在阻塞
process.StartInfo.RedirectStandardOutput = false;
process.StartInfo.RedirectStandardError = false;
process.StartInfo.CreateNoWindow = true;
process.Start();
process.WaitForExit();
if (process.ExitCode != 0 || !File.Exists(savePath))
{
return ResponseContent.Error(ErrorCode.OPERATION_FAILED, "生成封面失败");
}
return ResponseContent.OK();
}
public static ResponseContent SendCommand(string command)
{
try
{
ValidateFFmpegPath();
}
catch (FileNotFoundException ex)
{
return ResponseContent.Error(ErrorCode.OPERATION_FAILED, ex.Message);
}
Process process = new Process();
process.StartInfo.FileName = _ffmpeg_path;
process.StartInfo.Arguments = command;
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.CreateNoWindow = true;
process.Start();
process.WaitForExit();
if (process.ExitCode != 0)
{
return ResponseContent.Error(ErrorCode.OPERATION_FAILED);
}
return ResponseContent.OK();
}
/// <summary>
/// 获取视频时长(秒)
/// </summary>
public static ResponseContent<double?> GetVideoDuration(string path)
{
try
{
ValidateFFmpegPath();
}
catch (FileNotFoundException ex)
{
return ResponseContent<double?>.Error(ErrorCode.OPERATION_FAILED, ex.Message);
}
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
{
return ResponseContent<double?>.Error(ErrorCode.FILE_NOT_EXISTS);
}
Process process = new Process();
process.StartInfo.FileName = _ffmpeg_path;
process.StartInfo.Arguments = $"-i \"{path}\"";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
process.Start();
string output = process.StandardError.ReadToEnd();
process.WaitForExit();
// 解析 Duration: HH:MM:SS.mmm 格式
var durationPattern = @"Duration:\s*(?<hours>\d{2}):(?<minutes>\d{2}):(?<seconds>\d{2})\.(?<milliseconds>\d{2})";
var match = Regex.Match(output, durationPattern);
if (match.Success)
{
var hours = int.Parse(match.Groups["hours"].Value);
var minutes = int.Parse(match.Groups["minutes"].Value);
var seconds = int.Parse(match.Groups["seconds"].Value);
var milliseconds = int.Parse(match.Groups["milliseconds"].Value);
var totalSeconds = hours * 3600 + minutes * 60 + seconds + milliseconds / 100.0;
return ResponseContent<double?>.OK(totalSeconds);
}
return ResponseContent<double?>.Error(ErrorCode.OPERATION_FAILED, "无法解析视频时长");
}
/// <summary>
/// 生成带文本水印的HLS视频(m3u8 + ts)-【全程纯相对路径版 ✅ 最终完整版】
/// ✅ 无任何绝对路径 ✅ 兼容Pinyin4net1.0.0中文转拼音 ✅ 无报错 ✅ 拼音水印不溢出 ✅ FFmpeg8.x完美兼容
/// </summary>
public static ResponseContent TranscodeWithWatermark(
string inputPath,
string playlistPath,
string segmentTemplate,
int segmentTime = 10,
string watermarkText = "Your Watermark",
string watermarkPosition = "center",
int fontSize = 48,
string fontColor = "white")
{
try
{
ValidateFFmpegPath();
}
catch (FileNotFoundException ex)
{
return ResponseContent.Error(ErrorCode.OPERATION_FAILED, ex.Message);
}
if (string.IsNullOrWhiteSpace(inputPath) || !File.Exists(inputPath))
{
return ResponseContent.Error(ErrorCode.FILE_NOT_EXISTS, "输入视频文件不存在");
}
if (string.IsNullOrWhiteSpace(watermarkText)) watermarkText = "Watermark";
// 参数校验
segmentTime = segmentTime < 1 ? 10 : segmentTime;
fontSize = fontSize < 8 ? 48 : fontSize;
fontColor = string.IsNullOrWhiteSpace(fontColor) ? "white" : fontColor;
// ✅ 优化点1:补回拼音水印字号缩放逻辑,避免文字溢出画面(核心必加)
if (!string.IsNullOrWhiteSpace(watermarkText) && watermarkText.Any(c => c >= 0x4e00 && c <= 0x9fff))
{
fontSize = Convert.ToInt32(fontSize * 0.7);
}
// 水印位置表达式 (C#7.3兼容写法,无任何语法糖,编译绝对通过)
string position = watermarkPosition?.ToLower() ?? "center";
string drawtextPosition;
if (position == "top-left")
{
drawtextPosition = "x=20:y=20";
}
else if (position == "top-right")
{
drawtextPosition = "x=w-20-text_w:y=20";
}
else if (position == "bottom-left")
{
drawtextPosition = "x=20:y=h-20-text_h";
}
else if (position == "bottom-right")
{
drawtextPosition = "x=w-20-text_w:y=h-20-text_h";
}
else
{
// 默认居中
drawtextPosition = "x=(w-text_w)/2:y=(h-text_h)/2";
}
#region ✅ 字体文件-纯相对路径(无权限/路径问题)
string fontRelativePath = @"tools/ffmpeg/fonts/simhei.ttf";
string fontFilePath = fontRelativePath.Replace("\\", "/");
Console.WriteLine($"[FFMPEG] ✅ 字体路径: {fontFilePath} 存在:{File.Exists(fontFilePath)}");
#endregion
#region ✅ 核心修改:调用拼音转换方法 + 安全转义(你之前缺失的唯一逻辑)
string pinyinWatermark = ConvertChineseToPinyin(watermarkText);
string safeText = pinyinWatermark.Replace(":", "\\:").Replace("=", "\\=").Replace(" ", "\\ ");
Console.WriteLine($"[FFMPEG] ✅ 水印转换完成:原文本【{watermarkText}】→ 拼音【{pinyinWatermark}】");
#endregion
// 构建水印滤镜 (全程无引号,无绝对路径,FFmpeg8.x完美解析)
var drawtextFilter = $"drawtext=text={safeText}:fontfile={fontFilePath}:fontcolor={fontColor}:fontsize={fontSize}:{drawtextPosition}:box=1:boxcolor=#000000BF:boxborderw=5:shadowcolor=#000000CC:shadowx=2:shadowy=2";
// 输出目录
string outputDir = Path.GetDirectoryName(playlistPath);
string segmentFileName = "segment_%03d.ts";
// 构建FFmpeg命令 (全程无绝对路径,切片是纯相对路径)
string arguments = $"-y -i \"{inputPath}\" -vf \"{drawtextFilter}\" -c:v libx264 -preset medium -crf 23 -c:a aac -b:a 128k -f segment -segment_time {segmentTime} -segment_list_type m3u8 -segment_list \"{playlistPath}\" \"{segmentFileName}\"";
// 创建进程
var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = _ffmpeg_path,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
WorkingDirectory = outputDir // 核心必加:相对路径解析的关键
};
// 日志收集
var errorSb = new StringBuilder();
process.ErrorDataReceived += (s, e) => { if (!string.IsNullOrEmpty(e.Data)) errorSb.AppendLine(e.Data); };
process.OutputDataReceived += (s, e) => { if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); };
// 执行FFmpeg
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
process.WaitForExit(600000);
// 结果校验
string errorMsg = errorSb.ToString();
if (process.ExitCode != 0 || !File.Exists(playlistPath))
{
return ResponseContent.Error(ErrorCode.OPERATION_FAILED, $"FFmpeg转码失败,退出码:{process.ExitCode},错误:{errorMsg.Substring(0, Math.Min(errorMsg.Length, 500))}");
}
var tsFiles = Directory.GetFiles(outputDir, "segment_*.ts");
Console.WriteLine($"[FFMPEG] ✅ 转码成功!生成 {tsFiles.Length} 个TS切片文件");
return ResponseContent.OK();
}
/// <summary>
/// 清理临时文件的辅助方法
/// </summary>
private static void CleanupTempFile(string filePath)
{
try
{
if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
{
File.Delete(filePath);
}
}
catch
{
// 忽略删除失败
}
}
/// <summary>
/// 【完美兼容 Pinyin4net 1.0.0 极致阉割版】中文转拼音核心方法(水印专用,零报错)
/// ✅ 无ChineseChar类 ✅ 无枚举报错 ✅ 中文转首字母大写拼音 ✅ 英文/数字/符号原样保留 ✅ 编译绝对通过
/// </summary>
private static string ConvertChineseToPinyin(string chineseText)
{
if (string.IsNullOrWhiteSpace(chineseText))
{
return string.Empty;
}
StringBuilder pinyinSb = new StringBuilder();
// 拼音格式配置:你的阉割版仅支持这3个配置,全部有效无报错
var pinyinFormat = new HanyuPinyinOutputFormat
{
ToneType = HanyuPinyinToneType.WITHOUT_TONE, // 无声调 ✔️
CaseType = HanyuPinyinCaseType.LOWERCASE, // 全小写 ✔️
VCharType = HanyuPinyinVCharType.WITH_V // v代替ü ✔️
};
foreach (char c in chineseText)
{
// ✅ 核心替换:原生C#判断中文字符,替代被阉割的ChineseChar.IsChinese()
if (c >= 0x4e00 && c <= 0x9fff)
{
// ✅ 核心替换:直接写PinyinHelper,去掉Pinyin4net.前缀,阉割版支持
string[] pinyinArray = PinyinHelper.ToHanyuPinyinStringArray(c, pinyinFormat);
if (pinyinArray != null && pinyinArray.Length > 0)
{
string py = pinyinArray[0];
if (!string.IsNullOrEmpty(py))
{
// 手动实现首字母大写,补阉割版缺失的功能
py = char.ToUpper(py[0]) + py.Substring(1);
}
pinyinSb.Append(py);
}
}
else
{
// 非中文字符:英文、数字、标点、空格 原样保留
pinyinSb.Append(c);
}
}
return pinyinSb.ToString();
}
}
}