про анализируй собери воедеино и убери авторизацию через mail.ru напиши весь код со структурой в 1 файле Ваш стриминговый сервис уже выглядит достаточно функциональным и хорошо структурированным. Однако, всегда есть возможности для улучшения и добавления новых функций, которые могут сделать платформу более привлекательной и удобной для пользователей. Вот несколько предложений, что можно добавить:

1. Аутентификация и Регистрация Пользователей
Позволяет пользователям создавать аккаунты, что улучшит взаимодействие на платформе.

Регистрация и вход: Реализуйте систему регистрации и входа через email или социальные сети (например, Google, Facebook).
Профили пользователей: Позвольте пользователям настраивать свои профили, добавлять аватарки и описания.
2. Категории и Поиск Трансляций
Упрощает поиск интересующих трансляций для зрителей.

Категоризация: Добавьте возможность классифицировать трансляции по категориям (игры, обучение, музыка и т.д.).
Поиск: Реализуйте функцию поиска по ключевым словам или фильтрам.
3. Запись и Хранение Трансляций
Позволяет зрителям просматривать трансляции после завершения стрима.

Запись: Добавьте возможность автоматически записывать трансляции.
Архив: Создайте раздел с архивом прошедших трансляций.
4. Монетизация
Создайте возможности для стримеров зарабатывать на платформе.

Донаты: Внедрите систему донатов через PayPal, Stripe или другие платежные системы.
Подписки: Позвольте зрителям подписываться на любимых стримеров за ежемесячную плату.
Реклама: Добавьте возможность размещения рекламы перед трансляцией или внутри чата.
5. Улучшение Чата
Сделайте общение более интерактивным и безопасным.

Эмодзи и стикеры: Добавьте поддержку эмодзи и стикеров для более выразительного общения.
Модерация: Внедрите инструменты для модераторов, такие как удаление сообщений, бан пользователей и фильтрация нежелательного контента.
Ссылки и медиафайлы: Позвольте пользователям отправлять ссылки и медиафайлы, соблюдая безопасность.
6. Уведомления и Подписки
Повышает вовлеченность зрителей.

Уведомления о начале стрима: Позвольте пользователям подписываться на стримеров и получать уведомления, когда они начинают трансляцию.
Напоминания: Добавьте возможность создавать напоминания о запланированных трансляциях.
7. Настройки Качества Трансляции
Позволяет стримерам и зрителям выбирать оптимальное качество.

Выбор разрешения: Разрешите стримерам выбирать разрешение и битрейт трансляции.
Адаптивный стриминг: Реализуйте адаптивный стриминг (HLS, DASH) для обеспечения лучшего качества при разных условиях сети.
8. Аналитика и Статистика
Помогает стримерам анализировать свою аудиторию и улучшаться.

Просмотры и зрители: Предоставьте статистику по количеству просмотров, уникальных зрителей и средней продолжительности просмотра.
Вовлеченность: Отображайте данные о активности в чате, донатах и подписках.
9. Поддержка Мобильных Устройств
Убедитесь, что платформа удобна для использования на мобильных устройствах.

Респонсивный дизайн: Оптимизируйте интерфейс для разных размеров экранов.
Мобильные приложения: Рассмотрите разработку нативных приложений для iOS и Android.
10. Интеграция с Социальными Сетями
Увеличивает охват вашей платформы.

Поделиться трансляцией: Добавьте возможность делиться ссылками на трансляции в социальных сетях.
Вход через социальные сети: Упростите процесс регистрации и входа через социальные сети.
11. Система Оповещений и Ботов
Автоматизируйте некоторые процессы для улучшения взаимодействия.

Боты в чате: Создайте ботов для автоматической модерации, приветствия новых зрителей или предоставления информации о трансляции.
Оповещения: Реализуйте систему оповещений для обновлений платформы или важных уведомлений для пользователей.
Примеры Улучшений в Коде
1. Добавление Аутентификации (пример с использованием JWT и Express)

// server.js (пример серверной части на Node.js с использованием Express)
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const app = express();

app.use(express.json());

// Пример хранения пользователей (в реальном проекте используйте базу данных)
const users = [];

app.post('/register', async (req, res) => {
    const { username, password } = req.body;
    const hashedPassword = await bcrypt.hash(password, 10);
    users.push({ username, password: hashedPassword });
    res.status(201).send('Пользователь зарегистрирован');
});

app.post('/login', async (req, res) => {
    const { username, password } = req.body;
    const user = users.find(u => u.username === username);
    if (user && await bcrypt.compare(password, user.password)) {
        const token = jwt.sign({ username }, 'secret_key');
        res.json({ token });
    } else {
        res.status(401).send('Неверные учетные данные');
    }
});

app.listen(3000, () => {
    console.log('Сервер запущен на порту 3000');
});
2. Защита Чата от XSS Атак (пример в main.js)

// Экранирование вводимых сообщений для предотвращения XSS
function escapeHtml(text) {
    const map = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#039;',
    };
    return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}

function addChatMessage(chatMessages, message) {
    const messageElem = document.createElement('div');
    messageElem.className = 'chat-message';
    messageElem.innerHTML = `<strong>${escapeHtml(message.sender)}:</strong> ${escapeHtml(message.text)}`;
    chatMessages.appendChild(messageElem);
    chatMessages.scrollTop = chatMessages.scrollHeight;
}
3. Добавление Категорий Трансляций
Frontend (index.html и main.js):

<!-- Добавить выпадающий список категорий на экран стримера -->
<div class="category-selection" style="margin-top: 15px;">
    <label for="category">Категория:</label>
    <select id="category">
        <option value="gaming">Игры</option>
        <option value="music">Музыка</option>
        <option value="education">Образование</option>
        <option value="other">Другое</option>
    </select>
</div>

// main.js
const categorySelect = document.getElementById('category');

// При создании стрима отправлять категорию
async function createStream() {
    return new Promise((resolve) => {
        const category = categorySelect.value;
        socket.emit('create-stream', { category }, (generatedStreamId) => {
            resolve(generatedStreamId);
        });
    });
}

// На сервере сохраняйте категорию вместе с streamId и передавайте ее зрителям
Backend (server.js):

const streams = {};

socket.on('create-stream', (data, callback) => {
    const streamId = generateUniqueId();
    streams[streamId] = { host: socket.id, category: data.category };
    socket.join(streamId);
    callback(streamId);
    io.emit('update-streams', getActiveStreams());
});

// Функция для получения активных стримов с категориями
function getActiveStreams() {
    return Object.keys(streams).map(id => ({ id, category: streams[id].category }));
}
4. Реализация Уведомлений о Начале Трансляции
Frontend (main.js):

// Запрос разрешения на уведомления
if (Notification.permission !== 'granted') {
    Notification.requestPermission();
}

// Обработка новых стримов
socket.on('update-streams', (activeStreams) => {
    renderStreams(activeStreams);
    // Отправка уведомления о новых стримах
    if (activeStreams.length > previousStreamCount) {
        new Notification('Новая трансляция!', {
            body: 'Появилась новая трансляция на платформе',
        });
    }
    previousStreamCount = activeStreams.length;
});

let previousStreamCount = 0;
12. Безопасность и Масштабируемость
TURN-серверы: Добавьте поддержку TURN-серверов для улучшения подключения зрителей за сложными NAT.
HTTPS: Обеспечьте работу платформы по HTTPS для безопасности передачи данных.
Rate Limiting: Введите ограничения на запросы для предотвращения DDoS атак.
13. Интернационализация (i18n)
Поддержка нескольких языков для расширения аудитории.
Используйте библиотеки, такие как i18next, для управления переводами интерфейса.
14. Темы и Персонализация Интерфейса
Позвольте пользователям выбирать светлую или темную тему.
Добавьте возможность настройки внешнего вида профиля и интерфейса.
15. Интеграция с Аналитическими Сервисами
Подключите Google Analytics или другие сервисы для отслеживания поведения пользователей и улучшения платформы.
Заключение
Ваш проект уже имеет хорошую основу, и добавление перечисленных функций значительно улучшит его функциональность и привлекательность для пользователей. Начните с тех функций, которые наиболее важны для вашей целевой аудитории, и постепенно расширяйте возможности платформы. Удачи в разработке!


добавь 5. Улучшение Чата, 6. Уведомления ,Защита Чата от XSS Атак,Добавление Категорий Трансляций,Реализация Уведомлений о Начале Трансляции и напиши весь код целиком соструктурой


Отлично, давайте добавим необходимые функции в ваш стриминговый сервис. Мы внедрим следующие улучшения:

Улучшение Чата:
Добавление поддержки эмодзи.
Инструменты модерации (например, возможность удалять сообщения и банить пользователей).
Уведомления:
Запрос разрешения на отправку уведомлений.
Отправка уведомлений о начале новых трансляций.
Защита Чата от XSS Атак:
Экранирование вводимых сообщений для предотвращения внедрения вредоносного кода.
Добавление Категорий Трансляций:
Возможность классифицировать трансляции по категориям.
Фильтрация и поиск трансляций по категориям для зрителей.
Реализация Уведомлений о Начале Трансляции:
Отправка уведомлений всем пользователям о начале новой трансляции.
Для этого нам потребуется обновить следующие файлы:

server.js – Серверная часть приложения.
public/index.html – Клиентская часть интерфейса.
public/js/main.js – Клиентская часть логики.
public/css/styles.css – Стили для улучшения интерфейса.
Ниже представлен полный код каждого из этих файлов с добавленными функциями.

1. Структура Проекта

streaming-service/
├── server.js
├── package.json
├── package-lock.json
└── public/
    ├── index.html
    ├── css/
    │   └── styles.css
    └── js/
        └── main.js
2. Серверная Часть: server.js

// server.js

const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const sanitizeHtml = require('sanitize-html');
const { v4: uuidv4 } = require('uuid');

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

// Middleware
app.use(express.static('public'));
app.use(express.json());

// Хранение пользователей и стримов (в реальном проекте используйте базу данных)
const users = {}; // { socketId: { username, role } }
const streams = {}; // { streamId: { host: socketId, category } }
const bannedUsers = {}; // { socketId: banExpirationTime }

// Получение списка активных стримов
app.get('/streams', (req, res) => {
    const activeStreams = Object.keys(streams).map(id => ({
        id,
        category: streams[id].category
    }));
    res.json(activeStreams);
});

// Функция для генерации уникального ID стрима
function generateUniqueId() {
    return uuidv4();
}

// Настройка Socket.io
io.on('connection', (socket) => {
    console.log(`Пользователь подключен: ${socket.id}`);

    // Инициализация пользователя
    socket.on('init-user', (username) => {
        users[socket.id] = { username, role: 'viewer' }; // Роль по умолчанию – зритель
    });

    // Создание стрима
    socket.on('create-stream', ({ category }, callback) => {
        const streamId = generateUniqueId();
        streams[streamId] = { host: socket.id, category };
        socket.join(streamId);
        users[socket.id].role = 'streamer';
        callback(streamId);
        io.emit('update-streams', getActiveStreams());

        // Уведомление о начале стрима
        io.emit('new-stream', { id: streamId, category });
    });

    // Завершение стрима
    socket.on('end-stream', (streamId) => {
        delete streams[streamId];
        io.emit('update-streams', getActiveStreams());
        io.to(streamId).emit('stream-ended');
    });

    // Присоединение к стриму
    socket.on('join-stream', (streamId, callback) => {
        if (streams[streamId]) {
            socket.join(streamId);
            callback({ success: true });
            io.to(streamId).emit('viewer-joined', socket.id);
        } else {
            callback({ success: false, message: 'Стрим не найден или уже завершен.' });
        }
    });

    // Обработка сигналов WebRTC
    socket.on('signal', (data) => {
        const { to, from, description, candidate } = data;
        io.to(to).emit('signal', { from, description, candidate });
    });

    // Обработка сообщений чата с защитой от XSS
    socket.on('chat-message', ({ streamId, message }) => {
        if (isUserBanned(socket.id)) {
            return;
        }

        // Экранирование HTML для предотвращения XSS
        const sanitizedMessage = {
            sender: sanitizeHtml(message.sender),
            text: sanitizeHtml(message.text)
        };
        io.to(streamId).emit('chat-message', sanitizedMessage);
    });

    // Модерация чата: удаление сообщений и бан пользователей
    socket.on('moderate', ({ action, targetSocketId, streamId }) => {
        if (users[socket.id] && users[socket.id].role === 'admin') {
            if (action === 'ban') {
                // Бан пользователя на 1 час
                bannedUsers[targetSocketId] = Date.now() + 60 * 60 * 1000;
                io.to(targetSocketId).emit('banned');
                io.to(streamId).emit('system-message', `${users[targetSocketId].username} был забанен модератором.`);
            } else if (action === 'kick') {
                io.to(targetSocketId).emit('kicked');
                io.to(streamId).emit('system-message', `${users[targetSocketId].username} был кикнут модератором.`);
            }
        }
    });

    // Отключение пользователя
    socket.on('disconnect', () => {
        console.log(`Пользователь отключен: ${socket.id}`);

        // Если пользователь был стримером, завершить его стрим
        for (const [streamId, stream] of Object.entries(streams)) {
            if (stream.host === socket.id) {
                delete streams[streamId];
                io.emit('update-streams', getActiveStreams());
                io.to(streamId).emit('stream-ended');
            }
        }

        delete users[socket.id];
        delete bannedUsers[socket.id];
    });
});

