@fnlb-project/stanza
Version:
Modern XMPP in the browser, with a JSON API
293 lines (292 loc) • 10.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const Constants_1 = require("../Constants");
const ICESession_1 = tslib_1.__importDefault(require("./ICESession"));
const Intermediate_1 = require("./sdp/Intermediate");
const Protocol_1 = require("./sdp/Protocol");
function applyStreamsCompatibility(content) {
const application = content.application;
/* signal .streams as a=ssrc: msid */
if (application.streams &&
application.streams.length &&
application.sources &&
application.sources.length) {
const msid = application.streams[0];
application.sources[0].parameters.msid = `${msid.id} ${msid.track}`;
if (application.sourceGroups && application.sourceGroups.length > 0) {
application.sources.push({
parameters: {
cname: application.sources[0].parameters.cname,
msid: `${msid.id} ${msid.track}`
},
ssrc: application.sourceGroups[0].sources[1]
});
}
}
}
class MediaSession extends ICESession_1.default {
constructor(opts) {
super(opts);
this.includesAudio = false;
this.includesVideo = false;
this._ringing = false;
this.pc.addEventListener('track', (e) => {
this.onAddTrack(e.track, e.streams[0]);
});
if (opts.stream) {
for (const track of opts.stream.getTracks()) {
this.addTrack(track, opts.stream);
}
}
}
get ringing() {
return this._ringing;
}
set ringing(value) {
if (value !== this._ringing) {
this._ringing = value;
}
}
get streams() {
if (this.pc.signalingState !== 'closed') {
return this.pc.getRemoteStreams();
}
return [];
}
// ----------------------------------------------------------------
// Session control methods
// ----------------------------------------------------------------
async start(opts, next) {
this.state = 'pending';
if (arguments.length === 1 && typeof opts === 'function') {
next = opts;
opts = {};
}
next = next || (() => undefined);
opts = opts || {};
this.role = 'initiator';
this.offerOptions = opts;
try {
await this.processLocal(Constants_1.JingleAction.SessionInitiate, async () => {
const offer = await this.pc.createOffer(opts);
const json = (0, Intermediate_1.importFromSDP)(offer.sdp);
const jingle = (0, Protocol_1.convertIntermediateToRequest)(json, this.role, this.transportType);
jingle.sid = this.sid;
jingle.action = Constants_1.JingleAction.SessionInitiate;
for (const content of jingle.contents || []) {
content.creator = 'initiator';
applyStreamsCompatibility(content);
}
await this.pc.setLocalDescription(offer);
this.send('session-initiate', jingle);
});
next();
}
catch (err) {
this._log('error', 'Could not create WebRTC offer', err);
this.end('failed-application', true);
}
}
async accept(opts, next) {
// support calling with accept(next) or accept(opts, next)
if (arguments.length === 1 && typeof opts === 'function') {
next = opts;
opts = {};
}
next = next || (() => undefined);
opts = opts || {};
this._log('info', 'Accepted incoming session');
this.state = 'active';
this.role = 'responder';
try {
await this.processLocal(Constants_1.JingleAction.SessionAccept, async () => {
const answer = await this.pc.createAnswer(opts);
const json = (0, Intermediate_1.importFromSDP)(answer.sdp);
const jingle = (0, Protocol_1.convertIntermediateToRequest)(json, this.role, this.transportType);
jingle.sid = this.sid;
jingle.action = Constants_1.JingleAction.SessionAccept;
for (const content of jingle.contents || []) {
content.creator = 'initiator';
}
await this.pc.setLocalDescription(answer);
await this.processBufferedCandidates();
this.send('session-accept', jingle);
});
next();
}
catch (err) {
this._log('error', 'Could not create WebRTC answer', err);
this.end('failed-application');
}
}
end(reason = 'success', silent = false) {
for (const receiver of this.pc.getReceivers()) {
this.onRemoveTrack(receiver.track);
}
super.end(reason, silent);
}
ring() {
return this.processLocal('ring', async () => {
this._log('info', 'Ringing on incoming session');
this.ringing = true;
this.send(Constants_1.JingleAction.SessionInfo, {
info: {
infoType: Constants_1.JINGLE_INFO_RINGING
}
});
});
}
mute(creator, name) {
return this.processLocal('mute', async () => {
this._log('info', 'Muting', name);
this.send(Constants_1.JingleAction.SessionInfo, {
info: {
creator,
infoType: Constants_1.JINGLE_INFO_MUTE,
name
}
});
});
}
unmute(creator, name) {
return this.processLocal('unmute', async () => {
this._log('info', 'Unmuting', name);
this.send(Constants_1.JingleAction.SessionInfo, {
info: {
creator,
infoType: Constants_1.JINGLE_INFO_UNMUTE,
name
}
});
});
}
hold() {
return this.processLocal('hold', async () => {
this._log('info', 'Placing on hold');
this.send('session-info', {
info: {
infoType: Constants_1.JINGLE_INFO_HOLD
}
});
});
}
resume() {
return this.processLocal('resume', async () => {
this._log('info', 'Resuming from hold');
this.send('session-info', {
info: {
infoType: Constants_1.JINGLE_INFO_ACTIVE
}
});
});
}
// ----------------------------------------------------------------
// Track control methods
// ----------------------------------------------------------------
addTrack(track, stream, cb) {
if (track.kind === 'audio') {
this.includesAudio = true;
}
if (track.kind === 'video') {
this.includesVideo = true;
}
return this.processLocal('addtrack', async () => {
if (this.pc.addTrack) {
this.pc.addTrack(track, stream);
}
else {
this.pc.addStream(stream);
}
if (cb) {
cb();
}
});
}
async removeTrack(sender, cb) {
return this.processLocal('removetrack', async () => {
this.pc.removeTrack(sender);
if (cb) {
return cb();
}
});
}
// ----------------------------------------------------------------
// Track event handlers
// ----------------------------------------------------------------
onAddTrack(track, stream) {
this._log('info', 'Track added');
this.parent.emit('peerTrackAdded', this, track, stream);
}
onRemoveTrack(track) {
this._log('info', 'Track removed');
this.parent.emit('peerTrackRemoved', this, track);
}
// ----------------------------------------------------------------
// Jingle action handers
// ----------------------------------------------------------------
async onSessionInitiate(changes, cb) {
this._log('info', 'Initiating incoming session');
this.state = 'pending';
this.role = 'responder';
this.transportType = changes.contents[0].transport.transportType;
const json = (0, Protocol_1.convertRequestToIntermediate)(changes, this.peerRole);
for (const media of json.media) {
if (media.kind === 'audio') {
this.includesAudio = true;
}
if (media.kind === 'video') {
this.includesVideo = true;
}
if (!media.streams) {
media.streams = [{ stream: 'legacy', track: media.kind }];
}
}
const sdp = (0, Intermediate_1.exportToSDP)(json);
try {
await this.pc.setRemoteDescription({ type: 'offer', sdp });
await this.processBufferedCandidates();
return cb();
}
catch (err) {
this._log('error', 'Could not create WebRTC answer', err);
return cb({ condition: 'general-error' });
}
}
onSessionTerminate(changes, cb) {
for (const receiver of this.pc.getReceivers()) {
this.onRemoveTrack(receiver.track);
}
super.onSessionTerminate(changes, cb);
}
onSessionInfo(changes, cb) {
const info = changes.info || { infoType: '' };
switch (info.infoType) {
case Constants_1.JINGLE_INFO_RINGING:
this._log('info', 'Outgoing session is ringing');
this.ringing = true;
this.parent.emit('ringing', this);
return cb();
case Constants_1.JINGLE_INFO_HOLD:
this._log('info', 'On hold');
this.parent.emit('hold', this);
return cb();
case Constants_1.JINGLE_INFO_UNHOLD:
case Constants_1.JINGLE_INFO_ACTIVE:
this._log('info', 'Resuming from hold');
this.parent.emit('resumed', this);
return cb();
case Constants_1.JINGLE_INFO_MUTE:
this._log('info', 'Muting', info);
this.parent.emit('mute', this, info);
return cb();
case Constants_1.JINGLE_INFO_UNMUTE:
this._log('info', 'Unmuting', info);
this.parent.emit('unmute', this, info);
return cb();
default:
}
return cb();
}
}
exports.default = MediaSession;