Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev: Adds ability to run cap complextely local by adding s3 docker container #251

Merged
merged 13 commits into from
Jan 31, 2025
Merged
11 changes: 7 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@
# - REQUIRED ****************

NEXT_PUBLIC_ENVIRONMENT=development
NEXT_PUBLIC_URL=http://localhost:3000
NEXT_PUBLIC_PORT=3000
NEXT_PUBLIC_URL=http://localhost:${NEXT_PUBLIC_PORT}
NEXT_PUBLIC_TASKS_URL=http://localhost:3002

NEXT_PUBLIC_URL=http://localhost:${NEXT_PUBLIC_PORT}
NEXTAUTH_URL=${NEXT_PUBLIC_URL}
VITE_SERVER_URL=${NEXT_PUBLIC_URL}
VITE_ENVIRONMENT=${NEXT_PUBLIC_ENVIRONMENT}
Expand Down Expand Up @@ -53,9 +52,13 @@ NEXTAUTH_SECRET=
# -- aws ****************
## For use with AWS S3, to upload recorded caps. You can retrieve these credentials from your own AWS account.
## Uses CAP_ prefix to avoid conflict with env variables set in hosting environment. (e.g. Vercel)
CAP_AWS_ACCESS_KEY=
CAP_AWS_SECRET_KEY=
CAP_AWS_ACCESS_KEY=capS3root
CAP_AWS_SECRET_KEY=capS3root
# If using AWS S3 - leave ENDPOINT blank
NEXT_PUBLIC_CAP_AWS_ENDPOINT=http://localhost:3902
NEXT_PUBLIC_CAP_AWS_BUCKET=capso
# If using a Proxy such as Cloudfront, set the URL here to the full URL of the bucket
NEXT_PUBLIC_CAP_AWS_BUCKET_URL=
NEXT_PUBLIC_CAP_AWS_REGION=us-east-1

# -- Deepgram (for transcription) ****************
Expand Down
25 changes: 24 additions & 1 deletion apps/desktop/src-tauri/src/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub struct S3UploadMeta {
aws_region: String,
#[serde(default, deserialize_with = "deserialize_empty_object_as_string")]
aws_bucket: String,
#[serde(default)]
aws_endpoint: String,
}

fn deserialize_empty_object_as_string<'de, D>(deserializer: D) -> Result<String, D::Error>
Expand Down Expand Up @@ -82,12 +84,23 @@ impl S3UploadMeta {
&self.aws_bucket
}

pub fn new(id: String, user_id: String, aws_region: String, aws_bucket: String) -> Self {
pub fn aws_endpoint(&self) -> &str {
&self.aws_endpoint
}

pub fn new(
id: String,
user_id: String,
aws_region: String,
aws_bucket: String,
aws_endpoint: String,
) -> Self {
Self {
id,
user_id,
aws_region,
aws_bucket,
aws_endpoint,
}
}

Expand All @@ -100,6 +113,10 @@ impl S3UploadMeta {
self.aws_bucket =
std::env::var("NEXT_PUBLIC_CAP_AWS_BUCKET").unwrap_or_else(|_| "capso".to_string());
}
if self.aws_endpoint.is_empty() {
self.aws_endpoint = std::env::var("NEXT_PUBLIC_CAP_AWS_ENDPOINT")
.unwrap_or_else(|_| "https://s3.amazonaws.com".to_string());
}
}
}

Expand All @@ -110,6 +127,7 @@ struct S3UploadBody {
file_key: String,
aws_bucket: String,
aws_region: String,
aws_endpoint: String,
}

#[derive(serde::Serialize)]
Expand Down Expand Up @@ -197,6 +215,7 @@ pub async fn upload_video(
file_key: file_key.clone(),
aws_bucket: s3_config.aws_bucket().to_string(),
aws_region: s3_config.aws_region().to_string(),
aws_endpoint: s3_config.aws_endpoint().to_string(),
},
)?;

Expand Down Expand Up @@ -350,6 +369,7 @@ pub async fn upload_image(app: &AppHandle, file_path: PathBuf) -> Result<Uploade
file_key: file_key.clone(),
aws_bucket: s3_config.aws_bucket,
aws_region: s3_config.aws_region,
aws_endpoint: s3_config.aws_endpoint,
},
};

Expand Down Expand Up @@ -417,6 +437,7 @@ pub async fn upload_audio(app: &AppHandle, file_path: PathBuf) -> Result<Uploade
file_key: file_key.clone(),
aws_bucket: s3_config.aws_bucket.clone(),
aws_region: s3_config.aws_region.clone(),
aws_endpoint: s3_config.aws_endpoint.clone(),
},
)?;

Expand Down Expand Up @@ -717,6 +738,7 @@ pub async fn upload_individual_file(
file_key: file_key.clone(),
aws_bucket: s3_config.aws_bucket.clone(),
aws_region: s3_config.aws_region.clone(),
aws_endpoint: s3_config.aws_endpoint.clone(),
};

