[mirotalksfu] - fix setSinkId now require user gesture

هذا الالتزام موجود في:
Miroslav Pejic
2025-08-15 22:49:50 +02:00
الأصل c614ce9cf4
التزام f6c76519fd
6 ملفات معدلة مع 83 إضافات و32 حذوفات

عرض الملف

@@ -64,7 +64,7 @@ dev dependencies: {
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon * @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970 * @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com * @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.9.35 * @version 1.9.36
* *
*/ */

4
package-lock.json مولّد
عرض الملف

@@ -1,12 +1,12 @@
{ {
"name": "mirotalksfu", "name": "mirotalksfu",
"version": "1.9.35", "version": "1.9.36",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mirotalksfu", "name": "mirotalksfu",
"version": "1.9.35", "version": "1.9.36",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.864.0", "@aws-sdk/client-s3": "^3.864.0",

عرض الملف

@@ -1,6 +1,6 @@
{ {
"name": "mirotalksfu", "name": "mirotalksfu",
"version": "1.9.35", "version": "1.9.36",
"description": "WebRTC SFU browser-based video calls", "description": "WebRTC SFU browser-based video calls",
"main": "Server.js", "main": "Server.js",
"scripts": { "scripts": {

عرض الملف

@@ -110,7 +110,7 @@ let BRAND = {
}, },
about: { about: {
imageUrl: '../images/mirotalk-logo.gif', imageUrl: '../images/mirotalk-logo.gif',
title: '<strong>WebRTC SFU v1.9.35</strong>', title: '<strong>WebRTC SFU v1.9.36</strong>',
html: ` html: `
<button <button
id="support-button" id="support-button"

عرض الملف

@@ -11,7 +11,7 @@ if (location.href.substr(0, 5) !== 'https') location.href = 'https' + location.h
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon * @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970 * @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com * @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.9.35 * @version 1.9.36
* *
*/ */
@@ -5543,7 +5543,7 @@ function showAbout() {
position: 'center', position: 'center',
imageUrl: BRAND.about?.imageUrl && BRAND.about.imageUrl.trim() !== '' ? BRAND.about.imageUrl : image.about, imageUrl: BRAND.about?.imageUrl && BRAND.about.imageUrl.trim() !== '' ? BRAND.about.imageUrl : image.about,
customClass: { image: 'img-about' }, customClass: { image: 'img-about' },
title: BRAND.about?.title && BRAND.about.title.trim() !== '' ? BRAND.about.title : 'WebRTC SFU v1.9.35', title: BRAND.about?.title && BRAND.about.title.trim() !== '' ? BRAND.about.title : 'WebRTC SFU v1.9.36',
html: ` html: `
<br /> <br />
<div id="about"> <div id="about">

عرض الملف

@@ -9,7 +9,7 @@
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon * @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970 * @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com * @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.9.35 * @version 1.9.36
* *
*/ */
@@ -237,6 +237,8 @@ class RoomClient {
this.isMobileDevice = peer_info.is_mobile_device; this.isMobileDevice = peer_info.is_mobile_device;
this.isMobileSafari = this.isMobileDevice && peer_info.browser_name.toLowerCase().includes('safari'); this.isMobileSafari = this.isMobileDevice && peer_info.browser_name.toLowerCase().includes('safari');
this.pendingSinkId = null; // store desired sink id until next user gesture
this.localAudioEl = localAudioEl; this.localAudioEl = localAudioEl;
this.remoteAudioEl = remoteAudioEl; this.remoteAudioEl = remoteAudioEl;
this.videoMediaContainer = videoMediaContainer; this.videoMediaContainer = videoMediaContainer;
@@ -3562,39 +3564,88 @@ class RoomClient {
console.log(who + ' Success attached media ' + type); console.log(who + ' Success attached media ' + type);
} }
hasUserActivation() {
if (navigator.userActivation) return !!navigator.userActivation.isActive;
if ('hasTransientUserActivation' in document) return !!document.hasTransientUserActivation;
return false;
}
runOnNextUserActivation(callback) {
const fire = (e) => {
cleanup();
try {
// Call synchronously to keep the user-activation
callback(e);
} catch (err) {
console.error('runOnNextUserActivation callback error:', err);
}
};
const cleanup = () => {
window.removeEventListener('pointerdown', fire, true);
window.removeEventListener('click', fire, true);
window.removeEventListener('mousedown', fire, true);
window.removeEventListener('touchstart', fire, true);
window.removeEventListener('keydown', fire, true);
};
const opts = { capture: true, once: true, passive: true };
window.addEventListener('pointerdown', fire, opts);
window.addEventListener('click', fire, opts);
window.addEventListener('mousedown', fire, opts);
window.addEventListener('touchstart', fire, opts);
window.addEventListener('keydown', fire, opts);
}
async changeAudioDestination(audioElement = false) { async changeAudioDestination(audioElement = false) {
const audioDestination = speakerSelect.value; const sinkId = speakerSelect?.value;
if (audioElement) { if (!sinkId) return;
await this.attachSinkId(audioElement, audioDestination);
} else { // Defer until a user gesture if needed
const audioElements = this.remoteAudioEl.querySelectorAll('audio'); if (!this.hasUserActivation()) {
audioElements.forEach(async (audioElement) => { this.pendingSinkId = sinkId;
await this.attachSinkId(audioElement, audioDestination); this.userLog('info', 'Click once to apply the selected speaker', 'top-end', 3000);
this.runOnNextUserActivation(() => {
const els = audioElement ? [audioElement] : this.remoteAudioEl.querySelectorAll('audio');
els.forEach((el) => this.attachSinkId(el, this.pendingSinkId));
this.pendingSinkId = null;
}); });
return;
}
const els = audioElement ? [audioElement] : this.remoteAudioEl.querySelectorAll('audio');
for (const el of els) {
await this.attachSinkId(el, sinkId);
} }
} }
async attachSinkId(elem, sinkId) { async attachSinkId(elem, sinkId) {
if (typeof elem.sinkId !== 'undefined') { if (typeof elem.setSinkId !== 'function') {
elem.setSinkId(sinkId) const error = `Browser doesn't support output device selection.`;
.then(() => {
console.log(`Success, audio output device attached: ${sinkId}`);
})
.catch((err) => {
let errorMessage = err;
let speakerSelect = this.getId('speakerSelect');
if (err.name === 'SecurityError')
errorMessage = `You need to use HTTPS for selecting audio output device: ${err}`;
console.error('Attach SinkId error: ', errorMessage);
this.userLog('error', errorMessage, 'top-end', 6000);
speakerSelect.selectedIndex = 0;
refreshLsDevices();
});
} else {
const error = `Browser seems doesn't support output device selection.`;
console.warn(error); console.warn(error);
this.userLog('error', error, 'top-end', 6000); this.userLog('error', error, 'top-end', 6000);
return;
} }
return elem
.setSinkId(sinkId)
.then(() => console.log(`Success, audio output device attached: ${sinkId}`))
.catch((err) => {
const speakerSel = this.getId('speakerSelect');
if (err?.name === 'SecurityError') {
const msg = `Use HTTPS to select audio output device: ${err.message || err}`;
console.error('Attach SinkId error: ', msg);
this.userLog('error', msg, 'top-end', 6000);
} else if (err?.name === 'NotAllowedError' || /user gesture/i.test(err?.message || '')) {
// Retry on next user gesture
this.userLog('info', 'Click once to allow changing the speaker', 'top-end', 4000);
this.pendingSinkId = sinkId;
this.runOnNextUserActivation(() => this.attachSinkId(elem, this.pendingSinkId));
} else {
console.error('Attach SinkId error: ', err);
this.userLog('error', err, 'top-end', 6000);
}
if (speakerSel) speakerSel.selectedIndex = 0;
refreshLsDevices();
});
} }
event(evt) { event(evt) {