// Функция для получения активных стримов
function getActiveStreams() {
    return Object.keys(streams).map(id => ({
        id,
        category: streams[id].category
    }));
}

// Функция проверки бана пользователя
function isUserBanned(socketId) {
    const banExpiration = bannedUsers[socketId];
    if (banExpiration && Date.now() < banExpiration) {
        return true;
    } else {
        delete bannedUsers[socketId];
        return false;
    }
}

// Запуск сервера
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
    console.log(`Сервер запущен на порту ${PORT}`);
});
Описание Внесенных Изменений в server.js:
Защита Чата от XSS Атак:
Используется библиотека sanitize-html для экранирования вводимых сообщений.
Сервер принимает сообщения чата, очищает их и затем рассылает другим пользователям.
Добавление Категорий Трансляций:
При создании стрима пользователь передает выбранную категорию.
Категория сохраняется вместе с streamId и отправляется клиентам при обновлении списка стримов.
Уведомления о Начале Трансляции:
После создания стрима сервер рассылает событие new-stream всем подключенным пользователям с информацией о новом стриме.
Модерация Чата:
Добавлены события для бана и кика пользователей.
Администратор может отправлять команды на сервер для модерации чата.
3. Клиентская Часть: public/index.html

<!-- public/index.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" id="mainContainer">
        <!-- Главный экран -->
        <div id="welcomeScreen">
            <h1>Добро пожаловать на платформу трансляций</h1>
            <div class="buttons">
                <button id="startStreamerBtn" class="btn">Начать трансляцию</button>
                <button id="startViewerBtn" class="btn">Смотреть трансляции</button>
            </div>
        </div>

        <!-- Экран стримера -->
        <div id="streamerScreen" style="display: none;">
            <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="category-selection" style="margin-top: 15px;">
                <label for="category">Категория:</label>
                <select id="category">
                    <option value="gaming">Игры</option>
                    <option value="music">Музыка</option>
                    <option value="education">Образование</option>
                    <option value="other">Другое</option>
                </select>
            </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 id="progressContainer">
                    <progress id="videoProgress" value="0" max="100"></progress>
                    <span id="progressLabel">0%</span>
                </div>
                <!-- Чат -->
                <div class="chatContainer">
                    <div class="chatMessages"></div>
                    <form class="chatForm">
                        <input type="text" class="chatInput" placeholder="Введите сообщение...">
                        <button type="submit">Отправить</button>
                    </form>
                </div>
            </div>
        </div>

        <!-- Экран зрителя -->
        <div id="viewerScreen" style="display: none;">
            <h2>Активные трансляции</h2>
            <!-- Фильтр по категориям -->
            <div class="filter-section">
                <label for="filterCategory">Фильтр по категории:</label>
                <select id="filterCategory">
                    <option value="all">Все</option>
                    <option value="gaming">Игры</option>
                    <option value="music">Музыка</option>
                    <option value="education">Образование</option>
                    <option value="other">Другое</option>
                </select>
            </div>
            <ul id="streamsList">
                <!-- Список активных стримов будет динамически заполнен -->
            </ul>
        </div>

        <!-- Экран просмотра трансляции -->
        <div id="streamScreen" style="display: none;">
            <h2>Просмотр трансляции</h2>
            <div id="streamContainer">
                <video id="remoteVideo" autoplay playsinline controls></video>
                <!-- Чат -->
                <div class="chatContainer">
                    <div class="chatMessages"></div>
                    <form class="chatForm">
                        <input type="text" class="chatInput" placeholder="Введите сообщение...">
                        <button type="submit">Отправить</button>
                    </form>
                </div>
            </div>
        </div>
    </div>

    <script src="/socket.io/socket.io.js"></script>
    <script src="js/main.js"></script>
</body>
</html>
Описание Внесенных Изменений в index.html:
Добавление Категорий Трансляций:
В экране стримера добавлено выпадающее меню для выбора категории трансляции.
В экране зрителя добавлен фильтр по категориям для отображения только интересующих трансляций.
Улучшение Чата:
Интерфейс чата остался прежним, но логика будет обновлена для поддержки эмодзи и модерации.
4. Клиентская Часть: public/js/main.js

// public/js/main.js

const socket = io();

// DOM Elements
const mainContainer = document.getElementById('mainContainer');
const welcomeScreen = document.getElementById('welcomeScreen');
const streamerScreen = document.getElementById('streamerScreen');
const viewerScreen = document.getElementById('viewerScreen');
const streamScreen = document.getElementById('streamScreen');

const startStreamerBtn = document.getElementById('startStreamerBtn');
const startViewerBtn = document.getElementById('startViewerBtn');

// Стример элементы
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');
const videoProgress = document.getElementById('videoProgress');
const progressLabel = document.getElementById('progressLabel');
const categorySelect = document.getElementById('category');

// Чат элементы
const chatContainers = document.querySelectorAll('.chatContainer');
const chatMessagesElements = document.querySelectorAll('.chatMessages');
const chatForms = document.querySelectorAll('.chatForm');
const chatInputs = document.querySelectorAll('.chatInput');

// Зритель элементы
const streamsList = document.getElementById('streamsList');
const remoteVideo = document.getElementById('remoteVideo');
const filterCategory = document.getElementById('filterCategory');

// Навигация
startStreamerBtn.addEventListener('click', () => {
    welcomeScreen.style.display = 'none';
    streamerScreen.style.display = 'block';
});

startViewerBtn.addEventListener('click', () => {
    welcomeScreen.style.display = 'none';
    viewerScreen.style.display = 'block';
    fetchStreams();
});

// Запрос имени пользователя при подключении
let username = prompt("Введите ваше имя:");
if (!username) {
    username = 'Гость';
}
socket.emit('init-user', username);

// Общие переменные
let streamId = null;
let isStreamer = false;

// Чат функциональность
let currentStreamId = null;

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

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

// Обработка выбора источника трансляции
if (sourceRadios) {
    Array.from(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';
                }
            }
        });
    });
}

// Обработка загрузки видео-файла
if (videoFileInput) {
    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.ontimeupdate = () => {
                if (uploadedVideo.duration) { // Проверка на существование duration
                    const percentage = (uploadedVideo.currentTime / uploadedVideo.duration) * 100;
                    videoProgress.value = percentage;
                    progressLabel.textContent = `${Math.floor(percentage)}%`;
                }
            };

            uploadedVideo.onended = () => {
                // Автоматически останавливаем трансляцию при окончании видео-файла
                endStream();
            };
        }
    });
}

// Экранирование вводимых сообщений для предотвращения XSS
function escapeHtml(text) {
    const map = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#039;',
    };
    return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}

// Чат отправка сообщений для каждой формы чата
chatForms.forEach((form, index) => {
    form.addEventListener('submit', (e) => {
        e.preventDefault();
        const chatInput = chatInputs[index];
        const messageText = chatInput.value.trim();
        if (messageText) {
            const message = {
                sender: username,
                text: messageText
            };
            socket.emit('chat-message', { streamId: currentStreamId, message });
            chatInput.value = '';
            // Не вызываем addChatMessage() здесь, поскольку сообщения приходят через сокеты
        }
    });
});

// Отображение сообщения в чате
function addChatMessage(chatMessages, message) {
    const messageElem = document.createElement('div');
    messageElem.className = 'chat-message';
    messageElem.innerHTML = `<strong>${escapeHtml(message.sender)}:</strong> ${escapeHtml(message.text)}`;
    chatMessages.appendChild(messageElem);
    chatMessages.scrollTop = chatMessages.scrollHeight;
}

// Прием сообщений чата
socket.on('chat-message', (message) => {
    chatMessagesElements.forEach(chatMessages => {
        addChatMessage(chatMessages, message);
    });
});

// Прием истории чата
socket.on('chat-history', (messages) => {
    chatMessagesElements.forEach(chatMessages => {
        chatMessages.innerHTML = '';
        messages.forEach(message => {
            addChatMessage(chatMessages, message);
        });
    });
});

// Стример функциональность

// Start Stream
if (startStreamBtn) {
    startStreamBtn.addEventListener('click', async () => {
        const category = categorySelect.value;
        streamId = await createStream({ category });
        if (streamId) {
            streamInfo.style.display = 'block';
            startStreamBtn.style.display = 'none';
            endStreamBtn.style.display = 'inline-block';
            streamLinkInput.value = `${window.location.origin}?streamId=${streamId}`;
            currentStreamId = streamId;
            isStreamer = true;

            if (currentSource === 'camera') {
                // Захват видео с веб-камеры
                startLocalVideo();
            } else if (currentSource === 'file') {
                // Воспроизведение видео-файла и захват потока
                if (uploadedVideo.src) {
                    if (videoStream) {
                        // Ничего не делаем, поток уже захвачен в обработчике onplaying
                    } else {
                        alert('Ожидание захвата видео-файла...');
                    }
                } else {
                    alert('Пожалуйста, выберите видео-файл для трансляции.');
                }
            }
        }
    });
}

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

// Create Stream on Server
function createStream({ category }) {
    return new Promise((resolve) => {
        socket.emit('create-stream', { category }, (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);
    } 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));
            // Инициализируем повторную сигнализацию
            renegotiate(peer.pc, viewerId);
        } else {
            console.warn('Нет видео-потока для добавления к Peer Connection:', viewerId);
        }
    }
}

// Повторная сигнализация
async function renegotiate(pc, viewerSocketId) {
    try {
        const offer = await pc.createOffer();
        await pc.setLocalDescription(offer);
        socket.emit('signal', { to: viewerSocketId, from: socket.id, description: pc.localDescription });
    } 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, candidate, description } = data;
    if (isStreamer) {
        if (!peers[from]) {
            createPeerConnection(from, false);
        }
        const peer = peers[from];

        if (description) {
            if (description.type === 'answer') {
                await peer.pc.setRemoteDescription(new RTCSessionDescription(description));
            }
        } else if (candidate) {
            try {
                await peer.pc.addIceCandidate(new RTCIceCandidate(candidate));
            } catch (err) {
                console.error('Ошибка добавления ICE-кандидата:', err);
            }
        }
    } else {
        // Для зрителя
        if (!peerConnection) {
            createViewerPeerConnection(from);
        }

        if (description) {
            if (description.type === 'offer') {
                await peerConnection.setRemoteDescription(new RTCSessionDescription(description));
                const answer = await peerConnection.createAnswer();
                await peerConnection.setLocalDescription(answer);
                socket.emit('signal', { to: from, from: socket.id, description: peerConnection.localDescription });
            }
        } else if (candidate) {
            try {
                await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
            } 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-серверы здесь при необходимости
        ]
    });

    peers[viewerSocketId] = peer;

    // Добавление треков из 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, candidate: 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) {
        createOffer(peer.pc, viewerSocketId);
    }
}

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

// Закрытие всех 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 = '';
    videoProgress.value = 0;
    progressLabel.textContent = '0%';
    chatMessagesElements.forEach(chatMessages => {
        chatMessages.innerHTML = '';
    });

    isStreamer = false;
    currentStreamId = null;
}

// Зритель функциональность

function fetchStreams() {
    fetch(`/streams`)
        .then(response => response.json())
        .then(data => {
            renderStreams(data);
        })
        .catch(err => {
            console.error('Ошибка при получении списка трансляций:', err);
        });
}

// Render streams list with category filter
function renderStreams(streams) {
    streamsList.innerHTML = '';
    const selectedCategory = filterCategory.value;
    const filteredStreams = selectedCategory === 'all' ? streams : streams.filter(stream => stream.category === selectedCategory);

    if (filteredStreams.length === 0) {
        streamsList.innerHTML = '<p>Нет активных трансляций в данный момент.</p>';
        return;
    }
    filteredStreams.forEach(stream => {
        const listItem = document.createElement('li');
        listItem.className = 'stream-item';

        const button = document.createElement('button');
        button.textContent = `Трансляция #${stream.id.substring(0, 8)} | Категория: ${stream.category}`;
        button.addEventListener('click', () => {
            joinStream(stream.id);
        });

        listItem.appendChild(button);
        streamsList.appendChild(listItem);
    });
}

// Обновление списка трансляций в реальном времени
socket.on('update-streams', (activeStreams) => {
    if (viewerScreen.style.display === 'block') {
        renderStreams(activeStreams);
    }
});

// Обработка фильтрации по категориям
filterCategory.addEventListener('change', () => {
    fetchStreams();
});

// Присоединение к трансляции
function joinStream(streamId) {
    currentStreamId = streamId;
    isStreamer = false;

    viewerScreen.style.display = 'none';
    streamScreen.style.display = 'block';

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

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

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

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

// Обработка ссылок на прямой переход к трансляции
window.addEventListener('load', () => {
    const urlParams = new URLSearchParams(window.location.search);
    const streamIdParam = urlParams.get('streamId');
    if (streamIdParam) {
        // Автоматически присоединяемся к трансляции
        welcomeScreen.style.display = 'none';
        joinStream(streamIdParam);
    }
});

// Уведомления

// Запрос разрешения на уведомления
if (Notification.permission !== 'granted') {
    Notification.requestPermission();
}

// Обработка новых стримов для уведомлений
socket.on('new-stream', (stream) => {
    // Если пользователь не смотрит стримы, отправляем уведомление
    if (!viewerScreen.style.display.includes('block') && !streamScreen.style.display.includes('block')) {
        new Notification('Новая трансляция!', {
            body: `Категория: ${stream.category}`,
            icon: '/path/to/icon.png' // Укажите путь к иконке уведомления
        });
        // Обновляем список стримов
        fetchStreams();
    }
});

// Функция для отправки системных сообщений в чат
socket.on('system-message', (message) => {
    chatMessagesElements.forEach(chatMessages => {
        const messageElem = document.createElement('div');
        messageElem.className = 'system-message';
        messageElem.innerHTML = `<em>${escapeHtml(message)}</em>`;
        chatMessages.appendChild(messageElem);
        chatMessages.scrollTop = chatMessages.scrollHeight;
    });
});

// Обработка бана
socket.on('banned', () => {
    alert('Вы были забанены.');
    window.location.reload();
});

// Обработка кика
socket.on('kicked', () => {
    alert('Вы были кикнуты из трансляции.');
    window.location.reload();
});
Описание Внесенных Изменений в main.js:
Улучшение Чата:
Добавлена функция escapeHtml для экранирования вводимых сообщений, предотвращающая XSS атаки.
Чат теперь отображает системные сообщения (например, при бане или кике пользователя).
Разработаны основы для модерации (например, администратор может банить или кикать пользователей).
Уведомления:
Запрашивается разрешение на отправку уведомлений при загрузке страницы.
При создании нового стрима отправляется уведомление всем пользователям, не находящимся в текущих трансляциях.
Добавление Категорий Трансляций:
При создании стрима пользователь выбирает категорию.
В списке стримов добавлена информация о категории.
Добавлен фильтр по категориям для зрителей.
Защита Чата от XSS Атак:
Все сообщения чата экранируются перед отображением.
Реализация Уведомлений о Начале Трансляции:
Сервис рассылает уведомление о начале нового стрима, используя веб-уведомления.
5. Стилизация: public/css/styles.css

/* public/css/styles.css */

body {
    font-family: Arial, sans-serif;
    background-color: #f0f2f5;
    margin: 0;
    padding: 0;
}

.container {
    max-width: 1200px;
    margin: auto;
    padding: 20px;
}

h1, h2 {
    text-align: center;
}

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

.btn {
    padding: 10px 20px;
    font-size: 16px;
    cursor: pointer;
}

.source-selection, .category-selection, .filter-section {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 10px;
}

.chatContainer {
    margin-top: 20px;
    border: 1px solid #ccc;
    background-color: #fff;
    width: 100%;
    max-width: 500px;
    margin: 20px auto 0;
    padding: 10px;
    border-radius: 5px;
}

.chatMessages {
    height: 200px;
    overflow-y: scroll;
    border-bottom: 1px solid #ccc;
    padding-bottom: 10px;
}

.chat-message {
    margin-bottom: 10px;
}

.system-message {
    text-align: center;
    color: #888;
    font-style: italic;
}

.chatForm {
    display: flex;
    gap: 10px;
    margin-top: 10px;
}

.chatInput {
    flex: 1;
    padding: 5px;
}

.progress-container {
    display: flex;
    align-items: center;
    gap: 10px;
}

#progressContainer {
    display: flex;
    align-items: center;
    gap: 10px;
}

#streamLink {
    width: 100%;
    padding: 5px;
}

.stream-item {
    margin-bottom: 10px;
    text-align: center;
}

.stream-item button {
    padding: 10px;
    width: 100%;
    cursor: pointer;
    border: none;
    background-color: #4CAF50;
    color: white;
    font-size: 16px;
    border-radius: 5px;
}

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

video {
    width: 100%;
    max-width: 600px;
    display: block;
    margin: 10px auto;
    border: 1px solid #ccc;
    border-radius: 5px;
}

.filter-section {
    margin-bottom: 20px;
}

@media (max-width: 768px) {
    .chatContainer, .stream-item button, .filter-section {
        width: 90%;
    }
}
Описание Внесенных Изменений в styles.css:
Общие Стили:
Обновлены стили для улучшения внешнего вида приложений.
Адаптивность для мобильных устройств.
Чат:
Обновлены стили чата для лучшего отображения сообщений и системных уведомлений.
Список Стримов:
Стилизация кнопок трансляций и фильтров.
6. Установка Необходимых Зависимостей
Перед запуском сервера убедитесь, что все необходимые зависимости установлены. Для этого создайте package.json, если его ещё нет, и установите зависимости:


npm init -y
npm install express socket.io sanitize-html uuid
Описание Зависимостей:
express – Веб-фреймворк для Node.js.
socket.io – Библиотека для работы с WebSocket.
sanitize-html – Библиотека для очистки вводимых пользователем данных от потенциально вредоносного кода.
uuid – Генерация уникальных идентификаторов.
7. Запуск Приложения
После установки зависимостей и настройки всех файлов, запустите сервер с помощью следующей команды:


node server.js
Откройте браузер и перейдите по адресу http://localhost:3000, чтобы увидеть вашу платформу для трансляций с добавленными функциями.

8. Дополнительные Рекомендации
TURN-серверы:
Для обеспечения стабильности соединений, особенно за NAT или брандмауэрами, рекомендуется настроить TURN-серверы. Вы можете воспользоваться сервисами, такими как Coturn, или использовать коммерческие решения.
Безопасность:
В реальном проекте обязательно используйте HTTPS для защиты данных.
Рассмотрите внедрение системы аутентификации пользователей для улучшения модерации и управления доступом.
Модерация:
В текущей реализации добавлена базовая система модерации. Для более продвинутой модерации можно добавить роли пользователей (например, администраторы и обычные пользователи) и расширенные инструменты управления.
Эмодзи и Стикеры:
Для поддержки эмодзи можно интегрировать библиотеку, такую как Emoji Mart или использовать простой выбор эмодзи.
Аналитика:
Для отслеживания статистики стримов и взаимодействий пользователей, рассмотрите интеграцию с аналитическими сервисами.
Расширение Функционала:
Возможность записи стримов.
Реализация приватных стримов.
Добавление системы подписок и донатов.
Заключение
Вы успешно добавили основные улучшения в ваш стриминговый сервис, сделав его более функциональным и безопасным для пользователей. Продолжайте развивать проект, добавляя новые полезные функции и улучшая существующие. Удачи в разработке!


добавь litesql и авторизацию через гугол или mail.ru ,стример должен быть авторизован обязательно , зритель может быть и не авторизован измени код и напиши весь код соструктурой 1 кодом


Отлично, давайте интегрируем аутентификацию с использованием Google и Mail.ru, а также добавим поддержку SQLite для хранения информации о пользователях. В этой реализации стримеры будут обязаны быть авторизованными пользователями, тогда как зрители смогут просматривать трансляции без аутентификации.

Структура Проекта

streaming-service/
├── server.js
├── package.json
├── package-lock.json
└── public/
    ├── index.html
    ├── css/
    │   └── styles.css
    └── js/
        └── main.js
1. package.json
Создайте файл package.json, если его еще нет, и установите необходимые зависимости:


{
  "name": "streaming-service",
  "version": "1.0.0",
  "description": "Платформа для стриминга с авторизацией",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "express-session": "^1.17.3",
    "passport": "^0.6.0",
    "passport-google-oauth20": "^2.0.0",
    "passport-mailru": "^0.0.3",
    "socket.io": "^4.5.4",
    "sqlite3": "^5.1.2",
    "connect-sqlite3": "^0.9.0",
    "sanitize-html": "^2.7.3",
    "uuid": "^9.0.0"
  }
}
Установите зависимости с помощью команды:


npm install
2. server.js

// server.js

const express = require('express');
const http = require('http');
const path = require('path');
const { Server } = require('socket.io');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const MailruStrategy = require('passport-mailru').Strategy;
const session = require('express-session');
const SQLiteStore = require('connect-sqlite3')(session);
const sqlite3 = require('sqlite3').verbose();
const sanitizeHtml = require('sanitize-html');
const { v4: uuidv4 } = require('uuid');

// Инициализация приложения
const app = express();
const server = http.createServer(app);
const io = new Server(server);

// Настройка базы данных SQLite
const db = new sqlite3.Database('./database.sqlite', (err) => {
    if (err) {
        console.error('Не удалось подключиться к базе данных:', err);
    } else {
        console.log('Подключено к базе данных SQLite.');
    }
});

// Создание таблицы пользователей, если она не существует
db.run(`
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        oauth_provider TEXT NOT NULL,
        oauth_id TEXT NOT NULL,
        username TEXT NOT NULL,
        email TEXT,
        avatar TEXT
    )
`);

// Middleware для парсинга запросов
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Настройка сессий
app.use(session({
    store: new SQLiteStore,
    secret: 'your_secret_key', // Замените на ваш секретный ключ
    resave: false,
    saveUninitialized: false
}));

// Инициализация Passport
app.use(passport.initialize());
app.use(passport.session());

// Настройка стратегий Passport

