diff --git a/.env.example b/.env.example index 984d3df..e7251d5 100644 --- a/.env.example +++ b/.env.example @@ -10,3 +10,6 @@ PINATA_JWT=eyJhbGciOi... # X264_CRF=22 # AAC_BITRATE=128k # PORT=8080 + +# CORS: all origins allowed by default +NODE_ENV=development diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..513de16 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# Ignore environment variables file +.env + +node_modules +# Logs (generated per-node, should not be tracked) +logs/ +*.log diff --git a/Dockerfile b/Dockerfile index a3dc699..b91037c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # Simple ffmpeg + Node worker FROM node:20-bullseye-slim -# Install ffmpeg -RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg ca-certificates \ +# Install ffmpeg and curl for health checks +RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg curl ca-certificates \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/README.md b/README.md index b024ef0..4ebffda 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,97 @@ A tiny API that accepts a file upload, transcodes it to MP4 (H.264/AAC) with FFm - `GET /healthz` — health check - `POST /transcode` — multipart/form-data with a single field named `video` +- `GET /progress/:requestId` — **SSE (Server-Sent Events)** for real-time progress streaming +- `GET /logs` — get recent transcode operations (JSON) +- `GET /stats` — get transcoding statistics (JSON) **Response** ```json -{ "cid": "bafy...", "gatewayUrl": "https://gateway.pinata.cloud/ipfs/bafy..." } +{ + "success": true, + "data": { + "cid": "bafy...", + "gatewayUrl": "https://gateway.pinata.cloud/ipfs/bafy..." + } +} ``` +## 🆕 Real-Time Progress Streaming (SSE) + +The service now supports **Server-Sent Events (SSE)** for real-time progress updates during transcoding: + +### How It Works + +1. **Client generates a unique `correlationId`** before uploading +2. **Client opens SSE connection** to `/progress/:correlationId` +3. **Client sends POST to `/transcode`** with the same `correlationId` in form data +4. **Server broadcasts progress** to all connected SSE clients for that request + +### Progress Stages + +| Stage | Progress Range | Description | +|-------|---------------|-------------| +| `waiting` | 0% | SSE connected, waiting for upload | +| `receiving` | 5% | Server receiving file | +| `transcoding` | 10-80% | FFmpeg processing (based on video duration) | +| `uploading` | 80-100% | Uploading to Pinata IPFS | +| `complete` | 100% | Done! | +| `error` | 0% | Something went wrong | + +### SSE Client Example + +```javascript +// Generate unique ID +const requestId = `${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 6)}`; + +// Open SSE connection BEFORE uploading +const eventSource = new EventSource(`https://server/progress/${requestId}`); +eventSource.onmessage = (event) => { + const { progress, stage } = JSON.parse(event.data); + console.log(`Progress: ${progress}% - ${stage}`); + updateProgressBar(progress); +}; + +// Send upload with correlationId +const formData = new FormData(); +formData.append('video', file); +formData.append('correlationId', requestId); +fetch('https://server/transcode', { method: 'POST', body: formData }); +``` + +### Terminal Test + +```bash +# Test SSE progress with curl +TEST_ID="test-$(date +%s)" && \ +(curl -sN "https://minivlad.tail83ea3e.ts.net/video/progress/$TEST_ID" &) && \ +sleep 1 && \ +curl -X POST "https://minivlad.tail83ea3e.ts.net/video/transcode" \ + -F "video=@/path/to/video.mov" \ + -F "correlationId=$TEST_ID" +``` + +## Logging & Monitoring + +The service now includes rich structured logging that tracks: +- User/creator information +- File details (name, size) +- Processing duration +- Success/failure status +- Client IP addresses +- IPFS CIDs and gateway URLs + +**Logging Features:** +- Maintains last 100 operations in `logs/transcode.log` +- JSON-structured log entries for easy parsing +- Dashboard-friendly endpoints +- Rich console output with emojis and formatting + +**Dashboard Integration:** +- `GET /logs?limit=N` - Returns recent operations for dashboard display +- `GET /stats` - Returns aggregated statistics (success rate, avg duration, etc.) +- Designed to work with the Skatehive dashboard monitoring system + ## Quickstart (Docker) ```bash @@ -22,18 +107,62 @@ cp .env.example .env # 3) Build & run docker build -t video-worker . + +# Development (port 8080): docker run --env-file .env -p 8080:8080 --name video-worker video-worker -# 4) Test +# Production (port 8081 external, 8080 internal): +docker run --env-file .env -p 8081:8080 --name video-worker video-worker + +# Or use docker-compose (recommended for production): +docker compose up -d +``` + +```bash +# 4) Test (adjust port based on deployment) curl -F "video=@/path/to/input.mov" http://localhost:8080/transcode + +# 5) Test logging system (creates mock log entries) +npm run test-logs + +# 6) Check logs and stats +curl http://localhost:8080/logs +curl http://localhost:8080/stats +``` ``` ## Environment - `PINATA_JWT` (required) — Create in Pinata Dashboard → API Keys (JWT). - `PINATA_GATEWAY` (optional) — Defaults to `https://gateway.pinata.cloud/ipfs`. -- `MAX_UPLOAD_MB` (optional) — Upload limit, default `512`. +- `MAX_UPLOAD_MB` (optional) — Upload limit, default `512` (set to `200` on Mac Mini M4). - `X264_PRESET`, `X264_CRF`, `AAC_BITRATE` — FFmpeg tuning knobs. +- `PORT` (optional) — Internal port, defaults to `8080`. +- CORS is open to all origins by default. +- `NODE_ENV` — Environment mode (`development` or `production`). + +## Production Deployment (Mac Mini M4) + +**Current Live Configuration:** + +- **External URL:** `https://minivlad.tail83ea3e.ts.net/video/transcode` +- **External Port:** `8081` +- **Internal Port:** `8080` +- **Container:** `video-worker` +- **Upload Limit:** `200MB` +- **Network:** Tailscale Funnel (publicly accessible) + +**Port Mapping:** +```yaml +# docker-compose.yml +ports: + - "8081:8080" # Host:Container +``` + +This means: +- Service listens on port `8080` inside the container +- Accessible on port `8081` from the host (Mac Mini) +- Tailscale Funnel routes `https://minivlad.tail83ea3e.ts.net/video/*` to port `8081` ## Deploy Options diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8672c73 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +services: + video-worker: + build: . + ports: + - "8081:8080" + env_file: + - .env + environment: + - PORT=8080 + - NODE_NAME=${NODE_NAME:-macmini} + volumes: + - ./logs:/app/logs + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:8080/healthz', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] + interval: 30s + timeout: 10s + retries: 3 + container_name: video-worker \ No newline at end of file diff --git a/logs/transcode.log b/logs/transcode.log new file mode 100644 index 0000000..e5a88bf --- /dev/null +++ b/logs/transcode.log @@ -0,0 +1,100 @@ +{"id":"26fe2185","status":"started","user":"gorilaskt","filename":"IMG_4472.MOV","fileSize":11092170,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (Linux; Android 11; SM-A305G Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like ","origin":"https://skatehive.app","platform":"mobile","deviceInfo":"mobile/Linux/Chrome","browserInfo":"Chrome on Linux","userHP":"52.21666231042504","correlationId":"1759905271169-m2oav0wsu","viewport":"412x892","connectionType":"4g","startTime":1759905286149,"timestamp":"2025-10-08T06:34:46.149Z"} +{"id":"26fe2185","status":"completed","user":"gorilaskt","filename":"IMG_4472.MOV","cid":"bafybeid6uwhot4ahj6gtzapkm6inx4lk4xqaohk4pvyyfmnjd7uksywzwu","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeid6uwhot4ahj6gtzapkm6inx4lk4xqaohk4pvyyfmnjd7uksywzwu","duration":23342,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"mobile","deviceInfo":"mobile/Linux/Chrome","browserInfo":"Chrome on Linux","userHP":"52.21666231042504","correlationId":"1759905271169-m2oav0wsu","viewport":"412x892","connectionType":"4g","timestamp":"2025-10-08T06:35:09.491Z"} +{"id":"f30549e8","status":"started","user":"gorilaskt","filename":"IMG_4465.MOV","fileSize":13040071,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (Linux; Android 11; SM-A305G Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like ","origin":"https://skatehive.app","platform":"mobile","deviceInfo":"mobile/Linux/Chrome","browserInfo":"Chrome on Linux","userHP":"52.21666231042504","correlationId":"1759905471664-dylt4j6hr","viewport":"412x892","connectionType":"4g","startTime":1759905481721,"timestamp":"2025-10-08T06:38:01.721Z"} +{"id":"f30549e8","status":"completed","user":"gorilaskt","filename":"IMG_4465.MOV","cid":"bafybeid4qwe433tc367anl3h3bqujl2x4zth766gwjvnerdeetjgrtmubu","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeid4qwe433tc367anl3h3bqujl2x4zth766gwjvnerdeetjgrtmubu","duration":43788,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"mobile","deviceInfo":"mobile/Linux/Chrome","browserInfo":"Chrome on Linux","userHP":"52.21666231042504","correlationId":"1759905471664-dylt4j6hr","viewport":"412x892","connectionType":"4g","timestamp":"2025-10-08T06:38:45.509Z"} +{"id":"29e1fdf2","status":"started","user":"gorilaskt","filename":"IMG_4472.MOV","fileSize":11092170,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (Linux; Android 11; SM-A305G Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like ","origin":"https://skatehive.app","platform":"mobile","deviceInfo":"mobile/Linux/Chrome","browserInfo":"Chrome on Linux","userHP":"52.21666231042504","correlationId":"1759905632982-92dhrsnsf","viewport":"412x892","connectionType":"4g","startTime":1759905639934,"timestamp":"2025-10-08T06:40:39.934Z"} +{"id":"29e1fdf2","status":"completed","user":"gorilaskt","filename":"IMG_4472.MOV","cid":"bafybeid6uwhot4ahj6gtzapkm6inx4lk4xqaohk4pvyyfmnjd7uksywzwu","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeid6uwhot4ahj6gtzapkm6inx4lk4xqaohk4pvyyfmnjd7uksywzwu","duration":25481,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"mobile","deviceInfo":"mobile/Linux/Chrome","browserInfo":"Chrome on Linux","userHP":"52.21666231042504","correlationId":"1759905632982-92dhrsnsf","viewport":"412x892","connectionType":"4g","timestamp":"2025-10-08T06:41:05.416Z"} +{"id":"ba27128c","status":"started","user":"knowhow92","filename":"IMG_9558.mov","fileSize":26682627,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobi","origin":"https://skatehive.app","platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"4609.459009705275","correlationId":"1759905713360-pc3ilst9d","viewport":"393x852","connectionType":"unknown","startTime":1759905728396,"timestamp":"2025-10-08T06:42:08.396Z"} +{"id":"ba27128c","status":"completed","user":"knowhow92","filename":"IMG_9558.mov","cid":"bafybeicmc32ifsdsh7uwdh3j2grn7ihhgzntwiugfbdr7d7jfspyvq2xju","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeicmc32ifsdsh7uwdh3j2grn7ihhgzntwiugfbdr7d7jfspyvq2xju","duration":22649,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"4609.459009705275","correlationId":"1759905713360-pc3ilst9d","viewport":"393x852","connectionType":"unknown","timestamp":"2025-10-08T06:42:31.044Z"} +{"id":"8018d403","status":"started","user":"gorilaskt","filename":"IMG_4476.MOV","fileSize":11235087,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (Linux; Android 11; SM-A305G Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like ","origin":"https://skatehive.app","platform":"mobile","deviceInfo":"mobile/Linux/Chrome","browserInfo":"Chrome on Linux","userHP":"52.21666231042504","correlationId":"1759905995394-pllko1ob6","viewport":"412x892","connectionType":"4g","startTime":1759906006511,"timestamp":"2025-10-08T06:46:46.511Z"} +{"id":"8018d403","status":"completed","user":"gorilaskt","filename":"IMG_4476.MOV","cid":"bafybeidcwefykwxw3owlkhq4sgq7mqeadmriiq65jn4pugzihsnnkxt4hq","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeidcwefykwxw3owlkhq4sgq7mqeadmriiq65jn4pugzihsnnkxt4hq","duration":29796,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"mobile","deviceInfo":"mobile/Linux/Chrome","browserInfo":"Chrome on Linux","userHP":"52.21666231042504","correlationId":"1759905995394-pllko1ob6","viewport":"412x892","connectionType":"4g","timestamp":"2025-10-08T06:47:16.307Z"} +{"id":"329d6787","status":"started","user":"yungbresciani","filename":"IMG_7105.MOV","fileSize":21177347,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.","origin":"https://www.skatehive.app","platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"312.04333561020155","correlationId":"1760046997472-s72kyml7f","viewport":"2560x1440","connectionType":"unknown","startTime":1760047029678,"timestamp":"2025-10-09T21:57:09.678Z"} +{"id":"329d6787","status":"completed","user":"yungbresciani","filename":"IMG_7105.MOV","cid":"bafybeiavmrw3t5s3bq6yyf63e5ew5hmzgma76wsloigvuitna6sy2vlozi","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeiavmrw3t5s3bq6yyf63e5ew5hmzgma76wsloigvuitna6sy2vlozi","duration":24425,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"312.04333561020155","correlationId":"1760046997472-s72kyml7f","viewport":"2560x1440","connectionType":"unknown","timestamp":"2025-10-09T21:57:34.103Z"} +{"id":"bca5de6e","status":"started","user":"joaoparmagnani","filename":"Automatic.mov","fileSize":36496599,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.","origin":"https://skatehive.app","platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"388.0238598581248","correlationId":"1760048456779-dvrrmnw0g","viewport":"2560x1440","connectionType":"unknown","startTime":1760048503202,"timestamp":"2025-10-09T22:21:43.202Z"} +{"id":"bca5de6e","status":"completed","user":"joaoparmagnani","filename":"Automatic.mov","cid":"bafybeihrqhbztl22eufugrqoxrruitxtnxmsptsfg3waaupqzbraz2dlke","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeihrqhbztl22eufugrqoxrruitxtnxmsptsfg3waaupqzbraz2dlke","duration":30357,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"388.0238598581248","correlationId":"1760048456779-dvrrmnw0g","viewport":"2560x1440","connectionType":"unknown","timestamp":"2025-10-09T22:22:13.558Z"} +{"id":"70222cc7","status":"started","user":"bagre33","filename":"IMG_3113.MOV","fileSize":51558391,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Sa","origin":"https://www.skatehive.app","platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0","correlationId":"1760126054951-8hbhdtbsh","viewport":"1366x768","connectionType":"4g","startTime":1760126145378,"timestamp":"2025-10-10T19:55:45.378Z"} +{"id":"70222cc7","status":"completed","user":"bagre33","filename":"IMG_3113.MOV","cid":"bafybeifh4eojm7i2mfwcnrpc2ar5dyy5qtlnlqkbfogtbj7immou7cesaa","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeifh4eojm7i2mfwcnrpc2ar5dyy5qtlnlqkbfogtbj7immou7cesaa","duration":64707,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0","correlationId":"1760126054951-8hbhdtbsh","viewport":"1366x768","connectionType":"4g","timestamp":"2025-10-10T19:56:50.084Z"} +{"id":"8582cd5f","status":"started","user":"yungbresciani","filename":"IMG_7128.MOV","fileSize":6573217,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.","origin":"https://www.skatehive.app","platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"312.10929297426225","correlationId":"1760128588555-8oyiuofo1","viewport":"2560x1440","connectionType":"unknown","startTime":1760128599409,"timestamp":"2025-10-10T20:36:39.409Z"} +{"id":"8582cd5f","status":"completed","user":"yungbresciani","filename":"IMG_7128.MOV","cid":"bafybeicmsf3iymftdunqe3t6l3gspskwjz35gnop5y724ncmom6dqhrygu","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeicmsf3iymftdunqe3t6l3gspskwjz35gnop5y724ncmom6dqhrygu","duration":3901,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"312.10929297426225","correlationId":"1760128588555-8oyiuofo1","viewport":"2560x1440","connectionType":"unknown","timestamp":"2025-10-10T20:36:43.310Z"} +{"id":"8781517f","status":"started","user":"xvlad","filename":"trimmed_IMG_4398.mov","fileSize":24319877,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobi","origin":"https://skatehive.app","platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"4288.78537579837","correlationId":"1760137678203-l4sn9mjlm","viewport":"430x932","connectionType":"unknown","startTime":1760137694219,"timestamp":"2025-10-10T23:08:14.219Z"} +{"id":"8781517f","status":"completed","user":"xvlad","filename":"trimmed_IMG_4398.mov","cid":"bafybeifbgdavb42d4a4gxxmq3zyvyqvz5m5yzpeycqvid27ssyqexoeulq","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeifbgdavb42d4a4gxxmq3zyvyqvz5m5yzpeycqvid27ssyqexoeulq","duration":98587,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"4288.78537579837","correlationId":"1760137678203-l4sn9mjlm","viewport":"430x932","connectionType":"unknown","timestamp":"2025-10-10T23:09:52.806Z"} +{"id":"03e660c2","status":"started","user":"anonymous","filename":"IMG_0033.MOV","fileSize":16219641,"clientIP":"::ffff:192.168.65.1","userAgent":"curl/8.7.1","origin":"direct","platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":"","userHP":0,"correlationId":null,"viewport":null,"connectionType":null,"startTime":1760736696097,"timestamp":"2025-10-17T21:31:36.097Z"} +{"id":"03e660c2","status":"failed","user":"anonymous","filename":"IMG_0033.MOV","error":"Cannot read properties of null (reading 'toString')","duration":10871,"clientIP":"::ffff:192.168.65.1","success":false,"platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":null,"userHP":null,"correlationId":null,"viewport":null,"connectionType":null,"timestamp":"2025-10-17T21:31:46.967Z"} +{"id":"5477b1b7","status":"started","user":"test-user","filename":"test-video.mov","fileSize":31375508,"clientIP":"::ffff:192.168.65.1","userAgent":"node","origin":"direct","platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":"","userHP":0,"correlationId":null,"viewport":null,"connectionType":null,"startTime":1760736817093,"timestamp":"2025-10-17T21:33:37.093Z"} +{"id":"5477b1b7","status":"failed","user":"test-user","filename":"test-video.mov","error":"Cannot read properties of null (reading 'toString')","duration":2380,"clientIP":"::ffff:192.168.65.1","success":false,"platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":null,"userHP":null,"correlationId":null,"viewport":null,"connectionType":null,"timestamp":"2025-10-17T21:33:39.473Z"} +{"id":"e64162d9","status":"started","user":"xvlad","filename":"Skatehive Video (3).mov","fileSize":31375508,"clientIP":"::ffff:192.168.65.1","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.","origin":"https://skatehive.app","platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"4302.517319693305","correlationId":"1760736727593-zxyru5k07","viewport":"1728x1117","connectionType":"4g","startTime":1760736849774,"timestamp":"2025-10-17T21:34:09.774Z"} +{"id":"e64162d9","status":"completed","user":"xvlad","filename":"Skatehive Video (3).mov","cid":"bafybeihw6h6qwy3kit5vsmr5yqwvynrcm2cj6inehbnan7k4h73fyl7udi","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeihw6h6qwy3kit5vsmr5yqwvynrcm2cj6inehbnan7k4h73fyl7udi","duration":5278,"clientIP":"::ffff:192.168.65.1","success":true,"platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"4302.517319693305","correlationId":"1760736727593-zxyru5k07","viewport":"1728x1117","connectionType":"4g","timestamp":"2025-10-17T21:34:15.052Z"} +{"id":"bbf29344","status":"started","user":"anonymous","filename":"IMG_0033.MOV","fileSize":16219641,"clientIP":"::ffff:192.168.65.1","userAgent":"curl/8.7.1","origin":"direct","platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":"","userHP":0,"correlationId":null,"viewport":null,"connectionType":null,"startTime":1760737058464,"timestamp":"2025-10-17T21:37:38.464Z"} +{"id":"bbf29344","status":"failed","user":"anonymous","filename":"IMG_0033.MOV","error":"Cannot read properties of null (reading 'toString')","duration":10694,"clientIP":"::ffff:192.168.65.1","success":false,"platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":null,"userHP":null,"correlationId":null,"viewport":null,"connectionType":null,"timestamp":"2025-10-17T21:37:49.158Z"} +{"id":"74349e8a","status":"started","user":"test-user","filename":"test-video.mov","fileSize":31375508,"clientIP":"::ffff:192.168.65.1","userAgent":"node","origin":"direct","platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":"","userHP":0,"correlationId":null,"viewport":null,"connectionType":null,"startTime":1760737131314,"timestamp":"2025-10-17T21:38:51.314Z"} +{"id":"74349e8a","status":"failed","user":"test-user","filename":"test-video.mov","error":"Cannot read properties of null (reading 'toString')","duration":2244,"clientIP":"::ffff:192.168.65.1","success":false,"platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":null,"userHP":null,"correlationId":null,"viewport":null,"connectionType":null,"timestamp":"2025-10-17T21:38:53.558Z"} +{"id":"82284067","status":"started","user":"xvlad","filename":"no-comply-mam.mov","fileSize":1486533,"clientIP":"::ffff:192.168.65.1","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.","origin":"http://localhost:3000","platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"4302.529590861319","correlationId":"1760737188839-h5r5y5l11","viewport":"1728x1117","connectionType":"4g","startTime":1760737190814,"timestamp":"2025-10-17T21:39:50.814Z"} +{"id":"82284067","status":"completed","user":"xvlad","filename":"no-comply-mam.mov","cid":"bafybeicm3hmlgcpznx3nqsr2wrokj6u7w2ti4eggkxyblnbhadfcapy77q","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeicm3hmlgcpznx3nqsr2wrokj6u7w2ti4eggkxyblnbhadfcapy77q","duration":1813,"clientIP":"::ffff:192.168.65.1","success":true,"platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"4302.529590861319","correlationId":"1760737188839-h5r5y5l11","viewport":"1728x1117","connectionType":"4g","timestamp":"2025-10-17T21:39:52.626Z"} +{"id":"ad71352d","status":"started","user":"xvlad","filename":"fs flip - xv.mov","fileSize":7918853,"clientIP":"::ffff:192.168.65.1","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.","origin":"http://localhost:3000","platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"4302.529590861319","correlationId":"1760737366514-ub8mtyqxw","viewport":"1728x1117","connectionType":"4g","startTime":1760737471009,"timestamp":"2025-10-17T21:44:31.009Z"} +{"id":"ad71352d","status":"completed","user":"xvlad","filename":"fs flip - xv.mov","cid":"bafybeidetq7wbw2j6b5du2wyxhgfw7knv3lrzbtlelvu7t76zmpcyfescm","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeidetq7wbw2j6b5du2wyxhgfw7knv3lrzbtlelvu7t76zmpcyfescm","duration":3344,"clientIP":"::ffff:192.168.65.1","success":true,"platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"4302.529590861319","correlationId":"1760737366514-ub8mtyqxw","viewport":"1728x1117","connectionType":"4g","timestamp":"2025-10-17T21:44:34.353Z"} +{"id":"5c116b6c","status":"started","user":"anonymous","filename":"IMG_0033.MOV","fileSize":16219641,"clientIP":"::ffff:192.168.65.1","userAgent":"curl/8.7.1","origin":"direct","platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":"","userHP":0,"correlationId":null,"viewport":null,"connectionType":null,"startTime":1760737492275,"timestamp":"2025-10-17T21:44:52.275Z"} +{"id":"5c116b6c","status":"failed","user":"anonymous","filename":"IMG_0033.MOV","error":"Cannot read properties of null (reading 'toString')","duration":10411,"clientIP":"::ffff:192.168.65.1","success":false,"platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":null,"userHP":null,"correlationId":null,"viewport":null,"connectionType":null,"timestamp":"2025-10-17T21:45:02.686Z"} +{"id":"e1550ef9","status":"started","user":"test-user","filename":"test-video.mov","fileSize":31375508,"clientIP":"::ffff:192.168.65.1","userAgent":"node","origin":"direct","platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":"","userHP":0,"correlationId":null,"viewport":null,"connectionType":null,"startTime":1760737748855,"timestamp":"2025-10-17T21:49:08.855Z"} +{"id":"e1550ef9","status":"failed","user":"test-user","filename":"test-video.mov","error":"Cannot read properties of null (reading 'toString')","duration":2252,"clientIP":"::ffff:192.168.65.1","success":false,"platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":null,"userHP":null,"correlationId":null,"viewport":null,"connectionType":null,"timestamp":"2025-10-17T21:49:11.107Z"} +{"id":"5ed96ed3","status":"started","user":"anonymous","filename":"IMG_0033.MOV","fileSize":16219641,"clientIP":"::ffff:192.168.65.1","userAgent":"curl/8.7.1","origin":"direct","platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":"","userHP":0,"correlationId":null,"viewport":null,"connectionType":null,"startTime":1760737932523,"timestamp":"2025-10-17T21:52:12.523Z"} +{"id":"5ed96ed3","status":"failed","user":"anonymous","filename":"IMG_0033.MOV","error":"Cannot read properties of null (reading 'toString')","duration":10405,"clientIP":"::ffff:192.168.65.1","success":false,"platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":null,"userHP":null,"correlationId":null,"viewport":null,"connectionType":null,"timestamp":"2025-10-17T21:52:22.928Z"} +{"id":"8a31a4fb","status":"started","user":"mengao","filename":"IMG_0033.MOV","fileSize":16219641,"clientIP":"::ffff:192.168.65.1","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.","origin":"https://www.skatehive.app","platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"26575.769529404988","correlationId":"1760737950166-uympjne5s","viewport":"1440x900","connectionType":"4g","startTime":1760737968573,"timestamp":"2025-10-17T21:52:48.573Z"} +{"id":"8a31a4fb","status":"completed","user":"mengao","filename":"IMG_0033.MOV","cid":"bafybeifat4shpikcmu6oof3nfbgc4drr5haeo6iucq3t23jmnzzha7qude","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeifat4shpikcmu6oof3nfbgc4drr5haeo6iucq3t23jmnzzha7qude","duration":16242,"clientIP":"::ffff:192.168.65.1","success":true,"platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"26575.769529404988","correlationId":"1760737950166-uympjne5s","viewport":"1440x900","connectionType":"4g","timestamp":"2025-10-17T21:53:04.814Z"} +{"id":"3cdf3627","status":"started","user":"steemskate","filename":"IMG_4673.MOV","fileSize":34597538,"clientIP":"::ffff:192.168.65.1","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.","origin":"https://skatehive.app","platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"21932.196324675522","correlationId":"1761142808836-9coryt8fl","viewport":"1728x1117","connectionType":"4g","startTime":1761142838865,"timestamp":"2025-10-22T14:20:38.865Z"} +{"id":"3cdf3627","status":"completed","user":"steemskate","filename":"IMG_4673.MOV","cid":"bafybeieg5sumympodbrgvpiimedfiwrxbfst3aid4hckd52uvfxxeyh5my","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeieg5sumympodbrgvpiimedfiwrxbfst3aid4hckd52uvfxxeyh5my","duration":23658,"clientIP":"::ffff:192.168.65.1","success":true,"platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"21932.196324675522","correlationId":"1761142808836-9coryt8fl","viewport":"1728x1117","connectionType":"4g","timestamp":"2025-10-22T14:21:02.523Z"} +{"id":"3f9f0f0c","status":"started","user":"pharra","filename":"IMG_3958.mov","fileSize":18235799,"clientIP":"::ffff:173.211.12.65","userAgent":"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mo","origin":"https://www.skatehive.app","platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"95.42336639466065","correlationId":"1761357079135-b6te13kpj","viewport":"375x812","connectionType":"unknown","startTime":1761357091655,"timestamp":"2025-10-25T01:51:31.655Z"} +{"id":"3f9f0f0c","status":"completed","user":"pharra","filename":"IMG_3958.mov","cid":"bafybeia2ghmj6vwbhq7uc2v423z6l2eaayiqvlboxznqnfc2fdj7fhotwe","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeia2ghmj6vwbhq7uc2v423z6l2eaayiqvlboxznqnfc2fdj7fhotwe","duration":35928,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"95.42336639466065","correlationId":"1761357079135-b6te13kpj","viewport":"375x812","connectionType":"unknown","timestamp":"2025-10-25T01:52:07.583Z"} +{"id":"7daae1ec","status":"started","user":"anonymous","filename":"Rvamarin-2.mov","fileSize":38995011,"clientIP":"::ffff:173.211.12.65","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Sa","origin":"https://www.skatehive.app","platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.060546695898741065","correlationId":"1761481610260-dfezr4nhn","viewport":"1920x1080","connectionType":"4g","startTime":1761481662280,"timestamp":"2025-10-26T12:27:42.280Z"} +{"id":"7daae1ec","status":"completed","user":"anonymous","filename":"Rvamarin-2.mov","cid":"bafybeidm3nkftfegg5bjgc342zrx6hx2m6ryfvpct6mbfxgx7okentxyzu","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeidm3nkftfegg5bjgc342zrx6hx2m6ryfvpct6mbfxgx7okentxyzu","duration":27208,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.060546695898741065","correlationId":"1761481610260-dfezr4nhn","viewport":"1920x1080","connectionType":"4g","timestamp":"2025-10-26T12:28:09.488Z"} +{"id":"2d65d9ff","status":"started","user":"well13","filename":"IMG_2715.MOV","fileSize":107517287,"clientIP":"::ffff:173.211.12.65","userAgent":"Mozilla/5.0 (Linux; Android 10; SM-G9650 Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like ","origin":"https://www.skatehive.app","platform":"mobile","deviceInfo":"mobile/Linux/Chrome","browserInfo":"Chrome on Linux","userHP":"105.65402191678126","correlationId":"1761703606753-adl7radq1","viewport":"412x846","connectionType":"4g","startTime":1761703659363,"timestamp":"2025-10-29T02:07:39.363Z"} +{"id":"2d65d9ff","status":"completed","user":"well13","filename":"IMG_2715.MOV","cid":"bafybeifcdw3xpqtgtic64qe7yg5kaai7imuuz7o4ze4onmu33okhk3ixv4","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeifcdw3xpqtgtic64qe7yg5kaai7imuuz7o4ze4onmu33okhk3ixv4","duration":32744,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"mobile","deviceInfo":"mobile/Linux/Chrome","browserInfo":"Chrome on Linux","userHP":"105.65402191678126","correlationId":"1761703606753-adl7radq1","viewport":"412x846","connectionType":"4g","timestamp":"2025-10-29T02:08:12.107Z"} +{"id":"0680d6ae","status":"started","user":"anonymous","filename":"IMG_2683.mov","fileSize":45880715,"clientIP":"::ffff:173.211.12.65","userAgent":"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mo","origin":"https://www.skatehive.app","platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"0.06056589145012988","correlationId":"1761778087161-pu659drrs","viewport":"375x812","connectionType":"unknown","startTime":1761778115716,"timestamp":"2025-10-29T22:48:35.716Z"} +{"id":"0680d6ae","status":"completed","user":"anonymous","filename":"IMG_2683.mov","cid":"bafybeibveg4ldg2riyhbzcqgzcr3nsouppsgiijouhv56vyez3r6gxh27q","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeibveg4ldg2riyhbzcqgzcr3nsouppsgiijouhv56vyez3r6gxh27q","duration":24592,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"0.06056589145012988","correlationId":"1761778087161-pu659drrs","viewport":"375x812","connectionType":"unknown","timestamp":"2025-10-29T22:49:00.306Z"} +{"id":"9da81823","status":"started","user":"blessskateshop","filename":"IMG_6716.mov","fileSize":35532380,"clientIP":"::ffff:173.211.12.65","userAgent":"Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) V","origin":"https://skatehive.app","platform":"mobile","deviceInfo":"mobile/iOS/Safari","browserInfo":"Safari on iOS","userHP":"20921.708262321594","correlationId":"1762141073622-ajraftx71","viewport":"375x812","connectionType":"unknown","startTime":1762141145881,"timestamp":"2025-11-03T03:39:05.881Z"} +{"id":"9da81823","status":"completed","user":"blessskateshop","filename":"IMG_6716.mov","cid":"bafybeieksboewkz2xasqjmrztehc7rriyi4ou6tezoilndb7xfsphaoi4i","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeieksboewkz2xasqjmrztehc7rriyi4ou6tezoilndb7xfsphaoi4i","duration":14215,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"mobile","deviceInfo":"mobile/iOS/Safari","browserInfo":"Safari on iOS","userHP":"20921.708262321594","correlationId":"1762141073622-ajraftx71","viewport":"375x812","connectionType":"unknown","timestamp":"2025-11-03T03:39:20.095Z"} +{"id":"1d64a0a7","status":"started","user":"garciarodrigues","filename":"video-output-F64FB888-298F-492A-BD0E-95F36E0B9138-3.mov","fileSize":91718316,"clientIP":"::ffff:173.211.12.65","userAgent":"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mo","origin":"https://skatehive.app","platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"141.10132538789063","correlationId":"1762212896892-1m7kd1tzh","viewport":"375x812","connectionType":"unknown","startTime":1762212989122,"timestamp":"2025-11-03T23:36:29.122Z"} +{"id":"1d64a0a7","status":"completed","user":"garciarodrigues","filename":"video-output-F64FB888-298F-492A-BD0E-95F36E0B9138-3.mov","cid":"bafybeibu2eoxr3kzpcg5kebzgtszcc5rssa7365ol2ccy4yfubfg62v55y","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeibu2eoxr3kzpcg5kebzgtszcc5rssa7365ol2ccy4yfubfg62v55y","duration":33344,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"141.10132538789063","correlationId":"1762212896892-1m7kd1tzh","viewport":"375x812","connectionType":"unknown","timestamp":"2025-11-03T23:37:02.465Z"} +{"id":"ffdcb0a1","status":"started","user":"xvlad","filename":"IMG_4673.MOV","fileSize":34597538,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.","origin":"https://www.skatehive.app","platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"4478.35757589758","correlationId":"1762445332696-4yjes4nba","viewport":"1920x1080","connectionType":"unknown","startTime":1762445346908,"timestamp":"2025-11-06T16:09:06.908Z"} +{"id":"ffdcb0a1","status":"completed","user":"xvlad","filename":"IMG_4673.MOV","cid":"bafybeieg5sumympodbrgvpiimedfiwrxbfst3aid4hckd52uvfxxeyh5my","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeieg5sumympodbrgvpiimedfiwrxbfst3aid4hckd52uvfxxeyh5my","duration":25989,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"4478.35757589758","correlationId":"1762445332696-4yjes4nba","viewport":"1920x1080","connectionType":"unknown","timestamp":"2025-11-06T16:09:32.897Z"} +{"id":"a9f1188f","status":"started","user":"anonymous","filename":"IMG_4171.mov","fileSize":97464811,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mo","origin":"https://www.skatehive.app","platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"0.06073444942345621","correlationId":"1764553022410-89lysgeth","viewport":"375x812","connectionType":"unknown","startTime":1764553084526,"timestamp":"2025-12-01T01:38:04.526Z"} +{"id":"a9f1188f","status":"completed","user":"anonymous","filename":"IMG_4171.mov","cid":"bafybeicfwelvu3xrsll6flsmhjzsyfr4semc6mrhrkpykh74e5s2hybatm","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeicfwelvu3xrsll6flsmhjzsyfr4semc6mrhrkpykh74e5s2hybatm","duration":29003,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"0.06073444942345621","correlationId":"1764553022410-89lysgeth","viewport":"375x812","connectionType":"unknown","timestamp":"2025-12-01T01:38:33.530Z"} +{"id":"f58f985f","status":"started","user":"anonymous","filename":"IMG_4102.mov","fileSize":62823297,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mo","origin":"https://www.skatehive.app","platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"0.0607344571241931","correlationId":"1764553311696-wv4gzycsy","viewport":"375x812","connectionType":"unknown","startTime":1764553349006,"timestamp":"2025-12-01T01:42:29.006Z"} +{"id":"f58f985f","status":"completed","user":"anonymous","filename":"IMG_4102.mov","cid":"bafybeifkaojk4isfscxj3n55lzf56vrklmfd6eo75iu465sjhnlqpj3djy","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeifkaojk4isfscxj3n55lzf56vrklmfd6eo75iu465sjhnlqpj3djy","duration":23242,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"0.0607344571241931","correlationId":"1764553311696-wv4gzycsy","viewport":"375x812","connectionType":"unknown","timestamp":"2025-12-01T01:42:52.247Z"} +{"id":"db875a85","status":"started","user":"anonymous","filename":"IMG_4844.MOV","fileSize":36266719,"clientIP":"::ffff:173.211.12.65","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Sa","origin":"https://skatehive.app","platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.06074559223302733","correlationId":"1764775696687-h3vl0dbzh","viewport":"1680x1050","connectionType":"unknown","startTime":1764775751238,"timestamp":"2025-12-03T15:29:11.238Z"} +{"id":"db875a85","status":"completed","user":"anonymous","filename":"IMG_4844.MOV","cid":"bafybeietiob3eivmp6znzljriehgfcgdbm6a5ob67imysjg4f5yrfk4fpa","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeietiob3eivmp6znzljriehgfcgdbm6a5ob67imysjg4f5yrfk4fpa","duration":13061,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.06074559223302733","correlationId":"1764775696687-h3vl0dbzh","viewport":"1680x1050","connectionType":"unknown","timestamp":"2025-12-03T15:29:24.299Z"} +{"id":"c8af1d60","status":"started","user":"anonymous","filename":"IMG_4838.MOV","fileSize":126900312,"clientIP":"::ffff:173.211.12.65","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Sa","origin":"https://skatehive.app","platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.06074559223302733","correlationId":"1764776412547-6lldqhm1v","viewport":"1680x1050","connectionType":"unknown","startTime":1764776479184,"timestamp":"2025-12-03T15:41:19.184Z"} +{"id":"c8af1d60","status":"completed","user":"anonymous","filename":"IMG_4838.MOV","cid":"bafybeih2nmn64cdoc367xwvpeckpc2zjitxr3afna3b24r4clak7oohl3i","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeih2nmn64cdoc367xwvpeckpc2zjitxr3afna3b24r4clak7oohl3i","duration":43802,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.06074559223302733","correlationId":"1764776412547-6lldqhm1v","viewport":"1680x1050","connectionType":"unknown","timestamp":"2025-12-03T15:42:02.986Z"} +{"id":"64d39fcc","status":"started","user":"anonymous","filename":"IMG_4838.MOV","fileSize":126900312,"clientIP":"::ffff:173.211.12.65","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Sa","origin":"https://skatehive.app","platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.06074564229688926","correlationId":"1764776657682-urf0m6l2g","viewport":"1680x1050","connectionType":"unknown","startTime":1764776717439,"timestamp":"2025-12-03T15:45:17.439Z"} +{"id":"6d57fef8","status":"started","user":"anonymous","filename":"IMG_4838.MOV","fileSize":126900312,"clientIP":"::ffff:173.211.12.65","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Sa","origin":"https://skatehive.app","platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.06074564229688926","correlationId":"1764777003690-coa15fix8","viewport":"1680x1050","connectionType":"unknown","startTime":1764777074975,"timestamp":"2025-12-03T15:51:14.975Z"} +{"id":"6d57fef8","status":"completed","user":"anonymous","filename":"IMG_4838.MOV","cid":"bafybeih2nmn64cdoc367xwvpeckpc2zjitxr3afna3b24r4clak7oohl3i","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeih2nmn64cdoc367xwvpeckpc2zjitxr3afna3b24r4clak7oohl3i","duration":45282,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.06074564229688926","correlationId":"1764777003690-coa15fix8","viewport":"1680x1050","connectionType":"unknown","timestamp":"2025-12-03T15:52:00.257Z"} +{"id":"64d39fcc","status":"completed","user":"anonymous","filename":"IMG_4838.MOV","cid":"bafybeih2nmn64cdoc367xwvpeckpc2zjitxr3afna3b24r4clak7oohl3i","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeih2nmn64cdoc367xwvpeckpc2zjitxr3afna3b24r4clak7oohl3i","duration":911101,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.06074564229688926","correlationId":"1764776657682-urf0m6l2g","viewport":"1680x1050","connectionType":"unknown","timestamp":"2025-12-03T16:00:28.541Z"} +{"id":"ab077372","status":"started","user":"anonymous","filename":"IMG_4810.MOV","fileSize":99783692,"clientIP":"::ffff:173.211.12.65","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Sa","origin":"https://skatehive.app","platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.060745756125989286","correlationId":"1764779133683-c2qgvez9x","viewport":"1680x1050","connectionType":"unknown","startTime":1764779186306,"timestamp":"2025-12-03T16:26:26.306Z"} +{"id":"ab077372","status":"completed","user":"anonymous","filename":"IMG_4810.MOV","cid":"bafybeichbms3nq44qwlk5e7cq2nheh6wblsbrrowkc3g7vmmn3od3czfce","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeichbms3nq44qwlk5e7cq2nheh6wblsbrrowkc3g7vmmn3od3czfce","duration":31923,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.060745756125989286","correlationId":"1764779133683-c2qgvez9x","viewport":"1680x1050","connectionType":"unknown","timestamp":"2025-12-03T16:26:58.228Z"} +{"id":"54b0d196","status":"started","user":"anonymous","filename":"IMG_5625.mov","fileSize":21284215,"clientIP":"::ffff:173.211.12.65","userAgent":"Mozilla/5.0 (iPhone; CPU iPhone OS 18_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mo","origin":"https://www.skatehive.app","platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"0.06074681207879834","correlationId":"1764800871028-shxbaai5v","viewport":"390x844","connectionType":"unknown","startTime":1764800895478,"timestamp":"2025-12-03T22:28:15.478Z"} +{"id":"54b0d196","status":"completed","user":"anonymous","filename":"IMG_5625.mov","cid":"bafybeib3ngxxcojp33fwftxkq744wakjbzkwjypoaik7gygcd7spcykb2e","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeib3ngxxcojp33fwftxkq744wakjbzkwjypoaik7gygcd7spcykb2e","duration":4030,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"0.06074681207879834","correlationId":"1764800871028-shxbaai5v","viewport":"390x844","connectionType":"unknown","timestamp":"2025-12-03T22:28:19.508Z"} +{"id":"82afa1c7","status":"started","user":"anonymous","filename":"IMG_4777.MOV","fileSize":104545868,"clientIP":"::ffff:173.211.12.65","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Sa","origin":"https://skatehive.app","platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.060750796860943534","correlationId":"1764879638924-6aaoro4ap","viewport":"1680x1050","connectionType":"unknown","startTime":1764879712395,"timestamp":"2025-12-04T20:21:52.396Z"} +{"id":"82afa1c7","status":"completed","user":"anonymous","filename":"IMG_4777.MOV","cid":"bafybeic5e3k52oru25x7v47ngp77qz3s274wnhowet3mxalc7e2f52gpky","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeic5e3k52oru25x7v47ngp77qz3s274wnhowet3mxalc7e2f52gpky","duration":35632,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.060750796860943534","correlationId":"1764879638924-6aaoro4ap","viewport":"1680x1050","connectionType":"unknown","timestamp":"2025-12-04T20:22:28.027Z"} +{"id":"91cafd1b","status":"started","user":"blessskateshop","filename":"IMG_3630.mov","fileSize":41858888,"clientIP":"::ffff:173.211.12.65","userAgent":"Mozilla/5.0 (iPhone; CPU iPhone OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) M","origin":"https://skatehive.app","platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"21298.91484325076","correlationId":"1765150189807-zpf1nuvgg","viewport":"375x812","connectionType":"unknown","startTime":1765150225708,"timestamp":"2025-12-07T23:30:25.708Z"} +{"id":"91cafd1b","status":"completed","user":"blessskateshop","filename":"IMG_3630.mov","cid":"bafybeibknxfuflra7kotr75ecwglva7djfoj3dd4bmigcuppeianw354t4","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeibknxfuflra7kotr75ecwglva7djfoj3dd4bmigcuppeianw354t4","duration":19305,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"21298.91484325076","correlationId":"1765150189807-zpf1nuvgg","viewport":"375x812","connectionType":"unknown","timestamp":"2025-12-07T23:30:45.014Z"} +{"id":"15cfac69","status":"started","user":"garciarodrigues","filename":"copy_63B4678F-6563-409E-948F-06DC8AC586E0.mov","fileSize":38982278,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mo","origin":"https://www.skatehive.app","platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"141.5085923661999","correlationId":"1765237827408-sxi0jmocs","viewport":"375x812","connectionType":"unknown","startTime":1765237869074,"timestamp":"2025-12-08T23:51:09.074Z"} +{"id":"15cfac69","status":"completed","user":"garciarodrigues","filename":"copy_63B4678F-6563-409E-948F-06DC8AC586E0.mov","cid":"bafybeieysxlc2ew3dx75jdel5wsmdntfc6s6rr5yqcc3ciyzcfgluwvuse","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeieysxlc2ew3dx75jdel5wsmdntfc6s6rr5yqcc3ciyzcfgluwvuse","duration":9372,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"141.5085923661999","correlationId":"1765237827408-sxi0jmocs","viewport":"375x812","connectionType":"unknown","timestamp":"2025-12-08T23:51:18.445Z"} +{"id":"a40770b0","status":"started","user":"garciarodrigues","filename":"copy_63B4678F-6563-409E-948F-06DC8AC586E0.mov","fileSize":38982278,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (iPhone; CPU iPhone OS 18_6_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mo","origin":"https://www.skatehive.app","platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"141.50869132966633","correlationId":"1765238731174-pxm5ks96b","viewport":"375x812","connectionType":"unknown","startTime":1765238745083,"timestamp":"2025-12-09T00:05:45.083Z"} +{"id":"a40770b0","status":"completed","user":"garciarodrigues","filename":"copy_63B4678F-6563-409E-948F-06DC8AC586E0.mov","cid":"bafybeieysxlc2ew3dx75jdel5wsmdntfc6s6rr5yqcc3ciyzcfgluwvuse","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeieysxlc2ew3dx75jdel5wsmdntfc6s6rr5yqcc3ciyzcfgluwvuse","duration":8726,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"mobile","deviceInfo":"mobile/iOS/unknown","browserInfo":"unknown on iOS","userHP":"141.50869132966633","correlationId":"1765238731174-pxm5ks96b","viewport":"375x812","connectionType":"unknown","timestamp":"2025-12-09T00:05:53.809Z"} +{"id":"cd0ba255","status":"started","user":"humbertoperes","filename":"trimmed_maua-lines.m4v","fileSize":2882727,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.","origin":"https://www.skatehive.app","platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"114.05652113811881","correlationId":"1765299770877-04a2exgxs","viewport":"1680x1050","connectionType":"unknown","startTime":1765299782964,"timestamp":"2025-12-09T17:03:02.964Z"} +{"id":"cd0ba255","status":"completed","user":"humbertoperes","filename":"trimmed_maua-lines.m4v","cid":"bafybeibkkzk54ofymmkdvhj4rdg2fvxdljybqwfdvb4lp3tbppiu4wnxom","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeibkkzk54ofymmkdvhj4rdg2fvxdljybqwfdvb4lp3tbppiu4wnxom","duration":12712,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"desktop","deviceInfo":"desktop/macOS/Chrome","browserInfo":"Chrome on macOS","userHP":"114.05652113811881","correlationId":"1765299770877-04a2exgxs","viewport":"1680x1050","connectionType":"unknown","timestamp":"2025-12-09T17:03:15.677Z"} +{"id":"1bd48e82","status":"started","user":"bolinhosk8","filename":"IMG_7183.mov","fileSize":35982936,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (iPad; CPU OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148","origin":"https://www.skatehive.app","platform":"tablet","deviceInfo":"tablet/iOS/unknown","browserInfo":"unknown on iOS","userHP":"5.101223056362249","correlationId":"1765379824994-xlkn05eg6","viewport":"375x667","connectionType":"unknown","startTime":1765379846671,"timestamp":"2025-12-10T15:17:26.671Z"} +{"id":"1bd48e82","status":"completed","user":"bolinhosk8","filename":"IMG_7183.mov","cid":"bafybeibmp5wpr3dmyapwyimvtvr6lm2hqrbun7x7g5sdqv2o3mwf25pp4i","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeibmp5wpr3dmyapwyimvtvr6lm2hqrbun7x7g5sdqv2o3mwf25pp4i","duration":16735,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"tablet","deviceInfo":"tablet/iOS/unknown","browserInfo":"unknown on iOS","userHP":"5.101223056362249","correlationId":"1765379824994-xlkn05eg6","viewport":"375x667","connectionType":"unknown","timestamp":"2025-12-10T15:17:43.405Z"} +{"id":"47cba17f","status":"started","user":"anonymous","filename":"IMG_4678.MOV","fileSize":35871451,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Sa","origin":"https://skatehive.app","platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.06077581565124358","correlationId":"1765390881131-tcztzkitf","viewport":"1680x1050","connectionType":"unknown","startTime":1765390899854,"timestamp":"2025-12-10T18:21:39.854Z"} +{"id":"47cba17f","status":"completed","user":"anonymous","filename":"IMG_4678.MOV","cid":"bafybeicpj5yfufhrcx5jxkh4cssotkvr7dmjkztqdmwmdfkociy6iuipeu","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeicpj5yfufhrcx5jxkh4cssotkvr7dmjkztqdmwmdfkociy6iuipeu","duration":18992,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.06077581565124358","correlationId":"1765390881131-tcztzkitf","viewport":"1680x1050","connectionType":"unknown","timestamp":"2025-12-10T18:21:58.846Z"} +{"id":"7c6f43e5","status":"started","user":"anonymous","filename":"IMG_5304.MOV","fileSize":45399100,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Sa","origin":"https://skatehive.app","platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.06077581565124358","correlationId":"1765391862126-t304kr8ra","viewport":"1680x1050","connectionType":"unknown","startTime":1765391916995,"timestamp":"2025-12-10T18:38:36.995Z"} +{"id":"7c6f43e5","status":"completed","user":"anonymous","filename":"IMG_5304.MOV","cid":"bafybeif33trrgz2rtpt57hcw7t2pizpztczge3ajuig2z4n7teyl5tmo4q","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeif33trrgz2rtpt57hcw7t2pizpztczge3ajuig2z4n7teyl5tmo4q","duration":14755,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.06077581565124358","correlationId":"1765391862126-t304kr8ra","viewport":"1680x1050","connectionType":"unknown","timestamp":"2025-12-10T18:38:51.750Z"} +{"id":"b3d7dd87","status":"started","user":"anonymous","filename":"IMG_5276.MOV","fileSize":97031057,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Sa","origin":"https://skatehive.app","platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.06077667150439869","correlationId":"1765397893132-3vgi1031s","viewport":"1680x1050","connectionType":"unknown","startTime":1765397945021,"timestamp":"2025-12-10T20:19:05.021Z"} +{"id":"b3d7dd87","status":"completed","user":"anonymous","filename":"IMG_5276.MOV","cid":"bafybeib2yzu4e2hqietzbosqzlqnpeqc4eaz7rrauttdjheiskmobxlwem","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeib2yzu4e2hqietzbosqzlqnpeqc4eaz7rrauttdjheiskmobxlwem","duration":40888,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.06077667150439869","correlationId":"1765397893132-3vgi1031s","viewport":"1680x1050","connectionType":"unknown","timestamp":"2025-12-10T20:19:45.909Z"} +{"id":"bd1c989c","status":"started","user":"anonymous","filename":"IMG_4441.MOV","fileSize":36881760,"clientIP":"::ffff:172.20.0.1","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Sa","origin":"https://skatehive.app","platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.0607809313492168","correlationId":"1765478956523-pu33jcxga","viewport":"1680x1050","connectionType":"unknown","startTime":1765478992031,"timestamp":"2025-12-11T18:49:52.031Z"} +{"id":"bd1c989c","status":"completed","user":"anonymous","filename":"IMG_4441.MOV","cid":"bafybeiazgogxeloogghmckxvvw4kwj3typgzwjrsdfueveoyhi2r6rvzvq","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeiazgogxeloogghmckxvvw4kwj3typgzwjrsdfueveoyhi2r6rvzvq","duration":27212,"clientIP":"::ffff:172.20.0.1","success":true,"platform":"desktop","deviceInfo":"desktop/Windows/Chrome","browserInfo":"Chrome on Windows","userHP":"0.0607809313492168","correlationId":"1765478956523-pu33jcxga","viewport":"1680x1050","connectionType":"unknown","timestamp":"2025-12-11T18:50:19.242Z"} +{"id":"973390ca","status":"started","user":"test-user","filename":"test-video.mov","fileSize":31375508,"clientIP":"::ffff:172.20.0.1","userAgent":"curl/8.7.1","origin":"direct","platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":"","userHP":0,"correlationId":null,"viewport":null,"connectionType":null,"startTime":1765564642682,"timestamp":"2025-12-12T18:37:22.682Z"} +{"id":"973390ca","status":"failed","user":"test-user","filename":"test-video.mov","error":"Cannot read properties of null (reading 'toString')","duration":6664,"clientIP":"::ffff:172.20.0.1","success":false,"platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":null,"userHP":null,"correlationId":null,"viewport":null,"connectionType":null,"timestamp":"2025-12-12T18:37:29.346Z"} +{"id":"0405a9b0","status":"started","user":"test-user","filename":"test-video.mov","fileSize":31375508,"clientIP":"::ffff:173.211.12.65","userAgent":"curl/8.7.1","origin":"direct","platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":"","userHP":0,"correlationId":null,"viewport":null,"connectionType":null,"startTime":1765565178936,"timestamp":"2025-12-12T18:46:18.936Z"} +{"id":"0405a9b0","status":"completed","user":"test-user","filename":"test-video.mov","cid":"bafybeihw6h6qwy3kit5vsmr5yqwvynrcm2cj6inehbnan7k4h73fyl7udi","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeihw6h6qwy3kit5vsmr5yqwvynrcm2cj6inehbnan7k4h73fyl7udi","duration":7375,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":null,"userHP":null,"correlationId":null,"viewport":null,"connectionType":null,"timestamp":"2025-12-12T18:46:26.312Z"} +{"id":"b1de2a5e","status":"started","user":"test_user","filename":"test.mp4","fileSize":788493,"clientIP":"::ffff:173.211.12.65","userAgent":"curl/8.7.1","origin":"direct","platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":"","userHP":0,"correlationId":null,"viewport":null,"connectionType":null,"startTime":1765566949402,"timestamp":"2025-12-12T19:15:49.402Z"} +{"id":"b1de2a5e","status":"completed","user":"test_user","filename":"test.mp4","cid":"bafybeig2dqadr3wmbfj6iwli4oginc46brmu3mb6gpxi5a2rnbaqcxizqy","gatewayUrl":"https://gateway.pinata.cloud/ipfs/bafybeig2dqadr3wmbfj6iwli4oginc46brmu3mb6gpxi5a2rnbaqcxizqy","duration":1499,"clientIP":"::ffff:173.211.12.65","success":true,"platform":"unknown","deviceInfo":"desktop/unknown/unknown","browserInfo":null,"userHP":null,"correlationId":null,"viewport":null,"connectionType":null,"timestamp":"2025-12-12T19:15:50.901Z"} diff --git a/package-lock.json b/package-lock.json index f339917..81307a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,13 @@ "version": "1.0.0", "dependencies": { "axios": "^1.7.0", + "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", "form-data": "^4.0.0", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", + "cors": "^2.8.5", "uuid": "^9.0.1" } }, @@ -201,11 +203,36 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-K3t1JpNggCB2TF+FiYqS+8CA0nVddOZXS6jttuPAoBs+K6TfGsZ3jAC5vlaQt1zAr72Xd1LSeX776BF3/f6/DRw==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", diff --git a/package.json b/package.json index bbd71ba..98b9d29 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,13 @@ "main": "src/server.js", "scripts": { "start": "node src/server.js", - "dev": "NODE_ENV=development node src/server.js" + "dev": "NODE_ENV=development node src/server.js", + "test-logs": "node test-logs.js", + "test-rich-logs": "node test-rich-logs.js" }, "dependencies": { "axios": "^1.7.0", + "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", "form-data": "^4.0.0", diff --git a/src/logger.js b/src/logger.js new file mode 100644 index 0000000..0759cb1 --- /dev/null +++ b/src/logger.js @@ -0,0 +1,239 @@ +import fs from 'fs'; +import path from 'path'; +import os from 'os'; + +class TranscodeLogger { + constructor(logFilePath = null, maxLogs = 100) { + // Use NODE_NAME env var for unique log files, fallback to hostname + const nodeName = process.env.NODE_NAME || os.hostname().split('.')[0]; + this.nodeName = nodeName; + this.logFilePath = logFilePath || `logs/transcode-${nodeName}.log`; + this.maxLogs = maxLogs; + this.logs = []; + this.loadLogs(); + console.log(`📝 Logger initialized for node: ${nodeName}`); + } + + loadLogs() { + try { + // Ensure log directory exists + const logDir = path.dirname(this.logFilePath); + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + + if (fs.existsSync(this.logFilePath)) { + const data = fs.readFileSync(this.logFilePath, 'utf8'); + this.logs = data.trim().split('\n') + .filter(line => line.trim()) + .map(line => JSON.parse(line)) + .slice(-this.maxLogs); // Keep only the last maxLogs entries + } + } catch (error) { + console.warn('⚠️ Could not load existing logs:', error.message); + this.logs = []; + } + } + + saveLogs() { + try { + // Keep only the last maxLogs entries + this.logs = this.logs.slice(-this.maxLogs); + + const logData = this.logs.map(log => JSON.stringify(log)).join('\n') + '\n'; + fs.writeFileSync(this.logFilePath, logData); + } catch (error) { + console.error('❌ Could not save logs:', error.message); + } + } + + addLog(logEntry) { + const enrichedLog = { + ...logEntry, + timestamp: new Date().toISOString(), + id: logEntry.id || 'unknown', + node: this.nodeName + }; + + this.logs.push(enrichedLog); + this.saveLogs(); + + // Also log to console with rich formatting + this.consoleLog(enrichedLog); + } + + consoleLog(log) { + const emoji = this.getStatusEmoji(log.status); + const duration = log.duration ? `${log.duration}ms` : 'N/A'; + + console.log(`${emoji} [${log.timestamp}] [${log.node || 'unknown'}] ${log.status.toUpperCase()}`); + console.log(` 🆔 ID: ${log.id}`); + console.log(` 👤 User: ${log.user || 'anonymous'}${log.userHP ? ` (HP: ${log.userHP})` : ''}`); + console.log(` 📁 File: ${log.filename || 'unknown'} (${log.fileSize || 0} bytes)`); + console.log(` 📍 IP: ${log.clientIP || 'unknown'}`); + console.log(` 🖥️ Device: ${log.deviceInfo || 'unknown'}`); + console.log(` 🌐 Platform: ${log.platform || 'unknown'}`); + console.log(` ⏱️ Duration: ${duration}`); + + if (log.correlationId) { + console.log(` � Correlation: ${log.correlationId}`); + } + + if (log.viewport) { + console.log(` 📐 Viewport: ${log.viewport}`); + } + + if (log.connectionType) { + console.log(` 📶 Connection: ${log.connectionType}`); + } + + if (log.cid) { + console.log(` 📦 CID: ${log.cid}`); + } + + if (log.error) { + console.log(` ❌ Error: ${log.error}`); + } + + if (log.gatewayUrl) { + console.log(` 🌐 URL: ${log.gatewayUrl}`); + } + + console.log(''); // Empty line for readability + } + + getStatusEmoji(status) { + const emojis = { + 'started': '🚀', + 'processing': '⚙️', + 'uploading': '☁️', + 'completed': '✅', + 'failed': '❌', + 'error': '💥' + }; + return emojis[status] || '📝'; + } + + logTranscodeStart({ id, user, filename, fileSize, clientIP, userAgent, origin, platform, deviceInfo, browserInfo, userHP, correlationId, viewport, connectionType }) { + this.addLog({ + id, + status: 'started', + user: user || 'anonymous', + filename, + fileSize, + clientIP, + userAgent: userAgent?.substring(0, 100), + origin, + platform: platform || 'unknown', + deviceInfo: deviceInfo || 'unknown', + browserInfo: browserInfo || '', + userHP: userHP || 0, + correlationId: correlationId || null, + viewport: viewport || null, + connectionType: connectionType || null, + startTime: Date.now() + }); + } + + logTranscodeComplete({ id, user, filename, cid, gatewayUrl, duration, clientIP }) { + // Find the original start log to preserve device context + const startLog = this.logs.find(log => log.id === id && log.status === 'started'); + + this.addLog({ + id, + status: 'completed', + user: user || 'anonymous', + filename, + cid, + gatewayUrl, + duration, + clientIP, + success: true, + // Preserve device info from start log + platform: startLog?.platform || null, + deviceInfo: startLog?.deviceInfo || null, + browserInfo: startLog?.browserInfo || null, + userHP: startLog?.userHP || null, + correlationId: startLog?.correlationId || null, + viewport: startLog?.viewport || null, + connectionType: startLog?.connectionType || null + }); + } + + logTranscodeError({ id, user, filename, error, duration, clientIP }) { + // Find the original start log to preserve device context + const startLog = this.logs.find(log => log.id === id && log.status === 'started'); + + this.addLog({ + id, + status: 'failed', + user: user || 'anonymous', + filename, + error: error?.message || error || 'Unknown error', + duration, + clientIP, + success: false, + // Preserve device info from start log + platform: startLog?.platform || null, + deviceInfo: startLog?.deviceInfo || null, + browserInfo: startLog?.browserInfo || null, + userHP: startLog?.userHP || null, + correlationId: startLog?.correlationId || null, + viewport: startLog?.viewport || null, + connectionType: startLog?.connectionType || null + }); + } + + logFFmpegProgress({ id, progress, timeElapsed }) { + // Don't save progress logs to file (too noisy), just console log + console.log(`⏳ [FFMPEG-PROGRESS] ID: ${id} | Progress: ${progress} | Elapsed: ${timeElapsed}`); + } + + getRecentLogs(limit = 10) { + return this.logs.slice(-limit).reverse(); // Most recent first + } + + getLogsForDashboard(limit = 5) { + return this.logs.slice(-limit).reverse().map(log => ({ + id: log.id, + timestamp: log.timestamp, + user: log.user, + filename: log.filename, + status: log.status, + duration: log.duration, + error: log.error, + cid: log.cid, + fileSize: log.fileSize, + clientIP: log.clientIP, + platform: log.platform, + deviceInfo: log.deviceInfo, + userHP: log.userHP, + correlationId: log.correlationId, + viewport: log.viewport, + connectionType: log.connectionType, + node: log.node + })); + } + + getStats() { + const total = this.logs.length; + const successful = this.logs.filter(log => log.success === true).length; + const failed = this.logs.filter(log => log.success === false).length; + const inProgress = this.logs.filter(log => log.status === 'started' || log.status === 'processing').length; + + const avgDuration = this.logs + .filter(log => log.duration && log.success === true) + .reduce((sum, log, _, arr) => sum + log.duration / arr.length, 0); + + return { + total, + successful, + failed, + inProgress, + avgDuration: Math.round(avgDuration), + successRate: total > 0 ? Math.round((successful / total) * 100) : 0 + }; + } +} + +export default TranscodeLogger; diff --git a/src/server.js b/src/server.js index ca57707..7d490fd 100644 --- a/src/server.js +++ b/src/server.js @@ -9,32 +9,212 @@ import morgan from 'morgan'; import axios from 'axios'; import FormData from 'form-data'; import { v4 as uuidv4 } from 'uuid'; +import TranscodeLogger from './logger.js'; const app = express(); +const logger = new TranscodeLogger(); -// Open CORS (no credentials). Put this BEFORE routes. +/** + * Check if video is web-optimized (H.264/AAC, faststart, reasonable size) + * Returns { optimized: boolean, reason?: string, videoInfo?: object } + */ +async function checkWebOptimized(inputPath) { + return new Promise((resolve) => { + const proc = spawn('ffprobe', [ + '-v', 'quiet', + '-print_format', 'json', + '-show_format', + '-show_streams', + inputPath + ]); + + let stdout = ''; + proc.stdout.on('data', (d) => { stdout += d.toString(); }); + proc.on('close', (code) => { + if (code !== 0) { + return resolve({ optimized: false, reason: 'ffprobe failed' }); + } + + try { + const info = JSON.parse(stdout); + const videoStream = info.streams.find(s => s.codec_type === 'video'); + const audioStream = info.streams.find(s => s.codec_type === 'audio'); + + if (!videoStream || videoStream.codec_name !== 'h264') { + return resolve({ optimized: false, reason: `video codec: ${videoStream?.codec_name || 'none'}` }); + } + + if (audioStream && audioStream.codec_name !== 'aac') { + return resolve({ optimized: false, reason: `audio codec: ${audioStream.codec_name}` }); + } + + const height = parseInt(videoStream.height); + if (height > 1080) { + return resolve({ optimized: false, reason: `resolution too high: ${height}p` }); + } + + resolve({ + optimized: true, + videoInfo: { + codec: videoStream.codec_name, + resolution: `${videoStream.width}x${videoStream.height}`, + duration: parseFloat(info.format.duration), + bitrate: parseInt(info.format.bit_rate) + } + }); + } catch (err) { + resolve({ optimized: false, reason: 'parse error' }); + } + }); + }); +} + +// Store active transcoding progress for SSE clients +const activeJobs = new Map(); // requestId -> { progress, stage, clients: Set } + +// Enhanced CORS setup for web application compatibility +// --- CORS configuration --- +// Allow requests from any origin + +// Additional CORS headers for maximum compatibility app.use((req, res, next) => { - res.setHeader('Access-Control-Allow-Origin', '*'); // allow any origin - res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - if (req.method === 'OPTIONS') return res.sendStatus(204); // preflight OK + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS,PUT,DELETE'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization,Accept,X-Requested-With'); + res.setHeader('Access-Control-Max-Age', '86400'); // 24 hours + if (req.method === 'OPTIONS') { + return res.sendStatus(204); + } next(); }); -// Keep Render health probes happy (avoid 404 on / and HEAD /) -app.get('/', (_req, res) => res.send('OK')); -app.head('/', (_req, res) => res.sendStatus(200)); +// Enhanced logging middleware for debugging +app.use((req, res, next) => { + const startTime = Date.now(); + const clientIP = req.ip || req.connection.remoteAddress || req.headers['x-forwarded-for'] || 'unknown'; + const userAgent = req.get('User-Agent') || 'unknown'; + const origin = req.get('Origin') || req.get('Referer') || 'direct'; + + console.log(`🌐 [${new Date().toISOString()}] ${req.method} ${req.path} - Client: ${clientIP} - Origin: ${origin}`); + + // Log request details for transcode operations + if (req.path === '/transcode') { + console.log(`📊 TRANSCODE REQUEST START:`); + console.log(` 📍 Client IP: ${clientIP}`); + console.log(` 🌍 Origin: ${origin}`); + console.log(` 🖥️ User Agent: ${userAgent.substring(0, 100)}`); + console.log(` ⏰ Start Time: ${new Date().toISOString()}`); + } + + // Track response time + res.on('finish', () => { + const duration = Date.now() - startTime; + if (req.path === '/transcode') { + console.log(`✅ TRANSCODE REQUEST COMPLETE - ${res.statusCode} - ${duration}ms`); + } + }); + + next(); +}); const PORT = process.env.PORT || 8080; const PINATA_JWT = process.env.PINATA_JWT; -const PINATA_GATEWAY = process.env.PINATA_GATEWAY || 'https://gateway.pinata.cloud/ipfs'; // optional +const PINATA_GATEWAY = process.env.PINATA_GATEWAY || 'https://ipfs.skatehive.app/ipfs'; +const PINATA_GROUP_VIDEOS = process.env.PINATA_GROUP_VIDEOS || null; if (!PINATA_JWT) { console.warn('⚠️ PINATA_JWT is not set. Set it in your environment before starting.'); } +// Morgan logging for HTTP requests app.use(morgan('combined')); -app.get('/healthz', (_req, res) => res.json({ ok: true })); + +// Helper to detect browser requests +const wantsHtml = (req) => String(req.headers.accept || '').includes('text/html'); + +// Health check with HTML support for browsers +const sendHealth = (req, res, payload, title) => { + if (wantsHtml(req)) { + const html = [ + '', + '', + ` ${title}`, + ' ', + `

🎬 ${title}

`, + `

Status: ${payload.ok ? '✅ Healthy' : '❌ Error'}

`, + `

Service: ${payload.service || 'video-worker'}

`, + `

Timestamp: ${payload.timestamp}

`, + ' ', + '' + ].join('\n'); + res.type('html').send(html); + return; + } + res.json(payload); +}; + +app.get('/', (_req, res) => res.send('🎬 Video Worker - Ready for transcoding!')); +app.head('/', (_req, res) => res.sendStatus(200)); +app.get('/healthz', (req, res) => { + const payload = { ok: true, service: 'video-worker', timestamp: new Date().toISOString() }; + sendHealth(req, res, payload, 'Video Worker Health'); +}); + +// SSE endpoint for progress streaming +app.get('/progress/:requestId', (req, res) => { + const { requestId } = req.params; + + // Set SSE headers + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.flushHeaders(); + + // Create job entry if doesn't exist + if (!activeJobs.has(requestId)) { + activeJobs.set(requestId, { progress: 0, stage: 'waiting', clients: new Set() }); + } + + const job = activeJobs.get(requestId); + job.clients.add(res); + + // Send current state immediately + res.write(`data: ${JSON.stringify({ progress: job.progress, stage: job.stage })}\n\n`); + + // Cleanup on close + req.on('close', () => { + job.clients.delete(res); + if (job.clients.size === 0 && job.stage === 'complete') { + activeJobs.delete(requestId); + } + }); +}); + +// Helper to broadcast progress to all SSE clients for a job +function broadcastProgress(requestId, progress, stage) { + const job = activeJobs.get(requestId); + if (!job) return; + + job.progress = progress; + job.stage = stage; + + const message = JSON.stringify({ progress, stage }); + for (const client of job.clients) { + client.write(`data: ${message}\n\n`); + } +} + +// Dashboard endpoints +app.get('/logs', (_req, res) => { + const limit = parseInt(_req.query.limit) || 10; + const logs = logger.getLogsForDashboard(limit); + res.json({ logs, stats: logger.getStats() }); +}); + +app.get('/stats', (_req, res) => { + res.json(logger.getStats()); +}); // Configure multer to write incoming file to the OS temp dir const upload = multer({ @@ -47,71 +227,297 @@ const upload = multer({ } }); -function runFfmpeg(args) { +function parseDeviceInfo(userAgent, providedDeviceInfo) { + if (providedDeviceInfo) return providedDeviceInfo; + + // Parse device type from User-Agent + const ua = userAgent.toLowerCase(); + let deviceType = 'desktop'; + let os = 'unknown'; + let browser = 'unknown'; + + // Device type detection + if (ua.includes('mobile') || ua.includes('android')) deviceType = 'mobile'; + else if (ua.includes('tablet') || ua.includes('ipad')) deviceType = 'tablet'; + + // OS detection + if (ua.includes('windows')) os = 'windows'; + else if (ua.includes('mac')) os = 'macos'; + else if (ua.includes('linux')) os = 'linux'; + else if (ua.includes('android')) os = 'android'; + else if (ua.includes('iphone') || ua.includes('ipad')) os = 'ios'; + + // Browser detection + if (ua.includes('chrome') && !ua.includes('edg')) browser = 'chrome'; + else if (ua.includes('firefox')) browser = 'firefox'; + else if (ua.includes('safari') && !ua.includes('chrome')) browser = 'safari'; + else if (ua.includes('edg')) browser = 'edge'; + + return `${deviceType}/${os}/${browser}`; +} + +// Get video duration using ffprobe +function getVideoDuration(inputPath) { + return new Promise((resolve) => { + const proc = spawn('ffprobe', [ + '-v', 'error', + '-show_entries', 'format=duration', + '-of', 'default=noprint_wrappers=1:nokey=1', + inputPath + ]); + + let output = ''; + proc.stdout.on('data', (d) => { output += d.toString(); }); + proc.on('close', () => { + const duration = parseFloat(output.trim()); + resolve(isNaN(duration) ? 0 : duration); + }); + }); +} + +// Parse FFmpeg time string to seconds +function timeToSeconds(timeStr) { + const parts = timeStr.split(':'); + return parseFloat(parts[0]) * 3600 + parseFloat(parts[1]) * 60 + parseFloat(parts[2]); +} + +function runFfmpeg(args, requestId = 'unknown', totalDuration = 0) { return new Promise((resolve, reject) => { + const startTime = Date.now(); const proc = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'pipe'] }); let stderr = ''; - proc.stderr.on('data', (d) => { stderr += d.toString(); }); + + proc.stderr.on('data', (d) => { + stderr += d.toString(); + // Log progress if available + const progressMatch = d.toString().match(/time=(\d{2}:\d{2}:\d{2}\.\d{2})/); + if (progressMatch) { + const timeElapsed = Date.now() - startTime; + const currentTime = timeToSeconds(progressMatch[1]); + + // Calculate percentage (0-80% for transcoding, 80-100% for upload) + let percent = 0; + if (totalDuration > 0) { + percent = Math.min(80, Math.round((currentTime / totalDuration) * 80)); + } + + // Broadcast to SSE clients + broadcastProgress(requestId, percent, 'transcoding'); + + logger.logFFmpegProgress({ + id: requestId, + progress: progressMatch[1], + percent, + timeElapsed + }); + } + }); + proc.on('close', (code) => { - if (code === 0) resolve({ ok: true }); - else reject(new Error(`ffmpeg exited with ${code}: ${stderr.slice(-4000)}`)); + const duration = Date.now() - startTime; + if (code === 0) { + console.log(`✅ [FFMPEG-SUCCESS] ID: ${requestId} | Duration: ${duration}ms`); + broadcastProgress(requestId, 80, 'uploading'); // Transcoding done, now uploading + resolve({ ok: true }); + } else { + console.error(`❌ [FFMPEG-ERROR] ID: ${requestId} | Code: ${code} | Duration: ${duration}ms | Error: ${stderr.slice(-400)}`); + broadcastProgress(requestId, 0, 'error'); + reject(new Error(`ffmpeg exited with ${code}: ${stderr.slice(-4000)}`)); + } }); }); } -// POST /transcode (multipart form fields: video [required], creator [optional], thumbnail [optional]) +// POST /transcode (multipart form fields: video [required], creator [optional], thumbnail [optional], platform [optional], deviceInfo [optional]) app.post('/transcode', upload.single('video'), async (req, res) => { + const internalId = uuidv4().substring(0, 8); // Short ID for internal logging + const startTime = Date.now(); + const clientIP = req.ip || req.connection.remoteAddress || 'unknown'; + const userAgent = req.get('User-Agent') || 'unknown'; + const origin = req.get('Origin') || req.get('Referer') || 'direct'; + + // Extract rich user information from form data + // ALL FIELDS ARE OPTIONAL for backward compatibility with older app versions + // req.body may be undefined if multer didn't parse the request (e.g. wrong content-type) + const body = req.body || {}; + + // Core fields (with fallbacks) + const creator = body.creator || body.user || 'anonymous'; + + // SOURCE APP TRACKING - Critical for analytics + // Values: 'webapp' | 'mobile' | 'unknown' + // Mobile app sends 'mobile', webapp sends 'webapp' + const sourceApp = body.source_app || body.sourceApp || 'unknown'; + + // App version tracking (optional) + const appVersion = body.app_version || body.appVersion || ''; + + // Platform details (optional) + const platform = body.platform || 'unknown'; + const deviceInfo = body.deviceInfo || ''; + const browserInfo = body.browserInfo || ''; + const userHP = body.userHP || null; + + // Use client's correlationId for SSE progress (so client can subscribe before request) + // Fall back to internal ID if not provided + const correlationId = body.correlationId || null; + const requestId = correlationId || internalId; // Use correlationId for SSE progress! + const viewport = body.viewport || null; + const connectionType = body.connectionType || null; + + // Parse device info from User-Agent if not provided + const deviceDetails = parseDeviceInfo(userAgent, deviceInfo); + + console.log(`🔗 Request ID for SSE: ${requestId} (correlationId: ${correlationId || 'none'})`); + + // Log transcode start + logger.logTranscodeStart({ + id: requestId, + user: creator, + sourceApp, + appVersion, + filename: req.file?.originalname || 'unknown', + fileSize: req.file?.size || 0, + clientIP, + userAgent, + origin, + platform, + deviceInfo: deviceDetails, + browserInfo, + userHP, + correlationId, + viewport, + connectionType + }); + if (!req.file) { + const duration = Date.now() - startTime; + logger.logTranscodeError({ + id: requestId, + user: creator, + filename: 'unknown', + error: 'No file uploaded', + duration, + clientIP + }); return res.status(400).json({ error: 'No file uploaded. Send multipart/form-data with field "video".' }); } + const inputPath = req.file.path; - const outName = `${uuidv4()}.mp4`; - const outputPath = path.join(os.tmpdir(), outName); + const fileName = req.file.originalname; + const fileSize = req.file.size; + let outputPath = null; + let needsTranscoding = false; + let videoDuration = 0; + + // Initialize job tracking for SSE - PRESERVE existing clients if SSE connected first! + const existingJob = activeJobs.get(requestId); + const clients = existingJob?.clients || new Set(); + activeJobs.set(requestId, { progress: 0, stage: 'starting', clients }); + console.log(`📡 SSE clients for ${requestId}: ${clients.size}`); + broadcastProgress(requestId, 5, 'receiving'); try { - // Transcode to a broadly compatible H.264/AAC MP4 - const ffArgs = [ - '-y', - '-i', inputPath, - '-c:v', 'libx264', - '-preset', process.env.X264_PRESET || 'veryfast', - '-crf', process.env.X264_CRF || '22', - '-c:a', 'aac', - '-b:a', process.env.AAC_BITRATE || '128k', - '-movflags', '+faststart', - outputPath - ]; - await runFfmpeg(ffArgs); + // Check if file is already web-optimized + console.log(`🔍 Checking if ${fileName} is web-optimized...`); + const validation = await checkWebOptimized(inputPath); + + if (validation.optimized) { + console.log(`✅ File is already optimized: ${JSON.stringify(validation.videoInfo)}`); + console.log(`⚡ Skipping transcoding, uploading directly to IPFS`); + broadcastProgress(requestId, 50, 'optimized'); + outputPath = inputPath; // Use original file + needsTranscoding = false; + videoDuration = validation.videoInfo?.duration || 0; + } else { + console.log(`⚠️ File needs transcoding: ${validation.reason}`); + needsTranscoding = true; + broadcastProgress(requestId, 10, 'transcoding'); + + const outName = `${uuidv4()}.mp4`; + outputPath = path.join(os.tmpdir(), outName); + + // Get video duration for progress calculation + videoDuration = await getVideoDuration(inputPath); + console.log(`📏 Video duration: ${videoDuration}s`); + + // Adaptive CRF based on duration and file size + let crf = process.env.X264_CRF || '22'; + const durationMin = videoDuration / 60; + const sizeMB = fileSize / (1024 * 1024); + + if (durationMin > 5 || sizeMB > 50) { + crf = '24'; // More compression for long/large videos + console.log(`📉 Using CRF 24 for large video (${durationMin.toFixed(1)}min, ${sizeMB.toFixed(1)}MB)`); + } else if (durationMin < 1) { + crf = '20'; // Better quality for short clips + console.log(`📈 Using CRF 20 for short video (${durationMin.toFixed(1)}min)`); + } + + // Build FFmpeg args with optimizations + const ffArgs = [ + '-y', + '-i', inputPath, + '-c:v', 'libx264', + '-preset', process.env.X264_PRESET || 'medium', + '-crf', crf, + '-vf', 'scale=min(iw\\,1920):min(ih\\,1080):force_original_aspect_ratio=decrease', + '-maxrate', '5M', + '-bufsize', '10M', + '-c:a', 'aac', + '-b:a', process.env.AAC_BITRATE || '128k', + '-movflags', '+faststart', + outputPath + ]; + + await runFfmpeg(ffArgs, requestId, videoDuration); + } + + broadcastProgress(requestId, 80, 'uploading'); // Upload to Pinata if (!PINATA_JWT) { throw new Error('PINATA_JWT not configured on server'); } - // ---- NEW: read optional text fields from the same multipart form ---- - // Accept "creator" and either "thumbnail" or "thumbnailUrl" - const creator = - (req.body?.creator ?? '').toString().trim().slice(0, 64) || 'anonymous'; - const thumbnailRaw = - (req.body?.thumbnail ?? req.body?.thumbnailUrl ?? '').toString().trim(); + const thumbnailRaw = (req.body?.thumbnail ?? req.body?.thumbnailUrl ?? '').toString().trim(); const thumbnail = thumbnailRaw ? thumbnailRaw.slice(0, 2048) : ''; + const uploadName = needsTranscoding ? path.basename(outputPath) : fileName; const form = new FormData(); - form.append('file', fs.createReadStream(outputPath), { filename: outName, contentType: 'video/mp4' }); + form.append('file', fs.createReadStream(outputPath), { filename: uploadName, contentType: 'video/mp4' }); - // Pinata metadata with optional keyvalues + // Pinata metadata with rich keyvalues (standardized schema matching webapp) + // NOTE: Pinata limits to max 10 keyvalues - prioritize most important fields + const uploadDate = new Date().toISOString(); const metadata = { - name: `transcoded-${new Date().toISOString()}.mp4`, + name: `${creator}-${uploadDate}.mp4`, keyvalues: { - creator, // always include (defaults to "anonymous") - ...(thumbnail ? { thumbnail } : {}) // include only if provided + // Core identity (webapp-compatible) - 7 fields + creator, + source: 'video-worker', + uploadDate, + transcoded: needsTranscoding ? 'true' : 'passthrough', + originalFileName: req.file.originalname, + videoDuration: videoDuration ? videoDuration.toString() : 'unknown', + requestId, + + // Device/platform tracking - up to 3 more fields (10 total max) + ...(sourceApp && { sourceApp }), // webapp/mobile + ...(platform && { platform }), // mobile/desktop + ...(thumbnail && { thumbnailUrl: thumbnail }) } }; form.append('pinataMetadata', JSON.stringify(metadata)); - const options = { cidVersion: 1 }; + const options = { + cidVersion: 1, + ...(PINATA_GROUP_VIDEOS && { groupId: PINATA_GROUP_VIDEOS }) + }; form.append('pinataOptions', JSON.stringify(options)); + broadcastProgress(requestId, 85, 'uploading'); + const resp = await axios.post( 'https://api.pinata.cloud/pinning/pinFileToIPFS', form, @@ -122,23 +528,102 @@ app.post('/transcode', upload.single('video'), async (req, res) => { }, maxContentLength: Infinity, maxBodyLength: Infinity, + onUploadProgress: (progressEvent) => { + // Calculate upload progress (85-100% range) + if (progressEvent.total) { + const uploadPercent = Math.round((progressEvent.loaded / progressEvent.total) * 15); + broadcastProgress(requestId, 85 + uploadPercent, 'uploading'); + } + } } ); const { IpfsHash: cid } = resp.data; const gatewayUrl = `${PINATA_GATEWAY.replace(/\/+$/, '')}/${cid}`; - res.status(200).json({ cid, gatewayUrl }); + const totalDuration = Date.now() - startTime; + + // Broadcast completion + broadcastProgress(requestId, 100, 'complete'); + + // Log successful completion + logger.logTranscodeComplete({ + id: requestId, + user: creator, + filename: req.file.originalname, + cid, + gatewayUrl, + duration: totalDuration, + clientIP + }); + + res.status(200).json({ + cid, + gatewayUrl, + requestId, + duration: totalDuration, + creator, + sourceApp, + timestamp: new Date().toISOString() + }); } catch (err) { - console.error(err); - res.status(500).json({ error: err.message || 'Transcode failed' }); + const totalDuration = Date.now() - startTime; + + // Broadcast error to SSE clients + broadcastProgress(requestId, 0, 'error'); + + // Enhanced error logging for debugging Pinata issues + console.error('❌ [PINATA-ERROR] Full error details:', { + message: err.message, + response: err.response?.data, + status: err.response?.status, + headers: err.response?.headers + }); + + // Log error + logger.logTranscodeError({ + id: requestId, + user: creator, + filename: req.file?.originalname || 'unknown', + error: err.message || err, + duration: totalDuration, + clientIP + }); + + res.status(500).json({ + error: err.message || 'Transcode failed', + requestId, + duration: totalDuration, + timestamp: new Date().toISOString() + }); } finally { - // Cleanup - try { fs.unlinkSync(inputPath); } catch {} - try { fs.unlinkSync(outputPath); } catch {} + // Cleanup - only delete transcoded file if different from input + try { fs.unlinkSync(inputPath); } catch { } + if (needsTranscoding && outputPath && outputPath !== inputPath) { + try { fs.unlinkSync(outputPath); } catch { } + } + + // Clean up job tracking after a delay (let SSE clients receive final state) + setTimeout(() => { + if (activeJobs.has(requestId)) { + const job = activeJobs.get(requestId); + job?.clients?.forEach(client => { + try { client.end(); } catch { } + }); + activeJobs.delete(requestId); + } + }, 5000); } }); app.listen(PORT, () => { - console.log(`Video worker listening on :${PORT}`); + console.log(`🎬 Video worker listening on :${PORT}`); + console.log(`🔗 Health check: http://localhost:${PORT}/healthz`); + console.log(`🎯 Transcode endpoint: http://localhost:${PORT}/transcode`); + console.log(`🌊 Progress SSE: http://localhost:${PORT}/progress/:requestId`); + console.log(`📊 Logs endpoint: http://localhost:${PORT}/logs`); + console.log(`📈 Stats endpoint: http://localhost:${PORT}/stats`); + console.log(`📋 Dashboard monitoring enabled with structured logging`); + console.log(`📁 Logs saved to: ${logger.logFilePath}`); + console.log(`🔄 Keeping last ${logger.maxLogs} log entries`); }); diff --git a/test-video.mov b/test-video.mov new file mode 100644 index 0000000..30b8218 Binary files /dev/null and b/test-video.mov differ