DEV Community

Cover image for How to Build Sites like StreamEast
Stephen568hub
Stephen568hub

Posted on

How to Build Sites like StreamEast

StreamEast has become one of the most popular free sports streaming platforms online. Millions of people use it daily to watch live games without paying subscription fees or creating accounts. The site attracts users because it offers instant access to major sports like the NFL, NBA, and UFC through a clean, simple interface.

You may want to build a similar streaming application to serve sports fans who prefer free alternatives to expensive cable packages. Creating your own streaming app requires understanding how video players work, building user-friendly navigation, and handling live content delivery.

This guide shows you step-by-step how to develop a streaming application with the same core features that make StreamEast successful with its massive user base.

How to Develop Websites Like StreamEast

Building a sports streaming platform similar to StreamEast requires robust real-time video technology and seamless user experience. StreamEast's success comes from delivering live sports content instantly without complex registration processes, supporting thousands of concurrent viewers across multiple sports categories.

ZEGOCLOUD's live streaming SDK provides the infrastructure you need to build this functionality without wrestling with complex video streaming protocols.

Prerequisites

To follow along with this tutorial, you'll need the following prerequisites ready or available to set up during development:

  • A ZEGOCLOUD developer account – sign up
  • AppID and ServerSecret from ZEGOCLOUD Admin Console.
  • Knowledge of React and JavaScript.
  • Node.js installed (version 16 or higher).
  • A modern web browser for testing.

If you're ready with these, let's start building!

1. Setting Up Your Development Environment

Begin by creating a fresh React application optimized for fast development. Vite offers superior performance compared to traditional create-react-app, making it perfect for media-heavy applications like streaming platforms.

npm create vite@latest streameast-clone -- --template react
cd streameast-clone
npm install
Enter fullscreen mode Exit fullscreen mode

2. Integrating Live Streaming Capabilities

Your streaming platform needs ZEGOCLOUD's Express SDK to handle live video transmission. Install the web SDK that provides WebRTC functionality for browser-based streaming.

npm install zego-express-engine-webrtc
Enter fullscreen mode Exit fullscreen mode

After installation, organize your project structure to support streaming components and video management:

sports-streaming-site/
├── public/
├── src/
│   ├── components/
│   │   ├── VideoPlayer/
│   │   │   └── VideoPlayer.jsx
│   │   ├── SportsList/
│   │   │   └── SportsList.jsx
│   │   └── StreamBrowser/
│   │       └── StreamBrowser.jsx
│   ├── utils/
│   │   └── config.js
│   ├── App.jsx
│   ├── App.css
│   └── main.jsx
├── package.json
└── vite.config.js
Enter fullscreen mode Exit fullscreen mode

3. Building Your Configuration Foundation

Create a centralized configuration system that manages your ZEGOCLOUD credentials and application data. Keeping sensitive information organized while providing mock data for development testing makes the development process smoother. We’ll be doing this in the src/utils/config.js file:

export const STREAMING_CONFIG = {
    appID: your_zegocloud_app_id,
    serverSecret: "your_zegocloud_server_secret",
    serverURL: "wss://webliveroom-api.zego.im/ws"
};

export function createUserToken(userID, roomID) {
    const testTokens = {
        "sports_viewer_1": "development_token_here",
        "sports_viewer_2": "development_token_here", 
        "broadcaster_main": "development_token_here"
    };

    return testTokens[userID] || "";
}

export const SPORTS_DATA = [
    { id: 'football', displayName: 'NFL Football', emoji: '🏈', color: '#1a472a' },
    { id: 'basketball', displayName: 'NBA Basketball', emoji: '🏀', color: '#7c2d12' },
    { id: 'fighting', displayName: 'UFC & Boxing', emoji: '🥊', color: '#7c1d24' },
    { id: 'baseball', displayName: 'MLB Baseball', emoji: '', color: '#1e3a8a' },
    { id: 'hockey', displayName: 'NHL Hockey', emoji: '🏒', color: '#374151' },
    { id: 'soccer', displayName: 'World Soccer', emoji: '', color: '#166534' }
];