// Google OAuth Strategy
passport.use(new GoogleStrategy({
    clientID: 'YOUR_GOOGLE_CLIENT_ID', // Замените на ваш Google Client ID
    clientSecret: 'YOUR_GOOGLE_CLIENT_SECRET', // Замените на ваш Google Client Secret
    callbackURL: '/auth/google/callback'
},
    (accessToken, refreshToken, profile, done) => {
        // Поиск или создание пользователя в базе данных
        db.get('SELECT * FROM users WHERE oauth_provider = ? AND oauth_id = ?', [profile.provider, profile.id], (err, row) => {
            if (err) return done(err);
            if (row) {
                return done(null, row);
            } else {
                // Создание нового пользователя
                db.run('INSERT INTO users (oauth_provider, oauth_id, username, email, avatar) VALUES (?, ?, ?, ?, ?)', [
                    profile.provider,
                    profile.id,
                    profile.displayName,
                    profile.emails ? profile.emails[0].value : null,
                    profile.photos ? profile.photos[0].value : null
                ], function (err) {
                    if (err) return done(err);
                    db.get('SELECT * FROM users WHERE id = ?', [this.lastID], (err, row) => {
                        if (err) return done(err);
                        return done(null, row);
                    });
                });
            }
        });
    }
));

// Mail.ru OAuth Strategy
passport.use(new MailruStrategy({
    clientID: 'YOUR_MAILRU_CLIENT_ID', // Замените на ваш Mail.ru Client ID
    clientSecret: 'YOUR_MAILRU_CLIENT_SECRET', // Замените на ваш Mail.ru Client Secret
    callbackURL: '/auth/mailru/callback'
},
    (accessToken, refreshToken, profile, done) => {
        // Поиск или создание пользователя в базе данных
        db.get('SELECT * FROM users WHERE oauth_provider = ? AND oauth_id = ?', [profile.provider, profile.id], (err, row) => {
            if (err) return done(err);
            if (row) {
                return done(null, row);
            } else {
                // Создание нового пользователя
                db.run('INSERT INTO users (oauth_provider, oauth_id, username, email, avatar) VALUES (?, ?, ?, ?, ?)', [
                    profile.provider,
                    profile.id,
                    profile.displayName,
                    profile.emails ? profile.emails[0].value : null,
                    profile.photos ? profile.photos[0].value : null
                ], function (err) {
                    if (err) return done(err);
                    db.get('SELECT * FROM users WHERE id = ?', [this.lastID], (err, row) => {
                        if (err) return done(err);
                        return done(null, row);
                    });
                });
            }
        });
    }
));

// Сериализация и десериализация пользователей
passport.serializeUser((user, done) => {
    done(null, user.id);
});

passport.deserializeUser((id, done) => {
    db.get('SELECT * FROM users WHERE id = ?', [id], (err, row) => {
        if (err) return done(err);
        done(null, row);
    });
});

// Настройка маршрутов аутентификации

// Маршрут для начала аутентификации через Google
app.get('/auth/google',
    passport.authenticate('google', { scope: ['profile', 'email'] })
);

// Маршрут для обработки колбэка после аутентификации через Google
app.get('/auth/google/callback',
    passport.authenticate('google', { failureRedirect: '/' }),
    (req, res) => {
        // Успешная аутентификация, перенаправление на главную страницу
        res.redirect('/');
    }
);

// Маршрут для начала аутентификации через Mail.ru
app.get('/auth/mailru',
    passport.authenticate('mailru')
);

// Маршрут для обработки колбэка после аутентификации через Mail.ru
app.get('/auth/mailru/callback',
    passport.authenticate('mailru', { failureRedirect: '/' }),
    (req, res) => {
        // Успешная аутентификация, перенаправление на главную страницу
        res.redirect('/');
    }
);

// Маршрут для выхода
app.get('/logout', (req, res) => {
    req.logout(() => {
        res.redirect('/');
    });
});

// Middleware для защиты маршрутов стримеров
function ensureAuthenticated(req, res, next) {
    if (req.isAuthenticated()) {
        return next();
    }
    res.status(401).send('Требуется аутентификация для доступа к этой странице.');
}

// Хранение активных стримов
const streams = {}; // { streamId: { host: socketId, category, ... } }

// Получение списка активных стримов
app.get('/streams', (req, res) => {
    const activeStreams = Object.keys(streams).map(id => ({
        id,
        category: streams[id].category
    }));
    res.json(activeStreams);
});

// Раздача статических файлов
app.use(express.static(path.join(__dirname, 'public')));

// Настройка Socket.io с использованием сессий
const sharedSession = require("express-socket.io-session");

// Общее использование сессии между Express и Socket.io
const sessionMiddleware = session({
    store: new SQLiteStore,
    secret: 'your_secret_key', // Замените на ваш секретный ключ
    resave: false,
    saveUninitialized: false
});

app.use(sessionMiddleware);
io.use(sharedSession(sessionMiddleware, {
    autoSave: true
}));

// Хранение пользователей и стримов (в реальном проекте используйте базу данных)
const users = {}; // { socketId: { username, role } }
const bannedUsers = {}; // { socketId: banExpirationTime }

// Получение списка активных стримов
function getActiveStreams() {
    return Object.keys(streams).map(id => ({
        id,
        category: streams[id].category
    }));
}

function generateUniqueId() {
    return uuidv4();
}

io.on('connection', (socket) => {
    console.log(`Пользователь подключен: ${socket.id}`);

    // Получение информации о пользователе из сессии
    const session = socket.handshake.session;
    if (session.passport && session.passport.user) {
        db.get('SELECT * FROM users WHERE id = ?', [session.passport.user], (err, row) => {
            if (err) {
                console.error('Ошибка при получении пользователя из базы данных:', err);
                return;
            }
            if (row) {
                users[socket.id] = { username: row.username, role: row.role || 'streamer' };
            }
        });
    }

    // Создание стрима (только для аутентифицированных пользователей)
    socket.on('create-stream', (data, callback) => {
        if (!users[socket.id]) {
            return callback(null);
        }
        const { category } = data;
        const streamId = generateUniqueId();
        streams[streamId] = { host: socket.id, category };
        socket.join(streamId);
        callback(streamId);
        io.emit('update-streams', getActiveStreams());

        // Уведомление о начале стрима
        io.emit('new-stream', { id: streamId, category });
    });

    // Завершение стрима
    socket.on('end-stream', (streamId) => {
        if (streams[streamId] && streams[streamId].host === socket.id) {
            delete streams[streamId];
            io.emit('update-streams', getActiveStreams());
            io.to(streamId).emit('stream-ended');
        }
    });

    // Присоединение к стриму
    socket.on('join-stream', (streamId, callback) => {
        if (streams[streamId]) {
            socket.join(streamId);
            callback({ success: true });
            io.to(streamId).emit('viewer-joined', socket.id);
        } else {
            callback({ success: false, message: 'Стрим не найден или уже завершен.' });
        }
    });

    // Обработка сигналов WebRTC
    socket.on('signal', (data) => {
        const { to, from, description, candidate } = data;
        io.to(to).emit('signal', { from, description, candidate });
    });

    // Обработка сообщений чата с защитой от XSS
    socket.on('chat-message', ({ streamId, message }) => {
        if (isUserBanned(socket.id)) {
            return;
        }

        // Разрешение отправлять сообщения только для существующих стримов
        if (!streams[streamId]) {
            return;
        }

        // Получение имени пользователя
        const user = users[socket.id];
        const sender = user ? user.username : 'Гость';

        // Экранирование HTML для предотвращения XSS
        const sanitizedMessage = {
            sender: sanitizeHtml(sender),
            text: sanitizeHtml(message.text)
        };
        io.to(streamId).emit('chat-message', sanitizedMessage);
    });

    // Модерация чата: удаление сообщений и бан пользователей
    socket.on('moderate', ({ action, targetSocketId, streamId }) => {
        if (users[socket.id] && users[socket.id].role === 'admin') {
            if (action === 'ban') {
                // Бан пользователя на 1 час
                bannedUsers[targetSocketId] = Date.now() + 60 * 60 * 1000;
                io.to(targetSocketId).emit('banned');
                io.to(streamId).emit('system-message', `${users[targetSocketId].username} был забанен модератором.`);
            } else if (action === 'kick') {
                io.to(targetSocketId).emit('kicked');
                io.to(streamId).emit('system-message', `${users[targetSocketId].username} был кикнут модератором.`);
            }
        }
    });

    // Отключение пользователя
    socket.on('disconnect', () => {
        console.log(`Пользователь отключен: ${socket.id}`);

        // Если пользователь был стримером, завершить его стрим
        for (const [streamId, stream] of Object.entries(streams)) {
            if (stream.host === socket.id) {
                delete streams[streamId];
                io.emit('update-streams', getActiveStreams());
                io.to(streamId).emit('stream-ended');
            }
        }

        delete users[socket.id];
        delete bannedUsers[socket.id];
    });
});

// Функция проверки бана пользователя
function isUserBanned(socketId) {
    const banExpiration = bannedUsers[socketId];
    if (banExpiration && Date.now() < banExpiration) {
        return true;
    } else {
        delete bannedUsers[socketId];
        return false;
    }
}

// Запуск сервера
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
    console.log(`Сервер запущен на порту ${PORT}`);
});
3. public/index.html

<!-- public/index.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" id="mainContainer">
        <!-- Главный экран -->
        <div id="welcomeScreen">
            <h1>Добро пожаловать на платформу трансляций</h1>
            <div class="auth-buttons">
                <a href="/auth/google" class="btn">Войти через Google</a>
                <a href="/auth/mailru" class="btn">Войти через Mail.ru</a>
                <a href="/logout" class="btn">Выйти</a>
            </div>
            <div class="buttons">
                <button id="startStreamerBtn" class="btn">Начать трансляцию</button>
                <button id="startViewerBtn" class="btn">Смотреть трансляции</button>
            </div>
        </div>

        <!-- Экран стримера -->
        <div id="streamerScreen" style="display: none;">
            <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="category-selection" style="margin-top: 15px;">
                <label for="category">Категория:</label>
                <select id="category">
                    <option value="gaming">Игры</option>
                    <option value="music">Музыка</option>
                    <option value="education">Образование</option>
                    <option value="other">Другое</option>
                </select>
            </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 id="progressContainer">
                    <progress id="videoProgress" value="0" max="100"></progress>
                    <span id="progressLabel">0%</span>
                </div>
                <!-- Чат -->
                <div class="chatContainer">
                    <div class="chatMessages"></div>
                    <form class="chatForm">
                        <input type="text" class="chatInput" placeholder="Введите сообщение...">
                        <button type="submit">Отправить</button>
                    </form>
                </div>
            </div>
        </div>

        <!-- Экран зрителя -->
        <div id="viewerScreen" style="display: none;">
            <h2>Активные трансляции</h2>
            <!-- Фильтр по категориям -->
            <div class="filter-section">
                <label for="filterCategory">Фильтр по категории:</label>
                <select id="filterCategory">
                    <option value="all">Все</option>
                    <option value="gaming">Игры</option>
                    <option value="music">Музыка</option>
                    <option value="education">Образование</option>
                    <option value="other">Другое</option>
                </select>
            </div>
            <ul id="streamsList">
                <!-- Список активных стримов будет динамически заполнен -->
            </ul>
        </div>

        <!-- Экран просмотра трансляции -->
        <div id="streamScreen" style="display: none;">
            <h2>Просмотр трансляции</h2>
            <div id="streamContainer">
                <video id="remoteVideo" autoplay playsinline controls></video>
                <!-- Чат -->
                <div class="chatContainer">
                    <div class="chatMessages"></div>
                    <form class="chatForm">
                        <input type="text" class="chatInput" placeholder="Введите сообщение...">
                        <button type="submit">Отправить</button>
                    </form>
                </div>
            </div>
        </div>
    </div>

    <script src="/socket.io/socket.io.js"></script>
    <script src="js/main.js"></script>
</body>
</html>
4. public/js/main.js

// public/js/main.js

const socket = io();

