button {
border: 0;
background-color: orange;
padding: 10px;
color: white;
border-radius: 7px;
}
video {
border-radius: 15px;
}
.videoContainer {
display: flex;
margin: 20px;
width: 640px;
}
.videoContainer h2 {
color: white;
position: relative;
bottom: -380px;
left: -350px;
width: max-content;
}
#meet {
display: flex;
}
#recordButton.recording {
background-color: red;
}
#downloadButton {
background-color: #4caf50;
}
button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
A Free Bird Video Call
{{ request.user.full_name }}
Start Call
Start Recording
Download Recording
// Global variables
let peerConnection = null;
let localStream = null;
let isInitiator = false;
let iceCandidatesQueue = [];
let mediaRecorder = null;
let recordedChunks = [];
let isRecording = false;
const roomName = "{{ room_name }}";
const signalingChannel = new WebSocket(
`wss://${window.location.host}/ws/webrtc/${roomName}/`
);
setupMediaStream();
signalingChannel.onopen = async () => {
console.log("WebSocket connected!");
signalingChannel.send(
JSON.stringify({
type: "join",
room: roomName,
username: "{{ request.user.full_name }}",
})
);
};
async function setupMediaStream() {
try {
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
document.getElementById("localVideo").srcObject = localStream;
console.log("Local stream setup successful");
globalThis.localStream = localStream;
} catch (err) {
console.error("Error accessing media devices:", err);
throw err;
}
}
async function initializePeerConnection(localStream) {
const servers = {'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]}
peerConnection = new RTCPeerConnection(servers);
console.log("Created peer connection");
peerConnection.addStream(globalThis.localStream);
console.log("
peerConnection.ontrack = (event) => {
const userId = "{{ request.user.id }}"
console.log("Received remote track");
const remoteVideo = document.getElementById("remote-videos");
const stream = event.streams[0];
const container = document.createElement("div");
container.classList.add("videoContainer");
container.id = `remote-video-${userId}`;
const videoElement = document.createElement('video');
videoElement.srcObject = stream;
videoElement.play();
remoteVideo.appendChild(container);
container.appendChild(videoElement);
container.appendChild(document.createElement("h2"));
setupRecording();
};
// ICE candidate handling
peerConnection.onsignalingstatechange = (event) => {
console.log("Signaling state change:", peerConnection.signalingState);
};
peerConnection.onconnectionstatechange = (event) => {
console.log("Connection state change:", peerConnection.connectionState);
};
return peerConnection;
}
async function setupAndStart() {
try {
if (!peerConnection) {
await initializePeerConnection();
const remoteStreams = peerConnection.getRemoteStreams();
console.log(remoteStreams);
}
} catch (err) {
console.error("Error in setupAndStart:", err);
}
}
async function handleCandidate(message) {
try {
if (!peerConnection || !peerConnection.remoteDescription) {
console.log("Queueing ICE candidate");
iceCandidatesQueue.push(message.candidate);
}
if (message.candidate) {
await peerConnection.addIceCandidate(
new RTCIceCandidate(message.candidate)
);
console.log("Added ICE candidate");
}
} catch (err) {
console.error("Error adding ICE candidate:", err);
}
}
async function handleOffer(message) {
try {
if (!peerConnection) {
await setupAndStart();
}
await peerConnection.setRemoteDescription(
new RTCSessionDescription({
type: "offer",
sdp: message.sdp,
})
);
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
console.log("Sending ICE candidate");
signalingChannel.send(
JSON.stringify({
type: "ice-candidate",
candidate: event.candidate,
username: "{{ request.user.full_name }}",
})
);
console.log("ICE candidate sent");
}
};
// Process queued candidates
while (iceCandidatesQueue.length > 0) {
const candidate = iceCandidatesQueue.shift();
await peerConnection.addIceCandidate(
new RTCIceCandidate(candidate)
);
console.log("Added queued ICE candidate");
}
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
signalingChannel.send(
JSON.stringify({
type: "answer",
sdp: answer.sdp,
username: "{{ request.user.full_name }}",
})
);
} catch (err) {
console.error("Error handling offer:", err);
}
}
async function handleAnswer(message) {
try {
await peerConnection.setRemoteDescription(
new RTCSessionDescription({
type: "answer",
sdp: message.sdp,
})
);
// Process queued candidates
while (iceCandidatesQueue.length > 0) {
const candidate = iceCandidatesQueue.shift();
await peerConnection.addIceCandidate(
new RTCIceCandidate(candidate)
);
console.log("Added queued ICE candidate");
}
} catch (err) {
console.error("Error handling answer:", err);
}
}
async function startCall() {
try {
iceCandidatesQueue = [];
await setupAndStart();
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
signalingChannel.send(
JSON.stringify({
type: "offer",
sdp: offer.sdp,
username: "{{ request.user.full_name }}",
})
);
} catch (err) {
console.error("Error starting call:", err);
}
}
function updateRemoteUsername(message) {
const userId = "{{ request.user.id }}"
const remoteVideoContainer = document.getElementById(`remote-video-${userId}`);
if (remoteVideoContainer) {
const remoteNameElement = remoteVideoContainer.querySelector("h2");
if (remoteNameElement) {
remoteNameElement.textContent = message;
}
}
}
signalingChannel.onmessage = async (event) => {
const message = JSON.parse(event.data);
console.log("Received message:", message);
try {
switch (message.type) {
case "join":
if (!message.self) {
isInitiator = true;
updateRemoteUsername(message.username);
await setupAndStart();
}
break;
case "offer":
updateRemoteUsername(message.username);
await handleOffer(message);
break;
case "answer":
await handleAnswer(message);
break;
case "ice-candidate":
await handleCandidate(message);
break;
case "candidate":
await handleCandidate(message);
break;
default:
console.log("Unknown message type:", message.type);
}
} catch (err) {
console.error("Error handling message:", err);
}
};
async function setupRecording(stream) {
try {
// Create a canvas element to combine the videos
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
// Get video elements
const localVideo = document.getElementById("localVideo");
const remoteVideo = document.getElementById("remoteVideo");
// Wait for both streams to be available
if (!localVideo.srcObject || !remoteVideo.srcObject) {
console.log(
"Waiting for both streams before setting up recording..."
);
return;
}
// Set canvas size to match the video container width and height
const videoContainer = document.querySelector(".videoContainer");
const containerStyle = window.getComputedStyle(videoContainer);
canvas.width = videoContainer.offsetWidth * 2; // Width of two videos
canvas.height = videoContainer.offsetHeight;
// Create a stream from the canvas
const canvasStream = canvas.captureStream(30); // 30 FPS
// Add audio tracks from both streams
const localAudioTrack = localVideo.srcObject.getAudioTracks()[0];
const remoteAudioTrack = remoteVideo.srcObject.getAudioTracks()[0];
if (localAudioTrack) canvasStream.addTrack(localAudioTrack);
if (remoteAudioTrack) canvasStream.addTrack(remoteAudioTrack);
// Create MediaRecorder
mediaRecorder = new MediaRecorder(canvasStream, {
mimeType: "video/webm;codecs=vp8,opus",
});
// Handle data available event
mediaRecorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) {
recordedChunks.push(event.data);
}
};
// Handle recording stop
mediaRecorder.onstop = () => {
console.log("Recording stopped");
document.getElementById("downloadButton").disabled = false;
};
// Enable record button
document.getElementById("recordButton").disabled = false;
// Draw frames to canvas
function drawFrame() {
// Draw local video
ctx.drawImage(localVideo, 0, 0, canvas.width / 2, canvas.height);
// Draw remote video
ctx.drawImage(
remoteVideo,
canvas.width / 2,
0,
canvas.width / 2,
canvas.height
);
// Add names under videos
ctx.fillStyle = "white";
ctx.font = "20px Arial";
const localName = "{{ request.user.full_name }}";
const remoteName =
remoteVideo.parentElement.querySelector("h2").textContent;
// Add local name
ctx.fillText(localName, 10, canvas.height - 20);
// Add remote name
ctx.fillText(remoteName, canvas.width / 2 + 10, canvas.height - 20);
if (isRecording) {
requestAnimationFrame(drawFrame);
}
}
// Start the recording loop when recording starts
mediaRecorder.onstart = () => {
drawFrame();
};
console.log("Recording setup complete");
} catch (err) {
console.error("Error setting up recording:", err);
}
}
async function toggleRecording() {
const recordButton = document.getElementById('recordButton');
if (!isRecording) {
// Start recording
recordedChunks = [];
try {
if (!mediaRecorder) {
await setupRecording();
}
if (mediaRecorder && mediaRecorder.state === 'inactive') {
mediaRecorder.start(1000); // Record in 1-second chunks
isRecording = true;
recordButton.textContent = 'Stop Recording';
recordButton.classList.add('recording');
console.log("Recording started");
}
} catch (err) {
console.error("Error starting recording:", err);
}
} else {
// Stop recording
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
isRecording = false;
recordButton.textContent = 'Start Recording';
recordButton.classList.remove('recording');
console.log("Recording stopped");
}
}
}
function downloadRecording() {
try {
if (!recordedChunks.length) {
console.error("No recording data available");
return;
}
const blob = new Blob(recordedChunks, {
type: 'video/webm'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
document.body.appendChild(a);
a.style.display = 'none';
a.href = url;
a.download = `call-recording-${new Date().toISOString()}.webm`;
a.click();
// Cleanup
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
recordedChunks = [];
document.getElementById('downloadButton').disabled = true;
} catch (err) {
console.error("Error downloading recording:", err);
}
}
// Cleanup when the call ends
window.onbeforeunload = () => {
if (localStream) {
localStream.getTracks().forEach((track) => track.stop());
}
if (peerConnection) {
peerConnection.close();
}
if (signalingChannel) {
signalingChannel.close();
}
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
}
isRecording = false;
};
window.onunload = () => {
if (peerConnection) {
peerConnection.close();
}
};
< /code>
Есть подсказки относительно того, почему есть дополнительное удаленное видео? Кстати, я использую одну и ту же веб -камеру для обоих коллег. Один для местного. 2 для отдаленных сверстников. Должно быть только 2 потока веб -камеры. 1 для местного. 1 для отдаленных сверстников. Это потому, что у меня было только две вкладки, открытые для зала заседания видеозвонок.
Подробнее здесь: https://stackoverflow.com/questions/794 ... connection