сформулируй мне в ответе по данным в одном сообщении весь целиком проект стрименговой платформы моего кода с структурой 
node -v
npm -v
2. Create Project Directory

mkdir live-streaming-platform
cd live-streaming-platform
3. Initialize npm and Install Dependencies

npm init -y
npm install express socket.io uuid
4. Project Structure

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
Server-Side Implementation
The server manages signaling for WebRTC connections, keeps track of active streams, and serves static files.

server.js

// 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}`);
});
Explanation:

Express Server: Serves static files from the public directory.
Socket.io: Manages real-time communication for signaling between streamers and viewers.
Streams Management: Uses an in-memory object streams to keep track of active streams, mapping streamId to the streamer's socket.id.
Events:
create-stream: Streamer requests to create a new stream, receiving a unique streamId.
join-stream: Viewer attempts to join a stream by streamId.
signal: Used for WebRTC signaling between peers.
end-stream: Streamer can end the stream manually.
disconnect: Handles cleanup if a streamer disconnects unexpectedly.
Client-Side Implementation
1. Common HTML Template
We'll create separate HTML files for different functionalities.

public/index.html - Home Page
Redirects users to either the streamer or viewer interface.


<!-- 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>
2. Streamer Interface
public/streamer.html
Streamers can start a new stream, view their live video, and end the stream when done.


<!-- 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>
        <button id="startStreamBtn" class="btn">Начать стрим</button>
        <button id="endStreamBtn" class="btn" style="display:none;">Закончить стрим</button>
        <div id="streamInfo" style="display:none;">
            <p>Трансляция активна! Ссылка для зрителей:</p>
            <input type="text" id="streamLink" readonly>
            <video id="localVideo" autoplay muted playsinline></video>
        </div>
    </div>
    
    <script src="/socket.io/socket.io.js"></script>
    <script src="js/streamer.js"></script>
</body>
</html>
public/js/streamer.js
Handles stream creation, WebRTC peer connections, and broadcasting to viewers.


// 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');

// WebRTC Variables
let streamId = null;
let localStream = null;
const peers = {};

// 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}`;
        // Start capturing local video
        startLocalVideo();
    }
});

// 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;
    } catch (err) {
        console.error('Ошибка доступа к камере/микрофону:', err);
    }
}

// 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);
    }
    await peers[from].pc.setRemoteDescription(new RTCSessionDescription(signal));
    if (signal.type === 'offer') {
        const answer = await peers[from].pc.createAnswer();
        await peers[from].pc.setLocalDescription(answer);
        socket.emit('signal', { to: from, from: socket.id, signal: answer });
    }
});

// Create Peer Connection
function createPeerConnection(viewerSocketId, isInitiator) {
    const peer = {};
    peer.pc = new RTCPeerConnection({
        iceServers: [
            { urls: 'stun:stun.l.google.com:19302' },
            // You can add TURN servers here if needed
        ]
    });

    // Add local stream tracks to peer connection
    if (localStream) {
        localStream.getTracks().forEach(track => peer.pc.addTrack(track, localStream));
    }

    // Handle ICE candidates
    peer.pc.onicecandidate = (event) => {
        if (event.candidate) {
            socket.emit('signal', { to: viewerSocketId, from: socket.id, signal: event.candidate });
        }
    };

    // Handle track event to receive remote streams (optional for streamer)
    peer.pc.ontrack = (event) => {
        console.log('Получен удалённый трек:', event.streams[0]);
        // You can display the viewer's video if needed
    };

    // Create offer if initiator
    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;
}

// End Stream
function endStream() {
    // Close all peer connections
    for (const viewerId in peers) {
        peers[viewerId].pc.close();
        delete peers[viewerId];
    }

    // Stop all local tracks
    if (localStream) {
        localStream.getTracks().forEach(track => track.stop());
        localStream = null;
        localVideo.srcObject = null;
    }

    // Notify server to end stream
    socket.emit('end-stream', streamId);

    // Reset UI
    streamInfo.style.display = 'none';
    startStreamBtn.style.display = 'inline-block';
    endStreamBtn.style.display = 'none';
    streamLinkInput.value = '';
}
Explanation:

