(function (context) { "use strict"; context.JK = context.JK || {}; // Incremental web-client bridge shim. // // Phase 1: delegate to FakeJamClient so the install seam can be exercised in // forced-native browser mode without changing behavior. // Phase 2+: replace selected methods with real WebRTC / websocket-gateway / // REST-backed implementations while preserving the jamClient API surface. context.JK.WebJamClient = function (app, p2pMessageFactory) { var logger = context.JK.logger || console; var inner = new context.JK.FakeJamClient(app, p2pMessageFactory); var self = this; var state = { app: app, inSessionPage: false, inSession: false, currentSessionId: null, sessionEventCallbackName: "", sessionJoinLeaveCallbackName: "", recordingCallbacks: null, connectionStatusRefreshRateMs: 0, localMedia: { status: "idle", // idle | requesting | active | failed stream: null, error: null, autoStartEnabled: false } }; // TEMP/diagnostic recorder for manual capture runs. Disabled by default. // Enable with either: // localStorage['jk.webClient.recording'] = '1' // or window.JK_WEB_CLIENT_RECORDING_ENABLED = true var recorder = { enabled: false, events: [], maxEvents: 5000 }; logger.info("*** Web JamClient shim initialized (delegating to FakeJamClient). ***"); logger.info("*** Web JamClient shim mode: delegated fake + incremental overrides ***"); // Copy all enumerable members from the fake client instance so existing code // can call methods/properties directly on this object. Object.keys(inner).forEach(function(key) { var value = inner[key]; if (typeof value === "function") { this[key] = value.bind(inner); } else { this[key] = value; } }, this); function nowIso() { return (new Date()).toISOString(); } function readLocalStorageFlag(key) { try { if (!context.localStorage) { return false; } return context.localStorage.getItem(key) === "1"; } catch (e) { return false; } } function recorderEnabledByDefault() { return !!context.JK_WEB_CLIENT_RECORDING_ENABLED || readLocalStorageFlag("jk.webClient.recording") || !!(context.gon && context.gon.web_client_recording_enabled); } function safeSerialize(value) { try { return JSON.parse(JSON.stringify(value)); } catch (e) { return "[unserializable]"; } } function pushRecord(event) { if (!recorder.enabled) { return; } event = event || {}; event.ts = event.ts || nowIso(); recorder.events.push(event); if (recorder.events.length > recorder.maxEvents) { recorder.events.shift(); } } function instrumentThenable(methodName, result) { if (!result || typeof result.then !== "function") { return result; } try { result.then(function(resolved) { pushRecord({ kind: "jamClientCallResolved", method: methodName, result: safeSerialize(resolved) }); return resolved; }, function(rejected) { pushRecord({ kind: "jamClientCallRejected", method: methodName, error: safeSerialize(rejected) }); return rejected; }); } catch (e) { pushRecord({ kind: "jamClientInstrumentationError", method: methodName, error: (e && e.message) ? e.message : String(e) }); } return result; } function invokeDelegate(methodName, argsLike) { var fn = inner[methodName]; if (typeof fn !== "function") { return undefined; } var args = Array.prototype.slice.call(argsLike || []); pushRecord({ kind: "jamClientCall", method: methodName, args: safeSerialize(args) }); try { var result = fn.apply(inner, args); if (!result || typeof result.then !== "function") { pushRecord({ kind: "jamClientCallReturn", method: methodName, result: safeSerialize(result) }); } return instrumentThenable(methodName, result); } catch (e) { pushRecord({ kind: "jamClientCallThrow", method: methodName, error: (e && e.message) ? e.message : String(e) }); throw e; } } function shouldAutoStartLocalMedia() { return !!(context.gon && context.gon.web_client_media_autostart) || !!context.JK_WEB_CLIENT_MEDIA_AUTOSTART || readLocalStorageFlag("jk.webClient.mediaAutoStart"); } function startLocalMediaIfEnabled() { if (!context.navigator || !context.navigator.mediaDevices || !context.navigator.mediaDevices.getUserMedia) { state.localMedia.status = "failed"; state.localMedia.error = "getUserMedia_unavailable"; pushRecord({kind: "webClientMediaStatus", status: state.localMedia.status, error: state.localMedia.error}); return; } if (!shouldAutoStartLocalMedia()) { state.localMedia.autoStartEnabled = false; return; } if (state.localMedia.status === "requesting" || state.localMedia.status === "active") { return; } state.localMedia.autoStartEnabled = true; state.localMedia.status = "requesting"; state.localMedia.error = null; pushRecord({kind: "webClientMediaStatus", status: state.localMedia.status}); context.navigator.mediaDevices.getUserMedia({audio: true, video: false}) .then(function(stream) { state.localMedia.stream = stream; state.localMedia.status = "active"; state.localMedia.error = null; pushRecord({ kind: "webClientMediaStatus", status: state.localMedia.status, tracks: stream && stream.getAudioTracks ? stream.getAudioTracks().length : 0 }); }) .catch(function(err) { state.localMedia.stream = null; state.localMedia.status = "failed"; state.localMedia.error = (err && err.name) ? err.name : String(err); pushRecord({ kind: "webClientMediaStatus", status: state.localMedia.status, error: state.localMedia.error }); }); } function stopLocalMedia() { var stream = state.localMedia.stream; if (stream && stream.getTracks) { stream.getTracks().forEach(function(track) { try { track.stop(); } catch (e) {} }); } state.localMedia.stream = null; if (state.localMedia.status === "active" || state.localMedia.status === "requesting") { state.localMedia.status = "idle"; } pushRecord({kind: "webClientMediaStatus", status: state.localMedia.status}); } function installInstrumentationWrappers(target) { Object.keys(target).forEach(function(key) { var original = target[key]; if (typeof original !== "function") { return; } if (key.indexOf("__") === 0) { return; } if (key === "WebClientRecorderEnable" || key === "WebClientRecorderDisable" || key === "WebClientRecorderClear" || key === "WebClientRecorderGetLog" || key === "GetWebClientDebugState" || key === "IsWebClient") { return; } if (key === "RegisterSessionJoinLeaveRequestCallBack" || key === "RegisterRecordingCallbacks" || key === "SessionRegisterCallback" || key === "SessionSetConnectionStatusRefreshRate" || key === "SessionPageEnter" || key === "SessionPageLeave" || key === "JoinSession" || key === "LeaveSession") { return; } if (original.__jkWebClientInstrumented) { return; } target[key] = function() { var args = Array.prototype.slice.call(arguments); pushRecord({ kind: "jamClientCall", method: key, args: safeSerialize(args) }); try { var result = original.apply(target, args); if (!result || typeof result.then !== "function") { pushRecord({ kind: "jamClientCallReturn", method: key, result: safeSerialize(result) }); } return instrumentThenable(key, result); } catch (e) { pushRecord({ kind: "jamClientCallThrow", method: key, error: (e && e.message) ? e.message : String(e) }); throw e; } }; target[key].__jkWebClientInstrumented = true; }); } // Explicit markers for new mode detection/instrumentation. this.IsWebClient = function() { return true; }; // Conservative behavior for now: // keep reporting "not native" until the web shim is ready to satisfy more // native-only expectations. `gon.isNativeClient` remains the feature-mode flag. this.IsNativeClient = function() { return false; }; this.GetWebClientDebugState = function() { return { mode: "delegated-fake", state: { inSessionPage: state.inSessionPage, inSession: state.inSession, currentSessionId: state.currentSessionId, sessionEventCallbackName: state.sessionEventCallbackName, sessionJoinLeaveCallbackName: state.sessionJoinLeaveCallbackName, connectionStatusRefreshRateMs: state.connectionStatusRefreshRateMs, localMedia: { status: state.localMedia.status, error: state.localMedia.error, hasStream: !!state.localMedia.stream, autoStartEnabled: !!state.localMedia.autoStartEnabled } }, recorder: { enabled: recorder.enabled, eventCount: recorder.events.length } }; }; // ---- Recorder controls (TEMP / manual validation support) ---- this.WebClientRecorderEnable = function() { recorder.enabled = true; pushRecord({kind: "webClientRecorder", action: "enable"}); return true; }; this.WebClientRecorderDisable = function() { pushRecord({kind: "webClientRecorder", action: "disable"}); recorder.enabled = false; return true; }; this.WebClientRecorderClear = function() { recorder.events = []; return true; }; this.WebClientRecorderGetLog = function() { return recorder.events.slice(); }; // ---- Incremental method overrides (session lifecycle / callback bookkeeping) ---- this.RegisterSessionJoinLeaveRequestCallBack = function(callbackName) { state.sessionJoinLeaveCallbackName = callbackName || ""; return invokeDelegate("RegisterSessionJoinLeaveRequestCallBack", arguments); }; this.RegisterRecordingCallbacks = function() { state.recordingCallbacks = Array.prototype.slice.call(arguments); return invokeDelegate("RegisterRecordingCallbacks", arguments); }; this.SessionRegisterCallback = function(callbackName) { state.sessionEventCallbackName = callbackName || ""; return invokeDelegate("SessionRegisterCallback", arguments); }; this.SessionSetConnectionStatusRefreshRate = function(milliseconds) { state.connectionStatusRefreshRateMs = milliseconds || 0; return invokeDelegate("SessionSetConnectionStatusRefreshRate", arguments); }; this.SessionPageEnter = function() { state.inSessionPage = true; pushRecord({kind: "webClientSessionState", event: "SessionPageEnter", state: safeSerialize(this.GetWebClientDebugState().state)}); startLocalMediaIfEnabled(); return invokeDelegate("SessionPageEnter", arguments); }; this.SessionPageLeave = function() { state.inSessionPage = false; pushRecord({kind: "webClientSessionState", event: "SessionPageLeave", state: safeSerialize(this.GetWebClientDebugState().state)}); if (!state.inSession) { stopLocalMedia(); } return invokeDelegate("SessionPageLeave", arguments); }; this.JoinSession = function(sessionDescriptor) { state.inSession = true; if (sessionDescriptor && typeof sessionDescriptor === "object") { state.currentSessionId = sessionDescriptor.sessionID || state.currentSessionId; } pushRecord({kind: "webClientSessionState", event: "JoinSession", session: safeSerialize(sessionDescriptor)}); startLocalMediaIfEnabled(); return invokeDelegate("JoinSession", arguments); }; this.LeaveSession = function(sessionDescriptor) { state.inSession = false; state.currentSessionId = null; pushRecord({kind: "webClientSessionState", event: "LeaveSession", session: safeSerialize(sessionDescriptor)}); if (!state.inSessionPage) { stopLocalMedia(); } return invokeDelegate("LeaveSession", arguments); }; // Keep a handle to the delegate for incremental replacement/testing. this.__delegate = inner; this.__state = state; this.__recorder = recorder; recorder.enabled = recorderEnabledByDefault(); if (recorder.enabled) { pushRecord({kind: "webClientRecorder", action: "auto-enable"}); } installInstrumentationWrappers(this); }; })(window);