首先这次接触到IM,是因为项目里需要用到一个群聊视频,通讯录,消息发送等功能,但是Im提供的dom是 react框架写的,并没有vue框架的dom,于是自己根据IM的官方文档实现了在vue框架下的IM通讯。
这是最终效果图,因个人项目而异,弹窗或者不弹窗灵活改动,还有很多功能没写,待此功能重构后再继续更新,因信息数据保密所以打码
IM官方文档:https://doc.rentsoft.cn/#/integrate/web/integrate_login_web
–》web就看每一个功能下面web端文档说明
视频通讯文档 官方没有写,但是在IM项目的源码里面发现视频通讯功能使用的是LiveKit
LiveKit官方文档 :https://docs.livekit.io/guides/room/connect/
LiveKit GitHub: https://github.com/livekit/client-sdk-js
LiveKit JS Client SDK文档: https://docs.livekit.io/client-sdk-js/index.html
LiveKit其实只需要看 官方文档就OK,当然,除非你想深入了解它
下面贴实现过程代码
首先第一步 登录IM 我将它部分封装在一个JS里面
PS:音频功能需要在HTTPS 和 本地环境下浏览器才允许通过安全策略,否则调取本地摄像头你会报错,目前没找到浏览器关闭使用摄像头,目前找到的资料是挂断电话或者关闭通话弹窗时刷新此页面
import { OpenIMSDK } from 'open-im-sdk'
import axios from "axios";
import store from "@/store";
import { Message } from 'element-ui';
// 音视频邀请相关信息
// {
// "inviterUserID": "18666662412", //邀请者UserID
// "inviteeUserIDList": ["18349115126"], //被邀请者UserID列表,如果是单聊只有一个元素
// "groupID": "f2e77b9ec33e92298675ad511fdfa6ab", //如果是单聊,为""
// "roomID": "room_id_111", //房间ID,必须唯一,可以不设置。
// "timeout": 1000, //邀请超时时间(秒)
// "mediaType": "video", //video 或者audio
// "sessionType": 2 //1为单聊,2为群聊
// "platformID":1 // 创建时写什么就是什么 1 2 皆可
// }
let user = '';
const openIM = new OpenIMSDK();
const onGetImData = (datas) => {
user = JSON.parse(datas.serviceTag1)
let urls = window.location.host;
let obj = {
platform: "5",
userID: JSON.parse(datas.serviceTag1).openImUserId,
};
let urltype = 'https://****:端口/portaluserIm/im/user/obtainToken';
let urlstype =
document.location.protocol.indexOf("https") > -1 ? true : false;
axios
.post(urltype, obj)
.then((data) => {
console.log("openIMopenIM", data);
sessionStorage.setItem("SET_IMTOKEN", JSON.stringify(data.data.data));
var commonIp = ''
commonIp = eval("(" + sessionStorage.getItem('commonIp') + ")")
console.log('commonIp',commonIp);
const config = {
userID: data.data.data.userID,
token: data.data.data.token,
url: commonIp? commonIp.newImWss : '你的wss地址',
platformID: 5,
};
openIM
.login(config)
.then((res) => {
console.log("login 结果...", res);
sessionStorage.setItem("SET_IMUSER", JSON.stringify(res));
openIM.on("OnRecvNewMessage", (dataMessage) => {
console.log("会话消息监听测试", JSON.parse(dataMessage.data));
});
openIM.on("OnReceiveNewInvitation", (dataInvitation) => {
let row = dataInvitation;
row.objData = JSON.parse(row.data);
row.objDataName = row.objData.participant.groupInfo.groupName
console.log('urlstype', urlstype);
if (row.objData.invitation.inviterUserID != user.openImUserId) {
// 被邀请者需要点击接受后 方可进入视频房间
sessionStorage.removeItem("ImRow")
sessionStorage.setItem("ImRow", JSON.stringify(row))
store.commit("SET_IMTYPE", true);
console.log('this.$store.state.user.ImType', store.state.ImType);
} else {
}
console.log("被邀请者收到:音视频通话邀请", row,user);
});
openIM.on("OnInviteeAccepted", (dataAccepted) => {
console.log("邀请者收到:被邀请者同意音视频通话", dataAccepted);
});
openIM.on("OnInviteeRejected", (inviteeRejectedCallback) => {
console.log("邀请者收到:被邀请者拒绝音视频通话",inviteeRejectedCallback);
});
openIM.on("OnInvitationCancelled", (dataCancelled) => {
console.log("被邀请者收到:邀请者取消音视频通话", dataCancelled);
});
openIM.on("OnInvitationTimeout", (dataTimeout) => {
console.log("邀请者收到:被邀请者超时未接通", dataTimeout);
});
openIM.on("OnInviteeAcceptedByOtherDevice", (dataOtherDevice) => {
console.log(
"被邀请者(其他端)收到:比如被邀请者在手机接听,在pc上会收到此回调",
dataOtherDevice
);
});
openIM.on("OnInviteeRejectedByOtherDevice", (dataDevice) => {
console.log(
"被邀请者(其他端)收到:比如被邀请者在手机拒接,在pc上会收到此回调",
dataDevice
);
});
})
.catch((err) => {
console.log("login 报错结果...", err);
});
});
}
export default {
onGetImData: onGetImData,
openIM: openIM
};
上面是封装好的JS文件,既然封装了就得引用对吧,是的,你知道的
import openIMData from "@/components/ImToken";
const openIM = openIMData.openIM;
好了 接下来我们将一步一步来
1,创建群
https://doc.rentsoft.cn/#/integrate/web/integrate_group_web 官方文档有写
2,创建群组成功后 ,如果需要二次确认就获取下当前群组信息你能拿到当前群成员所有ID,如果不用那么就直接发起呼叫
// 音视频邀请相关信息
// {
// "inviterUserID": "18666662412", //邀请者UserID
// "inviteeUserIDList": ["18349115126"], //被邀请者UserID列表,如果是单聊只有一个元素
// "groupID": "f2e77b9ec33e92298675ad511fdfa6ab", //如果是单聊,为""
// "roomID": "room_id_111", //房间ID,必须唯一,可以不设置。
// "timeout": 1000, //邀请超时时间(秒)
// "mediaType": "video", //video 或者audio
// "sessionType": 2 //1为单聊,2为群聊
// "platformID":1
// }
let invitation = {
inviterUserID: user.userID,
inviteeUserIDList: this.qunUid.arrData,
groupID: this.qunData.groupID,
// groupID: "eaaf2f787bd59b5c8ac2c1039d8388ff", // 方便测试创建好一个群后你需要记录这个群的 ID
roomID: this.oncode(),// 唯一ID 你可以随机生成15位数字+字母
timeout: 10000,
mediaType: "video",
sessionType: 2,
};
openIM
.signalingInviteInGroup(invitation)
.then((res) => {
console.log("res11", res);
Message({
showClose: true,
message: "音频发起成功,如长时间未反应,请刷新页面后重新发起!",
type: "success",
duration: 5000,
});
this.qunUidView = res;
this.qunUidView.obj = JSON.parse(res.data);
let row = {
objData: {
invitation: invitation,
},
};
sessionStorage.setItem("ImRow", JSON.stringify(row));
store.commit("SET_CreateVideoUserType", true);
})
.catch((err) => {
console.log("err", err);
});
好的,上面这一块代码 你已经成功发起了 呼叫,现在回到封装好的JS文件,不管是创建群还是消息接受 OnRecvNewMessage 函数都会接收到反馈数据,比如别人创建了一个群 把你添加进去 此函数会返回数据,好了让我们继续往下走,或许你觉得我讲的废话很多,你可以关闭当前浏览器。
上面讲到发起了群聊视频,被邀请者 OnReceiveNewInvitation 方法会触发,此函数会返回 当前群聊视频房间所有信息,比如房间ID 房间成员,记住 这是视频群聊房间 不是上面创建的群。群不等于音频群聊房间。
你可以和我一样设置一个监听vuex -》 store.commit(“SET_IMTYPE”, true);当有人邀请我 需要弹出接受邀请按钮,下面我们来看看点击接受按钮后的事件
import { Room, RoomEvent, ParticipantEvent, Track } from "livekit-client";//记得引用
class VideoPreset {
constructor(width, height, maxBitrate, maxFramerate) {
this.width = width;
this.height = height;
this.encoding = {
maxBitrate,
maxFramerate,
};
}
get resolution() {
return {
width: this.width,
height: this.height,
frameRate: this.encoding.maxFramerate,
aspectRatio: this.width / this.height,
};
}
}
const VideoPresets = {
h90: new VideoPreset(160, 90, "60_000", 15),
h180: new VideoPreset(320, 180, "120_000", 15),
h216: new VideoPreset(384, 216, "180_000", 15),
h360: new VideoPreset(640, 360, "300_000", 20),
h540: new VideoPreset(960, 540, "600_000", 25),
h720: new VideoPreset(1280, 720, "2_000_000", 30),
h1080: new VideoPreset(1920, 1080, "3_000_000", 30),
h1440: new VideoPreset(2560, 1440, "5_000_000", 30),
h2160: new VideoPreset(3840, 2160, "8_000_000", 30),
/** @deprecated */
qvga: new VideoPreset(320, 180, "120_000", 10),
/** @deprecated */
vga: new VideoPreset(640, 360, "300_000", 20),
/** @deprecated */
qhd: new VideoPreset(960, 540, "600_000", 25),
/** @deprecated */
hd: new VideoPreset(1280, 720, "2_000_000", 30),
};
// created 里面 需要 this.room = new Room(); 初始化
let obj = {
opUserID: SET_IMTOKEN.userID,
invitation: {
inviterUserID: this.sessData.objData.invitation.inviterUserID, // 邀请者
inviteeUserIDList: [SET_IMTOKEN.userID], // 参与者ID
groupID: this.sessData.objData.invitation.groupID, // 群ID
roomID: this.sessData.objData.invitation.roomID, //房间ID,必须唯一,可以不设置。
timeout: 60, //邀请超时时间(秒)
mediaType: "video",
sessionType: 2,
},
};
console.log("接受视频邀请参数", obj);
openIM
.signalingAccept(obj)
.then(async ({ data }) => {
this.signalAcceptReqData = JSON.parse(data);
console.log(
"接受某人邀请返回房间进入token,和 ws 地址",
this.signalAcceptReqData
);
await this.room
.connect(
this.signalAcceptReqData.liveURL, // rtc 地址
this.signalAcceptReqData.token, // rtc 链接当前登录用户token
{
// to publish camera and microphone immediately after joining 加入后立即发布相机和麦克风
audio: true,
video: true,
// don't subscribe to other participants automatically 不要自动订阅其他参与者
autoSubscribe: true,
// automatically manage video quality 自动管理视频质量
autoManageVideo: true,
// default publish settings 默认发布
publishDefaults: {
simulcast: true,
},
// default capture settings 默认捕获设置
captureDefaults: {
videoResolution: VideoPresets.hd.resolution,
},
}
)
.then(async (room) => {
// 此时你已经连接进入到了群聊视频房间
console.log("livekit-client->connect 回调", room);
this.roomData = room;
room.on(
RoomEvent.TrackSubscribed,
(track, publication, participant) => {
let ele = document.getElementById("videodata");
var videoDomDiv = document.createElement("div"); // 创建文字标签
if (track.kind == "video") {
console.log("接受媒体", track, publication, participant);
videoDomDiv.setAttribute(
"style",
"height:150px;width:200px;display: flex;flex-direction: column-reverse;align-items: center;margin: 0px 5px;"
);
videoDomDiv.setAttribute("id", participant.identity);
let nameData = JSON.parse(participant.metadata);
videoDomDiv.innerHTML = nameData.userInfo.nickname
? nameData.userInfo.nickname
: nameData.userInfo.userID;
console.log(
nameData.userInfo.nickname
? nameData.userInfo.nickname
: nameData.userInfo.userID
);
ele.appendChild(videoDomDiv);
}
const element = track.attach();
element.className = participant.identity;
videoDomDiv.appendChild(element);
}
);
room.on(
RoomEvent.TrackPublished,
(track, publication, participant) => {
console.log("查看房间活动", track, publication, participant);
}
);
room.on(RoomEvent.LocalTrackPublished, (track, publication) => {
console.log("本地音轨已发布", new Track(), track, publication);
});
room.on(RoomEvent.RoomMetadataChanged, (track) => {
console.log("房间元数据已更改", track);
});
room.on(
RoomEvent.TrackStreamStateChanged,
(track, publication, participant) => {
console.log(
"跟踪流状态已更改",
track,
publication,
participant
);
if (document.getElementById(participant.identity)) {
console.log("当前ID,DOM存在", participant.identity);
document.getElementById(participant.identity).remove();
setTimeout(() => {
let ele = document.getElementById("videodata");
var videoDomDiv = document.createElement("div"); // 创建文字标签
if (track.kind == "video") {
console.log(
"跟踪流状态已更改后的媒体流数据",
track,
publication,
participant
);
videoDomDiv.setAttribute(
"style",
"height:150px;width:200px;display: flex;flex-direction: column-reverse;align-items: center;margin: 0px 5px;"
);
videoDomDiv.setAttribute("id", participant.identity);
let nameData = JSON.parse(participant.metadata);
videoDomDiv.innerHTML = nameData.userInfo.nickname
? nameData.userInfo.nickname
: nameData.userInfo.userID;
console.log(
nameData.userInfo.nickname
? nameData.userInfo.nickname
: nameData.userInfo.userID
);
ele.appendChild(videoDomDiv);
}
const element = track.track.attach();
element.className = participant.identity;
videoDomDiv.appendChild(element);
}, 200);
}
}
);
room.on(
RoomEvent.TrackUnsubscribed,
(track, publication, participant) => {
console.log(
"从所有附加元素中删除轨道",
track,
publication,
participant
);
if (track.detach) {
track.detach();
}
if (document.getElementById(participant.identity)) {
document.getElementById(participant.identity).remove();
}
}
);
room.on(RoomEvent.ConnectionQualityChanged, (participant) => {
// 目前这里输出 good excellent
console.log("模仿源码未知函数", participant);
});
room.on(
RoomEvent.TrackSubscriptionFailed,
(track, publication, participant) => {
console.log("跟踪订阅失败", track, publication, participant);
}
);
room.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
// show UI indicators when participant is speaking
// 当参与者发言时显示用户界面指示器
});
room.on(RoomEvent.Disconnected, () => {
console.log("参与者离开时");
});
room.on(RoomEvent.LocalTrackUnpublished, (track, participant) => {
console.log(
"当本地轨迹结束时,更新UI以将其从渲染中删除",
track,
participant
);
if (track.detach) {
track.detach();
document.getElementById(participant.identity).remove();
}
});
room.on(RoomEvent.ParticipantConnected, (participant) => {
// set up any per-participant callbacks 设置每个参与者的回调
participant.on(ParticipantEvent.TrackMuted, (publication) => {
console.log("设置每个参与者的回调", publication);
});
});
room.on(RoomEvent.ParticipantDisconnected, (track) => {
console.log("参与者 分离", track);
let elements = document.getElementsByClassName(track.identity);
for (var i = elements.length - 1; i >= 0; i--) {
elements[i].parentNode.removeChild(elements[i]);
}
});
room.on(RoomEvent.DataReceived, (payload, participant, kind) => {
// receive data from other participants
// 接收其他参与者的数据 RemoteTrackPublication
});
// publish local camera and mic tracks 发布本地摄像头和麦克风轨道
await room.localParticipant.enableCameraAndMicrophone();
const p = room.localParticipant;
await p
.setCameraEnabled(true)
.then((data) => {
console.log("有摄像头", data);
navigator.mediaDevices
.getUserMedia({ audio: false, video: true })
.then((stream) => {
console.log("stream", stream, stream.getTracks());
this.loading = false;
this.videoStream =
typeof stream.stop === "function"
? stream
: stream.getTracks();
let ele = document.getElementById("videodata"); // 获取父级标签
var videoDomDiv = document.createElement("div"); // 创建多媒体承载盒子标签
videoDomDiv.setAttribute(
"style",
"height:150px;width:200px;display: flex;flex-direction: column-reverse;align-items: center;"
);
videoDomDiv.innerHTML = store.state.userInfo.nickName;
var videoDom = document.createElement("video"); // 创建多媒体标签
videoDom.setAttribute("id", "newRTC");
videoDom.autoplay = true;
videoDom.setAttribute(
"style",
"height:125px;width:200px;"
);
videoDom.srcObject = stream;
videoDomDiv.appendChild(videoDom);
ele.appendChild(videoDomDiv);
})
.catch((err) => {
this.loading = false;
console.log(err);
});
})
.catch((err) => {
console.log("没有摄像头");
}); // 调取摄像头 没有会报错
await p
.setMicrophoneEnabled(true)
.then((data) => {
console.log("有麦克风", data);
})
.catch((err) => {
console.log("没有麦克风");
}); // 调取声音 没有会报错
// 屏幕 true 会分享屏幕
await p
.setScreenShareEnabled(false)
.then((data) => {
console.log("屏幕", data);
})
.catch((err) => {
console.log("屏幕", err);
});
room.on(RoomEvent.AudioPlaybackStatusChanged, () => {
if (!room.canPlayAudio) {
// UI is necessary.
console.log("333333");
button.onclick = () => {
room.startAudio().then(() => {
button.remove();
});
};
}
});
})
.catch((err) => {
console.log("livekit-client->connect 回调 报错", err);
});
})
.catch((err) => {
this.loading = false;
console.log("接受视频邀请 报错信息", err);
});
3,关闭房间记得退群,因项目而异。这里说的群 是上面创建的群
喜欢就关注吧,后续将持续输出一些接触到的新东西