@skyway-sdk/core
Version:
The official Next Generation JavaScript SDK for SkyWay
474 lines • 22.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());
});
};
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