WebRTC學習總結(3):從1v1視頻通話的實現說一說RTCPeerConnection的建立

RTCPeerConnection 接口代表一個由本地計算機到遠端的WebRTC連接。該接口提供了創建,保持,監控,關閉連接的方法的實現。

一對一視頻對話的時候,連接過程如下所示:
在這裏插入圖片描述
這個過程是不是看起來還挺清晰的?但是這只是表述了媒體信息交換的過程,別忘了還有網絡信息~

接下來我們就先好好捋一捋PC建立的過程吧!

  • A打開本地視頻流,創建PeerConnection對象,將本地音視頻流封裝成MediaStream添加到PeerConnection中;

  • A通過CreateOffer創建offer信息,調用setLocalDescription儲存本地offer,然後通過信令服務器將offer發送給B;

  • A在調用setLocalDescription的同時,向服務器發送了iceCandidate消息;

  • B打開本地視頻流,創建PeerConnection對象,將本地音視頻流封裝成MediaStream添加到PeerConnection中;

  • B收到offer後,通過setRemoteDescription儲存遠端offer,然後通過CreateAnswer創建answer信息,同樣調用setLocalDescription儲存本地answer描述,再將answer發送給A;

  • B在調用setLocalDescription的同時,向服務器發送了iceCandidate消息;

  • A收到B的answer後,再次調用setRemoteDescription設置遠端的answer描述。

那接下來就讓我們從頭看一看,一對一視頻對話究竟是怎麼實現的吧~ 這裏我會按照事件的發送順序,將客戶端和服務器端的代碼拆分開說,雖然從邏輯上來講是對的,但是代碼會比較亂,只用於理解,不能直接用哦。

當用戶訪問對話頁面時,會向服務器發送’create or join’消息

服務器收到’create or join’,並計算當前房間裏的人數

若 numClients === 0,則向客戶端發送’created’消息;客戶端收到’created’消息,則將當前用戶作爲會話發起人,令 isInitiator = true
若 numClients === 1, 則向會話發起人發送’join’消息,發起人端 isChannelReady = true; 向新用戶發送’joined’消息,新用戶端isChannelReady = true,表示參與對話的兩方都準備好了。

上面這一部分的代碼在服務器的搭建中都講過了。

客戶端進入網頁後會自動打開攝像頭:

navigator.mediaDevices.getUserMedia({
    audio: false,
    video: true
})
    .then(gotLocalMediaStream)
    .catch(function (e) {
        alert('getUserMedia() error: ' + e.name);
    });
function gotLocalMediaStream(mediastream) {
    console.log('Adding local stream.');
    localStream = mediastream;
    localVideo.srcObject = mediastream;
    sendMessage('got user media');
    if (isInitiator) {
        maybeStart();
    }
}

對於會話發起人,在打開攝像頭的時候,會發起maybeStart()方法,進而實例化一個RTCPeerConnection,然後將本地的視頻流加入pc中。

function maybeStart() {
    console.log('>>>>>>> maybeStart() ', isStarted, localStream, isChannelReady);
    if (!isStarted && typeof localStream !== 'undefined' && isChannelReady) {
        console.log('>>>>>> creating peer connection');
        createPeerConnection();
        pc.addStream(localStream);
        isStarted = true;
        console.log('isInitiator', isInitiator);
        if (isInitiator) {
            doCall();
        }
    }
}
function createPeerConnection() {
    try {
        pc = new RTCPeerConnection(null);
        pc.onicecandidate = handleIceCandidate;
        pc.onaddstream = handleRemoteStreamAdded;
        pc.onremovestream = handleRemoteStreamRemoved;
        console.log('Created RTCPeerConnnection');
    } catch (e) {
        console.log('Failed to create PeerConnection, exception: ' + e.message);
        alert('Cannot create RTCPeerConnection object.');
        return;
    }
}
function handleIceCandidate(event) {
    console.log('icecandidate event: ', event);
    if (event.candidate) {
        sendMessage({
            type: 'candidate',
            label: event.candidate.sdpMLineIndex,
            id: event.candidate.sdpMid,
            candidate: event.candidate.candidate
        });
    } else {
        console.log('End of candidates.');
    }
}
function handleRemoteStreamAdded(event) {
    console.log('Remote stream added.');
    remoteStream = event.stream;
    remoteVideo.srcObject = event.stream;
}
function handleRemoteStreamRemoved(event) {
    console.log('Remote stream removed. Event: ', event);
}

