@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.
- package/ElizaosCapacitorScreencapture.podspec +18 -0
- package/android/build.gradle +50 -0
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/ai/eliza/plugins/screencapture/ScreenCapturePlugin.kt +777 -0
- package/dist/esm/definitions.d.ts +101 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/web.d.ts +56 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +330 -0
- package/dist/plugin.cjs.js +346 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +349 -0
- package/dist/plugin.js.map +1 -0
- package/electrobun/src/index.ts +102 -0
- package/electrobun/tsconfig.json +18 -0
- package/ios/Sources/ScreenCapturePlugin/ScreenCapturePlugin.swift +758 -0
- package/package.json +84 -0
|
@@ -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":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,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"}
|
package/dist/esm/web.js
ADDED
|
@@ -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
|
+
}
|