diff --git a/.env.example b/.env.example index 964cc694..f2af00b7 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,7 @@ NEXT_PUBLIC_ENVIRONMENT=development NEXT_PUBLIC_URL=http://localhost:3000 +NEXT_PUBLIC_TASKS_URL=http://localhost:3002 # IMPORTANT FOR LOCAL DEV: # This determines whether or not the app will run in "local mode". @@ -41,6 +42,9 @@ CAP_AWS_SECRET_KEY= CAP_AWS_BUCKET= CAP_AWS_REGION= +# -- Deepgram (for transcription) **************** +DEEPGRAM_API_KEY= + # -- resend **************** ## For use with email authentication (sign up, sign in, forgot password) RESEND_API_KEY= diff --git a/README.md b/README.md index 493c1953..8f5fdaea 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

Cap

- Effortless, instant screen sharing. Open source and cross-platform. + The open source Loom alternative.
Cap.so ยป
@@ -24,7 +24,7 @@ > NOTE: Cap is under active development, and is currently in public beta. This repository is updated regularly with changes and new releases. -Cap is an open source alternative to Loom. It's a video messaging tool that allows you to record, edit and share videos in seconds. +Cap is the open source alternative to Loom. It's a video messaging tool that allows you to record, edit and share videos in seconds. ![cap-emoji-banner](https://github.com/CapSoftware/cap/assets/33632126/85425396-ad31-463b-b209-7c4bdf7e2e4f) diff --git a/apps/desktop/src-tauri/src/capture/src/quartz/capturer.rs b/apps/desktop/src-tauri/src/capture/src/quartz/capturer.rs index 6d83f693..5dd74761 100644 --- a/apps/desktop/src-tauri/src/capture/src/quartz/capturer.rs +++ b/apps/desktop/src-tauri/src/capture/src/quartz/capturer.rs @@ -56,6 +56,10 @@ impl Capturer { stream }; + if queue.is_null() { + return Err(CGError::Failure); + } + match unsafe { CGDisplayStreamStart(stream) } { CGError::Success => Ok(Capturer { stream, queue, width, height, format, display diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index 2c339f0f..55cf324d 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -95,6 +95,8 @@ pub async fn upload_file( "audio/aac" } else if file_path.to_lowercase().ends_with(".webm") { "audio/webm" + } else if file_path.to_lowercase().ends_with(".mp3") { + "audio/mpeg" } else { "video/mp2t" }; @@ -155,14 +157,14 @@ pub fn get_video_duration(file_path: &str) -> Result { .output()?; let output_str = str::from_utf8(&output.stderr).unwrap(); - let duration_regex = Regex::new(r"Duration: (\d{2}):(\d{2}):(\d{2})\.\d{2}").unwrap(); + let duration_regex = Regex::new(r"Duration: (\d{2}):(\d{2}):(\d{2})\.(\d{2})").unwrap(); let caps = duration_regex.captures(output_str).unwrap(); let hours: f64 = caps.get(1).unwrap().as_str().parse().unwrap(); let minutes: f64 = caps.get(2).unwrap().as_str().parse().unwrap(); - let seconds: f64 = caps.get(3).unwrap().as_str().parse().unwrap(); - - let duration = hours * 3600.0 + minutes * 60.0 + seconds; + let seconds: f64 = caps.get(3).unwrap().as_str().parse::().unwrap(); + let milliseconds: f64 = caps.get(4).unwrap().as_str().parse::().unwrap() / 100.0; + let duration = hours * 3600.0 + minutes * 60.0 + seconds + milliseconds; Ok(duration) } diff --git a/apps/embed/.eslintrc.cjs b/apps/embed/.eslintrc.cjs new file mode 100644 index 00000000..625ebb14 --- /dev/null +++ b/apps/embed/.eslintrc.cjs @@ -0,0 +1,7 @@ +module.exports = { + extends: [require.resolve("config/eslint/web.js")], + parserOptions: { + tsconfigRootDir: __dirname, + project: "./tsconfig.json", + }, +}; diff --git a/apps/embed/.gitignore b/apps/embed/.gitignore new file mode 100644 index 00000000..90f1072a --- /dev/null +++ b/apps/embed/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts +pnpm-lock.yaml \ No newline at end of file diff --git a/apps/embed/README.md b/apps/embed/README.md new file mode 100644 index 00000000..c0ed8b35 --- /dev/null +++ b/apps/embed/README.md @@ -0,0 +1,3 @@ +# Cap Embed App + +This Next.js app is used for Cap web embeds. diff --git a/apps/embed/app/favicon.ico b/apps/embed/app/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/apps/embed/app/favicon.ico differ diff --git a/apps/embed/app/globals.css b/apps/embed/app/globals.css new file mode 100644 index 00000000..235a1426 --- /dev/null +++ b/apps/embed/app/globals.css @@ -0,0 +1,26 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.wrapper { + @apply mx-auto w-[92%] max-w-screen-xl; +} + +.wrapper-max { + @apply max-w-screen-2xl; +} + +.wrapper-sm { + @apply max-w-5xl; +} + +*, +*:before, +*:after { + box-sizing: border-box; +} + +* { + min-width: 0; + min-height: 0; +} diff --git a/apps/embed/app/layout.tsx b/apps/embed/app/layout.tsx new file mode 100644 index 00000000..7628e7cf --- /dev/null +++ b/apps/embed/app/layout.tsx @@ -0,0 +1,57 @@ +import "@/app/globals.css"; +import { Toaster } from "react-hot-toast"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: + "Cap โ€” Effortless, instant screen sharing. Open source and cross-platform.", + description: + "Cap is the open source alternative to Loom. Lightweight, powerful, and stunning. Record and share in seconds.", + openGraph: { + title: + "Cap โ€” Effortless, instant screen sharing. Open source and cross-platform.", + description: + "Cap is the open source alternative to Loom. Lightweight, powerful, and stunning. Record and share in seconds.", + type: "website", + url: "https://cap.so", + images: ["https://cap.so/og.png"], + }, +}; + +export default async function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + + + + + + + + + +

{children}
+ + + ); +} diff --git a/apps/embed/app/page.tsx b/apps/embed/app/page.tsx new file mode 100644 index 00000000..52c0113a --- /dev/null +++ b/apps/embed/app/page.tsx @@ -0,0 +1,7 @@ +export default async function EmbedPage() { + return ( +
+

Embed

+
+ ); +} diff --git a/apps/embed/app/view/[videoId]/Share.tsx b/apps/embed/app/view/[videoId]/Share.tsx new file mode 100644 index 00000000..5a10e2df --- /dev/null +++ b/apps/embed/app/view/[videoId]/Share.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { ShareVideo } from "./_components/ShareVideo"; +import { comments as commentsSchema, videos } from "@cap/database/schema"; +import { userSelectProps } from "@cap/database/auth/session"; + +export const Share = ({ + data, + user, + comments, +}: { + data: typeof videos.$inferSelect; + user: typeof userSelectProps | null; + comments: (typeof commentsSchema.$inferSelect)[]; +}) => { + return ( +
+ +
+ ); +}; diff --git a/apps/embed/app/view/[videoId]/_components/AudioPlayer.tsx b/apps/embed/app/view/[videoId]/_components/AudioPlayer.tsx new file mode 100644 index 00000000..b7eead87 --- /dev/null +++ b/apps/embed/app/view/[videoId]/_components/AudioPlayer.tsx @@ -0,0 +1,42 @@ +import { memo, forwardRef, useEffect } from "react"; +import Hls from "hls.js"; + +export const AudioPlayer = memo( + forwardRef void }>( + ({ src, onReady }, ref) => { + useEffect(() => { + const audio = ref as React.MutableRefObject; + + if (!audio.current) return; + + let hls: Hls | null = null; + + if (Hls.isSupported()) { + hls = new Hls(); + hls.loadSource(src); + hls.attachMedia(audio.current); + hls.on(Hls.Events.MANIFEST_PARSED, () => { + onReady(); + }); + } else if (audio.current.canPlayType("application/vnd.apple.mpegurl")) { + audio.current.src = src; + audio.current.addEventListener( + "loadedmetadata", + () => { + onReady(); + }, + { once: true } + ); + } + + return () => { + if (hls) { + hls.destroy(); + } + }; + }, [src, onReady, ref]); + + return