UNPKG

@skyway-sdk/core

Version:

The official Next Generation JavaScript SDK for SkyWay

403 lines 17.1 kB
"use strict"; 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