@skyway-sdk/core
Version:
The official Next Generation JavaScript SDK for SkyWay
403 lines • 17.1 kB
JavaScript
;
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.detectDevice = exports.getRuntimeInfo = exports.createTestVideoTrack = exports.fmtpConfigParser = exports.getParameters = exports.getRtcRtpCapabilities = exports.waitForLocalStats = exports.createError = exports.createWarnPayload = exports.createLogPayload = exports.statsToArray = exports.getBitrateFromPeerConnection = void 0;
const common_1 = require("@skyway-sdk/common");
const bowser_1 = __importDefault(require("bowser"));
const sdp_transform_1 = __importDefault(require("sdp-transform"));
const ua_parser_js_1 = require("ua-parser-js");
const errors_1 = require("./errors");
const log = new common_1.Logger('packages/core/src/util.ts');
/**@internal */
function getBitrateFromPeerConnection(stream, direction, cb, selector) {
let preBytes = 0;
const id = setInterval(() => __awaiter(this, void 0, void 0, function* () {
const stats = yield stream._getStats(selector);
const stat = stats.find((v) => {
if (direction === 'inbound') {
return ((v === null || v === void 0 ? void 0 : v.id.includes('InboundRTPVideo')) || (v === null || v === void 0 ? void 0 : v.type.includes('inbound-rtp')));
}
return ((v === null || v === void 0 ? void 0 : v.id.includes('OutboundRTPVideo')) || (v === null || v === void 0 ? void 0 : v.type.includes('outbound-rtp')));
});
if (!stat) {
return;
}
const totalBytes = direction === 'inbound' ? stat.bytesReceived : stat.bytesSent;
const bitrate = (totalBytes - preBytes) * 8;
cb(bitrate);
preBytes = totalBytes;
}), 1000);
return () => clearInterval(id);
}
exports.getBitrateFromPeerConnection = getBitrateFromPeerConnection;
/**@internal */
function statsToArray(stats) {
const arr = [];
stats.forEach((stat) => {
arr.push(stat);
});
return arr;
}
exports.statsToArray = statsToArray;
/**@internal */
function createLogPayload({ operationName, channel, }) {
return __awaiter(this, void 0, void 0, function* () {
const payload = {
operationName,
appId: channel.appId,
channelId: channel.id,
};
if (channel.localPerson) {
const member = channel.localPerson;
const publishing = yield Promise.all(member.publications.map((p) => __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c;
const publication = {
id: p.id,
contentType: p.contentType,
state: p.state,
stats: {},
connectionStats: {},
};
if (p.stream) {
for (const { memberId, stats } of yield p.stream._getStatsAll()) {
const localCandidate = stats.find((s) => s.type.includes('local-candidate'));
publication.stats[memberId] = {
transportType: (_a = localCandidate === null || localCandidate === void 0 ? void 0 : localCandidate.protocol) !== null && _a !== void 0 ? _a : 'none',
relayProtocol: (_b = localCandidate === null || localCandidate === void 0 ? void 0 : localCandidate.relayProtocol) !== null && _b !== void 0 ? _b : 'none',
callType: (_c = p.subscriptions.find((s) => s.subscriber.id === memberId)) === null || _c === void 0 ? void 0 : _c.subscriber.subtype,
outbound: stats.find((s) => s.type.includes('outbound-rtp')),
localCandidate,
};
}
}
if (p.stream) {
for (const { memberId, connectionState, } of p.stream._getConnectionStateAll()) {
publication.connectionStats[memberId] = connectionState;
}
}
return publication;
})));
payload.publishing = publishing;
const subscribing = yield Promise.all(member.subscriptions.map((s) => __awaiter(this, void 0, void 0, function* () {
const subscription = {
id: s.id,
contentType: s.contentType,
stats: {},
};
subscription.callType = s.publication.publisher.subtype;
if (s.stream) {
const stats = yield s.stream._getStats();
subscription.stats = stats.find((s) => s.type.includes('inbound-rtp'));
const iceCandidate = stats.find((s) => s.type.includes('local-candidate'));
subscription.transportType = iceCandidate === null || iceCandidate === void 0 ? void 0 : iceCandidate.protocol;
subscription.relayProtocol = iceCandidate === null || iceCandidate === void 0 ? void 0 : iceCandidate.relayProtocol;
}
if (s.stream) {
subscription.connectionState = s.stream._getConnectionState();
}
return subscription;
})));
payload.subscribing = subscribing;
}
return payload;
});
}
exports.createLogPayload = createLogPayload;
/**@internal */
function createWarnPayload({ member, detail, channel, operationName, payload, }) {
const warn = {
operationName,
payload,
detail,
};
if (member) {
warn.appId = member.channel.appId;
warn.channelId = member.channel.id;
warn.memberId = member.id;
}
if (channel) {
warn.appId = channel.appId;
warn.channelId = channel.id;
}
return warn;
}
exports.createWarnPayload = createWarnPayload;
/**@internal */
function createError({ operationName, context, info, error, path, payload, channel, }) {
const errPayload = {
operationName,
payload,
};
if (channel) {
errPayload.appId = channel.appId;
errPayload.channelId = channel.id;
if (channel.localPerson) {
errPayload.memberId = channel.localPerson.id;
}
}
if (context) {
errPayload.info = context.info;
errPayload.plugins = context.plugins.map((p) => p.subtype);
}
return new common_1.SkyWayError({ error, info, payload: errPayload, path });
}
exports.createError = createError;
/**@internal */
const waitForLocalStats = ({ stream, remoteMember, end, interval, timeout, }) => __awaiter(void 0, void 0, void 0, function* () {
return new Promise((r, f) => {
const intervalMs = interval !== null && interval !== void 0 ? interval : 100;
const timeoutMs = timeout !== null && timeout !== void 0 ? timeout : 10000;
const checkStats = () => __awaiter(void 0, void 0, void 0, function* () {
for (let elapsed = 0;; elapsed += intervalMs) {
if (elapsed >= timeoutMs) {
f(createError({
operationName: 'Peer.waitForStats',
info: Object.assign(Object.assign({}, errors_1.errors.timeout), { detail: 'waitForStats timeout' }),
path: log.prefix,
}));
break;
}
const stats = yield stream._getStats(remoteMember);
if (end(stats)) {
r(stats);
break;
}
yield new Promise((r) => setTimeout(r, intervalMs));
}
});
checkStats().catch(f);
});
});
exports.waitForLocalStats = waitForLocalStats;
/**@internal */
function getRtcRtpCapabilities() {
return __awaiter(this, void 0, void 0, function* () {
const pc = new RTCPeerConnection();
pc.addTransceiver('audio', {
direction: 'sendonly',
});
pc.addTransceiver('video', {
direction: 'sendonly',
});
const offer = yield pc.createOffer();
try {
pc.close();
}
catch (_error) { }
const sdpObject = sdp_transform_1.default.parse(offer.sdp);
const [audio, video] = sdpObject.media;
return {
audio: audio.rtp.map((r) => (Object.assign(Object.assign({}, r), { payload: r.payload, mimeType: `audio/${r.codec}`, parameters: (0, exports.getParameters)(audio.fmtp, r.payload) }))),
video: video.rtp
.filter((r) => !['red', 'rtx', 'ulpfec'].includes(r.codec))
.map((r) => (Object.assign(Object.assign({}, r), { payload: r.payload, mimeType: `video/${r.codec}`, parameters: (0, exports.getParameters)(video.fmtp, r.payload) }))),
};
});
}
exports.getRtcRtpCapabilities = getRtcRtpCapabilities;
/**@internal */
const getParameters = (fmtp, payload) => { var _a, _b; return (0, exports.fmtpConfigParser)((_b = (_a = fmtp.find((f) => f.payload === payload)) === null || _a === void 0 ? void 0 : _a.config) !== null && _b !== void 0 ? _b : ''); };
exports.getParameters = getParameters;
/**@internal */
const fmtpConfigParser = (config) => {
const parameters = config
.split(';')
.reduce((acc, cur) => {
const [k, v] = cur.split('=');
if (k) {
acc[k] = !Number.isNaN(Number(v)) ? Number(v) : v;
}
return acc;
}, {});
return parameters;
};
exports.fmtpConfigParser = fmtpConfigParser;
/**@internal */
function createTestVideoTrack(width, height) {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const drawAnimation = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgb(200, 200, 200)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const date = new Date();
ctx.font = '45px Monaco,Consolas';
ctx.textAlign = 'center';
ctx.fillStyle = 'red';
const hours = `0${date.getHours()}`.slice(-2);
const minutes = `0${date.getMinutes()}`.slice(-2);
const seconds = `0${date.getSeconds()}`.slice(-2);
const milliseconds = `00${date.getMilliseconds()}`.slice(-3);
ctx.fillText(`${hours}:${minutes}:${seconds}.${milliseconds}`, canvas.width / 2, canvas.height / 2);
requestAnimationFrame(drawAnimation);
};
setTimeout(() => drawAnimation(), 0);
const [track] = canvas.captureStream().getVideoTracks();
return track;
}
exports.createTestVideoTrack = createTestVideoTrack;
/**
* @internal
* @description browser only
*/
const getRuntimeInfo = ({ isNotBrowser, } = {}) => {
if (isNotBrowser) {
return isNotBrowser;
}
const browser = bowser_1.default.getParser(window.navigator.userAgent);
const osName = browser.getOSName();
const osVersion = browser.getOSVersion();
const browserName = browser.getBrowserName();
const browserVersion = browser.getBrowserVersion();
return {
browserName,
browserVersion,
osName,
osVersion,
};
};
exports.getRuntimeInfo = getRuntimeInfo;
/**
* @internal
* @description from mediasoup-client
*/
function detectDevice() {
var _a, _b, _c, _d, _e, _f, _g, _h;
// React-Native.
// NOTE: react-native-webrtc >= 1.75.0 is required.
// NOTE: react-native-webrtc with Unified Plan requires version >= 106.0.0.
if (typeof navigator === 'object' && navigator.product === 'ReactNative') {
if (typeof RTCPeerConnection === 'undefined') {
return undefined;
}
if (typeof RTCRtpTransceiver !== 'undefined') {
return 'ReactNativeUnifiedPlan';
}
else {
return 'ReactNative';
}
}
// Browser.
else if (typeof navigator === 'object' &&
typeof navigator.userAgent === 'string') {
const ua = navigator.userAgent;
const uaParser = new ua_parser_js_1.UAParser(ua);
const browser = uaParser.getBrowser();
const browserName = (_b = (_a = browser.name) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== null && _b !== void 0 ? _b : '';
const browserVersion = parseInt((_c = browser.major) !== null && _c !== void 0 ? _c : '0', 10);
const engine = uaParser.getEngine();
const engineName = (_e = (_d = engine.name) === null || _d === void 0 ? void 0 : _d.toLowerCase()) !== null && _e !== void 0 ? _e : '';
const os = uaParser.getOS();
const osName = (_g = (_f = os.name) === null || _f === void 0 ? void 0 : _f.toLowerCase()) !== null && _g !== void 0 ? _g : '';
const osVersion = parseFloat((_h = os.version) !== null && _h !== void 0 ? _h : '0');
const isIOS = osName === 'ios';
const isChrome = [
'chrome',
'chromium',
'mobile chrome',
'chrome webview',
'chrome headless',
].includes(browserName);
const isFirefox = ['firefox', 'mobile firefox', 'mobile focus'].includes(browserName);
const isSafari = ['safari', 'mobile safari'].includes(browserName);
const isEdge = ['edge'].includes(browserName);
// Chrome, Chromium, and Edge.
if ((isChrome || isEdge) && !isIOS && browserVersion >= 111) {
return 'Chrome111';
}
else if ((isChrome && !isIOS && browserVersion >= 74) ||
(isEdge && !isIOS && browserVersion >= 88)) {
return 'Chrome74';
}
else if (isChrome && !isIOS && browserVersion >= 70) {
return 'Chrome70';
}
else if (isChrome && !isIOS && browserVersion >= 67) {
return 'Chrome67';
}
else if (isChrome && !isIOS && browserVersion >= 55) {
return 'Chrome55';
}
// Firefox.
else if (isFirefox && !isIOS && browserVersion >= 60) {
return 'Firefox60';
}
// Firefox on iOS (so Safari).
else if (isFirefox && isIOS && osVersion >= 14.3) {
return 'Safari12';
}
// Safari with Unified-Plan support enabled.
else if (isSafari &&
browserVersion >= 12 &&
typeof RTCRtpTransceiver !== 'undefined' &&
// biome-ignore lint/suspicious/noPrototypeBuiltins: Legacy compatibility
RTCRtpTransceiver.prototype.hasOwnProperty('currentDirection')) {
return 'Safari12';
}
// Safari with Plab-B support.
else if (isSafari && browserVersion >= 11) {
return 'Safari11';
}
// Old Edge with ORTC support.
else if (isEdge && !isIOS && browserVersion >= 11 && browserVersion <= 18) {
return 'Edge11';
}
// Best effort for WebKit based browsers in iOS.
else if (engineName === 'webkit' &&
isIOS &&
osVersion >= 14.3 &&
typeof RTCRtpTransceiver !== 'undefined' &&
// biome-ignore lint/suspicious/noPrototypeBuiltins: Legacy compatibility
RTCRtpTransceiver.prototype.hasOwnProperty('currentDirection')) {
return 'Safari12';
}
// Best effort for Chromium based browsers.
else if (engineName === 'blink') {
const match = ua.match(/(?:(?:Chrome|Chromium))[ /](\w+)/i);
if (match) {
const version = Number(match[1]);
if (version >= 111) {
return 'Chrome111';
}
else if (version >= 74) {
return 'Chrome74';
}
else if (version >= 70) {
return 'Chrome70';
}
else if (version >= 67) {
return 'Chrome67';
}
else {
return 'Chrome55';
}
}
else {
return 'Chrome111';
}
}
// Unsupported browser.
else {
return undefined;
}
}
// Unknown device.
else {
return undefined;
}
}
exports.detectDevice = detectDevice;
//# sourceMappingURL=util.js.map