UNPKG

@skyway-sdk/core

Version:

The official Next Generation JavaScript SDK for SkyWay

857 lines 40.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()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.applyCodecCapabilities = exports.Sender = void 0; const common_1 = require("@skyway-sdk/common"); const isEqual_1 = __importDefault(require("lodash/isEqual")); const sdpTransform = __importStar(require("sdp-transform")); const uuid_1 = require("uuid"); const errors_1 = require("../../../../errors"); 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/sender.ts'); class Sender extends peer_1.Peer { constructor(context, iceManager, signaling, analytics, localPerson, endpoint) { super(context, iceManager, signaling, analytics, localPerson, endpoint, 'sender'); this.id = (0, uuid_1.v4)(); this.onConnectionStateChanged = new common_1.Event(); this.publications = {}; this.transceivers = {}; this.datachannels = {}; this._pendingPublications = []; this._isNegotiating = false; this.promiseQueue = new common_1.PromiseQueue(); this._disposer = new common_1.EventDisposer(); this._ms = new MediaStream(); this._backoffIceRestarted = new common_1.BackOff({ times: 8, interval: 100, jitter: 100, }); this._connectionState = 'new'; this._log = log.createBlock({ localPersonId: this.localPerson.id, id: this.id, }); this._unsubscribeStreamEnableChange = {}; this._cleanupStreamCallbacks = {}; this._sendDataQueue = new common_1.PromiseQueue(); /**@throws */ this.restartIce = () => __awaiter(this, void 0, void 0, function* () { if (this._backoffIceRestarted.exceeded) { this._log.error((0, util_1.createError)({ operationName: 'Sender.restartIce', context: this._context, channel: this.localPerson.channel, info: Object.assign(Object.assign({}, errors_1.errors.internal), { detail: 'restartIce limit exceeded' }), path: log.prefix, })); this._setConnectionState('disconnected'); return; } this._log.warn('[start] restartIce', (0, util_1.createWarnPayload)({ operationName: 'Sender.restartIce', detail: 'start restartIce', channel: this.localPerson.channel, payload: { count: this._backoffIceRestarted.count }, })); const checkNeedEnd = () => { if (this.endpoint.state === 'left') { this._log.warn('endpointMemberLeft', (0, util_1.createWarnPayload)({ operationName: 'restartIce', detail: 'endpointMemberLeft', channel: this.localPerson.channel, payload: { endpointId: this.endpoint.id }, })); this._setConnectionState('disconnected'); return true; } if (this.pc.connectionState === 'connected') { this._log.warn('[end] restartIce', (0, util_1.createWarnPayload)({ operationName: 'restartIce', detail: 'reconnected', channel: this.localPerson.channel, payload: { count: this._backoffIceRestarted.count }, })); this._backoffIceRestarted.reset(); this._setConnectionState('connected'); if (this.localPerson._analytics && !this.localPerson._analytics.isClosed()) { // 再送時に他の処理をブロックしないためにawaitしない void this.localPerson._analytics.client.sendRtcPeerConnectionEventReport({ rtcPeerConnectionId: this.id, type: 'restartIce', data: undefined, createdAt: Date.now(), }); } return true; } }; this._setConnectionState('reconnecting'); yield this._backoffIceRestarted.wait(); if (checkNeedEnd()) return; let e = yield this._iceManager.updateIceParams().catch((e) => e); if (e) { this._log.warn('[failed] restartIce', (0, util_1.createWarnPayload)({ operationName: 'restartIce', detail: 'update IceParams failed', channel: this.localPerson.channel, payload: { count: this._backoffIceRestarted.count }, }), e); yield this.restartIce(); return; } if (this.pc.setConfiguration) { this.pc.setConfiguration(Object.assign(Object.assign({}, this.pc.getConfiguration()), { iceServers: this._iceManager.iceServers })); this._log.debug('<restartIce> setConfiguration', { iceServers: this._iceManager.iceServers, }); } if (checkNeedEnd()) return; if (this.signaling.connectionState !== 'connected') { this._log.warn('<restartIce> reconnect signaling service', (0, util_1.createWarnPayload)({ operationName: 'restartIce', detail: 'reconnect signaling service', channel: this.localPerson.channel, payload: { count: this._backoffIceRestarted.count }, })); e = yield this.signaling.onConnectionStateChanged .watch((s) => s === 'connected', 10000) .catch((e) => e) .then(() => { }); if (e instanceof common_1.SkyWayError) { yield this.restartIce(); return; } if (checkNeedEnd()) return; } const offer = yield this.pc.createOffer({ iceRestart: true }); if (this.localPerson._analytics && !this.localPerson._analytics.isClosed()) { // 再送時に他の処理をブロックしないためにawaitしない void this.localPerson._analytics.client.sendRtcPeerConnectionEventReport({ rtcPeerConnectionId: this.rtcPeerConnectionId, type: 'offer', data: { offer: JSON.stringify(offer), }, createdAt: Date.now(), }); } yield this.pc.setLocalDescription(offer); const message = { kind: 'senderRestartIceMessage', payload: { sdp: this.pc.localDescription }, }; e = yield this.signaling .send(this.endpoint, message, 10000) .catch((e) => e); if (e) { this._log.warn('<restartIce> [failed]', (0, util_1.createWarnPayload)({ operationName: 'restartIce', detail: 'timeout send signaling message', channel: this.localPerson.channel, payload: { count: this._backoffIceRestarted.count }, }), e); yield this.restartIce(); return; } e = yield this.waitForConnectionState('connected', this._context.config.rtcConfig.iceDisconnectBufferTimeout).catch((e) => e); if (!e) { if (checkNeedEnd()) return; } yield this.restartIce(); }); this._log.debug('spawned'); this._endpoint = endpoint; 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 'receiverAnswerMessage': { this.promiseQueue .push(() => this._handleReceiverAnswer(message.payload)) .catch((err) => this._log.error('handle receiverAnswerMessage', { localPersonId: this.localPerson.id, endpointId: this.endpoint.id, err, })); } break; case 'iceCandidateMessage': { const { role, candidate } = message.payload; if (role === 'receiver') { yield this.handleCandidate(candidate); } } break; } })) .disposer(this._disposer); this.onPeerConnectionStateChanged .add((state) => __awaiter(this, void 0, void 0, function* () { try { log.debug('onPeerConnectionStateChanged', { state }); switch (state) { case 'disconnected': case 'failed': { const e = yield this.waitForConnectionState('connected', context.config.rtcConfig.iceDisconnectBufferTimeout).catch((e) => e); if (e && this._connectionState !== 'reconnecting') { yield this.restartIce(); } } break; case 'connecting': case 'connected': this._setConnectionState(state); break; case 'closed': this._setConnectionState('disconnected'); break; } } catch (error) { log.error('onPeerConnectionStateChanged', error, this.id); } })) .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); } get hasMedia() { const count = Object.keys(this.publications).length; this._log.debug('hasMedia', { count }); if (count > 0) { return true; } return false; } _getMid(publication, sdpObject) { if (publication.contentType === 'data') { const media = sdpObject.media.find((m) => m.type === 'application'); if ((media === null || media === void 0 ? void 0 : media.mid) === undefined) { throw (0, util_1.createError)({ operationName: 'Sender._getMid', info: Object.assign(Object.assign({}, errors_1.errors.missingProperty), { detail: 'datachannel mid undefined' }), path: log.prefix, context: this._context, channel: this.localPerson.channel, }); } return media.mid.toString(); } else { const transceiver = this.transceivers[publication.id]; const mid = transceiver.mid; if (mid === null) { throw (0, util_1.createError)({ operationName: 'Sender._getMid', info: Object.assign(Object.assign({}, errors_1.errors.missingProperty), { detail: 'media mid undefined' }), path: log.prefix, context: this._context, channel: this.localPerson.channel, }); } return mid.toString(); } } _listenStreamEnableChange(stream, publicationId) { if (this._unsubscribeStreamEnableChange[publicationId]) { this._unsubscribeStreamEnableChange[publicationId](); } const { removeListener } = stream._onEnableChanged.add((track) => __awaiter(this, void 0, void 0, function* () { yield this._replaceTrack(publicationId, track).catch((e) => { log.warn((0, util_1.createWarnPayload)({ member: this.localPerson, detail: '_replaceTrack failed', operationName: 'Sender._listenStreamEnableChange', payload: e, })); }); })); this._unsubscribeStreamEnableChange[publicationId] = removeListener; } /**@throws {@link SkyWayError} */ add(publication) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { if (this._isNegotiating || this.pc.signalingState !== 'stable') { this._pendingPublications.push(publication); this._log.debug('<add> isNegotiating', { publication, isNegotiating: this._isNegotiating, signalingState: this.pc.signalingState, pendingPublications: this._pendingPublications.length, }); return; } this._isNegotiating = true; this._log.debug('<add> add publication', { publication }); this.publications[publication.id] = publication; const stream = publication.stream; if (!stream) { throw (0, util_1.createError)({ operationName: 'Sender.add', info: Object.assign(Object.assign({}, errors_1.errors.missingProperty), { detail: '<add> stream not found' }), path: log.prefix, context: this._context, channel: this.localPerson.channel, }); } this._cleanupStreamCallbacks[stream.id] = this._setupTransportAccessForStream(stream); if (stream.contentType === 'data') { const dc = this.pc.createDataChannel(new datachannel_1.DataChannelNegotiationLabel(publication.id, stream.id).toLabel(), stream.options); const dataStreamSubscriber = { id: this._endpoint.id, name: this._endpoint.name, }; dc.onopen = () => { stream.onWritable.emit(dataStreamSubscriber); }; dc.onclose = () => { stream.onUnwritable.emit(dataStreamSubscriber); }; dc.onerror = (err) => { if ('error' in err && err.error.errorDetail.includes('data-channel')) { this._log.error('datachannel.send failed', (0, util_1.createError)({ operationName: 'RTCDataChannel.onerror', info: errors_1.errors.dataChannelSendError, path: log.prefix, context: this._context, channel: this.localPerson.channel, })); } else { this._log.error('datachannel operation failed', (0, util_1.createError)({ operationName: 'RTCDataChannel.onerror', info: errors_1.errors.dataChannelGeneralError, path: log.prefix, context: this._context, channel: this.localPerson.channel, })); } }; dc.bufferedAmountLowThreshold = 65536; // 64 KiB let waitForBufferedAmountLowResolve; let waitForBufferedAmountLow = new Promise((resolve) => { waitForBufferedAmountLowResolve = resolve; }); dc.onbufferedamountlow = () => { waitForBufferedAmountLowResolve(); }; stream._onWriteData .add((data) => __awaiter(this, void 0, void 0, function* () { if (dc.readyState === 'open') { yield this._sendDataQueue.push(() => __awaiter(this, void 0, void 0, function* () { if (dc.bufferedAmount > dc.bufferedAmountLowThreshold) { yield waitForBufferedAmountLow; waitForBufferedAmountLow = new Promise((resolve) => { waitForBufferedAmountLowResolve = resolve; }); } dc.send(data); })); } else { this._log.error('datachannel.send failed', (0, util_1.createError)({ operationName: 'RTCDataChannel.onerror', info: errors_1.errors.dataChannelSendError, path: log.prefix, context: this._context, channel: this.localPerson.channel, })); } })) .disposer(this._disposer); this.datachannels[publication.id] = dc; } else { publication._onReplaceStream .add(({ newStream, oldStream }) => __awaiter(this, void 0, void 0, function* () { newStream._replacingTrack = true; this._listenStreamEnableChange(newStream, publication.id); if (this._cleanupStreamCallbacks[oldStream.id]) { this._cleanupStreamCallbacks[oldStream.id](); } this._cleanupStreamCallbacks[newStream.id] = this._setupTransportAccessForStream(newStream); yield this._replaceTrack(publication.id, newStream.track); newStream._replacingTrack = false; newStream._onReplacingTrackDone.emit(); })) .disposer(this._disposer); this._listenStreamEnableChange(stream, publication.id); const transceiver = this.pc.addTransceiver(stream.track, { direction: 'sendonly', streams: [this._ms], }); publication._onEncodingsChanged .add((encodings) => __awaiter(this, void 0, void 0, function* () { yield (0, util_2.setEncodingParams)(transceiver.sender, encodings).catch((e) => { this._log.error('_onEncodingsChanged failed', e); }); })) .disposer(this._disposer); this.transceivers[publication.id] = transceiver; } const offer = yield this.pc.createOffer().catch((err) => { throw (0, util_1.createError)({ operationName: 'Sender.add', info: Object.assign(Object.assign({}, errors_1.errors.internal), { detail: "can't create offer" }), path: log.prefix, context: this._context, channel: this.localPerson.channel, error: err, }); }); if (this.localPerson._analytics && !this.localPerson._analytics.isClosed()) { // 再送時に他の処理をブロックしないためにawaitしない void this.localPerson._analytics.client.sendRtcPeerConnectionEventReport({ rtcPeerConnectionId: this.rtcPeerConnectionId, type: 'offer', data: { offer: JSON.stringify(offer), }, createdAt: Date.now(), }); } yield this.pc.setLocalDescription(offer); const sdpObject = sdpTransform.parse(this.pc.localDescription.sdp); this._log.debug('<add> create offer base', sdpObject); const mid = this._getMid(publication, sdpObject); if (publication.contentType !== 'data') { applyCodecCapabilities((_a = publication.codecCapabilities) !== null && _a !== void 0 ? _a : [], mid, sdpObject); const offerSdp = sdpTransform.write(sdpObject); yield this.pc.setLocalDescription({ type: 'offer', sdp: offerSdp }); this._log.debug('<add> create offer', this.pc.localDescription); if (((_b = publication.encodings) === null || _b === void 0 ? void 0 : _b.length) > 0) { if ((0, util_2.isSafari)()) { this._safariSetupEncoding(publication); } else { const transceiver = this.transceivers[publication.id]; yield (0, util_2.setEncodingParams)(transceiver.sender, [ publication.encodings[0], ]); } } } const message = { kind: 'senderProduceMessage', payload: { sdp: this.pc.localDescription, publicationId: publication.id, info: { publicationId: publication.id, streamId: stream.id, mid, }, }, }; this._log.debug('[start] send message', message); yield this.signaling.send(this.endpoint, message).catch((error) => { this._log.error('[failed] send message :', error, { localPersonId: this.localPerson.id, endpointId: this.endpoint.id, }); throw error; }); this._log.debug('[end] send message', message); }); } _setupTransportAccessForStream(stream) { stream._getTransportCallbacks[this.endpoint.id] = () => ({ rtcPeerConnection: this.pc, connectionState: this._connectionState, }); stream._getStatsCallbacks[this.endpoint.id] = () => __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; } if (stream._replacingTrack) { yield stream._onReplacingTrackDone.asPromise(200); } const stats = yield this.pc.getStats(stream.track); const arr = (0, util_1.statsToArray)(stats); return arr; }); // replaceStream時に古いstreamに紐づくcallbackを削除するため、戻り値としてcallback削除用の関数を返し、replaceStream時に呼び出す const cleanupCallbacks = () => { delete stream._getTransportCallbacks[this.endpoint.id]; delete stream._getStatsCallbacks[this.endpoint.id]; }; this._disposer.push(() => { cleanupCallbacks(); }); this.onConnectionStateChanged .add((state) => { stream._setConnectionState(this.endpoint, 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); return cleanupCallbacks; } /**@throws {SkyWayError} */ remove(publicationId) { return __awaiter(this, void 0, void 0, function* () { const publication = this.publications[publicationId]; if (!publication) { this._log.warn('<remove> publication not found', (0, util_1.createWarnPayload)({ operationName: 'Sender.remove', detail: 'publication already removed', channel: this.localPerson.channel, payload: { publicationId }, })); return; } // 対向のConnectionがcloseされた際にanswerが帰ってこなくなり、 // _isNegotiatingが永久にfalseにならなくなる。 // この時点でpublicationを削除しないと、このConnectionのcloseIfNeedが // 正常に動作しなくなる delete this.publications[publicationId]; if (this._isNegotiating || this.pc.signalingState !== 'stable') { this._pendingPublications.push(publicationId); this._log.debug('<remove> isNegotiating', { publicationId, _isNegotiating: this._isNegotiating, signalingState: this.pc.signalingState, }); return; } this._isNegotiating = true; this._log.debug('<remove> [start]', { publicationId }); const stream = publication.stream; if (!stream) { throw (0, util_1.createError)({ operationName: 'Sender.remove', info: Object.assign(Object.assign({}, errors_1.errors.missingProperty), { detail: '<remove> publication not have stream' }), path: log.prefix, context: this._context, channel: this.localPerson.channel, payload: { publication }, }); } if (stream.contentType === 'data') { const dc = this.datachannels[publicationId]; dc.close(); delete this.datachannels[publicationId]; } else { const transceiver = this.transceivers[publicationId]; transceiver.stop(); delete this.transceivers[publicationId]; } const offer = yield this.pc.createOffer().catch((err) => { throw (0, util_1.createError)({ operationName: 'Sender.remove', info: Object.assign(Object.assign({}, errors_1.errors.internal), { detail: "<remove> can't create offer" }), path: log.prefix, context: this._context, channel: this.localPerson.channel, error: err, }); }); if (this.localPerson._analytics && !this.localPerson._analytics.isClosed()) { // 再送時に他の処理をブロックしないためにawaitしない void this.localPerson._analytics.client.sendRtcPeerConnectionEventReport({ rtcPeerConnectionId: this.rtcPeerConnectionId, type: 'offer', data: { offer: JSON.stringify(offer), }, createdAt: Date.now(), }); } yield this.pc.setLocalDescription(offer); const message = { kind: 'senderUnproduceMessage', payload: { sdp: this.pc.localDescription, publicationId }, }; this._log.debug('<remove> send message', { message }); yield this.signaling.send(this.endpoint, message).catch((error) => { this._log.error('<remove> in remote error :', error, { localPersonId: this.localPerson.id, endpointId: this.endpoint.id, }); throw error; }); this._log.debug('<remove> [end]', { publicationId }); }); } _replaceTrack(publicationId, track) { return __awaiter(this, void 0, void 0, function* () { const transceiver = this.transceivers[publicationId]; if (!transceiver) { this._log.warn("can't replace track, transceiver not found", (0, util_1.createWarnPayload)({ operationName: 'Sender._replaceTrack', detail: 'transceiver already removed', channel: this.localPerson.channel, payload: { publicationId }, })); return; } yield transceiver.sender.replaceTrack(track).catch((e) => { throw (0, util_1.createError)({ operationName: 'Sender._replaceTrack', context: this._context, info: errors_1.errors.internal, error: e, path: log.prefix, channel: this.localPerson.channel, }); }); }); } _handleReceiverAnswer({ sdp, }) { return __awaiter(this, void 0, void 0, function* () { if (this.pc.signalingState === 'closed') { return; } this._log.debug('<handleReceiverAnswer> [start]'); yield this.pc .setRemoteDescription(new RTCSessionDescription(sdp)) .catch((err) => { const error = (0, util_1.createError)({ operationName: 'Sender._handleReceiverAnswer', context: this._context, info: Object.assign(Object.assign({}, errors_1.errors.internal), { detail: 'failed to setRemoteDescription' }), path: log.prefix, payload: { sdp }, channel: this.localPerson.channel, error: err, }); this._log.error(error); throw error; }); this._log.debug('<handleReceiverAnswer> sRD'); yield this.resolveCandidates(); this._log.debug('<handleReceiverAnswer> resolveCandidates'); yield this.waitForSignalingState('stable'); this._log.debug('<handleReceiverAnswer> waitForSignalingState'); this._isNegotiating = false; yield this._resolvePendingSender(); this._log.debug('<handleReceiverAnswer> _resolvePendingSender', this._pendingPublications.length); this._log.debug('<handleReceiverAnswer> [end]'); }); } _safariSetupEncoding(publication) { // 映像の送信が始まる前にEncodeの設定をするとEncodeの設定の更新ができなくなる const transceiver = this.transceivers[publication.id]; const stream = publication.stream; this.waitForStats({ track: stream.track, cb: (stats) => { const outbound = stats.find((s) => s.id.includes('RTCOutboundRTP') || s.type.includes('outbound-rtp')); if ((outbound === null || outbound === void 0 ? void 0 : outbound.keyFramesEncoded) > 0) return true; return false; }, interval: 10, timeout: this._context.config.rtcConfig.timeout, }) .then(() => { log.debug('safari wait for stats resolved, setEncodingParams'); (0, util_2.setEncodingParams)(transceiver.sender, [publication.encodings[0]]).catch((e) => { this._log.error('setEncodingParams failed', e); }); }) .catch((e) => { this._log.error('waitForStats', e); }); } /**@throws {@link SkyWayError} */ _resolvePendingSender() { return __awaiter(this, void 0, void 0, function* () { const publication = this._pendingPublications.shift(); if (!publication) return; this._log.debug('resolve pending sender', { publication }); if (typeof publication === 'string') { yield this.remove(publication); } else { yield this.add(publication); } }); } close() { this._log.debug('closed'); this.unSetPeerConnectionListener(); Object.values(this._unsubscribeStreamEnableChange).forEach((f) => { f(); }); this.pc.close(); this._setConnectionState('disconnected'); this._disposer.dispose(); } } exports.Sender = Sender; function applyCodecCapabilities(codecCapabilities, mid, sdpObject) { var _a, _b; const media = sdpObject.media.find((m) => { var _a; return ((_a = m.mid) === null || _a === void 0 ? void 0 : _a.toString()) === mid; }); if (!media) { throw (0, util_1.createError)({ operationName: 'applyCodecCapabilities', info: Object.assign(Object.assign({}, errors_1.errors.notFound), { detail: 'media not found' }), path: log.prefix, }); } // parametersをfmtp形式に変換 codecCapabilities.forEach((cap) => { var _a; if (cap.parameters) { for (const [key, value] of Object.entries((_a = cap.parameters) !== null && _a !== void 0 ? _a : {})) { if (value === false || !cap.parameters[key]) { return; } if (key === 'usedtx' && value) { cap.parameters[key] = 1; } } } }); /**codec名とparametersの一致するものを探す */ const findCodecFromCodecCapability = (cap, rtp, fmtp) => { var _a; const rtpList = rtp.map((r) => (Object.assign(Object.assign({}, r), { parameters: (0, util_1.getParameters)(fmtp, r.payload) }))); const codecName = mimeTypeToCodec(cap.mimeType); if (!codecName) { return undefined; } const matched = (_a = rtpList.find((r) => { var _a, _b; if (r.codec.toLowerCase() !== codecName.toLowerCase()) { return false; } if (Object.keys((_a = cap.parameters) !== null && _a !== void 0 ? _a : {}).length === 0) { return true; } // audioはブラウザが勝手にfmtp configを足してくるので厳密にマッチさせる必要がない if (mimeTypeToContentType(cap.mimeType) === 'audio') { return true; } return (0, isEqual_1.default)(r.parameters, (_b = cap.parameters) !== null && _b !== void 0 ? _b : {}); })) !== null && _a !== void 0 ? _a : undefined; return matched; }; const preferredCodecs = codecCapabilities .map((cap) => findCodecFromCodecCapability(cap, media.rtp, media.fmtp)) .filter((v) => v !== undefined); const sorted = [ ...preferredCodecs, ...media.rtp.filter((rtp) => !preferredCodecs.find((p) => p.payload === rtp.payload)), ]; // apply codec fmtp for (const fmtp of media.fmtp) { const payloadType = fmtp.payload; const targetCodecWithPayload = sorted.find((c) => c.payload === payloadType); if (targetCodecWithPayload) { const targetCodecCapability = codecCapabilities.find((c) => findCodecFromCodecCapability(c, [targetCodecWithPayload], media.fmtp)); if (targetCodecCapability) { if (targetCodecCapability.parameters && Object.keys(targetCodecCapability.parameters).length > 0) { // codecCapabilitiesのfmtpを適用する fmtp.config = ''; Object.entries(targetCodecCapability.parameters).forEach(([key, value]) => { if (value === false || fmtp.config.includes(key)) { return; } if (fmtp.config.length > 0) { fmtp.config += `;${key}=${value}`; } else { fmtp.config = `${key}=${value}`; } }); } } } // opusDtxはデフォルトで有効に設定する const opus = sorted.find((rtp) => rtp.codec.toLowerCase() === 'opus'); const opusDtx = (_b = (_a = codecCapabilities.find((f) => mimeTypeToCodec(f.mimeType).toLowerCase() === 'opus')) === null || _a === void 0 ? void 0 : _a.parameters) === null || _b === void 0 ? void 0 : _b.usedtx; if (opus && opusDtx !== false && fmtp.payload === opus.payload && !fmtp.config.includes('usedtx')) { if (fmtp.config.length > 0) { fmtp.config += ';usedtx=1'; } else { fmtp.config = 'usedtx=1'; } } } media.payloads = sorted.map((rtp) => rtp.payload.toString()).join(' '); } exports.applyCodecCapabilities = applyCodecCapabilities; const mimeTypeToCodec = (mimeType) => mimeType.split('/')[1]; const mimeTypeToContentType = (mimeType) => mimeType.split('/')[0]; //# sourceMappingURL=sender.js.map