WebRTC H5实现服务器转发的视频聊天

WebRTC H5实现服务器转发的视频聊天

说明:

  1. 此处使用到的WebRTC皆为H5的API,实际上调用的是封装在浏览器的WebRTC的库,用于获取实时视频数据,传输数据则是使用WebSocket实现。
  2. 其中的实例语法只用到原生JS,版本为ES6,可能需要较高版本的浏览器支持(IE一般不支持)。

1.获取音视频数据

方法:navigator.mediaDevices.getUserMedia

1.1前置条件

基于浏览器的安全策略,通过WebRTC(具体为getUserMedia)调用摄像头和麦克风获取音视频数据,只能是在HTTPS下的网页,或者是本地localhost下才能调用,需要先校验。

function validate(){
    var isSecureOrigin = location.protocol === 'https:' ||
            location.hostname === 'localhost';
    if (!isSecureOrigin) {
        alert('getUserMedia() must be run from a secure origin: HTTPS or localhost.' +'\n\nChanging protocol to HTTPS');
        location.protocol = 'HTTPS';
    }
}

1.2初始化

直接获取音视频,一般默认是前置摄像头。

var constraints = {
    audio: true, 
    video: true  //设置为false则只获取音频
};

还可以设置视频的其他参数,此处设置视频的分辨率,min和max为分辨率的最值,ideal为理想值,浏览器会先尝试找到最接近指定的理想值的设定或者摄像头(如果设备拥有不止一个摄像头),但不保证。下面是获取分辨率为640*480的视频。

var constraints = {
    audio: true,
    video: {  width: { min: 640, ideal: 640, max: 640 },  height: { min: 480, ideal: 480, max: 480 }}
};

初始化

//链式操作,then和catch是并列关系
navigator.mediaDevices.getUserMedia(constraints)
    .then(handleSuccess)
    .catch(handleError);

1.3处理音视频数据

此时返回的只是一个流对象,包括了原始的音视频数据,不能直接操作,需要后续的MediaRecorder封装才能处理。

function handleSuccess(stream) {
    log('getUserMedia() got stream: ', stream);

    //stream即为返回成功的结果,设置为全局便于的后续上传操作
    window.stream = stream;

    //显示在页面上
    var recordedVideo = document.querySelector('video#recorded');
    recordedVideo.srcObject = stream;
    recordedVideo.onloadedmetadata = function(e) {
        log("Label: " + stream.label);
        //音轨,PCM
        log("AudioTracks" , stream.getAudioTracks());
        //视频图像
        log("VideoTracks" , stream.getVideoTracks());
    };
}

function handleError(error) {
    console.log('navigator.getUserMedia error: ', error);
    alert('navigator.getUserMedia error: ', error);
}

在当前页面显示需要通过\

<video id="recorded" autoplay muted>video>

2.处理音视频数据

2.1前置条件

使用MediaRecorder(录制视频的API),需要检测支持编码的格式。

var mimeType =  'video/webm;codecs=vp9';
if (!MediaRecorder.isTypeSupported(mimeType)) {
    console.log(mimeType + ' is not Supported');
    mimeType =  'video/webm;codecs=vp8';
    if (!MediaRecorder.isTypeSupported(mimeType)) {
        console.log(mimeType + ' is not Supported');
        mimeType =  'video/webm';
    }
}

2.2初始化

设置音视频的编码格式,同时也可以设置音视频码率,目前只支持WebM格式,支持的编码格式具体看浏览器。

var mineType = 'video/webm; codecs=vp9';
var options = {mimeType:mimeType};
//音频码率
options.audioBitsPerSecond  = 10000;
//视频码率
options.videoBitsPerSecond = 25000;

初始化MediaRecorder,添加音视频的编码格式。

try {
    var options = {mimeType:mimeType};
    mediaRecorder = new MediaRecorder(window.stream, options);
} catch (e) {
    console.error('Exception while creating MediaRecorder: ' + e);
    alert('Exception while creating MediaRecorder: '
          + e + '. mimeType: ' + options.mimeType);
    return;
}

