纯C#实现了RTSP摄像头拉流并转存MP4文件

在实际项目中,经常需要处理摄像头视频流,特别是RTSP协议的IPC摄像头。本文将介绍如何使用纯C#实现RTSP拉流,并将视频流保存为MP4文件,无需依赖外部库如FFmpeg。

技术选型

在.NET生态中,处理RTSP流通常有以下几种选择:

  1. FFmpeg.Autogen:通过P/Invoke调用FFmpeg库

  2. LibVLC:使用VLC的绑定库

  3. RtspClientSharp:纯C#实现的RTSP客户端

为了做到"纯C#实现",我选择了RtspClientSharp 作为RTSP客户端库,配合SharpAvi进行MP4编码。

环境准备

首先创建.NET 6.0控制台应用程序,并添加NuGet包:

dotnet add package RtspClientSharp

dotnet add package SharpAvi

dotnet add package SharpAvi.Output

实现原理

整个流程可以分为三个主要部分:

  1. RTSP连接和视频数据接收

  2. H.264/H.265视频帧解码

  3. AVI/MP4封装写入

代码实现

1. 创建RTSP客户端

using RtspClientSharp;

using RtspClientSharp.RawFrames;

using RtspClientSharp.RawFrames.Video;

public class RtspToMp4Recorder : IDisposable

{

private readonly string _rtspUrl;

private readonly string _outputPath;

private readonly string _username;

private readonly string _password;

private IRtspClient _rtspClient;

private VideoFileWriter _videoWriter;

private bool _isRecording;

private DateTime _startTime;

private readonly object _lockObject = new object();

public RtspToMp4Recorder(string rtspUrl, string outputPath, string username = "", string password = "")

{

_rtspUrl = rtspUrl;

_outputPath = outputPath;

_username = username;

_password = password;

}

public async Task StartRecordingAsync()

{

var connectionParameters = new ConnectionParameters(new Uri(_rtspUrl))

{

RequiredTcpPort = RtspPort.Auto,

UseTcpForRtsp = true,

UseTcpForRtp = true,

RtpTransport = RtpTransportProtocol.TCP

};

if (!string.IsNullOrEmpty(_username))

{

connectionParameters.UserAgent = _username;

connectionParameters.Password = _password;

}

_rtspClient = new RtspClient(connectionParameters);

_rtspClient.FrameReceived += OnFrameReceived;

_videoWriter = new VideoFileWriter();

_videoWriter.Open(_outputPath, 1920, 1080, 25, VideoCodec.Mpeg4);

_isRecording = true;

_startTime = DateTime.Now;

Console.WriteLine($"开始录制: {_rtspUrl}");

await _rtspClient.StartAsync();

// 保持连接

while (_isRecording)

{

await Task.Delay(1000);

}

}

public void StopRecording()

{

_isRecording = false;

_rtspClient?.Stop();

_videoWriter?.Close();

Console.WriteLine($"录制结束,文件保存至: {_outputPath}");

}

}

2. 处理视频帧

private void OnFrameReceived(object sender, RawFrame rawFrame)

{

if (!_isRecording) return;

try

{

// 处理视频帧

if (rawFrame is RawVideoFrame videoFrame)

{

ProcessVideoFrame(videoFrame);

}

}

catch (Exception ex)

{

Console.WriteLine($"处理帧时出错: {ex.Message}");

}

}

private void ProcessVideoFrame(RawVideoFrame videoFrame)

{

lock (_lockObject)

{

byte[] frameData = new byte[videoFrame.FrameSegment.Count];

Array.Copy(videoFrame.FrameSegment.Array,

videoFrame.FrameSegment.Offset,

frameData, 0,

videoFrame.FrameSegment.Count);

// 根据帧类型处理

switch (videoFrame)

{

case RawH264Frame h264Frame:

ProcessH264Frame(h264Frame, frameData);

break;

case RawH265Frame h265Frame:

ProcessH265Frame(h265Frame, frameData);

break;

case RawJpegFrame jpegFrame:

ProcessJpegFrame(jpegFrame, frameData);

break;

}

}

}

3. H.264帧处理

private void ProcessH264Frame(RawH264Frame h264Frame, byte[] frameData)

{

// 解析H.264 NALU

int nalType = frameData[0] & 0x1F;

// 根据NALU类型处理

switch (nalType)

{

case 7: // SPS

Console.WriteLine("收到SPS帧");

// 保存SPS用于解码器初始化

break;

case 8: // PPS

Console.WriteLine("收到PPS帧");

break;

case 5: // IDR帧(关键帧)

Console.WriteLine($"收到IDR帧,大小: {frameData.Length} bytes");

// 关键帧可以作为新的GOP开始

WriteVideoFrame(frameData, true);

break;

default: // 非关键帧

if (nalType <= 5)

{

WriteVideoFrame(frameData, false);

}

break;

}

}

private void WriteVideoFrame(byte[] frameData, bool isKeyFrame)

{

try

{

// 使用SharpAvi写入视频帧

// 注意:这里需要根据实际视频格式进行适当处理

_videoWriter?.WriteVideoFrame(frameData);

// 简单的帧率控制

TimeSpan elapsed = DateTime.Now - _startTime;

int expectedFrames = (int)(elapsed.TotalSeconds * 25);

// 可选:输出录制状态

if (_videoWriter.FramesWritten % 100 == 0)

{

Console.WriteLine($"已写入 {_videoWriter.FramesWritten} 帧");

}

}

catch (Exception ex)

{

Console.WriteLine($"写入视频帧失败: {ex.Message}");

}

}

4. 注意,RTSP数据需要处理后在获取NAL罗数据,在写入MP4文件

/// <summary>

/// 去掉 Annex-B 起始码(00 00 01 / 00 00 00 01),返回裸 NAL 数据。

/// </summary>

private byte[] StripAnnexBPrefix(ReadOnlySpan<byte> nal)

{

int offset = 0;

if (nal.Length >= 4 && nal[0] == 0x00 && nal[1] == 0x00 && nal[2] == 0x00 && nal[3] == 0x01)

{

offset = 4;

}

else if (nal.Length >= 3 && nal[0] == 0x00 && nal[1] == 0x00 && nal[2] == 0x01)

{

offset = 3;

}

return nal[offset..].ToArray();

}

相关推荐
石山代码6 分钟前
C++ 内存分区 堆区
java·开发语言·c++
无风听海26 分钟前
C# 隐式转换深度解析
java·开发语言·c#
LateFrames1 小时前
520 - 如何说晚安 (WPF)
c#·wpf·浪漫·ui体验
一只大袋鼠1 小时前
Git 进阶(二):分支管理、暂存栈、远程仓库与多人协作
java·开发语言·git
LuminousCPP2 小时前
数据结构 - 线性表第四篇:C 语言通讯录优化升级全记录(踩坑 + 思考)
c语言·开发语言·数据结构·经验分享·笔记·学习
魔法阵维护师2 小时前
从零开发游戏需要学习的c#模块,第十四章(保存和加载)
学习·游戏·c#
web3.08889992 小时前
1688 图搜接口(item_search_img / 拍立淘) 接入方法
开发语言·python
один but you3 小时前
从可变参数到 emplace:现代 C++ 性能优化的核心组合
java·开发语言
MY_TEUCK4 小时前
【Java 后端 | Nacos 注册中心】微服务治理原理、选型与注册发现实战
java·开发语言·微服务
测试员周周4 小时前
【Appium 系列】第13节-混合测试执行器 — API + UI 的协同执行
开发语言·人工智能·python·功能测试·ui·appium·pytest