let (upload_url, mut form) = if is_audio {
Expand Down Expand Up @@ -783,6 +805,7 @@ async fn prepare_screenshot_upload(
file_key: file_key.clone(),
aws_bucket: s3_config.aws_bucket.clone(),
aws_region: s3_config.aws_region.clone(),
aws_endpoint: s3_config.aws_endpoint.clone(),
},
};

Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/utils/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,8 @@ export type RequestRestartRecording = null
export type RequestStartRecording = null
export type RequestStopRecording = null
export type Resolution = { width: number; height: number }
export type S3UploadMeta = { id: string; user_id: string; aws_region?: string; aws_bucket?: string }
export type ScreenCaptureTarget = ({ variant: "window" } & CaptureWindow) | ({ variant: "screen" } & CaptureScreen) | ({ variant: "area" } & CaptureArea)
export type S3UploadMeta = { id: string; user_id: string; aws_region?: string; aws_bucket?: string; aws_endpoint?: string }
export type ScreenCaptureTarget = ({ variant: "window" } & CaptureWindow) | ({ variant: "screen" } & CaptureScreen)
export type SegmentRecordings = { display: Video; camera: Video | null; audio: Audio | null }
export type SerializedEditorInstance = { framesSocketUrl: string; recordingDuration: number; savedProjectConfig: ProjectConfiguration; recordings: ProjectRecordings; path: string; prettyName: string }
export type SharingMeta = { id: string; link: string }
Expand Down
7 changes: 4 additions & 3 deletions apps/embed/app/view/[videoId]/_components/ShareVideo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useRouter } from "next/navigation";
import toast from "react-hot-toast";
import moment from "moment";
import { Toolbar } from "./Toolbar";
import { S3_BUCKET_URL } from "@cap/utils";

declare global {
interface Window {
Expand Down Expand Up @@ -297,7 +298,7 @@ export const ShareVideo = ({
transcriptionUrl = `/api/playlist?userId=${data.ownerId}&videoId=${data.id}&fileType=transcription`;
} else {
// For default Cap storage
transcriptionUrl = `https://v.cap.so/${data.ownerId}/${data.id}/transcription.vtt`;
transcriptionUrl = `${S3_BUCKET_URL}/${data.ownerId}/${data.id}/transcription.vtt`;
}

try {
Expand Down Expand Up @@ -443,8 +444,8 @@ export const ShareVideo = ({
data.source.type === "MediaConvert")
? `${process.env.NEXT_PUBLIC_URL}/api/playlist?userId=${data.ownerId}&videoId=${data.id}&videoType=master`
: data.source.type === "MediaConvert"
? `https://v.cap.so/${data.ownerId}/${data.id}/output/video_recording_000.m3u8`
: `https://v.cap.so/${data.ownerId}/${data.id}/combined-source/stream.m3u8`
? `${S3_BUCKET_URL}/${data.ownerId}/${data.id}/output/video_recording_000.m3u8`
: `${S3_BUCKET_URL}/${data.ownerId}/${data.id}/combined-source/stream.m3u8`
}
/>
</div>
Expand Down
4 changes: 2 additions & 2 deletions apps/embed/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "embed",
"name": "@cap/embed",
"version": "0.3.1",
"private": true,
"scripts": {
Expand Down Expand Up @@ -48,4 +48,4 @@
"engines": {
"node": "20"
}
}
}
6 changes: 3 additions & 3 deletions apps/storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"private": true,
"type": "module",
"scripts": {
"dev": "storybook dev -p 6006",
"build": "storybook build"
"dev:storybook": "storybook dev -p 6006",
"build:storybook": "storybook build"
},
"dependencies": {
"@cap/ui-solid": "workspace:*",
Expand All @@ -26,4 +26,4 @@
"vite": "^5.3.4",
"vite-plugin-solid": "^2.10.2"
}
}
}
4 changes: 2 additions & 2 deletions apps/tasks/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "tasks",
"name": "@cap/tasks",
"version": "0.3.1",
"private": true,
"main": "src/index.ts",
Expand Down Expand Up @@ -44,4 +44,4 @@
"engines": {
"node": "20"
}
}
}
12 changes: 8 additions & 4 deletions apps/web/app/api/playlist/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "@/utils/video/ffmpeg/helpers";
import { getHeaders, CACHE_CONTROL_HEADERS } from "@/utils/helpers";
import { createS3Client, getS3Bucket } from "@/utils/s3";
import { S3_BUCKET_URL } from "@cap/utils";

export const revalidate = 3599;

Expand Down Expand Up @@ -87,7 +88,7 @@ export async function GET(request: NextRequest) {
status: 302,
headers: {
...getHeaders(origin),
Location: `https://v.cap.so/${userId}/${videoId}/result.mp4`,
Location: `${S3_BUCKET_URL}/${userId}/${videoId}/result.mp4`,
...CACHE_CONTROL_HEADERS,
},
});
Expand All @@ -98,13 +99,13 @@ export async function GET(request: NextRequest) {
status: 302,
headers: {
...getHeaders(origin),
Location: `https://v.cap.so/${userId}/${videoId}/output/video_recording_000.m3u8`,
Location: `${S3_BUCKET_URL}/${userId}/${videoId}/output/video_recording_000.m3u8`,
...CACHE_CONTROL_HEADERS,
},
});
}