// DOM Elements
const mainContainer = document.getElementById('mainContainer');
const welcomeScreen = document.getElementById('welcomeScreen');
const streamerScreen = document.getElementById('streamerScreen');
const viewerScreen = document.getElementById('viewerScreen');
const streamScreen = document.getElementById('streamScreen');

const startStreamerBtn = document.getElementById('startStreamerBtn');
const startViewerBtn = document.getElementById('startViewerBtn');

// Стример элементы
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');
const videoProgress = document.getElementById('videoProgress');
const progressLabel = document.getElementById('progressLabel');
const categorySelect = document.getElementById('category');

// Чат элементы
const chatContainers = document.querySelectorAll('.chatContainer');
const chatMessagesElements = document.querySelectorAll('.chatMessages');
const chatForms = document.querySelectorAll('.chatForm');
const chatInputs = document.querySelectorAll('.chatInput');

// Зритель элементы
const streamsList = document.getElementById('streamsList');
const remoteVideo = document.getElementById('remoteVideo');
const filterCategory = document.getElementById('filterCategory');

// Навигация
startStreamerBtn.addEventListener('click', () => {
    // Проверка авторизации стримера
    // Если пользователь не авторизован, не позволяем начать стрим
    fetch('/api/user', { credentials: 'include' })
        .then(response => response.json())
        .then(data => {
            if (data.authenticated && data.role === 'streamer') {
                welcomeScreen.style.display = 'none';
                streamerScreen.style.display = 'block';
            } else {
                alert('Вам необходимо авторизоваться как стример для начала трансляции.');
            }
        })
        .catch(err => {
            console.error('Ошибка при проверке аутентификации:', err);
            alert('Произошла ошибка. Пожалуйста, попробуйте снова.');
        });
});

startViewerBtn.addEventListener('click', () => {
    welcomeScreen.style.display = 'none';
    viewerScreen.style.display = 'block';
    fetchStreams();
});

// Общие переменные
let streamId = null;
let isStreamer = false;

// Чат функциональность
let currentStreamId = null;

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

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

// Обработка выбора источника трансляции
if (sourceRadios) {
    Array.from(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';
                }
            }
        });
    });
}

// Обработка загрузки видео-файла
if (videoFileInput) {
    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.ontimeupdate = () => {
                if (uploadedVideo.duration) { // Проверка на существование duration
                    const percentage = (uploadedVideo.currentTime / uploadedVideo.duration) * 100;
                    videoProgress.value = percentage;
                    progressLabel.textContent = `${Math.floor(percentage)}%`;
                }
            };

            uploadedVideo.onended = () => {
                // Автоматически останавливаем трансляцию при окончании видео-файла
                endStream();
            };
        }
    });
}

// Экранирование вводимых сообщений для предотвращения XSS
function escapeHtml(text) {
    const map = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#039;',
    };
    return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}

// Чат отправка сообщений для каждой формы чата
chatForms.forEach((form, index) => {
    form.addEventListener('submit', (e) => {
        e.preventDefault();
        const chatInput = chatInputs[index];
        const messageText = chatInput.value.trim();
        if (messageText) {
            const message = {
                sender: 'Гость', // Стример или пользователь будут установлены на сервере
                text: messageText
            };
            socket.emit('chat-message', { streamId: currentStreamId, message });
            chatInput.value = '';
            // Не вызываем addChatMessage() здесь, поскольку сообщения приходят через сокеты
        }
    });
});

// Отображение сообщения в чате
function addChatMessage(chatMessages, message) {
    const messageElem = document.createElement('div');
    messageElem.className = 'chat-message';
    messageElem.innerHTML = `<strong>${escapeHtml(message.sender)}:</strong> ${escapeHtml(message.text)}`;
    chatMessages.appendChild(messageElem);
    chatMessages.scrollTop = chatMessages.scrollHeight;
}

// Прием сообщений чата
socket.on('chat-message', (message) => {
    chatMessagesElements.forEach(chatMessages => {
        addChatMessage(chatMessages, message);
    });
});

// Прием истории чата
socket.on('chat-history', (messages) => {
    chatMessagesElements.forEach(chatMessages => {
        chatMessages.innerHTML = '';
        messages.forEach(message => {
            addChatMessage(chatMessages, message);
        });
    });
});

// Стример функциональность

// Start Stream
if (startStreamBtn) {
    startStreamBtn.addEventListener('click', async () => {
        const category = categorySelect.value;
        streamId = await createStream({ category });
        if (streamId) {
            streamInfo.style.display = 'block';
            startStreamBtn.style.display = 'none';
            endStreamBtn.style.display = 'inline-block';
            streamLinkInput.value = `${window.location.origin}?streamId=${streamId}`;
            currentStreamId = streamId;
            isStreamer = true;

            if (currentSource === 'camera') {
                // Захват видео с веб-камеры
                startLocalVideo();
            } else if (currentSource === 'file') {
                // Воспроизведение видео-файла и захват потока
                if (uploadedVideo.src) {
                    if (videoStream) {
                        // Ничего не делаем, поток уже захвачен в обработчике onplaying
                    } else {
                        alert('Ожидание захвата видео-файла...');
                    }
                } else {
                    alert('Пожалуйста, выберите видео-файл для трансляции.');
                }
            }
        }
    });
}

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

// Create Stream on Server
function createStream({ category }) {
    return new Promise((resolve) => {
        socket.emit('create-stream', { category }, (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);
    } 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));
            // Инициализируем повторную сигнализацию
            renegotiate(peer.pc, viewerId);
        } else {
            console.warn('Нет видео-потока для добавления к Peer Connection:', viewerId);
        }
    }
}

// Повторная сигнализация
async function renegotiate(pc, viewerSocketId) {
    try {
        const offer = await pc.createOffer();
        await pc.setLocalDescription(offer);
        socket.emit('signal', { to: viewerSocketId, from: socket.id, description: pc.localDescription });
    } 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, candidate, description } = data;
    if (isStreamer) {
        if (!peers[from]) {
            createPeerConnection(from, false);
        }
        const peer = peers[from];

        if (description) {
            if (description.type === 'answer') {
                await peer.pc.setRemoteDescription(new RTCSessionDescription(description));
            }
        } else if (candidate) {
            try {
                await peer.pc.addIceCandidate(new RTCIceCandidate(candidate));
            } catch (err) {
                console.error('Ошибка добавления ICE-кандидата:', err);
            }
        }
    } else {
        // Для зрителя
        if (!peerConnection) {
            createViewerPeerConnection(from);
        }

        if (description) {
            if (description.type === 'offer') {
                await peerConnection.setRemoteDescription(new RTCSessionDescription(description));
                const answer = await peerConnection.createAnswer();
                await peerConnection.setLocalDescription(answer);
                socket.emit('signal', { to: from, from: socket.id, description: peerConnection.localDescription });
            }
        } else if (candidate) {
            try {
                await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
            } 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-серверы здесь при необходимости
        ]
    });

    peers[viewerSocketId] = peer;

    // Добавление треков из 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, candidate: 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) {
        createOffer(peer.pc, viewerSocketId);
    }
}

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

// Закрытие всех 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 = '';
    videoProgress.value = 0;
    progressLabel.textContent = '0%';
    chatMessagesElements.forEach(chatMessages => {
        chatMessages.innerHTML = '';
    });

    isStreamer = false;
    currentStreamId = null;
}

// Зритель функциональность

function fetchStreams() {
    fetch(`/streams`)
        .then(response => response.json())
        .then(data => {
            renderStreams(data);
        })
        .catch(err => {
            console.error('Ошибка при получении списка трансляций:', err);
        });
}

// Render streams list with category filter
function renderStreams(streams) {
    streamsList.innerHTML = '';
    const selectedCategory = filterCategory.value;
    const filteredStreams = selectedCategory === 'all' ? streams : streams.filter(stream => stream.category === selectedCategory);

    if (filteredStreams.length === 0) {
        streamsList.innerHTML = '<p>Нет активных трансляций в данный момент.</p>';
        return;
    }
    filteredStreams.forEach(stream => {
        const listItem = document.createElement('li');
        listItem.className = 'stream-item';

        const button = document.createElement('button');
        button.textContent = `Трансляция #${stream.id.substring(0, 8)} | Категория: ${stream.category}`;
        button.addEventListener('click', () => {
            joinStream(stream.id);
        });

        listItem.appendChild(button);
        streamsList.appendChild(listItem);
    });
}

// Обновление списка трансляций в реальном времени
socket.on('update-streams', (activeStreams) => {
    if (viewerScreen.style.display === 'block') {
        renderStreams(activeStreams);
    }
});

// Обработка фильтрации по категориям
filterCategory.addEventListener('change', () => {
    fetchStreams();
});

// Присоединение к трансляции
function joinStream(streamId) {
    currentStreamId = streamId;
    isStreamer = false;

    viewerScreen.style.display = 'none';
    streamScreen.style.display = 'block';

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

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

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

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

// Обработка ссылок на прямой переход к трансляции
window.addEventListener('load', () => {
    const urlParams = new URLSearchParams(window.location.search);
    const streamIdParam = urlParams.get('streamId');
    if (streamIdParam) {
        // Автоматически присоединяемся к трансляции
        welcomeScreen.style.display = 'none';
        joinStream(streamIdParam);
    }
});

// Уведомления

// Запрос разрешения на уведомления
if (Notification.permission !== 'granted') {
    Notification.requestPermission();
}

// Обработка новых стримов для уведомлений
socket.on('new-stream', (stream) => {
    // Если пользователь не смотрит стримы и не является стримером, отправляем уведомление
    fetch('/api/user', { credentials: 'include' })
        .then(response => response.json())
        .then(data => {
            if (!data.authenticated || data.role !== 'streamer') {
                new Notification('Новая трансляция!', {
                    body: `Категория: ${stream.category}`,
                    icon: '/path/to/icon.png' // Укажите путь к иконке уведомления
                });
                // Обновляем список стримов
                fetchStreams();
            }
        })
        .catch(err => {
            console.error('Ошибка при проверке аутентификации для уведомления:', err);
        });
});

// Функция для отправки системных сообщений в чат
socket.on('system-message', (message) => {
    chatMessagesElements.forEach(chatMessages => {
        const messageElem = document.createElement('div');
        messageElem.className = 'system-message';
        messageElem.innerHTML = `<em>${escapeHtml(message)}</em>`;
        chatMessages.appendChild(messageElem);
        chatMessages.scrollTop = chatMessages.scrollHeight;
    });
});

// Обработка бана
socket.on('banned', () => {
    alert('Вы были забанены.');
    window.location.reload();
});

// Обработка кика
socket.on('kicked', () => {
    alert('Вы были кикнуты из трансляции.');
    window.location.reload();
});
5. server.js (Дополнительно: API для проверки пользователя)
Добавьте в server.js следующие маршруты для проверки текущего пользователя (например, для клиентской части):


// API для получения текущего пользователя
app.get('/api/user', (req, res) => {
    if (req.isAuthenticated()) {
        res.json({
            authenticated: true,
            username: req.user.username,
            role: req.user.role || 'streamer' // Роль по умолчанию 'streamer'
        });
    } else {
        res.json({
            authenticated: false
        });
    }
});
6. public/css/styles.css

/* public/css/styles.css */

body {
    font-family: Arial, sans-serif;
    background-color: #f0f2f5;
    margin: 0;
    padding: 0;
}

.container {
    max-width: 1200px;
    margin: auto;
    padding: 20px;
}

h1, h2 {
    text-align: center;
}

.buttons, .auth-buttons {
    display: flex;
    justify-content: center;
    gap: 20px;
    margin-top: 20px;
}

.btn {
    padding: 10px 20px;
    font-size: 16px;
    cursor: pointer;
    text-decoration: none;
    color: white;
    background-color: #4CAF50;
    border: none;
    border-radius: 5px;
}

.btn:hover {
    background-color: #45a049;
}

.source-selection, .category-selection, .filter-section {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 10px;
}

.chatContainer {
    margin-top: 20px;
    border: 1px solid #ccc;
    background-color: #fff;
    width: 100%;
    max-width: 500px;
    margin: 20px auto 0;
    padding: 10px;
    border-radius: 5px;
}

.chatMessages {
    height: 200px;
    overflow-y: scroll;
    border-bottom: 1px solid #ccc;
    padding-bottom: 10px;
}

.chat-message {
    margin-bottom: 10px;
}

.system-message {
    text-align: center;
    color: #888;
    font-style: italic;
}

.chatForm {
    display: flex;
    gap: 10px;
    margin-top: 10px;
}

.chatInput {
    flex: 1;
    padding: 5px;
}

#progressContainer {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 10px;
}

