Skip to content
Draft
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
106 changes: 77 additions & 29 deletions apps/web/app/(org)/dashboard/caps/components/UploadCapButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,29 +104,42 @@ async function legacyUploadCap(
setUploadStatus: (state: UploadStatus | undefined) => void,
queryClient: QueryClient,
) {
const parser = await import("@remotion/media-parser");
const webcodecs = await import("@remotion/webcodecs");
const { Input } = await import("mediabunny");

try {
setUploadStatus({ status: "parsing" });
const metadata = await parser.parseMedia({
src: file,
fields: {
durationInSeconds: true,
dimensions: true,
fps: true,
numberOfAudioChannels: true,
sampleRate: true,
},
const { BlobSource, ALL_FORMATS } = await import("mediabunny");
const input = new Input({
source: new BlobSource(file),
formats: ALL_FORMATS,
});

const duration = metadata.durationInSeconds
// Get metadata from the input
const videoTracks = await input.getVideoTracks();
const audioTracks = await input.getAudioTracks();
const videoTrack = videoTracks[0];
const audioTrack = audioTracks[0];
const fileDuration = await input.computeDuration();

const metadata = {
durationInSeconds: fileDuration,
dimensions: videoTrack
? { width: videoTrack.displayWidth, height: videoTrack.displayHeight }
: undefined,
fps: videoTrack
? (await videoTrack.computePacketStats()).averagePacketRate
: undefined,
numberOfAudioChannels: audioTrack?.numberOfChannels,
sampleRate: audioTrack?.sampleRate,
};

const videoDuration = metadata.durationInSeconds
? Math.round(metadata.durationInSeconds)
: undefined;

setUploadStatus({ status: "creating" });
const videoData = await createVideoAndGetUploadUrl({
duration,
duration: videoDuration,
resolution: metadata.dimensions
? `${metadata.dimensions.width}x${metadata.dimensions.height}`
: undefined,
Expand Down Expand Up @@ -165,24 +178,59 @@ async function legacyUploadCap(

const resizeOptions = calculateResizeOptions();

const convertResult = await webcodecs.convertMedia({
src: file,
container: "mp4",
videoCodec: "h264",
audioCodec: "aac",
...(resizeOptions && { resize: resizeOptions }),
onProgress: ({ overallProgress }) => {
if (overallProgress !== null) {
const progressValue = overallProgress * 100;
setUploadStatus({
status: "converting",
capId: uploadId,
progress: progressValue,
});
}
const {
Output,
Mp4OutputFormat,
BufferTarget,
Conversion,
BlobSource,
ALL_FORMATS,
} = await import("mediabunny");
const input = new Input({
source: new BlobSource(file),
formats: ALL_FORMATS,
});
const output = new Output({
format: new Mp4OutputFormat(),
target: new BufferTarget(),
});

const conversion = await Conversion.init({
input,
output,
video: {
codec: "avc",
...(resizeOptions && {
width: Math.round(metadata.dimensions!.width * resizeOptions.scale),
height: Math.round(
metadata.dimensions!.height * resizeOptions.scale,
),
}),
},
audio: {
codec: "aac",
},
});
optimizedBlob = await convertResult.save();

if (!conversion.isValid) {
throw new Error("Video conversion configuration is invalid");
}

conversion.onProgress = (progress) => {
const progressValue = progress * 100;
setUploadStatus({
status: "converting",
capId: uploadId,
progress: progressValue,
});
};

await conversion.execute();
const buffer = output.target.buffer;
if (!buffer) {
throw new Error("Conversion produced no output buffer");
}
optimizedBlob = new Blob([buffer]);

if (optimizedBlob.size === 0)
throw new Error("Conversion produced empty file");
Expand Down
22 changes: 17 additions & 5 deletions apps/web/app/(org)/verify-otp/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,24 @@ export function VerifyOTPForm({
const otpCode = code.join("");
if (otpCode.length !== 6) throw "Please enter a complete 6-digit code";

// shoutout https://github.com/buoyad/Tally/pull/14
const res = await fetch(
`/api/auth/callback/email?email=${encodeURIComponent(email)}&token=${encodeURIComponent(otpCode)}&callbackUrl=${encodeURIComponent("/login-success")}`,
);
const callback = next || "/dashboard";
const url = `/api/auth/callback/email?email=${encodeURIComponent(email)}&token=${encodeURIComponent(otpCode)}&callbackUrl=${encodeURIComponent(callback)}`;

if (!res.url.includes("/login-success")) {
const isSafari =
typeof navigator !== "undefined" &&
/^((?!chrome|android).)*safari/i.test(navigator.userAgent);

if (isSafari) {
window.location.assign(url);
return;
}

const res = await fetch(url, {
credentials: "include",
redirect: "follow",
});

if (!res.url.includes(callback)) {
setCode(["", "", "", "", "", ""]);
inputRefs.current[0]?.focus();
throw "Invalid code. Please try again.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export default async function ConversionPage(props: ConversionPageProps) {
faqs: [
{
question: `How does the ${sourceFormat.toUpperCase()} to ${targetFormat.toUpperCase()} converter work?`,
answer: `Our converter uses Remotion (remotion.dev) directly in your browser. When you upload a ${sourceFormat.toUpperCase()} file, it gets processed locally on your device and converted to ${targetFormat.toUpperCase()} format without ever being sent to a server.`,
answer: `Our converter uses Mediabunny (mediabunny.dev) directly in your browser. When you upload a ${sourceFormat.toUpperCase()} file, it gets processed locally on your device and converted to ${targetFormat.toUpperCase()} format without ever being sent to a server.`,
},
{
question: "Is there a file size limit?",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(site)/tools/convert/avi-to-mp4/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function AVIToMP4Page() {
{
question: "How does the AVI to MP4 converter work?",
answer:
"Our converter uses Remotion (remotion.dev) directly in your browser. When you upload an AVI file, it gets processed locally on your device and converted to MP4 format without ever being sent to a server.",
"Our converter uses Mediabunny (mediabunny.dev) directly in your browser. When you upload an AVI file, it gets processed locally on your device and converted to MP4 format without ever being sent to a server.",
},
{
question: "Is there a file size limit?",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(site)/tools/convert/mkv-to-mp4/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function MKVToMP4Page() {
{
question: "How does the MKV to MP4 converter work?",
answer:
"Our converter uses Remotion (remotion.dev) directly in your browser. When you upload an MKV file, it gets processed locally on your device and converted to MP4 format without ever being sent to a server.",
"Our converter uses Mediabunny (mediabunny.dev) directly in your browser. When you upload an MKV file, it gets processed locally on your device and converted to MP4 format without ever being sent to a server.",
},
{
question: "Is there a file size limit?",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(site)/tools/convert/mov-to-mp4/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function MOVToMP4Page() {
{
question: "How does the MOV to MP4 converter work?",
answer:
"Our converter uses Remotion (remotion.dev) directly in your browser. When you upload a MOV file, it gets processed locally on your device and converted to MP4 format without ever being sent to a server.",
"Our converter uses Mediabunny (mediabunny.dev) directly in your browser. When you upload a MOV file, it gets processed locally on your device and converted to MP4 format without ever being sent to a server.",
},
{
question: "Is there a file size limit?",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/(site)/tools/convert/mp4-to-gif/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ export default function MP4ToGIFPage() {
{
title: "High Quality Conversion",
description:
"We use Remotion technology to create optimized GIFs from your videos.",
"We use Mediabunny technology to create optimized GIFs from your videos.",
},
],
faqs: [
{
question: "How does the MP4 to GIF converter work?",
answer:
"Our converter uses Remotion (remotion.dev) directly in your browser. When you upload an MP4 file, it gets processed locally on your device and converted to an animated GIF without ever being sent to a server.",
"Our converter uses Mediabunny (mediabunny.dev) directly in your browser. When you upload an MP4 file, it gets processed locally on your device and converted to an animated GIF without ever being sent to a server.",
},
{
question: "Is there a file size limit?",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/(site)/tools/convert/mp4-to-mp3/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ export default function MP4ToMP3Page() {
{
title: "High Quality Conversion",
description:
"We use Remotion technology to ensure high-quality audio extraction.",
"We use Mediabunny technology to ensure high-quality audio extraction.",
},
],
faqs: [
{
question: "How does the MP4 to MP3 converter work?",
answer:
"Our converter uses Remotion (remotion.dev) directly in your browser. When you upload an MP4 file, it extracts the audio track locally on your device and saves it as an MP3 file without ever being sent to a server.",
"Our converter uses Mediabunny (mediabunny.dev) directly in your browser. When you upload an MP4 file, it extracts the audio track locally on your device and saves it as an MP3 file without ever being sent to a server.",
},
{
question: "Is there a file size limit?",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(site)/tools/convert/mp4-to-webm/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function MP4ToWebMPage() {
{
question: "How does the MP4 to WebM converter work?",
answer:
"Our converter uses Remotion (remotion.dev) directly in your browser. When you upload an MP4 file, it gets processed locally on your device and converted to WebM format without ever being sent to a server.",
"Our converter uses Mediabunny (mediabunny.dev) directly in your browser. When you upload an MP4 file, it gets processed locally on your device and converted to WebM format without ever being sent to a server.",
},
{
question: "Is there a file size limit?",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(site)/tools/convert/webm-to-mp4/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function WebmToMp4Page() {
{
question: "How does the WebM to MP4 converter work?",
answer:
"Our converter uses Remotion (remotion.dev) directly in your browser. When you upload a WebM file, it gets processed locally on your device and converted to MP4 format without ever being sent to a server.",
"Our converter uses Mediabunny (mediabunny.dev) directly in your browser. When you upload a WebM file, it gets processed locally on your device and converted to MP4 format without ever being sent to a server.",
},
{
question: "Is there a file size limit?",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/(site)/tools/video-speed-controller/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const content = {
"Instantly speed up or slow down any MP4, WebM or MOV in your browser. No uploads, no quality loss.",
featuresTitle: "Why Use Our Online Video Speed Controller?",
featuresDescription:
"Powered by WebCodecs + Remotion, Cap processes every frame locally for near-instant results—while keeping your files 100% private.",
"Powered by WebCodecs + Mediabunny, Cap processes every frame locally for near-instant results—while keeping your files 100% private.",
features: [
{
title: "WebCodecs-Level Speed",
Expand Down Expand Up @@ -69,7 +69,7 @@ export const metadata = {
title:
"Video Speed Controller Online – Speed Up or Slow Down Videos (0.25×-3×)",
description:
"Free WebCodecs-powered tool to change video speed online. Adjust playback from 0.25× to 3× without quality loss—processed locally for privacy.",
"Free Mediabunny-powered tool to change video speed online. Adjust playback from 0.25× to 3× without quality loss—processed locally for privacy.",
keywords: [
"video speed controller",
"speed up video online",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/api/notifications/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Notification as APINotification } from "@cap/web-api-contract";
import { and, ColumnBaseConfig, desc, eq, isNull, sql } from "drizzle-orm";
import { MySqlColumn } from "drizzle-orm/mysql-core";
import { NextResponse } from "next/server";
import { AvcProfileInfo } from "node_modules/@remotion/media-parser/dist/containers/avc/parse-avc";

import { z } from "zod";
import type { NotificationType } from "@/lib/Notification";
import { jsonExtractString } from "@/utils/sql";
Expand Down
21 changes: 17 additions & 4 deletions apps/web/app/s/[videoId]/_components/OtpForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,24 @@ const OtpForm = ({
const otpCode = code.join("");
if (otpCode.length !== 6) throw "Please enter a complete 6-digit code";

const res = await fetch(
`/api/auth/callback/email?email=${encodeURIComponent(email)}&token=${encodeURIComponent(otpCode)}&callbackUrl=${encodeURIComponent("/login-success")}`,
);
const callback = "/dashboard";
const url = `/api/auth/callback/email?email=${encodeURIComponent(email)}&token=${encodeURIComponent(otpCode)}&callbackUrl=${encodeURIComponent(callback)}`;

if (!res.url.includes("/login-success")) {
const isSafari =
typeof navigator !== "undefined" &&
/^((?!chrome|android).)*safari/i.test(navigator.userAgent);

if (isSafari) {
window.location.assign(url);
return;
}

const res = await fetch(url, {
credentials: "include",
redirect: "follow",
});

if (!res.url.includes(callback)) {
setCode(["", "", "", "", "", ""]);
inputRefs.current[0]?.focus();
throw "Invalid code. Please try again.";
Expand Down
Loading
Loading