WebRTC离不开音频和视频,那么最开始的问题就是,这些媒体数据是怎么获取的?在RTC中,采集音频或视频设备后会源源不断地产生媒体数据,这些数据被称为媒体流。例如,从Canvas、摄像头或计算机桌面捕获的流为视频流,从麦克风捕获的流为音频流。媒体流是朝远端发送音视频数据的前置条件,否则会出现连接建立后数据不通的情况,即看不到对方的视频或听不到对方的声音。

由于这些媒体流中混入的可能是多种数据,因此WebRTC又将其划分成多个轨道,分为视频媒体轨道和音频媒体轨道。这个和现实生活相比很好理解,比如高速要求开120KM,如果自行车上去了,就会造成整体速度的下降,所以才会分不同的车道,更快的火车就有铁轨,飞机也有飞机的路线,这样每个交通工具都能够利用带宽最大化。在WebRTC中,每个轨道对应于具体的设备,如视频直播中,主播的计算机上可能插入了多个高清摄像头和专业话筒。这些设备的名称和Id可以通过媒体轨道获取,从而可以对其进行控制或访问当前设备状态,如麦克风静音和取消静音。

媒体流和轨道相关API
MediaStream 媒体流可以通过getUserMedia或getDisplayMedia接口获取
MediaStreamTrack 媒体轨道通过MediaStream的getVideoTracks获取所有的视频轨道,通过getAudioTracks获取所有的音频轨道
Video.captureStream Video为视频源对象,如正在点播的电影,通过captureStream方法可以捕捉到其媒体流,传入的帧率越大,视频画面越流畅,同时所需的带宽也越大
Canvas.captureStream Canvas为浏览器画布对象,如通过Canvas实现的一个画板,通过captureStream方法可以捕获其媒体流,即可动态的获取画板中所画的内容。同理,传入的帧率越大,画板描绘的动作越流畅,当然,所需的带宽也越大,一般帧率取10就够用了

媒体流处理是通过MediaStream接口进行的。一个流包含几个轨道,比如视频轨道和音频轨道。有两种方法可以输出MediaStream对象:其一,可以将输出显示为视频或音频元素;其二,可以将输出发送到RTCPeerConnection对象,然后将其发送到远程计算机。媒体流不仅可以通过访问设备产生,还可以通过其他方式获取。归纳起来有以下几种方式。

  1. 摄像头:捕获用户的摄像头硬件设备。
  2. 麦克风:捕获用户的麦克风硬件设备。
  3. 计算机屏幕:捕获用户的计算机桌面。
  4. 画布Canvas:捕获浏览器的Canvas标签内容。
  5. 视频源Video:捕获Video标签播放的视频内容。
  6. 远端流:使用对等连接来接收新的流,如对方发过来的音视频流。

要进行媒体流的开发,就要先熟悉核心API,简单归纳一下MediaStream的属性、事件及方法。

  1. 属性通过MediaStream.active可以监听流是否处于活动状态,即是否有音视频流。常用属性如下所示。·
  2. MediaStream.active:如果MediaStream处于活动状态,则返回true,否则返回false。
  3. MediaStream.ended:如果在对象上已触发结束事件,则返回true,这意味着流已完全读取,如果未达到流结尾,则为false。
  4. MediaStream.id:对象的唯一标识符。
  5. MediaStream.label:用户代理分配的唯一标识符。
    在高速上,有可能因为车出了故障造成拥堵,也有可能因为变道降速、提速,这些在WebRTC中就叫做事件。

MediaStream事件用于监听流处理及活动状态变化。如当用户点击共享屏幕的“停止”按钮时会触发MediaStream.oninactive事件。当添加新的MediaStreamTrack对象时触发MediaStream.addTrack事件。
常用的事件如下。

  1. MediaStream.onactive:当MediaStream对象变为活动状态时触发的活动事件的处理程序。
  2. MediaStream.onaddtrack:在添加新的MediaStreamTrack对象时触发的addTrack事件的处理程序。
  3. MediaStream.onended:当流终止时触发的结束事件的处理程序。
  4. MediaStream.oninactive:当MediaStream对象变为非活动状态时触发的非活动事件的处理程序。
  5. MediaStream.onremovetrack:在从它移除MediaStreamTrack对象时触发的removeTrack事件的处理程序。

