[mirotalk] - fix: probe 48kHz support at init and guard worklet null access in RNNoise, update dep

This commit is contained in:
Miroslav Pejic
2026-04-08 09:38:07 +02:00
parent 61cbaae961
commit 7f484e0a5a
8 changed files with 108 additions and 67 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# ====================================================
# MiroTalk P2P v.1.7.94 - Environment Configuration
# MiroTalk P2P v.1.7.95 - Environment Configuration
# ====================================================
# App environment
+1 -1
View File
@@ -2,7 +2,7 @@
/**
* ==============================================
* MiroTalk P2P v.1.7.94 - Configuration File
* MiroTalk P2P v.1.7.95 - Configuration File
* ==============================================
*
* This file is the central configuration source.
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies: {
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.7.94
* @version 1.7.95
*
*/
+24 -44
View File
@@ -1,12 +1,12 @@
{
"name": "mirotalk",
"version": "1.7.94",
"version": "1.7.95",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mirotalk",
"version": "1.7.94",
"version": "1.7.95",
"license": "AGPL-3.0",
"dependencies": {
"@mattermost/client": "11.5.0",
@@ -19,7 +19,7 @@
"cors": "^2.8.6",
"crypto-js": "^4.2.0",
"dompurify": "^3.3.3",
"dotenv": "^17.4.0",
"dotenv": "^17.4.1",
"express": "^5.2.1",
"express-openid-connect": "^2.20.1",
"express-rate-limit": "^8.3.2",
@@ -27,9 +27,9 @@
"helmet": "^8.1.0",
"httpolyglot": "0.1.2",
"js-yaml": "^4.1.1",
"jsdom": "^29.0.1",
"jsdom": "^29.0.2",
"jsonwebtoken": "^9.0.3",
"nodemailer": "^8.0.4",
"nodemailer": "^8.0.5",
"openai": "^6.33.0",
"qs": "^6.15.0",
"socket.io": "^4.8.3",
@@ -47,55 +47,35 @@
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz",
"integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==",
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.6.tgz",
"integrity": "sha512-BXWCh8dHs9GOfpo/fWGDJtDmleta2VePN9rn6WQt3GjEbxzutVF4t0x2pmH+7dbMCLtuv3MlwqRsAuxlzFXqFg==",
"license": "MIT",
"dependencies": {
"@csstools/css-calc": "^3.1.1",
"@csstools/css-color-parser": "^4.0.2",
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-tokenizer": "^4.0.0",
"lru-cache": "^11.2.6"
"@csstools/css-tokenizer": "^4.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
"version": "11.2.7",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@asamuzakjp/dom-selector": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.3.tgz",
"integrity": "sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==",
"version": "7.0.8",
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.8.tgz",
"integrity": "sha512-erMO6FgtM02dC24NGm0xufMzWz5OF0wXKR7BpvGD973bq/GbmR8/DbxNZbj0YevQ5hlToJaWSVK/G9/NDgGEVw==",
"license": "MIT",
"dependencies": {
"@asamuzakjp/nwsapi": "^2.3.9",
"bidi-js": "^1.0.3",
"css-tree": "^3.2.1",
"is-potential-custom-element-name": "^1.0.1",
"lru-cache": "^11.2.7"
"is-potential-custom-element-name": "^1.0.1"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
"version": "11.2.7",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@asamuzakjp/nwsapi": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
@@ -2538,9 +2518,9 @@
}
},
"node_modules/dotenv": {
"version": "17.4.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.0.tgz",
"integrity": "sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==",
"version": "17.4.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz",
"integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@@ -4319,13 +4299,13 @@
}
},
"node_modules/jsdom": {
"version": "29.0.1",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz",
"integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==",
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz",
"integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==",
"license": "MIT",
"dependencies": {
"@asamuzakjp/css-color": "^5.0.1",
"@asamuzakjp/dom-selector": "^7.0.3",
"@asamuzakjp/css-color": "^5.1.5",
"@asamuzakjp/dom-selector": "^7.0.6",
"@bramus/specificity": "^2.4.2",
"@csstools/css-syntax-patches-for-csstree": "^1.1.1",
"@exodus/bytes": "^1.15.0",
@@ -4799,9 +4779,9 @@
}
},
"node_modules/nodemailer": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz",
"integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==",
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz",
"integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "mirotalk",
"version": "1.7.94",
"version": "1.7.95",
"description": "A free WebRTC browser-based video call",
"main": "server.js",
"scripts": {
@@ -54,7 +54,7 @@
"cors": "^2.8.6",
"crypto-js": "^4.2.0",
"dompurify": "^3.3.3",
"dotenv": "^17.4.0",
"dotenv": "^17.4.1",
"express": "^5.2.1",
"express-openid-connect": "^2.20.1",
"express-rate-limit": "^8.3.2",
@@ -62,9 +62,9 @@
"helmet": "^8.1.0",
"httpolyglot": "0.1.2",
"js-yaml": "^4.1.1",
"jsdom": "^29.0.1",
"jsdom": "^29.0.2",
"jsonwebtoken": "^9.0.3",
"nodemailer": "^8.0.4",
"nodemailer": "^8.0.5",
"openai": "^6.33.0",
"qs": "^6.15.0",
"socket.io": "^4.8.3",
+1 -1
View File
@@ -107,7 +107,7 @@ let brand = {
},
about: {
imageUrl: '../images/mirotalk-logo.gif',
title: 'WebRTC P2P v1.7.94',
title: 'WebRTC P2P v1.7.95',
html: `
<button
id="support-button"
+41 -12
View File
@@ -15,7 +15,7 @@
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.7.94
* @version 1.7.95
*
*/
@@ -2427,7 +2427,7 @@ function checkPeerAudioVideo() {
* the browser lacks AudioWorklet / WebAssembly.
* Call once during startup, before any audio stream is created.
*/
function initRNNoiseSuppression() {
async function initRNNoiseSuppression() {
if (typeof RNNoiseProcessor === 'undefined') {
console.warn('RNNoiseProcessor class is not available (script not loaded).');
handleRNNoiseNotSupported();
@@ -2440,6 +2440,13 @@ function initRNNoiseSuppression() {
return;
}
const supports48k = await RNNoiseProcessor.isSampleRateSupported();
if (!supports48k) {
console.warn('RNNoise: device does not support 48 kHz sample rate, skipping.');
handleRNNoiseNotSupported();
return;
}
// Tear down any leftover processor from a previous session / hot-reload.
stopNoiseSuppressionPipeline();
@@ -2490,6 +2497,10 @@ async function enableNoiseSuppression() {
console.warn('Noise suppression returned no usable stream, falling back to raw mic.');
stopNoiseSuppressionPipeline();
await refreshMyStreamToPeers(localAudioMediaStream, true);
toastMessage(
'warning',
'Noise suppression is not supported on this device. Using default WebRTC noise suppression instead.'
);
return false;
}
@@ -3827,16 +3838,32 @@ async function setupLocalAudioMedia() {
console.log('Requesting access to audio inputs');
// Check RNNoise support early, before audio streams are created.
initRNNoiseSuppression();
await initRNNoiseSuppression();
const audioConstraints = useAudio ? getAudioConstraints() : { audio: false };
try {
const stream = await navigator.mediaDevices.getUserMedia(audioConstraints);
if (stream) {
await loadLocalMedia(stream, 'audio');
/*
Verify the audio track is live on some mobile devices getUserMedia
succeeds but the track is muted/ended (e.g. built-in mic restrictions).
*/
let activeStream = stream;
const audioTrack = stream.getAudioTracks()[0];
if (audioTrack && (audioTrack.readyState === 'ended' || audioTrack.muted)) {
console.warn(
'Audio track obtained but is ' +
(audioTrack.muted ? 'muted' : 'ended') +
', retrying with relaxed constraints'
);
stream.getTracks().forEach((t) => t.stop());
activeStream = await navigator.mediaDevices.getUserMedia({ audio: true });
}
await loadLocalMedia(activeStream, 'audio');
if (useAudio) {
localAudioMediaStream = stream;
localAudioMediaStream = activeStream;
console.log('10. Access granted to audio device');
// Auto-enable noise suppression if the user had it active in a previous session.
@@ -7364,8 +7391,7 @@ function setupMySettings() {
if (!ok) {
lsSettings.mic_noise_suppression = false;
lS.setSettings(lsSettings);
e.currentTarget.checked = false;
toastMessage('warning', 'Noise suppression unavailable');
switchNoiseSuppression.checked = false;
} else {
toastMessage('success', 'Noise suppression enabled');
}
@@ -7375,7 +7401,7 @@ function setupMySettings() {
await disableNoiseSuppression(true);
toastMessage('info', 'Noise suppression disabled');
}
e.target.blur();
switchNoiseSuppression.blur();
};
// select audio output
@@ -7873,10 +7899,13 @@ function getAudioConstraints(deviceId = null) {
const useBuiltInNoiseSuppression = !buttons.settings.customNoiseSuppression || !isRNNoiseSupported;
// Enhanced audio constraints for better quality and volume on all devices
// On mobile, use { ideal: true } so getUserMedia succeeds even if the
// device's built-in mic cannot honour a constraint (e.g. iOS Safari may
// silently suppress audio when echoCancellation is strictly required).
const audioConstraints = {
echoCancellation: true, // Prevents echo/feedback
autoGainControl: true, // Automatically adjusts microphone volume
noiseSuppression: useBuiltInNoiseSuppression, // Use RNNoise instead
echoCancellation: isMobileDevice ? { ideal: true } : true,
autoGainControl: isMobileDevice ? { ideal: true } : true,
noiseSuppression: useBuiltInNoiseSuppression,
};
/*
deviceId handling is platform-dependent:
@@ -14937,7 +14966,7 @@ function showAbout() {
Swal.fire({
background: swBg,
position: 'center',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.7.94',
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.7.95',
imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about,
customClass: { image: 'img-about' },
html: `
+35 -3
View File
@@ -66,6 +66,12 @@ class WasmLoader {
async loadWasmBuffer() {
try {
const workletNode = this.getWorkletNode();
if (!workletNode) {
this.uiManager.updateStatus('⚠️ Worklet node not available, skipping WASM load', 'warning');
return;
}
this.uiManager.updateStatus('📦 Loading RNNoise sync module...', 'info');
const jsResponse = await fetch('../js/rnnoiseSync.js');
@@ -77,7 +83,13 @@ class WasmLoader {
const jsContent = await jsResponse.text();
this.uiManager.updateStatus('📦 Sending sync module to worklet...', 'info');
this.getWorkletNode().port.postMessage({
const node = this.getWorkletNode();
if (!node) {
this.uiManager.updateStatus('⚠️ Worklet node disconnected before WASM could be sent', 'warning');
return;
}
node.port.postMessage({
type: 'sync-module',
jsContent: jsContent,
});
@@ -124,6 +136,23 @@ class RNNoiseProcessor {
}
}
/**
* Probe whether the device actually supports a 48 kHz sample rate.
* Creates a temporary AudioContext, checks the real rate, then closes it.
* @returns {Promise<boolean>}
*/
static async isSampleRateSupported() {
try {
const AudioCtx = window.AudioContext || window.webkitAudioContext;
const ctx = new AudioCtx({ sampleRate: 48000 });
const actual = ctx.sampleRate;
await ctx.close();
return actual === 48000;
} catch (e) {
return false;
}
}
initializeUI() {
this.elements = {
labelNoiseSuppression: document.getElementById('labelNoiseSuppression'),
@@ -156,9 +185,12 @@ class RNNoiseProcessor {
return null;
}
// 48 kHz support is verified by isSampleRateSupported() at init.
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 48000 });
const sampleRate = this.audioContext.sampleRate;
this.uiManager.updateStatus(`🎵 Audio context created with sample rate: ${sampleRate}Hz`, 'info');
this.uiManager.updateStatus(
`🎵 Audio context created with sample rate: ${this.audioContext.sampleRate}Hz`,
'info'
);
if (this.audioContext.state === 'suspended') {
try {