服务端使用C++/CLR包装EasyRTSPClient实现RTSP播放。播放不产生画面,而是转换成JPEG图像通过WEBSOCKET发送到浏览器端。
服务端(C++ & C#):
浏览器端(实时性不错!):
内存使用情况:播放一路视频时占用39M内存。空闲时(无客户端)时自动关闭RTSP播放后内存占用为18MB.
关于WebSocket通讯,服务端向浏览器发送二进制帧,每一帧为一张JPEG图像。
可以看出Websocket播放的帧率为12帧。源码流也是12帧(使用海康威视相机H264子码流).每帧71KB.改变JPEG压缩质量可以更小,但更模糊。当前使用的压缩质量为0.6
服务端WebSocket代码(使用WebSocketSharp),C#:
1 public class WebSocketPlayBehavior : WebSocketBehavior, IClient { 2 3 static List<WebSocketPlayBehavior> client = new List<WebSocketPlayBehavior>(); 4 public static int[] InPlayChannel() { 5 return client.Select(x => x.ChannelId) 6 .Distinct() 7 .ToArray(); 8 } 9 10 public int ChannelId { 11 get; 12 private set; 13 } 14 private string clientEP = null; 15 public PlayServiceWork Service = null; 16 public WebSocketPlayBehavior() { 17 this.OriginValidator = (p) => { 18 System.Diagnostics.Debug.Print("OriginValidator:{0}", p); 19 return true; 20 }; 21 } 22 protected override void OnOpen() { 23 base.OnOpen(); 24 #region 获取参数(this.ChannelId) 25 { 26 var PARAM = this.Context.RequestUri.PathAndQuery.Split('?') 27 .LastOrDefault() 28 .Split('&') 29 .Select(x => x.Split('=')) 30 .Where(x => x.Length > 1) 31 .ToDictionary(x => x[0].ToLower(), x => System.Net.WebUtility.UrlDecode(x[1])); //WebSocketSharp.Ext.UrlDecode 32 33 int tempid = 1; 34 if (PARAM.TryGetValue("channel", out var value)) { 35 if (Regex.IsMatch(value, "^\d+$")) 36 tempid = Convert.ToInt32(value); 37 else { 38 var index = RtpConfig.Get().Items.FindIndex(x => string.Equals(x.Name, value)); 39 if (index > -1) tempid = index + 1; 40 } 41 } 42 this.ChannelId = Math.Max(1, tempid); 43 } 44 #endregion 45 Service.PlayOneIfNotExist(this.ChannelId - 1); 46 47 clientEP = this.Context.UserEndPoint.ToString(); 48 LEI.NLog.Logger.i("建立WS连接 播放客户端[{0}] <--> [通道:{1}]", clientEP, ChannelId); 49 client.Add(this); 50 RPlayer.AddClient(this); 51 } 52 protected override void OnClose(CloseEventArgs e) { 53 LEI.NLog.Logger.i("关闭WS连接 播放客户端[{0}] <--> [通道:{1}]", clientEP, ChannelId); 54 base.OnClose(e); 55 RPlayer.DelClient(this); 56 client.Remove(this); 57 } 58 59 60 public int GetChannelId() => ChannelId; 61 private bool insend = false; 62 private object lck = new object(); 63 public void SendImage(byte[] bufferImage) { 64 lock (lck) { 65 if (insend) 66 return; 67 insend = true; 68 } 69 70 try { 71 this.SendAsync(bufferImage, (completed) => { 72 insend = false; 73 }); 74 } catch (Exception e) { 75 insend = false; 76 LEI.NLog.Logger.e("发送图片失败:{0}", e.Message); 77 //if (this.State == WebSocketState.Closed) 78 // this.OnClose(null); 79 } 80 } 81 82 83 }
服务端YUV帧图像转JPEG图像代码(JPEG为转换后的图像),C++:
1 // 抓图函数实现 2 int take_snapshot(int w, int h, uint8_t *buffer, AVPixelFormat Format,PBYTE JPEG) 3 { 4 5 //MessageLog("take_snapshot"); 6 char *fileext = NULL; 7 enum AVCodecID codecid = AV_CODEC_ID_NONE; 8 struct SwsContext *sws_ctx = NULL; 9 AVPixelFormat swsofmt = AV_PIX_FMT_NONE; 10 AVFrame picture = {}; 11 int ret = -1; 12 13 AVFormatContext *fmt_ctxt = NULL; 14 AVOutputFormat *out_fmt = NULL; 15 AVStream *stream = NULL; 16 AVCodecContext *codec_ctxt = NULL; 17 AVCodec *codec = NULL; 18 AVPacket packet = {}; 19 int retry = 8; 20 int got = 0; 21 22 // init ffmpeg 23 av_register_all(); 24 25 //fileext = file + strlen(file) - 3; 26 //if (_stricmp(fileext, "png") == 0) { 27 // codecid = AV_CODEC_ID_APNG; 28 // swsofmt = AV_PIX_FMT_RGB24; 29 //} 30 //else 31 { 32 codecid = AV_CODEC_ID_MJPEG; 33 swsofmt = AV_PIX_FMT_YUVJ420P; 34 } 35 36 AVFrame video; 37 int numBytesIn; 38 numBytesIn = av_image_get_buffer_size(Format, w, h, 1); 39 av_image_fill_arrays(video.data, video.linesize, buffer, Format, w, h, 1); 40 video.width = w; 41 video.height = h; 42 video.format = Format; 43 44 // alloc picture 45 picture.format = swsofmt; 46 picture.width = w > 0 ? w : video.width; 47 picture.height = h > 0 ? h : video.height; 48 49 int numBytes = av_image_get_buffer_size(swsofmt, picture.width, picture.height , 1); 50 51 buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t)); 52 53 av_image_fill_arrays(picture.data, picture.linesize, buffer, swsofmt, picture.width, picture.height, 1); 54 55 // scale picture 56 sws_ctx = sws_getContext(video.width, video.height, (AVPixelFormat)Format/*video->format*/, 57 picture.width, picture.height, swsofmt, SWS_FAST_BILINEAR, NULL, NULL, NULL); 58 if (!sws_ctx) { 59 //MessageLog("could not initialize the conversion context jpg"); 60 ////av_log(NULL, AV_LOG_ERROR, "could not initialize the conversion context jpg "); 61 goto done; 62 } 63 sws_scale(sws_ctx, video.data, video.linesize, 0, video.height, picture.data, picture.linesize); 64 65 // do encoding 66 fmt_ctxt = avformat_alloc_context(); 67 out_fmt = av_guess_format("mjpeg", NULL, NULL); 68 fmt_ctxt->oformat = out_fmt; 69 if (!out_fmt) { 70 goto done; 71 } 72 73 74 if ( 75 avio_open_dyn_buf(&fmt_ctxt->pb) 76 //avio_open(&fmt_ctxt->pb, file, AVIO_FLAG_READ_WRITE) 77 < 0) { 78 goto done;//MessageLog("failed to open output fi"); 79 } 80 81 stream = avformat_new_stream(fmt_ctxt, 0); 82 if (!stream) { 83 goto done; //MessageLog("failed to create a new stream "); 84 } 85 86 codec_ctxt = stream->codec; 87 codec_ctxt->codec_id = out_fmt->video_codec; 88 codec_ctxt->codec_type = AVMEDIA_TYPE_VIDEO; 89 codec_ctxt->pix_fmt = swsofmt; 90 codec_ctxt->width = picture.width; 91 codec_ctxt->height = picture.height; 92 codec_ctxt->time_base.num = 1; 93 codec_ctxt->time_base.den = 25; 94 codec_ctxt->qcompress = CChannelManager::JPEG_LEVEL;//压缩质量 95 codec_ctxt->qmin = 2; 96 codec_ctxt->qmax = 31; 97 codec_ctxt->max_qdiff = 15; 98 99 codec = avcodec_find_encoder(codec_ctxt->codec_id); 100 if (!codec) 101 goto done; //MessageLog("failed to find encoder"); 102 103 if (avcodec_open2(codec_ctxt, codec, NULL) < 0) 104 goto done;//MessageLog("failed to open encoder"); 105 106 107 while (retry-- && !got) { 108 if (avcodec_encode_video2(codec_ctxt, &packet, &picture, &got) < 0) { 109 goto done;//MessageLog("failed to do picture encoding"); 110 } 111 112 if (got) { 113 ret = avformat_write_header(fmt_ctxt, NULL); 114 if (ret < 0) { 115 goto done; //MessageLog("error occurred when opening output file"); 116 } 117 av_write_frame(fmt_ctxt, &packet); 118 av_write_trailer(fmt_ctxt); 119 } 120 } 121 122 // ok 123 ret = 0; 124 125 done: 126 avcodec_close(codec_ctxt); 127 if (fmt_ctxt) { 128 //avio_close(fmt_ctxt->pb); 129 PUCHAR output; 130 ret = avio_close_dyn_buf(fmt_ctxt->pb, &output); 131 if (ret > 0) { 132 memcpy_s(JPEG, ret, output, ret); 133 av_freep(&output); 134 } 135 avformat_free_context(fmt_ctxt); 136 } 137 av_packet_unref(&packet); 138 sws_freeContext(sws_ctx); 139 av_free(buffer); 140 141 return ret; 142 }
EasyRTSPClient的代码就不展示了,网上很多!