Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Live Stream Player i Chat

Witaj @name! Czas stworzyć Live from the Deck - platformę live streamingu jak Twitch! 📺

To będzie live streaming platform - video player, live chat, emotes, subscriptions i clips.

Twitch vs YouTube/TikTok

YouTube:

  • Pre-recorded videos (upload → watch)
  • VOD (Video on Demand)
  • Long-form content

TikTok:

  • Short vertical videos
  • Algorithm-driven feed
  • Not primarily live

Twitch:

  • LIVE streaming (real-time broadcast)
  • Interactive chat with emotes
  • Subscriptions and bits (donations)
  • Categories and tags
  • Clips (highlight moments from streams)
  • VODs (recordings of past streams)
  • Raids (send viewers to another channel)
  • Follower/Subscriber system

Stream Structure

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}

Stream Player

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}

Live Chat

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="Wyślij wiadomość"
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}

Podsumowanie 🎓

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

W następnym ćwiczeniu dodamy channel page i subscriptions!

Do zobaczenia! 🚀

Przejdź do CodeWorlds