|
@@ -1,46 +1,54 @@
|
|
|
<template>
|
|
|
-
|
|
|
<div class="video-group-container">
|
|
|
-
|
|
|
- <div v-for="video in videos" :key="video.userId" class="video-card">
|
|
|
- <!-- 遍历渲染多人音视频控件 -->
|
|
|
- <div class="group-video-container">
|
|
|
- <video autoplay class="group-video" :srcObject="video.stream"></video>
|
|
|
- <span class="video-username">{{video.userName}}</span>
|
|
|
- </div>
|
|
|
+ <video autoplay class="select-video" ref="selectStream"></video>
|
|
|
+ <div class="video-list">
|
|
|
+ <div v-for="video in videos" :key="video.userId" class="video-card">
|
|
|
+ <!-- 遍历渲染多人音视频控件 -->
|
|
|
+ <div :class="['group-video-container', playerUserId === video.userId ? 'playerVideo' : 'defaultVideo']" @click="selectVideoPlay(video)">
|
|
|
+ <video autoplay class="group-video" :srcObject="video.stream"></video>
|
|
|
+ <span class="video-username">{{video.userName}}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
-
|
|
|
-
|
|
|
- <div style="clear: both;"></div>
|
|
|
<!-- 挂断通话 -->
|
|
|
- <svg class="leave-btn icon" t="1739368256646" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3572" width="40" height="40"><path d="M0 512a512 512 0 1 0 1024 0A512 512 0 1 0 0 512z" fill="#F75855" p-id="3573"></path><path d="M502.24 414.2c78.71 2.19 155.93 11.61 225.57 52.4 46.19 27.06 59.2 52.68 52.6 96.3-4.54 30-21.84 44.86-51.68 46.7-58.81 3.61-84.16-15.87-90.44-72.24-2.11-18.94-14-26.82-29.03-30.22-63.34-14.31-127.14-13.52-190.84-2.28-20.32 3.59-29.81 17.2-28.68 37.58 2.13 38.35-20.49 54.18-54.04 60.39-41.35 7.65-70-3.01-84.89-31.53-15.68-30.04-10.26-65.69 14.51-90.09 28.34-27.91 64.05-42.34 101.72-51.48 44.29-10.76 89.33-17.04 135.2-15.53z" fill="#FEFEFE" p-id="3574"></path></svg>
|
|
|
+ <svg class="leave-btn icon" @click="leave()" style="margin-left: -120px;" t="1739368256646" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3572" width="40" height="40"><path d="M0 512a512 512 0 1 0 1024 0A512 512 0 1 0 0 512z" fill="#F75855" p-id="3573"></path><path d="M502.24 414.2c78.71 2.19 155.93 11.61 225.57 52.4 46.19 27.06 59.2 52.68 52.6 96.3-4.54 30-21.84 44.86-51.68 46.7-58.81 3.61-84.16-15.87-90.44-72.24-2.11-18.94-14-26.82-29.03-30.22-63.34-14.31-127.14-13.52-190.84-2.28-20.32 3.59-29.81 17.2-28.68 37.58 2.13 38.35-20.49 54.18-54.04 60.39-41.35 7.65-70-3.01-84.89-31.53-15.68-30.04-10.26-65.69 14.51-90.09 28.34-27.91 64.05-42.34 101.72-51.48 44.29-10.76 89.33-17.04 135.2-15.53z" fill="#FEFEFE" p-id="3574"></path></svg>
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
import axios from 'axios';
|
|
|
- import { inject, defineComponent, ref, onMounted, onUnmounted, watch, onBeforeUnmount } from 'vue';
|
|
|
+ import { defineEmits, inject, defineComponent, ref, onMounted, onUnmounted, watch, onBeforeUnmount } from 'vue';
|
|
|
export default defineComponent({
|
|
|
props: {
|
|
|
openVideo: Boolean,
|
|
|
selectChatUserRef: Object
|
|
|
},
|
|
|
- setup(props) {
|
|
|
-
|
|
|
+ setup(props, { emit }) {
|
|
|
+
|
|
|
const socket = ref(null);
|
|
|
const messageQueue = ref([]); // 存储未发送的消息队列
|
|
|
const isConnected = ref(false);
|
|
|
const reconnectInterval = ref(5000); // 重连间隔时间,单位:毫秒
|
|
|
const heartbeatInterval = ref(30000); // 心跳间隔时间,单位:毫秒
|
|
|
-
|
|
|
+
|
|
|
const loginUser = ref(inject('loginUser'));
|
|
|
+ // 反转下角色,由接听人来发offer信息
|
|
|
+ const offerUser = ref(props.openVideo);
|
|
|
const selectChatUser = ref(props.selectChatUserRef);
|
|
|
// 本地音视频流
|
|
|
- const localVideoRef = ref(null)
|
|
|
const localStream = ref(null)
|
|
|
-
|
|
|
+ const playerUserId = ref(loginUser.value.userId)
|
|
|
+ const selectStream = ref(null)
|
|
|
+ // 标记当前在线所有用户
|
|
|
+ const onlineUserIds = ref([])
|
|
|
+ const onlineUsernames = ref({})
|
|
|
+ const exchangeUserId = ref("")
|
|
|
const videos = ref([])
|
|
|
+ // 当前已经完成peeConnection连接管理中的用户id
|
|
|
+ const connectedUserIds = ref([])
|
|
|
+ // 维护当前客户端与其他音视频在线用户的peerConnection对象
|
|
|
+ const peerConnnections = ref({})
|
|
|
|
|
|
// 配置ICE Server(用于网络穿透)
|
|
|
const iceServerConfiguration = {
|
|
@@ -51,73 +59,49 @@
|
|
|
]
|
|
|
}
|
|
|
|
|
|
- // 1. 每个用户进度,先获取本地媒体流
|
|
|
- // const peerConn = ref(null);
|
|
|
+ // 每个用户进度,先获取本地媒体流
|
|
|
// 修改点 1:使用标准的 RTCPeerConnection
|
|
|
- // const PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
|
|
|
+ const PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
|
|
|
+ // 获取当前客户端与指定客户端uid端到端之间的音视频连接对象
|
|
|
+ // 如果连接存在,不重复创建,直接服用,反之则创建新链接对象
|
|
|
+ const getPeerConnetion = (uid) => {
|
|
|
+ let peerConntion = null;
|
|
|
+ let peerConnKey = getPeerConnId(uid);
|
|
|
+ if(peerConnnections.value[peerConnKey]) {
|
|
|
+ peerConntion = peerConnnections.value[peerConnKey];
|
|
|
+ return peerConntion;
|
|
|
+ }
|
|
|
+ peerConntion = new PeerConnection(iceServerConfiguration);
|
|
|
+ peerConnnections.value[peerConnKey] = peerConntion;
|
|
|
+ return peerConntion;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 两两端之间音视频连接唯一标识获取
|
|
|
+ const getPeerConnId = (uid) => {
|
|
|
+ let peerConnKeyArr = [loginUser.value.userId, uid];
|
|
|
+ return peerConnKeyArr.sort().join('-');
|
|
|
+ }
|
|
|
+
|
|
|
// 修改点 2:重构初始化本地流的逻辑
|
|
|
const initLocalStream = async () => {
|
|
|
-
|
|
|
- // 作为呼叫方呼叫
|
|
|
- sendObjMessage({
|
|
|
- type: "call",
|
|
|
- userId: loginUser.value.userId,
|
|
|
- groupId: selectChatUser.value.userId
|
|
|
- });
|
|
|
-
|
|
|
try {
|
|
|
// 获取本地媒体流
|
|
|
- // localStream.value = await navigator.mediaDevices.getUserMedia({
|
|
|
- // video: true,
|
|
|
- // audio: true
|
|
|
- // });
|
|
|
-
|
|
|
- // videos.value.push({
|
|
|
- // userId: loginUser.value.userId,
|
|
|
- // userName: loginUser.value.userName,
|
|
|
- // stream: localStream.value
|
|
|
- // })
|
|
|
- // 初始化 PeerConnection
|
|
|
- // peerConn.value = new PeerConnection(iceServerConfiguration);
|
|
|
- // // 添加本地轨道(替代已废弃的 addStream)
|
|
|
- // localStream.value.getTracks().forEach(track => {
|
|
|
- // peerConn.value.addTrack(track, localStream.value);
|
|
|
- // });
|
|
|
- // // 修改点 3:使用现代 ontrack 事件监听
|
|
|
- // peerConn.value.ontrack = (event) => {
|
|
|
- // if (event.streams && event.streams.length > 0) {
|
|
|
- // remoteStream.value = event.streams[0];
|
|
|
- // remoteVideoRef.value.srcObject = remoteStream.value;
|
|
|
- // }
|
|
|
- // };
|
|
|
- // // ICE 候选处理
|
|
|
- // peerConn.value.onicecandidate = (event) => {
|
|
|
- // if (event.candidate) {
|
|
|
- // sendObjMessage({
|
|
|
- // type: "candidate",
|
|
|
- // candidate: event.candidate,
|
|
|
- // userId: loginUser.value.userId,
|
|
|
- // targetUserId: selectChatUser.value.userId
|
|
|
- // });
|
|
|
- // }
|
|
|
- // };
|
|
|
- // // 创建并发送 Offer
|
|
|
- // if (offerUser.value) {
|
|
|
- // const offer = await peerConn.value.createOffer();
|
|
|
- // await peerConn.value.setLocalDescription(offer);
|
|
|
- // sendObjMessage({
|
|
|
- // type: "offer",
|
|
|
- // offer: offer,
|
|
|
- // userId: loginUser.value.userId,
|
|
|
- // targetUserId: selectChatUser.value.userId
|
|
|
- // });
|
|
|
- // alert(selectChatUser.value.userName);
|
|
|
- // }
|
|
|
+ localStream.value = await navigator.mediaDevices.getUserMedia({
|
|
|
+ video: true,
|
|
|
+ audio: true
|
|
|
+ });
|
|
|
+ selectStream.value.srcObject = localStream.value;
|
|
|
+ videos.value.push({
|
|
|
+ userId: loginUser.value.userId,
|
|
|
+ userName: "我",
|
|
|
+ stream: localStream.value
|
|
|
+ })
|
|
|
+ initSignalServerConnection();
|
|
|
} catch (error) {
|
|
|
console.error('初始化本地流失败:', error);
|
|
|
}
|
|
|
};
|
|
|
-
|
|
|
+ initLocalStream();
|
|
|
|
|
|
// 初始化信令服务器连接(ws)
|
|
|
const initSignalServerConnection = () => {
|
|
@@ -129,25 +113,29 @@
|
|
|
messageQueue.value.forEach(message => socket.value.send(message));
|
|
|
messageQueue.value = [];
|
|
|
startHeartbeat();
|
|
|
- // 调起本地摄像头,初始化音视频流
|
|
|
- // 初始化用户本地音视频资源
|
|
|
- initLocalStream();
|
|
|
};
|
|
|
|
|
|
socket.value.onmessage = (event) => {
|
|
|
- console.log('收到WS消息:', event.data);
|
|
|
const messageData = JSON.parse(event.data);
|
|
|
+ console.log('收到WS消息 messageData.type:' + messageData.type);
|
|
|
switch(messageData.type) {
|
|
|
+ case "online":
|
|
|
+ handleUserOnline(messageData);
|
|
|
+ break;
|
|
|
+ case "offline":
|
|
|
+ handleUserOffline(messageData);
|
|
|
+ break;
|
|
|
case "offer":
|
|
|
- handleOffer(messageData.offer);
|
|
|
+ handleOffer(messageData.userId, messageData.offer);
|
|
|
break;
|
|
|
case "answer":
|
|
|
- handleAnswer(messageData.answer);
|
|
|
+ handleAnswer(messageData.userId, messageData.answer);
|
|
|
break;
|
|
|
case "candidate" :
|
|
|
- handleCandidate(messageData.candidate);
|
|
|
+ handleCandidate(messageData.userId, messageData.candidate);
|
|
|
break;
|
|
|
case "leave" :
|
|
|
+
|
|
|
break;
|
|
|
}
|
|
|
};
|
|
@@ -166,23 +154,22 @@
|
|
|
// 关闭当前连接
|
|
|
socket.value.close();
|
|
|
// 尝试重连
|
|
|
- setTimeout(connectWebSocket, reconnectInterval.value);
|
|
|
+ setTimeout(initSignalServerConnection, reconnectInterval.value);
|
|
|
};
|
|
|
|
|
|
socket.value.onclose = () => {
|
|
|
console.log('WebSocket 已关闭');
|
|
|
isConnected.value = false;
|
|
|
// 尝试重连
|
|
|
- setTimeout(connectWebSocket, reconnectInterval.value);
|
|
|
+ setTimeout(initSignalServerConnection, reconnectInterval.value);
|
|
|
};
|
|
|
}
|
|
|
-
|
|
|
// 初始化ws连接
|
|
|
- initSignalServerConnection();
|
|
|
+ // initSignalServerConnection();
|
|
|
|
|
|
const sendObjMessage = (obj) => {
|
|
|
sendMessage(JSON.stringify(obj))
|
|
|
- }
|
|
|
+ }
|
|
|
const sendMessage = (message) => {
|
|
|
if (isConnected.value) {
|
|
|
socket.value.send(message);
|
|
@@ -190,6 +177,206 @@
|
|
|
messageQueue.value.push(message);
|
|
|
}
|
|
|
};
|
|
|
+ // 处理用户上线
|
|
|
+ const handleUserOnline = (message) => {
|
|
|
+ exchangeUserId.value = message.userId;
|
|
|
+ onlineUserIds.value = message.onlineUserIds;
|
|
|
+ onlineUsernames.value = message.onlineUsernames;
|
|
|
+ // 处理call类型消息
|
|
|
+ if(message.onlineUserIds.length === 1) {
|
|
|
+ // 认为当前客户端为第一个加入音视频房间的用户,由当前客户端发起call消息(呼叫所有群成员)
|
|
|
+ // 作为呼叫方呼叫
|
|
|
+ sendObjMessage({
|
|
|
+ type: "call",
|
|
|
+ userId: loginUser.value.userId,
|
|
|
+ groupId: selectChatUser.value.userId
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // 基于Mesh模式,实现两两端到端音视频通信连接
|
|
|
+ handlePeerConnByOnlineUsersMessage();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 用户上线,基于Mesh模式实现多人音视频链接管理
|
|
|
+ const handlePeerConnByOnlineUsersMessage = async () => {
|
|
|
+ for(var i = 0; i < onlineUserIds.value.length; i ++) {
|
|
|
+ // 遍历当前所有音视频在线用户
|
|
|
+ let uid = onlineUserIds.value[i];
|
|
|
+ if (connectedUserIds.value.includes(uid)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // 表示当前远程用户已被当前客户端建立音视频连接接通道,避免后续重复创建
|
|
|
+ connectedUserIds.value.push(uid);
|
|
|
+ if (uid === loginUser.value.userId) {
|
|
|
+ // 当前用户自己
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ let uname = onlineUsernames.value[uid];
|
|
|
+ let peerConn = getPeerConnetion(uid);
|
|
|
+ // 添加本地轨道(替代已废弃的 addStream)
|
|
|
+ localStream.value.getTracks().forEach(track => {
|
|
|
+ peerConn.addTrack(track, localStream.value);
|
|
|
+ });
|
|
|
+ // 创建offer
|
|
|
+ // 这里来保证每两个客户端之间只有一个端来发起offer
|
|
|
+ // 对两个用户uid做自然排序,永远都只允许自排序更靠前的这个客户端来发起offer
|
|
|
+ let p2pClients = [loginUser.value.userId, uid];
|
|
|
+ let sortFirstUserId = p2pClients.sort()[0];
|
|
|
+ if (uid == sortFirstUserId) {
|
|
|
+ const offer = await peerConn.createOffer();
|
|
|
+ peerConn.setLocalDescription(offer);
|
|
|
+ sendObjMessage({
|
|
|
+ type: "offer",
|
|
|
+ offer: offer,
|
|
|
+ userId: loginUser.value.userId,
|
|
|
+ targetUserId: uid,
|
|
|
+ groupId: selectChatUser.value.userId
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 监听获取远程流
|
|
|
+ peerConn.ontrack = (event) => {
|
|
|
+ if (event.streams && event.streams.length > 0) {
|
|
|
+ const newStream = event.streams[0];
|
|
|
+ const existingVideoIndex = videos.value.findIndex(video => video.userId === uid);
|
|
|
+ if (existingVideoIndex !== -1) {
|
|
|
+ // 如果已经存在相同uid的对象,更新stream属性
|
|
|
+ videos.value[existingVideoIndex].stream = newStream;
|
|
|
+ } else {
|
|
|
+ // 如果不存在相同uid的对象,新增一个对象
|
|
|
+ videos.value.push({
|
|
|
+ userId: uid,
|
|
|
+ userName: uname,
|
|
|
+ stream: newStream
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ // ICE 候选处理
|
|
|
+ peerConn.onicecandidate = (event) => {
|
|
|
+ console.log("ICE Candidate:", event.candidate?.candidate);
|
|
|
+ if (event.candidate) {
|
|
|
+ sendObjMessage({
|
|
|
+ type: "candidate",
|
|
|
+ candidate: event.candidate,
|
|
|
+ userId: loginUser.value.userId,
|
|
|
+ targetUserId: uid,
|
|
|
+ groupId: selectChatUser.value.userId
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+ peerConn.oniceconnectionstatechange = () => {
|
|
|
+ console.log("ICE state:", peerConn.iceConnectionState);
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5:完善处理 Offer 的逻辑
|
|
|
+ const handleOffer = async (uid, offer) => {
|
|
|
+ try {
|
|
|
+ let peerConn = getPeerConnetion(uid);
|
|
|
+ if (offer && offer.type && offer.sdp) {
|
|
|
+ await peerConn.setRemoteDescription(new RTCSessionDescription(offer));
|
|
|
+ const answer = await peerConn.createAnswer();
|
|
|
+ await peerConn.setLocalDescription(answer);
|
|
|
+ sendObjMessage({
|
|
|
+ type: "answer",
|
|
|
+ answer: answer,
|
|
|
+ userId: loginUser.value.userId,
|
|
|
+ targetUserId: uid,
|
|
|
+ groupId: selectChatUser.value.userId
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ console.error("无效的 offer 参数");
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理 Offer 失败:", error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 完善处理 Answer 的逻辑
|
|
|
+ const handleAnswer = async (uid, answer) => {
|
|
|
+ try {
|
|
|
+ let peerConn = getPeerConnetion(uid);
|
|
|
+ // 检查状态是否为 have-local-offer
|
|
|
+ if (peerConn.signalingState !== "have-local-offer") {
|
|
|
+ console.warn("状态错误,当前状态:", peerConn.signalingState);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await peerConn.setRemoteDescription(new RTCSessionDescription(answer));
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理 Answer 失败:", error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleCandidate = async (uid, candidate) => {
|
|
|
+ try {
|
|
|
+ let peerConn = getPeerConnetion(uid);
|
|
|
+ if (peerConn.iceConnectionState === 'closed') {
|
|
|
+ console.error("PeerConnection 已关闭,无法添加 ICE 候选");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!candidate || typeof candidate !== 'object' || !candidate.candidate || !candidate.sdpMid) {
|
|
|
+ console.error("Invalid ICE candidate:", candidate);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ await peerConn.addIceCandidate(candidate);
|
|
|
+ console.log("成功添加 ICE 候选");
|
|
|
+ } catch (error) {
|
|
|
+ console.error("处理 ICE 候选失败:", error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理用户离线
|
|
|
+ const handleUserOffline = (message) => {
|
|
|
+ exchangeUserId.value = message.userId;
|
|
|
+ onlineUserIds.value = message.onlineUserIds;
|
|
|
+ videos.value = videos.value.filter(v => v.userId !== exchangeUserId.value);
|
|
|
+ const mesage = onlineUsernames.value[message.userId] + "退出";
|
|
|
+ const firstVideo = videos.value[0];
|
|
|
+ if (message.userId === playerUserId.value) {
|
|
|
+ playerUserId.value = firstVideo.userId;
|
|
|
+ selectStream.value.scrObject = firstVideo.stream;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const selectVideoPlay = (video) => {
|
|
|
+ playerUserId.value = video.userId;
|
|
|
+ selectStream.value.srcObject = video.stream;
|
|
|
+ }
|
|
|
+
|
|
|
+ const leave = () => {
|
|
|
+ cleanup();
|
|
|
+ // 触发关闭事件
|
|
|
+ emit('close', 'close');
|
|
|
+ }
|
|
|
+
|
|
|
+ const cleanup = () => {
|
|
|
+ // WebSocket 清理
|
|
|
+ if (socket.value) {
|
|
|
+ try {
|
|
|
+ if ([WebSocket.OPEN, WebSocket.CONNECTING].includes(socket.value.readyState)) {
|
|
|
+ socket.value.close()
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error('关闭 WebSocket 失败:', err)
|
|
|
+ } finally {
|
|
|
+ socket.value = null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // WebRTC 清理
|
|
|
+ if (peerConnnections.value) {
|
|
|
+ peerConnnections.value = null
|
|
|
+ }
|
|
|
+ // 媒体流清理 (关键补充)
|
|
|
+ if (localStream.value) {
|
|
|
+ localStream.value.getTracks().forEach(track => {
|
|
|
+ track.stop() // 必须停止每个媒体轨道
|
|
|
+ })
|
|
|
+ localStream.value = null
|
|
|
+ }
|
|
|
+ // 发送终止信令 (重要)
|
|
|
+ // TODO
|
|
|
+ }
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
cleanup();
|
|
@@ -202,10 +389,21 @@
|
|
|
});
|
|
|
|
|
|
return {
|
|
|
+ playerUserId,
|
|
|
+ selectStream,
|
|
|
+ onlineUserIds,
|
|
|
selectChatUser,
|
|
|
videos,
|
|
|
+ onlineUsernames,
|
|
|
+ leave,
|
|
|
initLocalStream,
|
|
|
- initSignalServerConnection
|
|
|
+ getPeerConnetion,
|
|
|
+ getPeerConnId,
|
|
|
+ initSignalServerConnection,
|
|
|
+ handleOffer,
|
|
|
+ handleAnswer,
|
|
|
+ handleCandidate,
|
|
|
+ selectVideoPlay
|
|
|
};
|
|
|
}
|
|
|
});
|
|
@@ -213,20 +411,39 @@
|
|
|
</script>
|
|
|
|
|
|
<style>
|
|
|
-
|
|
|
+ .video-group-container {
|
|
|
+ width: 1000px;
|
|
|
+ height: 600px;
|
|
|
+ overflow: hidden;
|
|
|
+ background-color: black;
|
|
|
+ }
|
|
|
+ .video-list {
|
|
|
+ position: absolute;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 5px;
|
|
|
+ background-color:rgb(18, 20, 21);
|
|
|
+ top: 50px;
|
|
|
+ right: 20px;
|
|
|
+ width: 230px;
|
|
|
+ height: 615px;
|
|
|
+ overflow-y: scroll;
|
|
|
+ }
|
|
|
.group-video-container {
|
|
|
position: relative;
|
|
|
- float: left;
|
|
|
- width: 250px;
|
|
|
- height: 200px;
|
|
|
+ width: 200px;
|
|
|
+ height: 150px;
|
|
|
overflow: hidden;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ border: 2px solid rgb(18, 20, 21);
|
|
|
+ }
|
|
|
+ .playerVideo {
|
|
|
+ border: 2px solid white;
|
|
|
}
|
|
|
.group-video-container video {
|
|
|
position: absolute;
|
|
|
left: 0px;
|
|
|
top: 0px;
|
|
|
- width: 250px;
|
|
|
- height: 200px;
|
|
|
+ width: 200px;
|
|
|
}
|
|
|
.video-username {
|
|
|
position: absolute;
|
|
@@ -238,5 +455,31 @@
|
|
|
padding: 3px 15px;
|
|
|
border-radius: 50px;
|
|
|
}
|
|
|
-
|
|
|
+ .select-video {
|
|
|
+ position: absolute;
|
|
|
+ left: 25px;
|
|
|
+ top: 50px;
|
|
|
+ width: 750px;
|
|
|
+ }
|
|
|
+ .video-list::-webkit-scrollbar {/*滚动条整体样式*/
|
|
|
+ width: 0px; /*高宽分别对应横竖滚动条的尺寸*/
|
|
|
+ height: 0px;
|
|
|
+ }
|
|
|
+ .video-list::-webkit-scrollbar-thumb {/*滚动条里面小方块*/
|
|
|
+ border-radius: 10px;
|
|
|
+ background-color: transparent;
|
|
|
+ background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, transparent 75%, transparent);
|
|
|
+ }
|
|
|
+ .video-list::-webkit-scrollbar-track {/*滚动条里面轨道*/
|
|
|
+ -webkit-box-shadow: inset 0 0 1px rgba(221, 35, 35, 0.2);
|
|
|
+ border-radius: 10px;
|
|
|
+ background: #EDEDED;
|
|
|
+ }
|
|
|
+ .leave-btn {
|
|
|
+ position: absolute;
|
|
|
+ left: 48%;
|
|
|
+ bottom: 20px;
|
|
|
+ border: 2px solid rgb(239, 133, 133);
|
|
|
+ border-radius: 100px;
|
|
|
+ }
|
|
|
</style>
|