• WebRTC学习笔记一 简单示例


    一、捕获本地媒体流getUserMedia

    1.index.html

    1. "en">
    2.    "UTF-8">
    3.    "viewport" content="width=device-width, initial-scale=1.0">
    4.    "X-UA-Compatible" content="ie=edge">
    5.    Document
    6.    
    7.        
    8.        
    9.    
  •    
  •        
  •    
  •    
  • 部署本地服务器后,chrome中访问192.168.11.129:9050会报错:navigator.getUserMedia is not a function。原因是,Chrome 47以后,getUserMedia API只能允许来自“安全可信”的客户端的视频音频请求,如HTTPS和本地的Localhost。所以将访问地址改成localhost:9050即可。

    二、同网页示例

    例子来源:https://codelabs.developers.google.com/codelabs/webrtc-web/#4

    1.index.html

    1.    
    2.         Realtime communication with WebRTC
    3.        
    4.    
    5.    
    6.        

      Realtime communication with WebRTC

    7.    
    8.        
    9.        
    10.    
    11.        
    12.            
    13.            
    14.            
    15.        
  •        
  •        
  •        
  • 2.main.js

    1. 'use strict';
    2. //log
    3. function trace(text) {
    4.  text = text.trim();
    5.  const now = (window.performance.now() / 1000).toFixed(3);
    6.  console.log(now, text);
    7. }
    8. // 设置两个video,分别显示本地视频流和远端视频流
    9. const localVideo = document.getElementById('localVideo');
    10. const remoteVideo = document.getElementById('remoteVideo');
    11. localVideo.addEventListener('loadedmetadata', logVideoLoaded);
    12. remoteVideo.addEventListener('loadedmetadata', logVideoLoaded);
    13. remoteVideo.addEventListener('onresize', logResizedVideo);
    14. function logVideoLoaded(event) {
    15.  const video = event.target;
    16.  trace(`${video.id} videoWidth: ${video.videoWidth}px, ` +
    17.        `videoHeight: ${video.videoHeight}px.`);
    18. }
    19. function logResizedVideo(event) {
    20.  logVideoLoaded(event);
    21.  if (startTime) {
    22.    const elapsedTime = window.performance.now() - startTime;
    23.    startTime = null;
    24.    trace(`Setup time: ${elapsedTime.toFixed(3)}ms.`);
    25. }
    26. }
    27. let startTime = null;
    28. let localStream;
    29. let remoteStream;
    30. // 建立两个对等连接对象,分表代表本地和远端
    31. let localPeerConnection;
    32. let remotePeerConnection;
    33. const startButton = document.getElementById('startButton');
    34. const callButton = document.getElementById('callButton');
    35. const hangupButton = document.getElementById('hangupButton');
    36. callButton.disabled = true;
    37. hangupButton.disabled = true;
    38. startButton.addEventListener('click', startAction);
    39. callButton.addEventListener('click', callAction);
    40. hangupButton.addEventListener('click', hangupAction);
    41. // 传输视频,不传输音频
    42. const mediaStreamConstraints = {
    43.  video: true,
    44.  audio: false
    45. };
    46. //开始事件,采集摄像头到本地
    47. function startAction() {
    48.  startButton.disabled = true;
    49.  navigator.getUserMedia(mediaStreamConstraints, gotLocalMediaStream, handleLocalMediaStreamError)
    50.  trace('Requesting local stream.');
    51. }
    52. function gotLocalMediaStream(mediaStream) {
    53.  localVideo.srcObject = mediaStream;
    54.  localStream = mediaStream;
    55.  trace('Received local stream.');
    56.  callButton.disabled = false;
    57. }
    58. function handleLocalMediaStreamError(error) {
    59.  trace(`navigator.getUserMedia error: ${error.toString()}.`);
    60. }
    61. // 设置只交换视频
    62. const offerOptions = {
    63.  offerToReceiveVideo: 1,
    64. };
    65. // 创建对等连接
    66. function callAction() {
    67.  callButton.disabled = true;
    68.  hangupButton.disabled = false;
    69.  trace('Starting call.');
    70.  startTime = window.performance.now();
    71.  const videoTracks = localStream.getVideoTracks();
    72.  const audioTracks = localStream.getAudioTracks();
    73.  if (videoTracks.length > 0) {
    74.    trace(`Using video device: ${videoTracks[0].label}.`);
    75. }
    76.  if (audioTracks.length > 0) {
    77.    trace(`Using audio device: ${audioTracks[0].label}.`);
    78. }
    79.  // 服务器配置
    80.  const servers = null;
    81.  localPeerConnection = new RTCPeerConnection(servers);
    82.  trace('Created local peer connection object localPeerConnection.');
    83.  localPeerConnection.addEventListener('icecandidate', handleConnection);
    84.  localPeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);
    85.  remotePeerConnection = new RTCPeerConnection(servers);
    86.  trace('Created remote peer connection object remotePeerConnection.');
    87.  remotePeerConnection.addEventListener('icecandidate', handleConnection);
    88.  remotePeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);
    89.  remotePeerConnection.addEventListener('addstream', gotRemoteMediaStream);
    90.  localPeerConnection.addStream(localStream);
    91.  trace('Added local stream to localPeerConnection.');
    92.  trace('localPeerConnection createOffer start.');
    93.  localPeerConnection.createOffer(offerOptions)
    94.   .then(createdOffer).catch(setSessionDescriptionError);
    95. }
    96. function getOtherPeer(peerConnection) {
    97.  return (peerConnection === localPeerConnection) ?
    98.      remotePeerConnection : localPeerConnection;
    99. }
    100. function getPeerName(peerConnection) {
    101.  return (peerConnection === localPeerConnection) ?
    102.      'localPeerConnection' : 'remotePeerConnection';
    103. }
    104. function handleConnection(event) {
    105.  const peerConnection = event.target;
    106.  const iceCandidate = event.candidate;
    107.  if (iceCandidate) {
    108.    const newIceCandidate = new RTCIceCandidate(iceCandidate);
    109.    const otherPeer = getOtherPeer(peerConnection);
    110.    otherPeer.addIceCandidate(newIceCandidate)
    111.     .then(() => {
    112.        handleConnectionSuccess(peerConnection);
    113.     }).catch((error) => {
    114.        handleConnectionFailure(peerConnection, error);
    115.     });
    116.    trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
    117.          `${event.candidate.candidate}.`);
    118. }
    119. }
    120. function handleConnectionSuccess(peerConnection) {
    121.  trace(`${getPeerName(peerConnection)} addIceCandidate success.`);
    122. };
    123. function handleConnectionFailure(peerConnection, error) {
    124.  trace(`${getPeerName(peerConnection)} failed to add ICE Candidate:\n`+
    125.        `${error.toString()}.`);
    126. }
    127. function handleConnectionChange(event) {
    128.  const peerConnection = event.target;
    129.  console.log('ICE state change event: ', event);
    130.  trace(`${getPeerName(peerConnection)} ICE state: ` +
    131.        `${peerConnection.iceConnectionState}.`);
    132. }
    133. function gotRemoteMediaStream(event) {
    134.  const mediaStream = event.stream;
    135.  remoteVideo.srcObject = mediaStream;
    136.  remoteStream = mediaStream;
    137.  trace('Remote peer connection received remote stream.');
    138. }
    139. function createdOffer(description) {
    140.  trace(`Offer from localPeerConnection:\n${description.sdp}`);
    141.  trace('localPeerConnection setLocalDescription start.');
    142.  localPeerConnection.setLocalDescription(description)
    143.   .then(() => {
    144.      setLocalDescriptionSuccess(localPeerConnection);
    145.   }).catch(setSessionDescriptionError);
    146.  trace('remotePeerConnection setRemoteDescription start.');
    147.  remotePeerConnection.setRemoteDescription(description)
    148.   .then(() => {
    149.      setRemoteDescriptionSuccess(remotePeerConnection);
    150.   }).catch(setSessionDescriptionError);
    151.  trace('remotePeerConnection createAnswer start.');
    152.  remotePeerConnection.createAnswer()
    153.   .then(createdAnswer)
    154.   .catch(setSessionDescriptionError);
    155. }
    156. function createdAnswer(description) {
    157.  trace(`Answer from remotePeerConnection:\n${description.sdp}.`);
    158.  trace('remotePeerConnection setLocalDescription start.');
    159.  remotePeerConnection.setLocalDescription(description)
    160.   .then(() => {
    161.      setLocalDescriptionSuccess(remotePeerConnection);
    162.   }).catch(setSessionDescriptionError);
    163.  trace('localPeerConnection setRemoteDescription start.');
    164.  localPeerConnection.setRemoteDescription(description)
    165.   .then(() => {
    166.      setRemoteDescriptionSuccess(localPeerConnection);
    167.   }).catch(setSessionDescriptionError);
    168. }
    169. function setSessionDescriptionError(error) {
    170.  trace(`Failed to create session description: ${error.toString()}.`);
    171. }
    172. function setLocalDescriptionSuccess(peerConnection) {
    173.  setDescriptionSuccess(peerConnection, 'setLocalDescription');
    174. }
    175. function setRemoteDescriptionSuccess(peerConnection) {
    176.  setDescriptionSuccess(peerConnection, 'setRemoteDescription');
    177. }
    178. function setDescriptionSuccess(peerConnection, functionName) {
    179.  const peerName = getPeerName(peerConnection);
    180.  trace(`${peerName} ${functionName} complete.`);
    181. }
    182. //断掉
    183. function hangupAction() {
    184.  localPeerConnection.close();
    185.  remotePeerConnection.close();
    186.  localPeerConnection = null;
    187.  remotePeerConnection = null;
    188.  hangupButton.disabled = true;
    189.  callButton.disabled = false;
    190.  trace('Ending call.');
    191. }

    【学习地址】:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发
    【文章福利】:

    免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击1079654574加群领取哦~

      

    3.源码分析

    点击开始,触发startAction没什么好说的。点击调用,直接看callAction: (1)首先使用new RTCPeerConnection创建了两个connection

    1.  const servers = null;
    2.  localPeerConnection = new RTCPeerConnection(servers);

    servers在这个例子中并没有用,是用来配置STUN and TURN s服务器的,先忽略。

    (2)添加事件侦听,先忽略

    1. //也可以使用onicecandidate这种写法
    2. addEventListener('icecandidate', handleConnection);
    3. addEventListener('iceconnectionstatechange', handleConnectionChange);

    (3)然后就是addStream和createOffer

    1.  localPeerConnection.addStream(localStream);
    2.  trace('Added local stream to localPeerConnection.');
    3.  trace('localPeerConnection createOffer start.');
    4.  localPeerConnection.createOffer(offerOptions)
    5.   .then(createdOffer).catch(setSessionDescriptionError);

    其中createOffer需要一个Options

    1. // 设置只交换视频
    2. const offerOptions = {
    3.  offerToReceiveVideo: 1,
    4. };

    这里我的理解是,createOffer为了产生SDP描述,要先使用addStream把视频流加载进去才能解析。

    上述过程可以在源码createOffer和createAnser中看到。

    (4)icecandidate事件 参考

    https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/icecandidate_event

    当 RTCPeerConnection通过RTCPeerConnection.setLocalDescription() (en-US)方法更改本地描述之后,该RTCPeerConnection会抛出icecandidate事件。该事件的监听器需要将更改后的描述信息传送给远端RTCPeerConnection,以更新远端的备选源。

    意思就是setLocalDescription被调用后,触发icecandidate事件,这一点可以在示例的console中得到验证。

    4.来张流程图,转自https://segmentfault.com/a/1190000037513346

    (5)addTrack,addTransceiver addStream() 已过时,官方不推荐使用.将一个MediaStream音频或视频的本地源,添加到WebRTC对等连接流对象中。官方推荐我们使用另外一个方法addTrack

    1.  remotePeerConnection.ontrack = function(evt) {
    2.      const mediaStream = evt.streams[0];
    3.    remoteVideo.srcObject = mediaStream;
    4.    remoteStream = mediaStream;
    5.    trace('Remote peer connection received remote stream.');
    6. }
    7. localStream.getTracks().forEach(track => {
    8.    localPeerConnection.addTrack(track, localStream);
    9.    // localPeerConnection.addTransceiver(track, {streams: [localStream]}); // 这个也可以
    10. });

    如果你是做音视频聊天相关的产品,那么addTrack 刚好能满足你的需求,毕竟需要使用到用户的摄像头、麦克风(浏览器会询问用户是否授权)。但是你只想建立音视频轨道,并不需要使用摄像头、麦克风,那我们应该怎么去做呢?

    addTransceiver创建一个新的RTCRtpTransceiver并将其添加到与关联的收发器集中RTCPeerConnection。每个收发器都代表一个双向流,并带有RTCRtpSender和RTCRtpReceiver。

    let rtcTransceiver = RTCPeerConnection .addTransceiver(trackOrKind,init);

    (a)trackOrKind: MediaStreamTrack以与所述收发器相关联,或者一个DOMString被用作kind接收器的的track。这里视频轨道就传"video",音频轨道就传"audio" (b)init: 可选参数。如下:

    举个例子: 添加一个单向的音视频流收发器

    1. this.rtcPeerConnection.addTransceiver("video", {
    2. direction: "recvonly"
    3. });
    4. this.rtcPeerConnection.addTransceiver("audio", {
    5. direction: "recvonly"
    6. });

    上述代码只会接收对端发过来的音视频流,不会将自己的音视频流传输给对端。direction:

    三、网络1V1示例

    源码参见https://github.com/wuyawei/webrtc-stream

    这个例子不再是同一个网页,所以需要借助socket.io通讯。 房间相关逻辑暂时忽略,看一下创建offer部分:

    1. socket.on('apply', data => { // 你点同意的地方
    2. ...
    3. this.$confirm(data.self + ' 向你请求视频通话, 是否同意?', '提示', {
    4. confirmButtonText: '同意',
    5. cancelButtonText: '拒绝',
    6. type: 'warning'
    7. }).then(async () => {
    8. await this.createP2P(data); // 同意之后创建自己的 peer 等待对方的 offer
    9. ... // 这里不发 offer
    10. })
    11. ...
    12. });
    13. socket.on('reply', async data =>{ // 对方知道你点了同意的地方
    14. switch (data.type) {
    15. case '1': // 只有这里发 offer
    16. await this.createP2P(data); // 对方同意之后创建自己的 peer
    17. this.createOffer(data); // 并给对方发送 offer
    18. break;
    19. ...
    20. }
    21. });

    本例采取的是呼叫方发送 Offer,这个地方一定得注意,只要有一方创建 Offer 就可以了,因为一旦连接就是双向的。

    和微信等视频通话一样,双方都需要进行媒体流输出,因为你们都要看见对方。所以这里和之前本地对等连接的区别就是都需要给自己的 RTCPeerConnection 实例添加媒体流,然后连接后各自都能拿到对方的视频流。在 初始化 RTCPeerConnection 时,记得加上 onicecandidate 函数,用以给对方发送 ICE 候选。

    1. async createP2P(data) {
    2. this.loading = true; // loading动画
    3. this.loadingText = '正在建立通话连接';
    4. await this.createMedia(data);
    5. },
    6. async createMedia(data) {
    7. ... // 获取并将本地流赋值给 video 同之前
    8. this.initPeer(data); // 获取到媒体流后,调用函数初始化 RTCPeerConnection
    9. },
    10. initPeer(data) {
    11. // 创建输出端 PeerConnection
    12. ...
    13. this.peer.addStream(this.localstream); // 都需要添加本地流
    14. this.peer.onicecandidate = (event) => {
    15. // 监听ICE候选信息 如果收集到,就发送给对方
    16. if (event.candidate) { // 发送 ICE 候选
    17. socket.emit('1v1ICE',
    18. {account: data.self, self: this.account, sdp: event.candidate});
    19. }
    20. };
    21. this.peer.onaddstream = (event) => {
    22. // 监听是否有媒体流接入,如果有就赋值给 rtcB 的 src,改变相应loading状态,赋值省略
    23. this.isToPeer = true;
    24. this.loading = false;
    25. ...
    26. };
    27. }

    createOffer 等信息交换和之前一样,只是需要通过 Socket 转发给对应的客户端。然后各自接收到消息后分别采取对应的措施。

    1. socket.on('1v1answer', (data) =>{ // 接收到 answer
    2.    this.onAnswer(data);
    3. });
    4. socket.on('1v1ICE', (data) =>{ // 接收到 ICE
    5.    this.onIce(data);
    6. });
    7. socket.on('1v1offer', (data) =>{ // 接收到 offer
    8.    this.onOffer(data);
    9. });
    10. async createOffer(data) { // 创建并发送 offer
    11.    try {
    12.        // 创建offer
    13.        let offer = await this.peer.createOffer(this.offerOption);
    14.        // 呼叫端设置本地 offer 描述
    15.        await this.peer.setLocalDescription(offer);
    16.        // 给对方发送 offer
    17.        socket.emit('1v1offer', {account: data.self, self: this.account, sdp: offer});
    18.   } catch (e) {
    19.        console.log('createOffer: ', e);
    20.   }
    21. },
    22. async onOffer(data) { // 接收offer并发送 answer
    23.    try {
    24.        // 接收端设置远程 offer 描述
    25.        await this.peer.setRemoteDescription(data.sdp);
    26.        // 接收端创建 answer
    27.        let answer = await this.peer.createAnswer();
    28.        // 接收端设置本地 answer 描述
    29.        await this.peer.setLocalDescription(answer);
    30.        // 给对方发送 answer
    31.        socket.emit('1v1answer', {account: data.self, self: this.account, sdp: answer});
    32.   } catch (e) {
    33.        console.log('onOffer: ', e);
    34.   }
    35. },
    36. async onAnswer(data) { // 接收answer
    37.    try {
    38.        await this.peer.setRemoteDescription(data.sdp); // 呼叫端设置远程 answer 描述
    39.   } catch (e) {
    40.        console.log('onAnswer: ', e);
    41.   }
    42. },
    43. async onIce(data) { // 接收 ICE 候选
    44.    try {
    45.        await this.peer.addIceCandidate(data.sdp); // 设置远程 ICE
    46.   } catch (e) {
    47.        console.log('onAnswer: ', e);
    48.   }
    49. }

    挂断的思路依然是将各自的 peer 关闭,但是这里挂断方还需要借助 Socket 告诉对方,你已经挂电话了,不然对方还在痴痴地等。

    1. hangup() { // 挂断通话 并做相应处理 对方收到消息后一样需要关闭连接
    2.    socket.emit('1v1hangup', {account: this.isCall, self: this.account});
    3.    this.peer.close();
    4.    this.peer = null;
    5.    this.isToPeer = false;
    6.    this.isCall = false;
    7. }
  • 相关阅读:
    will be initialized after [-Werror=reorder]
    【Python大数据笔记_day05_Hive基础操作】
    【Leetcode】单链表oj(下),难度提升,快来做做.
    软件测试:功能测试常用的测试用例大全
    零基础学前端(七)将项目发布成网站
    电脑msvcp100.dll丢失了怎么办?详细的5个修复方法
    【Linux】Linux终端执行docker内部shell脚本
    DVWA -xss
    微信小程序跳转到其他小程序
    RabbitMQ初步到精通系列目录
  • 原文地址:https://blog.csdn.net/irainsa/article/details/128102493