ID3 Tag는 JSON base64가 아니었다
StreamLive로 라이브 채널을 운영하다 보면 광고 마커나 이벤트 알림처럼, 재생 중인 스트림 안에 시간과 함께 값을 실어 보내야 하는 경우가 생깁니다. 이런 값을 timed metadata라고 부르고, HLS/TS 계열에서는 보통 ID3 tag 형태로 세그먼트 안에 심습니다. 이 기능을 테스트하다가 예상과 전혀 다른 결과를 마주쳤습니다.
처음에는 단순하게 생각했습니다. "JSON 하나 만들고, base64로 인코딩해서 넣으면 되지 않을까?" 싶었는데, 실제로는 되지 않았습니다. 정확히 말하면 API 요청 자체는 성공한 것처럼 보였지만, 세그먼트를 까보거나 플레이어에서 이벤트를 들어보면 기대한 값이 잡히지 않았습니다.
이 지점은 이름 때문에 오해하기 쉽습니다. metadata라고 하니 아무 문자열이나 넣어도 될 것 같지만, 실제 HLS/TS 구조에서는 정해진 형식을 따라야 합니다.
처음 넣으려던 값
처음에는 대략 이런 JSON을 넣으려고 했습니다.
{
"eventType": "ad_marker",
"adStartTime": "2026-05-05T10:00:00Z",
"markerId": "JUSTIN_TEST_001"
}그리고 이 JSON 문자열을 base64로 바꿔서 payload에 넣었습니다.
eyJldmVudFR5cGUiOiJhZF9tYXJrZXIiLCJhZFN0YXJ0VGltZSI6IjIwMjYtMDUtMDVUMTA6MDA6MDBaIiwibWFya2VySWQiOiJKVVNUSU5fVEVTVF8wMDEifQ==겉으로 보기엔 문제없어 보이지만, ID3 tag는 "base64로 감싼 문자열"을 뜻하지 않습니다.
ID3는 구조를 가진 바이너리다
ID3는 단순한 payload가 아니라, 버전과 flags, size, frame header, frame body를 가진 바이너리 포맷입니다. 아주 단순화하면 이런 구조입니다.
ID3 tag
-> ID3 header
-> Frame header
-> Frame payload예를 들어 TXXX frame을 쓴다면 내부는 이런 형태가 됩니다.
ID3
version: 2.4.0
frame: TXXX
encoding: UTF-8
description: ""
value: JUSTIN_TEST_001즉, JSON 문자열 자체를 base64로 넣는 것이 아니라, ID3 v2.4 spec에 맞는 완성된 ID3 tag 바이너리를 만든 뒤 그 바이너리를 base64로 넣어야 합니다. 이 차이가 핵심입니다.
틀린 접근:
JSON string -> base64
맞는 접근:
JSON or value -> ID3 tag binary -> base64TS에서 어떻게 보였나
실제로 MPEG-TS를 분석해보면 metadata PID가 따로 보일 수 있습니다. 예시는 다음과 같습니다.
PID Usage
0x0100 AVC video
0x0101 MPEG-2 AAC Audio
0x01F4 SCTE 35 Splice Info
0x0201 MetaData in PES packets
0x1001 PMT여기서 ID3는 보통 PES packet 안에 들어가고, 구체적으로는 private_stream_1 계열로 들어오는 경우가 많습니다. 분석 결과를 풀면 이런 구조가 됩니다.
PES header
stream_id = 0xBD
PTS 포함
ID3 tag
magic = ID3
version = 2.4.0
frame = TXXX
value = JUSTIN_TEST_001이렇게 확인되면 플레이어에서는 ID3 TXXX frame을 잡아서 value를 읽으면 됩니다.
strings로도 힌트를 볼 수 있다
간단히 payload 흔적만 보고 싶다면 strings도 쓸 수 있습니다.
strings segment.ts | grep JUSTIN물론 이건 정식 분석 도구는 아니라서 압축이나 바이너리 구조에 따라 보이지 않을 수도 있지만, 값이 아예 들어갔는지를 빠르게 확인할 때는 유용합니다. 제대로 확인하려면 TS 분석 도구로 PID와 PES를 확인하는 것이 정확합니다.
tsanalyze segment.ts원격 URL을 바로 넣으면 도구에 따라 동작하지 않을 수 있으니, 먼저 파일로 받아서 검사하는 것이 안전합니다.
curl -L -o segment.ts "https://example.com/live/segment.ts"
tsanalyze segment.ts플레이어에서는 무엇을 들어야 하나
브라우저 플레이어에서는 ID3 metadata event를 받을 수 있는지부터 확인해야 합니다. HLS 플레이어마다 이벤트 이름은 다르지만, 핵심 흐름은 같습니다.
HLS segment demux
-> ID3 metadata parse
-> TXXX frame extract
-> application callback여기서 JSON을 바로 기대하면 안 됩니다. ID3 frame 안의 value로 JSON 문자열을 넣었다면, 그 value를 다시 JSON.parse 해야 실제 데이터를 꺼낼 수 있습니다.
const value = id3Frame.value;
const payload = JSON.parse(value);정리
결론은 간단합니다. ID3 tag는 JSON base64가 아닙니다. ID3 spec에 맞는 tag 바이너리를 만들고, 그 바이너리를 base64로 넣어야 합니다. JSON 문자열을 그대로 base64로 감싸면 인코딩 자체는 된 것처럼 보이지만, 플레이어와 demuxer 입장에서는 정상적인 ID3 tag가 아닙니다.
metadata라는 표현이 추상적이라 문서만 보면 놓치기 쉬운 지점입니다. StreamLive처럼 실제 라이브 파이프라인에서 timed metadata를 다룰 때는, TS 안의 PID와 PES, ID3 frame까지 내려가서 확인해야 원인이 보입니다.