1

使用webrtc 模仿微信视频通讯(angular版)


webrtc实现是视频通话大致流程:(a->b)
1,a检查浏览器是否支持,是否有设备摄像头和麦克风设备
2,a捕获音视频流,并且a给b发送一个start call(开始通话)消息
3,b收到a的消息,跳转到视频页面,同样捕获自己的音视频流,成功后,b给a发送一个收到start connect(开始连接)
4,a收到b的startconnect(开始连接),给b一个回应startconnect(开始连接)消息,然后开始添加ice的触发器onicecandidate(当有offer发送时,会触发ice连接),然后把本地a的音播视频轨添加到RTCPeerConnection对象上,然后添加一个监听器(当有视频流加入时,显示播放),最后a创建一个offer并且给b发送offer
5,b收到a的startconnect回应时,开始添加ice的时间触发器onicecandidate(等a有ice连接请求时,开始添加RTCIceCandidateInit凭证),然后把本地的a开始音播视频轨添加到RTCPeerConnection对象上,最后添加一个监听器(当有视频流加入时,显示播放)
6,b收到offer时创建一个answer回应,发送给a,当ice连接成功,视频就通了。

这个流程是我根据下面代码写出来的,写的不太好,可以直接阅读下面源码(下面代码已经实现:视频通话,音视频互转,摄像头切换,网络切换时ice重启等),欢迎指正
@Component({
    selector: 'app-wechat-video',
    templateUrl: './wechat-video.page.html',
    styleUrls: ['./wechat-video.page.scss'],
})
export class WechatVideoPage implements OnInit, OnDestroy {

    constructor(private el: ElementRef,
                private renderer2: Renderer2,
                private events: Events,
                private http: HttpClient,
                public audioman: AudioManagement,
                private toastService: ToastService,
                private activatedRoute: ActivatedRoute,
                private androidPermission: AndroidPermissions,
                private navController: NavController,
                private mainTool: MainToolService,
                @Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService,
                private tio: TiowsNewService) {
    }


    @ViewChild('localVideo', {static: false}) localVideo: ElementRef;
    @ViewChild('remoteVideo', {static: false}) remoteVideo: ElementRef;
    servers: RTCConfiguration = {
        iceServers: [{urls: 'turn:xx.xx.xx.xx:3478', username: 'chenmiao', credential: '123456'}]
    };

    bgImg = 'assets/bg/avatar10.jpg';
    environment = environment;
    friend: Friend;
    mySelfName: string;
    localStream: MediaStream; // 本地视频流
    pc: RTCPeerConnection;
    // 判断当前对象属性
    isCaller = false;
    callTime = 0; // 通话时间
    // 计时器
    timer: any;
    // 相机设备
    cameraList = [];
    // 麦克风设备
    audioInputList = [];
    // 音频输出设备
    audioOutList = [];
    /// 切换摄像头
    switchCameraflag = 0;
    /// 切换麦克风
    switchAudioFlag = 0;
    // 切换主副显示画面
    switchVideoFlag = 0;
    // 音视频流
    switchAudioVideoFlag = 0;
    // 视频连接成功
    isCalling: boolean;

    ngOnInit() {
        this.mySelfName = this.tokenService.get().userId.toString();
        this.messageHandler();
        this.pc = new RTCPeerConnection(this.servers);
        // 判断运行环境
        if (this.mainTool.isRunOnCordova()) {
            this.checkPermission().then(result => {
                if (result) {
                    this.getFriendInfo();
                }
            }).catch(e => {
                console.log('请求权限后发生异常', e);
            });
        } else {
            this.getFriendInfo();
        }
    }

    changeAudioOutput() {
        // 设置声音大小
        // this.localVideo.nativeElement.setSinkId(this.audioOutList[this.audioOutList.length - 1].value.deviceId);
        this.audioman.getMaxVolume(AudioManagement.VolumeType.SYSTEM).then((res) => console.log('ring', res));
        this.audioman.setVolume(AudioManagement.VolumeType.SYSTEM, 5).then(() => console.log('system volume'));
    }

    getFriendInfo() {
        // 获取连接对象信息
        this.activatedRoute.queryParams.subscribe(param => {
            const url = `${environment.apiPrefix}/weChat/user/getUserInfo/${param.userId}`;
            this.http.get(url).subscribe((res: any) => {
                this.friend = res.body;
                this.isCaller = param.isCaller === 'true' ? true : false;
                this.initRtcConnection();
            });
        });
    }

