-
+
+
+ {
+ if (!isUpgraded()) {
+ await commands.showWindow("Upgrade");
+ }
+ }}
+ class={`text-[0.625rem] ${
+ isUpgraded()
+ ? "bg-blue-400 text-gray-50"
+ : "bg-gray-200 text-gray-400 cursor-pointer hover:bg-gray-300"
+ } rounded-lg px-1.5 py-0.5`}
+ >
+ {isUpgraded() ? "Pro" : "Free"}
+
+
diff --git a/apps/embed/app/view/[videoId]/_components/ShareVideo.tsx b/apps/embed/app/view/[videoId]/_components/ShareVideo.tsx
index 56c46e93..f70f5f84 100644
--- a/apps/embed/app/view/[videoId]/_components/ShareVideo.tsx
+++ b/apps/embed/app/view/[videoId]/_components/ShareVideo.tsx
@@ -83,29 +83,38 @@ export const ShareVideo = ({
useEffect(() => {
if (videoMetadataLoaded) {
- console.log("Metadata loaded");
setIsLoading(false);
}
}, [videoMetadataLoaded]);
useEffect(() => {
const onVideoLoadedMetadata = () => {
- console.log("Video metadata loaded");
- setVideoMetadataLoaded(true);
if (videoRef.current) {
setLongestDuration(videoRef.current.duration);
+ setVideoMetadataLoaded(true);
+ setIsLoading(false);
}
};
- const videoElement = videoRef.current;
+ const onCanPlay = () => {
+ setVideoMetadataLoaded(true);
+ setIsLoading(false);
+ };
- videoElement?.addEventListener("loadedmetadata", onVideoLoadedMetadata);
+ const videoElement = videoRef.current;
+ if (videoElement) {
+ videoElement.addEventListener("loadedmetadata", onVideoLoadedMetadata);
+ videoElement.addEventListener("canplay", onCanPlay);
+ }
return () => {
- videoElement?.removeEventListener(
- "loadedmetadata",
- onVideoLoadedMetadata
- );
+ if (videoElement) {
+ videoElement.removeEventListener(
+ "loadedmetadata",
+ onVideoLoadedMetadata
+ );
+ videoElement.removeEventListener("canplay", onCanPlay);
+ }
};
}, []);
diff --git a/apps/embed/next.config.mjs b/apps/embed/next.config.mjs
index 488d3b21..d360e34a 100644
--- a/apps/embed/next.config.mjs
+++ b/apps/embed/next.config.mjs
@@ -34,25 +34,7 @@ const nextConfig = {
remotePatterns: [
{
protocol: "https",
- hostname: "*.amazonaws.com",
- port: "",
- pathname: "**",
- },
- {
- protocol: "https",
- hostname: "*.cloudfront.net",
- port: "",
- pathname: "**",
- },
- {
- protocol: "https",
- hostname: "*v.cap.so",
- port: "",
- pathname: "**",
- },
- {
- protocol: "https",
- hostname: "*tasks.cap.so",
+ hostname: "**",
port: "",
pathname: "**",
},
diff --git a/apps/web/app/api/playlist/route.ts b/apps/web/app/api/playlist/route.ts
index 9b5e0460..c21841f6 100644
--- a/apps/web/app/api/playlist/route.ts
+++ b/apps/web/app/api/playlist/route.ts
@@ -78,6 +78,37 @@ export async function GET(request: NextRequest) {
}
}
+ if (!bucket || video.awsBucket === process.env.NEXT_PUBLIC_CAP_AWS_BUCKET) {
+ if (video.source.type === "desktopMP4") {
+ return new Response(null, {
+ status: 302,
+ headers: {
+ ...getHeaders(origin),
+ Location: `https://v.cap.so/${userId}/${videoId}/result.mp4`,
+ },
+ });
+ }
+
+ if (video.source.type === "MediaConvert") {
+ return new Response(null, {
+ status: 302,
+ headers: {
+ ...getHeaders(origin),
+ Location: `https://v.cap.so/${userId}/${videoId}/output/video_recording_000.m3u8`,
+ },
+ });
+ }
+
+ const playlistUrl = `https://v.cap.so/${userId}/${videoId}/combined-source/stream.m3u8`;
+ return new Response(null, {
+ status: 302,
+ headers: {
+ ...getHeaders(origin),
+ Location: playlistUrl,
+ },
+ });
+ }
+
const Bucket = getS3Bucket(bucket);
const videoPrefix = `${userId}/${videoId}/video/`;
const audioPrefix = `${userId}/${videoId}/audio/`;
@@ -129,7 +160,6 @@ export async function GET(request: NextRequest) {
{ expiresIn: 3600 }
);
- console.log({ playlistUrl });
return new Response(null, {
status: 302,
headers: {
diff --git a/apps/web/app/api/screenshot/route.ts b/apps/web/app/api/screenshot/route.ts
index 167a6ab8..4390806c 100644
--- a/apps/web/app/api/screenshot/route.ts
+++ b/apps/web/app/api/screenshot/route.ts
@@ -2,12 +2,13 @@ import { type NextRequest } from "next/server";
import { db } from "@cap/database";
import { s3Buckets, videos } from "@cap/database/schema";
import { eq } from "drizzle-orm";
-import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
+import { S3Client, ListObjectsV2Command, GetObjectCommand } from "@aws-sdk/client-s3";
import { getCurrentUser } from "@cap/database/auth/session";
import { getHeaders } from "@/utils/helpers";
import { createS3Client, getS3Bucket } from "@/utils/s3";
+import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
-export const revalidate = 3599;
+export const revalidate = 0;
export async function OPTIONS(request: NextRequest) {
const origin = request.headers.get("origin") as string;
@@ -47,7 +48,15 @@ export async function GET(request: NextRequest) {
);
}
- const { video, bucket } = query[0];
+ const result = query[0];
+ if (!result?.video || !result?.bucket) {
+ return new Response(
+ JSON.stringify({ error: true, message: "Video or bucket not found" }),
+ { status: 401, headers: getHeaders(origin) }
+ );
+ }
+
+ const { video, bucket } = result;
if (video.public === false) {
const user = await getCurrentUser();
@@ -85,7 +94,20 @@ export async function GET(request: NextRequest) {
);
}
- const screenshotUrl = `https://v.cap.so/${screenshot.Key}`;
+ let screenshotUrl: string;
+
+ if (video.awsBucket !== process.env.NEXT_PUBLIC_CAP_AWS_BUCKET) {
+ screenshotUrl = await getSignedUrl(
+ s3Client,
+ new GetObjectCommand({
+ Bucket,
+ Key: screenshot.Key
+ }),
+ { expiresIn: 3600 }
+ );
+ } else {
+ screenshotUrl = `https://v.cap.so/${screenshot.Key}`;
+ }
return new Response(JSON.stringify({ url: screenshotUrl }), {
status: 200,
diff --git a/apps/web/app/api/thumbnail/route.ts b/apps/web/app/api/thumbnail/route.ts
index bbfb3ac0..be5afb63 100644
--- a/apps/web/app/api/thumbnail/route.ts
+++ b/apps/web/app/api/thumbnail/route.ts
@@ -1,12 +1,13 @@
import type { NextRequest } from "next/server";
-import { ListObjectsV2Command } from "@aws-sdk/client-s3";
+import { ListObjectsV2Command, GetObjectCommand } from "@aws-sdk/client-s3";
import { getHeaders } from "@/utils/helpers";
import { db } from "@cap/database";
import { eq } from "drizzle-orm";
import { s3Buckets, videos } from "@cap/database/schema";
import { createS3Client, getS3Bucket } from "@/utils/s3";
+import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
-export const revalidate = 3500;
+export const revalidate = 0;
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
@@ -46,12 +47,33 @@ export async function GET(request: NextRequest) {
);
}
- const { video, bucket } = query[0];
- const Bucket = getS3Bucket(bucket);
+ const result = query[0];
+ if (!result?.video) {
+ return new Response(
+ JSON.stringify({ error: true, message: "Video not found" }),
+ {
+ status: 401,
+ headers: getHeaders(origin),
+ }
+ );
+ }
- const s3Client = createS3Client(bucket);
+ const { video } = result;
const prefix = `${userId}/${videoId}/`;
+ let thumbnailUrl: string;
+
+ if (!result.bucket || video.awsBucket === process.env.NEXT_PUBLIC_CAP_AWS_BUCKET) {
+ thumbnailUrl = `https://v.cap.so/${prefix}screenshot/screen-capture.jpg`;
+ return new Response(JSON.stringify({ screen: thumbnailUrl }), {
+ status: 200,
+ headers: getHeaders(origin),
+ });
+ }
+
+ const Bucket = getS3Bucket(result.bucket);
+ const s3Client = createS3Client(result.bucket);
+
try {
const listCommand = new ListObjectsV2Command({
Bucket,
@@ -61,24 +83,37 @@ export async function GET(request: NextRequest) {
const listResponse = await s3Client.send(listCommand);
const contents = listResponse.Contents || [];
- let thumbnailKey = contents.find((item) => item.Key?.endsWith(".png"))?.Key;
+ const thumbnailKey = contents.find((item) =>
+ item.Key?.endsWith("screen-capture.jpg")
+ )?.Key;
if (!thumbnailKey) {
- thumbnailKey = `${prefix}screenshot/screen-capture.jpg`;
+ return new Response(
+ JSON.stringify({
+ error: true,
+ message: "No thumbnail found for this video",
+ }),
+ {
+ status: 404,
+ headers: getHeaders(origin),
+ }
+ );
}
- const thumbnailUrl = `https://v.cap.so/${thumbnailKey}`;
-
- return new Response(JSON.stringify({ screen: thumbnailUrl }), {
- status: 200,
- headers: getHeaders(origin),
- });
+ thumbnailUrl = await getSignedUrl(
+ s3Client,
+ new GetObjectCommand({
+ Bucket,
+ Key: thumbnailKey
+ }),
+ { expiresIn: 3600 }
+ );
} catch (error) {
- console.error("Error generating thumbnail URL:", error);
return new Response(
JSON.stringify({
error: true,
message: "Error generating thumbnail URL",
+ details: error instanceof Error ? error.message : "Unknown error",
}),
{
status: 500,
@@ -86,4 +121,9 @@ export async function GET(request: NextRequest) {
}
);
}
+
+ return new Response(JSON.stringify({ screen: thumbnailUrl }), {
+ status: 200,
+ headers: getHeaders(origin),
+ });
}
diff --git a/apps/web/app/api/video/[videoId]/route.ts b/apps/web/app/api/video/[videoId]/route.ts
new file mode 100644
index 00000000..e2290f0c
--- /dev/null
+++ b/apps/web/app/api/video/[videoId]/route.ts
@@ -0,0 +1,41 @@
+import { db } from "@cap/database";
+import { s3Buckets, videos } from "@cap/database/schema";
+import { eq } from "drizzle-orm";
+import { NextResponse } from "next/server";
+
+export async function GET(
+ request: Request,
+ { params }: { params: { videoId: string } }
+) {
+ const videoId = params.videoId;
+
+ const query = await db
+ .select({
+ video: videos,
+ bucket: s3Buckets,
+ })
+ .from(videos)
+ .leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id))
+ .where(eq(videos.id, videoId));
+
+ if (query.length === 0) {
+ return NextResponse.json({ error: "Video not found" }, { status: 404 });
+ }
+
+ const result = query[0];
+ if (!result?.video) {
+ return NextResponse.json({ error: "Video not found" }, { status: 404 });
+ }
+
+ const defaultBucket = {
+ name: process.env.NEXT_PUBLIC_CAP_AWS_BUCKET,
+ region: process.env.NEXT_PUBLIC_CAP_AWS_REGION,
+ accessKeyId: process.env.CAP_AWS_ACCESS_KEY,
+ secretAccessKey: process.env.CAP_AWS_SECRET_KEY,
+ };
+
+ return NextResponse.json({
+ video: result.video,
+ bucket: result.bucket || defaultBucket,
+ });
+}
\ No newline at end of file
diff --git a/apps/web/app/api/video/og/route.tsx b/apps/web/app/api/video/og/route.tsx
index 2c7c256b..5ac8459b 100644
--- a/apps/web/app/api/video/og/route.tsx
+++ b/apps/web/app/api/video/og/route.tsx
@@ -1,6 +1,3 @@
-import { db } from "@cap/database";
-import { s3Buckets, videos } from "@cap/database/schema";
-import { eq } from "drizzle-orm";
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
@@ -11,25 +8,18 @@ export const runtime = "edge";
export async function GET(req: NextRequest) {
const videoId = req.nextUrl.searchParams.get("videoId") as string;
- const query = await db
- .select({
- video: videos,
- bucket: s3Buckets,
- })
- .from(videos)
- .leftJoin(s3Buckets, eq(videos.bucket, s3Buckets.id))
- .where(eq(videos.id, videoId));
- type FileKey = {
- type: "screen";
- key: string;
- };
-
- type ResponseObject = {
- screen: string | null;
- };
+ const response = await fetch(
+ `${process.env.NEXT_PUBLIC_URL}/api/video/${videoId}`,
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }
+ );
- if (query.length === 0 || query?.[0]?.video.public === false) {
+ if (!response.ok) {
return new ImageResponse(
(
-
Video not found
+
Cap not found
+
+ The video you are looking for does not exist or has moved.
+
),
{
@@ -53,37 +47,47 @@ export async function GET(req: NextRequest) {
);
}
- const { video, bucket } = query[0];
+ const { video, bucket } = await response.json();
- const s3Client = createS3Client(bucket);
+ if (!video || !bucket || video.public === false) {
+ return new ImageResponse(
+ (
+
+
Video or bucket not found
+
+ ),
+ {
+ width: 1200,
+ height: 630,
+ }
+ );
+ }
+ const s3Client = createS3Client(bucket);
const Bucket = getS3Bucket(bucket);
- const fileKeys: FileKey[] = [
- {
- type: "screen",
- key: `${video.ownerId}/${video.id}/screenshot/screen-capture.jpg`,
- },
- ];
- const responseObject: ResponseObject = {
- screen: null,
- };
+ const screenshotKey = `${video.ownerId}/${video.id}/screenshot/screen-capture.jpg`;
+ let screenshotUrl = null;
- await Promise.all(
- fileKeys.map(async ({ type, key }) => {
- try {
- const url = await getSignedUrl(
- s3Client,
- new GetObjectCommand({ Bucket, Key: key }),
- { expiresIn: 3600 }
- );
- responseObject[type] = url;
- } catch (error) {
- console.error("Error generating URL for:", key, error);
- responseObject[type] = null;
- }
- })
- );
+ try {
+ screenshotUrl = await getSignedUrl(
+ s3Client,
+ new GetObjectCommand({ Bucket, Key: screenshotKey }),
+ { expiresIn: 3600 }
+ );
+ } catch (error) {
+ console.error("Error generating URL for screenshot:", error);
+ }
return new ImageResponse(
(
@@ -95,7 +99,7 @@ export async function GET(req: NextRequest) {
alignItems: "center",
justifyContent: "center",
background:
- "radial-gradient(90.01% 80.01% at 53.53% 49.99%,#d3e5ff 30.65%,#82c6f1 88.48%,#fff 100%)",
+ "radial-gradient(90.01% 80.01% at 53.53% 49.99%,#d3e5ff 30.65%,#4785ff 88.48%,#fff 100%)",
}}
>
- {responseObject.screen && (
+ {screenshotUrl && (
)}
diff --git a/apps/web/app/s/[videoId]/_components/MP4VideoPlayer.tsx b/apps/web/app/s/[videoId]/_components/MP4VideoPlayer.tsx
index 266bbf6e..4c8e0990 100644
--- a/apps/web/app/s/[videoId]/_components/MP4VideoPlayer.tsx
+++ b/apps/web/app/s/[videoId]/_components/MP4VideoPlayer.tsx
@@ -21,24 +21,26 @@ export const MP4VideoPlayer = memo(
const video = videoRef.current;
if (!video) return;
- const startTime = Date.now();
- const maxDuration = 2 * 60 * 1000;
-
- const checkAndReload = () => {
- if (video.readyState === 0) {
- // HAVE_NOTHING
- video.load();
- }
-
- if (Date.now() - startTime < maxDuration) {
- setTimeout(checkAndReload, 3000);
- }
+ const handleLoadedMetadata = () => {
+ // Trigger a canplay event after metadata is loaded
+ video.dispatchEvent(new Event("canplay"));
};
- checkAndReload();
+ const handleError = (e: ErrorEvent) => {
+ console.error("Video loading error:", e);
+ // Attempt to reload on error
+ video.load();
+ };
+
+ video.addEventListener("loadedmetadata", handleLoadedMetadata);
+ video.addEventListener("error", handleError as EventListener);
+
+ // Initial load
+ video.load();
return () => {
- clearTimeout(checkAndReload as unknown as number);
+ video.removeEventListener("loadedmetadata", handleLoadedMetadata);
+ video.removeEventListener("error", handleError as EventListener);
};
}, [videoSrc]);
@@ -47,7 +49,7 @@ export const MP4VideoPlayer = memo(
id="video-player"
ref={videoRef}
className="w-full h-full object-contain"
- preload="metadata"
+ preload="auto"
playsInline
controls={false}
muted
diff --git a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx
index ac130c63..782d6032 100644
--- a/apps/web/app/s/[videoId]/_components/ShareVideo.tsx
+++ b/apps/web/app/s/[videoId]/_components/ShareVideo.tsx
@@ -139,21 +139,32 @@ export const ShareVideo = ({
useEffect(() => {
const onVideoLoadedMetadata = () => {
- setVideoMetadataLoaded(true);
if (videoRef.current) {
setLongestDuration(videoRef.current.duration);
+ setVideoMetadataLoaded(true);
+ setIsLoading(false);
}
};
- const videoElement = videoRef.current;
+ const onCanPlay = () => {
+ setVideoMetadataLoaded(true);
+ setIsLoading(false);
+ };
- videoElement?.addEventListener("loadedmetadata", onVideoLoadedMetadata);
+ const videoElement = videoRef.current;
+ if (videoElement) {
+ videoElement.addEventListener("loadedmetadata", onVideoLoadedMetadata);
+ videoElement.addEventListener("canplay", onCanPlay);
+ }
return () => {
- videoElement?.removeEventListener(
- "loadedmetadata",
- onVideoLoadedMetadata
- );
+ if (videoElement) {
+ videoElement.removeEventListener(
+ "loadedmetadata",
+ onVideoLoadedMetadata
+ );
+ videoElement.removeEventListener("canplay", onCanPlay);
+ }
};
}, []);
diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs
index 583d784d..4c2c888c 100644
--- a/apps/web/next.config.mjs
+++ b/apps/web/next.config.mjs
@@ -38,25 +38,7 @@ const nextConfig = {
remotePatterns: [
{
protocol: "https",
- hostname: "*.amazonaws.com",
- port: "",
- pathname: "**",
- },
- {
- protocol: "https",
- hostname: "*.cloudfront.net",
- port: "",
- pathname: "**",
- },
- {
- protocol: "https",
- hostname: "*v.cap.so",
- port: "",
- pathname: "**",
- },
- {
- protocol: "https",
- hostname: "*tasks.cap.so",
+ hostname: "**",
port: "",
pathname: "**",
},