Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 73a975e9e9 | |||
| d178d6cb73 | |||
| 3eeb9dc46f | |||
| d9330a5fa1 | |||
| 443618d1ac | |||
| ac087b9f3f | |||
| 977d8ab5e0 | |||
| 02e1f536d5 | |||
| a7f5341baa | |||
| 3499652622 | |||
| ee8b5dc3ff |
@@ -36,6 +36,16 @@
|
||||
"@ship.zone/szci": {
|
||||
"npmGlobalTools": []
|
||||
},
|
||||
"@git.zone/tsbundle": {
|
||||
"bundles": [
|
||||
{
|
||||
"from": "./html/index.ts",
|
||||
"to": "./dist_bundle/bundle.js",
|
||||
"outputMode": "bundle",
|
||||
"bundler": "esbuild"
|
||||
}
|
||||
]
|
||||
},
|
||||
"@git.zone/tswatch": {
|
||||
"preset": "element"
|
||||
}
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/npmextra.json"],
|
||||
"fileMatch": ["/.smartconfig.json"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
30
changelog.md
30
changelog.md
@@ -1,5 +1,35 @@
|
||||
# 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
|
||||
|
||||
- rename npmextra.json to .smartconfig.json and add tsbundle bundle configuration
|
||||
- update build script to use the new tsbundle invocation without the removed element subcommand
|
||||
- adjust tsconfig and dashboard property defaults for newer TypeScript compiler behavior
|
||||
- refresh package dependencies and include the new config file in published package contents
|
||||
|
||||
## 2026-01-27 - 3.8.0 - feat(sidebar)
|
||||
rename demoGroup to demoGroups, add multi-group support, search by group name, and context menu group navigation
|
||||
|
||||
|
||||
2
license
2
license
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2020 Lossless GmbH (hello@lossless.com)
|
||||
Copyright (c) 2020 Task Venture Capital GmbH (hello@task.vc)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
37
package.json
37
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@design.estate/dees-wcctools",
|
||||
"version": "3.8.0",
|
||||
"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": {
|
||||
@@ -10,27 +10,28 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "(npm run build)",
|
||||
"build": "(tsbuild tsfolders --allowimplicitany && tsbundle element)",
|
||||
"build": "(tsbuild tsfolders --allowimplicitany && tsbundle)",
|
||||
"watch": "tswatch",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"author": "Lossless GmbH",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@design.estate/dees-domtools": "^2.3.7",
|
||||
"@design.estate/dees-element": "^2.1.5",
|
||||
"@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.3.0",
|
||||
"@git.zone/tsbuild": "^4.1.2",
|
||||
"@git.zone/tsbundle": "^2.8.3",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.1.8",
|
||||
"@git.zone/tswatch": "^3.0.1",
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@types/node": "^25.0.10"
|
||||
"@api.global/typedserver": "^8.4.6",
|
||||
"@git.zone/tsbuild": "^4.4.0",
|
||||
"@git.zone/tsbundle": "^2.10.0",
|
||||
"@git.zone/tsrun": "^2.0.2",
|
||||
"@git.zone/tstest": "^3.6.3",
|
||||
"@git.zone/tswatch": "^3.3.2",
|
||||
"@push.rocks/projectinfo": "^5.1.0",
|
||||
"@types/node": "^25.6.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -41,7 +42,7 @@
|
||||
"dist_ts_web/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
".smartconfig.json",
|
||||
"readme.md"
|
||||
],
|
||||
"browserslist": [
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3451
pnpm-lock.yaml
generated
3451
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,26 @@
|
||||
# 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
|
||||
TypeScript 6.0.2 (shipped with tsbuild 4.4.0) changes `strict` to `true` by default. The project explicitly sets `"strict": false` in tsconfig.json to preserve pre-TS6 behavior. Also added `"types": ["node"]` since TS6 changed `@types/*` auto-discovery.
|
||||
|
||||
### Config Migration: npmextra.json → .smartconfig.json
|
||||
Build tools (tsbundle 2.10.0, tswatch 3.3.2) now use `@push.rocks/smartconfig` which reads `.smartconfig.json` (with leading dot). The old `npmextra.json` was renamed.
|
||||
|
||||
### tsbundle Configuration
|
||||
The `tsbundle element` subcommand no longer exists. Instead, bundle configuration lives in `.smartconfig.json` under `"@git.zone/tsbundle"` with a `bundles` array. Entry point is `./html/index.ts` → `./dist_bundle/bundle.js`.
|
||||
|
||||
---
|
||||
|
||||
## Section-based Configuration API (2025-12-27)
|
||||
|
||||
### Overview
|
||||
|
||||
21
readme.md
21
readme.md
@@ -293,6 +293,25 @@ export class MyButton extends DeesElement {
|
||||
|
||||
Each demo appears as a numbered item in an expandable folder in the sidebar.
|
||||
|
||||
### 🗂️ Demo Groups
|
||||
|
||||
Organize elements into groups within a section for better discoverability:
|
||||
|
||||
```typescript
|
||||
@customElement('my-input')
|
||||
export class MyInput extends DeesElement {
|
||||
// Single group
|
||||
public static demoGroups = 'Form Controls';
|
||||
|
||||
// Or multiple groups — element appears in each
|
||||
public static demoGroups = ['Form Controls', 'Inputs'];
|
||||
|
||||
public static demo = () => html`<my-input></my-input>`;
|
||||
}
|
||||
```
|
||||
|
||||
Groups appear as collapsible headers in the sidebar, sorted alphabetically. Searching matches group names too — searching "Form Controls" shows all elements in that group.
|
||||
|
||||
### ⏳ Async Demos
|
||||
|
||||
Return a `Promise` from `demo` for async setup:
|
||||
@@ -462,7 +481,7 @@ my-component-library/
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-wcctools',
|
||||
version: '3.8.0',
|
||||
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.'
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ export const getSectionItems = (section: IWccSection): Array<[string, any]> => {
|
||||
|
||||
// Apply filter if provided
|
||||
if (section.filter) {
|
||||
entries = entries.filter(([name, item]) => section.filter(name, item));
|
||||
const filterFn = section.filter;
|
||||
entries = entries.filter(([name, item]) => filterFn(name, item));
|
||||
}
|
||||
|
||||
// Apply sort if provided
|
||||
@@ -43,13 +44,13 @@ export class WccDashboard extends DeesElement {
|
||||
accessor selectedSection: IWccSection | null = null;
|
||||
|
||||
@property()
|
||||
accessor selectedType: TElementType;
|
||||
accessor selectedType: TElementType = 'element';
|
||||
|
||||
@property()
|
||||
accessor selectedItemName: string;
|
||||
accessor selectedItemName: string = '';
|
||||
|
||||
@property()
|
||||
accessor selectedItem: TTemplateFactory | DeesElement;
|
||||
accessor selectedItem: TTemplateFactory | DeesElement | null = null;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor selectedDemoIndex: number = 0;
|
||||
@@ -77,7 +78,7 @@ export class WccDashboard extends DeesElement {
|
||||
}
|
||||
|
||||
@property()
|
||||
accessor warning: string = null;
|
||||
accessor warning: string | null = null;
|
||||
|
||||
private frameScrollY: number = 0;
|
||||
private sidebarScrollY: number = 0;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
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"
|
||||
}
|
||||
@@ -4,7 +4,9 @@
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true
|
||||
"verbatimModuleSyntax": true,
|
||||
"strict": false,
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
|
||||
Reference in New Issue
Block a user