C# websocket流读取摄像头,实现web页面实时监控

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>

前端流接受完整栗子

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

11eleven

你的鼓励是我创作的动力 !

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值