const playlistUrl = `https://v.cap.so/${userId}/${videoId}/combined-source/stream.m3u8`;
const playlistUrl = `${S3_BUCKET_URL}/${userId}/${videoId}/combined-source/stream.m3u8`;
return new Response(null, {
status: 302,
headers: {
Expand Down Expand Up @@ -141,7 +142,10 @@ export async function GET(request: NextRequest) {
} catch (error) {
console.error("Error fetching transcription file:", error);
return new Response(
JSON.stringify({ error: true, message: "Transcription file not found" }),
JSON.stringify({
error: true,
message: "Transcription file not found",
}),
{
status: 404,
headers: getHeaders(origin),
Expand Down
13 changes: 9 additions & 4 deletions apps/web/app/api/screenshot/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ 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, GetObjectCommand } 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";
import { S3_BUCKET_URL } from "@cap/utils";

export const revalidate = 0;

Expand Down Expand Up @@ -99,13 +104,13 @@ export async function GET(request: NextRequest) {
screenshotUrl = await getSignedUrl(
s3Client,
new GetObjectCommand({
Bucket,
Key: screenshot.Key
Bucket,
Key: screenshot.Key,
}),
{ expiresIn: 3600 }
);
} else {
screenshotUrl = `https://v.cap.so/${screenshot.Key}`;
screenshotUrl = `${S3_BUCKET_URL}/${screenshot.Key}`;
}

return new Response(JSON.stringify({ url: screenshotUrl }), {
Expand Down
12 changes: 8 additions & 4 deletions apps/web/app/api/thumbnail/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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";
import { S3_BUCKET_URL } from "@cap/utils";

export const revalidate = 0;

Expand Down Expand Up @@ -63,8 +64,11 @@ export async function GET(request: NextRequest) {

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`;
if (
!result.bucket ||
video.awsBucket === process.env.NEXT_PUBLIC_CAP_AWS_BUCKET
) {
thumbnailUrl = `${S3_BUCKET_URL}/${prefix}screenshot/screen-capture.jpg`;
return new Response(JSON.stringify({ screen: thumbnailUrl }), {
status: 200,
headers: getHeaders(origin),
Expand All @@ -83,7 +87,7 @@ export async function GET(request: NextRequest) {
const listResponse = await s3Client.send(listCommand);
const contents = listResponse.Contents || [];

const thumbnailKey = contents.find((item) =>
const thumbnailKey = contents.find((item) =>
item.Key?.endsWith("screen-capture.jpg")
)?.Key;

Expand All @@ -104,7 +108,7 @@ export async function GET(request: NextRequest) {
s3Client,
new GetObjectCommand({
Bucket,
Key: thumbnailKey
Key: thumbnailKey,
}),
{ expiresIn: 3600 }
);
Expand Down
8 changes: 8 additions & 0 deletions apps/web/app/api/upload/signed/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ export async function POST(request: NextRequest) {
}
);

// When not using aws s3, we need to transform the url to the local endpoint
if (process.env.NEXT_PUBLIC_CAP_AWS_ENDPOINT) {
const endpoint = process.env.NEXT_PUBLIC_CAP_AWS_ENDPOINT;
const bucket = process.env.NEXT_PUBLIC_CAP_AWS_BUCKET;
const newUrl = `${endpoint}/${bucket}/`;
presignedPostData.url = newUrl;
}

console.log("Presigned URL created successfully");

// After successful presigned URL creation, trigger revalidation
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/api/video/individual/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ListObjectsV2Command } from "@aws-sdk/client-s3";
import { getCurrentUser } from "@cap/database/auth/session";
import { getHeaders } from "@/utils/helpers";
import { createS3Client, getS3Bucket } from "@/utils/s3";
import { S3_BUCKET_URL } from "@cap/utils";

export const revalidate = 3599;

Expand Down Expand Up @@ -93,7 +94,7 @@ export async function GET(request: NextRequest) {
const fileName = key.split("/").pop();
return {
fileName,
url: `https://v.cap.so/${key}`,
url: `${S3_BUCKET_URL}/${key}`,
};
});

Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/api/video/playlistUrl/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { videos } from "@cap/database/schema";
import { eq } from "drizzle-orm";
import { getHeaders } from "@/utils/helpers";
import { CACHE_CONTROL_HEADERS } from "@/utils/helpers";
import { S3_BUCKET_URL } from "@cap/utils";

export const revalidate = 0;

Expand Down Expand Up @@ -59,7 +60,7 @@ export async function GET(request: NextRequest) {
}

if (video.jobStatus === "COMPLETE") {
const playlistUrl = `https://v.cap.so/${video.ownerId}/${video.id}/output/video_recording_000_output.m3u8`;
const playlistUrl = `${S3_BUCKET_URL}/${video.ownerId}/${video.id}/output/video_recording_000_output.m3u8`;
return new Response(
JSON.stringify({ playlistOne: playlistUrl, playlistTwo: null }),
{
Expand Down
Loading
Loading