Перейти к содержанию

ADR-002: Video Streaming — Bunny Stream HLS + MP4 Fallback

Status: Accepted Date: 2026-02-09 Deciders: Dmitry Aleshkovskiy, Claude (AI assistant) Jira: AF-98

Context

App Factory needs a video delivery strategy for short micro-lesson content (30-120 seconds). The app is offline-first, requiring both streaming and download capabilities. Videos are uploaded by content editors through Django admin and consumed by Flutter mobile clients.

Key requirements: - Short videos (30-120 sec) per lesson - Offline playback (must work without network) - Adaptive streaming for varying network conditions - CDN-backed delivery with global reach - Automatic transcoding (editors upload source video, system handles the rest) - Cost-effective for a bootstrapped product - Flutter-compatible (iOS + Android)

Options Considered

1. Raw MP4 download only

  • Pros: Simplest approach, trivial offline support, no adaptive complexity
  • Cons: No adaptive bitrate (wastes bandwidth or buffers on slow networks), single resolution, large file sizes for high quality

2. Self-hosted HLS (FFmpeg transcoding + S3/CDN)

  • Pros: Full control, standard protocol, adaptive bitrate
  • Cons: Must build and maintain transcoding pipeline (FFmpeg workers, queue, storage for multiple renditions), significant DevOps overhead, higher infrastructure cost

3. AWS MediaConvert + CloudFront

  • Pros: Managed transcoding, tight AWS integration
  • Cons: Complex pricing, vendor lock-in, requires AWS account setup, no built-in player analytics

4. Bunny Stream (Bunny.net)

  • Pros: Fully managed transcoding (upload MP4 → auto-generate HLS + multiple MP4 resolutions), global CDN included, simple HTTP API, affordable pricing (~$1/1000 minutes), built-in thumbnail generation, MP4 fallback URLs for offline download, token authentication for content protection
  • Cons: Smaller ecosystem than AWS, no SLA for enterprise (adequate for current scale)

Decision

Bunny Stream for video hosting and delivery, with HLS adaptive streaming for online playback and MP4 fallback for offline download.

Rationale

  1. Zero transcoding infrastructure: Upload source MP4 via API → Bunny automatically transcodes to 240p, 360p, 480p, 720p, 1080p. No FFmpeg workers, no queue management, no storage planning.

  2. Short content advantage: For 30-120 sec videos, HLS adaptive streaming provides fast start without buffering, while MP4 files remain small enough (~5-15 MB at 720p) for practical offline download.

  3. Dual delivery model:

  4. Online: HLS via https://vz-{id}.b-cdn.net/{videoId}/playlist.m3u8 — adaptive bitrate, fast start
  5. Offline: MP4 via https://vz-{id}.b-cdn.net/{videoId}/play_720p.mp4 — download for local playback

  6. Flutter compatibility: Standard video_player package supports HLS natively on both iOS (AVPlayer) and Android (ExoPlayer). No special SDK needed.

  7. Content protection: Bunny CDN Token Authentication V2 (SHA256) with directory tokens protects HLS streams — single token covers playlist.m3u8 + all .ts segments.

  8. Cost efficiency: ~$1 per 1000 minutes of video delivery. For an MVP with hundreds of users and short videos, monthly cost is negligible.

  9. Unified CDN provider: Already using Bunny Storage for images/audio — single vendor simplifies billing, credentials, and operational overhead.

Architecture

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  Django Admin    │────>│  Bunny Stream    │────>│  Flutter Client  │
│  (upload MP4)   │     │  (transcode)     │     │  (HLS playback)  │
└─────────────────┘     └──────────────────┘     └─────────────────┘
        │                       │                        │
   AssetUploadService     Auto-generates:           video_player +
   → create video         - playlist.m3u8           chewie (HLS)
   → upload binary        - play_240p.mp4               │
   → poll encoding        - play_360p.mp4          Offline mode:
                          - play_480p.mp4          download MP4 →
                          - play_720p.mp4          local playback
                          - play_1080p.mp4
                          - thumbnail.jpg

URL Patterns

Purpose URL Pattern
HLS Adaptive https://vz-52e5090d-8c8.b-cdn.net/{videoId}/playlist.m3u8
MP4 720p https://vz-52e5090d-8c8.b-cdn.net/{videoId}/play_720p.mp4
Thumbnail https://vz-52e5090d-8c8.b-cdn.net/{videoId}/thumbnail.jpg
Signed HLS https://vz-52e5090d-8c8.b-cdn.net/bcdn_token={token}&expires={ts}/{videoId}/playlist.m3u8

Encoding Workflow

  1. Admin uploads video file in Django admin
  2. AssetUploadService calls Bunny Stream API: POST /library/{id}/videosPUT /library/{id}/videos/{guid}
  3. Bunny transcodes automatically (status 1→2→3→4)
  4. Celery periodic task polls GET /library/{id}/videos/{guid} until status=4 (Finished)
  5. On completion: update MediaAsset.url with HLS URL, store bunny_video_id, dimensions, duration

Packages

Backend

httpx>=0.28.0  # HTTP client for Bunny API

Frontend

dependencies:
  video_player: ^2.9.2   # HLS + MP4 playback
  chewie: ^1.8.5          # Video player UI controls

Consequences

  • All video content is hosted on Bunny Stream (library ID 596480)
  • Videos are NOT stored locally on the Django server — only metadata in PostgreSQL
  • MediaAsset model gains bunny_video_id and encoding_status fields
  • API responses include HLS URL (signed, 2h expiry) for streaming and MP4 URL for offline download
  • Flutter uses video_player with HLS URLs — no Bunny-specific SDK
  • Encoding is asynchronous — video is not immediately available after upload (typically 1-5 min for short clips)
  • Bunny.net becomes a critical vendor dependency for video delivery

References