Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions media/videoEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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(
Expand All @@ -403,6 +414,14 @@
);
}

if (msg.type === "transcode_pending") {
const bar = document.getElementById("transcodeBar");
if (bar) {
bar.classList.remove("warn", "done");
bar.innerHTML = `<div class="transcode-spinner"></div><span>${msg.message}</span>`;
}
}

if (msg.type === "audio_ready") {
const currentPos = video.currentTime;
const wasPlaying = !video.paused;
Expand Down Expand Up @@ -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 ─────────────────────────────────────────────
Expand Down
38 changes: 34 additions & 4 deletions src/VideoEditorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -219,7 +249,7 @@ export class VideoEditorProvider implements vscode.CustomReadonlyEditorProvider
const topBar = willExtractAudio
? `<div class="transcode-bar" id="transcodeBar"><div class="transcode-spinner"></div><span>Extracting audio for playback...</span></div>`
: showNoFfmpegWarn
? `<div class="transcode-bar warn" id="transcodeBar"><span>⚠ Install ffmpeg for audio support: <code>brew install ffmpeg</code></span></div>`
? `<div class="transcode-bar warn" id="transcodeBar"><span>⚠ Install ffmpeg for codec fallback and audio support.</span></div>`
: '';

return /* html */`
Expand Down