@design.estate/dees-wcctools 3.8.4 → 3.9.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/dist_bundle/bundle.js +31376 -1126
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/wcc-recording-panel.d.ts +2 -0
- package/dist_ts_web/elements/wcc-recording-panel.js +43 -7
- package/dist_ts_web/index.d.ts +1 -1
- package/dist_ts_web/index.js +1 -1
- package/dist_ts_web/services/recorder.service.d.ts +12 -1
- package/dist_ts_web/services/recorder.service.js +60 -14
- package/dist_watch/bundle.js +39810 -4318
- package/dist_watch/bundle.js.map +4 -4
- package/package.json +10 -3
- package/readme.hints.md +8 -0
- package/readme.md +14 -8
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/wcc-recording-panel.ts +38 -6
- package/ts_web/index.ts +1 -1
- package/ts_web/readme.md +16 -9
- package/ts_web/services/recorder.service.ts +73 -15
- package/ts_web/types/dom-mediacapture-stub/index.d.ts +12 -0
- package/ts_web/types/dom-mediacapture-stub/package.json +6 -0
- package/ts_web/types/dom-webcodecs-stub/index.d.ts +2 -0
- package/ts_web/types/dom-webcodecs-stub/package.json +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@design.estate/dees-wcctools",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.9.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
|
|
6
6
|
"exports": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"@design.estate/dees-domtools": "^2.5.4",
|
|
21
21
|
"@design.estate/dees-element": "^2.2.4",
|
|
22
22
|
"@push.rocks/smartdelay": "^3.0.5",
|
|
23
|
-
"lit": "^3.3.2"
|
|
23
|
+
"lit": "^3.3.2",
|
|
24
|
+
"mediabunny": "^1.40.1"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@api.global/typedserver": "^8.4.6",
|
|
@@ -59,5 +60,11 @@
|
|
|
59
60
|
"element testing",
|
|
60
61
|
"page development"
|
|
61
62
|
],
|
|
62
|
-
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
|
|
63
|
+
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977",
|
|
64
|
+
"pnpm": {
|
|
65
|
+
"overrides": {
|
|
66
|
+
"@types/dom-webcodecs": "./ts_web/types/dom-webcodecs-stub",
|
|
67
|
+
"@types/dom-mediacapture-transform": "./ts_web/types/dom-mediacapture-stub"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
63
70
|
}
|
package/readme.hints.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Project Hints and Findings
|
|
2
2
|
|
|
3
|
+
## Mediabunny / @types/dom-webcodecs Override (2026-04-12)
|
|
4
|
+
|
|
5
|
+
The `mediabunny` package depends on `@types/dom-webcodecs` and `@types/dom-mediacapture-transform`, which conflict with TypeScript 6's built-in WebCodecs types in `lib.dom.d.ts`. We override both via `pnpm.overrides` in `package.json`, pointing them to local stubs in `ts_web/types/`:
|
|
6
|
+
- `dom-webcodecs-stub/` — empty, since TS6 provides these types natively
|
|
7
|
+
- `dom-mediacapture-stub/` — provides `MediaStreamVideoTrack` and `MediaStreamAudioTrack` interfaces (not yet in `lib.dom.d.ts`)
|
|
8
|
+
|
|
9
|
+
If mediabunny drops these `@types` dependencies in a future version, the overrides can be removed.
|
|
10
|
+
|
|
3
11
|
## TypeScript 6.0 & Build Tooling (2026-04-12)
|
|
4
12
|
|
|
5
13
|
### TypeScript 6.0 Strict Defaults
|
package/readme.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
- 🔧 **Real-time Property Editing** — Modify component props on the fly with auto-detected editors
|
|
11
11
|
- 🌓 **Theme Switching** — Test light/dark modes instantly
|
|
12
12
|
- 📱 **Responsive Viewport Testing** — Phone, phablet, tablet, and desktop views
|
|
13
|
-
- 🎬 **Screen Recording** — Record component demos with audio
|
|
13
|
+
- 🎬 **Screen Recording** — Record component demos with audio, trimming, and MP4/WebM export
|
|
14
14
|
- 🧪 **Advanced Demo Tools** — Post-render hooks for interactive testing
|
|
15
15
|
- 📂 **Section-based Organization** — Group components into custom sections with filtering and sorting
|
|
16
16
|
- 🚀 **Zero-config Setup** — TypeScript and Lit support out of the box
|
|
@@ -235,15 +235,17 @@ public static styles = [
|
|
|
235
235
|
|
|
236
236
|
### 🎬 Screen Recording
|
|
237
237
|
|
|
238
|
-
Record component demos directly from the catalogue:
|
|
238
|
+
Record component demos directly from the catalogue with full export control:
|
|
239
239
|
|
|
240
240
|
- **Viewport Recording** — Record just the component viewport
|
|
241
241
|
- **Full Screen Recording** — Capture the entire screen
|
|
242
242
|
- **Audio Support** — Add microphone commentary with live level monitoring
|
|
243
|
-
- **Video Trimming** — Trim start/end before export with visual timeline
|
|
244
|
-
- **
|
|
243
|
+
- **Video Trimming** — Trim start/end before export with a visual timeline
|
|
244
|
+
- **60fps Capture** — Smooth, high-bitrate recording at up to 60 frames per second
|
|
245
|
+
- **MP4 Export** — Universal H.264/AAC format via [mediabunny](https://mediabunny.dev) WebCodecs conversion (plays everywhere: WhatsApp, iMessage, Slack, etc.)
|
|
246
|
+
- **WebM Export** — Native VP9 output for maximum quality
|
|
245
247
|
|
|
246
|
-
Click the red record button in the bottom toolbar
|
|
248
|
+
Click the red record button in the bottom toolbar, choose your format (MP4 or WebM), and start recording.
|
|
247
249
|
|
|
248
250
|
### 🧪 Demo Tools
|
|
249
251
|
|
|
@@ -438,7 +440,7 @@ The wrapper provides full DOM API access:
|
|
|
438
440
|
For custom recording integrations:
|
|
439
441
|
|
|
440
442
|
```typescript
|
|
441
|
-
import { RecorderService } from '@design.estate/dees-wcctools';
|
|
443
|
+
import { RecorderService, type TOutputFormat } from '@design.estate/dees-wcctools';
|
|
442
444
|
|
|
443
445
|
const recorder = new RecorderService({
|
|
444
446
|
onDurationUpdate: (duration) => console.log(`${duration}s`),
|
|
@@ -446,9 +448,13 @@ const recorder = new RecorderService({
|
|
|
446
448
|
onAudioLevelUpdate: (level) => console.log(`Audio: ${level}%`),
|
|
447
449
|
});
|
|
448
450
|
|
|
451
|
+
// Record (always captures as WebM internally)
|
|
449
452
|
await recorder.startRecording({ mode: 'viewport' });
|
|
450
453
|
// ... later
|
|
451
454
|
recorder.stopRecording();
|
|
455
|
+
|
|
456
|
+
// Convert to MP4 for universal playback (H.264 + AAC via WebCodecs)
|
|
457
|
+
const mp4Blob = await recorder.convertToMp4(recorder.recordedBlob);
|
|
452
458
|
```
|
|
453
459
|
|
|
454
460
|
## Project Structure
|
|
@@ -481,7 +487,7 @@ my-component-library/
|
|
|
481
487
|
|
|
482
488
|
## License and Legal Information
|
|
483
489
|
|
|
484
|
-
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [
|
|
490
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
|
485
491
|
|
|
486
492
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
487
493
|
|
|
@@ -493,7 +499,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
|
|
493
499
|
|
|
494
500
|
### Company Information
|
|
495
501
|
|
|
496
|
-
Task Venture Capital GmbH
|
|
502
|
+
Task Venture Capital GmbH
|
|
497
503
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
498
504
|
|
|
499
505
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@design.estate/dees-wcctools',
|
|
6
|
-
version: '3.
|
|
6
|
+
version: '3.9.0',
|
|
7
7
|
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
|
|
8
8
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DeesElement, customElement, html, css, property, state, type TemplateResult } from '@design.estate/dees-element';
|
|
2
|
-
import { RecorderService } from '../services/recorder.service.js';
|
|
2
|
+
import { RecorderService, type TOutputFormat } from '../services/recorder.service.js';
|
|
3
3
|
import type { WccDashboard } from './wcc-dashboard.js';
|
|
4
4
|
|
|
5
5
|
@customElement('wcc-recording-panel')
|
|
@@ -16,6 +16,9 @@ export class WccRecordingPanel extends DeesElement {
|
|
|
16
16
|
@state()
|
|
17
17
|
accessor recordingMode: 'viewport' | 'screen' = 'viewport';
|
|
18
18
|
|
|
19
|
+
@state()
|
|
20
|
+
accessor outputFormat: TOutputFormat = 'mp4';
|
|
21
|
+
|
|
19
22
|
@state()
|
|
20
23
|
accessor audioEnabled: boolean = false;
|
|
21
24
|
|
|
@@ -380,6 +383,9 @@ export class WccRecordingPanel extends DeesElement {
|
|
|
380
383
|
font-weight: 500;
|
|
381
384
|
cursor: pointer;
|
|
382
385
|
transition: all 0.15s ease;
|
|
386
|
+
display: inline-flex;
|
|
387
|
+
align-items: center;
|
|
388
|
+
gap: 0.5rem;
|
|
383
389
|
}
|
|
384
390
|
|
|
385
391
|
.preview-btn.secondary {
|
|
@@ -546,7 +552,7 @@ export class WccRecordingPanel extends DeesElement {
|
|
|
546
552
|
border-radius: 50%;
|
|
547
553
|
border-top-color: white;
|
|
548
554
|
animation: spin 0.8s linear infinite;
|
|
549
|
-
|
|
555
|
+
flex-shrink: 0;
|
|
550
556
|
}
|
|
551
557
|
|
|
552
558
|
@keyframes spin {
|
|
@@ -591,6 +597,24 @@ export class WccRecordingPanel extends DeesElement {
|
|
|
591
597
|
</div>
|
|
592
598
|
</div>
|
|
593
599
|
|
|
600
|
+
<div class="recording-option-group">
|
|
601
|
+
<div class="recording-option-label">Format</div>
|
|
602
|
+
<div class="recording-mode-buttons">
|
|
603
|
+
<button
|
|
604
|
+
class="recording-mode-btn ${this.outputFormat === 'mp4' ? 'selected' : ''}"
|
|
605
|
+
@click=${() => this.outputFormat = 'mp4'}
|
|
606
|
+
>
|
|
607
|
+
MP4 (H.264)
|
|
608
|
+
</button>
|
|
609
|
+
<button
|
|
610
|
+
class="recording-mode-btn ${this.outputFormat === 'webm' ? 'selected' : ''}"
|
|
611
|
+
@click=${() => this.outputFormat = 'webm'}
|
|
612
|
+
>
|
|
613
|
+
WebM (VP9)
|
|
614
|
+
</button>
|
|
615
|
+
</div>
|
|
616
|
+
</div>
|
|
617
|
+
|
|
594
618
|
<div class="recording-option-group">
|
|
595
619
|
<div class="recording-option-label">Audio</div>
|
|
596
620
|
<div class="audio-toggle">
|
|
@@ -716,7 +740,9 @@ export class WccRecordingPanel extends DeesElement {
|
|
|
716
740
|
?disabled=${this.isExporting}
|
|
717
741
|
@click=${() => this.downloadRecording()}
|
|
718
742
|
>
|
|
719
|
-
${this.isExporting
|
|
743
|
+
${this.isExporting
|
|
744
|
+
? html`<span class="export-spinner"></span>${this.outputFormat === 'mp4' ? 'Converting to MP4...' : 'Exporting...'}`
|
|
745
|
+
: `Download ${this.outputFormat === 'mp4' ? 'MP4' : 'WebM'}`}
|
|
720
746
|
</button>
|
|
721
747
|
</div>
|
|
722
748
|
</div>
|
|
@@ -764,7 +790,7 @@ export class WccRecordingPanel extends DeesElement {
|
|
|
764
790
|
await this.recorderService.startRecording({
|
|
765
791
|
mode: this.recordingMode,
|
|
766
792
|
audioDeviceId: this.audioEnabled ? this.selectedMicrophoneId : undefined,
|
|
767
|
-
viewportElement
|
|
793
|
+
viewportElement,
|
|
768
794
|
});
|
|
769
795
|
|
|
770
796
|
this.panelState = 'recording';
|
|
@@ -817,7 +843,7 @@ export class WccRecordingPanel extends DeesElement {
|
|
|
817
843
|
try {
|
|
818
844
|
let blobToDownload: Blob;
|
|
819
845
|
|
|
820
|
-
// Handle trimming if needed
|
|
846
|
+
// Handle trimming if needed — always produces WebM
|
|
821
847
|
const needsTrim = this.trimStart > 0.1 || this.trimEnd < this.videoDuration - 0.1;
|
|
822
848
|
|
|
823
849
|
if (needsTrim) {
|
|
@@ -831,9 +857,15 @@ export class WccRecordingPanel extends DeesElement {
|
|
|
831
857
|
blobToDownload = recordedBlob;
|
|
832
858
|
}
|
|
833
859
|
|
|
860
|
+
// Convert WebM → MP4 if MP4 format selected
|
|
861
|
+
if (this.outputFormat === 'mp4') {
|
|
862
|
+
blobToDownload = await this.recorderService.convertToMp4(blobToDownload);
|
|
863
|
+
}
|
|
864
|
+
|
|
834
865
|
// Trigger download
|
|
835
866
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
836
|
-
const
|
|
867
|
+
const ext = this.outputFormat === 'mp4' ? 'mp4' : 'webm';
|
|
868
|
+
const filename = `wcctools-recording-${timestamp}.${ext}`;
|
|
837
869
|
|
|
838
870
|
const url = URL.createObjectURL(blobToDownload);
|
|
839
871
|
const a = document.createElement('a');
|
package/ts_web/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { TTemplateFactory } from './elements/wcctools.helpers.js';
|
|
|
4
4
|
import type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
|
|
5
5
|
|
|
6
6
|
// Export recording components and service
|
|
7
|
-
export { RecorderService, type IRecorderEvents, type IRecordingOptions } from './services/recorder.service.js';
|
|
7
|
+
export { RecorderService, type IRecorderEvents, type IRecordingOptions, type TOutputFormat } from './services/recorder.service.js';
|
|
8
8
|
export { WccRecordButton } from './elements/wcc-record-button.js';
|
|
9
9
|
export { WccRecordingPanel } from './elements/wcc-recording-panel.js';
|
|
10
10
|
|
package/ts_web/readme.md
CHANGED
|
@@ -71,11 +71,12 @@ setupWccTools({
|
|
|
71
71
|
|
|
72
72
|
| Export | Description |
|
|
73
73
|
|--------|-------------|
|
|
74
|
-
| `RecorderService` | Service class for screen/viewport recording |
|
|
74
|
+
| `RecorderService` | Service class for screen/viewport recording and MP4 conversion |
|
|
75
75
|
| `WccRecordButton` | Record button UI component |
|
|
76
76
|
| `WccRecordingPanel` | Recording options and preview panel |
|
|
77
77
|
| `IRecorderEvents` | TypeScript interface for recorder callbacks |
|
|
78
78
|
| `IRecordingOptions` | TypeScript interface for recording options |
|
|
79
|
+
| `TOutputFormat` | Type for output format selection (`'webm' \| 'mp4'`) |
|
|
79
80
|
|
|
80
81
|
## Section Configuration
|
|
81
82
|
|
|
@@ -96,15 +97,15 @@ The module includes these internal web components:
|
|
|
96
97
|
| Component | Description |
|
|
97
98
|
|-----------|-------------|
|
|
98
99
|
| `wcc-dashboard` | Main dashboard container with routing |
|
|
99
|
-
| `wcc-sidebar` | Navigation sidebar with collapsible sections |
|
|
100
|
+
| `wcc-sidebar` | Navigation sidebar with collapsible sections and search |
|
|
100
101
|
| `wcc-frame` | Responsive viewport with size controls |
|
|
101
102
|
| `wcc-properties` | Property panel with live editing |
|
|
102
103
|
| `wcc-record-button` | Recording state indicator button |
|
|
103
|
-
| `wcc-recording-panel` | Recording workflow UI |
|
|
104
|
+
| `wcc-recording-panel` | Recording workflow UI with format selection |
|
|
104
105
|
|
|
105
106
|
## RecorderService API
|
|
106
107
|
|
|
107
|
-
For programmatic recording
|
|
108
|
+
For programmatic recording and MP4 conversion:
|
|
108
109
|
|
|
109
110
|
```typescript
|
|
110
111
|
import { RecorderService, type IRecorderEvents } from '@design.estate/dees-wcctools';
|
|
@@ -125,7 +126,7 @@ const mics = await recorder.loadMicrophones(true);
|
|
|
125
126
|
// Start audio level monitoring
|
|
126
127
|
await recorder.startAudioMonitoring(mics[0].deviceId);
|
|
127
128
|
|
|
128
|
-
// Start recording
|
|
129
|
+
// Start recording (always captures as WebM internally at up to 60fps)
|
|
129
130
|
await recorder.startRecording({
|
|
130
131
|
mode: 'viewport',
|
|
131
132
|
audioDeviceId: mics[0].deviceId,
|
|
@@ -135,7 +136,10 @@ await recorder.startRecording({
|
|
|
135
136
|
// Stop recording
|
|
136
137
|
recorder.stopRecording();
|
|
137
138
|
|
|
138
|
-
//
|
|
139
|
+
// Convert to MP4 for universal playback (H.264 + AAC via WebCodecs)
|
|
140
|
+
const mp4Blob = await recorder.convertToMp4(recorder.recordedBlob);
|
|
141
|
+
|
|
142
|
+
// Or export trimmed video
|
|
139
143
|
const trimmedBlob = await recorder.exportTrimmedVideo(videoElement, startTime, endTime);
|
|
140
144
|
|
|
141
145
|
// Cleanup
|
|
@@ -148,6 +152,9 @@ recorder.dispose();
|
|
|
148
152
|
ts_web/
|
|
149
153
|
├── index.ts # Main exports
|
|
150
154
|
├── wcctools.interfaces.ts # Type definitions
|
|
155
|
+
├── types/
|
|
156
|
+
│ ├── dom-webcodecs-stub/ # TS6 compatibility shim
|
|
157
|
+
│ └── dom-mediacapture-stub/ # MediaCapture Transform types
|
|
151
158
|
├── elements/
|
|
152
159
|
│ ├── wcc-dashboard.ts # Root dashboard component
|
|
153
160
|
│ ├── wcc-sidebar.ts # Navigation sidebar
|
|
@@ -157,7 +164,7 @@ ts_web/
|
|
|
157
164
|
│ ├── wcc-recording-panel.ts # Recording options/preview
|
|
158
165
|
│ └── wcctools.helpers.ts # Shared utilities
|
|
159
166
|
├── services/
|
|
160
|
-
│ └── recorder.service.ts # MediaRecorder
|
|
167
|
+
│ └── recorder.service.ts # MediaRecorder + mediabunny MP4 conversion
|
|
161
168
|
└── pages/
|
|
162
169
|
└── index.ts # Built-in pages
|
|
163
170
|
```
|
|
@@ -165,9 +172,9 @@ ts_web/
|
|
|
165
172
|
## Features
|
|
166
173
|
|
|
167
174
|
- 🎨 Interactive component preview
|
|
168
|
-
- 📂 Section-based sidebar with filtering &
|
|
175
|
+
- 📂 Section-based sidebar with filtering, sorting & search (by name, tag, or group)
|
|
169
176
|
- 🔧 Real-time property editing with type detection
|
|
170
177
|
- 🌓 Theme switching (light/dark)
|
|
171
178
|
- 📱 Responsive viewport testing
|
|
172
|
-
- 🎬 Screen recording with trimming
|
|
179
|
+
- 🎬 Screen recording with MP4/WebM export, trimming, and audio
|
|
173
180
|
- 🔗 URL-based deep linking
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* RecorderService - Handles all MediaRecorder, audio monitoring, and video export logic
|
|
2
|
+
* RecorderService - Handles all MediaRecorder, audio monitoring, and video export logic.
|
|
3
|
+
* Recording always uses MediaRecorder → WebM (the reliable browser path).
|
|
4
|
+
* MP4 output is produced by converting WebM → MP4 via mediabunny at export time.
|
|
3
5
|
*/
|
|
4
6
|
|
|
7
|
+
export type TOutputFormat = 'webm' | 'mp4';
|
|
8
|
+
|
|
5
9
|
export interface IRecorderEvents {
|
|
6
10
|
onDurationUpdate?: (duration: number) => void;
|
|
7
11
|
onRecordingComplete?: (blob: Blob) => void;
|
|
@@ -14,6 +18,7 @@ export interface IRecordingOptions {
|
|
|
14
18
|
mode: 'viewport' | 'screen';
|
|
15
19
|
audioDeviceId?: string;
|
|
16
20
|
viewportElement?: HTMLElement;
|
|
21
|
+
outputFormat?: TOutputFormat;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
export class RecorderService {
|
|
@@ -24,6 +29,7 @@ export class RecorderService {
|
|
|
24
29
|
private _duration: number = 0;
|
|
25
30
|
private _recordedBlob: Blob | null = null;
|
|
26
31
|
private _isRecording: boolean = false;
|
|
32
|
+
private _outputFormat: TOutputFormat = 'webm';
|
|
27
33
|
|
|
28
34
|
// Audio monitoring state
|
|
29
35
|
private audioContext: AudioContext | null = null;
|
|
@@ -56,6 +62,10 @@ export class RecorderService {
|
|
|
56
62
|
return this._recordedBlob;
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
get outputFormat(): TOutputFormat {
|
|
66
|
+
return this._outputFormat;
|
|
67
|
+
}
|
|
68
|
+
|
|
59
69
|
// Update event callbacks
|
|
60
70
|
setEvents(events: IRecorderEvents): void {
|
|
61
71
|
this.events = { ...this.events, ...events };
|
|
@@ -132,13 +142,16 @@ export class RecorderService {
|
|
|
132
142
|
|
|
133
143
|
async startRecording(options: IRecordingOptions): Promise<void> {
|
|
134
144
|
try {
|
|
145
|
+
this._outputFormat = options.outputFormat || 'webm';
|
|
146
|
+
|
|
135
147
|
// Stop audio monitoring before recording
|
|
136
148
|
this.stopAudioMonitoring();
|
|
137
149
|
|
|
138
150
|
// Get video stream based on mode
|
|
139
151
|
const displayMediaOptions: DisplayMediaStreamOptions = {
|
|
140
152
|
video: {
|
|
141
|
-
displaySurface: options.mode === 'viewport' ? 'browser' : 'monitor'
|
|
153
|
+
displaySurface: options.mode === 'viewport' ? 'browser' : 'monitor',
|
|
154
|
+
frameRate: { ideal: 60 },
|
|
142
155
|
} as MediaTrackConstraints,
|
|
143
156
|
audio: false
|
|
144
157
|
};
|
|
@@ -182,12 +195,23 @@ export class RecorderService {
|
|
|
182
195
|
// Store stream for cleanup
|
|
183
196
|
this.currentStream = combinedStream;
|
|
184
197
|
|
|
185
|
-
//
|
|
198
|
+
// Handle stream ending (user clicks "Stop sharing")
|
|
199
|
+
videoStream.getVideoTracks()[0].onended = () => {
|
|
200
|
+
if (this._isRecording) {
|
|
201
|
+
this.stopRecording();
|
|
202
|
+
this.events.onStreamEnded?.();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Always record as WebM — conversion to MP4 happens at export time
|
|
186
207
|
const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
|
|
187
208
|
? 'video/webm;codecs=vp9'
|
|
188
209
|
: 'video/webm';
|
|
189
210
|
|
|
190
|
-
this.mediaRecorder = new MediaRecorder(combinedStream, {
|
|
211
|
+
this.mediaRecorder = new MediaRecorder(combinedStream, {
|
|
212
|
+
mimeType,
|
|
213
|
+
videoBitsPerSecond: 8_000_000, // 8 Mbps for smooth, high-quality capture
|
|
214
|
+
});
|
|
191
215
|
this.recordedChunks = [];
|
|
192
216
|
|
|
193
217
|
this.mediaRecorder.ondataavailable = (e) => {
|
|
@@ -198,14 +222,6 @@ export class RecorderService {
|
|
|
198
222
|
|
|
199
223
|
this.mediaRecorder.onstop = () => this.handleRecordingComplete();
|
|
200
224
|
|
|
201
|
-
// Handle stream ending (user clicks "Stop sharing")
|
|
202
|
-
videoStream.getVideoTracks()[0].onended = () => {
|
|
203
|
-
if (this._isRecording) {
|
|
204
|
-
this.stopRecording();
|
|
205
|
-
this.events.onStreamEnded?.();
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
|
|
209
225
|
this.mediaRecorder.start(1000); // Capture in 1-second chunks
|
|
210
226
|
|
|
211
227
|
// Start duration timer
|
|
@@ -236,9 +252,7 @@ export class RecorderService {
|
|
|
236
252
|
}
|
|
237
253
|
|
|
238
254
|
private async handleRecordingComplete(): Promise<void> {
|
|
239
|
-
// Create blob from recorded chunks
|
|
240
255
|
const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
|
|
241
|
-
|
|
242
256
|
this._recordedBlob = blob;
|
|
243
257
|
|
|
244
258
|
// Stop all tracks
|
|
@@ -251,7 +265,51 @@ export class RecorderService {
|
|
|
251
265
|
this.events.onRecordingComplete?.(this._recordedBlob);
|
|
252
266
|
}
|
|
253
267
|
|
|
254
|
-
// ====================
|
|
268
|
+
// ==================== Conversion & Export ====================
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Converts a WebM blob to MP4 using mediabunny's Conversion API.
|
|
272
|
+
* Uses WebCodecs for hardware-accelerated H.264 encoding.
|
|
273
|
+
*/
|
|
274
|
+
async convertToMp4(webmBlob: Blob): Promise<Blob> {
|
|
275
|
+
const {
|
|
276
|
+
Input, Output, Conversion, BlobSource, BufferTarget, Mp4OutputFormat, WEBM, QUALITY_HIGH,
|
|
277
|
+
} = await import('mediabunny');
|
|
278
|
+
|
|
279
|
+
const input = new Input({
|
|
280
|
+
source: new BlobSource(webmBlob),
|
|
281
|
+
formats: [WEBM],
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const target = new BufferTarget();
|
|
285
|
+
const output = new Output({
|
|
286
|
+
format: new Mp4OutputFormat({ fastStart: 'in-memory' }),
|
|
287
|
+
target,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const conversion = await Conversion.init({
|
|
291
|
+
input,
|
|
292
|
+
output,
|
|
293
|
+
// Force transcoding from VP9 → H.264 and Opus → AAC
|
|
294
|
+
video: {
|
|
295
|
+
codec: 'avc',
|
|
296
|
+
bitrate: QUALITY_HIGH,
|
|
297
|
+
fit: 'contain',
|
|
298
|
+
},
|
|
299
|
+
audio: {
|
|
300
|
+
codec: 'aac',
|
|
301
|
+
bitrate: QUALITY_HIGH,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
await conversion.execute();
|
|
305
|
+
|
|
306
|
+
const buffer = target.buffer;
|
|
307
|
+
if (!buffer || buffer.byteLength === 0) {
|
|
308
|
+
throw new Error('MP4 conversion produced empty output');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return new Blob([buffer], { type: 'video/mp4' });
|
|
312
|
+
}
|
|
255
313
|
|
|
256
314
|
async exportTrimmedVideo(
|
|
257
315
|
videoElement: HTMLVideoElement,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Minimal type stubs for MediaCapture Transform API types not yet in lib.dom.d.ts.
|
|
2
|
+
// These specialize MediaStreamTrack for audio/video so mediabunny's API is type-safe.
|
|
3
|
+
|
|
4
|
+
interface MediaStreamAudioTrack extends MediaStreamTrack {
|
|
5
|
+
readonly kind: 'audio';
|
|
6
|
+
clone(): MediaStreamAudioTrack;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface MediaStreamVideoTrack extends MediaStreamTrack {
|
|
10
|
+
readonly kind: 'video';
|
|
11
|
+
clone(): MediaStreamVideoTrack;
|
|
12
|
+
}
|