Stream Creation: When the streamer clicks "Начать стрим", the client requests the server to create a new stream, receiving a unique streamId.
Local Video Capture: Accesses the user's webcam and microphone using getUserMedia and displays it in the video element.
WebRTC Peer Connections: For each viewer that joins, a new RTCPeerConnection is established. The streamer acts as the initiator, creating an offer and handling signaling data via Socket.io.
Ending Stream: Stops all peer connections and local media tracks, and notifies the server to remove the stream from the active list.
3. Viewer Interface
The viewer selects from a list of active streams and watches the selected stream in real-time.

public/viewer.html
Displays a list of active streams.


<!-- 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>
public/js/viewer.js
Handles fetching active streams and directing viewers to the selected stream.


// 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);
    });
}
Explanation:

Fetching Active Streams: On load and whenever the server updates the streams list, the viewer's client fetches the active streams and renders them.
Stream Selection: Each active stream is displayed as a link. Clicking the link directs the viewer to the specific stream's page.
4. Stream Viewing Interface
Each stream has its own page where viewers can watch the live broadcast.

public/stream.html
Displays the selected stream.


<!-- 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></video>
        </div>
    </div>

    <script src="/socket.io/socket.io.js"></script>
    <script src="js/stream.js"></script>
</body>
</html>
public/js/stream.js
Handles connecting to the specific stream and establishing a WebRTC connection to receive the video.


// 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' },
    // Add TURN servers here if needed
];

// 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 });

    peerConnection.onicecandidate = (event) => {
        if (event.candidate) {
            socket.emit('signal', { to: streamerId, from: socket.id, signal: event.candidate });
        }
    };

    peerConnection.ontrack = (event) => {
        remoteVideo.srcObject = event.streams[0];
    };
}
Explanation:

Stream Identification: Extracts the streamId from the URL query parameters to join the appropriate stream.
Joining the Stream: Emits join-stream to the server, which adds the viewer to the room and notifies the streamer.
WebRTC Connection: Establishes a peer connection with the streamer to receive the video stream.
Handling Stream End: Alerts the viewer and redirects them if the streamer ends the stream.
CSS Styles
For brevity and focus on functionality, here's a simple CSS to style the platform. You can enhance it as per your design preferences.

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-radius: 5px;
    transition: background-color 0.3s, transform 0.3s;
    font-size: 1em;
}

.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 {
    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;
}

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

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

Basic Styling: Centers content, adds padding, margins, background colors, and box shadows for visual appeal.
Buttons: Styled with hover effects for interactivity.
Video Elements: Responsive sizing with rounded corners and shadows.
Forms: Input fields and buttons styled for consistency.
Responsive Design: Ensures usability on smaller screens.
Running the Application
1. Start the Server
Ensure you're in the project directory (live-streaming-platform) and run:


node server.js
You should see:


Server is running on port 3000
2. Access the Platform
Open your browser and navigate to http://localhost:3000. You'll see the home page with options to start a stream or view active streams.

3. Starting a Stream
Click on "Начать трансляцию" to go to the streamer interface.
Click "Начать стрим". Grant permission to access your webcam and microphone when prompted.
Once the stream starts, you'll see your local video and a unique link. Share this link with viewers.
4. Viewing a Stream
Open another browser window or a different device.
Navigate to http://localhost:3000/viewer.html to see a list of active streams.
Click on the desired stream to watch it in real-time.
Note: For testing purposes, you can open multiple browser windows on the same machine. For real-world scenarios, ensure that the server is accessible over the internet and handle NAT traversal for WebRTC (using TURN servers if necessary).

Conclusion
This guide provides a foundational implementation of a live streaming platform using Node.js, Express, Socket.io, and WebRTC. While it serves as a solid starting point, consider the following enhancements for a production-ready application:

