parent
1ec1ba0028
commit
a5af009ba8
3 changed files with 176 additions and 151 deletions
146
desktop/app.js
146
desktop/app.js
|
|
@ -17,8 +17,9 @@ const state = {
|
|||
rawStream: null,
|
||||
videoStream: null,
|
||||
screenStream: null,
|
||||
audioContext: null,
|
||||
peerConnections: new Map(),
|
||||
videoSenders: new Map(),
|
||||
screenSenders: new Map(),
|
||||
muted: false,
|
||||
sharingVideo: false,
|
||||
sharingScreen: false,
|
||||
|
|
@ -789,37 +790,37 @@ function ensurePeerConnection(peerId) {
|
|||
return state.voice.peerConnections.get(peerId);
|
||||
}
|
||||
|
||||
const pc = new RTCPeerConnection({ iceServers: state.voice.iceServers });
|
||||
const pc = new RTCPeerConnection({
|
||||
iceServers: state.voice.iceServers
|
||||
});
|
||||
|
||||
pc.makingOffer = false;
|
||||
pc.ignoreOffer = false;
|
||||
pc.polite = !shouldInitiateOffer(peerId);
|
||||
|
||||
// Add transceivers in a fixed order to ensure stable SDP m-lines: [Audio, Camera, Screen]
|
||||
// 1. Audio
|
||||
if (state.voice.localStream) {
|
||||
pc.addTransceiver(state.voice.localStream.getAudioTracks()[0], {
|
||||
direction: 'sendrecv',
|
||||
streams: [state.voice.localStream]
|
||||
state.voice.localStream.getAudioTracks().forEach(track => {
|
||||
pc.addTrack(track, state.voice.localStream);
|
||||
});
|
||||
} else {
|
||||
pc.addTransceiver('audio', { direction: 'sendrecv' });
|
||||
pc.addTransceiver('audio', { direction: 'recvonly' });
|
||||
}
|
||||
|
||||
// 2. Camera Video
|
||||
if (state.voice.videoStream) {
|
||||
pc.addTransceiver(state.voice.videoStream.getVideoTracks()[0], {
|
||||
direction: 'sendrecv',
|
||||
streams: [state.voice.videoStream]
|
||||
state.voice.videoStream.getVideoTracks().forEach(track => {
|
||||
const sender = pc.addTrack(track, state.voice.videoStream);
|
||||
state.voice.videoSenders.set(peerId, sender);
|
||||
});
|
||||
} else {
|
||||
pc.addTransceiver('video', { direction: 'sendrecv' });
|
||||
}
|
||||
|
||||
// 3. Screen Video
|
||||
if (state.voice.screenStream) {
|
||||
pc.addTransceiver(state.voice.screenStream.getVideoTracks()[0], {
|
||||
direction: 'sendrecv',
|
||||
streams: [state.voice.screenStream]
|
||||
state.voice.screenStream.getVideoTracks().forEach(track => {
|
||||
const sender = pc.addTrack(track, state.voice.screenStream);
|
||||
state.voice.screenSenders.set(peerId, sender);
|
||||
});
|
||||
} else {
|
||||
pc.addTransceiver('video', { direction: 'sendrecv' });
|
||||
}
|
||||
|
||||
pc.onicecandidate = (event) => {
|
||||
|
|
@ -833,6 +834,7 @@ function ensurePeerConnection(peerId) {
|
|||
};
|
||||
|
||||
pc.ontrack = (event) => {
|
||||
const stream = event.streams[0];
|
||||
if (event.track.kind === "audio") {
|
||||
let audio = document.getElementById(`audio-${peerId}`);
|
||||
if (!audio) {
|
||||
|
|
@ -842,24 +844,35 @@ function ensurePeerConnection(peerId) {
|
|||
audio.playsInline = true;
|
||||
document.body.appendChild(audio);
|
||||
}
|
||||
audio.srcObject = event.streams[0];
|
||||
audio.srcObject = stream;
|
||||
} else if (event.track.kind === "video") {
|
||||
const peer = state.members.find(m => m.id === peerId) || { display_name: "Unknown" };
|
||||
// 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');
|
||||
renderVideo(peerId, peer.display_name, stream, event.track.id);
|
||||
|
||||
event.track.onmute = () => {
|
||||
document.getElementById(`video-${peerId}-${event.track.id}`)?.parentElement?.remove();
|
||||
};
|
||||
event.track.onended = () => {
|
||||
document.getElementById(`video-${peerId}-${event.track.id}`)?.parentElement?.remove();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pc.onnegotiationneeded = async () => {
|
||||
try {
|
||||
await sendOffer(peerId);
|
||||
pc.makingOffer = true;
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
state.voice.ws.send(JSON.stringify({
|
||||
type: "signal",
|
||||
to_user_id: peerId,
|
||||
kind: "offer",
|
||||
data: pc.localDescription,
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error("negotiation failed", err);
|
||||
} finally {
|
||||
pc.makingOffer = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -867,41 +880,23 @@ function ensurePeerConnection(peerId) {
|
|||
return pc;
|
||||
}
|
||||
|
||||
async function sendOffer(peerId) {
|
||||
const pc = ensurePeerConnection(peerId);
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
state.voice.ws.send(JSON.stringify({
|
||||
type: "signal",
|
||||
to_user_id: peerId,
|
||||
kind: "offer",
|
||||
data: offer,
|
||||
}));
|
||||
}
|
||||
|
||||
async function handleSignal(fromPeerId, kind, data) {
|
||||
const pc = ensurePeerConnection(fromPeerId);
|
||||
const polite = !shouldInitiateOffer(fromPeerId);
|
||||
|
||||
try {
|
||||
if (kind === "offer") {
|
||||
const offerCollision = (pc.signalingState !== "stable");
|
||||
if (offerCollision) {
|
||||
if (!polite) return;
|
||||
await Promise.all([
|
||||
pc.setLocalDescription({ type: "rollback" }),
|
||||
pc.setRemoteDescription(new RTCSessionDescription(data))
|
||||
]);
|
||||
} else {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(data));
|
||||
}
|
||||
const offerCollision = pc.makingOffer || pc.signalingState !== "stable";
|
||||
pc.ignoreOffer = !pc.polite && offerCollision;
|
||||
if (pc.ignoreOffer) return;
|
||||
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(data));
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
state.voice.ws.send(JSON.stringify({
|
||||
type: "signal",
|
||||
to_user_id: fromPeerId,
|
||||
kind: "answer",
|
||||
data: answer,
|
||||
data: pc.localDescription,
|
||||
}));
|
||||
} else if (kind === "answer") {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(data));
|
||||
|
|
@ -909,9 +904,7 @@ async function handleSignal(fromPeerId, kind, data) {
|
|||
try {
|
||||
await pc.addIceCandidate(data ? new RTCIceCandidate(data) : null);
|
||||
} catch (err) {
|
||||
if (pc.signalingState !== "stable") {
|
||||
// Ignore ICE candidates during negotiation
|
||||
} else {
|
||||
if (!pc.ignoreOffer) {
|
||||
console.warn("failed to add ice candidate", err);
|
||||
}
|
||||
}
|
||||
|
|
@ -952,11 +945,11 @@ async function joinVoice() {
|
|||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === "peers") {
|
||||
for (const peer of msg.peers) {
|
||||
if (shouldInitiateOffer(peer.user_id)) await sendOffer(peer.user_id);
|
||||
ensurePeerConnection(peer.user_id);
|
||||
}
|
||||
} else if (msg.type === "peer_joined") {
|
||||
playSound('peer-join');
|
||||
if (shouldInitiateOffer(msg.user_id)) await sendOffer(msg.user_id);
|
||||
ensurePeerConnection(msg.user_id);
|
||||
} else if (msg.type === "peer_left") {
|
||||
playSound('peer-leave');
|
||||
const pc = state.voice.peerConnections.get(msg.user_id);
|
||||
|
|
@ -996,6 +989,8 @@ async function joinVoice() {
|
|||
el.soundboard.classList.add("hidden");
|
||||
for (const pc of state.voice.peerConnections.values()) pc.close();
|
||||
state.voice.peerConnections.clear();
|
||||
state.voice.videoSenders.clear();
|
||||
state.voice.screenSenders.clear();
|
||||
stopAndClearAudioPipeline();
|
||||
state.voice.joinedChannelId = null;
|
||||
state.voice.ws = null;
|
||||
|
|
@ -1025,11 +1020,11 @@ async function toggleVideo() {
|
|||
if (state.voice.ws) {
|
||||
state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: false }));
|
||||
}
|
||||
for (const pc of state.voice.peerConnections.values()) {
|
||||
const transceivers = pc.getTransceivers();
|
||||
const cameraTransceiver = transceivers.find(t => t.receiver.track.kind === 'video' && t.mid === transceivers[1].mid);
|
||||
if (cameraTransceiver) {
|
||||
cameraTransceiver.sender.replaceTrack(null);
|
||||
for (const [peerId, pc] of state.voice.peerConnections) {
|
||||
const sender = state.voice.videoSenders.get(peerId);
|
||||
if (sender) {
|
||||
pc.removeTrack(sender);
|
||||
state.voice.videoSenders.delete(peerId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -1043,11 +1038,11 @@ async function toggleVideo() {
|
|||
state.voice.ws.send(JSON.stringify({ type: "set_video_status", is_sharing_video: true }));
|
||||
}
|
||||
|
||||
for (const pc of state.voice.peerConnections.values()) {
|
||||
const transceivers = pc.getTransceivers();
|
||||
if (transceivers[1]) {
|
||||
transceivers[1].sender.replaceTrack(stream.getVideoTracks()[0]);
|
||||
}
|
||||
for (const [peerId, pc] of state.voice.peerConnections) {
|
||||
stream.getVideoTracks().forEach(track => {
|
||||
const sender = pc.addTrack(track, stream);
|
||||
state.voice.videoSenders.set(peerId, sender);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("camera denied", err);
|
||||
|
|
@ -1067,15 +1062,16 @@ async function toggleScreenShare() {
|
|||
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 transceivers = pc.getTransceivers();
|
||||
if (transceivers[2]) {
|
||||
transceivers[2].sender.replaceTrack(null);
|
||||
for (const [peerId, pc] of state.voice.peerConnections) {
|
||||
const sender = state.voice.screenSenders.get(peerId);
|
||||
if (sender) {
|
||||
pc.removeTrack(sender);
|
||||
state.voice.screenSenders.delete(peerId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
|
||||
state.voice.screenStream = stream;
|
||||
state.voice.sharingScreen = true;
|
||||
renderVideo(state.me.id, state.me.display_name, stream, 'screen');
|
||||
|
|
@ -1089,11 +1085,11 @@ async function toggleScreenShare() {
|
|||
state.voice.ws.send(JSON.stringify({ type: "set_screen_status", is_sharing_screen: true }));
|
||||
}
|
||||
|
||||
for (const pc of state.voice.peerConnections.values()) {
|
||||
const transceivers = pc.getTransceivers();
|
||||
if (transceivers[2]) {
|
||||
transceivers[2].sender.replaceTrack(stream.getVideoTracks()[0]);
|
||||
}
|
||||
for (const [peerId, pc] of state.voice.peerConnections) {
|
||||
stream.getVideoTracks().forEach(track => {
|
||||
const sender = pc.addTrack(track, stream);
|
||||
state.voice.screenSenders.set(peerId, sender);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("screen share denied", err);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue