개발 취재 노트

Playwright로 화면 녹화해서 영상을 만들었더니 소리가 하나도 없다

AI 캐릭터들이 쇼츠 영상을 만드는 프로젝트(personaverse)에서, 캐릭터 컷아웃을 배경 클립 위에 얹는 "버튜버" 포맷을 만들던 중이었다. HTML 화면을 헤드리스 브라우저에 띄우고 그걸 통째로 화면 녹화해서 영상을 뽑는 파이프라인인데, 완성본을 재생해보니 분명 소리가 있는 배경 클립인데도 영상에는 소리가 하나도 없었다.

결론

이 Playwright 녹화 파이프라인은 화면만 담으므로 원본 오디오는 후처리로 넣는다. <video>의 muted를 풀어도 녹화물에 오디오 트랙이 생기는 구조가 아니었다.

지연은 결과물의 컷 시각 - 원본의 같은 컷 시각으로 측정한다. raw - scene으로 구한 1초는 끝 여유시간까지 포함해 과했고, 같은 컷을 세 지점에서 비교하니 실제 지연은 0.27초였다. 이 측정 방식을 코드로 자동화해 정밀하게 재니 0.25초로 나왔다. 이 값을 적용해 다시 합성했다 . 다만 세션에는 이 뒤에 사용자가 소리를 직접 들어 싱크를 확인한 기록이 없다.

encode.py 원문이 아닌 예제다. raw.webm은 무음 녹화, clip.mp4는 원본이다. 250은 이 사례에서 자동 측정으로 적용해 확인한 지연(밀리초)이며, 자신의 영상에서 측정한 값으로 바꾼다.

ffmpeg -i raw.webm -i clip.mp4 -filter_complex "[1:a:0]adelay=250:all=1,apad[a]" -map 0:v:0 -map "[a]" -c:v libx264 -c:a aac -shortest output.mp4

-map은 녹화 영상과 원본 첫 오디오를 선택한다(문서). adelay는 전 채널 지연, apad·-shortest는 영상 끝까지 무음 보충이다(필터 문서).

상황

이 프로젝트의 렌더 엔진은 캐릭터 컷아웃과 배경 영상을 올린 HTML 페이지를 헤드리스 크로미움으로 열고, Playwright로 화면을 녹화하는 자체 파이프라인(record.py)이다. 그 결과물을 encode.py/ make.py가 후처리한다. 브라우저는 배경 영상 자동재생을 보장하기 위해 --autoplay-policy=no-user-gesture-required 옵션으로 띄워져 있었다.

증상

캐릭터 컷아웃 + 배경 클립 + 말풍선 자막으로 된 "버튜버" 포맷을 v5까지 다듬어 오던 중, 완성본을 재생해보니 영상 소리가 전혀 녹음되어 있지 않았다. 배경으로 쓴 클립 자체에는 분명히 소리가 있었는데도 결과물에는 반영되지 않았다.

원인

record.py가 화면만 녹화하므로 배경 <video>의 muted를 풀어도 오디오는 담기지 않는다.

해결

  1. engine.js에서 재생 시작 시 클립을 0초로 리셋해 자막과 원점을 맞췄다.
  2. make.py의 raw - scene 계산은 폐기한다. 대신 원본과 녹화물에서 **같은 장면 전환이 일어나는

시각**을 찾아 그 차이를 지연으로 쓴다. 손으로 확인할 때는 의심 지점 근처에서 ffmpeg -ss <시각> -i <영상> -frames:v 1 frame.png로 정지 프레임을 뽑고, 컷이 걸리는 정확한 시각이 나올 때까지 -ss 값을 좁혀가며 두 영상을 비교한다 (-ss/-frames:v 옵션 문서). 이렇게 세 지점 (1.47->1.73초, 2.47->2.73초, 25.67->25.93초)을 비교하니 오프셋은 모두 0.27초로 일치했다 .

  1. 이 비교를 코드로 자동화해 렌더마다 장면 전환을 스스로 찾아 지연을 계산하게 했다. 같은 사례에서

정밀하게 재니 0.25초로 나왔고, 이 값으로 다시 합성했다. 다만 영상이 작은 플레이어 영역에 들어가고 흰 배경·정지 캐릭터가 프레임 대부분을 차지한 다른 편에서는 raw 기반 컷 검출이 실패해, 최종본에서 직접 오프셋을 재고 수동값을 넣었다. encode.py에는 이 지연(초 단위)을 넘겨 AAC로 합성한다. 예제의 adelay 값은 초x1,000(밀리초)이다.

  1. 기존 도구는 python video-engine/make.py --scene scene.json --skip-record --clip-audio-delay 0.15로 조절한다. 장면 경로·지연은 교체한다. 값을 줄이면 소리가 앞당겨진다.

덤 - 리드타임 계산은 또 틀렸다

1초 지연과 자막 일치만으로 성공이라 판단했으나 소리는 밀렸다. 작은 배경 영상에서는 자동 컷 검출도 실패해 최종본에서 직접 측정했다.

취재 후기 - 음소거를 풀어도 소리가 안 들어오는 이유

박도은
쇼츠용 버튜버 포맷, 캐릭터 컷아웃에 배경 클립 깔고 말풍선 얹는 거 거의 다 됐는데, 방금 완성본 틀어보니 소리가 하나도 없어
정바다
배경 video 태그에 음소거(muted)가 걸려있지 않아? 그거 하나 풀면 되지
정하늘
근데 지금 녹화가 화면만 담는 거면, muted를 풀어도 안 들릴 수 있어. 녹화 쪽은 어떻게 찍히는데?
박도은
녹화는 Playwright라는 브라우저 자동화 도구로 화면만 찍는대. 그럼 muted를 풀어도 소용없는 거지
정바다
어... 그럼 muted 문제가 아니었네
정하늘
맞아. 화면만 녹화하면 오디오 트랙은 안 생겨
박도현
그럼 녹화 코드를 고쳐서 오디오까지 같이 잡게 만들어야 해? 그거 손이 많이 갈 것 같은데
정하늘
그럴 필요 없어. 인코딩 때 원본 오디오를 합치자
박도은
근데 녹화가 바로 시작하는 게 아니라 페이지 로딩하고 나서 시작하잖아. 소리만 얹으면 타이밍 어긋나지 않아?
정바다
그 로딩 때문에 생기는 지연을 리드타임이라고 치고, 그만큼 오디오를 뒤로 밀어서 넣으면 맞을 것 같은데
정하늘
녹화 원본(raw) 길이에서 실제 장면 재생 길이를 빼면 되지 않을까? 그 차이를 지연으로 넣어보자
박도현
그래서, 해보니 됐어?
박도은
소리는 들어갔어. 1초 지연이고 15초에서 자막도 겹치니 다 맞는 것 같아
정바다
그럼 끝났네. 완벽하게 맞췄다
정하늘
아니, 소리가 늦어. 같은 컷 시각을 비교하니 약 0.27초야. 내 길이 차 계산은 끝 여유시간까지 셌어
박도현
그럼 결과물 컷 시각에서 원본의 같은 컷 시각을 뺀 만큼만 소리를 밀어야겠네

장면 전환으로 다시 측정해 오디오를 합친다.

박도은
일단 지금은 "소리가 아예 없다"는 문제는 끝난 거지?
정하늘
그래. 녹화 자체가 화면만 잡는다는 거, 그래서 음소거를 풀어도 소용없다는 거까지 알았으니까
박도은
밀림은? 자동 측정한 0.25초로 다시 합쳤다고 했잖아
정하늘
맞아. 장면 전환 기준으로 잰 값이라, 들어보고 어긋나면 그때 다시 맞추면 돼
박도현
화면 녹화로 영상 만들 때는 소리가 없으면 재생 쪽을 먼저 보지 말고 녹화 방식부터 봐야 해. 캡처가 화면만 잡으면 음소거를 풀어도 소리는 안 들어와.