视频转码与切片(HLS)完整教程

视频转码与切片(HLS)完整教程

一、前置准备

1. 安装 FFmpeg

下载并安装 FFmpeg:

  • 下载地址:https://ffmpeg.org/download.html

  • Windows:下载后解压,将 `bin` 目录添加到系统 PATH,或将 `ffmpeg.exe` 路径配置到环境变量 `FFMPEG_PATH`

2. 项目依赖

在 `.csproj` 中添加(如需要中文转拼音水印):

```xml

<PackageReference Include="Pinyin4net" Version="1.0.0" />

```

3. 命名空间引用

```csharp

using System;

using System.Collections.Generic;

using System.Diagnostics;

using System.IO;

using System.Linq;

using System.Text;

using System.Text.RegularExpressions;

using NP.PE.WorkSpace.Helper; // FFMPEGHelper 所在命名空间

using NP.PE.WorkSpace.Model;

using NP.PE.WorkSpace.Model.ResponseModel;

```


二、核心概念

转码 vs 切片

  • 转码:将视频从一种编码格式转换为另一种(如 MP4 → H.264)

  • 切片:将视频切分成多个小文件(TS 切片),生成播放列表(m3u8)

通常一起进行:转码的同时进行切片,生成 HLS 格式。


三、实现步骤

步骤 1:创建 FFMPEGHelper 辅助类

创建 `FFMPEGHelper.cs`:

```csharp

namespace NP.PE.WorkSpace.Helper

{

public class FFMPEGHelper

{

// FFmpeg 路径(从环境变量或默认路径获取)

private static readonly string _ffmpeg_path = GetFFmpegPath();

// 获取 FFmpeg 路径的方法

private static string GetFFmpegPath()

{

// 1. 优先从环境变量读取

var envPath = Environment.GetEnvironmentVariable("FFMPEG_PATH");

if (!string.IsNullOrWhiteSpace(envPath) && File.Exists(envPath))

{

return envPath;

}

// 2. 尝试系统 PATH 中的 ffmpeg

return "ffmpeg";

}

// 验证 FFmpeg 路径

private static void ValidateFFmpegPath()

{

if (string.IsNullOrWhiteSpace(_ffmpeg_path))

{

throw new FileNotFoundException("FFmpeg 路径未配置");

}

}

}

}

```

步骤 2:实现转码切片方法

在 `FFMPEGHelper` 中添加转码切片方法:

```csharp

/// <summary>

/// 执行 FFmpeg 命令(通用方法)

/// </summary>

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.RedirectStandardError = true;

process.StartInfo.CreateNoWindow = true;

process.StartInfo.StandardOutputEncoding = Encoding.UTF8;

process.StartInfo.StandardErrorEncoding = Encoding.UTF8;

process.Start();

process.WaitForExit();

if (process.ExitCode != 0)

{

return ResponseContent.Error(ErrorCode.OPERATION_FAILED, "FFmpeg 执行失败");

}

return ResponseContent.OK();

}

```

步骤 3:在 BLL 层实现转码逻辑

在 `CaseBLL.cs` 或 `VideoBLL.cs` 中实现:

