ii
All checks were successful
/ upload (release) Successful in 18s

This commit is contained in:
pavel 2026-02-24 01:02:26 +01:00
commit 737267567e
3 changed files with 125 additions and 9 deletions

View file

@ -16,10 +16,12 @@ const state = {
localStream: null,
rawStream: null,
videoStream: null,
screenStream: null,
audioContext: null,
peerConnections: new Map(),
muted: false,
sharingVideo: false,
sharingScreen: false,
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
},
voicePresencePollId: null,
@ -62,6 +64,7 @@ const el = {
voiceConnection: document.getElementById("voice-connection"),
vcChannelName: document.getElementById("vc-channel-name"),
voiceVideoBtn: document.getElementById("voice-video-btn"),
voiceScreenBtn: document.getElementById("voice-screen-btn"),
voiceMuteBtn: document.getElementById("voice-mute-btn"),
voiceLeaveBtn: document.getElementById("voice-leave-btn"),
videoGrid: document.getElementById("video-grid"),
@ -563,7 +566,17 @@ function stopAndClearVideoPipeline() {
}
state.voice.videoStream = null;
state.voice.sharingVideo = false;
document.getElementById(`video-${state.me.id}`)?.parentElement?.remove();
document.getElementById(`video-${state.me.id}-camera`)?.parentElement?.remove();
updateVideoGridVisibility();
}
function stopAndClearScreenPipeline() {
if (state.voice.screenStream) {
for (const track of state.voice.screenStream.getTracks()) track.stop();
}
state.voice.screenStream = null;
state.voice.sharingScreen = false;
document.getElementById(`video-${state.me.id}-screen`)?.parentElement?.remove();
updateVideoGridVisibility();
}
@ -576,14 +589,16 @@ function updateVideoGridVisibility() {
}
}
function renderVideo(peerId, displayName, stream) {
let videoEl = document.getElementById(`video-${peerId}`);
function renderVideo(peerId, displayName, stream, source) {
const videoId = `video-${peerId}-${source}`;
let videoEl = document.getElementById(videoId);
if (!videoEl) {
const container = document.createElement("div");
container.className = "video-item";
const label = source === 'screen' ? `${escapeHtml(displayName)}'s Screen` : escapeHtml(displayName);
container.innerHTML = `
<video id="video-${peerId}" autoplay playsinline ${peerId === state.me.id ? "muted" : ""}></video>
<div class="video-label">${escapeHtml(displayName)}</div>
<video id="${videoId}" autoplay playsinline ${peerId === state.me.id ? "muted" : ""}></video>
<div class="video-label">${label}</div>
`;
el.videoGrid.appendChild(container);
videoEl = container.querySelector("video");
@ -646,6 +661,12 @@ function ensurePeerConnection(peerId) {
}
}
if (state.voice.screenStream) {
for (const track of state.voice.screenStream.getTracks()) {
pc.addTrack(track, state.voice.screenStream);
}
}
pc.onicecandidate = (event) => {
if (!event.candidate || !state.voice.ws) return;
state.voice.ws.send(JSON.stringify({
@ -669,7 +690,13 @@ function ensurePeerConnection(peerId) {
audio.srcObject = event.streams[0];
} else if (event.track.kind === "video") {
const peer = state.members.find(m => m.id === peerId) || { display_name: "Unknown" };
renderVideo(peerId, peer.display_name, event.streams[0]);
// Distinguish screen share from camera by checking track labels or signaling
// For simplicity, we can use the stream index or a dedicated track check.
// A more robust way is to have separate transceivers/signaling.
// Here we check if the label suggests a screen.
const isScreen = event.track.label.toLowerCase().includes('screen') ||
event.track.label.toLowerCase().includes('monitor');
renderVideo(peerId, peer.display_name, event.streams[0], isScreen ? 'screen' : 'camera');
}
};
@ -785,7 +812,12 @@ async function joinVoice() {
await handleSignal(msg.from_user_id, msg.kind, msg.data);
} else if (msg.type === "video_status_changed") {
if (!msg.is_sharing_video) {
document.getElementById(`video-${msg.user_id}`)?.parentElement?.remove();
document.getElementById(`video-${msg.user_id}-camera`)?.parentElement?.remove();
updateVideoGridVisibility();
}
} else if (msg.type === "screen_status_changed") {
if (!msg.is_sharing_screen) {
document.getElementById(`video-${msg.user_id}-screen`)?.parentElement?.remove();
updateVideoGridVisibility();
}
}
@ -825,9 +857,10 @@ async function toggleVideo() {
if (state.voice.ws) {
state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: false }));
}
const trackId = `video-${state.me.id}-camera`;
for (const pc of state.voice.peerConnections.values()) {
const senders = pc.getSenders();
const videoSender = senders.find(s => s.track && s.track.kind === "video");
const videoSender = senders.find(s => s.track && s.track.kind === "video" && !s.track.label.toLowerCase().includes('screen'));
if (videoSender) pc.removeTrack(videoSender);
}
} else {
@ -835,7 +868,7 @@ async function toggleVideo() {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
state.voice.videoStream = stream;
state.voice.sharingVideo = true;
renderVideo(state.me.id, state.me.display_name, stream);
renderVideo(state.me.id, state.me.display_name, stream, 'camera');
if (state.voice.ws) {
state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: true }));
@ -858,6 +891,49 @@ async function toggleVideo() {
lucide.createIcons();
}
async function toggleScreenShare() {
if (state.voice.sharingScreen) {
stopAndClearScreenPipeline();
if (state.voice.ws) {
state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: false }));
}
for (const pc of state.voice.peerConnections.values()) {
const senders = pc.getSenders();
const screenSender = senders.find(s => s.track && s.track.kind === "video" && (s.track.label.toLowerCase().includes('screen') || s.track.label.toLowerCase().includes('monitor')));
if (screenSender) pc.removeTrack(screenSender);
}
} else {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
state.voice.screenStream = stream;
state.voice.sharingScreen = true;
renderVideo(state.me.id, state.me.display_name, stream, 'screen');
// Stop sharing if user clicks "Stop sharing" in browser UI
stream.getVideoTracks()[0].onended = () => {
if (state.voice.sharingScreen) toggleScreenShare();
};
if (state.voice.ws) {
state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: true }));
}
for (const pc of state.voice.peerConnections.values()) {
for (const track of stream.getTracks()) {
pc.addTrack(track, stream);
}
}
} catch (err) {
console.error("screen share denied", err);
return;
}
}
el.voiceScreenBtn.innerHTML = state.voice.sharingScreen ? '<i data-lucide="monitor"></i>' : '<i data-lucide="monitor-off"></i>';
el.voiceScreenBtn.style.color = state.voice.sharingScreen ? 'var(--green)' : 'var(--text-muted)';
lucide.createIcons();
}
// --- Mobile Logic ---
function toggleMobileMenu() {
@ -1032,6 +1108,7 @@ async function init() {
};
el.voiceVideoBtn.onclick = toggleVideo;
el.voiceScreenBtn.onclick = toggleScreenShare;
el.voiceMuteBtn.onclick = toggleMute;
el.voiceLeaveBtn.onclick = leaveVoice;

View file

@ -86,6 +86,7 @@
</div>
<div class="vc-actions">
<button id="voice-video-btn" title="Turn on Camera"><i data-lucide="video-off"></i></button>
<button id="voice-screen-btn" title="Share Screen"><i data-lucide="monitor-off"></i></button>
<button id="voice-mute-btn" title="Mute"><i data-lucide="mic"></i></button>
<button id="voice-leave-btn" title="Disconnect"><i data-lucide="phone-off"></i></button>
</div>