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
Binary file added export_result.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,23 @@ const nextConfig: NextConfig = {
config.resolve.fallback = { fs: false };
return config;
},
headers: async () => {
return [
{
source: "/(.*)",
headers: [
{
key: "Cross-Origin-Opener-Policy",
value: "same-origin",
},
{
key: "Cross-Origin-Embedder-Policy",
value: "require-corp",
},
],
},
];
},
};

export default nextConfig;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@types/node": "^25",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^6.0.2",
"eslint": "^9",
"eslint-config-next": "^15.1.8",
"eslint-plugin-jsx-a11y": "^6.10.2",
Expand Down
34 changes: 34 additions & 0 deletions print_error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});

try {
const page = await browser.newPage();
console.log("Navigating to http://localhost:3000...");
await page.goto('http://localhost:3000', { waitUntil: 'networkidle0' });

// Upload the video
console.log("Uploading test.mp4...");
const path = require('path');
const filePath = path.resolve(__dirname, 'test.mp4');
const fileInput = await page.$('input[type="file"]');
await fileInput.uploadFile(filePath);

console.log("Waiting 6 seconds...");
await new Promise(r => setTimeout(r, 6000));

const text = await page.evaluate(() => document.body.innerText);
console.log("------------------- PAGE TEXT -------------------");
console.log(text);
console.log("-------------------------------------------------");

} catch (error) {
console.error(error);
} finally {
await browser.close();
}
})();
16 changes: 16 additions & 0 deletions public/ffmpeg/core-mt/ffmpeg-core.js

Large diffs are not rendered by default.

Binary file added public/ffmpeg/core-mt/ffmpeg-core.wasm
Binary file not shown.
1 change: 1 addition & 0 deletions public/ffmpeg/core-mt/ffmpeg-core.worker.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions public/ffmpeg/core/ffmpeg-core.js

Large diffs are not rendered by default.

Binary file added public/ffmpeg/core/ffmpeg-core.wasm
Binary file not shown.
21 changes: 21 additions & 0 deletions public/ffmpeg/umd/ffmpeg-core.js

Large diffs are not rendered by default.

Binary file added public/ffmpeg/umd/ffmpeg-core.wasm
Binary file not shown.
96 changes: 96 additions & 0 deletions scripts/download_ffmpeg_local.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const fs = require('fs');
const path = require('path');
const https = require('https');
const urlModule = require('url');

const targetDir = path.resolve(__dirname, '../public/ffmpeg');
const coreDir = path.join(targetDir, 'core');
const coreMtDir = path.join(targetDir, 'core-mt');

// Ensure directories exist
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
if (!fs.existsSync(coreDir)) fs.mkdirSync(coreDir, { recursive: true });
if (!fs.existsSync(coreMtDir)) fs.mkdirSync(coreMtDir, { recursive: true });

const filesToDownload = [
// Single-threaded core
{
url: 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm/ffmpeg-core.js',
dest: path.join(coreDir, 'ffmpeg-core.js')
},
{
url: 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm/ffmpeg-core.wasm',
dest: path.join(coreDir, 'ffmpeg-core.wasm')
},
// Multi-threaded core
{
url: 'https://cdn.jsdelivr.net/npm/@ffmpeg/core-mt@0.12.6/dist/esm/ffmpeg-core.js',
dest: path.join(coreMtDir, 'ffmpeg-core.js')
},
{
url: 'https://cdn.jsdelivr.net/npm/@ffmpeg/core-mt@0.12.6/dist/esm/ffmpeg-core.worker.js',
dest: path.join(coreMtDir, 'ffmpeg-core.worker.js')
},
{
url: 'https://cdn.jsdelivr.net/npm/@ffmpeg/core-mt@0.12.6/dist/esm/ffmpeg-core.wasm',
dest: path.join(coreMtDir, 'ffmpeg-core.wasm')
}
];

function fetchWithRedirects(urlStr, destStream, redirectCount = 0) {
return new Promise((resolve, reject) => {
if (redirectCount > 10) {
reject(new Error("Too many redirects"));
return;
}

https.get(urlStr, (response) => {
const { statusCode } = response;

// Handle redirects
if (statusCode >= 300 && statusCode < 400 && response.headers.location) {
const nextUrl = urlModule.resolve(urlStr, response.headers.location);
resolve(fetchWithRedirects(nextUrl, destStream, redirectCount + 1));
return;
}

if (statusCode === 200) {
response.pipe(destStream);
response.on('end', () => resolve());
} else {
reject(new Error(`Failed with status code: ${statusCode} for URL: ${urlStr}`));
}
}).on('error', (err) => reject(err));
});
}

async function downloadFile(url, dest) {
if (fs.existsSync(dest) && fs.statSync(dest).size > 1000) {
console.log(`Skipping ${dest} - already downloaded.`);
return;
}

console.log(`Downloading ${url} -> ${dest} ...`);
const fileStream = fs.createWriteStream(dest);
try {
await fetchWithRedirects(url, fileStream);
fileStream.close();
console.log(`Successfully downloaded to ${dest}`);
} catch (error) {
fileStream.close();
fs.unlink(dest, () => {}); // Delete partial file
throw error;
}
}