export const DEMO_STREAMS = [
    {
        streamID: 'nfl_chiefs_bills',
        eventTitle: 'Kansas City Chiefs vs Buffalo Bills - AFC Championship',
        sport: 'football',
        currentViewers: 127543,
        status: 'live',
        quality: '1080p'
    },
    {
        streamID: 'nba_lakers_celtics', 
        eventTitle: 'Los Angeles Lakers vs Boston Celtics - Finals Game 7',
        sport: 'basketball',
        currentViewers: 89234,
        status: 'live',
        quality: '1080p'
    },
    {
        streamID: 'ufc_main_event',
        eventTitle: 'UFC 301 Championship Fight - Main Event',
        sport: 'fighting', 
        currentViewers: 156789,
        status: 'live',
        quality: '4K'
    }
];
Enter fullscreen mode Exit fullscreen mode

4. Creating Your Sports Navigation Component

Build the navigation header that handles sports category filtering. Navigation becomes the gateway for users to discover different types of live sports content. Create or update src/components/SportsList/SportsList.jsx:

import React from 'react';
import { SPORTS_DATA } from '../../utils/config';

const SportsList = ({ selectedSport, onSportSelect }) => {
  return (
    <nav className="main-navigation">
      <div className="nav-container">
        <div className="brand-section">
          <h1>🎯 LiveSports</h1>
          <span>Watch Live Sports Free</span>
        </div>

        <div className="sports-menu">
          <button 
            className={`sport-button ${!selectedSport ? 'selected' : ''}`}
            onClick={() => onSportSelect(null)}
          >
            All Games
          </button>
          {SPORTS_DATA.map(sport => (
            <button
              key={sport.id}
              className={`sport-button ${selectedSport === sport.id ? 'selected' : ''}`}
              onClick={() => onSportSelect(sport.id)}
              style={{ '--accent-color': sport.color }}
            >
              {sport.emoji} {sport.displayName}
            </button>
          ))}
        </div>
      </div>
    </nav>
  );
};

export default SportsList;
Enter fullscreen mode Exit fullscreen mode

The sports navigation component above provides intuitive filtering while maintaining visual consistency through dynamic styling. Each sport category gets its own unique color accent that creates visual hierarchy and helps users quickly identify their preferred content type.

5. Developing Your Stream Browser Component

Build the stream discovery interface that displays available live sports content. Users need to quickly browse and select streams based on their interests. Create src/components/StreamBrowser/StreamBrowser.jsx:

import React from 'react';
import { SPORTS_DATA } from '../../utils/config';

const StreamPreview = ({ streamData, onWatchStream }) => {
  const sportInfo = SPORTS_DATA.find(sport => sport.id === streamData.sport);

  return (
    <article className="stream-preview" onClick={() => onWatchStream(streamData)}>
      <div className="preview-video">
        <div className="sport-icon" style={{ backgroundColor: sportInfo?.color }}>
          {sportInfo?.emoji}
        </div>
        {streamData.status === 'live' && (
          <div className="live-indicator">🔴 LIVE</div>
        )}
        <div className="viewer-badge">
          👀 {streamData.currentViewers.toLocaleString()}
        </div>
      </div>

      <div className="stream-details">
        <h3>{streamData.eventTitle}</h3>
        <div className="stream-meta">
          <span className="sport-name">{sportInfo?.displayName}</span>
          <span className="quality-badge">{streamData.quality}</span>
        </div>
      </div>
    </article>
  );
};

const StreamBrowser = ({ availableStreams, onStreamWatch }) => {
  return (
    <section className="stream-gallery">
      {availableStreams.length === 0 ? (
        <div className="empty-gallery">
          <h2>No Live Streams Available</h2>
          <p>Check back soon for upcoming sports events and live games.</p>
        </div>
      ) : (
        <div className="streams-grid">
          {availableStreams.map(stream => (
            <StreamPreview
              key={stream.streamID}
              streamData={stream}
              onWatchStream={onStreamWatch}
            />
          ))}
        </div>
      )}
    </section>
  );
};

