@elizaos/capacitor-screencapture 1.0.0

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.
@@ -0,0 +1,101 @@
1
+ import type { PluginListenerHandle } from "@capacitor/core";
2
+ export interface ScreenshotOptions {
3
+ format?: "png" | "jpeg" | "webp";
4
+ quality?: number;
5
+ scale?: number;
6
+ captureSystemUI?: boolean;
7
+ }
8
+ export interface ScreenshotResult {
9
+ base64: string;
10
+ format: string;
11
+ width: number;
12
+ height: number;
13
+ timestamp: number;
14
+ }
15
+ export interface ScreenRecordingOptions {
16
+ quality?: "low" | "medium" | "high" | "highest";
17
+ maxDuration?: number;
18
+ maxFileSize?: number;
19
+ fps?: number;
20
+ bitrate?: number;
21
+ captureAudio?: boolean;
22
+ captureSystemAudio?: boolean;
23
+ captureMicrophone?: boolean;
24
+ showTouches?: boolean;
25
+ }
26
+ export interface ScreenRecordingState {
27
+ isRecording: boolean;
28
+ duration: number;
29
+ fileSize: number;
30
+ fps?: number;
31
+ }
32
+ export interface ScreenRecordingResult {
33
+ path: string;
34
+ duration: number;
35
+ width: number;
36
+ height: number;
37
+ fileSize: number;
38
+ mimeType: string;
39
+ }
40
+ export interface ScreenCapturePermissionStatus {
41
+ screenCapture: "granted" | "denied" | "prompt" | "not_supported";
42
+ microphone: "granted" | "denied" | "prompt";
43
+ }
44
+ export interface ScreenCaptureErrorEvent {
45
+ code: string;
46
+ message: string;
47
+ }
48
+ export interface ScreenCapturePlugin {
49
+ /**
50
+ * Check if screen capture is supported on this device
51
+ */
52
+ isSupported(): Promise<{
53
+ supported: boolean;
54
+ features: string[];
55
+ }>;
56
+ /**
57
+ * Capture a screenshot of the current screen
58
+ */
59
+ captureScreenshot(options?: ScreenshotOptions): Promise<ScreenshotResult>;
60
+ /**
61
+ * Start screen recording
62
+ */
63
+ startRecording(options?: ScreenRecordingOptions): Promise<void>;
64
+ /**
65
+ * Stop screen recording and return the video file
66
+ */
67
+ stopRecording(): Promise<ScreenRecordingResult>;
68
+ /**
69
+ * Pause the current recording
70
+ */
71
+ pauseRecording(): Promise<void>;
72
+ /**
73
+ * Resume a paused recording
74
+ */
75
+ resumeRecording(): Promise<void>;
76
+ /**
77
+ * Get current recording state
78
+ */
79
+ getRecordingState(): Promise<ScreenRecordingState>;
80
+ /**
81
+ * Check screen capture permissions
82
+ */
83
+ checkPermissions(): Promise<ScreenCapturePermissionStatus>;
84
+ /**
85
+ * Request screen capture permissions
86
+ */
87
+ requestPermissions(): Promise<ScreenCapturePermissionStatus>;
88
+ /**
89
+ * Add event listener for recording state changes
90
+ */
91
+ addListener(eventName: "recordingState", listenerFunc: (event: ScreenRecordingState) => void): Promise<PluginListenerHandle>;
92
+ /**
93
+ * Add event listener for errors
94
+ */
95
+ addListener(eventName: "error", listenerFunc: (event: ScreenCaptureErrorEvent) => void): Promise<PluginListenerHandle>;
96
+ /**
97
+ * Remove all event listeners
98
+ */
99
+ removeAllListeners(): Promise<void>;
100
+ }
101
+ //# sourceMappingURL=definitions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAE5D,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;IAChD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,6BAA6B;IAC5C,aAAa,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,eAAe,CAAC;IACjE,UAAU,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;CAC7C;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC;;OAEG;IACH,WAAW,IAAI,OAAO,CAAC;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IAEnE;;OAEG;IACH,iBAAiB,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAE1E;;OAEG;IACH,cAAc,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhE;;OAEG;IACH,aAAa,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAEhD;;OAEG;IACH,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhC;;OAEG;IACH,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjC;;OAEG;IACH,iBAAiB,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAEnD;;OAEG;IACH,gBAAgB,IAAI,OAAO,CAAC,6BAA6B,CAAC,CAAC;IAE3D;;OAEG;IACH,kBAAkB,IAAI,OAAO,CAAC,6BAA6B,CAAC,CAAC;IAE7D;;OAEG;IACH,WAAW,CACT,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAClD,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAEjC;;OAEG;IACH,WAAW,CACT,SAAS,EAAE,OAAO,EAClB,YAAY,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,GACrD,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAEjC;;OAEG;IACH,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { ScreenCapturePlugin } from "./definitions";
2
+ export * from "./definitions";
3
+ export declare const ScreenCapture: ScreenCapturePlugin;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEzD,cAAc,eAAe,CAAC;AAI9B,eAAO,MAAM,aAAa,qBAKzB,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { registerPlugin } from "@capacitor/core";
2
+ export * from "./definitions";
3
+ const loadWeb = () => import("./web").then((m) => new m.ScreenCaptureWeb());
4
+ export const ScreenCapture = registerPlugin("ScreenCapture", {
5
+ web: loadWeb,
6
+ });
@@ -0,0 +1,56 @@
1
+ import { WebPlugin } from "@capacitor/core";
2
+ import type { ScreenCaptureErrorEvent, ScreenCapturePermissionStatus, ScreenRecordingOptions, ScreenRecordingResult, ScreenRecordingState, ScreenshotOptions, ScreenshotResult } from "./definitions";
3
+ type ScreenCaptureEventData = ScreenRecordingState | ScreenCaptureErrorEvent;
4
+ export declare class ScreenCaptureWeb extends WebPlugin {
5
+ private mediaStream;
6
+ private mediaRecorder;
7
+ private recordedChunks;
8
+ private isRecording;
9
+ private isPaused;
10
+ private recordingStartTime;
11
+ private pausedDuration;
12
+ private pauseStartTime;
13
+ private recordingStateInterval;
14
+ private pluginListeners;
15
+ isSupported(): Promise<{
16
+ supported: boolean;
17
+ features: string[];
18
+ }>;
19
+ captureScreenshot(options?: ScreenshotOptions): Promise<ScreenshotResult>;
20
+ startRecording(options?: ScreenRecordingOptions): Promise<void>;
21
+ stopRecording(): Promise<ScreenRecordingResult>;
22
+ pauseRecording(): Promise<void>;
23
+ resumeRecording(): Promise<void>;
24
+ getRecordingState(): Promise<ScreenRecordingState>;
25
+ /**
26
+ * Check screen capture permissions.
27
+ *
28
+ * LIMITATION: The Screen Capture API (getDisplayMedia) does not support permission queries.
29
+ * Unlike camera/microphone, there's no way to check if permission was previously granted.
30
+ * Each call to getDisplayMedia always prompts the user.
31
+ *
32
+ * `screenCapture` will be:
33
+ * - "not_supported": getDisplayMedia API not available
34
+ * - "prompt": API available, but actual permission state is unknown (always requires prompt)
35
+ */
36
+ checkPermissions(): Promise<ScreenCapturePermissionStatus>;
37
+ /**
38
+ * Request screen capture permissions.
39
+ *
40
+ * LIMITATION: Screen capture (getDisplayMedia) cannot be pre-requested.
41
+ * The user is prompted only when an actual capture is initiated.
42
+ * This method only requests microphone permission for audio capture during recording.
43
+ *
44
+ * `screenCapture` will be:
45
+ * - "not_supported": getDisplayMedia API not available
46
+ * - "prompt": API available (permission prompt happens during actual capture)
47
+ */
48
+ requestPermissions(): Promise<ScreenCapturePermissionStatus>;
49
+ addListener(eventName: string, listenerFunc: (event: ScreenCaptureEventData) => void): Promise<{
50
+ remove: () => Promise<void>;
51
+ }>;
52
+ removeAllListeners(): Promise<void>;
53
+ protected notifyListeners(eventName: string, data: ScreenCaptureEventData): void;
54
+ }
55
+ export {};
56
+ //# sourceMappingURL=web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,EACV,uBAAuB,EACvB,6BAA6B,EAC7B,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,eAAe,CAAC;AAEvB,KAAK,sBAAsB,GAAG,oBAAoB,GAAG,uBAAuB,CAAC;AA4B7E,qBAAa,gBAAiB,SAAQ,SAAS;IAC7C,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,sBAAsB,CAA+C;IAC7E,OAAO,CAAC,eAAe,CAGf;IAEF,WAAW,IAAI,OAAO,CAAC;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IASlE,iBAAiB,CACrB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,gBAAgB,CAAC;IAoDtB,cAAc,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmG/D,aAAa,IAAI,OAAO,CAAC,qBAAqB,CAAC;IAuE/C,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IA2B/B,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAchC,iBAAiB,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAgBxD;;;;;;;;;;OAUG;IACG,gBAAgB,IAAI,OAAO,CAAC,6BAA6B,CAAC;IAiBhE;;;;;;;;;;OAUG;IACG,kBAAkB,IAAI,OAAO,CAAC,6BAA6B,CAAC;IAkB5D,WAAW,CACf,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,GACpD,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;IAWrC,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAIzC,SAAS,CAAC,eAAe,CACvB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,sBAAsB,GAC3B,IAAI;CAOR"}
@@ -0,0 +1,330 @@
1
+ import { WebPlugin } from "@capacitor/core";
2
+ const VIDEO_MIME_TYPES = [
3
+ "video/webm;codecs=vp9,opus",
4
+ "video/webm;codecs=vp8,opus",
5
+ "video/webm",
6
+ "video/mp4",
7
+ ];
8
+ const getSupportedMimeType = () => VIDEO_MIME_TYPES.find((m) => MediaRecorder.isTypeSupported(m)) ?? null;
9
+ const hasDisplayMedia = () => !!navigator.mediaDevices.getDisplayMedia;
10
+ const getDisplayMedia = (opts) => navigator.mediaDevices.getDisplayMedia(opts);
11
+ export class ScreenCaptureWeb extends WebPlugin {
12
+ constructor() {
13
+ super(...arguments);
14
+ this.mediaStream = null;
15
+ this.mediaRecorder = null;
16
+ this.recordedChunks = [];
17
+ this.isRecording = false;
18
+ this.isPaused = false;
19
+ this.recordingStartTime = 0;
20
+ this.pausedDuration = 0;
21
+ this.pauseStartTime = 0;
22
+ this.recordingStateInterval = null;
23
+ this.pluginListeners = [];
24
+ }
25
+ async isSupported() {
26
+ const supported = hasDisplayMedia();
27
+ const features = [];
28
+ if (supported)
29
+ features.push("screenshot", "recording");
30
+ if (typeof MediaRecorder !== "undefined")
31
+ features.push("video_encoding");
32
+ if (typeof AudioContext !== "undefined")
33
+ features.push("system_audio");
34
+ return { supported, features };
35
+ }
36
+ async captureScreenshot(options) {
37
+ const format = options?.format || "png";
38
+ const quality = (options?.quality || 100) / 100;
39
+ const scale = options?.scale || 1;
40
+ const stream = await getDisplayMedia({
41
+ video: { displaySurface: "monitor" },
42
+ audio: false,
43
+ });
44
+ const track = stream.getVideoTracks()[0];
45
+ const settings = track.getSettings();
46
+ const width = (settings.width || 1920) * scale;
47
+ const height = (settings.height || 1080) * scale;
48
+ const imageCapture = new ImageCapture(track);
49
+ const bitmap = await imageCapture.grabFrame();
50
+ stream.getTracks().forEach((t) => {
51
+ t.stop();
52
+ });
53
+ const canvas = document.createElement("canvas");
54
+ canvas.width = width;
55
+ canvas.height = height;
56
+ const ctx = canvas.getContext("2d");
57
+ if (!ctx) {
58
+ throw new Error("Failed to get canvas context");
59
+ }
60
+ ctx.drawImage(bitmap, 0, 0, width, height);
61
+ bitmap.close();
62
+ const mimeType = format === "png"
63
+ ? "image/png"
64
+ : format === "webp"
65
+ ? "image/webp"
66
+ : "image/jpeg";
67
+ const dataUrl = canvas.toDataURL(mimeType, quality);
68
+ const base64 = dataUrl.split(",")[1];
69
+ return {
70
+ base64,
71
+ format,
72
+ width,
73
+ height,
74
+ timestamp: Date.now(),
75
+ };
76
+ }
77
+ async startRecording(options) {
78
+ if (this.isRecording)
79
+ throw new Error("Recording already in progress");
80
+ const videoConstraints = {
81
+ displaySurface: "monitor",
82
+ };
83
+ if (options?.fps)
84
+ videoConstraints.frameRate = { ideal: options.fps };
85
+ this.mediaStream = await getDisplayMedia({
86
+ video: videoConstraints,
87
+ audio: options?.captureSystemAudio !== false,
88
+ });
89
+ if (options?.captureMicrophone) {
90
+ const micStream = await navigator.mediaDevices.getUserMedia({
91
+ audio: true,
92
+ });
93
+ micStream.getAudioTracks().forEach((t) => {
94
+ this.mediaStream?.addTrack(t);
95
+ });
96
+ }
97
+ const mimeType = getSupportedMimeType();
98
+ if (!mimeType) {
99
+ this.mediaStream.getTracks().forEach((t) => {
100
+ t.stop();
101
+ });
102
+ throw new Error("No supported video mime type found");
103
+ }
104
+ const recorderOptions = { mimeType };
105
+ if (options?.bitrate)
106
+ recorderOptions.videoBitsPerSecond = options.bitrate;
107
+ this.recordedChunks = [];
108
+ this.mediaRecorder = new MediaRecorder(this.mediaStream, recorderOptions);
109
+ this.mediaRecorder.ondataavailable = (event) => {
110
+ if (event.data.size > 0) {
111
+ this.recordedChunks.push(event.data);
112
+ }
113
+ };
114
+ this.mediaRecorder.onerror = (event) => {
115
+ this.notifyListeners("error", {
116
+ code: "RECORDING_ERROR",
117
+ message: `Recording error: ${event.message || "Unknown error"}`,
118
+ });
119
+ };
120
+ this.mediaStream.getVideoTracks()[0].addEventListener("ended", () => {
121
+ if (this.isRecording) {
122
+ this.stopRecording().catch((err) => {
123
+ console.error("[ScreenCapture] Auto-stop on track end failed:", err);
124
+ });
125
+ }
126
+ });
127
+ this.recordingStartTime = Date.now();
128
+ this.pausedDuration = 0;
129
+ this.isRecording = true;
130
+ this.isPaused = false;
131
+ this.mediaRecorder.start(1000);
132
+ this.notifyListeners("recordingState", {
133
+ isRecording: true,
134
+ duration: 0,
135
+ fileSize: 0,
136
+ });
137
+ let autoStopping = false;
138
+ this.recordingStateInterval = setInterval(() => {
139
+ if (!this.isRecording || this.isPaused || autoStopping)
140
+ return;
141
+ const duration = (Date.now() - this.recordingStartTime - this.pausedDuration) / 1000;
142
+ const fileSize = this.recordedChunks.reduce((acc, chunk) => acc + chunk.size, 0);
143
+ this.notifyListeners("recordingState", {
144
+ isRecording: true,
145
+ duration,
146
+ fileSize,
147
+ });
148
+ const overLimit = (options?.maxDuration && duration >= options.maxDuration) ||
149
+ (options?.maxFileSize && fileSize >= options.maxFileSize);
150
+ if (overLimit) {
151
+ autoStopping = true;
152
+ this.stopRecording().catch((err) => {
153
+ console.error("[ScreenCapture] Auto-stop recording failed:", err);
154
+ });
155
+ }
156
+ }, 500);
157
+ }
158
+ async stopRecording() {
159
+ if (!this.isRecording || !this.mediaRecorder) {
160
+ throw new Error("Not recording");
161
+ }
162
+ return new Promise((resolve, reject) => {
163
+ if (!this.mediaRecorder) {
164
+ reject(new Error("MediaRecorder not initialized"));
165
+ return;
166
+ }
167
+ const duration = (Date.now() - this.recordingStartTime - this.pausedDuration) / 1000;
168
+ this.mediaRecorder.onstop = () => {
169
+ if (this.recordingStateInterval) {
170
+ clearInterval(this.recordingStateInterval);
171
+ this.recordingStateInterval = null;
172
+ }
173
+ this.isRecording = false;
174
+ this.isPaused = false;
175
+ if (this.mediaStream) {
176
+ this.mediaStream.getTracks().forEach((track) => {
177
+ track.stop();
178
+ });
179
+ this.mediaStream = null;
180
+ }
181
+ const blob = new Blob(this.recordedChunks, {
182
+ type: this.mediaRecorder?.mimeType || "video/webm",
183
+ });
184
+ const url = URL.createObjectURL(blob);
185
+ const video = document.createElement("video");
186
+ video.src = url;
187
+ video.onloadedmetadata = () => {
188
+ resolve({
189
+ path: url,
190
+ duration,
191
+ width: video.videoWidth,
192
+ height: video.videoHeight,
193
+ fileSize: blob.size,
194
+ mimeType: this.mediaRecorder?.mimeType || "video/webm",
195
+ });
196
+ };
197
+ video.onerror = () => {
198
+ resolve({
199
+ path: url,
200
+ duration,
201
+ width: 0,
202
+ height: 0,
203
+ fileSize: blob.size,
204
+ mimeType: this.mediaRecorder?.mimeType || "video/webm",
205
+ });
206
+ };
207
+ this.notifyListeners("recordingState", {
208
+ isRecording: false,
209
+ duration,
210
+ fileSize: blob.size,
211
+ });
212
+ };
213
+ this.mediaRecorder.stop();
214
+ });
215
+ }
216
+ async pauseRecording() {
217
+ if (!this.isRecording || !this.mediaRecorder) {
218
+ throw new Error("Not recording");
219
+ }
220
+ if (this.isPaused) {
221
+ return;
222
+ }
223
+ this.mediaRecorder.pause();
224
+ this.isPaused = true;
225
+ this.pauseStartTime = Date.now();
226
+ const duration = (Date.now() - this.recordingStartTime - this.pausedDuration) / 1000;
227
+ const fileSize = this.recordedChunks.reduce((acc, chunk) => acc + chunk.size, 0);
228
+ this.notifyListeners("recordingState", {
229
+ isRecording: true,
230
+ duration,
231
+ fileSize,
232
+ });
233
+ }
234
+ async resumeRecording() {
235
+ if (!this.isRecording || !this.mediaRecorder) {
236
+ throw new Error("Not recording");
237
+ }
238
+ if (!this.isPaused) {
239
+ return;
240
+ }
241
+ this.pausedDuration += Date.now() - this.pauseStartTime;
242
+ this.mediaRecorder.resume();
243
+ this.isPaused = false;
244
+ }
245
+ async getRecordingState() {
246
+ const duration = this.isRecording
247
+ ? (Date.now() - this.recordingStartTime - this.pausedDuration) / 1000
248
+ : 0;
249
+ const fileSize = this.recordedChunks.reduce((acc, chunk) => acc + chunk.size, 0);
250
+ return {
251
+ isRecording: this.isRecording,
252
+ duration,
253
+ fileSize,
254
+ };
255
+ }
256
+ /**
257
+ * Check screen capture permissions.
258
+ *
259
+ * LIMITATION: The Screen Capture API (getDisplayMedia) does not support permission queries.
260
+ * Unlike camera/microphone, there's no way to check if permission was previously granted.
261
+ * Each call to getDisplayMedia always prompts the user.
262
+ *
263
+ * `screenCapture` will be:
264
+ * - "not_supported": getDisplayMedia API not available
265
+ * - "prompt": API available, but actual permission state is unknown (always requires prompt)
266
+ */
267
+ async checkPermissions() {
268
+ let microphone = "prompt";
269
+ try {
270
+ const result = await navigator.permissions.query({
271
+ name: "microphone",
272
+ });
273
+ microphone = result.state;
274
+ }
275
+ catch {
276
+ // Permissions API may not support microphone query in this browser
277
+ }
278
+ // Screen capture permission cannot be queried - getDisplayMedia always prompts
279
+ const screenCaptureStatus = hasDisplayMedia() ? "prompt" : "not_supported";
280
+ return { screenCapture: screenCaptureStatus, microphone };
281
+ }
282
+ /**
283
+ * Request screen capture permissions.
284
+ *
285
+ * LIMITATION: Screen capture (getDisplayMedia) cannot be pre-requested.
286
+ * The user is prompted only when an actual capture is initiated.
287
+ * This method only requests microphone permission for audio capture during recording.
288
+ *
289
+ * `screenCapture` will be:
290
+ * - "not_supported": getDisplayMedia API not available
291
+ * - "prompt": API available (permission prompt happens during actual capture)
292
+ */
293
+ async requestPermissions() {
294
+ let microphone = "denied";
295
+ try {
296
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
297
+ stream.getTracks().forEach((t) => {
298
+ t.stop();
299
+ });
300
+ microphone = "granted";
301
+ }
302
+ catch {
303
+ microphone = "denied";
304
+ }
305
+ // Cannot pre-request screen capture permission - it requires user gesture + actual capture
306
+ const screenCaptureStatus = hasDisplayMedia() ? "prompt" : "not_supported";
307
+ return { screenCapture: screenCaptureStatus, microphone };
308
+ }
309
+ async addListener(eventName, listenerFunc) {
310
+ const entry = { eventName, callback: listenerFunc };
311
+ this.pluginListeners.push(entry);
312
+ return {
313
+ remove: async () => {
314
+ const i = this.pluginListeners.indexOf(entry);
315
+ if (i >= 0)
316
+ this.pluginListeners.splice(i, 1);
317
+ },
318
+ };
319
+ }
320
+ async removeAllListeners() {
321
+ this.pluginListeners = [];
322
+ }
323
+ notifyListeners(eventName, data) {
324
+ this.pluginListeners
325
+ .filter((l) => l.eventName === eventName)
326
+ .forEach((l) => {
327
+ l.callback(data);
328
+ });
329
+ }
330
+ }