WebRTC:一个视频聊天的简单例子

相关API简介

在前面的章节中,已经对WebRTC相关的重要知识点进行了介绍,包括涉及的网络协议、会话描述协议、如何进行网络穿透等,剩下的就是WebRTC的API了。

WebRTC通信相关的API非常多,主要完成了如下功能:

  1. 信令交换
  2. 通信候选地址交换
  3. 音视频采集
  4. 音视频发送、接收

相关API太多,为避免篇幅过长,文中部分采用了伪代码进行讲解。详细代码参考文章末尾,也可以在笔者的Github上找到,有问题欢迎留言交流。

 

信令交换

信令交换是WebRTC通信中的关键环节,交换的信息包括编解码器、网络协议、候选地址等。对于如何进行信令交换,WebRTC并没有明确说明,而是交给应用自己来决定,比如可以采用WebSocket。

发送方伪代码如下:

const pc = new RTCPeerConnection(iceConfig);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
sendToPeerViaSignalingServer(SIGNALING_OFFER,offer); // 发送方发送信令消息

接收方伪代码如下:

const pc = new RTCPeerConnection(iceConfig);
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
sendToPeerViaSignalingServer(SIGNALING_ANSWER,answer); // 接收方发送信令消息

 

候选地址交换服务

当本地设置了会话描述信息,并添加了媒体流的情况下,ICE框架就会开始收集候选地址。两边收集到候选地址后,需要交换候选地址,并从中知道合适的候选地址对。

候选地址的交换,同样采用前面提到的信令服务,伪代码如下:

// 设置本地会话描述信息
const localPeer = new RTCPeerConnection(iceConfig);
const offer = await pc.createOffer();
await localPeer.setLocalDescription(offer);

// 本地采集音视频
const localVideo = document.getElementById('local-video');
const mediaStream = await navigator.mediaDevices.getUserMedia({ 
    video: true,audio: true
});
localVideo.srcObject = mediaStream;

// 添加音视频流
mediaStream.getTracks().forEach(track => {
    localPeer.addTrack(track,mediaStream);
});

// 交换候选地址
localPeer.onicecandidate = function(evt) {
    if (evt.candidate) {
        sendToPeerViaSignalingServer(SIGNALING_CANDIDATE,evt.candidate);
    }
}

 

音视频采集

可以使用浏览器提供的getUserMedia接口,采集本地的音视频。

const localVideo = document.getElementById('local-video');
const mediaStream = await navigator.mediaDevices.getUserMedia({ 
    video: true,audio: true
});
localVideo.srcObject = mediaStream;

 

音视频发送、接收

将采集到的音视频轨道,通过addTrack进行添加,发送给远端。

mediaStream.getTracks().forEach(track => {
    localPeer.addTrack(track,mediaStream);
});

远端可以通过监听ontrack来监听音视频的到达,并进行播放。

remotePeer.ontrack = function(evt) {
    const remoteVideo = document.getElementById('remote-video');
    remoteVideo.srcObject = evt.streams[0];
}

 

完整代码

包含两部分:客户端代码、服务端代码。

1、客户端代码

const socket = io.connect('http://localhost:3000');

const CLIENT_RTC_EVENT = 'CLIENT_RTC_EVENT';
const SERVER_RTC_EVENT = 'SERVER_RTC_EVENT';

const CLIENT_USER_EVENT = 'CLIENT_USER_EVENT';
const SERVER_USER_EVENT = 'SERVER_USER_EVENT';

const CLIENT_USER_EVENT_LOGIN = 'CLIENT_USER_EVENT_LOGIN'; // 登录

const SERVER_USER_EVENT_UPDATE_USERS = 'SERVER_USER_EVENT_UPDATE_USERS';

const SIGNALING_OFFER = 'SIGNALING_OFFER';
const SIGNALING_ANSWER = 'SIGNALING_ANSWER';
const SIGNALING_CANDIDATE = 'SIGNALING_CANDIDATE';

