foxconn-yumeeting-sdk-js
Version:
A js sdk for ion yumeeting
574 lines (573 loc) • 21.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeRemote = exports.LocalStream = exports.VideoConstraints = void 0;
const resolutions = ['qvga', 'vga', 'shd', 'hd', 'fhd', 'qhd'];
exports.VideoConstraints = {
qvga: {
resolution: {
width: { ideal: 320 },
height: { ideal: 180 },
frameRate: {
ideal: 15,
max: 30,
},
},
encodings: {
maxBitrate: 300000,
maxFramerate: 16.0,
},
screenEncodings: {
maxBitrate: 300000,
maxFramerate: 6.0,
},
screenMotionEncodings: {
maxBitrate: 300000,
maxFramerate: 24.0,
}
},
vga: {
resolution: {
width: { ideal: 640 },
height: { ideal: 360 },
frameRate: {
ideal: 30,
max: 60,
},
},
encodings: {
maxBitrate: 500000,
maxFramerate: 16.0,
},
screenEncodings: {
maxBitrate: 500000,
maxFramerate: 6.0,
},
screenMotionEncodings: {
maxBitrate: 800000,
maxFramerate: 24.0,
}
},
shd: {
resolution: {
width: { ideal: 960 },
height: { ideal: 540 },
frameRate: {
ideal: 30,
max: 60,
},
},
encodings: {
maxBitrate: 800000,
maxFramerate: 16.0,
},
screenEncodings: {
maxBitrate: 800000,
maxFramerate: 6.0,
},
screenMotionEncodings: {
maxBitrate: 1500000,
maxFramerate: 24.0,
}
},
hd: {
resolution: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: {
ideal: 30,
max: 60,
},
},
encodings: {
maxBitrate: 1800000,
maxFramerate: 16.0,
},
screenEncodings: {
maxBitrate: 1800000,
maxFramerate: 6.0,
},
screenMotionEncodings: {
maxBitrate: 2000000,
maxFramerate: 24.0,
}
},
fhd: {
resolution: {
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: {
ideal: 30,
max: 60,
},
},
encodings: {
maxBitrate: 2500000,
maxFramerate: 24.0,
},
screenEncodings: {
maxBitrate: 750000,
maxFramerate: 6.0,
},
screenMotionEncodings: {
maxBitrate: 4000000,
maxFramerate: 24.0,
}
},
qhd: {
resolution: {
width: { ideal: 2560 },
height: { ideal: 1440 },
frameRate: {
ideal: 30,
max: 60,
},
},
encodings: {
maxBitrate: 1000000,
maxFramerate: 24.0,
},
screenEncodings: {
maxBitrate: 500000,
maxFramerate: 6.0,
},
screenMotionEncodings: {
maxBitrate: 8000000,
maxFramerate: 24.0,
}
},
};
const defaults = {
resolution: 'hd',
codec: 'vp8',
audio: true,
video: true,
simulcast: false,
};
class LocalStream extends MediaStream {
constructor(stream, constraints) {
super(stream);
this.constraints = constraints;
}
static async getUserMedia(constraints = defaults) {
const stream = await navigator.mediaDevices.getUserMedia({
audio: LocalStream.computeAudioConstraints({
...defaults,
...constraints,
}),
video: LocalStream.computeVideoConstraints({
...defaults,
...constraints,
}),
});
return new LocalStream(stream, {
...defaults,
...constraints,
});
}
static async getDisplayMedia(stream, constraints = {
codec: 'vp8',
resolution: 'fhd',
audio: false,
video: true,
simulcast: false,
}) {
// @ts-ignore
// const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
return new LocalStream(stream, {
...defaults,
...constraints,
});
}
static async getDisplayMediaForWeb(constraints = {
codec: 'vp8',
resolution: 'fhd',
audio: false,
video: true,
simulcast: false,
}) {
// @ts-ignore
const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
return new LocalStream(stream, {
...defaults,
...constraints,
});
}
static computeAudioConstraints(constraints) {
// audio: {
// channelCount: constraints.channelCount || 2,
// // echoCancellation: constraints.echoCancellation || true,
// // latency: constraints.latency || 200,
// },
return constraints.channelCount ? Object.assign(constraints.audio, {
channelCount: constraints.channelCount,
}) : constraints.audio;
}
static computeVideoConstraints(constraints) {
if (constraints.video instanceof Object) {
return Object.assign(constraints.video, {
...exports.VideoConstraints[constraints.resolution].resolution,
});
}
else if (constraints.video && constraints.resolution) {
return {
...exports.VideoConstraints[constraints.resolution].resolution,
};
}
return constraints.video;
}
getTrack(kind) {
let tracks;
if (kind === 'video') {
tracks = this.getVideoTracks();
return tracks.length > 0 ? this.getVideoTracks()[0] : undefined;
}
tracks = this.getAudioTracks();
return tracks.length > 0 ? this.getAudioTracks()[0] : undefined;
}
async getNewTrack(kind) {
const stream = await navigator.mediaDevices.getUserMedia({
[kind]: kind === 'video'
? LocalStream.computeVideoConstraints(this.constraints)
: LocalStream.computeAudioConstraints(this.constraints),
});
return stream.getTracks()[0];
}
publishTrack(track) {
if (this.pc) {
if (track.kind === 'video' && this.constraints.simulcast) {
const idx = resolutions.indexOf(this.constraints.resolution);
let encodings = [
{
rid: 'f',
maxBitrate: exports.VideoConstraints[resolutions[idx]].encodings.maxBitrate,
maxFramerate: exports.VideoConstraints[resolutions[idx]].encodings.maxFramerate,
},
];
if (idx - 1 >= 0) {
encodings.push({
rid: 'h',
scaleResolutionDownBy: 2.0,
maxBitrate: exports.VideoConstraints[resolutions[idx - 1]].encodings.maxBitrate,
maxFramerate: exports.VideoConstraints[resolutions[idx - 1]].encodings.maxFramerate,
});
}
if (idx - 2 >= 0) {
encodings.push({
rid: 'q',
scaleResolutionDownBy: 4.0,
maxBitrate: exports.VideoConstraints[resolutions[idx - 2]].encodings.maxBitrate,
maxFramerate: exports.VideoConstraints[resolutions[idx - 2]].encodings.maxFramerate,
});
}
if ('contentHint' in track) {
let hint = 'motion';
if (track.label === 'Screen') {
let tmpEncodings = exports.VideoConstraints[resolutions[idx]].screenEncodings;
if (this.motion) {
hint = 'motion';
tmpEncodings = exports.VideoConstraints[resolutions[idx]].screenMotionEncodings;
}
else {
hint = 'detail';
}
encodings = [
{
rid: 'f',
maxBitrate: tmpEncodings.maxBitrate,
maxFramerate: tmpEncodings.maxFramerate,
},
];
if (idx - 1 >= 0) {
encodings.push({
rid: 'h',
scaleResolutionDownBy: 2.0,
maxBitrate: tmpEncodings.maxBitrate,
maxFramerate: tmpEncodings.maxFramerate,
});
}
if (idx - 2 >= 0) {
encodings.push({
rid: 'q',
scaleResolutionDownBy: 4.0,
maxBitrate: tmpEncodings.maxBitrate,
maxFramerate: tmpEncodings.maxFramerate,
});
}
}
track.contentHint = hint;
if (track.contentHint !== hint) {
// console.log('Invalid video track contentHint: \'' + hint + '\'');
}
}
else {
// console.log('MediaStreamTrack contentHint attribute not supported');
}
const transceiver = this.pc.addTransceiver(track, {
streams: [this],
direction: 'sendonly',
sendEncodings: encodings,
});
this.setPreferredCodec(transceiver, track.kind);
}
else {
const init = {
streams: [this],
direction: 'sendonly',
};
// video
if (track.kind === 'video') {
init.sendEncodings = [exports.VideoConstraints[this.constraints.resolution].encodings];
if ('contentHint' in track) {
let hint = 'motion';
if (track.label === 'Screen') {
if (this.motion) {
hint = 'motion';
init.sendEncodings = [exports.VideoConstraints[this.constraints.resolution].screenMotionEncodings];
}
else {
hint = 'detail';
init.sendEncodings = [exports.VideoConstraints[this.constraints.resolution].screenEncodings];
}
}
track.contentHint = hint;
if (track.contentHint !== hint) {
// console.log('Invalid video track contentHint: \'' + hint + '\'');
}
}
else {
// console.log('MediaStreamTrack contentHint attribute not supported');
}
}
// audio
if (track.kind === 'audio') {
if ('contentHint' in track) {
const hint = this.constraints.contentHintAudio || '';
track.contentHint = hint;
if (track.contentHint !== hint) {
// console.log('Invalid video track contentHint: \'' + hint + '\'');
}
}
else {
// console.log('MediaStreamTrack contentHint attribute not supported');
}
// init.sendEncodings = [VideoConstraints[this.constraints.resolution].encodings];
}
const transceiver = this.pc.addTransceiver(track, init);
this.setPreferredCodec(transceiver, track.kind);
}
}
}
setPreferredCodec(transceiver, kind) {
if ('setCodecPreferences' in transceiver) {
const cap = RTCRtpSender.getCapabilities(kind);
if (!cap)
return;
let selCodec;
if (this.constraints.preferredCodecProfile && kind === 'video') {
const allCodecProfiles = cap.codecs.filter((c) => c.mimeType.toLowerCase() === `video/${this.constraints.codec.toLowerCase()}`);
if (!allCodecProfiles) {
return;
}
selCodec = allCodecProfiles.find((c) => { var _a; return c.sdpFmtpLine && ((_a = c.sdpFmtpLine) === null || _a === void 0 ? void 0 : _a.indexOf(`profile-level-id=${this.constraints.preferredCodecProfile}`)) >= 0; });
if (!selCodec) {
// get first one
selCodec = allCodecProfiles[0];
}
}
else {
selCodec = cap.codecs.find((c) => c.mimeType.toLowerCase() === `video/${this.constraints.codec.toLowerCase()}` ||
c.mimeType.toLowerCase() === `audio/opus`);
}
if (selCodec) {
transceiver.setCodecPreferences([selCodec]);
}
}
}
updateTrack(next, prev) {
this.addTrack(next);
// If published, replace published track with track from new device
if (prev) {
this.removeTrack(prev);
prev.stop();
if (this.pc) {
this.pc.getSenders().forEach(async (sender) => {
var _a, _b, _c;
if (((_a = sender === null || sender === void 0 ? void 0 : sender.track) === null || _a === void 0 ? void 0 : _a.kind) === next.kind && prev.id === ((_b = sender === null || sender === void 0 ? void 0 : sender.track) === null || _b === void 0 ? void 0 : _b.id)) {
(_c = sender.track) === null || _c === void 0 ? void 0 : _c.stop();
sender.replaceTrack(next);
}
});
}
}
else {
this.addTrack(next);
if (this.pc) {
this.publishTrack(next);
}
}
}
initAudioEmptyTrack() {
// @ts-ignore
const AudioContext = window.AudioContext || window.webkitAudioContext;
const ctx = new AudioContext();
const oscillator = ctx.createOscillator();
oscillator.frequency.setValueAtTime(20000, ctx.currentTime);
const dst = oscillator.connect(ctx.createMediaStreamDestination());
oscillator.start();
return dst.stream.getAudioTracks()[0];
}
initVideoEmptyTrack(width, height) {
var _a;
const canvas = Object.assign(document.createElement('canvas'), { width, height });
(_a = canvas.getContext('2d')) === null || _a === void 0 ? void 0 : _a.fillRect(0, 0, width, height);
const stream = canvas.captureStream();
return stream.getVideoTracks()[0];
}
publish(transport, motion) {
this.pc = transport.pc;
this.api = transport.api;
this.motion = motion;
this.getTracks().forEach(this.publishTrack.bind(this));
}
unpublish() {
if (this.pc) {
const tracks = this.getTracks();
this.pc.getSenders().forEach((s) => {
if (s.track && tracks.includes(s.track)) {
this.pc.removeTrack(s);
}
});
}
}
async switchDevice(kind, deviceId) {
this.constraints = {
...this.constraints,
[kind]: this.constraints[kind] instanceof Object
? {
...this.constraints[kind],
deviceId,
}
: { deviceId },
};
const prev = this.getTrack(kind);
const next = await this.getNewTrack(kind);
this.updateTrack(next, prev);
}
mute(kind) {
const track = this.getTrack(kind);
if (track && this.constraints.sendEmptyOnMute) {
const emptyTrack = kind === 'audio'
? this.initAudioEmptyTrack()
: this.initVideoEmptyTrack((track === null || track === void 0 ? void 0 : track.getSettings().width) || 640, (track === null || track === void 0 ? void 0 : track.getSettings().height) || 360);
emptyTrack.enabled = false;
this.updateTrack(emptyTrack, track);
return;
}
if (track) {
track.stop();
}
}
async unmute(kind) {
const prev = this.getTrack(kind);
const track = await this.getNewTrack(kind);
this.updateTrack(track, prev);
}
async enableLayers(layers) {
const call = {
streamId: this.id,
layers,
};
const callStr = JSON.stringify(call);
if (this.api) {
if (this.api.readyState !== 'open') {
// queue call if we aren't open yet
this.api.onopen = () => { var _a; return (_a = this.api) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify(call)); };
}
else {
this.api.send(JSON.stringify(call));
}
}
const layerValues = ['high', 'medium', 'low'];
await Promise.all(layerValues.map(async (layer) => {
await this.updateMediaEncodingParams({ active: layers.includes(layer) }, layer);
}));
}
async updateMediaEncodingParams(encodingParams, layer) {
var _a;
if (!this.pc)
return;
const tracks = this.getTracks();
await Promise.all((_a = this.pc) === null || _a === void 0 ? void 0 : _a.getSenders().filter((sender) => sender.track && tracks.includes(sender.track)).map(async (sender) => {
const params = sender.getParameters();
if (!params.encodings) {
params.encodings = [{}];
}
let idx = 0;
if (this.constraints.simulcast && layer) {
const rid = layer === 'high' ? 'f' : layer === 'medium' ? 'h' : 'q';
idx = params.encodings.findIndex((encoding) => encoding.rid === rid);
if (params.encodings.length < idx + 1)
return;
}
params.encodings[idx] = {
...params.encodings[idx],
...encodingParams,
};
await sender.setParameters(params);
}));
}
}
exports.LocalStream = LocalStream;
function makeRemote(stream, transport) {
const remote = stream;
remote.audio = true;
remote.video = 'none';
remote.framerate = 'high';
remote._videoPreMute = 'high';
const select = () => {
const call = {
streamId: remote.id,
video: remote.video,
audio: remote.audio,
framerate: remote.framerate,
};
if (transport.api) {
if (transport.api.readyState !== 'open') {
// queue call if we aren't open yet
transport.api.onopen = () => { var _a; return (_a = transport.api) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify(call)); };
}
else {
transport.api.send(JSON.stringify(call));
}
}
};
remote.preferLayer = (layer) => {
remote.video = layer;
select();
};
remote.preferFramerate = (layer) => {
remote.framerate = layer;
select();
};
remote.mute = (kind) => {
if (kind === 'audio') {
remote.audio = false;
}
else if (kind === 'video') {
remote._videoPreMute = remote.video;
remote.video = 'none';
}
select();
};
remote.unmute = (kind) => {
if (kind === 'audio') {
remote.audio = true;
}
else if (kind === 'video') {
remote.video = remote._videoPreMute;
}
select();
};
return remote;
}
exports.makeRemote = makeRemote;