#streamLink {
    width: 100%;
    padding: 5px;
}

.stream-item {
    margin-bottom: 10px;
    text-align: center;
}

.stream-item button {
    padding: 10px;
    width: 100%;
    cursor: pointer;
    border: none;
    background-color: #4CAF50;
    color: white;
    font-size: 16px;
    border-radius: 5px;
}

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

video {
    width: 100%;
    max-width: 600px;
    display: block;
    margin: 10px auto;
    border: 1px solid #ccc;
    border-radius: 5px;
}

.filter-section {
    margin-bottom: 20px;
}

.auth-buttons a.btn {
    background-color: #4285F4; /* Google Blue */
}

.auth-buttons a.btn.mailru {
    background-color: #FF0000; /* Mail.ru Red */
}

@media (max-width: 768px) {
    .chatContainer, .stream-item button, .filter-section {
        width: 90%;
    }
}
7. public/js/main.js (Дополнительно: Обработка аутентификации на клиенте)
В предыдущем main.js добавлены запросы к /api/user для проверки аутентификации и разрешения доступа к стримингу. Это уже учтено в представленном коде выше.

8. Дополнительно: Настройка ролей пользователей
Для обеспечения того, чтобы только авторизованные пользователи могли быть стримерами, можно добавить поле role в таблицу пользователей и установить его при аутентификации. В данном примере роль по умолчанию для аутентифицированного пользователя – streamer.

Если требуется более сложная система ролей, можно расширить функционал:

Добавить административные права.
Разделить роли на viewer, streamer, admin и т.д.
9. Пример Запросов из Клиентской Части
В main.js при нажатии кнопки Начать трансляцию выполняется проверка аутентификации пользователя. Если пользователь аутентифицирован и имеет роль streamer, ему разрешается начинать трансляцию. В противном случае показывается сообщение об ошибке.

10. Запуск Приложения
Установка Зависимостей Убедитесь, что все зависимости установлены:

npm install
Настройка OAuth
Google:
Перейдите в Google Cloud Console.
Создайте новый проект или используйте существующий.
Перейдите в раздел APIs & Services > Credentials.
Создайте OAuth 2.0 Client ID, указав URI перенаправления, например, http://localhost:3000/auth/google/callback.
Скопируйте Client ID и Client Secret и вставьте их в server.js.
Mail.ru:
Перейдите в Mail.ru для разработчиков.
Зарегистрируйте приложение.
Получите Client ID и Client Secret.
Настройте URI перенаправления, например, http://localhost:3000/auth/mailru/callback.
Вставьте их в server.js.
Запуск Сервера Запустите сервер с помощью команды:

npm start
Доступ к Приложению Откройте браузер и перейдите по адресу http://localhost:3000.
11. Заключение
В данной реализации добавлена аутентификация пользователей через Google и Mail.ru, а также интеграция с SQLite для хранения данных пользователей. Теперь стримеры обязаны быть авторизованными, тогда как зрители могут просматривать трансляции без аутентификации. Кроме того, реализованы улучшения в чате, уведомления о новых стримах и защита от XSS атак.

Дополнительные Рекомендации:
Безопасность:
Обязательно используйте HTTPS в продакшене.
Храните секретные ключи и другие чувствительные данные в переменных окружения (.env файл).
Модерация:
Реализуйте более продвинутые механизмы модерации, например, автофильтрацию нежелательных слов.
Масштабируемость:
Рассмотрите возможность использования TURN-серверов для улучшения качества соединений.
Дополнительные Функции:
Добавьте возможность записи стримов.
Реализуйте систему донатов и подписок для стримеров.
Удачи в разработке вашего стримингового сервиса!


ошибка D:\live-streaming-platform>node server.js
node:internal/modules/cjs/loader:998
throw err;
^

Error: Cannot find module 'passport-google-oauth20'
Require stack:

D:\live-streaming-platform\server.js
at Module._resolveFilename (node:internal/modules/cjs/loader:995:15)
at Module._load (node:internal/modules/cjs/loader:841:27)
at Module.require (node:internal/modules/cjs/loader:1061:19)
at require (node:internal/modules/cjs/helpers:103:18)
at Object. (D:\live-streaming-platform\server.js:8:24)
at Module._compile (node:internal/modules/cjs/loader:1159:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1213:10)
at Module.load (node:internal/modules/cjs/loader:1037:32)
at Module._load (node:internal/modules/cjs/loader:878:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) {
code: 'MODULE_NOT_FOUND',
requireStack: [ 'D:\live-streaming-platform\server.js' ]
}
Node.js v18.12.1

D:\live-streaming-platform>


Извините за возникшие неудобства. Ошибка Cannot find module 'passport-google-oauth20' указывает на то, что модуль passport-google-oauth20 не установлен в вашем проекте. Давайте пошагово решим эту проблему и полностью интегрируем аутентификацию через Google и Mail.ru с использованием SQLite для вашего стримингового сервиса.

Шаг 1: Структура Проекта
Убедитесь, что ваш проект имеет следующую структуру:


streaming-service/
├── server.js
├── package.json
├── package-lock.json
└── public/
    ├── index.html
    ├── css/
    │   └── styles.css
    └── js/
        └── main.js
Шаг 2: Создание package.json и Установка Зависимостей
1. Создайте package.json

Если у вас еще нет файла package.json, создайте его с помощью команды:


npm init -y
2. Установите Необходимые Зависимости

Выполните следующую команду для установки всех необходимых пакетов:


npm install express express-session passport passport-google-oauth20 passport-mailru socket.io sqlite3 connect-sqlite3 sanitize-html uuid dotenv
3. Обновите package.json

Убедитесь, что ваш package.json содержит все необходимые зависимости. Пример:


{
  "name": "streaming-service",
  "version": "1.0.0",
  "description": "Платформа для стриминга с авторизацией",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "connect-sqlite3": "^0.9.0",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "express-session": "^1.17.3",
    "passport": "^0.6.0",
    "passport-google-oauth20": "^2.0.0",
    "passport-mailru": "^0.0.3",
    "sanitize-html": "^2.7.3",
    "socket.io": "^4.5.4",
    "sqlite3": "^5.1.2",
    "uuid": "^9.0.0"
  }
}
Шаг 3: Настройка OAuth Провайдеров
1. Google OAuth
Перейдите в Google Cloud Console.
Создайте новый проект или выберите существующий.
Перейдите в раздел APIs & Services > Credentials.
Нажмите Create Credentials > OAuth client ID.
Выберите Web application.
Укажите Authorized redirect URIs, например, http://localhost:3000/auth/google/callback.
Сохраните и скопируйте Client ID и Client Secret.
2. Mail.ru OAuth
Перейдите в Mail.ru для разработчиков.
Зарегистрируйте новое приложение.
Укажите Redirect URI, например, http://localhost:3000/auth/mailru/callback.
Сохраните и скопируйте Client ID и Client Secret.
3. Создание .env Файла
Создайте файл .env в корневой директории вашего проекта и добавьте в него ваши OAuth креденшелы и секреты:


GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
MAILRU_CLIENT_ID=your_mailru_client_id
MAILRU_CLIENT_SECRET=your_mailru_client_secret
SESSION_SECRET=your_session_secret
Важно: Никогда не публикуйте файл .env и не делитесь своими секретными ключами.

Шаг 4: Полный Код Проекта
Ниже приведен полный код всех необходимых файлов. Каждый файл разделен комментариями для удобства.


// package.json
{
  "name": "streaming-service",
  "version": "1.0.0",
  "description": "Платформа для стриминга с авторизацией",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "connect-sqlite3": "^0.9.0",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "express-session": "^1.17.3",
    "passport": "^0.6.0",
    "passport-google-oauth20": "^2.0.0",
    "passport-mailru": "^0.0.3",
    "sanitize-html": "^2.7.3",
    "socket.io": "^4.5.4",
    "sqlite3": "^5.1.2",
    "uuid": "^9.0.0"
  }
}

// server.js
require('dotenv').config();
const express = require('express');
const http = require('http');
const path = require('path');
const { Server } = require('socket.io');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const MailruStrategy = require('passport-mailru').Strategy;
const session = require('express-session');
const SQLiteStore = require('connect-sqlite3')(session);
const sqlite3 = require('sqlite3').verbose();
const sanitizeHtml = require('sanitize-html');
const { v4: uuidv4 } = require('uuid');
const sharedSession = require('express-socket.io-session');

// Инициализация приложения
const app = express();
const server = http.createServer(app);
const io = new Server(server);

// Настройка базы данных SQLite
const db = new sqlite3.Database('./database.sqlite', (err) => {
    if (err) {
        console.error('Не удалось подключиться к базе данных:', err);
    } else {
        console.log('Подключено к базе данных SQLite.');
    }
});

// Создание таблицы пользователей, если она не существует
db.run(`
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        oauth_provider TEXT NOT NULL,
        oauth_id TEXT NOT NULL,
        username TEXT NOT NULL,
        email TEXT,
        avatar TEXT,
        role TEXT NOT NULL DEFAULT 'streamer'
    )
`);

// Медиафайлы (опционально)
// db.run(`CREATE TABLE IF NOT EXISTS streams (
//     id TEXT PRIMARY KEY,
//     host TEXT NOT NULL,
//     category TEXT NOT NULL,
//     created_at DATETIME DEFAULT CURRENT_TIMESTAMP
// )`);

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Настройка сессий
const sessionMiddleware = session({
    store: new SQLiteStore,
    secret: process.env.SESSION_SECRET || 'your_session_secret', // Замените на ваш секретный ключ
    resave: false,
    saveUninitialized: false
});
app.use(sessionMiddleware);

// Инициализация Passport
app.use(passport.initialize());
app.use(passport.session());

// Настройка стратегий Passport

// Google OAuth Strategy
passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID, // Ваш Google Client ID
    clientSecret: process.env.GOOGLE_CLIENT_SECRET, // Ваш Google Client Secret
    callbackURL: '/auth/google/callback'
},
    (accessToken, refreshToken, profile, done) => {
        // Поиск или создание пользователя в базе данных
        db.get('SELECT * FROM users WHERE oauth_provider = ? AND oauth_id = ?', [profile.provider, profile.id], (err, row) => {
            if (err) return done(err);
            if (row) {
                return done(null, row);
            } else {
                // Создание нового пользователя
                db.run('INSERT INTO users (oauth_provider, oauth_id, username, email, avatar) VALUES (?, ?, ?, ?, ?)', [
                    profile.provider,
                    profile.id,
                    profile.displayName,
                    profile.emails ? profile.emails[0].value : null,
                    profile.photos ? profile.photos[0].value : null
                ], function (err) {
                    if (err) return done(err);
                    db.get('SELECT * FROM users WHERE id = ?', [this.lastID], (err, row) => {
                        if (err) return done(err);
                        return done(null, row);
                    });
                });
            }
        });
    }
));

