@ruc-lib/screen-recorder 2.9.2 → 3.1.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.
@@ -1,648 +1,636 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, Component, ChangeDetectionStrategy, Input, ViewChild, NgModule } from '@angular/core';
3
- import * as i4 from '@angular/common';
4
- import { CommonModule } from '@angular/common';
2
+ import { Injectable, ViewChild, Input, Component } from '@angular/core';
5
3
  import * as i2 from '@angular/platform-browser';
6
4
  import { BehaviorSubject, Subject, timer, Subscription } from 'rxjs';
7
5
  import { tap, takeWhile } from 'rxjs/operators';
6
+ import * as i4 from '@angular/common';
7
+ import { CommonModule } from '@angular/common';
8
8
 
9
- var ScreenRecorderPosition;
10
- (function (ScreenRecorderPosition) {
11
- ScreenRecorderPosition["Top"] = "top";
12
- ScreenRecorderPosition["Bottom"] = "bottom";
13
- ScreenRecorderPosition["Left"] = "left";
14
- ScreenRecorderPosition["Right"] = "right";
15
- })(ScreenRecorderPosition || (ScreenRecorderPosition = {}));
16
- var DisplayFormat;
17
- (function (DisplayFormat) {
18
- DisplayFormat["Icon"] = "icon";
19
- DisplayFormat["Text"] = "text";
20
- })(DisplayFormat || (DisplayFormat = {}));
21
- var RecordingState;
22
- (function (RecordingState) {
23
- RecordingState["Idle"] = "idle";
24
- RecordingState["Recording"] = "recording";
25
- RecordingState["Paused"] = "paused";
26
- RecordingState["Stopped"] = "stopped";
9
+ var ScreenRecorderPosition;
10
+ (function (ScreenRecorderPosition) {
11
+ ScreenRecorderPosition["Top"] = "top";
12
+ ScreenRecorderPosition["Bottom"] = "bottom";
13
+ ScreenRecorderPosition["Left"] = "left";
14
+ ScreenRecorderPosition["Right"] = "right";
15
+ })(ScreenRecorderPosition || (ScreenRecorderPosition = {}));
16
+ var DisplayFormat;
17
+ (function (DisplayFormat) {
18
+ DisplayFormat["Icon"] = "icon";
19
+ DisplayFormat["Text"] = "text";
20
+ })(DisplayFormat || (DisplayFormat = {}));
21
+ var RecordingState;
22
+ (function (RecordingState) {
23
+ RecordingState["Idle"] = "idle";
24
+ RecordingState["Recording"] = "recording";
25
+ RecordingState["Paused"] = "paused";
26
+ RecordingState["Stopped"] = "stopped";
27
27
  })(RecordingState || (RecordingState = {}));
28
28
 
29
- class ScreenRecorderConstants {
30
- constructor() {
31
- this.DEFAULT_ICONS = {
32
- record: 'fiber_manual_record',
33
- stop: 'stop',
34
- pause: 'pause',
35
- resume: 'play_arrow',
36
- play: 'play_circle_filled',
37
- download: 'download',
38
- audioOn: 'volume_up',
39
- audioOff: 'volume_off',
40
- };
41
- this.DEFAULT_LABELS = {
42
- recordButton: 'Start Recording',
43
- stopButton: 'Stop Recording',
44
- pauseButton: 'Pause Recording',
45
- resumeButton: 'Resume Recording',
46
- audioToggleButton: 'Toggle Audio',
47
- downloadButton: 'Download Recording',
48
- playbackVideo: 'Screen Recording Playback',
49
- };
50
- this.INACTIVE = 'inactive';
51
- this.AUDIO_ON = 'Audio ON';
52
- this.AUDIO_OFF = 'Audio OFF';
53
- this.RECORDING_IN_PROGRESS_ERROR = 'Recording is already in progress.';
54
- this.FAILED_TO_GET_DISPLAY_MEDIA_STREAM_ERROR = 'Failed to get display media stream';
55
- this.NO_SUPPORT_MIME = 'No supported MIME type found for MediaRecorder.';
56
- this.NO_RECORDING_DOWNLOAD = 'No recording available to download.';
57
- this.MEDIA_ERROR = 'MediaRecorder error';
58
- this.PERMISSION_DENIED = "Error starting screen recording: NotAllowedError - Permission denied";
59
- this.NO_SUPPORT = 'Screen recording is not supported in this browser.';
60
- }
61
- }
62
- ScreenRecorderConstantsfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ScreenRecorderConstants, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
63
- ScreenRecorderConstants.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ScreenRecorderConstants, providedIn: 'root' });
64
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ScreenRecorderConstants, decorators: [{
65
- type: Injectable,
66
- args: [{
67
- providedIn: 'root',
68
- }]
69
- }], ctorParameters: function () { return []; } });
29
+ class ScreenRecorderConstants {
30
+ constructor() {
31
+ this.DEFAULT_ICONS = {
32
+ record: 'fiber_manual_record', // Material Icon name
33
+ stop: 'stop',
34
+ pause: 'pause',
35
+ resume: 'play_arrow',
36
+ play: 'play_circle_filled',
37
+ download: 'download',
38
+ audioOn: 'volume_up',
39
+ audioOff: 'volume_off',
40
+ };
41
+ this.DEFAULT_LABELS = {
42
+ recordButton: 'Start Recording',
43
+ stopButton: 'Stop Recording',
44
+ pauseButton: 'Pause Recording',
45
+ resumeButton: 'Resume Recording',
46
+ audioToggleButton: 'Toggle Audio',
47
+ downloadButton: 'Download Recording',
48
+ playbackVideo: 'Screen Recording Playback',
49
+ };
50
+ this.INACTIVE = 'inactive';
51
+ this.AUDIO_ON = 'Audio ON';
52
+ this.AUDIO_OFF = 'Audio OFF';
53
+ this.RECORDING_IN_PROGRESS_ERROR = 'Recording is already in progress.';
54
+ this.FAILED_TO_GET_DISPLAY_MEDIA_STREAM_ERROR = 'Failed to get display media stream';
55
+ this.NO_SUPPORT_MIME = 'No supported MIME type found for MediaRecorder.';
56
+ this.NO_RECORDING_DOWNLOAD = 'No recording available to download.';
57
+ this.MEDIA_ERROR = 'MediaRecorder error';
58
+ this.PERMISSION_DENIED = "Error starting screen recording: NotAllowedError - Permission denied";
59
+ this.NO_SUPPORT = 'Screen recording is not supported in this browser.';
60
+ }
61
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ScreenRecorderConstants, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
62
+ static { thisprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ScreenRecorderConstants, providedIn: 'root' }); }
63
+ }
64
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ScreenRecorderConstants, decorators: [{
65
+ type: Injectable,
66
+ args: [{
67
+ providedIn: 'root',
68
+ }]
69
+ }], ctorParameters: () => [] });
70
70
 
