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",