From 41ef18dc60e66450c0d8c6ea6d7ece3c2baa525c Mon Sep 17 00:00:00 2001 From: Karol Konkol Date: Tue, 9 Jun 2026 19:11:24 +0200 Subject: [PATCH] Add live translation demo --- live-translation/.env.example | 6 + live-translation/.gitignore | 28 + live-translation/.yarnrc.yml | 1 + live-translation/README.md | 38 + live-translation/index.html | 18 + live-translation/package.json | 45 + live-translation/postcss.config.cjs | 6 + live-translation/public/avatar.svg | 7 + live-translation/public/favicon.svg | 5 + live-translation/public/fishjam-logo.svg | 10 + live-translation/public/gemini-logo.svg | 17 + .../src/components/DeviceSelect.tsx | 39 + .../src/components/VideoPlayer.tsx | 22 + .../src/components/moq/BrandHeader.tsx | 22 + .../src/components/moq/CallToolbar.tsx | 18 + .../src/components/moq/PublisherPanel.tsx | 127 + .../src/components/moq/RoomView.tsx | 167 + .../src/components/moq/VideoSurface.tsx | 130 + .../src/components/moq/VideoTile.tsx | 243 ++ .../src/components/moq/generateStreamName.ts | 72 + .../src/components/moq/quality.ts | 141 + .../src/components/moq/syncedPlayback.ts | 415 ++ live-translation/src/components/moq/types.ts | 48 + .../src/components/moq/useMoqConnection.ts | 238 ++ .../src/components/moq/useMoqStreamViewer.ts | 25 + .../src/components/moq/usePublisher.ts | 130 + .../src/components/moq/useSignalValue.ts | 21 + .../src/components/moq/useSyncedPlayback.ts | 59 + live-translation/src/components/moq/utils.ts | 186 + live-translation/src/components/ui/badge.tsx | 30 + live-translation/src/components/ui/button.tsx | 49 + live-translation/src/components/ui/card.tsx | 50 + live-translation/src/components/ui/label.tsx | 17 + live-translation/src/components/ui/select.tsx | 140 + live-translation/src/components/ui/sonner.tsx | 29 + live-translation/src/hooks/useWakeLock.ts | 16 + live-translation/src/index.css | 191 + live-translation/src/layout.tsx | 14 + live-translation/src/lib/utils.ts | 6 + live-translation/src/main.tsx | 44 + live-translation/src/pages/publish.tsx | 7 + live-translation/src/pages/watch.tsx | 75 + live-translation/src/vite-env.d.ts | 10 + live-translation/tailwind.config.cjs | 38 + live-translation/tsconfig.json | 29 + live-translation/vite.config.ts | 17 + live-translation/yarn.lock | 3331 +++++++++++++++++ 47 files changed, 6377 insertions(+) create mode 100644 live-translation/.env.example create mode 100644 live-translation/.gitignore create mode 100644 live-translation/.yarnrc.yml create mode 100644 live-translation/README.md create mode 100644 live-translation/index.html create mode 100644 live-translation/package.json create mode 100644 live-translation/postcss.config.cjs create mode 100644 live-translation/public/avatar.svg create mode 100644 live-translation/public/favicon.svg create mode 100644 live-translation/public/fishjam-logo.svg create mode 100644 live-translation/public/gemini-logo.svg create mode 100644 live-translation/src/components/DeviceSelect.tsx create mode 100644 live-translation/src/components/VideoPlayer.tsx create mode 100644 live-translation/src/components/moq/BrandHeader.tsx create mode 100644 live-translation/src/components/moq/CallToolbar.tsx create mode 100644 live-translation/src/components/moq/PublisherPanel.tsx create mode 100644 live-translation/src/components/moq/RoomView.tsx create mode 100644 live-translation/src/components/moq/VideoSurface.tsx create mode 100644 live-translation/src/components/moq/VideoTile.tsx create mode 100644 live-translation/src/components/moq/generateStreamName.ts create mode 100644 live-translation/src/components/moq/quality.ts create mode 100644 live-translation/src/components/moq/syncedPlayback.ts create mode 100644 live-translation/src/components/moq/types.ts create mode 100644 live-translation/src/components/moq/useMoqConnection.ts create mode 100644 live-translation/src/components/moq/useMoqStreamViewer.ts create mode 100644 live-translation/src/components/moq/usePublisher.ts create mode 100644 live-translation/src/components/moq/useSignalValue.ts create mode 100644 live-translation/src/components/moq/useSyncedPlayback.ts create mode 100644 live-translation/src/components/moq/utils.ts create mode 100644 live-translation/src/components/ui/badge.tsx create mode 100644 live-translation/src/components/ui/button.tsx create mode 100644 live-translation/src/components/ui/card.tsx create mode 100644 live-translation/src/components/ui/label.tsx create mode 100644 live-translation/src/components/ui/select.tsx create mode 100644 live-translation/src/components/ui/sonner.tsx create mode 100644 live-translation/src/hooks/useWakeLock.ts create mode 100644 live-translation/src/index.css create mode 100644 live-translation/src/layout.tsx create mode 100644 live-translation/src/lib/utils.ts create mode 100644 live-translation/src/main.tsx create mode 100644 live-translation/src/pages/publish.tsx create mode 100644 live-translation/src/pages/watch.tsx create mode 100644 live-translation/src/vite-env.d.ts create mode 100644 live-translation/tailwind.config.cjs create mode 100644 live-translation/tsconfig.json create mode 100644 live-translation/vite.config.ts create mode 100644 live-translation/yarn.lock diff --git a/live-translation/.env.example b/live-translation/.env.example new file mode 100644 index 0000000..52aa587 --- /dev/null +++ b/live-translation/.env.example @@ -0,0 +1,6 @@ +# Your Fishjam app ID, available at https://fishjam.io/app/ +VITE_FISHJAM_ID=your_fishjam_id + +# Optional: override the Media over QUIC relay URL. +# Defaults to https://moq.fishjam.work/public when unset. +# VITE_MOQ_URL=https://moq.fishjam.work/public diff --git a/live-translation/.gitignore b/live-translation/.gitignore new file mode 100644 index 0000000..d6f618b --- /dev/null +++ b/live-translation/.gitignore @@ -0,0 +1,28 @@ +# Dependencies +node_modules + +# Build output +dist +dist-ssr +*.local + +# Environment +.env + +# Yarn (Corepack fetches the version pinned in package.json) +.yarn + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor / OS +.idea +.vscode/* +!.vscode/extensions.json +.DS_Store +*.suo +*.sw? diff --git a/live-translation/.yarnrc.yml b/live-translation/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/live-translation/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/live-translation/README.md b/live-translation/README.md new file mode 100644 index 0000000..54d9fb6 --- /dev/null +++ b/live-translation/README.md @@ -0,0 +1,38 @@ +# Live Translation + +Live streaming with real-time AI translation — powered by [Fishjam](https://fishjam.io), +Gemini, and [Media over QUIC](https://moq.dev). A publisher broadcasts their camera and +microphone, and viewers can watch the stream with AI-generated audio translation in the +language of their choice. + +## Getting Started + +1. Copy `.env.example` to `.env` and fill in your Fishjam app ID: + + ```bash + cp .env.example .env + ``` + + You can obtain `VITE_FISHJAM_ID` by visiting https://fishjam.io/app/. + +2. Install dependencies: + + ```bash + yarn + ``` + +3. Start the development server: + + ```bash + yarn dev + ``` + +4. Open the printed local URL. The home page is the publisher; share the `watch/` + link with viewers to let them watch and pick a translation track. + +## Environment Variables + +| Variable | Required | Description | +| ------------------ | -------- | --------------------------------------------------------------------------- | +| `VITE_FISHJAM_ID` | Yes | Your Fishjam app ID, from https://fishjam.io/app/. | +| `VITE_MOQ_URL` | No | Override the Media over QUIC relay URL. Defaults to the built-in public relay. | diff --git a/live-translation/index.html b/live-translation/index.html new file mode 100644 index 0000000..5db9ce4 --- /dev/null +++ b/live-translation/index.html @@ -0,0 +1,18 @@ + + + + + + + Fishjam Live Translation + + + + + +
+ + + diff --git a/live-translation/package.json b/live-translation/package.json new file mode 100644 index 0000000..74e7b06 --- /dev/null +++ b/live-translation/package.json @@ -0,0 +1,45 @@ +{ + "name": "live-translation", + "private": true, + "version": "0.27.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@fishjam-cloud/react-client": "^0.27.0", + "@moq/publish": "0.2.11", + "@moq/signals": "^0.1.7", + "@moq/watch": "0.2.14", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-select": "^2.1.4", + "@radix-ui/react-slot": "^1.1.1", + "@svta/cml-utils": "1.4.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "lucide-react": "^0.476.0", + "next-themes": "^0.4.4", + "qrcode.react": "^4.2.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router": "^7.1.5", + "sonner": "^2.0.3", + "tailwind-merge": "^3.0.2", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@types/node": "^22.12.0", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react-swc": "^3.7.2", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.1", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.3", + "vite": "^6.0.11" + }, + "packageManager": "yarn@4.6.0" +} diff --git a/live-translation/postcss.config.cjs b/live-translation/postcss.config.cjs new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/live-translation/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/live-translation/public/avatar.svg b/live-translation/public/avatar.svg new file mode 100644 index 0000000..9dae71c --- /dev/null +++ b/live-translation/public/avatar.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/live-translation/public/favicon.svg b/live-translation/public/favicon.svg new file mode 100644 index 0000000..c64ac6d --- /dev/null +++ b/live-translation/public/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/live-translation/public/fishjam-logo.svg b/live-translation/public/fishjam-logo.svg new file mode 100644 index 0000000..cbae672 --- /dev/null +++ b/live-translation/public/fishjam-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/live-translation/public/gemini-logo.svg b/live-translation/public/gemini-logo.svg new file mode 100644 index 0000000..721d153 --- /dev/null +++ b/live-translation/public/gemini-logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/live-translation/src/components/DeviceSelect.tsx b/live-translation/src/components/DeviceSelect.tsx new file mode 100644 index 0000000..6a9c7a6 --- /dev/null +++ b/live-translation/src/components/DeviceSelect.tsx @@ -0,0 +1,39 @@ +import type { FC } from 'react'; + +import { Label } from './ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; + +type SelectableDevice = { + deviceId: string; + label: string; + kind?: string; +}; + +type Props = { + devices: SelectableDevice[]; + onSelectDevice: (deviceId: string) => void; + selectedDeviceId?: string; +}; + +export const DeviceSelect: FC = ({ devices, onSelectDevice, selectedDeviceId }) => { + const validDevices = devices.filter((device) => device.deviceId); + + if (!validDevices.length) { + return ; + } + + return ( + + ); +}; diff --git a/live-translation/src/components/VideoPlayer.tsx b/live-translation/src/components/VideoPlayer.tsx new file mode 100644 index 0000000..52ecc05 --- /dev/null +++ b/live-translation/src/components/VideoPlayer.tsx @@ -0,0 +1,22 @@ +import type { FC } from 'react'; +import { useEffect, useRef } from 'react'; + +interface VideoPlayerProps extends React.HTMLAttributes { + stream?: MediaStream | null; + peerId?: string; +} + +const VideoPlayer: FC = ({ stream, peerId, ...props }) => { + const videoRef = useRef(null); + + useEffect(() => { + if (!videoRef.current) { + return; + } + videoRef.current.srcObject = stream ?? null; + }, [stream]); + + return