Skip to content
Merged
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
2 changes: 1 addition & 1 deletion electrobun.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default {
app: {
name: "yafw",
identifier: "yafw.electrobun.dev",
version: "0.7",
version: "0.7.1",
},
build: {
// Vite builds to dist/, Electrobun copies from there into the bundle
Expand Down
51 changes: 39 additions & 12 deletions src/bun/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BrowserWindow, BrowserView, Updater, Utils } from "electrobun/bun";
import { BrowserWindow, BrowserView, Updater, Utils, Screen } from "electrobun/bun";
import { type AppRPCType } from "../shared/types";
import { basename, dirname, extname, join } from "path";
import { homedir } from "os";
Expand All @@ -18,8 +18,9 @@ const previewServer = Bun.serve({
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, HEAD",
"Access-Control-Allow-Headers": "Range",
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
"Access-Control-Allow-Headers": "Range, Content-Type",
"Access-Control-Max-Age": "86400",
},
});
}
Expand All @@ -34,7 +35,13 @@ const previewServer = Bun.serve({
}

return new Response(file, {
headers: { "Access-Control-Allow-Origin": "*" },
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
"Access-Control-Allow-Headers": "Range, Content-Type",
"Access-Control-Expose-Headers": "Content-Range, Content-Length, Accept-Ranges",
"Accept-Ranges": "bytes",
},
});
},
});
Expand Down Expand Up @@ -188,7 +195,7 @@ const rpc = BrowserView.defineRPC<AppRPCType>({
"ffprobe",
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=r_frame_rate,width,height,bit_rate",
"-show_entries", "stream=r_frame_rate,width,height,bit_rate,duration:format=duration",
"-of", "json",
inputPath,
], { stdout: "pipe", stderr: "pipe" });
Expand All @@ -198,6 +205,7 @@ const rpc = BrowserView.defineRPC<AppRPCType>({

const data = JSON.parse(output);
const stream = data?.streams?.[0] ?? {};
const format = data?.format ?? {};

// Parse r_frame_rate fraction (e.g. "30000/1001" → 29.97)
let fps = 30;
Expand All @@ -210,12 +218,13 @@ const rpc = BrowserView.defineRPC<AppRPCType>({
const height = stream.height ?? 1080;
// bit_rate is in bps, convert to kbps
const bitrate = stream.bit_rate ? Math.round(parseInt(stream.bit_rate) / 1000) : 0;
const duration = parseFloat(stream.duration || format.duration || "0");

console.log(`[YAFW] Probe: ${width}x${height} @ ${fps}fps, ${bitrate}kbps`);
return { fps, width, height, bitrate };
console.log(`[YAFW] Probe: ${width}x${height} @ ${fps}fps, ${bitrate}kbps, duration: ${duration}s`);
return { fps, width, height, bitrate, duration };
} catch (err) {
console.error("[YAFW] probeVideo error:", err);
return { fps: 30, width: 1920, height: 1080, bitrate: 0 };
return { fps: 30, width: 1920, height: 1080, bitrate: 0, duration: 0 };
}
},

Expand Down Expand Up @@ -293,15 +302,33 @@ async function getMainViewUrl(): Promise<string> {
const url = await getMainViewUrl();
console.log("[YAFW] Loading URL:", url);

const windowWidth = 1280;
const windowHeight = 1000;
let x = 100;
let y = 100;

try {
const primary = Screen.getPrimaryDisplay();
const workArea = primary.workArea || primary.bounds;
if (workArea && workArea.width && workArea.height) {
x = Math.round(workArea.x + (workArea.width - windowWidth) / 2);
y = Math.round(workArea.y + (workArea.height - windowHeight) / 2);
}
} catch (e) {
console.error("[YAFW] Failed to calculate centered window position:", e);
x = 510;
y = 200;
}

const mainWindow = new BrowserWindow({
title: "YAFW",
url,
rpc,
frame: {
width: 1280,
height: 1000,
x: 510,
y: 200,
width: windowWidth,
height: windowHeight,
x,
y,
},
// hiddenInset gives a clean look on macOS, but on Windows it removes
// the minimize/maximize/close buttons — so only use it on macOS.
Expand Down
2 changes: 1 addition & 1 deletion src/mainview/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const Header = ({
videoSrc,
onChangeVideo,
}: HeaderProps) => (
<header className={`${isStandalone && isMac ? "electrobun-webkit-app-region-drag pl-24" : ""} bg-mocha-crust/80 backdrop-blur border-b border-mocha-surface0 px-6 py-4 flex items-center justify-between z-20`}>
<header className={`${isStandalone && isMac ? "electrobun-webkit-app-region-drag pl-20" : ""} bg-mocha-crust/80 backdrop-blur border-b border-mocha-surface0 px-6 py-4 flex items-center justify-between z-20`}>
<div className="flex items-center gap-3 select-none">
<img src={logoUrl} alt="YAFW Logo" className="w-8 h-8 object-contain rounded-md" />
<div>
Expand Down
78 changes: 53 additions & 25 deletions src/mainview/hooks/useElectrobun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,64 @@ import { useEffect, useRef, useState } from "react";
import { isStandalone } from "../env";
import { type AppRPCType } from "../../shared/types";

let sharedElectroview: any = null;
let sharedSupportedEncoders: string[] = [];
let isInitStarted = false;
const listeners = new Set<(ev: any, encoders: string[]) => void>();

export const useElectrobun = () => {
const [supportedEncoders, setSupportedEncoders] = useState<string[]>([]);
const [electroview, setElectroview] = useState<any>(null);
const electroviewRef = useRef<any>(null);
const [supportedEncoders, setSupportedEncoders] = useState<string[]>(sharedSupportedEncoders);
const [electroview, setElectroview] = useState<any>(sharedElectroview);

useEffect(() => {
if (!isStandalone) return;
import("electrobun/view")
.then(({ Electroview }: any) => {
const rpc = Electroview.defineRPC<AppRPCType>({
maxRequestTime: 600_000, // 10 min — native dialogs block
handlers: { requests: {}, messages: {} },
});
const ev = new Electroview({ rpc });
electroviewRef.current = ev;
setElectroview(ev);
console.log("[YAFW] Electrobun RPC initialized");

// Detect hardware accelerators
ev.rpc.request.detectHardwareAccelerators({})
.then((res: any) => {
setSupportedEncoders(res.supportedEncoders || []);
})
.catch((err: any) => {
console.error("[YAFW] Failed to detect hardware accelerators:", err);

if (sharedElectroview) {
setElectroview(sharedElectroview);
setSupportedEncoders(sharedSupportedEncoders);
return;
}

const handleInit = (ev: any, encoders: string[]) => {
setElectroview(ev);
setSupportedEncoders(encoders);
};
listeners.add(handleInit);

if (!isInitStarted) {
isInitStarted = true;
import("electrobun/view")
.then(({ Electroview }: any) => {
const rpc = Electroview.defineRPC<AppRPCType>({
maxRequestTime: 600_000, // 10 min — native dialogs block
handlers: { requests: {}, messages: {} },
});
})
.catch((err: unknown) => {
console.warn("[YAFW] Electrobun unavailable:", err);
});
const ev = new Electroview({ rpc });
sharedElectroview = ev;
console.log("[YAFW] Electrobun RPC initialized");

// Detect hardware accelerators
ev.rpc.request.detectHardwareAccelerators({})
.then((res: any) => {
sharedSupportedEncoders = res.supportedEncoders || [];
listeners.forEach((l) => l(sharedElectroview, sharedSupportedEncoders));
listeners.clear();
})
.catch((err: any) => {
console.error("[YAFW] Failed to detect hardware accelerators:", err);
listeners.forEach((l) => l(sharedElectroview, []));
listeners.clear();
});
})
.catch((err: unknown) => {
console.warn("[YAFW] Electrobun unavailable:", err);
listeners.clear();
});
}

return () => {
listeners.delete(handleInit);
};
}, []);

return {
Expand Down
28 changes: 24 additions & 4 deletions src/mainview/hooks/useVideoEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,19 +192,39 @@ export const useVideoEditor = () => {
setOutputFormat(ext);
setExportError(null);
setExportSuccess(null);
detectVideoMetadata(previewUrl);

const probed = await probeWithWasm(previewUrl, result.path);
if (probed.fps !== null) {
console.log("[YAFW] Calling native probeVideo RPC...");
const probed = await electroview.rpc.request.probeVideo({ inputPath: result.path });
console.log("[YAFW] native probeVideo result:", probed);

// Initialize metadata states directly from native probe values
const duration = probed.duration || 0;
setVideoDuration(duration);
setStart(0);
setEnd(1000);
setPosition(0);

const w = probed.width || 1920;
const h = probed.height || 1080;
setOriginalWidth(w);
setOriginalHeight(h);
setOutputWidth(w);
setOutputHeight(h);

if (probed.fps) {
setOriginalFps(probed.fps);
setOutputFps(probed.fps);
} else {
setOriginalFps(30);
setOutputFps(30);
}
if (probed.bitrate !== null) {

if (probed.bitrate) {
setOriginalBitrate(probed.bitrate);
setBitrate(probed.bitrate);
} else {
setOriginalBitrate(1000);
setBitrate(1000);
}
} catch (err) {
console.error("[YAFW] handleNativeBrowse error:", err);
Expand Down
1 change: 1 addition & 0 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type AppRPCType = {
width: number;
height: number;
bitrate: number; // kbps
duration: number; // seconds
};
};
/** Probes FFmpeg to check which hardware encoders are supported */
Expand Down
Loading