websocket流读取摄像头,实现web页面实时监控
基于RTSP协议转发,websocket推流到web页面
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace HSJM.CameraServer
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddWebSocketManager();
services.AddCors();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseWebSockets();
app.MapWebSocketManager("/ws", app.ApplicationServices.CreateScope().ServiceProvider.GetService<CameraWebSocketHandler>());
app.UseRouting();
app.UseCors(builder =>
{
builder.AllowAnyOrigin();
builder.AllowAnyHeader();
builder.AllowAnyMethod();
});
app.UseEndpoints(endpoints =>
{
//endpoints.MapControllers();
});
app.UseStaticFiles();
}
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Arim.Utils.Network
{
/// <summary>
/// RTSP连接代理,不解析RTSP协议和报文内容,只做透明转发.
/// </summary>
public class RtspProxy
{
public RtspProxy()
{
}
private string hostname;
private int port;
private TcpClient tcpclient;
private Thread _thread;
private Stream _stream;
bool quitflag = false;
public bool Connect(string host, int port)
{
this.hostname = host;
this.port = port;
try
{
tcpclient = new TcpClient(host, port);
}
catch
{
return false;
}
if (!tcpclient.Connected)
{
return false;
}
_stream = tcpclient.GetStream();
return true;
}
public void Start()
{
quitflag = false;
if (_thread == null)
{
_thread = new Thread(Dowork);
_thread.Start();
}
}
public void Close()
{
quitflag = true;
if (_thread != null)
{
_thread.Join();
_thread = null;
}
Thread.Sleep(100);
try
{
if (_stream != null)
_stream.Close();
tcpclient.Close();
}
catch (Exception ex)
{
// Logger.Write(ex);
}
// Logger.Debug("Connection Close");
}
Queue<byte[]> dataQueue = new Queue<byte[]>();
public bool TryDequeData(out List<byte[]> datas, int max)
{
datas = null;
lock (dataQueue)
{
int count = dataQueue.Count;
int toread = count > max ? max : count;
if (toread > 0)
{
datas = new List<byte[]>();
for (int i = 0; i < toread; i++)
{
datas.Add(dataQueue.Dequeue());
}
return true;
}
return false;
}
}
Queue<byte[]> controlQueue = new Queue<byte[]>();
public bool TryDequeControl(out List<byte[]> datas, int max)
{
datas = null;
lock (controlQueue)
{
int count = controlQueue.Count;
int toread = count > max ? max : count;
if (toread > 0)
{
datas = new List<byte[]>();
for (int i=0;i<toread;i++)
{
datas.Add(controlQueue.Dequeue());
}
return true;
}
return false;
}
}
private void Dowork()
{
try
{
int remainlen = 0;//当前读取到的memory位置.
byte[] buffer = new byte[8192];
while (!quitflag)
{
int readlength = 0;
readlength = _stream.Read(buffer, remainlen, 4096);
if (readlength == 0)
{
break;
}//链接关闭.
int bufferlength = remainlen + readlength;
int pos = 0;
while (pos < bufferlength)
{
if (buffer[pos] == '$')
{
if (pos + 3 > bufferlength)
{
break;
}//need read more.
int l = (buffer[pos+2] << 8) + buffer[pos+3] + 4;
if (pos + l > bufferlength)
{
break;
}//need read more.
byte[] bs = new byte[l];
Array.Copy(buffer, pos, bs, 0, l);
lock(dataQueue)
{
if (dataQueue.Count < 1000)
{
dataQueue.Enqueue(bs);
}//discard data when >= 1000
}
pos += l;
}//data
else
{
string strline;
byte lineend = (byte)'\n';
string contentlengthkey = "Content-Length";
int contentlength = 0;
int start = pos, i = pos;
bool getcontentlength = false;
while (i < bufferlength)
{
if (buffer[i] == lineend)
{
int linelength = i - start - 1;//-1 for \r
if (!getcontentlength && linelength > 0)
{
strline = ASCIIEncoding.UTF8.GetString(buffer, start, linelength);
if (strline.Contains(contentlengthkey))
{
string[] parts = strline.Split(":");
contentlength = Convert.ToInt32(parts[1].Trim());
getcontentlength = true;
}
}
start = i + 1;//next line +1 for \n
if (linelength == 0)
{
break;
}//header end with \r\n\r\n
}
i++;
}
int end = start + contentlength;
if (end == pos || end > bufferlength)//end==pos 等于一行也没读到,end > bufferlength等于有剩余的内容没读完.
{
break;
}//need readmore.
var bs = new byte[end-pos];
Array.Copy(buffer, pos, bs, 0, end - pos);
lock(controlQueue)
{
controlQueue.Enqueue(bs);
}
pos = end;
}//control
}
remainlen = bufferlength - pos;
for (int j = 0; j < remainlen; j++)
{
buffer[j] = buffer[j + pos];
}
}
}
catch (IOException error)
{
//Logger.Error(error);
}
Close();
}
public async Task Send(byte[] data)
{
if (data != null && data.Length > 0)
{
await _stream.WriteAsync(data, 0, data.Length);
await _stream.FlushAsync();
}
}
}
}
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace HSJM.CameraServer
{
public class WebSocketManagerMiddleware
{
private readonly RequestDelegate _next;
private WebSocketHandler _webSocketHandler { get; set; }
public WebSocketManagerMiddleware(RequestDelegate next,
WebSocketHandler webSocketHandler)
{
_next = next;
_webSocketHandler = webSocketHandler;
}
public async Task Invoke(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
{
await _next.Invoke(context);
return;
}
if (context.Request.Headers.ContainsKey("Sec-WebSocket-Protocol"))
{
context.Response.Headers.TryAdd("Sec-WebSocket-Protocol", context.Request.Headers["Sec-WebSocket-Protocol"]);
}//在报文头加入Sec-WebSocket-Protocol.
var socket = await context.WebSockets.AcceptWebSocketAsync();
_webSocketHandler.OnConnected(socket);
await Receive(socket, async (result, buffer) =>
{
if (result.MessageType == WebSocketMessageType.Close)
{
try
{
await _webSocketHandler.OnDisconnected(socket);
}
catch (Exception ex)
{
// Logger.Error(String.Format("ws Close Error: {0}", ex.Message));
}
return;
}
else
{
try
{
await _webSocketHandler.ReceiveAsync(socket, result, buffer);
}
catch (Exception ex)
{
// Logger.Error(String.Format("ws Receive Error: {0}", ex.Message));
}
return;
}
});
//TODO - investigate the Kestrel exception thrown when this is the last middleware
//await _next.Invoke(context);
}
private async Task Receive(WebSocket socket, Action<WebSocketReceiveResult, byte[]> handleMessage)
{
var buffer = new byte[1024 * 4];
while (socket.State == WebSocketState.Open)
{
var result = await socket.ReceiveAsync(buffer: new ArraySegment<byte>(buffer),
cancellationToken: CancellationToken.None);
if (result != null && result.Count > 0)
{
var validbuffer = new byte[result.Count];
Array.Copy(buffer, validbuffer, result.Count);
handleMessage(result, validbuffer);
}
else
handleMessage(result, null);
}
}
}
public static class WebSocketManagerExtensions
{
public static IServiceCollection AddWebSocketManager(this IServiceCollection services, Assembly assembly = null)
{
services.AddTransient<WebSocketConnectionManager>();
Assembly ass = assembly ?? Assembly.GetEntryAssembly();
foreach (var type in ass.ExportedTypes)
{
if (type.GetTypeInfo().BaseType == typeof(WebSocketHandler))
{
services.AddSingleton(type);
}
}
return services;
}
public static IApplicationBuilder MapWebSocketManager(this IApplicationBuilder app,
PathString path,
WebSocketHandler handler)
{
return app.Map(path, (_app) => _app.UseMiddleware<WebSocketManagerMiddleware>(handler));
}
}
/// <summary>
/// keeps all active sockets in a thread-safe collection and assigns each a unique ID,
/// while also maintaning the collection (getting, adding and removing sockets).
/// </summary>
public class WebSocketConnectionManager
{
private ConcurrentDictionary<string, WebSocket> _sockets = new ConcurrentDictionary<string, WebSocket>();
public WebSocket GetSocketById(string id)
{
return _sockets.FirstOrDefault(p => p.Key == id).Value;
}
public ConcurrentDictionary<string, WebSocket> GetAll()
{
return _sockets;
}
public string GetId(WebSocket socket)
{
return _sockets.FirstOrDefault(p => p.Value == socket).Key;
}
public void AddSocket(WebSocket socket)
{
_sockets.TryAdd(CreateConnectionId(), socket);
}
public async Task RemoveSocket(string id)
{
WebSocket socket;
_sockets.TryRemove(id, out socket);
try
{
await socket.CloseOutputAsync(closeStatus: WebSocketCloseStatus.NormalClosure,
statusDescription: "Closed by the WebSocketManager",
cancellationToken: CancellationToken.None);//CloseAsync抛出异常
}
catch(Exception ex)
{
//Logger.Error(ex);
}
}
private string CreateConnectionId()
{
return Guid.NewGuid().ToString();
}
}
/// <summary>
/// handles connection and disconnection events and manages sending and receiving messages from the socket.
/// </summary>
public abstract class WebSocketHandler
{
protected WebSocketConnectionManager WebSocketConnectionManager { get; set; }
public WebSocketHandler(WebSocketConnectionManager webSocketConnectionManager)
{
WebSocketConnectionManager = webSocketConnectionManager;
}
public virtual void OnConnected(WebSocket socket)
{
WebSocketConnectionManager.AddSocket(socket);
}
public virtual async Task OnDisconnected(WebSocket socket)
{
await WebSocketConnectionManager.RemoveSocket(WebSocketConnectionManager.GetId(socket));
}
//TODO - decide if exposing the message string is better than exposing the result and buffer
public abstract Task ReceiveAsync(WebSocket socket, WebSocketReceiveResult result, byte[] buffer);
}
}
using Arim.Utils.Network;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace HSJM.CameraServer
{
public class CameraWebSocketHandler : WebSocketHandler
{
ConcurrentDictionary<string, WSRtspContext> ws_rtsps;
public CameraWebSocketHandler(WebSocketConnectionManager webSocketConnectionManager)
: base(webSocketConnectionManager)
{
ws_rtsps = new ConcurrentDictionary<string, WSRtspContext>();
}
public override void OnConnected(WebSocket socket)
{
base.OnConnected(socket);
}
public override async Task OnDisconnected(WebSocket socket)
{
var socketId = WebSocketConnectionManager.GetId(socket);
if (ws_rtsps.ContainsKey(socketId))
{
WSRtspContext wsrtsp;
ws_rtsps.TryRemove(socketId, out wsrtsp);
wsrtsp.StopReceive();
if (wsrtsp.Rtsp != null)
{
wsrtsp.Rtsp.Close();
}
}//关闭rtsp.
await base.OnDisconnected(socket);
}
public override async Task ReceiveAsync(WebSocket socket, WebSocketReceiveResult result, byte[] buffer)
{
if (buffer == null || buffer.Length == 0)
return;
var socketId = WebSocketConnectionManager.GetId(socket);
WSRtspContext wsrtsp = null ;
if (ws_rtsps.ContainsKey(socketId))
{
wsrtsp = ws_rtsps[socketId];
}
if (result.MessageType == WebSocketMessageType.Text)
{
string package = Encoding.UTF8.GetString(buffer);
string command = getWSPCommand(package);
string seq = getByKey(package, "seq");
if(command == "INIT")//建立新链接.
{
string host = getByKey(package, "host");
string port = getByKey(package, "port");
if (port == null)
port = "554";
if (host != null)
{
try
{
RtspProxy rtsp = new RtspProxy();
bool connected = rtsp.Connect(host, Convert.ToInt32(port));
if (connected)
{
rtsp.Start();
wsrtsp = new WSRtspContext(socket, rtsp, false);
wsrtsp.ControlWebSocketId = socketId;
ws_rtsps.TryAdd(socketId, wsrtsp);
wsrtsp.Seq = seq;
//返回握手.
WSRtspResponse response = new WSRtspResponse();
response.Seq = seq;
response.Shakehand = true;
response.Channel = socketId;
await socket.SendAsync(response.ToArray(), WebSocketMessageType.Text, true, CancellationToken.None);
//启动接受rtsp控制报文,发送给ws.
await wsrtsp.StartReceive();
}
else
{
await removeSocket(wsrtsp);
return;
}
}
catch(Exception ex)
{
//Logger.Error(String.Format("connect to rtsp {0} Error:{1}", package, ex.Message));
return;
}
}
}
else if (command == "JOIN")//建立数据通道.
{
string channel = getByKey(package, "channel");
if (channel != null && ws_rtsps.ContainsKey(channel))
{
WSRtspContext controlwsrtsp = ws_rtsps[channel];
controlwsrtsp.DataWebSocketId = socketId;
WSRtspContext datawsrtsp = new WSRtspContext(socket, controlwsrtsp.Rtsp, true);
datawsrtsp.ControlWebSocketId = controlwsrtsp.ControlWebSocketId;
datawsrtsp.DataWebSocketId = socketId;
ws_rtsps.TryAdd(socketId, datawsrtsp);
//返回握手.
WSRtspResponse response = new WSRtspResponse();
response.Seq = seq;
await socket.SendAsync(response.ToArray(), WebSocketMessageType.Text, true, CancellationToken.None);
//启动接受rtsp数据报文,发送给ws.
await datawsrtsp.StartReceive();
}
return;
}//WSP/1.1 JOIN channel: 127.0.0.1 - 2 18467 seq: 3
else
{
wsrtsp.Seq = seq;
try
{
await wsrtsp.Send(getRtspBuffer(package));
}
catch
{
await removeSocket(wsrtsp);
}
}
}
else if(wsrtsp != null)
{
try
{
await wsrtsp.Send(buffer);
}
catch
{
await removeSocket(wsrtsp);
}
}//WebSocketMessageType.Data
}
private async Task removeSocket(WSRtspContext wsrtsp)
{
if (wsrtsp != null)
{
if (!String.IsNullOrEmpty(wsrtsp.ControlWebSocketId))
await this.WebSocketConnectionManager.RemoveSocket(wsrtsp.ControlWebSocketId);
if (!String.IsNullOrEmpty(wsrtsp.DataWebSocketId))
await this.WebSocketConnectionManager.RemoveSocket(wsrtsp.DataWebSocketId);
}
}
//获取 WSP/1.1 WRAP 中的 WRAP.
private static string getWSPCommand(string source)
{
string proto = "WSP/1.1";
int protostart = source.IndexOf(proto);
int protoend = source.IndexOf("\r\n", protostart);
return source.Substring(proto.Length, protoend - proto.Length).Trim();
}
private static string getByKey(string source, string key)
{
int keyIndex = source.IndexOf(key);
if (keyIndex > -1)
{
int indexKeyEnd = source.IndexOf("\r\n", keyIndex);
if (indexKeyEnd > keyIndex)
{
return source.Substring(keyIndex + key.Length + 1, indexKeyEnd - keyIndex - key.Length - 1).Trim();
}
}
return null;
}
private static byte[] getRtspBuffer(string source)
{
if (source == null)
return null;
int wsmsgend = source.IndexOf("\r\n\r\n");
if (wsmsgend > -1)
{
int rtsplen = source.Length - wsmsgend - 4;
if(rtsplen>0)
{
string rtspmsg = source.Substring(wsmsgend + 4, rtsplen);
return ASCIIEncoding.UTF8.GetBytes(rtspmsg);
}
}
return null;
}
}
public class WSRtspResponse
{
public WSRtspResponse()
{
Shakehand = false;
}
const string Proto = "WSP/1.1 200 OK";
public string Channel { get; set; }
public string Seq { get; set; }
public bool Shakehand { get; set; }
public byte[] RtspBuffer { get; set; }
public byte[] ToArray()
{
StringBuilder sb = new StringBuilder();
sb.Append(Proto).Append("\r\n");
sb.Append("seq: ").Append(Seq).Append("\r\n");
if (Shakehand)
sb.Append("channel: ").Append(Channel).Append("\r\n");
sb.Append("\r\n");
byte[] wsheader = ASCIIEncoding.UTF8.GetBytes(sb.ToString());
if (!Shakehand && RtspBuffer != null)
{
int wsheaderlength = wsheader.Length;
if (RtspBuffer != null && RtspBuffer.Length > 0)
{
int rtsplength = RtspBuffer.Length;
byte[] result = new byte[wsheaderlength + rtsplength];
Array.Copy(wsheader, result, wsheaderlength);
Array.Copy(RtspBuffer, 0, result, wsheaderlength, rtsplength);
return result;
}
else
return wsheader;
}
else
return wsheader;
}
}
public class WSRtspContext
{
WebSocket _ws;
RtspProxy _rtsp;
bool _dataChannel;
bool quitFlag = false;
public WSRtspContext(WebSocket ws, RtspProxy rtsp, bool dataChannel)
{
_ws = ws;
_rtsp = rtsp;
_dataChannel = dataChannel;
}
public string ControlWebSocketId { get; set; }
public string DataWebSocketId { get; set; }
public RtspProxy Rtsp { get { return _rtsp; } }
public string Seq
{
get;set;
}
public async Task Send(byte[] buffer)
{
await _rtsp.Send(buffer);
}
public async Task StartReceive()
{
quitFlag = false;
while (!quitFlag)
{
List<byte[]> datas;
bool succeed;
if (_dataChannel)
succeed = _rtsp.TryDequeData(out datas, 10);
else
succeed = _rtsp.TryDequeControl(out datas, 1);
if (succeed)
{
foreach(byte[] data in datas)
{
if (!_dataChannel)
{
WSRtspResponse response = new WSRtspResponse();
response.Seq = Seq;
response.RtspBuffer = data;
await _ws.SendAsync(response.ToArray(), _dataChannel ? WebSocketMessageType.Binary : WebSocketMessageType.Text, true, CancellationToken.None);
}
else
{
await _ws.SendAsync(data, _dataChannel ? WebSocketMessageType.Binary : WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
else
{
if (_dataChannel)
await Task.Delay(1);
else
await Task.Delay(10);
}
}
}
public void StopReceive()
{
quitFlag = true;
}
}
}
前端使用free.player.1.8进行流解析输出
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Streamedian HTML5 RTSP player</title>
<style>
body {
max-width: 720px;
margin: 50px auto;
}
#test_video {
width: 720px;
}
.controls {
display: flex;
justify-content: space-around;
align-items: center;
}
input.input, .form-inline .input-group>.form-control {
width: 300px;
}
#logs {
overflow: auto;
width: 720px;
height: 150px;
padding: 5px;
border-top: solid 1px gray;
border-bottom: solid 1px gray;
}
button {
margin: 5px
}
</style>
</head>
<body>
<div>
<input id="stream_url" size="36">
<button id="set_new_url" onclick = "set_url(new_url)">Set</button>
</div>
<div>
<p style="color:#808080">Enter your rtsp link to the stream, for example: "rtsp://admin:jsj58568916@10.43.8.51:554/MPEG-4/ch1/main/av_stream"</p>
</div>
<video id="test_video" controls autoplay>
</video>
<script src="free.player.1.8.js"></script> <!-- Path to pleer js-->
<script>
var new_url = " ";
var player;
async function set_url(url)
{
if (player) {
try {
player.stop();
await player.destroy();
} catch (e) {
}
}
var text = document.getElementById('stream_url').value;
url = text;
test_video.src = url;
player = Streamedian.player('test_video', { socket: "ws://localhost:20072/ws/" });
//var player = document.getElementById('test_video');
}
</script>
<script>
// define a new console
var console=(function(oldConsole){
return {
log: function(){
let text = '';
let node = document.createElement("div");
for (let arg in arguments){
text +=' ' + arguments[arg];
}
oldConsole.log(text);
node.appendChild(document.createTextNode(text));
document.getElementById("logs").appendChild(node);
},
info: function () {
let text = '';
for (let arg in arguments){
text +='' + arguments[arg];
}
oldConsole.info(text);
},
warn: function () {
let text = '';
for (let arg in arguments){
text +=' ' + arguments[arg];
}
oldConsole.warn(text);
},
error: function () {
let text = '';
for (let arg in arguments){
text +=' ' + arguments[arg];
}
oldConsole.error(text);
}
};
}(window.console));
//Then redefine the old console
window.console = console;
function cleanLog(){
let node = document.getElementById("logs");
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}
function scrollLog(){
console.warn("scroll");
let node = document.getElementById("logs");
node.scrollTop = node.scrollHeight;
}
</script>
<p><br>Have any suggestions to improve our player? <br>Feel free to leave comments or ideas email: streamedian.player@gmail.com</p>
<p>View player log</p>
<div id="logs"></div>
<button class="btn btn-success" οnclick="cleanLog()">clear</button><button class="btn btn-success" οnclick="scrollLog()">scroll to end</button>
</body>
</html>