블로그 목록
Media25분 읽기

Agora SEI 메타데이터 — H.264·H.265 Payload 해석

H.264·H.265 SEI NAL unit의 payload type과 payload size를 읽는 방법을 설명합니다. Agora Media Push가 제공하는 metadata 형식은 공식 계약에 맞춰 처리하고, Annex B와 AVCC 입력을 구분해 emulation-prevention byte 제거, payload type 100·송출 경로·JSON schema를 확인합니다.

SEIH.264H.265NALAgora Media Push메타데이터Annex BAVCCRTMPFLVSwift
목차(40개 항목)
  1. 0. 핵심 명제 — SEI는 비디오 bitstream에 결합된 메타데이터
  2. 1. SEI란 무엇인가 — H.264/H.265 표준의 일부
  3. 2. 왜 시그널링 대신 SEI를 쓰나
  4. 3. Agora SEI의 JSON 구조
    1. 최상위 필드
    2. `canvas` 필드
    3. `regions` 배열의 각 요소 (호스트별)
    4. 실제 페이로드 예시
  5. 4. SEI 바이너리 구조 — H.264 표준 + Agora 커스텀
    1. 바이트별 의미
    2. NAL 헤더 0x06 분해
    3. payload_type 100의 정확한 위치
    4. NAL 스트림에서의 연관 관계
  6. 5. 0xFF 누적 길이 인코딩
    1. 인코딩 규칙
    2. 예시
    3. 파서 의사 코드
  7. 6. NAL 스트림 포맷 — Annex B vs AVCC
    1. Annex B 포맷 (raw byte stream·일부 MPEG-TS 처리 경로)
    2. length-prefixed 포맷 (MP4·FLV 처리 경로)
  8. 7. 시청자 측 파싱 흐름
    1. Swift / iOS 파서 예시
    2. 주의사항 — `emulation_prevention_byte`
  9. 8. 활용 패턴
    1. 8-1. 클릭 가능 영역 매핑 (Agora의 표준 사용 케이스)
    2. 8-2. `app_data` 필드 활용 — 자유 메타데이터
    3. 8-3. 정확도 한계
  10. 9. SEI vs 다른 동기화 메커니즘 — 의사결정
  11. 10. FAQ
    1. Q1. SEI를 쓰면 시그널링 인프라 필요 없나?
    2. Q2. SEI가 디코딩에 영향 주나?
    3. Q3. SEI가 매 프레임마다 박히나?
    4. Q4. RTC 원본 스트림에도 SEI가 있나?
    5. Q5. payload_type 100을 쓰면 다른 디코더와 충돌하나?
    6. Q6. H.265(HEVC)에서도 같은 형식인가?
  12. 11. SA 체크리스트
  13. 12. 한 줄 결론
    1. 한 장 요약
  14. 관련 글
  15. 참고 자료

"Agora Media Push로 합성 송출 받는 시청자 앱에서, 화면에 누가 어디 있는지 어떻게 알아내요?", "RTM으로 보내려니 비디오 프레임과 타이밍이 어긋나는데요". 답은 비디오 스트림 안에 메타데이터를 박아 보내는 SEI (Supplemental Enhancement Information).

이 글은 H.264/H.265 표준의 SEI가 무엇인지, Agora Media Push가 어떤 JSON을 어떤 바이트 포맷으로 박아 주는지, 시청자 측 파서가 어떻게 풀어내야 하는지를 정리합니다.


0. 핵심 명제 — SEI는 비디오 bitstream에 결합된 메타데이터

SEI는 H.264/H.265 bitstream 안에 부가 정보를 싣는다. 별도 시그널링보다 비디오 timeline에 결합하기 쉽지만, 중간 트랜스코더가 SEI를 보존하거나 클라이언트가 처리한다고 보장되지는 않는다.

흔한 오해와 정정:

오해정확한 이해
SEI는 Agora 전용 기술H.264/H.265(및 MPEG-2 video)의 표준 기능. 단 모든 비디오 코덱이 지원하는 건 아님 — VP9/AV1 등에는 SEI가 없고 각각 WebM BlockAddID(ITU-T T.35) / Metadata OBU(metadata_itut_t35) 같은 별도 메타데이터 메커니즘을 씀
SEI를 쓰면 시그널링 불필요SEI는 레이아웃 정보, 시그널링은 업링크 데이터 — 둘은 다른 채널
RTMP 위에 SEI 못 박음RTMP/FLV는 AVCC 포맷이지만 SEI NAL은 동일하게 실림
모든 디코더가 SEI를 앱에 전달SEI 처리는 선택적이며 플레이어·디코더 API마다 노출 범위가 다름

1. SEI란 무엇인가 — H.264/H.265 표준의 일부

SEI = Supplemental Enhancement Information. ITU-T H.264 / ISO/IEC 14496-10의 Annex D에 정의된 부가 정보 영역.

핵심 특성:

특성설명
표준성H.264/H.265 표준에 정의 — 모든 컴플라이언트 인코더/디코더가 알고 있는 형식
비강제성디코더는 SEI를 처리할 의무가 없음 — 무시해도 디코딩 정상
비디오 결합access unit 또는 bitstream 안에서 영상 timing과 함께 운반 가능
페이로드 자유도payload_type별로 표준 의미가 정해져 있고, type 5 = user_data_unregistered가 자유 데이터용

Annex D의 의미: SEI 메시지 문법과 semantics를 정의하지만, 모든 메시지를 애플리케이션에 노출해야 한다는 뜻은 아닙니다. 필요한 SEI가 전체 ingest→transcode→package→player 경로에서 보존되는지 시험합니다.


2. 왜 시그널링 대신 SEI를 쓰나

라이브 송출에서 자주 부딪히는 문제:

[발신측]
   │
   ├─ 비디오 프레임 t=10.0s ──→ [네트워크] ──→ [시청자] (예: 12.3s 도착, 합성된 화면)
   │
   └─ 시그널링 메시지 ──────→ [네트워크] ──→ [시청자] (예: 12.1s 또는 12.7s 도착, 별도 채널)

별도 시그널링과 비디오는 서로 다른 네트워크 경로를 거치므로 타이밍이 맞지 않음. "이 프레임에 호스트 A가 좌상단에 있다"는 정보가 비디오보다 0.4초 늦게 도착하면, 시청자 UI가 잘못된 위치를 클릭 가능 영역으로 인식.

SEI 해결책: 메타데이터를 NAL 스트림 안에 박아서 같은 프레임과 같이 도착시킴.

[발신측]
   │
   └─ NAL 스트림: [SEI: 레이아웃] [Slice: 비디오] ──→ [네트워크] ──→ [시청자]
                                                                       └─ 같은 bitstream에서 해석

뉘앙스: SEI의 적용 시점은 SEI message semantics와 access unit 배치에 따라 해석해야 합니다. 네트워크·컨테이너·트랜스코딩 단계에서 누락될 수 있으므로 ts와 비디오 timestamp를 함께 기록하고 폴백을 둡니다.


3. Agora SEI의 JSON 구조

Agora Media Push는 트랜스코딩된 H.264/H.265 스트림에 자동으로 SEI를 추가합니다 — 클라이언트 별도 설정 불필요.

최상위 필드

{
  "canvas":   { ... 캔버스 정보 ... },
  "regions":  [ ... 호스트별 레이아웃 ... ],
  "ver":      20190611,
  "ts":       1683195420123,
  "app_data": "사용자 정의"
}
필드설명
canvas전체 합성 화면(캔버스) 정보
regions캔버스 위에 배치된 각 호스트의 레이아웃 (배열)
verSEI 프로토콜 버전 (현재 문서 값 "20190611")
ts인코딩 시점의 타임스탬프 (ms)
app_data사용자 정의 추가 정보 (transcodingExtraInfo에 대응)

canvas 필드

키의미LiveTranscoding 대응
w캔버스 너비 (px)width
h캔버스 높이 (px)height
bgnd배경색 (RGB hex)backgroundColor

regions 배열의 각 요소 (호스트별)

키의미TranscodingUser 대응
uid호스트 UIDuid
suid문자열 계정 (선택)—
alpha투명도 [0.0 ~ 1.0]alpha
zorder레이어 순서 [0 ~ 100]zOrder
volume음량 dB [0 ~ 100]—
x, y좌상단 기준 위치x, y
w, h비디오 프레임 크기 (px)width, height