    checkPermission() {
        // 检查android的相机和麦克风权限
        return new Promise((resolve) => {
            Promise.all([this.androidPermission.checkPermission(this.androidPermission.PERMISSION.CAMERA),
                this.androidPermission.checkPermission(this.androidPermission.PERMISSION.RECORD_AUDIO)])
                .then(([video, audio]) => {
                    if (video.hasPermission === true && audio.hasPermission) {
                        resolve(true);
                    } else {
                        this.androidPermission.requestPermissions([
                            this.androidPermission.PERMISSION.CAMERA,
                            this.androidPermission.PERMISSION.RECORD_AUDIO
                        ]).then(([revideo, reaudio]) => {
                            console.log(revideo, reaudio);
                            if (revideo && reaudio) {
                                resolve(true);
                            } else {
                                resolve(false);
                            }
                        });
                    }
                }).catch(e => {
                console.log('请求发生异常', e);
                resolve(false);
            });
        });
    }

    ngOnDestroy() {

    }

    messageHandler() {
        this.events.subscribe('videoMessage', (data, time) => {
            switch (data.type) {
                case 'startConnection':
                    // 准备连接
                    if (this.isCaller) {
                        this.startConnect();
                    }
                    break;
                case 'readySucceed':
                    // 收到准备连接消息
                    if (!this.isCaller) {
                        this.startConnect();
                    }
                    break;
                case 'IceCandidate':
                    this.addIceCandidate(data.data.content);
                    break;
                case 'offer': // 接受发送的offer消息
                    this.setPcOffer(data.data.content);
                    break;
                case 'closeCall': // 接受发送的挂断消息
                    this.closeCall();
                    break;
                case 'switchToVoice': // 视频转音频
                    this.switchToVoice(false);
                    break;
                case 'switchToVideo': // 视频转音频
                    this.switchToVideo(false);
                    break;
            }
        });
    }

    hangup() {
        const messageData: MessageData = {
            from: this.mySelfName,
            to: this.friend.userId,
            cmd: 103,
            createTime: new Date().getTime(),
            content: 'close Call'
        };
        this.tio.sendMessage(messageData);
        this.closeCall();
    }

    closeCall() {
        // 结束通话 close calling
        this.pc.removeEventListener('addstream', () =>  console.log('取消监听'));
        this.isCalling = false;
        this.pc.close();
        this.events.unsubscribe('videoMessage', () => console.log('close video page'));
        if (this.timer) {
            this.timer.unsubscribe();
        }
        this.localStream.getTracks().forEach(stream => stream.stop());
        this.navController.navigateRoot('/tabs/chat-mess', {animated: true, animationDirection: 'back', replaceUrl: true});
    }

    initRtcConnection() {
        /// 打开摄像头设备 main
        if (!this.hasGetUserMedia()) {
            console.log('getUserMedia() is not supported in your browser,');
            return false;
        }
        this.getCameraDevices().then(deviceInfos => {
            // 获取设备硬件信息
            if (deviceInfos != null && deviceInfos.length > 0) {
                deviceInfos.forEach((item, i) => {
                    if (item.kind === 'videoinput') {
                        this.cameraList.push({id: item.deviceId, value: item});
                    } else if (item.kind === 'audiooutput') {
                        this.audioOutList.push({id: item.deviceId, value: item});
                    } else if (item.kind === 'audioinput') {
                        this.audioOutList.push({id: item.deviceId, value: item});
                    }
                });
                this.startLocalStream().then((stream: MediaStream) => {
                    this.localStream = stream;
                    this.localVideo.nativeElement.srcObject = stream;
                    if (typeof this.isCaller === 'boolean' && this.isCaller) {
                        const messageData: MessageData = {
                            from: this.mySelfName,
                            to: this.friend.userId,
                            cmd: 98,
                            createTime: new Date().getTime(),
                            content: 'start call'
                        };
                        this.tio.sendMessage(messageData);
                    } else {
                        const messageData: MessageData = {
                            from: this.mySelfName,
                            to: this.friend.userId,
                            cmd: 99,
                            createTime: new Date().getTime(),
                            content: 'start connection'
                        };
                        this.tio.sendMessage(messageData);
                    }
                }).catch((e) => {
                    console.log('获取视频流失败了', e);
                });
            } else {
                console.log('未获取到设备信息');
            }
        });
    }

    hasGetUserMedia(): boolean {
        // 查看浏览器是否可以使用摄像头 Note: Opera builds are unprefixed.
        return !!(navigator.getUserMedia || navigator.mediaDevices);
    }

