diff --git a/THIRD_PARTY_LICENSE b/THIRD_PARTY_LICENSE index 0d3b132..a438f9c 100644 --- a/THIRD_PARTY_LICENSE +++ b/THIRD_PARTY_LICENSE @@ -29,7 +29,7 @@ SOFTWARE. --- -@skyway-sdk/core@1.2.5 +@skyway-sdk/core@1.3.0 MIT @@ -68,7 +68,7 @@ MIT --- -@skyway-sdk/room@1.2.5 +@skyway-sdk/room@1.3.0 MIT @@ -192,7 +192,7 @@ SOFTWARE. --- -@skyway-sdk/sfu-bot@1.2.5 +@skyway-sdk/sfu-bot@1.3.0 MIT @@ -285,7 +285,7 @@ SOFTWARE. --- -@types/debug@4.1.7 +@types/debug@4.1.8 MIT diff --git a/package-lock.json b/package-lock.json index a04605b..7f0b859 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22716,11 +22716,11 @@ }, "packages/core": { "name": "@skyway-sdk/core", - "version": "1.2.4", + "version": "1.3.0", "license": "MIT", "dependencies": { - "@skyway-sdk/rtc-api-client": "^1.2.3", - "@skyway-sdk/signaling-client": "^1.0.0", + "@skyway-sdk/rtc-api-client": "^1.2.4", + "@skyway-sdk/signaling-client": "^1.0.1", "bowser": "^2.11.0", "deepmerge": "^4.2.2", "sdp-transform": "^2.14.1", @@ -22747,11 +22747,11 @@ }, "packages/room": { "name": "@skyway-sdk/room", - "version": "1.2.4", + "version": "1.3.0", "license": "MIT", "dependencies": { - "@skyway-sdk/core": "^1.2.4", - "@skyway-sdk/sfu-bot": "^1.2.4", + "@skyway-sdk/core": "^1.3.0", + "@skyway-sdk/sfu-bot": "^1.3.0", "uuid": "^9.0.0" }, "devDependencies": { @@ -22769,11 +22769,11 @@ }, "packages/rtc-api-client": { "name": "@skyway-sdk/rtc-api-client", - "version": "1.2.3", + "version": "1.2.4", "license": "MIT", "dependencies": { - "@skyway-sdk/rtc-rpc-api-client": "^1.2.3", - "@skyway-sdk/token": "^1.2.3", + "@skyway-sdk/rtc-rpc-api-client": "^1.2.4", + "@skyway-sdk/token": "^1.2.4", "deepmerge": "^4.2.2", "uuid": "^9.0.0" }, @@ -22792,7 +22792,7 @@ }, "packages/rtc-rpc-api-client": { "name": "@skyway-sdk/rtc-rpc-api-client", - "version": "1.2.3", + "version": "1.2.4", "license": "MIT", "dependencies": { "@skyway-sdk/common": "^1.2.3", @@ -22801,7 +22801,7 @@ "uuid": "^9.0.0" }, "devDependencies": { - "@skyway-sdk/token": "^1.2.3", + "@skyway-sdk/token": "^1.2.4", "@types/uuid": "^9.0.1" } }, @@ -22826,10 +22826,10 @@ }, "packages/sfu-bot": { "name": "@skyway-sdk/sfu-bot", - "version": "1.2.4", + "version": "1.3.0", "license": "MIT", "dependencies": { - "@skyway-sdk/core": "^1.2.4", + "@skyway-sdk/core": "^1.3.0", "@skyway-sdk/sfu-api-client": "^1.2.3", "lodash": "4.17.21", "mediasoup-client": "3.6.82" @@ -22838,7 +22838,7 @@ }, "packages/signaling-client": { "name": "@skyway-sdk/signaling-client", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "isomorphic-fetch": "^3.0.0", @@ -24240,7 +24240,7 @@ }, "packages/token": { "name": "@skyway-sdk/token", - "version": "1.2.3", + "version": "1.2.4", "license": "MIT", "dependencies": { "@skyway-sdk/common": "^1.2.3", @@ -29457,8 +29457,8 @@ "@skyway-sdk/core": { "version": "file:packages/core", "requires": { - "@skyway-sdk/rtc-api-client": "^1.2.3", - "@skyway-sdk/signaling-client": "^1.0.0", + "@skyway-sdk/rtc-api-client": "^1.2.4", + "@skyway-sdk/signaling-client": "^1.0.1", "@types/sdp-transform": "^2.4.5", "@types/uuid": "^9.0.1", "bowser": "^2.11.0", @@ -29480,8 +29480,8 @@ "@skyway-sdk/room": { "version": "file:packages/room", "requires": { - "@skyway-sdk/core": "^1.2.4", - "@skyway-sdk/sfu-bot": "^1.2.4", + "@skyway-sdk/core": "^1.3.0", + "@skyway-sdk/sfu-bot": "^1.3.0", "@types/uuid": "^9.0.1", "uuid": "^9.0.0" }, @@ -29496,8 +29496,8 @@ "@skyway-sdk/rtc-api-client": { "version": "file:packages/rtc-api-client", "requires": { - "@skyway-sdk/rtc-rpc-api-client": "^1.2.3", - "@skyway-sdk/token": "^1.2.3", + "@skyway-sdk/rtc-rpc-api-client": "^1.2.4", + "@skyway-sdk/token": "^1.2.4", "@types/uuid": "^9.0.1", "deepmerge": "^4.2.2", "uuid": "^9.0.0" @@ -29515,7 +29515,7 @@ "requires": { "@skyway-sdk/common": "^1.2.3", "@skyway-sdk/model": "^1.0.0", - "@skyway-sdk/token": "^1.2.3", + "@skyway-sdk/token": "^1.2.4", "@types/uuid": "^9.0.1", "isomorphic-ws": "^4.0.1", "uuid": "^9.0.0" @@ -29538,7 +29538,7 @@ "@skyway-sdk/sfu-bot": { "version": "file:packages/sfu-bot", "requires": { - "@skyway-sdk/core": "^1.2.4", + "@skyway-sdk/core": "^1.3.0", "@skyway-sdk/sfu-api-client": "^1.2.3", "lodash": "4.17.21", "mediasoup-client": "3.6.82" diff --git a/packages/core/package.json b/packages/core/package.json index fa04d82..fedb717 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@skyway-sdk/core", - "version": "1.2.5", + "version": "1.3.0", "description": "The official Next Generation JavaScript SDK for SkyWay", "homepage": "https://skyway.ntt.com/", "repository": { @@ -32,7 +32,7 @@ "graph": "dependency-cruiser --include-only '^src' --output-type dot src | dot -T svg > docs/dependencygraph.svg", "pre:test": "cd ../../ && npm run build && cd packages/core", "publish:npm": "npx can-npm-publish --verbose && npm run build && npm publish --access public", - "test-all": "npm run test-large && npm run test-middle && npm run test-small", + "test-all": "npm-run-all -p test-large test-middle test-small", "test-large": "karma start ./karma.large.js --single-run --browsers chrome_headless_with_fake_device", "test-large:dev": "karma start ./karma.large.js --browsers chrome_with_fake_device", "test-middle": "karma start ./karma.middle.js --single-run --browsers chrome_headless_with_fake_device", diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 8cd49cb..d7bc687 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -20,6 +20,11 @@ export type SkyWayConfigOptions = { timeout?: number; turnPolicy?: TurnPolicy; turnProtocol?: TurnProtocol; + /** + * @internal + * @description ms + * */ + iceDisconnectBufferTimeout?: number; }; token: { updateReminderSec?: number }; log: Partial<{ level: LogLevel; format: LogFormat }>; @@ -71,6 +76,7 @@ export class ContextConfig implements SkyWayConfigOptions { turnPolicy: 'enable', turnProtocol: 'all', encodedInsertableStreams: false, + iceDisconnectBufferTimeout: 5000, }; token: Required = { updateReminderSec: 30, diff --git a/packages/core/src/external/signaling.ts b/packages/core/src/external/signaling.ts index 6b6405a..fb84f4d 100644 --- a/packages/core/src/external/signaling.ts +++ b/packages/core/src/external/signaling.ts @@ -60,6 +60,7 @@ export class SignalingSession { readonly onConnectionFailed = new Event(); readonly onConnectionStateChanged = new Event(); readonly onMessage = new Event(); + closed = false; private _chunkedMessageBuffer: { [messageId: string]: string[] } = {}; private _backoffUpdateSkyWayAuthToken = new BackOff({ @@ -115,6 +116,7 @@ export class SignalingSession { } await reply({}).catch((e) => { + if (this.closed) return; log.warn( 'failed to reply', createWarnPayload({ @@ -179,7 +181,8 @@ export class SignalingSession { log.debug('[end] connect signalingService'); } - disconnect() { + close() { + this.closed = true; this._client.disconnect(); } @@ -224,18 +227,16 @@ export class SignalingSession { await this._client.request(target, chunkMessage as any, timeout / 1000); } } catch (error: any) { - if (target.state === 'joined') { - throw createError({ - operationName: 'SignalingSession.send', - context: this.context, - info: { ...errors.internal, detail: 'signalingClient' }, - error, - path: log.prefix, - payload: { target, data }, - }); - } else { - log.warn('target already left', error); - } + if (this.closed || target.state !== 'joined') return; + + throw createError({ + operationName: 'SignalingSession.send', + context: this.context, + info: { ...errors.internal, detail: 'signalingClient' }, + error, + path: log.prefix, + payload: { target, data }, + }); } } } diff --git a/packages/core/src/media/stream/local/base.ts b/packages/core/src/media/stream/local/base.ts index fe5a407..03fed91 100644 --- a/packages/core/src/media/stream/local/base.ts +++ b/packages/core/src/media/stream/local/base.ts @@ -9,12 +9,19 @@ import { Stream, WebRTCStats, ContentType } from '../base'; export abstract class LocalStreamBase implements Stream { readonly side = 'local'; /** + * @deprecated + * @use Publication.onConnectionStateChanged * @description [japanese] メディア通信の状態が変化した時に発火するイベント */ readonly onConnectionStateChanged = new Event<{ remoteMember: RemoteMember; state: TransportConnectionState; }>(); + /**@internal */ + readonly _onConnectionStateChanged = new Event<{ + remoteMember: RemoteMember; + state: TransportConnectionState; + }>(); readonly id: string = uuidV4(); /**@internal */ _label = ''; @@ -28,9 +35,14 @@ export abstract class LocalStreamBase implements Stream { _getStatsCallbacks: { [remoteMemberId: string]: () => Promise; } = {}; + private _connectionState: { + [remoteMemberId: string]: TransportConnectionState; + } = {}; /**@internal */ - constructor(readonly contentType: ContentType) {} + constructor(readonly contentType: ContentType) { + this._onConnectionStateChanged.pipe(this.onConnectionStateChanged); + } /**@internal */ _setLabel(label: string) { @@ -50,6 +62,16 @@ export abstract class LocalStreamBase implements Stream { return this._getTransportCallbacks[id]?.(); } + /**@internal */ + _setConnectionState( + remoteMember: RemoteMember, + state: TransportConnectionState + ) { + if (this._connectionState[remoteMember.id] === state) return; + this._connectionState[remoteMember.id] = state; + this._onConnectionStateChanged.emit({ remoteMember, state }); + } + /** * @deprecated * @use Publication.getStats @@ -101,14 +123,16 @@ export abstract class LocalStreamBase implements Stream { /**@internal */ _getConnectionState(selector: Member | string): TransportConnectionState { - return this._getTransport(selector)?.connectionState ?? 'new'; + const id = typeof selector === 'string' ? selector : selector.id; + return this._connectionState[id] ?? 'new'; } /**@internal */ _getConnectionStateAll() { - return Object.entries(this._getTransportCallbacks).map( - ([memberId, cb]) => ({ memberId, connectionState: cb().connectionState }) - ); + return Object.keys(this._getTransportCallbacks).map((memberId) => ({ + memberId, + connectionState: this._getConnectionState(memberId), + })); } /**@internal */ diff --git a/packages/core/src/media/stream/remote/base.ts b/packages/core/src/media/stream/remote/base.ts index 750bec3..f18a7f2 100644 --- a/packages/core/src/media/stream/remote/base.ts +++ b/packages/core/src/media/stream/remote/base.ts @@ -7,13 +7,27 @@ import { Stream, ContentType, WebRTCStats } from '../base'; export abstract class RemoteStreamBase implements Stream { readonly side = 'remote'; /** + * @deprecated + * @use Subscription.onConnectionStateChanged * @description [japanese] メディア通信の状態が変化した時に発火するイベント */ readonly onConnectionStateChanged = new Event(); + /**@internal */ + readonly _onConnectionStateChanged = new Event(); codec!: Codec; + private _connectionState: TransportConnectionState = 'new'; + + /**@internal */ + constructor(readonly id: string, readonly contentType: ContentType) { + this._onConnectionStateChanged.pipe(this.onConnectionStateChanged); + } /**@internal */ - constructor(readonly id: string, readonly contentType: ContentType) {} + _setConnectionState(state: TransportConnectionState) { + if (this._connectionState === state) return; + this._connectionState = state; + this._onConnectionStateChanged.emit(state); + } /**@internal */ _getTransport: () => Transport | undefined = () => undefined; @@ -46,7 +60,7 @@ export abstract class RemoteStreamBase implements Stream { } /**@internal */ _getConnectionState() { - return this._getTransport()?.connectionState ?? 'new'; + return this._connectionState; } /**@internal */ diff --git a/packages/core/src/member/localPerson/index.ts b/packages/core/src/member/localPerson/index.ts index 2d69180..7225eeb 100644 --- a/packages/core/src/member/localPerson/index.ts +++ b/packages/core/src/member/localPerson/index.ts @@ -491,7 +491,7 @@ export class LocalPersonImpl extends MemberImpl implements LocalPerson { ); const publication = this.channel._addPublication(published); - publication.stream = stream; + publication._setStream(stream); if (init.codecCapabilities?.length) { publication.setCodecCapabilities(init.codecCapabilities); @@ -886,7 +886,7 @@ export class LocalPersonImpl extends MemberImpl implements LocalPerson { clearInterval(this.ttlInterval); if (this._signaling) { - this._signaling.disconnect(); + this._signaling.close(); } this._getConnections().forEach((c) => c.close()); diff --git a/packages/core/src/plugin/internal/person/connection/index.ts b/packages/core/src/plugin/internal/person/connection/index.ts index 9e06e48..d2a5627 100644 --- a/packages/core/src/plugin/internal/person/connection/index.ts +++ b/packages/core/src/plugin/internal/person/connection/index.ts @@ -117,7 +117,7 @@ export class P2PConnection implements SkyWayConnection { } subscription.codec = stream.codec; - subscription.stream = stream; + subscription._setStream(stream); }); } diff --git a/packages/core/src/plugin/internal/person/connection/receiver.ts b/packages/core/src/plugin/internal/person/connection/receiver.ts index 1f1c77d..5310340 100644 --- a/packages/core/src/plugin/internal/person/connection/receiver.ts +++ b/packages/core/src/plugin/internal/person/connection/receiver.ts @@ -133,7 +133,7 @@ export class Receiver extends Peer { }) .disposer(this._disposer); - this.pc.ontrack = ({ track, transceiver }) => { + this.pc.ontrack = async ({ track, transceiver }) => { if (!transceiver.mid) { throw createError({ operationName: 'Receiver.pc.ontrack', @@ -184,7 +184,7 @@ export class Receiver extends Peer { }); }; - this.pc.ondatachannel = ({ channel }) => { + this.pc.ondatachannel = async ({ channel }) => { const { publicationId, streamId } = DataChannelNegotiationLabel.fromLabel( channel.label ); @@ -223,7 +223,12 @@ export class Receiver extends Peer { if (this._connectionState === state) { return; } - this._log.debug('onConnectionStateChanged', state); + this._log.debug( + 'onConnectionStateChanged', + this.id, + this._connectionState, + state + ); this._connectionState = state; this.onConnectionStateChanged.emit(state); } @@ -248,7 +253,7 @@ export class Receiver extends Peer { }); this.onConnectionStateChanged .add((state) => { - stream.onConnectionStateChanged.emit(state); + stream._setConnectionState(state); }) .disposer(this._disposer); } @@ -300,9 +305,12 @@ export class Receiver extends Peer { close() { this._log.debug('closed'); + this.unSetPeerConnectionListener(); - this._disposer.dispose(); this.pc.close(); + this._setConnectionState('disconnected'); + + this._disposer.dispose(); } add(subscription: SubscriptionImpl) { diff --git a/packages/core/src/plugin/internal/person/connection/sender.ts b/packages/core/src/plugin/internal/person/connection/sender.ts index 1580496..c69c9f3 100644 --- a/packages/core/src/plugin/internal/person/connection/sender.ts +++ b/packages/core/src/plugin/internal/person/connection/sender.ts @@ -119,7 +119,7 @@ export class Sender extends Peer { { const e = await this.waitForConnectionState( 'connected', - 5000 + context.config.rtcConfig.iceDisconnectBufferTimeout ).catch((e) => e as SkyWayError); if (e && this._connectionState !== 'reconnecting') { await this.restartIce(); @@ -142,7 +142,12 @@ export class Sender extends Peer { if (this._connectionState === state) { return; } - this._log.debug('onConnectionStateChanged', state); + this._log.debug( + 'onConnectionStateChanged', + this.id, + this._connectionState, + state + ); this._connectionState = state; this.onConnectionStateChanged.emit(state); } @@ -282,7 +287,10 @@ export class Sender extends Peer { return; } - e = await this.waitForConnectionState('connected', 5000).catch((e) => e); + e = await this.waitForConnectionState( + 'connected', + this._context.config.rtcConfig.iceDisconnectBufferTimeout + ).catch((e) => e); if (!e) { if (checkNeedEnd()) return; } @@ -521,10 +529,7 @@ export class Sender extends Peer { }); this.onConnectionStateChanged .add((state) => { - stream.onConnectionStateChanged.emit({ - remoteMember: this.endpoint, - state, - }); + stream._setConnectionState(this.endpoint, state); }) .disposer(this._disposer); } @@ -738,10 +743,13 @@ export class Sender extends Peer { close() { this._log.debug('closed'); + this.unSetPeerConnectionListener(); - this._disposer.dispose(); Object.values(this._unsubscribeStreamEnableChange).forEach((f) => f()); this.pc.close(); + this._setConnectionState('disconnected'); + + this._disposer.dispose(); } } diff --git a/packages/core/src/publication/index.ts b/packages/core/src/publication/index.ts index ea24427..bbb06aa 100644 --- a/packages/core/src/publication/index.ts +++ b/packages/core/src/publication/index.ts @@ -1,4 +1,4 @@ -import { Events, Logger } from '@skyway-sdk/common'; +import { EventDisposer, Events, Logger } from '@skyway-sdk/common'; import { Event } from '@skyway-sdk/common'; import { Encoding } from '@skyway-sdk/model'; @@ -15,7 +15,10 @@ import { LocalMediaStreamBase, LocalStream } from '../media/stream/local'; import { LocalAudioStream } from '../media/stream/local/audio'; import { LocalVideoStream } from '../media/stream/local/video'; import { Member } from '../member'; -import { RemoteMemberImplInterface } from '../member/remoteMember'; +import { + RemoteMember, + RemoteMemberImplInterface, +} from '../member/remoteMember'; import { TransportConnectionState } from '../plugin/interface'; import { Subscription } from '../subscription'; import { createError, createLogPayload, createWarnPayload } from '../util'; @@ -58,6 +61,13 @@ export interface Publication { onDisabled: Event; /** @description [japanese] stateが変化した時に発火するイベント */ onStateChanged: Event; + /** + * @description [japanese] メディア通信の状態が変化した時に発火するイベント + */ + onConnectionStateChanged: Event<{ + remoteMember: RemoteMember; + state: TransportConnectionState; + }>; //-------------------- @@ -129,7 +139,24 @@ export class PublicationImpl setEncodings(_encodings: Encoding[]) { this._encodings = _encodings; } - stream?: T; + private _stream?: T; + get stream(): T | undefined { + return this._stream; + } + /**@internal */ + _setStream(stream: LocalStream | undefined) { + this._stream = stream as T; + if (stream) { + stream._onConnectionStateChanged + .add((e) => { + log.debug('onConnectionStateChanged', this.id, e); + this.onConnectionStateChanged.emit(e); + }) + .disposer(this.streamEventDisposer); + } else { + this.streamEventDisposer.dispose(); + } + } /**@private */ readonly _channel: SkyWayChannelImpl; origin?: PublicationImpl; @@ -152,11 +179,16 @@ export class PublicationImpl readonly onEnabled = this._events.make(); readonly onDisabled = this._events.make(); readonly onStateChanged = this._events.make(); + readonly onConnectionStateChanged = new Event<{ + remoteMember: RemoteMember; + state: TransportConnectionState; + }>(); /**@private */ readonly _onEncodingsChanged = this._events.make(); /**@private */ readonly _onReplaceStream = this._events.make(); private readonly _onEnabled = this._events.make(); + private streamEventDisposer = new EventDisposer(); private _context: SkyWayContext; @@ -181,7 +213,9 @@ export class PublicationImpl this.origin = args.origin; this.setCodecCapabilities(args.codecCapabilities ?? []); this.setEncodings(normalizeEncodings(args.encodings ?? [])); - this.stream = args.stream; + if (args.stream) { + this._setStream(args.stream); + } this._state = args.isEnabled ? 'enabled' : 'disabled'; log.debug('publication spawned', this.toJSON()); @@ -221,9 +255,11 @@ export class PublicationImpl /**@private */ _unpublished() { this._state = 'canceled'; + if (this.stream) { this.stream._unpublished(); } + this.onCanceled.emit(); this.onStateChanged.emit(); @@ -249,7 +285,7 @@ export class PublicationImpl failed = true; f(e); }); - this.stream = undefined; + this._setStream(undefined); this.onCanceled .asPromise(this._context.config.rtcApi.timeout) .then(() => r()) @@ -510,7 +546,7 @@ export class PublicationImpl .catch((e) => e); stream.setEnabled(this.stream.isEnabled); - this.stream = stream as T; + this._setStream(stream as T); this._onReplaceStream.emit(stream); } diff --git a/packages/core/src/subscription/index.ts b/packages/core/src/subscription/index.ts index 3d23267..6893fa4 100644 --- a/packages/core/src/subscription/index.ts +++ b/packages/core/src/subscription/index.ts @@ -37,6 +37,10 @@ export interface Subscription< onCanceled: Event; /** @description [japanese] SubscriptionにStreamが紐つけられた時に発火するイベント */ onStreamAttached: Event; + /** + * @description [japanese] メディア通信の状態が変化した時に発火するイベント + */ + onConnectionStateChanged: Event; /** * @description [japanese] subscribeしているStreamの実体。 * ローカルでSubscribeしているSubscriptionでなければundefinedとなる @@ -98,6 +102,7 @@ export class SubscriptionImpl< private _stream?: T; readonly onCanceled = new Event(); readonly onStreamAttached = new Event(); + readonly onConnectionStateChanged = new Event(); /**@internal */ readonly _onChangeEncoding = new Event(); @@ -143,11 +148,14 @@ export class SubscriptionImpl< } } - set stream(stream: T | undefined) { + /**@internal */ + _setStream(stream: T) { this._stream = stream; - if (stream) { - this.onStreamAttached.emit(); - } + this.onStreamAttached.emit(); + stream._onConnectionStateChanged.add((e) => { + log.debug('onConnectionStateChanged', this.id, e); + this.onConnectionStateChanged.emit(e); + }); } get stream() { @@ -168,9 +176,9 @@ export class SubscriptionImpl< /**@private */ _canceled() { - this.stream = undefined; this._state = 'canceled'; this.onCanceled.emit(); + this._disposer.dispose(); } diff --git a/packages/core/src/util.ts b/packages/core/src/util.ts index 6ade6d9..2ee463d 100644 --- a/packages/core/src/util.ts +++ b/packages/core/src/util.ts @@ -98,6 +98,8 @@ export async function createLogPayload({ localCandidate, }; } + } + if (p.stream) { for (const { memberId, connectionState, diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index 43c8f46..878dfae 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -1 +1 @@ -export const PACKAGE_VERSION = '1.2.5'; +export const PACKAGE_VERSION = '1.3.0'; diff --git a/packages/room/karma.extra.js b/packages/room/karma.extra.js new file mode 100644 index 0000000..57805ff --- /dev/null +++ b/packages/room/karma.extra.js @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ + +const baseConfig = require('../../karma.base'); + +const testPath = 'tests/extra/**/*.ts'; + +module.exports = function (config) { + config.set({ + ...baseConfig, + files: ['src/**/*.ts', 'tests/common/**/*ts', testPath, '../../env.ts'], + preprocessors: { + 'src/**/*.ts': 'karma-typescript', + 'tests/common/**/*.ts': 'karma-typescript', + '../../env.ts': 'karma-typescript', + [testPath]: 'karma-typescript', + }, + logLevel: config.LOG_INFO, + }); +}; diff --git a/packages/room/karma.large.js b/packages/room/karma.large.js new file mode 100644 index 0000000..6bb53e7 --- /dev/null +++ b/packages/room/karma.large.js @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ + +const baseConfig = require('../../karma.base'); + +const testPath = 'tests/large/**/*.ts'; + +module.exports = function (config) { + config.set({ + ...baseConfig, + files: ['src/**/*.ts', 'tests/common/**/*ts', testPath, '../../env.ts'], + preprocessors: { + 'src/**/*.ts': 'karma-typescript', + 'tests/common/**/*.ts': 'karma-typescript', + '../../env.ts': 'karma-typescript', + [testPath]: 'karma-typescript', + }, + logLevel: config.LOG_INFO, + }); +}; diff --git a/packages/room/package.json b/packages/room/package.json index 358e787..2ca6b09 100644 --- a/packages/room/package.json +++ b/packages/room/package.json @@ -1,6 +1,6 @@ { "name": "@skyway-sdk/room", - "version": "1.2.5", + "version": "1.3.0", "description": "The official Next Generation JavaScript SDK for SkyWay", "homepage": "https://skyway.ntt.com/", "repository": { @@ -25,14 +25,18 @@ "doc": "npm run doc:html && npm run doc:md", "doc:html": "rm -rf docs/html && typedoc --excludePrivate --disableSources --excludeInternal --tsconfig ./tsconfig.build.json --out docs/html --plugin none ./src/index.ts ", "doc:md": "rm -rf docs/md && typedoc --excludePrivate --disableSources --excludeInternal --tsconfig ./tsconfig.build.json --out docs/md ./src/index.ts ", - "e2e": "jest && karma start ./karma.e2e.js --single-run --browsers chrome_headless_with_fake_device", - "e2e:dev": "karma start ./karma.e2e.js --browsers chrome_with_fake_device", - "e2e:firefox": "karma start ./karma.e2e.js --single-run --browsers FirefoxHeadlessAutoAllowGUM", - "e2e:safari": "karma start ./karma.e2e.js --single-run --browsers safari", + "e2e": "npm-run-all -p jest test-large test-extra", + "e2e:firefox": "karma start ./karma.large.js --single-run --browsers FirefoxHeadlessAutoAllowGUM && karma start ./karma.extra.js --single-run --browsers FirefoxHeadlessAutoAllowGUM", + "e2e:safari": "karma start ./karma.large.js --single-run --browsers safari && karma start ./karma.extra.js --single-run --browsers safari", "format": "eslint ./src --fix && eslint ./e2e --fix", - "lint": "eslint ./src --fix && eslint ./e2e --fix", "graph": "dependency-cruiser --include-only '^src' --output-type dot src | dot -T svg > docs/dependencygraph.svg", + "jest": "jest", + "lint": "eslint ./src --fix && eslint ./e2e --fix", "publish:npm": "npx can-npm-publish --verbose && npm run build && npm publish --access public", + "test-extra": "karma start ./karma.extra.js --single-run --browsers chrome_headless_with_fake_device", + "test-extra:dev": "karma start ./karma.extra.js --browsers chrome_with_fake_device", + "test-large": "karma start ./karma.large.js --single-run --browsers chrome_headless_with_fake_device", + "test-large:dev": "karma start ./karma.large.js --browsers chrome_with_fake_device", "type": "npm run type:main", "type:main": "tsc --noEmit -p ./tsconfig.json", "type:prod": "tsc --noEmit -p ./tsconfig.build.json", @@ -41,8 +45,8 @@ "watch:tsc": "tsc -p tsconfig.build.json -w" }, "dependencies": { - "@skyway-sdk/core": "^1.2.5", - "@skyway-sdk/sfu-bot": "^1.2.5", + "@skyway-sdk/core": "^1.3.0", + "@skyway-sdk/sfu-bot": "^1.3.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/packages/room/src/publication/index.ts b/packages/room/src/publication/index.ts index 3f04560..680bc5f 100644 --- a/packages/room/src/publication/index.ts +++ b/packages/room/src/publication/index.ts @@ -1,4 +1,4 @@ -import { EventDisposer } from '@skyway-sdk/common'; +import { EventDisposer, Logger } from '@skyway-sdk/common'; import { Event, Events } from '@skyway-sdk/common'; import { Codec, @@ -23,6 +23,7 @@ import { RoomSubscription } from '../subscription'; import { createError } from '../util'; const path = 'packages/room/src/publication/index.ts'; +const logger = new Logger(path); export interface RoomPublication { readonly id: string; @@ -67,6 +68,15 @@ export interface RoomPublication { readonly onDisabled: Event; /**@description [japanese] このPublicationの有効化状態が変化したときに発火するイベント */ readonly onStateChanged: Event; + /** + * @description [japanese] メディア通信の状態が変化した時に発火するイベント + * SFURoomの場合、remoteMemberはundefinedになる + * SFURoomの場合、memberがルームを離れたときのみ発火する + */ + readonly onConnectionStateChanged: Event<{ + remoteMember?: RoomMember; + state: TransportConnectionState; + }>; /** * @description [japanese] Metadataの更新 @@ -133,6 +143,10 @@ export class RoomPublicationImpl readonly onEnabled = this._events.make(); readonly onDisabled = this._events.make(); readonly onStateChanged = this._events.make(); + readonly onConnectionStateChanged = new Event<{ + remoteMember?: RoomMember; + state: TransportConnectionState; + }>(); constructor(public _publication: Publication, private _room: RoomImpl) { this.id = _publication.id; @@ -150,8 +164,7 @@ export class RoomPublicationImpl private _setEvents() { this._room.onStreamUnpublished.add((e) => { if (e.publication.id === this.id) { - this.onCanceled.emit(); - this._events.dispose(); + this._dispose(); } }); @@ -180,6 +193,21 @@ export class RoomPublicationImpl const publication = this._origin ?? this._publication; publication.onMetadataUpdated.pipe(this.onMetadataUpdated); } + + if (this._origin) { + this._origin.onConnectionStateChanged.add((e) => { + logger.debug('this._origin.onConnectionStateChanged', this.id, e); + this.onConnectionStateChanged.emit({ state: e.state }); + }); + } else { + this._publication.onConnectionStateChanged.add((e) => { + logger.debug('this._publication.onConnectionStateChanged', this.id, e); + this.onConnectionStateChanged.emit({ + state: e.state, + remoteMember: this._room._getMember(e.remoteMember.id), + }); + }); + } } get subscriptions() { @@ -262,8 +290,9 @@ export class RoomPublicationImpl this._preferredPublication.replaceStream(stream, options); }; - /**@internal */ - _dispose() { + private _dispose() { + this.onCanceled.emit(); + this._events.dispose(); this._disposer.dispose(); } diff --git a/packages/room/src/room/p2p.ts b/packages/room/src/room/p2p.ts index 8ed2e01..aab3856 100644 --- a/packages/room/src/room/p2p.ts +++ b/packages/room/src/room/p2p.ts @@ -110,7 +110,6 @@ export class P2PRoomImpl extends RoomImpl implements P2PRoom { private _handleOnStreamUnpublish(p: Publication) { const publication = this._getPublication(p.id); delete this._publications[p.id]; - publication._dispose(); this.onStreamUnpublished.emit({ publication }); this.onPublicationListChanged.emit({}); diff --git a/packages/room/src/room/sfu.ts b/packages/room/src/room/sfu.ts index 68f264d..56003f5 100644 --- a/packages/room/src/room/sfu.ts +++ b/packages/room/src/room/sfu.ts @@ -148,7 +148,6 @@ export class SfuRoomImpl extends RoomImpl implements SfuRoom { const publication = this._getPublication(p.id); delete this._publications[p.id]; - publication._dispose(); this.onStreamUnpublished.emit({ publication }); this.onPublicationListChanged.emit({}); diff --git a/packages/room/src/subscription/index.ts b/packages/room/src/subscription/index.ts index 7c9965e..b77ec62 100644 --- a/packages/room/src/subscription/index.ts +++ b/packages/room/src/subscription/index.ts @@ -34,6 +34,10 @@ export interface RoomSubscription< readonly onStreamAttached: Event; /**@description [japanese] このSubscriptionがUnsubscribeされた時に発火する */ readonly onCanceled: Event; + /** + * @description [japanese] メディア通信の状態が変化した時に発火するイベント + */ + onConnectionStateChanged: Event; readonly subscriber: RemoteRoomMember; /** * @description [japanese] subscribeしているStreamの実体。 @@ -85,6 +89,7 @@ export class RoomSubscriptionImpl< readonly onStreamAttached = new Event(); readonly onCanceled = new Event(); + readonly onConnectionStateChanged = new Event(); constructor( /**@private */ @@ -98,6 +103,10 @@ export class RoomSubscriptionImpl< _subscription.onStreamAttached.pipe(this.onStreamAttached); _subscription.onCanceled.pipe(this.onCanceled); + _subscription.onConnectionStateChanged.add((state) => { + log.debug('_subscription.onConnectionStateChanged', this.id, state); + this.onConnectionStateChanged.emit(state); + }); } get stream() { diff --git a/packages/room/src/version.ts b/packages/room/src/version.ts index 43c8f46..878dfae 100644 --- a/packages/room/src/version.ts +++ b/packages/room/src/version.ts @@ -1 +1 @@ -export const PACKAGE_VERSION = '1.2.5'; +export const PACKAGE_VERSION = '1.3.0'; diff --git a/packages/sfu-bot/bundle.mjs b/packages/sfu-bot/bundle.mjs index d1691ad..335b038 100644 --- a/packages/sfu-bot/bundle.mjs +++ b/packages/sfu-bot/bundle.mjs @@ -10,7 +10,7 @@ fs.writeFile( `export const PACKAGE_VERSION = '${pkg.version}';\n` ); -const globalName = 'skyway_sfu_client'; +const globalName = 'skyway_sfu_bot'; const dist = 'dist'; await $`npm run compile`; diff --git a/packages/sfu-bot/package.json b/packages/sfu-bot/package.json index a054435..702cced 100644 --- a/packages/sfu-bot/package.json +++ b/packages/sfu-bot/package.json @@ -1,6 +1,6 @@ { "name": "@skyway-sdk/sfu-bot", - "version": "1.2.5", + "version": "1.3.0", "description": "The official Next Generation JavaScript SDK for SkyWay", "homepage": "https://skyway.ntt.com/", "repository": { @@ -41,7 +41,7 @@ "watch:tsc": "tsc -p tsconfig.build.json -w" }, "dependencies": { - "@skyway-sdk/core": "^1.2.5", + "@skyway-sdk/core": "^1.3.0", "@skyway-sdk/sfu-api-client": "^1.2.3", "lodash": "4.17.21", "mediasoup-client": "3.6.82" diff --git a/packages/sfu-bot/src/connection/index.ts b/packages/sfu-bot/src/connection/index.ts index 27d5deb..ea25847 100644 --- a/packages/sfu-bot/src/connection/index.ts +++ b/packages/sfu-bot/src/connection/index.ts @@ -107,7 +107,7 @@ export class SFUConnection implements SkyWayConnection { log.elapsed(ts, '[end] _startSubscribing consume'); subscription.codec = codec; - subscription.stream = stream; + subscription._setStream(stream); } /**@internal */ diff --git a/packages/sfu-bot/src/connection/receiver.ts b/packages/sfu-bot/src/connection/receiver.ts index e3da76b..becdee3 100644 --- a/packages/sfu-bot/src/connection/receiver.ts +++ b/packages/sfu-bot/src/connection/receiver.ts @@ -194,9 +194,10 @@ export class Receiver { this._disposer.push(() => { stream._getTransport = () => undefined; }); - transport.onConnectionStateChanged - .add((state) => stream.onConnectionStateChanged.emit(state)) - .disposer(this._disposer); + transport.onConnectionStateChanged.add((state) => { + log.debug('transport connection state changed', transport.id, state); + stream._setConnectionState(state); + }); } unconsume() { diff --git a/packages/sfu-bot/src/connection/sender.ts b/packages/sfu-bot/src/connection/sender.ts index 46a220f..c60f558 100644 --- a/packages/sfu-bot/src/connection/sender.ts +++ b/packages/sfu-bot/src/connection/sender.ts @@ -1,4 +1,4 @@ -import { EventDisposer, Logger } from '@skyway-sdk/common'; +import { Event, EventDisposer, Logger } from '@skyway-sdk/common'; import { createError, createLogPayload, @@ -14,6 +14,7 @@ import { SkyWayContext, statsToArray, SubscriptionImpl, + TransportConnectionState, uuidV4, waitForLocalStats, } from '@skyway-sdk/core'; @@ -46,6 +47,9 @@ export class Sender { private _ackTransport?: SfuTransport; private _disposer = new EventDisposer(); private _unsubscribeStreamEnableChange?: () => void; + private _connectionState: TransportConnectionState = 'new'; + private readonly onConnectionStateChanged = + new Event(); closed = false; constructor( @@ -59,11 +63,24 @@ export class Sender { private _context: SkyWayContext ) {} + private _setConnectionState(state: TransportConnectionState) { + if (this._connectionState === state) { + return; + } + log.debug('_setConnectionState', { + state, + forwardingId: this.forwardingId, + }); + this._connectionState = state; + this.onConnectionStateChanged.emit(state); + } + toJSON() { return { forwarding: this.forwarding, broadcasterTransport: this._broadcasterTransport, ackTransport: this._ackTransport, + _connectionState: this._connectionState, }; } @@ -89,6 +106,16 @@ export class Sender { channel: this.channel, }); } + this.onConnectionStateChanged + .add((state) => { + log.debug( + 'transport connection state changed', + this._broadcasterTransport?.id, + state + ); + stream._setConnectionState(this._bot, state); + }) + .disposer(this._disposer); log.debug('[start] Sender startForwarding', { botId: this._bot.id, @@ -147,6 +174,13 @@ export class Sender { }); } + this._broadcasterTransport.onConnectionStateChanged + .add((state) => { + this._setConnectionState(state); + }) + .disposer(this._disposer); + this._setConnectionState(this._broadcasterTransport.connectionState); + if (ackTransportOptions) { this._ackTransport = this._transportRepository.createTransport( this._localPerson.id, @@ -559,18 +593,6 @@ export class Sender { delete stream._getTransportCallbacks[this._bot.id]; delete stream._getStatsCallbacks[this._bot.id]; }); - transport.onConnectionStateChanged - .add((state) => { - stream.onConnectionStateChanged.emit({ - remoteMember: this._bot, - state, - }); - }) - .disposer(this._disposer); - stream.onConnectionStateChanged.emit({ - remoteMember: this._bot, - state: transport.connectionState, - }); } private _handleMessage(consumer: DataConsumer, producer: DataProducer) { @@ -677,10 +699,12 @@ export class Sender { close() { this.closed = true; - this._disposer.dispose(); if (this._unsubscribeStreamEnableChange) { this._unsubscribeStreamEnableChange(); } + this._setConnectionState('disconnected'); + + this._disposer.dispose(); } get pc() { diff --git a/packages/sfu-bot/src/connection/transport/transport.ts b/packages/sfu-bot/src/connection/transport/transport.ts index 0deb387..eddbe54 100644 --- a/packages/sfu-bot/src/connection/transport/transport.ts +++ b/packages/sfu-bot/src/connection/transport/transport.ts @@ -123,7 +123,7 @@ export class SfuTransport { } const e = await this._waitForMsConnectionState( 'connected', - this._options.peerConnectionJitterTimeout + _context.config.rtcConfig.iceDisconnectBufferTimeout ).catch((e) => e as SkyWayError); if ( e && @@ -157,19 +157,21 @@ export class SfuTransport { } close() { + log.debug('close', this.id); // suppress firefox [RTCPeerConnection is gone] Exception if ((this.pc as any)?.peerIdentity) { (this.pc as any).peerIdentity.catch(() => {}); } this.msTransport.close(); + this._setConnectionState('disconnected'); } private _setConnectionState(state: TransportConnectionState) { if (this._connectionState === state) { return; } + log.debug('onConnectionStateChanged', this._connectionState, state, this); this._connectionState = state; - log.debug('onConnectionStateChanged', this); this.onConnectionStateChanged.emit(state); } @@ -266,7 +268,7 @@ export class SfuTransport { e = await this._waitForMsConnectionState( 'connected', - this._options.peerConnectionJitterTimeout + this._context.config.rtcConfig.iceDisconnectBufferTimeout ).catch((e) => e); if (!e && checkNeedEnd()) { return iceParameters; diff --git a/packages/sfu-bot/src/option.ts b/packages/sfu-bot/src/option.ts index 9f3722a..dae97ab 100644 --- a/packages/sfu-bot/src/option.ts +++ b/packages/sfu-bot/src/option.ts @@ -7,13 +7,11 @@ export type SfuBotPluginOptions = Omit & { endpointTimeout: number; ackTimeout: number; disableRestartIce: boolean; - peerConnectionJitterTimeout: number; }; export const defaultSfuBotPluginOptions: SfuBotPluginOptions = { ...defaultSfuApiOptions, endpointTimeout: 30_000, ackTimeout: 10_000, - peerConnectionJitterTimeout: 5_000, disableRestartIce: false, }; diff --git a/packages/sfu-bot/src/version.ts b/packages/sfu-bot/src/version.ts index 43c8f46..878dfae 100644 --- a/packages/sfu-bot/src/version.ts +++ b/packages/sfu-bot/src/version.ts @@ -1 +1 @@ -export const PACKAGE_VERSION = '1.2.5'; +export const PACKAGE_VERSION = '1.3.0';