Skip to content

v0.4.0 — real MP4 downloads: H.264/AAC, fragmented MP4, complete audio

Choose a tag to compare

@fdff87554 fdff87554 released this 25 Sep 13:31
· 84 commits to main since this release
d73606c

Playback and hardening release on top of v0.3.0. MP4 downloads are now real MP4s with H.264 video and complete audio. They open in QuickTime and suit hardware players such as car head units. The API surface, subprocess handling and edge configuration also got a round of fixes.

Changed behaviour

  • fmt=mp4 prefers H.264 and AAC over AV1 and VP9 (#113).
    • YouTube rarely offers H.264 above 1080p, so quality=best usually tops out at 1080p, where it used to reach 4K AV1.
    • The fixed qualities are ceilings. When a video's H.264 stops below the requested height, that lower H.264 is chosen: for example, 720p for quality=1080 on a video with 1080p AV1 but only 720p H.264.
    • The picker now reads Best (most compatible) and Up to 1080p / 720p / 480p.

MP4 downloads (#112, #108, #115)

Before Now
AV1 was selected at every quality whenever a video had it, and many hardware decoders cannot play AV1 (#112) H.264/AAC first, with other codecs only when no H.264 fits the ceiling (#113)
The body was MPEG-TS served as video/mp4, and AV1 inside it had no codec identification (#108) Fragmented MP4, the only MP4 layout that can be streamed without storing the file (#117, which lands #114)
Merged downloads over about 10 MiB were throttled to about 110 s, and their audio stopped early (#115) Video and audio are fetched separately with yt-dlp's chunked downloader and merged by our own ffmpeg (#117)

Measured on a 213 s video at 480p: 109 s with audio ending at 175 s → 4.5 s with complete audio. A 10-minute video downloads in 7.6 s with complete audio.

A download that fails before its first 64 KiB, such as a progressive MP4 with its index at the end (#119), now returns a JSON error instead of a 200 with an empty video.

Fixes and hardening

  • Downloads:
    • Failures surface as errors instead of empty 200s (#84).
    • A client disconnect kills the whole pipeline (#107), and subprocess teardown is hardened (#98).
    • Concurrent streams per client are capped (#93).
    • Playlist extraction is bounded (#94).
  • Input handling:
    • Only YouTube's own yt-dlp extractors run (#87).
    • Uppercase hosts are accepted (#95).
    • Unsupported YouTube URLs are reported as client errors (#103).
  • Edge and privacy:
    • Security headers apply on every location, and public docs are closed (#89).
    • Error envelopes survive nginx (#90).
    • Video URLs stay out of the access log (#91).
    • Rate limits key on the real client behind a proxy (#86).
    • Runtime writes are confined to tmpfs (#92).
  • Frontend: metadata is built through the DOM rather than innerHTML (#88), and the empty state is fixed (#100).
  • Runtime:
    • yt-dlp extraction runs off the event loop (#85).
    • The yt-dlp challenge solver scripts are installed (#110).
    • The entrypoint teardown and signal exits are fixed (#99, #109).
  • Supply chain and CI:
    • Actions are pinned to SHAs with read-only defaults (#96).
    • Backend lockfiles are hash-pinned (#97).
    • The app version has a single source (#101).
    • Test coverage is extended (#83, #102).
    • CI now runs the real-ffmpeg remux tests.

Known limitations

  • A progressive MP4 with its moov at the end cannot be remuxed from a pipe, and fails with an error (#119). No YouTube sample of that layout has been seen yet.
  • Each MP4 download resolves the video twice, once per track. Whether that raises the risk of YouTube's bot detection is unverified.
  • The mp3 stage still lacks the -xerror guard (#116).
  • Two pushes to main in quick succession can leave :main on the older image (#118).