1. 程式人生 > >C#FFmpeg視訊採集與推送RTMP伺服器程式碼思路整理

C#FFmpeg視訊採集與推送RTMP伺服器程式碼思路整理

C#視訊採集與推送RTMP伺服器程式碼思路整理:在看過FFmpeg後是否認為寫C#的視訊流採集和推送還是一頭霧水啊?深有此感。領導是C#的高手,說可以通過C或C++的程式碼直接複製貼上到C#工程然後進行適配程式碼就可以了,因為C#使用ffmpeg的類名和變數、方法等都與C保持高度一致的,經領導這麼一說C#裡面只需要參考C或C++的實現就可以完成相關的操作了,這樣就更容易理解了(涉及到指標問題,C#也支援)。本文旨在分析實現思路,而不提供原始碼,敬請諒解!

服務端和客戶端的職能


客戶端負責視訊採集,服務端負責視訊轉發或播放

整體設計視訊傳輸架構


說明:視訊資料傳送H264資料包給服務端,服務端獲取到資料包進行編解碼轉成FLV格式的流,而RTMP只支援FLV格式的流。

原理:socket視訊資料採集,socket 資料包傳送,編解碼處理,推流。

視訊直播推流效果展示

啟動順序:程式客戶端和服務端是兩個專案需要分別啟動,先啟動服務端,再啟動客戶端,允許多個例項存在。

第一幕:啟動視訊編解碼服務端

第二幕:啟動視訊採集攝像頭客戶端

點選開始連線到服務端。

第三幕:服務端處理要連線的客戶端並建立資料傳輸

選擇客戶端,並開始傳輸視訊,此時會彈出攝像頭訪問視窗。

第四幕:驗證直播是否進行

訪問RTMP伺服器狀態監控地址,如:http://172.16.20.10:1990/stat

第五幕:使用ffplay進行直播流播放

cmd進入ffmpeg的路徑執行:ffplay rtmp://172.16.20.10:1935/live/S013333333333

以上就是所有架構實現的效果了。

攝像頭視訊採集客戶端

客戶端窗體變數:

// socket資料包封裝物件
JTData jtdata = new JTData();
// 資料分包物件
SplitData splitData = new SplitData();
// TCP通訊通道
Network.TCPChannel channel;
// 標識使用者手機號
string SimKey = "013333333333";
// 執行緒物件
Thread thVideo;
// Socket例項物件
private Socket send_sock;
private Queue<Bitmap> mQueues = new Queue<Bitmap>();
// 攝像頭處理H264物件
Cam2H264 cam2H264 = new Cam2H264();

private Setting setting;
IPEndPoint iPEndPoint;
客戶端窗體開始錄影按鈕處理:
 private void btnStart_Click(object sender, EventArgs e)
        {
            channel = new Network.TCPChannel(txtServer.Text.Trim(), Convert.ToInt32(txtPort.Text.Trim()));//例項化TCP連線通道
            channel.DataReceive = Receive;
            channel.DataSend = DataSend;
            channel.ChannelConnect += new EventChannelConnect(channel_ChannelConnect);// 設定通道連線事件
            channel.Connect();
            //channel.StartReceiveAsync();


            btnStart.Enabled = false;
            btnStop.Enabled = true;
        }

客戶端窗體TCP通道回撥函式:
 void channel_ChannelConnect(object sender, ChannelConnectArg arg)
        {
            Console.WriteLine(arg.SocketError.ToString());
            SendHeart();
        }

        private void SendHeart()
        {
            var lst = jtdata.Package(0x0002, SimKey);
            SendAnswer(lst);
        }

 #region 鏈路
        public void Receive(object sender, ChannelReceiveArg arg)
        {
            splitData.SplitDataBy7E(arg.Data, ((byte[] bGps) =>
            {
                if (bGps.Length > 11 && CheckHelper.CheckXOR(bGps, 0, bGps.Length - 1) == bGps[bGps.Length - 1])//效驗通過
                {
                    var head = JTHeader.NewEntity(bGps);
                    JXData(bGps, head);
                }
            }));
        }

        public void JXData(byte[] bGps, JTHeader head)
        {
            switch (head.MsgId)
            {
                case 0x8001://通用應答
                    break;
                case 0x8B00://4.1.1 實時音視訊傳輸請求
                    if (thVideo != null && thVideo.IsAlive)
                    {
                        Console.WriteLine("已經在傳輸");
                    }
                    else
                    {
                        BaseLineTime = DateTime.Now;
                        SerialNumber = 0;
                        LastIFrameTime = DateTime.MinValue;
                        LastFrameTime = DateTime.MinValue;
                        var req = JTRealVideoTransferRequest.NewEntity(bGps, head.HeadLen);
                        thVideo = new Thread(StartVideo);
                        thVideo.IsBackground = true;
                        iPEndPoint = new IPEndPoint(IPAddress.Parse(req.IPAddress), req.UdpPort);
                        thVideo.Start(iPEndPoint);
                    }
                    break;
                default:
                    break;
            }
        }
        public void DataSend(object sender, ChannelSendArg arg)
        {
        }

  public bool SendAnswer(JTPData data)
        {
            foreach (var item in data)
            {
                channel.Send(item.Data.ToArray());
                //return true;
            }
            return true;
        }
