Skip to content

Implement presigned URLs for audio streaming #187

@test3207

Description

@test3207

Summary

Optimize audio streaming to reduce Node.js CPU usage by offloading stream handling to Nginx.

Current Problem

  • All audio data flows through Node.js: MinIO → Node.js → Client
  • 59 MB/s passes through Node.js at 1000 VUs
  • CPU reaches 84% (while PostgreSQL only 5%, MinIO 14%)

Options Analyzed

Option A: Presigned URLs (❌ Not Recommended)

Client → 302 redirect → MinIO presigned URL
Pros Cons
Simple implementation Exposes MinIO endpoint
Zero Node.js CPU CDN unfriendly - signed URLs expire, cache invalidation issues
Native Range support Security concerns

Option B: Nginx Reverse Proxy (✅ Recommended)

Client → Nginx → MinIO (for /stream/*)
       → Node.js (for /api/*)
Pros Cons
MinIO stays internal Moderate implementation effort
CDN friendly - stable URLs Nginx config changes
High security
C-level stream handling

Option C: Keep Current (⏸️ Acceptable for Now)

  • Current performance is sufficient (1000 VUs, 0% error)
  • Can defer optimization until CDN integration or higher load requirements

Recommended Solution: Option B (Nginx Proxy)

Architecture

Before:

Client → Node.js (:4000) → [stream processing] → MinIO
              ↑ bottleneck: 59MB/s through Node.js

After:

Client → Nginx (:80)
           ├── /api/* → Node.js (:4000)      # API requests
           └── /stream/* → MinIO (:9000)     # Audio streams (bypass Node.js)

Authentication Flow

  1. Client requests GET /api/songs/:id/stream with JWT
  2. Node.js validates JWT, checks user permission
  3. Returns 302 redirect to internal path /stream/{filePath}?_token={internal_signature}
  4. Nginx receives /stream/*, proxies to MinIO
  5. MinIO returns audio data directly to client via Nginx

Implementation Details

Backend code (dual mode support):

// backend/src/routes/songs.ts
app.get('/:id/stream', async (c) => {
  // ... auth validation ...
  
  if (process.env.STREAM_MODE === 'redirect') {
    // Production: redirect to Nginx proxy path
    const token = generateInternalToken(song.file.path);
    return c.redirect(`/stream/${song.file.path}?_token=${token}`);
  } else {
    // Development: keep existing Node.js proxy (simple but slower)
    return streamThroughNodeJS(c, song);
  }
});

Nginx config addition:

# Audio stream direct to MinIO (production only)
location /stream/ {
    internal;  # Only accept internal redirects
    
    proxy_pass http://m3w-minio:9000/m3w-music/;
    proxy_buffering off;
    proxy_http_version 1.1;
    proxy_set_header Range $http_range;
    proxy_set_header If-Range $http_if_range;
}

Environment control:

# Local dev (backend/.env) - no change needed
STREAM_MODE=proxy  # default, keeps current behavior

# Docker production (.env.docker)
STREAM_MODE=redirect  # enables Nginx direct streaming

Local Development Impact

Scenario Audio Path Config Required
npm run dev Node.js proxy None
docker-compose up (dev) Node.js proxy None
docker run m3w:prod Nginx direct Auto-enabled

Local development workflow unchanged.

CDN Integration Path (Future)

Client → CDN → Nginx → MinIO
  • Stable URLs (/api/songs/:id/stream) work well with CDN caching
  • Standard Cache-Control headers apply
  • No signed URL expiration issues

Acceptance Criteria

  • Nginx config supports /stream/* proxy to MinIO
  • Backend supports STREAM_MODE environment variable
  • Development mode (STREAM_MODE=proxy) works unchanged
  • Production mode (STREAM_MODE=redirect) reduces CPU significantly
  • Range requests (seeking) work correctly in both modes
  • Load test confirms CPU < 30% at 1000 VUs (production mode)

Priority

P2 - Low priority. Current performance is acceptable (1000 VUs, 0% error).

Implement when:

  • Preparing for CDN integration (Epic 3.4)
  • Experiencing actual performance issues in production
  • Vertical scaling becomes insufficient

References

Parent Issue: Epic 3.2 (#182)

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions