记一下最近做的腾讯实时视频(TCRC)

记一下最近做的腾讯实时视频
业务需要,在客户端加一下跳转小程序,在小程序里面进行和pc客户的视频/语音通话。于是乎就用到了腾讯的tcrc实时视频。博客也有很多大佬做过了,我只是班门弄斧,记录一下自己弄的,我这里只列出了小程序的实现方式和代码,pc的没有列出。
pc端效果:
在这里插入图片描述
移动端效果
呼叫中:
在这里插入图片描述

接通后语音模式:
在这里插入图片描述

接通后视频模式:
在这里插入图片描述

1,官方demo
下载官方demo,附地址:https://github.com/undefineders/WXMiniByUniapp-TRTC,然后导入本地HbuilderX中,运行到小程序,如果运行成功进行下一步。
填好secret以及sdkid啥的运行到开发者工具,记住这样是不行的,要真机调试才可以看到效果。
2,我做的是uniapp,所以官方的demo对于我来说不能直接用,找了找,果然有大佬,把官方的转成了uniapp版本。
地址:https://ext.dcloud.net.cn/plugin?id=1286。
3,下载下来,删掉不用的(我这里用的是1v1)
这是我的组件目录
在这里插入图片描述
要使用人家这个uniapp里面的两个js文件,人家重写了setData方法
在这里插入图片描述
组件代码:基本未动,你只需要监控里面的一些操作(如挂断,切换摄像头等),还有更改一些字体图标

<template>
	<view>
		<view class="trtc-room-container">
			<view v-if="template === '1v1'">
				<view data-type="template" data-is="1v1" data-attr="pusher, streamList, debug">
					<view class="template-1v1">
						<view
							v-for="(item, streamID) in streamList"
							:key="streamID"
							v-if="item.src && (item.hasVideo || item.hasAudio)"
							:class="'view-container player-container ' + (item.isVisible ? '' : 'none')"
						>
						<!-- 对方视频 -->
							<!-- <live-player
								class="player"
								:data-userid="item.userID"
								:data-streamid="item.streamID"
								:data-streamtype="item.streamType"
								:src="item.src"
								mode="RTC"
								:autoplay="item.autoplay"
								:mute-audio="item.muteAudio"
								:mute-video="item.muteVideo"
								:orientation="item.orientation"
								:object-fit="item.objectFit"
								:background-mute="item.enableBackgroundMute"
								:min-cache="item.minCache"
								:max-cache="item.maxCache"
								:sound-mode="item.soundMode"
								:enable-recv-message="item.enableRecvMessage"
								:auto-pause-if-navigate="item.autoPauseIfNavigate"
								:auto-pause-if-open-native="item.autoPauseIfOpenNative"
								:debug="debug"
								@statechange="playerStateChangeFun"
								@fullscreenchange="playerFullscreenChangeFun"
								@netstatus="playerNetStatusFun"
								@audiovolumenotify="playerAudioVolumeNotifyFun"
								:idAttr="item.streamID"
							></live-player> -->
						</view>
						<!-- 自身画面 -->
						<view v-show="pusher.enableCamera" :class="'view-container pusher-container ' + (pusher.isVisible ? '' : 'none') + ' ' + (JSON.stringify(streamList) == '[]' ? 'fullscreen' : '')">
							<live-pusher
								class="pusher"
								:url="pusher.url"
								:mode="pusher.mode"
								:autopush="pusher.autopush"
								:enable-camera="pusher.enableCamera"
								:enable-mic="pusher.enableMic"
								:enable-agc="pusher.enableAgc"
								:enable-ans="pusher.enableAns"
								:enable-ear-monitor="pusher.enableEarMonitor"
								:auto-focus="pusher.enableAutoFocus"
								:zoom="pusher.enableZoom"
								:min-bitrate="pusher.minBitrate"
								:max-bitrate="pusher.maxBitrate"
								:video-width="pusher.videoWidth"
								:video-height="pusher.videoHeight"
								:beauty="pusher.beautyLevel"
								:whiteness="pusher.whitenessLevel"
								:orientation="pusher.videoOrientation"
								:aspect="pusher.videoAspect"
								:device-position="pusher.frontCamera"
								:remote-mirror="pusher.enableRemoteMirror"
								:local-mirror="pusher.localMirror"
								:background-mute="pusher.enableBackgroundMute"
								:audio-quality="pusher.audioQuality"
								:audio-volume-type="pusher.audioVolumeType"
								:audio-reverb-type="pusher.audioReverbType"
								:waiting-image="pusher.waitingImage"
								:debug="debug"
								@statechange="pusherStateChangeHandlerFun"
								@netstatus="pusherNetStatusHandlerFun"
								@error="pusherErrorHandlerFun"
								@bgmstart="pusherBGMStartHandlerFun"
								@bgmprogress="pusherBGMProgressHandlerFun"
								@bgmcomplete="pusherBGMCompleteHandlerFun"
							></live-pusher>
							<view class="loading" v-if="streamList.length === 0">
								<view class="loading-text">等待接听中...</view>
							</view>
						</view>
						<cover-view v-if="streamList.length != 0" class="calling-time">通话时间 {{ videoOrVoiceObj.msgIPtime.longTime}}</cover-view>
						<cover-view class="handle-btns" v-if="videoBtnsVisible">
							<!--视频/语音   toggleVideoFun -->
							<cover-view class="box-wrap" v-if="streamList.length != 0">
								<cover-view class="btn-normal" @click="toggleVideoFun">
									<cover-image class="closeVideoImg" v-if="pusher.enableCamera" :src="closeVideoImg"></cover-image>
									<cover-image class="videoImg" v-else :src="videoImg"></cover-image>
								</cover-view>
								<cover-view class="btn-word">{{pusher.enableCamera?'关闭摄像头':'打开摄像头'}}</cover-view>
							</cover-view>
							<!-- 挂断 -->
							<cover-view class="box-wrap">
								<cover-view class="btn-hangup" @click="hangUpFun"><cover-image :src="overImg"></cover-image></cover-view>
								<cover-view class="btn-word">挂断</cover-view>
							</cover-view>
							<!-- 静音 -->
							<!-- <cover-view class="btn-normal" bindtap="toggleSoundModeFun">
									<cover-image
										:src="
											streamList[0].soundMode === 'ear'
												? audioImg
												: videoImg
										"
								></cover-image>
								</view> -->
							<!-- 翻转镜头 -->
							<cover-view class="box-wrap" v-if="streamList.length != 0">
								<cover-view class="btn-normal" @click="switchCamera"><cover-image :src="turnCamera"></cover-image></cover-view>
								<cover-view class="btn-word">翻转镜头</cover-view>
							</cover-view>
						</cover-view>
						<view class="bottom-btns">
							
						</view>
					</view>
				</view>
			</view>
		</view>
	</view>
</template>

