Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 639431824f | |||
| fec1dffd37 | |||
| 73a975e9e9 | |||
| d178d6cb73 | |||
| 3eeb9dc46f | |||
| d9330a5fa1 | |||
| 443618d1ac |
17
changelog.md
17
changelog.md
@@ -1,5 +1,22 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-12 - 3.9.0 - feat(docs)
|
||||||
|
document MP4 export support and enhanced recording capabilities
|
||||||
|
|
||||||
|
- Update recording documentation to cover MP4/WebM export options, 60fps capture, and conversion via RecorderService
|
||||||
|
- Add API and type references for output format selection and MP4 conversion in the TypeScript web docs
|
||||||
|
- Clarify related UI capabilities such as recording panel format selection and sidebar search support
|
||||||
|
|
||||||
|
## 2026-04-12 - 3.8.5 - fix(recording)
|
||||||
|
improve recording capture quality and align preview button loading state
|
||||||
|
|
||||||
|
- request 60fps screen capture and increase recorder bitrate to 8 Mbps for smoother, higher-quality videos
|
||||||
|
- update preview button layout to use inline flex alignment so spinner and label stay properly aligned
|
||||||
|
|
||||||
|
## 2026-04-12 - 3.8.4 - fix(repo)
|
||||||
|
no changes to commit
|
||||||
|
|
||||||
|
|
||||||
## 2026-04-12 - 3.8.3 - fix(sidebar)
|
## 2026-04-12 - 3.8.3 - fix(sidebar)
|
||||||
include component tag names in sidebar search filtering
|
include component tag names in sidebar search filtering
|
||||||
|
|
||||||
|
|||||||
13
package.json
13
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@design.estate/dees-wcctools",
|
"name": "@design.estate/dees-wcctools",
|
||||||
"version": "3.8.3",
|
"version": "3.9.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
|
"description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
"@design.estate/dees-domtools": "^2.5.4",
|
"@design.estate/dees-domtools": "^2.5.4",
|
||||||
"@design.estate/dees-element": "^2.2.4",
|
"@design.estate/dees-element": "^2.2.4",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"lit": "^3.3.2"
|
"lit": "^3.3.2",
|
||||||
|
"mediabunny": "^1.40.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@api.global/typedserver": "^8.4.6",
|
"@api.global/typedserver": "^8.4.6",
|
||||||
@@ -59,5 +60,11 @@
|
|||||||
"element testing",
|
"element testing",
|
||||||
"page development"
|
"page development"
|
||||||
],
|
],
|
||||||
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
|
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977",
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"@types/dom-webcodecs": "./ts_web/types/dom-webcodecs-stub",
|
||||||
|
"@types/dom-mediacapture-transform": "./ts_web/types/dom-mediacapture-stub"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -4,6 +4,10 @@ settings:
|
|||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
overrides:
|
||||||
|
'@types/dom-webcodecs': ./ts_web/types/dom-webcodecs-stub
|
||||||
|
'@types/dom-mediacapture-transform': ./ts_web/types/dom-mediacapture-stub
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
@@ -20,6 +24,9 @@ importers:
|
|||||||
lit:
|
lit:
|
||||||
specifier: ^3.3.2
|
specifier: ^3.3.2
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
|
mediabunny:
|
||||||
|
specifier: ^1.40.1
|
||||||
|
version: 1.40.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@api.global/typedserver':
|
'@api.global/typedserver':
|
||||||
specifier: ^8.4.6
|
specifier: ^8.4.6
|
||||||
@@ -3115,6 +3122,9 @@ packages:
|
|||||||
mdurl@2.0.0:
|
mdurl@2.0.0:
|
||||||
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
|
||||||
|
|
||||||
|
mediabunny@1.40.1:
|
||||||
|
resolution: {integrity: sha512-HU/stGzAkdWaJIly6ypbUVgAUvT9kt39DIg0IaErR7/1fwtTmgUYs4i8uEPYcgcjPjbB9gtBmUXOLnXi6J2LDw==}
|
||||||
|
|
||||||
memory-pager@1.5.0:
|
memory-pager@1.5.0:
|
||||||
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
|
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
|
||||||
|
|
||||||
@@ -8621,6 +8631,11 @@ snapshots:
|
|||||||
|
|
||||||
mdurl@2.0.0: {}
|
mdurl@2.0.0: {}
|
||||||
|
|
||||||
|
mediabunny@1.40.1:
|
||||||
|
dependencies:
|
||||||
|
'@types/dom-mediacapture-transform': link:ts_web/types/dom-mediacapture-stub
|
||||||
|
'@types/dom-webcodecs': link:ts_web/types/dom-webcodecs-stub
|
||||||
|
|
||||||
memory-pager@1.5.0: {}
|
memory-pager@1.5.0: {}
|
||||||
|
|
||||||
micromark-core-commonmark@2.0.3:
|
micromark-core-commonmark@2.0.3:
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# Project Hints and Findings
|
# Project Hints and Findings
|
||||||
|
|
||||||
|
## Mediabunny / @types/dom-webcodecs Override (2026-04-12)
|
||||||
|
|
||||||
|
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/`:
|
||||||
|
- `dom-webcodecs-stub/` — empty, since TS6 provides these types natively
|
||||||
|
- `dom-mediacapture-stub/` — provides `MediaStreamVideoTrack` and `MediaStreamAudioTrack` interfaces (not yet in `lib.dom.d.ts`)
|
||||||
|
|
||||||
|
If mediabunny drops these `@types` dependencies in a future version, the overrides can be removed.
|
||||||
|
|
||||||
## TypeScript 6.0 & Build Tooling (2026-04-12)
|
## TypeScript 6.0 & Build Tooling (2026-04-12)
|
||||||
|
|
||||||
### TypeScript 6.0 Strict Defaults
|
### TypeScript 6.0 Strict Defaults
|
||||||
|
|||||||
22
readme.md
22
readme.md
@@ -10,7 +10,7 @@
|
|||||||
- 🔧 **Real-time Property Editing** — Modify component props on the fly with auto-detected editors
|
- 🔧 **Real-time Property Editing** — Modify component props on the fly with auto-detected editors
|
||||||
- 🌓 **Theme Switching** — Test light/dark modes instantly
|
- 🌓 **Theme Switching** — Test light/dark modes instantly
|
||||||
- 📱 **Responsive Viewport Testing** — Phone, phablet, tablet, and desktop views
|
- 📱 **Responsive Viewport Testing** — Phone, phablet, tablet, and desktop views
|
||||||
- 🎬 **Screen Recording** — Record component demos with audio support and video trimming
|
- 🎬 **Screen Recording** — Record component demos with audio, trimming, and MP4/WebM export
|
||||||
- 🧪 **Advanced Demo Tools** — Post-render hooks for interactive testing
|
- 🧪 **Advanced Demo Tools** — Post-render hooks for interactive testing
|
||||||
- 📂 **Section-based Organization** — Group components into custom sections with filtering and sorting
|
- 📂 **Section-based Organization** — Group components into custom sections with filtering and sorting
|
||||||
- 🚀 **Zero-config Setup** — TypeScript and Lit support out of the box
|
- 🚀 **Zero-config Setup** — TypeScript and Lit support out of the box
|
||||||
@@ -235,15 +235,17 @@ public static styles = [
|
|||||||
|
|
||||||
### 🎬 Screen Recording
|
### 🎬 Screen Recording
|
||||||
|
|
||||||
Record component demos directly from the catalogue:
|
Record component demos directly from the catalogue with full export control:
|
||||||
|
|
||||||
- **Viewport Recording** — Record just the component viewport
|
- **Viewport Recording** — Record just the component viewport
|
||||||
- **Full Screen Recording** — Capture the entire screen
|
- **Full Screen Recording** — Capture the entire screen
|
||||||
- **Audio Support** — Add microphone commentary with live level monitoring
|
- **Audio Support** — Add microphone commentary with live level monitoring
|
||||||
- **Video Trimming** — Trim start/end before export with visual timeline
|
- **Video Trimming** — Trim start/end before export with a visual timeline
|
||||||
- **WebM Export** — High-quality video output
|
- **60fps Capture** — Smooth, high-bitrate recording at up to 60 frames per second
|
||||||
|
- **MP4 Export** — Universal H.264/AAC format via [mediabunny](https://mediabunny.dev) WebCodecs conversion (plays everywhere: WhatsApp, iMessage, Slack, etc.)
|
||||||
|
- **WebM Export** — Native VP9 output for maximum quality
|
||||||
|
|
||||||
Click the red record button in the bottom toolbar to start.
|
Click the red record button in the bottom toolbar, choose your format (MP4 or WebM), and start recording.
|
||||||
|
|
||||||
### 🧪 Demo Tools
|
### 🧪 Demo Tools
|
||||||
|
|
||||||
@@ -438,7 +440,7 @@ The wrapper provides full DOM API access:
|
|||||||
For custom recording integrations:
|
For custom recording integrations:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { RecorderService } from '@design.estate/dees-wcctools';
|
import { RecorderService, type TOutputFormat } from '@design.estate/dees-wcctools';
|
||||||
|
|
||||||
const recorder = new RecorderService({
|
const recorder = new RecorderService({
|
||||||
onDurationUpdate: (duration) => console.log(`${duration}s`),
|
onDurationUpdate: (duration) => console.log(`${duration}s`),
|
||||||
@@ -446,9 +448,13 @@ const recorder = new RecorderService({
|
|||||||
onAudioLevelUpdate: (level) => console.log(`Audio: ${level}%`),
|
onAudioLevelUpdate: (level) => console.log(`Audio: ${level}%`),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Record (always captures as WebM internally)
|
||||||
await recorder.startRecording({ mode: 'viewport' });
|
await recorder.startRecording({ mode: 'viewport' });
|
||||||
// ... later
|
// ... later
|
||||||
recorder.stopRecording();
|
recorder.stopRecording();
|
||||||
|
|
||||||
|
// Convert to MP4 for universal playback (H.264 + AAC via WebCodecs)
|
||||||
|
const mp4Blob = await recorder.convertToMp4(recorder.recordedBlob);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
@@ -481,7 +487,7 @@ my-component-library/
|
|||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
@@ -493,7 +499,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
|||||||
|
|
||||||
### Company Information
|
### Company Information
|
||||||
|
|
||||||
Task Venture Capital GmbH
|
Task Venture Capital GmbH
|
||||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
|
|
||||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-wcctools',
|
name: '@design.estate/dees-wcctools',
|
||||||
version: '3.8.3',
|
version: '3.9.0',
|
||||||
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
|
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DeesElement, customElement, html, css, property, state, type TemplateResult } from '@design.estate/dees-element';
|
import { DeesElement, customElement, html, css, property, state, type TemplateResult } from '@design.estate/dees-element';
|
||||||
import { RecorderService } from '../services/recorder.service.js';
|
import { RecorderService, type TOutputFormat } from '../services/recorder.service.js';
|
||||||
import type { WccDashboard } from './wcc-dashboard.js';
|
import type { WccDashboard } from './wcc-dashboard.js';
|
||||||
|
|
||||||
@customElement('wcc-recording-panel')
|
@customElement('wcc-recording-panel')
|
||||||
@@ -16,6 +16,9 @@ export class WccRecordingPanel extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
accessor recordingMode: 'viewport' | 'screen' = 'viewport';
|
accessor recordingMode: 'viewport' | 'screen' = 'viewport';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
accessor outputFormat: TOutputFormat = 'mp4';
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor audioEnabled: boolean = false;
|
accessor audioEnabled: boolean = false;
|
||||||
|
|
||||||
@@ -380,6 +383,9 @@ export class WccRecordingPanel extends DeesElement {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.15s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-btn.secondary {
|
.preview-btn.secondary {
|
||||||
@@ -546,7 +552,7 @@ export class WccRecordingPanel extends DeesElement {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border-top-color: white;
|
border-top-color: white;
|
||||||
animation: spin 0.8s linear infinite;
|
animation: spin 0.8s linear infinite;
|
||||||
margin-right: 0.5rem;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
@@ -591,6 +597,24 @@ export class WccRecordingPanel extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="recording-option-group">
|
||||||
|
<div class="recording-option-label">Format</div>
|
||||||
|
<div class="recording-mode-buttons">
|
||||||
|
<button
|
||||||
|
class="recording-mode-btn ${this.outputFormat === 'mp4' ? 'selected' : ''}"
|
||||||
|
@click=${() => this.outputFormat = 'mp4'}
|
||||||
|
>
|
||||||
|
MP4 (H.264)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="recording-mode-btn ${this.outputFormat === 'webm' ? 'selected' : ''}"
|
||||||
|
@click=${() => this.outputFormat = 'webm'}
|
||||||
|
>
|
||||||
|
WebM (VP9)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="recording-option-group">
|
<div class="recording-option-group">
|
||||||
<div class="recording-option-label">Audio</div>
|
<div class="recording-option-label">Audio</div>
|
||||||
<div class="audio-toggle">
|
<div class="audio-toggle">
|
||||||
@@ -716,7 +740,9 @@ export class WccRecordingPanel extends DeesElement {
|
|||||||
?disabled=${this.isExporting}
|
?disabled=${this.isExporting}
|
||||||
@click=${() => this.downloadRecording()}
|
@click=${() => this.downloadRecording()}
|
||||||
>
|
>
|
||||||
${this.isExporting ? html`<span class="export-spinner"></span>Exporting...` : 'Download WebM'}
|
${this.isExporting
|
||||||
|
? html`<span class="export-spinner"></span>${this.outputFormat === 'mp4' ? 'Converting to MP4...' : 'Exporting...'}`
|
||||||
|
: `Download ${this.outputFormat === 'mp4' ? 'MP4' : 'WebM'}`}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -764,7 +790,7 @@ export class WccRecordingPanel extends DeesElement {
|
|||||||
await this.recorderService.startRecording({
|
await this.recorderService.startRecording({
|
||||||
mode: this.recordingMode,
|
mode: this.recordingMode,
|
||||||
audioDeviceId: this.audioEnabled ? this.selectedMicrophoneId : undefined,
|
audioDeviceId: this.audioEnabled ? this.selectedMicrophoneId : undefined,
|
||||||
viewportElement
|
viewportElement,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.panelState = 'recording';
|
this.panelState = 'recording';
|
||||||
@@ -817,7 +843,7 @@ export class WccRecordingPanel extends DeesElement {
|
|||||||
try {
|
try {
|
||||||
let blobToDownload: Blob;
|
let blobToDownload: Blob;
|
||||||
|
|
||||||
// Handle trimming if needed
|
// Handle trimming if needed — always produces WebM
|
||||||
const needsTrim = this.trimStart > 0.1 || this.trimEnd < this.videoDuration - 0.1;
|
const needsTrim = this.trimStart > 0.1 || this.trimEnd < this.videoDuration - 0.1;
|
||||||
|
|
||||||
if (needsTrim) {
|
if (needsTrim) {
|
||||||
@@ -831,9 +857,15 @@ export class WccRecordingPanel extends DeesElement {
|
|||||||
blobToDownload = recordedBlob;
|
blobToDownload = recordedBlob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert WebM → MP4 if MP4 format selected
|
||||||
|
if (this.outputFormat === 'mp4') {
|
||||||
|
blobToDownload = await this.recorderService.convertToMp4(blobToDownload);
|
||||||
|
}
|
||||||
|
|
||||||
// Trigger download
|
// Trigger download
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||||
const filename = `wcctools-recording-${timestamp}.webm`;
|
const ext = this.outputFormat === 'mp4' ? 'mp4' : 'webm';
|
||||||
|
const filename = `wcctools-recording-${timestamp}.${ext}`;
|
||||||
|
|
||||||
const url = URL.createObjectURL(blobToDownload);
|
const url = URL.createObjectURL(blobToDownload);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { TTemplateFactory } from './elements/wcctools.helpers.js';
|
|||||||
import type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
|
import type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
|
||||||
|
|
||||||
// Export recording components and service
|
// Export recording components and service
|
||||||
export { RecorderService, type IRecorderEvents, type IRecordingOptions } from './services/recorder.service.js';
|
export { RecorderService, type IRecorderEvents, type IRecordingOptions, type TOutputFormat } from './services/recorder.service.js';
|
||||||
export { WccRecordButton } from './elements/wcc-record-button.js';
|
export { WccRecordButton } from './elements/wcc-record-button.js';
|
||||||
export { WccRecordingPanel } from './elements/wcc-recording-panel.js';
|
export { WccRecordingPanel } from './elements/wcc-recording-panel.js';
|
||||||
|
|
||||||
|
|||||||
@@ -71,11 +71,12 @@ setupWccTools({
|
|||||||
|
|
||||||
| Export | Description |
|
| Export | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `RecorderService` | Service class for screen/viewport recording |
|
| `RecorderService` | Service class for screen/viewport recording and MP4 conversion |
|
||||||
| `WccRecordButton` | Record button UI component |
|
| `WccRecordButton` | Record button UI component |
|
||||||
| `WccRecordingPanel` | Recording options and preview panel |
|
| `WccRecordingPanel` | Recording options and preview panel |
|
||||||
| `IRecorderEvents` | TypeScript interface for recorder callbacks |
|
| `IRecorderEvents` | TypeScript interface for recorder callbacks |
|
||||||
| `IRecordingOptions` | TypeScript interface for recording options |
|
| `IRecordingOptions` | TypeScript interface for recording options |
|
||||||
|
| `TOutputFormat` | Type for output format selection (`'webm' \| 'mp4'`) |
|
||||||
|
|
||||||
## Section Configuration
|
## Section Configuration
|
||||||
|
|
||||||
@@ -96,15 +97,15 @@ The module includes these internal web components:
|
|||||||
| Component | Description |
|
| Component | Description |
|
||||||
|-----------|-------------|
|
|-----------|-------------|
|
||||||
| `wcc-dashboard` | Main dashboard container with routing |
|
| `wcc-dashboard` | Main dashboard container with routing |
|
||||||
| `wcc-sidebar` | Navigation sidebar with collapsible sections |
|
| `wcc-sidebar` | Navigation sidebar with collapsible sections and search |
|
||||||
| `wcc-frame` | Responsive viewport with size controls |
|
| `wcc-frame` | Responsive viewport with size controls |
|
||||||
| `wcc-properties` | Property panel with live editing |
|
| `wcc-properties` | Property panel with live editing |
|
||||||
| `wcc-record-button` | Recording state indicator button |
|
| `wcc-record-button` | Recording state indicator button |
|
||||||
| `wcc-recording-panel` | Recording workflow UI |
|
| `wcc-recording-panel` | Recording workflow UI with format selection |
|
||||||
|
|
||||||
## RecorderService API
|
## RecorderService API
|
||||||
|
|
||||||
For programmatic recording control:
|
For programmatic recording and MP4 conversion:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { RecorderService, type IRecorderEvents } from '@design.estate/dees-wcctools';
|
import { RecorderService, type IRecorderEvents } from '@design.estate/dees-wcctools';
|
||||||
@@ -125,7 +126,7 @@ const mics = await recorder.loadMicrophones(true);
|
|||||||
// Start audio level monitoring
|
// Start audio level monitoring
|
||||||
await recorder.startAudioMonitoring(mics[0].deviceId);
|
await recorder.startAudioMonitoring(mics[0].deviceId);
|
||||||
|
|
||||||
// Start recording
|
// Start recording (always captures as WebM internally at up to 60fps)
|
||||||
await recorder.startRecording({
|
await recorder.startRecording({
|
||||||
mode: 'viewport',
|
mode: 'viewport',
|
||||||
audioDeviceId: mics[0].deviceId,
|
audioDeviceId: mics[0].deviceId,
|
||||||
@@ -135,7 +136,10 @@ await recorder.startRecording({
|
|||||||
// Stop recording
|
// Stop recording
|
||||||
recorder.stopRecording();
|
recorder.stopRecording();
|
||||||
|
|
||||||
// Export trimmed video
|
// Convert to MP4 for universal playback (H.264 + AAC via WebCodecs)
|
||||||
|
const mp4Blob = await recorder.convertToMp4(recorder.recordedBlob);
|
||||||
|
|
||||||
|
// Or export trimmed video
|
||||||
const trimmedBlob = await recorder.exportTrimmedVideo(videoElement, startTime, endTime);
|
const trimmedBlob = await recorder.exportTrimmedVideo(videoElement, startTime, endTime);
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
@@ -148,6 +152,9 @@ recorder.dispose();
|
|||||||
ts_web/
|
ts_web/
|
||||||
├── index.ts # Main exports
|
├── index.ts # Main exports
|
||||||
├── wcctools.interfaces.ts # Type definitions
|
├── wcctools.interfaces.ts # Type definitions
|
||||||
|
├── types/
|
||||||
|
│ ├── dom-webcodecs-stub/ # TS6 compatibility shim
|
||||||
|
│ └── dom-mediacapture-stub/ # MediaCapture Transform types
|
||||||
├── elements/
|
├── elements/
|
||||||
│ ├── wcc-dashboard.ts # Root dashboard component
|
│ ├── wcc-dashboard.ts # Root dashboard component
|
||||||
│ ├── wcc-sidebar.ts # Navigation sidebar
|
│ ├── wcc-sidebar.ts # Navigation sidebar
|
||||||
@@ -157,7 +164,7 @@ ts_web/
|
|||||||
│ ├── wcc-recording-panel.ts # Recording options/preview
|
│ ├── wcc-recording-panel.ts # Recording options/preview
|
||||||
│ └── wcctools.helpers.ts # Shared utilities
|
│ └── wcctools.helpers.ts # Shared utilities
|
||||||
├── services/
|
├── services/
|
||||||
│ └── recorder.service.ts # MediaRecorder abstraction
|
│ └── recorder.service.ts # MediaRecorder + mediabunny MP4 conversion
|
||||||
└── pages/
|
└── pages/
|
||||||
└── index.ts # Built-in pages
|
└── index.ts # Built-in pages
|
||||||
```
|
```
|
||||||
@@ -165,9 +172,9 @@ ts_web/
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🎨 Interactive component preview
|
- 🎨 Interactive component preview
|
||||||
- 📂 Section-based sidebar with filtering & sorting
|
- 📂 Section-based sidebar with filtering, sorting & search (by name, tag, or group)
|
||||||
- 🔧 Real-time property editing with type detection
|
- 🔧 Real-time property editing with type detection
|
||||||
- 🌓 Theme switching (light/dark)
|
- 🌓 Theme switching (light/dark)
|
||||||
- 📱 Responsive viewport testing
|
- 📱 Responsive viewport testing
|
||||||
- 🎬 Screen recording with trimming
|
- 🎬 Screen recording with MP4/WebM export, trimming, and audio
|
||||||
- 🔗 URL-based deep linking
|
- 🔗 URL-based deep linking
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* RecorderService - Handles all MediaRecorder, audio monitoring, and video export logic
|
* RecorderService - Handles all MediaRecorder, audio monitoring, and video export logic.
|
||||||
|
* Recording always uses MediaRecorder → WebM (the reliable browser path).
|
||||||
|
* MP4 output is produced by converting WebM → MP4 via mediabunny at export time.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export type TOutputFormat = 'webm' | 'mp4';
|
||||||
|
|
||||||
export interface IRecorderEvents {
|
export interface IRecorderEvents {
|
||||||
onDurationUpdate?: (duration: number) => void;
|
onDurationUpdate?: (duration: number) => void;
|
||||||
onRecordingComplete?: (blob: Blob) => void;
|
onRecordingComplete?: (blob: Blob) => void;
|
||||||
@@ -14,6 +18,7 @@ export interface IRecordingOptions {
|
|||||||
mode: 'viewport' | 'screen';
|
mode: 'viewport' | 'screen';
|
||||||
audioDeviceId?: string;
|
audioDeviceId?: string;
|
||||||
viewportElement?: HTMLElement;
|
viewportElement?: HTMLElement;
|
||||||
|
outputFormat?: TOutputFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RecorderService {
|
export class RecorderService {
|
||||||
@@ -24,6 +29,7 @@ export class RecorderService {
|
|||||||
private _duration: number = 0;
|
private _duration: number = 0;
|
||||||
private _recordedBlob: Blob | null = null;
|
private _recordedBlob: Blob | null = null;
|
||||||
private _isRecording: boolean = false;
|
private _isRecording: boolean = false;
|
||||||
|
private _outputFormat: TOutputFormat = 'webm';
|
||||||
|
|
||||||
// Audio monitoring state
|
// Audio monitoring state
|
||||||
private audioContext: AudioContext | null = null;
|
private audioContext: AudioContext | null = null;
|
||||||
@@ -56,6 +62,10 @@ export class RecorderService {
|
|||||||
return this._recordedBlob;
|
return this._recordedBlob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get outputFormat(): TOutputFormat {
|
||||||
|
return this._outputFormat;
|
||||||
|
}
|
||||||
|
|
||||||
// Update event callbacks
|
// Update event callbacks
|
||||||
setEvents(events: IRecorderEvents): void {
|
setEvents(events: IRecorderEvents): void {
|
||||||
this.events = { ...this.events, ...events };
|
this.events = { ...this.events, ...events };
|
||||||
@@ -132,13 +142,16 @@ export class RecorderService {
|
|||||||
|
|
||||||
async startRecording(options: IRecordingOptions): Promise<void> {
|
async startRecording(options: IRecordingOptions): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
this._outputFormat = options.outputFormat || 'webm';
|
||||||
|
|
||||||
// Stop audio monitoring before recording
|
// Stop audio monitoring before recording
|
||||||
this.stopAudioMonitoring();
|
this.stopAudioMonitoring();
|
||||||
|
|
||||||
// Get video stream based on mode
|
// Get video stream based on mode
|
||||||
const displayMediaOptions: DisplayMediaStreamOptions = {
|
const displayMediaOptions: DisplayMediaStreamOptions = {
|
||||||
video: {
|
video: {
|
||||||
displaySurface: options.mode === 'viewport' ? 'browser' : 'monitor'
|
displaySurface: options.mode === 'viewport' ? 'browser' : 'monitor',
|
||||||
|
frameRate: { ideal: 60 },
|
||||||
} as MediaTrackConstraints,
|
} as MediaTrackConstraints,
|
||||||
audio: false
|
audio: false
|
||||||
};
|
};
|
||||||
@@ -182,12 +195,23 @@ export class RecorderService {
|
|||||||
// Store stream for cleanup
|
// Store stream for cleanup
|
||||||
this.currentStream = combinedStream;
|
this.currentStream = combinedStream;
|
||||||
|
|
||||||
// Create MediaRecorder
|
// Handle stream ending (user clicks "Stop sharing")
|
||||||
|
videoStream.getVideoTracks()[0].onended = () => {
|
||||||
|
if (this._isRecording) {
|
||||||
|
this.stopRecording();
|
||||||
|
this.events.onStreamEnded?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Always record as WebM — conversion to MP4 happens at export time
|
||||||
const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
|
const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
|
||||||
? 'video/webm;codecs=vp9'
|
? 'video/webm;codecs=vp9'
|
||||||
: 'video/webm';
|
: 'video/webm';
|
||||||
|
|
||||||
this.mediaRecorder = new MediaRecorder(combinedStream, { mimeType });
|
this.mediaRecorder = new MediaRecorder(combinedStream, {
|
||||||
|
mimeType,
|
||||||
|
videoBitsPerSecond: 8_000_000, // 8 Mbps for smooth, high-quality capture
|
||||||
|
});
|
||||||
this.recordedChunks = [];
|
this.recordedChunks = [];
|
||||||
|
|
||||||
this.mediaRecorder.ondataavailable = (e) => {
|
this.mediaRecorder.ondataavailable = (e) => {
|
||||||
@@ -198,14 +222,6 @@ export class RecorderService {
|
|||||||
|
|
||||||
this.mediaRecorder.onstop = () => this.handleRecordingComplete();
|
this.mediaRecorder.onstop = () => this.handleRecordingComplete();
|
||||||
|
|
||||||
// Handle stream ending (user clicks "Stop sharing")
|
|
||||||
videoStream.getVideoTracks()[0].onended = () => {
|
|
||||||
if (this._isRecording) {
|
|
||||||
this.stopRecording();
|
|
||||||
this.events.onStreamEnded?.();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.mediaRecorder.start(1000); // Capture in 1-second chunks
|
this.mediaRecorder.start(1000); // Capture in 1-second chunks
|
||||||
|
|
||||||
// Start duration timer
|
// Start duration timer
|
||||||
@@ -236,9 +252,7 @@ export class RecorderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleRecordingComplete(): Promise<void> {
|
private async handleRecordingComplete(): Promise<void> {
|
||||||
// Create blob from recorded chunks
|
|
||||||
const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
|
const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
|
||||||
|
|
||||||
this._recordedBlob = blob;
|
this._recordedBlob = blob;
|
||||||
|
|
||||||
// Stop all tracks
|
// Stop all tracks
|
||||||
@@ -251,7 +265,51 @@ export class RecorderService {
|
|||||||
this.events.onRecordingComplete?.(this._recordedBlob);
|
this.events.onRecordingComplete?.(this._recordedBlob);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Trim & Export ====================
|
// ==================== Conversion & Export ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a WebM blob to MP4 using mediabunny's Conversion API.
|
||||||
|
* Uses WebCodecs for hardware-accelerated H.264 encoding.
|
||||||
|
*/
|
||||||
|
async convertToMp4(webmBlob: Blob): Promise<Blob> {
|
||||||
|
const {
|
||||||
|
Input, Output, Conversion, BlobSource, BufferTarget, Mp4OutputFormat, WEBM, QUALITY_HIGH,
|
||||||
|
} = await import('mediabunny');
|
||||||
|
|
||||||
|
const input = new Input({
|
||||||
|
source: new BlobSource(webmBlob),
|
||||||
|
formats: [WEBM],
|
||||||
|
});
|
||||||
|
|
||||||
|
const target = new BufferTarget();
|
||||||
|
const output = new Output({
|
||||||
|
format: new Mp4OutputFormat({ fastStart: 'in-memory' }),
|
||||||
|
target,
|
||||||
|
});
|
||||||
|
|
||||||
|
const conversion = await Conversion.init({
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
// Force transcoding from VP9 → H.264 and Opus → AAC
|
||||||
|
video: {
|
||||||
|
codec: 'avc',
|
||||||
|
bitrate: QUALITY_HIGH,
|
||||||
|
fit: 'contain',
|
||||||
|
},
|
||||||
|
audio: {
|
||||||
|
codec: 'aac',
|
||||||
|
bitrate: QUALITY_HIGH,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await conversion.execute();
|
||||||
|
|
||||||
|
const buffer = target.buffer;
|
||||||
|
if (!buffer || buffer.byteLength === 0) {
|
||||||
|
throw new Error('MP4 conversion produced empty output');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Blob([buffer], { type: 'video/mp4' });
|
||||||
|
}
|
||||||
|
|
||||||
async exportTrimmedVideo(
|
async exportTrimmedVideo(
|
||||||
videoElement: HTMLVideoElement,
|
videoElement: HTMLVideoElement,
|
||||||
|
|||||||
12
ts_web/types/dom-mediacapture-stub/index.d.ts
vendored
Normal file
12
ts_web/types/dom-mediacapture-stub/index.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Minimal type stubs for MediaCapture Transform API types not yet in lib.dom.d.ts.
|
||||||
|
// These specialize MediaStreamTrack for audio/video so mediabunny's API is type-safe.
|
||||||
|
|
||||||
|
interface MediaStreamAudioTrack extends MediaStreamTrack {
|
||||||
|
readonly kind: 'audio';
|
||||||
|
clone(): MediaStreamAudioTrack;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MediaStreamVideoTrack extends MediaStreamTrack {
|
||||||
|
readonly kind: 'video';
|
||||||
|
clone(): MediaStreamVideoTrack;
|
||||||
|
}
|
||||||
6
ts_web/types/dom-mediacapture-stub/package.json
Normal file
6
ts_web/types/dom-mediacapture-stub/package.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "@types/dom-mediacapture-transform",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Minimal stub providing MediaStreamVideoTrack/MediaStreamAudioTrack for TS 6",
|
||||||
|
"types": "index.d.ts"
|
||||||
|
}
|
||||||
2
ts_web/types/dom-webcodecs-stub/index.d.ts
vendored
Normal file
2
ts_web/types/dom-webcodecs-stub/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Empty stub: TypeScript 6 includes WebCodecs types in lib.dom.d.ts natively.
|
||||||
|
// This prevents @types/dom-webcodecs from conflicting with the built-in types.
|
||||||
6
ts_web/types/dom-webcodecs-stub/package.json
Normal file
6
ts_web/types/dom-webcodecs-stub/package.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "@types/dom-webcodecs",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Empty stub — TypeScript 6 provides WebCodecs types natively in lib.dom.d.ts",
|
||||||
|
"types": "index.d.ts"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user