js-test-meeting
Version:
我创建的第一个组件库
788 lines (787 loc) • 25.6 kB
JavaScript
var ke = Object.defineProperty;
var we = (w, e, t) => e in w ? ke(w, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : w[e] = t;
var g = (w, e, t) => we(w, typeof e != "symbol" ? e + "" : e, t);
import { defineComponent as Se, ref as v, watch as ie, onMounted as ye, onBeforeUnmount as Ie, resolveComponent as I, resolveDirective as be, createElementBlock as f, openBlock as p, Fragment as L, createElementVNode as a, createVNode as r, withDirectives as ne, createBlock as M, withCtx as k, renderList as z, toDisplayString as O, createTextVNode as q, normalizeClass as Ce, createCommentVNode as _e } from "vue";
import { _ as De } from "./_plugin-vue_export-helper-CHgC5LLL.js";
class Ue {
constructor(e, t, i) {
g(this, "connections", /* @__PURE__ */ new Map());
this.localStream = e, this.onTrack = t, this.sendSignal = i;
}
async createPeerConnection(e) {
const t = new RTCPeerConnection();
this.localStream.getTracks().forEach((i) => t.addTrack(i, this.localStream)), t.onicecandidate = (i) => {
i.candidate && this.sendSignal(e, { type: "ice-candidate", candidate: i.candidate });
}, t.ontrack = (i) => {
const l = new MediaStream([i.track]);
this.onTrack(e, l);
}, this.connections.set(e, t);
}
getPeerConnection(e) {
return this.connections.get(e);
}
async handleOffer(e, t) {
const i = this.connections.get(e);
if (!i)
return;
await i.setRemoteDescription(t);
const l = await i.createAnswer();
await i.setLocalDescription(l), this.sendSignal(e, { type: "answer", answer: l });
}
async handleAnswer(e, t) {
const i = this.connections.get(e);
i && await i.setRemoteDescription(t);
}
async handleIceCandidate(e, t) {
const i = this.connections.get(e);
i && await i.addIceCandidate(t);
}
closeAll() {
this.connections.forEach((e) => e.close()), this.connections.clear();
}
}
async function Ee(w, e = {}) {
const { baseURL: t = "", timeout: i = 1e4, headers: l = {}, ...u } = e, d = new AbortController(), h = setTimeout(() => d.abort(), i);
try {
const S = await fetch(t + w, {
signal: d.signal,
headers: {
"Content-Type": "application/json",
...l
},
...u
});
if (clearTimeout(h), !S.ok) {
const m = await S.text();
throw new Error(`HTTP ${S.status}: ${m}`);
}
return await S.json();
} catch (S) {
throw clearTimeout(h), new Error(S.message || "Request failed");
}
}
class Le {
constructor() {
g(this, "videoUIs", /* @__PURE__ */ new Set());
}
register(e) {
this.videoUIs.add(e);
}
unregister(e) {
this.videoUIs.delete(e);
}
setSinkIdForAll(e) {
this.videoUIs.forEach((t) => {
t.setSinkId(e);
});
}
}
const J = new Le();
class Me {
constructor(e) {
g(this, "el");
g(this, "videoElement");
this.el = e, this.videoElement = document.createElement("video"), this.videoElement.autoplay = !0, this.videoElement.playsInline = !0, this.videoElement.style.height = "100%", this.el.appendChild(this.videoElement), J.register(this);
}
async setVideo(e) {
console.log("[VideoUI] 设置视频:", e);
{
const t = this.videoElement;
t.src = "https://www.w3schools.com/html/mov_bbb.mp4", t.loop = !0, t.muted = !0, await t.play();
return;
}
}
setSinkId(e) {
(typeof this.videoElement.sinkId == "string" || "setSinkId" in this.videoElement) && this.videoElement.setSinkId(e).catch(console.error);
}
destroy() {
this.videoElement.srcObject = null, this.el.removeChild(this.videoElement), J.unregister(this);
}
}
class Ve {
constructor() {
g(this, "signaling");
g(this, "rtc");
g(this, "localStream");
g(this, "token");
g(this, "baseUrl");
g(this, "userId");
g(this, "streamUrl");
g(this, "allowAudio");
// 事件映射表:事件名 -> 回调函数数组
g(this, "eventMap", /* @__PURE__ */ new Map());
// 当前用户信息和会议 ID
g(this, "localUser");
g(this, "meetingId", "");
}
/**
* 初始化 SDK:建立信令连接并登录
*/
async init(e) {
const { serverAddr: t, userId: i, streamUrl: l } = e;
this.meetingId = "", this.token = "", this.userId = i, this.baseUrl = t || "", this.streamUrl = l || "http://192.168.40.51:8081";
}
/**
* 处理来自服务器的信令消息
*/
handleServerMessage(e) {
var u, d, h;
const { type: t, fromUserId: i, payload: l } = e;
switch (t) {
case "login-result":
this.emit("login-result", l);
break;
case "user-joined":
this.emit("user-joined", l);
break;
case "user-left":
this.emit("user-left", l);
break;
case "offer":
(u = this.rtc) == null || u.handleOffer(i, l);
break;
case "answer":
(d = this.rtc) == null || d.handleAnswer(i, l);
break;
case "ice-candidate":
(h = this.rtc) == null || h.handleIceCandidate(i, l);
break;
}
}
/**
* 获取本地媒体流(默认开启摄像头+麦克风)
*/
async startLocalStream(e = { audio: !0, video: !0 }) {
return this.localStream = await navigator.mediaDevices.getUserMedia(e), this.localStream;
}
/**
* 获取桌面共享的可用源(屏幕和窗口)
*/
async getCaptureSources() {
var e;
try {
return console.log("获取捕获源", this.baseUrl), await ((e = window.BeethovenTerminal) == null ? void 0 : e.window.getCaptureSources());
} catch (t) {
throw console.error("[SDK] 获取捕获源失败:", t), t;
}
}
/**
* 将共享屏幕id转换为共享流
*/
async getDesktopStream(e) {
try {
const t = {
audio: !1,
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: e,
maxFrameRate: 30
}
}
};
return navigator.mediaDevices.getUserMedia(t);
} catch (t) {
throw console.error("[SDK] 获取桌面流失败:", t), t;
}
}
/**
* 将共享屏幕id转换为共享流
*/
async getDeviceStream(e) {
try {
const t = { video: { deviceId: { exact: e } } };
return navigator.mediaDevices.getUserMedia(t);
} catch (t) {
throw console.error("[SDK] 获取桌面流失败:", t), t;
}
}
/**
* 加入会议房间,初始化 RTC 管理器
*/
async joinMeeting() {
if (!this.localStream)
throw new Error("Local stream not ready");
this.rtc = new Ue(
this.localStream,
// 远端用户媒体流回调
(e, t) => this.emit("stream-added", { userId: e, stream: t }),
// 向远端用户发送信令消息
(e, t) => this.signaling.send({ ...t, toUserId: e })
), this.signaling.send({
type: "join",
userId: this.localUser.userId,
meetingId: this.meetingId
});
}
/**
* 注册事件监听
*/
on(e, t) {
this.eventMap.has(e) || this.eventMap.set(e, []), this.eventMap.get(e).push(t);
}
/**
* 触发事件(内部调用)
*/
emit(e, t) {
var i;
(i = this.eventMap.get(e)) == null || i.forEach((l) => l(t));
}
/**
* 离开会议,关闭本地流和所有连接
*/
leaveMeeting() {
var e, t;
this.signaling.send({
type: "leave",
userId: this.localUser.userId,
meetingId: this.meetingId
}), (e = this.localStream) == null || e.getTracks().forEach((i) => i.stop()), (t = this.rtc) == null || t.closeAll();
}
/**
* 获取麦克风(音频输入设备)列表
*/
async getMicrophoneList() {
try {
return (await navigator.mediaDevices.enumerateDevices()).filter((i) => i.kind === "audioinput");
} catch (e) {
throw console.error("获取麦克风列表失败:", e), e;
}
}
/**
* 获取扬声器(音频输出设备)列表
*/
async getSpeakerList() {
try {
return (await navigator.mediaDevices.enumerateDevices()).filter((i) => i.kind === "audiooutput");
} catch (e) {
throw console.error("获取扬声器列表失败:", e), e;
}
}
createVideoUI(e, t) {
const i = new Me(e);
return t != null && t.notifyFrameReceived, i;
}
setSinkId(e) {
console.log("设置扬声器输出:", e), J.setSinkIdForAll(e);
}
async getCameraList() {
try {
return (await navigator.mediaDevices.enumerateDevices()).filter((i) => i.kind === "videoinput");
} catch (e) {
return console.error("获取摄像头列表失败:", e), [];
}
}
async login(e) {
return new Promise((t, i) => {
(async () => {
try {
const u = await Ee("/meeting/sdk/account/login", {
baseURL: this.baseUrl,
method: "POST",
body: JSON.stringify({ userId: e })
});
u.state ? (this.userId = e, console.log("登录成功:", this.userId), t(u)) : i(new Error("登录失败"));
} catch (u) {
i(u);
}
})();
});
}
SetHighBitrate(e) {
const u = "x-google-min-bitrate=8000;x-google-max-bitrate=10000;x-google-start-bitrate=8000", d = e.split(`
`);
let h = "";
for (let m = 0; m < d.length; m++)
d[m].startsWith("a=rtpmap:") && (d[m].includes("AV1") || d[m].includes("H264")) && (h = d[m].split(" ")[0].split(":")[1]), h && d[m].startsWith(`a=fmtp:${h}`) && (d[m] = `${d[m]};${u}`);
return d.join(`
`);
}
async pushStream(e, t) {
const i = e, l = new RTCPeerConnection();
l.oniceconnectionstatechange = () => {
console.log("oniceconnectionstatechange", l.iceConnectionState);
}, l.onicecandidate = (y) => {
console.log("onicecandidate", y.candidate);
}, i.getTracks().forEach((y) => {
l.addTrack(y, i);
});
const u = await l.createOffer();
if (!u.sdp) {
console.error("offer.sdp is null");
return;
}
const d = this.SetHighBitrate(u.sdp);
await l.setLocalDescription({ type: "offer", sdp: d });
let h = "";
t === 1 ? h = "share_" : h = "camera_";
const m = await (await fetch(
`${this.streamUrl}/webrtc/push/live/${h}${this.userId}`,
{
method: "POST",
mode: "cors",
cache: "no-cache",
credentials: "include",
redirect: "follow",
referrerPolicy: "no-referrer",
headers: { "Content-Type": "application/sdp" },
body: d
}
)).text();
console.log("remoteSdp", new RTCSessionDescription({ type: "answer", sdp: m })), await l.setRemoteDescription(
new RTCSessionDescription({ type: "answer", sdp: m })
);
}
async screenShare(e) {
const { monitorID: t, catchWnd: i } = e, l = await this.getDesktopStream(t || i);
this.pushStream(l, 1);
}
async cameraSet(e) {
const t = await this.getDeviceStream(e);
console.log("ssss", t), this.pushStream(t, 0);
}
}
const b = new Ve(), Te = { class: "tool-list" }, Re = { class: "tool-item" }, Ae = ["title"], xe = ["title"], Pe = { class: "tool-item" }, $e = ["title"], Be = { class: "tool-item" }, ze = { class: "tool-item" }, Oe = { class: "tool-item" }, je = { class: "tool-item" }, Fe = { class: "tool-item" }, Ne = { class: "tool-item" }, He = { class: "tool-item" }, Ke = { class: "share-screen-dialog" }, We = { class: "share-screen-list" }, qe = ["onClick"], Je = ["src"], Ge = { style: { "margin-top": "10px" } }, Qe = { class: "dialog-footer" }, Xe = /* @__PURE__ */ Se({
__name: "toolbar",
setup(w) {
const e = v([]), t = v("default"), i = (n) => n.replace(/\s*\([0-9a-f:]+\)\s*$/i, "").trim().replace("Default", "系统默认");
async function l() {
e.value = (await b.getMicrophoneList()).filter((n) => n.kind === "audioinput" && n.deviceId !== "communications"), e.value = e.value.map((n) => ({
deviceId: n.deviceId,
kind: n.kind,
groupId: n.groupId,
label: i(n.label)
}));
}
const u = v([]), d = v("default");
async function h() {
u.value = (await b.getSpeakerList()).filter((n) => n.kind === "audiooutput" && n.deviceId !== "communications"), u.value = u.value.map((n) => ({
deviceId: n.deviceId,
kind: n.kind,
groupId: n.groupId,
label: i(n.label)
})), console.log("扬声器列表:", u.value);
}
function S(n) {
b.setSinkId(n);
}
const m = v("default"), y = v([]);
async function ae() {
y.value = await b.getCameraList(), console.log("摄像头列表:", y.value), y.value = y.value.map((n) => ({
deviceId: n.deviceId,
kind: n.kind,
groupId: n.groupId,
label: i(n.label)
}));
}
const _ = v([]), V = v([]), T = v([]), C = v("");
async function se() {
var o;
const n = await b.getCaptureSources();
_.value = n.filter((c) => c.type === "screen"), V.value = n.filter((c) => c.type === "window"), console.log("共享屏幕列表:", _.value, V.value), T.value = _.value, C.value = ((o = _.value[0]) == null ? void 0 : o.id) || "";
}
function le(n) {
var o, c;
n === "screen" ? (T.value = _.value, C.value = ((o = _.value[0]) == null ? void 0 : o.id) || "") : n === "window" && (T.value = V.value, C.value = ((c = V.value[0]) == null ? void 0 : c.id) || "");
}
function re() {
console.log("pushShareScreen", C.value), b.screenShare({ monitorID: C.value });
}
function ce(n) {
b.cameraSet(n);
}
const j = v(!0), R = v(), F = v(!1), A = v(!1), x = v(!1), P = v(!1);
function U() {
j.value = !j.value;
}
function de(n) {
n.stopPropagation(), ae(), F.value = !0, A.value = !0, x.value = !1, P.value = !1;
}
function G() {
A.value = !1, F.value = !1;
}
const N = v(!0), $ = v();
function ue(n) {
n.stopPropagation(), l(), h(), P.value = !0, x.value = !0, A.value = !1, F.value = !1;
}
function H() {
x.value = !1, P.value = !1;
}
function Q() {
N.value = !N.value;
}
function X(n) {
var c, D;
const o = n.target;
$.value && !((c = $.value.popperRef) != null && c.contentRef.contains(o)) && H(), R.value && !((D = R.value.popperRef) != null && D.contentRef.contains(o)) && G();
}
const E = v(!1), K = v("screen");
function Y() {
}
function ve() {
console.log("toggleShareScreen", b), E.value = !0, K.value = "screen", se();
}
function me(n) {
le(n);
}
function pe(n) {
C.value = n;
}
function ge() {
}
function he() {
re(), E.value = !1;
}
return ie(
() => d.value,
(n) => {
S(n);
},
{ immediate: !0 }
), ie(
() => m.value,
(n) => {
ce(n);
},
{ immediate: !0 }
), ye(() => {
document.addEventListener("click", X);
}), Ie(() => {
document.removeEventListener("click", X);
}), (n, o) => {
const c = I("Icon"), D = I("el-radio"), B = I("el-radio-group"), W = I("el-divider"), Z = I("el-popover"), ee = I("el-radio-button"), te = I("el-button"), fe = I("el-dialog"), oe = be("popover");
return p(), f(L, null, [
a("div", Te, [
a("div", Re, [
N.value ? (p(), f("div", {
key: 0,
class: "icon-group",
onClick: Q
}, [
r(c, {
icon: "local-mic-out-round",
class: "tool-item-icon"
}),
o[6] || (o[6] = a("div", null, "静音", -1))
])) : (p(), f("div", {
key: 1,
class: "icon-group",
onClick: Q
}, [
r(c, {
icon: "local-close-mic-out-round",
class: "tool-item-icon"
}),
o[7] || (o[7] = a("div", null, "解除静音", -1))
])),
ne((p(), f("div", null, [
P.value ? (p(), M(c, {
key: 0,
icon: "local-triangle-up"
})) : (p(), M(c, {
key: 1,
icon: "local-triangle-down",
onClick: ue
}))
])), [
[oe, $.value]
])
]),
r(Z, {
ref_key: "popoverAudioRef",
ref: $,
visible: x.value,
trigger: "click",
"virtual-triggering": "",
persistent: "",
placement: "bottom",
width: "450"
}, {
default: k(() => [
o[8] || (o[8] = a("div", null, " 选择麦克风 ", -1)),
r(B, {
modelValue: t.value,
"onUpdate:modelValue": o[0] || (o[0] = (s) => t.value = s),
size: "large"
}, {
default: k(() => [
(p(!0), f(L, null, z(e.value, (s) => (p(), M(D, {
key: s.deviceId,
value: s.deviceId,
size: "large",
onClick: H
}, {
default: k(() => [
a("div", {
class: "el-radio-label",
title: s.label
}, O(s.label), 9, Ae)
]),
_: 2
}, 1032, ["value"]))), 128))
]),
_: 1
}, 8, ["modelValue"]),
r(W),
o[9] || (o[9] = a("div", null, " 选择扬声器 ", -1)),
r(B, {
modelValue: d.value,
"onUpdate:modelValue": o[1] || (o[1] = (s) => d.value = s),
size: "large"
}, {
default: k(() => [
(p(!0), f(L, null, z(u.value, (s) => (p(), M(D, {
key: s.deviceId,
value: s.deviceId,
size: "large",
onClick: H
}, {
default: k(() => [
a("div", {
class: "el-radio-label",
title: s.label
}, O(s.label), 9, xe)
]),
_: 2
}, 1032, ["value"]))), 128))
]),
_: 1
}, 8, ["modelValue"]),
r(W),
o[10] || (o[10] = a("div", null, " 音频设置 ", -1))
]),
_: 1,
__: [8, 9, 10]
}, 8, ["visible"]),
a("div", Pe, [
j.value ? (p(), f("div", {
key: 0,
class: "icon-group",
onClick: U
}, [
r(c, {
icon: "local-video-out-round",
class: "tool-item-icon"
}),
o[11] || (o[11] = a("div", null, "关摄像头", -1))
])) : (p(), f("div", {
key: 1,
class: "icon-group",
onClick: U
}, [
r(c, {
icon: "local-close-camera-out-round",
class: "tool-item-icon"
}),
o[12] || (o[12] = a("div", null, "开摄像头", -1))
])),
ne((p(), f("div", { onClick: de }, o[13] || (o[13] = [
q(" 1 ")
]))), [
[oe, R.value]
])
]),
r(Z, {
ref_key: "popoverCameraRef",
ref: R,
visible: A.value,
trigger: "click",
placement: "bottom",
"virtual-triggering": "",
persistent: "",
width: "450"
}, {
default: k(() => [
o[14] || (o[14] = a("div", null, " 选择摄像头 ", -1)),
r(B, {
modelValue: m.value,
"onUpdate:modelValue": o[2] || (o[2] = (s) => m.value = s),
size: "large"
}, {
default: k(() => [
(p(!0), f(L, null, z(y.value, (s) => (p(), M(D, {
key: s.deviceId,
value: s.deviceId,
size: "large",
onClick: G
}, {
default: k(() => [
a("div", {
class: "el-radio-label",
title: s.label
}, O(s.label), 9, $e)
]),
_: 2
}, 1032, ["value"]))), 128))
]),
_: 1
}, 8, ["modelValue"]),
r(W),
o[15] || (o[15] = a("div", null, " 视频设置 ", -1))
]),
_: 1,
__: [14, 15]
}, 8, ["visible"]),
a("div", Be, [
a("div", {
class: "icon-group",
onClick: Y
}, [
r(c, {
icon: "local-invite-out-round",
class: "tool-item-icon"
}),
o[16] || (o[16] = a("div", null, "成员", -1))
])
]),
a("div", ze, [
a("div", {
class: "icon-group",
onClick: ve
}, [
r(c, {
icon: "local-share-out-round",
class: "tool-item-icon"
}),
o[17] || (o[17] = a("div", null, "共享", -1))
])
]),
a("div", Oe, [
a("div", {
class: "icon-group",
onClick: Y
}, [
r(c, {
icon: "local-chat-outline",
class: "tool-item-icon"
}),
o[18] || (o[18] = a("div", null, "聊天", -1))
])
]),
a("div", je, [
a("div", {
class: "icon-group",
onClick: U
}, [
r(c, {
icon: "local-video-out-round",
class: "tool-item-icon"
}),
o[19] || (o[19] = a("div", null, "录制", -1))
])
]),
a("div", Fe, [
a("div", {
class: "icon-group",
onClick: U
}, [
r(c, {
icon: "local-setting",
class: "tool-item-icon"
}),
o[20] || (o[20] = a("div", null, "设置", -1))
])
]),
a("div", Ne, [
a("div", {
class: "icon-group",
onClick: U
}, [
r(c, {
icon: "local-book-account",
class: "tool-item-icon"
}),
o[21] || (o[21] = a("div", null, "成为主持人", -1))
])
]),
a("div", He, [
a("div", {
class: "icon-group",
onClick: ge
}, [
r(c, {
icon: "local-screen-exchange-out-round",
class: "tool-item-icon"
}),
o[22] || (o[22] = a("div", null, "双屏模式", -1))
])
])
]),
r(fe, {
modelValue: E.value,
"onUpdate:modelValue": o[5] || (o[5] = (s) => E.value = s),
width: "750",
title: "选取视频流"
}, {
footer: k(() => [
a("div", Qe, [
r(te, {
onClick: o[4] || (o[4] = (s) => E.value = !1)
}, {
default: k(() => o[23] || (o[23] = [
q(" 取消 ")
])),
_: 1,
__: [23]
}),
r(te, {
type: "primary",
onClick: he
}, {
default: k(() => o[24] || (o[24] = [
q(" 共享 ")
])),
_: 1,
__: [24]
})
])
]),
default: k(() => [
a("div", Ke, [
a("div", null, [
r(B, {
modelValue: K.value,
"onUpdate:modelValue": o[3] || (o[3] = (s) => K.value = s),
size: "large",
onChange: me
}, {
default: k(() => [
r(ee, {
label: "屏幕",
value: "screen"
}),
r(ee, {
label: "窗口",
value: "window"
})
]),
_: 1
}, 8, ["modelValue"])
]),
a("div", We, [
(p(!0), f(L, null, z(T.value, (s) => (p(), f("div", {
key: s.id,
class: Ce(["share-screen-item", { "is-active": s.id === C.value }]),
onClick: (Ye) => pe(s.id)
}, [
s.thumbnail ? (p(), f("img", {
key: 0,
src: s.thumbnail,
alt: "缩略图",
width: "150",
height: "82"
}, null, 8, Je)) : _e("", !0),
a("strong", Ge, O(s.name), 1)
], 10, qe))), 128))
])
])
]),
_: 1
}, 8, ["modelValue"])
], 64);
};
}
}), ot = /* @__PURE__ */ De(Xe, [["__scopeId", "data-v-59adb0a4"]]);
export {
ot as default
};