很显然,高速上如果有某个事件,那就需要比如打高速交警,开双闪等,在web RTC中,也有类似的对事件的处理。主要就是用方法MediaStream,用于添加、删除、克隆及获取音视频轨道。方法及说明如下

  1. MediaStream.addTrack():将作为参数的MediaStreamTrack对象添加到MediaStream中。如果已经添加了音轨,则没有发生任何事情。
  2. MediaStream.clone():使用新id返回MediaStream对象的克隆。
  3. MediaStream.getAudioTracks():从MediaStream对象返回音频MediaStreamTrack对象的列表。
  4. MediaStream.getTrackById():通过id返回跟踪。如果参数为空或未找到id,则返回null。如果多个轨道具有相同的id,则返回第一个轨道
  5. MediaStream.getTracks():从MediaStream对象返回所有MediaStreamTrack对象的列表。
  6. MediaStream.getVideoTracks():从MediaStream对象返回视频MediaStreamTrack对象的列表。
  7. MediaStream.removeTrack():从MediaStream中删除作为参数的MediaStreamTrack对象。如果已删除该轨道,则不会发生任何操作。

再来讲一下很重要的媒体录制。影像及声音的保存在有些场景下是有必要的,如医生会诊,举行重要视频会议,老师教授课程等场景,目的是便于日后回放。

MediaRecorder是控制媒体录制的API,在原生App开发中是一个应用广泛的API,用于在App内录制音频和视频。事实上随着Web的应用越来越富媒体化,W3C也制定了相应的Web标准,称为MediaRecorder API,它给我们的Web页面赋予了录制音视频的能力,使得Web可以脱离服务器、客户端的辅助,独立进行媒体流的录制。

任何媒体形式的标签都可以录制,包括音频标签、视频标签以及画布标签,其中与可以来自网络媒体文件,也可以来自本机设备采集。而的内容则更加自由,任何绘制在画布上的用户操作、2D或3D图像,都可以进行录制。它为Web提供了更多可能性,我们甚至可以把一个HTML5游戏流程录成视频,保存落地或进行实况传输。

录制出来的是经过标准编码后的媒体流数据,可以注入标签,也可以打包生成文件,还可以进行流级别的数据处理,比如画面识别、动态插入内容、播放跳转控制等。最后生成的文件可以是mp3、mp4、ogg以及webm等格式。

常用API可以使用MediaRecorder.start(timeslice)录制媒体,其中,timeslice是可选项,如果没有设置,则在整个录制完成后触发ondataavailable事件,如果设置了,比如设置为10,就会每录制10毫秒触发一次ondataavailable事件。常用方法如下所示。

  • MediaRecorder.stop():停止录制。
  • MediaRecorder.pause():暂停录制。
  • MediaRecorder.resume():恢复录制。
  • MediaRecorder.isTypeSupported():检查是否支持录制某个格式。

制事件MediaRecorder有两个重要事件,

  1. MediaRecorder.ondataavailable:当数据有效时触发的事件,数据有效时可以把数据存储到缓存区里。
  2. MediaRecorder.onerror:当有错误时触发的事件,出错时录制会被停止。

bitsPerSecond:指定音频和视频的比特率,此属性可以用来指定上面两个属性。如果只有上述两个属性之一或此属性被指定,则此属性可以用于设定另外一个属性。

通过MediaRecorder对象可以将麦克风采集的音频数据录制成一个声音文件,如ogg文件。