```csharp

// 常量定义

private const int DEFAULT_SEGMENT_TIME_SECONDS = 10; // 每个切片10秒

/// <summary>

/// 对视频进行HLS切片转码

/// </summary>

private ResponseContent<bool> TranscodeCaseVideo(

Guid mediaId,

string originalFilePath,

string originalRelativePath)

{

try

{

// 1. 验证原始文件是否存在

if (!File.Exists(originalFilePath))

{

return ResponseContent<bool>.Error("原始视频文件不存在");

}

// 2. 获取视频文件所在目录

var videoDirectory = Path.GetDirectoryName(originalFilePath);

if (string.IsNullOrWhiteSpace(videoDirectory))

{

return ResponseContent<bool>.Error("无法获取视频文件目录");

}

// 3. 创建 hls 目录

var hlsDirectory = Path.Combine(videoDirectory, "hls");

if (!Directory.Exists(hlsDirectory))

{

Directory.CreateDirectory(hlsDirectory);

}

// 4. 定义输出文件路径

var playlistFileName = "index.m3u8"; // 播放列表文件名

var playlistFullPath = Path.Combine(hlsDirectory, playlistFileName);

var segmentTemplate = Path.Combine(hlsDirectory, "segment_%03d.ts"); // TS切片模板

// 5. 构建 FFmpeg 命令

// 参数说明:

// -y : 自动覆盖已存在的文件

// -i : 输入文件路径

// -c:v libx264 : 视频编码器使用 H.264

// -c:a aac : 音频编码器使用 AAC

// -preset fast : 编码速度预设(fast/medium/slow)

// -f segment : 输出格式为分段

// -segment_time : 每个切片的时长(秒)

// -segment_list : m3u8 播放列表路径

var arguments =

$"-y -i \"{originalFilePath}\" " +

"-c:v libx264 -c:a aac -preset fast " +

$"-f segment -segment_time {DEFAULT_SEGMENT_TIME_SECONDS} " +

$"-segment_list \"{playlistFullPath}\" \"{segmentTemplate}\"";

// 6. 执行 FFmpeg 转码

var ffResult = FFMPEGHelper.SendCommand(arguments);

if (!ffResult.Success)

{

return ResponseContent<bool>.Error($"视频转码失败: {ffResult.Message}");

}

// 7. 修正 m3u8 文件中的 TS 切片路径(改为 API 路径)

FixM3u8SegmentPaths(playlistFullPath, hlsDirectory);

// 8. 获取视频信息(时长等)

int? duration = null;

var durationResult = FFMPEGHelper.GetVideoDuration(originalFilePath);

if (durationResult.Success && durationResult.Data.HasValue)

{

duration = (int)Math.Round(durationResult.Data.Value);

}

// 9. 计算切片数量

var segmentCount = Directory.GetFiles(hlsDirectory, "segment_*.ts").Length;

// 10. 将绝对路径转换为相对路径(存数据库)

var relativeM3u8 = StoragePathHelper.ToRelativePath(playlistFullPath);

// 11. 更新数据库,保存 m3u8 路径

var updateSql = $@"

UPDATE TbMedia

SET mediaUrl = @M3u8Path, duration = @Duration, UpdateTime = @UpdateTime

WHERE Id = @Id";

var updateParams = new Dictionary<string, object>

{

{ "@Id", mediaId },

{ "@M3u8Path", relativeM3u8 },

{ "@Duration", duration ?? 0 },

{ "@UpdateTime", DateTime.Now }

};

var updateResult = ExecuteNonQuery(updateSql, updateParams, SystemEnum.KNOWLEDGEBASE);

if (updateResult <= 0)

{

return ResponseContent<bool>.Error("更新媒体信息失败");

}

return ResponseContent<bool>.OK(true);

}

catch (Exception ex)

{

return ResponseContent<bool>.Error($"视频转码异常: {ex.Message}");

}

}

```

步骤 4:修正 m3u8 文件中的 TS 路径

```csharp

/// <summary>

/// 修正 m3u8 文件中的 TS 切片路径,将相对路径改为完整的 API 路径

/// </summary>

private static void FixM3u8SegmentPaths(string m3u8FilePath, string hlsDirectory)

{

if (!File.Exists(m3u8FilePath))

{

return;

}

try

{

// 1. 读取 m3u8 文件内容

var m3u8Content = File.ReadAllText(m3u8FilePath, Encoding.UTF8);

// 2. 将 hls 目录转换为相对路径

var relativeHlsDir = StoragePathHelper.ToRelativePath(hlsDirectory);

// 3. 逐行处理,替换 TS 切片路径为 API 路径

var lines = m3u8Content.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);

var modifiedLines = new List<string>();

foreach (var line in lines)

{

var trimmedLine = line.Trim();

// 检查是否是 TS 切片文件行(以 .ts 结尾且不是注释行)

if (trimmedLine.EndsWith(".ts", StringComparison.OrdinalIgnoreCase) &&

!trimmedLine.StartsWith("#") &&

!trimmedLine.Contains("file="))

{

// TS 切片文件名

var tsFileName = trimmedLine;

var tsFilePath = $"{relativeHlsDir}/{tsFileName}";

// 修改为 API 路径格式

modifiedLines.Add($"/api/case/GetCaseMediaFile?file={Uri.EscapeDataString(tsFilePath)}");

}

else

{

// 其他行(注释、配置等)保持不变

modifiedLines.Add(line);

}

}

// 4. 写回 m3u8 文件

File.WriteAllText(m3u8FilePath, string.Join("\n", modifiedLines), Encoding.UTF8);

}

catch

{

// 如果修改失败,静默失败(不影响播放)

}

}

```

步骤 5:在保存视频时触发转码

在保存视频文件的方法中,如果是视频类型,自动触发转码:

```csharp

// 保存视频文件后

if (mediaType == 3) // 3-视频

{

// 保存文件到数据库后,触发转码

var transcodeResult = TranscodeCaseVideo(mediaId, filePath, mediaUrl);

if (!transcodeResult.Success)

{

// 转码失败不影响整体流程,记录日志即可

Console.WriteLine($"[CaseBLL] 视频转码失败: MediaId={mediaId}, Error={transcodeResult.Message}");

}

}

```


四、FFmpeg 命令参数详解

基础转码切片命令