<script>
import overImg from '@/static/img/挂断.png'
import turnCamera from '@/static/img/翻转镜头.png'
import audioImg from '@/static/img/电话.png'
import videoImg from '@/static/img/摄像头.png'
import closeVideoImg from '@/static/img/关闭摄像头.png'
import { setData } from './debug/GenerateTestUserSig';
import UserController from './controller/user-controller';
import Pusher from './model/pusher';
import { EVENT } from './common/constants';
import Event from './utils/event';
import * as ENV from './utils/environment';
const TAG_NAME = 'TRTC-ROOM';
export default {
	data() {
		return {
			//几个图片
			overImg,
			turnCamera,
			audioImg,
			videoImg,
			closeVideoImg,
			//控制视频按钮显隐
			videoBtnsVisible:true,
			//自己写一个计时功能
			courentTime:0,
			videoOrVoiceObj:{
			//计时器
				msgIPtime:{
					timer: "",
					content: "",
					hour: 0,
					minutes: 0,
					seconds: 0,
					longTime:'00:00:00'
				}
			},
			pusher: null,
			// debugMode: false, // 是否开启调试模式
			debugPanel: true,
			// 是否打开组件调试面板
			debug: false,
			// 是否打开player pusher 的调试信息
			streamList: [],
			// 用于渲染player列表,存储stram
			userList: [],
			// 扁平化的数据用来返回给用户
			template: '',
			// 不能设置默认值,当默认值和传入组件的值不一致时,iOS渲染失败
			cameraPosition: '',
			panelName: '',
			// 控制面板名称,包括 setting-panel  memberlist-panel
			localVolume: 0,
			remoteVolumeList: [],
			appVersion: ENV.APP_VERSION,
			libVersion: ENV.LIB_VERSION,
			debugMode: ''
		};
	},

	components: {},
	props: {
		// 必要的初始化参数
		config: {
			type: Object,
			default: () => ({
				sdkAppID: '',
				userID: '',
				userSig: '',
				template: '',
				debugMode: ''
			})
		}
	},
	watch: {
		streamList(newVal, oldVal){
			console.log('streamList::::::::',newVal, oldVal)
			this.streamList = newVal
		},
		config: {
			handler: function(newVal, oldVal) {
				console.log('watch config');
				this.propertyObserverFun({
					name: 'config',
					newVal,
					oldVal
				});
			},
			deep: true
		}
	},
	created: function() {
		// 在组件实例刚刚被创建时执行
		console.log(TAG_NAME,'组建内部生命周期created', ENV);
	},
	beforeMount: function() {
		// 在组件实例进入页面节点树时执行
		console.log(TAG_NAME, '组建内部生命周期beforeMount  attached');
		this.initFun();
	},
	mounted: function() {
		// 在组件在视图层布局完成后执行
		console.log(TAG_NAME, '组建内部生命周期Mounted  ready');
	},
	destroyed: function() {
		// 在组件实例被从页面节点树移除时执行
		console.log(TAG_NAME, '组建内部生命周期destroyed  detached'); // 停止所有拉流,并重置数据
		//退房
		this.exitRoom();
	},
	error: function(error) {
		// 每当组件方法抛出错误时执行
		console.log(TAG_NAME, '组建方法报错  error', error);
	},
	onPageShow: function() {
		// 组件所在的页面被展示时执行
		console.log(TAG_NAME, '组建所在页面展示  show status:', this.status);

		if (this.status.isPending) {
			// 经历了 5000 挂起事件
			this.status.isPending = false;
		}

		if (this.status.isPush) {
			// 小程序hide - show 有一定概率本地黑屏或静止,远端正常,或者远端和本地同时黑屏或静止,需要手动启动预览,非必现
			// this.data.pusher.getPusherContext().startPreview()
			// this.data.pusher.getPusherContext().resume()
		}
	},
	onPageHide: function() {
		// 组件所在的页面被隐藏时执行
		console.log(TAG_NAME, 'hide');
	},
	onPageResize: function(size) {
		// 组件所在的页面尺寸变化时执行
		console.log(TAG_NAME, 'resize', size);
	},
	methods: {
		setData,
		//隐藏
		hiddenBtns(){
			console.log('隐藏')
			this.videoBtnsVisible = false
		},
		//显示
		refreshBtns(){
			console.log('显示')
			this.videoBtnsVisible = true
		},
		//我写的计时功能
		initTime() {
			console.log('不执行吗?')
			/*时间处理*/
			this.timer = setInterval(() => {
				this.courentTime = new Date().getTime();
			}, 1000);
		},
		//通话时长
		ipVoiceOrVideoLongTimeClick(){
			this.videoOrVoiceObj.msgIPtime.timer = setInterval(this.startMsgIPtimeTimer, 1000);
		},
		startMsgIPtimeTimer () {
			this.videoOrVoiceObj.msgIPtime.seconds += 1;
			if ( this.videoOrVoiceObj.msgIPtime.seconds >= 60) {
				this.videoOrVoiceObj.msgIPtime.seconds = 0;
				this.videoOrVoiceObj.msgIPtime.minutes += 1;
			}
			if ( this.videoOrVoiceObj.msgIPtime.minutes >= 60) {
				this.videoOrVoiceObj.msgIPtime.minutes = 0;
				this.videoOrVoiceObj.msgIPtime.hour =  this.videoOrVoiceObj.msgIPtime.hour + 1;
			}
		this.videoOrVoiceObj.msgIPtime.longTime =( this.videoOrVoiceObj.msgIPtime.hour < 10 ? '0' +  this.videoOrVoiceObj.msgIPtime.hour :  this.videoOrVoiceObj.msgIPtime.hour) + ':'+ ( this.videoOrVoiceObj.msgIPtime.minutes < 10 ? '0' +  this.videoOrVoiceObj.msgIPtime.minutes :  this.videoOrVoiceObj.msgIPtime.minutes) + ':' + ( this.videoOrVoiceObj.msgIPtime.seconds < 10 ? '0' +  this.videoOrVoiceObj.msgIPtime.seconds :  this.videoOrVoiceObj.msgIPtime.seconds);
		},
		resetMsgIPtime(){  //重置
			clearInterval(this.videoOrVoiceObj.msgIPtime.timer);
			this.videoOrVoiceObj.msgIPtime.hour=0;
			this.videoOrVoiceObj.msgIPtime.minute=0;
			this.videoOrVoiceObj.msgIPtime.ms=0;
			this.videoOrVoiceObj.msgIPtime.seconds=0;
			this.videoOrVoiceObj.msgIPtime.longTime="00:00:00";
		},
		stopMsgIPtime(){  //暂停
			clearInterval(this.videoOrVoiceObj.msgIPtime.timer);
		},
		/**
		 * 初始化各项参数和用户控制模块,在组件实例触发 attached 时调用,此时不建议对View进行变更渲染(调用setData方法)
		 */
		initFun: function() {
			console.log(TAG_NAME, '初始化各参数  _init');
			this.userController = new UserController(this);
			this._emitter = new Event();
			this.EVENT = EVENT;
			//初始化推流拉流状态
			this.initStatusFun();
			//远端用户和音视频状态处理
			this.bindEventFun();
			//grid布局, 绑定事件
			this.bindEventGridFun();
			console.log(TAG_NAME, '初始化参数成功 _init success component:', this);
		},

		/**
		 * 进房
		 * @param {Object} params 必传 roomID 取值范围 1 ~ 4294967295
		 * @returns {Promise}
		 */
		enterRoom: function(params) {
			return new Promise((resolve, reject) => {
				// this.pusher = {}
				console.log(TAG_NAME, 'enterRoom');
				console.log(TAG_NAME, 'params', params);
				console.log(TAG_NAME, 'config', this.config);
				console.log(TAG_NAME, 'pusher', this.pusher); // 1. 补齐进房参数,校验必要参数是否齐全
				console.log('进房......', params, this.config, this.pusher);
				if (params) {
					Object.assign(this.pusher, params);
					Object.assign(this.config, params);
				}
				if (!this.checkParamFun(this.config)) {
					reject(new Error('缺少必要参数'));
					return;
				} // 2. 根据参数拼接 push url,赋值给 live-pusher,
				console.log('解决',this.config)
				this.getPushUrlFun(this.config)
					.then(pushUrl => {
						this.pusher.url = pushUrl;
						this.setData(
							{
								pusher: this.pusher,
							},
							() => {
								console.log(TAG_NAME, '进房成功', this.pusher); // view 渲染成功回调后,开始推流
								this.pusher.getPusherContext().start();
								this.status.isPush = true;
								resolve();
							}
						);
					})
					.catch(res => {
						// 获取 room sig 失败, 进房失败需要通过 pusher state 事件通知
						console.error(TAG_NAME, '进房失败', res);
						reject(res);
					});
			});
		},

		/**
		 * 退房,停止推流和拉流,并重置数据
		 * @returns {Promise}
		 */
		exitRoom: function() {
			return new Promise((resolve, reject) => {
				console.log(TAG_NAME, '退出房间');
				this.pusher.reset();
				this.status.isPush = false;
				const result = this.userController.reset();
				this.setData(
					{
						pusher: this.pusher,
						userList: result.userList,
						streamList: result.streamList
					},
					() => {
						// 在销毁页面时调用,不会走到这里
						resolve({
							userList: this.userList,
							streamList: this.streamList
						});
						console.log(TAG_NAME, '退出房间成功', this.pusher, this.streamList, this.userList);
					}
				);
			});
		},

		/**
		 * 开启摄像头
		 * @returns {Promise}
		 */
		publishLocalVideo: function() {
			// 设置 pusher enableCamera
			console.log(TAG_NAME, '这里这里publishLocalVideo 开启摄像头',this.pusher,this.cameraPosition);
			return this.setPusherConfigFun({
				enableCamera: true,
			});
		},

		/**
		 * 关闭摄像头
		 * @returns {Promise}
		 */
		unpublishLocalVideo: function() {
			// 设置 pusher enableCamera
			console.log(TAG_NAME, 'unpublshLocalVideo 关闭摄像头');
			return this.setPusherConfigFun({
				enableCamera: false
			});
		},

		/**
		 * 开启麦克风
		 * @returns {Promise}
		 */
		publishLocalAudio: function() {
			// 设置 pusher enableCamera
			console.log(TAG_NAME, 'publishLocalAudio 开启麦克风');
			return this.setPusherConfigFun({
				enableMic: true
			});
		},

		/**
		 * 关闭麦克风
		 * @returns {Promise}
		 */
		unpublishLocalAudio: function() {
			// 设置 pusher enableCamera
			console.log(TAG_NAME, 'unpublshLocalAudio 关闭麦克风');
			return this.setPusherConfigFun({
				enableMic: false
			});
		},

		/**
		 * 订阅远端视频 主流 小画面 辅流
		 * @param {Object} params {userID,streamType} streamType 传入 small 时修改对应的主流url的 streamtype 参数为small
		 * @returns {Promise}
		 */
		subscribeRemoteVideo(params) {
			console.log(TAG_NAME, '订阅远端视频', params); // 设置指定 user streamType 的 muteVideo 为 false

			const config = {
				muteVideo: false
			}; // 本地数据结构里的 streamType 只支持 main 和 aux ,订阅small 也是对main进行处理

			const streamType = params.streamType === 'small' ? 'main' : params.streamType;

			if (params.streamType === 'small' || params.streamType === 'main') {
				const stream = this.userController.getStream({
					userID: params.userID,
					streamType: streamType
				});

				if (stream && stream.streamType === 'main') {
					console.log(TAG_NAME, 'subscribeRemoteVideo switch small', stream.src);

					if (params.streamType === 'small') {
						config.src = stream.src.replace('main', 'small');
						config._definitionType = 'small'; // 用于设置面板的渲染
					} else if (params.streamType === 'main') {
						stream.src = stream.src.replace('small', 'main');
						config._definitionType = 'main';
					}

					console.log(TAG_NAME, 'subscribeRemoteVideo', stream.src);
				}
			}

			return this.setPlayerConfigFun({
				userID: params.userID,
				streamType: streamType,
				config: config
			});
		},

		/**
		 * 取消订阅远端视频
		 * @param {Object} params {userID,streamType}
		 * @returns {Promise}
		 */
		unsubscribeRemoteVideo(params) {
			console.log(TAG_NAME, '取消订阅远端视频', params); // 设置指定 user streamType 的 muteVideo 为 true

			return this.setPlayerConfigFun({
				userID: params.userID,
				streamType: params.streamType,
				config: {
					muteVideo: true
				}
			});
		},

		/**
		 * 订阅远端音频
		 * @param {Object} params userID 用户ID
		 * @returns {Promise}
		 */
		subscribeRemoteAudio(params) {
			console.log(TAG_NAME, '订阅远端音频', params);
			return this.setPlayerConfigFun({
				userID: params.userID,
				streamType: 'main',
				config: {
					muteAudio: false
				}
			});
		},

		/**
		 * 取消订阅远端音频
		 * @param {Object} params userID 用户ID
		 * @returns {Promise}
		 */
		unsubscribeRemoteAudio(params) {
			console.log(TAG_NAME, '取消订阅远端音频', params);
			return this.setPlayerConfigFun({
				userID: params.userID,
				streamType: 'main',
				config: {
					muteAudio: true
				}
			});
		},

		on: function(eventCode, handler, context) {
			this._emitter.on(eventCode, handler, context);
		},
		off: function(eventCode, handler) {
			this._emitter.off(eventCode, handler);
		},
		getRemoteUserList: function() {
			return this.userList;
		},

		/**
		 * 切换前后摄像头
		 */
		switchCamera: function(val) {
			console.log('点击了翻转镜头',val)
			if (!this.cameraPosition) {
				// this.data.pusher.cameraPosition 是初始值,不支持动态设置
				this.cameraPosition = this.pusher.frontCamera;
			}

			console.log(TAG_NAME, '翻转镜头', this.cameraPosition);
			this.cameraPosition = this.cameraPosition === 'front' ? 'back' : 'front';
			this.setData(
				{
					cameraPosition: this.cameraPosition
				},
				() => {
					console.log(TAG_NAME, '翻转镜头 成功', this.cameraPosition);
				}
			); // wx 7.0.9 不支持动态设置 pusher.devicePosition ,需要调用api设置,这里修改cameraPosition是为了记录状态

			this.pusher.getPusherContext().switchCamera();
		},

		/**
		 * 设置指定player view的渲染坐标和尺寸
		 * @param {object} params
		 * userID: string
		 * streamType: string
		 * xAxis: number
		 * yAxis: number
		 * width: number
		 * height: number
		 * @returns {Promise}
		 */
		setViewRect: function(params) {
			console.log(TAG_NAME, 'setViewRect', params);

			if (this.pusher.template !== 'custom') {
				console.warn(`如需使用setViewRect方法,请设置template:"custom", 当前 template:"${this.pusher.template}"`);
			}

			if (this.pusher.userID === params.userID) {
				return this.setPusherConfigFun({
					xAxis: params.xAxis,
					yAxis: params.yAxis,
					width: params.width,
					height: params.height
				});
			}

			return this.setPlayerConfigFun({
				userID: params.userID,
				streamType: params.streamType,
				config: {
					xAxis: params.xAxis,
					yAxis: params.yAxis,
					width: params.width,
					height: params.height
				}
			});
		},

		/**
		 * 设置指定 player 或者 pusher view 是否可见
		 * @param {object} params
		 * userID: string
		 * streamType: string
		 * isVisible:boolean
		 * @returns {Promise}
		 */
		setViewVisible: function(params) {
			console.log(TAG_NAME, 'setViewVisible', params); // if (this.data.pusher.template !== 'custom') {
			//   console.warn(`如需使用setViewVisible方法,请设置template:"custom", 当前 template:"${this.data.pusher.template}"`)
			// }

			if (this.pusher.userID === params.userID) {
				return this.setPusherConfigFun({
					isVisible: params.isVisible
				});
			}

			return this.setPlayerConfigFun({
				userID: params.userID,
				streamType: params.streamType,
				config: {
					isVisible: params.isVisible
				}
			});
		},

		/**
		 * 设置指定player view的层级
		 * @param {Object} params
		 * userID: string
		 * streamType: string
		 * zindex: number
		 * @returns {Promise}
		 */
		setViewZIndex: function(params) {
			console.log(TAG_NAME, 'setViewZIndex', params);

			if (this.pusher.template !== 'custom') {
				console.warn(`如需使用setViewZIndex方法,请设置template:"custom", 当前 template:"${this.pusher.template}"`);
			}

			if (this.pusher.userID === params.userID) {
				return this.setPusherConfigFun({
					zindex: params.zindex
				});
			}

			return this.setPlayerConfigFun({
				userID: params.userID,
				streamType: params.streamType,
				config: {
					zindex: params.zindex
				}
			});
		},

		/**
		 * 播放背景音
		 * @param {Object} params url
		 * @returns {Promise}
		 */
		playBGM: function(params) {
			return new Promise((resolve, reject) => {
				this.pusher.getPusherContext().playBGM({
					url: params.url,
					// 已经有相关事件不需要在这里监听,目前用于测试
					success: () => {
						console.log(TAG_NAME, '播放背景音成功'); // this._emitter.emit(EVENT.BGM_PLAY_START)

						resolve();
					},
					fail: () => {
						console.log(TAG_NAME, '播放背景音失败');

						this._emitter.emit(EVENT.BGM_PLAY_FAIL);

						reject(new Error('播放背景音失败'));
					} // complete: () => {
					//   console.log(TAG_NAME, '背景完成')
					//   this._emitter.emit(EVENT.BGM_PLAY_COMPLETE)
					// },
				});
			});
		},
		stopBGM: function() {
			this.pusher.getPusherContext().stopBGM();
		},
		pauseBGM: function() {
			this.pusher.getPusherContext().pauseBGM();
		},
		resumeBGM: function() {
			this.pusher.getPusherContext().resumeBGM();
		},

		/**
		 * 设置背景音音量
		 * @param {Object} params volume
		 */
		setBGMVolume: function(params) {
			this.pusher.getPusherContext().setBGMVolume({
				volume: params.volume
			});
		},

		/**
		 * 设置麦克风音量
		 * @param {Object} params volume
		 */
		setMICVolume: function(params) {
			this.pusher.getPusherContext().setMICVolume({
				volume: params.volume
			});
		},

		/**
		 * 发送SEI消息
		 * @param {Object} params message
		 * @returns {Promise}
		 */
		sendSEI: function(params) {
			return new Promise((resolve, reject) => {
				this.pusher.getPusherContext().sendMessage({
					msg: params.message,
					success: function(result) {
						resolve(result);
					}
				});
			});
		},

		/**
		 * pusher 和 player 的截图并保存
		 * @param {Object} params userID streamType
		 * @returns {Promise}
		 */
		snapshot: function(params) {
			console.log(TAG_NAME, 'snapshot', params);
			return new Promise((resolve, reject) => {
				this.captureSnapshot(params)
					.then(result => {
						wx.saveImageToPhotosAlbum({
							filePath: result.tempImagePath,

							success(res) {
								wx.showToast({
									title: '已保存到相册'
								});
								console.log('save photo is success', res);
								resolve(result);
							},

							fail: function(error) {
								wx.showToast({
									icon: 'none',
									title: '保存失败'
								});
								console.log('save photo is fail', error);
								reject(error);
							}
						});
					})
					.catch(error => {
						reject(error);
					});
			});
		},

		/**
		 * 获取pusher 和 player 的截图
		 * @param {Object} params userID streamType
		 * @returns {Promise}
		 */
		captureSnapshot: function(params) {
			return new Promise((resolve, reject) => {
				if (params.userID === this.pusher.userID) {
					// pusher
					this.pusher.getPusherContext().snapshot({
						quality: 'raw',
						complete: result => {
							console.log(TAG_NAME, 'snapshot pusher', result);

							if (result.tempImagePath) {
								resolve(result);
							} else {
								console.log('snapShot 回调失败', result);
								reject(new Error('截图失败'));
							}
						}
					});
				} else {
					// player
					this.userController.getStream(params).playerContext.snapshot({
						quality: 'raw',
						complete: result => {
							console.log(TAG_NAME, 'snapshot player', result);

							if (result.tempImagePath) {
								resolve(result);
							} else {
								console.log('snapShot 回调失败', result);
								reject(new Error('截图失败'));
							}
						}
					});
				}
			});
		},

		/**
		 * 将远端视频全屏
		 * @param {Object} params userID streamType direction
		 * @returns {Promise}
		 */
		enterFullscreen: function(params) {
			console.log(TAG_NAME, 'enterFullscreen', params);
			return new Promise((resolve, reject) => {
				this.userController.getStream(params).playerContext.requestFullScreen({
					direction: params.direction || 0,
					success: event => {
						console.log(TAG_NAME, 'enterFullscreen success', event);
						resolve(event);
					},
					fail: event => {
						console.log(TAG_NAME, 'enterFullscreen fail', event);
						reject(event);
					}
				});
			});
		},

		/**
		 * 将远端视频取消全屏
		 * @param {Object} params userID streamType
		 * @returns {Promise}
		 */
		exitFullscreen: function(params) {
			console.log(TAG_NAME, 'exitFullscreen', params);
			return new Promise((resolve, reject) => {
				this.userController.getStream(params).playerContext.exitFullScreen({
					success: event => {
						console.log(TAG_NAME, 'exitFullScreen success', event);
						resolve(event);
					},
					fail: event => {
						console.log(TAG_NAME, 'exitFullScreen fail', event);
						reject(event);
					}
				});
			});
		},

		/**
		 * 设置 player 视图的横竖屏显示
		 * @param {Object} params userID streamType orientation: vertical, horizontal
		 * @returns {Promise}
		 */
		setRemoteOrientation: function(params) {
			return this.setPlayerConfigFun({
				userID: params.userID,
				streamType: params.streamType,
				config: {
					orientation: params.orientation
				}
			});
		},
		// 改为:
		setViewOrientation: function(params) {
			return this.setPlayerConfigFun({
				userID: params.userID,
				streamType: params.streamType,
				config: {
					orientation: params.orientation
				}
			});
		},

		/**
		 * 设置 player 视图的填充模式
		 * @param {Object} params userID streamType fillMode: contain,fillCrop
		 * @returns {Promise}
		 */
		setRemoteFillMode: function(params) {
			return this.setPlayerConfigFun({
				userID: params.userID,
				streamType: params.streamType,
				config: {
					objectFit: params.fillMode
				}
			});
		},
		// 改为:
		setViewFillMode: function(params) {
			return this.setPlayerConfigFun({
				userID: params.userID,
				streamType: params.streamType,
				config: {
					objectFit: params.fillMode
				}
			});
		},

		/**
		 * 切换 player 大小画面
		 * @param {Object} params userID streamType definition: HD SD
		 * @returns {Promise}
		 */
		setRemoteDefinitionFun: function(params) {
			params.streamType = 'main';
			return new Promise((resolve, reject) => {
				const stream = this.userController.getStream({
					userID: params.userID,
					streamType: params.streamType
				});

				if (stream && stream.streamType === 'main') {
					console.log(TAG_NAME, '_switchStreamType', stream.src); // stream.volume = volume

					if (stream.src.indexOf('main') > -1) {
						stream.src = stream.src.replace('main', 'small');
						stream._streamType = 'small'; // 用于设置面板的渲染
					} else if (stream.src.indexOf('small') > -1) {
						stream.src = stream.src.replace('small', 'main');
						stream._streamType = 'main';
					}

					console.log(TAG_NAME, '_switchStreamType', stream.src);
					this.setData(
						{
							streamList: this.streamList
						},
						() => {}
					);
				}
			});
		},

		initStatusFun() {
			this.status = {
				isPush: false,
				// 推流状态
				isPending: false // 挂起状态,触发5000事件标记为true,onShow后标记为false
			};
			this._lastTapTime = 0;
			this._beforeLastTapTime = 0;
			this._isFullscreen = false;
		},

		/**
		 * 设置推流参数并触发页面渲染更新
		 * @param {Object} config live-pusher 的配置
		 * @returns {Promise}
		 */
		setPusherConfigFun(config) {
			console.log(TAG_NAME, '_setPusherConfig', config, this.pusher);
			return new Promise((resolve, reject) => {
				if (!this.pusher) {
					this.pusher = new Pusher(config);
				} else {
					Object.assign(this.pusher, config);
				}

				this.setData(
					{
						pusher: this.pusher
					},
					() => {
						// console.log(TAG_NAME, '_setPusherConfig setData compelete', 'config:', config, 'pusher:', this.data.pusher)
						resolve(config);
					}
				);
			});
		},

		/**
		 *
		 * @param {Object} params include userID,streamType,config
		 * @returns {Promise}
		 */
		setPlayerConfigFun(params) {
			const userID = params.userID;
			const streamType = params.streamType;
			const config = params.config;
			console.log(TAG_NAME, '_setPlayerConfig', params);
			return new Promise((resolve, reject) => {
				// 获取指定的userID streamType 的 stream
				const user = this.userController.getUser(userID);

				if (user && user.streams[streamType]) {
					user.streams[streamType] = Object.assign(user.streams[streamType], config); // user.streams引用的对象和 streamList 里的是同一个

					this.setData(
						{
							streamList: this.streamList
						},
						() => {
							// console.log(TAG_NAME, '_setPlayerConfig complete', params, 'streamList:', this.data.streamList)
							resolve(params);
						}
					);
				} else {
					// 不需要reject,静默处理
					console.warn(TAG_NAME, '指定 userID 或者 streamType 不存在'); // reject(new Error('指定 userID 或者 streamType 不存在'))
				}
			});
		},

		/**
		 * 必选参数检测
		 * @param {Object} rtcConfig rtc参数
		 * @returns {Boolean}
		 */
		checkParamFun: function(rtcConfig) {
			console.log(TAG_NAME, 'checkParam config:', rtcConfig);

			if (!rtcConfig.sdkAppID) {
				console.error('未设置 sdkAppID');
				return false;
			}

			if (rtcConfig.roomID === undefined) {
				console.error('未设置 roomID');
				return false;
			}

			if (rtcConfig.roomID < 1 || rtcConfig.roomID > 4294967296) {
				console.error('roomID 超出取值范围 1 ~ 4294967295');
				return false;
			}

			if (!rtcConfig.userID) {
				console.error('未设置 userID');
				return false;
			}

			if (!rtcConfig.userSig) {
				console.error('未设置 userSig');
				return false;
			}

			if (!rtcConfig.template) {
				console.error('未设置 template');
				return false;
			}

			return true;
		},
		getPushUrlFun: function(rtcConfig) {
			// 拼接 puhser url rtmp 方案
			console.log(TAG_NAME, 'getPushUrl', rtcConfig);

			if (ENV.IS_TRTC) {
				// 版本高于7.0.8,基础库版本高于2.10.0 使用新的 url
				return new Promise((resolve, reject) => {
					// appscene videocall live
					// cloudenv PRO CCC DEV UAT
					// encsmall 0
					// 对外的默认值是rtc ,对内的默认值是videocall
					rtcConfig.scene = !rtcConfig.scene || rtcConfig.scene === 'rtc' ? 'videocall' : 'live';
					rtcConfig.enableBlackStream = rtcConfig.enableBlackStream || 1;
					rtcConfig.encsmall = rtcConfig.encsmall || 0;
					rtcConfig.cloudenv = rtcConfig.cloudenv || 'PRO';
					setTimeout(() => {
						const pushUrl =
							'room://cloud.tencent.com/rtc?sdkappid=' +
							rtcConfig.sdkAppID +
							'&roomid=' +
							rtcConfig.roomID +
							'&userid=' +
							rtcConfig.userID +
							'&usersig=' +
							rtcConfig.userSig +
							'&appscene=' +
							rtcConfig.scene +
							'&encsmall=' +
							rtcConfig.encsmall +
							'&cloudenv=' +
							rtcConfig.cloudenv;
						console.log(TAG_NAME, 'getPushUrl result:', pushUrl);
						resolve(pushUrl);
					}, 0);
				});
			}

			return this.requestSigServerFun(rtcConfig);
		},

		/**
		 * 获取签名和推流地址
		 * @param {Object} rtcConfig 进房参数配置
		 * @returns {Promise}
		 */
		requestSigServerFun: function(rtcConfig) {
			console.log('requestSigServer:', rtcConfig);
			const sdkAppID = rtcConfig.sdkAppID;
			const userID = rtcConfig.userID;
			const userSig = rtcConfig.userSig;
			const roomID = rtcConfig.roomID;
			const privateMapKey = rtcConfig.privateMapKey;
			rtcConfig.useCloud = rtcConfig.useCloud === undefined ? true : rtcConfig.useCloud;
			let url = rtcConfig.useCloud ? 'https://official.opensso.tencent-cloud.com/v4/openim/jsonvideoapp' : 'https://yun.tim.qq.com/v4/openim/jsonvideoapp';
			url += '?sdkappid=' + sdkAppID + '&identifier=' + userID + '&usersig=' + userSig + '&random=' + Date.now() + '&contenttype=json';
			const reqHead = {
				Cmd: 1,
				SeqNo: 1,
				BusType: 7,
				GroupId: roomID
			};
			const reqBody = {
				PrivMapEncrypt: privateMapKey,
				TerminalType: 1,
				FromType: 3,
				SdkVersion: 26280566
			};
			console.log('requestSigServer:', url, reqHead, reqBody);
			return new Promise((resolve, reject) => {
				wx.request({
					url: url,
					data: {
						ReqHead: reqHead,
						ReqBody: reqBody
					},
					method: 'POST',
					success: res => {
						console.log('requestSigServer success:', res);

						if (res.data['ErrorCode'] || res.data['RspHead']['ErrorCode'] !== 0) {
							// console.error(res.data['ErrorInfo'] || res.data['RspHead']['ErrorInfo'])
							console.error('获取roomsig失败');
							reject(res);
						}

						const roomSig = JSON.stringify(res.data['RspBody']);
						let pushUrl = 'room://cloud.tencent.com?sdkappid=' + sdkAppID + '&roomid=' + roomID + '&userid=' + userID + '&roomsig=' + encodeURIComponent(roomSig); // TODO 需要重新整理的逻辑
						// 如果有配置纯音频推流或者recordId参数

						if (rtcConfig.pureAudioPushMod || rtcConfig.recordId) {
							const bizbuf = {
								Str_uc_params: {
									pure_audio_push_mod: 0,
									record_id: 0
								}
							}; // 纯音频推流

							if (rtcConfig.pureAudioPushMod) {
								bizbuf.Str_uc_params.pure_audio_push_mod = rtcConfig.pureAudioPushMod;
							} else {
								delete bizbuf.Str_uc_params.pure_audio_push_mod;
							} // 自动录制时业务自定义id

							if (rtcConfig.recordId) {
								bizbuf.Str_uc_params.record_id = rtcConfig.recordId;
							} else {
								delete bizbuf.Str_uc_params.record_id;
							}

							pushUrl += '&bizbuf=' + encodeURIComponent(JSON.stringify(bizbuf));
						}

						console.log('roomSigInfo', pushUrl);
						resolve(pushUrl);
					},
					fail: res => {
						console.log('requestSigServer fail:', res);
						reject(res);
					}
				});
			});
		},
		doubleTabToggleFullscreenFun: function(event) {
			const curTime = event.timeStamp;
			const lastTime = this._lastTapTime; // 已知问题:上次全屏操作后,必须等待1.5s后才能再次进行全屏操作,否则引发SDK全屏异常,因此增加节流逻辑

			const beforeLastTime = this._beforeLastTapTime;
			console.log(TAG_NAME, 'doubleTabToggleFullscreenFun', event, lastTime, beforeLastTime);

			if (curTime - lastTime > 0 && curTime - lastTime < 300 && lastTime - beforeLastTime > 1500) {
				const userID = event.currentTarget.dataset.userid;
				const streamType = event.currentTarget.dataset.streamtype;

				if (this._isFullscreen) {
					this.exitFullscreen({
						userID,
						streamType
					})
						.then(() => {
							this._isFullscreen = false;
						})
						.catch(() => {});
				} else {
					// const stream = this.userController.getStream({ userID, streamType })
					let direction; // // 已知问题:视频的尺寸需要等待player触发NetStatus事件才能获取到,如果进房就双击全屏,全屏后的方向有可能不对。
					// if (stream && stream.videoWidth && stream.videoHeight) {
					//   // 如果是横视频,全屏时进行横屏处理。如果是竖视频,则为0
					//   direction = stream.videoWidth > stream.videoHeight ? 90 : 0
					// }

					this.enterFullscreen({
						userID,
						streamType,
						direction
					})
						.then(() => {
							this._isFullscreen = true;
						})
						.catch(() => {});
				}

				this._beforeLastTapTime = lastTime;
			}

			this._lastTapTime = curTime;
		},

		/**
		 * TRTC-room 远端用户和音视频状态处理
		 */
		bindEventFun: function() {
			// 远端用户进房
			this.userController.on(EVENT.REMOTE_USER_JOIN, event => {
				console.log(TAG_NAME, '远端用户进房', event, event.data.userID);
				this.setData(
					{
						userList: event.data.userList
					},
					() => {
						this._emitter.emit(EVENT.REMOTE_USER_JOIN, {
							userID: event.data.userID
						});
					},this
				);
				console.log(TAG_NAME, 'REMOTE_USER_JOIN', 'streamList:', this.streamList, 'userList:', this.userList);
			}); // 远端用户离开

			this.userController.on(EVENT.REMOTE_USER_LEAVE, event => {
				console.log(TAG_NAME, '远端用户离开', event, event.data.userID);

				if (event.data.userID) {
					this.setData(
						{
							userList: event.data.userList,
							streamList: event.data.streamList
						},
						() => {
							this._emitter.emit(EVENT.REMOTE_USER_LEAVE, {
								userID: event.data.userID
							});
						},this
					);
				}

				console.log(TAG_NAME, 'REMOTE_USER_LEAVE', 'streamList:', this.streamList, 'userList:', this.userList);
			}); // 视频状态 true

			this.userController.on(EVENT.REMOTE_VIDEO_ADD, event => {
				console.log(TAG_NAME, '远端视频可用', event, event.data.stream.userID);
				const stream = event.data.stream;
				this.setData(
					{
						userList: event.data.userList,
						streamList: event.data.streamList
					},
					() => {
						// 完善 的stream 的 playerContext
						stream.playerContext = wx.createLivePlayerContext(stream.streamID, this); // 新增的需要触发一次play 默认属性才能生效
						// stream.playerContext.play()
						// console.log(TAG_NAME, 'REMOTE_VIDEO_ADD playerContext.play()', stream)
						// TODO 视频通话模版默认订阅且显示

						this._emitter.emit(EVENT.REMOTE_VIDEO_ADD, {
							userID: stream.userID,
							streamType: stream.streamType
						});
					},this
				);
				console.log(TAG_NAME, 'REMOTE_VIDEO_ADD', 'streamList:', this.streamList, 'userList:', this.userList);
			}); // 视频状态 false

			this.userController.on(EVENT.REMOTE_VIDEO_REMOVE, event => {
				console.log(TAG_NAME, '远端视频移除', event, event.data.stream.userID);
				const stream = event.data.stream;
				this.setData(
					{
						userList: event.data.userList,
						streamList: event.data.streamList
					},
					() => {
						// 有可能先触发了退房事件,用户名下的所有stream都已清除
						if (stream.userID && stream.streamType) {
							this._emitter.emit(EVENT.REMOTE_VIDEO_REMOVE, {
								userID: stream.userID,
								streamType: stream.streamType
							});
						}
					},this
				);
				console.log(TAG_NAME, 'REMOTE_VIDEO_REMOVE', 'streamList:', this.streamList, 'userList:', this.userList);
			}); // 音频可用

			this.userController.on(EVENT.REMOTE_AUDIO_ADD, event => {
				console.log(TAG_NAME, '远端音频可用', event);
				const stream = event.data.stream;
				this.setData(
					{
						userList: event.data.userList,
						streamList: event.data.streamList
					},
					() => {
						stream.playerContext = wx.createLivePlayerContext(stream.streamID, this); // 新增的需要触发一次play 默认属性才能生效
						// stream.playerContext.play()
						// console.log(TAG_NAME, 'REMOTE_AUDIO_ADD playerContext.play()', stream)

						this._emitter.emit(EVENT.REMOTE_AUDIO_ADD, {
							userID: stream.userID,
							streamType: stream.streamType
						});
					},this
				);
				console.log(TAG_NAME, 'REMOTE_AUDIO_ADD', 'streamList:', this.streamList, 'userList:', this.userList);
			}); // 音频不可用

			this.userController.on(EVENT.REMOTE_AUDIO_REMOVE, event => {
				console.log(TAG_NAME, '远端音频移除', event, event.data.stream.userID);
				const stream = event.data.stream;
				this.setData(
					{
						userList: event.data.userList,
						streamList: event.data.streamList
					},
					() => {
						// 有可能先触发了退房事件,用户名下的所有stream都已清除
						if (stream.userID && stream.streamType) {
							this._emitter.emit(EVENT.REMOTE_AUDIO_REMOVE, {
								userID: stream.userID,
								streamType: stream.streamType
							});
						}
					},this
				);
				console.log(TAG_NAME, 'REMOTE_AUDIO_REMOVE', 'streamList:', this.streamList, 'userList:', this.userList);
			});
		},

		/**
		 * pusher event handler
		 * @param {*} event 事件实例
		 */
		pusherStateChangeHandlerFun: function(event) {
			const code = event.detail.code;
			const message = event.detail.message;
			console.log(TAG_NAME, 'pusherStateChange:', code, event);

			switch (code) {
				case 0:
					console.log(message, code);
					break;

				case 1001:
					console.log('已经连接推流服务器', code);
					break;

				case 1002:
					console.log('已经与服务器握手完毕,开始推流', code);
					break;

				case 1003:
					console.log('打开摄像头成功', code);
					break;

				case 1004:
					console.log('录屏启动成功', code);
					break;

				case 1005:
					console.log('推流动态调整分辨率', code);
					break;

				case 1006:
					console.log('推流动态调整码率', code);
					break;

				case 1007:
					console.log('首帧画面采集完成', code);
					break;

				case 1008:
					console.log('编码器启动', code);
					break;

				case 1018:
					console.log('进房成功', code);
					this._emitter.emit(EVENT.LOCAL_JOIN, {
						userID: this.pusher.userID
					});
					//进房成功  手动默认关闭摄像头  转后置
					this.switchCamera()
					this.unpublishLocalVideo()
					// //计时功能
					this.initTime()
					this.ipVoiceOrVideoLongTimeClick()

					break;

				case 1019:
					console.log('退出房间', code);

					this._emitter.emit(EVENT.LOCAL_LEAVE, {
						userID: this.pusher.userID
					});

					break;

				case 2003:
					console.log('渲染首帧视频', code);
					break;

				case 1020:
				case 1031:
				case 1032:
				case 1033:
				case 1034:
					// 通过 userController 处理 1020 1031 1032 1033 1034
					this.userController.userEventHandler(event);
					break;

				case -1301:
					console.error('打开摄像头失败: ', code);

					this._emitter.emit(EVENT.ERROR, {
						code,
						message
					});

					break;

				case -1302:
					console.error('打开麦克风失败: ', code);

					this._emitter.emit(EVENT.ERROR, {
						code,
						message
					});

					break;

				case -1303:
					console.error('视频编码失败: ', code);

					this._emitter.emit(EVENT.ERROR, {
						code,
						message
					});

					break;

				case -1304:
					console.error('音频编码失败: ', code);

					this._emitter.emit(EVENT.ERROR, {
						code,
						message
					});

					break;

				case -1307:
					console.error('推流连接断开: ', code);

					this._emitter.emit(EVENT.ERROR, {
						code,
						message
					});

					break;

				case -100018:
					console.error('进房失败: ', code, message);

					this._emitter.emit(EVENT.ERROR, {
						code,
						message
					});

					break;

				case 5000:
					console.log('小程序被挂起: ', code); // 终端 sdk 建议执行退房操作,唤起时重新进房,临时解决方案,待小程序SDK完全实现自动重新推流后可以去掉

					this.status.isPending = true;

					if (this.status.isPush) {
						// this.exitRoom()
						const tempUrl = this.pusher.url;
						this.pusher.url = ''; // console.log('5000 小程序被挂起后更换pusher', this.data.pusher.getPusherContext().webviewId)

						this.setData(
							{
								pusher: this.pusher
							},
							() => {
								this.pusher.url = tempUrl;
								this.setData(
									{
										pusher: this.pusher
									},
									() => {
										this.pusher.getPusherContext().start();
										console.log('5000 小程序被挂起后更换pusher', this.pusher);
									}
								);
							}
						);
					}

					break;

				case 1021:
					console.log('网络类型发生变化,需要重新进房', code);
					break;

				case 2007:
					console.log('本地视频播放loading: ', code);
					break;

				case 2004:
					console.log('本地视频播放开始: ', code);
					break;

				default:
					console.log(message, code);
			}

			this._emitter.emit(EVENT.LOCAL_STATE_UPDATE, {
				data: event
			});
		},
		pusherNetStatusHandlerFun: function(event) {
			// 触发 LOCAL_NET_STATE_UPDATE
			this._emitter.emit(EVENT.LOCAL_NET_STATE_UPDATE, event);
		},
		pusherErrorHandlerFun: function(event) {
			// 触发 ERROR
			console.warn(TAG_NAME, 'pusher error', event);

			try {
				const code = event.detail.errCode;
				const message = event.detail.errMsg;

				this._emitter.emit(EVENT.ERROR, {
					code,
					message
				});
			} catch (exception) {
				console.error(TAG_NAME, 'pusher error data parser exception', event, exception);
			}
		},
		pusherBGMStartHandlerFun: function(event) {
			// 触发 BGM_START 已经在playBGM方法中进行处理
			// this._emitter.emit(EVENT.BGM_PLAY_START, { data: event })
		},
		pusherBGMProgressHandlerFun: function(event) {
			// BGM_PROGRESS
			this._emitter.emit(EVENT.BGM_PLAY_PROGRESS, event);
		},
		pusherBGMCompleteHandlerFun: function(event) {
			// BGM_COMPLETE
			this._emitter.emit(EVENT.BGM_PLAY_COMPLETE, event);
		},
		// player event handler
		// 获取 player ID 再进行触发
		playerStateChangeFun: function(event) {
			// console.log(TAG_NAME, 'playerStateChangeFun', event)
			this._emitter.emit(EVENT.REMOTE_STATE_UPDATE, event);
		},
		playerFullscreenChangeFun: function(event) {
			// console.log(TAG_NAME, '_playerFullscreenChange', event)
			this._emitter.emit(EVENT.REMOTE_NET_STATE_UPDATE, event);
		},
		playerNetStatusFun: function(event) {
			// console.log(TAG_NAME, 'playerNetStatusFun', event)
			// 获取player 视频的宽高
			const stream = this.userController.getStream({
				userID: event.currentTarget.dataset.userid,
				streamType: event.currentTarget.dataset.streamtype
			});

			if (stream && (stream.videoWidth !== event.detail.info.videoWidth || stream.videoHeight !== event.detail.info.videoHeight)) {
				console.log(TAG_NAME, 'playerNetStatusFun update video size', event);
				stream.videoWidth = event.detail.info.videoWidth;
				stream.videoHeight = event.detail.info.videoHeight;
			}

			this._emitter.emit(EVENT.REMOTE_FULLSCREEN_UPDATE, event);
		},
		playerAudioVolumeNotifyFun: function(event) {
			// console.log(TAG_NAME, 'playerAudioVolumeNotifyFun', event)
			this._emitter.emit(EVENT.REMOTE_AUDIO_VOLUME_UPDATE, event);
		},

		/**
		 * 监听组件属性变更,外部变更组件属性时触发该监听,用于检查属性设置是否正常
		 * @param {Object} data 变更数据
		 */
		propertyObserverFun: function(data) {
			console.log(TAG_NAME, '_propertyObserver', data, this.config);

			if (data.name === 'config') {
				// const config = Object.assign(DEFAULT_PUSHER_CONFIG, data.newVal)
				const config = data.newVal; // querystring 只支持String类型,做一个类型防御

				if (typeof config.debugMode === 'string') {
					config.debugMode === 'true' ? true : false;
				} // 独立设置与pusher无关的配置

				this.setData({
					template: config.template,
					debugMode: config.debugMode || false,
					debug: config.debugMode || false
				});
				this.setPusherConfigFun(config);
			}
		},

		toggleVideoFun() {
			if (this.pusher.enableCamera) {
				this.unpublishLocalVideo();
			} else {
				this.publishLocalVideo();
			}
		},

		toggleAudioFun() {
			if (this.pusher.enableMic) {
				this.unpublishLocalAudio();
			} else {
				this.publishLocalAudio();
			}
		},

		debugToggleRemoteVideoFun(event) {
			console.log(TAG_NAME, '_debugToggleRemoteVideo', event.currentTarget.dataset);
			const userID = event.currentTarget.dataset.userID;
			const streamType = event.currentTarget.dataset.streamType;
			const stream = this.streamList.find(item => {
				return item.userID === userID && item.streamType === streamType;
			});

			if (stream.muteVideo) {
				this.subscribeRemoteVideo({
					userID,
					streamType
				});
				this.setViewVisible({
					userID,
					streamType,
					isVisible: true
				});
			} else {
				this.unsubscribeRemoteVideo({
					userID,
					streamType
				});
				this.setViewVisible({
					userID,
					streamType,
					isVisible: false
				});
			}
		},

		debugToggleRemoteAudioFun(event) {
			console.log(TAG_NAME, '_debugToggleRemoteAudio', event.currentTarget.dataset);
			const userID = event.currentTarget.dataset.userID;
			const streamType = event.currentTarget.dataset.streamType;
			const stream = this.streamList.find(item => {
				return item.userID === userID && item.streamType === streamType;
			});

			if (stream.muteAudio) {
				this.subscribeRemoteAudio({
					userID
				});
			} else {
				this.unsubscribeRemoteAudio({
					userID
				});
			}
		},

		debugToggleVideoDebugFun() {
			this.setData({
				debug: !this.debug
			});
		},

		debugExitRoomFun() {
			this.exitRoom();
		},

		debugEnterRoomFun() {
			this.publishLocalVideo();
			this.publishLocalAudio();
			this.enterRoom({
				roomID: this.config.roomID
			}).then(() => {
				// 进房后开始推送视频或音频
			});
		},

		debugGoBackFun() {
			wx.navigateBack({
				delta: 1
			});
		},

		debugTogglePanelFun() {
			this.setData({
				debugPanel: !this.debugPanel
			});
		},

		toggleAudioVolumeTypeFun() {
			if (this.pusher.audioVolumeType === 'voicecall') {
				this.setPusherConfigFun({
					audioVolumeType: 'media'
				});
			} else {
				this.setPusherConfigFun({
					audioVolumeType: 'voicecall'
				});
			}
		},

		toggleSoundModeFun(val) {
			console.log('点击了切换语音/视频',val)
			if (this.userList.length === 0) {
				return;
			}

			const stream = this.userController.getStream({
				userID: this.userList[0].userID,
				streamType: 'main'
			});

			if (stream) {
				if (stream.soundMode === 'speaker') {
					stream['soundMode'] = 'ear';
				} else {
					stream['soundMode'] = 'speaker';
				}

				this.setPlayerConfigFun({
					userID: stream.userID,
					streamType: 'main',
					config: {
						soundMode: stream['soundMode']
					}
				});
			}
		},

		/**
		 * 退出通话
		 */
		hangUpFun: function() {
			console.log('点击了挂断')
			// this.exitRoom();
			uni.reLaunch({
				url: '/pages/callOver/index',
				fail: e=>{
					uni.switchTab({
						url:'/pages/callOver/index'
					})
				}
			})
			setTimeout(()=>{
				this.exitRoom();
				//停止定时器,重置计时时间
				this.resetMsgIPtime()
				this.stopMsgIPtime()
			},300)
			// uni.navigateBack({
			// 	delta: 1
			// });
		},

		/**
		 * 切换订阅音频状态
		 */
		handleSubscribeAudio: function() {
			if (this.pusher.enableMic) {
				this.unpublishLocalAudio();
			} else {
				this.publishLocalAudio();
			}
		},

		/**
		 * 切换订阅远端视频状态
		 * @param event
		 */
		handleSubscribeRemoteVideoFun: function(event) {
			const userID = event.currentTarget.dataset.userID;
			const streamType = event.currentTarget.dataset.streamType;
			const stream = this.streamList.find(item => {
				return item.userID === userID && item.streamType === streamType;
			});

			if (stream.muteVideo) {
				this.subscribeRemoteVideo({
					userID,
					streamType
				});
			} else {
				this.unsubscribeRemoteVideo({
					userID,
					streamType
				});
			}
		},

		/**
		 * 将远端视频取消全屏
		 * @param event
		 */
		handleSubscribeRemoteAudioFun: function(event) {
			const userID = event.currentTarget.dataset.userID;
			const streamType = event.currentTarget.dataset.streamType;
			const stream = this.streamList.find(item => {
				return item.userID === userID && item.streamType === streamType;
			});

			if (stream.muteAudio) {
				this.subscribeRemoteAudio({
					userID
				});
			} else {
				this.unsubscribeRemoteAudio({
					userID
				});
			}
		},

		/**
		 * grid布局, 唤起 memberlist-panel
		 */
		switchMemberListPanelFun() {
			this.setData({
				panelName: this.panelName !== 'memberlist-panel' ? 'memberlist-panel' : ''
			});
		},

		/**
		 * grid布局, 唤起setting-panel
		 */
		switchSettingPanelFun() {
			this.setData({
				panelName: this.panelName !== 'setting-panel' ? 'setting-panel' : ''
			});
		},

		handleMaskerClickFun() {
			this.setData({
				panelName: ''
			});
		},

		setPuserPropertyFun(event) {
			// console.log(TAG_NAME, '_setPuserProperty', event)
			const key = event.currentTarget.dataset.key;
			let value = event.currentTarget.dataset.value;
			const config = {};

			if (value === 'true') {
				value = true;
			} else if (value === 'false') {
				value = false;
			}

			if (typeof value === 'boolean') {
				config[key] = !this.pusher[key];
			} else if (typeof value === 'string' && value.indexOf('|') > 0) {
				value = value.split('|');

				if (this.pusher[key] === value[0]) {
					config[key] = value[1];
				} else {
					config[key] = value[0];
				}
			} // console.log(TAG_NAME, '_setPuserProperty', config)

			this.setPusherConfigFun(config);
		},

		setPlayerPropertyFun(event) {
			console.log(TAG_NAME, '_setPlayerProperty', event);
			const userID = event.currentTarget.dataset.userid;
			const streamType = event.currentTarget.dataset.streamtype;
			const key = event.currentTarget.dataset.key;
			let value = event.currentTarget.dataset.value;
			const stream = this.userController.getStream({
				userID: userID,
				streamType: streamType
			});

			if (!stream) {
				return;
			}

			const config = {};

			if (value === 'true') {
				value = true;
			} else if (value === 'false') {
				value = false;
			}

			if (typeof value === 'boolean') {
				config[key] = !stream[key];
			} else if (typeof value === 'string' && value.indexOf('|') > 0) {
				value = value.split('|');

				if (stream[key] === value[0]) {
					config[key] = value[1];
				} else {
					config[key] = value[0];
				}
			}

			console.log(TAG_NAME, '_setPlayerProperty', config);
			this.setPlayerConfigFun({
				userID,
				streamType,
				config
			});
		},

		switchStreamTypeFun(event) {
			const userID = event.currentTarget.dataset.userid;
			const streamType = event.currentTarget.dataset.streamtype;
			const stream = this.userController.getStream({
				userID: userID,
				streamType: streamType
			});

			if (stream && stream.streamType === 'main') {
				if (stream._definitionType === 'small') {
					this.subscribeRemoteVideo({
						userID,
						streamType: 'main'
					});
				} else {
					this.subscribeRemoteVideo({
						userID,
						streamType: 'small'
					});
				}
			}
		},

		handleSnapshotClickFun(event) {
			wx.showToast({
				title: '开始截屏',
				icon: 'none',
				duration: 1000
			});
			const userID = event.currentTarget.dataset.userid;
			const streamType = event.currentTarget.dataset.streamtype;
			this.snapshot({
				userID,
				streamType
			});
		},

		/**
		 * grid布局, 绑定事件
		 */
		bindEventGridFun() {
			// 远端音量变更
			this.on(EVENT.REMOTE_AUDIO_VOLUME_UPDATE, event => {
				const data = event.data;
				const userID = data.currentTarget.dataset.userid;
				const streamType = data.currentTarget.dataset.streamtype;
				const volume = data.detail.volume; // console.log(TAG_NAME, '远端音量变更', userID, streamType, volume)

				const stream = this.userController.getStream({
					userID: userID,
					streamType: streamType
				});

				if (stream) {
					stream.volume = volume;
				}

				this.setData(
					{
						streamList: this.streamList
					},
					() => {}
				);
			});
		},

		toggleFullscreenFun(event) {
			console.log(TAG_NAME, '_toggleFullscreen', event);
			const userID = event.currentTarget.dataset.userID;
			const streamType = event.currentTarget.dataset.streamType;

			if (this._isFullscreen) {
				this.exitFullscreen({
					userID,
					streamType
				})
					.then(() => {
						this._isFullscreen = false;
					})
					.catch(() => {});
			} else {
				// const stream = this.userController.getStream({ userID, streamType })
				const direction = 0; // 已知问题:视频的尺寸需要等待player触发NetStatus事件才能获取到,如果进房就双击全屏,全屏后的方向有可能不对。
				// if (stream && stream.videoWidth && stream.videoHeight) {
				//   // 如果是横视频,全屏时进行横屏处理。如果是竖视频,则为0
				//   direction = stream.videoWidth > stream.videoHeight ? 90 : 0
				// }

				this.enterFullscreen({
					userID,
					streamType,
					direction
				})
					.then(() => {
						this._isFullscreen = true;
					})
					.catch(() => {});
			}
		}
	}
};
</script>
<style>
@import './trtc-room.css';
</style>

