@skyway-sdk/analytics-client
Version:
The official Next Generation JavaScript SDK for SkyWay
547 lines • 26.7 kB
JavaScript
"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