Compare commits

...

9 Commits

Author SHA1 Message Date
73a975e9e9 v3.8.5
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-12 23:21:45 +00:00
d178d6cb73 fix(recording): improve recording capture quality and align preview button loading state 2026-04-12 23:21:45 +00:00
3eeb9dc46f feat(recording): add output format options and MP4 conversion support
- Introduced `outputFormat` state in `WccRecordingPanel` for selecting between MP4 and WebM formats.
- Updated `RecorderService` to handle MP4 conversion using mediabunny.
- Added type stubs for `MediaStreamAudioTrack` and `MediaStreamVideoTrack` to ensure type safety.
- Updated documentation to reflect changes in output format handling.
2026-04-12 23:17:21 +00:00
d9330a5fa1 v3.8.4
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-12 17:17:40 +00:00
443618d1ac fix(repo): no changes to commit 2026-04-12 17:17:40 +00:00
ac087b9f3f v3.8.3
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-12 17:17:04 +00:00
977d8ab5e0 fix(sidebar): include component tag names in sidebar search filtering 2026-04-12 17:17:04 +00:00
02e1f536d5 v3.8.2
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-12 10:23:02 +00:00
a7f5341baa fix(sidebar): restore search input focus after clearing the sidebar search 2026-04-12 10:23:02 +00:00
13 changed files with 200 additions and 26 deletions

View File

@@ -1,5 +1,27 @@
# Changelog
## 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)
include component tag names in sidebar search filtering
- Updates sidebar section and entry filtering to match search queries against each item's custom element tag name via the `is` field.
- Keeps existing name and demo group matching behavior while making search results easier to find by tag.
## 2026-04-12 - 3.8.2 - fix(sidebar)
restore search input focus after clearing the sidebar search
- Updates the sidebar clearSearch behavior to focus the .search-input element after resetting the query and dispatching searchChanged.
- Improves search usability by letting users continue typing immediately after clearing the current search.
## 2026-04-12 - 3.8.1 - fix(build)
migrate smart config and update build tooling for latest tsbundle and TypeScript defaults

View File

