簡單快速上手的 webRTC,完整前端和後端, 服務器是node,服務器邏輯簡單

我也是第一次寫webRTC,之前一直沒有寫過,也是一邊百度一邊寫 最終還是寫出來了,我來簡單說一下了,webRTC其實人家已經寫的很完整的,我們要做的僅僅就是 將兩個機器的  信令交互一下 

先看一下效果圖吧,因爲是在本地 所以兩個瀏覽器顯示的都是 一個攝像頭內容

大家可以將 文件放到兩臺電腦上  在同一個局域網裏面 啓動node就可以使用了

 

完整的代碼 可以到github上下載https://github.com/wenccro/webRTC

webRTC API不懂可以查詢https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Signaling_and_video_calling#Starting_a_call

     首先,說一下我這個demo的協議規則

我到規定是,主動呼叫 需要發送 offer, 接受端 在收到offer 時 需要回復一個answer,定義的這樣個規則也符合人之常情,當別叫你名字的時候 你不得 答應人家一下

let obj = {
      "type": "", // 有 offer,answer
      "sdp": 
    }

 

     然後 說一下 信令服務器

我這裏使用的是 HttpRequest,然後定時器循環去請求。最好還是用webSocket ,因爲你可以直接在呼叫後 直接發送給被呼叫着

而不是 使用輪詢的方式去查詢

下面這個就是我的node服務器跑的流程圖,

全局聲明get和post地址

  // 請更換成你node本地跑起來 所在的 ip地址     
 let getUrl = "http://(node服務器地址:3001)/data/local"
 let postUrl = "http://(node服務器地址:3001)/data/remo"

 

好了 我們就開始 來一點一點的  揭開 webRTC的面紗吧

先上 信令服務器上的內容  也就是  node 代碼

var express = require('express')
const bodyParser = require('body-parser')
var app = express()
const Router = require('router')
const router = Router()

router.__dataStore = {}

// 自定義跨域中間件
var allowCors = function (req, res, next) {
  res.header('Access-Control-Allow-Origin', req.headers.origin)
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
  res.header('Access-Control-Allow-Headers', 'Content-Type')
  res.header('Access-Control-Allow-Credentials', 'true')
  next()
}
app.use(allowCors) // 使用跨域中間件

app.get('/data/:id', function (req, res) {
  console.log('get')
  const deviceId = req.params.id
  console.log(deviceId)
  if (!router.__dataStore[deviceId] || router.__dataStore[deviceId].length === 0) {
    console.log('無數據')
    res.statusCode = 200
    res.end('11')
  } else {
    console.log(router.__dataStore)
    const data = router.__dataStore[deviceId].shift()
    console.log('我發給了誰' + deviceId)
    console.log(data)
    res.statusCode = 200
    res.end(JSON.stringify(data))
  }
})
app.use(bodyParser.urlencoded({
  extended: true
}))
app.use(bodyParser.json({ type: 'application/*+json' }))
// var urlencodedParser = bodyParser.urlencoded({ extended: true })
app.post('/data/:id', (req, res) => {
  console.log('post請求成功')
  const deviceId = req.params.id
  console.log(deviceId)
  if (!router.__dataStore[deviceId]) {
    router.__dataStore[deviceId] = []
  }
  console.log('我準備push了')
  console.log(req.body)
  router.__dataStore[deviceId].push(req.body)
  res.statusCode = 200
  res.end('11')
})

app.listen(3001, () => {
  console.log(3001)
})

node 服務器就做了2件事 ,就是一個get請求  查詢 有沒有他的  sdp內容,第二個是post請求,將自己的spd 存入對方的腳下

就是信令服務器代碼了  就兩個 一個get 一個post ,你可以用node以外的任何後端語言來實現 它的邏輯。

但最推薦  得還算實用webSocket 

接下來 我們來看前端 代碼 

我的  前端有兩個頁面 和兩個js  頁面和js都是差不多的

唯一不同的僅僅只是 get和post請求的 url地址不同

這裏 我就以 local.html 和local.js來講述它的原理,完整的代碼我已經上傳帶github上了  在文章開頭就寫了  你可以直接下載

 

下面 我們就在看一下前端代碼

html 

 第一了視頻顯示的位置 和綁定事件

