요약
- ffmpeg 5.x(Debian bookworm)는
aselect같은 필터 expression의 항(operand) 수가 약 100개를 넘으면Error while parsing expression으로 실패한다. ffmpeg 4.x는 수백 개도 허용했다. - 입력 크기에 비례해 커지는 동적 생성 표현식은 OS/ffmpeg 버전 업그레이드에서 조용히 깨질 수 있고, 청크로 나눠 여러 필터로 체이닝하면 회피된다.
본문
증상: Python 3.10→3.11 런타임 업그레이드가 Docker base 이미지를 bullseye→bookworm으로 바꾸면서 시스템 ffmpeg가 4.x→5.x로 동반 상승했다. 긴 통화 녹음을 STT용으로 변환할 때 침묵 제거 단계에서만 ffmpeg.Error로 실패. 짧은 통화는 정상이라 스테이징에서 안 잡혔다.
원인: 침묵 구간마다 between(t,a,b)를 +로 이어붙인 단일 표현식 aselect=not(b1+b2+...+bN)을 만드는 구조였는데, 통화가 길수록 침묵 구간이 많아 N이 커진다. ffmpeg 5.0에서 표현식 파서(libavutil eval.c)에 재귀 깊이 제한이 도입되어 약 100항부터 거부한다.
재현(docker 실측):
| ffmpeg | between 항 수 |
|---|---|
| 4.3 (bullseye) | 600항도 OK |
| 5.1 (bookworm) | N=98 OK, N=100 FAIL (Error while parsing expression) |
수정: not(b1+b2) ≡ not(b1) AND not(b2). aselect는 통과 프레임만 남기므로 필터를 체이닝하면 교집합 = 단일 표현식과 결과가 동일하다. 조건을 50개 단위 청크로 나눠 aselect 필터를 여러 개 체이닝하고, asetpts(타임스탬프 재설정)는 모든 aselect 뒤에 1회만 적용해 원본 t 기준을 유지한다.
# Before (실패)
aselect=not(b1+b2+...+b150) , asetpts=N/SR/TB
# After (회피)
aselect=not(b1..b50) , aselect=not(b51..b100) , aselect=not(b101..b150) , asetpts=N/SR/TB
일반화된 교훈:
- 시스템 바이너리는 OS 베이스 이미지에 묶여 있다. ffmpeg·imagemagick·libvips 등은 언어 런타임 업그레이드(베이스 이미지 변경)에 의해 의도치 않게 메이저 버전이 동반 상승한다. 업그레이드 영향 분석 시 system dependency까지 본다.
- 입력 크기에 비례해 커지는 동적 생성 명령/표현식은 상한을 의심한다. 표현식 길이·복잡도, argv 길이, 필터그래프 노드 수 등에 암묵적 한계가 있을 수 있으므로, 상한 비의존적(청크·스트리밍) 설계로 바꾼다.
- 데이터 의존 버그는 스테이징에서 안 잡힐 수 있다. 짧은 입력 vs 긴 입력처럼 규모에 따라 분기하는 경로는 경계·대용량 케이스를 의도적으로 테스트한다.
관련 노트
- ts에서 aac로 copy로 변경하면 duration이 변경될 수 있다.
- 스트림 복사는 디코딩·재인코딩을 수행하지 않고 입력 컨테이너에서 읽은 압축된 패킷을 출력 컨테이너에 그대로 기록하는 동작
- python 3.11 이후부턴 gevent 성능이 악화될 수 있다.