From 3b848d42b729c0edc9b2659bae0e672412dc8910 Mon Sep 17 00:00:00 2001 From: MathMan05 Date: Fri, 23 May 2025 13:22:28 -0500 Subject: [PATCH] some VC updates --- src/webpage/channel.ts | 66 +++++++++++++++++++++++++++----- src/webpage/icons/stopstream.svg | 1 + src/webpage/style.css | 28 ++++++++++---- src/webpage/voice.ts | 51 +++++++++++++++++++++--- translations/en.json | 4 +- 5 files changed, 128 insertions(+), 22 deletions(-) create mode 100644 src/webpage/icons/stopstream.svg diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index d9f5e0c..c28dcce 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -1124,12 +1124,26 @@ class Channel extends SnowFlake { console.log("vid", elm); box.append(elm); } + makeBig(box: HTMLElement) { + const par = box.parentElement; + if (!par) return; + if (par.children[0] !== box || !box.classList.contains("bigBox")) { + box.classList.add("bigBox"); + if (par.children[0] !== box) { + par.children[0].classList.remove("bigBox"); + } + } else { + par.children[0].classList.remove("bigBox"); + } + par.prepend(box); + } decorateLive(id: string) { if (!this.voice) return; const box = this.liveMap.get(id); if (!box) return; box.innerHTML = ""; const live = this.voice.getLive(id); + const self = id === this.localuser.user.id; if (!this.voice.open) { const span = document.createElement("span"); span.textContent = I18n.vc.joinForStream(); @@ -1137,18 +1151,27 @@ class Channel extends SnowFlake { } else if (live) { const leave = document.createElement("button"); leave.classList.add("leave"); - leave.textContent = I18n.vc.leavestream(); - leave.onclick = () => { - this.voice?.leaveLive(id); + leave.textContent = self ? I18n.vc.stopstream() : I18n.vc.leavestream(); + leave.onclick = (e) => { + e.stopImmediatePropagation(); + if (self) { + this.voice?.stopStream(); + } else { + this.voice?.leaveLive(id); + } }; box.append(live, leave); - } else { + } else if (!self) { const joinB = document.createElement("button"); joinB.textContent = I18n.vc.joinstream(); joinB.classList.add("joinb"); box.append(joinB); joinB.onclick = () => { if (!this.voice) return; + box.innerHTML = ""; + const span = document.createElement("span"); + span.textContent = I18n.vc.joiningStream(); + box.append(span); this.voice.joinLive(id); }; } @@ -1194,6 +1217,9 @@ class Channel extends SnowFlake { } else if (!live && change.live && box) { const livediv = document.createElement("div"); this.liveMap.set(id, livediv); + livediv.onclick = () => { + this.makeBig(livediv); + }; box.parentElement?.prepend(livediv); this.decorateLive(id); } @@ -1217,6 +1243,9 @@ class Channel extends SnowFlake { async makeUserBox(user: User, users: HTMLElement) { const memb = Member.resolveMember(user, this.guild); const box = document.createElement("div"); + box.onclick = () => { + this.makeBig(box); + }; this.boxMap.set(user.id, box); if (user.accent_color != undefined) { box.style.setProperty( @@ -1297,6 +1326,7 @@ class Channel extends SnowFlake { updateVideoIcon(); video.onclick = async () => { if (!this.voice) return; + if (!this.voice.open) return; if (this.localuser.voiceFactory?.video) { this.voice.stopVideo(); } else { @@ -1317,23 +1347,39 @@ class Channel extends SnowFlake { video.classList.add("callVoiceIcon"); const updateLiveIcon = () => { - lspan.classList.remove("svg-video", "svg-novideo"); - lspan.classList.add(true ? "svg-stream" : "svg-stopstream"); + lspan.classList.remove("svg-stream", "svg-stopstream"); + lspan.classList.add(this.voice?.isLive() ? "svg-stopstream" : "svg-stream"); }; const live = document.createElement("div"); const lspan = document.createElement("span"); live.append(lspan); updateLiveIcon(); live.onclick = async () => { - const stream = await navigator.mediaDevices.getDisplayMedia(); - this.voice?.createLive(this.localuser.user.id, stream); + if (!this.voice?.open) return; + if (this.voice?.isLive()) { + this.voice?.stopStream(); + } else { + const stream = await navigator.mediaDevices.getDisplayMedia(); + const v = await this.voice?.createLive(stream); + console.log(v); + } updateLiveIcon(); }; live.classList.add("callVoiceIcon"); - buttonRow.append(mute, call, video, live); + buttonRow.append(mute, video, live, call); const users = document.createElement("div"); + const mut = new MutationObserver(() => { + const arr = Array.from(users.children); + const box = arr.find((_) => _.classList.contains("bigBox")); + if (box && arr[0] !== box) { + users.prepend(box); + } + }); + mut.observe(users, { + childList: true, + }); users.classList.add("voiceUsers"); this.voice.userids.forEach(async (_, id) => { @@ -1351,6 +1397,7 @@ class Channel extends SnowFlake { this.boxVid(id, vid); }; this.voice.onGotStream = (_vid, id) => { + updateLiveIcon(); this.decorateLive(id); }; this.voice.onconnect = () => { @@ -1365,6 +1412,7 @@ class Channel extends SnowFlake { this.voice.onLeave = () => { updateCallIcon(); + updateVideoIcon(); for (const [id] of this.boxMap) { this.purgeVid(id); } diff --git a/src/webpage/icons/stopstream.svg b/src/webpage/icons/stopstream.svg new file mode 100644 index 0000000..83c8b6e --- /dev/null +++ b/src/webpage/icons/stopstream.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/webpage/style.css b/src/webpage/style.css index 35643ea..6a61a32 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -218,6 +218,7 @@ body { .leave:hover { background: color-mix(in srgb, var(--red) 85%, white); } + .voiceUsers > * { background: var(--accent_color, var(--primary-bg)); border-radius: 8px; @@ -225,11 +226,11 @@ body { box-sizing: border-box; margin: 8px; width: 340px; - height: 220px; + aspect-ratio: 3/2; display: flex; - justify-content: center; align-items: center; - + justify-content: center; + cursor: pointer; img { width: 60px; height: 60px; @@ -237,12 +238,20 @@ body { } video { position: absolute; - top: 0px; - left: 0px; - width: 100%; - height: 100%; + flex-grow: 1; } } +.voiceUsers:has(.bigBox) > * { + width: 220px; + flex-grow: 0; + flex-shrink: 0; +} +.bigBox { + width: min(80%, 600px) !important; + height: unset; + margin: 0 calc((100% - min(80%, 600px)) / 2); +} + .buttonRow > * { margin-right: 6px; width: 54px; @@ -529,6 +538,11 @@ textarea { mask: url(/icons/stream.svg); mask-size: contain !important; } +.svg-stopstream { + mask: url(/icons/stopstream.svg); + mask-size: contain !important; + background-color: var(--red); +} .svg-video { mask: url(/icons/video.svg); mask-size: contain !important; diff --git a/src/webpage/voice.ts b/src/webpage/voice.ts index f57b3b5..c41cb7d 100644 --- a/src/webpage/voice.ts +++ b/src/webpage/voice.ts @@ -70,6 +70,7 @@ class VoiceFactory { }, }); } + updateSelf() { if (this.currentVoice && this.currentVoice.open) { this.handleGateway({ @@ -111,6 +112,16 @@ class VoiceFactory { op: 4, }; } + leaveLive() { + const userid = this.settings.id; + const stream_key = `${this.curGuild === "@me" ? "call" : `guild:${this.curGuild}`}:${this.curChan}:${userid}`; + this.handleGateway({ + op: 19, + d: { + stream_key, + }, + }); + } live = new Map void>(); steamTokens = new Map>(); steamTokensRes = new Map void>(); @@ -134,7 +145,8 @@ class VoiceFactory { } islive = false; liveStream?: MediaStream; - async createLive(userid: string, stream: MediaStream) { + async createLive(stream: MediaStream) { + const userid = this.settings.id; this.islive = true; this.liveStream = stream; const stream_key = `${this.curGuild === "@me" ? "call" : `guild:${this.curGuild}`}:${this.curChan}:${userid}`; @@ -207,6 +219,9 @@ class VoiceFactory { }; voice2.gotStream(voice, user); + console.warn(voice2); + const res = this.live.get(create.d.stream_key); + if (res) res(voice); } } streamServerUpdate(update: streamServerUpdate) { @@ -410,6 +425,7 @@ class Voice { if (!ld) throw new Error("localDescription isn't defined"); const parsed = Voice.parsesdp(ld.sdp); const group = parsed.atr.get("group"); + console.warn(parsed); if (!group) throw new Error("group isn't in sdp"); const [_, ...bundles] = (group.entries().next().value as [string, string])[0].split(" "); bundles[bundles.length - 1] = bundles[bundles.length - 1].replace("\r", ""); @@ -508,13 +524,19 @@ a=rtcp-mux\r`; const sendOffer = async () => { console.trace("neg need"); await pc.setLocalDescription(); + console.log("set local"); const senders = this.senders.difference(this.ssrcMap); + console.log(senders, this.ssrcMap); for (const sender of senders) { - for (const thing of (await sender.getStats()) as Map) { + console.log(sender); + const d = (await sender.getStats()) as Map; + console.log([...d]); + for (const thing of d) { if (thing[1].ssrc) { this.ssrcMap.set(sender, thing[1].ssrc); this.makeOp12(sender); + console.warn("ssrc"); } } } @@ -532,14 +554,20 @@ a=rtcp-mux\r`; this.status = "Done"; } }; + function logState(thing: string, message = "") { + console.log(thing + (message ? ":" + message : "")); + } pc.addEventListener("negotiationneeded", async () => { + logState("negotiationneeded"); await sendOffer(); console.log(this.ssrcMap); }); pc.addEventListener("signalingstatechange", async () => { + logState("signalingstatechange", pc.signalingState); detectDone(); while (!this.counter) await new Promise((res) => setTimeout(res, 100)); if (this.pc && this.counter) { + console.warn("in here :3"); if (pc.signalingState === "have-local-offer") { const counter = this.counter; const remote: {sdp: string; type: RTCSdpType} = { @@ -549,17 +577,20 @@ a=rtcp-mux\r`; console.log(remote); await pc.setRemoteDescription(remote); } + } else { + console.warn("uh oh!"); } }); pc.addEventListener("connectionstatechange", async () => { + logState("connectionstatechange", pc.connectionState); detectDone(); if (pc.connectionState === "connecting") { await pc.setLocalDescription(); } }); pc.addEventListener("icegatheringstatechange", async () => { + logState("icegatheringstatechange", pc.iceGatheringState); detectDone(); - console.log("icegatheringstatechange", pc.iceGatheringState, this.pc, this.counter); if (this.pc && this.counter) { if (pc.iceGatheringState === "complete") { pc.setLocalDescription(); @@ -567,6 +598,7 @@ a=rtcp-mux\r`; } }); pc.addEventListener("iceconnectionstatechange", async () => { + logState("iceconnectionstatechange", pc.iceConnectionState); detectDone(); if (pc.iceConnectionState === "checking") { sendOffer(); @@ -754,14 +786,17 @@ a=rtcp-mux\r`; } liveMap = new Map(); voiceMap = new Map(); + isLive() { + return !!this.voiceMap.get(this.userid); + } getLive(id: string) { return this.liveMap.get(id); } joinLive(id: string) { return this.owner.joinLive(id); } - createLive(id: string, stream: MediaStream) { - return this.owner.createLive(id, stream); + createLive(stream: MediaStream) { + return this.owner.createLive(stream); } leaveLive(id: string) { const v = this.voiceMap.get(id); @@ -771,6 +806,10 @@ a=rtcp-mux\r`; this.liveMap.delete(id); this.onLeaveStream(id); } + stopStream() { + this.leaveLive(this.userid); + this.owner.leaveLive(); + } onLeaveStream = (_user: string) => {}; onGotStream = (_v: HTMLVideoElement, _user: string) => {}; gotStream(voice: Voice, user: string) { @@ -1168,7 +1207,9 @@ a=rtcp-mux\r`; console.warn("leave"); this.open = false; this.status = "Left voice chat"; + if (!this.settings.stream) this.owner.video = false; this.onLeave(); + for (const thing of this.liveMap) { this.leaveLive(thing[0]); } diff --git a/translations/en.json b/translations/en.json index 3ebe7c5..6b659d9 100644 --- a/translations/en.json +++ b/translations/en.json @@ -8,7 +8,9 @@ "vc": { "joinstream": "Watch stream", "leavestream": "Leave stream", - "joinForStream": "Join the VC to watch" + "joinForStream": "Join the VC to watch", + "stopstream": "Stop stream", + "joiningStream": "Joining stream..." }, "readableName": "English", "pinMessage": "Pin Message",