실제 페이로드 예시

{
  "canvas": {
    "w": 1920,
    "h": 1080,
    "bgnd": "#000000"
  },
  "regions": [
    {
      "uid": 1001,
      "x": 0, "y": 0, "w": 960, "h": 540,
      "alpha": 1.0, "zorder": 1, "volume": 50
    },
    {
      "uid": 1002,
      "x": 960, "y": 0, "w": 960, "h": 540,
      "alpha": 1.0, "zorder": 1, "volume": 50
    },
    {
      "uid": 1003,
      "x": 0, "y": 540, "w": 1920, "h": 540,
      "alpha": 1.0, "zorder": 1, "volume": 50
    }
  ],
  "ver": "20190611",
  "ts": 1683195420123,
  "app_data": "{\"chapter\":\"intro\"}"
}

이 JSON 전체가 SEI 페이로드로 들어갑니다.


4. SEI 바이너리 구조 — H.264 표준 + Agora 커스텀

SEI의 첫 몇 바이트가 핵심입니다. 예시:

06 64 bd [...JSON content...]

바이트별 의미

바이트의미
06NAL 헤더 — forbidden_zero_bit(1) + nal_ref_idc(2) + nal_unit_type(5) = 0 00 00110 → SEI
64payload_type = 100 (Agora 커스텀 선택)
bdpayload_size = 189 바이트 (이 예시 한정 — 단일 바이트로 표현 가능한 길이)

NAL 헤더 0x06 분해

0x06 = 0000 0110
       │ ││ │└┴┴── nal_unit_type (5 bits) = 0b00110 = 6 → SEI
       │ └┴── nal_ref_idc (2 bits) = 0 (SEI는 비참조)
       └── forbidden_zero_bit = 0

NAL unit type 6 = SEI, 표준 정의. RFC 6184도 SEI NAL은 nal_ref_idc=0이어야 한다고 명시.

payload_type 100의 정확한 위치

Agora 공식 문서는 이 형식에서 payload type을 user-defined 값 100으로 정의합니다. 일반적인 vendor-neutral 사용자 데이터에는 type 5 user_data_unregistered와 UUID를 쓰는 방식도 있습니다.

따라서 payload type 100만 보고 모든 H.264 스트림을 Agora 형식으로 해석하지 말고, 송출 경로와 JSON schema를 함께 검증합니다.

payload_type표준 의미
0buffering_period
1pic_timing
5user_data_unregistered (UUID 네임스페이싱 — 표준 커스텀 데이터)
22post_filter_hint
100표준 미정의값 (Agora가 점유, 공식 문서상 user-defined)
136time_code

NAL 스트림에서의 연관 관계

[Slice: 프레임 N-1] [SEI: payload] [Slice: 프레임 N] [Slice: 프레임 N+1]
                    ↑ 프레임 N과 묶여서 도착

이 배열은 설명용 예시입니다. 실제 bitstream에는 여러 SEI message가 있을 수 있고, H.264/H.265의 prefix·suffix 및 access unit 규칙에 따라 위치가 달라집니다.


5. 0xFF 누적 길이 인코딩

payload_type과 payload_size 모두 H.264 표준의 동일한 가변 길이 인코딩을 씁니다.

인코딩 규칙

length = 0;
byte = read_byte();
while (byte == 0xFF) {
    length += 255;
    byte = read_byte();
}
length += byte;

→ 0xFF가 나오면 255씩 누적, 0xFF가 아닌 마지막 바이트의 값을 더해 종료.

예시

길이인코딩분해
234EA234 < 255 → 단일 바이트
572FF FF 3E2 × 255 + 62 = 572
922FF FF FF 9D3 × 255 + 157 = 922
1500FF FF FF FF FF E15 × 255 + 225 = 1500

파서 의사 코드

def read_variable_length(buf, pos):
    length = 0
    while buf[pos] == 0xFF:
        length += 255
        pos += 1
    length += buf[pos]
    pos += 1
    return length, pos

# SEI 시작 위치(NAL 헤더 다음)에서:
payload_type, pos = read_variable_length(buf, pos)
payload_size, pos = read_variable_length(buf, pos)
payload = buf[pos:pos + payload_size]

실제 파서는 NAL header 차이, RBSP trailing bits, 여러 SEI message, buffer bounds, emulation-prevention byte 제거를 추가로 처리해야 합니다.


6. NAL 스트림 포맷 — Annex B vs AVCC

같은 SEI NAL이라도 어떤 컨테이너에 담기느냐에 따라 framing이 다릅니다.

Annex B 포맷 (raw byte stream·일부 MPEG-TS 처리 경로)

[start code: 00 00 00 01] [NAL header: 06] [payload_type] [payload_size] [payload]
  • 4바이트 start code(00 00 00 01) 또는 3바이트(00 00 01)로 NAL 경계 표시
  • byte stream을 스캔해서 start code 찾으면 NAL 시작

length-prefixed 포맷 (MP4·FLV 처리 경로)

[length: 4 bytes big-endian] [NAL header: 06] [payload_type] [payload_size] [payload]
  • start code 없음 — avcC의 lengthSizeMinusOne에 따른 길이 prefix로 NAL 크기 명시(흔히 4바이트)
  • length만큼 읽고 다음 NAL로 점프

자주 틀리는 포인트: transport 이름만 보고 Annex B/length-prefix를 단정하면 안 됩니다. RTMP의 AVC video packet을 FLV demux한 뒤 codec configuration에서 NAL length size를 읽어 경계를 찾습니다.

컨테이너포맷NAL 경계 표시
MPEG-TS의 H.264 byte streamAnnex Bstart code (00 00 00 01)
MP4, MOVAVCC4바이트 length prefix
RTMP, FLVAVCC4바이트 length prefix
RTP/H.264RFC 6184 packetizationdepacketize 후 NAL unit 복원; start code는 RTP 규격 일부가 아님

7. 시청자 측 파싱 흐름

전체 파서 흐름을 한 장에 정리.

[RTMP 도착]
   ↓
FLV 디먹스 → AVCC 포맷 NAL 단위 추출
   ↓
[NAL: 06 64 bd ... ]
   ↓
첫 바이트 0x06 → SEI NAL
   ↓
payload_type 읽기 (0xFF 누적)
   ↓
payload_type == 100? (Agora 커스텀)
   ↓ Yes
payload_size 읽기 (0xFF 누적)
   ↓
payload bytes (= JSON 문자열)
   ↓
JSON.parse
   ↓
canvas / regions / ver / ts / app_data 추출
   ↓
UI 업데이트 (예: 클릭 가능 영역 매핑)

Swift / iOS 파서 예시

아래 코드는 흐름을 보여 주는 축약 예시입니다. production parser는 먼저 framing을 제거하고 EBSP→RBSP 변환과 bounds check를 끝낸 nalUnit만 받아야 합니다.

func parseAgoraSEI(nalUnit: Data) -> AgoraSEI? {
    var pos = 0
    
    // ① NAL header
    guard nalUnit[pos] == 0x06 else { return nil }  // SEI NAL?
    pos += 1
    
    // ② payload_type (0xFF accumulation)
    var payloadType = 0
    while nalUnit[pos] == 0xFF {
        payloadType += 255
        pos += 1
    }
    payloadType += Int(nalUnit[pos])
    pos += 1
    
    guard payloadType == 100 else { return nil }  // Agora 커스텀
    
    // ③ payload_size (0xFF accumulation)
    var payloadSize = 0
    while nalUnit[pos] == 0xFF {
        payloadSize += 255
        pos += 1
    }
    payloadSize += Int(nalUnit[pos])
    pos += 1
    
    // ④ payload (JSON)
    let payload = nalUnit.subdata(in: pos..<(pos + payloadSize))
    
    // ⑤ JSON 파싱
    return try? JSONDecoder().decode(AgoraSEI.self, from: payload)
}

struct AgoraSEI: Codable {
    let canvas: Canvas
    let regions: [Region]
    let ver: String
    let ts: Int64
    let app_data: String?
}

struct Canvas: Codable {
    let w: Int
    let h: Int
    let bgnd: String
}

struct Region: Codable {
    let uid: Int
    let x: Int
    let y: Int
    let w: Int
    let h: Int
    let alpha: Float
    let zorder: Int
    let volume: Int
}

주의사항 — emulation_prevention_byte

H.264 NAL 페이로드에서 00 00 00이 우연히 등장하면 인코더가 00 00 03 00으로 escape합니다 (start code 충돌 방지). 파싱 시 0x03을 제거해야 원본 페이로드가 됩니다.

def strip_emulation_prevention(buf):
    out = bytearray()
    i = 0
    while i < len(buf):
        if i + 2 < len(buf) and buf[i] == 0x00 and buf[i+1] == 0x00 and buf[i+2] == 0x03:
            out.append(0x00)
            out.append(0x00)
            i += 3
        else:
            out.append(buf[i])
            i += 1
    return bytes(out)

escape byte의 존재 여부는 실제 payload 바이트 패턴에 달려 있습니다. NAL의 EBSP를 RBSP로 되돌린 뒤 SEI payload를 해석합니다.


8. 활용 패턴

8-1. 클릭 가능 영역 매핑 (Agora의 표준 사용 케이스)

시청자 화면(합성된 1920x1080):
┌─────────────────────────────────────┐
│  [호스트 A: uid=1001]               │
│  (0,0) ~ (960,540)                  │
│ ┌──────────┐                        │
│ │ A 화면   │ [호스트 B: uid=1002]   │
│ └──────────┘ (960,0) ~ (1920,540)   │
│                                     │
│  [호스트 C: uid=1003]               │
│  (0,540) ~ (1920,1080)              │
└─────────────────────────────────────┘

시청자가 좌측 상단을 탭 → SEI의 regions 배열 탐색 →
matching region.uid = 1001 → "호스트 A 클릭" 이벤트 발화

8-2. app_data 필드 활용 — 자유 메타데이터

app_data는 고객이 자유롭게 쓸 수 있는 필드. 예:

활용app_data 예시
자막 큐{"caption":"안녕하세요","start":1683195420123}
광고 트리거{"ad":"midroll","duration":30}
채팅 동기화{"chat_seq":4521}
챕터 마커{"chapter":"intro"}

bitstream과 함께 전달할 가치가 있는 작은 메타데이터의 후보입니다. 자막·광고처럼 별도 규격과 신뢰성 요구가 있는 기능은 전용 timed-metadata 방식과 비교합니다.

8-3. 정확도 한계

  • Agora 문서는 Media Push의 트랜스코딩된 H.264/H.265 출력에 기본 encoding information SEI를 추가한다고 설명함
  • 시청자 디바이스의 SEI 파싱이 실패해도 비디오 재생은 정상 — 폴백 안전
  • 공개 문서는 삽입 주기를 명시하지 않으므로 매 프레임·키프레임 주기를 가정하지 말고 출력에서 측정

9. SEI vs 다른 동기화 메커니즘 — 의사결정

메커니즘시간 결합 방식페이로드전달 특성용도
SEIbitstream timeline에 결합작게 유지transcoding·packaging 전후의 SEI를 추출해 보존 여부 측정레이아웃, encoding 정보
별도 RTM/WebSocket앱 timestamp·sequence로 결합자유미디어와 독립된 재시도·순서 정책채팅, 업링크 데이터
RTP timestamp 기반 외부 채널공통 clock mapping 필요자유두 경로의 손실·지연을 함께 처리A/V 외부 큐
픽셀 워터마크영상에 직접 합성매우 제한적트랜스코딩 후에도 보일 수 있음출처 표시, forensic marker

결론:

  • 비디오 bitstream과 함께 운반해야 하는 작은 메타데이터 → SEI 보존성 검증 후 사용
  • 자유 형식, 양방향, 빈번한 메시지 → RTM/WebSocket
  • 둘은 보완 관계, 대체재 아님

10. FAQ

Q1. SEI를 쓰면 시그널링 인프라 필요 없나?

아닙니다. 이 SEI에 들어가는 건 다운링크 encoding·레이아웃 정보입니다. 업링크 채팅이나 호스트 명령에는 별도 시그널링이 필요합니다. app_data만 일부 용도가 겹칠 수 있습니다.

Q2. SEI가 디코딩에 영향 주나?

SEI 처리는 선택적이므로 알 수 없는 메시지를 무시할 수 있습니다. 다만 잘못된 길이·RBSP를 안전하게 처리하지 못하는 자체 파서는 오류나 크래시를 낼 수 있으므로 bounds check와 fuzz test가 필요합니다.

Q3. SEI가 매 프레임마다 박히나?

Agora 공개 문서는 삽입 cadence를 명시하지 않습니다. 실제 출력에서 빈도와 변경 시점을 측정하고, 미수신·지연·중복 시의 TTL과 기본 레이아웃을 정의합니다.

Q4. RTC 원본 스트림에도 SEI가 있나?

이 글의 JSON schema는 Agora가 문서화한 Media Push 트랜스코딩 출력 형식입니다. 다른 RTC 전송 경로에 같은 형식이 있다고 일반화하지 않습니다.

Q5. payload_type 100을 쓰면 다른 디코더와 충돌하나?

이론상 가능. 다른 인코더가 같은 type 100을 다른 의미로 사용 중이면 충돌. 표준 권장은 type 5 (user_data_unregistered) + UUID 네임스페이싱이지만, Agora는 type 100 직접 점유 — 실무에서 큰 이슈는 없으나 다른 SEI 파서가 type 100을 잘못 해석할 가능성은 있음.

Q6. H.265(HEVC)에서도 같은 형식인가?

H.265 SEI도 같은 Annex D 모델 + 0xFF 누적 길이 인코딩 사용. NAL header 형식은 다름(2바이트). payload_type/payload_size 인코딩은 동일.


11. SA 체크리스트

□ 1. 송출 시나리오가 Agora Media Push인지 확인 (RTC 원본은 SEI 없음)
□ 2. 트랜스코딩 옵션이 활성화되어 있는지 (raw passthrough면 SEI 없음)
□ 3. 시청자 측 파서가 AVCC vs Annex B 포맷을 정확히 이해
□ 4. NAL header 0x06 → SEI 식별
□ 5. payload_type 100 (0x64) 확인 후 Agora SEI로 분기
□ 6. 0xFF 누적 길이 인코딩 정확히 처리
□ 7. emulation_prevention_byte (00 00 03) 제거
□ 8. JSON 파싱 후 canvas / regions / app_data 활용
□ 9. SEI 미수신 시 폴백 동작 정의 (이전 값 유지 or 기본 레이아웃)
□ 10. (옵션) RTM/WebSocket으로 업링크 채널 별도 구축

12. 한 줄 결론

SEI는 H.264/H.265 bitstream의 부가 정보 메커니즘이다. Agora Media Push 문서는 트랜스코딩 출력의 payload type 100에 encoding 정보 JSON을 넣는 형식을 정의한다. 파서는 framing·RBSP·길이·schema를 방어적으로 처리하고 전체 경로의 보존성을 검증해야 한다.

한 장 요약

┌──────────────────────────────────────────────────────┐
│  Agora Media Push                                    │
│       │                                              │
│       ▼ (자동)                                       │
│  H.264 NAL 스트림에 SEI 삽입                         │
│       │                                              │
│       ▼                                              │
│  [06 64 bd ... JSON ...]                             │
│   │   │  │  └─ canvas, regions, app_data, ts, ver   │
│   │   │  └──── payload_size (0xFF 누적)             │
│   │   └─────── payload_type 100 (Agora 커스텀)      │
│   └─────────── NAL type 6 (SEI)                     │
│       │                                              │
│       ▼                                              │
│  RTMP/AVCC 또는 RTSP/Annex B로 송출                  │
│       │                                              │
│       ▼                                              │
│  시청자 클라이언트 파서                               │
│  ① NAL 디먹스 → ② SEI 식별 → ③ 길이 파싱            │
│  ④ emulation_prevention 제거 → ⑤ JSON 파싱          │
│       │                                              │
│       ▼                                              │
│  UI 활용 (클릭 영역, 자막 큐, 광고 트리거 등)        │
└──────────────────────────────────────────────────────┘

관련 글


참고 자료

© 2026 Frank Kim. All rights reserved.