diff --git a/.gitignore b/.gitignore
index 667aaef..bceaed0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
+.env
### STS ###
.apt_generated
diff --git a/src/main/resources/static/app.html b/src/main/resources/static/app.html
index c6ef4e5..cdaf9af 100644
--- a/src/main/resources/static/app.html
+++ b/src/main/resources/static/app.html
@@ -4,7 +4,7 @@
-
+
Random Video Chat
@@ -12,7 +12,7 @@
-
+
@@ -20,6 +20,13 @@
+
+
+
Searching for a partner...
@@ -31,18 +38,22 @@
-
-
-
-
-
-
+
+
+
+
+
+
@@ -59,7 +70,7 @@ Find New Partner?
-
+
diff --git a/src/main/resources/static/privacy.html b/src/main/resources/static/privacy.html
index 80d675e..15b2c90 100644
--- a/src/main/resources/static/privacy.html
+++ b/src/main/resources/static/privacy.html
@@ -9,7 +9,7 @@
-
+
diff --git a/src/main/resources/static/script.js b/src/main/resources/static/script.js
index edd8536..137bafd 100644
--- a/src/main/resources/static/script.js
+++ b/src/main/resources/static/script.js
@@ -116,15 +116,24 @@ ws.onmessage = async (message) => {
break;
case "offer":
setControlsEnabled(true);
- await ensurePeerConnection();
- await peerConnection.setRemoteDescription(
- new RTCSessionDescription(data.offer)
- );
+
+ // If not in have-remote-offer, forcibly reset connection
+ if (!peerConnection || peerConnection.signalingState !== "have-remote-offer") {
+ if (peerConnection) peerConnection.close();
+ await ensurePeerConnection();
+ await ensureLocalMediaAndTracks();
+ }
+
+ await peerConnection.setRemoteDescription(new RTCSessionDescription(data.offer));
await processQueuedIceCandidates();
- await ensureLocalMediaAndTracks();
- const answer = await peerConnection.createAnswer();
- await peerConnection.setLocalDescription(answer);
- ws.send(JSON.stringify({ type: "answer", answer: answer }));
+
+ if (peerConnection.signalingState === "have-remote-offer" || peerConnection.signalingState === "have-local-pranswer") {
+ const answer = await peerConnection.createAnswer();
+ await peerConnection.setLocalDescription(answer);
+ ws.send(JSON.stringify({ type: "answer", answer: answer }));
+ } else {
+ console.warn("Skipping createAnswer: signalingState is", peerConnection.signalingState);
+ }
break;
case "answer":
await peerConnection.setRemoteDescription(
@@ -164,7 +173,7 @@ async function ensurePeerConnection() {
remoteIceCandidatesQueue = [];
const iceServersFromBackend = await fetchTurnConfig();
- console.log(iceServersFromBackend);
+ //console.log(iceServersFromBackend);
peerConnection = new RTCPeerConnection({
iceServers: iceServersFromBackend
@@ -464,4 +473,62 @@ async function fetchTurnConfig() {
console.warn('Failed to fetch TURN config:', e);
return [];
}
-}
\ No newline at end of file
+}
+
+const audioOutputBtn = document.getElementById("audio-output-btn");
+const audioOutputIcon = document.getElementById("audio-output-icon");
+
+let isAudioStopped = false;
+
+// Detect and set default output
+async function setDefaultAudioOutput() {
+ if (!navigator.mediaDevices?.enumerateDevices) return;
+
+ try {
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ const earphones = devices.find(
+ (d) =>
+ d.kind === "audiooutput" &&
+ (d.label.toLowerCase().includes("headphone") ||
+ d.label.toLowerCase().includes("bluetooth"))
+ );
+
+ if (typeof remoteVideo.setSinkId === "function") {
+ if (earphones) {
+ await remoteVideo.setSinkId(earphones.deviceId);
+ // console.log("Earphones detected - switched to earphones.");
+ showNotification("Earphones connected");
+ audioOutputIcon.className = "fa-solid fa-headphones";
+ } else {
+ await remoteVideo.setSinkId("");
+ // console.log("Defaul to speaker.");
+ showNotification("Using speaker");
+ audioOutputIcon.className = "fa-solid fa-volume-high";
+ }
+ }
+ } catch (err) {
+ console.warn("Audio device detection failed:", err);
+ }
+}
+
+// device connection changes
+if (navigator.mediaDevices?.addEventListener) {
+ navigator.mediaDevices.addEventListener("devicechange", setDefaultAudioOutput);
+}
+
+// stop/start button
+audioOutputBtn.addEventListener("click", () => {
+ if (isAudioStopped) {
+ remoteVideo.muted = false;
+ showNotification("Audio resumed 🔊");
+ audioOutputIcon.className = "fa-solid fa-volume-high";
+ } else {
+ remoteVideo.muted = true;
+ showNotification("Audio stopped 🔇");
+ audioOutputIcon.className = "fa-solid fa-volume-xmark";
+ }
+ isAudioStopped = !isAudioStopped;
+});
+
+// on load
+setDefaultAudioOutput();
diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css
index bad3708..d9e38bf 100644
--- a/src/main/resources/static/style.css
+++ b/src/main/resources/static/style.css
@@ -302,6 +302,20 @@ video {
background-color: #000;
}
+.top-left-btn {
+ position: absolute;
+ top: 20px;
+ left: 20px;
+ z-index: 50;
+}
+
+.top-right-btn {
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ z-index: 50;
+}
+
@media (max-width: 768px) {
#localVideo {
bottom: 85px;
@@ -324,4 +338,13 @@ video {
width: 95%;
padding: 1.25rem;
}
+
+ .top-left-btn {
+ top: 10px;
+ left: 10px;
+ }
+ .top-right-btn {
+ top: 10px;
+ right: 10px;
+ }
}
\ No newline at end of file