<!DOCTYPE html>
<html>
  <head>
    <title>Realtime communication with WebRTC</title>
    <style>
      body {
        font-family: sans-serif;
      }

      video {
        max-width: 100%;
        width: 320px;
      }
    </style>
  </head>

  <body>
    <h1>Realtime communication with WebRTC</h1>

    <video id="localVideo" autoplay playsinline></video>
    <video id="remoteVideo" autoplay playsinline></video>

    <div>
      <button id="startButton">A端開始</button>
      <button id="callButton">A端調用</button>
      <button id="hangupButton">掛斷A端定時器</button>
    </div>
    <script src="./jquery.js"></script>
    <script src="./local.js"></script>
  </body>
</html>

 

點擊了開始按鈕

我們先來看第一個  按鈕 點擊後做了什麼,就是循環去發送get請求  由於 我node不太熟,所以每次get node服務器它在沒有數據的時候 會給我返回一個11的標誌位 這裏面有一個 關鍵的 方法  就是chackData(),這個方法就是每次拿到數據後 ,做自己的規則檢查  也就是 收到offer回覆 answer

 // 點擊開始按鈕
  $('#startButton').click(function () {
    startQuery()
  })
// 這個方法循環去請求去
  function startQuery () {
    timer = setInterval(function () {
      $.ajax({
        type: "GET",
        url: getUrl,
        success: function (res) {
          if (res === '11' || res === ' ') {

          } else {
            let msg = JSON.parse(res)
            chackData(msg)
          }
        },
        error: function (res) {}
      })
    }, 1000)
  }

現在我們就是 看一下 這個  chackData具體做了什麼,看完代碼 你會發現 原來 它對這個信息做了一個分支.

解釋:msg.MessageType: 1 表示 收到對方發送的 offer,  2 表示收到對方發送的 answer ,3 表示 收到對方的ice 

// 檢測函數
  function chackData (msg) {
    switch (msg.MessageType) {
      case '1':
        handleVideoOfferMsg(msg);
        break;
      case '2':
        handleVideoAnswerMsg(msg);
        break;
      case '3':
        handleNewICECandidateMsg(msg);
        break;
    }
  }

接着 我們來看 第一件事,就是收到 offer後 它到底做了些什麼

  // 收到別的 offer 需要調用post發送 內容
  async function handleVideoOfferMsg (msg) {
    console.log('收到offer')
    if (!localPeerConnection) {
      createPeerConnection();
    }
    let obj = {
      "type": "offer",
      "sdp": msg.Data
    }
    var desc = new RTCSessionDescription(obj);
    localPeerConnection.setRemoteDescription(desc)
    if (!webcamStream) {
      try { // 播放本地視頻
        webcamStream = await navigator.mediaDevices.getUserMedia(mediaStreamConstraints);
        localVideo.srcObject = webcamStream
      } catch (err) {
        handleGetUserMediaError(err);
        return;
      }
      try {
        webcamStream.getTracks().forEach(
          transceiver = track => localPeerConnection.addTransceiver(track, { streams: [webcamStream] })
        );
      } catch (err) {
        handleGetUserMediaError(err);
      }
    }
    await localPeerConnection.setLocalDescription(await localPeerConnection.createAnswer());
    // 調用 post 請求 回覆 
    let objs = sendDataByType('Answer')
    startPost(objs)
  }

收到 offer以後 一 檢查 localPeerConnection(new RTCPeerConnection 對等連接對象有沒有) 

 沒有就執行 createPeerConnection()方法,後面會講。接着往下看

 let obj = { "type": "offer", "sdp": msg.Data }

    var desc = new RTCSessionDescription(obj);

    localPeerConnection.setRemoteDescription(desc)

這個是收到對方的sdp 並將其設置爲 本地遠端繪畫描述。

接着 二 判斷,webcamStream  這個就是  本地視頻流 ,本地視頻流如果沒有打開 就開啓本地視頻流

localPeerConnection.addTransceiver 這個是將 本地流發送給對方的。

三 就是使用post將 自己的sdp發送給對方

 

接下來是補充  收到offer 這個handleVideoOfferMsg方法中引用的其方法

第一個  是createPeerConnection()創建對等連接

