Race condition on startup of AV still...
This commit is contained in:
parent
b26366eb05
commit
6588672a3c
@ -13,6 +13,113 @@ import { Session } from "./GlobalContext";
|
||||
|
||||
const debug = true;
|
||||
|
||||
const createAnimatedVideoTrack = ({ width = 320, height = 240 } = {}): MediaStreamTrack => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) throw new Error("Could not get canvas context");
|
||||
|
||||
// Ball properties
|
||||
const ball = {
|
||||
x: width / 2,
|
||||
y: height / 2,
|
||||
radius: Math.min(width, height) * 0.06,
|
||||
dx: 3,
|
||||
dy: 2,
|
||||
color: "#00ff88",
|
||||
};
|
||||
|
||||
// Create stream BEFORE starting animation
|
||||
const stream = canvas.captureStream(15);
|
||||
const track = stream.getVideoTracks()[0];
|
||||
|
||||
function drawFrame() {
|
||||
if (!ctx) return;
|
||||
// Clear canvas
|
||||
ctx.fillStyle = "#000000";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Update ball position
|
||||
ball.x += ball.dx;
|
||||
ball.y += ball.dy;
|
||||
|
||||
// Bounce off walls
|
||||
if (ball.x + ball.radius >= width || ball.x - ball.radius <= 0) {
|
||||
ball.dx = -ball.dx;
|
||||
}
|
||||
if (ball.y + ball.radius >= height || ball.y - ball.radius <= 0) {
|
||||
ball.dy = -ball.dy;
|
||||
}
|
||||
|
||||
// Keep ball in bounds
|
||||
ball.x = Math.max(ball.radius, Math.min(width - ball.radius, ball.x));
|
||||
ball.y = Math.max(ball.radius, Math.min(height - ball.radius, ball.y));
|
||||
|
||||
// Draw ball
|
||||
ctx.beginPath();
|
||||
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = ball.color;
|
||||
ctx.fill();
|
||||
|
||||
// Add frame number or timestamp for debugging
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.font = "12px Arial";
|
||||
ctx.fillText(`Frame: ${Date.now() % 10000}`, 10, 20);
|
||||
}
|
||||
|
||||
// Draw initial frame
|
||||
drawFrame();
|
||||
|
||||
// Start animation - CRITICAL: Request animation frame for better performance
|
||||
function animate() {
|
||||
drawFrame();
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
animate();
|
||||
|
||||
track.enabled = true;
|
||||
return track;
|
||||
};
|
||||
|
||||
const createBlackVideoTrack = ({ width = 320, height = 240 } = {}): MediaStreamTrack => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
ctx.fillStyle = "black";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
// Use 1 FPS instead of 30 for synthetic tracks
|
||||
const stream = canvas.captureStream(1);
|
||||
return stream.getVideoTracks()[0];
|
||||
};
|
||||
|
||||
// Helper function to create a silent audio track
|
||||
const createSilentAudioTrack = (): MediaStreamTrack => {
|
||||
const audioContext = new AudioContext();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
const destination = audioContext.createMediaStreamDestination();
|
||||
|
||||
// Set gain to 0 for silence
|
||||
gainNode.gain.value = 0;
|
||||
|
||||
// Connect: oscillator -> gain -> destination
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(destination);
|
||||
|
||||
oscillator.start();
|
||||
|
||||
const track = destination.stream.getAudioTracks()[0];
|
||||
track.enabled = true;
|
||||
return track;
|
||||
};
|
||||
|
||||
// Types for peer and track context
|
||||
interface Peer {
|
||||
session_id: string;
|
||||
@ -25,13 +132,14 @@ interface Peer {
|
||||
local: boolean;
|
||||
dead: boolean;
|
||||
connection?: RTCPeerConnection;
|
||||
queuedCandidates?: RTCIceCandidateInit[];
|
||||
}
|
||||
export type { Peer };
|
||||
|
||||
interface TrackContext {
|
||||
media: MediaStream | null;
|
||||
audio: boolean;
|
||||
video: boolean;
|
||||
has_audio: boolean;
|
||||
has_video: boolean;
|
||||
}
|
||||
|
||||
interface AddPeerConfig {
|
||||
@ -63,24 +171,42 @@ interface VideoProps extends React.VideoHTMLAttributes<HTMLVideoElement> {
|
||||
|
||||
const Video: React.FC<VideoProps> = ({ srcObject, local, ...props }) => {
|
||||
const refVideo = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!refVideo.current) {
|
||||
if (!refVideo.current || !srcObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ref = refVideo.current;
|
||||
if (debug) console.log("media-control - video <video> bind");
|
||||
console.log("Setting video srcObject:", srcObject);
|
||||
|
||||
ref.srcObject = srcObject;
|
||||
if (local) {
|
||||
ref.muted = true;
|
||||
}
|
||||
|
||||
// Force play the video
|
||||
const playVideo = async () => {
|
||||
try {
|
||||
await ref.play();
|
||||
console.log("Video started playing");
|
||||
} catch (error) {
|
||||
console.error("Error playing video:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Play after a short delay to ensure srcObject is set
|
||||
setTimeout(playVideo, 100);
|
||||
|
||||
return () => {
|
||||
if (debug) console.log("media-control - <video> unbind");
|
||||
console.log("Cleaning up video srcObject");
|
||||
if (ref) {
|
||||
(ref as any).srcObject = undefined;
|
||||
ref.srcObject = null;
|
||||
}
|
||||
};
|
||||
}, [srcObject, local]);
|
||||
return <video ref={refVideo} {...props} />;
|
||||
|
||||
return <video ref={refVideo} autoPlay playsInline muted={local} {...props} />;
|
||||
};
|
||||
|
||||
type MediaAgentProps = {
|
||||
@ -93,7 +219,7 @@ type MediaAgentProps = {
|
||||
const MediaAgent = (props: MediaAgentProps) => {
|
||||
const { peers, setPeers, socketUrl, session } = props;
|
||||
// track: null = no local media, TrackContext = local media
|
||||
const [track, setTrack] = useState<TrackContext | null>(null);
|
||||
const [context, setContext] = useState<TrackContext | null>(null);
|
||||
|
||||
const { sendJsonMessage, lastJsonMessage } = useWebSocket(socketUrl, {
|
||||
share: true,
|
||||
@ -183,6 +309,12 @@ const MediaAgent = (props: MediaAgentProps) => {
|
||||
peer.connection = connection;
|
||||
connection.addEventListener("connectionstatechange", (event) => {
|
||||
console.log(`media-agent - connectionstatechange - `, connection.connectionState, event);
|
||||
if (connection.connectionState === "failed") {
|
||||
console.log("Connection failed, attempting recovery...");
|
||||
// Implement retry logic or notify user
|
||||
peer.dead = true;
|
||||
setPeers({ ...peers });
|
||||
}
|
||||
});
|
||||
connection.addEventListener("negotiationneeded", (event) => {
|
||||
console.log(`media-agent - negotiationneeded - `, connection.connectionState, event);
|
||||
@ -215,12 +347,31 @@ const MediaAgent = (props: MediaAgentProps) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
connection.ontrack = (e: RTCTrackEvent) => refOnTrack.current(e);
|
||||
connection.ontrack = (e: RTCTrackEvent) => {
|
||||
console.log("media-agent - ontrack event received", e);
|
||||
console.log("Stream:", e.streams[0]);
|
||||
console.log("Track:", e.track);
|
||||
refOnTrack.current(e);
|
||||
};
|
||||
connection.oniceconnectionstatechange = (event) => {
|
||||
console.log(`media-agent - iceconnectionstatechange - `, connection.iceConnectionState, event);
|
||||
if (connection.iceConnectionState === "failed") {
|
||||
console.log("ICE connection failed, attempting recovery...");
|
||||
// Implement retry logic or notify user
|
||||
peer.dead = true;
|
||||
setPeers({ ...peers });
|
||||
}
|
||||
};
|
||||
|
||||
// Only add local tracks if present
|
||||
if (track && track.media) {
|
||||
track.media.getTracks().forEach((t) => {
|
||||
connection.addTrack(t, track.media!);
|
||||
if (context && context.media) {
|
||||
console.log("Adding local tracks to new peer connection");
|
||||
context.media.getTracks().forEach((t) => {
|
||||
console.log("Adding track:", t.kind, t.enabled);
|
||||
connection.addTrack(t, context.media!);
|
||||
});
|
||||
} else {
|
||||
console.log("No local tracks available when creating peer");
|
||||
}
|
||||
if (config.should_create_offer) {
|
||||
connection
|
||||
@ -241,55 +392,60 @@ const MediaAgent = (props: MediaAgentProps) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
[peers, setPeers, track, sendJsonMessage]
|
||||
[peers, setPeers, context, sendJsonMessage]
|
||||
);
|
||||
|
||||
const sessionDescription = useCallback(
|
||||
({ peer_id, session_description }: SessionDescriptionData) => {
|
||||
const peer = peers[peer_id];
|
||||
if (!peer) {
|
||||
console.error(`media-agent - sessionDescription - No peer for ${peer_id}`);
|
||||
return;
|
||||
}
|
||||
const { connection } = peer;
|
||||
if (!connection) {
|
||||
console.error(`media-agent - sessionDescription - No connection for peer ${peer_id}`);
|
||||
return;
|
||||
}
|
||||
console.log(`media-agent - sessionDescription - `, { peer_id, session_description, peer });
|
||||
if (!peer?.connection) return;
|
||||
|
||||
const desc = new RTCSessionDescription(session_description);
|
||||
connection
|
||||
peer.connection
|
||||
.setRemoteDescription(desc)
|
||||
.then(() => {
|
||||
if (debug) console.log(`media-agent - sessionDescription - setRemoteDescription succeeded`);
|
||||
if (session_description.type === "offer") {
|
||||
if (debug) console.log(`media-agent - sessionDescription - Creating answer`);
|
||||
connection
|
||||
.createAnswer()
|
||||
.then((local_description) => {
|
||||
if (debug) console.log(`media-agent - sessionDescription - Answer description is: `, local_description);
|
||||
connection
|
||||
.setLocalDescription(local_description)
|
||||
console.log("Remote description set successfully");
|
||||
|
||||
// Process queued ICE candidates after remote description is set
|
||||
if (peer.queuedCandidates && peer.queuedCandidates.length > 0) {
|
||||
console.log(`Processing ${peer.queuedCandidates.length} queued candidates`);
|
||||
const candidatePromises = peer.queuedCandidates.map((candidate) =>
|
||||
peer.connection!.addIceCandidate(new RTCIceCandidate(candidate))
|
||||
);
|
||||
|
||||
Promise.all(candidatePromises)
|
||||
.then(() => {
|
||||
console.log("All queued candidates processed");
|
||||
peer.queuedCandidates = [];
|
||||
})
|
||||
.catch((err) => console.error("Error processing queued candidates:", err));
|
||||
}
|
||||
|
||||
// Handle offer/answer logic...
|
||||
if (session_description.type === "offer") {
|
||||
console.log("Creating answer for received offer");
|
||||
return peer.connection!.createAnswer();
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.then((answer) => {
|
||||
if (answer && session_description.type === "offer") {
|
||||
return peer.connection!.setLocalDescription(answer).then(() => {
|
||||
sendJsonMessage({
|
||||
type: "relaySessionDescription",
|
||||
config: {
|
||||
peer_id,
|
||||
session_description: local_description,
|
||||
peer_id: peer_id,
|
||||
session_description: answer,
|
||||
},
|
||||
});
|
||||
if (debug) console.log(`media-agent - sessionDescription - Answer setLocalDescription succeeded`);
|
||||
})
|
||||
.catch(() => {
|
||||
console.error(`media-agent - sessionDescription - Answer setLocalDescription failed!`);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
console.log("Answer sent successfully");
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(`media-agent - sessionDescription - setRemoteDescription error: `, error);
|
||||
console.error("Failed to set remote description:", error);
|
||||
});
|
||||
},
|
||||
[peers, sendJsonMessage]
|
||||
@ -316,28 +472,28 @@ const MediaAgent = (props: MediaAgentProps) => {
|
||||
|
||||
const iceCandidate = useCallback(
|
||||
({ peer_id, candidate }: IceCandidateData) => {
|
||||
/**
|
||||
* The offerer will send a number of ICE Candidate blobs to the
|
||||
* answerer so they can begin trying to find the best path to one
|
||||
* another on the net.
|
||||
*/
|
||||
const peer = peers[peer_id];
|
||||
if (!peer) {
|
||||
console.error(`media-agent - iceCandidate - No peer for ${peer_id}`, peers);
|
||||
console.log(`media-agent - iceCandidate - `, { peer_id, candidate, peer });
|
||||
if (!peer?.connection) {
|
||||
console.error(`No peer or connection for ${peer_id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Critical fix: Queue candidates if remote description not set
|
||||
if (peer.connection.remoteDescription) {
|
||||
peer.connection
|
||||
?.addIceCandidate(new RTCIceCandidate(candidate))
|
||||
.then(() => {
|
||||
if (debug) console.log(`media-agent - iceCandidate - Successfully added Ice Candidate for ${peer_id}`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error, peer, candidate);
|
||||
});
|
||||
.addIceCandidate(new RTCIceCandidate(candidate))
|
||||
.then(() => console.log(`Added ICE candidate for ${peer_id}`))
|
||||
.catch((err) => console.error("Failed to add ICE candidate:", err));
|
||||
} else {
|
||||
// Queue the candidate for later processing
|
||||
if (!peer.queuedCandidates) peer.queuedCandidates = [];
|
||||
peer.queuedCandidates.push(candidate);
|
||||
console.log(`Queued ICE candidate for ${peer_id} (no remote description yet)`);
|
||||
}
|
||||
},
|
||||
[peers]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!lastJsonMessage) {
|
||||
return;
|
||||
@ -346,10 +502,6 @@ const MediaAgent = (props: MediaAgentProps) => {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
console.log("media-agent - message", data);
|
||||
if (["addPeer", "removePeer", "iceCandidate", "sessionDescription"].includes(data.type)) {
|
||||
console.log(`media-agent - message - ${data.type}`, peers);
|
||||
}
|
||||
switch (data.type) {
|
||||
case "addPeer":
|
||||
addPeer(data.data);
|
||||
@ -373,40 +525,62 @@ const MediaAgent = (props: MediaAgentProps) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`media-control - Track changed`, track);
|
||||
console.log(`media-control - Context changed`, context);
|
||||
|
||||
const join = () => {
|
||||
sendJsonMessage({
|
||||
type: "join",
|
||||
data: {
|
||||
has_audio: track && track.audio ? track.audio : false,
|
||||
has_video: track && track.video ? track.video : false,
|
||||
has_audio: context && context.has_audio ? context.has_audio : false,
|
||||
has_video: context && context.has_video ? context.has_video : false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (track !== undefined) {
|
||||
console.log(`media-control - issuing join request: `, track);
|
||||
if (context) {
|
||||
console.log(`media-control - issuing join request: `, context);
|
||||
for (let peer in peers) {
|
||||
if (peers[peer].local && peers[peer].dead) {
|
||||
/* Allocate a new Object so <MediaControl> will trigger */
|
||||
peers[peer] = Object.assign({}, peers[peer]);
|
||||
// Mark as alive
|
||||
peers[peer].dead = false;
|
||||
setPeers(Object.assign({}, peers));
|
||||
peers[peer] = { ...peers[peer], dead: false };
|
||||
setPeers({ ...peers });
|
||||
}
|
||||
}
|
||||
join();
|
||||
}
|
||||
}, [track, peers, setPeers, sendJsonMessage]);
|
||||
}, [context, peers, setPeers, sendJsonMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!context || !context.media) return;
|
||||
|
||||
console.log("Track changed, updating all peer connections");
|
||||
|
||||
// Add tracks to all existing peer connections
|
||||
for (let peer_id in peers) {
|
||||
const peer = peers[peer_id];
|
||||
if (peer.connection && !peer.local && !peer.dead) {
|
||||
console.log(`Adding tracks to existing peer ${peer.peerName}`);
|
||||
if (!context || !context.media) return;
|
||||
context.media.getTracks().forEach((t) => {
|
||||
// Check if track is already added
|
||||
const senders = peer.connection!.getSenders();
|
||||
const trackAlreadyAdded = senders.some((sender) => sender.track === t);
|
||||
|
||||
if (!trackAlreadyAdded) {
|
||||
console.log(`Adding ${t.kind} track to ${peer.peerName}`);
|
||||
peer.connection!.addTrack(t, context.media!);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [context, peers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
let update = false;
|
||||
if (track) {
|
||||
if (!(session.id in peers)) {
|
||||
if (context && !(session.id in peers)) {
|
||||
update = true;
|
||||
peers[session.id] = {
|
||||
peerName: session.name || "Unknown",
|
||||
@ -414,16 +588,15 @@ const MediaAgent = (props: MediaAgentProps) => {
|
||||
local: true,
|
||||
muted: true,
|
||||
video_on: false,
|
||||
has_video: track.video,
|
||||
has_audio: track.audio,
|
||||
has_video: context.has_video,
|
||||
has_audio: context.has_audio,
|
||||
attributes: {
|
||||
local: true,
|
||||
srcObject: track.media,
|
||||
srcObject: context.media,
|
||||
},
|
||||
dead: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/* Renaming the local connection requires the peer to be deleted
|
||||
* and re-established with the signaling server */
|
||||
@ -438,74 +611,47 @@ const MediaAgent = (props: MediaAgentProps) => {
|
||||
if (debug) console.log(`media-agent - Setting global peers`, peers);
|
||||
setPeers(Object.assign({}, peers));
|
||||
}
|
||||
}, [peers, setPeers, track, session]);
|
||||
}, [peers, setPeers, context, session]);
|
||||
|
||||
const setup_local_media = async (): Promise<TrackContext> => {
|
||||
console.log(`media-agent - Requesting access to local audio / video inputs`);
|
||||
const context: TrackContext = { media: null, audio: true, video: true };
|
||||
const context: TrackContext = { media: null, has_audio: true, has_video: true };
|
||||
|
||||
// Try to get user media with fallback logic
|
||||
while (context.audio || context.video) {
|
||||
while (context.has_audio || context.has_video) {
|
||||
console.log(context);
|
||||
try {
|
||||
context.media = await navigator.mediaDevices.getUserMedia({
|
||||
audio: context.audio,
|
||||
video: context.video,
|
||||
});
|
||||
break;
|
||||
const constraints: any = {};
|
||||
if (context.has_audio) {
|
||||
constraints.audio = true;
|
||||
}
|
||||
if (context.has_video) {
|
||||
constraints.video = true;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`media-agent - Attempting to get user media: audio=${context.has_audio}, video=${context.has_video}`
|
||||
);
|
||||
context.media = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
/* Success -- on failure, an exception is thrown */
|
||||
return context;
|
||||
} catch (error) {
|
||||
console.error(`media-agent - Error accessing local media: `, error);
|
||||
if (context.video) {
|
||||
console.log(`media-agent - Disabling video and trying again`);
|
||||
context.video = false;
|
||||
} else if (context.audio) {
|
||||
console.log(`media-agent - Disabling audio and trying again`);
|
||||
context.audio = false;
|
||||
if (context.has_video && context.has_audio) {
|
||||
console.log(`media-agent - Disabling video and trying just audio`);
|
||||
context.has_video = false;
|
||||
context.has_audio = true;
|
||||
} else if (context.has_audio && !context.has_video) {
|
||||
console.log(`media-agent - Disabling audio and trying just video`);
|
||||
context.has_video = true;
|
||||
context.has_audio = false;
|
||||
} else {
|
||||
console.log(`media-agent - No media available`);
|
||||
break;
|
||||
context.has_video = false;
|
||||
context.has_audio = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create a black video track
|
||||
const createBlackVideoTrack = ({ width = 640, height = 480 } = {}): MediaStreamTrack => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
ctx.fillStyle = "black";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
const stream = canvas.captureStream(30); // 30 FPS
|
||||
const track = stream.getVideoTracks()[0];
|
||||
track.enabled = true;
|
||||
return track;
|
||||
};
|
||||
|
||||
// Helper function to create a silent audio track
|
||||
const createSilentAudioTrack = (): MediaStreamTrack => {
|
||||
const audioContext = new AudioContext();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
const destination = audioContext.createMediaStreamDestination();
|
||||
|
||||
// Set gain to 0 for silence
|
||||
gainNode.gain.value = 0;
|
||||
|
||||
// Connect: oscillator -> gain -> destination
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(destination);
|
||||
|
||||
oscillator.start();
|
||||
|
||||
const track = destination.stream.getAudioTracks()[0];
|
||||
track.enabled = true;
|
||||
return track;
|
||||
};
|
||||
|
||||
// Process the results and create appropriate media stream
|
||||
const tracks: MediaStreamTrack[] = [];
|
||||
let hasRealAudio = false;
|
||||
@ -540,7 +686,7 @@ const MediaAgent = (props: MediaAgentProps) => {
|
||||
}
|
||||
|
||||
if (!hasRealVideo) {
|
||||
tracks.push(createBlackVideoTrack());
|
||||
tracks.push(createAnimatedVideoTrack());
|
||||
console.log("media-agent - Using synthetic black video");
|
||||
}
|
||||
|
||||
@ -548,8 +694,8 @@ const MediaAgent = (props: MediaAgentProps) => {
|
||||
context.media = new MediaStream(tracks);
|
||||
|
||||
// Update context flags to reflect what we actually have
|
||||
context.audio = hasRealAudio;
|
||||
context.video = hasRealVideo;
|
||||
context.has_audio = true; //hasRealAudio;
|
||||
context.has_video = true; //hasRealVideo;
|
||||
|
||||
const mediaType =
|
||||
hasRealAudio && hasRealVideo
|
||||
@ -570,15 +716,19 @@ const MediaAgent = (props: MediaAgentProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (track === null) {
|
||||
if (context === null) {
|
||||
setup_local_media()
|
||||
.then((context) => {
|
||||
sendJsonMessage({ type: "media_status", ...context, media: undefined });
|
||||
setTrack(context);
|
||||
setContext(context);
|
||||
})
|
||||
.catch(() => setTrack(null));
|
||||
.catch((error) => {
|
||||
console.error("media-agent - Failed to get local media:", error);
|
||||
sendJsonMessage({ type: "media_status", has_audio: false, has_video: false, media: undefined });
|
||||
setContext(null);
|
||||
});
|
||||
}
|
||||
}, [track, session, sendJsonMessage]);
|
||||
}, [context, session, sendJsonMessage]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -599,8 +749,10 @@ const MediaControl: React.FC<MediaControlProps> = ({ isSelf, peer, className })
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`media-control - peer changed`, peer);
|
||||
if (peer && peer.peerName) {
|
||||
const el = document.querySelector(`.MediaControl[data-peer="${peer.session_id}"]`);
|
||||
console.log(`media-control - setting target for ${peer.peerName}`, el);
|
||||
setTarget(el ?? undefined);
|
||||
}
|
||||
}, [setTarget, peer]);
|
||||
@ -646,13 +798,13 @@ const MediaControl: React.FC<MediaControlProps> = ({ isSelf, peer, className })
|
||||
if (!media || media.dead || !peer) {
|
||||
return;
|
||||
}
|
||||
console.log(`media-control - media changed`, media);
|
||||
if (media.attributes.srcObject) {
|
||||
console.log(`media-control - audio enable - ${peer.peerName}:${!muted}`);
|
||||
(media.attributes.srcObject.getAudioTracks() as MediaStreamTrack[]).forEach((track: MediaStreamTrack) => {
|
||||
track.enabled = media.has_audio && !muted;
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [muted, media, peer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!media || media.dead || !peer) {
|
||||
|
@ -172,18 +172,20 @@ async def join(
|
||||
|
||||
if not session.ws:
|
||||
logger.error(
|
||||
f"{session.short}:{session.name} - No WebSocket connection. Media not available."
|
||||
f"{getSessionName(session)} - No WebSocket connection. Media not available."
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(f"{getSessionName(session)} <- join({getLobbyName(lobby)})")
|
||||
|
||||
if session.id in lobby.peers:
|
||||
logger.info(f"{getSessionName(session)} - Already joined to Media.")
|
||||
return
|
||||
# if session.id in lobby.peers:
|
||||
# logger.info(f"{getSessionName(session)} - Already joined to Media.")
|
||||
# return
|
||||
|
||||
# Notify all existing RTC peers
|
||||
for peer_session in lobby.peers.values():
|
||||
if peer_session.id == session.id:
|
||||
continue
|
||||
if not peer_session.ws:
|
||||
logger.warning(
|
||||
f"{getSessionName(peer_session)} - No WebSocket connection. Skipping."
|
||||
@ -191,7 +193,7 @@ async def join(
|
||||
continue
|
||||
|
||||
logger.info(
|
||||
f"{getSessionName(peer_session)} -> addPeer({getSessionName(session), getLobbyName(lobby)})"
|
||||
f"{getSessionName(peer_session)} -> addPeer({getSessionName(session), getLobbyName(lobby)}, video={has_video}, audio={has_audio}, should_create_offer=False)"
|
||||
)
|
||||
await peer_session.ws.send_json(
|
||||
{
|
||||
@ -209,7 +211,7 @@ async def join(
|
||||
# Add each other peer to the caller
|
||||
if session.ws:
|
||||
logger.info(
|
||||
f"{getSessionName(session)} -> addPeer({getSessionName(peer_session), getLobbyName(lobby)})"
|
||||
f"{getSessionName(session)} -> addPeer({getSessionName(peer_session), getLobbyName(lobby)}, video={peer_session.has_video}, audio={peer_session.has_audio}, should_create_offer=True)"
|
||||
)
|
||||
await session.ws.send_json(
|
||||
{
|
||||
@ -327,7 +329,7 @@ async def websocket_lobby(
|
||||
return
|
||||
session = getSession(session_id)
|
||||
if not session:
|
||||
logger.error(f"Invalid session ID {session_id}")
|
||||
# logger.error(f"Invalid session ID {session_id}")
|
||||
await websocket.send_json(
|
||||
{"type": "error", "error": f"Invalid session ID {session_id}"}
|
||||
)
|
||||
@ -381,8 +383,8 @@ async def websocket_lobby(
|
||||
await update_users(lobby, session)
|
||||
|
||||
case "media_status":
|
||||
has_audio = data.get("audio", False)
|
||||
has_video = data.get("video", False)
|
||||
has_audio = data.get("has_audio", False)
|
||||
has_video = data.get("has_video", False)
|
||||
logger.info(
|
||||
f"{getSessionName(session)}: <- media-status(audio: {has_audio}, video: {has_video})"
|
||||
)
|
||||
@ -390,8 +392,9 @@ async def websocket_lobby(
|
||||
session.has_video = has_video
|
||||
|
||||
case "join":
|
||||
has_audio = data.get("audio", False)
|
||||
has_video = data.get("video", False)
|
||||
logger.info(f"{getSessionName(session)} <- join {data}")
|
||||
has_audio = data.get("has_audio", False)
|
||||
has_video = data.get("has_video", False)
|
||||
await join(lobby, session, has_video, has_audio)
|
||||
|
||||
case "part":
|
||||
@ -401,11 +404,26 @@ async def websocket_lobby(
|
||||
logger.info(f"{getSessionName(session)} <- relayICECandidate")
|
||||
if session.id not in lobby.peers:
|
||||
logger.error(
|
||||
f"{session.short}:{session.name} <- relayICECandidate - Not an RTC peer"
|
||||
f"{session.short}:{session.name} <- relayICECandidate - Not an RTC peer ({session.id})"
|
||||
)
|
||||
return
|
||||
await websocket.send_json(
|
||||
{"type": "error", "error": "Not joined to media session"}
|
||||
)
|
||||
continue
|
||||
|
||||
peer_id = data.get("config", {}).get("peer_id")
|
||||
if peer_id not in lobby.peers:
|
||||
logger.error(
|
||||
f"{getSessionName(session)} <- relayICECandidate - Not an RTC peer({peer_id})"
|
||||
)
|
||||
await websocket.send_json(
|
||||
{
|
||||
"type": "error",
|
||||
"error": f"Target peer {peer_id} not found",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
candidate = data.get("config", {}).get("candidate")
|
||||
|
||||
message = {
|
||||
@ -419,7 +437,10 @@ async def websocket_lobby(
|
||||
logger.warning(
|
||||
f"{lobby.peers[peer_id].short}:{lobby.peers[peer_id].name} - No WebSocket connection. Skipping."
|
||||
)
|
||||
return
|
||||
break
|
||||
logger.info(
|
||||
f"{getSessionName(session)} -> iceCandidate({getSessionName(lobby.peers[peer_id])})"
|
||||
)
|
||||
await ws.send_json(message)
|
||||
|
||||
case "relaySessionDescription":
|
||||
@ -428,8 +449,14 @@ async def websocket_lobby(
|
||||
logger.error(
|
||||
f"{session.short}:{session.name} - relaySessionDescription - Not an RTC peer"
|
||||
)
|
||||
return
|
||||
break
|
||||
peer_id = data.get("config", {}).get("peer_id")
|
||||
peer = lobby.peers.get(peer_id, None)
|
||||
if not peer:
|
||||
logger.error(
|
||||
f"{getSessionName(session)} <- relaySessionDescription - Not an RTC peer({peer_id})"
|
||||
)
|
||||
break
|
||||
session_description = data.get("config", {}).get(
|
||||
"session_description"
|
||||
)
|
||||
@ -440,14 +467,16 @@ async def websocket_lobby(
|
||||
"session_description": session_description,
|
||||
},
|
||||
}
|
||||
if peer_id in lobby.peers:
|
||||
ws = lobby.peers[peer_id].ws
|
||||
if not ws:
|
||||
if not peer.ws:
|
||||
logger.warning(
|
||||
f"{lobby.peers[peer_id].short}:{lobby.peers[peer_id].name} - No WebSocket connection. Skipping."
|
||||
)
|
||||
return
|
||||
await ws.send_json(message)
|
||||
break
|
||||
|
||||
logger.info(
|
||||
f"{getSessionName(session)} -> sessionDescription({getSessionName(lobby.peers[peer_id])})"
|
||||
)
|
||||
await peer.ws.send_json(message)
|
||||
|
||||
case _:
|
||||
await websocket.send_json(
|
||||
@ -461,11 +490,16 @@ async def websocket_lobby(
|
||||
logger.info(f"{getSessionName(session)} <- WebSocket disconnected for user.")
|
||||
# Cleanup: remove session from lobby and sessions dict
|
||||
session.ws = None
|
||||
if lobby and session:
|
||||
if session.id in lobby.peers:
|
||||
await part(lobby, session)
|
||||
|
||||
await update_users(lobby)
|
||||
# if session_id in sessions:
|
||||
# del sessions[session_id]
|
||||
|
||||
# Clean up empty lobbies
|
||||
if not lobby.sessions:
|
||||
if lobby.id in lobbies:
|
||||
del lobbies[lobby.id]
|
||||
logger.info(f"Cleaned up empty lobby {lobby.short}")
|
||||
|
||||
|
||||
# Serve static files or proxy to frontend development server
|
||||
|
Loading…
x
Reference in New Issue
Block a user