Persistent Storage: Integrate a database (e.g., MongoDB) to persist streams, user data, and chat messages.
Authentication: Implement secure user authentication to manage streamers and viewers.
Scalability: Use TURN servers to handle WebRTC connections behind NATs and firewalls. Consider scalable media servers or cloud-based solutions for handling a large number of streams.
Security: Ensure secure transmission using HTTPS and secure WebRTC implementations.
Enhanced UI/UX: Improve the front-end design for better user experience.
Additional Features: Incorporate chat moderation, stream recording, viewer analytics, and more.
Feel free to expand upon this foundation to build a more robust and feature-rich live streaming platform tailored to your specific requirements.


на странице streamer.html для контента трансляции выбираем или веб камеру или видио файл исправь


Конечно! Я обновил вашу платформу стриминга, добавив возможность выбора источника контента для трансляции: веб-камера или видео-файл. Ниже представлены обновлённые файлы streamer.html и streamer.js, а также необходимые изменения в стилях. Эти изменения позволят стримерам выбирать между живой трансляцией через веб-камеру или воспроизведением видео-файла.

Обновлённая Структура Проекта

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. Обновление streamer.html
Добавлены опции для выбора источника трансляции: Веб-камера или Видео-файл. В зависимости от выбора, стример может либо захватить видео с камеры, либо загрузить и воспроизвести видео-файл, который будет транслироваться зрителям.

Обновлённый 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>
Объяснение изменений:

Выбор Источника Трансляции:
Добавлены радиокнопки для выбора между Веб-камерой и Видео-файлом.
При выборе Видео-файл, отображается контейнер для загрузки файла.
Загрузка Видео-Файла:
Контейнер fileInputContainer содержит элемент <input> для выбора видео-файла.
По умолчанию скрыт и отображается только при выборе источника Видео-файл.
Видео Элементы:
<video id="localVideo">: Для отображения захваченного видео с камеры или воспроизводимого видео-файла.
<video id="uploadedVideo">: Скрытый видео-элемент для воспроизведения загруженного видео-файла. Его поток будет захватываться и транслироваться.
2. Обновление streamer.js
Добавлены функции для обработки выбора источника, загрузки видео-файла и захвата соответствующего видео-потока для трансляции.

Обновлённый 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;
const peers = {};

// Current Source: 'camera' или 'file'
let currentSource = 'camera';
let videoStream = null; // Используется для captureStream()

// Обработка выбора источника трансляции
sourceRadios.forEach(radio => {
    radio.addEventListener('change', () => {
        currentSource = radio.value;
        if (currentSource === 'file') {
            fileInputContainer.style.display = 'block';
        } else {
            fileInputContainer.style.display = 'none';
        }
    });
});

// Обработка загрузки видео-файла
videoFileInput.addEventListener('change', () => {
    const file = videoFileInput.files[0];
    if (file) {
        const fileURL = URL.createObjectURL(file);
        uploadedVideo.src = fileURL;
        uploadedVideo.load();
        uploadedVideo.play();
        uploadedVideo.addEventListener('canplay', () => {
            // Поток с видео-файла
            videoStream = uploadedVideo.captureStream();
        });
    }
});

// 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) {
                startVideoFileStream();
            } 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; // Используется для captureStream()

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

// Start Video File Stream
function startVideoFileStream() {
    try {
        // videoStream уже захвачен через captureStream() при загрузке файла
        localVideo.srcObject = videoStream;
        // Настройка WebRTC Peer Connections для всех зрителей
        setupPeerConnections();
    } catch (err) {
        console.error('Ошибка при запуске видео-файла:', err);
    }
}

// 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);
        }
    }
});

// Setup Peer Connections for Existing Streamers
function setupPeerConnections() {
    // Здесь можно добавить любые дополнительные настройки, если необходимо
}

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

    // Добавление треков из видеоStream в peer connection
    if (videoStream) {
        videoStream.getTracks().forEach(track => peer.pc.addTrack(track, videoStream));
    }

    // Handle ICE candidates
    peer.pc.onicecandidate = (event) => {
        if (event.candidate) {
            socket.emit('signal', { to: viewerSocketId, from: socket.id, signal: event.candidate });
        }
    };

    // Handle track event (для стримера не требуется, но может быть использовано для обратной связи)
    peer.pc.ontrack = (event) => {
        console.log('Получен удалённый трек:', event.streams[0]);
        // Можно добавить отображение видео зрителей, если необходимо
    };

    // Create offer если initiator
    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;
}

// End Stream
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;
    }

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

    // Сброс UI
    streamInfo.style.display = 'none';
    startStreamBtn.style.display = 'inline-block';
    endStreamBtn.style.display = 'none';
    streamLinkInput.value = '';
    uploadedVideo.style.display = 'none';
}

// Получение сигналов для камеры и видео-файла
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);
        }
    }
});
Объяснение изменений:

Выбор Источника Трансляции:
Добавлены обработчики событий для радиокнопок, позволяющих стримеру выбирать между Веб-камерой и Видео-файлом.
При выборе Видео-файл, отображается контейнер для загрузки файла.
Загрузка и Воспроизведение Видео-Файла:
При выборе и загрузке видео-файла, оно воспроизводится в скрытом <video> элементе uploadedVideo.
С помощью captureStream(), поток видео-файла захватывается и используется для трансляции.
Видео-файл может быть повторно воспроизводимым или останавливаться после окончания, в зависимости от необходимой логики.
Захват Видео-Источника:
Веб-камера: Используется getUserMedia для захвата реального видео с камеры и микрофона.
Видео-Файл: Используется captureStream() на <video> элементе для захвата видео-файла и передачи его через WebRTC.
Управление WebRTC Peer Connections:
Независимо от выбранного источника, WebRTC Peer Connections настраиваются одинаково, обеспечивая передачу видео-потока зрителям.
Закрытие Трансляции:
При окончании трансляции все peer connections закрываются, локальные треки прекращаются, и видео-файл останавливается.
3. Обновление styles.css
Добавлены стили для новых элементов интерфейса, таких как выбор источника трансляции и загрузка видео-файла.

Обновлённый public/css/styles.css

/* public/css/styles.css */

/* Основные стили остаются без изменений */

/* Стили для выбора источника трансляции */
.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;
}

/* Дополнительные стили, если необходимо */
Объяснение изменений:

Выбор Источника Трансляции (source-selection):
Центрированное расположение радиокнопок с отступами и анимацией появления.
Загрузка Видео-Файла:
Стили для элемента <input type="file"> обеспечивают удобный внешний вид.
Видео-элементы (localVideo и uploadedVideo) имеют одинаковые стили для унификации интерфейса.
4. Тестирование Функциональности
После обновления файлов необходимо проверить функциональность:

Запуск Сервера:
Убедитесь, что вы обновили server.js (если это необходимо) и запустите сервер:

node server.js
Переход на Страницу Стримера:
Откройте браузер и перейдите по адресу http://localhost:3000/streamer.html.
Выбор Источника Трансляции:
Веб-камера:
Убедитесь, что Веб-камера выбрана по умолчанию.
Нажмите кнопку "Начать стрим".
Предоставьте разрешение на доступ к веб-камере и микрофону.
Убедитесь, что видео отображается в localVideo и зрители могут видеть вашу трансляцию.
Видео-Файл:
Выберите опцию "Видео-файл".
Загрузите видео-файл через появившийся файл-инпут.
Нажмите кнопку "Начать стрим".
Убедитесь, что выбранный видео-файл воспроизводится и транслируется зрителям.
Переход на Страницу Зрителя:
Откройте другую вкладку или окно браузера и перейдите на http://localhost:3000/viewer.html, чтобы убедиться, что активные трансляции отображаются и доступны для просмотра.
Завершение Трансляции:
Нажмите кнопку "Закончить стрим" на странице стримера и убедитесь, что трансляция завершается корректно, а зрители уведомляются об окончании.
5. Заключение
С этими обновлениями ваша платформа стриминга теперь поддерживает как живые трансляции с веб-камеры, так и воспроизведение видео-файлов. Это расширяет возможности для стримеров и делает вашу платформу более гибкой и удобной для различных сценариев использования.