2.3处理音视频数据

开始录制,最好设置录制的间隔参数,1000代表的是每1000毫秒调用handleDataAvailable处理音视频数据,我们可以通过handleDataAvailable间接上传数据。

//1000毫秒上传一次数据,过小(如10ms)会导致接收端浏览器处理失败
mediaRecorder.start(1000); 

时间设置必须合理,不然接收端会处理不来,报错。

Uncaught TypeError: Failed to execute ‘appendBuffer’ on ‘SourceBuffer’: No function was found that matched the signature provided.

​ at DataConnection.dataConnectionMessage (chatRoom.html:272)

停止录制。

mediaRecorder.stop();

处理音视频数据,此时可以上传到服务器。

mediaRecorder.ondataavailable = handleDataAvailable;

//根据mediaRecorder.start(1000)设置的时间会触发一次
function handleDataAvailable(event) {
    if (event.data && event.data.size > 0) {
        console.log('正在发送数据...');
        //recordedBlobs.push(event.data);

        //通过WebSocket发送到后台
        socket.send( event.data );//实际数据,音视频同帧
        socket.send( event.timeStamp );
    }
}

3.显示(服务器推送的)音视频数据

3.1前置条件

使用MediaSource(直播流MSE API),检测支持的解码格式。

var mimeType =  'video/webm;codecs=vp9';
if (!MediaSource.isTypeSupported(mimeType)) {
    console.log(mimeType + ' is not Supported');
    mimeType =  'video/webm;codecs=vp8';
    if (!MediaSource.isTypeSupported(mimeType)) {
        console.log(mimeType + ' is not Supported');
        mimeType =  'video/webm';
    }
}

3.2初始化

var vidElement = document.getElementById('vid');
var mediaSource = new MediaSource();
vidElement.src = URL.createObjectURL(mediaSource);

因为URL.createObjectURL(mediaSource)这一步在MediaSource是异步的,需要监听sourceopen等其创建完成后进行下一步。

mediaSource.addEventListener('sourceopen', sourceOpen);

添加sourceBuffer,实际上数据的交互在此对象中。

var sourceBuffer;
//当加载完成后
function sourceOpen(e) {
    console.log('sourceOpen');
    //节省内存,可不用这句
    URL.revokeObjectURL(vidElement.src); 
    var mime = mineType;
    var mediaSource = e.target;

    //添加解码格式
    sourceBuffer = mediaSource.addSourceBuffer(mime);
}

3.3处理数据

接收数据(只支持ArrayBuffer,可以在WebSocket上设置binaryType实现)。

sourceBuffer.appendBuffer(data);

在当前页面显示需要通过\

<video id="vid" autoplay muted>video>

注意:首包包含了格式,接收方一定要确定收到,不然无法解析。

4.WebSocket

4.1客户端

浏览器已实现具体细节,使用WebSocket调用,注意HTTP协议下WebSocket使用WS协议,HTTPS下使用WWS。

var wsHeader = 'ws://';
if(location.protocol === 'https:') wsHeader = 'wss://';

// URL形如 ws://localhost:8080/user,其中/user代表服务端自定义的实现类,可带参数
var ws = new WebSocket( wsHeader+window.location.host+'/user'+'?chatId=123');

通过websocket.binaryType设置传输的格式,此处设置为arraybuffer,后端的对象自动转换。发送端的类型为Blob。

ws.binaryType = 'arraybuffer';

这一步相当于显式的

FileReader.addEventListener("load", function(){sourceBuffer.appendBuffer(this.result)});
this.reader.readAsArrayBuffer(event.data);

接收数据

ws.onmessage = function (event){
    //实际数据在event.data
    console.log('正在接收数据...',event.data);

    //sourceBuffer.appendBuffer(event.data);
};

发送数据

var data = new Blob();
ws.send(data);

4.2服务端

基于tomcat的实现类WebSocketServletMessageInbound实现。

package org.rtc.chat;

