|
1 | | -"use client" |
| 1 | +"use client"; |
2 | 2 |
|
| 3 | +import PageHeader from "@/components/PageHeader"; |
3 | 4 | import WhepClientVideo from "@/components/WhepClientVideo"; |
4 | 5 | import { WhipClient } from "@/utils/whip-client"; |
5 | 6 | import { useRef, useState } from "react"; |
6 | 7 |
|
7 | 8 | // Base url of a WHIP/WHEP server. By default, Smelter exposes this server on |
8 | 9 | // port 9000, but the value can be changed via SMELTER_WHIP_WHEP_SERVER_PORT |
9 | 10 | // environment variable. |
10 | | -const SMELTER_WHIP_WHEP_URL = new URL("http://127.0.0.1:9000") |
11 | | - |
12 | | -const WHIP_AUTH_TOKEN = "example_token" |
| 11 | +const SMELTER_WHIP_WHEP_URL = new URL("http://127.0.0.1:9000"); |
13 | 12 |
|
14 | 13 | // API of Node.js server from `/server` directory. |
15 | | -const BACKEND_URL = new URL("http://127.0.0.1:3001") |
| 14 | +const BACKEND_URL = new URL("http://127.0.0.1:3001"); |
16 | 15 |
|
17 | | -export default function Home() { |
18 | | - return ( |
19 | | - <div className="flex min-h-screen items-center justify-center bg-zinc-800 font-sans"> |
20 | | - <main className="flex flex-row min-h-screen justify-between py-32 px-16"> |
21 | | - <WhepClientVideo |
22 | | - url={new URL("/whep/output", SMELTER_WHIP_WHEP_URL).toString()} |
23 | | - poster="https://placehold.co/1920x1080/000000/333333?text=Waiting+for+stream..." |
24 | | - playsInline autoPlay controls |
25 | | - className='min-w-0 min-h-0 w-full h-full object-cover bg-black' |
26 | | - /> |
27 | | - <Controls /> |
28 | | - </main> |
29 | | - </div> |
30 | | - ); |
31 | | -} |
| 16 | +const WHIP_AUTH_TOKEN = "example_token"; |
32 | 17 |
|
33 | | -function Controls() { |
| 18 | +type Connection = "camera" | "screen-share" | "none"; |
| 19 | + |
| 20 | +export default function StreamerPage() { |
34 | 21 | const clientRef = useRef<WhipClient>(new WhipClient()); |
35 | | - const [connection, setConnection] = useState<'camera' | 'screen-share' | 'none'>('none'); |
36 | | - const [showInstructions, setShowInstruction] = useState(true); |
| 22 | + const [connection, setConnection] = useState<Connection>("none"); |
| 23 | + const [showInstructions, setShowInstructions] = useState(true); |
37 | 24 |
|
38 | 25 | const toggleCamera = async () => { |
39 | | - setConnection('none') |
| 26 | + setConnection("none"); |
40 | 27 | await clientRef.current.close(); |
41 | | - if (connection !== 'camera') { |
| 28 | + if (connection !== "camera") { |
42 | 29 | const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); |
43 | 30 | await clientRef.current.connect(stream, new URL("/whip/input", SMELTER_WHIP_WHEP_URL), WHIP_AUTH_TOKEN); |
44 | | - setConnection('camera') |
| 31 | + setConnection("camera"); |
45 | 32 | } |
46 | | - } |
| 33 | + }; |
47 | 34 |
|
48 | 35 | const toggleScreenShare = async () => { |
49 | | - setConnection('none') |
| 36 | + setConnection("none"); |
50 | 37 | await clientRef.current.close(); |
51 | | - if (connection !== 'screen-share') { |
| 38 | + if (connection !== "screen-share") { |
52 | 39 | const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }); |
53 | 40 | await clientRef.current.connect(stream, new URL("/whip/input", SMELTER_WHIP_WHEP_URL), WHIP_AUTH_TOKEN); |
54 | | - setConnection('screen-share') |
| 41 | + setConnection("screen-share"); |
55 | 42 | } |
56 | | - } |
| 43 | + }; |
57 | 44 |
|
58 | 45 | const toggleInstructions = async () => { |
59 | | - setShowInstruction(!showInstructions) |
| 46 | + setShowInstructions(!showInstructions); |
60 | 47 | await fetch(new URL("/layout-update", BACKEND_URL), { |
61 | | - method: 'POST', |
62 | | - mode: 'cors', |
63 | | - headers: { |
64 | | - 'content-type': 'application/json', |
65 | | - }, |
| 48 | + method: "POST", |
| 49 | + mode: "cors", |
| 50 | + headers: { "content-type": "application/json" }, |
66 | 51 | body: JSON.stringify({ showInstructions: !showInstructions }), |
67 | 52 | }); |
68 | | - } |
| 53 | + }; |
69 | 54 |
|
70 | 55 | return ( |
71 | | - <div className="w-1/3 p-10 flex flex-col items-start"> |
72 | | - <button className="bg-purple-800 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded mb-10 w-full" onClick={toggleScreenShare}> |
73 | | - {connection === 'screen-share' ? 'Stop' : 'Start'} screen share |
74 | | - </button> |
75 | | - <button className="bg-purple-800 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded mb-10 w-full" onClick={toggleCamera}> |
76 | | - {connection === 'camera' ? 'Stop' : 'Start'} camera |
77 | | - </button> |
78 | | - <Checkbox description="Show instructions" isChecked={showInstructions} onChange={toggleInstructions} /> |
79 | | - </div> |
80 | | - ) |
81 | | -} |
| 56 | + <div className="min-h-screen bg-background text-foreground font-sans"> |
| 57 | + <PageHeader |
| 58 | + title="Streamer" |
| 59 | + statusDot={connection === "none" ? "bg-muted" : "bg-red-500 animate-pulse"} |
| 60 | + statusText={connection === "none" ? "Not streaming" : connection === "camera" ? "Camera" : "Screen"} |
| 61 | + /> |
82 | 62 |
|
83 | | -function Checkbox(props: { description: string, isChecked: boolean, onChange: (update: boolean) => void }) { |
84 | | - return ( |
85 | | - <div className="flex items-start gap-3 p-4 border border-slate-200 rounded-lg transition-colors cursor-pointer mb-10 w-full" |
86 | | - onClick={() => props.onChange(!props.isChecked)}> |
| 63 | + <main className="max-w-6xl mx-auto px-6 py-8"> |
| 64 | + <div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> |
| 65 | + <div className="lg:col-span-2"> |
| 66 | + <div className="bg-card border border-border rounded-lg overflow-hidden"> |
| 67 | + <div className="aspect-video bg-black"> |
| 68 | + <WhepClientVideo |
| 69 | + url={new URL("/whep/output", SMELTER_WHIP_WHEP_URL).toString()} |
| 70 | + poster="https://placehold.co/1920x1080/0f0f0f/27272a?text=Preview" |
| 71 | + playsInline autoPlay controls |
| 72 | + className="w-full h-full object-contain" |
| 73 | + /> |
| 74 | + </div> |
| 75 | + </div> |
| 76 | + <p className="mt-3 text-sm text-muted text-center">Output preview</p> |
| 77 | + </div> |
87 | 78 |
|
88 | | - <div className="flex items-center h-5"> |
89 | | - <input |
90 | | - type="checkbox" |
91 | | - checked={props.isChecked} |
92 | | - onChange={() => props.onChange(!props.isChecked)} |
93 | | - className="w-4 h-4 text-purple-800 border-gray-300 rounded focus:ring-purple-700 cursor-pointer" |
94 | | - /> |
95 | | - </div> |
| 79 | + <div className="space-y-4"> |
| 80 | + <div className="bg-card border border-border rounded-lg p-5"> |
| 81 | + <h2 className="text-sm font-medium text-muted uppercase tracking-wide mb-4">Source</h2> |
| 82 | + <div className="space-y-3"> |
| 83 | + <button onClick={toggleScreenShare} className={`w-full py-3 px-4 rounded-lg font-medium transition-all ${connection === "screen-share" ? "bg-red-500/20 text-red-400 border border-red-500/30 hover:bg-red-500/30" : "bg-accent hover:bg-accent-hover text-white"}`}> |
| 84 | + {connection === "screen-share" ? "Stop" : "Start"} Screen Share |
| 85 | + </button> |
| 86 | + <button onClick={toggleCamera} className={`w-full py-3 px-4 rounded-lg font-medium transition-all ${connection === "camera" ? "bg-red-500/20 text-red-400 border border-red-500/30 hover:bg-red-500/30" : "bg-accent hover:bg-accent-hover text-white"}`}> |
| 87 | + {connection === "camera" ? "Stop" : "Start"} Camera |
| 88 | + </button> |
| 89 | + </div> |
| 90 | + </div> |
96 | 91 |
|
97 | | - <div className="flex flex-col"> |
98 | | - <label |
99 | | - className="font-medium text-slate-900 cursor-pointer text-white" |
100 | | - > |
101 | | - {props.description} |
102 | | - </label> |
103 | | - </div> |
| 92 | + <div className="bg-card border border-border rounded-lg p-5"> |
| 93 | + <h2 className="text-sm font-medium text-muted uppercase tracking-wide mb-4">Overlay</h2> |
| 94 | + <label className="flex items-center justify-between cursor-pointer"> |
| 95 | + <span>Show instructions</span> |
| 96 | + <div |
| 97 | + className={`relative w-11 h-6 rounded-full transition-colors ${showInstructions ? "bg-accent" : "bg-border"}`} |
| 98 | + onClick={toggleInstructions} |
| 99 | + > |
| 100 | + <div className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${showInstructions ? "translate-x-6" : "translate-x-1"}`} /> |
| 101 | + </div> |
| 102 | + </label> |
| 103 | + </div> |
| 104 | + </div> |
| 105 | + </div> |
| 106 | + </main> |
104 | 107 | </div> |
105 | 108 | ); |
106 | | -}; |
| 109 | +} |
0 commit comments