How a stale audio batch caused 7 seconds of A/V desync after backward seeks

від

у

Як застарілий пакет аудіо викликав 7 секунд роз’єднання A/V після зворотних пошуків

Я збирав FastPlay, мінімальний нативний відеоплеєр для Windows, написаний на Rust.

Репозиторій FastPlay: github.com/CalvinSturm/FastPlay

FastPlay використовує FFmpeg для демультиплексування та декодування, D3D11/DXGI для презентації відео та WASAPI для виходу аудіо. Продуктова мета навмисне вузька: швидке локальне відтворення на Windows, плавний пошук та перемотування, останні файли, відновлення відтворення та інтерфейс користувача, який «не заважає».

Під час релізного циклу v0.3.0 однією з найбільш важливих виправлень була не UI-проблема. Це була проблема коректності відтворення.

Після деяких зворотних пошуків аудіо могло перескакувати на кілька секунд уперед порівняно з відео.

Найгіршим варіантом, який я відтворив, була приблизно 7 секунд розриву A/V.

Остаточний виправлення виявилось маленьким. Вчення — більш значущим.

Симптом
FastPlay міг відкривати файл, відтворювати нормально, зупиняти, відновлювати, шукати вперед і перемотувати без явних проблем. Але після деяких зворотних пошуків відтворення поверталося з сильним розсинхронізованим відтворенням.

Напрямок мав значення.

Шукати вперед було переважно нормально. Зворотні пошуки відкривали проблему.

Це був перший корисний підказ. Якщо б всі пошуки були зламані, я б підозрював загальний шлях пошуку, скидання декодера або повторне прив’язування годинника. Але помилка, яка з’являється в основному після зворотних пошуків, зазвичай означає, що застарілий стан виживає десь у системі.

Медіапошук — це не лише «перемістити відтворення на інший часовий штамп». Він скасовує багато станів конвеєра:

– розкодовані пакети у польоті
– чергові кадри відео
– чергові аудіосемпли
– стан субтитрів
– якорі годинника
– буфери sink
– очікуваний вихід працівників
– метрики, прив’язані до старої позиції відтворення

FastPlay вже мав перевірки генерацій у кількох місцях, щоб застарілі відеокадри та результати пошуку не прослизали до поточної лінії відтворення.

Але один аудіо-допоміжник усе ще зберігав стан після пошуку.

Шлях відтворення
На високому рівні FastPlay має окремі шляхи відео та аудіо.

Відео проходить через декодування FFmpeg та презентацію D3D11.

Аудіо проходить через декодування FFmpeg, невеликий шар пакетування та вихід WASAPI у спільному режимі.

Шар пакетування аудіо існує, бо декодований аудіо не завжди надходить у точному обсязі чанків, який хоче приймач. Він нормалізує віддеблоковані зразки в пакети, які можна чисто подати на аудіослух.

У цього шару пакетування був внутрішній стан:

– очікувані декодовані зразки
– відмітка часу для поточного пакету
– припущення щодо формату виходу
– правила розміру пакету

Це нормально для відтворення аудіо. Помилка полягала в тому, що цей стан не повністю поважав межі пошуку.

Помилка
Пакувальник аудіо припускав, що якщо у нього є частковий пакет, цей пакет все ще належить до поточної лінії відтворення.

Здебільшого це було правдою.

Але пошук порушує цю найменшу припущення.

Помилка виглядала так:

1. Відтворення доходить приблизно до 14 секунд.
2. AudioBatcher утримує частковий пакет аудіо, позначений приблизно на 14 секунді.
3. Користувач виконує зворотний пошук приблизно до 7 секунд.
4. Стан кодека FFmpeg очищається.
5. Декодування поновлюється з нової цілі пошуку.
6. Нова пост-пошукова аудіо надходить приблизно до 7 секунд.
7. Старий частковий пакет аудіо все ще живий.
8. Пакувальник видає аудіо після пошуку з використанням зіпсованого таймстампа до пошуку.

Це означало, що аудіо з приблизно 7 секунд могло бути подано якби воно належало близько до 14 секунд.

Головний годинник аудіо потім прив’язувався до неправильного таймстампа. Відео намагалося презентувати близько до цілі пошуку, тоді як аудіо казало залишку гравця, що час уже на кілька секунд вперед.

Видимий результат був простим:

Аудіо випереджало відео на кілька секунд після зворотного пошуку.

Чому пошуки вперед приховали помилку
Ця помилка легше пропускалася, бо пошуки вперед випадково маскували її.

FastPlay вже мав логіку відкидання пошуку. Після пошуку розкодовані кадри або зразки перед цільовою позицією відтворення можна ігнорувати, доки декодер не наздожене бажану позицію лінії часу.

Ця логіка захищає від застарілої роботи.

Під час пошуку вперед застарілий аудіо до пошуку зазвичай має часовий штамп позаду нової цілі. Оскільки він позаду цілі, логіка відкидання може видалити його.

Але зворотні пошуки інші.

Коли ви шукаєте назад, застарілі позапошукові часові штампи випереджають нову ціль. Вони можуть виглядати як дійсне майбутнє аудіо замість очевидно застарілого аудіо.

Ось чому пошуки вперед виглядали переважно нормально, тоді як зворотні пошуки могли викликати великий розрив.

