вот мой проект исправь ошибки у стримера и у клиента который смотрит ,что бы стримелось видио и клиент мог смотреть стрим проверь на ошибки вот код Структура Проекта

live-streaming-platform/
│
├── server.js
├── package.json
├── package-lock.json
└── public/
    ├── index.html
    ├── streamer.html
    ├── viewer.html
    ├── stream.html
    ├── css/
    │   └── styles.css
    └── js/
        ├── streamer.js
        ├── viewer.js
        └── stream.js
1. server.js
Ваш сервер на Node.js с использованием Express и Socket.io. Он остаётся без изменений, так как проблемы возникают на стороне клиента.


// server.js

const express = require('express');
const http = require('http');
const { v4: uuidV4 } = require('uuid');
const socketIO = require('socket.io');
const path = require('path');

const app = express();
const server = http.createServer(app);
const io = socketIO(server);

// Serve static files from 'public' directory
app.use(express.static(path.join(__dirname, 'public')));

// In-memory store for active streams
const streams = {};

// Route to list active streams
app.get('/streams', (req, res) => {
    res.json(Object.keys(streams));
});

// Socket.io connection handling
io.on('connection', socket => {
    console.log('A user connected:', socket.id);

    // Streamer initiates a new stream
    socket.on('create-stream', (callback) => {
        const streamId = uuidV4();
        streams[streamId] = socket.id;
        socket.join(streamId);
        console.log(`Stream created: ${streamId} by ${socket.id}`);
        callback(streamId);
        io.emit('update-streams', Object.keys(streams));
    });

    // Viewer requests to join a stream
    socket.on('join-stream', (streamId, callback) => {
        if (streams[streamId]) {
            console.log(`Viewer ${socket.id} joining stream: ${streamId}`);
            socket.join(streamId);
            // Notify the streamer to initiate peer connection
            io.to(streams[streamId]).emit('viewer-joined', socket.id);
            callback({ success: true });
        } else {
            callback({ success: false, message: 'Stream not found.' });
        }
    });

    // Handle signaling data
    socket.on('signal', (data) => {
        const { to, from, signal } = data;
        io.to(to).emit('signal', { from, signal });
    });

    // Handle stream end
    socket.on('end-stream', (streamId) => {
        if (streams[streamId]) {
            io.to(streamId).emit('stream-ended');
            delete streams[streamId];
            io.emit('update-streams', Object.keys(streams));
            console.log(`Stream ended: ${streamId}`);
        }
    });

    // Disconnect handling
    socket.on('disconnect', () => {
        console.log('User disconnected:', socket.id);
        // Find and remove any streams created by this socket
        for (const [streamId, streamerId] of Object.entries(streams)) {
            if (streamerId === socket.id) {
                io.to(streamId).emit('stream-ended');
                delete streams[streamId];
                io.emit('update-streams', Object.keys(streams));
                console.log(`Stream ended due to disconnect: ${streamId}`);
                break;
            }
        }
    });
});

// Start the server
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});
2. public/index.html
Главная страница с выбором между стримером и зрителем.


<!-- public/index.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Live Streaming Platform</title>
    <link rel="stylesheet" href="css/styles.css">
</head>
<body>
    <div class="container">
        <h1>Добро пожаловать на платформу трансляций</h1>
        <div class="buttons">
            <a href="/streamer.html" class="btn">Начать трансляцию</a>
            <a href="/viewer.html" class="btn">Смотреть трансляции</a>
        </div>
    </div>
</body>
</html>
3. public/streamer.html
Обновлённый файл для стримера с выбором источника видео (камера или файл).


<!-- public/streamer.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Стример - Начать трансляцию</title>
    <link rel="stylesheet" href="css/styles.css">
