前几篇都是写的关于一对一实现音视频通话,然后在此基础上加了桌面共享、文件传输、关闭/开启麦克风/摄像头、使用外部视频替换以及使用虚拟背景这几个功能。那么这节就来看看如何实现一对多直播模式,看看和一对一音视频通话有啥区别,以及能不能直接将一对一音视频通话实现的功能直接移植过来。最后,再加一个弹幕功能,与主播实现实时互动效果。
注意:该节内容还是在一对一音视频的代码基础上修改的!
分析
考虑一下,一对多直播模式其实就是一个人和多个人建立连接,每当有人进入房间时,就自动和当前主播发起连接请求。本质上来说,一对多直播模式还是一对一音视频,只是一个人连接了多次而已。但是又有点不同,不同点在于主播看不到所有与他连接的观众,而观众可以看到主播。所以,当观众与主播连接时,观众不需要发送音视频给主播,只需要接收主播的音视频即可。而主播需要将他的音视频发送给每个连接的观众,不用接收观众的音视频。
这就是不同点,一对一是可收可发,一对多是观众只收不发。
那么如何才能实现上面这种效果呢?这就需要用到PeerConnection对象中的addTransceiver方法。官方地址在这里,想深入了解的可以前往。
这个addTransceiver方法在文档中的定义如下:
partial interface RTCPeerConnection {
RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind,
optional RTCRtpTransceiverInit init = {});
};
第一个参数:可以是一个媒体轨道,或者是"audio"和"video"中的一个。如果第一个参数是字符串,但不是"audio"和"video"中的一个,则会抛出错误。
第二个参数则有多个属性,定义如下:
dictionary RTCRtpTransceiverInit {
RTCRtpTransceiverDirection direction = "sendrecv";
sequence<MediaStream> streams = [];
sequence<RTCRtpEncodingParameters> sendEncodings = [];
};
direction: 可选的值为"sendrecv"、"sendonly"、"recvonly"、"inactive"、"stopped"。默认为"sendrecv"。
streams: 当远程 PeerConnection 的跟踪事件对应于添加的 RTCRtpReceiver 触发时,这些是将放入事件中的流。(尚未真正理解,但目前用不上这个)。
sequence: 包含用于发送媒体 RTP 编码的参数的序列。
这里重点关注direction的值,"sendrecv"为 可接收可发送,"sendonly"为 只发送,"recvonly"为 只接收,"inactive"为 非活动,即两边都关闭,"stopped"为 停止,两边都停止任何活动。
对于主播来说,可接收可发送。但对于观众来说,则只能是只接收了。
还有一个点要注意,看文档中的这段描述:
Create a new RTCRtpTransceiver and add it to the set of transceivers.
Adding a transceiver will cause future calls to createOffer to add a media description for the corresponding transceiver, as defined in [RFC8829] (section 5.2.2.).
大致意思就是新建一个收发器,然后这个收发器将会影响到创建的offer。但如果在已经建立连接的过程中执行了这个方法,不知道会不会导致媒体重新协商,有待尝试。还有,这种方式能不能直接用于关闭/开始麦克风/摄像头呢?理论上来说也是可行的,有待尝试。机智如我,要是可行的话,那么这种方式将是最简单的控制麦克风/摄像头的方法了,有空试一试。
上面还提到一个弹幕功能,实现这个功能用到了danmaku这个安装包,具体文档地址可前往这里,就不再详细介绍了。
实操
api已经知道了,再来捋一遍过程。首先是区分主播和观众,将房间内带有pub标识的用户当成主播,而其他的则为观众。这样,当主播进入房间后,观众自动连接主播,后进入的观众也是如此。然后,主播的话,则可以接收和发送音视频;而观众则设置为只接收音视频。
根据上面这个过程,只需要在一对一音视频的基础上做一些简单的改动即可,代码如下:
- 新加上
pub属性,标识主播
onMounted(() => {
// 带有pub表示为发布者,即主播
// 观众是不带有pub的
const { nickName, roomId, userId, pub } = props;
Object.assign(userInfo, { nickName, roomId, userId, pub });
init(nickName, roomId, userId, pub);
// 这个是虚拟背景用到
nextTick(() => {
videoDom = document.getElementById("videoDom");
if (pub) {
canvasDom = document.getElementById("canvasDom");
canvasCtx = canvasDom.getContext("2d");
}
});
});
- 当拿到房间内所有用户时,找出主播并连接
linkSocket.value.on("roomUserList", async (data) => {
roomUserList.value = data;
if (roomUserList.value.length) {
await initMeetingRoomPc();
initDanmuContainer();
}
});
const initMeetingRoomPc = async () => {
if (userInfo.pub) {
localStream.value = await getLocalUserMedia({ video: true, audio: true });
//将本地直播流挂到video标签,在自己的页面显示
setDomVideoStream("video", localStream.value);
}
const localUserId = userInfo.userId;
// 找到当前房间的视频流发布者,即主播
const pub = roomUserList.value.find((item) => item.pub === "pub");
if (!pub) {
return;
}
// 如果是自己 且 自己是主播 则直接返回
if (pub.userId === localUserId) {
return;
}
publisher.value = pub;
// 和发布者建立rtc连接 不发送自己的视频流
const pcKey = localUserId + "-" + publisher.value.userId;
let pc = RtcPcMaps.get(pcKey);
if (!pc) {
pc = new PeerConnection();
RtcPcMaps.set(pcKey, pc);
}
// 设置收发器 设置为只接收
pc.addTransceiver("audio", { direction: "recvonly" });
pc.addTransceiver("video", { direction: "recvonly" });
onPcEvent(pc, localUserId, publisher.value.userId);
// 创建数据通道
await createDataChannels(pc, localUserId, publisher.value.userId);
// 创建offer
const offer = await pc.createOffer();
// 设置offer为本地描述
await pc.setLocalDescription(offer);
// 发送offer给远端
const params = {
userId: localUserId,
targetUserId: publisher.value.userId,
offer,
};
linkSocket.value.emit("offer", params);
};
这里不同于一对一,而是先找到主播,进行判断。没有主播则不进行连接,如果主播是自己也是不用连接。找到后再设置下收发器,再进行连接。后面连接的过程和一对一一音视频是一样的,就不再说了。
到这里其实就完成了一对多直播模式的音视频通话了,非常简单。和一对一的区别只有两点,一是每个用户进入之后,主动找到主播并进行连接。二是观众的收发器是只进行接收的。除此之外,其他没有区别了。所以,再来回答开头那个问题,之前在一对一音视频通话基础上拓展的功能也是可以移植过来的,没啥问题。
弹幕实现
新增弹幕功能是为了能增加互动效果,没有弹幕光看直播,感觉怪怪的。
弹幕的api如何使用就不在说了,我看文档里的使用写的很清楚了,地址是这里。重点是实现的思路,有以下两种方式:
- 使用
PeerConnection.channel:
当观众给主播发送消息后,主播收到消息,再遍历所有建立连接的channel单独发送消息。推荐使用这种方式,即使当你服务器挂了,观众还是可以正常发送弹幕。
- 使用
socket.io:
观众利用socket的广播来实现,发送消息后触发msg事件,然后后端监听该事件,执行io.to(roomKey).emit('msg',msg);即可达到房间内全员接收的效果。这种方式实现很简单,一行代码即可搞定。但是需要服务器的配合,一旦服务器出现问题就没法实现了。
关于第二种方式就不写了,很简单的。这里看看第一种实现,代码如下:
这是html部分,加了个弹幕的按钮
<div class="content" ref="content">
<div class="videoWrap" ref="videoWrap">
<div v-if="userInfo.pub === 'pub'">
<video autoplay controls muted class="demo1" id="videoDom"></video>
<canvas
class="canvas"
id="canvasDom"
width="800px"
height="720px"
></canvas>
</div>
<video
ref="remoteVideoRef"
autoplay
controls
muted
class="demo1"
v-else
></video>
</div>
<div class="barrageWrap" v-if="userInfo.pub !== 'pub'">
<el-input v-model="barrage" placeholder="发送弹幕" />
<el-button type="primary" @click="sendMsgToPub">发送弹幕</el-button>
</div>
</div>
关于js部分
第一步是初始化弹幕容器,这里加了一条自动弹幕,当初始化之后会自动出现
// 初始化弹幕容器
const initDanmuContainer = () => {
danmaku.value = new Danmaku({
container: videoWrap.value,
speed: 30,
});
// 首条弹幕
danmaku.value.emit({
text: "欢迎进入直播间,发个弹幕试试",
style: {
color: "red",
fontSize: "16px",
marginTop: "20px",
},
});
};
第二步是发送弹幕,也就是观众给主播发,主播给所有连接的用户发
pc.ondatachannel = (e) => {
console.log("收到数据通道", e);
e.channel.onopen = () => {
console.log("通道打开");
};
e.channel.onclose = () => {
console.log("通道关闭");
};
e.channel.onmessage = (data) => {
console.log("收到消息", data.data);
// 弹幕发送到屏幕上
onAllMessage(data.data);
};
};
// 指定数据通道发送数据
const clientDataChannelMsg = (userId, targetUserId, msg) => {
const channel = dataChannelMap.get(userId + "-" + targetUserId);
if (channel) {
channel.send(msg);
}
};
// 发送弹幕
const sendMsgToPub = () => {
// 不是发布者 则给发布者发送消息 发布者收到再广播给其他客户端
clientDataChannelMsg(userInfo.userId, publisher.value.userId, barrage.value);
barrage.value = "";
};
// 广播消息
const onAllMessage = (msg) => {
danmuForRoller(msg);
if (userInfo.pub === "pub") {
// 如果是发布者 则遍历所有数据通道给每个客户端发送消息
dataChannelMap.forEach((value, key) => {
if (value.readyState === "open") {
value.send(msg);
} else {
// 处理通道未打开的情况,可以等待通道打开后再发送数据
value.onopen = () => {
value.send(msg);
};
}
});
}
};
// 直播弹幕留言
const danmuForRoller = (msg) => {
if (danmaku.value) {
danmaku.value.emit({
text: msg,
style: { color: "red", fontSize: "20px" },
});
}
};
当观众执行sendMsgToPub方法后,通过channel给主播发送消息。主播则会在e.channel.onmessage方法中监听并接收到消息。然后主播触发onAllMessage事件,在这个事件中先显示弹幕,然后判断是否为主播,如果是主播则再进行消息广播。然后观众再通过e.channel.onmessage方法中监听并接收到消息;不是主播,则只需要显示弹幕就行。
如果是主播自己发送的弹幕,则可以直接调用onAllMessage方法了。
第二种方式代码量确实比第一种多一些,逻辑也复杂一些。但它有一个最大的好处就是连接不断,就能一直发送弹幕,不受服务器影响。
小节
本小节介绍了如何实现一对多直播模式,只需在一对一的基础上进行小小的改动,一是将手动连接改为进入房间后自动连接;二是区分观众和主播,将观众的收发器改为只接收。这样就轻松的实现了一对多直播模式了。所以,一对一的所有功能也都可以在一对多上实现。然后,新增了弹幕功能,增加了趣味性和互动性。
下一小节再来看看如何实现多人多房间,以及有哪些方式可以实现,冲!!!