fix: Jitsi meeting join rendered blank page — call JitsiMeetExternalAPI with 'new'
- Bind JitsiMeetExternalAPI as extension type external constructor; the old external function binding invoked the ES6 class without 'new' and threw at runtime (silently, in release builds) - joinMeeting: wait up to 5s for platform-view container div (post-frame race), build options via setProperty (jsify cannot carry DOM nodes), return success bool, log failures to browser console - jitsi_screen: show 'Could not start the meeting' UI instead of blank - v1.6.2+10, deployed to app2.m8chat.au Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// Version: 1.1.0 | Created: 2026-04-05 | Updated: 2026-04-10
|
||||
// Version: 1.2.0 | Created: 2026-04-05 | Updated: 2026-07-04
|
||||
// Web implementation: uses JitsiMeetExternalAPI via dart:js_interop.
|
||||
// This file is web-only — guarded by the Flutter web build.
|
||||
// The external_api.js script is lazy-loaded on first use — not in index.html.
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:js_interop_unsafe';
|
||||
import 'dart:ui_web' as ui_web;
|
||||
|
||||
import 'package:web/web.dart' as web;
|
||||
@@ -77,9 +78,14 @@ class JitsiWebService {
|
||||
}
|
||||
|
||||
/// Starts a Jitsi meeting inside the container div created by the platform view.
|
||||
/// Call this AFTER the HtmlElementView has been mounted (e.g. in a post-frame callback).
|
||||
/// Lazy-loads the Jitsi script on first call.
|
||||
Future<void> joinMeeting({
|
||||
/// Call this AFTER the HtmlElementView has been scheduled for mounting
|
||||
/// (e.g. in a post-frame callback). Lazy-loads the Jitsi script on first
|
||||
/// call and waits for the platform-view container div to appear — the
|
||||
/// post-frame callback can run before the HtmlElementView factory has
|
||||
/// inserted the div, which previously made this method bail out silently
|
||||
/// and leave the meeting screen blank.
|
||||
/// Returns true if the meeting iframe was created.
|
||||
Future<bool> joinMeeting({
|
||||
required String roomName,
|
||||
String? jwt,
|
||||
String? displayName,
|
||||
@@ -90,53 +96,73 @@ class JitsiWebService {
|
||||
|
||||
// Lazy-load the external_api.js script if not already present.
|
||||
final loaded = await _ensureScriptLoaded();
|
||||
if (!loaded) return;
|
||||
if (!loaded) {
|
||||
web.console.warn('[Jitsi] external_api.js failed to load'.toJS);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the container div — there should be exactly one with our prefix.
|
||||
final containers = web.document.querySelectorAll('[id^="jitsi-container-"]');
|
||||
if (containers.length == 0) return;
|
||||
final parentNode = containers.item(containers.length - 1);
|
||||
if (parentNode == null) return;
|
||||
// Wait for the container div — the platform view may not be in the DOM
|
||||
// yet when the post-frame callback fires.
|
||||
web.Node? parentNode;
|
||||
for (var i = 0; i < 50; i++) {
|
||||
final containers =
|
||||
web.document.querySelectorAll('[id^="jitsi-container-"]');
|
||||
if (containers.length > 0) {
|
||||
parentNode = containers.item(containers.length - 1);
|
||||
break;
|
||||
}
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
if (parentNode == null) {
|
||||
web.console.warn('[Jitsi] container div never appeared'.toJS);
|
||||
return false;
|
||||
}
|
||||
|
||||
final configOverwrite = <String, Object?>{
|
||||
'startAudioMuted': 0,
|
||||
'startVideoMuted': 0,
|
||||
'disableDeepLinking': true,
|
||||
'prejoinPageEnabled': true,
|
||||
}.jsify();
|
||||
// Build the options as a real JS object. Passing a DOM node through
|
||||
// Map.jsify() is not supported and threw at runtime in the dart2js build.
|
||||
final configOverwrite = JSObject()
|
||||
..setProperty('startAudioMuted'.toJS, 0.toJS)
|
||||
..setProperty('startVideoMuted'.toJS, 0.toJS)
|
||||
..setProperty('disableDeepLinking'.toJS, true.toJS)
|
||||
..setProperty('prejoinPageEnabled'.toJS, true.toJS);
|
||||
|
||||
final interfaceConfigOverwrite = <String, Object?>{
|
||||
'SHOW_CHROME_EXTENSION_BANNER': false,
|
||||
}.jsify();
|
||||
final interfaceConfigOverwrite = JSObject()
|
||||
..setProperty('SHOW_CHROME_EXTENSION_BANNER'.toJS, false.toJS);
|
||||
|
||||
final options = <String, Object?>{
|
||||
'roomName': roomName,
|
||||
'parentNode': parentNode,
|
||||
'width': '100%',
|
||||
'height': '100%',
|
||||
'configOverwrite': configOverwrite,
|
||||
'interfaceConfigOverwrite': interfaceConfigOverwrite,
|
||||
};
|
||||
final options = JSObject()
|
||||
..setProperty('roomName'.toJS, roomName.toJS)
|
||||
..setProperty('parentNode'.toJS, parentNode as JSAny)
|
||||
..setProperty('width'.toJS, '100%'.toJS)
|
||||
..setProperty('height'.toJS, '100%'.toJS)
|
||||
..setProperty('configOverwrite'.toJS, configOverwrite)
|
||||
..setProperty('interfaceConfigOverwrite'.toJS, interfaceConfigOverwrite);
|
||||
|
||||
if (jwt != null && jwt.isNotEmpty) {
|
||||
options['jwt'] = jwt;
|
||||
options.setProperty('jwt'.toJS, jwt.toJS);
|
||||
}
|
||||
|
||||
if (displayName != null && displayName.isNotEmpty) {
|
||||
options['userInfo'] = <String, Object?>{
|
||||
'displayName': displayName,
|
||||
if (avatarUrl != null) 'avatarUrl': avatarUrl,
|
||||
}.jsify();
|
||||
final userInfo = JSObject()
|
||||
..setProperty('displayName'.toJS, displayName.toJS);
|
||||
if (avatarUrl != null) {
|
||||
userInfo.setProperty('avatarUrl'.toJS, avatarUrl.toJS);
|
||||
}
|
||||
options.setProperty('userInfo'.toJS, userInfo);
|
||||
}
|
||||
|
||||
final jsOptions = options.jsify();
|
||||
_api = _createJitsiApi(AppConfig.jitsiDomain.toJS, jsOptions as JSObject);
|
||||
try {
|
||||
_api = _createJitsiApi(AppConfig.jitsiDomain.toJS, options);
|
||||
} catch (e) {
|
||||
web.console.error('[Jitsi] JitsiMeetExternalAPI threw: $e'.toJS);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (onReadyToClose != null && _api != null) {
|
||||
_addEventListener(_api!, 'readyToClose'.toJS, () {
|
||||
onReadyToClose();
|
||||
}.toJS);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Cleans up the Jitsi iframe.
|
||||
@@ -148,17 +174,21 @@ class JitsiWebService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls `new JitsiMeetExternalAPI(domain, options)`.
|
||||
/// Binding for the JitsiMeetExternalAPI ES6 class.
|
||||
/// The external constructor compiles to `new JitsiMeetExternalAPI(...)`.
|
||||
/// (A plain external function binding calls it WITHOUT `new`, which throws
|
||||
/// "Class constructor cannot be invoked without 'new'" — the original cause
|
||||
/// of the blank meeting screen.)
|
||||
@JS('JitsiMeetExternalAPI')
|
||||
external JSObject _createJitsiApi(JSString domain, JSObject options);
|
||||
|
||||
/// Calls `api.addEventListener(event, callback)`.
|
||||
@JS()
|
||||
extension type _JitsiApi(JSObject _) implements JSObject {
|
||||
extension type _JitsiApi._(JSObject _) implements JSObject {
|
||||
external _JitsiApi(JSString domain, JSObject options);
|
||||
external void addEventListener(JSString event, JSFunction callback);
|
||||
external void dispose();
|
||||
}
|
||||
|
||||
JSObject _createJitsiApi(JSString domain, JSObject options) =>
|
||||
_JitsiApi(domain, options);
|
||||
|
||||
void _addEventListener(JSObject api, JSString event, JSFunction callback) {
|
||||
(api as _JitsiApi).addEventListener(event, callback);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user