利用 WebRtcStreamer 实现及时视频流播放

打印 上一主题 下一主题

主题 1034|帖子 1034|积分 3102

WebRtcStreamer 是一个基于 WebRTC 协议的轻量级开源工具,可以在浏览器中直接播放 RTSP 视频流。它利用 WebRTC 的强大功能,提供低耽误的视频流播放体验,非常适合及时监控和其他视频流应用场景。
本文将先容如何在Vue.js项目中利用 WebRtcStreamer 实现及时视频流播放,并分享相干的代码示例。
注意:只支持H264格式
流媒体方式文章
利用 Vue 和 flv.js 实现流媒体视频播放:完整教程
VUE项目中优雅利用EasyPlayer及时播放摄像头多种格式视频利用版本信息为5.xxxx
实现步调



  • 安装和设置 WebRtcStreamer 服务端
    要利用 WebRtcStreamer,需要先在服务器上摆设其服务端。以下是基本的安装步调:
  • 从 WebRtcStreamer 官方仓库 下载代码。

    启动下令

    或者双击exe程序

    服务启动后,默认会监听 8000 端口,访问 http://<server_ip>:8000 可查看状态。

    更改默认端口下令:webrtc-streamer.exe -o -H 0.0.0.0:9527
2.集成到vue中
webRtcStreamer.js 不需要在html文件中引入webRtcStreamer相干代码
  1. /**
  2. * @constructor
  3. * @param {string} videoElement -  dom ID
  4. * @param {string} srvurl -  WebRTC 流媒体服务器的 URL(默认为当前页面地址)
  5. */
  6. class WebRtcStreamer {
  7.   constructor(videoElement, srvurl) {
  8.     if (typeof videoElement === 'string') {
  9.       this.videoElement = document.getElementById(videoElement);
  10.     } else {
  11.       this.videoElement = videoElement;
  12.     }
  13.     this.srvurl =
  14.       srvurl || `${location.protocol}//${window.location.hostname}:${window.location.port}`;
  15.     this.pc = null; // PeerConnection 实例
  16.     // 媒体约束条件
  17.     this.mediaConstraints = {
  18.       offerToReceiveAudio: true,
  19.       offerToReceiveVideo: true,
  20.     };
  21.     this.iceServers = null; // ICE 服务器配置
  22.     this.earlyCandidates = []; // 提前收集的候选者
  23.   }
  24.   /**
  25.    * HTTP 错误处理器
  26.    * @param {Response} response - HTTP 响应
  27.    * @throws {Error} 当响应不成功时抛出错误
  28.    */
  29.   _handleHttpErrors(response) {
  30.     if (!response.ok) {
  31.       throw Error(response.statusText);
  32.     }
  33.     return response;
  34.   }
  35.   /**
  36.    * 连接 WebRTC 视频流到指定的 videoElement
  37.    * @param {string} videourl - 视频流 URL
  38.    * @param {string} audiourl - 音频流 URL
  39.    * @param {string} options - WebRTC 通话的选项
  40.    * @param {MediaStream} localstream - 本地流
  41.    * @param {string} prefmime - 优先的 MIME 类型
  42.    */
  43.   connect(videourl, audiourl, options, localstream, prefmime) {
  44.     this.disconnect();
  45.     if (!this.iceServers) {
  46.       console.log('获取 ICE 服务器配置...');
  47.       fetch(`${this.srvurl}/api/getIceServers`)
  48.         .then(this._handleHttpErrors)
  49.         .then((response) => response.json())
  50.         .then((response) =>
  51.           this.onReceiveGetIceServers(response, videourl, audiourl, options, localstream, prefmime),
  52.         )
  53.         .catch((error) => this.onError(`获取 ICE 服务器错误: ${error}`));
  54.     } else {
  55.       this.onReceiveGetIceServers(
  56.         this.iceServers,
  57.         videourl,
  58.         audiourl,
  59.         options,
  60.         localstream,
  61.         prefmime,
  62.       );
  63.     }
  64.   }
  65.   /**
  66.    * 断开 WebRTC 视频流,并清空 videoElement 的视频源
  67.    */
  68.   disconnect() {
  69.     if (this.videoElement?.srcObject) {
  70.       this.videoElement.srcObject.getTracks().forEach((track) => {
  71.         track.stop();
  72.         this.videoElement.srcObject.removeTrack(track);
  73.       });
  74.     }
  75.     if (this.pc) {
  76.       fetch(`${this.srvurl}/api/hangup?peerid=${this.pc.peerid}`)
  77.         .then(this._handleHttpErrors)
  78.         .catch((error) => this.onError(`hangup ${error}`));
  79.       try {
  80.         this.pc.close();
  81.       } catch (e) {
  82.         console.log(`Failure close peer connection: ${e}`);
  83.       }
  84.       this.pc = null;
  85.     }
  86.   }
  87.   /**
  88.    * 获取 ICE 服务器配置的回调
  89.    * @param {Object} iceServers - ICE 服务器配置
  90.    * @param {string} videourl - 视频流 URL
  91.    * @param {string} audiourl - 音频流 URL
  92.    * @param {string} options - WebRTC 通话的选项
  93.    * @param {MediaStream} stream - 本地流
  94.    * @param {string} prefmime - 优先的 MIME 类型
  95.    */
  96.   onReceiveGetIceServers(iceServers, videourl, audiourl, options, stream, prefmime) {
  97.     this.iceServers = iceServers;
  98.     this.pcConfig = iceServers || { iceServers: [] };
  99.     try {
  100.       this.createPeerConnection();
  101.       let callurl = `${this.srvurl}/api/call?peerid=${this.pc.peerid}&url=${encodeURIComponent(
  102.         videourl,
  103.       )}`;
  104.       if (audiourl) {
  105.         callurl += `&audiourl=${encodeURIComponent(audiourl)}`;
  106.       }
  107.       if (options) {
  108.         callurl += `&options=${encodeURIComponent(options)}`;
  109.       }
  110.       if (stream) {
  111.         this.pc.addStream(stream);
  112.       }
  113.       this.earlyCandidates.length = 0;
  114.       this.pc
  115.         .createOffer(this.mediaConstraints)
  116.         .then((sessionDescription) => {
  117.           // console.log(`创建 Offer: ${JSON.stringify(sessionDescription)}`);
  118.           if (prefmime !== undefined) {
  119.             const [prefkind] = prefmime.split('/');
  120.             const codecs = RTCRtpReceiver.getCapabilities(prefkind).codecs;
  121.             const preferredCodecs = codecs.filter((codec) => codec.mimeType === prefmime);
  122.             this.pc
  123.               .getTransceivers()
  124.               .filter((transceiver) => transceiver.receiver.track.kind === prefkind)
  125.               .forEach((tcvr) => {
  126.                 if (tcvr.setCodecPreferences) {
  127.                   tcvr.setCodecPreferences(preferredCodecs);
  128.                 }
  129.               });
  130.           }
  131.           this.pc
  132.             .setLocalDescription(sessionDescription)
  133.             .then(() => {
  134.               fetch(callurl, {
  135.                 method: 'POST',
  136.                 body: JSON.stringify(sessionDescription),
  137.               })
  138.                 .then(this._handleHttpErrors)
  139.                 .then((response) => response.json())
  140.                 .then((response) => this.onReceiveCall(response))
  141.                 .catch((error) => this.onError(`调用错误: ${error}`));
  142.             })
  143.             .catch((error) => console.log(`setLocalDescription error: ${JSON.stringify(error)}`));
  144.         })
  145.         .catch((error) => console.log(`创建 Offer 失败: ${JSON.stringify(error)}`));
  146.     } catch (e) {
  147.       this.disconnect();
  148.       alert(`连接错误: ${e}`);
  149.     }
  150.   }
  151.   /**
  152.    * 创建 PeerConnection 实例
  153.    */
  154.   createPeerConnection() {
  155.     console.log('创建 PeerConnection...');
  156.     this.pc = new RTCPeerConnection(this.pcConfig);
  157.     this.pc.peerid = Math.random(); // 生成唯一的 peerid
  158.     // 监听 ICE 候选者事件
  159.     this.pc.onicecandidate = (evt) => this.onIceCandidate(evt);
  160.     this.pc.onaddstream = (evt) => this.onAddStream(evt);
  161.     this.pc.oniceconnectionstatechange = () => {
  162.       if (this.videoElement) {
  163.         if (this.pc.iceConnectionState === 'connected') {
  164.           this.videoElement.style.opacity = '1.0';
  165.         } else if (this.pc.iceConnectionState === 'disconnected') {
  166.           this.videoElement.style.opacity = '0.25';
  167.         } else if (['failed', 'closed'].includes(this.pc.iceConnectionState)) {
  168.           this.videoElement.style.opacity = '0.5';
  169.         } else if (this.pc.iceConnectionState === 'new') {
  170.           this.getIceCandidate();
  171.         }
  172.       }
  173.     };
  174.     return this.pc;
  175.   }
  176.   onAddStream(event) {
  177.     console.log(`Remote track added: ${JSON.stringify(event)}`);
  178.     this.videoElement.srcObject = event.stream;
  179.     const promise = this.videoElement.play();
  180.     if (promise !== undefined) {
  181.       promise.catch((error) => {
  182.         console.warn(`error: ${error}`);
  183.         this.videoElement.setAttribute('controls', true);
  184.       });
  185.     }
  186.   }
  187.   onIceCandidate(event) {
  188.     if (event.candidate) {
  189.       if (this.pc.currentRemoteDescription) {
  190.         this.addIceCandidate(this.pc.peerid, event.candidate);
  191.       } else {
  192.         this.earlyCandidates.push(event.candidate);
  193.       }
  194.     } else {
  195.       console.log('End of candidates.');
  196.     }
  197.   }
  198.   /**
  199.    * 添加 ICE 候选者到 PeerConnection
  200.    * @param {RTCIceCandidate} candidate - ICE 候选者
  201.    */
  202.   addIceCandidate(peerid, candidate) {
  203.     fetch(`${this.srvurl}/api/addIceCandidate?peerid=${peerid}`, {
  204.       method: 'POST',
  205.       body: JSON.stringify(candidate),
  206.     })
  207.       .then(this._handleHttpErrors)
  208.       .catch((error) => this.onError(`addIceCandidate ${error}`));
  209.   }
  210.   /**
  211.    * 处理 WebRTC 通话的响应
  212.    * @param {Object} message - 来自服务器的响应消息
  213.    */
  214.   onReceiveCall(dataJson) {
  215.     const descr = new RTCSessionDescription(dataJson);
  216.     this.pc
  217.       .setRemoteDescription(descr)
  218.       .then(() => {
  219.         while (this.earlyCandidates.length) {
  220.           const candidate = this.earlyCandidates.shift();
  221.           this.addIceCandidate(this.pc.peerid, candidate);
  222.         }
  223.         this.getIceCandidate();
  224.       })
  225.       .catch((error) => console.log(`设置描述文件失败: ${JSON.stringify(error)}`));
  226.   }
  227.   getIceCandidate() {
  228.     fetch(`${this.srvurl}/api/getIceCandidate?peerid=${this.pc.peerid}`)
  229.       .then(this._handleHttpErrors)
  230.       .then((response) => response.json())
  231.       .then((response) => this.onReceiveCandidate(response))
  232.       .catch((error) => this.onError(`getIceCandidate ${error}`));
  233.   }
  234.   onReceiveCandidate(dataJson) {
  235.     if (dataJson) {
  236.       dataJson.forEach((candidateData) => {
  237.         const candidate = new RTCIceCandidate(candidateData);
  238.         this.pc
  239.           .addIceCandidate(candidate)
  240.           .catch((error) => console.log(`addIceCandidate error: ${JSON.stringify(error)}`));
  241.       });
  242.     }
  243.   }
  244.   /**
  245.    * 错误处理器
  246.    * @param {string} message - 错误信息
  247.    */
  248.   onError(status) {
  249.     console.error(`WebRTC 错误: ${status}`);
  250.   }
  251. }
  252. export default WebRtcStreamer;