import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WebSocketServlet;
import org.apache.commons.lang.StringUtils;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 管理用户活动和实时音视频数据,用户前端获取UserConnection实例
 *
 */
//具体路径
@WebServlet(urlPatterns = { "/user"})
public class UserWebSocketServlet extends WebSocketServlet {

    private static final long serialVersionUID = 1L;

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        System.out.println("websocket chat by "+request.getParameter("uid")+"  "+request.getParameter("rid"));
        super.doGet(request, response);
    }

    @Override
    protected StreamInbound createWebSocketInbound(String s, HttpServletRequest request) {
        String userId = request.getParameter("uid");
        String roomId = request.getParameter("rid");
        roomId = StringUtils.isNotEmpty(roomId)? roomId: "testRoom";
        return  new UserConnection(roomId, userId);
    }

}

具体业务实现MessageInbound,比如UserConnection,里面主要是要接收浏览器通过WebSocket接收数据。

Java WebSocket接收数据的方法

//主要是接收前端的Blob二进制对象
protected void onBinaryMessage(ByteBuffer message){}
//接收文本
protected void onTextMessage(CharBuffer message) {}

发送数据

//发送二进制数据到前端
MessageInbound.getWsOutbound().writeBinaryMessage(ByteBuffer)
//发送字符数据到前端
MessageInbound.getWsOutbound().writeTextMessage(CharBuffer)

注意:org.apache.catalina.websocket.MessageInboundByteBuffer是复用的,需要读取实际数据,建议将实际数据提取出来,如保存到字节数组中。

//ByteBuffer message
System.out.println(message.mark());
byte[] b = new byte[message.limit()];
message.get(b, 0, message.limit());

5.WebView

设置可访问不安全的https

wv.setWebViewClient(new WebViewClient(){
        //访问https
        @Override
        public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error){
        //handler.cancel(); 默认的处理方式,WebView变成空白页
        handler.proceed(); //接受证书
        //handleMessage(Message msg); 其他处理
        }
});

设置可调用WebRTC

wv.setWebChromeClient(new WebChromeClient(){
        //---WebRTC:
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        @Override
        public void onPermissionRequest(PermissionRequest request) {
            request.grant(request.getResources());
        }
});

WebSettings setting = wv.getSettings();
setting.setJavaScriptEnabled(true);

6.总结

上述方法只是取巧地实现了视频聊天功能,其中遇到的问题:

  1. 和内置浏览器的表现一样,移动端浏览器需要设置才能自动播放。但是火狐可设置为自动播放,谷歌浏览器只能自动播放静音视频。

  2. MediaRecorderSourceBufferBlob都需要编码,浏览器支持程度不一样。Chrome支持采集视频编码格式为H264和VPx的WebM,但只支持播放H264的MP4和VPx的WebM。

  3. 华为P10不支持播放vp9编码的,坚果支持。安卓5,6不支持。webM迅雷播放webm不行,会认为是直播,转为MP4可以,直接在网页上播放也行,腾讯视频也可正常播放。

  4. 接收端需要存够足够的时间才能播放流。append()函数时需要一定的执行时间的,如果在很短的时间内接收到两片及以上的数据,就会引发两次append()函数,在前次append()函数未执行结束的情况下再次调用append()函数会产生错误从而导致该sourcebuffer被移除从而出现错误

    Uncaught DOMException: Failed to execute ‘appendBuffer’ on ‘SourceBuffer’: This SourceBuffer is still processing an ‘appendBuffer’ or ‘remove’ operation.

  5. facingMode: "environment"属性对安卓系统无效。

WebRTC基于C++编写的去中心化项目,在浏览器上封装了H5的API,涉及音视频的采集,编解码,优化等,其中的数据传输部分只有点对点的实现,如果想实现视频的录制、回放等功能,需要另行编码实现。而市面上也有成熟的流服务器(MCU),但是基于C++编写的,或者是NodeJS的,或者只有SDK供调用。

你可能感兴趣的:(java,前端)