Дополнительные Рекомендации:

Управление Проигрыванием Видео-Файлов:
Можно добавить опции для повторного проигрывания, остановки или переключения видео-файлов во время трансляции.
Воспроизведение видео-файлов должно быть синхронизировано с трансляцией, чтобы зрители видели всё одинаково.
Оптимизация Потоков:
При использовании видео-файлов убедитесь, что качество трансляции соответствует возможностям вашей сети и серверных ресурсов.
Рассмотрите возможность использования адаптивного битрейта для улучшения качества вещания в зависимости от пропускной способности зрителей.
Безопасность:
Реализуйте механизмы аутентификации и авторизации для стримеров и зрителей.
Ограничьте доступ к трансляциям только авторизованным пользователям или по защищённым ссылкам.
Интеграция TURN-серверов:
Для обеспечения надёжного соединения между стримерами и зрителями независимо от сетевых условий, рекомендуется настроить и интегрировать TURN-серверы.
UI/UX Улучшения:
Улучшите интерфейс стримера, добавив индикаторы активности, панели управления трансляцией и другие полезные элементы.
Обновите дизайн зрительского интерфейса для лучшего взаимодействия и просмотра.
Если у вас возникнут дополнительные вопросы или потребуется дальнейшая помощь в развитии вашей платформы стриминга, не стесняйтесь обращаться!


я выбрал файл для стрима ,зашол как зритель но видио нет исправь это


исправь я добавил файл но в трансляции нет видио


Извините за неудобства, связанные с отсутствием видео в трансляции при использовании видео-файла. Давайте тщательно проанализируем и исправим проблему. Ваша цель — обеспечить корректную передачу видео-файла от стримера к зрителям через WebRTC.

Возможные Причины Проблемы:
Неправильное Захватывание Видео-Файла:
Убедитесь, что captureStream() вызывается после того, как видео-файл начал воспроизводиться, иначе поток может быть пустым.
Неправильное Передача Потока:
Убедитесь, что захваченный поток (videoStream) правильно добавляется к RTCPeerConnection для каждого зрителя.
Обработчики Событий:
Необходимо убедиться, что события WebRTC правильно обрабатываются как на стороне стримера, так и на стороне зрителя.
Отсутствие Видео-Трека для Зрителя:
Возможно, стример не отправляет видео-трек зрителю, если источник — видео-файл.
Решение:
Мы внесём изменения в streamer.js, чтобы гарантировать:

Правильное захватывание и передача видео-файла.
Добавление видео-треков к каждой новой Peer Connection.
Обновление интерфейса стримера для отображения активного видео-потока.
Обновление stream.js (если необходимо) для корректного отображения полученного потока зрителем.
1. Обновление streamer.js
Давайте убедимся, что captureStream() вызывается после того, как видео-файл начал воспроизводиться, и что видео-трек добавляется к каждой Peer Connection.


// 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; // Используется для captureStream()
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;
            }
        }
    });
});

// Обработка загрузки видео-файла
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);
        };
        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) {
                // Поток уже захвачен в обработчике 'onplaying'
                if (videoStream) {
                    setupPeerConnections();
                } 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);
    }
}

// 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 = '';
}
2. Обновление stream.js (Viewer-side)
Убедимся, что зритель корректно принимает и отображает поток.


// 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) => {
        remoteVideo.srcObject = 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';
        }
    };
}
3. Обновление 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. Обновление styles.css
Добавим стили для новых элементов интерфейса.


/* public/css/styles.css */

/* Основные стили остаются без изменений */

/* Стили для выбора источника трансляции */
.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;
}
5. Проверка viewer.js (Если Необходимо)
Убедимся, что зритель правильно отображает полученный поток. В нашем случае stream.js уже настроен на установку remoteVideo.srcObject.

