diff --git a/changelog.md b/changelog.md
index 6f52f66..125488b 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,18 @@
# Changelog
+## 2025-12-11 - 2.0.0 - BREAKING CHANGE(recorder)
+Remove FFmpeg-based MP4 conversion; simplify recorder/export to WebM and improve recorder/editor robustness
+
+- Removed FFmpegService and all client-side MP4 conversion logic â exports are now WebM-only (MP4 conversion and related UI/controls removed).
+- ts_web/elements/wcc-recording-panel: dropped outputFormat and conversion states/UI; download flow simplified to always export WebM.
+- ts_web/index.ts: removed FFmpegService exports and conversion types from public API.
+- package.json: removed @ffmpeg/* dependencies.
+- RecorderService: handleRecordingComplete is now async and fixes recorded blob assignment and cleanup timing.
+- wcc-properties: improved element detection and robustness â recursive search through light/shadow DOM with retry/delay, plus an advanced JSON editor for Object/Array props (supports multiple open editors and frame resize events).
+- wcc-sidebar: force re-render after selecting demos to ensure child demo selection indicators update correctly.
+- dees-demowrapper: ensure slotted content is rendered before calling runAfterRender (small timing/stability improvements).
+- Test update: demo definitions can be arrays (multiple demos) â test-demoelement updated to use multiple demo entries.
+
## 2025-12-11 - 1.3.0 - feat(recording-panel)
Add demo wrapper utilities, improve recording trim behavior, and harden property panel element detection; update documentation
diff --git a/package.json b/package.json
index f9510ca..f19abb1 100644
--- a/package.json
+++ b/package.json
@@ -19,8 +19,6 @@
"dependencies": {
"@design.estate/dees-domtools": "^2.3.6",
"@design.estate/dees-element": "^2.1.3",
- "@ffmpeg/ffmpeg": "^0.12.15",
- "@ffmpeg/util": "^0.12.1",
"@push.rocks/smartdelay": "^3.0.5",
"lit": "^3.3.1"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6766c2b..e2d04c7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,12 +14,6 @@ importers:
'@design.estate/dees-element':
specifier: ^2.1.3
version: 2.1.3
- '@ffmpeg/ffmpeg':
- specifier: ^0.12.15
- version: 0.12.15
- '@ffmpeg/util':
- specifier: ^0.12.1
- version: 0.12.2
'@push.rocks/smartdelay':
specifier: ^3.0.5
version: 3.0.5
@@ -455,18 +449,6 @@ packages:
cpu: [x64]
os: [win32]
- '@ffmpeg/ffmpeg@0.12.15':
- resolution: {integrity: sha512-1C8Obr4GsN3xw+/1Ww6PFM84wSQAGsdoTuTWPOj2OizsRDLT4CXTaVjPhkw6ARyDus1B9X/L2LiXHqYYsGnRFw==}
- engines: {node: '>=18.x'}
-
- '@ffmpeg/types@0.12.4':
- resolution: {integrity: sha512-k9vJQNBGTxE5AhYDtOYR5rO5fKsspbg51gbcwtbkw2lCdoIILzklulcjJfIDwrtn7XhDeF2M+THwJ2FGrLeV6A==}
- engines: {node: '>=16.x'}
-
- '@ffmpeg/util@0.12.2':
- resolution: {integrity: sha512-ouyoW+4JB7WxjeZ2y6KpRvB+dLp7Cp4ro8z0HIVpZVCM7AwFlHa0c4R8Y/a4M3wMqATpYKhC7lSFHQ0T11MEDw==}
- engines: {node: '>=18.x'}
-
'@fortawesome/fontawesome-common-types@7.1.0':
resolution: {integrity: sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==}
engines: {node: '>=6'}
@@ -4691,14 +4673,6 @@ snapshots:
'@esbuild/win32-x64@0.27.1':
optional: true
- '@ffmpeg/ffmpeg@0.12.15':
- dependencies:
- '@ffmpeg/types': 0.12.4
-
- '@ffmpeg/types@0.12.4': {}
-
- '@ffmpeg/util@0.12.2': {}
-
'@fortawesome/fontawesome-common-types@7.1.0': {}
'@fortawesome/fontawesome-svg-core@7.1.0':
diff --git a/test/elements/test-demoelement.ts b/test/elements/test-demoelement.ts
index 9c9ae6e..8eb073e 100644
--- a/test/elements/test-demoelement.ts
+++ b/test/elements/test-demoelement.ts
@@ -18,7 +18,11 @@ enum ETestEnum {
@customElement('test-demoelement')
export class TestDemoelement extends DeesElement {
- public static demo = () => html`This is a slot text`;
+ public static demo = [
+ () => html`This is demo 1`,
+ () => html`This is demo 2`,
+ () => html`This is demo 2`,
+ ]
@property()
accessor notTyped = 'hello';
diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts
index 4b3a3c8..f522843 100644
--- a/ts_web/00_commitinfo_data.ts
+++ b/ts_web/00_commitinfo_data.ts
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@design.estate/dees-wcctools',
- version: '1.3.0',
+ version: '2.0.0',
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
}
diff --git a/ts_web/elements/wcc-recording-panel.ts b/ts_web/elements/wcc-recording-panel.ts
index b21e708..90873cc 100644
--- a/ts_web/elements/wcc-recording-panel.ts
+++ b/ts_web/elements/wcc-recording-panel.ts
@@ -1,6 +1,5 @@
import { DeesElement, customElement, html, css, property, state, type TemplateResult } from '@design.estate/dees-element';
import { RecorderService } from '../services/recorder.service.js';
-import { getFFmpegService, type IConversionProgress } from '../services/ffmpeg.service.js';
import type { WccDashboard } from './wcc-dashboard.js';
@customElement('wcc-recording-panel')
@@ -52,15 +51,6 @@ export class WccRecordingPanel extends DeesElement {
@state()
accessor isExporting: boolean = false;
- @state()
- accessor outputFormat: 'mp4' | 'webm' = 'mp4';
-
- @state()
- accessor conversionProgress: IConversionProgress | null = null;
-
- @state()
- accessor isConverting: boolean = false;
-
// Service instance
private recorderService: RecorderService;
@@ -563,94 +553,6 @@ export class WccRecordingPanel extends DeesElement {
to { transform: rotate(360deg); }
}
- /* Format Selection Styles */
- .format-section {
- margin-top: 1.25rem;
- padding-top: 1.25rem;
- border-top: 1px solid rgba(255, 255, 255, 0.05);
- }
-
- .format-section-header {
- margin-bottom: 0.75rem;
- }
-
- .format-section-title {
- font-size: 0.75rem;
- font-weight: 500;
- color: #888;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- }
-
- .format-buttons {
- display: flex;
- gap: 0.5rem;
- }
-
- .format-btn {
- flex: 1;
- padding: 0.6rem 0.75rem;
- background: var(--input);
- border: 1px solid transparent;
- border-radius: var(--radius-sm);
- color: #999;
- font-size: 0.75rem;
- cursor: pointer;
- transition: all 0.15s ease;
- text-align: center;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 0.5rem;
- }
-
- .format-btn:hover {
- border-color: var(--primary);
- color: #ccc;
- }
-
- .format-btn.selected {
- background: rgba(59, 130, 246, 0.15);
- border-color: var(--primary);
- color: var(--primary);
- }
-
- .format-icon {
- font-size: 1rem;
- }
-
- .format-note {
- margin-top: 0.5rem;
- padding: 0.5rem;
- background: rgba(59, 130, 246, 0.1);
- border-radius: var(--radius-sm);
- font-size: 0.7rem;
- color: #888;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- }
-
- .note-icon {
- font-size: 0.9rem;
- }
-
- /* Conversion Progress Styles */
- .conversion-progress {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- }
-
- .progress-text {
- font-size: 0.75rem;
- }
-
- .progress-percent {
- font-family: 'Consolas', 'Monaco', monospace;
- font-size: 0.7rem;
- color: rgba(255, 255, 255, 0.7);
- }
`
];
@@ -806,51 +708,15 @@ export class WccRecordingPanel extends DeesElement {
-
-
@@ -951,7 +817,7 @@ export class WccRecordingPanel extends DeesElement {
try {
let blobToDownload: Blob;
- // Step 1: Handle trimming if needed
+ // Handle trimming if needed
const needsTrim = this.trimStart > 0.1 || this.trimEnd < this.videoDuration - 0.1;
if (needsTrim) {
@@ -965,43 +831,9 @@ export class WccRecordingPanel extends DeesElement {
blobToDownload = recordedBlob;
}
- // Step 2: Convert to MP4 if selected
- if (this.outputFormat === 'mp4') {
- this.isConverting = true;
- this.isExporting = false; // Switch to conversion state
-
- try {
- const ffmpegService = getFFmpegService();
- blobToDownload = await ffmpegService.convertToMp4({
- inputBlob: blobToDownload,
- outputFormat: 'mp4',
- onProgress: (progress) => {
- this.conversionProgress = progress;
- }
- });
- } catch (conversionError) {
- console.error('MP4 conversion failed:', conversionError);
- // Offer WebM fallback
- const useFallback = confirm(
- 'MP4 conversion failed. Would you like to download as WebM instead?'
- );
- if (!useFallback) {
- this.isConverting = false;
- this.conversionProgress = null;
- return;
- }
- // Continue with original WebM blob (already set)
- this.outputFormat = 'webm';
- }
-
- this.isConverting = false;
- this.conversionProgress = null;
- }
-
- // Step 3: Trigger download
+ // Trigger download
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
- const extension = this.outputFormat;
- const filename = `wcctools-recording-${timestamp}.${extension}`;
+ const filename = `wcctools-recording-${timestamp}.webm`;
const url = URL.createObjectURL(blobToDownload);
const a = document.createElement('a');
@@ -1016,8 +848,6 @@ export class WccRecordingPanel extends DeesElement {
} catch (error) {
console.error('Error exporting video:', error);
this.isExporting = false;
- this.isConverting = false;
- this.conversionProgress = null;
}
}
diff --git a/ts_web/elements/wcc-sidebar.ts b/ts_web/elements/wcc-sidebar.ts
index bb5a83f..e2c169c 100644
--- a/ts_web/elements/wcc-sidebar.ts
+++ b/ts_web/elements/wcc-sidebar.ts
@@ -333,5 +333,9 @@ export class WccSidebar extends DeesElement {
);
this.dashboardRef.buildUrl();
+
+ // Force re-render to update demo child selection indicator
+ // (needed when switching between demos of the same element)
+ this.requestUpdate();
}
}
diff --git a/ts_web/index.ts b/ts_web/index.ts
index bbd0189..384379d 100644
--- a/ts_web/index.ts
+++ b/ts_web/index.ts
@@ -4,7 +4,6 @@ import type { TTemplateFactory } from './elements/wcctools.helpers.js';
// Export recording components and service
export { RecorderService, type IRecorderEvents, type IRecordingOptions } from './services/recorder.service.js';
-export { FFmpegService, getFFmpegService, type IConversionProgress, type IConversionOptions } from './services/ffmpeg.service.js';
export { WccRecordButton } from './elements/wcc-record-button.js';
export { WccRecordingPanel } from './elements/wcc-recording-panel.js';
diff --git a/ts_web/services/ffmpeg.service.ts b/ts_web/services/ffmpeg.service.ts
deleted file mode 100644
index b3aeb9e..0000000
--- a/ts_web/services/ffmpeg.service.ts
+++ /dev/null
@@ -1,314 +0,0 @@
-/**
- * FFmpegService - Handles client-side video format conversion using FFmpeg.wasm
- * Uses a custom worker implementation to bypass COEP/CORS issues with the standard library
- */
-
-export interface IConversionProgress {
- stage: 'loading' | 'converting' | 'finalizing';
- progress: number; // 0-100
- message: string;
-}
-
-export interface IConversionOptions {
- inputBlob: Blob;
- outputFormat: 'mp4' | 'webm';
- onProgress?: (progress: IConversionProgress) => void;
-}
-
-// Message types for worker communication
-type WorkerMessageType = 'LOAD' | 'EXEC' | 'WRITE_FILE' | 'READ_FILE' | 'DELETE_FILE' | 'LOG' | 'PROGRESS' | 'ERROR';
-
-interface WorkerMessage {
- id: number;
- type: WorkerMessageType;
- data?: any;
-}
-
-export class FFmpegService {
- private worker: Worker | null = null;
- private core: any = null;
- private loadPromise: Promise | null = null;
- private messageId = 0;
- private pendingMessages: Map = new Map();
- private onLog?: (message: string) => void;
- private onProgress?: (progress: number) => void;
-
- /**
- * Lazy load FFmpeg.wasm from CDN using custom worker
- */
- async ensureLoaded(onProgress?: (progress: IConversionProgress) => void): Promise {
- if (this.worker && this.core) return;
-
- if (this.loadPromise) {
- await this.loadPromise;
- return;
- }
-
- this.loadPromise = this.loadFFmpeg(onProgress);
- await this.loadPromise;
- }
-
- private async loadFFmpeg(onProgress?: (progress: IConversionProgress) => void): Promise {
- console.log('[FFmpeg] Starting FFmpeg load with custom worker...');
-
- onProgress?.({
- stage: 'loading',
- progress: 0,
- message: 'Loading FFmpeg library...'
- });
-
- // Import toBlobURL utility
- const { toBlobURL } = await import('@ffmpeg/util');
-
- // Use jsdelivr CDN (has proper CORS/CORP headers)
- const coreBaseURL = 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/umd';
-
- onProgress?.({
- stage: 'loading',
- progress: 10,
- message: 'Downloading FFmpeg core (~31MB)...'
- });
-
- console.log('[FFmpeg] Creating blob URLs...');
- const coreURL = await toBlobURL(`${coreBaseURL}/ffmpeg-core.js`, 'text/javascript');
- const wasmURL = await toBlobURL(`${coreBaseURL}/ffmpeg-core.wasm`, 'application/wasm');
- console.log('[FFmpeg] Blob URLs created');
-
- onProgress?.({
- stage: 'loading',
- progress: 50,
- message: 'Initializing FFmpeg...'
- });
-
- // Create custom worker code that bypasses @ffmpeg/ffmpeg wrapper issues
- const workerCode = `
- let ffmpeg = null;
-
- self.onmessage = async (e) => {
- const { id, type, data } = e.data;
-
- try {
- switch (type) {
- case 'LOAD': {
- const { coreURL, wasmURL } = data;
- console.log('[FFmpeg Worker] Loading core...');
- importScripts(coreURL);
-
- console.log('[FFmpeg Worker] Initializing with WASM...');
- ffmpeg = await self.createFFmpegCore({
- mainScriptUrlOrBlob: coreURL + '#' + btoa(JSON.stringify({ wasmURL }))
- });
-
- // Set up logging
- ffmpeg.setLogger((log) => {
- self.postMessage({ type: 'LOG', data: log });
- });
-
- // Set up progress
- ffmpeg.setProgress((progress) => {
- self.postMessage({ type: 'PROGRESS', data: progress });
- });
-
- console.log('[FFmpeg Worker] Core initialized successfully');
- self.postMessage({ id, type: 'LOAD', data: true });
- break;
- }
-
- case 'EXEC': {
- const { args, timeout = -1 } = data;
- ffmpeg.setTimeout(timeout);
- ffmpeg.exec(...args);
- const ret = ffmpeg.ret;
- ffmpeg.reset();
- self.postMessage({ id, type: 'EXEC', data: ret });
- break;
- }
-
- case 'WRITE_FILE': {
- const { path, fileData } = data;
- ffmpeg.FS.writeFile(path, fileData);
- self.postMessage({ id, type: 'WRITE_FILE', data: true });
- break;
- }
-
- case 'READ_FILE': {
- const { path } = data;
- const fileData = ffmpeg.FS.readFile(path);
- self.postMessage({ id, type: 'READ_FILE', data: fileData }, [fileData.buffer]);
- break;
- }
-
- case 'DELETE_FILE': {
- const { path } = data;
- ffmpeg.FS.unlink(path);
- self.postMessage({ id, type: 'DELETE_FILE', data: true });
- break;
- }
-
- default:
- throw new Error('Unknown message type: ' + type);
- }
- } catch (err) {
- console.error('[FFmpeg Worker] Error:', err);
- self.postMessage({ id, type: 'ERROR', data: err.message });
- }
- };
- `;
-
- // Create worker from blob
- const workerBlob = new Blob([workerCode], { type: 'text/javascript' });
- const workerURL = URL.createObjectURL(workerBlob);
- this.worker = new Worker(workerURL);
-
- // Set up message handler
- this.worker.onmessage = (e: MessageEvent) => {
- const { id, type, data } = e.data;
-
- if (type === 'LOG') {
- console.log('[FFmpeg Log]', data);
- this.onLog?.(data.message || data);
- return;
- }
-
- if (type === 'PROGRESS') {
- this.onProgress?.(data);
- return;
- }
-
- const pending = this.pendingMessages.get(id);
- if (pending) {
- this.pendingMessages.delete(id);
- if (type === 'ERROR') {
- pending.reject(new Error(data));
- } else {
- pending.resolve(data);
- }
- }
- };
-
- this.worker.onerror = (e) => {
- console.error('[FFmpeg] Worker error:', e);
- };
-
- // Initialize FFmpeg in worker
- console.log('[FFmpeg] Initializing worker...');
- await this.sendMessage('LOAD', { coreURL, wasmURL });
- this.core = true; // Mark as loaded
- console.log('[FFmpeg] Worker initialized successfully');
-
- onProgress?.({
- stage: 'loading',
- progress: 100,
- message: 'FFmpeg loaded successfully'
- });
- }
-
- private sendMessage(type: WorkerMessageType, data?: any): Promise {
- return new Promise((resolve, reject) => {
- const id = ++this.messageId;
- this.pendingMessages.set(id, { resolve, reject });
- this.worker!.postMessage({ id, type, data });
- });
- }
-
- /**
- * Convert WebM blob to MP4
- */
- async convertToMp4(options: IConversionOptions): Promise {
- const { inputBlob, onProgress } = options;
-
- // Check file size limit (2GB)
- const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024;
- if (inputBlob.size > MAX_FILE_SIZE) {
- throw new Error('File size exceeds 2GB limit for conversion');
- }
-
- // Set up progress callback
- this.onProgress = (progress: number) => {
- onProgress?.({
- stage: 'converting',
- progress: Math.round(progress * 100),
- message: `Converting video... ${Math.round(progress * 100)}%`
- });
- };
-
- await this.ensureLoaded(onProgress);
-
- onProgress?.({
- stage: 'converting',
- progress: 0,
- message: 'Preparing video for conversion...'
- });
-
- // Read input blob as Uint8Array
- const inputData = new Uint8Array(await inputBlob.arrayBuffer());
-
- // Write input file to virtual filesystem
- await this.sendMessage('WRITE_FILE', { path: 'input.webm', fileData: inputData });
-
- // Execute conversion with optimized settings for web playback
- await this.sendMessage('EXEC', {
- args: [
- '-i', 'input.webm',
- '-c:v', 'libx264',
- '-preset', 'fast',
- '-crf', '23',
- '-c:a', 'aac',
- '-b:a', '128k',
- '-movflags', '+faststart',
- 'output.mp4'
- ]
- });
-
- onProgress?.({
- stage: 'finalizing',
- progress: 95,
- message: 'Finalizing video...'
- });
-
- // Read output file
- const outputData: Uint8Array = await this.sendMessage('READ_FILE', { path: 'output.mp4' });
-
- // Clean up virtual filesystem
- await this.sendMessage('DELETE_FILE', { path: 'input.webm' });
- await this.sendMessage('DELETE_FILE', { path: 'output.mp4' });
-
- onProgress?.({
- stage: 'finalizing',
- progress: 100,
- message: 'Conversion complete!'
- });
-
- return new Blob([new Uint8Array(outputData)], { type: 'video/mp4' });
- }
-
- /**
- * Check if FFmpeg is currently loaded
- */
- get isLoaded(): boolean {
- return this.worker !== null && this.core !== null;
- }
-
- /**
- * Terminate FFmpeg worker to free resources
- */
- async terminate(): Promise {
- if (this.worker) {
- this.worker.terminate();
- this.worker = null;
- this.core = null;
- this.loadPromise = null;
- this.pendingMessages.clear();
- }
- }
-}
-
-// Singleton instance for caching
-let ffmpegServiceInstance: FFmpegService | null = null;
-
-export function getFFmpegService(): FFmpegService {
- if (!ffmpegServiceInstance) {
- ffmpegServiceInstance = new FFmpegService();
- }
- return ffmpegServiceInstance;
-}
diff --git a/ts_web/services/recorder.service.ts b/ts_web/services/recorder.service.ts
index be552ab..dc919e9 100644
--- a/ts_web/services/recorder.service.ts
+++ b/ts_web/services/recorder.service.ts
@@ -235,9 +235,11 @@ export class RecorderService {
}
}
- private handleRecordingComplete(): void {
+ private async handleRecordingComplete(): Promise {
// Create blob from recorded chunks
- this._recordedBlob = new Blob(this.recordedChunks, { type: 'video/webm' });
+ const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
+
+ this._recordedBlob = blob;
// Stop all tracks
if (this.currentStream) {