复制代码
组件中利用
  1. <template>
  2.   <div>
  3.     <video id="video" controls muted autoplay></video>
  4.     <button @click="startStream">开始播放</button>
  5.     <button @click="stopStream">停止播放</button>
  6.   </div>
  7. </template>
  8. <script>
  9. import WebRtcStreamer from "@/utils/webRtcStreamer";
  10. export default {
  11.   name: "VideoStreamer",
  12.   data() {
  13.     return {
  14.       webRtcServer: null,
  15.     };
  16.   },
  17.   methods: {
  18.     startStream() {
  19.     const srvurl = "127.0.0.1:9527"
  20.       this.webRtcServer = new WebRtcStreamer(
  21.         "video",
  22.         `${location.protocol}//${srvurl}`
  23.       );
  24.       const videoPath = "stream_name"; // 替换为你的流地址
  25.       this.webRtcServer.connect(videoPath);
  26.     },
  27.     stopStream() {
  28.       if (this.webRtcServer) {
  29.         this.webRtcServer.disconnect(); // 销毁
  30.       }
  31.     },
  32.   },
  33. };
  34. </script>
  35. <style>
  36. video {
  37.   width: 100%;
  38.   height: 100%;
  39.   object-fit: fill;
  40. }
  41. </style>
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

河曲智叟

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表