let remoteUser = ''; // 远端用户
let localUser = ''; // 本地登录用户

function log(msg) {
    console.log(`[client] ${msg}`);
}

socket.on('connect',function() {
    log('ws connect.');
});

socket.on('connect_error',function() {
    log('ws connect_error.');
});

socket.on('error',function(errorMessage) {
    log('ws error,' + errorMessage);
});

socket.on(SERVER_USER_EVENT,function(msg) {
    const type = msg.type;
    const payload = msg.payload;

    switch(type) {
        case SERVER_USER_EVENT_UPDATE_USERS:
            updateUserList(payload);
            break;
    }
    log(`[${SERVER_USER_EVENT}] [${type}],${jsON.stringify(msg)}`);
});

socket.on(SERVER_RTC_EVENT,function(msg) {
    const {type} = msg;

    switch(type) {
        case SIGNALING_OFFER:
            handleReceiveOffer(msg);
            break;
        case SIGNALING_ANSWER:
            handleReceiveAnswer(msg);
            break;
        case SIGNALING_CANDIDATE:
            handleReceiveCandidate(msg);
            break;
    }
});

async function handleReceiveOffer(msg) {
    log(`receive remote description from ${msg.payload.from}`);
    
    // 设置远端描述
    const remoteDescription = new RTCSessionDescription(msg.payload.sdp);
    remoteUser = msg.payload.from;
    createPeerConnection();
    await pc.setRemoteDescription(remoteDescription); // TODO 错误处理

    // 本地音视频采集
    const localVideo = document.getElementById('local-video');
    const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true,audio: true });
    localVideo.srcObject = mediaStream;
    mediaStream.getTracks().forEach(track => {
        pc.addTrack(track,mediaStream);
        // pc.addTransceiver(track,{streams: [mediaStream]}); // 这个也可以
    });
    // pc.addStream(mediaStream); // 目前这个也可以,不过接口后续会废弃

    const answer = await pc.createAnswer(); // TODO 错误处理
    await pc.setLocalDescription(answer);
    sendRTCEvent({
        type: SIGNALING_ANSWER,payload: {
            sdp: answer,from: localUser,target: remoteUser
        }
    });
}

async function handleReceiveAnswer(msg) {
    log(`receive remote answer from ${msg.payload.from}`);
    
    const remoteDescription = new RTCSessionDescription(msg.payload.sdp);
    remoteUser = msg.payload.from;

    await pc.setRemoteDescription(remoteDescription); // TODO 错误处理
}

async function handleReceiveCandidate(msg){
    log(`receive candidate from ${msg.payload.from}`);
    await pc.addIceCandidate(msg.payload.candidate); // TODO 错误处理
}

/**
 * 发送用户相关消息给服务器
 * @param {Object} msg 格式如 { type: 'xx',payload: {} }
 */
function sendUserEvent(msg) {
    socket.emit(CLIENT_USER_EVENT,jsON.stringify(msg));
}

/**
 * 发送RTC相关消息给服务器
 * @param {Object} msg 格式如{ type: 'xx',payload: {} }
 */
function sendRTCEvent(msg) {
    socket.emit(CLIENT_RTC_EVENT,JSON.stringify(msg));
}

let pc = null;

/**
 * 邀请用户加入视频聊天
 *  1、本地启动视频采集
 *  2、交换信令
 */
async function startVideoTalk() {
    // 开启本地视频
    const localVideo = document.getElementById('local-video');
    const mediaStream = await navigator.mediaDevices.getUserMedia({
        video: true,audio: true
    });
    localVideo.srcObject = mediaStream;

    // 创建 peerConnection
    createPeerConnection();

    // 将媒体流添加到webrtc的音视频收发器
    mediaStream.getTracks().forEach(track => {
        pc.addTrack(track,{streams: [mediaStream]});
    });
    // pc.addStream(mediaStream); // 目前这个也可以,不过接口后续会废弃
}

