유튜브 자막 확보 — 플레이어 요청 가로채기
유튜브 영상에 번역 자막을 얹어주는 브라우저 확장을 개인적으로 만들고 있습니다. 자막을 번역하려면 먼저 원본 자막을 확보해야 하는데, 이 부분에서 방법을 세 번 갈아엎었습니다.
1세대 — timedtext 직접 호출 (차단됨)
유튜브 자막은 timedtext API 로 받습니다. 확장에서 그 URL 을 직접 부르면
될 것 같았습니다.
그런데 안 됩니다. 유튜브가 이 API 에 세션 토큰(pot)을 요구하도록 바꿔서, 확장이 만든 요청에는 그게 없으니 200 OK 인데 본문이 0바이트로 옵니다. 상태 코드가 200 이라 응답만 봐서는 실패한 줄도 모릅니다.
2세대 — 자막 패널 DOM 스크래핑 (느림)
유튜브가 화면에 보여주는 자막 패널을 긁는 방법입니다. 확실히 동작은 합니다.
문제는 쓰는 느낌이었는데, 패널이 뜨는 데 수 초에서 24초까지 걸리는 데다 설명란을 펼치는 과정에서 화면이 움직이고, 언어를 바꾸는 로직도 따로 필요했습니다. 옆에서 보면 꽤 부산스럽습니다.
3세대 — 플레이어 요청 가로채기 (현재)
플레이어 자신이 보내는 timedtext 요청은 항상 성공합니다. 세션 토큰이 그 요청에는 들어 있기 때문입니다. 그래서 확장이 요청을 만들지 않고, 플레이어가 보내는 요청의 응답을 읽기만 합니다.
yt-main.js 가 페이지 컨텍스트(MAIN world)에서 fetch 와 XHR 을 래핑해서,
timedtext 응답이 지나갈 때 그걸 읽어 확장으로 넘깁니다. 자막이 꺼져 있으면
플레이어 API 로 잠깐 켰다가 받고 원상복구합니다.
응답 1건이 영상 전체 자막이라, 3.5시간짜리 영상도 1~2초에 확보됩니다. DOM 을 건드리지 않으니 화면도 안 움직입니다.
가로채기에서 걸린 것들
동작하기까지 실측으로 알게 된 것들입니다.
플레이어는 fetch 가 아니라 XHR 을 쓴다. 요즘 웹은 대부분 fetch 라 생각하기 쉬운데, 유튜브 플레이어는 XHR 로 자막을 받습니다. 둘 다 래핑해야 놓치지 않습니다.
리스너를 붙이기 전에 지나간 요청은 유실된다. 확장이 로드되기 전에 플레이어가 이미 자막을 받아버리면 놓칩니다. 그래서 페이지 쪽에 버퍼를 두고, 확장이 준비되면 재전송하게 했습니다.
tracklist 가 빈 배열인데 자막은 있는 영상이 실존한다. 자막 목록을 물어보면
비어 있는데 실제로는 자막이 나오는 영상이 있습니다. 이런 건 playerResponse
에서 자막 정보를 직접 구성해서 우회합니다.
폴백 체인을 유지한다. 가로채기 → 직접 baseUrl → 패널 스크래핑 → 실시간 미러. 앞 방법이 실패하면 다음으로 넘어가게 해뒀습니다. 유튜브가 언제 또 바꿀지 모르니, 한 방법에만 의존하지 않습니다.
1~3세대 — 직접 만들기에서 플레이어 읽기로
1세대는 유튜브의 정책 변화로 막혔고, 2세대는 동작은 했지만 사용자 경험이 나빴습니다. 3세대에서 방향을 바꿨습니다. 자막을 직접 만들어내는 대신, 이미 자막을 처리하고 있는 플레이어에서 그 결과를 읽어오는 쪽으로 갔습니다.
가장 안정적인 방법이 가장 적게 개입하는 방법이었습니다.
댓글 0