I am currently working on a peer to peer video chat using webrtc with options to turn on and off the microphone and camera. The initial settings are working correctly, if i have my microphone selected, only my microphone gets sent to the other user, vice versa with the video feed or both, but if i try to change it (mute myself or turn off camera) on the "first offering" participiant the other participant will receive nothing and even if i try to turn it back on, it still shows an empty feed. Sometimes i can manage to get it to work again but i have no idea what pattern makes it work.
My main js, saveOffer, getOffer getAnswer setAnswer etc. are ajax requests to a server where the sdp-s are stored as files as a "signaling channel"
let element = document.getElementById('mysdp');
function SignalingChannel() {
// Create an array to store event listeners
this.listeners = [];
// Method to add an event listener
this.addEventListener = function (eventName, callback) {
this.listeners.push({ eventName, callback });
};
// Method to trigger the "message" event
this.postMessage = function (data) {
// Emit the "message" event
this.emitEvent("message", data);
};
// Method to trigger the "send" function
this.send = function (data) {
// Display the data
//console.log("Sent data: " + data);
};
// Method to emit events
this.emitEvent = function (eventName, data) {
// Find the listeners for the given event
const eventListeners = this.listeners.filter(
(listener) => listener.eventName === eventName
);
// Call the callback functions for each listener
eventListeners.forEach((listener) => {
listener.callback(data);
});
};
}
const signalingChannel = new SignalingChannel();
var options = { offerToReceiveAudio: true, offerToReceiveVideo: true };
signalingChannel.addEventListener('message', message => {
console.log(message);
});
const configuration = {'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]}
const peerConnection = new RTCPeerConnection(configuration);
let dataChannel;
let sender;
function addTrack(stream){
console.log("AddTrack start");
const senders = peerConnection.getSenders();
senders.forEach((sender) => peerConnection.removeTrack(sender));
console.log("Previous tracks removed");
stream.getTracks().forEach(track => {
console.log("Adding track: ");
console.log(track);
sender = peerConnection.addTrack(track, localStream);
});
console.log("AddTrack end");
}
//Used for renegotiation
async function changeCall(roomid){
console.log("Creating offer");
let offer = await peerConnection.createOffer(options);
console.log("New offer created");
await peerConnection.setLocalDescription(offer);
console.log("Local description set");
let lsdp = JSON.stringify(peerConnection.localDescription);
let lsdpoffer = {'offer' : lsdp};
console.log("Renegotiating, saving offer to server");
await saveOffer(lsdpoffer,roomid);
//await dataChannel.send('{"renegotiation":true}');
console.log("Sending renegotiation ping");
await dataChannel.send('{"renegotiation":true}');
}
//Used for initiating a call
async function makeCall(roomid) {
console.log("call");
signalingChannel.addEventListener('message', async message => {
message = JSON.parse(message);
if (message.answer) {
const remoteDesc = new RTCSessionDescription(message.answer);
await peerConnection.setRemoteDescription(remoteDesc);
console.log("remote description set");
}
});
dataChannel = peerConnection.createDataChannel("Channel");
dataChannel.onmessage = e => {
try {
console.log(e.data);
let json = JSON.parse(e.data);
if(json.renegotiation){
getOffer(roomid);
}
} catch (e) {
return false;
}
};
dataChannel.onopen = e => console.log("open!!!!");
dataChannel.onclose = e => leave(roomid);
peerConnection.onnegotiationneeded = e => {
renegotiateStart(roomid)
}
peerConnection.onicecandidate = e => {
console.log(" NEW ice candidate!! on localconnection reprinting SDP " )
let lsdp = JSON.stringify(peerConnection.localDescription);
let lsdpoffer = {'offer' : lsdp};
saveOffer(lsdpoffer,roomid);
tryUntilAnswer(roomid);
//console.log(lsdpoffer);
}
let offer = await peerConnection.createOffer(options);
await peerConnection.setLocalDescription(offer);
console.log("Local description set");
let lsdp = JSON.stringify(peerConnection.localDescription);
let lsdpoffer = {'offer' : lsdp};
//console.log(lsdpoffer);
console.log("Calling");
saveOffer(lsdpoffer,roomid);
//console.log(JSON.stringify(peerConnection.localDescription))
//console.log(tosendoffer);
}
//Answering to a call
signalingChannel.addEventListener('message', async message => {
message = JSON.parse(message);
if (message.offer) {
peerConnection.setRemoteDescription(message.offer);
peerConnection.onnegotiationneeded = e => {
renegotiateStart(roomid)
}
peerConnection.onicecandidate = e => {
console.log(" NEW ice candidate!! on localconnection reprinting SDP " )
let lsdp = JSON.stringify(peerConnection.localDescription);
let lsdpanswer = {'answer':lsdp};
//console.log(lsdpanswer);
setAnswer(lsdpanswer,roomid);
}
peerConnection.ondatachannel= e => {
dataChannel = e.channel;
dataChannel.onmessage =e => {
try {
console.log(e.data);
let json = JSON.parse(e.data);
if(json.renegotiation){
getOffer(roomid);
}
} catch (e) {
return false;
}
};
dataChannel.onopen = e => console.log("open!!!!");
dataChannel.onclose =e => leave(roomid);
peerConnection.channel = dataChannel;
}
let answer = await peerConnection.createAnswer(options);
await peerConnection.setLocalDescription(answer);
}
});
// Listen for connectionstatechange on the local RTCPeerConnection
const remoteVideo = document.getElementById('remoteVideo');
peerConnection.addEventListener('track', async (event) => {
console.log("type: "+typeof(event.streams));
console.log("streams: "+event.streams);
const [remoteStream] = event.streams;
remoteVideo.srcObject = remoteStream;
console.log("New remoteStream:", remoteStream);
});
peerConnection.addEventListener('connectionstatechange', event => {
if (peerConnection.connectionState === 'connected') {
// Peers connected!
console.log("Connected!");
}
});
Code that toggles camera and microphone
function toggleVisible(eid){
let e = document.getElementById(eid);
if(e.classList.contains('d-none')){
e.classList.remove('d-none');
}
else{
e.classList.add('d-none');
}
}
async function toggleCamera(){
console.log(localStream);
let togbut = document.getElementById("toggleCameraButton");
let togmic = document.getElementById("toggleMicrophoneButton");
let audio = false;
if(localStream){
localStream.getTracks().forEach((track) => track.stop());
localStream = null;
}
if(togmic.classList.contains('on')){
audio = true;
}
let localPlayer = document.getElementById('localVideo');
if(togbut.classList.contains('off')){
if(await playVideoFromCamera(true, audio) == false) {
return false;
}
togbut.src = "/ozekiservices/chatvideo/attachments/camera-video.svg";
togbut.classList.remove('off');
togbut.classList.add('on');
return;
}
togbut.src = "/ozekiservices/chatvideo/attachments/camera-video-off.svg";
localPlayer.pause();
if(audio){
if(await playVideoFromCamera(false, audio) == false){
return false;
}
}
togbut.classList.add('off');
togbut.classList.remove('on');
localPlayer.classList.add('d-none');
//makeCall(roomid);
}
async function toggleMicrophone(){
console.log(localStream);
console.log("toggleMic");
let togbut = document.getElementById("toggleMicrophoneButton");
let togcam = document.getElementById("toggleCameraButton");
console.log("Stopping tracks");
if(localStream){
localStream.getTracks().forEach((track) => track.stop());
localStream = null;
}
console.log("Tracks stopped");
let video = false;
if(togcam.classList.contains('on')){
console.log("CAM feed ON");
video = true;
}
let localPlayer = document.getElementById('localVideo');
if(togbut.classList.contains('off')){
console.log("Turn MIC ON");
if(await playVideoFromCamera(video, true) == false) return false;
togbut.src = "/ozekiservices/chatvideo/attachments/mic.svg";
togbut.classList.remove('off');
togbut.classList.add('on');
return;
}
togbut.src = "/ozekiservices/chatvideo/attachments/mic-mute.svg";
localPlayer.pause();
if(video){
if(await playVideoFromCamera(video, false) == false) return false;
}
togbut.classList.add('off');
togbut.classList.remove('on');
if(!video){
localPlayer.classList.add('d-none');
}
}
Code that opens camera feed
let minWidth= 200;
let minHeight= 200;
function cameratooption(camera)
{
const cameraOption = document.createElement('option');
cameraOption.label = camera.label;
cameraOption.value = camera.deviceId;
return cameraOption;
}
function updateCameraList(cameras) {
const listElement = document.getElementById('availableCameras');
listElement.innerHTML = '';
let cameraoptions = cameras.map(camera => cameratooption(camera));
console.log(cameraoptions);
cameraoptions.forEach(cameraOption => listElement.add(cameraOption));
}
// Fetch an array of devices of a certain type
async function getConnectedDevices(type) {
const devices = await navigator.mediaDevices.enumerateDevices();
let cameras = devices.filter(device => device.kind === type);
updateCameraList(cameras);
}
// Get the initial set of cameras connected
const videoCameras = getConnectedDevices('videoinput');
// Listen for changes to media devices and update the list accordingly
navigator.mediaDevices.addEventListener('devicechange', event => {
const newCameraList = getConnectedDevices('videoinput');
});
// Open camera with at least minWidth and minHeight capabilities
async function openCamera(cameraId, minWidth, minHeight, video, audio) {
if(video){
video = {
'deviceId': cameraId,
'width': {'min': minWidth},
'height': {'min': minHeight}
}
}
if(audio){
audio = {'echoCancellation': true}
}
const constraints = {
'audio': audio,
'video': video
}
let result = false;
try{
result = await navigator.mediaDevices.getUserMedia(constraints);
}
catch{
console.log("cam not accesible");
}
return result;
}
var localStream;
async function playVideoFromCamera(video, audio) {
if(!(video || audio)){
return;
}
try {
e = document.getElementById('availableCameras');
cameraId = e.options[e.selectedIndex].value;
console.log(cameraId);
const stream = await openCamera(cameraId, minWidth, minHeight, video, audio);
if(stream == false){
return false;
}
localStream = stream;
const videoElement = document.querySelector('video#localVideo');
if(video){
videoElement.classList.remove('d-none');
}
let newStream = new MediaStream(stream.getTracks());
let videoTrack = newStream.getVideoTracks()[0];
let audioTrack = newStream.getAudioTracks()[0];
if(audioTrack){
newStream.removeTrack(audioTrack);
}
videoElement.srcObject = newStream;
addTrack(stream);
console.log("streamdata: "+stream);
} catch(error) {
console.error('Error opening video camera.', error);
}
}
I have tried to debug basically every part of my code (Which is quite hard, due to my botched together code), tried different approaches with no luck. If anyone can tell me what i am doing wrong, i will be really thankful because i am out of ideas at this point and honestly just stuck.