    getCameraDevices(): Promise<MediaDeviceInfo[]> {
        /// 获取摄像头设备 Get camera device
        return navigator.mediaDevices.enumerateDevices();
    }

    switchVideoScreen(videoFlag) {
        // 切换主副画面 Switch display screen
        if (this.switchVideoFlag === 0 && videoFlag === 1) {
            this.switchVideoFlag = 1;
        }
        if (this.switchVideoFlag === 1 && videoFlag === 0) {
            this.switchVideoFlag = 0;
        }
    }


    switchCamera() {
        // 切换摄像头 Switch camera
        if (this.canExecuted()) {
            if (this.cameraList.length > 1 && this.switchCameraflag === 0) {
                this.switchCameraflag = this.cameraList.length - 1;
            } else {
                this.switchCameraflag = 0;
            }
            this.startLocalStream().then((stream: MediaStream) => {
                this.localStream = stream;
                this.localVideo.nativeElement.srcObject = stream;
                this.localStream.getVideoTracks().forEach(track => {
                    this.pc.getSenders().forEach((sender: RTCRtpSender) => {
                        if (sender.track.kind === 'video') {
                            sender.replaceTrack(track).then(() => console.log('replace'));
                        }
                    });
                });
            });
        }
    }

    switchAudioVideo(flag) {
        // 切换音视频 switch audio and video 0:video 1:audio
        // this.switchAudioVideoFlag
        console.log(flag);
        if (flag === 1) {
            this.switchToVoice(true);
        } else {
            this.switchToVideo(true);
        }
    }

    switchToVoice(isSender: boolean) {
        // 切换到语音 switch to voice
        if (this.canExecuted()) {
            this.switchCameraflag = -1;
            this.localStream.getVideoTracks().forEach(stream => stream.stop());
            if (isSender) {
                const messageData: MessageData = {
                    from: this.mySelfName,
                    to: this.friend.userId,
                    cmd: 104,
                    createTime: new Date().getTime(),
                    content: 'switch to voice'
                };
                this.tio.sendMessage(messageData);
            }
            this.switchAudioVideoFlag = 1;
        }
    }

    switchToVideo(isSender: boolean) {
        // 切换到视频 switch to video
        this.switchCameraflag = 0;
        if (isSender) {
            const messageData: MessageData = {
                from: this.mySelfName,
                to: this.friend.userId,
                cmd: 105,
                createTime: new Date().getTime(),
                content: 'switch to video'
            };
            this.tio.sendMessage(messageData);
        }
        // this.localStream.getVideoTracks().forEach(stream => stream.isolated);
        this.startLocalStream().then(stream => {
            this.localStream = stream;
            this.localVideo.nativeElement.srcObject = stream;
            if (this.pc && this.pc.getSenders()) {
                this.localStream.getTracks().forEach(track => this.pc.addTrack(track, this.localStream));
            }
            if (this.isCaller) {
                // this.localStream.getTracks().forEach(track => this.pc.getSenders(track, this.localStream));
                setTimeout(() => {
                    this.getPcOffer().then(offer => this.sendOfferFn(offer));
                }, 1000);
            }
        });
        this.switchAudioVideoFlag = 0;
    }

    startTimer() {
        // 计时器
        this.timer = timer(1000, 1000).subscribe((res) => {
            this.callTime = res;
        });
    }

    startConnect() {
        if (this.isCaller) {
            const messageData: MessageData = {
                from: this.mySelfName,
                to: this.friend.userId,
                cmd: 100,
                createTime: new Date().getTime(),
                content: 'start success'
            };
            this.tio.sendMessage(messageData);
        }
        if (!!this.pc) {
            this.pc.onicecandidate = event => {
                if (!!event.candidate) {
                    // this.pc.addIceCandidate(event.candidate).then(() => console.log('add ice success'));
                    console.log(`${this.mySelfName} 发送candidate:{}`, event.candidate);
                    const messageData: MessageData = {
                        from: this.mySelfName,
                        to: this.friend.userId,
                        cmd: 101,
                        createTime: new Date().getTime(),
                        content: JSON.stringify(event.candidate)
                    };
                    this.tio.sendMessage(messageData);
                }
            };
            this.localStream.getTracks().forEach(track => this.pc.addTrack(track, this.localStream));
            this.pc.oniceconnectionstatechange = e => this.onIceStateChange(this.pc, e, 'pc');
            this.pc.addEventListener('addstream', (event: any) => {
                if (!!event && !!this.remoteVideo) {
                    this.remoteVideo.nativeElement.srcObject = event.stream;
                }
            });
            if (this.isCaller) {
                this.getPcOffer().then((offer: RTCSessionDescriptionInit) => {
                    this.sendOfferFn(offer);
                });
            }
        }
    }

