diff --git a/package-lock.json b/package-lock.json
index 4d6a398..0736b51 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"floating-vue": "^2.0.0-beta.24",
+ "hark": "^1.2.3",
"interactjs": "^1.10.18",
"qrcodejs": "github:danielgjackson/qrcodejs",
"vue": "^3.3.4",
@@ -1260,6 +1261,14 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
+ "node_modules/hark": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/hark/-/hark-1.2.3.tgz",
+ "integrity": "sha512-u68vz9SCa38ESiFJSDjqK8XbXqWzyot7Cj6Y2b6jk2NJ+II3MY2dIrLMg/kjtIAun4Y1DHF/20hfx4rq1G5GMg==",
+ "dependencies": {
+ "wildemitter": "^1.2.0"
+ }
+ },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -2056,6 +2065,11 @@
"node": ">= 8"
}
},
+ "node_modules/wildemitter": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/wildemitter/-/wildemitter-1.2.1.tgz",
+ "integrity": "sha512-UMmSUoIQSir+XbBpTxOTS53uJ8s/lVhADCkEbhfRjUGFDPme/XGOb0sBWLx5sTz7Wx/2+TlAw1eK9O5lw5PiEw=="
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
diff --git a/package.json b/package.json
index 9d1e26a..2021015 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
},
"dependencies": {
"floating-vue": "^2.0.0-beta.24",
+ "hark": "^1.2.3",
"interactjs": "^1.10.18",
"qrcodejs": "github:danielgjackson/qrcodejs",
"vue": "^3.3.4",
diff --git a/public/static/css/chat.css b/public/static/css/chat.css
index 450dfba..8d6af2d 100644
--- a/public/static/css/chat.css
+++ b/public/static/css/chat.css
@@ -241,11 +241,17 @@ img {
width: 168px;
height: 112px;
background-color: black;
+ border: 1px solid black;
margin: 3px;
overflow: hidden;
resize: both;
}
+/* A speaking webcam */
+.feed.is-speaking {
+ border: 1px solid #09F;
+}
+
/* A popped-out video feed window */
div.feed.popped-out {
position: absolute;
diff --git a/src/App.vue b/src/App.vue
index 9c25b56..61c3421 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -4,6 +4,7 @@ import FloatingVue from 'floating-vue';
import 'floating-vue/dist/style.css';
import { Mentionable } from 'vue-mention';
import EmojiPicker from 'vue3-emoji-picker';
+import hark from 'hark';
import AlertModal from './components/AlertModal.vue';
import LoginModal from './components/LoginModal.vue';
@@ -244,6 +245,7 @@ export default {
muted: {}, // muted bool per username
booted: {}, // booted bool per username
poppedOut: {}, // popped-out video per username
+ speaking: {}, // speaking boolean per username
// RTCPeerConnections per username.
pc: {},
@@ -1760,6 +1762,9 @@ export default {
this.WebRTC.muted[username] = true;
$ref.muted = true;
}
+
+ // Set up the speech detector.
+ this.initSpeakingEvents(username, $ref);
});
// Inform them they are being watched.
@@ -1789,7 +1794,7 @@ export default {
this.WebRTC.frozenStreamInterval[username] = setInterval(() => {
if (videoTrack.muted) freezeDetected();
}, 3000);
- })
+ });
};
// ANSWERER: add our video to the connection so that the offerer (the one who
@@ -2256,6 +2261,9 @@ export default {
// Begin dark video detection.
this.initDarkVideoDetection();
+
+ // Begin monitoring for speaking events.
+ this.initSpeakingEvents(this.username, this.webcam.elem);
}).catch(err => {
this.ChatClient(`Webcam error: ${err}
Please see the troubleshooting guide for help.`);
}).finally(() => {
@@ -2943,6 +2951,27 @@ export default {
})
},
+ // Webcam "is speaking" functions.
+ initSpeakingEvents(username, element) {
+ // element is the