camera.js 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. // electron/utils/readConfig.js
  2. const path = require('path');
  3. const fs = require('fs');
  4. const Log = require('ee-core/log');
  5. const { spawn } = require('child_process');
  6. const { EventEmitter } = require('events');
  7. const { liveShow, liveHide, setParams, capture, getParams,CMD,captureLive,closeOtherWindow,minimizeSmartShooter } = require('../api/camera');
  8. const { dialog } = require('electron'); // 引入 electron 的 dialog 模块
  9. const CoreWindow = require("ee-core/electron/window");
  10. const { readConfigFile, updateConfigFile } = require('../utils/config');
  11. const defaultConfig = require('./config.default');
  12. // 进程状态管理
  13. let cameraProcess = null; // 子进程引用
  14. let isProcessRunning = false; // 进程运行状态
  15. const processEmitter = new EventEmitter(); // 进程事件发射器
  16. // Smart Shooter 5 / 6 与 digiCamControl 并存。
  17. // 5 和 6 共用同一套 External API(54543 事件 / 54544 请求),不能同时开,按 controlType 只启动其中一个。
  18. const CONTROLLERS = {
  19. digiCamControl: {
  20. exe: "CameraControl.exe",
  21. pathKey: "digiCamControlPath",
  22. label: "digiCamControl",
  23. },
  24. SmartShooter: {
  25. exe: "SmartShooter5.exe",
  26. pathKey: "SmartShooterPath",
  27. label: "Smart Shooter 5",
  28. },
  29. SmartShooter6: {
  30. exe: "SmartShooter6.exe",
  31. pathKey: "SmartShooter6Path",
  32. label: "Smart Shooter 6",
  33. },
  34. }
  35. // 历史配置 controlType 仍是 SmartShooter,保持原行为
  36. function normalizeControlType(type) {
  37. return CONTROLLERS[type] ? type : "SmartShooter"
  38. }
  39. function isSmartShooterType(type) {
  40. return type === "SmartShooter" || type === "SmartShooter6"
  41. }
  42. function sameDir(a, b) {
  43. if (!a || !b) return false
  44. const norm = (p) => path.normalize(String(p)).replace(/[\\/]+$/, "").toLowerCase()
  45. return norm(a) === norm(b)
  46. }
  47. // 只在仍使用出厂的 Smart Shooter 6 目录时回退,手动改过安装目录的保持原选择
  48. function isDefaultSmartShooter6Dir(dir) {
  49. return sameDir(dir, defaultConfig.SmartShooter6Path) || sameDir(dir, defaultConfig.controlPath)
  50. }
  51. function findSmartShooter5Dir(config) {
  52. const candidates = [config.SmartShooterPath, defaultConfig.SmartShooterPath]
  53. const seen = new Set()
  54. for (const dir of candidates) {
  55. if (!dir) continue
  56. const key = path.normalize(String(dir)).replace(/[\\/]+$/, "").toLowerCase()
  57. if (seen.has(key)) continue
  58. seen.add(key)
  59. if (fs.existsSync(path.join(dir, CONTROLLERS.SmartShooter.exe))) return dir
  60. }
  61. return ""
  62. }
  63. function saveControllerChoice(type, dir) {
  64. const spec = CONTROLLERS[type]
  65. if (!spec || !dir) return
  66. updateConfigFile({
  67. controlType: type,
  68. controlPath: dir,
  69. [spec.pathKey]: dir,
  70. })
  71. }
  72. function controllerChoiceMatches(config, type, dir) {
  73. const spec = CONTROLLERS[type]
  74. if (!spec) return false
  75. return config.controlType === type
  76. && sameDir(config.controlPath, dir)
  77. && sameDir(config[spec.pathKey], dir)
  78. }
  79. function uniqueDirs(dirs) {
  80. const result = []
  81. for (const dir of dirs) {
  82. if (!dir) continue
  83. if (result.some((item) => sameDir(item, dir))) continue
  84. result.push(dir)
  85. }
  86. return result
  87. }
  88. function rememberController(config, detected, dir) {
  89. if (!controllerChoiceMatches(config, detected, dir)) {
  90. Log.info(`按安装目录识别为 ${CONTROLLERS[detected].label}: ${dir}`)
  91. saveControllerChoice(detected, dir)
  92. }
  93. return {
  94. type: detected,
  95. ...CONTROLLERS[detected],
  96. dir,
  97. }
  98. }
  99. function getController() {
  100. const config = readConfigFile() || {}
  101. const type = normalizeControlType(config.controlType)
  102. const spec = CONTROLLERS[type]
  103. const dir = config.controlPath || config[spec.pathKey] || ""
  104. // 上次选中的目录里有 exe 时,以那个 exe 为准。
  105. // 避免 controlPath 已是 Smart Shooter 6,controlType 仍是 digiCamControl。
  106. const selected = detectController(config.controlPath)
  107. if (selected) return rememberController(config, selected, config.controlPath)
  108. // controlPath 无效时,再找该类型自己的目录和出厂目录,避免坏路径挡住已经装好的程序。
  109. const fallbackDirs = uniqueDirs([
  110. config[spec.pathKey],
  111. defaultConfig[spec.pathKey],
  112. type === "SmartShooter6" ? defaultConfig.controlPath : "",
  113. ])
  114. for (const candidate of fallbackDirs) {
  115. if (detectController(candidate) === type) return rememberController(config, type, candidate)
  116. }
  117. // 默认 Smart Shooter 6 目录下没有程序时,再找 Smart Shooter 5。只影响本次启动,不回写配置。
  118. if (type === "SmartShooter6" && isDefaultSmartShooter6Dir(dir) && !fs.existsSync(path.join(dir, spec.exe))) {
  119. const ss5Dir = findSmartShooter5Dir(config)
  120. if (ss5Dir) {
  121. Log.info(`默认目录未找到 SmartShooter6.exe,改用 Smart Shooter 5: ${ss5Dir}`)
  122. return {
  123. type: "SmartShooter",
  124. ...CONTROLLERS.SmartShooter,
  125. dir: ss5Dir,
  126. }
  127. }
  128. }
  129. return {
  130. type,
  131. ...spec,
  132. dir,
  133. }
  134. }
  135. function getExePath () {
  136. const controller = getController()
  137. const exePath = controller.dir ? path.join(controller.dir, controller.exe) : ""
  138. console.log('ex============ePath');
  139. console.log(exePath);
  140. return exePath
  141. }
  142. // 用户手动选安装目录时,按目录里实际存在的 exe 识别,5 和 6 互不覆盖
  143. function detectController(dir) {
  144. if (!dir) return ""
  145. if (fs.existsSync(path.join(dir, CONTROLLERS.SmartShooter6.exe))) return "SmartShooter6"
  146. if (fs.existsSync(path.join(dir, CONTROLLERS.SmartShooter.exe))) return "SmartShooter"
  147. if (fs.existsSync(path.join(dir, CONTROLLERS.digiCamControl.exe))) return "digiCamControl"
  148. return ""
  149. }
  150. let ensureControllerPromise = null
  151. async function checkCameraControlCmdExists() {
  152. if (!ensureControllerPromise) {
  153. ensureControllerPromise = ensureCameraController().finally(() => {
  154. ensureControllerPromise = null
  155. })
  156. }
  157. return ensureControllerPromise
  158. }
  159. async function ensureCameraController() {
  160. try {
  161. // 拼接 CameraControlCmd.exe 的完整路径
  162. let exePath = getExePath()
  163. // 检查文件是否存在
  164. const exists = await fs.promises.access(exePath, fs.constants.F_OK)
  165. .then(() => true)
  166. .catch(() => false);
  167. if (!exists) {
  168. // 弹出文件夹选择对话框
  169. const dialogOptions = {
  170. title: '选择 相机控制安装软件 文件夹',
  171. properties: ['openDirectory']
  172. }
  173. const mainWindow = CoreWindow.getMainWindow()
  174. const { canceled, filePaths } = mainWindow && !mainWindow.isDestroyed()
  175. ? await dialog.showOpenDialog(mainWindow, dialogOptions)
  176. : await dialog.showOpenDialog(dialogOptions);
  177. if (!canceled && filePaths.length > 0) {
  178. const selectedPath = filePaths[0];
  179. const detected = detectController(selectedPath);
  180. if (!detected) {
  181. console.error('所选目录不是已知的相机控制软件');
  182. return {
  183. status:-1,
  184. msg:"所选目录中未找到 SmartShooter6.exe、SmartShooter5.exe 或 CameraControl.exe",
  185. }
  186. }
  187. saveControllerChoice(detected, selectedPath)
  188. } else {
  189. console.error('用户未选择文件夹');
  190. return {
  191. status:-1,
  192. msg:"无法找到 SmartShooter6.exe、SmartShooter5.exe 或 CameraControl.exe",
  193. }
  194. }
  195. }
  196. const res = await openCameraControlCmd();
  197. // Smart Shooter 5 / 6 启动后最小化窗口;digiCamControl 走自己的最小化命令
  198. if(isSmartShooterType(getController().type)){
  199. // 等待软件启动完成
  200. setTimeout(async () => {
  201. try {
  202. await minimizeSmartShooter();
  203. } catch (error) {
  204. console.log('最小化SmartShooter失败:', error);
  205. }
  206. }, 3000); // 等待3秒让软件完全启动
  207. }
  208. return res;
  209. } catch (error) {
  210. Log.error('检查 第三方相机控制器 是否存在时出错:', error);
  211. throw error;
  212. }
  213. }
  214. function openCameraControlCmd() {
  215. return new Promise((resolve, reject) => {
  216. const exePath = getExePath()
  217. fs.promises.access(exePath, fs.constants.F_OK).then(() => {
  218. let settled = false
  219. const finish = (fn, value) => {
  220. if (settled) return
  221. settled = true
  222. fn(value)
  223. }
  224. let child
  225. try {
  226. child = spawn(exePath, [], {
  227. cwd: path.dirname(exePath),
  228. stdio: 'ignore',
  229. })
  230. } catch (error) {
  231. Log.error('无法启动第三方相机控制器:', error)
  232. finish(reject, error)
  233. return
  234. }
  235. cameraProcess = child
  236. isProcessRunning = true
  237. console.log('[CAMERA-DEBUG] spawn PID:', child.pid, 'cwd:', path.dirname(exePath), '时间:', new Date().toISOString());
  238. child.on('close', (code) => {
  239. console.log('[CAMERA-DEBUG] close 事件触发! PID:', cameraProcess?.pid, '退出码:', code, '时间:', new Date().toISOString());
  240. isProcessRunning = false;
  241. cameraProcess = null;
  242. Log.info(`相机控制软件已关闭,退出码: ${code}`);
  243. processEmitter.emit('camera-process-closed');
  244. });
  245. child.on('error', (err) => {
  246. console.log('[CAMERA-DEBUG] error 事件触发:', err.message);
  247. isProcessRunning = false;
  248. cameraProcess = null;
  249. Log.error('相机控制软件进程错误:', err);
  250. finish(reject, err)
  251. });
  252. // 图形界面程序通常没有 stdout,进程拉起即视为启动成功
  253. child.once('spawn', () => finish(resolve, true))
  254. if (child.pid) finish(resolve, true)
  255. }).catch((error) => {
  256. Log.error('无法找到或运行 第三方相机控制器:', error);
  257. reject(error)
  258. })
  259. })
  260. }
  261. async function closeCameraControlTips() {
  262. try {
  263. await closeOtherWindow()
  264. }catch (e) {
  265. console.log(e)
  266. }
  267. }
  268. module.exports = {
  269. checkCameraControlCmdExists,
  270. closeCameraControlTips,
  271. minimizeSmartShooter,
  272. CONTROLLERS,
  273. isSmartShooterType,
  274. getController,
  275. // 进程状态管理
  276. isProcessRunning: () => isProcessRunning,
  277. onProcessClosed: (callback) => processEmitter.on('camera-process-closed', callback),
  278. removeProcessClosedListener: (callback) => processEmitter.removeListener('camera-process-closed', callback),
  279. };