Raspberry Pi 实时视频流传输到 .NET






4.87/5 (29投票s)
一个简单的示例,展示如何实现从 Raspberry Pi 摄像头到 .NET 应用程序的实时视频流传输。
引言
下面的示例演示了如何在 Raspberry Pi 上实现一个服务应用程序,该应用程序从摄像头捕获视频并将其流式传输到 .NET 客户端,客户端会处理并显示视频。
为了实现这个场景,需要在实现中解决以下主题:
- 由服务应用程序从 Raspberry Pi 摄像头捕获视频。
- 在网络上传输视频流。
- .NET 客户端应用程序处理和显示视频。
从 Raspberry Pi 摄像头捕获视频
Raspberry Pi 摄像头 是一款高清摄像头,以原始 H 264 格式生成视频数据。
为了控制和捕获视频,Raspberry Pi 提供了命令行应用程序 raspivid
,可以通过各种参数执行该应用程序,指定如何捕获视频。例如,您可以指定宽度、高度或每秒帧数等参数,以及视频是生成到文件还是标准输出 (stdout)。
本示例中实现的此服务应用程序内部使用了 raspivid 应用程序。要捕获视频,服务会启动 raspivid,然后从 raspivid 的 stdout 读取传入的视频数据。
服务使用以下参数执行 raspivid(这些参数适合实时流传输):
raspivid -n -vf -hf -ih -w 320 -h 240 -fps 24 -t 0 -o -
-n | 无预览。 |
-vf -hf | 垂直和水平翻转视频。 |
-ih | 将 SPS 和 PPS 内联头插入视频流(这样,如果第二个客户端连接到正在进行的流,它就可以与图像帧同步)。 |
-w 320 -h 240 | 生成 320 x 240 像素的视频。 |
-fps 24 | 生成每秒 24 帧的视频。 |
-t 0 | 无限时间捕获视频。 |
-o - | 将视频输出到标准输出(以便服务应用程序可以捕获它)。 |
您还可以尝试其他参数。要列出所有参数,可以运行:
raspivid -h
在网络上传输视频流
从 raspivid stdout 连续传入的实时视频数据需要通过网络传输到已连接的客户端。
为了传输数据,该实现使用了 Eneter Messaging Framework,这是一个轻量级的跨平台进程间通信库。
为了避免序列化/反序列化,通信直接基于双工通道。这意味着在 Raspberry Pi 上运行的服务应用程序使用双工输入通道,在 PC 上运行的 .NET 客户端使用双工输出通道。
然后,当从 raspivid stdout 读取一块视频数据时,服务会使用双工输入通道将数据发送到已连接的客户端。然后,.NET 客户端使用双工输出通道接收视频数据,并通知它进行进一步处理(例如显示)。
.NET 客户端处理和显示视频
虽然 H 264 是一种非常常见的编码,但播放这种编解码器编码的实时视频并非易事。
主要问题是 Media Element(WPF 的 UI 控件)不支持从内存流或仅从字节数组播放视频。它需要一个本地文件或 URL 的路径。
我找到了 一些 hack,建议注册自己的协议,这样当协议名称出现在 URI 中时,已注册的库就会被调用,但我不想采用这种解决方案。
另一个问题是,如果您使用的是 Windows Vista(或 Windows XP),您将需要安装 H 264 的编解码器(当我这样做时,我仍然无法播放存储在文件中的原始 H 264 字节)。
另一个选择是使用 Video Lan 的 VLC 库。虽然 VLC 不支持从内存流或字节数组播放,但它支持从命名管道播放。这意味着如果视频源指定为:
stream://\\\.\pipe\MyPipeName
那么 VLC 将尝试打开 MyPipeName 并播放视频。
但这也不是开箱即用的解决方案。
主要问题是 VLC 库仅导出纯 C 方法。因此,它不包含您可以直接拖放到 UI 中的 WPF UI 控件。
有几种实现基于 VLC 的 UI 控件的包装器(例如,我测试过的非常有前途的解决方案是 Roman Ginzburg 实现的 nVLC),但这些实现看起来相当复杂,有些还存在 bug。
我喜欢 Richard Starkey 描述的方法(第一部分和第二部分),即只提供一个薄包装器并直接使用 VLC 功能。优点是解决方案轻量级,并为您提供了完整的灵活性。而且正如您将看到的,它确实不难使用。
因此,我稍微修改了 Richard 的原始代码,并将其用于 .NET 客户端的实现。整个包装器的实现可以在 VLC.cs
文件中找到。
运行示例
下载
- 下载并解压此示例。
- 从 http://www.eneter.net/ProductDownload.htm 下载“Eneter for .NET”和“Eneter for Java”。
- 从 https://www.videolan.org/ 下载并安装 VLC 媒体播放器。(VLC 库将由 .NET 应用程序使用来播放视频流)
Raspberry Pi 服务应用程序
- 在 Eclipse 中打开 Java 项目 raspberry-camera-service,并添加对您下载的 eneter-messaging.jar 的引用。
(右键单击项目 -> Properties -> Java Build Path -> Libraries -> Add External Jars -> eneter-messaging-6.0.1.jar) - 构建项目,然后将其导出为可执行 jar。
(右键单击项目 -> Export... -> Java -> Runable JAR file -> Launch configuration -> Export Destination -> Package required libraries into generated JAR -> Finish.) - 将生成的 jar 复制到 Raspberry 设备。
- 启动应用程序
java -jar raspberry-camera-service.jar
.NET 客户端应用程序
- 打开 RaspberryCameraClient 解决方案,并添加对您下载的 Eneter.Messaging.Framework.dll 的引用。
- 检查 MainWindow.xaml.cs 中的 VLC 路径是否正确。
- 在 MainWindow.xaml.cs 中为您的 Raspberry Pi 服务提供正确的 IP 地址。
- 编译并运行。
- 按下“开始捕获”。
Raspberry Pi 服务应用程序
Raspberry Pi 服务是一个用 Java 实现的简单命令行应用程序。它侦听客户端。当第一个客户端连接时,它会启动 raspivid 应用程序以开始视频捕获。然后它消耗 raspivid 的 stdout 并将视频数据转发给已连接的客户端。
当第二个客户端连接时,它也开始将正在进行的流转发给第二个客户端。客户端将能够与视频流同步,因为它包含 SPS 和 PPS 内联头。
代码非常简单。
package eneter.camera.service;
import java.io.InputStream;
import java.util.HashSet;
import eneter.messaging.diagnostic.EneterTrace;
import eneter.messaging.messagingsystems.messagingsystembase.*;
import eneter.messaging.messagingsystems.tcpmessagingsystem.TcpMessagingSystemFactory;
import eneter.messaging.messagingsystems.udpmessagingsystem.UdpMessagingSystemFactory;
import eneter.net.system.EventHandler;
class CameraService
{
// Channel used to response the image.
private IDuplexInputChannel myVideoChannel;
private Process myRaspiVidProcess;
private InputStream myVideoStream;
private HashSet<String> myConnectedClients = new HashSet<String>();
private boolean myClientsUpdatedFlag;
private Object myConnectionLock = new Object();
public void startService(String ipAddress, int port) throws Exception
{
try
{
// Use TCP messaging.
// Note: you can try UDP or WebSockets too.
IMessagingSystemFactory aMessaging = new TcpMessagingSystemFactory();
//IMessagingSystemFactory aMessaging = new UdpMessagingSystemFactory();
myVideoChannel = aMessaging.createDuplexInputChannel(
"tcp://" + ipAddress + ":" + port + "/");
myVideoChannel.responseReceiverConnected().subscribe(myClientConnected);
myVideoChannel.responseReceiverDisconnected().subscribe(myClientDisconnected);
myVideoChannel.startListening();
}
catch (Exception err)
{
stopService();
throw err;
}
}
public void stopService()
{
if (myVideoChannel != null)
{
myVideoChannel.stopListening();
}
}
private void onClientConnected(Object sender, ResponseReceiverEventArgs e)
{
EneterTrace.info("Client connected.");
try
{
synchronized (myConnectionLock)
{
myConnectedClients.add(e.getResponseReceiverId());
myClientsUpdatedFlag = true;
// If camera is not running start it.
if (myRaspiVidProcess == null)
{
// Captured video: 320x240 pixels, 24 frames/s
// And it also inserts SPS and PPS inline headers (-ih) so that
// later connected clients can synchronize to ongoing video frames.
String aToExecute = "raspivid -n -vf -hf -ih -w 320 -h 240 -fps 24 -t 0 -o -";
myRaspiVidProcess = Runtime.getRuntime().exec(aToExecute);
myVideoStream = myRaspiVidProcess.getInputStream();
Thread aRecordingThread = new Thread(myCaptureWorker);
aRecordingThread.start();
}
}
}
catch (Exception err)
{
String anErrorMessage = "Failed to start video capturing.";
EneterTrace.error(anErrorMessage, err);
return;
}
}
private void onClientDisconnected(Object sender, ResponseReceiverEventArgs e)
{
EneterTrace.info("Client disconnected.");
synchronized (myConnectionLock)
{
myConnectedClients.remove(e.getResponseReceiverId());
myClientsUpdatedFlag = true;
// If no client is connected then turn off the camera.
if (myConnectedClients.isEmpty() && myRaspiVidProcess != null)
{
myRaspiVidProcess.destroy();
myRaspiVidProcess = null;
}
}
}
private void doCaptureVideo()
{
try
{
String[] aClients = {};
byte[] aVideoData = new byte[4096];
while (myVideoStream.read(aVideoData) != -1)
{
// Only if amount of connected clients changed update
// the local list.
if (myClientsUpdatedFlag)
{
aClients = new String[myConnectedClients.size()];
synchronized (myConnectionLock)
{
myConnectedClients.toArray(aClients);
myClientsUpdatedFlag = false;
}
}
for (String aClient : aClients)
{
try
{
// Send captured data to all connected clients.
myVideoChannel.sendResponseMessage(aClient, aVideoData);
}
catch (Exception err)
{
// Ignore if sending to one of clients failed.
// E.g. in case it got disconnected.
}
}
}
}
catch (Exception err)
{
// Stream from raspivid got closed.
}
EneterTrace.info("Capturing thread ended.");
}
private EventHandler<ResponseReceiverEventArgs> myClientConnected
= new EventHandler<ResponseReceiverEventArgs>()
{
@Override
public void onEvent(Object sender, ResponseReceiverEventArgs e)
{
onClientConnected(sender, e);
}
};
private EventHandler<ResponseReceiverEventArgs> myClientDisconnected
= new EventHandler<ResponseReceiverEventArgs>()
{
@Override
public void onEvent(Object sender, ResponseReceiverEventArgs e)
{
onClientDisconnected(sender, e);
}
};
private Runnable myCaptureWorker = new Runnable()
{
@Override
public void run()
{
doCaptureVideo();
}
};
}
.NET 客户端应用程序
.NET 客户端是一个简单的 WPF 应用程序。当用户单击“开始捕获”时,它会创建一个命名管道,并将 VLC 设置为使用该命名管道作为其视频源。它还将 VLC 设置为期望原始 H 264 编码的视频数据。然后,使用 Eneter,它打开与 Raspberry Pi 服务的连接。收到视频数据后,它会将收到的数据写入命名管道,以便 VLC 可以处理和显示它。
请不要忘记为您的 Raspberry Pi 提供正确的 IP 地址,并检查您是否需要更新 VLC 的路径!
代码非常简单。
using System;
using System.IO.Pipes;
using System.Threading;
using System.Windows;
using Eneter.Messaging.MessagingSystems.MessagingSystemBase;
using Eneter.Messaging.MessagingSystems.TcpMessagingSystem;
using Eneter.Messaging.MessagingSystems.UdpMessagingSystem;
using VLC;
namespace RaspberryCameraClient
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private IDuplexOutputChannel myVideoChannel;
// VLC will read video from the named pipe.
private NamedPipeServerStream myVideoPipe;
private VlcInstance myVlcInstance;
private VlcMediaPlayer myPlayer;
public MainWindow()
{
InitializeComponent();
System.Windows.Forms.Panel aVideoPanel = new System.Windows.Forms.Panel();
aVideoPanel.BackColor = System.Drawing.Color.Black;
VideoWindow.Child = aVideoPanel;
// Provide path to your VLC.
myVlcInstance = new VlcInstance(@"c:\Program Files\VideoLAN\VLC\");
// Use TCP messaging.
// You can try to use UDP or WebSockets too.
myVideoChannel = new TcpMessagingSystemFactory()
//myVideoChannel = new UdpMessagingSystemFactory()
// Note: Provide address of your service here.
.CreateDuplexOutputChannel("tcp://192.168.1.17:8093/");
myVideoChannel.ResponseMessageReceived += OnResponseMessageReceived;
}
private void Window_Closed(object sender, EventArgs e)
{
StopCapturing();
}
private void OnStartCapturingButtonClick(object sender, RoutedEventArgs e)
{
StartCapturing();
}
private void OnStopCapturingButtonClick(object sender, RoutedEventArgs e)
{
StopCapturing();
}
private void StartCapturing()
{
// Use unique name for the pipe.
string aVideoPipeName = Guid.NewGuid().ToString();
// Open pipe that will be read by VLC.
myVideoPipe = new NamedPipeServerStream(@"\" + aVideoPipeName,
PipeDirection.Out, 1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous, 0, 32764);
ManualResetEvent aVlcConnectedPipe = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem(x =>
{
myVideoPipe.WaitForConnection();
// Indicate VLC has connected the pipe.
aVlcConnectedPipe.Set();
});
// VLC connects the pipe and starts playing.
using (VlcMedia aMedia = new VlcMedia(myVlcInstance,
@"stream://\\\.\pipe\" + aVideoPipeName))
{
// Setup VLC so that it can process raw h264 data (i.e. not in mp4 container)
aMedia.AddOption(":demux=H264");
myPlayer = new VlcMediaPlayer(aMedia);
myPlayer.Drawable = VideoWindow.Child.Handle;
// Note: This will connect the pipe and read the video.
myPlayer.Play();
}
// Wait until VLC connects the pipe so that it is ready to receive the stream.
if (!aVlcConnectedPipe.WaitOne(5000))
{
throw new TimeoutException("VLC did not open connection with the pipe.");
}
// Open connection with service running on Raspberry.
myVideoChannel.OpenConnection();
}
private void StopCapturing()
{
// Close connection with the service on Raspberry.
myVideoChannel.CloseConnection();
// Close the video pipe.
if (myVideoPipe != null)
{
myVideoPipe.Close();
myVideoPipe = null;
}
// Stop VLC.
if (myPlayer != null)
{
myPlayer.Dispose();
myPlayer = null;
}
}
private void OnResponseMessageReceived(object sender, DuplexChannelMessageEventArgs e)
{
byte[] aVideoData = (byte[])e.Message;
// Forward received data to the named pipe so that VLC can process it.
myVideoPipe.Write(aVideoData, 0, aVideoData.Length);
}
}
}