WebRTC可以直接捕获用户电脑的整个屏幕,也可捕获某个应用窗口。旧的Chrome浏览器还需要借助插件来解决这一问题,新的Chrome则可以直接获取。当获取到用户电脑的数据流stream后,再使用MediaRecorder则可将其录制成视频文件。显示器的分辨率可以设置成不同的值,如2880×1800、1440×900。分辨率越高,清晰度越高。当显示器的分辨率较高时,如果捕获时设置了一个较小的约束,则可能最终录下来的视频不清晰。

最后附上相关代码:

import React from "react";
import {Button} from "antd";
let mediaRecorder;
let recordedBlobs;
let stream;
class RecordScreen extends React.Component {
    startCaptureScreen = async (e) => {         
        try {
        stream = await navigator.mediaDevices.getDisplayMedia({
            video: {
                width: 2880,
                height: 1800}
        });              
                const video = this.refs['myVideo'];                
                const videoTracks = stream.getVideoTracks();                     
                console.log(`视频资源名称: ${videoTracks[0].label}`);             
                window.stream = stream;                     
                video.srcObject = stream;              
                this.startRecord(); 
        } catch (e) {             
                console.log('getUserMedia错误:' + error);
        }
    }

    startRecord = (e) => {                
        stream.addEventListener('inactive', e => {             
            console.log('监听到屏幕捕获停止后停止录制!');             
            this.stopRecord(e);
    });               
        recordedBlobs = [];         
        try {                        
            mediaRecorder = new MediaRecorder(window.stream, {mimeType: 'video/webm'});
    } catch (e) {             
            console.error('创建MediaRecorder错误:', e);             
            return;
    }             
        mediaRecorder.onstop = (event) => {             
            console.log('录制停止: ', event);             
            console.log('录制的Blobs数据为: ', recordedBlobs);
    };                
        mediaRecorder.ondataavailable = (event) => {             
            console.log('handleDataAvailable', event);                         
            if (event.data && event.data.size > 0) {                            
                recordedBlobs.push(event.data);}
    };                 
        mediaRecorder.start(10);         
        console.log('MediaRecorder started', mediaRecorder);
    }
        stopRecord = (e) => {         
            mediaRecorder.stop();     
            stream.getTracks().forEach(track => track.stop());           
            stream = null;                
            const blob = new Blob(recordedBlobs, {type: 'video/webm'});             
            const url = window.URL.createObjectURL(blob);         
            const a = document.createElement('a');         
            a.style.display = 'none';         
            a.href = url;            
            a.download = 'screen.webm';                
            document.body.appendChild(a);         
            a.click();            
            mediaRecorder = new MediaRecorder(window.stream, {mimeType: 'video/webm'});
    }
    catch(e) {             
        console.error('创建MediaRecorder错误:', e);             
        return;
    }
    mediaRecorder.onstop = (event) => {             
        console.log('录制停止: ', event);             
        console.log('录制的Blobs数据为: ', recordedBlobs);
    };
    mediaRecorder.ondataavailable = (event) => {             
        console.log('handleDataAvailable', event); 
            if (event.data && event.data.size > 0) {               
                recordedBlobs.push(event.data);
            }
    };
    mediaRecorder.start(10;console.log(
    'MediaRecorder started'
    mediaRecorder;
}      
stopRecord = (e) => {          
    mediaRecorder.stop();             
    stream.getTracks().forEach(track => track.stop());          
    stream = null;            
    const blob = new Blob(recordedBlobs, {type: 'video/webm'});         
    const url = window.URL.createObjectURL(blob);      
    const a = document.createElement('a');     
    a.style.display = 'none';        
    a.href = url;       
    a.download = 'screen.webm';        
    document.body.appendChild(a);     
    a.click();

当我们学会了如何通过多种方式获取本地媒体流、音视频设置以及控制媒体流的内容后,就要考虑如何把本地的媒体数据以流的方式发送到远端,远端接收到媒体流后,渲染视频并播放声音,从而达到通话的目的。

在下一篇中,就会讲一对一的视频是怎么实现的了。

「本文为个人原创,首发于 RTC开发者社区」