</head>
<body>
    <div class="container">
        <h2>Начать трансляцию</h2>
        
        <!-- Выбор источника трансляции -->
        <div class="source-selection">
            <label>
                <input type="radio" name="source" value="camera" checked>
                Веб-камера
            </label>
            <label>
                <input type="radio" name="source" value="file">
                Видео-файл
            </label>
        </div>
        
        <!-- Загрузка видео-файла -->
        <div id="fileInputContainer" style="display: none; margin-top: 15px;">
            <input type="file" id="videoFileInput" accept="video/*">
        </div>
        
        <!-- Кнопки управления трансляцией -->
        <div class="buttons">
            <button id="startStreamBtn" class="btn">Начать стрим</button>
            <button id="endStreamBtn" class="btn" style="display:none;">Закончить стрим</button>
        </div>
        
        <!-- Информация о трансляции -->
        <div id="streamInfo" style="display:none; margin-top: 20px;">
            <p>Трансляция активна! Ссылка для зрителей:</p>
            <input type="text" id="streamLink" readonly>
            <!-- Видео для локального отображения -->
            <video id="localVideo" autoplay muted playsinline></video>
            <!-- Видео-файл, который транслируется -->
            <video id="uploadedVideo" style="display:none;"></video>
        </div>
    </div>
    
    <script src="/socket.io/socket.io.js"></script>
    <script src="js/streamer.js"></script>
</body>
</html>
4. public/js/streamer.js
Обновлённый скрипт для стримера, обеспечивающий выбор источника видео и корректную передачу видео-потока.


// public/js/streamer.js

const socket = io();

// DOM Elements
const startStreamBtn = document.getElementById('startStreamBtn');
const endStreamBtn = document.getElementById('endStreamBtn');
const streamInfo = document.getElementById('streamInfo');
const streamLinkInput = document.getElementById('streamLink');
const localVideo = document.getElementById('localVideo');
const uploadedVideo = document.getElementById('uploadedVideo');
const sourceRadios = document.getElementsByName('source');
const fileInputContainer = document.getElementById('fileInputContainer');
const videoFileInput = document.getElementById('videoFileInput');

// WebRTC Variables
let streamId = null;
let localStream = null;
let videoStream = null; // Используется для передачи потоков
const peers = {};

// Current Source: 'camera' или 'file'
let currentSource = 'camera';

// Обработка выбора источника трансляции
sourceRadios.forEach(radio => {
    radio.addEventListener('change', () => {
        currentSource = radio.value;
        if (currentSource === 'file') {
            fileInputContainer.style.display = 'block';
        } else {
            fileInputContainer.style.display = 'none';
            // Останавливаем видео-файл при переключении обратно на камеру
            if (uploadedVideo.src) {
                uploadedVideo.pause();
                uploadedVideo.src = '';
                videoStream = null;
                uploadedVideo.style.display = 'none';
            }
        }
    });
});

// Обработка загрузки видео-файла
videoFileInput.addEventListener('change', () => {
    const file = videoFileInput.files[0];
    if (file) {
        const fileURL = URL.createObjectURL(file);
        uploadedVideo.src = fileURL;
        uploadedVideo.style.display = 'block';
        uploadedVideo.load();
        uploadedVideo.play();
        uploadedVideo.onplaying = () => {
            // Захватываем поток после начала воспроизведения
            videoStream = uploadedVideo.captureStream();
            console.log('Видео-файл захвачен:', videoStream);
            // Если стрим уже начат, добавляем видео-файл к существующим соединениям
            if (streamInfo.style.display === 'block') {
                addVideoStreamToPeers();
            }
        };
        uploadedVideo.onended = () => {
            // Автоматически останавливаем трансляцию при окончании видео-файла
            endStream();
        };
    }
});

// Start Stream
startStreamBtn.addEventListener('click', async () => {
    streamId = await createStream();
    if (streamId) {
        streamInfo.style.display = 'block';
        startStreamBtn.style.display = 'none';
        endStreamBtn.style.display = 'inline-block';
        streamLinkInput.value = `${window.location.origin}/stream.html?streamId=${streamId}`;
        
        if (currentSource === 'camera') {
            // Захват видео с веб-камеры
            startLocalVideo();
        } else if (currentSource === 'file') {
            // Воспроизведение видео-файла и захват потока
            if (uploadedVideo.src) {
                if (videoStream) {
                    addVideoStreamToPeers();
                } else {
                    alert('Ожидание захвата видео-файла...');
                }
            } else {
                alert('Пожалуйста, выберите видео-файл для трансляции.');
            }
        }
    }
});

// End Stream
endStreamBtn.addEventListener('click', () => {
    endStream();
});

// Create Stream on Server
function createStream() {
    return new Promise((resolve) => {
        socket.emit('create-stream', (generatedStreamId) => {
            resolve(generatedStreamId);
        });
    });
}

// Start Local Video
async function startLocalVideo() {
    try {
        localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
        localVideo.srcObject = localStream;
        videoStream = localStream; // Используется для передачи в Peer Connections
        console.log('Локальный поток захвачен:', videoStream);

        // Настройка WebRTC Peer Connections для всех зрителей
        setupPeerConnections();
    } catch (err) {
        console.error('Ошибка доступа к камере/микрофону:', err);
        alert('Не удалось получить доступ к камере/микрофону.');
    }
}

// Добавление видео-файла к существующим Peer Connections
function addVideoStreamToPeers() {
    for (const viewerId in peers) {
        const peer = peers[viewerId];
        if (videoStream) {
            videoStream.getTracks().forEach(track => {
                peer.pc.addTrack(track, videoStream).catch(err => {
                    console.error('Ошибка добавления трека:', err);
                });
            });
        } else {
            console.warn('Нет видео-потока для добавления к Peer Connection:', viewerId);
        }
    }
}

// Handle Viewer Joined
socket.on('viewer-joined', (viewerSocketId) => {
    console.log('Новый зритель присоединился:', viewerSocketId);
    createPeerConnection(viewerSocketId, true);
});

// Handle Signaling Data
socket.on('signal', async (data) => {
    const { from, signal } = data;
    if (!peers[from]) {
        createPeerConnection(from, false);
    }
    if (signal.type === 'offer') {
        await peers[from].pc.setRemoteDescription(new RTCSessionDescription(signal));
        const answer = await peers[from].pc.createAnswer();
        await peers[from].pc.setLocalDescription(answer);
        socket.emit('signal', { to: from, from: socket.id, signal: answer });
    } else if (signal.type === 'candidate') {
        try {
            await peers[from].pc.addIceCandidate(new RTCIceCandidate(signal));
        } catch (err) {
            console.error('Ошибка добавления ICE-кандидата:', err);
        }
    }
});

// Создание Peer Connection
function createPeerConnection(viewerSocketId, isInitiator) {
    const peer = {};
    peer.pc = new RTCPeerConnection({
        iceServers: [
            { urls: 'stun:stun.l.google.com:19302' },
            // Добавьте TURN-серверы здесь при необходимости
        ]
    });

    // Добавление треков из videoStream в Peer Connection
    if (videoStream) {
        videoStream.getTracks().forEach(track => peer.pc.addTrack(track, videoStream));
    } else {
        console.warn('Нет видео-потока для передачи зрителю:', viewerSocketId);
    }

    // Обработка ICE-кандидатов
    peer.pc.onicecandidate = (event) => {
        if (event.candidate) {
            socket.emit('signal', { to: viewerSocketId, from: socket.id, signal: event.candidate });
        }
    };

    // Обработка событий соединения
    peer.pc.onconnectionstatechange = () => {
        console.log(`Состояние соединения с ${viewerSocketId}:`, peer.pc.connectionState);
        if (peer.pc.connectionState === 'disconnected' || peer.pc.connectionState === 'failed' || peer.pc.connectionState === 'closed') {
            peer.pc.close();
            delete peers[viewerSocketId];
            console.log(`Соединение с ${viewerSocketId} закрыто.`);
        }
    };

    // Создание предложения, если инициатор
    if (isInitiator) {
        peer.pc.onnegotiationneeded = async () => {
            try {
                const offer = await peer.pc.createOffer();
                await peer.pc.setLocalDescription(offer);
                socket.emit('signal', { to: viewerSocketId, from: socket.id, signal: offer });
            } catch (err) {
                console.error('Ошибка создания предложения:', err);
            }
        };
    }

    peers[viewerSocketId] = peer;
}

// Закрытие всех Peer Connections и остановка трансляции
function endStream() {
    // Закрытие всех peer connections
    for (const viewerId in peers) {
        peers[viewerId].pc.close();
        delete peers[viewerId];
    }

    // Остановить все локальные треки
    if (localStream) {
        localStream.getTracks().forEach(track => track.stop());
        localStream = null;
        localVideo.srcObject = null;
    }

    // Остановить воспроизведение видео-файла
    if (uploadedVideo.src) {
        uploadedVideo.pause();
        uploadedVideo.src = '';
        videoStream = null;
        uploadedVideo.style.display = 'none';
    }

    // Уведомление сервера о завершении трансляции
    socket.emit('end-stream', streamId);

    // Сброс UI
    streamInfo.style.display = 'none';
    startStreamBtn.style.display = 'inline-block';
    endStreamBtn.style.display = 'none';
    streamLinkInput.value = '';
}
5. public/viewer.html
Страница для зрителей с отображением списка активных трансляций.


<!-- public/viewer.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Зритель - Выбрать трансляцию</title>
    <link rel="stylesheet" href="css/styles.css">
</head>
<body>
    <div class="container">
        <h2>Активные трансляции</h2>
        <ul id="streamsList">
            <!-- Список активных стримов будет динамически заполнен -->
        </ul>
    </div>

    <script src="/socket.io/socket.io.js"></script>
    <script src="js/viewer.js"></script>
</body>
</html>
6. public/js/viewer.js
Скрипт для зрителей, который отображает список активных трансляций и позволяет выбрать для просмотра.


// public/js/viewer.js

const socket = io();

// DOM Elements
const streamsList = document.getElementById('streamsList');

// Connect and fetch active streams
socket.on('connect', () => {
    console.log('Connected to server as viewer');
});

// Update streams list when streams are updated
socket.on('update-streams', (activeStreams) => {
    renderStreams(activeStreams);
});

// Initial fetch of active streams
fetch(`/streams`)
    .then(response => response.json())
    .then(data => {
        renderStreams(data);
    });

// Render streams list
function renderStreams(streams) {
    streamsList.innerHTML = '';
    if (streams.length === 0) {
        streamsList.innerHTML = '<p>Нет активных трансляций в данный момент.</p>';
        return;
    }
    streams.forEach(streamId => {
        const listItem = document.createElement('li');
        listItem.className = 'stream-item';
        const link = document.createElement('a');
        link.href = `/stream.html?streamId=${streamId}`;
        link.textContent = `Трансляция #${streamId}`;
        listItem.appendChild(link);
        streamsList.appendChild(listItem);
    });
}
7. public/stream.html
Страница трансляции, где зритель будет смотреть видео.


<!-- public/stream.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Трансляция</title>
    <link rel="stylesheet" href="css/styles.css">
</head>
<body>
    <div class="container">
        <h2>Просмотр трансляции</h2>
        <div id="streamContainer">
            <video id="remoteVideo" autoplay playsinline controls></video>
        </div>
    </div>

    <script src="/socket.io/socket.io.js"></script>
    <script src="js/stream.js"></script>
</body>
</html>
8. public/js/stream.js
Скрипт для страницы трансляции, который устанавливает WebRTC соединение для получения видео.


// public/js/stream.js

const socket = io();

// DOM Elements
const remoteVideo = document.getElementById('remoteVideo');

// Extract streamId from URL
const urlParams = new URLSearchParams(window.location.search);
const streamId = urlParams.get('streamId');

if (!streamId) {
    alert('Трансляция не найдена.');
    window.location.href = '/viewer.html';
}

// WebRTC Variables
let peerConnection = null;
const iceServers = [
    { urls: 'stun:stun.l.google.com:19302' },
    // Добавьте TURN-серверы здесь при необходимости
];

// Join the stream room
socket.emit('join-stream', streamId, (response) => {
    if (response.success) {
        console.log('Присоединился к трансляции:', streamId);
    } else {
        alert(response.message);
        window.location.href = '/viewer.html';
    }
});

// Handle signaling data
socket.on('signal', async (data) => {
    const { from, signal } = data;
    if (!peerConnection) {
        createPeerConnection(from);
    }
    if (signal.type === 'offer') {
        await peerConnection.setRemoteDescription(new RTCSessionDescription(signal));
        const answer = await peerConnection.createAnswer();
        await peerConnection.setLocalDescription(answer);
        socket.emit('signal', { to: from, from: socket.id, signal: answer });
    } else if (signal.type === 'candidate') {
        try {
            await peerConnection.addIceCandidate(new RTCIceCandidate(signal));
        } catch (err) {
            console.error('Ошибка добавления ICE-кандидата:', err);
        }
    }
});

// Handle stream ended
socket.on('stream-ended', () => {
    alert('Трансляция завершена.');
    window.location.href = '/viewer.html';
});

// Create Peer Connection
function createPeerConnection(streamerId) {
    peerConnection = new RTCPeerConnection({ iceServers });

    // Обработка ICE-кандидатов
    peerConnection.onicecandidate = (event) => {
        if (event.candidate) {
            socket.emit('signal', { to: streamerId, from: socket.id, signal: event.candidate });
        }
    };

    // Получение удалённого потока
    peerConnection.ontrack = (event) => {
        if (remoteVideo.srcObject !== event.streams[0]) {
            remoteVideo.srcObject = event.streams[0];
            console.log('Получен удалённый поток:', event.streams[0]);
        }
    };

    // Обработка состояний соединения
    peerConnection.onconnectionstatechange = () => {
        console.log(`Состояние соединения:`, peerConnection.connectionState);
        if (peerConnection.connectionState === 'disconnected' || peerConnection.connectionState === 'failed' || peerConnection.connectionState === 'closed') {
            remoteVideo.srcObject = null;
            peerConnection.close();
            peerConnection = null;
            alert('Соединение было прервано.');
            window.location.href = '/viewer.html';
        }
    };
}
9. public/css/styles.css
Обновлённые стили для новой функциональности.


/* public/css/styles.css */

body {
    background-color: #f4f4f4;
    font-family: 'Roboto', sans-serif;
    color: #333;
}

.container {
    max-width: 800px;
    margin: 30px auto;
    padding: 20px;
    background-color: #fff;
    border-radius: 8px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    text-align: center;
}

h1, h2, h3 {
    margin-bottom: 20px;
}

.buttons {
    display: flex;
    justify-content: center;
    gap: 20px;
    flex-wrap: wrap;
}

.btn {
    padding: 15px 25px;
    background-color: #4CAF50;
    color: white;
    text-decoration: none;
    border: none;
    border-radius: 5px;
    transition: background-color 0.3s, transform 0.3s;
    font-size: 1em;
    cursor: pointer;
}

.btn:hover {
    background-color: #45a049;
    transform: scale(1.05);
}

#streamsList {
    list-style-type: none;
    padding: 0;
}

.stream-item {
    margin: 10px 0;
}

.stream-item a {
    display: block;
    padding: 10px;
    background-color: #3498db;
    color: white;
    text-decoration: none;
    border-radius: 4px;
    transition: background-color 0.3s;
}

.stream-item a:hover {
    background-color: #2980b9;
}

video {
    width: 100%;
    max-width: 800px;
    border-radius: 8px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

input[type="text"], textarea {
    width: 100%;
    padding: 12px;
    border: 1px solid #ccc;
    border-radius: 5px;
    resize: vertical;
}

button {
    padding: 12px 20px;
    background-color: #4CAF50;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s, transform 0.3s;
    font-size: 1em;
}

button:hover {
    background-color: #45a049;
    transform: scale(1.02);
}

video#localVideo, video#remoteVideo, video#uploadedVideo {
    margin-top: 20px;
    width: 100%;
    max-width: 600px;
    height: auto;
}

#streamLink {
    width: 100%;
    padding: 10px;
    margin-top: 10px;
    border: 1px solid #4CAF50;
    border-radius: 4px;
}

.stream-item button {
    background-color: #e74c3c;
}

.stream-item button:hover {
    background-color: #c0392b;
}

/* Стили для выбора источника трансляции */
.source-selection {
    display: flex;
    justify-content: center;
    gap: 20px;
    margin-top: 20px;
    animation: fadeIn 0.5s;
}

.source-selection label {
    font-size: 1em;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 5px;
}

/* Стили для загрузки видео-файла */
#fileInputContainer input[type="file"] {
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 5px;
}

#localVideo, #uploadedVideo {
    width: 100%;
    max-width: 600px;
    height: auto;
    border-radius: 10px;
    box-shadow: 0 4px 8px rgba(0,0,0,0.2);
    margin-top: 20px;
    animation: fadeIn 0.5s;
}

/* Responsive Design */
@media (max-width: 600px) {
    .btn {
        width: 100%;
        margin-bottom: 10px;
    }

    #live-stream iframe {
        height: 250px;
    }
}

/* Анимация для плавного появления элементов */
@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}