서론

RTMP는 라이브 스트리밍 분야에서 널리 사용되는 프로토콜이다. RTMP의 개념부터 시작해 상세한 스펙까지 자세히 알아보자.

언제나 그렇지만 정확한 스펙을 확인할 때는 공식 문서를 참고하자.


RTMP (Real-Time Messaging Protocol)

RTMP는 Adobe Systems에서 멀티미디어 데이터를 전송하기 위해 개발한 TCP 기반의 스트리밍 프로토콜이다.

RTMP는 비디오, 오디오 및 상호작용 콘텐츠(interactive content)를 송수신하기 위한 포맷으로 메시지(message)를 사용한다.

RTMP에서는 특정 유형의 메시지를 전달하는 논리적인 채널(channel)을 스트림(stream)이라고 부른다. RTMP는 하나의 TCP 연결에 하나 이상의 스트림을 다중화(mulptiplexing)할 수 있다.

또한, RTMP는 양방향(full-duplex) 통신을 지원하여 클라이언트가 서버로 멀티미디어 메시지를 전송하는 동시에 서버가 클라이언트로 제어 메시지를 보낼 수 있다.


RTMP 종류

Type Feature
RTMP Plain RTMP, TCP/IP 연결 위에서 동작한다. (default port : 1935)
RTMPS RTMP over TLS/SSL, TLS/SSL 연결 위에서 동작한다.
RTMPE Adobe의 독점적인 보안 메커니즘을 사용하는 RTMP
RTMPT RTMP over HTTP/HTTPS, HTTP 터널링을 통해 방화벽을 우회하려는 목적으로 많이 사용한다. (default port : 80 or 443)
RTMPTE RTMPT + RTMPE
RTMFP TCP 대신 UDP를 사용하는 RTMP


RTMP 장점

1. 안정적인 프로토콜

RTMP는 신뢰성 있는 TCP 프로토콜 위에서 동작하기 때문에, 네트워크 상황에 관계없이 안정적으로 데이터를 스트리밍할 수 있다.

2. 저지연 프로토콜

RTMP는 약 3-5초의 지연시간을 가지는 저지연(low-latency) 프로토콜이다.

물론 최근에 등장한 스트리밍 프로토콜(e.g. SRT, WebRTC, HESP 등)들은 RTMP보다 지연 시간이 짧지만 이 정도면 꽤 빠르다고 할 수 있다.

3. 여전히 인기있는 Ingest 프로토콜

RTMP는 현재까지도 ingest 단계에서 가장 많이 사용되는 프로토콜이다.

Ingest 단계 : 스트리밍 워크플로우 중, 카메라 또는 인코더가 미디어 데이터를 미디어 서버에게 전송하는 단계

Youtube, Facebook, Twitch와 같은 미디어 플랫폼을 비롯한 많은 인코더 및 미디어 서버가 RTMP를 지원하기 때문에 RTMP는 여러 가지 단점이 존재함에도 불구하고 여전히 인기가 많다.

특히 ingest 프로토콜로 RTMP를 사용하고 egress 프로토콜로 HLS, DASH를 사용하는 라이브 스트리밍 방식은 매우 보편화되어 있다.

Egress(Delivery) 단계 : 스트리밍 워크플로우 중, 미디어 서버가 미디어 데이터를 플레이어에게 전송하는 단계


RTMP 단점

1. 재생에 적합하지 않은 프로토콜

과거 인터넷 브라우저들이 RTMP 재생을 지원했던 것과 달리 현재 인터넷 브라우저들은 RTMP 재생을 지원하지 않는다.

RTMP 재생을 지원하지 않는 이유는 다양하지만, 가장 큰 이유는 RTMP보다 재생에 유리한 HAS(HTTP Adative Streaming) 프로토콜이 등장했기 때문이다.

HAS 프로토콜은 HTTP 위에서 동작하기 때문에 RTMP보다 방화벽 문제로부터 자유롭고 플레이어 측 네트워크 상황에 맞게 유연하게 비트레이트(bitrate)를 조절할 수 있어 재생 경험이 우수하다. (+ HTTP 캐싱, CDN 캐싱 등을 사용할 수 있어 확장성도 좋다.)

즉, RTMP는 재생에 최적화된 프로토콜이라 보기 어렵다.

2. TCP 기반 프로토콜의 한계

RTMP는 TCP 기반 프로토콜이므로 TCP 프로토콜이 가진 한계를 극복할 수 없다.

TCP는 연결 수립 과정에서 3-way handshake로 인해 최소 1-RTT(Round-Trip-Time)가 발생한다. (TLS 1.2를 적용하면 2-RTT 추가, TLS 1.3을 적용하면 1-RTT 추가)

네트워크 종단을 왕복하는 RTT는 지연 시간에 많은 영향을 미치므로 TCP는 연결 수립 과정에서 불가피한 지연 시간이 발생한다.

또한 TCP는 패킷이 손상되거나 누락될 경우, 해당 패킷을 재전송 받을 때까지 그 뒤의 패킷들은 모두 커널에서 대기해야하는 HOLB(Head-of-Line Blocking) 문제가 있다.

HOLB 문제는 네트워크 상황이 나쁠수록 지연 시간에 매우 큰 영향을 미친다.

이러한 한계들은 TCP 기반 프로토콜이 1초 미만의 초저지연(ultra-low-latency) 프로토콜이 될 수 없게 만든다.

3. 제한된 코덱 지원

RTMP가 지원하는 대표적인 코덱은 다음과 같다.

  • 비디오 코덱 : H.264, VP8
  • 오디오 코덱 : AAC, MP3, Speex

RTMP는 압축율이 높은 최신 코덱(e.g. H.265, AV1, VP9 등)들을 지원하지 않는다.

이를 극복하기 위해 ERTMP(Enhanced-RTMP), 커스텀 스펙들이 존재하지만 여기서는 다루지 않겠다.


RTMP Message

앞서 RTMP는 비디오, 오디오 및 상호작용 콘텐츠를 송수신하기 위한 포맷으로 메시지를 사용한다고 이야기했다.

RTMP는 실제로 메시지를 보낼 때, 전송에 적합한 크기로 메시지를 조각화(fragmentation)한다. 조각화된 메시지는 청크(chunk)라고 부른다.

RTMP는 청크 스트리밍을 통해 메시지를 전송하지만, 다른 전송 프로토콜을 통해 완전한 RTMP 메시지를 전송할 수도 있다.

그럼 먼저 조각화되지 않은 완전한 RTMP 메시지 포맷에 대해 알아보자.

RTMP Message Format

image

Field Type Description
Message Type 1 byte 메시지 타입 ID
Payload Length 3 bytes byte 단위의 메시지 페이로드 길이 (big-endian)
Timestamp 4 bytes 메시지 타임스탬프 (big-endian)
Message Stream ID 3 bytes 메시지 스트림 ID (big-endian)
Message Payload * 메시지에 포함된 실제 데이터

RTMP Message Type

Type ID Message Type Description
1 Set Chunk Size 상대에게 최대 청크 크기를 알리는 메시지
2 Abort Message 수신 측이 여러 청크로 나뉘어진 메시지의 일부만 받았을 때, 송신 측에서 나머지 청크를 보내지 않겠다고 알리는 메시지
(메시지를 받은 수신 측은 불완전한 청크를 폐기)
3 Acknowledgement 상대로부터 받은 메시지의 크기가 윈도우 크기와 같을 때마다 수신 측에서 송신 측에 알리는 메시지
(메시지는 지금까지 받은 바이트 수를 포함하고 있음)
4 User Control Messages 상대에게 사용자 제어 이벤트를 알리는 메시지
(e.g. 스트림 수신 준비 완료, 스트림 종료 등)
5 Window Acknowledgement Size 상대에게 본인의 윈도우 크기(송신 측이 수신 측으로부터 확인 받지 않고 보낼 수 잇는 최대 바이트 수)를 알려주는 메시지
(메시지를 받은 수신 측은 윈도우 크기만큼 데이터를 받거나 세션을 시작하는 경우, Acknowledgement 메시지를 응답)
6 Set Peer Bandwidth 상대의 출력 대역폭(윈도우 크기)을 제한하는 메시지
(메시지를 받은 수신 측은 윈도우 크기가 달라질 경우, Window Acknowledgement Size 메시지를 응답)
8 Audio Message 오디오 데이터가 포함된 메시지
9 Video Message 비디오 데이터가 포함된 메시지
15(AMF3), 18(AMF0) Data Message 메타데이터 혹은 사용자 정의 데이터가 포함된 메시지
16(AMF3), 19(AMF0) Shared Object Message 여러 클라이언트와 인스턴스를 동기화하기 위한 이벤트 정보를 알려주는 메시지 (name-value 쌍의 flash 객체로 구성)
17(AMF3), 20(AMF0) Command Message 상대에게 특정 동작을 수행하도록 명령하는 메시지 (e.g. 연결, 종료, 스트림 생성 등)
22 Aggregate Message 청크 수를 줄이기 위해 메시지 안에 다수의 하위 메시지를 포함시키는 메시지
(하나의 청크 안에 두 개 이상의 메시지를 포함할 수 없으니 해당 방법을 사용)

메시지 타입 ID가 1, 2, 3, 5, 6인 메시지는 RTMP 청크 스트림 프로토콜 계층에서 사용하는 프로토콜 제어 메시지(Protocol Control Message)이며, 메시지 타입 ID가 4인 메시지는 RTMP 프로토콜 계층에서 사용하는 유저 제어 메시지(User Control Message)이다.

프로토콜 제어 메시지와 유저 제어 메시지가 청크 단위로 전송될 경우, Message Stream ID는 0, Chunked Stream ID는 2로 고정 값을 가진다. 또한, 해당 메시지는 수신 즉시 적용되며 타임스탬프 값은 무시된다.


RTMP Chunk

메시지를 전송에 적합한 크기로 조각화한 청크에 대해 알아보자.

RTMP Chunk Format

image

Field Size Description
Basic Header 1 ~ 3 bytes 청크 스트림 ID와 청크 타입 (청크 스트림 ID에 따라 길이가 달라짐)
Message Header 0, 3, 7, 11 bytes 메시지에 대한 정보 (청크 타입에 따라 길이가 달라짐)
Extended Timestamp 0, 4 bytes Message Header에 기록된 타임스탬프 값이 16777215(0xFFFFFF) 보다 큰 값을 가질때 사용
Chunk Data * 청크 데이터 (설정된 청크 최대 크기를 넘을 수 없음)

RTMP 데이터는 네트워크 상에서 청크 단위로 전송되며, 각 청크의 청크 스트림 ID를 기반으로 수신 측에서 재조립되어 완전한 메시지를 구성한다.

이때 청크 헤더는 메시지 헤더 정보를 포함하고 있기 때문에(없더라도 유추할 수 있는 정보를 주기 때문에) 청크 데이터에는 메시지 헤더 정보를 포함하지 않는다.

기본적으로 청크의 최대 크기는 128 bytes로 설정되어 있으며, Protocol Control Message(Set Chunk Size)를 통해 변경할 수 있다.

청크의 크기가 클수록 CPU 사용량이 줄어들지만 대역폭이 낮은 환경에서는 다른 스트림의 청크가 지연될 수 있음을 고려해야 한다.

이제 각 청크 헤더별 정보를 알아보자.

Chunk Basic Header

Chunk Basic Header는 청크 스트림 ID(CS ID) 값에 따라 1, 2, 3 바이트 헤더로 나뉜다.

image

Field Size Description
FMT 2 bits 청크 (메시지 헤더) 타입 (0, 1, 2, 3)
CS ID 6 bits 0: 2 bytes basic header를 의미
1: 3 bytes basic header를 의미
2~64: 청크 스트림 ID
CS ID - 64 8, 16 bits 2 byte basic header (64-319): 청크 스트림 ID - 64, (second byte) + 64로 계산 가능
3 byte basic header (64-65599): 청크 스트림 ID - 64, (third byte)*256 + (the second byte) + 64로 계산 가능

Chunk Message Header

Chunk Message Header는 앞서 Chunk Basic Header의 청크 타입(FMT)에 따라 0, 1, 2, 3 타입으로 나뉜다.

image

Field Size Description
Timestamp 3 bytes For a type-0 chunk, the absolute timestamp of the message is sent here.
If the timestamp is greater than or equal to 16777215 (hexadecimal 0xFFFFFF), this field MUST be 16777215,
indicating the presence of the Extended Timestamp field to encode the full 32 bit timestamp.
Otherwise, this field SHOULD be the entire timestamp.
Timestamp Delta 3 bytes For a type-1 or type-2 chunk, the difference between the previous chunk’s timestamp and the current chunk’s timestamp is sent here.
If the delta is greater than or equal to 16777215 (hexadecimal 0xFFFFFF), this field MUST be 16777215,
indicating the presence of the Extended Timestamp field to encode the full 32 bit delta.
Otherwise, this field SHOULD be the actual delta.
Message Length 3 bytes For a type-0 or type-1 chunk, the length of the message is sent here.
Note that this is generally not the same as the length of the chunk payload.
The chunk payload length is the maximum chunk size for all but the last chunk,
and the remainder (which may be the entire length, for small messages) for the last chunk.
Message Type ID 1 bytes For a type-0 or type-1 chunk, type of the message is sent here.
Message Stream ID 4 bytes For a type-0 chunk, the message stream ID is stored. Message stream ID is stored in little-endian format.
Typically, all messages in the same chunk stream will come from the same message stream.
While it is possible to multiplex separate message streams into the same chunk stream,
this defeats the benefits of the header compression.
However, if one message stream is closed and another one subsequently opened,
there is no reason an existing chunk stream cannot be reused by sending a new type-0 chunk.

타입 0 청크는 청크 스트림의 시작이나 스트림의 타임스탬프가 뒤로 갈 때(e.g. seek) 반드시 사용된다.

타입 1 청크는 앞의 청크와 동일한 Message Stream ID를 가지며, 스트림이 다양한 크기의 메시지를 전송하는 상황에서 첫 메시지 이후 다음 메시지의 첫 번째 청크에 사용된다.

타입 2 청크는 앞의 청크와 동일한 Message Stream ID, Message Length를 가지며, 스트림이 일정한 크기의 메시지를 전송하는 상황에서 첫 메시지 이후 각 메시지의 첫 번째 청크에 사용된다.

타입 3 청크는 앞의 청크와 동일한 Message Stream ID를 가진다. 단일 메시지가 청크로 분할되는 경우, 첫 번째 청크를 제외한 모든 청크는 해당 타입을 가져야 한다.

또한, 타입 2 청크 뒤에 타입 3 청크가 오면 Timestamp Delta가 동일하고, 타입 0 청크 뒤에 타입 3 청크가 오면 Timestamp가 동일하다.

Chunk Extended Timestamp

Chunk Extended Timestamp는 24bits 크기의 Timestamp 또는 Timestamp Delta 값이 16777215(0xFFFFFF)보다 클 때 이를 확장하기 위한 32bits 크기의 필드이다.

타입 0, 1, 2 청크는 Chunk Message Header의 Timestamp 혹은 Timestamp Delta 값이 16777215(0xFFFFFF)일 경우, Chunk Extended Timestamp 필드를 가진다.

타입 3 청크는 동일한 Chunk Stream ID의 이전 타입 0, 1, 2 청크가 Chunk Extended Timestamp를 가지고 있을 경우, Chunk Extended Timestamp 필드를 가진다.


RTMP Video/Audio Data Format

RTMP를 통해 전송되는 비디오, 오디오 데이터는 FLV 태그 데이터 포맷으로 캡슐화된다.

자세한 내용은 다루기엔 양이 많으니 FLV 포스트를 참고하자.


RTMP 동작

RTMP 동작은 크게 3단계로 구성된다.

1. Handshake

TCP 연결이 수립된 후, RTMP 연결을 수립하기 위해 handshake를 진행한다.

클라이언트와 서버는 handshake 과정에서 각각 3개의 청크를 주고 받는다.

Handshake 과정에서 주고 받는 청크는 앞에서 설명했던 RTMP 청크와 다른 포맷이다.

클라이언트가 서버로 전송하는 청크는 C0, C1, C2로 표현하고 서버가 클라이언트로 전송하는 청크는 S0, S1, S2로 표현한다.

  • C0/S0 : RTMP 버전 번호 (사실상 0x03만 사용)
  • C1/S1 : 청크 스트림의 동기화에 사용되는 timestamp + 각 peer를 구분하기 위한 랜덤 데이터
  • C2/S2 : C2는 S1에 대한 echo, S2는 C1에 대한 echo
+-------------+                           +-------------+
|    Client   |       TCP/IP Network      |    Server   |
+-------------+            |              +-------------+
      |                    |                     |
 Uninitialized             |               Uninitialized
      |          C0        |                     |
      |------------------->|         C0          |
      |                    |-------------------->|
      |          C1        |                     |
      |------------------->|         S0          |
      |                    |<--------------------|
      |                    |         S1          |
 Version sent              |<--------------------|
      |          S0        |                     |
      |<-------------------|                     |
      |          S1        |                     |
      |<-------------------|                Version sent
      |                    |         C1          |
      |                    |-------------------->|
      |          C2        |                     |
      |------------------->|         S2          |
      |                    |<--------------------|
   Ack sent                |                  Ack Sent
      |          S2        |                     |
      |<-------------------|                     |
      |                    |         C2          |
      |                    |-------------------->|
 Handshake Done            |               Handshake Done
      |                    |                     |

클라이언트는 C0를 전송한 후 S0가 수신될 때까지 대기하지 않고 이어서 C1를 전송한다.

C2와 S2가 수신되면 handshake를 마친다.

2. Connect

Handshake 이후 클라이언트와 서버는 AMF(Action Message Format)로 인코딩된 메시지를 교환하여 연결을 협상한다.

AMF(Action Message Format) : Action Script의 객체 그래프(object graph)를 직렬화한(serialize) 바이너리 포맷, Adobe Flash에서 메시지를 주고 받는 목적으로도 사용된다.

+--------------+                              +-------------+
|    Client    |             |                |    Server   |
+------+-------+             |                +------+------+
       |              Handshaking done               |
       |                     |                       |
       |                     |                       |
       |----------- Command Message(connect) ------->|
       |                                             |
       |<------- Window Acknowledgement Size --------|
       |                                             |  
       |<----------- Set Peer Bandwidth -------------|
       |                                             | 
       |-------- Window Acknowledgement Size ------->|
       |                                             |  
       |<------ User Control Message(StreamBegin) ---|
       |                                             |   
       |<------------ Command Message ---------------|
       |       (_result- connect response)           |
       |                                             |

클라이언트는 서버에 Command Message(connnect)를 전송하여 연결을 요청한다.

서버와 클라이언트는 Protocol Control Message(Window Acknowledgement Size, Set Peer Bandwidth)를 주고 받아 윈도우 크기를 설정한다.

서버는 클라이언트에게 스트림 수신 준비가 되었다는 User Control Message(StreamBegin)를 전송한 뒤 연결 상태를 알려주는 Command Message(_result)를 전송한다.

연결 상태가 성공이면 RTMP 연결이 수립된 것이다.

이때 클라이언트와 서버가 주고 받은 Command Message 내용은 다음과 같다.

(Invoke) "connect"
(Transaction ID) 1.0
(Object1) { app: "sample", flashVer: "MAC 10,2,153,2", swfUrl: null,
              tcUrl: "rtmpt://127.0.0.1/sample ", fpad: false,
              capabilities: 9947.75 , audioCodecs: 3191, videoCodecs: 252,
              videoFunction: 1 , pageUrl: null, objectEncoding: 3.0 }
(Invoke) "_result"
(transaction ID) 1.0
(Object1) { fmsVer: "FMS/3,5,5,2004", capabilities: 31.0, mode: 1.0 }
(Object2) { level: "status", code: "NetConnection.Connect.Success",
                   description: "Connection succeeded",
                   data: (array) { version: "3,5,5,2004" },
                   clientId: 1728724019, objectEncoding: 3.0 }

3. Stream (Publish or Play)

RTMP 연결이 수립된 후, 클라이언트는 서버에게 Command Message(createStream)를 전송하여 메시지를 전달하기 위한 논리적인 채널(channel)을 생성한다.

서버로부터 스트림이 생성되었다는 Command Message(_result)를 받으면 클라이언트는 다음 Command Message들을 사용할 수 있다.

  • play, play2, deleteStream, closeStream, receiveAudio, receiveVideo, publish, seek, pause, …

이 중 클라이언트가 서버로 스트림을 수신받는 play와 클라이언트가 서버로 스트림을 전송하는 publish의 동작 과정은 다음과 같다.

     +-------------+                            +------------+
     | Play Client |             |              |   Server   |
     +------+------+             |              +------+-----+
            |        Handshaking and Application       |
            |             connect done                 |
            |                    |                     |
            |                    |                     |
   ---+---- |----- Command Message(createStream) ----->|
Create|     |                                          |
Stream|     |                                          |
   ---+---- |<---------- Command Message --------------|
            |     (_result- createStream response)     |
            |                                          |
   ---+---- |------ Command Message (play) ----------->|
      |     |                                          |
      |     |<------------- SetChunkSize --------------|
      |     |                                          |
      |     |<---- User Control (StreamIsRecorded) ----|
 Play |     |                                          |
      |     |<---- UserControl (StreamBegin) ----------|
      |     |                                          |
      |     |<- Command Message(onStatus-play reset) --|
      |     |                                          |
      |     |<- Command Message(onStatus-play start) --|
      |     |                                          |
      |     |<------------ Audio Message --------------|
      |     |                                          |
      |     |<------------ Video Message --------------|
      |     |                    |                     |
                                 |
        Keep receiving audio and video stream till finishes
     +--------------------+                     +-----------+
     |  Publisher Client  |        |            |   Server  |
     +----------+---------+        |            +-----+-----+
                |        Handshaking and Application  |
                |             connect done            |
                |                  |                  |
                |                  |                  |
       ---+---- |--- Command Message(createStream) -->|
   Create |     |                                     |
   Stream |     |                                     |
       ---+---- |<------- Command Message ------------|
                | (_result- createStream response)    |
                |                                     |
       ---+---- |---- Command Message(publish) ------>|
          |     |                                     |
          |     |<----- User Control(StreamBegin) ----|
          |     |                                     |
          |     |----- Data Message(Metadata) ------->|
          |     |                                     |
Publishing|     |------------ Audio Data ------------>|
  Content |     |                                     |
          |     |------------ SetChunkSize ---------->|
          |     |                                     |
          |     |<--------- Command Message ----------|
          |     |      (_result- publish result)      |
          |     |                                     |
          |     |------------- Video Data ----------->|
          |     |                  |                  |
          |     |                  |                  |
                |    Until the stream is complete     |
                |                  |                  |

요즘은 RTMP를 ingest 프로토콜로 많이 사용하기 때문에 publish를 많이 사용한다.

추가로 위 sequence diagram은 RTMP 공식 문서에서 제공하는 내용이지만 누락된 부분들이 존재한다.

RTMP 패킷을 분석하거나 구현체를 확인해보면 공식 문서에는 언급하지 않는 Command Message가 보인다.

  • Publish 과정에서 실제로 주고 받는 메시지 예시
    • C -> S : Command Message (releaseStream)
    • C -> S : Command Message (FCPublish)
    • C -> S : Command Message (createStream)
    • C <- S : Command Message (_result for createStream)
    • C <- S : User Control Message (Stream Begin)
    • C -> S : Command Message (publish)
    • C <- S : Command Message (_result for publish)
    • C <- S : Command Message (onStatus “NetStream.Publish.Start”)
    • C -> S : Data Message (@setDataFrame, onMetaData)
    • C -> S : Video/Audio Data Message
  • Unpublish 과정에서 실제로 주고 받는 메시지 예시
    • C → S : Command Message (FCUnpublish)
    • C → S : Command Message (deleteStream)

이는 RTMP가 Adobe에서 독점적으로 사용하던 프로토콜이기 때문에 완전히 스펙을 공개하지 않았으며, 많은 RTMP 구현체들이 RTMP 프로토콜을 리버스 엔지니어링하여 개발되었기 때문이다.

그래서 정보를 찾다 보면 빈 구멍(?)들이 많다..


Reference

  • https://rtmp.veriskope.com/docs/spec
  • https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol
  • https://www.wowza.com/blog/rtmp-streaming-real-time-messaging-protocol
  • https://heesu0.github.io/rfc/rtmp/rtmp_specification_1.0.pdf
  • https://heesu0.github.io/rfc/rtmp/amf0-file-format-spec.pdf
  • https://heesu0.github.io/rfc/rtmp/amf3-file-format-spec.pdf
  • https://heesu0.github.io/rfc/rtmp/video_file_format_spec_v10.pdf
  • https://heesu0.github.io/rfc/rtmp/video_file_format_spec_v10_1.pdf