UNPKG

trtc-electron-sdk

Version:

trtc electron sdk

1,172 lines (1,171 loc) 80.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.TRTCMediaMixingManager = exports.TRTCMediaMixingEvent = void 0; const events_1 = require("events"); const trtc_define_1 = require("../../trtc_define"); const types_1 = require("./types"); const utils_1 = require("../../utils"); const constant_1 = require("../../constant"); const index_1 = __importDefault(require("../../MediaMixingDesigner/index")); const StreamLayout_1 = require("./StreamLayout"); const PromiseStore_1 = __importDefault(require("../../base/PromiseStore")); const DevicePixelRatioObserver_1 = __importDefault(require("../../base/DevicePixelRatioObserver")); const Renderer_1 = require("../../Renderer"); const util_1 = require("../../Renderer/util"); const logger_1 = __importDefault(require("../../logger")); const NodeTRTCEngine = require('../../../build/Release/trtc_electron_sdk.node'); /** * 媒体源本地混流错误码 * @enum {Number} */ const TRTCMediaMixingErrorCode_HACK_JSDOC = { /** 成功 */ Success: 0, /** 通用错误码 */ Error: -1, /** 参数非法 */ InvalidParams: 2, /** 找不到媒体源 */ NotFoundSource: 3, /** 图片加载失败 */ ImageSourceLoadFailed: 4, /** 摄像头未授权 */ CameraNotAuthorized: 5, /** 摄像头被占用 */ CameraIsOccupied: 6, /** 摄像头断开连接 */ CameraDisconnected: 7, /** 不支持的协议类型 */ UnsupportedOnlineVideoProtocol: -8, /** 不支持的文件格式 */ UnsupportedLocalVideoFileFormat: -9, /** 无法连接服务器 */ OnlineVideoConnectFailed: -10, /** 连接断开 */ OnlineVideoConnectionLost: -11, /** 不支持 HEVC 解码 */ NoAvailableHevcDecoder: -12, /** 视频文件不存在 */ VideoFileNotExist: -13 }; /** * 媒体源类型 * @enum {Number} */ const TRTCMediaSourceType_HACK_JSDOC = { /** 摄像头 */ kCamera: 0, /** 屏幕/窗口分享 */ kScreen: 1, /** 图片 */ kImage: 2, /** 手机投屏 */ kPhoneMirror: 4, /** 在线视频 */ kOnlineVideo: 5, /** 本地视频文件 */ kVideoFile: 6, }; /** * 手机投屏媒体源参数 * * @typedef {Object} TRTCPhoneMirrorParam * @property {Number} platformType -1:未知类型, 0:安卓系统, 1:iOS 系统 * @property {Number} connectType -1:未知类型, 0:USB有线投屏, 1:WIFI无线投屏 * @property {String} deviceId 设备 ID * @property {String} deviceName 设备名称 * @property {String} placeholderImagePath 垫片图路径 * @property {Number} frameRate 视频帧率 * @property {Number} bitrateKbps 音频码率 * */ const TRTCPhoneMirrorParam_HACK_JSDOC = null; /** * 媒体源数据 * * @typedef {Object} TRTCMediaSource * @property {TRTCMediaSourceType} sourceType 媒体源类型 * @property {String} sourceId 媒体源唯一 ID * @property {Number} zOrder 媒体源展示层级,取值 0 - 15 * @property {Rect} rect 媒体源显示区域 * @property {Boolean} isSelected 媒体源是否选中,可选属性,默认:false * @property {TRTCVideoRotation} rotation 媒体源选中角度,可选属性 * @property {TRTCVideoFillMode} fillMode 媒体源显示模式,可选属性,默认 `TRTCVideoFillMode_Fit` * @property {TRTCVideoMirrorType} mirrorType 媒体源是否镜像,可选属性,默认不镜像 * @property {TRTCPhoneMirrorConnectionParam} connectionParam 手机投屏类型媒体源,连接参数 */ const TRTCMediaSource_HACK_JSDOC = null; /** * 媒体源本地混流编码参数 * * @typedef {Object} TRTCMediaMixingEncParam * @property {TRTCVideoEncParam} videoEncoderParams 本地混流编码参数 * @property {Number} canvasColor 混流视频背景色,默认值:0x0 代表黑色。格式为十六进制数字,比如:`0x61B9F1` 代表 RGB 颜色(97、158、241)。 * @property {Number} selectedBorderColor 选中媒体源的高亮边框颜色,默认黄色:0xFFFF00。 */ const TRTCMediaMixingEncParam_HACK_JSDOC = null; /** * 连麦视频流布局模式 * * 本地混流高性能渲染模式下,多路视频流布局模式 * * @enum {String} */ const TRTCStreamLayoutMode_HACK_JSDOC = { /** 自定义布局 */ Custom: 'Custom', /** 无布局,恢复默认 */ None: 'None', }; /** * 单路连麦视频流布局信息 * * 本地混流高性能渲染模式下,单路视频流布局信息 * @typedef {Object} TRTCStreamInfo * @property {String} userId 用户 ID,本地用户需填空字符长 * @property {TRTCVideoFillMode} fillMode 视频填充模式 * @property {Rect} rect 视频流显示区域 * @property {Number} zOrder 视频流展示层级,取值 0 - 15 */ const TRTCStreamInfo_HACK_JSDOC = null; /** * 连麦视频流布局信息 * * 本地混流高性能渲染模式下,所有视频流布局信息 * @typedef {Object} TRTCStreamLayout * @property {TRTCStreamLayoutMode} layoutMode 布局模式 * @property {Array<TRTCStreamInfo>} userList 非必填,用户视频流布局 */ const TRTCStreamLayout_HACK_JSDOC = null; const initResolutionMap = () => { const map = new Map(); for (const key in trtc_define_1.TRTCVideoResolution) { if (isNaN(Number(key))) { const value = trtc_define_1.TRTCVideoResolution[key]; const tmp = key.split('_'); map.set(value, { width: parseInt(tmp[1]), height: parseInt(tmp[2]), }); } } return map; }; const resolutionMap = initResolutionMap(); /** * @namespace TRTCMediaMixingEvent * @description 目前只支持 `Windows` 操作系统 */ var TRTCMediaMixingEvent; (function (TRTCMediaMixingEvent) { /** * @description 媒体源选中事件 * * @event TRTCMediaMixingEvent#onSourceSelected * @param {TRTCMediaInfo | null} mediaSource 新选中的媒体源数据,取消选中则为`null` */ TRTCMediaMixingEvent["onSourceSelected"] = "onSourceSelected"; /** * @description 媒体源移动事件 * * @event TRTCMediaMixingEvent#onSourceMoved * @param {TRTCMediaInfo} mediaSource 被移动的媒体源 * @param {Rect} rect 移动后的位置、区域数据 */ TRTCMediaMixingEvent["onSourceMoved"] = "onSourceMoved"; /** * @description 媒体源尺寸变化事件 * * @event TRTCMediaMixingEvent#onSourceResized * @param {TRTCMediaInfo} mediaSource 尺寸变化的媒体源 * @param {Rect} rect 尺寸变化后的位置、区域数据 */ TRTCMediaMixingEvent["onSourceResized"] = "onSourceResized"; /** * @description 媒体源鼠标右键事件 * * @event TRTCMediaMixingEvent#onRightButtonClicked * @param {TRTCMediaInfo} mediaSource 鼠标右键点击的媒体源 * @param {Object} clickPoint 鼠标右键点击的位置 * @param {Number} clickPoint.windowX 鼠标右键点击相对窗口的 X 坐标 * @param {Number} clickPoint.windowY 鼠标右键点击相对窗口的 Y 坐标 * @param {Number} clickPoint.screenX 鼠标右键点击相对屏幕的 X 坐标 * @param {Number} clickPoint.screenY 鼠标右键点击相对屏幕的 Y 坐标 */ TRTCMediaMixingEvent["onRightButtonClicked"] = "onRightButtonClicked"; /** * @description 错误事件 * * @event TRTCMediaMixingEvent#onError * @param {TRTCMediaMixingErrorCode} errorCode 错误码 * @param {String} errorMessage 错误信息 * @param {TRTCMediaSource|undefined} 错误涉及的媒体源,可能为空 * */ TRTCMediaMixingEvent["onError"] = "onError"; /** * @description 设备插入事件 * * @event TRTCMediaMixingEvent#onSourcePlugged * @param {TRTCPhoneMirrorParam} source 投屏手机状态信息 * @param {String} detail 投屏手机状态变化详情 */ TRTCMediaMixingEvent["onSourcePlugged"] = "onSourcePlugged"; /** * @description 设备链接事件 * * @event TRTCMediaMixingEvent#onSourceConnected * @param {TRTCPhoneMirrorParam} source 投屏手机状态信息 * @param {String} detail 投屏手机状态变化详情 */ TRTCMediaMixingEvent["onSourceConnected"] = "onSourceConnected"; /** * @description 设备断开链接事件 * * @event TRTCMediaMixingEvent#onSourceDisconnected * @param {TRTCPhoneMirrorParam} source 投屏手机状态信息 * @param {String} detail 投屏手机状态变化详情 */ TRTCMediaMixingEvent["onSourceDisconnected"] = "onSourceDisconnected"; /** * @description 设备拔出事件 * * @event TRTCMediaMixingEvent#onSourceUnplugged * @param {TRTCPhoneMirrorParam} source 投屏手机状态信息 * @param {String} detail 投屏手机状态变化详情 */ TRTCMediaMixingEvent["onSourceUnplugged"] = "onSourceUnplugged"; /** * @description 媒体源画面大小发生变化 * * 当输入媒体源的画面大小发生变化时,会通过该接口返回该输入源最新的尺寸,您可以根据该尺寸来动态调整画面的比例。 * @event TRTCMediaMixingEvent#onMediaSourceSizeChanged * @param {TRTCMediaSource} mediasource 媒体源信息 * @param {Object} size 媒体源最新的画面大小 * @param {Number} size.width 媒体源最新宽度 * @param {Number} size.height 媒体源最新高度 */ TRTCMediaMixingEvent["onMediaSourceSizeChanged"] = "onMediaSourceSizeChanged"; TRTCMediaMixingEvent["onMediaMixingServerLost"] = "onMediaMixingServerLost"; })(TRTCMediaMixingEvent = exports.TRTCMediaMixingEvent || (exports.TRTCMediaMixingEvent = {})); const promiseKeys = { startMediaMixingServer: 'startMediaMixingServer', stopMediaMixingServer: 'stopMediaMixingServer', setStreamLayout: 'setStreamLayout', addMediaSource: 'addMediaSource', removeMediaSource: 'removeMediaSource', updateMediaSource: 'updateMediaSource', setCameraCaptureParam: 'setCameraCaptureParam', setPhoneMirrorParam: 'setPhoneMirrorParam', setScreenCaptureProperty: 'setScreenCaptureProperty', startPublish: 'startPublish', stopPublish: 'stopPublish', updatePublishParams: 'updatePublishParams', }; var TRTCMediaMixingServerStatus; (function (TRTCMediaMixingServerStatus) { TRTCMediaMixingServerStatus[TRTCMediaMixingServerStatus["Idle"] = 0] = "Idle"; TRTCMediaMixingServerStatus[TRTCMediaMixingServerStatus["Starting"] = 1] = "Starting"; TRTCMediaMixingServerStatus[TRTCMediaMixingServerStatus["Started"] = 2] = "Started"; TRTCMediaMixingServerStatus[TRTCMediaMixingServerStatus["Lost"] = 3] = "Lost"; })(TRTCMediaMixingServerStatus || (TRTCMediaMixingServerStatus = {})); var TRTCMediaMixingServerMode; (function (TRTCMediaMixingServerMode) { TRTCMediaMixingServerMode[TRTCMediaMixingServerMode["Embedded"] = 0] = "Embedded"; TRTCMediaMixingServerMode[TRTCMediaMixingServerMode["Independent"] = 1] = "Independent"; })(TRTCMediaMixingServerMode || (TRTCMediaMixingServerMode = {})); /** * 本地混流管理器 * * 目前只支持 `Windows` 操作系统 */ class TRTCMediaMixingManager { constructor(options) { this.logPrefix = '[TRTCMediaMixingManager]'; this.mediaMixingDesigner = null; this.windowID = 0; this.view = null; this.resizeObserver = null; this.mixingVideoWidth = 0; this.mixingVideoHeight = 0; this.sourceList = []; this.streamLayoutManager = null; this.serverStatus = TRTCMediaMixingServerStatus.Idle; this.serverMode = TRTCMediaMixingServerMode.Independent; // Per-source timeout handles for addMediaSource. Keyed by the same promise // key used in promiseStore. Used to guard against the case where the native // cpp layer never emits SM_OnSourceInserted (e.g. internal OnlineVideoSource // state machine stuck after a previous failure), which would otherwise leave // the returned Promise pending forever and the UI stuck on "loading". this.addMediaSourceTimers = new Map(); this.webRenderer = null; this.pixelFormat = trtc_define_1.TRTCVideoPixelFormat.TRTCVideoPixelFormat_BGRA32; this.pixelLength = 4; this.streamType = trtc_define_1.TRTCVideoStreamType.TRTCVideoStreamTypeBig; this.eventEmitter = new events_1.EventEmitter(); this.promiseStore = new PromiseStore_1.default(); this.handleCppCallbackEvent = this.handleCppCallbackEvent.bind(this); this.nodeMediaMixingPlugin = new NodeTRTCEngine.NodeRemoteMultiSourcePlugin(); this.nodeMediaMixingPlugin.setRemoteMultiSourcePluginCallback(this.handleCppCallbackEvent); this.deviceManager = options.deviceManager; this.nodeTRTCCloud = options.nodeTRTCCloud; this.trtcParamsStore = options.trtcParamsStore; this.autoControlServer = options.autoControlServer; this.paramsStore = {}; this.previewRenderParams = { fillMode: trtc_define_1.TRTCVideoFillMode.TRTCVideoFillMode_Fit, rotation: trtc_define_1.TRTCVideoRotation.TRTCVideoRotation0, mirrorType: trtc_define_1.TRTCVideoMirrorType.TRTCVideoMirrorType_Auto, }; if (this.autoControlServer) { this.startMediaMixingServer('', TRTCMediaMixingServerMode.Embedded) .then(() => { logger_1.default.info(`${this.logPrefix}startMediaMixingServer success.`); }) .catch((err) => { logger_1.default.error(`${this.logPrefix}startMediaMixingServer failed.`, err); }); } this.publishParams = { videoEncoderParams: { videoResolution: trtc_define_1.TRTCVideoResolution.TRTCVideoResolution_1920_1080, resMode: trtc_define_1.TRTCVideoResolutionMode.TRTCVideoResolutionModeLandscape, videoFps: 30, videoBitrate: 3000, minVideoBitrate: 3000, enableAdjustRes: true, }, canvasColor: 0x0 }; this.onSourceSelected = this.onSourceSelected.bind(this); this.onSourceMoved = this.onSourceMoved.bind(this); this.onSourceResized = this.onSourceResized.bind(this); this.onRightButtonClicked = this.onRightButtonClicked.bind(this); this.onPreviewAreaResize = (0, utils_1.debounce)(this.onPreviewAreaResize.bind(this), 100); this.onVisibilityChange = this.onVisibilityChange.bind(this); document.addEventListener('visibilitychange', this.onVisibilityChange); this.onDevicePixelRatioChange = this.onDevicePixelRatioChange.bind(this); DevicePixelRatioObserver_1.default.on('change', this.onDevicePixelRatioChange); this.videoRenderBuffer = new trtc_define_1.VideoBufferInfo({ userId: '', id: (0, util_1.generateUniqueId)(), streamType: this.streamType, width: 1920, height: 1080, pixelFormat: this.pixelFormat, }); this.webRendererCallback = this.webRendererCallback.bind(this); } destroy() { return __awaiter(this, void 0, void 0, function* () { DevicePixelRatioObserver_1.default.off('change', this.onDevicePixelRatioChange); document.removeEventListener('visibilitychange', this.onVisibilityChange); this.destroyResizeObserver(); this.destroyLayoutManager(); this.view = null; this.windowID = 0; this.destroyDesigner(); this.destroyWebRenderer(); if (this.autoControlServer) { try { yield this.stopMediaMixingServer(); logger_1.default.info(`${this.logPrefix}stopMediaMixingServer success.`); } catch (err) { logger_1.default.error(`${this.logPrefix}stopMediaMixingServer failed.`, err); } } this.nodeTRTCCloud = null; this.deviceManager = null; this.nodeMediaMixingPlugin = null; this.eventEmitter = null; this.promiseStore.destroy(); // Cancel any pending addMediaSource timeout timers to avoid late-firing // callbacks touching a destroyed instance. this.addMediaSourceTimers.forEach(timer => clearTimeout(timer)); this.addMediaSourceTimers.clear(); this.paramsStore = null; this.trtcParamsStore = null; }); } /** * @deprecated * @private * 设置视频流预览参数 * * @param windowID {Number|Uint8Array} - 操作系统层的窗口 ID,Electron 下可以通过 Electron API [BrowserWindow.getNativeWindowHandle()]{@link https://www.electronjs.org/docs/latest/api/browser-window#wingetnativewindowhandle} 接口获取 * @param viewOrRegion {HTMLElement | Rect | null} - 本地混流视频显示位置 * - 传入 HTMLElement 元素,则 SDK 将本地混流视频显示在 HTMLElement 元素内,同时支持点击选中、移动、缩放媒体源,支持触发右键菜单事件。HTMLElement 元素必须是块元素。 * - 传入 Rect 显示区域,则 SDK 将本地混流视频显示在 Rect 指定区域内,不支持点击选中、移动、缩放媒体源,不支持触发右键菜单事件,这些功能您可以在 Web 页面中自行实现。 * - 传入 null 则 SDK 将停止显示本地混流视频。 * * @example * // Display in HTML Element * import TRTCCloud from 'trtc-electron-sdk'; * * const trtcCloud = TRTCCloud.getTRTCShareInstance({ * isIPCMode: true * }); * * const mediaMixingManager = trtcCloud.getMediaMixingManager(); * * const windowID = 0; // Use Electron API BrowserWindow.getNativeWindowHandle() * const previewDOM = document.getElementById("preview-local-mixed-media-stream"); * mediaMixingManager.setDisplayParams(windowID, previewDOM); * * @example * // Display in a rectangle section * import TRTCCloud from 'trtc-electron-sdk'; * * const trtcCloud = TRTCCloud.getTRTCShareInstance({ * isIPCMode: true * }); * * const mediaMixingManager = trtcCloud.getMediaMixingManager(); * * const windowID = 0; // Use Electron API BrowserWindow.getNativeWindowHandle() * const previewDOM = document.getElementById("preview-local-mixed-media-stream"); * const domRect = previewDOM.getBoundingClientRect(); * const rect = { * left: domRect.left * window.devicePixelRatio, * right: domRect.right * window.devicePixelRatio, * top: domRect.top * window.devicePixelRatio, * bottom: domRect.bottom * window.devicePixelRatio * }; * * mediaMixingManager.setDisplayParams(windowID, rect); */ setDisplayParams(windowID, viewOrRegion) { return __awaiter(this, void 0, void 0, function* () { logger_1.default.log(`${this.logPrefix}setDisplayParams:`, windowID, viewOrRegion); yield this.bindPreviewArea(windowID, viewOrRegion); }); } /** * 设置视频流预览区域 * * @param windowID {Uint8Array|Number} - 操作系统层的窗口 ID,Electron 下可以通过 Electron API [BrowserWindow.getNativeWindowHandle()]{@link https://www.electronjs.org/docs/latest/api/browser-window#wingetnativewindowhandle} 接口获取 * @param viewOrRegion {HTMLElement | Rect | null} - 本地混流视频显示位置 * - 传入 HTMLElement 元素,则 SDK 将本地混流视频显示在 HTMLElement 元素内,同时支持点击选中、移动、缩放媒体源,支持触发右键菜单事件。HTMLElement 元素必须是块元素。 * - 传入 Rect 显示区域,则 SDK 将本地混流视频显示在 Rect 指定区域内,不支持点击选中、移动、缩放媒体源,不支持触发右键菜单事件,这些功能您可以在 Web 页面中自行实现。 * - 传入 null 则 SDK 将停止显示本地混流视频。 * * @example * // Display in HTML Element * import TRTCCloud from 'trtc-electron-sdk'; * * const trtcCloud = TRTCCloud.getTRTCShareInstance({ * isIPCMode: true * }); * * const mediaMixingManager = trtcCloud.getMediaMixingManager(); * * const windowID = 0; // Use Electron API BrowserWindow.getNativeWindowHandle() * const previewDOM = document.getElementById("preview-local-mixed-media-stream"); * await mediaMixingManager.bindPreviewArea(windowID, previewDOM); * * @example * // Display in a rectangle section * import TRTCCloud from 'trtc-electron-sdk'; * * const trtcCloud = TRTCCloud.getTRTCShareInstance({ * isIPCMode: true * }); * * const mediaMixingManager = trtcCloud.getMediaMixingManager(); * * const windowID = 0; // Use Electron API BrowserWindow.getNativeWindowHandle() * const previewDOM = document.getElementById("preview-local-mixed-media-stream"); * const domRect = previewDOM.getBoundingClientRect(); * const rect = { * left: domRect.left * window.devicePixelRatio, * right: domRect.right * window.devicePixelRatio, * top: domRect.top * window.devicePixelRatio, * bottom: domRect.bottom * window.devicePixelRatio * }; * * await mediaMixingManager.bindPreviewArea(windowID, rect); */ bindPreviewArea(windowID, viewOrRegion) { return __awaiter(this, void 0, void 0, function* () { logger_1.default.log(`${this.logPrefix}bindPreviewArea:`, windowID, viewOrRegion); let realWindowID = 0; if (windowID instanceof Uint8Array) { realWindowID = (0, utils_1.convertUint8ArrayToNumber)(windowID); } else { realWindowID = windowID; } this.windowID = (0, utils_1.isWindows)() ? realWindowID : 0; if (this.windowID !== 0 && viewOrRegion !== null) { yield this.previewInNativeWindow(viewOrRegion); } else if (this.windowID === 0 && viewOrRegion !== null) { yield this.previewInWebElement(viewOrRegion); } else { this.previewInNone(); } }); } /** * 添加本地混流媒体源 * @param mediaSource {TRTCMediaSource} - 媒体源信息 * @returns {Promise<void>} */ addMediaSource(mediaSource) { return __awaiter(this, void 0, void 0, function* () { logger_1.default.log(`${this.logPrefix}addMediaSource:`, mediaSource); const index = this.findMediaSourceIndex(mediaSource); if (index !== -1) { return Promise.reject({ code: types_1.TRTCMediaMixingErrorCode.InvalidParams, message: "Media source already existed" }); } const newMediaSource = (0, utils_1.safelyParse)(JSON.stringify(mediaSource)); if (newMediaSource.isSelected) { const oldSelectedIndex = this.findSelectedMediaSource(); if (oldSelectedIndex !== -1) { yield this.unselectMediaSource(oldSelectedIndex); } } return new Promise((resolve, reject) => { var _a; const key = `${promiseKeys.addMediaSource}-${newMediaSource.sourceType}-${newMediaSource.sourceId}`; this.promiseStore.addPromise(key, resolve, reject); // Arm a timeout safety net. The underlying cpp layer should normally // call back via SM_OnSourceInserted, but for OnlineVideo / VideoFile we // have observed the native MediaPlayer state machine getting stuck on // the 2nd attempt after a previous network failure, with no callback // ever emitted. Without this timer the returned Promise stays pending // forever, freezing upstream UI (e.g. the "child view" loading state). const timeoutMs = this.getAddMediaSourceTimeoutMs(newMediaSource.sourceType); const timer = setTimeout(() => { try { // Defensive guard: destroy() may have been invoked while this // macrotask was already scheduled but before the clearTimeout() // loop in destroy() ran. In that case promiseStore / sourceList // are no longer in a consistent state, and any further side- // effect (e.g. ms.control = {}) could throw on undefined. if (!this.nodeMediaMixingPlugin || !this.promiseStore) { return; } // If the promise was already settled by the real callback, this is // a no-op because PromiseStore.rejectPromise returns false on // missing key. const rejected = this.promiseStore.rejectPromise(key, { code: types_1.TRTCMediaMixingErrorCode.InsertTimeout, message: 'addMediaSource timeout', }); if (rejected) { logger_1.default.warn(`${this.logPrefix}addMediaSource timeout for sourceType:${newMediaSource.sourceType} sourceId:${newMediaSource.sourceId} timeoutMs:${timeoutMs}`); // Mark the source as failed so subsequent removeMediaSource can // take the fast (sync) path without waiting for cpp callbacks // again. const idx = this.findMediaSourceIndex({ sourceId: newMediaSource.sourceId, sourceType: newMediaSource.sourceType, }); if (idx !== -1) { const ms = this.sourceList[idx]; if (ms) { if (!ms.control) { ms.control = {}; } ms.control.isAddFailed = true; } } } } finally { // Always clear our own timer slot, even if the rejection path // throws upstream. Without this, the Map would keep stale handles // alive until destroy(). this.addMediaSourceTimers.delete(key); } }, timeoutMs); this.addMediaSourceTimers.set(key, timer); this.nodeMediaMixingPlugin.addMediaSource(newMediaSource); (_a = this.mediaMixingDesigner) === null || _a === void 0 ? void 0 : _a.addMedia({ id: `${newMediaSource.sourceType}__${newMediaSource.sourceId}`, rect: newMediaSource.rect, isSelected: newMediaSource.isSelected || false, zOrder: newMediaSource.zOrder, origin: newMediaSource }); this.sourceList.push(newMediaSource); }); }); } /** * Returns the per-sourceType timeout (ms) used by addMediaSource. The * defaults are intentionally larger than the cpp-layer timeout in * SourceManagerProxy so that, in the normal case, the cpp side resolves the * promise first. This JS timer is the last-resort safety net in case the * cpp side itself fails to emit any callback (older builds, ipc lost, etc.). */ getAddMediaSourceTimeoutMs(sourceType) { switch (sourceType) { case types_1.TRTCMediaSourceType.kOnlineVideo: return 11000; case types_1.TRTCMediaSourceType.kVideoFile: return 6000; default: return 5000; } } /** * 删除本地混流媒体源 * @param mediaSource {TRTCMediaSource} - 媒体源信息 * @returns {Promise<void>} */ removeMediaSource(mediaSource) { var _a, _b; logger_1.default.log(`${this.logPrefix}removeMediaSource:`, mediaSource); const index = this.findMediaSourceIndex(mediaSource); if (index === -1) { return Promise.reject({ code: types_1.TRTCMediaMixingErrorCode.NotFoundSource, message: "Not existing media source to remove" }); } else { const mediaSourceToDelete = this.sourceList[index]; if (((_a = mediaSourceToDelete === null || mediaSourceToDelete === void 0 ? void 0 : mediaSourceToDelete.control) === null || _a === void 0 ? void 0 : _a.isAddFailed) || ((_b = mediaSourceToDelete === null || mediaSourceToDelete === void 0 ? void 0 : mediaSourceToDelete.control) === null || _b === void 0 ? void 0 : _b.isRemoved)) { this.syncRemoveMediaSource(mediaSource, index); return Promise.resolve(); } else { return new Promise((resolve, reject) => { const key = `${promiseKeys.removeMediaSource}-${mediaSource.sourceType}-${mediaSource.sourceId}`; this.promiseStore.addPromise(key, resolve, reject); this.syncRemoveMediaSource(mediaSource, index); }); } } } syncRemoveMediaSource(mediaSource, index) { try { this.nodeMediaMixingPlugin.removeMediaSource(mediaSource); this.jsRemoveMediaSource(mediaSource, index); } catch (error) { logger_1.default.warn(`${this.logPrefix}removeMediaSource exception:`, error); } } jsRemoveMediaSource(mediaSource, index) { var _a; (_a = this.mediaMixingDesigner) === null || _a === void 0 ? void 0 : _a.removeMedia({ id: `${mediaSource.sourceType}__${mediaSource.sourceId}`, rect: mediaSource.rect, isSelected: mediaSource.isSelected || false, zOrder: mediaSource.zOrder, origin: (0, utils_1.safelyParse)(JSON.stringify(mediaSource)) }); this.sourceList.splice(index, 1); } /** * 更新本地混流媒体源 * @param mediaSource {TRTCMediaSource} - 媒体源信息 * @returns {Promise<void>} */ updateMediaSource(mediaSource) { var _a; return __awaiter(this, void 0, void 0, function* () { logger_1.default.log(`${this.logPrefix}updateMediaSource:`, mediaSource); const index = this.findMediaSourceIndex(mediaSource); if (index === -1) { return Promise.reject({ code: types_1.TRTCMediaMixingErrorCode.NotFoundSource, message: "Not existing media source to update" }); } const newMediaSource = Object.assign(Object.assign({}, this.sourceList[index]), (0, utils_1.safelyParse)(JSON.stringify(mediaSource))); if (mediaSource.isSelected) { const oldSelectedIndex = this.findSelectedMediaSource(); if (oldSelectedIndex !== -1 && oldSelectedIndex !== index) { yield this.unselectMediaSource(oldSelectedIndex); } } yield this.nodeMediaMixingPlugin.updateMediaSource(newMediaSource); (_a = this.mediaMixingDesigner) === null || _a === void 0 ? void 0 : _a.updateMedia({ id: `${mediaSource.sourceType}__${mediaSource.sourceId}`, rect: mediaSource.rect, isSelected: mediaSource.isSelected || false, zOrder: mediaSource.zOrder, origin: (0, utils_1.safelyParse)(JSON.stringify(mediaSource)) }); this.sourceList[index] = newMediaSource; }); } /** * 设置摄像头采集参数 * @param cameraID {string} - 摄像头 ID * @param params {TRTCCameraCaptureParams} - 摄像头采集参数 */ setCameraCaptureParam(cameraID, params) { this.nodeMediaMixingPlugin.setCameraCaptureParam(cameraID, params); if (params.colorRange !== undefined || params.colorSpace !== undefined) { this.nodeMediaMixingPlugin.callExperimentalAPI(JSON.stringify({ "api": "setCameraCaptureParamEx", "params": { "cameraId": cameraID, "colorRange": params.colorRange || trtc_define_1.TRTCVideoColorRange.TRTCVideoColorRange_Auto, "colorSpace": params.colorSpace || trtc_define_1.TRTCVideoColorSpace.TRTCVideoColorSpace_Auto, } })); } } /** * 设置手机投屏参数 * @private * @param phoneMirrorSourceId {string} - 手机投屏媒体源 ID * @param param {TRTCPhoneMirrorParam} - 手机投屏参数 */ setPhoneMirrorParam(phoneMirrorSourceId, param) { this.nodeMediaMixingPlugin.setPhoneMirrorParam(phoneMirrorSourceId, param); } /** * 设置屏幕采集参数 * @param screenOrWindowID {String} - 屏幕 ID 或 窗口 ID * @param property {TRTCScreenCaptureProperty} - 屏幕采集属性 */ setScreenCaptureProperty(screenOrWindowID, property) { this.nodeMediaMixingPlugin.setScreenCaptureProperty(screenOrWindowID, property); } /** * 设置在线视频源播放参数 * @param url {string} - 在线视频源 URL * @param param {TRTCOnlineVideoParam} - 在线视频源播放参数 */ setOnlineVideoParam(url, param) { this.nodeMediaMixingPlugin.setOnlineVideoParam(url, param); } /** * 设置视频文件播放参数 * @param videoFilePath {string} - 视频文件路径 * @param param {TRTCVideoFileParam} - 视频文件播放参数 */ setVideoFileParam(videoFilePath, param) { this.nodeMediaMixingPlugin.setVideoFileParam(videoFilePath, param); } /** * 本地混流开始推流 * @returns {Promise<void>} */ startPublish() { return __awaiter(this, void 0, void 0, function* () { logger_1.default.log(`${this.logPrefix}startPublish`); return yield this.nodeMediaMixingPlugin.startPublish(); }); } /** * 本地混流停止推流 * @returns {Promise<void>} */ stopPublish() { return __awaiter(this, void 0, void 0, function* () { logger_1.default.log(`${this.logPrefix}stopPublish`); return yield this.nodeMediaMixingPlugin.stopPublish(); }); } /** * 更新本地混流编码参数 * @param params {TRTCMediaMixingEncParam} - 推流视频编码参数、背景色等参数 * @returns {Promise<void>} */ updatePublishParams(params) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { logger_1.default.log(`${this.logPrefix}updatePublishParams:`, params); this.publishParams = params; this.updateSelectedBorderColor(); this.updateMixingVideoSize(params.videoEncoderParams); (_a = this.streamLayoutManager) === null || _a === void 0 ? void 0 : _a.updateOptions({ width: this.mixingVideoWidth, height: this.mixingVideoHeight }); (_b = this.mediaMixingDesigner) === null || _b === void 0 ? void 0 : _b.updateOptions({ width: this.mixingVideoWidth, height: this.mixingVideoHeight }); const result = yield this.nodeMediaMixingPlugin.updatePublishParams(Object.assign(Object.assign({}, params), { videoEncoderParams: { videoResolution: 0, resMode: 0, videoFps: params.videoEncoderParams.videoFps, videoBitrate: 0, minVideoBitrate: 0, enableAdjustRes: 0, } })); const encodeParamEx = { videoWidth: this.mixingVideoWidth > this.mixingVideoHeight ? this.mixingVideoWidth : this.mixingVideoHeight, videoHeight: this.mixingVideoWidth > this.mixingVideoHeight ? this.mixingVideoHeight : this.mixingVideoWidth, videoFps: params.videoEncoderParams.videoFps, videoBitrate: params.videoEncoderParams.videoBitrate, gop: 1, resolutionMode: params.videoEncoderParams.resMode, streamType: trtc_define_1.TRTCVideoStreamType.TRTCVideoStreamTypeBig, }; if (params.videoEncoderParams.colorRange !== undefined || params.videoEncoderParams.colorSpace !== undefined || params.videoEncoderParams.complexity !== undefined) { encodeParamEx.colorRange = params.videoEncoderParams.colorRange || trtc_define_1.TRTCVideoColorRange.TRTCVideoColorRange_Auto; encodeParamEx.colorSpace = params.videoEncoderParams.colorSpace || trtc_define_1.TRTCVideoColorSpace.TRTCVideoColorSpace_Auto; encodeParamEx.complexity = params.videoEncoderParams.complexity || trtc_define_1.TRTCVideoEncodeComplexity.TRTCVideoEncodeComplexity_Fastest; } this.nodeMediaMixingPlugin.callExperimentalAPI(JSON.stringify({ api: 'setVideoEncodeParamEx', params: encodeParamEx })); return result; }); } /** * 设置连麦视频流布局 * * @param layout {TRTCStreamLayout} - 视频流布局信息 * * @example * // 设置自定义连麦布局 * import TRTCCloud, { TRTCStreamLayout, TRTCStreamLayoutMode, TRTCVideoFillMode } from 'trtc-electron-sdk'; * * const trtcCloud = TRTCCloud.getTRTCShareInstance({ * isIPCMode: true * }); * * const mediaMixingManager = trtcCloud.getMediaMixingManager(); * if (mediaMixingManager) { * const streamLayout: TRTCStreamLayout = { * "layoutMode": TRTCStreamLayoutMode.Custom, * "userList": [ * { * "userId": "", // 本地用户 userId * "rect": { * "left": 0, * "top": 0, * "right": 296, * "bottom": 494 * }, * "fillMode": TRTCVideoFillMode.TRTCVideoFillMode_Fill, // 视频填满显示区 * "zOrder": 0 * }, * { * "userId": "849014", * "rect": { * "left": 296, * "top": 0, * "right": 592, * "bottom": 247 * }, * "fillMode": TRTCVideoFillMode.TRTCVideoFillMode_Fill, * "zOrder": 1 * }, * { * "userId": "p20", * "rect": { * "left": 296, * "top": 247, * "right": 592, * "bottom": 494 * }, * "fillMode": TRTCVideoFillMode.TRTCVideoFillMode_Fill, * "zOrder": 2 * } * ] * }; * mediaMixingManager.setStreamLayout(streamLayout); * } * * @example * // 清除连麦布局,本地混流恢复居中填充显示(默认效果) * import TRTCCloud, { TRTCStreamLayout, TRTCStreamLayoutMode, TRTCVideoFillMode } from 'trtc-electron-sdk'; * * const trtcCloud = TRTCCloud.getTRTCShareInstance({ * isIPCMode: true * }); * * const mediaMixingManager = trtcCloud.getMediaMixingManager(); * if (mediaMixingManager) { * const streamLayout: TRTCStreamLayout = { * "layoutMode": TRTCStreamLayoutMode.None * }; * mediaMixingManager.setStreamLayout(streamLayout); * } */ setStreamLayout(layout) { var _a, _b, _c; logger_1.default.debug(`${this.logPrefix}setStreamLayout:`, JSON.stringify(layout)); if (this.paramsStore) { try { this.paramsStore.streamLayout = JSON.parse(JSON.stringify(layout)); } catch (err) { this.paramsStore.streamLayout = undefined; } } if (this.view) { this.createOrUpdateLayoutManager(layout); const viewRect = this.view.getBoundingClientRect(); if ((_a = layout.userList) === null || _a === void 0 ? void 0 : _a.length) { const localUser = layout.userList.filter(user => user.userId === constant_1.LOCAL_USER_ID)[0]; if (localUser === null || localUser === void 0 ? void 0 : localUser.rect) { const workingArea = localUser.rect; (_b = this.mediaMixingDesigner) === null || _b === void 0 ? void 0 : _b.setWorkingArea({ left: workingArea.left / viewRect.width, top: workingArea.top / viewRect.height, right: workingArea.right / viewRect.width, bottom: workingArea.bottom / viewRect.height, }, localUser.fillMode); if (localUser.fillMode !== undefined && this.webRenderer) { this.previewRenderParams.fillMode = localUser.fillMode; this.webRenderer.setContentMode(localUser.fillMode); } } } } else { logger_1.default.warn(`${this.logPrefix}setStreamLayout: view is null, no container HTML element. User control the layout manually in device pixel unit.`); } (_c = this.streamLayoutManager) === null || _c === void 0 ? void 0 : _c.setLayout(layout); } /** * @private * @deprecated */ setMediaServerPath(path) { return __awaiter(this, void 0, void 0, function* () { return this.startMediaMixingServer(path); }); } /** * @private * 启动独立混流渲染进程 * * 开发模式,默认路径:node_modules\\trtc-electron-sdk\\build\\Release\\liteav_media_server.exe * 构建模式,默认路径:${resourcesPath}\\liteav_media_server.exe * * 如果用户应用有特殊配置,默认路径可能找不到服务进程程序,需要自行传入路径。 * * @param path {string} - 混流渲染程序路径,不传入参数时,SDK 内部按照默认路径启动服务进程。 * * @returns {Promise<void>} */ startMediaMixingServer(path, mode = TRTCMediaMixingServerMode.Independent) { return __awaiter(this, void 0, void 0, function* () { let serverPath = ''; if (path && path.trim() !== '') { serverPath = path; } else { const { resourcesPath, execPath } = globalThis.process; if (execPath.endsWith('\\electron.exe')) { serverPath = 'node_modules\\trtc-electron-sdk\\build\\Release\\liteav_media_server.exe'; } else { serverPath = `${resourcesPath}\\liteav_media_server.exe`; } } logger_1.default.info(`${this.logPrefix}startMediaMixingServer path:${serverPath}`); return new Promise((resolve, reject) => { this.serverStatus = TRTCMediaMixingServerStatus.Starting; const key = promiseKeys.startMediaMixingServer; this.promiseStore.addPromise(key, resolve, reject); if (mode === TRTCMediaMixingServerMode.Independent) { this.serverMode = mode; this.nodeMediaMixingPlugin.startMediaServer(serverPath); } else { this.serverMode = TRTCMediaMixingServerMode.Embedded; this.nodeMediaMixingPlugin.startMediaServer(''); } this.updateSelectedBorderColor(); }); }); } /** * @private * 关闭独立混流渲染进程 * * @returns {Promise<void>} */ stopMediaMixingServer() { return __awaiter(this, void 0, void 0, function* () { logger_1.default.info(`${this.logPrefix}stopMediaMixingServer`); return new Promise((resolve, reject) => { const key = promiseKeys.stopMediaMixingServer; this.promiseStore.addPromise(key, resolve, reject); this.nodeMediaMixingPlugin.stopMediaServer(); }); }); } callExperimentalAPI(jsonStr) { logger_1.default.debug(`${this.logPrefix}callExperimentalAPI:${jsonStr}`); this.nodeMediaMixingPlugin.callExperimentalAPI(jsonStr); } /** * 注册事件监听 * * @param event {TRTCMediaMixingEvent} - 事件名称 * @param func {Function} - 事件回调函数 */ on(event, func) { var _a; (_a = this.eventEmitter) === null || _a === void 0 ? void 0 : _a.on(event, func); } /** * 取消事件监听 * * @param event {TRTCMediaMixingEvent} - 事件名 * @param func {Function} - 事件回调函数 */ off(event, func) { var _a; (_a = this.eventEmitter) === null || _a === void 0 ? void 0 : _a.off(event, func); } createResizeObserver() { if (this.view) { this.resizeObserver = new ResizeObserver(this.onPreviewAreaResize); this.resizeObserver.observe(this.view); } } destroyResizeObserver() { if (this.resizeObserver) { if (this.view) { this.resizeObserver.unobserve(this.view); } this.resizeObserver.disconnect(); this.resizeObserver = null; } } onPreviewAreaResize(entries) { logger_1.default.log(`${this.logPrefix}onPreviewAreaResize:`, entries); for (const entry of entries) { if (this.windowID && entry.target === this.view) { this.setDisplayRect(); break; } } } onVisibilityChange() { logger_1.default.log(`${this.logPrefix}onVisibilityChange document.hidden:${document.hidden}`); if (document.hidden) { this.nodeMediaMixingPlugin.setDisplayParams(this.windowID, { left: 0, right: 0, top: 0, bottom: 0 }); } else { this.setDisplayRect(); } } setDisplayRect() { if (this.windowID && this.view) { const clientRect = this.view.getBoundingClientRect(); const rect = { left: clientRect.left * window.devicePixelRatio, right: clientRect.right * window.devicePixelRatio, top: clientRect.top * window.devicePixelRatio, bottom: clientRect.bottom * window.devicePixelRatio, }; logger_1.default.debug(`${this.logPrefix}setDisplayRect:`, this.windowID, rect); this.nodeMediaMixingPlugin.setDisplayParams(this.windowID, rect); } } calcHighlightColor() { let highlightColor = constant_1.DEFAULT_HIGHLIGHT_COLOR; if (this.serverMode === TRTCMediaMixingServerMode.Independent) { highlightColor = constant_1.TRANSPARENT_HIGHLIGHT_COLOR; } else { highlightColor = this.publishParams.selectedBorderColor || constant_1.DEFAULT_HIGHLIGHT_COLOR; } return highlightColor; } createDesigner() { if (this.view) { this.updateMixingVideoSize(this.publishParams.videoEncoderParams); const highlightColor = this.calcHighlightColor(); this.mediaMixingDesigner = new index_1.default({ view: this.view, width: this.mixingVideoWidth, height: this.mixingVideoHeight, canExceedContainer: true, highlightColor: highlightColor, }); this.listenDesignerEvent(); if (this.sourceList.length >= 1) { this.sourceList.forEach((mediaSource => { var _a; (_a = this.mediaMixingDesigner) === null || _a === void 0 ? void 0 : _a.addMedia({ id: `${mediaSource.sourceType}__${mediaSource.sourceId}`, rect: mediaSource.rect, isSelected: mediaSource.isSelected || false, zOrder: mediaSource.zOrder, origin: mediaSource }); })); } } } destroyDesigner() { var _a; if (this.mediaMixingDesigner) { this.unlistenDesignerEvent(); (_a = this.mediaMixingDesigner) === null || _a === void 0 ? void 0 : _a.destroy(); this.mediaMixingDesigner = null; } } createOrUpdateLayoutManager(layout) { const context = { container: this.view, mixingVideoSize: { width: this.mixingVideoWidth, height: this.mixingVideoHeight }, mediaMixingDesigner: this.mediaMixingDesigner }; if (!this.streamLayoutManager) { this.streamLayoutManager = StreamLayout_1.StreamLayoutFactory.create(layout.layoutMode, this.nodeMediaMixingPlugin, context); } else if (this.streamLayoutManager.getLayoutMode() !== layout.layoutMode) { this.streamLayoutManager.destroy(); this.streamLayoutManager = StreamLayout_1.StreamLayoutFactory.create(layout.layoutMode, this.nodeMediaMixingPlugin, context); } } destroyLayoutManager() { if (this.streamLayoutManager) { this.streamLayoutManager.destroy(); this.streamLayoutManager = null; } } updateMixingVideoSize(params) { if (resolutionMap.has(params.videoResolution)) { const { width, height } = resolutionMap.get(params.videoResolution); if (params.resMode === trtc_define_1.TRTCVideoResolutionMode.TRTCVideoResolutionModeLandscape) { this.mixingVideoWidth = width; this.mixingVideoHeight = height; } else { this.mixingVideoWidth = height; this.mixingVideoHeight = width; } } } updateSelectedBorderColor() { if (this.mediaMixingDesigner && this.publishParams.selectedBorderColor !== undefined) { const color = this.calcHighlightColor(); this.mediaMixingDesigner.setHighlightColor(color); } } listenDesignerEvent() { var _a, _b, _c, _d, _e;