export default StreamBrowser;
Enter fullscreen mode Exit fullscreen mode

Each stream preview card showcases essential information like live status, viewer count, and video quality. The component structure separates individual stream cards from the overall gallery layout, making the code more maintainable and reusable across different parts of your application.

6. Building Your Video Player Component

Here, we’ll build the core video player that integrates with ZEGOCLOUD's streaming technology. Your video player needs to handle connection states, stream quality, and user controls seamlessly.

Create or update your src/components/VideoPlayer/VideoPlayer.jsx file if you haven’t created that yet:

import React, { useState, useEffect, useRef } from 'react';
import { ZegoExpressEngine } from 'zego-express-engine-webrtc';
import { STREAMING_CONFIG, SPORTS_DATA } from '../../utils/config';

const VideoPlayer = ({ streamInfo, onReturnHome }) => {
  const videoContainerRef = useRef(null);
  const [streamEngine, setStreamEngine] = useState(null);
  const [connectionState, setConnectionState] = useState('initializing');
  const [playbackError, setPlaybackError] = useState(null);

  useEffect(() => {
    establishStreamConnection();
    return () => {
      disconnectStream();
    };
  }, [streamInfo]);

  const establishStreamConnection = async () => {
    try {
      setConnectionState('connecting');
      setPlaybackError(null);

      const engine = new ZegoExpressEngine(
        STREAMING_CONFIG.appID, 
        STREAMING_CONFIG.serverURL
      );

      engine.on('roomStateUpdate', (roomID, state, errorCode) => {
        if (state === 'CONNECTED') {
          setConnectionState('connected');
        } else if (state === 'DISCONNECTED') {
          setConnectionState('disconnected');
        }
      });

      engine.on('roomStreamUpdate', async (roomID, updateType, streamList) => {
        if (updateType === 'ADD' && streamList.length > 0) {
          try {
            const incomingStream = await engine.startPlayingStream(streamList[0].streamID);
            if (videoContainerRef.current) {
              incomingStream.play(videoContainerRef.current);
              setConnectionState('streaming');
            }
          } catch (streamError) {
            setPlaybackError('Unable to start video playback');
          }
        } else if (updateType === 'DELETE') {
          setPlaybackError('Stream has ended');
        }
      });

      const viewerID = `viewer_${Date.now()}`;
      const roomIdentifier = streamInfo.streamID;

      await engine.loginRoom(roomIdentifier, {
        userID: viewerID,
        userName: `SportsFan${viewerID.slice(-4)}`
      });

      setStreamEngine(engine);

      setTimeout(() => {
        if (connectionState === 'connecting') {
          setConnectionState('demo');
        }
      }, 4000);

    } catch (connectionError) {
      setPlaybackError('Connection to stream failed');
      setConnectionState('error');
    }
  };

  const disconnectStream = async () => {
    if (streamEngine) {
      try {
        await streamEngine.logoutRoom();
        streamEngine.destroyEngine();
      } catch (disconnectError) {
        console.error('Clean disconnect failed:', disconnectError);
      }
    }
  };

  const renderStreamStatus = () => {
    if (connectionState === 'connecting') {
      return (
        <div className="stream-status connecting">
          <div className="connection-spinner"></div>
          <h3>Connecting to Live Stream</h3>
          <p>Establishing connection to {streamInfo.eventTitle}</p>
        </div>
      );
    }

    if (playbackError) {
      return (
        <div className="stream-status error">
          <h3>⚠️ Stream Unavailable</h3>
          <p>{playbackError}</p>
          <p className="demo-note">In production, live video would stream here</p>
        </div>
      );
    }

    if (connectionState === 'demo') {
      return (
        <div className="stream-status demo">
          <h3>🎬 Demo Stream Interface</h3>
          <p>Live {streamInfo.sport.toUpperCase()} stream would play here</p>
          <p>Connected to room: {streamInfo.streamID}</p>
          <p className="technical-info">Resolution: {streamInfo.quality} | Viewers: {streamInfo.currentViewers.toLocaleString()}</p>
        </div>
      );
    }

    return null;
  };

  const sportDetails = SPORTS_DATA.find(sport => sport.id === streamInfo.sport);

  return (
    <div className="stream-player-page">
      <header className="player-navigation">
        <button onClick={onReturnHome} className="return-button">
           Back to Streams
        </button>
        <div className="current-stream-info">
          <h2>{streamInfo.eventTitle}</h2>
          <div className="live-stats">
            🔴 LIVE  {streamInfo.currentViewers.toLocaleString()} watching
          </div>
        </div>
      </header>

      <main className="video-player-section">
        <div className="player-frame">
          <div className="video-display" ref={videoContainerRef}>
            {renderStreamStatus()}
          </div>

          <div className="playback-controls">
            <div className="stream-options">
              <label htmlFor="quality-select">Video Quality:</label>
              <select id="quality-select" defaultValue={streamInfo.quality}>
                <option value="4K">4K Ultra HD</option>
                <option value="1080p">1080p Full HD</option>
                <option value="720p">720p HD</option>
                <option value="480p">480p Standard</option>
              </select>
            </div>

            <div className="connection-info">
              <span>📊 Status: {connectionState}</span>
              <span>👥 {streamInfo.currentViewers.toLocaleString()} viewers</span>
            </div>
          </div>
        </div>

        <aside className="event-information">
          <h3>Event Details</h3>
          <dl className="event-details">
            <dt>Match:</dt>
            <dd>{streamInfo.eventTitle}</dd>

            <dt>Sport:</dt>
            <dd>{sportDetails?.displayName}</dd>

            <dt>Status:</dt>
            <dd className="live-status">
              {streamInfo.status === 'live' ? '🔴 Broadcasting Live' : '⏸️ Stream Offline'}
            </dd>

            <dt>Quality:</dt>
            <dd>{streamInfo.quality} Resolution</dd>

            <dt>Viewers:</dt>
            <dd>{streamInfo.currentViewers.toLocaleString()} currently watching</dd>
          </dl>
        </aside>
      </main>
    </div>
  );
};

