vc / static /app.js
Vineet692's picture
Deploy VineetVC VP8 AV1 VedicTHG
37434a6 verified
Raw
History Blame Contribute Delete
9.41 kB
let pc;
let ws;
let localStream;
let videoSender;
let lastVideoBytes = 0;
let lastVideoTs = 0;
let currentMode = "vp8";
let autoLowCount = 0;
let autoMidCount = 0;
let autoHighCount = 0;
const localVideo = document.getElementById("local");
const remoteVideo = document.getElementById("remote");
const statsBox = document.getElementById("stats");
const iceServers = [
{ urls: "stun:stun.l.google.com:19302" }
];
document.getElementById("start").onclick = async () => {
localStream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
},
video: {
width: 640,
height: 360,
frameRate: 30
}
});
localVideo.srcObject = localStream;
statsBox.textContent = "Camera and microphone started.";
};
document.getElementById("connect").onclick = async () => {
if (!localStream) {
await document.getElementById("start").onclick();
}
pc = new RTCPeerConnection({ iceServers });
for (const track of localStream.getTracks()) {
const sender = pc.addTrack(track, localStream);
if (track.kind === "video") videoSender = sender;
}
pc.ontrack = (ev) => {
remoteVideo.srcObject = ev.streams[0];
};
pc.onicecandidate = (ev) => {
if (ev.candidate && ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "candidate", candidate: ev.candidate }));
}
};
const room = document.getElementById("room").value.trim() || "demo-room";
const proto = location.protocol === "https:" ? "wss" : "ws";
ws = new WebSocket(`${proto}://${location.host}/ws/${room}`);
ws.onmessage = async (ev) => {
const msg = JSON.parse(ev.data);
if (msg.type === "joined") {
statsBox.textContent = `Joined room. Peers in room: ${msg.peers}`;
if (msg.peers >= 2) {
await applyModePreferences(document.getElementById("mode").value, false);
await makeOffer();
}
return;
}
if (msg.type === "offer") {
await pc.setRemoteDescription(msg.offer);
await applyModePreferences(document.getElementById("mode").value, false);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
ws.send(JSON.stringify({ type: "answer", answer }));
return;
}
if (msg.type === "answer") {
await pc.setRemoteDescription(msg.answer);
return;
}
if (msg.type === "candidate") {
try {
await pc.addIceCandidate(msg.candidate);
} catch (_) {}
}
};
setInterval(logAndControlStats, 1000);
};
document.getElementById("applyMode").onclick = async () => {
await applyModePreferences(document.getElementById("mode").value, true);
};
async function makeOffer() {
if (!pc || !ws || ws.readyState !== WebSocket.OPEN) return;
if (pc.signalingState !== "stable") return;
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
ws.send(JSON.stringify({ type: "offer", offer }));
}
async function applyModePreferences(mode, renegotiate = true) {
if (!pc || !videoSender) return;
currentMode = mode;
if (mode === "vp8") {
preferVideoCodec("video/VP8");
await setVideoParams({
maxBitrate: 1500000,
maxFramerate: 30,
scaleResolutionDownBy: 1
});
await replaceWithCameraTrack();
}
if (mode === "av1") {
preferVideoCodec("video/AV1");
await setVideoParams({
maxBitrate: 160000,
maxFramerate: 10,
scaleResolutionDownBy: 4
});
await replaceWithCameraTrack();
}
if (mode === "ai") {
await setVideoParams({
maxBitrate: 70000,
maxFramerate: 15,
scaleResolutionDownBy: 2
});
await replaceWithAiTrack();
}
if (renegotiate) {
await makeOffer();
}
}
function preferVideoCodec(mimeType) {
const transceiver = pc.getTransceivers().find(t => t.sender === videoSender);
if (!transceiver || !RTCRtpSender.getCapabilities) return;
const caps = RTCRtpSender.getCapabilities("video");
if (!caps?.codecs?.length) return;
const preferred = [
...caps.codecs.filter(c => c.mimeType.toLowerCase() === mimeType.toLowerCase()),
...caps.codecs.filter(c => c.mimeType.toLowerCase() !== mimeType.toLowerCase())
];
if (preferred.length) {
transceiver.setCodecPreferences(preferred);
}
}
async function setVideoParams({ maxBitrate, maxFramerate, scaleResolutionDownBy }) {
try {
const p = videoSender.getParameters();
p.encodings = p.encodings?.length ? p.encodings : [{}];
Object.assign(p.encodings[0], {
maxBitrate,
maxFramerate,
scaleResolutionDownBy
});
await videoSender.setParameters(p);
} catch (e) {
console.warn("Could not set sender params:", e);
}
}
async function replaceWithCameraTrack() {
const track = localStream.getVideoTracks()[0];
if (track) {
await videoSender.replaceTrack(track);
localVideo.srcObject = localStream;
}
}
async function replaceWithAiTrack() {
const audioTrack = localStream.getAudioTracks()[0];
if (!audioTrack) {
throw new Error("No local audio track available.");
}
statsBox.textContent = "Mode 3: capturing face/audio and calling /api/thg...";
const faceBlob = await captureFaceFrame(localVideo);
const audioBlob = await recordAudioChunk(audioTrack, 3000);
const form = new FormData();
form.append("face", faceBlob, "face.png");
form.append("audio", audioBlob, "audio.webm");
const res = await fetch("/api/thg", {
method: "POST",
body: form
});
if (!res.ok) {
const err = await res.text();
throw new Error(err);
}
const mp4 = await res.blob();
const aiVideo = document.createElement("video");
aiVideo.src = URL.createObjectURL(mp4);
aiVideo.loop = true;
aiVideo.muted = true;
aiVideo.playsInline = true;
await aiVideo.play();
const aiStream = aiVideo.captureStream
? aiVideo.captureStream(15)
: aiVideo.mozCaptureStream(15);
const aiTrack = aiStream.getVideoTracks()[0];
if (!aiTrack) {
throw new Error("Could not capture AI video stream.");
}
await videoSender.replaceTrack(aiTrack);
localVideo.srcObject = aiStream;
statsBox.textContent = "Mode 3 active: AI video track replaced.";
}
function captureFaceFrame(video) {
return new Promise((resolve, reject) => {
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth || 640;
canvas.height = video.videoHeight || 360;
const ctx = canvas.getContext("2d");
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => {
if (!blob) reject(new Error("Could not capture face frame."));
else resolve(blob);
}, "image/png");
});
}
function recordAudioChunk(track, ms) {
return new Promise((resolve, reject) => {
const stream = new MediaStream([track]);
const mimeType = MediaRecorder.isTypeSupported("audio/webm;codecs=opus")
? "audio/webm;codecs=opus"
: "";
const rec = new MediaRecorder(stream, mimeType ? { mimeType } : undefined);
const chunks = [];
rec.ondataavailable = (e) => {
if (e.data.size > 0) chunks.push(e.data);
};
rec.onerror = (e) => reject(e.error || e);
rec.onstop = () => {
resolve(new Blob(chunks, { type: "audio/webm" }));
};
rec.start();
setTimeout(() => rec.stop(), ms);
});
}
async function logAndControlStats() {
if (!pc) return;
const report = await pc.getStats();
let videoKbps = 0;
let availableKbps = null;
let frames = 0;
let codecId = "";
report.forEach(s => {
if (s.type === "outbound-rtp" && s.kind === "video") {
const now = s.timestamp;
const bytes = s.bytesSent || 0;
if (lastVideoTs && now > lastVideoTs) {
videoKbps = 8 * (bytes - lastVideoBytes) / (now - lastVideoTs);
}
lastVideoBytes = bytes;
lastVideoTs = now;
frames = s.framesEncoded || 0;
codecId = s.codecId || "";
}
if (
s.type === "candidate-pair" &&
s.state === "succeeded" &&
(s.nominated || s.selected)
) {
if (s.availableOutgoingBitrate) {
availableKbps = s.availableOutgoingBitrate / 1000;
}
}
});
if (document.getElementById("autoMode").checked && availableKbps) {
await maybeAutoSwitch(availableKbps);
}
statsBox.textContent =
`activeMode=${currentMode}\n` +
`videoSend=${videoKbps.toFixed(1)} kbps\n` +
`availableOutgoing=${availableKbps ? availableKbps.toFixed(1) + " kbps" : "not exposed"}\n` +
`framesEncoded=${frames}\n` +
`codecId=${codecId}\n` +
`pcState=${pc.connectionState}`;
}
async function maybeAutoSwitch(availableKbps) {
if (availableKbps < 90) {
autoLowCount++;
autoMidCount = 0;
autoHighCount = 0;
} else if (availableKbps < 400) {
autoMidCount++;
autoLowCount = 0;
autoHighCount = 0;
} else {
autoHighCount++;
autoLowCount = 0;
autoMidCount = 0;
}
if (autoLowCount >= 3 && currentMode !== "ai") {
document.getElementById("mode").value = "ai";
await applyModePreferences("ai", true);
}
if (autoMidCount >= 3 && currentMode !== "av1") {
document.getElementById("mode").value = "av1";
await applyModePreferences("av1", true);
}
if (autoHighCount >= 3 && currentMode !== "vp8") {
document.getElementById("mode").value = "vp8";
await applyModePreferences("vp8", true);
}
}