markupr 2.1.8

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.
Files changed (299) hide show
  1. package/.claude/commands/review-feedback.md +47 -0
  2. package/.eslintrc.json +35 -0
  3. package/.github/CODEOWNERS +16 -0
  4. package/.github/FUNDING.yml +1 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
  7. package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
  8. package/.github/dependabot.yml +70 -0
  9. package/.github/workflows/ci.yml +184 -0
  10. package/.github/workflows/deploy-landing.yml +134 -0
  11. package/.github/workflows/nightly.yml +288 -0
  12. package/.github/workflows/release.yml +318 -0
  13. package/CHANGELOG.md +127 -0
  14. package/CLAUDE.md +137 -0
  15. package/CODE_OF_CONDUCT.md +9 -0
  16. package/CONTRIBUTING.md +390 -0
  17. package/LICENSE +21 -0
  18. package/PRODUCT_VISION.md +277 -0
  19. package/README.md +517 -0
  20. package/SECURITY.md +51 -0
  21. package/SIGNING_INSTRUCTIONS.md +284 -0
  22. package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
  23. package/assets/svg-source/dmg-background.svg +70 -0
  24. package/assets/svg-source/icon.svg +20 -0
  25. package/assets/svg-source/tray-icon-processing.svg +7 -0
  26. package/assets/svg-source/tray-icon-recording.svg +7 -0
  27. package/assets/svg-source/tray-icon.svg +6 -0
  28. package/assets/tray-complete.png +0 -0
  29. package/assets/tray-complete@2x.png +0 -0
  30. package/assets/tray-completeTemplate.png +0 -0
  31. package/assets/tray-completeTemplate@2x.png +0 -0
  32. package/assets/tray-error.png +0 -0
  33. package/assets/tray-error@2x.png +0 -0
  34. package/assets/tray-errorTemplate.png +0 -0
  35. package/assets/tray-errorTemplate@2x.png +0 -0
  36. package/assets/tray-icon-processing.png +0 -0
  37. package/assets/tray-icon-processing@2x.png +0 -0
  38. package/assets/tray-icon-processingTemplate.png +0 -0
  39. package/assets/tray-icon-processingTemplate@2x.png +0 -0
  40. package/assets/tray-icon-recording.png +0 -0
  41. package/assets/tray-icon-recording@2x.png +0 -0
  42. package/assets/tray-icon-recordingTemplate.png +0 -0
  43. package/assets/tray-icon-recordingTemplate@2x.png +0 -0
  44. package/assets/tray-icon.png +0 -0
  45. package/assets/tray-icon@2x.png +0 -0
  46. package/assets/tray-iconTemplate.png +0 -0
  47. package/assets/tray-iconTemplate@2x.png +0 -0
  48. package/assets/tray-idle.png +0 -0
  49. package/assets/tray-idle@2x.png +0 -0
  50. package/assets/tray-idleTemplate.png +0 -0
  51. package/assets/tray-idleTemplate@2x.png +0 -0
  52. package/assets/tray-processing-0.png +0 -0
  53. package/assets/tray-processing-0@2x.png +0 -0
  54. package/assets/tray-processing-0Template.png +0 -0
  55. package/assets/tray-processing-0Template@2x.png +0 -0
  56. package/assets/tray-processing-1.png +0 -0
  57. package/assets/tray-processing-1@2x.png +0 -0
  58. package/assets/tray-processing-1Template.png +0 -0
  59. package/assets/tray-processing-1Template@2x.png +0 -0
  60. package/assets/tray-processing-2.png +0 -0
  61. package/assets/tray-processing-2@2x.png +0 -0
  62. package/assets/tray-processing-2Template.png +0 -0
  63. package/assets/tray-processing-2Template@2x.png +0 -0
  64. package/assets/tray-processing-3.png +0 -0
  65. package/assets/tray-processing-3@2x.png +0 -0
  66. package/assets/tray-processing-3Template.png +0 -0
  67. package/assets/tray-processing-3Template@2x.png +0 -0
  68. package/assets/tray-processing.png +0 -0
  69. package/assets/tray-processing@2x.png +0 -0
  70. package/assets/tray-processingTemplate.png +0 -0
  71. package/assets/tray-processingTemplate@2x.png +0 -0
  72. package/assets/tray-recording.png +0 -0
  73. package/assets/tray-recording@2x.png +0 -0
  74. package/assets/tray-recordingTemplate.png +0 -0
  75. package/assets/tray-recordingTemplate@2x.png +0 -0
  76. package/build/DMG_BACKGROUND_SPEC.md +50 -0
  77. package/build/dmg-background.png +0 -0
  78. package/build/dmg-background@2x.png +0 -0
  79. package/build/entitlements.mac.inherit.plist +27 -0
  80. package/build/entitlements.mac.plist +41 -0
  81. package/build/favicon-16.png +0 -0
  82. package/build/favicon-180.png +0 -0
  83. package/build/favicon-192.png +0 -0
  84. package/build/favicon-32.png +0 -0
  85. package/build/favicon-48.png +0 -0
  86. package/build/favicon-512.png +0 -0
  87. package/build/favicon-64.png +0 -0
  88. package/build/icon-128.png +0 -0
  89. package/build/icon-16.png +0 -0
  90. package/build/icon-24.png +0 -0
  91. package/build/icon-256.png +0 -0
  92. package/build/icon-32.png +0 -0
  93. package/build/icon-48.png +0 -0
  94. package/build/icon-64.png +0 -0
  95. package/build/icon.icns +0 -0
  96. package/build/icon.ico +0 -0
  97. package/build/icon.iconset/icon_128x128.png +0 -0
  98. package/build/icon.iconset/icon_128x128@2x.png +0 -0
  99. package/build/icon.iconset/icon_16x16.png +0 -0
  100. package/build/icon.iconset/icon_16x16@2x.png +0 -0
  101. package/build/icon.iconset/icon_256x256.png +0 -0
  102. package/build/icon.iconset/icon_256x256@2x.png +0 -0
  103. package/build/icon.iconset/icon_32x32.png +0 -0
  104. package/build/icon.iconset/icon_32x32@2x.png +0 -0
  105. package/build/icon.iconset/icon_512x512.png +0 -0
  106. package/build/icon.iconset/icon_512x512@2x.png +0 -0
  107. package/build/icon.png +0 -0
  108. package/build/installer-header.bmp +0 -0
  109. package/build/installer-header.png +0 -0
  110. package/build/installer-sidebar.bmp +0 -0
  111. package/build/installer-sidebar.png +0 -0
  112. package/build/installer.nsh +45 -0
  113. package/build/overlay-processing.png +0 -0
  114. package/build/overlay-recording.png +0 -0
  115. package/build/toolbar-record.png +0 -0
  116. package/build/toolbar-screenshot.png +0 -0
  117. package/build/toolbar-settings.png +0 -0
  118. package/build/toolbar-stop.png +0 -0
  119. package/dist/main/index.mjs +12612 -0
  120. package/dist/preload/index.mjs +907 -0
  121. package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
  122. package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
  123. package/dist/renderer/index.html +27 -0
  124. package/docs/AI_AGENT_QUICKSTART.md +42 -0
  125. package/docs/AI_PIPELINE_DESIGN.md +595 -0
  126. package/docs/API.md +514 -0
  127. package/docs/ARCHITECTURE.md +460 -0
  128. package/docs/CONFIGURATION.md +336 -0
  129. package/docs/DEVELOPMENT.md +508 -0
  130. package/docs/EXPORT_FORMATS.md +451 -0
  131. package/docs/GETTING_STARTED.md +236 -0
  132. package/docs/KEYBOARD_SHORTCUTS.md +334 -0
  133. package/docs/TROUBLESHOOTING.md +418 -0
  134. package/docs/landing/index.html +672 -0
  135. package/docs/landing/script.js +342 -0
  136. package/docs/landing/styles.css +1543 -0
  137. package/electron-builder.yml +140 -0
  138. package/electron.vite.config.ts +63 -0
  139. package/package.json +108 -0
  140. package/railway.json +12 -0
  141. package/scripts/build.mjs +51 -0
  142. package/scripts/generate-icons.mjs +314 -0
  143. package/scripts/generate-installer-images.cjs +253 -0
  144. package/scripts/generate-tray-icons.mjs +258 -0
  145. package/scripts/notarize.cjs +180 -0
  146. package/scripts/one-click-clean-test.sh +147 -0
  147. package/scripts/postinstall.mjs +36 -0
  148. package/scripts/setup-markupr.sh +55 -0
  149. package/setup +17 -0
  150. package/site/index.html +1835 -0
  151. package/site/package.json +11 -0
  152. package/site/railway.json +12 -0
  153. package/site/server.js +31 -0
  154. package/src/main/AutoUpdater.ts +392 -0
  155. package/src/main/CrashRecovery.ts +655 -0
  156. package/src/main/ErrorHandler.ts +703 -0
  157. package/src/main/HotkeyManager.ts +399 -0
  158. package/src/main/MenuManager.ts +529 -0
  159. package/src/main/PermissionManager.ts +420 -0
  160. package/src/main/SessionController.ts +1465 -0
  161. package/src/main/TrayManager.ts +540 -0
  162. package/src/main/ai/AIPipelineManager.ts +199 -0
  163. package/src/main/ai/ClaudeAnalyzer.ts +339 -0
  164. package/src/main/ai/ImageOptimizer.ts +176 -0
  165. package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
  166. package/src/main/ai/index.ts +16 -0
  167. package/src/main/ai/types.ts +258 -0
  168. package/src/main/analysis/ClarificationGenerator.ts +385 -0
  169. package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
  170. package/src/main/analysis/index.ts +19 -0
  171. package/src/main/audio/AudioCapture.ts +978 -0
  172. package/src/main/audio/audioUtils.ts +100 -0
  173. package/src/main/audio/index.ts +20 -0
  174. package/src/main/capture/index.ts +1 -0
  175. package/src/main/index.ts +1693 -0
  176. package/src/main/ipc/captureHandlers.ts +272 -0
  177. package/src/main/ipc/index.ts +45 -0
  178. package/src/main/ipc/outputHandlers.ts +302 -0
  179. package/src/main/ipc/sessionHandlers.ts +56 -0
  180. package/src/main/ipc/settingsHandlers.ts +471 -0
  181. package/src/main/ipc/types.ts +56 -0
  182. package/src/main/ipc/windowHandlers.ts +277 -0
  183. package/src/main/output/ClipboardService.ts +369 -0
  184. package/src/main/output/ExportService.ts +539 -0
  185. package/src/main/output/FileManager.ts +416 -0
  186. package/src/main/output/MarkdownGenerator.ts +791 -0
  187. package/src/main/output/MarkdownPatcher.ts +299 -0
  188. package/src/main/output/index.ts +186 -0
  189. package/src/main/output/sessionAdapter.ts +207 -0
  190. package/src/main/output/templates/html-template.ts +553 -0
  191. package/src/main/pipeline/FrameExtractor.ts +330 -0
  192. package/src/main/pipeline/PostProcessor.ts +399 -0
  193. package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
  194. package/src/main/pipeline/index.ts +36 -0
  195. package/src/main/platform/WindowsTaskbar.ts +600 -0
  196. package/src/main/platform/index.ts +16 -0
  197. package/src/main/settings/SettingsManager.ts +730 -0
  198. package/src/main/settings/index.ts +19 -0
  199. package/src/main/transcription/ModelDownloadManager.ts +494 -0
  200. package/src/main/transcription/TierManager.ts +219 -0
  201. package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
  202. package/src/main/transcription/WhisperService.ts +748 -0
  203. package/src/main/transcription/index.ts +56 -0
  204. package/src/main/transcription/types.ts +135 -0
  205. package/src/main/windows/PopoverManager.ts +284 -0
  206. package/src/main/windows/TaskbarIntegration.ts +452 -0
  207. package/src/main/windows/index.ts +23 -0
  208. package/src/preload/index.ts +1047 -0
  209. package/src/renderer/App.tsx +515 -0
  210. package/src/renderer/AppWrapper.tsx +28 -0
  211. package/src/renderer/assets/logo-dark.svg +7 -0
  212. package/src/renderer/assets/logo.svg +7 -0
  213. package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
  214. package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
  215. package/src/renderer/components/AnnotationOverlay.tsx +836 -0
  216. package/src/renderer/components/AudioWaveform.tsx +811 -0
  217. package/src/renderer/components/ClarificationQuestions.tsx +656 -0
  218. package/src/renderer/components/CountdownTimer.tsx +495 -0
  219. package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
  220. package/src/renderer/components/DonateButton.tsx +127 -0
  221. package/src/renderer/components/ErrorBoundary.tsx +308 -0
  222. package/src/renderer/components/ExportDialog.tsx +872 -0
  223. package/src/renderer/components/HotkeyHint.tsx +261 -0
  224. package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
  225. package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
  226. package/src/renderer/components/Onboarding.tsx +1830 -0
  227. package/src/renderer/components/ProcessingOverlay.tsx +157 -0
  228. package/src/renderer/components/RecordingOverlay.tsx +423 -0
  229. package/src/renderer/components/SessionHistory.tsx +1746 -0
  230. package/src/renderer/components/SessionReview.tsx +1321 -0
  231. package/src/renderer/components/SettingsPanel.tsx +217 -0
  232. package/src/renderer/components/Skeleton.tsx +347 -0
  233. package/src/renderer/components/StatusIndicator.tsx +86 -0
  234. package/src/renderer/components/ThemeProvider.tsx +429 -0
  235. package/src/renderer/components/Tooltip.tsx +370 -0
  236. package/src/renderer/components/TranscriptionPreview.tsx +183 -0
  237. package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
  238. package/src/renderer/components/UpdateNotification.tsx +377 -0
  239. package/src/renderer/components/WindowSelector.tsx +947 -0
  240. package/src/renderer/components/index.ts +99 -0
  241. package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
  242. package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
  243. package/src/renderer/components/primitives/DangerButton.tsx +45 -0
  244. package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
  245. package/src/renderer/components/primitives/Dropdown.tsx +34 -0
  246. package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
  247. package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
  248. package/src/renderer/components/primitives/Slider.tsx +43 -0
  249. package/src/renderer/components/primitives/Toggle.tsx +36 -0
  250. package/src/renderer/components/primitives/index.ts +10 -0
  251. package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
  252. package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
  253. package/src/renderer/components/settings/GeneralTab.tsx +40 -0
  254. package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
  255. package/src/renderer/components/settings/RecordingTab.tsx +84 -0
  256. package/src/renderer/components/settings/index.ts +9 -0
  257. package/src/renderer/components/settings/settingsStyles.ts +673 -0
  258. package/src/renderer/components/settings/tabConfig.tsx +85 -0
  259. package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
  260. package/src/renderer/contexts/ProcessingContext.tsx +227 -0
  261. package/src/renderer/contexts/RecordingContext.tsx +683 -0
  262. package/src/renderer/contexts/UIContext.tsx +326 -0
  263. package/src/renderer/contexts/index.ts +24 -0
  264. package/src/renderer/donateMessages.ts +69 -0
  265. package/src/renderer/hooks/index.ts +75 -0
  266. package/src/renderer/hooks/useAnimation.tsx +544 -0
  267. package/src/renderer/hooks/useTheme.ts +313 -0
  268. package/src/renderer/index.html +26 -0
  269. package/src/renderer/main.tsx +52 -0
  270. package/src/renderer/styles/animations.css +1093 -0
  271. package/src/renderer/styles/app-shell.css +662 -0
  272. package/src/renderer/styles/globals.css +515 -0
  273. package/src/renderer/styles/theme.ts +578 -0
  274. package/src/renderer/types/electron.d.ts +385 -0
  275. package/src/shared/hotkeys.ts +283 -0
  276. package/src/shared/types.ts +809 -0
  277. package/tests/clipboard.test.ts +228 -0
  278. package/tests/e2e/criticalPaths.test.ts +594 -0
  279. package/tests/feedbackAnalyzer.test.ts +303 -0
  280. package/tests/integration/sessionFlow.test.ts +583 -0
  281. package/tests/markdownGenerator.test.ts +418 -0
  282. package/tests/output.test.ts +96 -0
  283. package/tests/setup.ts +486 -0
  284. package/tests/unit/appIntegration.test.ts +676 -0
  285. package/tests/unit/appViewState.test.ts +281 -0
  286. package/tests/unit/audioIpcChannels.test.ts +17 -0
  287. package/tests/unit/exportService.test.ts +492 -0
  288. package/tests/unit/hotkeys.test.ts +92 -0
  289. package/tests/unit/navigationPreload.test.ts +94 -0
  290. package/tests/unit/onboardingFlow.test.ts +345 -0
  291. package/tests/unit/permissionManager.test.ts +175 -0
  292. package/tests/unit/permissionManagerExpanded.test.ts +296 -0
  293. package/tests/unit/screenRecordingRenderer.test.ts +368 -0
  294. package/tests/unit/sessionController.test.ts +515 -0
  295. package/tests/unit/tierManager.test.ts +61 -0
  296. package/tests/unit/tierManagerExpanded.test.ts +142 -0
  297. package/tests/unit/transcriptAnalyzer.test.ts +64 -0
  298. package/tsconfig.json +25 -0
  299. package/vitest.config.ts +46 -0