// Mail.ru OAuth Strategy
passport.use(new MailruStrategy({
    clientID: process.env.MAILRU_CLIENT_ID, // Ваш Mail.ru Client ID
    clientSecret: process.env.MAILRU_CLIENT_SECRET, // Ваш Mail.ru Client Secret
    callbackURL: '/auth/mailru/callback'
},
    (accessToken, refreshToken, profile, done) => {
        // Поиск или создание пользователя в базе данных
        db.get('SELECT * FROM users WHERE oauth_provider = ? AND oauth_id = ?', [profile.provider, profile.id], (err, row) => {
            if (err) return done(err);
            if (row) {
                return done(null, row);
            } else {
                // Создание нового пользователя
                db.run('INSERT INTO users (oauth_provider, oauth_id, username, email, avatar) VALUES (?, ?, ?, ?, ?)', [
                    profile.provider,
                    profile.id,
                    profile.displayName,
                    profile.emails ? profile.emails[0].value : null,
                    profile.photos ? profile.photos[0].value : null
                ], function (err) {
                    if (err) return done(err);
                    db.get('SELECT * FROM users WHERE id = ?', [this.lastID], (err, row) => {
                        if (err) return done(err);
                        return done(null, row);
                    });
                });
            }
        });
    }
));

// Сериализация и десериализация пользователей
passport.serializeUser((user, done) => {
    done(null, user.id);
});

passport.deserializeUser((id, done) => {
    db.get('SELECT * FROM users WHERE id = ?', [id], (err, row) => {
        if (err) return done(err);
        done(null, row);
    });
});

// Маршруты аутентификации

// Маршрут для начала аутентификации через Google
app.get('/auth/google',
    passport.authenticate('google', { scope: ['profile', 'email'] })
);

// Маршрут для обработки колбэка после аутентификации через Google
app.get('/auth/google/callback',
    passport.authenticate('google', { failureRedirect: '/' }),
    (req, res) => {
        // Успешная аутентификация, перенаправление на главную страницу
        res.redirect('/');
    }
);

// Маршрут для начала аутентификации через Mail.ru
app.get('/auth/mailru',
    passport.authenticate('mailru', { scope: ['profile', 'email'] })
);

// Маршрут для обработки колбэка после аутентификации через Mail.ru
app.get('/auth/mailru/callback',
    passport.authenticate('mailru', { failureRedirect: '/' }),
    (req, res) => {
        // Успешная аутентификация, перенаправление на главную страницу
        res.redirect('/');
    }
);

// Маршрут для выхода
app.get('/logout', (req, res) => {
    req.logout((err) => {
        if (err) {
            console.error('Ошибка при выходе:', err);
        }
        res.redirect('/');
    });
});

// API для получения текущего пользователя
app.get('/api/user', (req, res) => {
    if (req.isAuthenticated()) {
        res.json({
            authenticated: true,
            username: req.user.username,
            role: req.user.role || 'streamer' // Роль по умолчанию 'streamer'
        });
    } else {
        res.json({
            authenticated: false
        });
    }
});

// Раздача статических файлов
app.use(express.static(path.join(__dirname, 'public')));

// Настройка Socket.io с использованием сессий
io.use(sharedSession(sessionMiddleware, {
    autoSave: true
}));

// Хранение активных стримов и пользователей
const streams = {}; // { streamId: { host: socketId, category } }
const users = {}; // { socketId: { username, role } }
const bannedUsers = {}; // { socketId: banExpirationTime }

// Получение списка активных стримов
function getActiveStreams() {
    return Object.keys(streams).map(id => ({
        id,
        category: streams[id].category
    }));
}

function generateUniqueId() {
    return uuidv4();
}

io.on('connection', (socket) => {
    console.log(`Пользователь подключен: ${socket.id}`);

    // Получение информации о пользователе из сессии
    const session = socket.handshake.session;
    if (session.passport && session.passport.user) {
        db.get('SELECT * FROM users WHERE id = ?', [session.passport.user], (err, row) => {
            if (err) {
                console.error('Ошибка при получении пользователя из базы данных:', err);
                return;
            }
            if (row) {
                users[socket.id] = { username: row.username, role: row.role || 'streamer' };
            }
        });
    }

    // Создание стрима (только для аутентифицированных пользователей)
    socket.on('create-stream', (data, callback) => {
        if (!users[socket.id]) {
            return callback(null);
        }
        const { category } = data;
        const streamId = generateUniqueId();
        streams[streamId] = { host: socket.id, category };
        socket.join(streamId);
        callback(streamId);
        io.emit('update-streams', getActiveStreams());

        // Уведомление о начале стрима
        io.emit('new-stream', { id: streamId, category });
    });

    // Завершение стрима
    socket.on('end-stream', (streamId) => {
        if (streams[streamId] && streams[streamId].host === socket.id) {
            delete streams[streamId];
            io.emit('update-streams', getActiveStreams());
            io.to(streamId).emit('stream-ended');
        }
    });

    // Присоединение к стриму
    socket.on('join-stream', (streamId, callback) => {
        if (streams[streamId]) {
            socket.join(streamId);
            callback({ success: true });
            io.to(streamId).emit('viewer-joined', socket.id);
        } else {
            callback({ success: false, message: 'Стрим не найден или уже завершен.' });
        }
    });

    // Обработка сигналов WebRTC
    socket.on('signal', (data) => {
        const { to, from, description, candidate } = data;
        io.to(to).emit('signal', { from, description, candidate });
    });

    // Обработка сообщений чата с защитой от XSS
    socket.on('chat-message', ({ streamId, message }) => {
        if (isUserBanned(socket.id)) {
            return;
        }

        // Разрешение отправлять сообщения только для существующих стримов
        if (!streams[streamId]) {
            return;
        }

        // Получение имени пользователя
        const user = users[socket.id];
        const sender = user ? user.username : 'Гость';

        // Экранирование HTML для предотвращения XSS
        const sanitizedMessage = {
            sender: sanitizeHtml(sender),
            text: sanitizeHtml(message.text)
        };
        io.to(streamId).emit('chat-message', sanitizedMessage);
    });

    // Модерация чата: удаление сообщений и бан пользователей
    socket.on('moderate', ({ action, targetSocketId, streamId }) => {
        if (users[socket.id] && users[socket.id].role === 'admin') {
            if (action === 'ban') {
                // Бан пользователя на 1 час
                bannedUsers[targetSocketId] = Date.now() + 60 * 60 * 1000;
                io.to(targetSocketId).emit('banned');
                io.to(streamId).emit('system-message', `${users[targetSocketId].username} был забанен модератором.`);
            } else if (action === 'kick') {
                io.to(targetSocketId).emit('kicked');
                io.to(streamId).emit('system-message', `${users[targetSocketId].username} был кикнут модератором.`);
            }
        }
    });

    // Отключение пользователя
    socket.on('disconnect', () => {
        console.log(`Пользователь отключен: ${socket.id}`);

        // Если пользователь был стримером, завершить его стрим
        for (const [streamId, stream] of Object.entries(streams)) {
            if (stream.host === socket.id) {
                delete streams[streamId];
                io.emit('update-streams', getActiveStreams());
                io.to(streamId).emit('stream-ended');
            }
        }

        delete users[socket.id];
        delete bannedUsers[socket.id];
    });
});

// Функция проверки бана пользователя
function isUserBanned(socketId) {
    const banExpiration = bannedUsers[socketId];
    if (banExpiration && Date.now() < banExpiration) {
        return true;
    } else {
        delete bannedUsers[socketId];
        return false;
    }
}

// Запуск сервера
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
    console.log(`Сервер запущен на порту ${PORT}`);
});

<!-- public/index.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" id="mainContainer">
        <!-- Главный экран -->
        <div id="welcomeScreen">
            <h1>Добро пожаловать на платформу трансляций</h1>
            <div class="auth-buttons">
                <!-- Если пользователь аутентифицирован, покажите кнопку выхода -->
                <!-- В противном случае покажите кнопки входа -->
                <div id="authButtons">
                    <!-- Кнопки будут управляемы через JavaScript -->
                </div>
            </div>
            <div class="buttons">
                <button id="startStreamerBtn" class="btn">Начать трансляцию</button>
                <button id="startViewerBtn" class="btn">Смотреть трансляции</button>
            </div>
        </div>

        <!-- Экран стримера -->
        <div id="streamerScreen" style="display: none;">
            <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="category-selection" style="margin-top: 15px;">
                <label for="category">Категория:</label>
                <select id="category">
                    <option value="gaming">Игры</option>
                    <option value="music">Музыка</option>
                    <option value="education">Образование</option>
                    <option value="other">Другое</option>
                </select>
            </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 id="progressContainer">
                    <progress id="videoProgress" value="0" max="100"></progress>
                    <span id="progressLabel">0%</span>
                </div>
                <!-- Чат -->
                <div class="chatContainer">
                    <div class="chatMessages"></div>
                    <form class="chatForm">
                        <input type="text" class="chatInput" placeholder="Введите сообщение...">
                        <button type="submit">Отправить</button>
                    </form>
                </div>
            </div>
        </div>

        <!-- Экран зрителя -->
        <div id="viewerScreen" style="display: none;">
            <h2>Активные трансляции</h2>
            <!-- Фильтр по категориям -->
            <div class="filter-section">
                <label for="filterCategory">Фильтр по категории:</label>
                <select id="filterCategory">
                    <option value="all">Все</option>
                    <option value="gaming">Игры</option>
                    <option value="music">Музыка</option>
                    <option value="education">Образование</option>
                    <option value="other">Другое</option>
                </select>
            </div>
            <ul id="streamsList">
                <!-- Список активных стримов будет динамически заполнен -->
            </ul>
        </div>

        <!-- Экран просмотра трансляции -->
        <div id="streamScreen" style="display: none;">
            <h2>Просмотр трансляции</h2>
            <div id="streamContainer">
                <video id="remoteVideo" autoplay playsinline controls></video>
                <!-- Чат -->
                <div class="chatContainer">
                    <div class="chatMessages"></div>
                    <form class="chatForm">
                        <input type="text" class="chatInput" placeholder="Введите сообщение...">
                        <button type="submit">Отправить</button>
                    </form>
                </div>
            </div>
        </div>
    </div>

    <script src="/socket.io/socket.io.js"></script>
    <script src="js/main.js"></script>
</body>
</html>

// public/js/main.js
const socket = io();

// DOM Elements
const mainContainer = document.getElementById('mainContainer');
const welcomeScreen = document.getElementById('welcomeScreen');
const streamerScreen = document.getElementById('streamerScreen');
const viewerScreen = document.getElementById('viewerScreen');
const streamScreen = document.getElementById('streamScreen');

const startStreamerBtn = document.getElementById('startStreamerBtn');
const startViewerBtn = document.getElementById('startViewerBtn');

// Стример элементы
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');
const videoProgress = document.getElementById('videoProgress');
const progressLabel = document.getElementById('progressLabel');
const categorySelect = document.getElementById('category');

// Чат элементы
const chatContainers = document.querySelectorAll('.chatContainer');
const chatMessagesElements = document.querySelectorAll('.chatMessages');
const chatForms = document.querySelectorAll('.chatForm');
const chatInputs = document.querySelectorAll('.chatInput');

// Зритель элементы
const streamsList = document.getElementById('streamsList');
const remoteVideo = document.getElementById('remoteVideo');
const filterCategory = document.getElementById('filterCategory');

// Аутентификационные элементы
const authButtons = document.getElementById('authButtons');

// Навигация
startStreamerBtn.addEventListener('click', () => {
    // Проверка авторизации стримера
    fetch('/api/user', { credentials: 'include' })
        .then(response => response.json())
        .then(data => {
            if (data.authenticated && data.role === 'streamer') {
                welcomeScreen.style.display = 'none';
                streamerScreen.style.display = 'block';
            } else {
                alert('Вам необходимо авторизоваться как стример для начала трансляции.');
            }
        })
        .catch(err => {
            console.error('Ошибка при проверке аутентификации:', err);
            alert('Произошла ошибка. Пожалуйста, попробуйте снова.');
        });
});

startViewerBtn.addEventListener('click', () => {
    welcomeScreen.style.display = 'none';
    viewerScreen.style.display = 'block';
    fetchStreams();
});

