01.08.2027 335 материалов

Аудио играет на компьютере, но молчит на iPhone — всё из-за одного атома в MP4-файле

Один разработчик обнаружил, что его подкасты отлично играют в Chrome и Firefox, но полностью отказываются воспроизводиться на iPhone и iPad. Причина оказалась коварной — дело не в сервере, не в формате и не в кодеке, а в порядке блоков внутри самого MP4-файла.

Аудио играет на компьютере, но молчит на iPhone — всё из-за одного атома в MP4-файле

iPhone и iPad молча отказываются воспроизводить аудио, если метаданные файла находятся в его конце, а не в начале. Ни ошибок, ни предупреждений — просто тишина. Исправляется одна команда без перекодирования.

Проблема: звук есть, а воспроизведения нет

Разработчик разместил партию аудиофайлов в формате .m4a на Cloudflare R2 и встроил их на страницу обычным тегом <audio>. На рабочем столе — в Chrome и Firefox — всё работало безупречно. Но на iPhone и iPad ничего не происходило: нажимаешь на кнопку воспроизведения, и тишина. Никаких ошибок, консоль чистая.

Подобная ситуация знакома многим веб-разработчикам. iOS Safari славится своей избирательностью к мультимедийному контенту, и в первую очередь обычно проверяют три вещи: правильный MIME-тип, поддержку HTTP Range-запросов и совместимость кодека. В данном случае всё три пункта были в порядке — сервер отдавал корректный Content-Type: audio/mp4, запросы с заголовком Range возвращали статус 206 Partial Content, а формат .m4a (AAC) — самый нативный для Apple-устройств. CORS тоже не являлся причиной: тег <audio> без атрибута crossorigin воспроизводит кросс-доменный контент без необходимости в CORS-заголовках.

В чём же дело

Файл MP4 (и m4a — это по сути тот же MP4-контейнер, только для звука) состоит из структурных блоков, которые в спецификации называются «боксами» или «атомами». Для воспроизведения критичны два из них:

  • mdat — содержит сами аудиоданные. Это самый большой блок, в файле автора он занимал 11 МБ.
  • moov — содержит индекс воспроизведения и метаданные: временна́я шкала, таблицы сэмплов. Плеер обязан прочитать этот блок первым, чтобы понять, как декодировать аудио.

Проблема в том, что FFmpeg по умолчанию располагает блоки в следующем порядке: сначала mdat (данные), а moov (индекс) — в самом конце файла. Это стандартное поведение, и на десктопе оно не вызывает проблем.

Десктопные браузеры — Chrome и Firefox — поступают хитрее: они отправляют Range-запрос к концу файла, забирают оттуда moov, находят индекс и начинают воспроизведение. iOS Safari при атрибуте preload="metadata" запрашивает только начало файла. Не находит там moov — и молча сдаётся. Никакой ошибки, никакого предупреждения.

Все устройства Apple используют движок WebKit для Safari, поэтому и iPhone, и iPad ведут себя одинаково.

Как это проверить

Можно за 30 секунд убедиться, что проблема именно в этом. Нужно запросить первые 48 байт файла и посмотреть, какой блок идёт сразу после ftyp:

curl -s -H "Range: bytes=0-47" "https://your-domain/xxx.m4a" | xxd | head -3

Если сразу после ftyp идёт mdat — это и есть проблема. Если moov — всё в порядке.

Решение: одна команда без потери качества

Исправление сводится к одной строке:

ffmpeg -i input.m4a -c copy -movflags +faststart output.m4a

Ключевой параметр — -c copy. Он не перекодирует аудио, а лишь перемещает блок moov в начало файла. На практике это означает:

  • качество звука не меняется;
  • размер файла остаётся прежним (byte в byte);
  • процесс занимает доли секунды даже для файлов в десятки мегабайт.

После этого достаточно перезалить файл поверх оригинала на хостинге. Если используется CDN вроде Cloudflare, стоит убедиться, что на краевых серверах не остался закешированный старый вариант. Для чистого тестирования — открыть в приватной вкладке, чтобы обойти локальный кеш браузера.

Как не попадаться снова

Если в рабочем процессе есть этап «сгенерировать аудио → загрузить на хостинг», стоит добавить faststart как обязательный шаг перед загрузкой. Сырой вывод FFmpeg или любого конвертера не стоит отправлять напрямую — нужна промежуточная обработка. Автор оригинала встроил этот этап в свой скрипт загрузки: remux с faststart, загрузка, автоматическая проверка через curl, что moov действительно стоит в начале файла. Три шага — и баг больше не повторяется.

Почему важно тестировать на реальных устройствах

Этот случай хорошо иллюстрирует классическую ловушку: «у меня работает». Сервер был настроен правильно, файл отдавался корректно, на десктопе всё воспроизводилось — все «очевидные» места были чисты. Дефект прятался внутри структуры самого файла, в порядке атомов, и проявлялся только на платформе, которая ведёт себя иначе.

Проблемы, которые всплывают только на реальных устройствах — особенно на iOS с её жёстким WebKit, — легко проскакивают мимо автоматизированных тестов и десктопной разработки. Если что-то работает на компьютере, но отказывается на мобильном, имеет смысл задуматься не только о коде, но и о формате файла и платформенных различиях в том, как этот файл обрабатывается.