UNPKG

@nuralogix.ai/anura-web-core-sdk

Version:

Anura Web Core SDK

1,223 lines (1,153 loc) 284 kB
var version = "0.1.0-beta.19"; var HTTPMethod = /* @__PURE__ */ ((HTTPMethod2) => { HTTPMethod2["DELETE"] = "DELETE"; HTTPMethod2["GET"] = "GET"; HTTPMethod2["PATCH"] = "PATCH"; HTTPMethod2["POST"] = "POST"; HTTPMethod2["PUT"] = "PUT"; HTTPMethod2["CONNECT"] = "CONNECT"; return HTTPMethod2; })(HTTPMethod || {}); var OnBeforeRESTCallErrors = /* @__PURE__ */ ((OnBeforeRESTCallErrors2) => { OnBeforeRESTCallErrors2["ON_BEFORE_REST_CALL_ERROR"] = "ON_BEFORE_REST_CALL_ERROR"; return OnBeforeRESTCallErrors2; })(OnBeforeRESTCallErrors || {}); const isModuleWorker = typeof DedicatedWorkerGlobalScope !== "undefined"; const _fetch = async ({ baseUrl, urlFragment, method, headers, data }, onAfterRESTCall) => { const url = `${baseUrl}/${urlFragment}`; let responseBody = { Code: "", Message: "" }; try { const response = await fetch(url, { method, cache: "no-store", credentials: "include", headers, ...data != null && { body: JSON.stringify(data) } }); const contentType = response.headers.get("content-type"); const status = response.status.toString(); responseBody = { Code: "UNSUPPORTED_RESPONSE_CONTENT_TYPE" /* UNSUPPORTED_RESPONSE_CONTENT_TYPE */, Message: "UNSUPPORTED_RESPONSE_CONTENT_TYPE" /* UNSUPPORTED_RESPONSE_CONTENT_TYPE */ }; if (status === "503") responseBody = { Code: "SERVICE_UNAVAILABLE", Message: "Service unavailable" }; if (status === "429") responseBody = { Code: "TOO_MANY_REQUESTS", Message: "Too many requests" }; if (contentType != null) { if (contentType.startsWith("application/json;")) responseBody = await response.json(); if (contentType.startsWith("text/plain;")) responseBody = { data: await response.text() }; } let responseHeaders = response.headers; if (isModuleWorker) { const headersObj = {}; response.headers.forEach((value, key) => { headersObj[key] = value; }); responseHeaders = headersObj; } try { onAfterRESTCall( status, status === "200" ? void 0 : responseBody ); } catch (e) { console.error("Error running onAfterRESTCall callback", e); } ; return { status, headers: responseHeaders, body: responseBody }; } catch (e) { if (e instanceof SyntaxError) { responseBody = { Code: "SYNTAX_ERROR" /* SYNTAX_ERROR */, Message: "SYNTAX_ERROR" /* SYNTAX_ERROR */ }; } else { responseBody = { Code: "UNEXPECTED_ERROR" /* UNEXPECTED_ERROR */, Message: "UNEXPECTED_ERROR" /* UNEXPECTED_ERROR */ }; } return { status: "UNKNOWN" /* UNKNOWN */, headers: isModuleWorker ? {} : new Headers(), body: responseBody }; } }; const getHeader = () => new Headers({ "Content-Type": "application/json" }); var __defProp$a = Object.defineProperty; var __defNormalProp$a = (obj, key, value) => key in obj ? __defProp$a(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$a = (obj, key, value) => __defNormalProp$a(obj, key + "" , value); class Main { /** @internal */ constructor(parent) { __publicField$a(this, "parent"); this.parent = parent; } async onBeforeRESTCall() { try { this.parent.onBeforeRESTCall(); return { Code: "SUCCESS", Message: "SUCCESS" }; } catch (e) { return { Code: OnBeforeRESTCallErrors.ON_BEFORE_REST_CALL_ERROR, Message: OnBeforeRESTCallErrors.ON_BEFORE_REST_CALL_ERROR }; } } getDefaults(method) { return { baseUrl: JSON.parse(this.parent.getUrl())["http"].slice(0, -1), method: HTTPMethod[method], headers: getHeader() }; } getHeaderWithDeviceToken() { const headers = getHeader(); headers.set( "Authorization", `Bearer ${this.parent.getSession().deviceToken}` ); return headers; } getHeaderWithUserToken() { const headers = getHeader(); headers.set( "Authorization", `Bearer ${this.parent.getSession().userToken}` ); return headers; } getHeaderWithSessionEnabled() { const headers = getHeader(); headers.set("x-nura-session", "true"); return headers; } } class Measurements extends Main { /** * Begins a new data capture session and returns a measurement ID * property, which should be referenced for adding data chunks and * retreiving results. * * Resolution: currently can be either 0 or 100. (default is 100) * * * 100 means the result set will have 100% of the original size * * * 0 returns 1 value per signal * * PartnerID is mandatory or optional (based on License policy). * * Endpoint Action ID = 504 */ async create(data, tokenType) { const { Code, Message } = await this.onBeforeRESTCall(); if (Code === OnBeforeRESTCallErrors.ON_BEFORE_REST_CALL_ERROR) { return { status: Code, body: { Code, Message }, headers: new Headers() }; } const { status, body, headers } = await _fetch( { ...this.getDefaults(HTTPMethod.POST), headers: tokenType === "device" ? this.getHeaderWithDeviceToken() : this.getHeaderWithUserToken(), urlFragment: "measurements", data }, this.parent.onAfterRESTCall ); if (status === "200") { this.parent.setSession({ lastMeasurementId: body.ID }); } return { status, body, headers }; } } class Studies extends Main { /** * Retrieves a study's binary config data that has to be used to initialize the DFX SDK Factory object. * Get the SDKID parameter by calling GetSDKId on the DFX SDK Factory object. A response of 304 means * that existing file in hand is up to date. * * Endpoint Action ID = 806 */ async retrieveSdkConfigData(data, tokenType) { const { Code, Message } = await this.onBeforeRESTCall(); if (Code === OnBeforeRESTCallErrors.ON_BEFORE_REST_CALL_ERROR) { return { status: Code, body: { Code, Message }, headers: new Headers() }; } const { status, body, headers } = await _fetch( { ...this.getDefaults(HTTPMethod.POST), headers: tokenType === "device" ? this.getHeaderWithDeviceToken() : this.getHeaderWithUserToken(), urlFragment: "studies/sdkconfig", data }, this.parent.onAfterRESTCall ); if (status === "200") { this.parent.setSession({ studyCfgData: body.ConfigFile, studyCfgHash: body.MD5Hash }); } return { status, body, headers }; } } class Auths extends Main { /** * Renew user/device access and refresh token. When you register a license or login with * user's credentials, a pair of Token and RefreshToken is sent to a client. The client * needs to send the matching pair to exchange it with a new pair. The old pair will not * be valid after calling this endpoint. RefreshToken is one-time use. * * Endpoint Action ID = 2304 */ async renew(data, tokenType) { const { Code, Message } = await this.onBeforeRESTCall(); if (Code === OnBeforeRESTCallErrors.ON_BEFORE_REST_CALL_ERROR) { return { status: Code, body: { Code, Message }, headers: new Headers() }; } const { status, body, headers } = await _fetch( { ...this.getDefaults(HTTPMethod.POST), headers: tokenType === "device" ? this.getHeaderWithDeviceToken() : this.getHeaderWithUserToken(), urlFragment: "auths/renew", data }, this.parent.onAfterRESTCall ); if (status === "200") { const newToken = body.Token; const newRefreshToken = body.RefreshToken; const session = { ...tokenType === "user" && { userToken: newToken, userRefreshToken: newRefreshToken }, ...tokenType === "device" && { deviceToken: newToken, deviceRefreshToken: newRefreshToken } }; this.parent.setSession(session); } return { status, body, headers }; } /** * Validates a token passed as a authentication header and returns the decoded token with permissions. * * Endpoint Action ID = unknown at this time */ async validate(tokenType) { const { Code, Message } = await this.onBeforeRESTCall(); if (Code === OnBeforeRESTCallErrors.ON_BEFORE_REST_CALL_ERROR) { return { status: Code, body: { Code, Message }, headers: new Headers() }; } return await _fetch( { ...this.getDefaults(HTTPMethod.GET), headers: tokenType === "device" ? this.getHeaderWithDeviceToken() : this.getHeaderWithUserToken(), urlFragment: "auth/v1/validate" }, this.parent.onAfterRESTCall ); } } var __defProp$9 = Object.defineProperty; var __typeError$4 = (msg) => { throw TypeError(msg); }; var __defNormalProp$9 = (obj, key, value) => key in obj ? __defProp$9(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$9 = (obj, key, value) => __defNormalProp$9(obj, typeof key !== "symbol" ? key + "" : key, value); var __accessCheck$4 = (obj, member, msg) => member.has(obj) || __typeError$4("Cannot " + msg); var __privateGet$3 = (obj, member, getter) => (__accessCheck$4(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj)); var __privateAdd$4 = (obj, member, value) => member.has(obj) ? __typeError$4("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value); var __privateSet$3 = (obj, member, value, setter) => (__accessCheck$4(obj, member, "write to private field"), member.set(obj, value), value); var __url, __session; class Client { constructor(config) { __privateAdd$4(this, __url, { http: new URL("https://api.deepaffex.ai") }); /** * Session info */ __privateAdd$4(this, __session); __publicField$9(this, "onBeforeRESTCall"); __publicField$9(this, "onAfterRESTCall"); __publicField$9(this, "http"); __privateSet$3(this, __session, { deviceToken: "", deviceRefreshToken: "", userToken: "", userRefreshToken: "", deviceId: "", roleId: "", userId: "", selectedStudy: "", lastMeasurementId: "", studyCfgHash: "", studyCfgData: "" }); this.http = { measurements: new Measurements(this), studies: new Studies(this), auths: new Auths(this) }; this.onBeforeRESTCall = config?.onBeforeRESTCall ? config.onBeforeRESTCall : () => { }; this.onAfterRESTCall = config?.onAfterRESTCall ? config.onAfterRESTCall : () => { }; if (config?.url != null) __privateSet$3(this, __url, config.url); } static new(config) { return new this(config ?? void 0); } /** * Gets url */ getUrl() { return JSON.stringify(__privateGet$3(this, __url)); } /** * Sets url */ setUrl(http) { __privateSet$3(this, __url, { http: new URL(http) }); } /** * Gets session info * @returns {ISession} Returns session info */ getSession() { return __privateGet$3(this, __session); } /** * Sets session info * @param {ISession} sessionInfo Sets session info */ setSession(sessionInfo) { const validKeys = [ "deviceToken", "deviceRefreshToken", "userToken", "userRefreshToken", "deviceId", "userId", "roleId", "selectedStudy", "lastMeasurementId", "studyCfgHash", "studyCfgData" ]; const sessionInfoKeys = Object.keys(sessionInfo); const isAllKeysValid = sessionInfoKeys.every( (e) => validKeys.includes(e) && typeof sessionInfo[e] === "string" ); if (isAllKeysValid) { __privateSet$3(this, __session, { ...__privateGet$3(this, __session), ...sessionInfo }); } } } __url = new WeakMap(); __session = new WeakMap(); const client = (config) => Client.new(config); var __defProp$8 = Object.defineProperty; var __typeError$3 = (msg) => { throw TypeError(msg); }; var __defNormalProp$8 = (obj, key, value) => key in obj ? __defProp$8(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$8 = (obj, key, value) => __defNormalProp$8(obj, key + "" , value); var __accessCheck$3 = (obj, member, msg) => member.has(obj) || __typeError$3("Cannot " + msg); var __privateAdd$3 = (obj, member, value) => member.has(obj) ? __typeError$3("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value); var __privateMethod$3 = (obj, member, method) => (__accessCheck$3(obj, member, "access private method"), method); var _AssetDownloader_instances, dispatch_fn, setBrotliDecode_fn; let brotliDecode = (bytes) => { return new Int8Array(bytes); }; class AssetDownloader extends EventTarget { constructor() { super(...arguments); __privateAdd$3(this, _AssetDownloader_instances); __publicField$8(this, "canDecompress", false); } static init() { return new this(); } /* Dispatches a custom event */ dispatch(eventType, payload) { this.dispatchEvent(new CustomEvent(eventType, { detail: payload })); } getBytesDownloadedEvent(bytes, uncompressedSize, url, done) { const event = new CustomEvent("bytesDownloaded" /* BYTES_DOWNLOADED */, { detail: { bytes, uncompressedSize, url, done } }); return event; } getBytesDownloadErrorEvent(url, error) { const event = new CustomEvent("downloadedError" /* DOWNLOAD_ERROR */, { detail: { url, error } }); return event; } /** Decompresses a Brotli compressed/based 64 encoded string and returns an ArrayBuffer */ decompressBrotli(compressedBuffer) { const binaryString = atob(compressedBuffer); const byteArray = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { byteArray[i] = binaryString.charCodeAt(i); } const int8Array = new Int8Array(byteArray.length); for (let i = 0; i < byteArray.length; i++) { int8Array[i] = byteArray[i] >= 128 ? byteArray[i] - 256 : byteArray[i]; } const decompressed = brotliDecode(int8Array); return decompressed.buffer; } /** Returns either an ArrayBuffer or undefined */ async fetchAsset(assetSize, path, file, decompress, compressionType) { const url = path + file; const uncompressedSize = assetSize.find((asset) => asset.file === file).uncompressedSize; const dispatch = (bytes, uncompressedSize2, url2, done) => __privateMethod$3(this, _AssetDownloader_instances, dispatch_fn).call(this, this.getBytesDownloadedEvent(bytes, uncompressedSize2, url2, done)); try { const response = await fetch(url); const reader = response.body?.getReader(); if (!reader) { throw new Error("Failed to get reader from response body."); } let bytes = 0; const stream = new ReadableStream({ async start(controller) { await pump(); async function pump() { const { done, value } = await reader.read(); if (done) { controller.close(); dispatch(bytes, uncompressedSize, url, true); return; } if (value) { controller.enqueue(value); bytes += value.length; dispatch(bytes, uncompressedSize, url, false); } await pump(); } } }); const newStream = new Response(stream); if (decompress) { const json = await newStream.json(); const { base64EncodedValue } = json; if (compressionType === "gzip") { await __privateMethod$3(this, _AssetDownloader_instances, setBrotliDecode_fn).call(this, base64EncodedValue); return void 0; } const arrayBuffer = this.decompressBrotli(base64EncodedValue); return arrayBuffer; } else { const arrayBuffer = await newStream.arrayBuffer(); return arrayBuffer; } } catch (e) { __privateMethod$3(this, _AssetDownloader_instances, dispatch_fn).call(this, this.getBytesDownloadErrorEvent(url, e)); } } } _AssetDownloader_instances = new WeakSet(); // BytesDownloaded dispatch_fn = function(event) { this.dispatchEvent(event); }; setBrotliDecode_fn = async function(base64EncodedValue) { const compressedData = Uint8Array.from(atob(base64EncodedValue), (c) => c.charCodeAt(0)); const compressedStream = new Blob([compressedData]).stream(); const decompressedStream = compressedStream.pipeThrough(new DecompressionStream("gzip")); const decompressedArrayBuffer = await new Response(decompressedStream).arrayBuffer(); const text = new TextDecoder().decode(new Uint8Array(decompressedArrayBuffer)); const blob = new Blob([text], { type: "application/javascript" }); const blobUrl = URL.createObjectURL(blob); const module = await import(blobUrl); brotliDecode = module.BrotliDecode; this.canDecompress = true; URL.revokeObjectURL(blobUrl); }; var __defProp$7 = Object.defineProperty; var __defNormalProp$7 = (obj, key, value) => key in obj ? __defProp$7(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$7 = (obj, key, value) => __defNormalProp$7(obj, typeof key !== "symbol" ? key + "" : key, value); class Metrics { constructor() { __publicField$7(this, "version", { webSDK: "", extractionLib: { version: "", sdkId: "" }, faceTracker: { blazeFace: { version: "", backend: "" }, faceMesh: { version: "", backend: "" } } }); __publicField$7(this, "debugLogs", []); } appendLog(logEntry) { this.debugLogs.push(logEntry); } generateHTMLTable() { const uniqueThreads = [...new Set(this.debugLogs.map((item) => item.thread))]; let htmlContent = ` <html lang="en"> <head> <meta charset="UTF-8"> <title>Anura Web Core SDK - Logs</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> body { font-family: Arial, sans-serif; margin: 20px; display: flex; flex-direction: column; } .header { display: flex; justify-content: space-between;} .box { padding: 20px; margin-bottom: 20px; border-radius: 5px; border: 1px solid #ccc; } svg { display: block; width: 100%; height: auto; margin: 20px 0; } .tooltip { position: absolute; background-color: rgba(0, 0, 0, 0.7); color: white; padding: 5px; border-radius: 5px; display: none; } .axis-label { font-size: 14px; text-anchor: middle; } label { font-size: 16px; margin-bottom: 10px; } select { font-size: 16px; padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; background-color: #f9f9f9; outline: none; transition: all 0.3s ease; width: 320px; } select:hover { border-color: #888; } select:focus { border-color: #555; box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); } #download {width: 200px;} table {width: 100%; max-width: 100%; table-layout: fixed; border-collapse: collapse;} th, td {border: 1px solid #ccc; padding: 8px; text-align: left; word-wrap: break-word;} td pre { margin: 0; white-space: pre-wrap; /* Preserve whitespace and line breaks */ } .json-key { color: blue; } th {background-color: #f2f2f2;} .odd-row { background-color: #f9f9f9; } /* Light grey for odd rows */ .even-row { background-color: #ffffff; } /* White for even rows */ .col-60 { width: 60px } .col-200 { width: 200px } .col-230 { width: 230px } .col-350 { width: 350px } .chart-container { width: 100%; margin-top: 40px;} </style> </head> <body> <div class="header"> <div> <h1>Anura Web Core SDK</h1> </div> <div> <button type="button" id="download">Download</button> </div> </div> <br><br> <div id="info-table"></div> <br><br> <h4>Network Delays</h4> <div id="network-table"></div> <br><br> <div class="chart-container"> <div class="box"> <h2>Instantaneous video playback FPS between consecutive frames</h2> FPS = 1 / time difference between consecutive frames. <br><br> The time difference is the gap between the <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/requestVideoFrameCallback#capturetime" target="_blank" rel="noopener noreferrer">capturetime</a> values of two consecutive frames. This method will provide a better representation of the FPS at any given point in time, as it accounts for variations in frame rate over time (e.g., if the video slows down or experiences drops). It is generally more useful for tracking real-time performance. <svg id="fps-chart" viewBox="0 0 800 400"></svg> </div> <div class="box"> <h2>Tracking Time per Frame by Face Tracker Worker</h2> To track every frame in a video with a playback speed of 30 frames per second (FPS), the face tracking time per frame should be less than or equal to the time allocated for each frame, which is: <br><br> Frame time= 1 / FPS = 1 / 30 \u2248 0.0333 seconds or 33.3 milliseconds. <br><br> To ensure smooth and consistent face tracking, the face tracking process should ideally complete in less than 33.3 milliseconds per frame. <svg id="ft-tracking-time-chart" viewBox="0 0 800 400"></svg> </div> <div class="box"> <h2>Face tracking Throughput Over Time</h2> This chart will help you monitor the system's face tracking capacity over time. It displays the number of frames tracked per unit time (1 second). <svg id="ft-throughput-chart" viewBox="0 0 800 400"></svg> </div> <div class="box"> <h2>Frame Latency Per Frame</h2> This chart shows total processing delay per frame, including face tracking time and DFX processing time. <br> <br> <div id="total-latency"></div> <svg id="frame-latency-chart" viewBox="0 0 800 400"></svg> </div> <div class="tooltip" id="tooltip"></div> </div> <label for="thread-filter">Filter by Thread:</label> <select id="thread-filter"> <option value="none">None</option> ${uniqueThreads.map((thread) => `<option value="${thread}">${thread}</option>`).join("")} </select> <br> <br> <div id="logs-table"></div> <script> const logs = ${JSON.stringify(this.debugLogs, null, 2)}; const logsTable = document.getElementById('logs-table'); const infoTable = document.getElementById('info-table'); const networkTable = document.getElementById('network-table'); const filter = document.getElementById('thread-filter'); const fpsChart = document.getElementById('fps-chart'); const ftTrackingTimeChart = document.getElementById('ft-tracking-time-chart'); const ftThroughputChart = document.getElementById('ft-throughput-chart'); const frameLatencyChart = document.getElementById('frame-latency-chart'); const totalLatency = document.getElementById('total-latency'); const tooltip = document.getElementById('tooltip'); const frameNumCaptureTimeArr = logs.filter( item => item.category === 'SDK' && "captureTime" in item.meta && "presentedFrames" in item.meta ).map((item) => item.meta); const ftResolution = logs.find( item => item.category === 'SDK' && "ftWidth" in item.meta && "ftHeight" in item.meta ).meta; const frameMetrics = logs.find( item => item.category === 'SDK' && "numOfFramesPresented" in item.meta && "warmupFrameNumber" in item.meta && "numOftrackedVideoFrames" in item.meta ).meta; const { numOfFramesPresented, warmupFrameNumber, numOftrackedVideoFrames } = frameMetrics; const numOfFramesPresentedSinceWarmUp = numOfFramesPresented - warmupFrameNumber; const numOfDroppedFrames = Math.max( numOfFramesPresented - numOftrackedVideoFrames - warmupFrameNumber, 0 ); const percentageOfDroppedFrames = numOfDroppedFrames === 0 || numOfFramesPresented === warmupFrameNumber ? 0 : (numOfDroppedFrames * 100) / (numOfFramesPresented - warmupFrameNumber); const wsCallDelayArr = logs.filter( item => item.category === 'WebSocket' && "delay" in item.meta && "actionId" in item.meta ).map(item => ({ protocol: "WebSocket", timestamp: item.timestamp.slice(1, -1), ...item.meta })); const restCallDelayArr = logs.filter( item => item.category === 'DFX API client' && "delay" in item.meta && "actionId" in item.meta ).map(item => ({ protocol: "HTTP", timestamp: item.timestamp.slice(1, -1), ...item.meta })); const measurement = restCallDelayArr.find(item => "measurementId" in item); const measurementId = measurement ? measurement.measurementId : 'N/A'; const ftTrackingTimeArr = logs.filter( item => item.category === 'MediaPipe' && item.meta && "frameNumber" in item.meta && "frameTimestamp" in item.meta && "frameTrackingTime" in item.meta ).map(item => ({thread: item.thread, ...item.meta}) ).sort((a, b) => a.frameNumber - b.frameNumber); const ftChartData = ftTrackingTimeArr.slice(4); // remove the first 4 elements (warm-up frames) from the array const frameThroughput = calculateThroughput(ftChartData, 1000); const frameLatencyArr = logs.filter( item => item.category === 'DFX Worker' && item.meta && "frameNumber" in item.meta && "frameTrackingTime" in item.meta && "dfxProcessingTime" in item.meta) .map(item => ({overall: item.meta.frameTrackingTime + item.meta.dfxProcessingTime, ...item.meta})) .sort((a, b) => a.frameNumber - b.frameNumber); const latency = frameLatencyArr.reduce((sum, frame) => sum + frame.overall, 0); totalLatency.innerHTML = \`Total Frames Processed: <strong>\${frameLatencyArr.length}</strong><br><br>Total Latency: <strong>\${(latency / 1000).toFixed(2)} seconds</strong>\`; const getCategoryColor = (category) => { switch (category) { case 'Before REST call event': return 'red'; case 'After REST call event': return 'orange'; case 'MediaPipe': return 'green'; case 'DFX Extraction lib WASM': return 'violet'; case 'DFX API client': return '#d34110'; case 'WebSocket': return '#d34110'; case 'SDK': return '#d34110'; case 'DFX Worker': return '#ad4393'; default: return 'black'; } }; // Function to escape HTML special characters function escapeHTML(html) { const div = document.createElement('div'); div.textContent = html; return div.innerHTML; } function formatJsonWithKeyStyling(jsonString) { return jsonString.replace(/"(.*?)":/g, (match, p1) => \`<span class="json-key">"\${p1}"</span>:\`); } // Function to find and extract the JSON string function extractJson(str) { let jsonStart = str.indexOf('{'); if (jsonStart === -1) return null; // No JSON found let stack = []; let jsonEnd = jsonStart; for (let i = jsonStart; i < str.length; i++) { if (str[i] === '{') stack.push('{'); if (str[i] === '}') stack.pop(); if (stack.length === 0) { jsonEnd = i + 1; break; } } return str.slice(jsonStart, jsonEnd); } // Function to format and display the string function formatString(str) { const jsonPart = extractJson(str); let formattedString = escapeHTML(str); if (jsonPart) { try { const jsonObject = JSON.parse(jsonPart); const formattedJson = JSON.stringify(jsonObject, null, 4); const styledJson = formatJsonWithKeyStyling(formattedJson); // Replace the JSON part with formatted JSON formattedString = str.replace(jsonPart, \`<pre>\${styledJson}</pre>\`); } catch (e) { console.error('Invalid JSON:', e); } } return formattedString; } function renderInfoTable(data) { const tableHtml = \` <table> <thead> <tr> <th class="col-350">Title</th> <th>Description</th> </tr> </thead> <tbody> \${data.map((item, index) => { const rowClass = index % 2 === 0 ? 'even-row' : 'odd-row'; const [title, description] = item; return \` <tr class="\${rowClass}"> <td class="col-350">\${title}</td> <td>\${description}</td> </tr> \`; }).join("")} </tbody> </table> \`; infoTable.innerHTML = tableHtml; } function renderNetworkTable(data) { const tableHtml = \` <table> <thead> <tr> <th class="col-230">Timestamp</th> <th class="col-200">Protocol</th> <th class="col-200">Delay (ms)</th> <th>Info</th> </tr> </thead> <tbody> \${data.map((item, index) => { const rowClass = index % 2 === 0 ? 'even-row' : 'odd-row'; const { timestamp, protocol, delay, actionId, description } = item; return \` <tr class="\${rowClass}"> <td class="col-230">\${timestamp}</td> <td class="col-200">\${protocol}</td> <td class="col-200">\${delay.toFixed(2)}</td> <td>\${description} - Action ID: \${actionId}</td> </tr> \`; }).join("")} </tbody> </table> \`; networkTable.innerHTML = tableHtml; } function renderLogsTable(data) { const tableHtml = \` <table> <thead> <tr> <th class="col-60">Index</th> <th class="col-230">Timestamp</th> <th class="col-230">Thread</th> <th class="col-200">Category</th> <th>Message</th> </tr> </thead> <tbody> \${data.map((item, index) => { const rowClass = index % 2 === 0 ? 'even-row' : 'odd-row'; const categoryColor = getCategoryColor(item.category); return \` <tr class="\${rowClass}"> <td class="col-60">\${index + 1}</td> <td class="col-230">\${item.timestamp.slice(1, -1)}</td> <td class="col-230">\${item.thread}</td> <td class="col-200" style="color: \${categoryColor};">\${item.category}</td> <td>\${formatString(item.message)}</td> </tr> \`; }).join("")} </tbody> </table> \`; logsTable.innerHTML = tableHtml; } // Instantaneous FPS (between consecutive frames) const instantaneousFrameRates = frameNumCaptureTimeArr.map((point, index) => { if (index === 0) { return { captureTime: point.captureTime, frameRate: 0 }; // No FPS for the first frame } else { const prevPoint = frameNumCaptureTimeArr[index - 1]; const timeDifference = point.captureTime - prevPoint.captureTime; // Time difference in seconds const fps = timeDifference > 0 ? 1000 / timeDifference : 0; // Convert to FPS by multiplying by 1000 return { captureTime: point.captureTime, frameRate: fps }; } }); function calculateThroughput(frameData, intervalMs = 1000) { // Sort frame data by frameTimestamp frameData.sort((a, b) => a.frameTimestamp - b.frameTimestamp); // Find the time range const startTime = frameData[0].frameTimestamp; const endTime = frameData[frameData.length - 1].frameTimestamp; // Group frames into time intervals const throughputData = []; for (let t = startTime; t <= endTime; t += intervalMs) { const nextInterval = t + intervalMs; const framesInInterval = frameData.filter( frame => frame.frameTimestamp >= t && frame.frameTimestamp < nextInterval ).length; throughputData.push({ time: t - startTime, // Time offset in milliseconds throughput: framesInInterval, }); } return throughputData; }; // Function to render the SVG chart function renderChart(data, svg, xKey, yKey, xAxisLabel, yAxisLabel, lineColor, getCircleColor, xValuesCallback, tooltipCallback) { const width = svg.getAttribute("viewBox").split(" ")[2]; const height = svg.getAttribute("viewBox").split(" ")[3]; const padding = 70; // Increase padding to provide more space for y-axis title const titlePadding = 30; // Adjust this for more space between title and axis const maxTime = Math.max(...data.map(d => d[xKey])); const minTime = Math.min(...data.map(d => d[xKey])); const maxFrameRate = Math.max(...data.map(d => d[yKey])); const minFrameRate = Math.min(...data.map(d => d[yKey])); const xScale = (value) => padding + (value / maxTime) * (width - 2 * padding); const yScale = (value) => height - padding - (value / maxFrameRate) * (height - 2 * padding); // Clear existing SVG content svg.innerHTML = ""; // Draw grid lines function drawGridLines() { const gridLines = document.createElementNS("http://www.w3.org/2000/svg", "g"); for (let i = 1; i <= 5; i++) { const x = xScale(i * (maxTime / 5)); const y1 = padding; const y2 = height - padding; const gridLine = document.createElementNS("http://www.w3.org/2000/svg", "line"); gridLine.setAttribute("x1", x); gridLine.setAttribute("y1", y1); gridLine.setAttribute("x2", x); gridLine.setAttribute("y2", y2); gridLine.setAttribute("stroke", "#ccc"); gridLine.setAttribute("stroke-width", 1); gridLine.setAttribute("stroke-dasharray", "5,5"); gridLines.appendChild(gridLine); } for (let i = 1; i <= 5; i++) { const y = yScale(i * (maxFrameRate / 5)); const x1 = padding; const x2 = width - padding; const gridLine = document.createElementNS("http://www.w3.org/2000/svg", "line"); gridLine.setAttribute("x1", x1); gridLine.setAttribute("y1", y); gridLine.setAttribute("x2", x2); gridLine.setAttribute("y2", y); gridLine.setAttribute("stroke", "#ccc"); gridLine.setAttribute("stroke-width", 1); gridLine.setAttribute("stroke-dasharray", "5,5"); gridLines.appendChild(gridLine); } svg.appendChild(gridLines); } drawGridLines(); // Draw axes const xAxis = document.createElementNS("http://www.w3.org/2000/svg", "line"); xAxis.setAttribute("x1", padding); xAxis.setAttribute("y1", height - padding); xAxis.setAttribute("x2", width - padding); xAxis.setAttribute("y2", height - padding); xAxis.setAttribute("stroke", "black"); svg.appendChild(xAxis); const yAxis = document.createElementNS("http://www.w3.org/2000/svg", "line"); yAxis.setAttribute("x1", padding); yAxis.setAttribute("y1", padding); yAxis.setAttribute("x2", padding); yAxis.setAttribute("y2", height - padding); yAxis.setAttribute("stroke", "black"); svg.appendChild(yAxis); // Add axis labels const xLabel = document.createElementNS("http://www.w3.org/2000/svg", "text"); xLabel.setAttribute("x", width / 2); xLabel.setAttribute("y", height - padding / 3); xLabel.setAttribute("class", "axis-label"); xLabel.textContent = xAxisLabel; svg.appendChild(xLabel); // Add axis label for Y (Frame Rate) const yLabel = document.createElementNS("http://www.w3.org/2000/svg", "text"); yLabel.setAttribute("x", -height / 2); // Rotate around the center of the y-axis yLabel.setAttribute("y", padding / 2 - 24); // Adjust vertical position to add space yLabel.setAttribute("class", "axis-label"); yLabel.setAttribute("transform", "rotate(-90)"); yLabel.setAttribute("text-anchor", "middle"); yLabel.textContent = yAxisLabel; svg.appendChild(yLabel); // Add axis ticks and labels for X (time) const numXTicks = 5; for (let i = 0; i <= numXTicks; i++) { const xValue = minTime + (i * (maxTime - minTime) / numXTicks); const x = xScale(xValue); const tick = document.createElementNS("http://www.w3.org/2000/svg", "line"); tick.setAttribute("x1", x); tick.setAttribute("y1", height - padding); tick.setAttribute("x2", x); tick.setAttribute("y2", height - padding + 6); tick.setAttribute("stroke", "black"); svg.appendChild(tick); const tickLabel = document.createElementNS("http://www.w3.org/2000/svg", "text"); tickLabel.setAttribute("x", x); tickLabel.setAttribute("y", height - padding + 20); tickLabel.setAttribute("class", "axis-label"); tickLabel.textContent = xValuesCallback(xValue); svg.appendChild(tickLabel); } // Add axis ticks and labels for Y (frame rate) const numYTicks = 5; for (let i = 0; i <= numYTicks; i++) { const yValue = minFrameRate + (i * (maxFrameRate - minFrameRate) / numYTicks); const y = yScale(yValue); const tick = document.createElementNS("http://www.w3.org/2000/svg", "line"); tick.setAttribute("x1", padding - 6); // Move ticks to the left a bit tick.setAttribute("y1", y); tick.setAttribute("x2", padding); // Position ticks a bit further from the axis line tick.setAttribute("y2", y); tick.setAttribute("stroke", "black"); svg.appendChild(tick); const tickLabel = document.createElementNS("http://www.w3.org/2000/svg", "text"); tickLabel.setAttribute("x", padding - 10); // Adjust the label placement to shift it to the right tickLabel.setAttribute("y", y + 5); // Center the label vertically on the tick tickLabel.setAttribute("text-anchor", "end"); tickLabel.textContent = yValue.toFixed(1); svg.appendChild(tickLabel); } // Draw the line chart const line = document.createElementNS("http://www.w3.org/2000/svg", "path"); const lineData = data.map((point, index) => { const x = xScale(point[xKey]); const y = yScale(point[yKey]); return index === 0 ? 'M' + x + ',' + y : 'L' + x + ',' + y; }).join(" "); line.setAttribute("d", lineData); line.setAttribute("fill", "none"); line.setAttribute("stroke", lineColor); line.setAttribute("stroke-width", 2); svg.appendChild(line); // Add tooltip on hover data.forEach((point) => { const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); circle.setAttribute("cx", xScale(point[xKey])); circle.setAttribute("cy", yScale(point[yKey])); circle.setAttribute("r", 4); circle.setAttribute("fill", getCircleColor(point)); circle.addEventListener("mouseover", (event) => { tooltip.style.display = "block"; tooltip.style.left = event.pageX + 10 + 'px'; tooltip.style.top = event.pageY + 10 + 'px'; tooltip.innerHTML = tooltipCallback(point); }); circle.addEventListener("mouseout", () => { tooltip.style.display = "none"; }); svg.appendChild(circle); }); } const renderFpsTooltip = (point) => { return \`Time: \${(point.captureTime/1000).toFixed(4)}s, Frame Rate: \${point.frameRate.toFixed(2)} FPS\`; }; const renderTrackingTimeTooltip = (point) => { return \`Frame: \${point.frameNumber}, Tracking Time: \${point.frameTrackingTime.toFixed(2)} ms, Thread: \${point.thread}\`; }; const renderThroughputTooltip = (point) => { return \`Time: \${point.time / 1000}s, Throughput: \${point.throughput} FPS\`; }; const renderFrameLatencyTooltip = (point) => { return \`Frame: \${point.frameNumber}, Overall: \${point.overall.toFixed(2)} ms, Face Tracking Time: \${point.frameTrackingTime.toFixed(2)} ms, DFX processing: \${point.dfxProcessingTime.toFixed(2)} ms\`; }; const getFTCircleColor = (point) => { const colors = ['#c7e9c0' ,'#74c476', '#31a354', '#006d2c']; const index = parseInt(point.thread.slice(-1)); return colors[index]; }; const getUTCDate = () => { const now = new Date(); const year = now.getUTCFullYear(); const month = String(now.getUTCMonth() + 1).padStart(2, '0'); // Months are 0-indexed const date = String(now.getUTCDate()).padStart(2, '0'); const hours = String(now.getUTCHours()).padStart(2, '0'); const minutes = String(now.getUTCMinutes()).padStart(2, '0'); const seconds = String(now.getUTCSeconds()).padStart(2, '0'); return year + '-' + month + '-' + date + ' ' + hours + ':' + minutes + ':' + seconds + ' UTC'; }; const info = [ ['Report date', getUTCDate()], ['SDK Version', '${this.version.webSDK}'], ['Face Tracker Version', '${this.version.faceTracker}'], ['Extraction Lib Version', '${this.version.extractionLib.version}'], ['Extraction Lib SDK ID', '${this.version.extractionLib.sdkId}'], ['Video Resolution (w x h)', ftResolution.frameWidth + ' x ' + ftResolution.frameHeight + ' pixels'], ['Face Tracker Resolution (w x h)', ftResolution.ftWidth + ' x ' + ftResolution.ftHeight + ' pixels'], ['Measurement ID', measurementId], ['Number of Frames Presented', numOfFramesPresented], ['Face Trackers Warmup Frame Number', warmupFrameNumber], ['Number of Tracked Video Frames', numOftrackedVideoFrames], ['Number of Frames Presented Since Warmup', numOfFramesPresentedSinceWarmUp], ['Number of dropped Frames', numOfDroppedFrames], ['Percentage of dropped frames', percentageOfDroppedFrames.toFixed(2) + '%'], ]; info.push([ 'Touch screen', 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0 ? 'Available' : 'Not available', ]); info.push([ 'Window size (w x h)', '${window.innerWidth} x ${window.innerHeight} pixels', ]); info.push([ 'Screen orientation', window.innerHeight > window.innerWidth ? 'Portrait' : 'Landscape', ]); info.push([ 'User agent', window.navigator.userAgent, ]); info.push([ 'Time Zone', Intl.DateTimeFormat().resolvedOptions().timeZone, ]); info.push([ 'Locale', Intl.DateTimeFormat().resolvedOptions().locale, ]); renderInfoTable(info); renderNetworkTable([...restCallDelayArr, ...wsCallDelayArr]); renderLogsTable(logs); renderChart( instantaneousFrameRates, fpsChart, 'captureTime', 'frameRate', 'Time (s)', 'Frame Rate (FPS)', 'blue', (point) => 'blue', (xValue) => (xValue / 1000).toFixed(1), renderFpsTooltip ); renderChart( ftChartData, ftTrackingTimeChart, 'frameNumber', 'frameTrackingTime', 'Frame Number', 'Frame Tracking Time (ms)', 'green', getFTCircleColor, (xValue) => Math.trunc(xValue), renderTrackingTimeTooltip ); renderChart( frameThroughput,