Key Design
Audio Splitting
Audio files are split into HLS segments via the same Lambda as video, enabling streaming playback with seek support.
Pipeline
Raw audio in staging
│
├─ 1. Download from S3: {owner}/{project}/staging/{hash}
├─ 2. Probe with ffprobe (sampleRate, channels, codec, duration)
├─ 3. Transcode + split with ffmpeg
│ -c:a aac -b:a 128k -vn -f hls -hls_time 6
├─ 4. Parse segment durations from ffmpeg m3u8
├─ 5. Hash each .ts segment (SHA256[:16])
│ Upload to chunks/{hash}.ts (skip if exists = dedup)
├─ 6. Build stream m3u8 with bare hex hashes
├─ 7. Upload playlist: tracks/audio/{id}/stream/{streamHash}.m3u8
├─ 8. Update tracks/audio/{id}/meta.json
└─ 9. Callback PATCH to BSSffmpeg Command
ffmpeg -i input.wav \
-c:a aac -b:a 128k \ # always transcode to AAC
-vn \ # strip any video stream
-f hls \
-hls_time 6 \ # 6-second segments
-hls_list_size 0 \ # keep all segments
-hls_segment_type mpegts \
-hls_flags independent_segments \
-hls_segment_filename seg_%05d.ts \
out.m3u8Key difference from video: audio always transcodes to AAC regardless of input codec (WAV, FLAC, MP3, etc.). This ensures universal HLS compatibility. The original raw file is preserved in the staging pool.
Why Always Transcode?
| Input Codec | HLS Compatible? | Decision |
|---|---|---|
| AAC | Yes | Transcode anyway — consistent output |
| MP3 | Partially | Transcode for reliability |
| WAV/PCM | No | Must transcode |
| FLAC | No | Must transcode |
| Opus | No (in TS) | Must transcode |
Transcoding to AAC 128k is fast and produces consistent, small segments. Quality loss is negligible for streaming. The original lossless file remains in staging for download.
Seek Granularity
HLS handles random access at the segment level, not the codec level. Each 6-second .ts segment starts with a fresh AAC frame boundary.
- Seek to 30s → player downloads the segment containing 30s
- Granularity ≈ 6 seconds (segment duration)
- Within a segment, audio decodes linearly (milliseconds for 6s of audio)
Shared Infrastructure
Audio reuses the same infrastructure as video:
| Component | Shared? |
|---|---|
Global chunk dedup pool (chunks/{hash}.ts) | Yes |
| M3U8 bare-hash format | Yes |
rewriteM3u8() function | Yes |
| Cache headers (immutable, 1-year TTL) | Yes |
Lambda function (hls-splitter) | Yes (dispatched by type field) |
Streaming Endpoint
GET /audio/:audioId/stream/:hash.m3u8Same pattern as video — fetches m3u8 from S3, rewrites bare hashes to CDN URLs, returns with immutable cache headers.
Meta.json After Splitting
{
"audioId": "...",
"sampleRate": 44100,
"channels": 2,
"codec": "aac",
"bitRate": 128000,
"durationSec": 45.5,
"streams": ["def789abc012"],
"updatedAt": "2026-04-14T..."
}