(async () => {
try {
for (const item of filesToDownload) {
await downloadFile(item.url, item.dest);
}
console.log("All FFmpeg WASM files downloaded successfully!");
} catch (error) {
console.error("Failed to download local FFmpeg files:", error);
process.exit(1);
}
})();
78 changes: 78 additions & 0 deletions scripts/download_ffmpeg_umd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const fs = require('fs');
const path = require('path');
const https = require('https');
const urlModule = require('url');

const targetDir = path.resolve(__dirname, '../public/ffmpeg/umd');

// Ensure directory exists
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });

const filesToDownload = [
{
url: 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/umd/ffmpeg-core.js',
dest: path.join(targetDir, 'ffmpeg-core.js')
},
{
url: 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/umd/ffmpeg-core.wasm',
dest: path.join(targetDir, 'ffmpeg-core.wasm')
}
];

function fetchWithRedirects(urlStr, destStream, redirectCount = 0) {
return new Promise((resolve, reject) => {
if (redirectCount > 10) {
reject(new Error("Too many redirects"));
return;
}

https.get(urlStr, (response) => {
const { statusCode } = response;

// Handle redirects
if (statusCode >= 300 && statusCode < 400 && response.headers.location) {
const nextUrl = urlModule.resolve(urlStr, response.headers.location);
resolve(fetchWithRedirects(nextUrl, destStream, redirectCount + 1));
return;
}

if (statusCode === 200) {
response.pipe(destStream);
response.on('end', () => resolve());
} else {
reject(new Error(`Failed with status code: ${statusCode} for URL: ${urlStr}`));
}
}).on('error', (err) => reject(err));
});
}

async function downloadFile(url, dest) {
if (fs.existsSync(dest) && fs.statSync(dest).size > 1000) {
console.log(`Skipping ${dest} - already downloaded.`);
return;
}

console.log(`Downloading ${url} -> ${dest} ...`);
const fileStream = fs.createWriteStream(dest);
try {
await fetchWithRedirects(url, fileStream);
fileStream.close();
console.log(`Successfully downloaded to ${dest}`);
} catch (error) {
fileStream.close();
fs.unlink(dest, () => {}); // Delete partial file
throw error;
}
}

(async () => {
try {
for (const item of filesToDownload) {
await downloadFile(item.url, item.dest);
}
console.log("All FFmpeg UMD WASM files downloaded successfully!");
} catch (error) {
console.error("Failed to download local FFmpeg UMD files:", error);
process.exit(1);
}
})();
Binary file added selected_format.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ThemeProvider } from "@/components/ThemeProvider";
import { ThemeToggle } from "@/components/ThemeToggle";
import ScrollToTop from "@/components/ScrollToTop";
import BrandLogo from "@/components/BrandLogo";
import SplashScreen from "@/components/SplashScreen";

export const metadata: Metadata = {
title: "Reframe — Resize, trim, and export videos in your browser",
Expand Down Expand Up @@ -75,6 +76,7 @@ export default function RootLayout({
Skip to main content
</a>
<ThemeProvider>
<SplashScreen />
<ErrorBoundary>
<header
role="banner"
Expand Down
28 changes: 25 additions & 3 deletions src/components/AudioSpeedControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { EditRecipe } from "@/lib/types"
import { SPEED_STEPS } from "@/lib/constants";
import { Volume2, VolumeX, Gauge, AlertTriangle } from "lucide-react";
import { Volume2, VolumeX, Gauge, AlertTriangle, Rewind } from "lucide-react";
import { cn } from "@/lib/utils";

interface Props {
Expand All @@ -21,7 +21,7 @@ export default function AudioSpeedControl({ recipe, onChange }: Props) {
return "Very Fast";
};

const isModified = recipe.speed !== 1 || !recipe.keepAudio;
const isModified = recipe.speed !== 1 || !recipe.keepAudio || recipe.reverse;

return (
<div className="space-y-4">
Expand All @@ -30,7 +30,7 @@ export default function AudioSpeedControl({ recipe, onChange }: Props) {
<button
type="button"
aria-label="Reset audio settings to default"
onClick={() => onChange({ speed: 1, keepAudio: true })}
onClick={() => onChange({ speed: 1, keepAudio: true, reverse: false })}
className="text-sm font-heading font-semibold uppercase tracking-wider text-film-600 hover:text-film-700 hover:underline transition-all duration-150"
>
Reset to Default
Expand Down Expand Up @@ -70,6 +70,28 @@ export default function AudioSpeedControl({ recipe, onChange }: Props) {
</kbd>
</button>

<button
type="button"
onClick={() => onChange({ reverse: !recipe.reverse })}
aria-label={recipe.reverse ? "Disable reverse playback" : "Enable reverse playback"}
aria-pressed={recipe.reverse}
className={cn(
"w-full flex items-center gap-3 p-3 rounded-lg border transition-all duration-150",
"hover:scale-[1.01] active:scale-[0.99]",
recipe.reverse
? "border-film-300 bg-film-50 text-film-700"
: "border-[var(--border)] bg-[var(--surface)] text-[var(--muted)]"
)}
>
<Rewind size={16} aria-hidden="true" />
<span className="sr-only">
{recipe.reverse ? "Turn reverse playback off" : "Turn reverse playback on"}
</span>
<span className="text-sm font-heading font-semibold flex-1 text-left">
Reverse Playback
</span>
</button>

<div>
<div className="flex items-center justify-between mb-2">
<label
Expand Down
Loading