```bash

ffmpeg -y -i "输入视频.mp4" \

-c:v libx264 \ # 视频编码器:H.264

-c:a aac \ # 音频编码器:AAC

-preset fast \ # 编码速度:fast/medium/slow

-f segment \ # 输出格式:分段

-segment_time 10 \ # 每个切片时长:10秒

-segment_list "index.m3u8" \ # m3u8 播放列表路径

"segment_%03d.ts" # TS 切片文件名模板(%03d = 001, 002, 003...)

```

参数说明

| 参数 | 说明 | 示例 |

|------|------|------|

| `-y` | 自动覆盖已存在文件 | `-y` |

| `-i` | 输入文件路径 | `-i "video.mp4"` |

| `-c:v` | 视频编码器 | `libx264` (H.264) |

| `-c:a` | 音频编码器 | `aac` |

| `-preset` | 编码速度 | `fast` / `medium` / `slow` |

| `-f segment` | 分段输出格式 | `-f segment` |

| `-segment_time` | 切片时长(秒) | `10` |

| `-segment_list` | m3u8 播放列表路径 | `"index.m3u8"` |

| `segment_%03d.ts` | TS 切片文件名模板 | `segment_000.ts`, `segment_001.ts`... |


五、文件结构示例

转码后的文件结构:

```

视频文件夹/

├── source.mp4 # 原始视频文件

└── hls/ # HLS 切片目录

├── index.m3u8 # 播放列表文件

├── segment_000.ts # TS 切片 1

├── segment_001.ts # TS 切片 2

├── segment_002.ts # TS 切片 3

└── ...

```


六、m3u8 文件格式示例

```m3u8

#EXTM3U

#EXT-X-VERSION:3

#EXT-X-TARGETDURATION:10

#EXTINF:10.0,

/api/case/GetCaseMediaFile?file=05-案例/视频/视频名/hls/segment_000.ts

#EXTINF:10.0,

/api/case/GetCaseMediaFile?file=05-案例/视频/视频名/hls/segment_001.ts

#EXTINF:10.0,

/api/case/GetCaseMediaFile?file=05-案例/视频/视频名/hls/segment_002.ts

#EXT-X-ENDLIST

```


七、完整流程总结

  1. 上传视频文件 → 保存到指定目录

  2. 创建 hls 子目录

  3. 调用 FFmpeg 进行转码切片

  4. 生成 `index.m3u8` 和多个 `segment_*.ts` 文件

  5. 修正 m3u8 中的 TS 路径为 API 路径

  6. 获取视频信息(时长、分辨率等)

  7. 更新数据库,保存 m3u8 路径

  8. 前端通过 m3u8 路径播放视频


八、常见问题

1. FFmpeg 路径找不到

  • 设置环境变量 `FFMPEG_PATH`

  • 或将 `ffmpeg.exe` 放在项目 `tools/ffmpeg/` 目录下

2. 转码失败

  • 检查输入文件是否存在

  • 检查输出目录是否有写入权限

  • 查看 FFmpeg 错误日志

3. m3u8 播放失败

  • 检查 TS 切片路径是否正确

  • 检查 API 接口是否允许匿名访问

  • 检查 CORS 响应头是否正确设置


九、扩展功能

带水印的转码

```csharp

// 使用 FFMPEGHelper.TranscodeWithWatermark 方法

var ffResult = FFMPEGHelper.TranscodeWithWatermark(

inputPath, // 输入视频路径

playlistPath, // m3u8 路径

segmentTemplate, // TS 切片模板

segmentTime, // 切片时长

watermarkText, // 水印文本

watermarkPosition // 水印位置:center/top-left/top-right/bottom-left/bottom-right

);

```


以上是完整的实现流程。按步骤实现即可完成视频转码和切片功能。

相关推荐
lfq7612042 小时前
.NET Framework 下 C# MVC 项目敏感信息安全存储方法
安全·c#·mvc·.net
m5655bj2 小时前
通过 C# 设置 Word 文档背景颜色、背景图
开发语言·c#·word
A_nanda13 小时前
c# MOdbus rto读写串口,如何不相互影响
算法·c#·多线程
码云数智-园园15 小时前
使用 C# 将 PowerPoint 演示文稿高效转换为 PDF 格式
c#
PfCoder16 小时前
WinForm真入门(23)---PictureBox 控件详细用法
开发语言·windows·c#·winform
gc_229919 小时前
C#学习调用OpenMcdf模块解析ole数据的基本用法(1)
c#·ole·openmcdf
MM_MS1 天前
Halcon图像点运算、获取直方图、直方图均衡化
图像处理·人工智能·算法·目标检测·计算机视觉·c#·视觉检测
老骥伏枥~1 天前
C# 控制台:Console.ReadLine / WriteLine
开发语言·c#
PfCoder2 天前
C#中定时器之System.Timers.Timer
c#·.net·visual studio·winform