    startLocalStream(): Promise<MediaStream> {
        // 开始读取本地流  start local stream
        if (!!this.localStream && this.localStream.getTracks().length > 0) {
            this.localStream.getTracks().forEach(item => item.stop());
        }
        return navigator.mediaDevices.getUserMedia({
            audio: {
                deviceId: {
                    exact: this.audioInputList.length > 0 ?
                        this.audioInputList[this.switchAudioFlag].value.deviceId : undefined
                }
            },
            video: {
                deviceId: {
                    exact: this.cameraList.length > 0 ?
                        this.cameraList[this.switchCameraflag].value.deviceId : undefined
                }
            }
        });
    }

    getPcOffer(): Promise<RTCSessionDescriptionInit> {
        // 生成一个offer
        const rTCOfferOptions: RTCOfferOptions = {iceRestart: true};
        return new Promise(resolve => {
            this.pc.createOffer(rTCOfferOptions).then(offer => {
                resolve(offer);
            });
        });
    }

    getPcAnswer(): Promise<RTCSessionDescriptionInit> {
        // 生成一个answer
        const rTCOfferOptions: RTCOfferOptions = {iceRestart: true};
        return new Promise(resolve => {
            this.pc.createAnswer(rTCOfferOptions).then(answer => {
                resolve(answer);
            });
        });
    }

    sendOfferFn(desc) {
        if (!!desc) {
            this.pc.setLocalDescription(desc).then(() => console.log('set local description'));
            const messageData: MessageData = {
                from: this.mySelfName,
                to: this.friend.userId,
                cmd: 102,
                createTime: new Date().getTime(),
                content: desc
            };
            this.tio.sendMessage(messageData);
        }
    }

    sendAnswerFn(desc) {
        this.pc.setLocalDescription(desc).then(() => console.log('set local description'));
        const messageData: MessageData = {
            from: this.mySelfName,
            to: this.friend.userId,
            cmd: 102,
            createTime: new Date().getTime(),
            content: desc
        };
        this.tio.sendMessage(messageData);
    }

    addIceCandidate(res) {
        if (!!res && !!this.pc) {
            const candidate: RTCIceCandidateInit = JSON.parse(res);
            try {
                this.pc.addIceCandidate(candidate).then(() => console.log(`addIceCandidate success`));
            } catch (e) {
                console.log(`failed to add ICE Candidate: ${e.toString()}`);
            }
        }
    }

    onIceStateChange(pc, event, name) {
        if (pc) {
            if (pc.iceConnectionState === 'connected') {
                if (!this.isCalling) {
                    this.isCalling = true;
                    this.startTimer();
                }
            } else if (pc.iceConnectionState === 'Failed' || pc.iceConnectionState === 'disconnected') {
                // 当切换网络 ice重启
                if (this.isCaller) {
                    console.log('restart connection');
                    this.getPcOffer().then(offer => this.sendOfferFn(offer));
                }
            }
            console.log(`${name} ICE state: ${pc.iceConnectionState}`);
            console.log('ICE state change event: ', event);
        }
    }


    setPcOffer(des) {
        if (!!des && !!this.pc) {
            const offer = JSON.parse(des);
            this.pc.setRemoteDescription(offer).then(() => console.log('远程流成功'), (e) => console.log('远程流失败'));
            if (!this.isCaller) {
                this.getPcAnswer().then((answer: RTCSessionDescriptionInit) => {
                    this.sendAnswerFn(answer);
                });
            }
        }
    }

    isIceConnected(): boolean {
        // 判断当前通讯是否正常 Determine whether the current communication is normal
        // console.log(!!this.pc ? this.pc.iceConnectionState : undefined);
        return !!this.pc && this.pc.iceConnectionState === 'connected';
    }

    getIceConnected() {
        if (this.pc && this.pc.iceConnectionState) {
            return this.pc.iceConnectionState;
        } else {
            return 'null';
        }
    }

效果展示(文章不能放视频就随便截两张图片):

微信图片_20200417110401.png 微信图片_20200417110407.png

chenmiao
4 声望1 粉丝