jam-cloud/web/app/assets/javascripts/webJamClient.js

414 lines
13 KiB
JavaScript

(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);