Welcome @name! Time to create Live from the Deck - a live streaming platform like Twitch! 📺
This will be a live streaming platform - video player, live chat, emotes, subscriptions, and clips.
YouTube:
TikTok:
Twitch:
1interface Stream {
2 id: string;
3 channelId: string;
4 channel: Channel;
5
6 // Stream info
7 title: string;
8 category: Category;
9 tags: string[];
10
11 // Video
12 videoUrl: string; // HLS or RTMP stream URL
13 thumbnailUrl: string;
14 quality: StreamQuality[];
15
16 // Metadata
17 isLive: boolean;
18 viewerCount: number;
19 startedAt: Date;
20 duration: number; // seconds (if VOD)
21
22 // Chat
23 chatEnabled: boolean;
24 slowMode: boolean; // limit message frequency
25 subscribersOnly: boolean;
26 emotesOnly: boolean;
27}
28
29interface Channel {
30 id: string;
31 username: string;
32 displayName: string;
33 avatar: string;
34 banner?: string;
35 description: string;
36
37 // Stats
38 followers: number;
39 subscribers: number;
40 totalViews: number;
41
42 // Monetization
43 isPartner: boolean;
44 isAffiliate: boolean;
45
46 // Current stream
47 isLive: boolean;
48 currentStream?: Stream;
49}
50
51interface StreamQuality {
52 label: string; // "1080p60", "720p60", "480p", "360p", "160p", "Audio only"
53 resolution: { width: number; height: number };
54 bitrate: number;
55 fps: number;
56}
57
58interface Category {
59 id: string;
60 name: string;
61 boxArtUrl: string;
62 viewerCount: number;
63 tags: string[];
64}1'use client';
2
3import { useState, useRef, useEffect } from 'react';
4
5interface StreamPlayerProps {
6 stream: Stream;
7 onQualityChange: (quality: string) => void;
8}
9
10export default function StreamPlayer({ stream, onQualityChange }: StreamPlayerProps) {
11 const [isPlaying, setIsPlaying] = useState(true);
12 const [isMuted, setIsMuted] = useState(false);
13 const [volume, setVolume] = useState(50);
14 const [selectedQuality, setSelectedQuality] = useState('1080p60');
15 const [isFullscreen, setIsFullscreen] = useState(false);
16 const videoRef = useRef<HTMLVideoElement>(null);
17
18 const togglePlay = () => {
19 if (videoRef.current) {
20 if (isPlaying) {
21 videoRef.current.pause();
22 } else {
23 videoRef.current.play();
24 }
25 setIsPlaying(!isPlaying);
26 }
27 };
28
29 const toggleMute = () => {
30 if (videoRef.current) {
31 videoRef.current.muted = !isMuted;
32 setIsMuted(!isMuted);
33 }
34 };
35
36 const handleVolumeChange = (newVolume: number) => {
37 if (videoRef.current) {
38 videoRef.current.volume = newVolume / 100;
39 setVolume(newVolume);
40 setIsMuted(newVolume === 0);
41 }
42 };
43
44 const changeQuality = (quality: string) => {
45 setSelectedQuality(quality);
46 onQualityChange(quality);
47 };
48
49 const toggleFullscreen = () => {
50 if (!document.fullscreenElement) {
51 videoRef.current?.requestFullscreen();
52 setIsFullscreen(true);
53 } else {
54 document.exitFullscreen();
55 setIsFullscreen(false);
56 }
57 };
58
59 return (
60 <div className="relative bg-black group">
61 {/* Video */}
62 <video
63 ref={videoRef}
64 src={stream.videoUrl}
65 className="w-full aspect-video"
66 autoPlay
67 playsInline
68 />
69
70 {/* Live Badge */}
71 {stream.isLive && (
72 <div className="absolute top-4 left-4 flex items-center gap-2">
73 <div className="px-2 py-1 bg-red-600 text-white text-xs font-bold rounded">
74 LIVE
75 </div>
76 <div className="px-2 py-1 bg-black bg-opacity-70 text-white text-xs rounded">
77 {stream.viewerCount.toLocaleString()} viewers
78 </div>
79 </div>
80 )}
81
82 {/* Controls Overlay */}
83 <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black to-transparent p-4 opacity-0 group-hover:opacity-100 transition-opacity">
84 <div className="flex items-center gap-4">
85 {/* Play/Pause */}
86 <button
87 onClick={togglePlay}
88 className="text-white text-2xl hover:scale-110 transition"
89 >
90 {isPlaying ? '⏸' : '▶'}
91 </button>
92
93 {/* Volume */}
94 <button
95 onClick={toggleMute}
96 className="text-white text-xl hover:scale-110 transition"
97 >
98 {isMuted || volume === 0 ? '🔇' : volume < 50 ? '🔉' : '🔊'}
99 </button>
100
101 <input
102 type="range"
103 min={0}
104 max={100}
105 value={isMuted ? 0 : volume}
106 onChange={(e) => handleVolumeChange(parseInt(e.target.value))}
107 className="w-20 h-1 bg-gray-600 rounded-lg"
108 />
109
110 <div className="flex-1" />
111
112 {/* Quality Selector */}
113 <div className="relative group/quality">
114 <button className="px-3 py-1 bg-gray-800 bg-opacity-80 text-white text-sm rounded hover:bg-opacity-100">
115 ⚙️ {selectedQuality}
116 </button>
117 <div className="absolute bottom-full right-0 mb-2 bg-gray-900 rounded overflow-hidden opacity-0 group-hover/quality:opacity-100 transition-opacity">
118 {stream.quality.map(q => (
119 <button
120 key={q.label}
121 onClick={() => changeQuality(q.label)}
122 className={`
123 block w-full text-left px-4 py-2 text-white text-sm hover:bg-purple-600
124 ${selectedQuality === q.label ? 'bg-purple-700' : ''}
125 `}
126 >
127 {q.label}
128 </button>
129 ))}
130 </div>
131 </div>
132
133 {/* Fullscreen */}
134 <button
135 onClick={toggleFullscreen}
136 className="text-white text-xl hover:scale-110 transition"
137 >
138 {isFullscreen ? '⛶' : '⛶'}
139 </button>
140 </div>
141 </div>
142 </div>
143 );
144}1interface ChatMessage {
2 id: string;
3 userId: string;
4 username: string;
5 displayName: string;
6 userBadges: Badge[];
7 message: string;
8 emotes: Emote[];
9 timestamp: Date;
10 isHighlighted: boolean; // paid highlight
11 isPinned: boolean;
12}
13
14interface Badge {
15 type: 'broadcaster' | 'moderator' | 'subscriber' | 'vip' | 'partner';
16 label: string;
17 imageUrl: string;
18}
19
20interface Emote {
21 id: string;
22 name: string; // e.g., "Kappa", "PogChamp"
23 imageUrl: string;
24 startIndex: number;
25 endIndex: number;
26}
27
28'use client';
29
30export default function LiveChat({ streamId }: { streamId: string }) {
31 const [messages, setMessages] = useState<ChatMessage[]>([]);
32 const [inputValue, setInputValue] = useState('');
33 const [showEmotePicker, setShowEmotePicker] = useState(false);
34
35 const sendMessage = () => {
36 if (!inputValue.trim()) return;
37
38 const newMessage: ChatMessage = {
39 id: Date.now().toString(),
40 userId: 'user123',
41 username: 'currentUser',
42 displayName: 'Current User',
43 userBadges: [],
44 message: inputValue,
45 emotes: [],
46 timestamp: new Date(),
47 isHighlighted: false,
48 isPinned: false
49 };
50
51 setMessages([...messages, newMessage]);
52 setInputValue('');
53 };
54
55 const parseMessageWithEmotes = (msg: ChatMessage) => {
56 if (msg.emotes.length === 0) return msg.message;
57
58 const parts: Array<{ text?: string; emote?: Emote }> = [];
59 let lastIndex = 0;
60
61 msg.emotes
62 .sort((a, b) => a.startIndex - b.startIndex)
63 .forEach(emote => {
64 if (emote.startIndex > lastIndex) {
65 parts.push({
66 text: msg.message.slice(lastIndex, emote.startIndex)
67 });
68 }
69
70 parts.push({ emote });
71 lastIndex = emote.endIndex;
72 });
73
74 if (lastIndex < msg.message.length) {
75 parts.push({ text: msg.message.slice(lastIndex) });
76 }
77
78 return parts.map((part, i) =>
79 part.emote ? (
80 <img
81 key={i}
82 src={part.emote.imageUrl}
83 alt={part.emote.name}
84 className="inline h-6 align-middle"
85 />
86 ) : (
87 <span key={i}>{part.text}</span>
88 )
89 );
90 };
91
92 return (
93 <div className="h-full bg-gray-900 flex flex-col">
94 {/* Chat Header */}
95 <div className="p-3 border-b border-gray-800">
96 <h3 className="text-white font-semibold">STREAM CHAT</h3>
97 </div>
98
99 {/* Messages */}
100 <div className="flex-1 overflow-y-auto p-3 space-y-2">
101 {messages.map(msg => (
102 <div
103 key={msg.id}
104 className={`
105 text-sm
106 ${msg.isHighlighted ? 'bg-purple-900 bg-opacity-30 p-2 rounded' : ''}
107 `}
108 >
109 {/* Badges */}
110 {msg.userBadges.map(badge => (
111 <img
112 key={badge.type}
113 src={badge.imageUrl}
114 alt={badge.label}
115 className="inline h-4 mr-1"
116 title={badge.label}
117 />
118 ))}
119
120 {/* Username */}
121 <span className="font-semibold text-purple-400">
122 {msg.displayName}:
123 </span>
124
125 {/* Message */}
126 <span className="text-white ml-1">
127 {parseMessageWithEmotes(msg)}
128 </span>
129 </div>
130 ))}
131 </div>
132
133 {/* Input */}
134 <div className="p-3 border-t border-gray-800">
135 <div className="flex gap-2">
136 <button
137 onClick={() => setShowEmotePicker(!showEmotePicker)}
138 className="p-2 text-gray-400 hover:text-white"
139 >
140 😀
141 </button>
142
143 <input
144 type="text"
145 value={inputValue}
146 onChange={(e) => setInputValue(e.target.value)}
147 onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
148 placeholder="Send a message"
149 className="flex-1 bg-gray-800 text-white px-3 py-2 rounded outline-none focus:ring-2 focus:ring-purple-500"
150 />
151
152 <button
153 onClick={sendMessage}
154 disabled={!inputValue.trim()}
155 className="px-4 py-2 bg-purple-600 text-white rounded font-semibold hover:bg-purple-700 disabled:bg-gray-700 disabled:cursor-not-allowed"
156 >
157 Chat
158 </button>
159 </div>
160 </div>
161 </div>
162 );
163}✅ Stream player - video playback, play/pause, volume ✅ Quality selector - 1080p60, 720p60, 480p, etc. ✅ Live badge - LIVE indicator + viewer count ✅ Fullscreen - toggle fullscreen mode ✅ Live chat - real-time messages ✅ Chat emotes - parse and display emotes ✅ User badges - broadcaster, mod, subscriber, VIP ✅ Highlighted messages - paid highlights
In the next exercise we'll add channel page and subscriptions!
See you soon! 🚀