// Получение информации о текущем пользователе и отображение кнопок аутентификации
function loadAuthButtons() {
    fetch('/api/user', { credentials: 'include' })
        .then(response => response.json())
        .then(data => {
            authButtons.innerHTML = '';
            if (data.authenticated) {
                authButtons.innerHTML = `
                    <span>Привет, ${data.username}!</span>
                    <a href="/logout" class="btn">Выйти</a>
                `;
            } else {
                authButtons.innerHTML = `
                    <a href="/auth/google" class="btn">Войти через Google</a>
                    <a href="/auth/mailru" class="btn">Войти через Mail.ru</a>
                `;
            }
        })
        .catch(err => {
            console.error('Ошибка при загрузке аутентификационных кнопок:', err);
        });
}

// Вызов функции при загрузке страницы
window.onload = loadAuthButtons;

// Общие переменные
let streamId = null;
let isStreamer = false;

// Чат функциональность

let currentStreamId = null;

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

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

// Обработка выбора источника трансляции
if (sourceRadios) {
    Array.from(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';
                }
            }
        });
    });
}

// Обработка загрузки видео-файла
if (videoFileInput) {
    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.ontimeupdate = () => {
                if (uploadedVideo.duration) { // Проверка на существование duration
                    const percentage = (uploadedVideo.currentTime / uploadedVideo.duration) * 100;
                    videoProgress.value = percentage;
                    progressLabel.textContent = `${Math.floor(percentage)}%`;
                }
            };

            uploadedVideo.onended = () => {
                // Автоматически останавливаем трансляцию при окончании видео-файла
                endStream();
            };
        }
    });
}

// Экранирование вводимых сообщений для предотвращения XSS
function escapeHtml(text) {
    const map = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#039;',
    };
    return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}

// Чат отправка сообщений для каждой формы чата
chatForms.forEach((form, index) => {
    form.addEventListener('submit', (e) => {
        e.preventDefault();
        const chatInput = chatInputs[index];
        const messageText = chatInput.value.trim();
        if (messageText) {
            const message = {
                sender: 'Гость', // Стример или пользователь будут установлены на сервере
                text: messageText
            };
            socket.emit('chat-message', { streamId: currentStreamId, message });
            chatInput.value = '';
            // Не вызываем addChatMessage() здесь, поскольку сообщения приходят через сокеты
        }
    });
});

// Отображение сообщения в чате
function addChatMessage(chatMessages, message) {
    const messageElem = document.createElement('div');
    messageElem.className = 'chat-message';
    messageElem.innerHTML = `<strong>${escapeHtml(message.sender)}:</strong> ${escapeHtml(message.text)}`;
    chatMessages.appendChild(messageElem);
    chatMessages.scrollTop = chatMessages.scrollHeight;
}

// Прием сообщений чата
socket.on('chat-message', (message) => {
    chatMessagesElements.forEach(chatMessages => {
        addChatMessage(chatMessages, message);
    });
});

// Прием истории чата (опционально, зависит от серверной реализации)
socket.on('chat-history', (messages) => {
    chatMessagesElements.forEach(chatMessages => {
        chatMessages.innerHTML = '';
        messages.forEach(message => {
            addChatMessage(chatMessages, message);
        });
    });
});

// Стример функциональность

// Start Stream
if (startStreamBtn) {
    startStreamBtn.addEventListener('click', async () => {
        const category = categorySelect.value;
        streamId = await createStream({ category });
        if (streamId) {
            streamInfo.style.display = 'block';
            startStreamBtn.style.display = 'none';
            endStreamBtn.style.display = 'inline-block';
            streamLinkInput.value = `${window.location.origin}?streamId=${streamId}`;
            currentStreamId = streamId;
            isStreamer = true;

            if (currentSource === 'camera') {
                // Захват видео с веб-камеры
                startLocalVideo();
            } else if (currentSource === 'file') {
                // Воспроизведение видео-файла и захват потока
                if (uploadedVideo.src) {
                    if (videoStream) {
                        // Ничего не делаем, поток уже захвачен в обработчике onplaying
                    } else {
                        alert('Ожидание захвата видео-файла...');
                    }
                } else {
                    alert('Пожалуйста, выберите видео-файл для трансляции.');
                }
            }
        }
    });
}

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

// Create Stream on Server
function createStream({ category }) {
    return new Promise((resolve) => {
        socket.emit('create-stream', { category }, (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);
    } 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));
            // Инициализируем повторную сигнализацию
            renegotiate(peer.pc, viewerId);
        } else {
            console.warn('Нет видео-потока для добавления к Peer Connection:', viewerId);
        }
    }
}

// Повторная сигнализация
async function renegotiate(pc, viewerSocketId) {
    try {
        const offer = await pc.createOffer();
        await pc.setLocalDescription(offer);
        socket.emit('signal', { to: viewerSocketId, from: socket.id, description: pc.localDescription });
    } 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, candidate, description } = data;
    if (isStreamer) {
        if (!peers[from]) {
            createPeerConnection(from, false);
        }
        const peer = peers[from];

        if (description) {
            if (description.type === 'answer') {
                await peer.pc.setRemoteDescription(new RTCSessionDescription(description));
            }
        } else if (candidate) {
            try {
                await peer.pc.addIceCandidate(new RTCIceCandidate(candidate));
            } catch (err) {
                console.error('Ошибка добавления ICE-кандидата:', err);
            }
        }
    } else {
        // Для зрителя
        if (!peerConnection) {
            createViewerPeerConnection(from);
        }

        if (description) {
            if (description.type === 'offer') {
                await peerConnection.setRemoteDescription(new RTCSessionDescription(description));
                const answer = await peerConnection.createAnswer();
                await peerConnection.setLocalDescription(answer);
                socket.emit('signal', { to: from, from: socket.id, description: peerConnection.localDescription });
            }
        } else if (candidate) {
            try {
                await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
            } 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-серверы здесь при необходимости
        ]
    });

    peers[viewerSocketId] = peer;

    // Добавление треков из 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, candidate: 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) {
        createOffer(peer.pc, viewerSocketId);
    }
}

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

// Закрытие всех 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 = '';
    videoProgress.value = 0;
    progressLabel.textContent = '0%';
    chatMessagesElements.forEach(chatMessages => {
        chatMessages.innerHTML = '';
    });

    isStreamer = false;
    currentStreamId = null;
}

// Зритель функциональность

function fetchStreams() {
    fetch(`/streams`)
        .then(response => response.json())
        .then(data => {
            renderStreams(data);
        })
        .catch(err => {
            console.error('Ошибка при получении списка трансляций:', err);
        });
}

// Render streams list with category filter
function renderStreams(streams) {
    streamsList.innerHTML = '';
    const selectedCategory = filterCategory.value;
    const filteredStreams = selectedCategory === 'all' ? streams : streams.filter(stream => stream.category === selectedCategory);

    if (filteredStreams.length === 0) {
        streamsList.innerHTML = '<p>Нет активных трансляций в данный момент.</p>';
        return;
    }
    filteredStreams.forEach(stream => {
        const listItem = document.createElement('li');
        listItem.className = 'stream-item';

        const button = document.createElement('button');
        button.textContent = `Трансляция #${stream.id.substring(0, 8)} | Категория: ${stream.category}`;
        button.addEventListener('click', () => {
            joinStream(stream.id);
        });

        listItem.appendChild(button);
        streamsList.appendChild(listItem);
    });
}

// Обновление списка трансляций в реальном времени
socket.on('update-streams', (activeStreams) => {
    if (viewerScreen.style.display === 'block') {
        renderStreams(activeStreams);
    }
});

// Обработка фильтрации по категориям
filterCategory.addEventListener('change', () => {
    fetchStreams();
});

// Присоединение к трансляции
function joinStream(streamId) {
    currentStreamId = streamId;
    isStreamer = false;

    viewerScreen.style.display = 'none';
    streamScreen.style.display = 'block';

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

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

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

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

// Обработка ссылок на прямой переход к трансляции
window.addEventListener('load', () => {
    const urlParams = new URLSearchParams(window.location.search);
    const streamIdParam = urlParams.get('streamId');
    if (streamIdParam) {
        // Автоматически присоединяемся к трансляции
        welcomeScreen.style.display = 'none';
        joinStream(streamIdParam);
    }
});

// Уведомления

// Запрос разрешения на уведомления
if (Notification.permission !== 'granted') {
    Notification.requestPermission();
}

// Обработка новых стримов для уведомлений
socket.on('new-stream', (stream) => {
    // Если пользователь не смотрит стримы и не является стримером, отправляем уведомление
    fetch('/api/user', { credentials: 'include' })
        .then(response => response.json())
        .then(data => {
            if (!data.authenticated || data.role !== 'streamer') {
                new Notification('Новая трансляция!', {
                    body: `Категория: ${stream.category}`,
                    // icon: '/path/to/icon.png' // Укажите путь к иконке уведомления, если есть
                });
                // Обновляем список стримов
                fetchStreams();
            }
        })
        .catch(err => {
            console.error('Ошибка при проверке аутентификации для уведомления:', err);
        });
});

// Функция для отправки системных сообщений в чат
socket.on('system-message', (message) => {
    chatMessagesElements.forEach(chatMessages => {
        const messageElem = document.createElement('div');
        messageElem.className = 'system-message';
        messageElem.innerHTML = `<em>${escapeHtml(message)}</em>`;
        chatMessages.appendChild(messageElem);
        chatMessages.scrollTop = chatMessages.scrollHeight;
    });
});

// Обработка бана
socket.on('banned', () => {
    alert('Вы были забанены.');
    window.location.reload();
});

// Обработка кика
socket.on('kicked', () => {
    alert('Вы были кикнуты из трансляции.');
    window.location.reload();
});

/* public/css/styles.css */

body {
    font-family: Arial, sans-serif;
    background-color: #f0f2f5;
    margin: 0;
    padding: 0;
}

.container {
    max-width: 1200px;
    margin: auto;
    padding: 20px;
}

h1, h2 {
    text-align: center;
}

.buttons, .auth-buttons {
    display: flex;
    justify-content: center;
    gap: 20px;
    margin-top: 20px;
}

.btn {
    padding: 10px 20px;
    font-size: 16px;
    cursor: pointer;
    text-decoration: none;
    color: white;
    background-color: #4CAF50;
    border: none;
    border-radius: 5px;
}

.btn:hover {
    background-color: #45a049;
}

.source-selection, .category-selection, .filter-section {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 10px;
}

.chatContainer {
    margin-top: 20px;
    border: 1px solid #ccc;
    background-color: #fff;
    width: 100%;
    max-width: 500px;
    margin: 20px auto 0;
    padding: 10px;
    border-radius: 5px;
}

.chatMessages {
    height: 200px;
    overflow-y: scroll;
    border-bottom: 1px solid #ccc;
    padding-bottom: 10px;
}

.chat-message {
    margin-bottom: 10px;
}

.system-message {
    text-align: center;
    color: #888;
    font-style: italic;
}

.chatForm {
    display: flex;
    gap: 10px;
    margin-top: 10px;
}

.chatInput {
    flex: 1;
    padding: 5px;
}

#progressContainer {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 10px;
}

#streamLink {
    width: 100%;
    padding: 5px;
}

.stream-item {
    margin-bottom: 10px;
    text-align: center;
}

.stream-item button {
    padding: 10px;
    width: 100%;
    cursor: pointer;
    border: none;
    background-color: #4CAF50;
    color: white;
    font-size: 16px;
    border-radius: 5px;
}

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

video {
    width: 100%;
    max-width: 600px;
    display: block;
    margin: 10px auto;
    border: 1px solid #ccc;
    border-radius: 5px;
}

.filter-section {
    margin-bottom: 20px;
}

.auth-buttons a.btn {
    background-color: #4285F4; /* Google Blue */
}

.auth-buttons a.btn.mailru {
    background-color: #FF0000; /* Mail.ru Red */
}

@media (max-width: 768px) {
    .chatContainer, .stream-item button, .filter-section {
        width: 90%;
    }
}