<template> <div id="RongYun"> <el-input v-model="value" type="text" placeholder="自定义用户名" /> <div @click="Dengl">点击登录</div> <div class="rong-container"> <div id="rongUser" class="rong-user"> <span>用户 ID:</span> <span id="rongUserId"></span> </div> <div class="rong-im" id="rongIM"> <p>请先进行 IM 连接</p> <!--appkey--> <div class="im-item"> <label>App Key</label> <el-input type="text" id="appkey" v-model="AppKey" placeholder="请输入 App Key" /> </div> <!--token--> <div class="im-item"> <label>Token</label> <el-input type="text" id="token" v-model="tokens" placeholder="请输入 Token" /> </div> <!--navi--> <div class="im-item"> <label>Navi</label> <el-input type="text" id="navi" placeholder="请输入 Navi 地址" /> <p>非必填;私有云环境必填</p> </div> <!--mediaServer--> <div class="im-item"> <label>MediaServer</label> <el-input type="text" id="mediaServer" placeholder="请输入 MediaServer 地址" /> <p>非必填;音视频服务地址</p> </div> <!--连接--> <div class="im-item"> <button id="DengLuBTN" ref="DengLuBTN" @click="connectIM()"> 连接 </button> </div> </div> <div class="rong-call" id="rongCall" style="display: none"> <!--呼叫选项--> <div id="callParam" class="call-param"> <!--选择通话类型--> <div class="param-item"> <label>通话类型</label> <select name="" id="callType" @change="callTypeChange()"> <option value="1">单聊</option> <option value="3">群聊</option> </select> <p>必选</p> </div> <!--mediaType--> <div class="param-item"> <label>媒体类型</label> <select name="" id="callMediaType" @change="callMediaTypeChange()"> <option value="1">音频</option> <option value="2">音视频</option> </select> <p>必选</p> </div> <!--targetId--> <div id="paramPrivate" class="param-item"> <label>对方 ID</label> <el-input id="targetId" type="text" placeholder="对方 userId" /> <p> 必填;对方的 userId,可通过<a target="_blank" href="https://developer.rongcloud.cn/" >[开发者后台] -> [服务管理] -> [API 调用] -> [用户服务] -> [获取 token]</a >获取,且登录成功 </p> </div> <!--targetId--> <div id="paramGroupId" class="param-item" style="display: none"> <label>群组 ID</label> <el-input id="groupId" type="text" placeholder="群组 ID" /> <p> 必填;可通过 <a target="_blank" href="https://developer.rongcloud.cn/" >[开发者后台] -> [服务管理] -> [API 调用] -> [群组服务] -> [加入群组]</a > 加入群组后获取 </p> </div> <!--userIds 只有群显示--> <div id="paramInvitedIds" class="param-item" style="display: none"> <label>被邀请者 ID</label> <el-input id="userIds" type="text" placeholder="多个 userId 用英文半角逗号分开" /> <p> 必填;需加入群后,方可收到邀请。多个 userId 用英文半角逗号分开 </p> </div> </div> <!--通话操作按钮--> <div class="opt-btn"> <button id="callBtn" class="btn-call" @click="call()">呼叫</button> <button id="acceptBtn" class="btn-accept" @click="accept()"> 接听 </button> <button id="hungupBtn" class="btn-hungup" @click="hungup()"> 挂断 </button> </div> <!--通话视图展示--> <div id="videoView" class="video-view"></div> </div> </div> <!-- 显示一些操作的信息 --> <div class="toast-wrap"> <span class="toast-msg"></span> </div> </div> </template> <script setup name="RongYun"> import { ref, reactive, toRefs, onMounted, nextTick } from "vue"; import { userGetToken } from "@/api/RongYun"; import configs from "@/utils/config.js"; // 插件内置:------------------------------------------------------------------E const AllData = reactive({ AppKey: configs.AppKey, tokens: "", DengLuBTN: ref(null), userId: "newfiber", }); const { AppKey, tokens, DengLuBTN, userId } = toRefs(AllData); // 插件内置:------------------------------------------------------------------A /** * utils */ const RCDom = { get: (id) => { return document.getElementById(id); }, show: (id) => { var rongIMDom = document.getElementById(id); rongIMDom.setAttribute("style", "display:inline-block;"); }, showBlock: (id) => { var rongIMDom = document.getElementById(id); rongIMDom.setAttribute("style", "display:block;"); }, hide: (id) => { var rongIMDom = document.getElementById(id); rongIMDom.setAttribute("style", "display:none;"); }, }; const RCToast = (msg) => { setTimeout(function () { document .getElementsByClassName("toast-wrap")[0] .getElementsByClassName("toast-msg")[0].innerHTML = msg; var toastTag = document.getElementsByClassName("toast-wrap")[0]; toastTag.className = toastTag.className.replace("toastAnimate", ""); setTimeout(function () { toastTag.className = toastTag.className + " toastAnimate"; }, 10); }, 10); }; const RCCallView = { connectedIM: () => { RCDom.show("rongUser"); }, readyToCall: () => { RCDom.hide("rongIM"); RCDom.show("rongCall"); }, outgoing: () => { RCDom.hide("callParam"); RCDom.hide("callBtn"); RCDom.show("hungupBtn"); }, incomming: () => { RCDom.hide("callParam"); RCDom.hide("callBtn"); RCDom.show("acceptBtn"); RCDom.show("hungupBtn"); }, inTheCall: () => { RCDom.hide("acceptBtn"); RCDom.hide("callParam"); RCDom.show("hungupBtn"); }, end: () => { RCDom.show("callParam"); RCDom.show("callBtn"); RCDom.hide("acceptBtn"); RCDom.hide("hungupBtn"); }, }; /** * im */ /** * 初始化、链接 IM 相关逻辑 */ // IM 实例 let imClient; const connectIM = () => { const appkey = RCDom.get("appkey").value; const token = RCDom.get("token").value; const navi = RCDom.get("navi").value; if (!appkey) { RCToast("请输入 App Key"); return; } if (!token) { RCToast("请输入 Token"); return; } // IM 客户端初始化 imClient = RongIMLib.init({ appkey, navigators: navi ? [navi] : undefined, logLevel: 1, }); // 初始化 RTC CallLib initRTC(); initCall(); imClient.watch({ // 监听 IM 连接状态变化 status(evt) { console.log("connection status change:", evt.status); }, }); RCToast("正在链接 IM ... ☕️"); imClient .connect({ token }) .then((user) => { RCCallView.connectedIM(); RCCallView.readyToCall(); RCDom.get("rongUserId").innerText = user.id; RCToast(`用户 ${user.id} IM 链接成功 ✌🏻`); }) .catch((error) => { console.log(error); RCToast("IM 链接失败,请检查网络后再试"); }); }; /** *call */ /** * call 主要相关逻辑 */ const { ConversationType } = RongIMLib; const { RCCallMediaType, RCCallErrorCode } = RCCall; // CallSession 实例 let callSession; // Call 呼叫类型 let callType = ConversationType.PRIVATE; // Call 媒体类型 let mediaType = RCCallMediaType.AUDIO; // RTC 实例 let rtcClient; // CallLib 实例 let callClient; /** * RTC 初始化 * 在 IM 初始化后进行初始化 (具体位置:im.js) */ const initRTC = () => { const mediaServer = RCDom.get("mediaServer").value; rtcClient = imClient.install(window.RCRTC.installer, { mediaServer: mediaServer || undefined, timeout: 30 * 1000, logLevel: window.RCEngine.LogLevel.DEBUG, }); }; /** * CallLib 初始化 * 在 IM 初始化后进行初始化 (具体位置:im.js) */ const initCall = () => { callClient = imClient.install(window.RCCall.installer, { rtcClient: rtcClient, onSession: (session) => { callSession = session; mediaType = session.getMediaType(); registerCallSessionEvent(callSession); RCToast(`收到 ${session.getCallerId()} 的通话邀请`); RCCallView.incomming(); }, onSessionClose: (session, summary) => { RCToast("通话已结束"); RCCallView.end(); removeVideoEl(); }, }); }; /** * 通话类型监听 */ const callTypeChange = () => { const callTypeDom = RCDom.get("callType"); callType = Number(callTypeDom.value); if (callType === ConversationType.GROUP) { RCDom.showBlock("paramGroupId"); RCDom.showBlock("paramInvitedIds"); RCDom.hide("paramPrivate"); } else { RCDom.hide("paramGroupId"); RCDom.hide("paramInvitedIds"); RCDom.showBlock("paramPrivate"); } }; /** * 媒体类型监听 */ const callMediaTypeChange = () => { const mediaTypeDom = RCDom.get("callMediaType"); mediaType = Number(mediaTypeDom.value); }; /** * CallSession 事件 */ const getCallSessionEvent = () => { return { onRinging: (sender) => { // 当远端用户已开始响铃, 该函数有 2 个参数: sender 是发送者,session 是通话实例。这时用户可以在业务层做响铃的 UI 展示。 RCToast(`收到 ${sender.userId} 振铃`); message.info(`收到 ${sender.userId} 振铃`); }, onAccept: (sender) => { // 当远端用户已同意接听, 该函数有 2 个参数: sender 是发送者,session 是通话实例。这时用户可以把 UI 的‘响铃’变成‘通话中’。 RCToast(`${sender.userId} 已接听`); }, onHungup: (sender) => { // 当有远端用户挂断, 该函数有 3 个参数: sender 是发送者,reason 是挂断原因,session 是通话实例。这时用户可以在 UI 层提示‘xxx已挂断’。 RCToast(`${sender.userId} 已挂断`); // 群组中移除相应节点 const videoViewDom = RCDom.get("videoView"); const videoDom = RCDom.get(`video-${sender.userId}`); videoDom && videoViewDom.removeChild(videoDom); }, onTrackReady: (track) => { // 当本端资源或远端资源已获取, 该函数有 2 个参数:track 是本端资源或远端资源, session 是通话实例。这时用户可以用拿到的 track 播放音频、视频。 appendVideoEl(track); if (!track.isLocalTrack()) { RCToast("通话已建立"); RCCallView.inTheCall(); } }, onMemberModify: (sender, invitedUsers) => { // 群组通话中有其他人被邀请加入, 该函数有 3 个参数: sender 是发送者,invitedUsers 是被邀请的用户列表, session 是通话实例。这时用户可以在 UI 层提示‘xxx加入通话’。 }, onMediaModify: (sender) => { // 通话类型改变时触发, 该函数有3个参数: sender 是发送者,mediaType 通话类型, session 是通话实例。这时用户可以在 UI 层提示‘已降级成音频通话’。 }, onAudioMuteChange: (muteUser) => { // 对方静音后触发, 该函数有2个参数: muteUser 是已静音的用户, session 是通话实例。这时用户可以在 UI 层提示‘xxx已静音’。 }, onVideoMuteChange: (muteUser) => { // 对方禁用视频后触发, 该函数有2个参数: muteUser 是已禁用视频的用户, session 是通话实例。这时用户可以在 UI 层提示‘xxx已禁用视频’。 }, }; }; /** * callSession 事件注册 */ const registerCallSessionEvent = (session) => { const events = getCallSessionEvent(); session.registerSessionListener(events); }; /** * callSession 呼叫 */ const call = () => { const events = getCallSessionEvent(); const isPrivateCall = callType === ConversationType.PRIVATE; const params = { targetId: RCDom.get(`${isPrivateCall ? "targetId" : "groupId"}`).value, mediaType: mediaType, listener: events, }; if (isPrivateCall) { if (!RCDom.get("targetId").value) { RCToast("请输入对方 ID"); return; } privateCall(params); } else { if (!RCDom.get("groupId").value) { RCToast("请输入群组 ID"); return; } if (!RCDom.get("userIds").value) { RCToast("请输入被邀请者 ID"); return; } groupCall(params); } }; /** * 单呼 */ const privateCall = (params) => { callClient.call(params).then(({ code, session }) => { if (code === RCCallErrorCode.SUCCESS) { registerCallSessionEvent(session); callSession = session; RCCallView.outgoing(); } else { RCToast(`呼叫失败,错误原因:${code}`); } }); }; /** * 群呼 */ const groupCall = (params) => { params.userIds = (RCDom.get("userIds").value || []).split(","); callClient.callInGroup(params).then(({ code, session }) => { if (code === RCCallErrorCode.SUCCESS) { registerCallSessionEvent(session); callSession = session; RCCallView.outgoing(); } else { const reason = code === RCCallErrorCode.NOT_IN_GROUP ? "当前用户未加入群组" : code; RCToast(`呼叫失败,错误原因:${reason}`); removeVideoEl(); } }); }; /** * 接听当前 callSession */ const accept = () => { callSession.accept().then(({ code }) => { if (code === RCCallErrorCode.SUCCESS) { RCToast("接听成功"); } else { RCToast(`接听失败,错误原因:${code}`); } }); }; /** * 挂断当前 callSession */ const hungup = () => { callSession.hungup().then(({ code }) => { if (code === RCCallErrorCode.SUCCESS) { RCToast("挂断成功"); } else { RCToast(`挂断失败,错误原因:${code}`); } }); }; /** * video 视图渲染 */ const appendVideoEl = (track) => { const container = RCDom.get("videoView"); if (track.isAudioTrack()) { const uid = track.getUserId(); const node = document.createElement("div"); node.setAttribute("id", `video-${uid}`); const videoTpl = `<span class="video-user-id">ID: ${uid}</span> <span class="video-media-type">${mediaType === 1 ? "音频" : ""}</span> <video id="${uid}"></video>`; node.innerHTML = videoTpl; node.classList = "video-item"; container.appendChild(node); track.play(); } else { const videoEl = RCDom.get(track.getUserId()); track.play(videoEl); } }; /** * 通话结束后,清除所有 video 标签 */ const removeVideoEl = () => { RCDom.get("videoView").innerHTML = ""; }; // 默认登录,获取当前电脑用户PCUser的通话token const Dengl = async () => { let res = await userGetToken(`?userId=${userId.value}&name=管理员`); if (res.code == 200) { // 融云登录成功 tokens.value = res.token; nextTick(() => { connectIM(); }); } }; onMounted(() => { //Dengl(); }); </script> <style lang="scss" scoped> #RongYun { width: 100%; height: 100%; .rong-container { background: #222831; // min-height: 100%; // min-width: 1400px; color: #fff; font-size: 18px; text-align: center; } .rong-container .rong-title { margin: 0; padding: 25px 0 10px; text-align: center; color: #fff; font-size: 30px; } .rong-container .rong-user { display: none; position: absolute; top: 70px; left: 100px; } .rong-container button { width: 150px; height: 30px; border-radius: 2px; background: #2da2ea; border: none; color: #fff; box-shadow: 1px 1px 6px #00000069; cursor: pointer; } .rong-container button:active { width: 150px; height: 30px; border-radius: 2px; background: #0f93e4; border: none; color: #fff; box-shadow: 1px 1px 6px #00000069; cursor: pointer; /* opacity: .8; */ } .rong-container .rong-im { background: #eeeeee1c; width: 850px; border-radius: 2px; display: inline-block; } .rong-container .rong-im .im-item { text-align: center; margin-bottom: 10px; height: 50px; position: relative; } .rong-container .rong-im .im-tips { width: 100px; } .rong-container .rong-im .im-item label { display: inline-block; width: 150px; text-align: right; } .rong-container .rong-im .im-item input { border: 1px solid #ccc; border-radius: 2px; height: 25px; width: 600px; } .rong-container .rong-im .im-item p { margin: 0px; display: inline-block; position: absolute; left: 200px; top: 30px; color: #fff; font-size: 13px; } .rong-container .rong-call { text-align: center; margin-bottom: 10px; } .rong-container .rong-call .call-param { padding: 10px; background: #eeeeee1c; width: 650px; border-radius: 2px; display: inline-block; } .rong-container .rong-call select { background: #4285f4; width: 450px; height: 30px; border-radius: 2px; border: none; color: #fff; text-align: center; } .rong-container .rong-call label { display: inline-block; width: 150px; text-align: right; } .rong-container .rong-call input { border: 1px solid #ccc; border-radius: 2px; height: 25px; width: 450px; } .rong-container .rong-call .call-item, .rong-container .rong-call .call-param, .rong-container .rong-call .opt-btn { margin-bottom: 10px; } .rong-container .rong-call .call-param .param-item { margin-bottom: 15px; } .rong-container .rong-call .call-param .param-item { height: 50px; position: relative; } .rong-container .rong-call .call-param .param-item p { margin: 0px; display: inline-block; position: absolute; left: 180px; top: 30px; color: #fff; font-size: 13px; text-align: left; } .rong-container .rong-call .opt-btn .btn-accept, .rong-container .rong-call .opt-btn .btn-hungup { display: none; } .rong-container .rong-call .video-view .video-item { width: 320px; height: 240px; background: #1a7cb9; position: relative; border-radius: 2px; display: inline-block; margin: 8px; } .rong-container .rong-call .video-view .video-item .video-user-id { background: #a8d8ea9c; position: absolute; left: 2px; display: inline-block; padding: 0 5px; border-radius: 2px; } .rong-container .rong-call .video-view .video-item .video-media-type { position: absolute; top: 41%; right: 42%; } .rong-container .rong-call .video-view video { width: 320px; height: 240px; border-radius: 2px; } /* toast */ .toast-wrap { display: inline-block; opacity: 0; position: fixed; top: 5%; color: #fff; width: 100%; text-align: center; } .toast-msg { background-color: rgba(89, 148, 236, 0.918); border-radius: 2px; padding: 10px 15px; } .toastAnimate { animation: toastKF 2s; } @keyframes toastKF { 0% { opacity: 0; } 25% { opacity: 1; z-index: 9999; } 50% { opacity: 1; z-index: 9999; } 75% { opacity: 1; z-index: 9999; } 100% { opacity: 0; z-index: 0; } } } </style>