使用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';
}
}
效果展示(文章不能放视频就随便截两张图片):
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。