camera.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  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 getController() {
  80. const config = readConfigFile() || {}
  81. const type = normalizeControlType(config.controlType)
  82. const spec = CONTROLLERS[type]
  83. const dir = config.controlPath || config[spec.pathKey] || ""
  84. // 以目录里实际存在的 exe 为准。
  85. // 否则会出现 controlPath 已是 Smart Shooter 6,controlType 仍是 digiCamControl,
  86. // 程序却去这个目录里找 CameraControl.exe。
  87. const detected = detectController(dir)
  88. if (detected) {
  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. // 默认 Smart Shooter 6 目录下没有程序时,再找 Smart Shooter 5。只影响本次启动,不回写配置。
  100. if (type === "SmartShooter6" && isDefaultSmartShooter6Dir(dir) && !fs.existsSync(path.join(dir, spec.exe))) {
  101. const ss5Dir = findSmartShooter5Dir(config)
  102. if (ss5Dir) {
  103. Log.info(`默认目录未找到 SmartShooter6.exe,改用 Smart Shooter 5: ${ss5Dir}`)
  104. return {
  105. type: "SmartShooter",
  106. ...CONTROLLERS.SmartShooter,
  107. dir: ss5Dir,
  108. }
  109. }
  110. }
  111. return {
  112. type,
  113. ...spec,
  114. dir,
  115. }
  116. }
  117. function getExePath () {
  118. const controller = getController()
  119. const exePath = controller.dir ? path.join(controller.dir, controller.exe) : ""
  120. console.log('ex============ePath');
  121. console.log(exePath);
  122. return exePath
  123. }
  124. // 用户手动选安装目录时,按目录里实际存在的 exe 识别,5 和 6 互不覆盖
  125. function detectController(dir) {
  126. if (!dir) return ""
  127. if (fs.existsSync(path.join(dir, CONTROLLERS.SmartShooter6.exe))) return "SmartShooter6"
  128. if (fs.existsSync(path.join(dir, CONTROLLERS.SmartShooter.exe))) return "SmartShooter"
  129. if (fs.existsSync(path.join(dir, CONTROLLERS.digiCamControl.exe))) return "digiCamControl"
  130. return ""
  131. }
  132. let ensureControllerPromise = null
  133. async function checkCameraControlCmdExists() {
  134. if (!ensureControllerPromise) {
  135. ensureControllerPromise = ensureCameraController().finally(() => {
  136. ensureControllerPromise = null
  137. })
  138. }
  139. return ensureControllerPromise
  140. }
  141. async function ensureCameraController() {
  142. try {
  143. // 拼接 CameraControlCmd.exe 的完整路径
  144. let exePath = getExePath()
  145. // 检查文件是否存在
  146. const exists = await fs.promises.access(exePath, fs.constants.F_OK)
  147. .then(() => true)
  148. .catch(() => false);
  149. if (!exists) {
  150. // 弹出文件夹选择对话框
  151. const { canceled, filePaths } = await dialog.showOpenDialog({
  152. title: '选择 相机控制安装软件 文件夹',
  153. properties: ['openDirectory']
  154. });
  155. if (!canceled && filePaths.length > 0) {
  156. const selectedPath = filePaths[0];
  157. const detected = detectController(selectedPath);
  158. if (!detected) {
  159. console.error('所选目录不是已知的相机控制软件');
  160. return {
  161. status:-1,
  162. msg:"所选目录中未找到 SmartShooter6.exe、SmartShooter5.exe 或 CameraControl.exe",
  163. }
  164. }
  165. saveControllerChoice(detected, selectedPath)
  166. } else {
  167. console.error('用户未选择文件夹');
  168. return {
  169. status:-1,
  170. msg:"无法找到 SmartShooter6.exe、SmartShooter5.exe 或 CameraControl.exe",
  171. }
  172. }
  173. }
  174. const res = await openCameraControlCmd();
  175. // Smart Shooter 5 / 6 启动后最小化窗口;digiCamControl 走自己的最小化命令
  176. if(isSmartShooterType(getController().type)){
  177. // 等待软件启动完成
  178. setTimeout(async () => {
  179. try {
  180. await minimizeSmartShooter();
  181. } catch (error) {
  182. console.log('最小化SmartShooter失败:', error);
  183. }
  184. }, 3000); // 等待3秒让软件完全启动
  185. }
  186. return res;
  187. } catch (error) {
  188. Log.error('检查 第三方相机控制器 是否存在时出错:', error);
  189. throw error;
  190. }
  191. }
  192. function openCameraControlCmd() {
  193. return new Promise((resolve, reject) => {
  194. const exePath = getExePath()
  195. fs.promises.access(exePath, fs.constants.F_OK).then(() => {
  196. let settled = false
  197. const finish = (fn, value) => {
  198. if (settled) return
  199. settled = true
  200. fn(value)
  201. }
  202. let child
  203. try {
  204. child = spawn(exePath, [], { cwd: path.dirname(exePath) })
  205. } catch (error) {
  206. Log.error('无法启动第三方相机控制器:', error)
  207. finish(reject, error)
  208. return
  209. }
  210. cameraProcess = child
  211. isProcessRunning = true
  212. console.log('[CAMERA-DEBUG] spawn PID:', child.pid, 'cwd:', path.dirname(exePath), '时间:', new Date().toISOString());
  213. child.stdout.on('data', (data) => {
  214. console.log('[CAMERA-DEBUG] stdout 收到数据:', data.toString().substring(0, 100));
  215. });
  216. child.on('close', (code) => {
  217. console.log('[CAMERA-DEBUG] close 事件触发! PID:', cameraProcess?.pid, '退出码:', code, '时间:', new Date().toISOString());
  218. isProcessRunning = false;
  219. cameraProcess = null;
  220. Log.info(`相机控制软件已关闭,退出码: ${code}`);
  221. processEmitter.emit('camera-process-closed');
  222. });
  223. child.on('error', (err) => {
  224. console.log('[CAMERA-DEBUG] error 事件触发:', err.message);
  225. isProcessRunning = false;
  226. cameraProcess = null;
  227. Log.error('相机控制软件进程错误:', err);
  228. finish(reject, err)
  229. });
  230. // 图形界面程序通常没有 stdout,进程拉起即视为启动成功
  231. child.once('spawn', () => finish(resolve, true))
  232. if (child.pid) finish(resolve, true)
  233. }).catch((error) => {
  234. Log.error('无法找到或运行 第三方相机控制器:', error);
  235. reject(error)
  236. })
  237. })
  238. }
  239. async function closeCameraControlTips() {
  240. try {
  241. await closeOtherWindow()
  242. }catch (e) {
  243. console.log(e)
  244. }
  245. }
  246. module.exports = {
  247. checkCameraControlCmdExists,
  248. closeCameraControlTips,
  249. minimizeSmartShooter,
  250. CONTROLLERS,
  251. isSmartShooterType,
  252. getController,
  253. // 进程状态管理
  254. isProcessRunning: () => isProcessRunning,
  255. onProcessClosed: (callback) => processEmitter.on('camera-process-closed', callback),
  256. removeProcessClosedListener: (callback) => processEmitter.removeListener('camera-process-closed', callback),
  257. };