function createPeerConnection() {
    const iceConfig = {"iceServers": [
        {url: 'stun:stun.ekiga.net'},{url: 'turn:turnserver.com',username: 'user',credential: 'pass'}
    ]};
    
    pc = new RTCPeerConnection(iceConfig);

    pc.onnegotiationneeded = onnegotiationneeded;
    pc.onicecandidate = onicecandidate;
    pc.onicegatheringstatechange = onicegatheringstatechange;
    pc.oniceconnectionstatechange = oniceconnectionstatechange;
    pc.onsignalingstatechange = onsignalingstatechange;
    pc.ontrack = ontrack;
    
    return pc;
}

async function onnegotiationneeded() {
    log(`onnegotiationneeded.`);

    const offer = await pc.createOffer();
    await pc.setLocalDescription(offer); // TODO 错误处理

    sendRTCEvent({
        type: SIGNALING_OFFER,payload: {
            from: localUser,target: remoteUser,sdp: pc.localDescription // TODO 直接用offer?
        }
    });
}

function onicecandidate(evt) {
    if (evt.candidate) {
        log(`onicecandidate.`);

        sendRTCEvent({
            type: SIGNALING_CANDIDATE,payload: {
                from: localUser,candidate: evt.candidate
            }
        });
    }
}

function onicegatheringstatechange(evt) {
    log(`onicegatheringstatechange,pc.iceGatheringState is ${pc.iceGatheringState}.`);
}

function oniceconnectionstatechange(evt) {
    log(`oniceconnectionstatechange,pc.iceConnectionState is ${pc.iceConnectionState}.`);
}

function onsignalingstatechange(evt) {
    log(`onsignalingstatechange,pc.signalingstate is ${pc.signalingstate}.`);
}

