uniappx 腾讯云1V1音视频通话 UTS 插件[Android版]

0 阅读3分钟

1V1语音通话流程

先要了解流程是怎样的,起初写的时候就像一只无头苍蝇 image.png 上图来自于腾讯云 TRTC云助手cloud.tencent.com/document/pr…

官方文档

1V1音视频通话场景通常需要依赖腾讯云 即时通信 IM实时音视频 TRTC 两项付费 PaaS 服务构建。

创建uts插件

整体目录结构

image.png

在app-android文件下创建config.json文件,添加远程依赖(doc.dcloud.net.cn/uni-app-x/p…

{
  "minSdkVersion": "21",
	"abis": [
	    "arm64-v8a","armeabi-v7a"
	],
	"dependencies": [
            "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.20",
            "com.tencent.liteav:LiteAVSDK_TRTC:12.6.0.18891",
            "com.tencent.imsdk:imsdk-plus:8.6.7019"
	]
}
鉴权凭证(cloud.tencent.com/document/pr…

正确的 UserSig 签发方式是将 UserSig 的计算代码集成到服务端,为了方便测试放在了客户端

image.png GenerateTestUserSig.java文件中SDKAPPI和SECRETKEY值 需要修改 (开通了在控制台查看)

在index.uts 中调用

import { GenerateTestUserSig } from "com.tencent.qcloud.tim.demo.signature";
// 获取签名 鉴权凭证
export function generateSign(userID:string):string{
  const signText  = GenerateTestUserSig.genTestUserSig(userID) // 静态方法应该通过类名直接调用
	return signText
}

如果这一步测试的话,一定要打基座 !!!

主要代码index.uts

引入IM 和 TRTC 需要用到的类

// IM
import V2TIMManager from 'com.tencent.imsdk.v2.V2TIMManager';
import V2TIMSDKListener from 'com.tencent.imsdk.v2.V2TIMSDKListener'
import V2TIMSDKConfig from 'com.tencent.imsdk.v2.V2TIMSDKConfig'
import V2TIMLogListener from 'com.tencent.imsdk.v2.V2TIMLogListener'
import V2TIMSignalingManager from 'com.tencent.imsdk.v2.V2TIMSignalingManager'
import V2TIMOfflinePushInfo from 'com.tencent.imsdk.v2.V2TIMOfflinePushInfo'

// Trtc
import TRTCCloud from 'com.tencent.trtc.TRTCCloud';
import TRTCCloudDef from 'com.tencent.trtc.TRTCCloudDef';

创建IM 相关类 封装登录 信令相关的方法 index.uts文件中

export class CallIM {
	config : V2TIMSDKConfig
	private instance : V2TIMManager
	private imListener : V2TIMSDKListener
	private signalingInstance : V2TIMSignalingManager
	private callObserver : SignalingListener
	private listenerMap : Map<string, Array<(res : any) => void>> = new Map();
	constructor() {
		this.config = new V2TIMSDKConfig()
		// 指定 log 输出级别
		this.config.setLogLevel(V2TIMSDKConfig.V2TIM_LOG_INFO);
		// 2. 创建日志监听器
		const loglistener = new MyLogListener()
		this.config.setLogListener(loglistener)

		this.instance = V2TIMManager.getInstance() // 获取 V2TIMManager 管理器实例
		this.signalingInstance = V2TIMManager.getSignalingManager() // 信令管理类实例

		// 添加事件监听器
		this.imListener = new ImSdkListener({
			listener: (eventName : string, data : any) => {
				this.handleEvent(eventName, data)
			},
		})
		this.instance.addIMSDKListener(this.imListener)

		// 添加信令监听器
		const callObserve : ICallObserve = {
			listener: (eventName : string, data : any) => {
				this.handleEvent(eventName, data)
			},
		};
		this.callObserver = new SignalingListener(callObserve)
		this.signalingInstance.addSignalingListener(this.callObserver)

		// 初始化
		this.instance.initSDK(context, sdkAppId.toInt(), this.config)

	}
	// 处理事件分发
	private handleEvent(eventName : string, data : any) {
		const listeners = this.listenerMap.get(eventName)
		if (listeners != null) {
			listeners.forEach(listener => {
				try {
					listener(data)
				} catch (e) {
					console.error(`事件监听器执行错误 [${eventName}]`, e)
				}
			})
		}
	}
	// 登录
	public login(options : LoginOptions) {
		const cb : ICallback = {
			apiName: "login",
			success: () => {
				options.success?.();
			},
			fail: (errCode : number, errMsg : string) => {
				options.fail?.(errCode, errMsg);
			},
		};
		console.log('options===', options)
		this.instance.login(options.userID, options.userSig, new Callback(cb))
	}

	// 登出
	public logout(options : CallbackOptions) {
		const cb : ICallback = {
			apiName: "logout",
			success: () => {
				options.success?.();
			},
			fail: (errCode : number, errMsg : string) => {
				options.fail?.(errCode, errMsg);
			},
		};
		this.instance.logout(new Callback(cb))
	}

	// 发送邀请
	public inviteCall(options : InviteCallOptions):string|null {
		const cb : ICallback = {
			apiName: "inviteCall",
			success: () => {
				options.success?.();
			},
			fail: (errCode : number, errMsg : string) => {
				options.fail?.(errCode, errMsg);
			},
		};
		console.log('inviteCall===', options)

		let v2TIMOfflinePushInfo = new V2TIMOfflinePushInfo();
		// v2TIMOfflinePushInfo.setExt(new Gson().toJson(containerBean).getBytes());
		// // OPPO 必须设置 ChannelID 才可以收到推送消息,这个 ChannelID 需要和控制台一致
		// v2TIMOfflinePushInfo.setAndroidOPPOChannelID("tuikit");
		// v2TIMOfflinePushInfo.setAndroidHuaWeiCategory("IM");
		// v2TIMOfflinePushInfo.setAndroidVIVOCategory("IM");
		v2TIMOfflinePushInfo.setTitle(options.offlinePushInfo.title as string);
		v2TIMOfflinePushInfo.setDesc(options.offlinePushInfo.description as string);


		// v2TIMOfflinePushInfo.setDesc("You have a new call invitation");
		// // 设置自定义铃声

		// v2TIMOfflinePushInfo.setIOSSound("phone_ringing.mp3");
		// v2TIMOfflinePushInfo.setAndroidSound("phone_ringing");
		return  this.signalingInstance.invite(options.invitee, options.data, options.onlineUserOnly, v2TIMOfflinePushInfo, options.timeout.toInt(), new Callback(cb))
	}



	// 取消邀请
	public cancelCall(options : AcceptCallOptions) {
		const cb : ICallback = {
			apiName: "cancelCall",
			success: () => {
				options.success?.();
			},
			fail: (errCode : number, errMsg : string) => {
				options.fail?.(errCode, errMsg);
			},
		};
		this.signalingInstance.cancel(options.inviteId, options.data, new Callback(cb))
	}



	// 同意接听信令
	public acceptCall(options : AcceptCallOptions) {
		const cb : ICallback = {
			apiName: "acceptCall",
			success: () => {
				options.success?.();
			},
			fail: (errCode : number, errMsg : string) => {
				options.fail?.(errCode, errMsg);
			},
		};
		this.signalingInstance.accept(options.inviteId, options.data, new Callback(cb))
	}

	// 拒绝接听信令
	public rejectCall(options : AcceptCallOptions) {
		const cb : ICallback = {
			apiName: "rejectCall",
			success: () => {
				options.success?.();
			},
			fail: (errCode : number, errMsg : string) => {
				options.fail?.(errCode, errMsg);
			},
		};
		this.signalingInstance.reject(options.inviteId, options.data, new Callback(cb))
	}

	// 信令相关事件监听 如收到邀请
	@UTSJS.keepAlive
	public on(eventName : string, listener : (res : any) => void) : void {
		const listeners : Array<(res : any) => void> = [listener];
		this.listenerMap.get(eventName)?.forEach((item) => {
			listeners.push(item);
		});
		this.listenerMap.set(eventName, listeners);
	}

	// 取消事件监听
	public off(eventName : string) : void {
		this.listenerMap.delete(eventName)
		// this.signalingInstance.removeSignalingListener(this.callObserver)
	}

	// 销毁
	public destroy() {
		this.instance.removeIMSDKListener(this.imListener)
		this.signalingInstance.removeSignalingListener(this.callObserver)
		this.instance.unInitSDK()
	}

}


type IMSDkListenerType = {
  listener : (eventType : string, data : any) => void
}
// 实现IM监听
class ImSdkListener extends V2TIMSDKListener{
	// 必须调用父类构造函数
	private listener : (eventType : string, data : any) => void;
	constructor(options:IMSDkListenerType) {
		super()
		this.listener = options.listener;
	}
	override onConnecting() {
		console.log('IM SDK 正在连接到腾讯云服务器')
	}
	override onConnectSuccess() {
		console.log('IM SDK 已经成功连接到腾讯云服务器')
	}
	override onKickedOffline() {
		this.listener('onKickedOffline',{})
		console.log('当前用户被踢下线,此时可以 UI 提示用户')
	}
}



// 日志监听
class MyLogListener extends V2TIMLogListener {
	// 必须调用父类构造函数
	constructor() {
		super() // 显式调用父类构造函数
	}
  override onLog(logLevel: Int, logContent: string) {
    console.log(`[CustomLog][${logLevel}] ${logContent}`)
    // 这里添加您的日志处理逻辑
		// logContent 为 SDK 日志内容
  }
}

trtc 相关类 接听后进入房间 退出房间

// TRTC 类 进房通话
export class TrtcCloud {
	private mTRTCCloud:TRTCCloud; // 声明成员变量
	private trtcListener:TrtcListener;
	private listenerMap: Map<string, Array<(res: any) => void>> = new Map();

	constructor(){
		// 创建 TRTC 实例
		this.mTRTCCloud = TRTCCloud.sharedInstance(context);

		// 设置监听
		const trtcObserve: trtcListenerType = {
      listener: (eventName: string, data: any) => {
        this.handleEvent(eventName, data)
      },
    };
		this.trtcListener = new TrtcListener(trtcObserve)
		this.mTRTCCloud.addListener(this.trtcListener)
	}
		// 处理事件分发
	private handleEvent(eventName: string, data: any) {
		const listeners = this.listenerMap.get(eventName)
		if (listeners != null) {
			listeners.forEach(listener => {
				try {
					listener(data)
				} catch (e) {
					console.error(`事件监听器执行错误 [${eventName}]`, e)
				}
			})
		}
	}
	// 事件监听
  @UTSJS.keepAlive
  public on(eventName: string, listener: (res: any) => void): void {
    const listeners: Array<(res: any) => void> = [listener];
    this.listenerMap.get(eventName)?.forEach((item) => {
      listeners.push(item);
    });
    this.listenerMap.set(eventName, listeners);
  }

	// 取消事件监听
  public off(eventName: string): void {
		this.listenerMap.delete(eventName)
		this.mTRTCCloud.removeListener(this.trtcListener)
  }
	

	// 进房开启通话
	public startAudioCall(options:StartAudioCallOptions) {
    let params = new TRTCCloudDef.TRTCParams();
    params.sdkAppId = sdkAppId.toInt();    // TRTC应用标识, 在控制台获取
    params.userSig = options.userSig;      // TRTC鉴权凭证, 在服务端生成
    params.roomId = options.roomId.toInt();     // 房间号,数字
    params.userId = options.userID;        // 用户名, 建议和IM保持同步
    
		console.log('params===',params)
    this.mTRTCCloud.startLocalAudio(TRTCCloudDef.TRTC_AUDIO_QUALITY_SPEECH);
    this.mTRTCCloud.setAudioRoute(TRTCCloudDef.TRTC_AUDIO_ROUTE_SPEAKER); // 扬声器播放
    this.mTRTCCloud.enterRoom(params, TRTCCloudDef.TRTC_APP_SCENE_AUDIOCALL);
 }
  // 挂断通话
	public endAudioCall() {
		this.mTRTCCloud.stopLocalAudio();
		this.mTRTCCloud.exitRoom();
	}
}

回调封装 #### 登录,登出,同意接听信令,拒绝接听信令操作回调


import V2TIMCallback  from 'com.tencent.imsdk.v2.V2TIMCallback'

export type ICallback = {
  apiName : string,
  success : () => void,
  fail : (errCode : number, errMsg : string) => void,
}
// 实现回调
export class Callback extends V2TIMCallback{
  private apiName : string;
  private success : () => void;
  private fail : (errCode : number, errMsg : string) => void;

  constructor(options : ICallback) {
    super();
    this.apiName = options.apiName;
    this.success = options.success;
    this.fail = options.fail;
  }
  override onSuccess() {
      console.log(`${this.apiName} success`);
      this.success();
  }
  override onError(code:Int, desc:string) {
    // 如果返回以下错误码,表示使用 UserSig 已过期,请您使用新签发的 UserSig 进行再次登录。
    // 1. ERR_USER_SIG_EXPIRED(6206)
    // 2. ERR_SVR_ACCOUNT_USERSIG_EXPIRED(70001)
    // 注意:其他的错误码,请不要在这里调用登录接口,避免 IM SDK 登录进入死循环。
    console.log(`${this.apiName} failure, code:${code}, desc:${desc}`);
    this.fail(code as number,desc);
  }
}

实现信令监听


import V2TIMSignalingListener  from 'com.tencent.imsdk.v2.V2TIMSignalingListener'

export type ICallObserve = {
  listener : (eventType : string, data : any) => void
}
// 实现信令监听
export class SignalingListener extends  V2TIMSignalingListener{
	private listener : (eventType : string, data : any) => void;
	// 必须调用父类构造函数
	constructor(options : ICallObserve) {
		super()
		this.listener = options.listener;
		console.log(`SignalingListener ok`);
	}

	// 邀请已被接受
  override onInviteeAccepted(inviteID:string,invitee:string,data:string) {
		console.log(`inviteID${inviteID}inviter${invitee}data${data}`)
		this.listener('onInvitationAccepted',{
			inviteID,
			invitee,
			data
		 })
  }

	// 被邀请者拒绝邀请
	override onInviteeRejected(inviteID:string,invitee:string,data:string) {
		console.log(`inviteID${inviteID}invitee${invitee}data${data}`)
		this.listener('onInviteeRejected',{
			inviteID,
			invitee,
			data
		 })
	}

  // 收到邀请
  override onReceiveNewInvitation(
          inviteID:string,inviter:string, groupId:string,inviteeList:List<String>,data:string) {

		 console.log('test')
		 this.listener('onReceiveNewInvitation',{
			inviteID,
			inviter,
			groupId,
			inviteeList,
			data
		 })
  }

	// 邀请被取消
	override onInvitationCancelled(inviteID:string,inviter:string,data:string) {
		console.log(`inviteID${inviteID}inviter${inviter}data${data}`)
		this.listener('onInvitationCancelled',{
			inviteID,
			inviter,
			data
		 })
	}

	// 邀请超时
	override onInvitationTimeout(inviteID:string,inviteeList:List<String>) {
		console.log(`inviteID${inviteID}inviteeList${inviteeList}`)
		this.listener('onInvitationTimeout',{
			inviteID,
			inviteeList,
		 })
	}
}

trtc 监听回调事件

import TRTCCloudListener from 'com.tencent.trtc.TRTCCloudListener';
import Bundle from 'android.os.Bundle';

export type trtcListenerType = {  
  listener : (eventType : string, data : any) => void
}
export class TrtcListener extends TRTCCloudListener {
  private listener : (eventType : string, data : any) => void;
  constructor(options : trtcListenerType) {
    super();
    this.listener = options.listener;
  }
  
  override onError( errCode:Int, errMsg:string,  extraInfo:Bundle|null) {
    console.log('onError',errCode,errMsg)
    this.listener('onError',{
      errCode,
      errMsg,
      extraInfo
    })
  }
    // 进入房间成功与否的事件回调
  override onEnterRoom(result:Long) {
    // super.onEnterRoom(result);
    console.log('onEnterRoom', result);
    this.listener('onEnterRoom',{
      result
    })
  }

  // 退出房间回调
  override onExitRoom(reason:Int) {
    console.log('onExitRoom', reason);
    // 离开房间原因,0:主动调用 exitRoom 退出房间;1:被服务器踢出当前房间;2:当前房间整个被解散 3: 服务器状态异常。
    this.listener('onExitRoom',{
      reason
    })
  }

  // 远端进房回调
  override onRemoteUserEnterRoom( userId:string) {
    console.log('onRemoteUserEnterRoom',  userId);
    this.listener('onRemoteUserEnterRoom',{
      userId
    })
  }

  // 远端离开房间 
  override onRemoteUserLeaveRoom( userId:string,reason:Int) {
    console.log('onRemoteUserLeaveRoom',  userId);
    // userId远端用户的用户标识。
    // 离开原因,0表示用户主动退出房间,1表示用户超时退出,2表示被踢出房间,3表示主播因切换到观众退出房间
    this.listener('onRemoteUserLeaveRoom',{
      userId,
      reason
    })
  }
}

使用

项目引入

import { CallIM, generateSign, TrtcCloud } from "@/uni_modules/xm-tim";

初始化

const callStore = reactive({
	imEngine: null,
	trtcCloud: null,
	callStatus: CallStatus.IDLE,
	inviteID: '',// 邀请id
	inviteer: '', // 邀请的用户
	roomId: 0,// 房间id
	userId: '', // 当前登录的用户
	userSig: '',
	callDuration: '00:00:00'
} as storeType)
 
callStore.imEngine = new CallIM()

登录

callStore.userSig = generateSign(callStore.userId);
callStore.imEngine?.login({
        userID: callStore.userId,
        userSig: callStore.userSig,
        success: () => {
              console.log('call login success')
        },
        fail: (errCode : number, errMsg : string) => {
                console.log('call login fail', errCode, errMsg)
        }
})

登出

callStore.imEngine?.logout({
    success: () => {
            console.log('logout success')
    },
    fail: (errCode : number, errMsg : string) => {
            console.log('logout fail', errCode, errMsg)
    }
})

发送邀请

    //返回邀请id  需要保存下来  在后续取消通话时需要用到
    let id =  callStore.imEngine?.inviteCall({
            invitee: callStore.inviteer,
            data:'',
            onlineUserOnly: false, // 
            offlinePushInfo: {
                    disablePush: false, // 如果接收方不在线,则进行离线推送
                    disableVoipPush: false, // 开启 voip 推送需要同时开启离线推送
                    title: '测试', // 离线推送标题
                    description: '', // 离线推送内容
                    androidOPPOChannelID: '' // 离线推送设置 OPPO 手机 8.0 系统及以上的渠道 ID
            },
            timeout: 30, // 设置 30s 超时
            success: () => {
            },
            fail: (errCode : number, errMsg : string) => {
                    console.log('inviteCall fail', errCode, errMsg)
            }
    })
    if(id!= null){
            callStore.inviteID = id
    }

取消通话

if (callStore.inviteID != '') {
    callStore.imEngine?.cancelCall({
        inviteId: callStore.inviteID,
        data:'',
        success: () => {
             console.log('cancelCall success')
        },
        fail: (errCode : number, errMsg : string) => {
            console.log('cancelCall fail', errCode, errMsg)
        }
    })
}

事件监听

// 被邀请人接受了邀请  开启语音通话
callStore.imEngine?.on('onInvitationAccepted', (res) => {
    console.log('onInvitationAccepted', res);

    initTRTC() // 初始化 TRTTC
    if (callStore.trtcCloud != null) {
        listenTRTC() // 监听进房状态
        enterRoom() // 进入房间
    }

    callStore.callStatus = CallStatus.ACCEPTED;

})
callStore.imEngine?.on('onInviteeRejected', (res) => {
        console.log('onInviteeRejected', res);
        uni.showToast({
                title: '对方正在通话中,请稍后再拨',
                icon: 'none',
                duration: 3000,
        })
})

// 取消邀请
callStore.imEngine?.on('onInvitationCancelled', (res) => {
        console.log('onInvitationCancelled', res);
        callStore.callStatus = CallStatus.CANCELLED;
        setTimeout(() => {
                uni.showToast({
                        title:'已取消通话!',
                        icon: 'none',
                        duration: 3000,
                })
        }, 100)

});


// 邀请超时
callStore.imEngine?.on('onInvitationTimeout', (res) => {
        console.log('onInvitationTimeout', res);
        setTimeout(() => {
                uni.showToast({
                        title: '呼叫超时',
                        icon: 'none',
                        duration: 3000,
                })
                resetCall()
        }, 2000)

});
// 被踢下线
callStore.imEngine?.on('onKickedOffline', (res) => {
        console.log('onKickedOffline', res);
        uni.showModal({
                content: '其他用户已登录和您相同的账号!'
        })
})

取消监听

callStore.imEngine?.off('onReceiveNewInvitation')
callStore.imEngine?.off('onInvitationCancelled')
callStore.imEngine?.off('onInvitationTimeout')
callStore.imEngine?.off('onKickedOffline')

初始化trtc

callStore.trtcCloud = new TrtcCloud();

事件监听

callStore.trtcCloud?.on('onError', (res) => {
		console.log('onError');
	})

	callStore.trtcCloud?.on('onEnterRoom', (res) => {
		console.log('onEnterRoom', res);
		let resData = res as UTSJSONObject
		let result = resData.result as number
		if (result > 0) {
			console.log(`enter room success`)
		} else {
			console.log(`enter room failed,error code = ${result}`)
		}
	})
	callStore.trtcCloud?.on('onExitRoom', (res) => {
		console.log('onExitRoom', res);
	})

	callStore.trtcCloud?.on('onRemoteUserEnterRoom', (res) => {
		console.log('onRemoteUserEnterRoom', res);
		// 远端用户进房成功
		callStore.callStatus = CallStatus.CONNECTED;
	})
	callStore.trtcCloud?.on('onRemoteUserLeaveRoom', (res) => {
		console.log('onRemoteUserLeaveRoom', res);
		setTimeout(() => {
			uni.showToast({
				title: '对方已挂断电话,通话已结束!',
				icon: 'none',
				duration: 2000
			})
		}, 500)
	})

进入房间开始通话

callStore.trtcCloud?.startAudioCall({
    roomId: callStore.roomId,
    userID: callStore.userId as string,
    userSig: callStore.userSig,
})

挂断通话退出房间

callStore.trtcCloud?.endAudioCall()

取消监听事件

callStore.trtcCloud?.off('onEnterRoom')
callStore.trtcCloud?.off('onExitRoom')
callStore.trtcCloud?.off('onRemoteUserEnterRoom')
callStore.trtcCloud?.off('onRemoteUserLeaveRoom')

完整源代码:gitee.com/WTYHC/tim