Chromecast로 영상을 재생하면 TV에서 재생을 담당하는 것은 Cast Web Receiver 앱입니다. 그런데 네트워크가 충분히 빠른데도 리시버의 화질이 1080p까지 올라가지 않고 480p나 720p에 머무는 문제가 있었습니다.
원인을 따라가 보니 문제는 우리 앱 코드가 아니라 Google의 Cast 리시버 SDK가 내부에서 사용하는 Shaka Player에 있었습니다. 이 글은 그 원인을 분석해 Shaka Player 저장소에 버그 리포트(#10938)를 올리고, 메인테이너의 수정이 다음 릴리스에 반영되기까지의 과정을 정리한 기록입니다.
| 시점 (UTC) | 진행 |
|---|---|
| 11월 9일 09:37 | 이슈 #10938 등록 |
| 11월 10일 08:34 | 메인테이너가 component: HLS 라벨 지정 |
| 11월 10일 08:54 | 수정 PR #10941 생성 |
| 11월 10일 12:21 | 리뷰 승인 후 머지, 이슈 종료 |
| 11월 10일 12:22 | 5.4.0 릴리스 PR에 포함 |
이슈를 올린 지 약 27시간 만에 수정까지 마무리되었습니다.
Cast Web Receiver는 Google의 CAF(Cast Application Framework) 리시버 SDK로 만듭니다. CAF는 useShakaForHls 옵션을 켜면 HLS를 SDK에 내장된 Shaka Player로 재생합니다. 즉 리시버 앱은 Shaka Player를 직접 생성하지 않고, CAF가 만든 Player 인스턴스를 CAF의 설정을 거쳐 사용합니다.
문제가 발생한 환경은 다음과 같습니다.
| 항목 | 값 |
|---|---|
| 기기 | Chromecast with Google TV (4K) |
| CAF 리시버 SDK | 3.0.0156 |
| SDK에 포함된 Shaka Player | 4.15.56 |
| 콘텐츠 | HLS, MPEG-TS 세그먼트, AVC + AAC, DRM 없음 |
| Variant | 270p, 360p, 480p, 720p, 1080p |
Shaka Player의 대역폭 추정치는 약 30~37Mbps였고, 1080p variant가 선언한 BANDWIDTH는 약 10.7Mbps였습니다. 그런데도 ABR은 720p와 1080p에 머물지 못했습니다. 화질 상한은 아직 한 번도 로드하지 않은 variant에 묶였고, 세션마다 480p가 되기도 하고 720p가 되기도 했습니다.
디버그 오버레이로 플레이어 내부를 확인한 결과는 다음과 같았습니다.
video.mimeType은 video/mp2t였습니다. 세그먼트 확장자 .ts에서 결정된 값입니다.video/mp4였습니다.video/mp2t와 video/mp4)뿐이었습니다.Shaka Player의 mediaSource.codecSwitchingStrategy는 서로 다른 코덱 사이를 전환하는 방식을 정합니다. SMOOTH는 SourceBuffer.changeType()으로 끊김 없이 전환하고, RELOAD는 MediaSource를 다시 만들어 전환합니다.
CAF는 원격 플래그 use_change_type_for_codec_switching이 켜져 있지 않으면 Shaka를 설정할 때 이 값을 RELOAD로 덮어씁니다. 따라서 Shaka로 HLS를 재생하는 CAF 리시버는 기본적으로 RELOAD로 동작합니다.
이 설정이 중요한 이유는 ABR 후보군을 만드는 기준이 바뀌기 때문입니다.
SMOOTH가 아니면 compareCodecs가 true가 됩니다. 이때 AdaptationSet은 기준(root) variant와 서로 전환 가능한 variant만 후보로 남깁니다.
HLS의 마스터 플레이리스트에는 variant별 코덱 정보는 있지만 세그먼트 컨테이너 정보는 없습니다. Shaka Player는 각 variant의 미디어 플레이리스트를 실제로 필요할 때 로드합니다. 그 전까지 비디오 스트림의 mimeType은 추측값으로 채웁니다.
로드하기 전 비디오 스트림에는 guessMimeTypeBeforeLoading_()이 값을 정하지 못해 이 폴백이 적용되고, 모든 비디오 스트림이 video/mp4로 시작합니다. 미디어 플레이리스트를 로드하면 .ts 확장자를 보고 video/mp2t로 바뀝니다. 하지만 다른 variant의 추측값은 고쳐지지 않습니다.
두 스트림이 전환 가능한지는 AdaptationSet.canTransitionBetween_이 판단합니다.
mimeType 문자열이 다르면 코덱을 비교하기도 전에 false를 돌려줍니다. 같은 MPEG-TS, 같은 AVC와 AAC 조합이라도 하나는 로드되어 video/mp2t, 다른 하나는 아직 video/mp4라면 전환할 수 없는 스트림으로 판단합니다.
이 조건들이 맞물리면 사다리가 위에서부터 줄어듭니다.
video/mp2t가 됩니다.video/mp4입니다. canTransitionBetween_()이 false를 돌려주므로 1080p는 후보에서 빠집니다.codecSwitchingStrategy를 smooth로 바꾸면 모든 variant가 다시 후보가 되어 같은 기기와 스트림에서 1080p까지 올라갔습니다. 다만 이 우회책은 버전에 따라 막혀 있었습니다. 5.3.x와 main에서는 Chromecast의 supportsSmoothCodecSwitching()이 Linux 기반 기기에서 false를 돌려주므로, 그런 기기에서는 smooth를 설정해도 위 문제를 피할 수 없습니다.
이슈 #10938은 Shaka Player의 Bug Report 템플릿을 따라 작성했습니다. 메인테이너가 바로 재현하고 판단할 수 있도록 다음 내용을 담았습니다.
codecSwitchingStrategy: 'reload' 하나이고, 나머지 설정은 영향이 없다는 점을 밝혔습니다.RELOAD에서도 서로 전환 가능해야 한다는 기대와, 실제로 관찰한 mimeType과 AdaptationSet 구성을 나눠 적었습니다.hls_parser.js의 추측 mimeType, preference_based_criteria.js의 compareCodecs, adaptation_set.js의 mimeType 비교입니다. 4.15.56, 5.3.4, main에서 같은 코드인 것도 확인했습니다.메인테이너가 올린 PR #10941 "fix(ABR): Compare the transmuxed container when building adaptation sets"는 리뷰 승인을 받아 머지되었습니다.
수정은 이슈에서 제안한 두 방법이 아니었습니다. 메인테이너는 추측값을 특별하게 다루는 대신 비교 기준 자체를 바꿨습니다. 매니페스트에 적힌 mimeType이 아니라, SourceBuffer가 실제로 받는 타입을 비교합니다.
getFullOrConvertedType()은 트랜스먹서가 처리할 수 있는 타입이면 변환된 타입을 돌려줍니다. MPEG-TS를 MP4로 트랜스먹싱하는 환경에서는 video/mp2t 스트림도 video/mp4로 비교되므로, 로드한 TS variant와 아직 video/mp4인 variant가 같은 AdaptationSet에 남습니다.
PR 설명에 따르면 이는 MediaSourceEngine이 리셋 여부를 결정할 때 쓰는 기준과 같습니다. 트랜스먹싱한 TS 스트림과 같은 코덱의 MP4 스트림 사이에는 리셋이 필요 없으니, 후보군에서 제외할 이유도 없다는 판단입니다. 추측값이라는 HLS만의 사정에 기대지 않고, 전환 가능 여부를 실제 재생 경로 기준으로 맞춘 해결입니다.
PR에는 다음 테스트가 추가되었습니다.
video/mp2t, 나머지는 video/mp4인 매니페스트에서 모든 variant가 후보에 남는지 확인TS를 트랜스먹싱하지 않는 플랫폼에서는 회귀 테스트를 건너뛰도록 되어 있습니다. 변환 여부에 따라 결과가 달라지는 동작이기 때문입니다.
수정 커밋은 머지 직후 릴리스 자동화가 관리하는 5.4.0 릴리스 PR의 Bug Fixes 항목에 포함되었습니다.
ABR: Compare the transmuxed container when building adaptation sets (#10941)
이 글을 쓰는 시점에는 릴리스 PR이 열려 있고 5.4.0은 아직 배포되지 않았습니다. 따라서 체인지로그 항목은 배포 시 바뀔 수 있습니다.
Cast 리시버에서 이 수정을 받으려면 CAF SDK에 포함된 Shaka Player 버전이 올라가야 합니다. 문제가 발생한 환경의 SDK는 Shaka Player 4.15.56을 사용했고, 이 수정은 5.4.0 릴리스에 포함될 예정입니다. 리시버 앱에서 Shaka Player 버전을 직접 정하지 않는다면, Cast SDK가 수정된 버전을 포함할 때까지 기다려야 합니다.
이슈와 PR의 기록을 돌아보면, 리포트가 빠르게 수정으로 이어진 데에는 몇 가지 요소가 있었습니다.
codecSwitchingStrategy 하나만 문제라는 점을 밝혀, 메인테이너가 Cast 환경 없이도 판단할 수 있었습니다.main에서도 같은 문제가 남아 있다는 점을 보였습니다.이번 기여는 의도적으로 원인 분석과 제보까지만 진행했습니다. 이슈 템플릿의 "PR을 보낼 계획인가요?"라는 질문에도 "No"라고 답했습니다.
버그 제보자인 저는 문제가 드러난 코드 경로, 즉 Shaka Player의 단편적인 부분만 이해하고 있었습니다. 같은 비교 로직이 다른 포맷이나 기기, 설정 조합에서 어떻게 쓰이는지까지 알지 못한 채 코드를 고치면, 눈앞의 증상만 막는 수정이 될 수 있습니다. 그래서 수정 방향은 제안으로만 남기고, 이슈 해결과 코드 변경은 프로젝트 전체를 이해하는 주 메인테이너에게 맡기는 편이 더 가치 있는 업데이트로 이어질 것이라고 판단했습니다.
결과도 그랬습니다. 메인테이너는 제안한 두 방법 대신 SourceBuffer가 실제로 받는 타입을 비교하는 더 일반적인 방식으로 고쳤습니다. HLS의 추측 mimeType이라는 특정 상황이 아니라, MediaSourceEngine과 같은 기준으로 전환 가능 여부를 판단하도록 바로잡은 수정입니다. 제보자는 원인을 정확히 전달하고, 해결은 프로젝트를 가장 잘 아는 사람이 맡는 역할 분담도 오픈소스에 기여하는 좋은 방법이라는 것을 이번 과정에서 확인했습니다.