631 lines
16 KiB
Markdown
631 lines
16 KiB
Markdown
# 🛠️ ИНСТРУКЦИЯ ПО ПЕРЕПИСЫВАНИЮ СИСТЕМЫ VIDEOREADER
|
||
|
||
**Версия:** 1.0
|
||
**Дата:** 9 октября 2025 г.
|
||
**Цель:** Создание собственной безопасной системы видеонаблюдения
|
||
|
||
---
|
||
|
||
## 📋 ПЛАН МИГРАЦИИ
|
||
|
||
### Этап 1: Анализ и планирование (1-2 недели)
|
||
### Этап 2: Инфраструктура (2-3 недели)
|
||
### Етап 3: Backend разработка (3-4 недели)
|
||
### Этап 4: Клиентские приложения (4-6 недель)
|
||
### Этап 5: Тестирование и деплой (2 недели)
|
||
|
||
---
|
||
|
||
## 🏗️ СОВРЕМЕННАЯ АРХИТЕКТУРА
|
||
|
||
### Рекомендуемый стек технологий:
|
||
|
||
#### 🌐 Backend (Сигналинг сервер)
|
||
```
|
||
├── Node.js + TypeScript
|
||
├── Socket.IO / WebSocket
|
||
├── Redis (для сессий)
|
||
├── PostgreSQL (метаданные)
|
||
├── Docker + Docker Compose
|
||
└── Nginx (Reverse Proxy)
|
||
```
|
||
|
||
#### 📱 Android приложение
|
||
```
|
||
├── Kotlin / Java
|
||
├── WebRTC Android API
|
||
├── Camera2 API
|
||
├── Retrofit (HTTP клиент)
|
||
├── Room (локальная БД)
|
||
└── Dagger/Hilt (DI)
|
||
```
|
||
|
||
#### 💻 Desktop приложение
|
||
```
|
||
Вариант 1: Electron + React/Vue
|
||
├── TypeScript
|
||
├── WebRTC Web API
|
||
├── FFmpeg.js
|
||
└── Material-UI / Ant Design
|
||
|
||
Вариант 2: .NET MAUI
|
||
├── C# .NET 8
|
||
├── WebRTC.NET
|
||
├── FFMpegCore
|
||
└── Avalonia UI
|
||
|
||
Вариант 3: Flutter Desktop
|
||
├── Dart
|
||
├── WebRTC Flutter plugin
|
||
├── FFmpeg Flutter
|
||
└── Material Design
|
||
```
|
||
|
||
---
|
||
|
||
## 🚀 ЭТАП 1: НАСТРОЙКА ИНФРАСТРУКТУРЫ
|
||
|
||
### 1.1 Создание сигналинг сервера
|
||
|
||
```bash
|
||
# Создание проекта
|
||
mkdir videoreader-signaling
|
||
cd videoreader-signaling
|
||
npm init -y
|
||
|
||
# Установка зависимостей
|
||
npm install express socket.io redis ioredis
|
||
npm install @types/node typescript ts-node nodemon --save-dev
|
||
```
|
||
|
||
### 1.2 Базовая структура сервера
|
||
|
||
```typescript
|
||
// src/server.ts
|
||
import express from 'express';
|
||
import { createServer } from 'http';
|
||
import { Server } from 'socket.io';
|
||
import Redis from 'ioredis';
|
||
|
||
const app = express();
|
||
const server = createServer(app);
|
||
const io = new Server(server, {
|
||
cors: { origin: "*" }
|
||
});
|
||
|
||
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
|
||
|
||
interface Device {
|
||
id: string;
|
||
type: 'sender' | 'receiver';
|
||
channel: string;
|
||
socketId: string;
|
||
}
|
||
|
||
// Управление каналами и устройствами
|
||
class ChannelManager {
|
||
async addDevice(device: Device) {
|
||
await redis.hset(`channel:${device.channel}`, device.type, device.socketId);
|
||
await redis.expire(`channel:${device.channel}`, 3600); // TTL 1 час
|
||
}
|
||
|
||
async getPartner(channel: string, deviceType: string) {
|
||
const partnerType = deviceType === 'sender' ? 'receiver' : 'sender';
|
||
return await redis.hget(`channel:${channel}`, partnerType);
|
||
}
|
||
}
|
||
|
||
const channelManager = new ChannelManager();
|
||
|
||
io.on('connection', (socket) => {
|
||
socket.on('join-channel', async (data: {channel: string, type: 'sender'|'receiver'}) => {
|
||
await channelManager.addDevice({
|
||
id: socket.id,
|
||
type: data.type,
|
||
channel: data.channel,
|
||
socketId: socket.id
|
||
});
|
||
|
||
// Поиск партнера
|
||
const partner = await channelManager.getPartner(data.channel, data.type);
|
||
if (partner) {
|
||
socket.emit('partner-found', { partnerId: partner });
|
||
io.to(partner).emit('partner-found', { partnerId: socket.id });
|
||
}
|
||
});
|
||
|
||
// WebRTC signaling
|
||
socket.on('webrtc-offer', (data) => {
|
||
io.to(data.to).emit('webrtc-offer', { from: socket.id, offer: data.offer });
|
||
});
|
||
|
||
socket.on('webrtc-answer', (data) => {
|
||
io.to(data.to).emit('webrtc-answer', { from: socket.id, answer: data.answer });
|
||
});
|
||
|
||
socket.on('webrtc-ice-candidate', (data) => {
|
||
io.to(data.to).emit('webrtc-ice-candidate', { from: socket.id, candidate: data.candidate });
|
||
});
|
||
});
|
||
|
||
server.listen(3000, () => console.log('Signaling server running on port 3000'));
|
||
```
|
||
|
||
### 1.3 Docker конфигурация
|
||
|
||
```dockerfile
|
||
# Dockerfile
|
||
FROM node:18-alpine
|
||
WORKDIR /app
|
||
COPY package*.json ./
|
||
RUN npm ci --only=production
|
||
COPY . .
|
||
RUN npm run build
|
||
EXPOSE 3000
|
||
CMD ["npm", "start"]
|
||
```
|
||
|
||
```yaml
|
||
# docker-compose.yml
|
||
version: '3.8'
|
||
services:
|
||
signaling:
|
||
build: .
|
||
ports:
|
||
- "3000:3000"
|
||
environment:
|
||
- REDIS_URL=redis://redis:6379
|
||
depends_on:
|
||
- redis
|
||
|
||
redis:
|
||
image: redis:7-alpine
|
||
ports:
|
||
- "6379:6379"
|
||
|
||
nginx:
|
||
image: nginx:alpine
|
||
ports:
|
||
- "80:80"
|
||
- "443:443"
|
||
volumes:
|
||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||
- ./ssl:/etc/nginx/ssl
|
||
```
|
||
|
||
---
|
||
|
||
## 📱 ЭТАП 2: ANDROID ПРИЛОЖЕНИЕ (KOTLIN)
|
||
|
||
### 2.1 Создание проекта
|
||
|
||
```kotlin
|
||
// build.gradle (Module: app)
|
||
dependencies {
|
||
implementation 'org.webrtc:google-webrtc:1.0.32006'
|
||
implementation 'io.socket:socket.io-client:2.0.0'
|
||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||
implementation 'androidx.camera:camera-camera2:1.3.0'
|
||
implementation 'androidx.camera:camera-lifecycle:1.3.0'
|
||
implementation 'androidx.camera:camera-view:1.3.0'
|
||
}
|
||
```
|
||
|
||
### 2.2 WebRTC интеграция
|
||
|
||
```kotlin
|
||
// WebRTCManager.kt
|
||
class WebRTCManager(private val context: Context) {
|
||
private var peerConnection: PeerConnection? = null
|
||
private var localVideoTrack: VideoTrack? = null
|
||
private var socket: Socket? = null
|
||
|
||
fun initialize() {
|
||
// Инициализация PeerConnectionFactory
|
||
PeerConnectionFactory.initializeAndroidGlobals(
|
||
context,
|
||
true,
|
||
true,
|
||
true
|
||
)
|
||
|
||
val factory = PeerConnectionFactory.builder()
|
||
.createPeerConnectionFactory()
|
||
|
||
// Настройка ICE серверов
|
||
val iceServers = listOf(
|
||
PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer()
|
||
)
|
||
|
||
peerConnection = factory.createPeerConnection(
|
||
PeerConnection.RTCConfiguration(iceServers),
|
||
object : PeerConnection.Observer {
|
||
override fun onIceCandidate(candidate: IceCandidate) {
|
||
sendIceCandidate(candidate)
|
||
}
|
||
// ... другие callbacks
|
||
}
|
||
)
|
||
}
|
||
|
||
fun startCapture() {
|
||
val videoCapturer = Camera2Enumerator(context).run {
|
||
deviceNames.firstOrNull()?.let { createCapturer(it, null) }
|
||
}
|
||
|
||
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", null)
|
||
val videoSource = factory.createVideoSource(false)
|
||
videoCapturer?.initialize(surfaceTextureHelper, context, videoSource.capturerObserver)
|
||
|
||
localVideoTrack = factory.createVideoTrack("local_video", videoSource)
|
||
peerConnection?.addTrack(localVideoTrack, listOf("stream_id"))
|
||
}
|
||
|
||
private fun sendIceCandidate(candidate: IceCandidate) {
|
||
socket?.emit("webrtc-ice-candidate", JSONObject().apply {
|
||
put("to", partnerId)
|
||
put("candidate", candidate.toJson())
|
||
})
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.3 Сигналинг клиент
|
||
|
||
```kotlin
|
||
// SignalingClient.kt
|
||
class SignalingClient(private val serverUrl: String) {
|
||
private var socket: Socket? = null
|
||
private var webRTCManager: WebRTCManager? = null
|
||
|
||
fun connect(channel: String) {
|
||
socket = IO.socket(serverUrl).apply {
|
||
on(Socket.EVENT_CONNECT) {
|
||
emit("join-channel", JSONObject().apply {
|
||
put("channel", channel)
|
||
put("type", "sender")
|
||
})
|
||
}
|
||
|
||
on("partner-found") { args ->
|
||
val data = args[0] as JSONObject
|
||
val partnerId = data.getString("partnerId")
|
||
createOffer(partnerId)
|
||
}
|
||
|
||
on("webrtc-offer") { args ->
|
||
val data = args[0] as JSONObject
|
||
handleOffer(data)
|
||
}
|
||
|
||
connect()
|
||
}
|
||
}
|
||
|
||
private fun createOffer(partnerId: String) {
|
||
webRTCManager?.createOffer { offer ->
|
||
socket?.emit("webrtc-offer", JSONObject().apply {
|
||
put("to", partnerId)
|
||
put("offer", offer.toJson())
|
||
})
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 💻 ЭТАП 3: DESKTOP ПРИЛОЖЕНИЕ (ELECTRON)
|
||
|
||
### 3.1 Инициализация проекта
|
||
|
||
```bash
|
||
mkdir videoreader-desktop
|
||
cd videoreader-desktop
|
||
npm init -y
|
||
npm install electron react react-dom typescript
|
||
npm install simple-peer socket.io-client --save
|
||
```
|
||
|
||
### 3.2 WebRTC компонент
|
||
|
||
```typescript
|
||
// src/components/VideoReceiver.tsx
|
||
import React, { useEffect, useRef, useState } from 'react';
|
||
import io from 'socket.io-client';
|
||
import SimplePeer from 'simple-peer';
|
||
|
||
interface VideoReceiverProps {
|
||
channel: string;
|
||
serverUrl: string;
|
||
}
|
||
|
||
export const VideoReceiver: React.FC<VideoReceiverProps> = ({ channel, serverUrl }) => {
|
||
const videoRef = useRef<HTMLVideoElement>(null);
|
||
const [connected, setConnected] = useState(false);
|
||
const [peer, setPeer] = useState<SimplePeer.Instance | null>(null);
|
||
const socket = useRef(io(serverUrl));
|
||
|
||
useEffect(() => {
|
||
socket.current.emit('join-channel', {
|
||
channel,
|
||
type: 'receiver'
|
||
});
|
||
|
||
socket.current.on('partner-found', (data: { partnerId: string }) => {
|
||
const newPeer = new SimplePeer({
|
||
initiator: false,
|
||
trickle: false
|
||
});
|
||
|
||
newPeer.on('signal', (signal) => {
|
||
socket.current.emit('webrtc-answer', {
|
||
to: data.partnerId,
|
||
answer: signal
|
||
});
|
||
});
|
||
|
||
newPeer.on('stream', (stream) => {
|
||
if (videoRef.current) {
|
||
videoRef.current.srcObject = stream;
|
||
setConnected(true);
|
||
}
|
||
});
|
||
|
||
setPeer(newPeer);
|
||
});
|
||
|
||
socket.current.on('webrtc-offer', (data: { from: string; offer: any }) => {
|
||
if (peer) {
|
||
peer.signal(data.offer);
|
||
}
|
||
});
|
||
|
||
return () => {
|
||
socket.current.disconnect();
|
||
peer?.destroy();
|
||
};
|
||
}, [channel, serverUrl, peer]);
|
||
|
||
return (
|
||
<div className="video-receiver">
|
||
<h2>Channel: {channel}</h2>
|
||
<div className={`status ${connected ? 'connected' : 'disconnected'}`}>
|
||
{connected ? 'Connected' : 'Waiting for connection...'}
|
||
</div>
|
||
<video
|
||
ref={videoRef}
|
||
autoPlay
|
||
muted
|
||
style={{ width: '100%', maxWidth: '800px' }}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
```
|
||
|
||
### 3.3 Главное окно Electron
|
||
|
||
```typescript
|
||
// src/main.ts
|
||
import { app, BrowserWindow } from 'electron';
|
||
import * as path from 'path';
|
||
|
||
function createWindow() {
|
||
const mainWindow = new BrowserWindow({
|
||
width: 1200,
|
||
height: 800,
|
||
webPreferences: {
|
||
nodeIntegration: true,
|
||
contextIsolation: false,
|
||
webSecurity: false // Для разработки, в продакшене нужно настроить правильно
|
||
}
|
||
});
|
||
|
||
if (process.env.NODE_ENV === 'development') {
|
||
mainWindow.loadURL('http://localhost:3001');
|
||
mainWindow.webContents.openDevTools();
|
||
} else {
|
||
mainWindow.loadFile(path.join(__dirname, '../build/index.html'));
|
||
}
|
||
}
|
||
|
||
app.whenReady().then(createWindow);
|
||
```
|
||
|
||
---
|
||
|
||
## 🔐 ЭТАП 4: БЕЗОПАСНОСТЬ
|
||
|
||
### 4.1 Аутентификация пользователей
|
||
|
||
```typescript
|
||
// auth/AuthService.ts
|
||
import jwt from 'jsonwebtoken';
|
||
import bcrypt from 'bcrypt';
|
||
|
||
export class AuthService {
|
||
private static readonly JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
||
|
||
static async hashPassword(password: string): Promise<string> {
|
||
return bcrypt.hash(password, 12);
|
||
}
|
||
|
||
static async verifyPassword(password: string, hash: string): Promise<boolean> {
|
||
return bcrypt.compare(password, hash);
|
||
}
|
||
|
||
static generateToken(userId: string): string {
|
||
return jwt.sign({ userId }, this.JWT_SECRET, { expiresIn: '24h' });
|
||
}
|
||
|
||
static verifyToken(token: string): any {
|
||
return jwt.verify(token, this.JWT_SECRET);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.2 HTTPS и WSS
|
||
|
||
```nginx
|
||
# nginx.conf
|
||
server {
|
||
listen 443 ssl http2;
|
||
server_name your-domain.com;
|
||
|
||
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||
|
||
location / {
|
||
proxy_pass http://signaling:3000;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 ЭТАП 5: МОНИТОРИНГ И МАСШТАБИРОВАНИЕ
|
||
|
||
### 5.1 Логирование
|
||
|
||
```typescript
|
||
// logger/Logger.ts
|
||
import winston from 'winston';
|
||
|
||
export const logger = winston.createLogger({
|
||
level: 'info',
|
||
format: winston.format.combine(
|
||
winston.format.timestamp(),
|
||
winston.format.errors({ stack: true }),
|
||
winston.format.json()
|
||
),
|
||
defaultMeta: { service: 'videoreader-signaling' },
|
||
transports: [
|
||
new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
||
new winston.transports.File({ filename: 'combined.log' }),
|
||
new winston.transports.Console({
|
||
format: winston.format.simple()
|
||
})
|
||
]
|
||
});
|
||
```
|
||
|
||
### 5.2 Метрики
|
||
|
||
```typescript
|
||
// metrics/Metrics.ts
|
||
import prometheus from 'prom-client';
|
||
|
||
export const metrics = {
|
||
connectionsTotal: new prometheus.Counter({
|
||
name: 'connections_total',
|
||
help: 'Total number of connections'
|
||
}),
|
||
|
||
activeConnections: new prometheus.Gauge({
|
||
name: 'active_connections',
|
||
help: 'Number of active connections'
|
||
}),
|
||
|
||
channelsActive: new prometheus.Gauge({
|
||
name: 'channels_active',
|
||
help: 'Number of active channels'
|
||
})
|
||
};
|
||
|
||
prometheus.register.registerMetric(metrics.connectionsTotal);
|
||
prometheus.register.registerMetric(metrics.activeConnections);
|
||
prometheus.register.registerMetric(metrics.channelsActive);
|
||
```
|
||
|
||
---
|
||
|
||
## 🚀 ДЕПЛОЙMENT
|
||
|
||
### Kubernetes манифесты
|
||
|
||
```yaml
|
||
# k8s/deployment.yaml
|
||
apiVersion: apps/v1
|
||
kind: Deployment
|
||
metadata:
|
||
name: videoreader-signaling
|
||
spec:
|
||
replicas: 3
|
||
selector:
|
||
matchLabels:
|
||
app: videoreader-signaling
|
||
template:
|
||
metadata:
|
||
labels:
|
||
app: videoreader-signaling
|
||
spec:
|
||
containers:
|
||
- name: signaling
|
||
image: your-registry/videoreader-signaling:latest
|
||
ports:
|
||
- containerPort: 3000
|
||
env:
|
||
- name: REDIS_URL
|
||
value: "redis://redis-service:6379"
|
||
- name: JWT_SECRET
|
||
valueFrom:
|
||
secretKeyRef:
|
||
name: app-secrets
|
||
key: jwt-secret
|
||
```
|
||
|
||
---
|
||
|
||
## 💡 РЕКОМЕНДАЦИИ ПО УЛУЧШЕНИЮ
|
||
|
||
### 1. Производительность
|
||
- Использование WebRTC для P2P соединений
|
||
- CDN для статических файлов
|
||
- Load balancing для сигналинг серверов
|
||
- Redis Cluster для масштабирования
|
||
|
||
### 2. Безопасность
|
||
- End-to-end шифрование
|
||
- Rate limiting
|
||
- DDoS защита
|
||
- Регулярные security аудиты
|
||
|
||
### 3. Пользовательский опыт
|
||
- Progressive Web App (PWA) версия
|
||
- Адаптивный дизайн
|
||
- Офлайн режим
|
||
- Уведомления
|
||
|
||
### 4. Мониторинг
|
||
- Grafana дашборды
|
||
- Alertmanager для уведомлений
|
||
- Jaeger для трейсинга
|
||
- ELK стек для логов
|
||
|
||
---
|
||
|
||
## 📈 ПЛАН РАЗВИТИЯ
|
||
|
||
### Версия 2.0
|
||
- [ ] Групповые видеозвонки
|
||
- [ ] Запись и хранение видео
|
||
- [ ] AI анализ контента
|
||
- [ ] Мобильные push уведомления
|
||
|
||
### Версия 3.0
|
||
- [ ] Облачное хранилище
|
||
- [ ] API для интеграций
|
||
- [ ] Белый лейбл решение
|
||
- [ ] Международная локализация
|
||
|
||
---
|
||
|
||
**🎯 РЕЗУЛЬТАТ:** Современная, безопасная и масштабируемая система видеонаблюдения с собственной инфраструктурой, готовая к продакшн использованию и дальнейшему развитию. |