From 83597ee1e4fb251e911948feea75b4969435b6ed Mon Sep 17 00:00:00 2001 From: Jakub Pawlinski <10334982+quasarea@users.noreply.github.com> Date: Mon, 11 May 2026 19:19:14 +0100 Subject: [PATCH] Add WebM codec fallback support --- media/videoEditor.js | 34 +++++++++++++++++++++++++++++++--- src/VideoEditorProvider.ts | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/media/videoEditor.js b/media/videoEditor.js index 4c9899f..4e76101 100644 --- a/media/videoEditor.js +++ b/media/videoEditor.js @@ -31,6 +31,8 @@ let audioEl = null; let videoLoaded = false; // guard — outside handler so it persists across messages + let requestedCodecFallback = false; + let currentObjectUrl = null; video.volume = 1.0; video.muted = true; @@ -72,6 +74,12 @@ }); video.addEventListener("error", () => { + if (!requestedCodecFallback) { + requestedCodecFallback = true; + vscode.postMessage({ type: "native_playback_failed" }); + return; + } + vscode.postMessage({ type: "error", message: "Failed to load video or unsupported codec.", @@ -376,14 +384,17 @@ const msg = event.data; if (msg.type === "video_src") { - // Guard — only load once, ignore duplicate messages - if (videoLoaded) return; + if (videoLoaded && !msg.replace) return; videoLoaded = true; fetch(msg.src) .then((r) => r.blob()) .then((blob) => { - video.src = URL.createObjectURL(blob); + if (currentObjectUrl) { + URL.revokeObjectURL(currentObjectUrl); + } + currentObjectUrl = URL.createObjectURL(blob); + video.src = currentObjectUrl; video.muted = true; video.load(); video.addEventListener( @@ -403,6 +414,14 @@ ); } + if (msg.type === "transcode_pending") { + const bar = document.getElementById("transcodeBar"); + if (bar) { + bar.classList.remove("warn", "done"); + bar.innerHTML = `
${msg.message}`; + } + } + if (msg.type === "audio_ready") { const currentPos = video.currentTime; const wasPlaying = !video.paused; @@ -443,6 +462,15 @@ setTimeout(() => bar.remove(), 5000); } } + + if (msg.type === "transcode_failed") { + const bar = document.getElementById("transcodeBar"); + if (bar) { + bar.textContent = "⚠ Codec fallback failed. Try opening in an external player."; + bar.classList.add("warn"); + setTimeout(() => bar.remove(), 5000); + } + } }); // ── SVG Icons ───────────────────────────────────────────── diff --git a/src/VideoEditorProvider.ts b/src/VideoEditorProvider.ts index 4dcd8f1..f3a11e1 100644 --- a/src/VideoEditorProvider.ts +++ b/src/VideoEditorProvider.ts @@ -16,8 +16,9 @@ const FFMPEG_PATHS = [ 'C:\\Program Files\\ffmpeg\\bin\\ffmpeg.exe', ]; -const NEEDS_TRANSCODE = new Set(['.webm', '.mkv', '.avi', '.ogv']); +const NEEDS_TRANSCODE = new Set(['.mkv', '.avi', '.ogv']); const NEEDS_AUDIO_EXTRACT = new Set(['.mp4', '.mov', '.m4v']); +const SUPPORTS_FALLBACK_TRANSCODE = new Set(['.webm']); export class VideoEditorProvider implements vscode.CustomReadonlyEditorProvider { public static readonly viewType = 'videoPreview.viewer'; @@ -58,14 +59,17 @@ export class VideoEditorProvider implements vscode.CustomReadonlyEditorProvider const needsTranscode = NEEDS_TRANSCODE.has(ext); const needsAudio = NEEDS_AUDIO_EXTRACT.has(ext); - const ffmpegBin = (needsTranscode || needsAudio) ? await this.findFfmpeg() : null; + const supportsFallbackTranscode = SUPPORTS_FALLBACK_TRANSCODE.has(ext); + const ffmpegBin = (needsTranscode || needsAudio || supportsFallbackTranscode) ? await this.findFfmpeg() : null; const willTranscode = needsTranscode && ffmpegBin !== null; const willExtractAudio = needsAudio && ffmpegBin !== null; + const canFallbackTranscode = supportsFallbackTranscode && ffmpegBin !== null; + let transcodeRequested = false; webview.html = this.getHtml({ webview, scriptUri, styleUri, filename, nonce, port, willExtractAudio: willExtractAudio || willTranscode, - showNoFfmpegWarn: (needsTranscode || needsAudio) && !ffmpegBin + showNoFfmpegWarn: (needsTranscode || needsAudio || supportsFallbackTranscode) && !ffmpegBin }); let audioToken: string | null = null; @@ -79,6 +83,31 @@ export class VideoEditorProvider implements vscode.CustomReadonlyEditorProvider webview.postMessage({ type: 'video_src', src: videoUrl }); } break; + case 'native_playback_failed': + if (canFallbackTranscode && ffmpegBin && !transcodeRequested) { + transcodeRequested = true; + webview.postMessage({ type: 'transcode_pending', message: 'Codec unsupported in VS Code. Transcoding with ffmpeg...' }); + + this.transcodeToMp4(ffmpegBin, document.uri.fsPath) + .then((mp4Path) => { + const mp4Token = this.server.register(mp4Path); + webview.postMessage({ type: 'video_src', src: this.server.url(mp4Token), replace: true }); + return this.extractAudio(ffmpegBin, document.uri.fsPath); + }) + .then((audioPath) => { + audioToken = this.server.register(audioPath); + webview.postMessage({ type: 'audio_ready', src: this.server.url(audioToken) }); + }) + .catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + webview.postMessage({ type: 'transcode_failed', message }); + vscode.window.showErrorMessage(`Video Preview: Failed to transcode unsupported codec. ${message}`); + }); + break; + } + + vscode.window.showErrorMessage('Video Preview: Failed to load video or unsupported codec. Install ffmpeg for codec fallback support.'); + break; case 'error': vscode.window.showErrorMessage(`Video Preview: ${msg.message}`); break; @@ -103,6 +132,7 @@ export class VideoEditorProvider implements vscode.CustomReadonlyEditorProvider }); if (willTranscode && ffmpegBin) { + transcodeRequested = true; this.transcodeToMp4(ffmpegBin, document.uri.fsPath) .then((mp4Path) => { const mp4Token = this.server.register(mp4Path); @@ -219,7 +249,7 @@ export class VideoEditorProvider implements vscode.CustomReadonlyEditorProvider const topBar = willExtractAudio ? `
Extracting audio for playback...
` : showNoFfmpegWarn - ? `
⚠ Install ffmpeg for audio support: brew install ffmpeg
` + ? `
⚠ Install ffmpeg for codec fallback and audio support.
` : ''; return /* html */`