mirror of
https://github.com/DanielnetoDotCom/YouPHPTube
synced 2025-10-04 02:09:22 +02:00
368 lines
14 KiB
JavaScript
368 lines
14 KiB
JavaScript
const socketCall = io(WebRTC2RTMPURL); // Connect to the Socket.IO server
|
|
const peers = {}; // Store RTCPeerConnections by peerId
|
|
const remoteStreams = {}; // Store remote MediaStreams by peerId
|
|
let currentRoom = null; // Track the current room the user is in
|
|
|
|
/**
|
|
* Join a room and set up the necessary listeners for existing/new peers.
|
|
* @param {string} roomId - The room identifier.
|
|
* @param {MediaStream} localStream - The local media stream (audio/video).
|
|
*/
|
|
function joinRoom(roomId, localStream) {
|
|
currentRoom = roomId;
|
|
socketCall.emit('join-room', roomId);
|
|
console.log(`Call Events: Requesting to join room: ${roomId}`);
|
|
|
|
// 1. Handle the current list of peers in the room
|
|
socketCall.on('peer-list', (peerList) => {
|
|
console.log(`Call Events: Peers in room ${roomId}:`, peerList);
|
|
peerList.forEach((peerId) => {
|
|
if (peerId !== socketCall.id && !peers[peerId]) {
|
|
console.log(`Call Events: Creating RTCPeerConnection and offer for peerId: ${peerId}`);
|
|
const peerConnection = createPeerConnection(peerId, localStream);
|
|
peers[peerId] = peerConnection;
|
|
|
|
// Create an offer for each existing peer
|
|
peerConnection.createOffer()
|
|
.then((offer) => {
|
|
console.log(`Call Events: Offer created for ${peerId}:`, offer);
|
|
return peerConnection.setLocalDescription(offer);
|
|
})
|
|
.then(() => {
|
|
console.log(`Call Events: Sending offer to ${peerId}`);
|
|
socketCall.emit('signal', {
|
|
roomId,
|
|
to: peerId,
|
|
offer: peers[peerId].localDescription
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
console.error('Call Events: Error creating/sending offer:', error);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// 2. Handle notification that a new peer joined the room
|
|
socketCall.on('new-peer', (peerId) => {
|
|
console.log(`Call Events: New peer joined room ${currentRoom}: ${peerId}`);
|
|
|
|
if (!peers[peerId]) {
|
|
console.log(`Call Events: Creating connection for new peer: ${peerId}`);
|
|
const peerConnection = createPeerConnection(peerId, localStream);
|
|
peers[peerId] = peerConnection;
|
|
|
|
// Optionally create an offer for the new peer here
|
|
peerConnection.createOffer()
|
|
.then((offer) => {
|
|
console.log(`Call Events: Created offer for ${peerId}`, offer);
|
|
return peerConnection.setLocalDescription(offer);
|
|
})
|
|
.then(() => {
|
|
console.log(`Call Events: Sending offer to ${peerId}`);
|
|
socketCall.emit('signal', {
|
|
roomId: currentRoom,
|
|
to: peerId,
|
|
offer: peers[peerId].localDescription
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
console.error('Call Events: Error creating/sending offer:', error);
|
|
});
|
|
} else {
|
|
console.log(`Call Events: Peer is already in peers: ${peerId}`);
|
|
}
|
|
});
|
|
|
|
// 3. Handle signaling data (offer, answer, ICE) for the room
|
|
socketCall.on('signal', async ({ from, offer, answer, candidate }) => {
|
|
console.log(`Call Events: Signal received from ${from} in room ${roomId}`);
|
|
|
|
// If there is no existing connection for this peer, create one
|
|
if (!peers[from]) {
|
|
console.log(`Call Events: Creating RTCPeerConnection for peer: ${from}`);
|
|
peers[from] = createPeerConnection(from, localStream);
|
|
}
|
|
|
|
const peerConnection = peers[from];
|
|
|
|
if (offer) {
|
|
// Process offer
|
|
console.log(`Call Events: Received offer from ${from}`);
|
|
try {
|
|
await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
|
|
console.log('Call Events: Offer set as RemoteDescription');
|
|
|
|
const localAnswer = await peerConnection.createAnswer();
|
|
console.log(`Call Events: Creating answer for offer from ${from}`);
|
|
|
|
await peerConnection.setLocalDescription(localAnswer);
|
|
console.log('Call Events: Answer set as LocalDescription');
|
|
|
|
// Send the answer back
|
|
socketCall.emit('signal', {
|
|
roomId,
|
|
to: from,
|
|
answer: localAnswer
|
|
});
|
|
console.log(`Call Events: Sending answer to ${from}`);
|
|
} catch (error) {
|
|
console.error(`Call Events: Error processing offer from ${from}:`, error);
|
|
}
|
|
} else if (answer) {
|
|
// Process answer
|
|
console.log(`Call Events: Received answer from ${from}`);
|
|
try {
|
|
await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
|
|
console.log('Call Events: Answer set as RemoteDescription');
|
|
} catch (error) {
|
|
console.error(`Call Events: Error setting RemoteDescription for answer from ${from}:`, error);
|
|
}
|
|
} else if (candidate) {
|
|
// Process ICE candidate
|
|
console.log(`Call Events: Received ICE candidate from ${from}:`, candidate);
|
|
try {
|
|
await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
|
|
console.log('Call Events: ICE candidate successfully added');
|
|
} catch (error) {
|
|
console.error(`Call Events: Error adding ICE candidate from ${from}:`, error);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 4. Handle a peer that disconnected
|
|
socketCall.on('peer-disconnected', (peerId) => {
|
|
console.log(`Call Events: Peer disconnected from room ${roomId}: ${peerId}`);
|
|
|
|
if (peers[peerId]) {
|
|
console.log(`Call Events: Closing connection and removing peerId: ${peerId}`);
|
|
peers[peerId].close();
|
|
delete peers[peerId];
|
|
}
|
|
|
|
// Remove the corresponding video element
|
|
removeVideo(peerId);
|
|
|
|
// Also remove the remote stream if tracking
|
|
delete remoteStreams[peerId];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create an RTCPeerConnection for a given peerId.
|
|
* @param {string} peerId - The identifier of the peer.
|
|
* @param {MediaStream} localStream - The local stream to send to the peer.
|
|
* @returns {RTCPeerConnection} The newly created RTCPeerConnection.
|
|
*/
|
|
function createPeerConnection(peerId, localStream) {
|
|
console.log(`Call Events: Creating RTCPeerConnection for peerId: ${peerId}`);
|
|
|
|
const configuration = {
|
|
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
|
};
|
|
const peerConnection = new RTCPeerConnection(configuration);
|
|
|
|
// Add local tracks (audio/video)
|
|
localStream.getTracks().forEach((track) => {
|
|
console.log(`Call Events: Adding track (${track.kind}) to peerId ${peerId}`);
|
|
peerConnection.addTrack(track, localStream);
|
|
});
|
|
|
|
// ICE candidate event
|
|
peerConnection.onicecandidate = (event) => {
|
|
if (event.candidate) {
|
|
//console.log(`Call Events: Sending ICE candidate to ${peerId}:`, event.candidate);
|
|
socketCall.emit('signal', {
|
|
roomId: currentRoom,
|
|
to: peerId,
|
|
candidate: event.candidate
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Remote track event:
|
|
* By default, ontrack fires once per track (audio, then video).
|
|
* To avoid duplicating <video> elements, we collect tracks in `remoteStreams[peerId]`.
|
|
*/
|
|
peerConnection.ontrack = (event) => {
|
|
console.log(`Call Events: Remote track received from ${peerId}:`, event.streams);
|
|
|
|
// If we don't have a stream object yet for this peer, create one
|
|
if (!remoteStreams[peerId]) {
|
|
remoteStreams[peerId] = new MediaStream();
|
|
}
|
|
|
|
// Add this track to the existing remote stream
|
|
remoteStreams[peerId].addTrack(event.track);
|
|
|
|
// If there's no video element yet for this peer, create it now
|
|
if (!document.getElementById(peerId)) {
|
|
console.log(`Call Events: Creating a single video for peerId: ${peerId}`);
|
|
addVideo(peerId, remoteStreams[peerId], 'remoteVideo');
|
|
} else {
|
|
console.log(`Call Events: Video element for ${peerId} already exists; just adding track`);
|
|
}
|
|
};
|
|
|
|
return peerConnection;
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a Mute/Unmute Button for the Microphone.
|
|
* @param {string} peerId - The peer ID.
|
|
* @param {MediaStream} stream - The stream associated with the microphone.
|
|
* @returns {jQuery} - The jQuery button element.
|
|
*/
|
|
function createMicButton(peerId, stream) {
|
|
const $micButton = $('<button>', {
|
|
id: `mic-${peerId}`,
|
|
title: 'Mute/Unmute Microphone'
|
|
}).addClass('btn btn-link mute-unmute-btn mute-unmute-btn-mic');
|
|
|
|
const $micIcon = $('<i>', {
|
|
class: 'fas fa-microphone' // Default icon for microphone on
|
|
});
|
|
$micButton.append($micIcon);
|
|
|
|
$micButton.on('click', function () {
|
|
const audioTracks = stream.getAudioTracks();
|
|
if (audioTracks.length === 0) {
|
|
console.warn(`No audio tracks found for peerId: ${peerId}`);
|
|
return;
|
|
}
|
|
|
|
const audioTrack = audioTracks[0];
|
|
audioTrack.enabled = !audioTrack.enabled; // Toggle microphone
|
|
|
|
console.log(`Microphone for peerId ${peerId} is now ${audioTrack.enabled ? 'unmuted' : 'muted'}`);
|
|
|
|
// Update the icon based on the state
|
|
$micIcon.attr('class', audioTrack.enabled ? 'fas fa-microphone' : 'fas fa-microphone-slash');
|
|
});
|
|
|
|
return $micButton;
|
|
}
|
|
|
|
/**
|
|
* Create a Volume Control (Slider and Mute/Unmute Button) for the Speaker.
|
|
* @param {string} peerId - The peer ID.
|
|
* @param {HTMLVideoElement} videoElement - The video element associated with the speaker.
|
|
* @returns {jQuery[]} - An array containing the volume slider and mute/unmute button.
|
|
*/
|
|
function createVolumeControl(peerId, videoElement) {
|
|
// Volume slider
|
|
const $volumeControl = $('<input>', {
|
|
type: 'range',
|
|
min: 0,
|
|
max: 1,
|
|
step: 0.1,
|
|
value: 1, // Default volume
|
|
id: `volume-${peerId}`
|
|
}).addClass('volume-control');
|
|
|
|
$volumeControl.on('input', function () {
|
|
const volume = parseFloat($(this).val());
|
|
videoElement.volume = volume; // Adjust speaker volume
|
|
console.log(`Volume for peerId ${peerId} set to ${volume}`);
|
|
});
|
|
|
|
// Mute/unmute button
|
|
const $speakerButton = $('<button>', {
|
|
id: `speaker-${peerId}`,
|
|
title: 'Mute/Unmute Speaker'
|
|
}).addClass('btn btn-link mute-unmute-btn mute-unmute-btn-speaker');
|
|
|
|
const $speakerIcon = $('<i>', {
|
|
class: 'fas fa-volume-up' // Default icon for speaker on
|
|
});
|
|
$speakerButton.append($speakerIcon);
|
|
|
|
$speakerButton.on('click', function () {
|
|
const isMuted = videoElement.muted;
|
|
videoElement.muted = !isMuted; // Toggle speaker mute
|
|
|
|
console.log(`Speaker for peerId ${peerId} is now ${videoElement.muted ? 'muted' : 'unmuted'}`);
|
|
|
|
// Update the icon based on the state
|
|
$speakerIcon.attr('class', videoElement.muted ? 'fas fa-volume-mute' : 'fas fa-volume-up');
|
|
});
|
|
|
|
return [$volumeControl, $speakerButton];
|
|
}
|
|
|
|
|
|
/**
|
|
* Add a video element for the specified peer.
|
|
* @param {string} peerId - The peer ID.
|
|
* @param {MediaStream} stream - The remote stream to display.
|
|
* @param {string} className - The CSS class for the video element.
|
|
*/
|
|
function addVideo(peerId, stream, className, isLocal=false) {
|
|
// 1. Create the video wrapper
|
|
const $videoWrapper = $('<div>', {
|
|
id: `wrapper-${peerId}`
|
|
}).addClass('video-wrapper'); // Optional class
|
|
|
|
// 2. Create the <video> element
|
|
const $video = $('<video>', {
|
|
id: peerId,
|
|
autoplay: true,
|
|
playsinline: true,
|
|
controls: false, // Disable native controls
|
|
muted: isLocal
|
|
}).addClass(className);
|
|
|
|
// Assign MediaStream to video
|
|
$video[0].srcObject = stream;
|
|
|
|
|
|
// 6. Append elements to video wrapper
|
|
$videoWrapper.append($video);
|
|
if(!isLocal){
|
|
const [$volumeControl, $speakerButton] = createVolumeControl(peerId, $video[0]);
|
|
$videoWrapper.append($volumeControl);
|
|
$videoWrapper.append($speakerButton);
|
|
|
|
|
|
//$videoWrapper.append($micButton);
|
|
//$videoWrapper.append($volumeControl);
|
|
|
|
// 7. Add video wrapper to container
|
|
$('#videoContainer').append($videoWrapper);
|
|
}else{
|
|
const $micButton = createMicButton(peerId, stream);
|
|
$videoWrapper.append($micButton);
|
|
|
|
$('#localVideoContainer').append($videoWrapper);
|
|
$('#localVideo')[0].muted = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove the video element for the specified peer.
|
|
* @param {string} peerId - The peer ID.
|
|
*/
|
|
function removeVideo(peerId) {
|
|
console.log(`Call Events: Attempting to remove video wrapper for peerId: ${peerId}`);
|
|
const wrapper = document.getElementById(`wrapper-${peerId}`);
|
|
if (wrapper) {
|
|
console.log(`Call Events: Removing video wrapper for peerId: ${peerId}`);
|
|
wrapper.remove();
|
|
} else {
|
|
console.log(`Call Events: Video wrapper not found for peerId: ${peerId}`);
|
|
}
|
|
}
|
|
|
|
// Example usage: join "example-room" and capture local video/audio
|
|
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
|
.then((stream) => {
|
|
console.log('Call Events: Successfully captured local media');
|
|
joinRoom(roomId, stream);
|
|
|
|
addVideo('localVideo', stream, 'localVideo', true);
|
|
})
|
|
.catch((error) => {
|
|
console.error('Call Events: Error accessing media devices:', error);
|
|
});
|