CSAI(Client-Side Ad Insertion)는 플레이어가 콘텐츠와 광고를 클라이언트에서 직접 합치는 방식입니다. PC나 모바일에서는 비교적 무난하게 도입할 수 있는 패턴이지만, 무대가 스마트 TV로 바뀌면 이야기가 달라집니다.
스마트 TV는 PC와 다른 제약 위에서 동작합니다. 단일 비디오 디코더, 제한된 메모리, 그리고 일반 사용자 환경에서는 거의 마주칠 일이 없는 구버전 브라우저 런타임. 이 글에서는 스마트 TV에 CSAI를 적용하면서 마주친 콘텐츠 영상이 멈추는 이슈를 어떻게 좁혀 갔는지, 그리고 그 과정에서 마주친 두 단계의 함정을 1편으로 정리합니다.
스마트 TV의 큰 갈래는 삼성 Tizen과 LG WebOS 두 가지입니다. 두 플랫폼 모두 다음과 같은 공통 제약을 가집니다.
PC 환경이라면 광고 영상과 콘텐츠 영상을 각각 다른 <video> 엘리먼트에 띄워두고 보이지 않게 토글할 수 있지만, 스마트 TV에서는 같은 디코더를 광고와 콘텐츠가 번갈아 점유해야 합니다. 이게 모든 이슈의 출발점입니다.
이슈를 이야기하기 전에, 그 시점의 구현 상태를 먼저 정리해 둡니다. 당시 구조는 다음과 같았습니다.
<video> 엘리먼트가 하나 있습니다.<video> 엘리먼트가 추가됩니다.<video> 엘리먼트를 제거하고, 콘텐츠용 <video>를 마지막 재생 위치로부터 다시 재개하려는 흐름이었습니다.PC 브라우저에서는 매우 자연스러운 패턴입니다. 두 개의 엘리먼트가 각자 자기 책임을 가지고, 광고가 끝나면 광고 엘리먼트만 정리한 뒤 원래 보고 있던 콘텐츠로 돌아가면 됩니다.
그러나 앞서 짚었듯 스마트 TV는 하나의 디코더를 두 엘리먼트가 번갈아 점유하는 무대입니다. PC 환경의 머릿속 모델로 짠 위 구조를 그대로 가져왔을 때, 그 충돌이 첫 번째 증상으로 드러났습니다.
광고가 끝나고 콘텐츠 영상으로 돌아오면, 영상이 재생되지 않고 그대로 멈추는 현상이 발생했습니다. <video> 엘리먼트가 까맣게 굳어버린 상태에서 currentTime이 더 이상 흐르지 않습니다.
원인을 좁히기 위해 가장 먼저 살펴본 값은 VideoElement.readyState 였습니다. 영상이 정상적으로 재생되는 동안에는 이 값이 3(HAVE_FUTURE_DATA)과 4(HAVE_ENOUGH_DATA)를 오가며 디코더가 충분한 데이터를 들고 있다는 신호를 보내고 있었습니다. 그런데 광고 영상을 재생하고 콘텐츠로 돌아왔을 때 readyState를 다시 찍어보면 1(HAVE_METADATA)으로 되돌아가 있었습니다.
readyState가 1이라는 건 메타데이터는 파싱했지만 현재 시각에 재생할 데이터가 없다는 뜻입니다. 즉 광고를 거치는 동안 콘텐츠 영상이 디코더에 채워둔 버퍼가 비워져버린 상태라고 해석할 수 있었습니다. 단일 디코더가 광고에 점유되었다가 돌아온 사이, 콘텐츠 쪽 버퍼가 일관성을 잃고 비어버린 것 같다는 가설이 자연스럽게 따라왔습니다.
콘텐츠 영상이 다시 잘 재생되도록 만들기 위해 다양한 방면으로 접근을 시도했습니다.
스마트 TV의 단일 디코더 특성을 고려하면, 광고와 콘텐츠의 전환은 반드시 순차적으로 이루어져야 합니다. 첫 번째 접근은 이 전환 과정의 Promise 체인을 섬세하게 다듬는 것이었습니다.
전환 흐름을 양방향으로 정리하면 다음과 같습니다.
콘텐츠 → 광고로 진입할 때
광고 → 콘텐츠로 복귀할 때
load하고 play.핵심은 양방향 모두 해제 → (해제 완료 확인) → 다음 로드 순서가 깨지지 않도록 만든다는 점입니다. 이 과정에서 비동기 작업이 적절히 순차적으로 이루어지지 않으면, 디코더 버퍼에 Stuck이 발생하여 그 자체가 다음 재생 실패의 원인이 됩니다. 광고 재생이 시작될 때 콘텐츠 쪽 디코더가 깔끔히 비워지지 않은 상태이거나, 콘텐츠를 다시 로드할 때 광고 쪽 리소스가 아직 살아있는 상태라면, 단일 디코더 위에서 두 스트림의 상태가 충돌하게 되는 셈입니다.
Shaka Player의 unload()와 load()는 모두 Promise를 반환하기 때문에, 이를 활용해 전환 단계마다 명시적으로 await을 걸어주는 식으로 정리했습니다.
첫 번째 접근 방식을 적용한 후에 현상이 많이 나아짐을 확인할 수 있었습니다. 광고 직후에 콘텐츠가 멈추는 빈도가 눈에 띄게 줄었습니다.
그러나 1차 접근만으로 완전히 해결된 것은 아니었습니다. 콘텐츠 영상을 재생한 이후 일정 시간이 지나서 광고 영상을 재생하고 돌아오면, 다시 콘텐츠 영상이 재생되지 못하는 이슈가 발생했습니다.
이번 증상은 두 가지 특징이 있었습니다.
즉 같은 사용자 시나리오라도 비DRM 콘텐츠에서는 거의 보이지 않다가, DRM이 걸린 콘텐츠에서 더 자주 재현되었습니다. 여기서 원인이 디코더 자체가 아니라 DRM 파이프라인 쪽에 있을 수 있다는 가설을 세웠습니다.
분석 결과 원인은 DRM 라이선스 토큰의 유효기간 만료였습니다. 흐름을 정리하면 다음과 같습니다.
unload()했다가 광고 종료 후 다시 load().unload() → load() 사이클이 일어나면 DRM 라이선스 핸드셰이크가 처음부터 다시 진행됩니다.다시 말해 1차 접근이 디코더 레벨의 순차성을 정리했다면, 2차 이슈는 그 위에 얹혀 있는 DRM 라이선스 라이프사이클이 광고 전환과 충돌하는 문제였습니다.
DRM 콘텐츠에서 unload() → load()사이클이 일어날 때는 라이선스 핸드셰이크가 다시 진행된다는 점을 항상 염두에 둬야 합니다. 단순히 영상 소스만 갈아끼우는 것이 아니라, 라이선스 협상이라는 별도의 비동기 흐름이 같이 따라붙습니다.
해결책은 명확했습니다. 라이선스 협상이 진행되는 시점에 항상 Fresh 상태의 토큰을 취하여 협상을 완료하도록 처리했습니다.
핵심은 토큰을 한 번 받아두고 재사용하는 것이 아니라, 라이선스 요청 직전에 매번 새 토큰을 발급받도록 흐름을 바꾼 것입니다. 이렇게 하면 콘텐츠 시청 시간이 아무리 길어졌더라도, 광고 후 재진입 시점의 라이선스 협상은 항상 유효한 토큰으로 진행됩니다.
이 처리를 적용한 뒤로는 시간이 누적된 상황에서도 광고 → 콘텐츠 전환이 안정적으로 동작하게 되었습니다.
지금까지 재생이 멈추는 이슈를 해결하기 위한 접근 및 시도를 다루었으며, 스마트 TV에서 CSAI를 다루며 알게 된 사실들 중 앞으로 이 영역을 개발할 분들에게 도움이 될 만한 내용을 정리해 둡니다.
즉 같은 코드를 PC Chrome에서 잘 돌리더라도 5년 전 출시된 TV에서 잘 돈다는 보장이 없습니다. 출시 연도별 TV가 어떤 Chromium 버전을 쓰는지를 미리 파악해 두는 편이 안전합니다.
내장 Chromium 버전이 낮으므로, 최신 자바스크립트 문법과 API를 그대로 쓰면 SyntaxError 또는 ReferenceError가 그대로 노출됩니다. 하위 버전 호환을 위해 Babel, Polyfill 같은 처리가 요구됩니다.
빌드 타깃을 결정할 때 가장 보수적인 라인을 지원 디바이스 중 가장 낮은 Chromium 버전에 맞추는 것이 안전합니다.
video.play()의 반환값 차이가장 함정처럼 자주 마주치는 케이스입니다.
HTMLMediaElement.prototype.play()는 Promise를 반환합니다.undefined가 반환됩니다.이 부분을 염두에 두지 않으면 무심코 다음과 같이 작성하기 쉽습니다.
낮은 버전 Chromium에서는 play()의 반환값이 undefined이므로 위 코드는 undefined.then 호출로 인해 런타임 오류가 발생합니다. 안전한 패턴은 반환값을 한 번 받아 확인한 뒤 분기하는 것입니다.
video.play() 한 줄짜리 코드지만, 스마트 TV에서는 이 한 줄이 앱 전체의 런타임 안정성을 좌우하기도 합니다.
스마트 TV에서 CSAI를 적용하며 마주친 두 단계의 함정을 한 번에 정리하면 다음과 같습니다.
unload() → load() 사이클은 라이선스 핸드셰이크를 다시 트리거하기 때문에, 그 시점에 사용되는 토큰은 항상 최신이어야 한다. 시청 시간이 누적되어 토큰이 만료된 상태에서 광고 후 재진입하면 라이선스 협상에 실패해 콘텐츠가 다시 재생되지 못한다.한 줄로 요약하면 다음과 같습니다.
"스마트 TV의 CSAI는 디코더 순차성과 DRM 토큰의 신선도, 두 축을 함께 관리해야 안정적으로 동작한다."
<video> 또는 <video> + <audio> 태그 사용이 공식적으로 지원되지 않는다는 LG 공식 안내<video> 엘리먼트와 AVPlay 인스턴스를 동시에 재생할 수 없음을 명시한 Samsung 공식 FAQtemporary 타입이며, persistent-license로 옵션을 켜야 세션을 재사용할 수 있다는 공식 안내. 바꿔 말해 기본 설정에서는 unload() → load() 마다 라이선스 협상이 새로 트리거된다는 근거