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.
This commit is contained in:
2026-04-12 23:17:21 +00:00
parent d9330a5fa1
commit 3eeb9dc46f
10 changed files with 160 additions and 21 deletions

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,6 +142,8 @@ export class RecorderService {
async startRecording(options: IRecordingOptions): Promise<void> {
try {
this._outputFormat = options.outputFormat || 'webm';
// Stop audio monitoring before recording
this.stopAudioMonitoring();
@@ -182,7 +194,15 @@ 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';
@@ -198,14 +218,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 +248,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 +261,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,