4,这是使用的页面使用的方式(原生组件,所以有个元素层级默认最高,覆盖所有,得使用cover-view才能盖在上面)。一定要有ref,否则不能初始化方法,不能创建实例,还得有config相关配置

<trtc-room id="trtcComponent" :config.sync="playVidoe.input.rtcConfig" ref="trtcComponent"> </trtc-room>

4.1别忘了下载两个js到本地
用于测试研签,如果你有后端,签名和用户id就是后端提供给你。但你仍需要历练的setData方法
在这里插入图片描述

5,生命周期函数里面(等页面元素有了之后)
初始化

data(){
	return{
	callVideo:{
	    input:{
	      template: '1v1',
	      debugMode: false,
	      cloudenv: 'PRO',
	      callingMode:'video',  //通话模式  语音/视频  audio/video
	      cameraMode:'back',  //通话模式  前/后  front、after
	    }
	  },
	  playVidoe:{
	    input:{
	      rtcConfig: {
	        sdkAppID: xxxxxx, // 必要参数 开通实时音视频服务创建应用后分配的 sdkAppID
	        userID: 'test001', // 必要参数 用户 ID 可以由您的帐号系统指定
	        userSig: '"xxxxxx"', // 必要参数 身份签名,相当于登录密码的作用
	        template: '1v1', // 必要参数 组件模版,支持的值 1v1 grid custom ,注意:不支持动态修改, iOS 不支持 pusher 动态渲染
	      },
	    }
	  },
	}
}
async mounted() {
      // 获取 rtcroom 实例
      this.trtcComponent = this.$refs['trtcComponent']
      console.log('this.trtcComponent',this.trtcComponent)
      // 监听TRTC Room 关键事件
      this.methods('bindTRTCRoomEvent')
      //基本数据 this.callVideo.input  通话模式等
      Object.getOwnPropertyNames(this.callVideo.input).forEach(key => {

        if (this.callVideo.input[key] === 'true') {
          this.callVideo.input[key] = true;
        }
  
        if (this.callVideo.input[key] === 'false') {
          this.callVideo.input[key] = false;
        }
      });
      this.options = this.callVideo.input;
  },