71
- class ScreenRecorderService {
72
- constructor(screenRecordingConstant) {
73
- this.screenRecordingConstant = screenRecordingConstant;
74
- this.stream = null;
75
- this.mediaRecorder = null;
76
- this.recordedBlobs = [];
77
- this.currentRecordedTime = 0;
78
- this.recordingStateSubject = new BehaviorSubject(RecordingState.Idle);
79
- this.recordingState$ = this.recordingStateSubject.asObservable();
80
- this.recordedTimeSubject = new BehaviorSubject(0);
81
- this.recordedTime$ = this.recordedTimeSubject.asObservable();
82
- this.recordedUrlSubject = new BehaviorSubject(null);
83
- this.recordedUrl$ = this.recordedUrlSubject.asObservable();
84
- this.recordingTimestampSubject = new BehaviorSubject('');
85
- this.recordingTimestamp$ = this.recordingTimestampSubject.asObservable();
86
- this.errorSubject = new Subject();
87
- this.error$ = this.errorSubject.asObservable();
88
- }
89
- /**
90
- * Starts the screen recording with optional audio
91
- *
92
- * @param options - Configuration options for recording
93
- * @param options.audio - Whether to include audio in the recording
94
- *
95
- * @throws Error if recording is already in progress or if media access is denied
96
- * @returns Promise<void>
97
- */
98
- async startRecording(options) {
99
- if (this.recordingStateSubject.value === RecordingState.Recording || this.recordingStateSubject.value === RecordingState.Paused) {
100
- this.handleError(this.screenRecordingConstant.RECORDING_IN_PROGRESS_ERROR);
101
- return;
102
- }
103
- this.cleanupPreviousRecording();
104
- try {
105
- // The browser's native picker handles "specific area" selection (window, tab, screen).
106
- const displayStream = await navigator.mediaDevices.getDisplayMedia({
107
- video: true
108
- });
109
- // If audio is enabled, get the audio stream
110
- let audioStream = null;
111
- if (options.audio) {
112
- audioStream = await navigator.mediaDevices.getUserMedia({
113
- audio: {
114
- echoCancellation: true,
115
- noiseSuppression: true,
116
- sampleRate: 44100
117
- }
118
- });
119
- }
120
- // Combine the streams if audio is enabled
121
- if (audioStream) {
122
- this.stream = new MediaStream([...displayStream.getTracks(), ...audioStream.getTracks()]);
123
- }
124
- else {
125
- this.stream = displayStream;
126
- }
127
- // Listen for when the user stops sharing via browser UI
128
- this.stream.getVideoTracks()[0].onended = () => {
129
- if (this.recordingStateSubject.value === RecordingState.Recording || this.recordingStateSubject.value === RecordingState.Paused) {
130
- this.stopRecordingInternal();
131
- }
132
- };
133
- // Handle errors
134
- if (!this.stream) {
135
- this.handleError(this.screenRecordingConstant.FAILED_TO_GET_DISPLAY_MEDIA_STREAM_ERROR);
136
- return;
137
- }
138
- // Set recording timestamp
139
- const now = new Date();
140
- const timestamp = now.toLocaleString('en-IN', {
141
- year: 'numeric',
142
- month: '2-digit',
143
- day: '2-digit',
144
- hour: '2-digit',
145
- minute: '2-digit',
146
- hour12: true
147
- });
148
- this.recordingTimestampSubject.next(timestamp);
149
- this.recordedBlobs = [];
150
- const mimeType = this.getSupportedMimeType();
151
- if (!mimeType) {
152
- this.handleError(this.screenRecordingConstant.NO_SUPPORT_MIME);
153
- this.stream?.getTracks().forEach(track => track.stop());
154
- this.stream = null;
155
- return;
156
- }
157
- // Create a canvas for timestamp overlay
158
- const canvas = document.createElement('canvas');
159
- const videoTrack = this.stream?.getVideoTracks()[0];
160
- if (videoTrack) {
161
- const video = document.createElement('video');
162
- video.srcObject = this.stream;
163
- // Wait for video to load metadata
164
- video.onloadedmetadata = () => {
165
- canvas.width = video.videoWidth;
166
- canvas.height = video.videoHeight;
167
- const ctx = canvas.getContext('2d');
168
- if (ctx) {
169
- // Draw video frame
170
- ctx.drawImage(video, 0, 0);
171
- // Draw timestamp
172
- ctx.font = '16px Arial';
173
- ctx.fillStyle = 'white';
174
- ctx.textAlign = 'right';
175
- ctx.textBaseline = 'top';
176
- ctx.fillText(`Recorded on: ${this.recordingTimestampSubject.value}`, canvas.width - 10, 10);
177
- }
178
- };
179
- }
180
- // Create a new stream with the canvas
181
- const canvasStream = canvas.captureStream();
182
- const combinedStream = new MediaStream([...this.stream?.getTracks() ?? [], canvasStream.getVideoTracks()[0]]);
183
- this.mediaRecorder = new MediaRecorder(combinedStream, { mimeType });
184
- this.mediaRecorder.ondataavailable = (event) => {
185
- if (event.data && event.data.size > 0) {
186
- this.recordedBlobs.push(event.data);
187
- }
188
- };
189
- this.mediaRecorder.onstop = () => {
190
- const superBuffer = new Blob(this.recordedBlobs, { type: mimeType });
191
- const url = window.URL.createObjectURL(superBuffer);
192
- this.recordedUrlSubject.next(url);
193
- this.recordingStateSubject.next(RecordingState.Stopped);
194
- this.stopTimer();
195
- };
196
- this.mediaRecorder.onerror = (event) => {
197
- const errorEvent = event; // More specific type if available
198
- let message = this.screenRecordingConstant.MEDIA_ERROR;
199
- if (errorEvent.error && errorEvent.error.name) {
200
- message += `: ${errorEvent.error.name}`;
201
- if (errorEvent.error.message)
202
- message += ` - ${errorEvent.error.message}`;
203
- }
204
- this.handleError(message);
205
- this.recordingStateSubject.next(RecordingState.Idle);
206
- this.stopTimer();
207
- };
208
- this.mediaRecorder.start(); // Start recording
209
- this.recordingStateSubject.next(RecordingState.Recording);
210
- this.startTimer();
211
- }
212
- catch (err) {
213
- this.handleError(`Error starting screen recording: ${err.name} - ${err.message}`);
214
- this.recordingStateSubject.next(RecordingState.Idle);
215
- }
216
- }
217
- /**
218
- * Gets the first supported MIME type for MediaRecorder
219
- *
220
- * @returns string|null - The supported MIME type or null if none found
221
- */
222
- getSupportedMimeType() {
223
- const mimeTypes = [
224
- 'video/webm;codecs=vp9,opus',
225
- 'video/webm;codecs=vp8,opus',
226
- 'video/webm;codecs=h264,opus',
227
- 'video/mp4;codecs=h264,aac',
228
- 'video/webm',
229
- ];
230
- for (const mimeType of mimeTypes) {
231
- if (MediaRecorder.isTypeSupported(mimeType)) {
232
- return mimeType;
233
- }
234
- }
235
- return null;
236
- }
237
- /**
238
- * Internal method to stop the recording process
239
- *
240
- * @private
241
- * @returns void
242
- */
243
- stopRecordingInternal() {
244
- if (this.mediaRecorder && (this.recordingStateSubject.value === RecordingState.Recording || this.recordingStateSubject.value === RecordingState.Paused)) {
245
- this.mediaRecorder.stop();
246
- }
247
- this.stream?.getTracks().forEach(track => track.stop());
248
- this.stream = null;
249
- // State will be updated by onstop handler
250
- }
251
- /**
252
- *
253
- * @returns Promise<void>
254
- * @throws Error if recording is not in progress
255
- */
256
- stopRecording() {
257
- this.stopRecordingInternal();
258
- }
259
- /**
260
- * Pauses the current recording
261
- *
262
- * @returns Promise<void>
263
- * @throws Error if recording is not in progress
264
- */
265
- pauseRecording() {
266
- if (this.mediaRecorder && this.recordingStateSubject.value === RecordingState.Recording) {
267
- this.mediaRecorder.pause();
268
- this.recordingStateSubject.next(RecordingState.Paused);
269
- this.stopTimer(); // Pauses the timer display
270
- }
271
- }
272
- /**
273
- * Resumes a paused recording
274
- *
275
- * @returns Promise<void>
276
- * @throws Error if recording is not paused
277
- */
278
- resumeRecording() {
279
- if (this.mediaRecorder && this.recordingStateSubject.value === RecordingState.Paused) {
280
- this.mediaRecorder.resume();
281
- this.recordingStateSubject.next(RecordingState.Recording);
282
- this.startTimer(this.currentRecordedTime); // Resumes timer display
283
- }
284
- }
285
- /**
286
- *
287
- * @param fileName
288
- */
289
- downloadRecording(fileName = 'recording.webm') {
290
- const url = this.recordedUrlSubject.value;
291
- const timestamp = this.recordingTimestampSubject.value;
292
- if (url && timestamp) {
293
- // Format the timestamp for filename (remove spaces and special characters)
294
- const formattedTimestamp = timestamp.replace(/[^a-zA-Z0-9]/g, '_');
295
- // Create filename with timestamp
296
- const fileExtension = fileName.split('.').pop() || 'webm';
297
- const newFileName = `screen_recording_${formattedTimestamp}.${fileExtension}`;
298
- const a = document.createElement('a');
299
- a.style.display = 'none';
300
- a.href = url;
301
- a.download = newFileName;
302
- document.body.appendChild(a);
303
- a.click();
304
- document.body.removeChild(a);
305
- // No need to revoke URL here if user might want to play it again.
306
- // Revoke on cleanup or new recording.
307
- }
308
- else {
309
- this.handleError(this.screenRecordingConstant.NO_RECORDING_DOWNLOAD);
310
- }
311
- }
312
- /**
313
- * Starts the recording timer
314
- *
315
- * @param startTime - Optional start time for the timer
316
- * @private
317
- * @returns void
318
- */
319
- startTimer(startTime = 0) {
320
- this.stopTimer(); // Ensure no multiple timers
321
- this.currentRecordedTime = startTime;
322
- this.recordedTimeSubject.next(this.currentRecordedTime);
323
- this.timerSubscription = timer(0, 1000)
324
- .pipe(tap(() => {
325
- if (this.recordingStateSubject.value === RecordingState.Recording) {
326
- this.currentRecordedTime++;
327
- this.recordedTimeSubject.next(this.currentRecordedTime);
328
- }
329
- }), takeWhile(() => this.recordingStateSubject.value === RecordingState.Recording))
330
- .subscribe();
331
- }
332
- /**
333
- * Stops the recording timer
334
- *
335
- * @private
336
- * @returns void
337
- */
338
- stopTimer() {
339
- if (this.timerSubscription) {
340
- this.timerSubscription.unsubscribe();
341
- this.timerSubscription = null;
342
- }
343
- }
344
- /**
345
- * Cleans up resources from previous recording
346
- *
347
- * @private
348
- * @returns void
349
- */
350
- cleanupPreviousRecording() {
351
- if (this.recordedUrlSubject.value) {
352
- window.URL.revokeObjectURL(this.recordedUrlSubject.value);
353
- this.recordedUrlSubject.next(null);
354
- }
355
- this.recordedBlobs = [];
356
- this.currentRecordedTime = 0;
357
- this.recordedTimeSubject.next(0);
358
- if (this.stream) {
359
- this.stream.getTracks().forEach(track => track.stop());
360
- this.stream = null;
361
- }
362
- if (this.mediaRecorder && this.mediaRecorder.state !== this.screenRecordingConstant.INACTIVE) {
363
- this.mediaRecorder.stop();
364
- }
365
- this.mediaRecorder = null;
366
- }
367
- /**
368
- * Resets the recording state to idle
369
- *
370
- * @returns void
371
- */
372
- resetToIdle() {
373
- this.cleanupPreviousRecording();
374
- this.recordingStateSubject.next(RecordingState.Idle);
375
- this.errorSubject.next(''); // Clear any previous errors
376
- }
377
- /**
378
- * Handles and broadcasts recording errors
379
- *
380
- * @private
381
- * @param error - The error message to handle
382
- * @returns void
383
- */
384
- handleError(error) {
385
- this.errorSubject.next(error);
386
- }
387
- }
388
- ScreenRecorderService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ScreenRecorderService, deps: [{ token: ScreenRecorderConstants }], target: i0.ɵɵFactoryTarget.Injectable });
389
- ScreenRecorderServiceprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ScreenRecorderService, providedIn: 'root' });
390
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ScreenRecorderService, decorators: [{
391
- type: Injectable,
392
- args: [{
393
- providedIn: 'root', // Or provide in your module if preferred
394
- }]
395
- }], ctorParameters: function () { return [{ type: ScreenRecorderConstants }]; } });
71
+ /* eslint-disable @typescript-eslint/no-inferrable-types */
72
+ //import { ScreenRecorderConstants } from '../screen-recorder/screen-recorder-constant';
73
+ class ScreenRecorderService {
74
+ constructor(screenRecordingConstant) {
75
+ this.screenRecordingConstant = screenRecordingConstant;
76
+ this.stream = null;
77
+ this.mediaRecorder = null;
78
+ this.recordedBlobs = [];
79
+ this.currentRecordedTime = 0;
80
+ this.recordingStateSubject = new BehaviorSubject(RecordingState.Idle);
81
+ this.recordingState$ = this.recordingStateSubject.asObservable();
82
+ this.recordedTimeSubject = new BehaviorSubject(0);
83
+ this.recordedTime$ = this.recordedTimeSubject.asObservable();
84
+ this.recordedUrlSubject = new BehaviorSubject(null);
85
+ this.recordedUrl$ = this.recordedUrlSubject.asObservable();
86
+ this.recordingTimestampSubject = new BehaviorSubject('');
87
+ this.recordingTimestamp$ = this.recordingTimestampSubject.asObservable();
88
+ this.errorSubject = new Subject();
89
+ this.error$ = this.errorSubject.asObservable();
90
+ }
91
+ /**
92
+ * Starts the screen recording with optional audio
93
+ *
94
+ * @param options - Configuration options for recording
95
+ * @param options.audio - Whether to include audio in the recording
96
+ *
97
+ * @throws Error if recording is already in progress or if media access is denied
98
+ * @returns Promise<void>
99
+ */
100
+ async startRecording(options) {
101
+ if (this.recordingStateSubject.value === RecordingState.Recording || this.recordingStateSubject.value === RecordingState.Paused) {
102
+ this.handleError(this.screenRecordingConstant.RECORDING_IN_PROGRESS_ERROR);
103
+ return;
104
+ }
105
+ this.cleanupPreviousRecording();
106
+ try {
107
+ // The browser's native picker handles "specific area" selection (window, tab, screen).
108
+ const displayStream = await navigator.mediaDevices.getDisplayMedia({
109
+ video: true
110
+ });
111
+ // If audio is enabled, get the audio stream
112
+ let audioStream = null;
113
+ if (options.audio) {
114
+ audioStream = await navigator.mediaDevices.getUserMedia({
115
+ audio: {
116
+ echoCancellation: true,
117
+ noiseSuppression: true,
118
+ sampleRate: 44100
119
+ }
120
+ });
121
+ }
122
+ // Combine the streams if audio is enabled
123
+ if (audioStream) {
124
+ this.stream = new MediaStream([...displayStream.getTracks(), ...audioStream.getTracks()]);
125
+ }
126
+ else {
127
+ this.stream = displayStream;
128
+ }
129
+ // Listen for when the user stops sharing via browser UI
130
+ this.stream.getVideoTracks()[0].onended = () => {
131
+ if (this.recordingStateSubject.value === RecordingState.Recording || this.recordingStateSubject.value === RecordingState.Paused) {
132
+ this.stopRecordingInternal();
133
+ }
134
+ };
135
+ // Handle errors
136
+ if (!this.stream) {
137
+ this.handleError(this.screenRecordingConstant.FAILED_TO_GET_DISPLAY_MEDIA_STREAM_ERROR);
138
+ return;
139
+ }
140
+ // Set recording timestamp
141
+ const now = new Date();
142
+ const timestamp = now.toLocaleString('en-IN', {
143
+ year: 'numeric',
144
+ month: '2-digit',
145
+ day: '2-digit',
146
+ hour: '2-digit',
147
+ minute: '2-digit',
148
+ hour12: true
149
+ });
150
+ this.recordingTimestampSubject.next(timestamp);
151
+ this.recordedBlobs = [];
152
+ const mimeType = this.getSupportedMimeType();
153
+ if (!mimeType) {
154
+ this.handleError(this.screenRecordingConstant.NO_SUPPORT_MIME);
155
+ this.stream?.getTracks().forEach(track => track.stop());
156
+ this.stream = null;
157
+ return;
158
+ }
159
+ // Create a canvas for timestamp overlay
160
+ const canvas = document.createElement('canvas');
161
+ const videoTrack = this.stream?.getVideoTracks()[0];
162
+ if (videoTrack) {
163
+ const video = document.createElement('video');
164
+ video.srcObject = this.stream;
165
+ // Wait for video to load metadata
166
+ video.onloadedmetadata = () => {
167
+ canvas.width = video.videoWidth;
168
+ canvas.height = video.videoHeight;
169
+ const ctx = canvas.getContext('2d');
170
+ if (ctx) {
171
+ // Draw video frame
172
+ ctx.drawImage(video, 0, 0);
173
+ // Draw timestamp
174
+ ctx.font = '16px Arial';
175
+ ctx.fillStyle = 'white';
176
+ ctx.textAlign = 'right';
177
+ ctx.textBaseline = 'top';
178
+ ctx.fillText(`Recorded on: ${this.recordingTimestampSubject.value}`, canvas.width - 10, 10);
179
+ }
180
+ };
181
+ }
182
+ // Create a new stream with the canvas
183
+ const canvasStream = canvas.captureStream();
184
+ const combinedStream = new MediaStream([...this.stream?.getTracks() ?? [], canvasStream.getVideoTracks()[0]]);
185
+ this.mediaRecorder = new MediaRecorder(combinedStream, { mimeType });
186
+ this.mediaRecorder.ondataavailable = (event) => {
187
+ if (event.data && event.data.size > 0) {
188
+ this.recordedBlobs.push(event.data);
189
+ }
190
+ };
191
+ this.mediaRecorder.onstop = () => {
192
+ const superBuffer = new Blob(this.recordedBlobs, { type: mimeType });
193
+ const url = window.URL.createObjectURL(superBuffer);
194
+ this.recordedUrlSubject.next(url);
195
+ this.recordingStateSubject.next(RecordingState.Stopped);
196
+ this.stopTimer();
197
+ };
198
+ this.mediaRecorder.onerror = (event) => {
199
+ const errorEvent = event; // More specific type if available
200
+ let message = this.screenRecordingConstant.MEDIA_ERROR;
201
+ if (errorEvent.error && errorEvent.error.name) {
202
+ message += `: ${errorEvent.error.name}`;
203
+ if (errorEvent.error.message)
204
+ message += ` - ${errorEvent.error.message}`;
205
+ }
206
+ this.handleError(message);
207
+ this.recordingStateSubject.next(RecordingState.Idle);
208
+ this.stopTimer();
209
+ };
210
+ this.mediaRecorder.start(); // Start recording
211
+ this.recordingStateSubject.next(RecordingState.Recording);
212
+ this.startTimer();
213
+ }
214
+ catch (err) {
215
+ this.handleError(`Error starting screen recording: ${err.name} - ${err.message}`);
216
+ this.recordingStateSubject.next(RecordingState.Idle);
217
+ }
218
+ }
219
+ /**
220
+ * Gets the first supported MIME type for MediaRecorder
221
+ *
222
+ * @returns string|null - The supported MIME type or null if none found
223
+ */
224
+ getSupportedMimeType() {
225
+ const mimeTypes = [
226
+ 'video/webm;codecs=vp9,opus',
227
+ 'video/webm;codecs=vp8,opus',
228
+ 'video/webm;codecs=h264,opus',
229
+ 'video/mp4;codecs=h264,aac', // MP4 might have broader compatibility but less browser support for recording
230
+ 'video/webm',
231
+ ];
232
+ for (const mimeType of mimeTypes) {
233
+ if (MediaRecorder.isTypeSupported(mimeType)) {
234
+ return mimeType;
235
+ }
236
+ }
237
+ return null;
238
+ }
239
+ /**
240
+ * Internal method to stop the recording process
241
+ *
242
+ * @private
243
+ * @returns void
244
+ */
245
+ stopRecordingInternal() {
246
+ if (this.mediaRecorder && (this.recordingStateSubject.value === RecordingState.Recording || this.recordingStateSubject.value === RecordingState.Paused)) {
247
+ this.mediaRecorder.stop();
248
+ }
249
+ this.stream?.getTracks().forEach(track => track.stop());
250
+ this.stream = null;
251
+ // State will be updated by onstop handler
252
+ }
253
+ /**
254
+ *
255
+ * @returns Promise<void>
256
+ * @throws Error if recording is not in progress
257
+ */
258
+ stopRecording() {
259
+ this.stopRecordingInternal();
260
+ }
261
+ /**
262
+ * Pauses the current recording
263
+ *
264
+ * @returns Promise<void>
265
+ * @throws Error if recording is not in progress
266
+ */
267
+ pauseRecording() {
268
+ if (this.mediaRecorder && this.recordingStateSubject.value === RecordingState.Recording) {
269
+ this.mediaRecorder.pause();
270
+ this.recordingStateSubject.next(RecordingState.Paused);
271
+ this.stopTimer(); // Pauses the timer display
272
+ }
273
+ }
274
+ /**
275
+ * Resumes a paused recording
276
+ *
277
+ * @returns Promise<void>
278
+ * @throws Error if recording is not paused
279
+ */
280
+ resumeRecording() {
281
+ if (this.mediaRecorder && this.recordingStateSubject.value === RecordingState.Paused) {
282
+ this.mediaRecorder.resume();
283
+ this.recordingStateSubject.next(RecordingState.Recording);
284
+ this.startTimer(this.currentRecordedTime); // Resumes timer display
285
+ }
286
+ }
287
+ /**
288
+ *
289
+ * @param fileName
290
+ */
291
+ downloadRecording(fileName = 'recording.webm') {
292
+ const url = this.recordedUrlSubject.value;
293
+ const timestamp = this.recordingTimestampSubject.value;
294
+ if (url && timestamp) {
295
+ // Format the timestamp for filename (remove spaces and special characters)
296
+ const formattedTimestamp = timestamp.replace(/[^a-zA-Z0-9]/g, '_');
297
+ // Create filename with timestamp
298
+ const fileExtension = fileName.split('.').pop() || 'webm';
299
+ const newFileName = `screen_recording_${formattedTimestamp}.${fileExtension}`;
300
+ const a = document.createElement('a');
301
+ a.style.display = 'none';
302
+ a.href = url;
303
+ a.download = newFileName;
304
+ document.body.appendChild(a);
305
+ a.click();
306
+ document.body.removeChild(a);
307
+ // No need to revoke URL here if user might want to play it again.
308
+ // Revoke on cleanup or new recording.
309
+ }
310
+ else {
311
+ this.handleError(this.screenRecordingConstant.NO_RECORDING_DOWNLOAD);
312
+ }
313
+ }
314
+ /**
315
+ * Starts the recording timer
316
+ *
317
+ * @param startTime - Optional start time for the timer
318
+ * @private
319
+ * @returns void
320
+ */
321
+ startTimer(startTime = 0) {
322
+ this.stopTimer(); // Ensure no multiple timers
323
+ this.currentRecordedTime = startTime;
324
+ this.recordedTimeSubject.next(this.currentRecordedTime);
325
+ this.timerSubscription = timer(0, 1000)
326
+ .pipe(tap(() => {
327
+ if (this.recordingStateSubject.value === RecordingState.Recording) {
328
+ this.currentRecordedTime++;
329
+ this.recordedTimeSubject.next(this.currentRecordedTime);
330
+ }
331
+ }), takeWhile(() => this.recordingStateSubject.value === RecordingState.Recording))
332
+ .subscribe();
333
+ }
334
+ /**
335
+ * Stops the recording timer
336
+ *
337
+ * @private
338
+ * @returns void
339
+ */
340
+ stopTimer() {
341
+ if (this.timerSubscription) {
342
+ this.timerSubscription.unsubscribe();
343
+ this.timerSubscription = null;
344
+ }
345
+ }
346
+ /**
347
+ * Cleans up resources from previous recording
348
+ *
349
+ * @private
350
+ * @returns void
351
+ */
352
+ cleanupPreviousRecording() {
353
+ if (this.recordedUrlSubject.value) {
354
+ window.URL.revokeObjectURL(this.recordedUrlSubject.value);
355
+ this.recordedUrlSubject.next(null);
356
+ }
357
+ this.recordedBlobs = [];
358
+ this.currentRecordedTime = 0;
359
+ this.recordedTimeSubject.next(0);
360
+ if (this.stream) {
361
+ this.stream.getTracks().forEach(track => track.stop());
362
+ this.stream = null;
363
+ }
364
+ if (this.mediaRecorder && this.mediaRecorder.state !== this.screenRecordingConstant.INACTIVE) {
365
+ this.mediaRecorder.stop();
366
+ }
367
+ this.mediaRecorder = null;
368
+ }
369
+ /**
370
+ * Resets the recording state to idle
371
+ *
372
+ * @returns void
373
+ */
374
+ resetToIdle() {
375
+ this.cleanupPreviousRecording();
376
+ this.recordingStateSubject.next(RecordingState.Idle);
377
+ this.errorSubject.next(''); // Clear any previous errors
378
+ }
379
+ /**
380
+ * Handles and broadcasts recording errors
381
+ *
382
+ * @private
383
+ * @param error - The error message to handle
384
+ * @returns void
385
+ */
386
+ handleError(error) {
387
+ this.errorSubject.next(error);
388
+ }
389
+ static { thisfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ScreenRecorderService, deps: [{ token: ScreenRecorderConstants }], target: i0.ɵɵFactoryTarget.Injectable }); }
390
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ScreenRecorderService, providedIn: 'root' }); }
391
+ }
392
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: ScreenRecorderService, decorators: [{
393
+ type: Injectable,
394
+ args: [{
395
+ providedIn: 'root', // Or provide in your module if preferred
396
+ }]
397
+ }], ctorParameters: () => [{ type: ScreenRecorderConstants }] });
396
398
 