創建了對象並寫了 回調事件

async function createPeerConnection () {
    localPeerConnection = new RTCPeerConnection();
    localPeerConnection.onicecandidate = handleICECandidateEvent;
    localPeerConnection.ontrack = handleTrackEvent;
    localPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent;
  }

這3個回調 的方法 我就不不再這裏敘述了 你直接gethub下載下來就可以看到了。回調是什麼 ,做了什麼 也很清楚

收到offer 這個handleVideoOfferMsg方法中引用的其方法

第二個  是 發送post個服務器

let objs = sendDataByType('Answer')

startPost(objs)

// 返回需要發送的數據
  function sendDataByType (type) {
    let obj = {
      Data: localPeerConnection.localDescription.sdp,
      IceDataSeparator: ' '
    }
    switch (type) {
      case "Offer":
        obj.MessageType = '1';
        break;
      case "Answer":
        obj.MessageType = '2';
        break;
    }
    return obj
  }
 // post請求方法
  function startPost (obj) {
    $.ajax({
      type: "post",
      url: postUrl,
      data: obj,
      dataType: 'json',
      success: function (res) {},
      error: function (res) { }
    })
  }

 

第二件事 就是收到answer後 做了些什麼?

收到answer和收到offer都做了(解釋在上面) RTCSessionDescription,和 setRemoteDescription

  // 收到answer
  async function handleVideoAnswerMsg (msg) {
    console.log('收到answer')
    let obj = {
      "type": "answer",
      "sdp": msg.Data
    }
    var desc = new RTCSessionDescription(obj);
    await localPeerConnection.setRemoteDescription(desc).catch();
  }

第三件事 收到 ice,就是收到ICE候選人 做了

 new RTCIceCandidate()和localPeerConnection.addIceCandidate

  async function handleNewICECandidateMsg (msg) {
    let arr = msg.Data.split(msg.IceDataSeparator)
    let obj = {
      "candidate": arr[0],
      "sdpMid": arr[1],
      "sdpMLineIndex": arr[2]
    }
    var candidate = new RTCIceCandidate(obj);
    try {
      await localPeerConnection.addIceCandidate(candidate)
    } catch (err) {}
  }

至此 以上部分就是 點擊 開始後  接收 offer 或者 answer 或者 ice 的所有事件了。

接下來 是 點擊了調用按鈕

點擊開始是 等待被呼叫,點擊調用就是主動去呼叫,

我這裏 做了兩件事 第一件事 就是重新打開get請求 也就是爲了防止 在沒有點擊開始  就點擊 調用了  報錯問題

  $('#callButton').click(function () {
    startQuery()
    startAction()
  })

startQuery()這個方法上面已經說過了  現在 我們來重點看看 startAction()方法

做了兩件事 ,第一件事就是  打開本地視頻流  第二件事就是發佈自己的本地視頻流

ceatePeerConnection() 在上面已經說過了  這裏就不說了

  async function startAction () {
    createPeerConnection()
    try { // 播放本地視頻
      webcamStream = await navigator.mediaDevices.getUserMedia(mediaStreamConstraints);
      localVideo.srcObject = webcamStream
    } catch (err) {
      handleGetUserMediaError(err);
      return;
    }
    try {
      webcamStream.getTracks().forEach(
        transceiver = track => localPeerConnection.addTransceiver(track, { streams: [webcamStream] })
      );
    } catch (err) {
      handleGetUserMediaError(err);
    }
  }

最後 就是點擊關閉按鈕

這裏 沒有寫完  只是簡單的寫了一下  結束定時器 get數據。你還應該寫情況視頻流 關閉攝像頭等事件

 $("#hangupButton").click(function () {
    clearInterval(timer)
  })

 

至此  整個wenRTC就可以實現簡單的 通話了,總結一下  就是 首先 必須有一個 信令服務器  作用就是交換 彼此的SDP,其次就是在 正確的時間做正確的事,也就是 在收到offer 後給人家回覆 answer  在收到別人的 answer後設置成遠端流並,方便對方打開攝像頭。收到ice就及時的交換ice候選人信息。我開頭所發的參考API的那個地址裏面有更加詳細的描述,你也可以參考它的和我的 來寫  

加油小寶貝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章