在快速集成中,我们使用官方推荐的Electron Forge创建程序。可以用来开发react但是需要很多配置,我们在这个例子中换一个更简单的脚手架: create-electron
npm create @quick-start/electron> npx
> "create-electron"
✔ Project name: … electron-app-react
✔ Select a framework: › react
✔ Add TypeScript? … No / Yes
✔ Add Electron updater plugin? … No / Yes
✔ Enable Electron download mirror proxy? … No / Yes
Scaffolding project in electron-app-react...
Done. Now run:
cd electron-app-react
npm install
npm run dev执行命令
npm i nodeplayer-addonimport { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
import NodePlayerAddon from 'nodeplayer-addon'
function createWindow() {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false
}
})
mainWindow.on('ready-to-show', () => {
mainWindow.maximize();
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
mainWindow.on('closed', () => {
NodePlayerAddon.unregisterIpc(ipcMain)
})
NodePlayerAddon.registerIpc(ipcMain, {
getWindow: () => mainWindow,
licensePath: app.isPackaged
? join(process.resourcesPath, 'license.dat')
: join(__dirname, 'license.dat'),
})
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.electron')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
// IPC test
ipcMain.on('ping', () => console.log('pong'))
createWindow()
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('electronAPI', {
createPlayer: (id) => ipcRenderer.invoke('player:create', id),
startPlayer: (id, url) => ipcRenderer.invoke('player:start', id, url),
stopPlayer: (id) => ipcRenderer.invoke('player:stop', id),
destroyPlayer: (id) => ipcRenderer.invoke('player:destroy', id),
startRecord: (id, filePath) => ipcRenderer.invoke('player:startRecord', id, filePath),
stopRecord: (id) => ipcRenderer.invoke('player:stopRecord', id),
// 截图:将渲染进程生成的 JPG base64 数据保存到指定路径(默认由主进程自动生成)
saveScreenshot: (id, outputPath, base64Data) => ipcRenderer.invoke('player:saveScreenshot', id, outputPath, base64Data),
// 预探测:在创建播放器前分析 URL(连接性 / 编码 / 分辨率 / 首帧截图)
getMediaInfo: (url) => ipcRenderer.invoke('player:getMediaInfo', url),
onEvent: (id, callback) => {
const channel = `player:event:${id}`
const handler = (event, data) => callback(data)
ipcRenderer.on(channel, handler)
return () => ipcRenderer.removeListener(channel, handler)
},
onInfo: (id, callback) => {
const channel = `player:info:${id}`
const handler = (event, info) => callback(info)
ipcRenderer.on(channel, handler)
return () => ipcRenderer.removeListener(channel, handler)
},
onData: (id, callback) => {
const channel = `player:data:${id}`
const handler = (event, data) => callback(data)
ipcRenderer.on(channel, handler)
return () => ipcRenderer.removeListener(channel, handler)
}
})import { useRef, useEffect, useState, useCallback } from 'react'
import NodePlayerView from 'nodeplayer-addon/NodePlayerView'
function App() {
const videoRef = useRef(null)
const playerRef = useRef(null)
const [url, setUrl] = useState('')
const [status, setStatus] = useState('')
const [recording, setRecording] = useState(false)
useEffect(() => {
const player = new NodePlayerView(videoRef.current, `player-${Date.now()}`)
player.on('event', (code, msg) => {
if (code === 3001) setRecording(true)
if (code === 3002 || code === 3003) setRecording(false)
})
player.on('error', (err) => { setStatus(err.message) })
playerRef.current = player
return () => {
player.stop()
}
}, [])
const handleStart = useCallback(() => {
playerRef.current?.start(url)
}, [url])
const handleStop = useCallback(() => {
playerRef.current?.stop()
}, [])
const handleRecord = useCallback(() => {
const p = playerRef.current
if (!p) return
p.isRecording ? p.stopRecord() : p.startRecord()
}, [])
const handleScreenshot = useCallback(async () => {
const p = playerRef.current
if (!p) return
const r = await p.saveScreenshot()
setStatus(r.success ? '截图已保存:' + r.path : (r.error || '截图失败'))
}, [])
return (
<div style={{ maxWidth: 800, margin: '10px', fontFamily: 'sans-serif' }}>
<div style={{ position: 'relative', width: '100%', aspectRatio: '16 / 9', background: '#000' }}>
<video
ref={videoRef}
autoPlay
muted
playsInline
style={{ width: '100%', height: '100%', display: 'block', objectFit: 'contain' }}
/>
{status && (
<div
style={{
position: 'absolute',
top: 8,
left: 8,
color: '#0f0',
fontFamily: 'monospace',
fontSize: 12
}}
>
{status}
</div>
)}
</div>
<div style={{ display: 'flex', gap: 8, padding: '10px 0' }}>
<input
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="rtsp:// 或 http://"
style={{ flex: 1, padding: '6px 10px', fontSize: 14 }}
/>
<button onClick={handleStart}>播放</button>
<button onClick={handleStop}>停止</button>
<button onClick={handleRecord}>{recording ? '停止录像' : '录像'}</button>
<button onClick={handleScreenshot}>截图</button>
</div>
</div>
)
}
export default App上面的示例只演示了「播放 / 停止 / 录像 / 截图」。registerIpc 实际在主进程注册了更多能力,下面按需选用。
播放器通过 player.on('event', (code, msg) => {}) 推送事件,常用码如下:
| 范围 | code | 含义 |
|---|---|---|
| 连接 | 1000 | 正在连接 |
| 1001 | 已连接 | |
| 1002 | 连接失败 | |
| 1003 | 重连中 | |
| 1004 | 已断开 | |
| 1005 | 网络错误 | |
| 1006 | 连接超时 | |
| 录像 | 3001 | 录像开始 |
| 3002 | 录像停止 | |
| 3003 | 录像错误 |
流的编码、分辨率、采样率等参数通过
player.on('info', (info) => {})单独推送,不走event。
在加入播放列表前,可先探测地址是否可达、获取音视频参数并截取首帧预览图。
👉 完整 API、codec 参考表与示例:get-media-info.md
NodePlayerView 提供两种截图方式,均在流就绪后调用:
player.captureScreenshot(quality?)→ 返回data:image/jpeg;base64,...字符串(仅在内存中,不落盘)player.saveScreenshot(outputPath?, quality?)→ 通过 IPC 将 JPG 写入磁盘,返回{ success, path },路径省略时由主进程自动生成
// 直接预览(不落盘)
const dataUrl = player.captureScreenshot(0.9)
if (dataUrl) snapshotImg.src = dataUrl
// 保存到文件
const r = await player.saveScreenshot()
if (r.success) console.log('已保存:', r.path)- QQ: 281269007
- Email: service@nodemedia.cn


