UNPKG

@skyway-sdk/core

Version:

The official Next Generation JavaScript SDK for SkyWay

474 lines 22.1 kB
"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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; 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.Receiver = void 0; const common_1 = require("@skyway-sdk/common"); const sdpTransform = __importStar(require("sdp-transform")); const uuid_1 = require("uuid"); const errors_1 = require("../../../../errors"); const factory_1 = require("../../../../media/stream/remote/factory"); const util_1 = require("../../../../util"); const util_2 = require("../util"); const datachannel_1 = require("./datachannel"); const peer_1 = require("./peer"); const log = new common_1.Logger('packages/core/src/plugin/internal/person/connection/receiver.ts'); class Receiver extends peer_1.Peer { constructor(context, iceManager, signaling, analytics, localPerson, endpoint) { super(context, iceManager, signaling, analytics, localPerson, endpoint, 'receiver'); this.id = (0, uuid_1.v4)(); this.onConnectionStateChanged = new common_1.Event(); this.onStreamAdded = new common_1.Event(); this.onError = new common_1.Event(); this._connectionState = 'new'; this._publicationInfo = {}; this.streams = {}; this._subscriptions = {}; this._promiseQueue = new common_1.PromiseQueue(); this._disposer = new common_1.EventDisposer(); this._log = log.createBlock({ localPersonId: this.localPerson.id, id: this.id, }); this._log.debug('spawned'); this.signaling.onMessage .add(({ src, data }) => __awaiter(this, void 0, void 0, function* () { if (!(src.id === endpoint.id && src.name === endpoint.name)) return; const message = data; switch (message.kind) { case 'senderProduceMessage': { this._promiseQueue .push(() => this._handleSenderProduce(message.payload)) .catch((err) => this._log.error('handle senderProduceMessage failed', err, { localPersonId: this.localPerson.id, endpointId: this.endpoint.id, })); } break; case 'senderUnproduceMessage': { this._promiseQueue .push(() => this._handleSenderUnproduce(message.payload)) .catch((err) => this._log.error('handle handleSenderUnproduce', err, { localPersonId: this.localPerson.id, endpointId: this.endpoint.id, })); } break; case 'senderRestartIceMessage': { this._promiseQueue .push(() => this._handleSenderRestartIce(message.payload)) .catch((err) => this._log.error('_handleSenderRestartIce', err, { localPersonId: this.localPerson.id, endpointId: this.endpoint.id, })); } break; case 'iceCandidateMessage': { const { role, candidate } = message.payload; if (role === 'sender') { yield this.handleCandidate(candidate); } } break; } })) .disposer(this._disposer); this.pc.ontrack = ({ track, transceiver }) => __awaiter(this, void 0, void 0, function* () { if (!transceiver.mid) { throw (0, util_1.createError)({ operationName: 'Receiver.pc.ontrack', info: Object.assign(Object.assign({}, errors_1.errors.missingProperty), { detail: 'mid missing' }), path: log.prefix, context: this._context, channel: this.localPerson.channel, }); } const info = Object.values(this._publicationInfo).find((i) => { var _a; return i.mid === ((_a = transceiver.mid) === null || _a === void 0 ? void 0 : _a.toString()); }); if (!info) { const error = (0, util_1.createError)({ operationName: 'Receiver.pc.ontrack', info: Object.assign(Object.assign({}, errors_1.errors.notFound), { detail: 'publicationInfo not found' }), path: log.prefix, context: this._context, channel: localPerson.channel, payload: { endpointId: this.endpoint.id, publicationInfo: this._publicationInfo, mid: transceiver.mid, }, }); this.onError.emit(error); this._log.error(error); return; } const sdpObject = sdpTransform.parse(this.pc.remoteDescription.sdp); const codec = this._getCodecFromSdp(sdpObject, transceiver, track.kind); const stream = (0, factory_1.createRemoteStream)(info.streamId, track, codec); stream.codec = codec; this._setupTransportAccessForStream(stream); this.streams[info.publicationId] = stream; this._log.debug('MediaStreamTrack added', info, track, codec); this.onStreamAdded.emit({ publicationId: info.publicationId, stream, }); }); this.pc.ondatachannel = ({ channel }) => __awaiter(this, void 0, void 0, function* () { const { publicationId, streamId } = datachannel_1.DataChannelNegotiationLabel.fromLabel(channel.label); const codec = { mimeType: 'datachannel' }; const stream = (0, factory_1.createRemoteStream)(streamId, channel, codec); this._setupTransportAccessForStream(stream); this.streams[publicationId] = stream; this._log.debug('DataChannel added', publicationId, channel, codec); this.onStreamAdded.emit({ publicationId, stream, }); }); this.onPeerConnectionStateChanged .add((state) => { switch (state) { case 'connecting': case 'connected': this._setConnectionState(state); break; case 'failed': case 'closed': this._setConnectionState('disconnected'); break; } }) .disposer(this._disposer); } _setConnectionState(state) { if (this._connectionState === state) { return; } this._log.debug('onConnectionStateChanged', this.id, this._connectionState, state); this._connectionState = state; this.onConnectionStateChanged.emit(state); } _setupTransportAccessForStream(stream) { stream._getTransport = () => ({ rtcPeerConnection: this.pc, connectionState: (0, util_2.convertConnectionState)(this.pc.connectionState), }); stream._getStats = () => __awaiter(this, void 0, void 0, function* () { if (stream.contentType === 'data') { const stats = yield this.pc.getStats(); const arr = (0, util_1.statsToArray)(stats); return arr; } const stats = yield this.pc.getStats(stream.track); const arr = (0, util_1.statsToArray)(stats); return arr; }); this._disposer.push(() => { stream._getTransport = () => undefined; }); this.onConnectionStateChanged .add((state) => { stream._setConnectionState(state); if (this.localPerson._analytics && !this.localPerson._analytics.isClosed()) { void this.localPerson._analytics.client.sendRtcPeerConnectionEventReport({ rtcPeerConnectionId: this.rtcPeerConnectionId, type: 'skywayConnectionStateChange', data: { skywayConnectionState: state, }, createdAt: Date.now(), }); } }) .disposer(this._disposer); } _getCodecFromSdp(sdpObject, transceiver, kind) { var _a, _b; const media = sdpObject.media.find( // sdpTransformのmidは実際はnumber (m) => { var _a, _b; return ((_a = m.mid) === null || _a === void 0 ? void 0 : _a.toString()) === ((_b = transceiver.mid) === null || _b === void 0 ? void 0 : _b.toString()); }); if (!media) { throw (0, util_1.createError)({ operationName: 'Receiver._getCodecFromSdp', info: Object.assign(Object.assign({}, errors_1.errors.notFound), { detail: 'm-line not exist' }), path: log.prefix, context: this._context, channel: this.localPerson.channel, }); } const codecPT = (_b = (_a = media.payloads) === null || _a === void 0 ? void 0 : _a.toString()) === null || _b === void 0 ? void 0 : _b.split(' ')[0]; const rtp = media.rtp.find((r) => r.payload.toString() === codecPT); const mimeType = `${kind}/${rtp.codec}`.toLowerCase(); let parameters = {}; const fmtp = media.fmtp.find((f) => f.payload.toString() === codecPT); if (fmtp === null || fmtp === void 0 ? void 0 : fmtp.config) { parameters = (0, util_1.fmtpConfigParser)(fmtp.config); } const codec = { mimeType, parameters }; return codec; } get hasMedia() { const count = Object.values(this.streams).length; this._log.debug('hasMedia', { count }); if (count > 0) { return true; } return false; } close() { this._log.debug('closed'); this.unSetPeerConnectionListener(); this.pc.close(); this._setConnectionState('disconnected'); this._disposer.dispose(); } add(subscription) { this._subscriptions[subscription.id] = subscription; } remove(subscriptionId) { const subscription = this._subscriptions[subscriptionId]; if (!subscription) return; delete this._subscriptions[subscription.id]; const publicationId = subscription.publication.id; const stream = this.streams[publicationId]; if (!stream) return; delete this.streams[publicationId]; } /**@throws {SkyWayError} */ _validateRemoteOffer(sdp) { const sdpObject = sdpTransform.parse(sdp); this._log.debug('_validateRemoteOffer', { sdpObject }); for (const sdpMediaLine of sdpObject.media) { if (sdpMediaLine.direction === 'inactive') { continue; } const exist = Object.values(this._publicationInfo).find((info) => { var _a; return ((_a = sdpMediaLine.mid) === null || _a === void 0 ? void 0 : _a.toString()) === info.mid; }); if (!exist) { const error = (0, util_1.createError)({ operationName: 'Receiver._validateRemoteOffer', info: Object.assign(Object.assign({}, errors_1.errors.notFound), { detail: 'mismatch between sdp and state' }), path: log.prefix, context: this._context, channel: this.localPerson.channel, payload: { sdpMedia: sdpObject.media, sdpMediaLine, info: this._publicationInfo, }, }); this.onError.emit(error); throw error; } } } get isWrongSignalingState() { return ((this.pc.signalingState === 'have-local-offer' && this.pc.remoteDescription) || this.pc.signalingState === 'have-remote-offer'); } /**@throws {SkyWayError} */ _handleSenderProduce({ sdp, publicationId, info, }) { return __awaiter(this, void 0, void 0, function* () { if (this.pc.signalingState === 'closed') { return; } if (this.pc.signalingState !== 'stable') { if (this.isWrongSignalingState) { this._log.warn('_handleSenderProduce wait for be stable', (0, util_1.createWarnPayload)({ operationName: 'Receiver._handleSenderProduce', channel: this.localPerson.channel, detail: '_handleSenderProduce wait for be stable', payload: { signalingState: this.pc.signalingState }, })); yield this.waitForSignalingState('stable'); yield this._handleSenderProduce({ sdp, publicationId, info, }); return; } throw (0, util_1.createError)({ operationName: 'Receiver._handleSenderProduce', context: this._context, channel: this.localPerson.channel, info: Object.assign(Object.assign({}, errors_1.errors.internal), { detail: 'wrong signalingState' }), payload: { signalingState: this.pc.signalingState }, path: log.prefix, }); } this._log.debug('_handleSenderProduce', { info, publicationId, publicationInfo: Object.values(this._publicationInfo), }); this._publicationInfo[info.publicationId] = info; this._validateRemoteOffer(sdp.sdp); yield this.sendAnswer(sdp); yield this.resolveCandidates(); }); } /**@throws {SkyWayError} */ _handleSenderUnproduce({ sdp, publicationId, }) { return __awaiter(this, void 0, void 0, function* () { if (this.pc.signalingState === 'closed') { this._log.warn('signalingState closed', (0, util_1.createWarnPayload)({ channel: this.localPerson.channel, detail: 'signalingState closed', operationName: 'Receiver._handleSenderUnproduce', })); return; } this._log.debug('<handleSenderUnproduce> start', { sdp, publicationId }); if (this.pc.signalingState !== 'stable') { if (this.isWrongSignalingState) { this._log.warn('signalingState is not stable', (0, util_1.createWarnPayload)({ channel: this.localPerson.channel, detail: 'signalingState is not stable', operationName: 'Receiver._handleSenderUnproduce', payload: { signalingState: this.pc.signalingState }, })); yield this.waitForSignalingState('stable'); yield this._handleSenderUnproduce({ sdp, publicationId, }); return; } throw (0, util_1.createError)({ operationName: 'Receiver._handleSenderProduce', context: this._context, channel: this.localPerson.channel, info: Object.assign(Object.assign({}, errors_1.errors.internal), { detail: 'wrong signalingState' }), payload: { signalingState: this.pc.signalingState }, path: log.prefix, }); } delete this._publicationInfo[publicationId]; yield this.sendAnswer(sdp); yield this.resolveCandidates(); this._log.debug('<handleSenderUnproduce> end', { publicationId }); }); } /**@throws {SkyWayError} */ _handleSenderRestartIce({ sdp, }) { return __awaiter(this, void 0, void 0, function* () { if (this.pc.signalingState === 'closed') { return; } if (this.pc.signalingState !== 'stable') { if (this.isWrongSignalingState) { this._log.warn('signalingState is not stable', (0, util_1.createWarnPayload)({ channel: this.localPerson.channel, detail: 'signalingState is not stable', operationName: 'Receiver._handleSenderRestartIce', payload: { signalingState: this.pc.signalingState }, })); yield this.waitForSignalingState('stable'); yield this._handleSenderRestartIce({ sdp }); return; } throw (0, util_1.createError)({ operationName: 'Receiver._handleSenderRestartIce', context: this._context, channel: this.localPerson.channel, info: Object.assign(Object.assign({}, errors_1.errors.internal), { detail: 'wrong signalingState' }), payload: { signalingState: this.pc.signalingState }, path: log.prefix, }); } this._setConnectionState('reconnecting'); yield this.sendAnswer(sdp); yield this.resolveCandidates(); if (this.pc.connectionState === 'connected') { this._setConnectionState('connected'); } }); } sendAnswer(sdp) { return __awaiter(this, void 0, void 0, function* () { this._log.debug(`[receiver] start: sendAnswer`); yield this.pc.setRemoteDescription(sdp); const answer = yield this.pc.createAnswer(); if (this.localPerson._analytics && !this.localPerson._analytics.isClosed()) { // 再送時に他の処理をブロックしないためにawaitしない void this.localPerson._analytics.client.sendRtcPeerConnectionEventReport({ rtcPeerConnectionId: this.rtcPeerConnectionId, type: 'answer', data: { answer: JSON.stringify(answer), }, createdAt: Date.now(), }); } const offerObject = sdpTransform.parse(this.pc.remoteDescription.sdp); const answerObject = sdpTransform.parse(answer.sdp); // fmtpの一部の設定(stereo)はremote側でも設定しないと効果を発揮しない offerObject.media.forEach((offerMedia, i) => { const answerMedia = answerObject.media[i]; answerMedia.fmtp = (0, common_1.deepCopy)(answerMedia.fmtp).map((answerFmtp) => { const offerFmtp = offerMedia.fmtp.find((f) => f.payload === answerFmtp.payload); if (offerFmtp) { return offerFmtp; } return answerFmtp; }); }); const munged = sdpTransform.write(answerObject); yield this.pc.setLocalDescription({ type: 'answer', sdp: munged }); const message = { kind: 'receiverAnswerMessage', payload: { sdp: this.pc.localDescription }, }; yield this.signaling.send(this.endpoint, message).catch((e) => this._log.error('failed to send answer', e, { localPersonId: this.localPerson.id, endpointId: this.endpoint.id, })); this._log.debug(`[receiver] end: sendAnswer`); }); } get subscriptions() { return this._subscriptions; } } exports.Receiver = Receiver; //# sourceMappingURL=receiver.js.map