@skyway-sdk/core
Version:
The official Next Generation JavaScript SDK for SkyWay
659 lines • 32 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
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.LocalPersonImpl = void 0;
const common_1 = require("@skyway-sdk/common");
const errors_1 = require("../../errors");
const member_1 = require("../../member");
const member_2 = require("../../plugin/internal/unknown/member");
const publication_1 = require("../../publication");
const util_1 = require("../../util");
const remoteMember_1 = require("../remoteMember");
const agent_1 = require("./agent");
__exportStar(require("./adapter"), exports);
__exportStar(require("./factory"), exports);
const log = new common_1.Logger('packages/core/src/member/localPerson/index.ts');
/**@internal */
class LocalPersonImpl extends member_1.MemberImpl {
/**@private */
constructor(args) {
super(args);
this.args = args;
this.type = 'person';
this.subtype = 'person';
this.side = 'local';
this.keepaliveIntervalSec = this.args.keepaliveIntervalSec;
this.keepaliveIntervalGapSec = this.args.keepaliveIntervalGapSec;
this.preventAutoLeaveOnBeforeUnload = this.args.preventAutoLeaveOnBeforeUnload;
this.disableSignaling = this.args.disableSignaling;
this.disableAnalytics = this.args.disableAnalytics;
this.config = this.context.config;
this.onStreamPublished = this._events.make();
this.onStreamUnpublished = this._events.make();
this.onPublicationListChanged = this._events.make();
this.onPublicationSubscribed = this._events.make();
this.onPublicationUnsubscribed = this._events.make();
this.onSubscriptionListChanged = this._events.make();
this.onFatalError = this._events.make();
this._onStreamSubscribeFailed = this._events.make();
/**@private */
this._onDisposed = this._events.make();
this._disposer = new common_1.EventDisposer();
this._subscribing = {};
this._requestQueue = new common_1.PromiseQueue();
/**@private */
this.iceManager = this.args.iceManager;
/**@private */
this._disposed = false;
this._publishingAgent = new agent_1.PublishingAgent(this);
this._subscribingAgent = new agent_1.SubscribingAgent(this);
this._signaling = args.signaling;
this._analytics = args.analytics;
this._listenChannelEvent();
this._listenBeforeUnload();
}
static Create(...args) {
return __awaiter(this, void 0, void 0, function* () {
const person = new LocalPersonImpl(...args);
yield person._setupTtlTimer();
if (person._analytics) {
void person._analytics.client.sendJoinReport({
memberId: person.id,
});
}
return person;
});
}
_listenChannelEvent() {
this.channel.onPublicationSubscribed
.add(({ subscription }) => __awaiter(this, void 0, void 0, function* () {
yield this._handleOnPublicationSubscribe(subscription).catch((e) => log.error('_handleOnStreamSubscribe', e));
}))
.disposer(this._disposer);
this.channel.onPublicationUnsubscribed
.add(({ subscription }) => __awaiter(this, void 0, void 0, function* () {
yield this._handleOnPublicationUnsubscribe(subscription).catch((e) => log.error('_handleOnStreamUnsubscribe', e));
}))
.disposer(this._disposer);
this.channel._onDisposed.once(() => {
this.dispose();
});
this.onLeft.once(() => {
this.dispose();
});
}
/**@throws {@SkyWayError} */
_setupTtlTimer() {
return __awaiter(this, void 0, void 0, function* () {
const { keepaliveIntervalSec, keepaliveIntervalGapSec } = this;
if (keepaliveIntervalSec == null)
return;
log.debug('_setupTtlTimer', this.toJSON(), {
keepaliveIntervalSec,
keepaliveIntervalGapSec,
});
if (keepaliveIntervalSec === -1) {
return;
}
const updateTtl = () => __awaiter(this, void 0, void 0, function* () {
if (this._disposed) {
return;
}
const now = yield this.context._api.getServerUnixtimeInSec();
this.ttlSec = Math.floor(now + keepaliveIntervalSec + (keepaliveIntervalGapSec !== null && keepaliveIntervalGapSec !== void 0 ? keepaliveIntervalGapSec : 0));
try {
yield this.channel._updateMemberTtl(this.id, this.ttlSec);
log.debug('updateTtl', this.toJSON(), {
now,
ttlSec: this.ttlSec,
keepaliveIntervalSec: keepaliveIntervalSec !== null && keepaliveIntervalSec !== void 0 ? keepaliveIntervalSec : 0,
keepaliveIntervalGapSec: keepaliveIntervalGapSec !== null && keepaliveIntervalGapSec !== void 0 ? keepaliveIntervalGapSec : 0,
diff: this.ttlSec - now,
});
}
catch (error) {
if (this._disposed) {
return;
}
throw error;
}
});
yield updateTtl();
this.ttlInterval = setInterval(() => __awaiter(this, void 0, void 0, function* () {
yield updateTtl().catch((error) => {
if (!this._disposed) {
this.onFatalError.emit((0, util_1.createError)({
operationName: 'localPerson._setupTtlTimer',
path: log.prefix,
info: Object.assign(Object.assign({}, errors_1.errors.internal), { detail: 'updateMemberTtl failed' }),
channel: this.channel,
context: this.context,
error,
}));
this.dispose();
}
});
}), keepaliveIntervalSec * 1000);
});
}
_listenBeforeUnload() {
if (window && !this.preventAutoLeaveOnBeforeUnload) {
const leave = () => __awaiter(this, void 0, void 0, function* () {
window.removeEventListener('beforeunload', leave);
if (this.state !== 'joined') {
return;
}
log.debug('leave by beforeunload', this.toJSON());
yield this.leave();
});
window.addEventListener('beforeunload', leave);
}
}
/**@throws {@link SkyWayError} */
_handleOnPublicationSubscribe(subscription) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
if (subscription.subscriber.id === this.id) {
try {
const timestamp = log.info('[start] startSubscribing', yield (0, util_1.createLogPayload)({
operationName: 'onPublicationSubscribed',
channel: this.channel,
}), { subscription });
const options = (_a = this._subscribing[subscription.publication.id]) === null || _a === void 0 ? void 0 : _a.options;
if (options) {
subscription.preferredEncoding = options.preferredEncodingId;
}
yield this._subscribingAgent.startSubscribing(subscription);
this.onPublicationSubscribed.emit({
subscription,
stream: subscription.stream,
});
this.onSubscriptionListChanged.emit();
log.elapsed(timestamp, '[end] startSubscribing', yield (0, util_1.createLogPayload)({
operationName: 'onPublicationSubscribed',
channel: this.channel,
}), {
subscription,
});
}
catch (error) {
this._onStreamSubscribeFailed.emit({ error, subscription });
throw error;
}
}
if (subscription.publication.publisher.id === this.id) {
if (subscription.subscriber.id === this.id) {
throw (0, util_1.createError)({
operationName: 'localPerson._handleOnStreamSubscribe',
path: log.prefix,
info: Object.assign(Object.assign({}, errors_1.errors.internal), { detail: 'can not subscribe own Publication' }),
channel: this.channel,
context: this.context,
});
}
const timestamp = log.info('[start] startPublishing', yield (0, util_1.createLogPayload)({
operationName: 'onPublicationSubscribed',
channel: this.channel,
}), { subscription });
yield this._publishingAgent.startPublishing(subscription).catch((e) => {
log.error('[failed] startPublishing', e, { subscription });
throw e;
});
log.elapsed(timestamp, '[end] startPublishing', yield (0, util_1.createLogPayload)({
operationName: 'onPublicationSubscribed',
channel: this.channel,
}), { subscription });
}
});
}
/**@throws {@link SkyWayError} */
_handleOnPublicationUnsubscribe(subscription) {
return __awaiter(this, void 0, void 0, function* () {
if (subscription.publication.publisher.id === this.id) {
const timestamp = log.info('[start] stopPublishing', yield (0, util_1.createLogPayload)({
operationName: 'onPublicationUnsubscribed',
channel: this.channel,
}), { subscription });
yield this._publishingAgent
.stopPublishing(subscription.publication, subscription.subscriber)
.catch((e) => {
log.error('[failed] stopPublishing', e, { subscription });
throw e;
});
log.elapsed(timestamp, '[end] stopPublishing', yield (0, util_1.createLogPayload)({
operationName: 'onPublicationUnsubscribed',
channel: this.channel,
}), { subscription });
}
if (subscription.subscriber.id === this.id) {
const timestamp = log.info('[start] stopSubscribing', yield (0, util_1.createLogPayload)({
operationName: 'onPublicationUnsubscribed',
channel: this.channel,
}), { subscription });
yield this._subscribingAgent.stopSubscribing(subscription).catch((e) => {
log.error('[failed] stopSubscribing', { subscription }, e);
throw e;
});
this.onPublicationUnsubscribed.emit({ subscription });
this.onSubscriptionListChanged.emit();
log.elapsed(timestamp, '[end] stopSubscribing', yield (0, util_1.createLogPayload)({
operationName: 'onPublicationUnsubscribed',
channel: this.channel,
}), { subscription });
}
});
}
/**@throws {@link SkyWayError} */
publish(stream, options = {}) {
var _a, _b, _c, _d, _e, _f;
return __awaiter(this, void 0, void 0, function* () {
const timestamp = log.info('[start] publish', yield (0, util_1.createLogPayload)({
operationName: 'localPerson.publish',
channel: this.channel,
}), { options });
if (this.state !== 'joined') {
throw (0, util_1.createError)({
operationName: 'localPerson.publish',
info: errors_1.errors.localPersonNotJoinedChannel,
path: log.prefix,
channel: this.channel,
context: this.context,
});
}
if (stream.published) {
throw (0, util_1.createError)({
operationName: 'localPerson.publish',
channel: this.channel,
context: this.context,
info: errors_1.errors.alreadyPublishedStream,
path: log.prefix,
});
}
stream.published = true;
if (options.codecCapabilities) {
options.codecCapabilities = options.codecCapabilities.filter((c) => c !== undefined && c !== null);
}
options.type = (_a = options.type) !== null && _a !== void 0 ? _a : 'p2p';
const init = {
metadata: options.metadata,
publisher: this.id,
channel: this.channel.id,
contentType: stream.contentType,
codecCapabilities: (_b = options.codecCapabilities) !== null && _b !== void 0 ? _b : [],
isEnabled: options.isEnabled,
type: options.type,
};
if (stream.contentType === 'video' &&
((_c = init.codecCapabilities) === null || _c === void 0 ? void 0 : _c.length) === 0) {
init.codecCapabilities = [{ mimeType: 'video/vp8' }];
}
if (options.encodings && options.encodings.length > 0) {
init.encodings = (0, publication_1.normalizeEncodings)((0, publication_1.sortEncodingParameters)(options.encodings));
}
const published = yield this._requestQueue.push(() => this.channel._publish(init).catch((e) => {
throw (0, util_1.createError)({
operationName: 'localPerson.publish',
context: this.context,
channel: this.channel,
info: e.info,
path: log.prefix,
error: e,
});
}));
// publication作成時にpublication.state=isEnabledとなり、その後isEnabledに合わせてpublicationのenableStream/disableStreamを呼び出してもsetIsEnabled/setEnabledが実行されない。
// そのままではpublication.stateとstreamで状態の乖離が発生する場合があるため、ここで直接実行し一致させておく。
if (stream.contentType === 'data') {
stream.setIsEnabled(published.isEnabled);
}
else {
yield stream.setEnabled(published.isEnabled);
}
const publication = this.channel._addPublication(published);
publication._setStream(stream);
if ((_d = init.codecCapabilities) === null || _d === void 0 ? void 0 : _d.length) {
publication.setCodecCapabilities(init.codecCapabilities);
}
if ((_e = init.encodings) === null || _e === void 0 ? void 0 : _e.length) {
publication.setEncodings(init.encodings);
}
yield this._handleOnStreamPublish(publication);
log.elapsed(timestamp, '[end] publish', yield (0, util_1.createLogPayload)({
operationName: 'localPerson.publish',
channel: this.channel,
}), { publication });
// dataの場合はMediaDeviceがないので送信処理をしない
if (['video', 'audio'].includes(publication.contentType) &&
this._analytics &&
!this._analytics.isClosed()) {
// 再送時に他の処理をブロックしないためにawaitしない
void this._analytics.client.sendMediaDeviceReport({
publicationId: publication.id,
mediaDeviceName: publication.deviceName,
mediaDeviceTrigger: 'publish',
updatedAt: Date.now(),
});
const encodings = (_f = init.encodings) !== null && _f !== void 0 ? _f : [];
void this._analytics.client.sendPublicationUpdateEncodingsReport({
publicationId: publication.id,
encodings: encodings,
updatedAt: Date.now(),
});
}
return publication;
});
}
_handleOnStreamPublish(publication) {
return __awaiter(this, void 0, void 0, function* () {
log.info('onStreamPublished', yield (0, util_1.createLogPayload)({
operationName: 'onStreamPublished',
channel: this.channel,
}));
this.onStreamPublished.emit({ publication });
this.onPublicationListChanged.emit();
});
}
/**@throws {@link SkyWayError} */
unpublish(target) {
return __awaiter(this, void 0, void 0, function* () {
const timestamp = log.info('[start] unpublish', yield (0, util_1.createLogPayload)({
operationName: 'localPerson.unpublish',
channel: this.channel,
}));
const publicationId = typeof target === 'string' ? target : target.id;
if (this.state !== 'joined') {
throw (0, util_1.createError)({
operationName: 'localPerson.unpublish',
info: errors_1.errors.localPersonNotJoinedChannel,
path: log.prefix,
context: this.context,
channel: this.channel,
});
}
const publication = this.channel._getPublication(publicationId);
if (!publication) {
throw (0, util_1.createError)({
operationName: 'localPerson.unpublish',
info: errors_1.errors.publicationNotExist,
path: log.prefix,
context: this.context,
channel: this.channel,
payload: { publicationId },
});
}
if (publication.stream) {
publication.stream._unpublished();
}
yield this._requestQueue.push(() => this.channel._unpublish(publicationId));
publication._setStream(undefined);
publication.subscriptions
.map((s) => s.subscriber)
.forEach((s) => {
if ((0, remoteMember_1.isRemoteMember)(s)) {
this._publishingAgent.stopPublishing(publication, s).catch((e) => {
log.error('[failed] stopPublishing', e, { publication });
});
}
});
yield this._handleOnStreamUnpublished(publication);
log.elapsed(timestamp, '[end] unpublish', yield (0, util_1.createLogPayload)({
operationName: 'localPerson.unpublish',
channel: this.channel,
}), { publication });
});
}
_handleOnStreamUnpublished(publication) {
return __awaiter(this, void 0, void 0, function* () {
log.info('onStreamUnpublished', yield (0, util_1.createLogPayload)({
operationName: 'onStreamUnpublished',
channel: this.channel,
}));
this.onStreamUnpublished.emit({ publication });
this.onPublicationListChanged.emit();
});
}
/**@throws {@link SkyWayError} */
subscribe(target, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
const timestamp = log.info('[start] subscribe', yield (0, util_1.createLogPayload)({
operationName: 'localPerson.subscribe',
channel: this.channel,
}), { target });
const publicationId = typeof target === 'string' ? target : target.id;
if (this.state !== 'joined') {
throw (0, util_1.createError)({
operationName: 'localPerson.subscribe',
info: errors_1.errors.localPersonNotJoinedChannel,
path: log.prefix,
context: this.context,
channel: this.channel,
payload: { target },
});
}
const publication = this.channel._getPublication(publicationId);
if (publication === undefined) {
throw (0, util_1.createError)({
operationName: 'localPerson.subscribe',
info: errors_1.errors.publicationNotExist,
path: log.prefix,
context: this.context,
channel: this.channel,
payload: publication,
});
}
this._validatePublicationForSubscribe(publication);
this._subscribing[publication.id] = {
options,
processing: true,
};
const subscribing = this._subscribing[publication.id];
try {
const subscriptionDto = yield this._requestQueue.push(() => this.channel._subscribe(this.id, publicationId));
log.elapsed(timestamp, '[elapsed] subscribe / subscriptionDto received', {
subscriptionDto,
});
const subscription = this.channel._addSubscription(subscriptionDto);
if (!subscription.stream) {
yield Promise.race([
new Promise((r, f) => {
this.onPublicationSubscribed
.watch(({ subscription }) => subscription.publication.id === publicationId, this.context.config.rtcApi.timeout)
.then(r)
.catch((e) => __awaiter(this, void 0, void 0, function* () {
if (subscribing.processing) {
f((0, util_1.createError)({
operationName: 'localPerson.subscribe',
info: Object.assign(Object.assign({}, errors_1.errors.timeout), { detail: 'failed to subscribe publication. maybe publisher already left room' }),
path: log.prefix,
context: this.context,
channel: this.channel,
payload: { subscription, publication },
error: e,
}));
}
}));
}),
new Promise((r, f) => {
this.channel.onMemberLeft
.watch((e) => e.member.id === publication.publisher.id, this.context.config.rtcApi.timeout + 1000)
.then(() => {
if (subscribing.processing) {
f((0, util_1.createError)({
operationName: 'localPerson.subscribe',
info: Object.assign(Object.assign({}, errors_1.errors.internal), { detail: 'failed to subscribe publication. publisher already left room' }),
path: log.prefix,
context: this.context,
channel: this.channel,
payload: { subscription, publication },
}));
}
})
.catch(r);
}),
new Promise((r, f) => {
this._onStreamSubscribeFailed
.watch((e) => e.subscription.publication.id === publication.id, this.context.config.rtcApi.timeout + 1000)
.then((e) => {
var _a, _b;
if (subscribing.processing) {
const info = (_b = (_a = e === null || e === void 0 ? void 0 : e.error) === null || _a === void 0 ? void 0 : _a.info) !== null && _b !== void 0 ? _b : Object.assign(Object.assign({}, errors_1.errors.internal), { detail: 'subscribe _onStreamSubscribeFailed' });
f((0, util_1.createError)({
operationName: 'localPerson.subscribe',
info,
path: log.prefix,
context: this.context,
channel: this.channel,
error: e.error,
payload: { subscription, publication },
}));
}
})
.catch(r);
}),
]);
}
subscribing.processing = false;
log.elapsed(timestamp, '[end] subscribe', yield (0, util_1.createLogPayload)({
operationName: 'localPerson.subscribe',
channel: this.channel,
}), { subscription, publication });
return {
subscription: subscription,
stream: subscription.stream,
};
}
catch (error) {
subscribing.processing = false;
// 対象のPublicationがすでにUnPublishされている時に失敗しうる
log.warn('[failed] subscribe', error, { publication });
throw error;
}
});
}
/**@throws {@link SkyWayError} */
_validatePublicationForSubscribe(publication) {
if (publication.publisher.id === this.id) {
throw (0, util_1.createError)({
operationName: 'localPerson._validatePublicationForSubscribe',
info: errors_1.errors.publicationNotExist,
path: log.prefix,
context: this.context,
channel: this.channel,
payload: { publication },
});
}
if (publication.publisher instanceof member_2.UnknownMemberImpl) {
throw (0, util_1.createError)({
operationName: 'localPerson._validatePublicationForSubscribe',
info: errors_1.errors.unknownMemberType,
path: log.prefix,
context: this.context,
channel: this.channel,
payload: { publication },
});
}
if (this.subscriptions.find((s) => s.publication.id === publication.id)) {
throw (0, util_1.createError)({
operationName: 'localPerson._validatePublicationForSubscribe',
info: errors_1.errors.alreadySubscribedPublication,
path: log.prefix,
context: this.context,
channel: this.channel,
payload: { publication },
});
}
}
/**@throws {@link SkyWayError} */
unsubscribe(target) {
return __awaiter(this, void 0, void 0, function* () {
const timestamp = log.info('[start] unsubscribe', yield (0, util_1.createLogPayload)({
operationName: 'localPerson.unsubscribe',
channel: this.channel,
}));
const subscriptionId = typeof target === 'string' ? target : target.id;
if (this.state !== 'joined') {
throw (0, util_1.createError)({
operationName: 'localPerson.unsubscribe',
info: errors_1.errors.localPersonNotJoinedChannel,
path: log.prefix,
context: this.context,
channel: this.channel,
});
}
const subscription = this.subscriptions.find((s) => s.id === subscriptionId);
if (!subscription) {
throw (0, util_1.createError)({
operationName: 'localPerson.unsubscribe',
info: errors_1.errors.subscriptionNotExist,
path: log.prefix,
context: this.context,
channel: this.channel,
payload: { subscriptionId },
});
}
delete this._subscribing[subscription.publication.id];
yield this._requestQueue.push(() => this.channel._unsubscribe(subscriptionId));
log.elapsed(timestamp, '[end] unsubscribe', yield (0, util_1.createLogPayload)({
operationName: 'localPerson.unsubscribe',
channel: this.channel,
}), { subscription });
});
}
_getRemoteMemberConnections() {
const connections = this.channel.members
.filter(remoteMember_1.isRemoteMember)
.map((m) => m._getConnection(this.id));
const active = connections.filter((c) => (c === null || c === void 0 ? void 0 : c.closed) === false);
return active;
}
/**
* リソース解放
* - メッセージサービスとのセッション
* - メディア通信
* - イベントリスナー
* - TTL更新
*/
dispose() {
if (this._disposed) {
return;
}
this._disposed = true;
log.debug('disposed', this.toJSON());
clearInterval(this.ttlInterval);
if (this._signaling) {
this._signaling.close();
}
if (this._analytics) {
this._analytics.close();
}
this._getRemoteMemberConnections().forEach((c) => {
c.close({ reason: 'localPerson disposed' });
});
this._onDisposed.emit();
this._events.dispose();
this._disposer.dispose();
}
}
exports.LocalPersonImpl = LocalPersonImpl;
//# sourceMappingURL=index.js.map