客戶端窗體執行緒處理視訊資料:
unsafe void StartVideo(object ep)
        {
            try
            {
                send_sock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
                send_sock.SendBufferSize = 1000000;



                cam2H264.Run(setting.VideoDevice, SendToServer, 0);

            }
            catch (Exception ex)
            {
            }
        }

具體264的處理基本上可以參考FFmpeg的C實現方式來做,下面是步驟:

視訊資料包轉發服務端

注:實際服務端未做轉碼處理,已在客戶端執行緒中處理。

服務端窗體常量:

// 開啟本機TCP連線
Network.TCPServer ser = new Network.TCPServer("0.0.0.0", 9700);
// RTP服務端
RTPServer rtServer;
// 連線到的客戶端列表
List<VideoClient> lstTCPChannel = new List<VideoClient>();
// 資料包拆分物件
JX.SplitData splitData = new JX.SplitData();

服務端窗體事件繫結與客戶端連線自動發現:
        #region 窗體事件
        private void frmServer_FormClosing(object sender, FormClosingEventArgs e)
        {
            try
            {
                ser.Stop();
            }
            catch
            {
            }
            try
            {
                rtServer.Dispose();
            }
            catch
            {
            }
        }
        private void btnStart_Click(object sender, EventArgs e)
        {
            var vc = lstTCPChannel[lstBoxTcp.SelectedIndex];

            //rtServer.AddClient(vc.SimKey, "rtmp://localhost:32768/rtmplive/S" + vc.SimKey);
            rtServer.AddClient(vc.SimKey, "rtmp://172.16.20.10:1935/live/S" + vc.SimKey);// 新增直播客戶端---RTMP推流伺服器地址和流名稱
            vc.SendVideoCtrl(new JTRealVideoTransferRequest
            {
                IPAddress = "127.0.0.1",
                UdpPort = 9700,
                Channel = 0,
                DataType = 1,
                StreamType =  StreamType.SubStream
            });
        }

        private void frmServer_Load(object sender, EventArgs e)
        {
            rtServer = new RTPServer(9700, pictureBox1);//RTP服務端影象傳輸物件
            rtServer.Start();
            ser.ChannelConnect += new Network.EventChannelConnect(ser_ChannelConnect);// 通道連線事件
            ser.ChannelDispose += new Network.EventChannelDispose(ser_ChannelDispose);// 通道連線斷開事件
            ser.Start();

            Network.Utils.Setup(10);
        }
        #endregion

服務端窗體客戶端連線列表控制:
 #region 列表控制
        VideoClient AddToList(Channel channel)
        {
            var item = new VideoClient(channel, splitData);
            channel.Tag = item;
            lstTCPChannel.Add(item);
            //rtServer.AddClient(item.SimKey, "rtmp://localhost:32768/rtmplive/S" + item.SimKey);
            rtServer.AddClient(item.SimKey, "rtmp://172.16.20.10:1935/live/S" + item.SimKey);// 新增直播客戶端---RTMP推流伺服器地址和流名稱
            lstBoxTcp.BeginInvoke(new MethodInvoker(() =>
            {
                lstBoxTcp.Items.Add(channel.RemoteHost + ":" + channel.RemotePort);
            }));

            return item;
        }

        void RemoveList(Channel channel)
        {
            var item = (VideoClient)channel.Tag;
            lstTCPChannel.Remove(item);
            rtServer.RemoveClient(item.SimKey);
            lstBoxTcp.Invoke(new MethodInvoker(() =>
            {
                lstBoxTcp.Items.Remove(channel.RemoteHost + ":" + channel.RemotePort);
            }));
        }
        #endregion
服務端窗體通訊鏈路事件方法:
        #region 鏈路相關
        void ser_ChannelDispose(object sender, Network.ChannelDisposeArg arg)
        {
            RemoveList(arg.Channel);
        }

        void ser_ChannelConnect(object sender, Network.ChannelConnectArg arg)
        {
            var item = AddToList(arg.Channel);
            arg.Channel.DataReceive = item.Receive;
            arg.Channel.DataSend = item.DataSend;
            arg.Channel.StartReceiveAsync();
        }
        #endregion

大致程式碼的思路都在此了。

需要思考的問題

想達到這樣的目的:視訊能夠一邊下載一邊播放。如何程式設計實現伺服器端的視訊流資訊向客戶端的傳送? 客服端又如何接收並播放視訊?

使用System.Net的NetStream和使用System.IO的FileStream、MediaPlayer播放外掛。

(1)在服務端建立一個Socket服務,將檔案分段放入緩衝區。
(2)在客戶端建立一個Socket客戶端,讀取服務端的緩衝區內容。
(3)將讀到的部分發送給MediaPlayer進行播放。


上面的客戶端和服務端的情況就是按照這種思路來寫的,後續需要加入視訊預覽、播放的功能。實際上就是將H264流資料轉成YUV/RGB->Bitmap前端進行展示,然後加入音訊資料傳輸的執行緒等進行處理同步播放。


上圖是伺服器端獲取到的H264->YUV播放示例。