From d6afaf0bb2621f8eedacc0cd8493d0a6c212b73d Mon Sep 17 00:00:00 2001 From: Karol Konkol Date: Tue, 16 Jun 2026 19:08:32 +0200 Subject: [PATCH 1/7] Add translation demo --- translation-demo/.env.example | 2 + translation-demo/.gitignore | 27 + translation-demo/.yarnrc.yml | 15 + translation-demo/README.md | 28 + translation-demo/index.html | 18 + translation-demo/package.json | 46 + translation-demo/postcss.config.cjs | 6 + translation-demo/public/favicon.svg | 10 + translation-demo/public/fishjam-logo.svg | 10 + translation-demo/public/gemini-logo.svg | 17 + .../src/components/BrandHeader.tsx | 24 + .../src/components/DeviceSelect.tsx | 39 + .../src/components/StreamToolbar.tsx | 18 + .../src/components/VideoPlayer.tsx | 21 + .../src/components/moq/PublisherPanel.tsx | 124 + .../src/components/moq/StreamPlayer.tsx | 360 ++ .../src/components/moq/StreamView.tsx | 171 + .../src/components/moq/VideoSurface.tsx | 43 + .../src/components/moq/googleLanguages.ts | 110 + translation-demo/src/components/moq/types.ts | 38 + .../src/components/moq/useMoqConnection.ts | 297 ++ .../src/components/moq/useMoqStreamViewer.ts | 25 + .../src/components/moq/usePublisher.ts | 123 + .../src/components/moq/useSignalValue.ts | 21 + .../components/moq/useSyncedStreamPlayer.ts | 410 +++ .../moq/useTranslationTranscription.ts | 308 ++ translation-demo/src/components/moq/utils.ts | 139 + translation-demo/src/components/ui/badge.tsx | 30 + translation-demo/src/components/ui/button.tsx | 49 + translation-demo/src/components/ui/card.tsx | 50 + translation-demo/src/components/ui/label.tsx | 17 + translation-demo/src/components/ui/select.tsx | 107 + translation-demo/src/components/ui/sonner.tsx | 20 + translation-demo/src/hooks/useWakeLock.ts | 16 + translation-demo/src/index.css | 8 + translation-demo/src/layout.tsx | 14 + translation-demo/src/main.tsx | 19 + translation-demo/src/pages/publish.tsx | 7 + translation-demo/src/pages/watch.tsx | 73 + translation-demo/src/utils.ts | 6 + translation-demo/src/vite-env.d.ts | 10 + translation-demo/tailwind.config.cjs | 38 + translation-demo/tsconfig.json | 29 + translation-demo/vite.config.ts | 17 + translation-demo/yarn.lock | 3244 +++++++++++++++++ 45 files changed, 6204 insertions(+) create mode 100644 translation-demo/.env.example create mode 100644 translation-demo/.gitignore create mode 100644 translation-demo/.yarnrc.yml create mode 100644 translation-demo/README.md create mode 100644 translation-demo/index.html create mode 100644 translation-demo/package.json create mode 100644 translation-demo/postcss.config.cjs create mode 100644 translation-demo/public/favicon.svg create mode 100644 translation-demo/public/fishjam-logo.svg create mode 100644 translation-demo/public/gemini-logo.svg create mode 100644 translation-demo/src/components/BrandHeader.tsx create mode 100644 translation-demo/src/components/DeviceSelect.tsx create mode 100644 translation-demo/src/components/StreamToolbar.tsx create mode 100644 translation-demo/src/components/VideoPlayer.tsx create mode 100644 translation-demo/src/components/moq/PublisherPanel.tsx create mode 100644 translation-demo/src/components/moq/StreamPlayer.tsx create mode 100644 translation-demo/src/components/moq/StreamView.tsx create mode 100644 translation-demo/src/components/moq/VideoSurface.tsx create mode 100644 translation-demo/src/components/moq/googleLanguages.ts create mode 100644 translation-demo/src/components/moq/types.ts create mode 100644 translation-demo/src/components/moq/useMoqConnection.ts create mode 100644 translation-demo/src/components/moq/useMoqStreamViewer.ts create mode 100644 translation-demo/src/components/moq/usePublisher.ts create mode 100644 translation-demo/src/components/moq/useSignalValue.ts create mode 100644 translation-demo/src/components/moq/useSyncedStreamPlayer.ts create mode 100644 translation-demo/src/components/moq/useTranslationTranscription.ts create mode 100644 translation-demo/src/components/moq/utils.ts create mode 100644 translation-demo/src/components/ui/badge.tsx create mode 100644 translation-demo/src/components/ui/button.tsx create mode 100644 translation-demo/src/components/ui/card.tsx create mode 100644 translation-demo/src/components/ui/label.tsx create mode 100644 translation-demo/src/components/ui/select.tsx create mode 100644 translation-demo/src/components/ui/sonner.tsx create mode 100644 translation-demo/src/hooks/useWakeLock.ts create mode 100644 translation-demo/src/index.css create mode 100644 translation-demo/src/layout.tsx create mode 100644 translation-demo/src/main.tsx create mode 100644 translation-demo/src/pages/publish.tsx create mode 100644 translation-demo/src/pages/watch.tsx create mode 100644 translation-demo/src/utils.ts create mode 100644 translation-demo/src/vite-env.d.ts create mode 100644 translation-demo/tailwind.config.cjs create mode 100644 translation-demo/tsconfig.json create mode 100644 translation-demo/vite.config.ts create mode 100644 translation-demo/yarn.lock diff --git a/translation-demo/.env.example b/translation-demo/.env.example new file mode 100644 index 0000000..a4f5498 --- /dev/null +++ b/translation-demo/.env.example @@ -0,0 +1,2 @@ +# Required: URL of the MoQ relay to connect to. +VITE_MOQ_URL= diff --git a/translation-demo/.gitignore b/translation-demo/.gitignore new file mode 100644 index 0000000..09a7283 --- /dev/null +++ b/translation-demo/.gitignore @@ -0,0 +1,27 @@ +# Dependencies +node_modules + +# Build output +dist +dist-ssr +*.local + +# Env +.env + +# Yarn (Berry) +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Logs +*.log + +# Editor / OS +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store diff --git a/translation-demo/.yarnrc.yml b/translation-demo/.yarnrc.yml new file mode 100644 index 0000000..5628a4a --- /dev/null +++ b/translation-demo/.yarnrc.yml @@ -0,0 +1,15 @@ +nodeLinker: node-modules + +packageExtensions: + "@moq/hang@0.2.10": + dependencies: + "@svta/cml-utils": "1.5.0" + "@moq/loc@0.1.1": + dependencies: + zod: "^4.4.3" + "@moq/publish@0.2.15": + dependencies: + zod: "^4.4.3" + "@moq/watch@0.2.17": + dependencies: + zod: "^4.4.3" diff --git a/translation-demo/README.md b/translation-demo/README.md new file mode 100644 index 0000000..c88ea36 --- /dev/null +++ b/translation-demo/README.md @@ -0,0 +1,28 @@ +# Translation Demo + +An example React app that streams over the MoQ protocol with Fishjam, with live audio translation and captions for the viewer. + +## Getting Started + +Install dependencies: + +```bash +yarn +``` + +Configure the MoQ relay (required): + +```bash +cp .env.example .env +# then set VITE_MOQ_URL in .env to your MoQ relay URL +``` + +Start the development server: + +```bash +yarn dev +``` + +## Environment Variables + +- `VITE_MOQ_URL` (required) — URL of the MoQ relay to connect to. The app has no built-in default and will not connect until this is set. diff --git a/translation-demo/index.html b/translation-demo/index.html new file mode 100644 index 0000000..2867acc --- /dev/null +++ b/translation-demo/index.html @@ -0,0 +1,18 @@ + + + + + + + Translation Demo + + + + + +
+ + + diff --git a/translation-demo/package.json b/translation-demo/package.json new file mode 100644 index 0000000..8f007ff --- /dev/null +++ b/translation-demo/package.json @@ -0,0 +1,46 @@ +{ + "name": "@moq/translation-demo", + "private": true, + "version": "0.1.0", + "description": "MoQ streaming demo with live audio translation and captions", + "license": "(MIT OR Apache-2.0)", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@moq/publish": "0.2.15", + "@moq/signals": "0.1.9", + "@moq/watch": "0.2.17", + "@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.5.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "lucide-react": "^0.476.0", + "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", + "unique-names-generator": "^4.7.1" + }, + "devDependencies": { + "@types/node": "^22.12.0", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.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/translation-demo/postcss.config.cjs b/translation-demo/postcss.config.cjs new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/translation-demo/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/translation-demo/public/favicon.svg b/translation-demo/public/favicon.svg new file mode 100644 index 0000000..c5971d1 --- /dev/null +++ b/translation-demo/public/favicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/translation-demo/public/fishjam-logo.svg b/translation-demo/public/fishjam-logo.svg new file mode 100644 index 0000000..cbae672 --- /dev/null +++ b/translation-demo/public/fishjam-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/translation-demo/public/gemini-logo.svg b/translation-demo/public/gemini-logo.svg new file mode 100644 index 0000000..721d153 --- /dev/null +++ b/translation-demo/public/gemini-logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/translation-demo/src/components/BrandHeader.tsx b/translation-demo/src/components/BrandHeader.tsx new file mode 100644 index 0000000..af24bc1 --- /dev/null +++ b/translation-demo/src/components/BrandHeader.tsx @@ -0,0 +1,24 @@ +import { cn } from '@/utils'; + +type Props = { + className?: string; +}; + +// Fishjam x Gemini lockup, shown centered at the top of every stream view. Both logos +// render at the same height; the Gemini wordmark sits a touch low in its viewBox, so a +// small upward nudge keeps it baseline-aligned with the Fishjam mark. A short tagline +// sits underneath to explain what the demo does. +export const BrandHeader = ({ className }: Props) => ( +
+
+ + Fishjam + + × + Gemini +
+

+ Live streaming with real-time AI translation — powered by Fishjam, Gemini, and Media over QUIC. +

+
+); diff --git a/translation-demo/src/components/DeviceSelect.tsx b/translation-demo/src/components/DeviceSelect.tsx new file mode 100644 index 0000000..6a9c7a6 --- /dev/null +++ b/translation-demo/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/translation-demo/src/components/StreamToolbar.tsx b/translation-demo/src/components/StreamToolbar.tsx new file mode 100644 index 0000000..0e2696b --- /dev/null +++ b/translation-demo/src/components/StreamToolbar.tsx @@ -0,0 +1,18 @@ +import { Button } from '@/components/ui/button'; + +type Props = { + onDisconnect: () => void; +}; + +export const StreamToolbar = ({ onDisconnect }: Props) => { + return ( +
+ +
+ ); +}; diff --git a/translation-demo/src/components/VideoPlayer.tsx b/translation-demo/src/components/VideoPlayer.tsx new file mode 100644 index 0000000..f9439c6 --- /dev/null +++ b/translation-demo/src/components/VideoPlayer.tsx @@ -0,0 +1,21 @@ +import type { FC } from 'react'; +import { useEffect, useRef } from 'react'; + +interface VideoPlayerProps extends React.HTMLAttributes { + stream?: MediaStream | null; +} + +const VideoPlayer: FC = ({ stream, ...props }) => { + const videoRef = useRef(null); + + useEffect(() => { + if (!videoRef.current) { + return; + } + videoRef.current.srcObject = stream ?? null; + }, [stream]); + + return