nodeplayer-addon 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var _0x3f0c6b = require('events');
4
- var _0x276f4e = require('fs');
5
- var _0x4813c5 = require('path');
6
- var _0x96cc6a = require('module');
3
+ var require$$0 = require('events');
4
+ var require$$1 = require('fs');
5
+ var require$$2 = require('path');
6
+ var require$$3 = require('module');
7
7
 
8
8
  function getDefaultExportFromCjs (x) {
9
9
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
@@ -13,7 +13,452 @@ function commonjsRequire(path) {
13
13
  throw new Error('Could not dynamically require "' + path + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.');
14
14
  }
15
15
 
16
- var js,hasRequiredJs;function requireJs(){if(hasRequiredJs)return js;hasRequiredJs=0x1;const {EventEmitter:_0x5417c3}=_0x3f0c6b,{existsSync:_0x3493a3,mkdirSync:_0x5552c4,writeFileSync:_0x170372}=_0x276f4e,{join:_0x4ebb7e,dirname:_0x3fb82c}=_0x4813c5,{createRequire:_0x421ace}=_0x96cc6a,{platform:_0x447a66,arch:_0x1466e3}=process;let _0xd112dc=null;function _0xe68ad2(){if(!_0xd112dc){if(typeof __non_webpack_require__!=='undef'+'ined')_0xd112dc=__non_webpack_require__;else try{_0xd112dc=_0x421ace(process['cwd']()+('/pack'+'age.j'+'son'));}catch(_0x2bbfc0){_0xd112dc=commonjsRequire;}}return _0xd112dc;}function _0x1de5e0(){const _0x146217=_0xe68ad2();try{const _0x2027b0=_0x146217['resol'+'ve']('nodep'+'layer'+'-addo'+'n/pac'+'kage.'+'json');return _0x3fb82c(_0x2027b0);}catch(_0x302c2){}const _0x1a5662=_0x4ebb7e(__dirname,'..');if(_0x3493a3(_0x4ebb7e(_0x1a5662,'packa'+'ge.js'+'on')))return _0x1a5662;return _0x1a5662;}let _0x3b53db=null;function _0x3dc6c1(){if(!_0x3b53db){const _0x242b1e=_0xe68ad2(),_0x3cbf2b=_0x1de5e0(),_0x1f9e5c=_0x4ebb7e(_0x3cbf2b,'prebu'+'ilds',_0x447a66+'-'+_0x1466e3,'node_'+'playe'+'r.nod'+'e'),_0x1dbfc0=_0x4ebb7e(_0x3cbf2b,'build','Relea'+'se','node_'+'playe'+'r.nod'+'e');if(_0x3493a3(_0x1f9e5c))_0x3b53db=_0x242b1e(_0x1f9e5c);else {if(_0x3493a3(_0x1dbfc0))_0x3b53db=_0x242b1e(_0x1dbfc0);else throw new Error('[node'+'playe'+'r-add'+'on]\x20C'+'annot'+'\x20find'+'\x20nati'+'ve\x20mo'+'dule.'+'\x0a'+('\x20\x20Sea'+'rched'+':\x0a')+('\x20\x20\x20\x20P'+'rebui'+'ld:\x20'+_0x1f9e5c+'\x0a')+('\x20\x20\x20\x20L'+'ocal:'+'\x20\x20\x20\x20'+_0x1dbfc0+'\x0a')+('\x20\x20Res'+'olved'+'\x20root'+':\x20'+_0x3cbf2b+'\x0a')+('\x20\x20__d'+'irnam'+'e:\x20\x20\x20'+'\x20\x20'+__dirname+'\x0a')+('\x20\x20Pla'+'tform'+':\x20\x20\x20\x20'+'\x20\x20'+_0x447a66+'-'+_0x1466e3+'\x0a\x0a')+('If\x20us'+'ing\x20w'+'ebpac'+'k/vit'+'e,\x20ad'+'d\x20to\x20'+'exter'+'nals:'+'\x0a')+('\x20\x20web'+'pack:'+'\x20exte'+'rnals'+':\x20{\x20\x27'+'nodep'+'layer'+'-addo'+'n\x27:\x20\x27'+'commo'+'njs\x20n'+'odepl'+'ayer-'+'addon'+'\x27\x20}\x0a')+('\x20\x20vit'+'e:\x20\x20\x20'+'\x20opti'+'mizeD'+'eps:\x20'+'{\x20exc'+'lude:'+'\x20[\x27no'+'depla'+'yer-a'+'ddon\x27'+']\x20}'));}}return _0x3b53db;}const _0x521bbc=new Map(),_0x1502f=['playe'+'r:cre'+'ate','playe'+'r:sta'+'rt','playe'+'r:sto'+'p','playe'+'r:des'+'troy','playe'+'r:sta'+'rtRec'+'ord','playe'+'r:sto'+'pReco'+'rd','playe'+'r:scr'+'eensh'+'ot'];class _0x22b320 extends _0x5417c3{constructor(_0x5c1adf){super();const _0x57f96e=_0x5c1adf||{};this['_play'+'er']=new(_0x3dc6c1())['NodeP'+'layer'](_0x57f96e),this['_star'+'ted']=![],this['_tria'+'lMode']=!_0x57f96e['licen'+'sePat'+'h'],this['_play'+'er']['on']('event',(_0x4ebfbb,_0x1d2a70)=>{this['emit']('event',_0x4ebfbb,_0x1d2a70);}),this['_play'+'er']['on']('info',_0x48e77d=>{this['emit']('info',_0x48e77d);}),this['_play'+'er']['on']('data',_0x1fcb1d=>{this['emit']('data',_0x1fcb1d);});}get['isTri'+'alMod'+'e'](){return this['_tria'+'lMode'];}['start'](_0x2c3a36){if(this['_star'+'ted'])throw new Error('Pipel'+'ine\x20a'+'lread'+'y\x20sta'+'rted');try{return this['_play'+'er']['start'](_0x2c3a36),this['_star'+'ted']=!![],!![];}catch(_0x1dd156){return this['emit']('error',_0x1dd156),![];}}['stop'](){this['_star'+'ted']&&(this['_play'+'er']['stop'](),this['_star'+'ted']=![]);}['start'+'Recor'+'d'](_0x314fbf){if(!this['_star'+'ted'])throw new Error('Pipel'+'ine\x20n'+'ot\x20st'+'arted');this['_play'+'er']['start'+'Recor'+'d'](_0x314fbf);}['stopR'+'ecord'](){this['_play'+'er']['stopR'+'ecord']();}static['regis'+'terIp'+'c'](_0x339e0e,_0x23f16b={}){const {getWindow:_0x384454,licensePath:_0x43da5f}=_0x23f16b,_0x34fbfe=(_0x117a79,_0x15f25d)=>{const _0x2a6cd5=_0x384454&&_0x384454();_0x2a6cd5&&!_0x2a6cd5['isDes'+'troye'+'d']()&&_0x2a6cd5['webCo'+'ntent'+'s']['send'](_0x117a79,_0x15f25d);};_0x339e0e['handl'+'e']('playe'+'r:cre'+'ate',(_0x4be8bb,_0x484a57,_0xa97ad9)=>{if(_0x521bbc['has'](_0x484a57))return {'success':![],'error':'Playe'+'r\x20alr'+'eady\x20'+'exist'+'s'};const _0x1245b1=_0xa97ad9||{};!_0x1245b1['licen'+'sePat'+'h']&&_0x43da5f&&(_0x1245b1['licen'+'sePat'+'h']=_0x43da5f);const _0x41a2a0=new _0x22b320(_0x1245b1);return _0x41a2a0['on']('event',(_0x223550,_0x56de31)=>{_0x34fbfe('playe'+'r:eve'+'nt:'+_0x484a57,{'code':_0x223550,'msg':_0x56de31});}),_0x41a2a0['on']('info',_0x4145c8=>{_0x34fbfe('playe'+'r:inf'+'o:'+_0x484a57,_0x4145c8);}),_0x41a2a0['on']('data',_0x7707df=>{_0x34fbfe('playe'+'r:dat'+'a:'+_0x484a57,_0x7707df);}),_0x521bbc['set'](_0x484a57,_0x41a2a0),{'success':!![]};}),_0x339e0e['handl'+'e']('playe'+'r:sta'+'rt',(_0x35b5e2,_0x49cba3,_0x2f33fc)=>{const _0x4381a7=_0x521bbc['get'](_0x49cba3);if(!_0x4381a7)return {'success':![],'error':'Playe'+'r\x20not'+'\x20foun'+'d'};try{const _0x4cbcae=_0x4381a7['start'](_0x2f33fc);return {'success':_0x4cbcae};}catch(_0x492d56){return {'success':![],'error':_0x492d56['messa'+'ge']};}}),_0x339e0e['handl'+'e']('playe'+'r:sto'+'p',(_0x3f0ae0,_0x33c038)=>{const _0x33db35=_0x521bbc['get'](_0x33c038);if(!_0x33db35)return {'success':![],'error':'Playe'+'r\x20not'+'\x20foun'+'d'};try{return _0x33db35['stop'](),{'success':!![]};}catch(_0x1a354d){return {'success':![],'error':_0x1a354d['messa'+'ge']};}}),_0x339e0e['handl'+'e']('playe'+'r:des'+'troy',(_0x336736,_0x2899b4)=>{const _0x15ad4d=_0x521bbc['get'](_0x2899b4);if(!_0x15ad4d)return {'success':![],'error':'Playe'+'r\x20not'+'\x20foun'+'d'};try{return _0x15ad4d['stop'](),_0x521bbc['delet'+'e'](_0x2899b4),{'success':!![]};}catch(_0x1a3cf5){return {'success':![],'error':_0x1a3cf5['messa'+'ge']};}}),_0x339e0e['handl'+'e']('playe'+'r:sta'+'rtRec'+'ord',(_0x591e67,_0x20f19c,_0x52a616)=>{const _0x42c55a=_0x521bbc['get'](_0x20f19c);if(!_0x42c55a)return {'success':![],'error':'Playe'+'r\x20not'+'\x20foun'+'d'};try{const _0xd3d496=_0x52a616||_0x4ebb7e(process['cwd'](),'recor'+'d_'+_0x20f19c+'_'+Date['now']()+'.mp4'),_0x3a8f8d=_0x3fb82c(_0xd3d496);return !_0x3493a3(_0x3a8f8d)&&_0x5552c4(_0x3a8f8d,{'recursive':!![]}),_0x42c55a['start'+'Recor'+'d'](_0xd3d496),{'success':!![],'path':_0xd3d496};}catch(_0x1d2cd2){return {'success':![],'error':_0x1d2cd2['messa'+'ge']};}}),_0x339e0e['handl'+'e']('playe'+'r:sto'+'pReco'+'rd',(_0x3e1a08,_0x49f290)=>{const _0x15fb4e=_0x521bbc['get'](_0x49f290);if(!_0x15fb4e)return {'success':![],'error':'Playe'+'r\x20not'+'\x20foun'+'d'};try{return _0x15fb4e['stopR'+'ecord'](),{'success':!![]};}catch(_0x2c6f8d){return {'success':![],'error':_0x2c6f8d['messa'+'ge']};}}),_0x339e0e['handl'+'e']('playe'+'r:scr'+'eensh'+'ot',(_0x14fddb,_0x3bc1dc,_0x4751b7,_0x46bf98)=>{try{const _0x368e2c=_0x4751b7||_0x4ebb7e(process['cwd'](),'scree'+'nshot'+'_'+_0x3bc1dc+'_'+Date['now']()+'.jpg'),_0x4c028f=_0x3fb82c(_0x368e2c);return !_0x3493a3(_0x4c028f)&&_0x5552c4(_0x4c028f,{'recursive':!![]}),_0x170372(_0x368e2c,Buffer['from'](_0x46bf98,'base6'+'4')),{'success':!![],'path':_0x368e2c};}catch(_0xcc2823){return {'success':![],'error':_0xcc2823['messa'+'ge']};}});}static['unreg'+'ister'+'Ipc'](_0x421478){_0x521bbc['forEa'+'ch'](_0x3b6ec0=>{try{_0x3b6ec0['stop']();}catch(_0x1b184f){}}),_0x521bbc['clear'](),_0x1502f['forEa'+'ch'](_0x3f5f6c=>_0x421478['remov'+'eHand'+'ler'](_0x3f5f6c));}}return js=_0x22b320,js;}
16
+ var js;
17
+ var hasRequiredJs;
18
+
19
+ function requireJs () {
20
+ if (hasRequiredJs) return js;
21
+ hasRequiredJs = 1;
22
+ const { EventEmitter } = require$$0;
23
+ const { existsSync, mkdirSync, writeFileSync } = require$$1;
24
+ const { join, dirname } = require$$2;
25
+ const { createRequire } = require$$3;
26
+ const { platform, arch } = process;
27
+
28
+ /**
29
+ * 获取原生 require,绕过 webpack/vite 的模块替换
30
+ * - webpack 环境使用 __non_webpack_require__
31
+ * - 其他环境使用 createRequire() 基于运行时目录创建
32
+ */
33
+ let _nativeRequire = null;
34
+ function getNativeRequire() {
35
+ if (!_nativeRequire) {
36
+ if (typeof __non_webpack_require__ !== 'undefined') {
37
+ _nativeRequire = __non_webpack_require__;
38
+ } else {
39
+ try {
40
+ // 基于运行时工作目录创建 require,不受打包工具对 __dirname/__filename 的篡改
41
+ _nativeRequire = createRequire(process.cwd() + '/package.json');
42
+ } catch (e) {
43
+ _nativeRequire = commonjsRequire;
44
+ }
45
+ }
46
+ }
47
+ return _nativeRequire
48
+ }
49
+
50
+ /**
51
+ * 解析包根目录(兼容打包后 __dirname 失效)
52
+ * - 优先通过 require.resolve 找到包在 node_modules 中的真实路径
53
+ * - __dirname 检查作为 fallback(未打包环境)
54
+ */
55
+ function resolvePackageRoot() {
56
+ const req = getNativeRequire();
57
+
58
+ // Strategy 1: 通过 require.resolve 找到包的真实安装路径
59
+ try {
60
+ const pkgJsonPath = req.resolve('nodeplayer-addon/package.json');
61
+ return dirname(pkgJsonPath)
62
+ } catch (e) {
63
+ // ignore
64
+ }
65
+
66
+ // Strategy 2: __dirname 上级存在 package.json(未打包环境)
67
+ const fallback = join(__dirname, '..');
68
+ if (existsSync(join(fallback, 'package.json'))) {
69
+ return fallback
70
+ }
71
+
72
+ return fallback
73
+ }
74
+
75
+ // 延迟加载 native 模块,优先从预编译目录加载
76
+ let native = null;
77
+ function getNative() {
78
+ if (!native) {
79
+ const req = getNativeRequire();
80
+ const basePath = resolvePackageRoot();
81
+
82
+ const prebuildPath = join(basePath, 'prebuilds', `${platform}-${arch}`, 'node_player.node');
83
+ const localPath = join(basePath, 'build', 'Release', 'node_player.node');
84
+
85
+ if (existsSync(prebuildPath)) {
86
+ native = req(prebuildPath);
87
+ } else if (existsSync(localPath)) {
88
+ native = req(localPath);
89
+ } else {
90
+ throw new Error(
91
+ `[nodeplayer-addon] Cannot find native module.\n` +
92
+ ` Searched:\n` +
93
+ ` Prebuild: ${prebuildPath}\n` +
94
+ ` Local: ${localPath}\n` +
95
+ ` Resolved root: ${basePath}\n` +
96
+ ` __dirname: ${__dirname}\n` +
97
+ ` Platform: ${platform}-${arch}\n\n` +
98
+ `If using webpack/vite, add to externals:\n` +
99
+ ` webpack: externals: { 'nodeplayer-addon': 'commonjs nodeplayer-addon' }\n` +
100
+ ` vite: optimizeDeps: { exclude: ['nodeplayer-addon'] }`
101
+ )
102
+ }
103
+ }
104
+ return native
105
+ }
106
+
107
+ // ============ Electron IPC 管理 ============
108
+ const _ipcPlayers = new Map();
109
+ const _ipcChannels = [
110
+ 'player:create',
111
+ 'player:start',
112
+ 'player:stop',
113
+ 'player:destroy',
114
+ 'player:startRecord',
115
+ 'player:stopRecord',
116
+ 'player:screenshot',
117
+ 'player:getMediaInfo'
118
+ ];
119
+
120
+ /**
121
+ * NodePlayer - RTSP/RTMP/KMP 流媒体播放器
122
+ *
123
+ * 支持的事件:
124
+ * - 'event': (code, msg) => {} - 管线事件
125
+ * - 'info': (info) => {} - 流信息
126
+ * - 'data': (buffer) => {} - fMP4 分片数据
127
+ *
128
+ * 使用示例:
129
+ * // 正式授权模式
130
+ * const player = new NodePlayer({ licensePath: '/path/to/license.dat' })
131
+ * player.on('info', (info) => console.log(info))
132
+ * player.on('data', (buffer) => console.log(buffer.length))
133
+ * player.start('rtsp://...')
134
+ * player.startRecord('./record.mp4')
135
+ * // ...
136
+ * player.stopRecord()
137
+ * player.stop()
138
+ *
139
+ * // 试用模式(无需许可证,累计 10 分钟)
140
+ * const player = new NodePlayer()
141
+ * player.start('rtsp://...')
142
+ */
143
+ class NodePlayer extends EventEmitter {
144
+ /**
145
+ * @param {object} [options] - 配置选项
146
+ * @param {string} [options.licensePath] - 许可证文件路径。
147
+ * 传入有效路径 → 正式授权模式;
148
+ * 不传或空字符串 → 试用模式(累计 10 分钟)。
149
+ */
150
+ constructor(options) {
151
+ super();
152
+ const opts = options || {};
153
+ this._player = new (getNative().NodePlayer)(opts);
154
+ this._started = false;
155
+ this._trialMode = !opts.licensePath;
156
+
157
+ // 转发原生事件
158
+ this._player.on('event', (code, msg) => {
159
+ this.emit('event', code, msg);
160
+ });
161
+
162
+ this._player.on('info', (info) => {
163
+ this.emit('info', info);
164
+ });
165
+
166
+ this._player.on('data', (buffer) => {
167
+ this.emit('data', buffer);
168
+ });
169
+ }
170
+
171
+ /**
172
+ * 是否处于试用模式
173
+ * @returns {boolean}
174
+ */
175
+ get isTrialMode() {
176
+ return this._trialMode
177
+ }
178
+
179
+ /**
180
+ * 启动管线(内部会先验证许可证)
181
+ * @param {string} url - 输入地址 (RTSP/RTMP/KMP)
182
+ * @returns {boolean} 是否成功启动
183
+ */
184
+ start(url) {
185
+ if (this._started) {
186
+ throw new Error('Pipeline already started')
187
+ }
188
+ try {
189
+ this._player.start(url);
190
+ this._started = true;
191
+ return true
192
+ } catch (err) {
193
+ this.emit('error', err);
194
+ return false
195
+ }
196
+ }
197
+
198
+ /**
199
+ * 停止管线
200
+ */
201
+ stop() {
202
+ if (this._started) {
203
+ this._player.stop();
204
+ this._started = false;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * 开始录像
210
+ * @param {string} outputPath - 输出 MP4 文件路径
211
+ */
212
+ startRecord(outputPath) {
213
+ if (!this._started) {
214
+ throw new Error('Pipeline not started')
215
+ }
216
+ this._player.startRecord(outputPath);
217
+ }
218
+
219
+ /**
220
+ * 停止录像(生成完整 MP4 文件)
221
+ */
222
+ stopRecord() {
223
+ this._player.stopRecord();
224
+ }
225
+
226
+ // ============ Electron IPC 静态方法 ============
227
+
228
+ /**
229
+ * 注册 Electron ipcMain 处理器,将播放器操作桥接到渲染进程。
230
+ *
231
+ * 注册后,渲染进程可通过 preload 调用:
232
+ * - ipcRenderer.invoke('player:getMediaInfo', url) // 预探测(无需 id)
233
+ * - ipcRenderer.invoke('player:create', id, options)
234
+ * - ipcRenderer.invoke('player:start', id, url)
235
+ * - ipcRenderer.invoke('player:stop', id)
236
+ * - ipcRenderer.invoke('player:destroy', id)
237
+ * - ipcRenderer.invoke('player:startRecord', id, outputPath?)
238
+ * - ipcRenderer.invoke('player:stopRecord', id)
239
+ *
240
+ * 事件通过 mainWindow.webContents.send 推送到渲染进程:
241
+ * - player:event:${id} → { code, msg }
242
+ * - player:info:${id} → info
243
+ * - player:data:${id} → Buffer
244
+ *
245
+ * @param {object} ipcMain - Electron ipcMain
246
+ * @param {object} options
247
+ * @param {function} options.getWindow - 返回当前 BrowserWindow 的函数(窗口可重建)
248
+ * @param {string} [options.licensePath] - 许可证路径(不传则为试用模式)
249
+ *
250
+ * @example
251
+ * // main.js
252
+ * const { ipcMain, app } = require('electron')
253
+ * const NodePlayer = require('nodeplayer-addon')
254
+ *
255
+ * NodePlayer.registerIpc(ipcMain, {
256
+ * getWindow: () => mainWindow,
257
+ * licensePath: app.isPackaged
258
+ * ? path.join(process.resourcesPath, 'license.dat')
259
+ * : path.join(__dirname, 'license.dat'),
260
+ * })
261
+ */
262
+ static registerIpc(ipcMain, options = {}) {
263
+ const { getWindow, licensePath } = options;
264
+
265
+ const send = (channel, data) => {
266
+ const win = getWindow && getWindow();
267
+ if (win && !win.isDestroyed()) {
268
+ win.webContents.send(channel, data);
269
+ }
270
+ };
271
+
272
+ // 预探测:分析 URL 是否可连接 + 视频/音频参数 + 截图(与 player 实例无关)
273
+ // 返回 { success, info?: { video, audio, screenshot } }
274
+ ipcMain.handle('player:getMediaInfo', async (event, url) => {
275
+ try {
276
+ const info = await NodePlayer.getMediaInfo(url);
277
+ return { success: true, info }
278
+ } catch (e) {
279
+ return { success: false, error: e.message }
280
+ }
281
+ });
282
+
283
+ ipcMain.handle('player:create', (event, id, playerOptions) => {
284
+ if (_ipcPlayers.has(id)) {
285
+ return { success: false, error: 'Player already exists' }
286
+ }
287
+
288
+ const opts = playerOptions || {};
289
+ if (!opts.licensePath && licensePath) {
290
+ opts.licensePath = licensePath;
291
+ }
292
+
293
+ const player = new NodePlayer(opts);
294
+
295
+ player.on('event', (code, msg) => {
296
+ send(`player:event:${id}`, { code, msg });
297
+ });
298
+
299
+ player.on('info', (info) => {
300
+ send(`player:info:${id}`, info);
301
+ });
302
+
303
+ player.on('data', (data) => {
304
+ send(`player:data:${id}`, data);
305
+ });
306
+
307
+ _ipcPlayers.set(id, player);
308
+ return { success: true }
309
+ });
310
+
311
+ ipcMain.handle('player:start', (event, id, url) => {
312
+ const player = _ipcPlayers.get(id);
313
+ if (!player) {
314
+ return { success: false, error: 'Player not found' }
315
+ }
316
+ try {
317
+ const result = player.start(url);
318
+ return { success: result }
319
+ } catch (e) {
320
+ return { success: false, error: e.message }
321
+ }
322
+ });
323
+
324
+ ipcMain.handle('player:stop', (event, id) => {
325
+ const player = _ipcPlayers.get(id);
326
+ if (!player) {
327
+ return { success: false, error: 'Player not found' }
328
+ }
329
+ try {
330
+ player.stop();
331
+ return { success: true }
332
+ } catch (e) {
333
+ return { success: false, error: e.message }
334
+ }
335
+ });
336
+
337
+ ipcMain.handle('player:destroy', (event, id) => {
338
+ const player = _ipcPlayers.get(id);
339
+ if (!player) {
340
+ return { success: false, error: 'Player not found' }
341
+ }
342
+ try {
343
+ player.stop();
344
+ _ipcPlayers.delete(id);
345
+ return { success: true }
346
+ } catch (e) {
347
+ return { success: false, error: e.message }
348
+ }
349
+ });
350
+
351
+ ipcMain.handle('player:startRecord', (event, id, outputPath) => {
352
+ const player = _ipcPlayers.get(id);
353
+ if (!player) {
354
+ return { success: false, error: 'Player not found' }
355
+ }
356
+ try {
357
+ const savePath = outputPath || join(
358
+ process.cwd(),
359
+ `record_${id}_${Date.now()}.mp4`
360
+ );
361
+ const dir = dirname(savePath);
362
+ if (!existsSync(dir)) {
363
+ mkdirSync(dir, { recursive: true });
364
+ }
365
+ player.startRecord(savePath);
366
+ return { success: true, path: savePath }
367
+ } catch (e) {
368
+ return { success: false, error: e.message }
369
+ }
370
+ });
371
+
372
+ ipcMain.handle('player:stopRecord', (event, id) => {
373
+ const player = _ipcPlayers.get(id);
374
+ if (!player) {
375
+ return { success: false, error: 'Player not found' }
376
+ }
377
+ try {
378
+ player.stopRecord();
379
+ return { success: true }
380
+ } catch (e) {
381
+ return { success: false, error: e.message }
382
+ }
383
+ });
384
+
385
+ ipcMain.handle('player:screenshot', (event, id, outputPath, base64Data) => {
386
+ try {
387
+ const savePath = outputPath || join(
388
+ process.cwd(),
389
+ `screenshot_${id}_${Date.now()}.jpg`
390
+ );
391
+ const dir = dirname(savePath);
392
+ if (!existsSync(dir)) {
393
+ mkdirSync(dir, { recursive: true });
394
+ }
395
+ writeFileSync(savePath, Buffer.from(base64Data, 'base64'));
396
+ return { success: true, path: savePath }
397
+ } catch (e) {
398
+ return { success: false, error: e.message }
399
+ }
400
+ });
401
+ }
402
+
403
+ /**
404
+ * 注销 Electron ipcMain 处理器,停止所有播放器并清理资源。
405
+ *
406
+ * @param {object} ipcMain - Electron ipcMain
407
+ *
408
+ * @example
409
+ * mainWindow.on('closed', () => {
410
+ * NodePlayer.unregisterIpc(ipcMain)
411
+ * })
412
+ */
413
+ static unregisterIpc(ipcMain) {
414
+ _ipcPlayers.forEach(player => {
415
+ try { player.stop(); } catch (e) { /* ignore */ }
416
+ });
417
+ _ipcPlayers.clear();
418
+
419
+ _ipcChannels.forEach(ch => ipcMain.removeHandler(ch));
420
+ }
421
+
422
+ // ============ 流媒体探测(静态方法)============
423
+
424
+ /**
425
+ * 异步获取流媒体信息(不阻塞 Node 事件循环)。
426
+ *
427
+ * 用于在添加 URL 到播放器之前,先分析流:
428
+ * - 是否可连接
429
+ * - 视频/音频编码、分辨率、采样率
430
+ * - 截取首帧作为预览图(MJPEG Buffer)
431
+ *
432
+ * @param {string} url - 流地址 (RTSP/RTMP/KMP/HTTP...)
433
+ * @returns {Promise<{video: object, audio: object, screenshot: Buffer|null}>}
434
+ * - video: { codecId:number, width:number, height:number }
435
+ * codecId 为 AV_CODEC_ID_* 原始值(27=H264, 173=HEVC...)
436
+ * - audio: { codecId:number, sampleRate:number, channels:number }
437
+ * 无音频时三个字段均为 0
438
+ * - screenshot: MJPEG 二进制 Buffer;解码失败/无视频时为 null
439
+ *
440
+ * @example
441
+ * const info = await NodePlayer.getMediaInfo('rtsp://...')
442
+ * console.log(info.video.width + 'x' + info.video.height)
443
+ * if (info.screenshot) {
444
+ * const base64 = info.screenshot.toString('base64')
445
+ * previewImg.src = 'data:image/jpeg;base64,' + base64
446
+ * }
447
+ */
448
+ static getMediaInfo(url) {
449
+ if (typeof url !== 'string' || !url) {
450
+ // 同步抛出 TypeError,遵循 Node.js fs.promises.* 的惯例
451
+ throw new TypeError('url must be a non-empty string')
452
+ }
453
+ // 原生层返回 Promise;用 Promise.resolve 统一异常路径,避免被调用方漏 catch
454
+ return Promise.resolve(getNative().getMediaInfo(url))
455
+ }
456
+
457
+ }
458
+
459
+ js = NodePlayer;
460
+ return js;
461
+ }
17
462
 
18
463
  var jsExports = requireJs();
19
464
  var index = /*@__PURE__*/getDefaultExportFromCjs(jsExports);