UNPKG

@skyway-sdk/analytics-client

Version:

The official Next Generation JavaScript SDK for SkyWay

547 lines 26.7 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AnalyticsClient = void 0; const clientEvent_1 = require("./clientEvent"); const payloadTypes_1 = require("./payloadTypes"); const socket_1 = require("./socket"); const backoff_1 = require("./utils/backoff"); const event_1 = require("./utils/event"); const ANALYTICS_LOGGING_SERVER_DOMAIN = 'analytics-logging.skyway.ntt.com'; const API_VERSION = 'v2'; const TIMEOUT_SEC = 5; class AnalyticsClient { constructor({ token, sdkVersion, contextId }, options) { this.onConnectionStateChanged = new event_1.Event(); this.onConnectionFailed = new event_1.Event(); this.onAnalyticsNotEnabledError = new event_1.Event(); this._isClosed = false; this._responseCallbacks = new Map(); this._acknowledgeCallbacks = new Map(); this._mediaDeviceVersion = new Map(); this._encodingsVersion = new Map(); this._preferredEncodingVersion = new Map(); this._previousSubscriptionStats = new Map(); this._statsRequest = { // connect()時のopenServerEventPayload.statsRequest代入でそれぞれ値が入るが,一度初期値として定義しておく intervalSec: 5, types: [], }; this._pendingSdkLogs = []; this._token = token; this._newToken = undefined; this._sdkVersion = sdkVersion; this._contextId = contextId; const defaultOptions = { analyticsLoggingServerDomain: ANALYTICS_LOGGING_SERVER_DOMAIN, secure: true, logger: { debug: (message, ...optionalParams) => { console.debug(message, ...optionalParams); }, warn: (message, ...optionalParams) => { console.warn(message, ...optionalParams); }, error: (error) => { console.error(error); }, }, }; this._options = Object.assign({}, defaultOptions, options !== null && options !== void 0 ? options : {}); this._logger = this._options.logger; this._logger.debug(`Created instance with the options: ${this._options}`); this._sdkLogTimer = setInterval(() => { if (this._pendingSdkLogs.length > 0) { const logs = this._pendingSdkLogs.splice(0, this._pendingSdkLogs.length); this.sendSdkLogReport(logs).catch((err) => { this._logger.warn('sendSdkLogReport (interval) failed', err); }); } }, 5 * 1000); } get connectionState() { var _a, _b; return (_b = (_a = this._socket) === null || _a === void 0 ? void 0 : _a.connectionState) !== null && _b !== void 0 ? _b : 'closed'; } connect() { return __awaiter(this, void 0, void 0, function* () { const WSProtocol = this._options.secure ? 'wss' : 'ws'; const analyticsLoggingServerDomain = this._options.analyticsLoggingServerDomain || ANALYTICS_LOGGING_SERVER_DOMAIN; this._socket = new socket_1.Socket({ sessionEndpoint: `${WSProtocol}://${analyticsLoggingServerDomain}/${API_VERSION}/client/ws`, contextId: this._contextId, token: this._token, logger: this._logger, sdkVersion: this._sdkVersion, }); this._socket.onEventReceived.addListener((data) => { try { this._eventReceivedHandler(data); } catch (error) { this._logger.error('in _eventReceivedHandler', error); } }); this._socket.onConnectionFailed.addListener((data) => { // 現状の実装として4000はダッシュボード上でAnalyticsが有効になってない場合のエラーである // 初回接続時のconnectWithTimeoutのtimeoutよりPromiseを先に解決させるためのEventをemitする if (data.code === 4000) { this.onAnalyticsNotEnabledError.emit(data); } this.onConnectionFailed.emit(); this.dispose(); }); this._socket.onConnectionStateChanged.addListener((state) => { var _a; if (state === 'closed' && !this.isClosed() && ((_a = this._socket) === null || _a === void 0 ? void 0 : _a.isClosed())) { this._isClosed = true; this.dispose(); } this.onConnectionStateChanged.emit(state); }); this._socket.onTokenExpired.addListener(() => { void this._reconnectWithNewSkyWayAuthToken(); }); const openServerEventPayload = yield this._socket.onOpened.asPromise(); if (openServerEventPayload !== undefined) { this._statsRequest = openServerEventPayload.statsRequest; return; } else { this._logger.error('First time connection payload is undefined', new Error()); this.onConnectionFailed.emit(); return; } }); } bufferOrSendSdkLog(log) { const shouldImmediateSend = log.level === 'warn' || log.level === 'error'; this._pendingSdkLogs.push(log); if (shouldImmediateSend || this._pendingSdkLogs.length >= AnalyticsClient.MAX_PENDING_SDK_LOGS) { const logsToSend = [...this._pendingSdkLogs]; this._pendingSdkLogs.length = 0; this.sendSdkLogReport(logsToSend).catch((err) => { this._logger.warn('sendSdkLogReport failed', err); }); } } dispose() { clearInterval(this._sdkLogTimer); this._disconnect(); this._cleanupAnalyticsClientMaps(); } setNewSkyWayAuthToken(token) { if (this._socket !== undefined) { this._newToken = token; this._logger.debug('setNewSkyWayAuthToken is success'); } } cleanupOnUnpublished(publicationId) { this._mediaDeviceVersion.delete(publicationId); this._encodingsVersion.delete(publicationId); } cleanupOnUnsubscribed(subscriptionId) { this._preferredEncodingVersion.delete(subscriptionId); this._previousSubscriptionStats.delete(subscriptionId); } _disconnect() { var _a; (_a = this._socket) === null || _a === void 0 ? void 0 : _a.destroy(); this._socket = undefined; this._responseCallbacks.clear(); this._acknowledgeCallbacks.clear(); } sendMediaDeviceReport(report) { return __awaiter(this, void 0, void 0, function* () { let currentMediaDeviceVersion = this._mediaDeviceVersion.get(report.publicationId); if (currentMediaDeviceVersion === undefined) { currentMediaDeviceVersion = 0; } else { currentMediaDeviceVersion++; } this._mediaDeviceVersion.set(report.publicationId, currentMediaDeviceVersion); const payload = { publicationId: report.publicationId, mediaDeviceName: report.mediaDeviceName, mediaDeviceVersion: currentMediaDeviceVersion, mediaDeviceTrigger: report.mediaDeviceTrigger, updatedAt: report.updatedAt, }; const clientEvent = new clientEvent_1.ClientEvent('MediaDeviceReport', payload); yield this._sendClientEvent(clientEvent).catch((err) => { this._logger.warn('_sendClientEvent in sendMediaDeviceReport is failed', err); }); }); } sendSdkLogReport(logs) { return __awaiter(this, void 0, void 0, function* () { if (logs.length === 0) return; const sdkLogs = logs.map((l) => ({ timestamp: l.timestamp, level: l.level, message: Array.isArray(l.message) ? l.message.map((m) => (typeof m === 'string' ? m : JSON.stringify(m))).join(',') : String(l.message), })); const clientEvent = new clientEvent_1.ClientEvent('SdkLog', { sdkLogs, contextId: this._contextId, }); yield this._sendClientEvent(clientEvent).catch((err) => { this._logger.warn('_sendClientEvent in sendSdkLogReport is failed', err); }); }); } sendBindingRtcPeerConnectionToSubscription(bindingData) { return __awaiter(this, void 0, void 0, function* () { const clientEvent = new clientEvent_1.ClientEvent('BindingRtcPeerConnectionToSubscription', bindingData); yield this._sendClientEvent(clientEvent).catch((err) => { this._logger.warn('_sendClientEvent in sendBindingRtcPeerConnectionToSubscription is failed', err); }); }); } /** * RTCStatsReportにはcandidate-pair, local-candidate, remote-candidateが複数含まれる場合がある。 * 現在利用されているもののみを選出して返す。 */ filterStatsReport(report) { /** * candidate-pairの選出について * transportから現在利用されているcandidate-pairを特定することができる。 * ただしFirefoxの場合はtransportが含まれていない。(2024/09/04時点) * 代わりにcandidate-pairにselectedが含まれているのでFFではこれを利用する。 */ const connectedTransport = Array.from(report.values()).find((rtcStatsReportValue) => rtcStatsReportValue.type === 'transport' && rtcStatsReportValue.dtlsState === 'connected'); const candidatePairKeys = []; if (connectedTransport) { /** * connectedTransportが取れる場合: * ChromeやSafariの場合はtransportの情報を使ってcandidate-pairを選出する */ const nominatedCandidatePair = Array.from(report.values()).find((rtcStatsReportValue) => rtcStatsReportValue.type === 'candidate-pair' && rtcStatsReportValue.nominated && rtcStatsReportValue.id === (connectedTransport === null || connectedTransport === void 0 ? void 0 : connectedTransport.selectedCandidatePairId)); if (nominatedCandidatePair) { candidatePairKeys.push(nominatedCandidatePair.id, nominatedCandidatePair.localCandidateId, nominatedCandidatePair.remoteCandidateId, nominatedCandidatePair.transportId); } } else { /** * connectedTransportが取れない場合: * 現行FFの場合はcandidate-pairを直接みてnominated:trueかつselected:trueのものを選出する */ const nominatedCandidatePair = Array.from(report.values()).find((rtcStatsReportValue) => rtcStatsReportValue.type === 'candidate-pair' && rtcStatsReportValue.nominated && rtcStatsReportValue.selected); if (nominatedCandidatePair) { candidatePairKeys.push(nominatedCandidatePair.id, nominatedCandidatePair.localCandidateId, nominatedCandidatePair.remoteCandidateId, nominatedCandidatePair.transportId); } } const filteredReport = new Map(); const duplicatableTypes = ['candidate-pair', 'local-candidate', 'remote-candidate', 'transport']; for (const [key, rtcStatsReportValue] of report.entries()) { if (duplicatableTypes.includes(rtcStatsReportValue.type)) { // 重複し得るstats typeはnominateされたcandidate-pairから選出する if (candidatePairKeys.includes(rtcStatsReportValue.id)) { filteredReport.set(key, rtcStatsReportValue); } } else { filteredReport.set(key, rtcStatsReportValue); } } return filteredReport; } bundleStatsReportByStatsType(report) { const stats = {}; for (const v of report.values()) { stats[v.type] = v; } return stats; } sendSubscriptionStatsReport(report, subscriptionParams) { var _a; return __awaiter(this, void 0, void 0, function* () { const previousSubscriptionStat = this._previousSubscriptionStats.get(subscriptionParams.subscriptionId); this._previousSubscriptionStats.set(subscriptionParams.subscriptionId, { stats: report, createdAt: subscriptionParams.createdAt, }); if (previousSubscriptionStat === undefined) { // 初回の場合は時間あたりの値が出せないので送信しない return; } const filteredPreviousSubscriptionStats = this.filterStatsReport(previousSubscriptionStat.stats); const prevBundledSubscriptionStats = this.bundleStatsReportByStatsType(filteredPreviousSubscriptionStats); const previousCreatedAt = previousSubscriptionStat.createdAt; const duration = (subscriptionParams.createdAt - previousCreatedAt) / 1000; // mills to sec. if (duration <= 0) { throw new Error('duration must be greater than 0. also sendSubscriptionStatsReport was duplicated.'); } const filteredStatsReport = this.filterStatsReport(report); const bundledStatsReport = this.bundleStatsReportByStatsType(filteredStatsReport); // StatsReportから必要な値だけを抽出してSubscriptionStatsに格納する const subscriptionStats = {}; for (const { type, properties } of this._statsRequest.types) { for (const [prop, { normalization: normRequired, outputKey, contentType }] of Object.entries(properties)) { if (!contentType.includes(subscriptionParams.contentType)) { continue; } const statsReport = bundledStatsReport[type]; if (statsReport === undefined || statsReport[prop] === undefined) { continue; } if (normRequired) { const previousValue = (_a = prevBundledSubscriptionStats[type]) === null || _a === void 0 ? void 0 : _a[prop]; if (previousValue === undefined) { this._logger.warn(`${type} in previous statsReport is undefined`); continue; } const perSecondValue = (Number(statsReport[prop]) - Number(previousValue)) / duration; subscriptionStats[type] = Object.assign(Object.assign({}, subscriptionStats[type]), { [outputKey]: String(perSecondValue) }); } else { subscriptionStats[type] = Object.assign(Object.assign({}, subscriptionStats[type]), { [outputKey]: String(statsReport[prop]) }); } } } const payload = { subscriptionId: subscriptionParams.subscriptionId, stats: subscriptionStats, role: subscriptionParams.role, createdAt: subscriptionParams.createdAt, }; const clientEvent = new clientEvent_1.ClientEvent('SubscriptionStatsReport', payload); yield this._sendClientEvent(clientEvent).catch((err) => { this._logger.warn('_sendClientEvent in sendSubscriptionStatsReport is failed', err); }); }); } sendRtcPeerConnectionEventReport(report) { return __awaiter(this, void 0, void 0, function* () { const clientEvent = new clientEvent_1.ClientEvent('RtcPeerConnectionEventReport', report); yield this._sendClientEvent(clientEvent).catch((err) => { this._logger.warn('_sendClientEvent in sendRtcPeerConnectionEventReport is failed', err); }); }); } sendPublicationUpdateEncodingsReport(report) { return __awaiter(this, void 0, void 0, function* () { let currentEncodingsVersion = this._encodingsVersion.get(report.publicationId); if (currentEncodingsVersion === undefined) { currentEncodingsVersion = 0; } else { currentEncodingsVersion++; } this._encodingsVersion.set(report.publicationId, currentEncodingsVersion); const payload = { publicationId: report.publicationId, encodings: report.encodings, encodingsVersion: currentEncodingsVersion, updatedAt: report.updatedAt, }; const clientEvent = new clientEvent_1.ClientEvent('PublicationUpdateEncodingsReport', payload); yield this._sendClientEvent(clientEvent).catch((err) => { this._logger.warn('_sendClientEvent in sendPublicationUpdateEncodingsReport is failed', err); }); }); } sendSubscriptionUpdatePreferredEncodingReport(report) { return __awaiter(this, void 0, void 0, function* () { let currentPreferredEncodingVersion = this._preferredEncodingVersion.get(report.subscriptionId); if (currentPreferredEncodingVersion === undefined) { currentPreferredEncodingVersion = 0; } else { currentPreferredEncodingVersion++; } this._preferredEncodingVersion.set(report.subscriptionId, currentPreferredEncodingVersion); const payload = { subscriptionId: report.subscriptionId, preferredEncodingIndex: report.preferredEncodingIndex, preferredEncodingVersion: currentPreferredEncodingVersion, updatedAt: report.updatedAt, }; const clientEvent = new clientEvent_1.ClientEvent('SubscriptionUpdatePreferredEncodingReport', payload); yield this._sendClientEvent(clientEvent).catch((err) => { this._logger.warn('_sendClientEvent in sendSubscriptionUpdatePreferredEncodingReport is failed', err); }); }); } sendJoinReport(report) { return __awaiter(this, void 0, void 0, function* () { const clientEvent = new clientEvent_1.ClientEvent('JoinReport', report); yield this._sendClientEvent(clientEvent).catch((err) => { this._logger.warn('_sendClientEvent in sendJoinReport is failed', err); }); }); } _sendClientEvent(clientEvent) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { if (this._socket === undefined || this._socket.connectionState === 'closed') { reject(new Error('websocket is not connected')); return; } // 初回の接続に時間がかかっている場合はここで再送用のキューとacknowledgeのリストに入れる if (this._socket.connectionState === 'connecting') { this._socket.pushResendClientEventsQueue(clientEvent); this._setAcknowledgeCallback(clientEvent.id, (data) => __awaiter(this, void 0, void 0, function* () { if (data.ok) { this._acknowledgeCallbacks.delete(clientEvent.id); resolve(); } else { this._acknowledgeCallbacks.delete(clientEvent.id); reject(data); } })); this._logger.debug(`pushResendClientEventsQueue and setAcknowledgeCallback. clientEvent.id: ${clientEvent.id}`); reject(new Error('websocket is connecting now')); return; } const backoff = new backoff_1.BackOff({ times: 6, interval: 500, jitter: 100 }); for (; !backoff.exceeded;) { const timer = setTimeout(() => __awaiter(this, void 0, void 0, function* () { if (this._socket === undefined) { this._acknowledgeCallbacks.delete(clientEvent.id); reject(new Error('Socket closed when trying to resend')); return; } else { this._socket.resendAfterReconnect(clientEvent); } reject(new Error('Timeout to send data')); return; }), TIMEOUT_SEC * 1000); // 送信に失敗した際の再送ロジックはsend()内で処理される this._logger.debug(`send clientEvent, ${JSON.stringify(clientEvent)}`); this._socket.send(clientEvent).catch((err) => { this._acknowledgeCallbacks.delete(clientEvent.id); clearTimeout(timer); reject(err); return; }); /** * _waitForAcknowledgeはresultに次の2種類の値を返す * 1. undefined: 送信が成功し、undefinedでresolveされた場合 * 2. AcknowledgePayload型の値:送信は成功したがサーバーから ok: false のacknowledgeが返されたため、acknowledge payloadでrejectされた場合 * 何らかのエラーによってrejectされた場合: * これは_messageHandlerで弾かれるので考慮しなくて良い. */ const result = yield this._waitForAcknowledge(clientEvent.id).catch((err) => { return err; }); clearTimeout(timer); if ((0, payloadTypes_1.isAcknowledgePayload)(result)) { if (result.reason === 'unexpected') { yield backoff.wait(); } else { reject(result); return; } } else { resolve(); return; } } reject(new Error('unexpected has occurred at server')); return; })); }); } _waitForAcknowledge(clientEventId) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => { this._setAcknowledgeCallback(clientEventId, (data) => __awaiter(this, void 0, void 0, function* () { if (data.ok) { this._acknowledgeCallbacks.delete(clientEventId); resolve(); } else { this._acknowledgeCallbacks.delete(clientEventId); reject(data); } })); }); }); } _reconnectWithNewSkyWayAuthToken() { return __awaiter(this, void 0, void 0, function* () { this._disconnect(); if (this._newToken !== undefined) { this._token = this._newToken; this._newToken = undefined; yield this.connect(); } else { this._logger.warn('new token is not set. so not reconnect.'); } }); } _eventReceivedHandler(data) { switch (data.type) { case 'Acknowledge': this._acknowledgeHandler(data.payload); break; case 'Open': break; // nop default: { // eslint-disable-next-line @typescript-eslint/no-unused-vars const _ = data.type; this._logger.warn(`Unknown event: ${data.type}`); } } } _acknowledgeHandler(payload) { if (!(0, payloadTypes_1.isAcknowledgePayload)(payload)) { throw new Error('Invalid payload'); } const { eventId } = payload; if (!this._acknowledgeCallbacks.has(eventId)) { throw new Error(`acknowledge event has unknown eventId: ${eventId}`); } const callback = this._acknowledgeCallbacks.get(eventId); if (callback) { this._acknowledgeCallbacks.delete(eventId); callback(payload); } } _setAcknowledgeCallback(eventId, callback) { this._acknowledgeCallbacks.set(eventId, callback); } _cleanupAnalyticsClientMaps() { this._mediaDeviceVersion.clear(); this._encodingsVersion.clear(); this._preferredEncodingVersion.clear(); this._previousSubscriptionStats.clear(); } getIntervalSec() { return this._statsRequest.intervalSec; } isConnectionEstablished() { if (!this._socket || this._socket.connectionState === 'connecting' || this._socket.connectionState === 'closed') { return false; } else { return true; } } isClosed() { return this._isClosed; } } exports.AnalyticsClient = AnalyticsClient; AnalyticsClient.MAX_PENDING_SDK_LOGS = 50; //# sourceMappingURL=analyticsClient.js.map