trtc-electron-sdk
Version:
trtc electron sdk
1,172 lines (1,171 loc) • 80.1 kB
JavaScript
"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;