静态编译的ffmpeg用法

秩序存到根目录的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();
        }
    }
}
相关推荐
_chirs2 天前
编译不依赖动态库的FFMPEG(麒麟国防 V10)
arm开发·ffmpeg
熊猫钓鱼>_>2 天前
从零到一:打造“抗造” Electron 录屏神器的故事
前端·javascript·ffmpeg·electron·node·录屏·record
UpYoung!2 天前
【格式转换工具】专业级多媒体格式转换解决方案——Freemake Video Converter 完全指南:轻量化视频剪辑媒体格式转换
ffmpeg·短视频·实用工具·开源工具·多媒体格式转换·运维必备·视频转换格式
试剂小课堂 Pro3 天前
Ald-PEG-Ald:丙醛与聚乙二醇两端连接的对称分子
java·c语言·c++·python·ffmpeg
MaoSource3 天前
Debian 12 安装 FFmpeg 命令
服务器·ffmpeg·debian
白云千载尽4 天前
交换空间扩容与删除、hugginface更换默认目录、ffmpeg视频处理、清理空间
python·ffmpeg·控制·mpc·navsim
xmRao4 天前
Qt+FFmpeg 实现 PCM 转 WAV
qt·ffmpeg·pcm
eWidget4 天前
Shell循环进阶:break/continue,循环嵌套与优化技巧
运维·开发语言·ffmpeg·运维开发
Knight_AL4 天前
Java + FFmpeg 实现视频分片合并(生成 list.txt 自动合并)
java·ffmpeg·音视频