// 调用 pc.addTrack(track,mediaStream),remote peer的 onTrack 会触发两次
// 实际上两次触发时,evt.streams[0] 指向同一个mediaStream引用
// 这个行为有点奇怪,github issue 也有提到 https://github.com/meetecho/janus-gateway/issues/1313
let stream;
function ontrack(evt) {
    // if (!stream) {
    //     stream = evt.streams[0];
    // } else {
    //     console.log(`${stream === evt.streams[0]}`); // 这里为true
    // }
    log(`ontrack.`);
    const remoteVideo = document.getElementById(

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


HTML代码中要想改变字体颜色,常常需要使用CSS样式表。CSS是一门用来描述网页上样式的语言,通过编写CSS代码可以实现网页中各元素的大小、颜色、字体等各种样式的控制。那么如何在HTML代码中应用CSS样式来改变字体颜色呢?这里为大家介绍一下。 首先,在HTML代码...
HTML代码如何让字体盖住图片呢?需要使用CSS的position属性及z-index属性。 img { position: relative; z-index: -1; } p { position: absolute; to...
HTML代码字体设置 在HTML中,我们可以使用标签来设置网页中的文字字体。常用的字体标签是font和style,下面我们来学习如何使用这些标签。 1. font标签 使用font标签可以改变文字的字体、颜色和大小。它有三个属性font-family、color和...
在网页设计中,HTML代码的字体和字号选择是非常重要的一个环节,因为它们直接关系到页面的可读性和视觉效果。 要指定文本的字体和字号,可以使用HTML中的样式属性。使用样式属性设置字体和字号,如下所示: <p style="font-family: Aria...
HTML(Hypertext Markup Language,超文本标记语言)是一种用于创建网页的标准语言。它由许多标签(一对尖括号包围的关键字)组成,这些标签告诉浏览器如何显示内容。使用HTML代码,我们可以轻松地创建各种类型的图像和图形,如太极图。 在HTM...
外链是指在一个网页中添加一个指向其他网站的链接,用户可以通过这个链接直接跳转到其他网站。在HTML中,实现外链的方法很简单,只需要使用标签就可以了。 <a href="http://www.example.com">这是一个外链,点击跳转到www.ex...
HTML代码是实现网页界面的基础,而网页中的各种表单则是用户和网站进行交互的重要方式之一。下面我们来介绍如何使用HTML代码实现一个简单的报名表格。 <form action="submit.php" method="post"> &lt...
HTML是一种标记语言,用于开发网站和其他互联网内容。字体是网站设计中的关键元素之一,它可以决定网站的整体风格和呈现效果。HTML提供了字体编辑器,使网站设计变得更加容易。 <font face="Arial"> 这里是Arial字体 &...
HTML代码中,字体样式是开发者们必备的一部分。在HTML中,我们可以通过特定的标签和属性设置字体的样式、颜色和大小,以达到更好的排版效果。 <p style="font-size: 14px; color: #333; font-family:...
HTML中的字体可以设为粗体,以强调文本信息。我们可以通过使用一些标签来实现这一功能。其中,常用的标签包括: 1. 标签:该标签会把文本加粗显示,语法如下: 这是一段加粗的文本 2. 标签:与标签作用相同,但语义更强,表示该文本内容的重要性。语法如下:...
HTML代码可以实现文件的上传和下载,在网页开发中相当常见。通过使用<input>标签和<form>标签,我们可以轻松创建一个文件上传表单。 <form action="upload.php" method="post" enct...
HTML代码非常常见于网页设计中。在一些需要处理时间相关数据的场景下,可能需要将时间戳转换为实际时间,这时候就需要使用一些特定的HTML代码。 function timeStamp2Time(time){ var date = new Date(time...
HTML是一种用于创建网页的标记语言。在HTML中,我们可以使用超链接标签实现下载文件到本地的功能。 具体实现步骤如下: <a href="文件的URL" download="文件名">下载文件</a> 其中,href属性是文件...
在HTML代码中,对于字体靠左对齐有各种方法。其中最简单的方式之一是使用pre标签。 使用pre标签可以保留一段文本中的空格和换行符,从而使代码排版更加整齐。下面是一个例子: <p>这是一个段落。</p> &lt...
HTML代码字典是一本解释HTML标记和属性的参考文献。这本字典中包含了最常用的HTML代码以及它们的属性和值的详细描述。 例如,以下是HTML代码字典中的一部分内容: <a href="url">link text</a> 在...
在网页开发过程中,遇到一些需要用户复制的内容,我们通常都会提供复制按钮,让用户更方便地复制所需内容。下面我们来介绍如何使用html代码实现一键复制的功能。 var copyBtn = document.querySelector('#copy-bt...
用户登录 欢迎来到公司登录界面,请输入用户名和密码登录 用户名: 密码: 代码解释: 第1行:定义了一个 HTML 文档 第2行:开始了 HTML 头部 第3行:定义了...
HTML 代码是用来创建网页的语言,它包含了许多不同的元素和属性,让我们可以在网页中添加各种不同的元素和内容,如文字、图片、链接等等。在编写 HTML 代码时,我们可以使用各种不同的样式来美化我们的网页,例如更改字体、颜色、大小等等。 font-family:...
HTML代码中的字体转移 在编写HTML代码时,我们经常会使用各种字体来增强页面的可读性和视觉效果。但是,有些字符或特殊符号可能会在HTML中具有不同的含义,这就需要使用字体转义转换成HTML可以正常显示的字符。 在HTML中,使用"&"符号表示一个特殊字符将要被转...
HTML 编程语言中,你可以使用字体属性来更改文本的字体大小、颜色和样式。其中,字体颜色是最常用的样式更改。在 HTML 中,你可以使用 "color" 属性来更改文本的颜色。下面是一个使用 "pre" 标签的代码示例,演示如何使用 "color" 属性来更改字体颜色...