65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (29投票s)

2014 年 8 月 31 日

CPOL

6分钟阅读

viewsIcon

94803

downloadIcon

1646

一个简单的示例,展示如何实现从 Raspberry Pi 摄像头到 .NET 应用程序的实时视频流传输。

引言

下面的示例演示了如何在 Raspberry Pi 上实现一个服务应用程序,该应用程序从摄像头捕获视频并将其流式传输到 .NET 客户端,客户端会处理并显示视频。
为了实现这个场景,需要在实现中解决以下主题:

  • 由服务应用程序从 Raspberry Pi 摄像头捕获视频。
  • 在网络上传输视频流。
  • .NET 客户端应用程序处理和显示视频。

 

810004/RaspberryVideostreamingToNET.png

从 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 文件中找到。

运行示例

下载

  1. 下载并解压此示例。
  2. http://www.eneter.net/ProductDownload.htm 下载“Eneter for .NET”和“Eneter for Java”。
  3. https://www.videolan.org/ 下载并安装 VLC 媒体播放器。(VLC 库将由 .NET 应用程序使用来播放视频流)

Raspberry Pi 服务应用程序

  1. 在 Eclipse 中打开 Java 项目 raspberry-camera-service,并添加对您下载的 eneter-messaging.jar 的引用。
    (右键单击项目 -> Properties -> Java Build Path -> Libraries -> Add External Jars -> eneter-messaging-6.0.1.jar)
  2. 构建项目,然后将其导出为可执行 jar。
    (右键单击项目 -> Export... -> Java -> Runable JAR file -> Launch configuration -> Export Destination -> Package required libraries into generated JAR -> Finish.)
  3. 将生成的 jar 复制到 Raspberry 设备。
  4. 启动应用程序
    java -jar raspberry-camera-service.jar

.NET 客户端应用程序

  1. 打开 RaspberryCameraClient 解决方案,并添加对您下载的 Eneter.Messaging.Framework.dll 的引用。
  2. 检查 MainWindow.xaml.cs 中的 VLC 路径是否正确。
  3. MainWindow.xaml.cs 中为您的 Raspberry Pi 服务提供正确的 IP 地址。
  4. 编译并运行。
  5. 按下“开始捕获”。

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);
        }
    }
}
© . All rights reserved.