@@ -1,6 +1,6 @@
{
"name": "@design.estate/dees-wcctools",
"version": "3.8.1",
"version": "3.8.5",
"private": false,
"description": "A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.",
"exports": {
@@ -20,7 +20,8 @@
"@design.estate/dees-domtools": "^2.5.4",
"@design.estate/dees-element": "^2.2.4",
"@push.rocks/smartdelay": "^3.0.5",
"lit": "^3.3.2"
"lit": "^3.3.2",
"mediabunny": "^1.40.1"
},
"devDependencies": {
"@api.global/typedserver": "^8.4.6",
@@ -59,5 +60,11 @@
"element testing",
"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
View File

@@ -4,6 +4,10 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
'@types/dom-webcodecs': ./ts_web/types/dom-webcodecs-stub
'@types/dom-mediacapture-transform': ./ts_web/types/dom-mediacapture-stub
importers:
.:
@@ -20,6 +24,9 @@ importers:
lit:
specifier: ^3.3.2
version: 3.3.2
mediabunny:
specifier: ^1.40.1
version: 1.40.1
devDependencies:
'@api.global/typedserver':
specifier: ^8.4.6
@@ -3115,6 +3122,9 @@ packages:
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
mediabunny@1.40.1:
resolution: {integrity: sha512-HU/stGzAkdWaJIly6ypbUVgAUvT9kt39DIg0IaErR7/1fwtTmgUYs4i8uEPYcgcjPjbB9gtBmUXOLnXi6J2LDw==}
memory-pager@1.5.0:
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
@@ -8621,6 +8631,11 @@ snapshots:
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: {}
micromark-core-commonmark@2.0.3:

View File

@@ -1,5 +1,13 @@
# 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 Strict Defaults

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@design.estate/dees-wcctools',
version: '3.8.1',
version: '3.8.5',
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
}

View File

@@ -1,5 +1,5 @@
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';
@customElement('wcc-recording-panel')
@@ -16,6 +16,9 @@ export class WccRecordingPanel extends DeesElement {
@state()
accessor recordingMode: 'viewport' | 'screen' = 'viewport';
@state()
accessor outputFormat: TOutputFormat = 'mp4';
@state()
accessor audioEnabled: boolean = false;
@@ -380,6 +383,9 @@ export class WccRecordingPanel extends DeesElement {
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.preview-btn.secondary {
@@ -546,7 +552,7 @@ export class WccRecordingPanel extends DeesElement {
border-radius: 50%;
border-top-color: white;
animation: spin 0.8s linear infinite;
margin-right: 0.5rem;
flex-shrink: 0;
}
@keyframes spin {
@@ -591,6 +597,24 @@ export class WccRecordingPanel extends DeesElement {
</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-label">Audio</div>
<div class="audio-toggle">
@@ -716,7 +740,9 @@ export class WccRecordingPanel extends DeesElement {
?disabled=${this.isExporting}
@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>
</div>
</div>
@@ -764,7 +790,7 @@ export class WccRecordingPanel extends DeesElement {
await this.recorderService.startRecording({
mode: this.recordingMode,
audioDeviceId: this.audioEnabled ? this.selectedMicrophoneId : undefined,
viewportElement
viewportElement,
});
this.panelState = 'recording';
@@ -817,7 +843,7 @@ export class WccRecordingPanel extends DeesElement {
try {
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;
if (needsTrim) {
@@ -831,9 +857,15 @@ export class WccRecordingPanel extends DeesElement {
blobToDownload = recordedBlob;
}
// Convert WebM → MP4 if MP4 format selected
if (this.outputFormat === 'mp4') {
blobToDownload = await this.recorderService.convertToMp4(blobToDownload);
}
// Trigger download
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 a = document.createElement('a');

View File

@@ -654,6 +654,8 @@ export class WccSidebar extends DeesElement {
const entries = getSectionItems(section);
const filteredEntries = entries.filter(([name, item]) => {
if (this.matchesSearch(name)) return true;
const tagName = (item as any).is;
if (tagName && this.matchesSearch(tagName)) return true;
const rawGroups = (item as any).demoGroups;
if (!rawGroups) return false;
const groups: string[] = Array.isArray(rawGroups) ? rawGroups : [rawGroups];
@@ -692,6 +694,8 @@ export class WccSidebar extends DeesElement {
// Filter entries by search query
const filteredEntries = entries.filter(([name, item]) => {
if (this.matchesSearch(name)) return true;
const tagName = (item as any).is;
if (tagName && this.matchesSearch(tagName)) return true;
const rawGroups = (item as any).demoGroups;
if (!rawGroups) return false;
const groups: string[] = Array.isArray(rawGroups) ? rawGroups : [rawGroups];
@@ -867,6 +871,8 @@ export class WccSidebar extends DeesElement {
private clearSearch() {
this.searchQuery = '';
this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery }));
const input = this.shadowRoot.querySelector('.search-input') as HTMLInputElement;
if (input) input.focus();
}
private handleMenuScroll(e: Event) {

View File

@@ -4,7 +4,7 @@ import type { TTemplateFactory } from './elements/wcctools.helpers.js';
import type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
// 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 { WccRecordingPanel } from './elements/wcc-recording-panel.js';

View File

@@ -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 {
onDurationUpdate?: (duration: number) => void;
onRecordingComplete?: (blob: Blob) => void;
@@ -14,6 +18,7 @@ export interface IRecordingOptions {
mode: 'viewport' | 'screen';
audioDeviceId?: string;
viewportElement?: HTMLElement;
outputFormat?: TOutputFormat;
}
export class RecorderService {
@@ -24,6 +29,7 @@ export class RecorderService {
private _duration: number = 0;
private _recordedBlob: Blob | null = null;
private _isRecording: boolean = false;
private _outputFormat: TOutputFormat = 'webm';
// Audio monitoring state
private audioContext: AudioContext | null = null;
@@ -56,6 +62,10 @@ export class RecorderService {
return this._recordedBlob;
}
get outputFormat(): TOutputFormat {
return this._outputFormat;
}
// Update event callbacks
setEvents(events: IRecorderEvents): void {
this.events = { ...this.events, ...events };
@@ -132,13 +142,16 @@ export class RecorderService {
async startRecording(options: IRecordingOptions): Promise<void> {
try {
this._outputFormat = options.outputFormat || 'webm';
// Stop audio monitoring before recording
this.stopAudioMonitoring();
// Get video stream based on mode
const displayMediaOptions: DisplayMediaStreamOptions = {
video: {
displaySurface: options.mode === 'viewport' ? 'browser' : 'monitor'
displaySurface: options.mode === 'viewport' ? 'browser' : 'monitor',
frameRate: { ideal: 60 },
} as MediaTrackConstraints,
audio: false
};
@@ -182,12 +195,23 @@ export class RecorderService {
// Store stream for cleanup
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')
? 'video/webm;codecs=vp9'
: '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.mediaRecorder.ondataavailable = (e) => {
@@ -198,14 +222,6 @@ export class RecorderService {
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
// Start duration timer
@@ -236,9 +252,7 @@ export class RecorderService {
}
private async handleRecordingComplete(): Promise<void> {
// Create blob from recorded chunks
const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
this._recordedBlob = blob;
// Stop all tracks
@@ -251,7 +265,51 @@ export class RecorderService {
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(
videoElement: HTMLVideoElement,

View 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;
}

View 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"
}

View 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.

View 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"
}