然后我这里用了websocket,在create里面建立连接,等待推房间号给我,我进房
this.methods('initRtcConfig',{userID :'test001',roomID:this.curRoomId,template:'1v1'})
this.methods('enterRoomBefore')

6,事件
初始化并监控组件事件(如远端退房等)

//引入js
import { genTestUserSig } from '../../libs/debug/GenerateTestUserSig.js'
//初始化config
initRtcConfig(data){
    data.template = data.template
    data.roomID = data.roomID
    data.userID = data.userID
    console.log('* room enterRoom','走进来99999999', data)

    const Signature = genTestUserSig(data.userID)
    data.sdkAppID = Signature.sdkAppID
    data.userSig = Signature.userSig
    this.template = data.template


    this.playVidoe.input.rtcConfig = {
      sdkAppID: data.sdkAppID, // 您的实时音视频服务创建应用后分配的 sdkAppID
      userID: data.userID,
      roomID : data.roomID,
      userSig: data.userSig,
      template: data.template, // 1v1 grid custom
      debugMode: data.debugMode, // 非必要参数,打开组件的调试模式,开发调试时建议设置为 true
      beautyLevel: 9, // 默认开启美颜
      enableIM: false, // 可选,仅支持初始化设置(进房前设置),不支持动态修改
      audioVolumeType: data.audioVolumeType,
    }
  }
  async enterRoomBefore(){
    let isOk = await this.methods('checkDeviceAuthorize')
    if(isOk){
      //近房间
      let data = {
        roomID:this.playVidoe.input.rtcConfig.roomID,
        userID:this.playVidoe.input.rtcConfig.userID,
        template:this.callVideo.input.template,
        debugMode:this.callVideo.input.debugMode,
        cloudenv:this.callVideo.input.cloudenv,
      }
      this.methods('enterRoom',data)
    }else{
      console.log('出错')
    }
  }
  async checkDeviceAuthorize(){
    let isOk = false
    this.hasOpenDeviceAuthorizeModal = false
    await new Promise((resolve, reject) => {
      if (!uni.getSetting || !uni.getSetting()) {
        // 微信测试版 获取授权API异常,目前只能即使没授权也可以通过
        resolve()
      }
      uni.getSetting().then((result)=> {
        // console.log('getSetting', result[1])
        this.authorizeMic = result[1].authSetting['scope.record']
        this.authorizeCamera = result[1].authSetting['scope.camera']
        if (result[1].authSetting['scope.camera'] && result[1].authSetting['scope.record']) {
          // 授权成功
          isOk = true
          resolve()
        } else {
          // 没有授权,弹出授权窗口
          // 注意: uni.authorize 只有首次调用会弹框,之后调用只返回结果,如果没有授权需要自行弹框提示处理
          console.log('getSetting 没有授权,弹出授权窗口', result)
          uni.authorize({
            scope: 'scope.record',
          }).then((res)=>{
            console.log('authorize mic', res)
            this.authorizeMic = true
            if (this.authorizeCamera) {
              resolve()
            }
          }).catch((error)=>{
            console.log('authorize mic error', error)
            this.authorizeMic = false
          })
          uni.authorize({
            scope: 'scope.camera',
          }).then((res)=>{
            console.log('authorize camera', res)
            this.authorizeCamera = true
            if (this.authorizeMic) {
              resolve()
            } else {
              this.openConfirm()
              reject(new Error('authorize fail'))
            }
          }).catch((error)=>{
            console.log('authorize camera error', error)
            this.authorizeCamera = false
            this.openConfirm()
            reject(new Error('authorize fail'))
          })
        }
      })
    })
    return isOk
  }
  enterRoom(data) {
    this.trtcComponent.enterRoom({ roomID: data.roomID }).then(()=>{
      console.log('HDFH11111111111111111111111111111',data)
      if (this.template === '1v1') {
        // 设置推流端视窗的坐标和尺寸
        console.log('HDFH',data)
        this.trtcComponent.setViewRect({
          userID: data.userID,
          xAxis: '480rpx',
          yAxis: '160rpx',
          width: '240rpx',
          height: '320rpx',
        })
      }
    }).catch((res)=>{
    	//这里出现了一下奇怪的报错  连续拨通挂断,不懂是腾讯对签名有限制还是怎样,连续操作20几次会出错,进不去房间。。。。
    	这里需要加个检测失败的事件,失败跳出
      console.error('组建所在页面 进房失败:', res)
    })
  }

  bindTRTCRoomEvent() {
    const TRTC_EVENT = this.trtcComponent.EVENT
    this.timestamp = []
    // 初始化事件订阅
    this.trtcComponent.on(TRTC_EVENT.LOCAL_JOIN, (event)=>{
      // console.log('* room LOCAL_JOIN', event)
      // 进房成功,触发该事件后可以对本地视频和音频进行设置
      if (this.options.localVideo === true || this.options.template === '1v1') {
        this.trtcComponent.publishLocalVideo()
      }
      if (this.options.localAudio === true || this.options.template === '1v1') {
        this.trtcComponent.publishLocalAudio()
      }
      if (this.callVideo.input.template === 'custom') {
        this.trtcComponent.setViewRect({
          userID: event.userID,
          xAxis: '0rpx',
          yAxis: '0rpx',
          width: '240rpx',
          height: '320rpx',
        })
      }
    })
    this.trtcComponent.on(TRTC_EVENT.LOCAL_LEAVE, (event)=>{
      console.log('* room LOCAL_LEAVE', event)
    })
    this.trtcComponent.on(TRTC_EVENT.ERROR, (event)=>{
      console.log('* room ERROR', event)
    })
    // 远端用户进房
    this.trtcComponent.on(TRTC_EVENT.REMOTE_USER_JOIN, (event)=>{
      console.log('组件所在页面  远端用户进房', event, this.trtcComponent.getRemoteUserList())
      this.timestamp.push(new Date())
      // 1v1视频通话时限制人数为两人的简易逻辑,建议通过后端实现房间人数管理
      // 2人以上同时进行通话请选择网格布局
      if (this.template === '1v1' && this.timestamp.length > 1) {
        const interval = this.timestamp[1] - this.timestamp[0]
        if (interval < 1000) {
          // 房间里已经有两个人
          // this.setData({
          //   showTipToast: true,
          // }, () => {
          //   setTimeout(()=>{
          //     this.setData({
          //       showTipToast: false,
          //     })
              uni.navigateBack({
                delta: 1,
              })
          //   }, 4000)
          // })
        }
      }
    })
    // 远端用户退出
    this.trtcComponent.on(TRTC_EVENT.REMOTE_USER_LEAVE, (event)=>{
      console.log('组件所在页面  远端用户退出', event, this.trtcComponent.getRemoteUserList())
      //跳转到感谢致电页面
      this.methods('toPage','/pages/callOver/index')
      if (this.template === '1v1') {
        this.timestamp = []
      }
      if (this.template === '1v1' && this.remoteUser === event.data.userID) {
        this.remoteUser = null
      }
    })
    // 远端用户推送视频
    this.trtcComponent.on(TRTC_EVENT.REMOTE_VIDEO_ADD, (event)=>{
      console.log('组件所在页面  远端用户推送视频', event, this.template,this.remoteUser)
      // 订阅视频
      const userList = this.trtcComponent.getRemoteUserList()
      const data = event.data
      if (this.template === '1v1' && (!this.remoteUser || this.remoteUser === data.userID)) {
        // 1v1 只订阅第一个远端流
        this.remoteUser = data.userID;
        this.trtcComponent.subscribeRemoteVideo({
          userID: data.userID,
          streamType: data.streamType
        });
      } else if (this.template === 'grid') {
        this.trtcComponent.subscribeRemoteVideo({
          userID: data.userID,
          streamType: data.streamType
        });
      }
      if (this.template === 'custom' && data.userID && data.streamType) {
        let index = userList.findIndex((item)=>{
          return item.userID === data.userID
        })
        index++
        const y = 320 * index + 160
        // 设置远端视图坐标和尺寸
        this.trtcComponent.setViewRect({
          userID: data.userID,
          streamType: data.streamType,
          xAxis: '480rpx',
          yAxis: y + 'rpx',
          width: '240rpx',
          height: '320rpx',
        })
      }
    })
    // 远端用户取消推送视频
    this.trtcComponent.on(TRTC_EVENT.REMOTE_VIDEO_REMOVE, (event)=>{
      console.log('组件所在页面  远端用户取消推送视频', event, this.trtcComponent.getRemoteUserList())
    })
    // 远端用户推送音频
    this.trtcComponent.on(TRTC_EVENT.REMOTE_AUDIO_ADD, (event)=>{
      console.log('组件所在页面  远端用户推送音频', event, this.template,this.remoteUser )
      // 订阅音频
      const data = event.data
      if (this.template === '1v1' && (!this.remoteUser || this.remoteUser === data.userID)) {
        this.remoteUser = data.userID;
        this.trtcComponent.subscribeRemoteAudio({
          userID: data.userID
        });
      } else if (this.template === 'grid' || this.template === 'custom') {
        this.trtcComponent.subscribeRemoteAudio({
          userID: data.userID
        });
      } 
    })
    // 远端用户取消推送音频
    this.trtcComponent.on(TRTC_EVENT.REMOTE_AUDIO_REMOVE, (event)=>{
      console.log('组件所在页面  远端用户取消推送音频', event, this.trtcComponent.getRemoteUserList())
    })
    // this.trtcComponent.on(TRTC_EVENT.LOCAL_NET_STATE_UPDATE, (event)=>{
    //   console.log('* room LOCAL_NET_STATE_UPDATE', event)
    // })
    // this.trtcComponent.on(TRTC_EVENT.REMOTE_NET_STATE_UPDATE, (event)=>{
    //   console.log('* room REMOTE_NET_STATE_UPDATE', event)
    // })
    this.trtcComponent.on(TRTC_EVENT.IM_READY, (event)=>{
      console.log('* room IM_READY', event)
    })
    this.trtcComponent.on(TRTC_EVENT.IM_MESSAGE_RECEIVED, (event)=>{
      console.log('* room IM_MESSAGE_RECEIVED', event)
    })
  }

记得在微信公众平台打开视频推流拉流权限
在这里插入图片描述
无意间在博客看到一个广告,也不知道怎么样,有没有人用过声网的这个实时视频sdk
在这里插入图片描述

https://www.agora.io/cn/usecase,看起来和腾讯的很相似

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_小郑有点困了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值