Застарілий часовий штамп не був випадковим. Він був правдоподібний. Це було просто з неправильного покоління відтворення.

Фактична корінна причина
Корінна причина була такою:

AudioBatcher зберіг часткову попередню пошукову вибірку через DecodeSession::seek.

Шлях пошуку очистив стан кодека FFmpeg, але не скинув аудіо-пакувальник на рівні застосунку.

Це розрізнення має значення.

Легко припустити, що очищення декодера означає чистий стан пошуку. Але плеєр мультимедіа зазвичай має кілька шарів буферизації над декодером. Ці буфери також потрібно скинути.

У цьому випадку:

– стан декодера FFmpeg був очищений.
– черги відео були безпечною генерацією.
– існувала логіка відкидання пошуку.
– аудіо-пакувальник зберігав частковий стан з часовими відзначеннями.

Один застарілий частковий пакет був достатнім, щоб зіпсувати часовий штамп аудіо після пошуку.

Виправлення
Виправлення полягало у доданні методу AudioBatcher::reset() та виклику його під час DecodeSession::seek після очищення стану кодека.

Важлива поведінка полягає в тому, що:

Коли декодер шукає, будь-яка часткова аудіо-пакета з попередньої лінії відтворення викидається.

Після цього перший пост-пошуковий пакет аудіо штампує часовим штампом пост-пошукової аудіо, а не штампом, успадкованим від до пошуку.

Це зберігає очікувану поведінку годинника:

– пошук до 7 секунд
– декодування аудіо поруч із 7 секундами
– подача аудіо з штампом поруч із 7 секундами
– прив’язка годинника аудіо до близько 7 секунд
– демонстрація відео біля 7 секунд

Зміни були невеликими, але вони закрили витік генерації.

Регресійний тест
Такий тип помилки потребує тесту, бо легко повторно ввести пізніше.

Форма тесту була простою:

1. Подати аудіо перед пошуком до пакувальника.
2. Залишити пакувальник із частковим очікуваним станом.
3. Скинути пакувальник як під час пошуку.
4. Подати аудіо після пошуку.
5. Переконатися, що відтворений пакет використовує пост-пошуковий штамп.

Ключове твердження не лише про те, що аудіо виходить. Головне — що аудіо штамповано з правильної генерації відтворення.

Я також зберіг тестову форму, яка документує режим відмови: без скидання застарілий попередній PTS потрапляє в наступний пакет, що видається.

Це важливо, бо помилка була не «аудіо зникло» або «декодер не працює». Помилка була більш тонкою. Аудіо існувало, але його штамп належав іншій лінії відтворення.

Валідація
До виправлення зворотний пошук міг викликати величезний відрив A/V. У одному попередньому тесті аудіо випереджало відео приблизно на 7 секунд.

Після виправлення той самий тест синхронізації аудіо/відео знову стабілізувався у нормальній погрішності. Повторні зворотні пошуки більше не призводили до стрибка годинника аудіо вперед із застарілими штампами до пошуку.

Валідація охоплювала:

– нормальне відкриття
– відтворення
– паузу/відновлення
– пошук вперед
– пошук назад
– повторний перемотування
– закрити/відкрити заново
– від A/V синхронізація після зворотного пошуку
– також тести автоматичної системи були оновлені, з акцентом на поведінку скидання пакувальника

Більше уроків
Урок не був «пам’ятай скинути цього помічника». Більше він стосувався:

Кожен компонент, який зберігає часові позначки стану медіа, повинен або скидатись під час пошуку, або явно відстежувати генерацію відтворення.

У медіаплеєра багато місць, де стан із часовими позначками може ховатися:

– черги пакетів
– черги розкодованих кадрів
– аудіо-пакувальники
– ресемплери
– стан субтитрів
– буфери sink
– стан презентації
– якорі годинника
– канали потоку робіт
– метрики, прив’язані до позиції відтворення

Якщо компонент не може відповісти на питання «до якої генерації пошуку належить цей буферизований даний?», це, ймовірно, майбутня помилка.

Це особливо вірно у плеєрах з постійними сесіями декодування, фоновими працівниками, обмеженими чергами та окремими шляхами аудіо/відео таймінгу.

Чому помилка була важливою для продукту
FastPlay навмисне вузький. Він не прагне замінити усі можливості VLC або MPC-HC. Мета — швидке локальне відтворення на Windows із хорошою поведінкою відкриття, пошуку, перемотування та відновлення.

Це робить коректність пошуку центральною для продукту.

Відеоплеєр може мати чистий UI, швидке відображення першого кадру та красиве вікно з останніми файлами. Але якщо перемотування зламає синхронізацію, користувачі не довірятимуть йому.

Ця помилка була малою за обсягою коду, але великою за відчуттям у продукті. Виправлення зробило повторні пошуки та зворотне перемотування набагато надійнішими.

Урок, який я виніс: простий

Пошук — це не лише операція декодера. Це повна межа конвеєра генерації.

Ось такий тип помилки я радий знайти рано.

Спробуйте
FastPlay відкритий код і наразі тільки для Windows.

Репозиторій та завантаження MSI: github.com/CalvinSturm/FastPlay

v0.3.0 включає останні файли, відновлення відтворення, виправлення синхронізації A/V після зворотного пошуку та локальну середу бенчмарку для вимірювань відкриття/пошуку/паузи/відновлення.

June 26, 2026 at 05:04PM