export default VideoPlayer;
Enter fullscreen mode Exit fullscreen mode

Your video player component handles the complete streaming lifecycle from initial connection through active playback and graceful disconnection. The ZEGOCLOUD SDK integration manages the complex WebRTC protocols while your React component provides user-friendly status updates and controls.

7. Assembling Your Main Application

Bring together all your components in the main application file that orchestrates navigation and state management between different views. Replace the contents of src/App.jsx:

import React, { useState, useEffect } from 'react';
import VideoPlayer from './components/VideoPlayer/VideoPlayer';
import StreamBrowser from './components/StreamBrowser/StreamBrowser';
import SportsList from './components/SportsList/SportsList';
import { SPORTS_DATA, DEMO_STREAMS } from './utils/config';
import './App.css';

const StreamHomepage = ({ activeSport, onStreamSelect }) => {
  const [visibleStreams, setVisibleStreams] = useState(DEMO_STREAMS);

  useEffect(() => {
    if (activeSport) {
      setVisibleStreams(DEMO_STREAMS.filter(stream => stream.sport === activeSport));
    } else {
      setVisibleStreams(DEMO_STREAMS);
    }
  }, [activeSport]);

  const currentSportInfo = SPORTS_DATA.find(sport => sport.id === activeSport);

  return (
    <div className="homepage-content">
      <section className="hero-banner">
        <h1>🔴 Live Sports Streaming Hub</h1>
        <p>Experience live sports action without registration. Watch your favorite teams compete in real-time.</p>
        {currentSportInfo && (
          <div className="active-filter">
            Now showing: {currentSportInfo.emoji} {currentSportInfo.displayName}
          </div>
        )}
      </section>

      <StreamBrowser 
        availableStreams={visibleStreams} 
        onStreamWatch={onStreamSelect} 
      />
    </div>
  );
};

