@design.estate/dees-wcctools 3.8.2 → 3.8.5
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 +31380 -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/elements/wcc-sidebar.js +7 -1
- 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/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/wcc-recording-panel.ts +38 -6
- package/ts_web/elements/wcc-sidebar.ts +4 -0
- package/ts_web/index.ts +1 -1
- 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.8.
|
|
3
|
+
"version": "3.8.5",
|
|
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
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@design.estate/dees-wcctools',
|
|
6
|
-
version: '3.8.
|
|
6
|
+
version: '3.8.5',
|
|
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');
|
|
@@ -654,6 +654,8 @@ export class WccSidebar extends DeesElement {
|
|
|
654
654
|
const entries = getSectionItems(section);
|
|
655
655
|
const filteredEntries = entries.filter(([name, item]) => {
|
|
656
656
|
if (this.matchesSearch(name)) return true;
|
|
657
|
+
const tagName = (item as any).is;
|
|
658
|
+
if (tagName && this.matchesSearch(tagName)) return true;
|
|
657
659
|
const rawGroups = (item as any).demoGroups;
|
|
658
660
|
if (!rawGroups) return false;
|
|
659
661
|
const groups: string[] = Array.isArray(rawGroups) ? rawGroups : [rawGroups];
|
|
@@ -692,6 +694,8 @@ export class WccSidebar extends DeesElement {
|
|
|
692
694
|
// Filter entries by search query
|
|
693
695
|
const filteredEntries = entries.filter(([name, item]) => {
|
|
694
696
|
if (this.matchesSearch(name)) return true;
|
|
697
|
+
const tagName = (item as any).is;
|
|
698
|
+
if (tagName && this.matchesSearch(tagName)) return true;
|
|
695
699
|
const rawGroups = (item as any).demoGroups;
|
|
696
700
|
if (!rawGroups) return false;
|
|
697
701
|
const groups: string[] = Array.isArray(rawGroups) ? rawGroups : [rawGroups];
|
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
|
|
|
@@ -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
|
+
}
|