397
- class ScreenRecorderComponent {
398
- set rucInputData(value) {
399
- this._options = {
400
- position: ScreenRecorderPosition.Bottom,
401
- displayFormat: DisplayFormat.Icon,
402
- showAudioToggle: true,
403
- defaultAudioState: true,
404
- downloadFileName: 'screen-recording.webm',
405
- icons: { ...this.screenRecordingConstant.DEFAULT_ICONS },
406
- showTimer: true,
407
- showPauseResume: true,
408
- showPlaybackControls: true,
409
- showDownloadButton: true,
410
- useUTCForTimer: false,
411
- allowSpecificAreaSelection: true,
412
- accessibilityLabels: { ...this.screenRecordingConstant.DEFAULT_LABELS },
413
- ...value,
414
- };
415
- // Deep merge icons and labels
416
- if (value.icons) {
417
- this._options.icons = { ...this.screenRecordingConstant.DEFAULT_ICONS, ...value.icons };
418
- }
419
- if (value.accessibilityLabels) {
420
- this._options.accessibilityLabels = { ...this.screenRecordingConstant.DEFAULT_LABELS, ...value.accessibilityLabels };
421
- }
422
- this.isAudioEnabled = this._options.defaultAudioState ?? true;
423
- }
424
- get rucInputData() {
425
- return this._options;
426
- }
427
- /**
428
- * Checks if recording is currently in progress
429
- *
430
- * @returns boolean - True if recording is in progress, false otherwise
431
- */
432
- isRecordingInProgress() {
433
- return this.recordingState === RecordingState.Recording;
434
- }
435
- /**
436
- * Gets the appropriate icon for recording based on audio state and settings
437
- * @returns string - The icon name to display
438
- */
439
- getRecordingIcon() {
440
- if (this.isAudioEnabled && this.rucInputData.showAudioToggle) {
441
- return this.getIcon('recordWithAudio');
442
- }
443
- return this.getIcon('record');
444
- }
445
- constructor(screenRecorderService, sanitizer, cdr, screenRecordingConstant) {
446
- this.screenRecorderService = screenRecorderService;
447
- this.sanitizer = sanitizer;
448
- this.cdr = cdr;
449
- this.screenRecordingConstant = screenRecordingConstant;
450
- this.recordingState = RecordingState.Idle;
451
- this.recordedTime = 0;
452
- this.formattedTime = '00:00:00';
453
- this.safeRecordedUrl = null;
454
- this.recordingTimestamp = '';
455
- this.errorMessage = null;
456
- this.isBrowserSupported = true;
457
- this.isAudioEnabled = true;
458
- // Expose enums to template
459
- this.DisplayFormat = DisplayFormat;
460
- this.RecordingState = RecordingState;
461
- this.ScreenRecorderPosition = ScreenRecorderPosition;
462
- this.subscriptions = new Subscription();
463
- // Initialize with default options. The setter will override if an input is provided.
464
- if (!this._options) {
465
- this.rucInputData = {}; // Trigger setter with empty object to apply defaults
466
- }
467
- }
468
- ngOnInit() {
469
- this.subscriptions.add(this.screenRecorderService.recordingState$.subscribe((state) => {
470
- this.recordingState = state;
471
- if (state === RecordingState.Idle || state === RecordingState.Stopped) {
472
- // Ensure audio toggle is reset to default if needed, or based on user action
473
- }
474
- this.cdr.detectChanges();
475
- }));
476
- this.subscriptions.add(this.screenRecorderService.recordingTimestamp$.subscribe((timestamp) => {
477
- this.recordingTimestamp = timestamp;
478
- this.cdr.detectChanges();
479
- }));
480
- this.subscriptions.add(this.screenRecorderService.recordedTime$.subscribe((time) => {
481
- this.recordedTime = time;
482
- this.formattedTime = this.formatTimeDisplay(time);
483
- this.cdr.detectChanges();
484
- }));
485
- this.subscriptions.add(this.screenRecorderService.recordedUrl$.subscribe((url) => {
486
- this.safeRecordedUrl = url ? this.sanitizer.bypassSecurityTrustUrl(url) : null;
487
- this.cdr.detectChanges();
488
- }));
489
- this.subscriptions.add(this.screenRecorderService.error$.subscribe((error) => {
490
- if (error != this.screenRecordingConstant.PERMISSION_DENIED) {
491
- this.errorMessage = error;
492
- }
493
- this.cdr.detectChanges();
494
- }));
495
- }
496
- ngOnDestroy() {
497
- if (this.subscriptions) {
498
- this.subscriptions.unsubscribe();
499
- }
500
- }
501
- /**
502
- * Toggles the recording state between start and stop
503
- *
504
- * @returns Promise<void>
505
- */
506
- async toggleRecord() {
507
- this.errorMessage = null; // Clear previous errors
508
- if (this.recordingState === RecordingState.Idle || this.recordingState === RecordingState.Stopped) {
509
- // Set recording timestamp when starting recording
510
- const now = new Date();
511
- this.recordingTimestamp = now.toLocaleString('en-IN', {
512
- year: 'numeric',
513
- month: '2-digit',
514
- day: '2-digit',
515
- hour: '2-digit',
516
- minute: '2-digit',
517
- hour12: true
518
- });
519
- await this.screenRecorderService.startRecording({ audio: this.isAudioEnabled });
520
- }
521
- else {
522
- this.screenRecorderService.stopRecording();
523
- }
524
- }
525
- /**
526
- * Toggles between pause and resume states during recording
527
- *
528
- * @returns void
529
- */
530
- togglePauseResume() {
531
- this.errorMessage = null;
532
- if (this.recordingState === RecordingState.Recording) {
533
- this.screenRecorderService.pauseRecording();
534
- }
535
- else if (this.recordingState === RecordingState.Paused) {
536
- this.screenRecorderService.resumeRecording();
537
- }
538
- }
539
- /**
540
- * Checks if recording can be resumed
541
- *
542
- * @returns boolean - True if recording can be resumed, false otherwise
543
- */
544
- canResume() {
545
- return this.recordingState === RecordingState.Paused;
546
- }
547
- /**
548
- * Downloads the recorded video file
549
- *
550
- * @returns void
551
- */
552
- onDownload() {
553
- this.errorMessage = null;
554
- this.screenRecorderService.downloadRecording(this.rucInputData.downloadFileName);
555
- }
556
- /**
557
- * Checks if recording can be downloaded
558
- *
559
- * @returns boolean - True if recording can be downloaded, false otherwise
560
- */
561
- canDownload() {
562
- return this.recordingState === RecordingState.Stopped;
563
- }
564
- /**
565
- * Toggles audio recording on/off
566
- *
567
- * @returns void
568
- */
569
- toggleAudio() {
570
- this.isAudioEnabled = !this.isAudioEnabled;
571
- this.cdr.detectChanges();
572
- }
573
- /**
574
- *
575
- * @param iconName
576
- * @returns
577
- */
578
- getIcon(iconName) {
579
- return this.rucInputData.icons?.[iconName] || this.screenRecordingConstant.DEFAULT_ICONS[iconName];
580
- }
581
- /**
582
- *
583
- * @param labelName
584
- * @returns
585
- */
586
- getLabel(labelName) {
587
- return this.rucInputData.accessibilityLabels?.[labelName] || this.screenRecordingConstant.DEFAULT_LABELS[labelName];
588
- }
589
- /**
590
- *
591
- * @param time - The time in seconds to format
592
- * @returns string - Formatted time string (HH:MM:SS)
593
- */
594
- formatTimeDisplay(time) {
595
- const hours = Math.floor(time / 3600);
596
- const minutes = Math.floor((time % 3600) / 60);
597
- const seconds = time % 60;
598
- const pad = (num) => num.toString().padStart(2, '0');
599
- if (this.rucInputData && this.rucInputData.useUTCForTimer) {
600
- // This is a simple duration format, not a specific UTC time.
601
- // For actual UTC time of day, you'd use new Date().toUTCString() or similar.
602
- // Assuming timer should show elapsed time formatted as HH:MM:SS.
603
- return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
604
- }
605
- return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
606
- }
607
- // Method to reset the component and service to initial state
608
- reset() {
609
- this.screenRecorderService.resetToIdle();
610
- this.isAudioEnabled = this.rucInputData.defaultAudioState ?? true;
611
- this.cdr.detectChanges();
612
- }
613
- }
614
- ScreenRecorderComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ScreenRecorderComponent, deps: [{ token: ScreenRecorderService }, { token: i2.DomSanitizer }, { token: i0.ChangeDetectorRef }, { token: ScreenRecorderConstants }], target: i0.ɵɵFactoryTarget.Component });
615
- ScreenRecorderComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: ScreenRecorderComponent, selector: "uxp-screen-recorder", inputs: { customTheme: "customTheme", rucInputData: "rucInputData" }, viewQueries: [{ propertyName: "videoPlayer", first: true, predicate: ["videoPlayer"], descendants: true }], ngImport: i0, template: "<div class=\"screen-recorder-controls\" [ngClass]=\"customTheme + ' position-' + (rucInputData.position || ScreenRecorderPosition.Bottom)\"\r\n *ngIf=\"isBrowserSupported\">\r\n\r\n <!-- Error Message -->\r\n <div *ngIf=\"errorMessage\" class=\"recorder-error\">\r\n {{ errorMessage }}\r\n </div>\r\n\r\n <!-- Record/Stop Button -->\r\n <button\r\n *ngIf=\"recordingState === RecordingState.Idle || recordingState === RecordingState.Stopped\" (click)=\"toggleRecord()\"\r\n [attr.aria-label]=\"getLabel('recordButton')\" [attr.aria-pressed]=\"false\" class=\"recorder-button record-button\">\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Icon\">\r\n <span class=\"material-icons\">{{ getRecordingIcon() || getIcon('record') }}</span>\r\n </ng-container>\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Text\">{{ getLabel('recordButton')\r\n }}</ng-container>\r\n </button>\r\n\r\n <button\r\n *ngIf=\"recordingState === RecordingState.Recording || recordingState === RecordingState.Paused\"\r\n (click)=\"toggleRecord()\" [attr.aria-label]=\"getLabel('stopButton')\" [attr.aria-pressed]=\"true\"\r\n class=\"recorder-button stop-button\">\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Icon\">\r\n <span class=\"material-icons\">{{ getIcon('stop') }}</span>\r\n </ng-container>\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Text\">{{ getLabel('stopButton') }}</ng-container>\r\n </button>\r\n\r\n <!-- Pause/Resume Button -->\r\n <button *ngIf=\"rucInputData.showPauseResume && recordingState === RecordingState.Recording\"\r\n (click)=\"togglePauseResume()\" [attr.aria-label]=\"getLabel('pauseButton')\" [attr.aria-pressed]=\"false\"\r\n class=\"recorder-button pause-button\">\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Icon\">\r\n <span class=\"material-icons\">{{ getIcon('pause') }}</span>\r\n </ng-container>\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Text\">{{ getLabel('pauseButton') }}</ng-container>\r\n </button>\r\n <button *ngIf=\"rucInputData.showPauseResume && recordingState === RecordingState.Paused\"\r\n (click)=\"togglePauseResume()\" [attr.aria-label]=\"getLabel('resumeButton')\" [attr.aria-pressed]=\"true\"\r\n class=\"recorder-button resume-button\">\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Icon\">\r\n <span class=\"material-icons\">{{ getIcon('resume') }}</span>\r\n </ng-container>\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Text\">{{ getLabel('resumeButton')\r\n }}</ng-container>\r\n </button>\r\n\r\n <!-- Audio Toggle Button -->\r\n <button\r\n *ngIf=\"rucInputData.showAudioToggle && (recordingState === RecordingState.Idle || recordingState === RecordingState.Stopped)\"\r\n (click)=\"toggleAudio()\" [attr.aria-label]=\"getLabel('audioToggleButton')\" [attr.aria-pressed]=\"isAudioEnabled\"\r\n class=\"recorder-button audio-toggle-button\">\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Icon\">\r\n <span class=\"material-icons\">{{ isAudioEnabled ? getIcon('audioOn') : getIcon('audioOff') }}</span>\r\n </ng-container>\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Text\">\r\n {{ isAudioEnabled ? screenRecordingConstant.AUDIO_ON : screenRecordingConstant.AUDIO_OFF }}\r\n </ng-container>\r\n </button>\r\n\r\n <!-- Timer -->\r\n <div\r\n *ngIf=\"rucInputData.showTimer && (recordingState === RecordingState.Recording || recordingState === RecordingState.Paused)\"\r\n class=\"recorder-timer\" aria-live=\"polite\" aria-atomic=\"true\">\r\n {{ formattedTime }}\r\n </div>\r\n\r\n <!-- Download Button -->\r\n <button\r\n *ngIf=\"rucInputData.showDownloadButton && recordingState === RecordingState.Stopped && safeRecordedUrl\"\r\n (click)=\"onDownload()\" [attr.aria-label]=\"getLabel('downloadButton')\" class=\"recorder-button download-button\">\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Icon\">\r\n <span class=\"material-icons\">{{ getIcon('download') }}</span>\r\n </ng-container>\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Text\">{{ getLabel('downloadButton')\r\n }}</ng-container>\r\n </button>\r\n\r\n <!-- Playback Area -->\r\n <div *ngIf=\"rucInputData.showPlaybackControls && recordingState === RecordingState.Stopped && safeRecordedUrl\"\r\n class=\"playback-area\">\r\n <video #videoPlayer [src]=\"safeRecordedUrl\" controls [attr.aria-label]=\"getLabel('playbackVideo')\"></video>\r\n <div class=\"video-timer-overlay\">\r\n <span>Recorded on: {{ recordingTimestamp }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- Reset Button (optional, good for testing/clearing state) -->\r\n <button *ngIf=\"recordingState === RecordingState.Stopped && safeRecordedUrl\" (click)=\"reset()\"\r\n aria-label=\"New Recording\" class=\"recorder-button reset-button\">\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Icon\">\r\n <span class=\"material-icons\">refresh</span>\r\n </ng-container>\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Text\">New</ng-container>\r\n </button>\r\n\r\n</div>\r\n\r\n<div *ngIf=\"!isBrowserSupported\" class=\"recorder-notsupported\" [ngClass]=\"customTheme\">\r\n {{screenRecordingConstant.NO_SUPPORT}}\r\n</div>", styles: [":host{display:block;font-family:sans-serif}.screen-recorder-controls{display:flex;gap:8px;align-items:center;padding:8px;border-radius:4px;position:relative;z-index:1000}.screen-recorder-controls.position-top-left{margin-top:10px;margin-left:10px;flex-direction:row}.screen-recorder-controls.position-top-right{margin-top:10px;margin-right:10px;flex-direction:row}.screen-recorder-controls.position-bottom-left{margin-bottom:10px;margin-left:10px;flex-direction:row}.screen-recorder-controls.position-bottom-right{margin-bottom:10px;margin-right:10px;flex-direction:row}.screen-recorder-controls.position-left,.screen-recorder-controls.position-right{float:right}.recorder-button{padding:8px 12px;border:1px solid #ccc;border-radius:4px;background-color:#fff;cursor:pointer;display:inline-flex;align-items:center;gap:4px}.recorder-button:hover{background-color:#e9e9e9}.recorder-button:active{background-color:#d0d0d0}.recorder-button.record-button{color:red}.material-icons{font-family:Material Icons;font-weight:400;font-style:normal;font-size:20px;display:inline-block;line-height:1;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:\"liga\"}.recorder-timer{padding:0 8px;font-feature-settings:\"tnum\";font-variant-numeric:tabular-nums}.playback-area{width:100%;max-width:100%;margin-top:1rem;position:relative}.playback-area video{width:100%;height:auto;max-width:100%}.playback-area .video-timer-overlay{position:absolute;top:10px;right:10px;background:rgba(0,0,0,.7);color:#fff;padding:5px 10px;border-radius:4px;font-size:14px;z-index:10}.recorder-error{color:red;padding:8px;border:1px solid red;background-color:#ffe0e0;border-radius:4px;margin-bottom:8px}.recorder-notsupported{padding:10px;background-color:#fff3cd;border:1px solid #ffeeba;color:#856404;border-radius:4px}\n"], dependencies: [{ kind: "directive", type: i4.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
616
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: ScreenRecorderComponent, decorators: [{
617
- type: Component,
618
- args: [{ selector: 'uxp-screen-recorder', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"screen-recorder-controls\" [ngClass]=\"customTheme + ' position-' + (rucInputData.position || ScreenRecorderPosition.Bottom)\"\r\n *ngIf=\"isBrowserSupported\">\r\n\r\n <!-- Error Message -->\r\n <div *ngIf=\"errorMessage\" class=\"recorder-error\">\r\n {{ errorMessage }}\r\n </div>\r\n\r\n <!-- Record/Stop Button -->\r\n <button\r\n *ngIf=\"recordingState === RecordingState.Idle || recordingState === RecordingState.Stopped\" (click)=\"toggleRecord()\"\r\n [attr.aria-label]=\"getLabel('recordButton')\" [attr.aria-pressed]=\"false\" class=\"recorder-button record-button\">\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Icon\">\r\n <span class=\"material-icons\">{{ getRecordingIcon() || getIcon('record') }}</span>\r\n </ng-container>\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Text\">{{ getLabel('recordButton')\r\n }}</ng-container>\r\n </button>\r\n\r\n <button\r\n *ngIf=\"recordingState === RecordingState.Recording || recordingState === RecordingState.Paused\"\r\n (click)=\"toggleRecord()\" [attr.aria-label]=\"getLabel('stopButton')\" [attr.aria-pressed]=\"true\"\r\n class=\"recorder-button stop-button\">\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Icon\">\r\n <span class=\"material-icons\">{{ getIcon('stop') }}</span>\r\n </ng-container>\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Text\">{{ getLabel('stopButton') }}</ng-container>\r\n </button>\r\n\r\n <!-- Pause/Resume Button -->\r\n <button *ngIf=\"rucInputData.showPauseResume && recordingState === RecordingState.Recording\"\r\n (click)=\"togglePauseResume()\" [attr.aria-label]=\"getLabel('pauseButton')\" [attr.aria-pressed]=\"false\"\r\n class=\"recorder-button pause-button\">\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Icon\">\r\n <span class=\"material-icons\">{{ getIcon('pause') }}</span>\r\n </ng-container>\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Text\">{{ getLabel('pauseButton') }}</ng-container>\r\n </button>\r\n <button *ngIf=\"rucInputData.showPauseResume && recordingState === RecordingState.Paused\"\r\n (click)=\"togglePauseResume()\" [attr.aria-label]=\"getLabel('resumeButton')\" [attr.aria-pressed]=\"true\"\r\n class=\"recorder-button resume-button\">\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Icon\">\r\n <span class=\"material-icons\">{{ getIcon('resume') }}</span>\r\n </ng-container>\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Text\">{{ getLabel('resumeButton')\r\n }}</ng-container>\r\n </button>\r\n\r\n <!-- Audio Toggle Button -->\r\n <button\r\n *ngIf=\"rucInputData.showAudioToggle && (recordingState === RecordingState.Idle || recordingState === RecordingState.Stopped)\"\r\n (click)=\"toggleAudio()\" [attr.aria-label]=\"getLabel('audioToggleButton')\" [attr.aria-pressed]=\"isAudioEnabled\"\r\n class=\"recorder-button audio-toggle-button\">\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Icon\">\r\n <span class=\"material-icons\">{{ isAudioEnabled ? getIcon('audioOn') : getIcon('audioOff') }}</span>\r\n </ng-container>\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Text\">\r\n {{ isAudioEnabled ? screenRecordingConstant.AUDIO_ON : screenRecordingConstant.AUDIO_OFF }}\r\n </ng-container>\r\n </button>\r\n\r\n <!-- Timer -->\r\n <div\r\n *ngIf=\"rucInputData.showTimer && (recordingState === RecordingState.Recording || recordingState === RecordingState.Paused)\"\r\n class=\"recorder-timer\" aria-live=\"polite\" aria-atomic=\"true\">\r\n {{ formattedTime }}\r\n </div>\r\n\r\n <!-- Download Button -->\r\n <button\r\n *ngIf=\"rucInputData.showDownloadButton && recordingState === RecordingState.Stopped && safeRecordedUrl\"\r\n (click)=\"onDownload()\" [attr.aria-label]=\"getLabel('downloadButton')\" class=\"recorder-button download-button\">\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Icon\">\r\n <span class=\"material-icons\">{{ getIcon('download') }}</span>\r\n </ng-container>\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Text\">{{ getLabel('downloadButton')\r\n }}</ng-container>\r\n </button>\r\n\r\n <!-- Playback Area -->\r\n <div *ngIf=\"rucInputData.showPlaybackControls && recordingState === RecordingState.Stopped && safeRecordedUrl\"\r\n class=\"playback-area\">\r\n <video #videoPlayer [src]=\"safeRecordedUrl\" controls [attr.aria-label]=\"getLabel('playbackVideo')\"></video>\r\n <div class=\"video-timer-overlay\">\r\n <span>Recorded on: {{ recordingTimestamp }}</span>\r\n </div>\r\n </div>\r\n\r\n <!-- Reset Button (optional, good for testing/clearing state) -->\r\n <button *ngIf=\"recordingState === RecordingState.Stopped && safeRecordedUrl\" (click)=\"reset()\"\r\n aria-label=\"New Recording\" class=\"recorder-button reset-button\">\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Icon\">\r\n <span class=\"material-icons\">refresh</span>\r\n </ng-container>\r\n <ng-container *ngIf=\"rucInputData.displayFormat === DisplayFormat.Text\">New</ng-container>\r\n </button>\r\n\r\n</div>\r\n\r\n<div *ngIf=\"!isBrowserSupported\" class=\"recorder-notsupported\" [ngClass]=\"customTheme\">\r\n {{screenRecordingConstant.NO_SUPPORT}}\r\n</div>", styles: [":host{display:block;font-family:sans-serif}.screen-recorder-controls{display:flex;gap:8px;align-items:center;padding:8px;border-radius:4px;position:relative;z-index:1000}.screen-recorder-controls.position-top-left{margin-top:10px;margin-left:10px;flex-direction:row}.screen-recorder-controls.position-top-right{margin-top:10px;margin-right:10px;flex-direction:row}.screen-recorder-controls.position-bottom-left{margin-bottom:10px;margin-left:10px;flex-direction:row}.screen-recorder-controls.position-bottom-right{margin-bottom:10px;margin-right:10px;flex-direction:row}.screen-recorder-controls.position-left,.screen-recorder-controls.position-right{float:right}.recorder-button{padding:8px 12px;border:1px solid #ccc;border-radius:4px;background-color:#fff;cursor:pointer;display:inline-flex;align-items:center;gap:4px}.recorder-button:hover{background-color:#e9e9e9}.recorder-button:active{background-color:#d0d0d0}.recorder-button.record-button{color:red}.material-icons{font-family:Material Icons;font-weight:400;font-style:normal;font-size:20px;display:inline-block;line-height:1;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:\"liga\"}.recorder-timer{padding:0 8px;font-feature-settings:\"tnum\";font-variant-numeric:tabular-nums}.playback-area{width:100%;max-width:100%;margin-top:1rem;position:relative}.playback-area video{width:100%;height:auto;max-width:100%}.playback-area .video-timer-overlay{position:absolute;top:10px;right:10px;background:rgba(0,0,0,.7);color:#fff;padding:5px 10px;border-radius:4px;font-size:14px;z-index:10}.recorder-error{color:red;padding:8px;border:1px solid red;background-color:#ffe0e0;border-radius:4px;margin-bottom:8px}.recorder-notsupported{padding:10px;background-color:#fff3cd;border:1px solid #ffeeba;color:#856404;border-radius:4px}\n"] }]
619
- }], ctorParameters: function () { return [{ type: ScreenRecorderService }, { type: i2.DomSanitizer }, { type: i0.ChangeDetectorRef }, { type: ScreenRecorderConstants }]; }, propDecorators: { customTheme: [{
620
- type: Input
621
- }], rucInputData: [{
622
- type: Input
623
- }], videoPlayer: [{
624
- type: ViewChild,
625
- args: ['videoPlayer']
399
+ /* eslint-disable @typescript-eslint/no-inferrable-types */
400
+ class RuclibScreenRecorderComponent {
401
+ set rucInputData(value) {
402
+ this._options = {
403
+ position: ScreenRecorderPosition.Bottom,
404
+ displayFormat: DisplayFormat.Icon,
405
+ showAudioToggle: true,
406
+ defaultAudioState: true,
407
+ downloadFileName: 'screen-recording.webm',
408
+ icons: { ...this.screenRecordingConstant.DEFAULT_ICONS },
409
+ showTimer: true,
410
+ showPauseResume: true,
411
+ showPlaybackControls: true,
412
+ showDownloadButton: true,
413
+ useUTCForTimer: false,
414
+ allowSpecificAreaSelection: true, // Default to allowing browser's native picker
415
+ accessibilityLabels: { ...this.screenRecordingConstant.DEFAULT_LABELS },
416
+ ...value,
417
+ };
418
+ // Deep merge icons and labels
419
+ if (value.icons) {
420
+ this._options.icons = { ...this.screenRecordingConstant.DEFAULT_ICONS, ...value.icons };
421
+ }
422
+ if (value.accessibilityLabels) {
423
+ this._options.accessibilityLabels = { ...this.screenRecordingConstant.DEFAULT_LABELS, ...value.accessibilityLabels };
424
+ }
425
+ this.isAudioEnabled = this._options.defaultAudioState ?? true;
426
+ }
427
+ get rucInputData() {
428
+ return this._options;
429
+ }
430
+ /**
431
+ * Checks if recording is currently in progress
432
+ *
433
+ * @returns boolean - True if recording is in progress, false otherwise
434
+ */
435
+ isRecordingInProgress() {
436
+ return this.recordingState === RecordingState.Recording;
437
+ }
438
+ /**
439
+ * Gets the appropriate icon for recording based on audio state and settings
440
+ * @returns string - The icon name to display
441
+ */
442
+ getRecordingIcon() {
443
+ if (this.isAudioEnabled && this.rucInputData.showAudioToggle) {
444
+ return this.getIcon('recordWithAudio');
445
+ }
446
+ return this.getIcon('record');
447
+ }
448
+ constructor(screenRecorderService, sanitizer, cdr, screenRecordingConstant) {
449
+ this.screenRecorderService = screenRecorderService;
450
+ this.sanitizer = sanitizer;
451
+ this.cdr = cdr;
452
+ this.screenRecordingConstant = screenRecordingConstant;
453
+ this.recordingState = RecordingState.Idle;
454
+ this.recordedTime = 0;
455
+ this.formattedTime = '00:00:00';
456
+ this.safeRecordedUrl = null;
457
+ this.recordingTimestamp = '';
458
+ this.errorMessage = null;
459
+ this.isBrowserSupported = true;
460
+ this.isAudioEnabled = true;
461
+ // Expose enums to template
462
+ this.DisplayFormat = DisplayFormat;
463
+ this.RecordingState = RecordingState;
464
+ this.ScreenRecorderPosition = ScreenRecorderPosition;
465
+ this.subscriptions = new Subscription();
466
+ // Initialize with default options. The setter will override if an input is provided.
467
+ if (!this._options) {
468
+ this.rucInputData = {}; // Trigger setter with empty object to apply defaults
469
+ }
470
+ }
471
+ ngOnInit() {
472
+ this.subscriptions.add(this.screenRecorderService.recordingState$.subscribe((state) => {
473
+ this.recordingState = state;
474
+ if (state === RecordingState.Idle || state === RecordingState.Stopped) {
475
+ // Ensure audio toggle is reset to default if needed, or based on user action
476
+ }
477
+ this.cdr.detectChanges();
478
+ }));
479
+ this.subscriptions.add(this.screenRecorderService.recordingTimestamp$.subscribe((timestamp) => {
480
+ this.recordingTimestamp = timestamp;
481
+ this.cdr.detectChanges();
482
+ }));
483
+ this.subscriptions.add(this.screenRecorderService.recordedTime$.subscribe((time) => {
484
+ this.recordedTime = time;
485
+ this.formattedTime = this.formatTimeDisplay(time);
486
+ this.cdr.detectChanges();
487
+ }));
488
+ this.subscriptions.add(this.screenRecorderService.recordedUrl$.subscribe((url) => {
489
+ this.safeRecordedUrl = url ? this.sanitizer.bypassSecurityTrustUrl(url) : null;
490
+ this.cdr.detectChanges();
491
+ }));
492
+ this.subscriptions.add(this.screenRecorderService.error$.subscribe((error) => {
493
+ if (error != this.screenRecordingConstant.PERMISSION_DENIED) {
494
+ this.errorMessage = error;
495
+ }
496
+ this.cdr.detectChanges();
497
+ }));
498
+ }
499
+ ngOnDestroy() {
500
+ if (this.subscriptions) {
501
+ this.subscriptions.unsubscribe();
502
+ }
503
+ }
504
+ /**
505
+ * Toggles the recording state between start and stop
506
+ *
507
+ * @returns Promise<void>
508
+ */
509
+ async toggleRecord() {
510
+ this.errorMessage = null; // Clear previous errors
511
+ if (this.recordingState === RecordingState.Idle || this.recordingState === RecordingState.Stopped) {
512
+ // Set recording timestamp when starting recording
513
+ const now = new Date();
514
+ this.recordingTimestamp = now.toLocaleString('en-IN', {
515
+ year: 'numeric',
516
+ month: '2-digit',
517
+ day: '2-digit',
518
+ hour: '2-digit',
519
+ minute: '2-digit',
520
+ hour12: true
521
+ });
522
+ await this.screenRecorderService.startRecording({ audio: this.isAudioEnabled });
523
+ }
524
+ else {
525
+ this.screenRecorderService.stopRecording();
526
+ }
527
+ }
528
+ /**
529
+ * Toggles between pause and resume states during recording
530
+ *
531
+ * @returns void
532
+ */
533
+ togglePauseResume() {
534
+ this.errorMessage = null;
535
+ if (this.recordingState === RecordingState.Recording) {
536
+ this.screenRecorderService.pauseRecording();
537
+ }
538
+ else if (this.recordingState === RecordingState.Paused) {
539
+ this.screenRecorderService.resumeRecording();
540
+ }
541
+ }
542
+ /**
543
+ * Checks if recording can be resumed
544
+ *
545
+ * @returns boolean - True if recording can be resumed, false otherwise
546
+ */
547
+ canResume() {
548
+ return this.recordingState === RecordingState.Paused;
549
+ }
550
+ /**
551
+ * Downloads the recorded video file
552
+ *
553
+ * @returns void
554
+ */
555
+ onDownload() {
556
+ this.errorMessage = null;
557
+ this.screenRecorderService.downloadRecording(this.rucInputData.downloadFileName);
558
+ }
559
+ /**
560
+ * Checks if recording can be downloaded
561
+ *
562
+ * @returns boolean - True if recording can be downloaded, false otherwise
563
+ */
564
+ canDownload() {
565
+ return this.recordingState === RecordingState.Stopped;
566
+ }
567
+ /**
568
+ * Toggles audio recording on/off
569
+ *
570
+ * @returns void
571
+ */
572
+ toggleAudio() {
573
+ this.isAudioEnabled = !this.isAudioEnabled;
574
+ this.cdr.detectChanges();
575
+ }
576
+ /**
577
+ *
578
+ * @param iconName
579
+ * @returns
580
+ */
581
+ getIcon(iconName) {
582
+ return this.rucInputData.icons?.[iconName] || this.screenRecordingConstant.DEFAULT_ICONS[iconName];
583
+ }
584
+ /**
585
+ *
586
+ * @param labelName
587
+ * @returns
588
+ */
589
+ getLabel(labelName) {
590
+ return this.rucInputData.accessibilityLabels?.[labelName] || this.screenRecordingConstant.DEFAULT_LABELS[labelName];
591
+ }
592
+ /**
593
+ *
594
+ * @param time - The time in seconds to format
595
+ * @returns string - Formatted time string (HH:MM:SS)
596
+ */
597
+ formatTimeDisplay(time) {
598
+ const hours = Math.floor(time / 3600);
599
+ const minutes = Math.floor((time % 3600) / 60);
600
+ const seconds = time % 60;
601
+ const pad = (num) => num.toString().padStart(2, '0');
602
+ if (this.rucInputData && this.rucInputData.useUTCForTimer) {
603
+ // This is a simple duration format, not a specific UTC time.
604
+ // For actual UTC time of day, you'd use new Date().toUTCString() or similar.
605
+ // Assuming timer should show elapsed time formatted as HH:MM:SS.
606
+ return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
607
+ }
608
+ return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;
609
+ }
610
+ // Method to reset the component and service to initial state
611
+ reset() {
612
+ this.screenRecorderService.resetToIdle();
613
+ this.isAudioEnabled = this.rucInputData.defaultAudioState ?? true;
614
+ this.cdr.detectChanges();
615
+ }
616
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: RuclibScreenRecorderComponent, deps: [{ token: ScreenRecorderService }, { token: i2.DomSanitizer }, { token: i0.ChangeDetectorRef }, { token: ScreenRecorderConstants }], target: i0.ɵɵFactoryTarget.Component }); }
617
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.9", type: RuclibScreenRecorderComponent, isStandalone: true, selector: "uxp-ruclib-screen-recorder", inputs: { customTheme: "customTheme", rucInputData: "rucInputData" }, viewQueries: [{ propertyName: "videoPlayer", first: true, predicate: ["videoPlayer"], descendants: true }], ngImport: i0, template: "@if (isBrowserSupported) {\r\n <div class=\"screen-recorder-controls\" [ngClass]=\"customTheme + ' position-' + (rucInputData.position || ScreenRecorderPosition.Bottom)\"\r\n >\r\n <!-- Error Message -->\r\n @if (errorMessage) {\r\n <div class=\"recorder-error\">\r\n {{ errorMessage }}\r\n </div>\r\n }\r\n <!-- Record/Stop Button -->\r\n @if (recordingState === RecordingState.Idle || recordingState === RecordingState.Stopped) {\r\n <button\r\n (click)=\"toggleRecord()\"\r\n [attr.aria-label]=\"getLabel('recordButton')\" [attr.aria-pressed]=\"false\" class=\"recorder-button record-button\">\r\n @if (rucInputData.displayFormat === DisplayFormat.Icon) {\r\n <span class=\"material-icons\">{{ getRecordingIcon() || getIcon('record') }}</span>\r\n }\r\n @if (rucInputData.displayFormat === DisplayFormat.Text) {\r\n {{ getLabel('recordButton')\r\n }}\r\n }\r\n </button>\r\n }\r\n @if (recordingState === RecordingState.Recording || recordingState === RecordingState.Paused) {\r\n <button\r\n (click)=\"toggleRecord()\" [attr.aria-label]=\"getLabel('stopButton')\" [attr.aria-pressed]=\"true\"\r\n class=\"recorder-button stop-button\">\r\n @if (rucInputData.displayFormat === DisplayFormat.Icon) {\r\n <span class=\"material-icons\">{{ getIcon('stop') }}</span>\r\n }\r\n @if (rucInputData.displayFormat === DisplayFormat.Text) {\r\n {{ getLabel('stopButton') }}\r\n }\r\n </button>\r\n }\r\n <!-- Pause/Resume Button -->\r\n @if (rucInputData.showPauseResume && recordingState === RecordingState.Recording) {\r\n <button\r\n (click)=\"togglePauseResume()\" [attr.aria-label]=\"getLabel('pauseButton')\" [attr.aria-pressed]=\"false\"\r\n class=\"recorder-button pause-button\">\r\n @if (rucInputData.displayFormat === DisplayFormat.Icon) {\r\n <span class=\"material-icons\">{{ getIcon('pause') }}</span>\r\n }\r\n @if (rucInputData.displayFormat === DisplayFormat.Text) {\r\n {{ getLabel('pauseButton') }}\r\n }\r\n </button>\r\n }\r\n @if (rucInputData.showPauseResume && recordingState === RecordingState.Paused) {\r\n <button\r\n (click)=\"togglePauseResume()\" [attr.aria-label]=\"getLabel('resumeButton')\" [attr.aria-pressed]=\"true\"\r\n class=\"recorder-button resume-button\">\r\n @if (rucInputData.displayFormat === DisplayFormat.Icon) {\r\n <span class=\"material-icons\">{{ getIcon('resume') }}</span>\r\n }\r\n @if (rucInputData.displayFormat === DisplayFormat.Text) {\r\n {{ getLabel('resumeButton')\r\n }}\r\n }\r\n </button>\r\n }\r\n <!-- Audio Toggle Button -->\r\n @if (rucInputData.showAudioToggle && (recordingState === RecordingState.Idle || recordingState === RecordingState.Stopped)) {\r\n <button\r\n (click)=\"toggleAudio()\" [attr.aria-label]=\"getLabel('audioToggleButton')\" [attr.aria-pressed]=\"isAudioEnabled\"\r\n class=\"recorder-button audio-toggle-button\">\r\n @if (rucInputData.displayFormat === DisplayFormat.Icon) {\r\n <span class=\"material-icons\">{{ isAudioEnabled ? getIcon('audioOn') : getIcon('audioOff') }}</span>\r\n }\r\n @if (rucInputData.displayFormat === DisplayFormat.Text) {\r\n {{ isAudioEnabled ? screenRecordingConstant.AUDIO_ON : screenRecordingConstant.AUDIO_OFF }}\r\n }\r\n </button>\r\n }\r\n <!-- Timer -->\r\n @if (rucInputData.showTimer && (recordingState === RecordingState.Recording || recordingState === RecordingState.Paused)) {\r\n <div\r\n class=\"recorder-timer\" aria-live=\"polite\" aria-atomic=\"true\">\r\n {{ formattedTime }}\r\n </div>\r\n }\r\n <!-- Download Button -->\r\n @if (rucInputData.showDownloadButton && recordingState === RecordingState.Stopped && safeRecordedUrl) {\r\n <button\r\n (click)=\"onDownload()\" [attr.aria-label]=\"getLabel('downloadButton')\" class=\"recorder-button download-button\">\r\n @if (rucInputData.displayFormat === DisplayFormat.Icon) {\r\n <span class=\"material-icons\">{{ getIcon('download') }}</span>\r\n }\r\n @if (rucInputData.displayFormat === DisplayFormat.Text) {\r\n {{ getLabel('downloadButton')\r\n }}\r\n }\r\n </button>\r\n }\r\n <!-- Playback Area -->\r\n @if (rucInputData.showPlaybackControls && recordingState === RecordingState.Stopped && safeRecordedUrl) {\r\n <div\r\n class=\"playback-area\">\r\n <video #videoPlayer [src]=\"safeRecordedUrl\" controls [attr.aria-label]=\"getLabel('playbackVideo')\"></video>\r\n <div class=\"video-timer-overlay\">\r\n <span>Recorded on: {{ recordingTimestamp }}</span>\r\n </div>\r\n </div>\r\n }\r\n <!-- Reset Button (optional, good for testing/clearing state) -->\r\n @if (recordingState === RecordingState.Stopped && safeRecordedUrl) {\r\n <button (click)=\"reset()\"\r\n aria-label=\"New Recording\" class=\"recorder-button reset-button\">\r\n @if (rucInputData.displayFormat === DisplayFormat.Icon) {\r\n <span class=\"material-icons\">refresh</span>\r\n }\r\n @if (rucInputData.displayFormat === DisplayFormat.Text) {\r\n New\r\n }\r\n </button>\r\n }\r\n </div>\r\n}\r\n\r\n@if (!isBrowserSupported) {\r\n <div class=\"recorder-notsupported\" [ngClass]=\"customTheme\">\r\n {{screenRecordingConstant.NO_SUPPORT}}\r\n </div>\r\n}\r\n", styles: [":host{display:block;font-family:sans-serif}.screen-recorder-controls{display:flex;gap:8px;align-items:center;padding:8px;border-radius:4px;position:relative;z-index:1000}.screen-recorder-controls.position-top-left{margin-top:10px;margin-left:10px;flex-direction:row}.screen-recorder-controls.position-top-right{margin-top:10px;margin-right:10px;flex-direction:row}.screen-recorder-controls.position-bottom-left{margin-bottom:10px;margin-left:10px;flex-direction:row}.screen-recorder-controls.position-bottom-right{margin-bottom:10px;margin-right:10px;flex-direction:row}.screen-recorder-controls.position-left,.screen-recorder-controls.position-right{float:right}.recorder-button{padding:8px 12px;border:1px solid #ccc;border-radius:4px;background-color:#fff;cursor:pointer;display:inline-flex;align-items:center;gap:4px}.recorder-button:hover{background-color:#e9e9e9}.recorder-button:active{background-color:#d0d0d0}.recorder-button.record-button{color:red}.material-icons{font-family:Material Icons;font-weight:400;font-style:normal;font-size:20px;display:inline-block;line-height:1;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:\"liga\"}.recorder-timer{padding:0 8px;font-variant-numeric:tabular-nums}.playback-area{width:100%;max-width:100%;margin-top:1rem;position:relative}.playback-area video{width:100%;height:auto;max-width:100%}.playback-area .video-timer-overlay{position:absolute;top:10px;right:10px;background:#000000b3;color:#fff;padding:5px 10px;border-radius:4px;font-size:14px;z-index:10}.recorder-error{color:red;padding:8px;border:1px solid red;background-color:#ffe0e0;border-radius:4px;margin-bottom:8px}.recorder-notsupported{padding:10px;background-color:#fff3cd;border:1px solid #ffeeba;color:#856404;border-radius:4px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i4.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] }); }
618
+ }
619
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: RuclibScreenRecorderComponent, decorators: [{
620
+ type: Component,
621
+ args: [{ selector: 'uxp-ruclib-screen-recorder', imports: [CommonModule], template: "@if (isBrowserSupported) {\r\n <div class=\"screen-recorder-controls\" [ngClass]=\"customTheme + ' position-' + (rucInputData.position || ScreenRecorderPosition.Bottom)\"\r\n >\r\n <!-- Error Message -->\r\n @if (errorMessage) {\r\n <div class=\"recorder-error\">\r\n {{ errorMessage }}\r\n </div>\r\n }\r\n <!-- Record/Stop Button -->\r\n @if (recordingState === RecordingState.Idle || recordingState === RecordingState.Stopped) {\r\n <button\r\n (click)=\"toggleRecord()\"\r\n [attr.aria-label]=\"getLabel('recordButton')\" [attr.aria-pressed]=\"false\" class=\"recorder-button record-button\">\r\n @if (rucInputData.displayFormat === DisplayFormat.Icon) {\r\n <span class=\"material-icons\">{{ getRecordingIcon() || getIcon('record') }}</span>\r\n }\r\n @if (rucInputData.displayFormat === DisplayFormat.Text) {\r\n {{ getLabel('recordButton')\r\n }}\r\n }\r\n </button>\r\n }\r\n @if (recordingState === RecordingState.Recording || recordingState === RecordingState.Paused) {\r\n <button\r\n (click)=\"toggleRecord()\" [attr.aria-label]=\"getLabel('stopButton')\" [attr.aria-pressed]=\"true\"\r\n class=\"recorder-button stop-button\">\r\n @if (rucInputData.displayFormat === DisplayFormat.Icon) {\r\n <span class=\"material-icons\">{{ getIcon('stop') }}</span>\r\n }\r\n @if (rucInputData.displayFormat === DisplayFormat.Text) {\r\n {{ getLabel('stopButton') }}\r\n }\r\n </button>\r\n }\r\n <!-- Pause/Resume Button -->\r\n @if (rucInputData.showPauseResume && recordingState === RecordingState.Recording) {\r\n <button\r\n (click)=\"togglePauseResume()\" [attr.aria-label]=\"getLabel('pauseButton')\" [attr.aria-pressed]=\"false\"\r\n class=\"recorder-button pause-button\">\r\n @if (rucInputData.displayFormat === DisplayFormat.Icon) {\r\n <span class=\"material-icons\">{{ getIcon('pause') }}</span>\r\n }\r\n @if (rucInputData.displayFormat === DisplayFormat.Text) {\r\n {{ getLabel('pauseButton') }}\r\n }\r\n </button>\r\n }\r\n @if (rucInputData.showPauseResume && recordingState === RecordingState.Paused) {\r\n <button\r\n (click)=\"togglePauseResume()\" [attr.aria-label]=\"getLabel('resumeButton')\" [attr.aria-pressed]=\"true\"\r\n class=\"recorder-button resume-button\">\r\n @if (rucInputData.displayFormat === DisplayFormat.Icon) {\r\n <span class=\"material-icons\">{{ getIcon('resume') }}</span>\r\n }\r\n @if (rucInputData.displayFormat === DisplayFormat.Text) {\r\n {{ getLabel('resumeButton')\r\n }}\r\n }\r\n </button>\r\n }\r\n <!-- Audio Toggle Button -->\r\n @if (rucInputData.showAudioToggle && (recordingState === RecordingState.Idle || recordingState === RecordingState.Stopped)) {\r\n <button\r\n (click)=\"toggleAudio()\" [attr.aria-label]=\"getLabel('audioToggleButton')\" [attr.aria-pressed]=\"isAudioEnabled\"\r\n class=\"recorder-button audio-toggle-button\">\r\n @if (rucInputData.displayFormat === DisplayFormat.Icon) {\r\n <span class=\"material-icons\">{{ isAudioEnabled ? getIcon('audioOn') : getIcon('audioOff') }}</span>\r\n }\r\n @if (rucInputData.displayFormat === DisplayFormat.Text) {\r\n {{ isAudioEnabled ? screenRecordingConstant.AUDIO_ON : screenRecordingConstant.AUDIO_OFF }}\r\n }\r\n </button>\r\n }\r\n <!-- Timer -->\r\n @if (rucInputData.showTimer && (recordingState === RecordingState.Recording || recordingState === RecordingState.Paused)) {\r\n <div\r\n class=\"recorder-timer\" aria-live=\"polite\" aria-atomic=\"true\">\r\n {{ formattedTime }}\r\n </div>\r\n }\r\n <!-- Download Button -->\r\n @if (rucInputData.showDownloadButton && recordingState === RecordingState.Stopped && safeRecordedUrl) {\r\n <button\r\n (click)=\"onDownload()\" [attr.aria-label]=\"getLabel('downloadButton')\" class=\"recorder-button download-button\">\r\n @if (rucInputData.displayFormat === DisplayFormat.Icon) {\r\n <span class=\"material-icons\">{{ getIcon('download') }}</span>\r\n }\r\n @if (rucInputData.displayFormat === DisplayFormat.Text) {\r\n {{ getLabel('downloadButton')\r\n }}\r\n }\r\n </button>\r\n }\r\n <!-- Playback Area -->\r\n @if (rucInputData.showPlaybackControls && recordingState === RecordingState.Stopped && safeRecordedUrl) {\r\n <div\r\n class=\"playback-area\">\r\n <video #videoPlayer [src]=\"safeRecordedUrl\" controls [attr.aria-label]=\"getLabel('playbackVideo')\"></video>\r\n <div class=\"video-timer-overlay\">\r\n <span>Recorded on: {{ recordingTimestamp }}</span>\r\n </div>\r\n </div>\r\n }\r\n <!-- Reset Button (optional, good for testing/clearing state) -->\r\n @if (recordingState === RecordingState.Stopped && safeRecordedUrl) {\r\n <button (click)=\"reset()\"\r\n aria-label=\"New Recording\" class=\"recorder-button reset-button\">\r\n @if (rucInputData.displayFormat === DisplayFormat.Icon) {\r\n <span class=\"material-icons\">refresh</span>\r\n }\r\n @if (rucInputData.displayFormat === DisplayFormat.Text) {\r\n New\r\n }\r\n </button>\r\n }\r\n </div>\r\n}\r\n\r\n@if (!isBrowserSupported) {\r\n <div class=\"recorder-notsupported\" [ngClass]=\"customTheme\">\r\n {{screenRecordingConstant.NO_SUPPORT}}\r\n </div>\r\n}\r\n", styles: [":host{display:block;font-family:sans-serif}.screen-recorder-controls{display:flex;gap:8px;align-items:center;padding:8px;border-radius:4px;position:relative;z-index:1000}.screen-recorder-controls.position-top-left{margin-top:10px;margin-left:10px;flex-direction:row}.screen-recorder-controls.position-top-right{margin-top:10px;margin-right:10px;flex-direction:row}.screen-recorder-controls.position-bottom-left{margin-bottom:10px;margin-left:10px;flex-direction:row}.screen-recorder-controls.position-bottom-right{margin-bottom:10px;margin-right:10px;flex-direction:row}.screen-recorder-controls.position-left,.screen-recorder-controls.position-right{float:right}.recorder-button{padding:8px 12px;border:1px solid #ccc;border-radius:4px;background-color:#fff;cursor:pointer;display:inline-flex;align-items:center;gap:4px}.recorder-button:hover{background-color:#e9e9e9}.recorder-button:active{background-color:#d0d0d0}.recorder-button.record-button{color:red}.material-icons{font-family:Material Icons;font-weight:400;font-style:normal;font-size:20px;display:inline-block;line-height:1;text-transform:none;letter-spacing:normal;word-wrap:normal;white-space:nowrap;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:\"liga\"}.recorder-timer{padding:0 8px;font-variant-numeric:tabular-nums}.playback-area{width:100%;max-width:100%;margin-top:1rem;position:relative}.playback-area video{width:100%;height:auto;max-width:100%}.playback-area .video-timer-overlay{position:absolute;top:10px;right:10px;background:#000000b3;color:#fff;padding:5px 10px;border-radius:4px;font-size:14px;z-index:10}.recorder-error{color:red;padding:8px;border:1px solid red;background-color:#ffe0e0;border-radius:4px;margin-bottom:8px}.recorder-notsupported{padding:10px;background-color:#fff3cd;border:1px solid #ffeeba;color:#856404;border-radius:4px}\n"] }]
622
+ }], ctorParameters: () => [{ type: ScreenRecorderService }, { type: i2.DomSanitizer }, { type: i0.ChangeDetectorRef }, { type: ScreenRecorderConstants }], propDecorators: { customTheme: [{
623
+ type: Input
624
+ }], rucInputData: [{
625
+ type: Input
626
+ }], videoPlayer: [{
627
+ type: ViewChild,
628
+ args: ['videoPlayer']
626
629
  }] } });
627
630
 
628
- class RuclibScreenRecorderModule {
629
- }
630
- RuclibScreenRecorderModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: RuclibScreenRecorderModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
631
- RuclibScreenRecorderModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: RuclibScreenRecorderModule, declarations: [ScreenRecorderComponent], imports: [CommonModule], exports: [ScreenRecorderComponent] });
632
- RuclibScreenRecorderModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: RuclibScreenRecorderModule, providers: [ScreenRecorderService], imports: [CommonModule] });
633
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: RuclibScreenRecorderModule, decorators: [{
634
- type: NgModule,
635
- args: [{
636
- imports: [CommonModule],
637
- declarations: [ScreenRecorderComponent],
638
- exports: [ScreenRecorderComponent],
639
- providers: [ScreenRecorderService],
640
- }]
641
- }] });
642
-
643
- /**
644
- * Generated bundle index. Do not edit.
631
+ /**
632
+ * Generated bundle index. Do not edit.
645
633
  */
646
634
 
647
- export { DisplayFormat, RecordingState, RuclibScreenRecorderModule, ScreenRecorderComponent, ScreenRecorderPosition, ScreenRecorderService };
635
+ export { DisplayFormat, RecordingState, RuclibScreenRecorderComponent, ScreenRecorderPosition, ScreenRecorderService };
648
636
  //# sourceMappingURL=ruc-lib-screen-recorder.mjs.map