function App() {
  const [currentView, setCurrentView] = useState('home');
  const [selectedSport, setSelectedSport] = useState(null);
  const [activeStream, setActiveStream] = useState(null);

  const handleSportSelection = (sportID) => {
    setSelectedSport(sportID);
    setCurrentView('home');
  };

  const handleStreamSelection = (streamData) => {
    setActiveStream(streamData);
    setCurrentView('player');
  };

  const handleReturnToHome = () => {
    setCurrentView('home');
    setActiveStream(null);
  };

  const renderCurrentView = () => {
    switch (currentView) {
      case 'home':
        return (
          <StreamHomepage
            activeSport={selectedSport}
            onStreamSelect={handleStreamSelection}
          />
        );
      case 'player':
        return (
          <VideoPlayer
            streamInfo={activeStream}
            onReturnHome={handleReturnToHome}
          />
        );
      default:
        return (
          <StreamHomepage
            activeSport={selectedSport}
            onStreamSelect={handleStreamSelection}
          />
        );
    }
  };

  return (
    <div className="sports-streaming-app">
      <SportsList
        selectedSport={selectedSport}
        onSportSelect={handleSportSelection}
      />
      <main className="app-content">
        {renderCurrentView()}
      </main>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Your main application coordinates between different views while maintaining clean state management. The component architecture allows each piece to focus on its specific responsibility while communicating through well-defined props and callbacks.

8. Designing Your UI

Create an engaging visual design that captures the excitement of live sports while maintaining professional usability. Replace src/App.css with comprehensive styling:

root {
  --primary-bg: #0a0e1a;
  --secondary-bg: #151925;
  --accent-red: #dc2626;
  --accent-orange: #ea580c;
  --text-primary: #f8fafc;
  --text-secondary: #94a3b8;
  --border-color: rgba(148, 163, 184, 0.2);
  --success-green: #059669;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  background: var(--primary-bg);
  color: var(--text-primary);
  line-height: 1.6;
}

.sports-streaming-app {
  min-height: 100vh;
  background: linear-gradient(145deg, var(--primary-bg) 0%, var(--secondary-bg) 100%);
}

.main-navigation {
  background: rgba(10, 14, 26, 0.95);
  backdrop-filter: blur(20px);
  border-bottom: 1px solid var(--border-color);
  position: sticky;
  top: 0;
  z-index: 50;
}

.nav-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 1rem 2rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 2rem;
}

.brand-section h1 {
  font-size: 1.8rem;
  font-weight: 800;
  background: linear-gradient(135deg, var(--accent-red), var(--accent-orange));
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  margin-bottom: 0.25rem;
}

.brand-section span {
  font-size: 0.875rem;
  color: var(--text-secondary);
  font-weight: 500;
}

.sports-menu {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.sport-button {
  padding: 0.75rem 1.25rem;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid var(--border-color);
  border-radius: 0.75rem;
  color: var(--text-primary);
  font-weight: 600;
  font-size: 0.875rem;
  cursor: pointer;
  transition: all 0.3s ease;
  position: relative;
  overflow: hidden;
}

.sport-button::before {
  content: '';
  position: absolute;
  top: 0;
  left: -100%;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
  transition: left 0.5s ease;
}

.sport-button:hover::before {
  left: 100%;
}

.sport-button:hover {
  background: rgba(255, 255, 255, 0.15);
  transform: translateY(-2px);
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
}

.sport-button.selected {
  background: linear-gradient(135deg, var(--accent-red), var(--accent-orange));
  border-color: transparent;
  color: white;
  box-shadow: 0 4px 20px rgba(220, 38, 38, 0.4);
}

.app-content {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
}

.homepage-content {
  space-y: 3rem;
}

.hero-banner {
  text-align: center;
  padding: 3rem 0;
  margin-bottom: 3rem;
}

.hero-banner h1 {
  font-size: 3rem;
  font-weight: 900;
  margin-bottom: 1rem;
  background: linear-gradient(135deg, var(--text-primary), var(--text-secondary));
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.hero-banner p {
  font-size: 1.125rem;
  color: var(--text-secondary);
  max-width: 600px;
  margin: 0 auto 2rem;
}

.active-filter {
  display: inline-block;
  padding: 0.75rem 1.5rem;
  background: rgba(220, 38, 38, 0.15);
  border: 1px solid rgba(220, 38, 38, 0.3);
  border-radius: 0.75rem;
  color: #fca5a5;
  font-weight: 600;
  font-size: 0.875rem;
}

.stream-gallery {
  margin-bottom: 4rem;
}

.streams-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
  gap: 2rem;
  margin-top: 2rem;
}

.empty-gallery {
  grid-column: 1 / -1;
  text-align: center;
  padding: 4rem 2rem;
  background: rgba(255, 255, 255, 0.05);
  border-radius: 1rem;
  border: 1px solid var(--border-color);
}

.empty-gallery h2 {
  font-size: 1.5rem;
  margin-bottom: 0.5rem;
  color: var(--text-primary);
}

.empty-gallery p {
  color: var(--text-secondary);
}

.stream-preview {
  background: rgba(255, 255, 255, 0.05);
  border-radius: 1rem;
  overflow: hidden;
  cursor: pointer;
  transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  border: 1px solid var(--border-color);
  position: relative;
}

.stream-preview:hover {
  background: rgba(255, 255, 255, 0.1);
  transform: translateY(-8px) scale(1.02);
  box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
}

.preview-video {
  position: relative;
  height: 200px;
  background: linear-gradient(135deg, #1e293b, #334155);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.sport-icon {
  font-size: 3rem;
  width: 80px;
  height: 80px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-weight: bold;
}

.live-indicator {
  position: absolute;
  top: 1rem;
  left: 1rem;
  background: var(--accent-red);
  color: white;
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  box-shadow: 0 4px 12px rgba(220, 38, 38, 0.4);
  animation: pulse-live 2s infinite;
}

@keyframes pulse-live {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.8; }
}

.viewer-badge {
  position: absolute;
  bottom: 1rem;
  right: 1rem;
  background: rgba(0, 0, 0, 0.8);
  color: white;
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  font-size: 0.75rem;
  font-weight: 600;
  backdrop-filter: blur(10px);
}

.stream-details {
  padding: 1.5rem;
}

.stream-details h3 {
  font-size: 1.125rem;
  font-weight: 700;
  margin-bottom: 0.75rem;
  color: var(--text-primary);
  line-height: 1.4;
}

.stream-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.sport-name {
  color: var(--text-secondary);
  font-size: 0.875rem;
  font-weight: 500;
}

.quality-badge {
  background: var(--success-green);
  color: white;
  padding: 0.25rem 0.75rem;
  border-radius: 0.375rem;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
}

.stream-player-page {
  max-width: 1400px;
  margin: 0 auto;
}

.player-navigation {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 2rem;
  flex-wrap: wrap;
  gap: 1rem;
}

.return-button {
  padding: 0.75rem 1.5rem;
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid var(--border-color);
  border-radius: 0.75rem;
  color: var(--text-primary);
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s ease;
}

.return-button:hover {
  background: rgba(255, 255, 255, 0.2);
  transform: translateY(-2px);
}

.current-stream-info h2 {
  font-size: 1.5rem;
  font-weight: 700;
  margin-bottom: 0.5rem;
  color: var(--text-primary);
}

.live-stats {
  color: var(--accent-red);
  font-size: 0.875rem;
  font-weight: 600;
}

.video-player-section {
  display: grid;
  grid-template-columns: 1fr 300px;
  gap: 2rem;
  margin-bottom: 2rem;
}

.player-frame {
  background: rgba(0, 0, 0, 0.9);
  border-radius: 1rem;
  overflow: hidden;
  border: 1px solid var(--border-color);
}

.video-display {
  position: relative;
  width: 100%;
  padding-bottom: 56.25%;
  background: #000;
}

.stream-status {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 2rem;
}

.stream-status.connecting {
  background: linear-gradient(135deg, #1e293b, #334155);
}

.stream-status.demo {
  background: linear-gradient(135deg, #1e293b, #0f172a);
}

.stream-status.error {
  background: linear-gradient(135deg, #7f1d1d, #991b1b);
}

.connection-spinner {
  width: 48px;
  height: 48px;
  border: 4px solid rgba(255, 255, 255, 0.3);
  border-left-color: var(--accent-orange);
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin-bottom: 1rem;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

.stream-status h3 {
  font-size: 1.5rem;
  margin-bottom: 1rem;
  color: var(--text-primary);
}

.stream-status p {
  color: var(--text-secondary);
  margin-bottom: 0.5rem;
}

.demo-note {
  color: #fca5a5 !important;
  font-weight: 600;
}

.technical-info {
  font-family: 'Monaco', 'Menlo', monospace;
  font-size: 0.75rem;
  color: #a78bfa;
  margin-top: 1rem;
}

.playback-controls {
  padding: 1.5rem;
  background: rgba(0, 0, 0, 0.95);
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 1rem;
}

.stream-options {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.stream-options label {
  color: var(--text-secondary);
  font-weight: 600;
  font-size: 0.875rem;
}

.stream-options select {
  padding: 0.5rem 1rem;
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid var(--border-color);
  border-radius: 0.5rem;
  color: var(--text-primary);
  font-weight: 600;
  font-size: 0.875rem;
}

.connection-info {
  display: flex;
  gap: 1.5rem;
  color: var(--text-secondary);
  font-weight: 600;
  font-size: 0.875rem;
}

.event-information {
  background: rgba(255, 255, 255, 0.05);
  border-radius: 1rem;
  padding: 2rem;
  border: 1px solid var(--border-color);
  height: fit-content;
}

.event-information h3 {
  font-size: 1.25rem;
  font-weight: 700;
  margin-bottom: 1.5rem;
  color: var(--text-primary);
}

.event-details {
  display: grid;
  gap: 1rem;
}

.event-details dt {
  color: var(--text-secondary);
  font-weight: 600;
  font-size: 0.875rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.event-details dd {
  color: var(--text-primary);
  font-weight: 600;
  margin-bottom: 1rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid var(--border-color);
}

.event-details dd:last-child {
  border-bottom: none;
  margin-bottom: 0;
  padding-bottom: 0;
}

.live-status {
  color: var(--accent-red) !important;
}

@media (max-width: 1024px) {
  .video-player-section {
    grid-template-columns: 1fr;
  }

  .nav-container {
    flex-direction: column;
    gap: 1rem;
  }

  .sports-menu {
    justify-content: center;
  }
}

@media (max-width: 768px) {
  .app-content {
    padding: 1rem;
  }

  .hero-banner h1 {
    font-size: 2rem;
  }

  .streams-grid {
    grid-template-columns: 1fr;
    gap: 1rem;
  }

  .player-navigation {
    flex-direction: column;
    text-align: center;
  }

  .playback-controls {
    flex-direction: column;
    gap: 1rem;
  }

  .connection-info {
    justify-content: center;
  }
}

@media (max-width: 480px) {
  .sports-menu {
    gap: 0.5rem;
  }

  .sport-button {
    padding: 0.5rem 1rem;
    font-size: 0.75rem;
  }

  .event-information {
    padding: 1.5rem;
  }
}
Enter fullscreen mode Exit fullscreen mode

9. Testing Your Sports Streaming Platform

Launch your development environment to see your streaming platform in action:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Navigate to http://localhost:5173 where you can explore all functionality and verify that everything works as expected.

Verify that your homepage displays sports categories correctly and shows available streams in an organized grid layout.

homepage

Test the navigation by switching between different sports categories and confirming that the stream list updates appropriately. Click on individual stream cards to access the video player interface and examine how the ZEGOCLOUD SDK initializes connection attempts.

streaming

Get the full source code by cloning the GitHub repository, then add your ZEGOCLOUD AppID and credentials to see the streaming platform in action.

Conclusion

Your sports streaming website is now complete and ready to be used by sports lovers. The app loads fast, looks great on phones and computers, and can handle lots of viewers watching games at the same time.

ZEGOCLOUD takes care of all the hard video stuff, so you don't have to worry about streaming problems. You can add more sports, improve the design, or even add features like chat rooms for fans. The best part is that your code is clean and easy to update when you want to make changes later.

Top comments (0)