這裏用到了一個實現定義的方法sendMessage,這個方法可以讓一個用戶端將信息發送給服務器,之後通過服務器,將這個消息轉發給其他的用戶端,在整個webRTC通訊過程中發揮了非常大的作用。

//客戶端代碼
function sendMessage(message) {
    console.log('Client sending message: ', message);
    socket.emit('message', message);
}
//服務器端相應處理的代碼
socket.on('message', function (message) {
    socket.broadcast.emit('message', message);
});

作爲會話發起人,要率先執行docall操作,也就是向會話對方發送offer,這個過程中,發起人端會通過setLocalDescription自動生成保存本地音視頻相關參數的SDP對象。注意,發起人端在調用setLocalDescription的時候,還觸發了handleIceCandidate,向服務器發送了iceCandidate信息。

function doCall() {
    console.log('Sending offer to peer');
    pc.createOffer(setLocalAndSendMessage, handleCreateOfferError);
}
function setLocalAndSendMessage(sessionDescription) {
    pc.setLocalDescription(sessionDescription);
    console.log('setLocalAndSendMessage sending message', sessionDescription);
    sendMessage(sessionDescription);
}
function handleCreateOfferError(event) {
    console.log('createOffer() error: ', event);

發起人端通過sendMessage將offer信息和iceCandidate信息經由服務器發送給接收端,那麼相應的接收端會怎麼反應呢?接收端,其實大部分時間都是“被動反應”的。它根據接收到的消息,進行相對應的操作。

當接收端接受到來自發起人端的offer類型的message後,即開始初始化自己的peerConnection,
除了將本地視頻流添加到pc中,還會將收到的offer信息和candidate信息通過setRemoteDescriptionaddIceCandidate保存起來。
之後,接收端通過doAnswer()向發起人端發送對話描述和candidate消息。

// This client receives a message
socket.on('message', function (message) {
    console.log('Client received message:', message);
    if (message === 'got user media') {
        maybeStart();
    } else if (message.type === 'offer') {
        console.log("收到offer!"); //接收端
        if (!isInitiator && !isStarted) {
            maybeStart();
        }
        pc.setRemoteDescription(new RTCSessionDescription(message));
        doAnswer();
    } else if (message.type === 'answer' && isStarted) {
        pc.setRemoteDescription(new RTCSessionDescription(message)); //發起人端
    } else if (message.type === 'candidate' && isStarted) {
        console.log("收到candidate!"); //接收端
        var candidate = new RTCIceCandidate({
            sdpMLineIndex: message.label,
            candidate: message.candidate
        });
        pc.addIceCandidate(candidate);
    } else if (message === 'bye' && isStarted) {
        handleRemoteHangup();
    }
});

function doAnswer() {
    console.log('Sending answer to peer.');
    pc.createAnswer().then(
        setLocalAndSendMessage,
        onCreateSessionDescriptionError
    );

發起人端接收到接收端發來的answer和candidate,同樣通過setRemoteDescriptionaddIceCandidate保存,這樣,參與對話的兩方交換了會話信息和ice信息,建立了音視頻傳輸的P2P通道。

建立連接之後,兩端的通過上面的handleRemoteStreamAdded方法將接受到的視頻流顯示到對應的視頻標籤內。

1v1的視頻通話,就這麼搞定了。這裏展示的代碼主要是幫助大家理清RTCPeerConnection建立的流程,但是並不完善,但是網絡上完整的項目很多,大家可以多找找~

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