Если вы заметили, что у зрителя всё ещё нет видео, попробуйте добавить дополнительные логирования в streamer.js и stream.js, чтобы убедиться, что потоки передаются корректно.

6. Дополнительные Советы для Отладки
Проверка Консоли Браузера:
Откройте инструменты разработчика (F12) на обоих устройствах (стример и зритель) и проверьте наличие ошибок в консоли.
Проверка Потока Видео-Файла:
Убедитесь, что видео-файл правильно загружается и воспроизводится в стримере.
Проверьте, что videoStream правильно захвачен после начала воспроизведения.
Проверка Peer Connections:
Добавьте console.log внутри функций createPeerConnection для отслеживания настройки соединений.
Убедитесь, что зритель получает описание удалённого соединения и устанавливает его корректно.
Проверка Signaling:
Убедитесь, что сигнальные сообщения (offer, answer, candidate) правильно отправляются и принимаются между стримером и зрителем.
Проверка Правил CORS и HTTPS:
Если вы запускаете сервер локально, убедитесь, что оба клиента подключаются через тот же протокол и порт, чтобы избежать проблем с CORS.
Для WebRTC требуется, чтобы сайт был доступен через HTTPS, особенно при доступе из браузеров Chrome и Firefox. Для локальных тестов это можно обойти с помощью localhost.
7. Полный Обновлённый Код
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>
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; // Используется для captureStream()
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);
            setupPeerConnections();
        };
        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) {
                // Поток уже захвачен в обработчике 'onplaying'
                if (videoStream) {
                    setupPeerConnections();
                } 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);
    }
}

// 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 = '';
}
3. Проверка stream.js (Зритель)
Убедимся, что зритель корректно принимает и отображает поток. В большинстве случаев stream.js из предыдущего ответа должен работать корректно. Однако, для дополнительной уверенности, добавим несколько обновлений.


// 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';
        }
    };
}
4. Дополнительные Рекомендации
Проверка Браузера:
Убедитесь, что вы используете современные браузеры, поддерживающие WebRTC и captureStream(), такие как последние версии Chrome, Firefox или Edge.
HTTPS:
Для работы WebRTC в некоторых браузерах требуется, чтобы сайт был доступен через HTTPS. Для локальной разработки localhost обычно считается безопасным контекстом, но если вы размещаете сервер на удалённом хостинге, убедитесь, что используете SSL-сертификат.
TURN-серверы:
При использовании WebRTC важно иметь настроенные STUN и TURN-серверы для обеспечения стабильных соединений, особенно при NAT-переполнениях. В нашем примере используется только STUN-сервер Google. Для более надёжных соединений рекомендуется добавить TURN-серверы.
Отладка:
Добавьте дополнительные console.log в streamer.js и stream.js для отслеживания состояния соединений и получения подробной информации о передаче потоков.
Обработка Ошибок:
Добавьте обработчики ошибок для различных событий WebRTC, чтобы своевременно получать уведомления о возможных сбоях.
5. Тестирование
После внесения изменений рекомендуется выполнить следующие шаги для тестирования:

Запустите Сервер:

node server.js
Убедитесь, что сервер запущен без ошибок.
Запустите Стример:
Перейдите в браузере по адресу http://localhost:3000/streamer.html.
Выберите "Веб-камера" или "Видео-файл".
Если выбрали "Видео-файл", загрузите видео-файл и дождитесь его начала воспроизведения.
Нажмите "Начать стрим". Ссылка для зрителей появится в поле streamLink.
Запустите Зрителя:
Откройте новое окно или вкладку браузера.
Перейдите по адресу http://localhost:3000/viewer.html.
Выберите активный стрим из списка.
Должно открыться окно stream.html с видео-трансляцией.
Проверьте Трансляцию:
Убедитесь, что видео отображается у зрителя.
Протестируйте переключение между стримером с веб-камерой и стримером с видео-файлом.
Проверьте, что при окончании трансляции зрители получают уведомление и перенаправляются на список стримов.