async function startBidirectionalStream(inputStream, initialPrompt = "A painting") {
const API_BASE = "http://localhost:8000";
// 1. Get ICE servers
const iceResponse = await fetch(`${API_BASE}/api/v1/webrtc/ice-servers`);
const { iceServers } = await iceResponse.json();
// 2. Create peer connection
const pc = new RTCPeerConnection({ iceServers });
// State
let sessionId = null;
const queuedCandidates = [];
// 3. Create data channel
const dataChannel = pc.createDataChannel("parameters", { ordered: true });
dataChannel.onopen = () => {
console.log("Data channel ready");
};
dataChannel.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "stream_stopped") {
console.log("Stream stopped:", data.error_message);
pc.close();
}
};
// 4. Add LOCAL video track (for sending to server)
inputStream.getTracks().forEach((track) => {
if (track.kind === "video") {
console.log("Adding video track for sending");
pc.addTrack(track, inputStream);
}
});
// 5. Handle REMOTE video track (from server)
pc.ontrack = (event) => {
if (event.streams && event.streams[0]) {
document.getElementById("outputVideo").srcObject = event.streams[0];
}
};
// 6. Connection monitoring
pc.onconnectionstatechange = () => {
console.log("Connection state:", pc.connectionState);
};
// 7. ICE candidate handling
pc.onicecandidate = async (event) => {
if (event.candidate) {
if (sessionId) {
await sendIceCandidate(sessionId, event.candidate);
} else {
queuedCandidates.push(event.candidate);
}
}
};
// 8. Create and send offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const response = await fetch(`${API_BASE}/api/v1/webrtc/offer`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sdp: pc.localDescription.sdp,
type: pc.localDescription.type,
initialParameters: {
input_mode: "video",
prompts: [{ text: initialPrompt, weight: 1.0 }],
denoising_step_list: [700, 500]
}
})
});
const answer = await response.json();
sessionId = answer.sessionId;
// 9. Set remote description
await pc.setRemoteDescription({
type: answer.type,
sdp: answer.sdp
});
// 10. Flush queued candidates
for (const candidate of queuedCandidates) {
await sendIceCandidate(sessionId, candidate);
}
queuedCandidates.length = 0;
return { pc, dataChannel, sessionId };
}
async function sendIceCandidate(sessionId, candidate) {
await fetch(`http://localhost:8000/api/v1/webrtc/offer/${sessionId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
candidates: [{
candidate: candidate.candidate,
sdpMid: candidate.sdpMid,
sdpMLineIndex: candidate.sdpMLineIndex
}]
})
});
}