Эмуляторный декодер (c2.goldfish) отказывался декодировать 893x480 (не кратно 16). Теперь после scale любой ширины pad доводит кадр до кратного 16 чёрными полями, пропорции не меняются.
Прод-инцидент (3 джоба failed подряд): HttpURLConnection рвал скачивание
7.2 ГБ на ~5-й минуте ('Can't download video from Jellyfin'), при этом curl
с той же машины качал стабильно (HTTP/1.1 и HTTP/2). Сеть и Jellyfin чисты.
- JellyfinInput: OkHttpClient (readTimeout=0, followRedirects), byteStream→Files.copy,
retry до 3 попыток с паузой 5с на IOException, warn-лог каждой попытки
- InputService: лог с полным stacktrace (реальная причина обрыва теперь видна)
Проблема: returnStaleToNew (stale-timeout 30м) смотрит только taken_at,
который ставится один раз при взятии джоба. Долгий джоб (скачивание 7.2 ГБ
11 мин + конвертация 24 мин > 30 мин) возвращался в очередь и перехватывался
вторым воркером → дубль работы, падение на скачивании, статус failed
при успешно работающем первом воркере.
Решение: heartbeat — пока джоб выполняется, воркер каждые 10 сек обновляет
taken_at (UPDATE ... WHERE status='processing'). Мёртвый воркер по-прежнему
перехватывается через stale-timeout; живой — никогда не считается зависшим.
- JobRepository.heartbeat(id): UPDATE taken_at/updated_at с guard processing
- JobProcessor: параллельная корутина heartbeat (свой CoroutineScope+Dispatchers.IO),
отмена в finally (done/failed/cancelled)
- AppProperties.heartbeatInterval = 10s, application.yaml heartbeat-interval: 10s
- JobRepositoryTest: heartbeat обновляет processing-джоб, не трогает non-processing
- подкласс Stream.Data с @SerialName(data), Subtitle.startTime nullable
- JobExecutor не трогался: filterIsInstance игнорирует Data
- тесты: JSON-фикстура с data-потоком, реальный .mov с tmcd-треком