@@ -0,0 +1,492 @@
1
+ /**
2
+ * ScreenRecordingRenderer - Renderer-side full session screen recorder.
3
+ *
4
+ * Captures the selected desktop source continuously with MediaRecorder and
5
+ * streams chunks to the main process for durable file writing.
6
+ */
7
+
8
+ interface StartOptions {
9
+ sessionId: string;
10
+ sourceId: string;
11
+ }
12
+
13
+ interface StopResult {
14
+ success: boolean;
15
+ path?: string;
16
+ bytes?: number;
17
+ mimeType?: string;
18
+ error?: string;
19
+ }
20
+
21
+ interface DesktopVideoConstraints extends MediaTrackConstraints {
22
+ mandatory?: {
23
+ chromeMediaSource: 'desktop';
24
+ chromeMediaSourceId: string;
25
+ minWidth?: number;
26
+ minHeight?: number;
27
+ maxWidth?: number;
28
+ maxHeight?: number;
29
+ maxFrameRate?: number;
30
+ };
31
+ }
32
+
33
+ const MIME_TYPE_CANDIDATES = [
34
+ 'video/webm;codecs=vp8',
35
+ 'video/webm;codecs=vp9',
36
+ 'video/webm',
37
+ ] as const;
38
+
39
+ function chooseMimeType(): string {
40
+ for (const candidate of MIME_TYPE_CANDIDATES) {
41
+ if (MediaRecorder.isTypeSupported(candidate)) {
42
+ return candidate;
43
+ }
44
+ }
45
+ return 'video/webm';
46
+ }
47
+
48
+ export class ScreenRecordingRenderer {
49
+ private mediaStream: MediaStream | null = null;
50
+ private mediaRecorder: MediaRecorder | null = null;
51
+ private activeSessionId: string | null = null;
52
+ private inFlightWrites: Set<Promise<void>> = new Set();
53
+ private startPromise: Promise<void> | null = null;
54
+ private stopping = false;
55
+ private stopPromise: Promise<StopResult> | null = null;
56
+ private recordingStartTime: number | null = null;
57
+
58
+ private stopTracks(stream: MediaStream | null | undefined): void {
59
+ if (!stream) {
60
+ return;
61
+ }
62
+ try {
63
+ stream.getTracks().forEach((track) => {
64
+ try {
65
+ track.enabled = false;
66
+ track.stop();
67
+ } catch {
68
+ // Best effort.
69
+ }
70
+ });
71
+ } catch {
72
+ // Best effort.
73
+ }
74
+ }
75
+
76
+ private hasLiveTrack(stream: MediaStream | null | undefined): boolean {
77
+ if (!stream) {
78
+ return false;
79
+ }
80
+ return stream.getTracks().some((track) => track.readyState === 'live');
81
+ }
82
+
83
+ private getDesktopConstraints(
84
+ sourceId: string,
85
+ highQuality: boolean
86
+ ): MediaStreamConstraints {
87
+ if (highQuality) {
88
+ return {
89
+ audio: false,
90
+ video: {
91
+ mandatory: {
92
+ chromeMediaSource: 'desktop',
93
+ chromeMediaSourceId: sourceId,
94
+ minWidth: 1280,
95
+ minHeight: 720,
96
+ maxWidth: 3840,
97
+ maxHeight: 2160,
98
+ maxFrameRate: 30,
99
+ },
100
+ } as DesktopVideoConstraints,
101
+ };
102
+ }
103
+
104
+ return {
105
+ audio: false,
106
+ video: {
107
+ mandatory: {
108
+ chromeMediaSource: 'desktop',
109
+ chromeMediaSourceId: sourceId,
110
+ },
111
+ } as DesktopVideoConstraints,
112
+ };
113
+ }
114
+
115
+ private async getCandidateSourceIds(preferredSourceId: string): Promise<string[]> {
116
+ const candidates = new Set<string>([preferredSourceId]);
117
+ const captureApi = window.markupr?.capture;
118
+ if (!captureApi?.getSources) {
119
+ return Array.from(candidates);
120
+ }
121
+
122
+ try {
123
+ const sources = await captureApi.getSources();
124
+ for (const source of sources) {
125
+ if (source.type === 'screen') {
126
+ candidates.add(source.id);
127
+ }
128
+ }
129
+ } catch (error) {
130
+ console.warn('[ScreenRecordingRenderer] Failed to enumerate capture sources:', error);
131
+ }
132
+
133
+ return Array.from(candidates);
134
+ }
135
+
136
+ private async acquireScreenStream(sourceId: string): Promise<MediaStream> {
137
+ let lastError: unknown;
138
+ const candidates = await this.getCandidateSourceIds(sourceId);
139
+
140
+ for (const candidateId of candidates) {
141
+ const highQualityConstraints = this.getDesktopConstraints(candidateId, true);
142
+ const fallbackConstraints = this.getDesktopConstraints(candidateId, false);
143
+
144
+ try {
145
+ return await navigator.mediaDevices.getUserMedia(highQualityConstraints);
146
+ } catch (primaryError) {
147
+ console.warn(
148
+ `[ScreenRecordingRenderer] High-quality capture failed for ${candidateId}, retrying fallback:`,
149
+ primaryError
150
+ );
151
+ lastError = primaryError;
152
+ }
153
+
154
+ try {
155
+ return await navigator.mediaDevices.getUserMedia(fallbackConstraints);
156
+ } catch (fallbackError) {
157
+ console.warn(
158
+ `[ScreenRecordingRenderer] Fallback capture failed for ${candidateId}:`,
159
+ fallbackError
160
+ );
161
+ lastError = fallbackError;
162
+ }
163
+ }
164
+
165
+ const message =
166
+ lastError instanceof Error
167
+ ? lastError.message
168
+ : 'Unable to acquire a desktop capture stream.';
169
+ throw new Error(message);
170
+ }
171
+
172
+ isRecording(): boolean {
173
+ return this.mediaRecorder !== null && this.mediaRecorder.state !== 'inactive';
174
+ }
175
+
176
+ isPaused(): boolean {
177
+ return this.mediaRecorder?.state === 'paused';
178
+ }
179
+
180
+ getSessionId(): string | null {
181
+ return this.activeSessionId;
182
+ }
183
+
184
+ getRecordingStartTime(): number | null {
185
+ return this.recordingStartTime;
186
+ }
187
+
188
+ async start(options: StartOptions): Promise<void> {
189
+ if (this.startPromise) {
190
+ return this.startPromise;
191
+ }
192
+
193
+ const startTask = (async () => {
194
+ if (this.stopPromise) {
195
+ await this.stopPromise.catch(() => {
196
+ // Best effort; continuing to start allows a clean retry path.
197
+ });
198
+ }
199
+
200
+ this.forceReleaseOrphanedCapture();
201
+
202
+ if (this.isRecording()) {
203
+ return;
204
+ }
205
+
206
+ const mimeType = chooseMimeType();
207
+ const stream = await this.acquireScreenStream(options.sourceId);
208
+
209
+ const recordingStartTime = Date.now();
210
+ const startResult = await window.markupr.screenRecording.start(
211
+ options.sessionId,
212
+ mimeType,
213
+ recordingStartTime
214
+ );
215
+ if (!startResult.success) {
216
+ stream.getTracks().forEach((track) => track.stop());
217
+ throw new Error(startResult.error || 'Unable to start screen recording persistence.');
218
+ }
219
+
220
+ let recorder: MediaRecorder;
221
+ try {
222
+ recorder = new MediaRecorder(stream, { mimeType, videoBitsPerSecond: 5_000_000 });
223
+ } catch (error) {
224
+ // MediaRecorder construction failed — clean up the main-process artifact and stream.
225
+ stream.getTracks().forEach((track) => track.stop());
226
+ await window.markupr.screenRecording.stop(options.sessionId).catch(() => {});
227
+ throw error;
228
+ }
229
+
230
+ recorder.ondataavailable = (event: BlobEvent) => {
231
+ if (!event.data || event.data.size === 0 || !this.activeSessionId) {
232
+ return;
233
+ }
234
+
235
+ const sessionId = this.activeSessionId;
236
+ const writePromise = event.data
237
+ .arrayBuffer()
238
+ .then((buffer) =>
239
+ window.markupr.screenRecording.appendChunk(sessionId, new Uint8Array(buffer))
240
+ )
241
+ .then((result) => {
242
+ if (!result.success) {
243
+ throw new Error(result.error || 'Failed to append recording chunk.');
244
+ }
245
+ })
246
+ .catch((error) => {
247
+ console.error('[ScreenRecordingRenderer] Chunk write failed:', error);
248
+ })
249
+ .finally(() => {
250
+ this.inFlightWrites.delete(writePromise);
251
+ });
252
+
253
+ this.inFlightWrites.add(writePromise);
254
+ };
255
+
256
+ this.mediaStream = stream;
257
+ this.mediaRecorder = recorder;
258
+ this.activeSessionId = options.sessionId;
259
+ this.stopping = false;
260
+ this.recordingStartTime = recordingStartTime;
261
+
262
+ // Emit chunks every second for near-real-time persistence.
263
+ try {
264
+ recorder.start(1000);
265
+ } catch (error) {
266
+ // recorder.start() failed — clean up everything.
267
+ this.cleanupStream();
268
+ this.mediaRecorder = null;
269
+ this.activeSessionId = null;
270
+ this.recordingStartTime = null;
271
+ await window.markupr.screenRecording.stop(options.sessionId).catch(() => {});
272
+ throw error;
273
+ }
274
+ })();
275
+
276
+ this.startPromise = startTask.finally(() => {
277
+ this.startPromise = null;
278
+ });
279
+ return this.startPromise;
280
+ }
281
+
282
+ async stop(): Promise<StopResult> {
283
+ if (this.stopPromise) {
284
+ return this.stopPromise;
285
+ }
286
+
287
+ if (this.startPromise) {
288
+ await this.startPromise.catch(() => {
289
+ // If start failed, stop should still continue to clean up any residual state.
290
+ });
291
+ }
292
+
293
+ if (this.stopping) {
294
+ return { success: true };
295
+ }
296
+
297
+ if (!this.mediaRecorder || !this.activeSessionId) {
298
+ // Defensive cleanup for partially-initialized recorder state.
299
+ this.cleanupStream();
300
+ this.mediaRecorder = null;
301
+ this.activeSessionId = null;
302
+ this.stopping = false;
303
+ this.recordingStartTime = null;
304
+ return { success: true };
305
+ }
306
+
307
+ const stopTask = (async (): Promise<StopResult> => {
308
+ this.stopping = true;
309
+ const sessionId = this.activeSessionId;
310
+ const recorder = this.mediaRecorder;
311
+ let result: StopResult = { success: true };
312
+
313
+ if (!recorder || !sessionId) {
314
+ this.cleanupStream();
315
+ this.mediaRecorder = null;
316
+ this.activeSessionId = null;
317
+ this.recordingStartTime = null;
318
+ this.stopping = false;
319
+ return result;
320
+ }
321
+
322
+ try {
323
+ await new Promise<void>((resolve) => {
324
+ const timeout = setTimeout(resolve, 4000);
325
+ recorder.onstop = () => {
326
+ clearTimeout(timeout);
327
+ resolve();
328
+ };
329
+ try {
330
+ if (recorder.state === 'recording') {
331
+ try {
332
+ recorder.requestData();
333
+ } catch {
334
+ // Best effort.
335
+ }
336
+ }
337
+ recorder.stop();
338
+ } catch {
339
+ clearTimeout(timeout);
340
+ resolve();
341
+ }
342
+ });
343
+
344
+ recorder.ondataavailable = null;
345
+ recorder.onerror = null;
346
+ recorder.onstop = null;
347
+ this.stopTracks(recorder.stream);
348
+
349
+ // Release screen-capture tracks immediately so macOS indicator turns off
350
+ // even if persistence finalization takes longer than expected.
351
+ this.cleanupStream();
352
+ this.mediaRecorder = null;
353
+ this.activeSessionId = null;
354
+ this.recordingStartTime = null;
355
+
356
+ await Promise.allSettled(Array.from(this.inFlightWrites));
357
+ this.inFlightWrites.clear();
358
+
359
+ try {
360
+ const finalized = await Promise.race([
361
+ window.markupr.screenRecording.stop(sessionId),
362
+ new Promise<StopResult>((resolve) => {
363
+ setTimeout(
364
+ () =>
365
+ resolve({
366
+ success: false,
367
+ error: 'Timed out while finalizing screen recording persistence.',
368
+ }),
369
+ 7000
370
+ );
371
+ }),
372
+ ]);
373
+ result = finalized;
374
+ } catch (error) {
375
+ result = {
376
+ success: false,
377
+ error: error instanceof Error ? error.message : 'Failed to finalize screen recording.',
378
+ };
379
+ }
380
+ } finally {
381
+ // Defensive cleanup for any partial/failed stop paths.
382
+ this.cleanupStream();
383
+ this.mediaRecorder = null;
384
+ this.activeSessionId = null;
385
+ this.stopping = false;
386
+ this.recordingStartTime = null;
387
+ }
388
+
389
+ return result;
390
+ })();
391
+
392
+ this.stopPromise = stopTask.finally(() => {
393
+ this.stopPromise = null;
394
+ });
395
+ return this.stopPromise;
396
+ }
397
+
398
+ async pause(): Promise<void> {
399
+ if (!this.mediaRecorder || this.mediaRecorder.state !== 'recording') {
400
+ return;
401
+ }
402
+
403
+ try {
404
+ this.mediaRecorder.pause();
405
+ } catch (error) {
406
+ console.warn('[ScreenRecordingRenderer] Failed to pause recording:', error);
407
+ }
408
+ }
409
+
410
+ async resume(): Promise<void> {
411
+ if (!this.mediaRecorder || this.mediaRecorder.state !== 'paused') {
412
+ return;
413
+ }
414
+
415
+ try {
416
+ this.mediaRecorder.resume();
417
+ } catch (error) {
418
+ console.warn('[ScreenRecordingRenderer] Failed to resume recording:', error);
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Immediately release all capture tracks to clear the macOS recording indicator.
424
+ * This is a fast operation that does not wait for MediaRecorder finalization,
425
+ * in-flight chunk writes, or the main-process persistence layer. Call this
426
+ * before the full stop() for immediate user feedback when the user clicks stop.
427
+ *
428
+ * Idempotent: safe to call multiple times or when no tracks are active.
429
+ */
430
+ releaseCaptureTracks(): void {
431
+ if (this.mediaStream) {
432
+ this.stopTracks(this.mediaStream);
433
+ this.mediaStream = null;
434
+ }
435
+ // Also stop tracks on the recorder's internal stream reference if it differs
436
+ // from the stored mediaStream (e.g. after partial cleanup).
437
+ if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
438
+ try {
439
+ const recorderStream = this.mediaRecorder.stream;
440
+ if (recorderStream) {
441
+ this.stopTracks(recorderStream);
442
+ }
443
+ } catch {
444
+ // Best effort.
445
+ }
446
+ }
447
+ }
448
+
449
+ forceReleaseOrphanedCapture(): void {
450
+ const hasStreamLeak = this.hasLiveTrack(this.mediaStream);
451
+ const hasRecorderLeak = this.hasLiveTrack(this.mediaRecorder?.stream);
452
+ if (!hasStreamLeak && !hasRecorderLeak) {
453
+ return;
454
+ }
455
+
456
+ try {
457
+ if (this.mediaRecorder) {
458
+ this.mediaRecorder.ondataavailable = null;
459
+ this.mediaRecorder.onerror = null;
460
+ this.mediaRecorder.onstop = null;
461
+ }
462
+ } catch {
463
+ // Best effort.
464
+ }
465
+
466
+ this.stopTracks(this.mediaRecorder?.stream);
467
+ this.cleanupStream();
468
+ this.mediaRecorder = null;
469
+ this.activeSessionId = null;
470
+ this.stopping = false;
471
+ this.recordingStartTime = null;
472
+ }
473
+
474
+ private cleanupStream(): void {
475
+ if (!this.mediaStream) {
476
+ return;
477
+ }
478
+ this.stopTracks(this.mediaStream);
479
+ this.mediaStream = null;
480
+ }
481
+ }
482
+
483
+ let screenRecordingRendererInstance: ScreenRecordingRenderer | null = null;
484
+
485
+ export function getScreenRecordingRenderer(): ScreenRecordingRenderer {
486
+ if (!screenRecordingRendererInstance) {
487
+ screenRecordingRendererInstance = new ScreenRecordingRenderer();
488
+ }
489
+ return screenRecordingRendererInstance;
490
+ }
491
+
492
+ export default getScreenRecordingRenderer;