@skyway-sdk/core
Version:
The official Next Generation JavaScript SDK for SkyWay
857 lines • 40.1 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 __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