This commit is contained in:
parent
3676dc46ed
commit
1a3930dd87
5 changed files with 430 additions and 11 deletions
383
README.md
Normal file
383
README.md
Normal file
|
|
@ -0,0 +1,383 @@
|
||||||
|
# realms.india
|
||||||
|
|
||||||
|
A self-hosted live streaming and gaming platform with real-time chat, game server integration, and media management.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Live Streaming** - RTMP, SRT, and WebRTC ingest with HLS/LLHLS output
|
||||||
|
- **Real-time Chat** - WebSocket-based chat with moderation tools
|
||||||
|
- **Game Server** - Nakama-powered multiplayer games and social features
|
||||||
|
- **Media Library** - E-book hosting and audio/video management
|
||||||
|
- **User Authentication** - JWT-based auth with optional PGP key support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
### Minimum
|
||||||
|
- 4GB RAM
|
||||||
|
- 2 CPU cores
|
||||||
|
- 20GB storage
|
||||||
|
|
||||||
|
### Recommended
|
||||||
|
- 8GB+ RAM
|
||||||
|
- 4+ CPU cores
|
||||||
|
- 100GB+ SSD storage
|
||||||
|
|
||||||
|
### Software
|
||||||
|
- Docker 24+
|
||||||
|
- Docker Compose v2+
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### Operating System
|
||||||
|
- **Linux** (Ubuntu 22.04 recommended) - best for production
|
||||||
|
- macOS
|
||||||
|
- Windows with WSL2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start (Development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone the repository
|
||||||
|
git clone <repository-url>
|
||||||
|
cd realms.india
|
||||||
|
|
||||||
|
# Copy environment template
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Generate secrets and edit .env (see Configuration section below)
|
||||||
|
nano .env
|
||||||
|
|
||||||
|
# Build and start all services
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Access the application at **http://localhost**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Required Environment Variables
|
||||||
|
|
||||||
|
Copy `.env.example` to `.env` and configure these required variables:
|
||||||
|
|
||||||
|
| Variable | Description | How to Generate |
|
||||||
|
|----------|-------------|-----------------|
|
||||||
|
| `DB_PASSWORD` | PostgreSQL database password | `openssl rand -base64 24` |
|
||||||
|
| `JWT_SECRET` | Secret key for JWT token signing | `openssl rand -base64 32` |
|
||||||
|
| `OME_API_TOKEN` | OvenMediaEngine API authentication | `openssl rand -hex 32` |
|
||||||
|
| `REDIS_PASSWORD` | Redis authentication password | `openssl rand -base64 24` |
|
||||||
|
| `NAKAMA_SERVER_KEY` | Nakama client authentication key | `openssl rand -hex 16` |
|
||||||
|
| `NAKAMA_CONSOLE_PASSWORD` | Nakama admin console password | Choose a strong password |
|
||||||
|
| `VITE_NAKAMA_SERVER_KEY` | Frontend Nakama key (must match `NAKAMA_SERVER_KEY`) | Same as `NAKAMA_SERVER_KEY` |
|
||||||
|
|
||||||
|
### Optional Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `DB_HOST` | `postgres` | PostgreSQL host |
|
||||||
|
| `DB_NAME` | `streaming` | Database name |
|
||||||
|
| `DB_USER` | `streamuser` | Database user |
|
||||||
|
| `DB_PORT` | `5432` | PostgreSQL port |
|
||||||
|
| `REDIS_HOST` | `redis` | Redis host |
|
||||||
|
| `REDIS_PORT` | `6379` | Redis port |
|
||||||
|
| `REDIS_DB` | `0` | Redis database number |
|
||||||
|
| `CHAT_REDIS_DB` | `1` | Chat service Redis database |
|
||||||
|
| `APP_ENV` | `production` | Environment mode (`development` or `production`) |
|
||||||
|
| `VITE_NAKAMA_HOST` | `localhost` | Nakama host for frontend |
|
||||||
|
| `VITE_NAKAMA_PORT` | `80` | Nakama port for frontend |
|
||||||
|
| `VITE_NAKAMA_USE_SSL` | `false` | Enable SSL for Nakama connection |
|
||||||
|
|
||||||
|
### Generate All Secrets at Once
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "DB_PASSWORD=$(openssl rand -base64 24)"
|
||||||
|
echo "JWT_SECRET=$(openssl rand -base64 32)"
|
||||||
|
echo "OME_API_TOKEN=$(openssl rand -hex 32)"
|
||||||
|
echo "REDIS_PASSWORD=$(openssl rand -base64 24)"
|
||||||
|
NAKAMA_KEY=$(openssl rand -hex 16)
|
||||||
|
echo "NAKAMA_SERVER_KEY=$NAKAMA_KEY"
|
||||||
|
echo "VITE_NAKAMA_SERVER_KEY=$NAKAMA_KEY"
|
||||||
|
echo "NAKAMA_CONSOLE_PASSWORD=<choose-a-strong-password>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Services Overview
|
||||||
|
|
||||||
|
| Service | Description | Internal Port |
|
||||||
|
|---------|-------------|---------------|
|
||||||
|
| **postgres** | PostgreSQL 16 database for persistent storage | 5432 |
|
||||||
|
| **redis** | Redis 7 for caching, sessions, and message queues | 6379 |
|
||||||
|
| **drogon-backend** | Main API server (C++ Drogon) - streaming, users, realms | 8080 |
|
||||||
|
| **chat-service** | Real-time chat WebSocket server (C++ Drogon) | 8081 |
|
||||||
|
| **nakama** | Game server for multiplayer features | 7350 |
|
||||||
|
| **ovenmediaengine** | Media streaming server (RTMP/SRT/WebRTC ingest, HLS output) | 8081 (API) |
|
||||||
|
| **openresty** | Nginx-based reverse proxy with Lua scripting | 80, 443 |
|
||||||
|
| **sveltekit** | SvelteKit web frontend | 3000 |
|
||||||
|
| **certbot** | Automatic SSL certificate management | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exposed Ports
|
||||||
|
|
||||||
|
### Web Traffic
|
||||||
|
| Port | Protocol | Description |
|
||||||
|
|------|----------|-------------|
|
||||||
|
| 80 | TCP | HTTP |
|
||||||
|
| 443 | TCP | HTTPS |
|
||||||
|
| 8088 | TCP | HLS/LLHLS streaming proxy |
|
||||||
|
|
||||||
|
### Streaming Ingest
|
||||||
|
| Port | Protocol | Description |
|
||||||
|
|------|----------|-------------|
|
||||||
|
| 1935 | TCP | RTMP ingest |
|
||||||
|
| 9999 | UDP | SRT ingest |
|
||||||
|
|
||||||
|
### WebRTC
|
||||||
|
| Port | Protocol | Description |
|
||||||
|
|------|----------|-------------|
|
||||||
|
| 3333 | TCP | WebRTC signaling |
|
||||||
|
| 3478 | UDP | STUN/TURN server |
|
||||||
|
| 10000-10009 | UDP | WebRTC ICE candidates |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Streaming Setup
|
||||||
|
|
||||||
|
### OBS Studio Configuration
|
||||||
|
|
||||||
|
1. Open OBS Studio and go to **Settings > Stream**
|
||||||
|
2. Set **Service** to "Custom..."
|
||||||
|
3. Configure your stream:
|
||||||
|
|
||||||
|
**RTMP:**
|
||||||
|
```
|
||||||
|
Server: rtmp://your-domain:1935/app
|
||||||
|
Stream Key: <your-stream-key>
|
||||||
|
```
|
||||||
|
|
||||||
|
**SRT:**
|
||||||
|
```
|
||||||
|
Server: srt://your-domain:9999
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting Your Stream Key
|
||||||
|
|
||||||
|
Stream keys are generated per-realm in the application. After creating a realm, you can find your stream key in the realm settings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
1. A server with the recommended specifications
|
||||||
|
2. A domain name pointed to your server
|
||||||
|
3. Ports 80, 443, 1935, 9999, 3333, 3478, and 10000-10009 open in your firewall
|
||||||
|
|
||||||
|
### Deployment Steps
|
||||||
|
|
||||||
|
#### 1. Prepare the Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create application directory
|
||||||
|
sudo mkdir -p /opt/realms
|
||||||
|
cd /opt/realms
|
||||||
|
|
||||||
|
# Copy required files
|
||||||
|
# - docker-compose.prod.yml (rename to docker-compose.yml)
|
||||||
|
# - .env (configured with your secrets)
|
||||||
|
# - init.sql (from database/init.sql)
|
||||||
|
# - Server.xml (from ovenmediaengine/Server.xml)
|
||||||
|
# - config.json (from backend/config.json.example)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Configure Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy and configure environment
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env
|
||||||
|
|
||||||
|
# Update these for production:
|
||||||
|
# - Set all secrets (see Configuration section)
|
||||||
|
# - Set VITE_NAKAMA_HOST to your domain
|
||||||
|
# - Set VITE_NAKAMA_USE_SSL=true
|
||||||
|
# - Set APP_ENV=production
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Configure SSL
|
||||||
|
|
||||||
|
The Certbot container handles automatic SSL certificate generation. Ensure:
|
||||||
|
- Your domain's DNS A record points to your server
|
||||||
|
- Ports 80 and 443 are accessible from the internet
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set your email for Let's Encrypt notifications
|
||||||
|
export CERTBOT_EMAIL=admin@your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Start Services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull images and start
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
docker-compose -f docker-compose.prod.yml ps
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose -f docker-compose.prod.yml logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
### Firewall Configuration (UFW)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Allow web traffic
|
||||||
|
sudo ufw allow 80/tcp
|
||||||
|
sudo ufw allow 443/tcp
|
||||||
|
|
||||||
|
# Allow streaming
|
||||||
|
sudo ufw allow 1935/tcp # RTMP
|
||||||
|
sudo ufw allow 9999/udp # SRT
|
||||||
|
sudo ufw allow 8088/tcp # HLS proxy
|
||||||
|
|
||||||
|
# Allow WebRTC
|
||||||
|
sudo ufw allow 3333/tcp # Signaling
|
||||||
|
sudo ufw allow 3478/udp # STUN/TURN
|
||||||
|
sudo ufw allow 10000:10009/udp # ICE candidates
|
||||||
|
|
||||||
|
# Enable firewall
|
||||||
|
sudo ufw enable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updating
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/realms
|
||||||
|
|
||||||
|
# Pull latest images
|
||||||
|
docker-compose -f docker-compose.prod.yml pull
|
||||||
|
|
||||||
|
# Restart with new images
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Health Checks
|
||||||
|
|
||||||
|
Check if services are healthy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View service status
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Check specific service health
|
||||||
|
docker inspect --format='{{.State.Health.Status}}' realms-drogon-backend-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Check Endpoints
|
||||||
|
|
||||||
|
| Service | Endpoint |
|
||||||
|
|---------|----------|
|
||||||
|
| Backend API | `http://localhost:8080/api/health` |
|
||||||
|
| Chat Service | `http://localhost:8081/` |
|
||||||
|
| Nakama | `http://localhost:7350/healthcheck` |
|
||||||
|
|
||||||
|
### Viewing Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# All services
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Specific service
|
||||||
|
docker-compose logs -f drogon-backend
|
||||||
|
docker-compose logs -f chat-service
|
||||||
|
docker-compose logs -f nakama
|
||||||
|
|
||||||
|
# Last 100 lines
|
||||||
|
docker-compose logs --tail=100 drogon-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### Services won't start
|
||||||
|
```bash
|
||||||
|
# Check for port conflicts
|
||||||
|
sudo lsof -i :80
|
||||||
|
sudo lsof -i :443
|
||||||
|
|
||||||
|
# Verify .env file exists and has all required variables
|
||||||
|
cat .env | grep -v "^#" | grep -v "^$"
|
||||||
|
|
||||||
|
# Rebuild containers
|
||||||
|
docker-compose down
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Database connection errors
|
||||||
|
```bash
|
||||||
|
# Check PostgreSQL is healthy
|
||||||
|
docker-compose logs postgres
|
||||||
|
|
||||||
|
# Verify database credentials match .env
|
||||||
|
docker-compose exec postgres psql -U streamuser -d streaming -c "SELECT 1"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Stream not working
|
||||||
|
```bash
|
||||||
|
# Check OvenMediaEngine logs
|
||||||
|
docker-compose logs ovenmediaengine
|
||||||
|
|
||||||
|
# Verify stream key is correct
|
||||||
|
# Check that RTMP port 1935 is accessible
|
||||||
|
nc -zv your-domain 1935
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SSL certificate issues
|
||||||
|
```bash
|
||||||
|
# Check Certbot logs
|
||||||
|
docker-compose logs certbot
|
||||||
|
|
||||||
|
# Manually request certificate
|
||||||
|
docker-compose exec certbot certbot certonly --webroot \
|
||||||
|
--webroot-path=/var/www/certbot \
|
||||||
|
-d your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset Everything
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop all containers and remove volumes (WARNING: deletes all data)
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
# Remove all images
|
||||||
|
docker-compose down --rmi all
|
||||||
|
|
||||||
|
# Fresh start
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
1. **Never commit `.env`** - Keep your secrets out of version control
|
||||||
|
2. **Rotate secrets regularly** - Recommended every 90 days
|
||||||
|
3. **Restrict file permissions** - `chmod 600 .env`
|
||||||
|
4. **Use strong passwords** - Minimum 16 characters with mixed case, numbers, and symbols
|
||||||
|
5. **Keep Docker updated** - Regularly update Docker and base images
|
||||||
|
6. **Enable firewall** - Only expose necessary ports
|
||||||
|
7. **Use HTTPS** - Always use SSL in production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See LICENSE file for details.
|
||||||
|
|
@ -952,7 +952,9 @@ void ChatWebSocketController::handleRename(const WebSocketConnectionPtr& wsConnP
|
||||||
lastRenameTime_[wsConnPtr] = std::chrono::steady_clock::now();
|
lastRenameTime_[wsConnPtr] = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
// SECURITY FIX #31: Store guest name server-side for persistence
|
// SECURITY FIX #31: Store guest name server-side for persistence
|
||||||
if (!info.fingerprint.empty()) {
|
// Only store for actual guests (isGuest check prevents authenticated users
|
||||||
|
// who haven't cleared fingerprint from storing their username)
|
||||||
|
if (info.isGuest && !info.fingerprint.empty()) {
|
||||||
services::RedisMessageStore::getInstance().setGuestName(info.fingerprint, newName);
|
services::RedisMessageStore::getInstance().setGuestName(info.fingerprint, newName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1030,6 +1032,11 @@ void ChatWebSocketController::handleAuthMessage(const WebSocketConnectionPtr& ws
|
||||||
it->second.isSiteModerator = claims->isModerator; // isModerator in claims is site-wide
|
it->second.isSiteModerator = claims->isModerator; // isModerator in claims is site-wide
|
||||||
it->second.avatarUrl = claims->avatarUrl;
|
it->second.avatarUrl = claims->avatarUrl;
|
||||||
|
|
||||||
|
// SECURITY FIX: Clear fingerprint when upgrading to authenticated user
|
||||||
|
// This prevents the registered username from being stored against the fingerprint
|
||||||
|
// which would cause it to appear on guest sessions with the same fingerprint
|
||||||
|
it->second.fingerprint.clear();
|
||||||
|
|
||||||
// Update usernameToConnection_ map: remove old guest name, add new authenticated name
|
// Update usernameToConnection_ map: remove old guest name, add new authenticated name
|
||||||
if (!oldUsername.empty()) {
|
if (!oldUsername.empty()) {
|
||||||
std::string lowerOld = oldUsername;
|
std::string lowerOld = oldUsername;
|
||||||
|
|
|
||||||
|
|
@ -208,16 +208,28 @@ void WatchSyncController::broadcastRoomSync(const std::string& realmId) {
|
||||||
|
|
||||||
// Check if video has ended and should auto-advance
|
// Check if video has ended and should auto-advance
|
||||||
if (it->second.playbackState == "playing" &&
|
if (it->second.playbackState == "playing" &&
|
||||||
it->second.durationSeconds > 0 &&
|
|
||||||
!it->second.currentVideoId.empty()) {
|
!it->second.currentVideoId.empty()) {
|
||||||
double expectedTime = getExpectedTime(it->second);
|
double expectedTime = getExpectedTime(it->second);
|
||||||
|
|
||||||
|
// Debug logging to diagnose end detection
|
||||||
|
static int debugCounter = 0;
|
||||||
|
if (++debugCounter % 10 == 0) { // Log every 10 sync cycles
|
||||||
|
LOG_DEBUG << "Room " << realmId << " sync check: expectedTime=" << expectedTime
|
||||||
|
<< ", durationSeconds=" << it->second.durationSeconds
|
||||||
|
<< ", videoId=" << it->second.currentVideoId
|
||||||
|
<< ", locked=" << it->second.currentVideoLocked;
|
||||||
|
}
|
||||||
|
|
||||||
// Add 1 second buffer to account for timing variations
|
// Add 1 second buffer to account for timing variations
|
||||||
if (expectedTime >= static_cast<double>(it->second.durationSeconds) + 1.0) {
|
if (it->second.durationSeconds > 0 &&
|
||||||
|
expectedTime >= static_cast<double>(it->second.durationSeconds) + 1.0) {
|
||||||
shouldAutoAdvance = true;
|
shouldAutoAdvance = true;
|
||||||
// Mark as ended to prevent multiple auto-advance calls
|
// Mark as ended to prevent multiple auto-advance calls
|
||||||
it->second.playbackState = "ended";
|
it->second.playbackState = "ended";
|
||||||
it->second.stateVersion++;
|
it->second.stateVersion++;
|
||||||
LOG_INFO << "Video ended in room " << realmId << ", auto-advancing";
|
LOG_INFO << "Video ended in room " << realmId << ", auto-advancing"
|
||||||
|
<< " (expectedTime=" << expectedTime
|
||||||
|
<< ", duration=" << it->second.durationSeconds << ")";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -863,6 +875,12 @@ void WatchSyncController::handleJoinRoom(const WebSocketConnectionPtr& wsConnPtr
|
||||||
state.currentPlaylistItemId = video["id"].asInt64();
|
state.currentPlaylistItemId = video["id"].asInt64();
|
||||||
state.currentVideoTitle = video["title"].asString();
|
state.currentVideoTitle = video["title"].asString();
|
||||||
state.durationSeconds = video["durationSeconds"].asInt();
|
state.durationSeconds = video["durationSeconds"].asInt();
|
||||||
|
state.currentVideoLocked = video.isMember("isLocked") && video["isLocked"].asBool();
|
||||||
|
|
||||||
|
LOG_INFO << "Room " << newRealmId << " initialized: videoId=" << state.currentVideoId
|
||||||
|
<< ", duration=" << state.durationSeconds
|
||||||
|
<< ", locked=" << state.currentVideoLocked
|
||||||
|
<< ", playbackState=" << state.playbackState;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1486,6 +1504,20 @@ void WatchSyncController::handleUpdateDuration(const WebSocketConnectionPtr& wsC
|
||||||
LOG_INFO << "Duration reported for playlist item " << playlistItemId
|
LOG_INFO << "Duration reported for playlist item " << playlistItemId
|
||||||
<< " in room " << info.realmId << ": " << durationSeconds << "s";
|
<< " in room " << info.realmId << ": " << durationSeconds << "s";
|
||||||
|
|
||||||
|
// Update in-memory state immediately (don't wait for backend confirmation)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(roomStatesMutex_);
|
||||||
|
auto it = roomStates_.find(info.realmId);
|
||||||
|
if (it != roomStates_.end() &&
|
||||||
|
it->second.currentPlaylistItemId == playlistItemId) {
|
||||||
|
if (it->second.durationSeconds == 0 || it->second.durationSeconds != durationSeconds) {
|
||||||
|
LOG_INFO << "Updating in-memory duration from " << it->second.durationSeconds
|
||||||
|
<< " to " << durationSeconds << " for room " << info.realmId;
|
||||||
|
it->second.durationSeconds = durationSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Forward to backend API
|
// Forward to backend API
|
||||||
auto client = HttpClient::newHttpClient("http://drogon-backend:8080");
|
auto client = HttpClient::newHttpClient("http://drogon-backend:8080");
|
||||||
Json::Value reqBody;
|
Json::Value reqBody;
|
||||||
|
|
|
||||||
|
|
@ -120,17 +120,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report duration when video starts playing (duration is now available)
|
// Report duration when video starts playing (duration is now available)
|
||||||
// Only report if we haven't already for this playlist item
|
// Always report if we haven't already for this playlist item - server needs accurate duration
|
||||||
const storeState = $watchSync;
|
const storeState = $watchSync;
|
||||||
const playlistItemId = storeState.currentVideo?.id;
|
const playlistItemId = storeState.currentVideo?.id;
|
||||||
const storedDuration = storeState.currentVideo?.durationSeconds || 0;
|
|
||||||
|
|
||||||
if (playlistItemId &&
|
if (playlistItemId && playlistItemId !== durationReportedForItemId) {
|
||||||
playlistItemId !== durationReportedForItemId &&
|
|
||||||
storedDuration === 0) {
|
|
||||||
const playerDuration = player.getDuration();
|
const playerDuration = player.getDuration();
|
||||||
if (playerDuration > 0) {
|
if (playerDuration > 0) {
|
||||||
console.log(`Reporting duration for playlist item ${playlistItemId}: ${playerDuration}s`);
|
console.log(`Reporting duration for playlist item ${playlistItemId}: ${Math.floor(playerDuration)}s`);
|
||||||
watchSync.reportDuration(playlistItemId, playerDuration);
|
watchSync.reportDuration(playlistItemId, playerDuration);
|
||||||
durationReportedForItemId = playlistItemId;
|
durationReportedForItemId = playlistItemId;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
loading = true;
|
loading = true;
|
||||||
await Promise.all([loadUsers(), loadStreams(), loadStickers(), loadHonkSounds(), loadChatSettings(), loadSiteSettings(), loadRealms(), loadStickerSubmissions(), loadVideos(), loadAudios(), loadEbooks(), loadDefaultAvatars(), loadBotApiKeys(), loadSSLSettings()]);
|
await Promise.all([loadUsers(), loadStreams(), loadStickers(), loadHonkSounds(), loadChatSettings(), loadSiteSettings(), loadRealms(), loadStickerSubmissions(), loadVideos(), loadAudios(), loadEbooks(), loadDefaultAvatars(), loadBotApiKeys(), loadSSLSettings(), loadUberbannedFingerprints()]);
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue