RTMP 미디어 데이터 전달 과정에서 필드 값에 음수 값이 들어오거나, offset에서 메모리 아웃바운드 문제가 발생하였다. Chunk Stream Buffer Data를 객체로 파싱하는 과정에서 문제가 있다고 생각하여 파싱 로직을 다시 살펴보게 되었다.
결과적으로 파싱에서 문제가 있음은 확실했는데, 로직에서 이상함을 찾기 힘들어 오랜 시간을 고민했다.
아래는 문제 식별과 해결을 위해 어떤 부분이 문제인지, 어떻게 해결하면 좋을지 고민했던 흔적들이다.
/*
청크를 분리하는 영역에 도착하였다.
1. 데이터가 오면 Basic Header를 탐색한다.
1.1. Basic Header의 Chunk Type을 판단한다.
1.2. Basic Header의 Stream ID를 판단한다.
1.2.1. Stream ID가 0 또는 1일 경우 Basic Header의 길이를 재정의한다. 1 -> 2 or 3
2. Basic Header의 Chunk Type으로 Message Header의 유형을 판단한다.
2.1. Chunk Type이 0인 경우
Chunk Type이 0인 경우에는 총 11Bytes로 구성돼 모든 데이터를 포함하기 때문에
이후에 전달되는 Chunk Type에 따라 이 청크의 데이터를 참조할 수 있어 모든 데이터를 저장하도록 한다.
특히 Chunk Type 0의 Timestamp는 이후 Chunk Type 0이 오기 전까지의 기준 Timestamp로 사용될 수 있어야 한다.
저장되는 데이터: Timestamp, Message Length, Message Type ID, Message Stream ID, Timestamp Delta 0으로 초기화
참조하는 데이터: X
2.2. Chunk Type이 1인 경우
Chunk Type이 1인 경우에는 총 7Bytes로 구성돼 Message Stream ID를 이전 청크에서 참조해야 한다.
Message Stream ID를 제외한 모든 데이터를 포함하기 때문에 해당하는 데이터를 저장하도록 한다.
주의할 것은 Chunk Type 1의 Timestamp는 Timestamp Delta로써 Chunk Type 0의 기준 Timestamp에 더하여 사용된다.
저장되는 데이터: Timestamp Delta, Message Length, Message Type ID
참조하는 데이터: Message Stream ID,
2.3. Chunk Type이 2인 경우
Chunk Type이 2인 경우에는 총 3Bytes로 구성돼 Timestamp Delta를 제외한 나머지 값을 이전 청크에서 참조해야 한다.
저장되는 데이터: Timestamp Delta
참조하는 데이터: Message Length, Message Type ID, Message Stream ID
2.4. Chunk Type이 3인 경우
Chunk Type이 3인 경우에는 Message Header가 존재하지 않아 모든 데이터를 이전 청크에서 참조해야 한다.
저장되는 데이터: X
참조하는 데이터: Timestamp Delta, Message Length, Message Type ID, Message Stream ID
3. Extended Timestamp 사용 여부를 판단한다.
3.1 Timestamp Delta를 더한 Timestamp 크기가 0xffffff 이상인 경우 Timestamp는 언제나 0xffffff를 유지한다.
3.2 Timestamp가 0xffffff인 경우 4Bytes 크기의 Extended Timestamp 필드를 별도로 사용한다.
3.3 Extended Timestamp의 최대 크기는 0xffffffff 이기 때문에 이를 초과한다면 Rollover 현상에 대한 wrapper round 처리를 수행해줘야 한다.
4. Payload를 판단한다.
주의할 점은 Message Length가 Payload의 길이가 아님을 알아야 한다.
Payload는 Max Chunk Size가 존재하고 Message Length는 해당 Message Stream의 총 길이를 판단한다.
4.1 Chunk Stream의 첫 시작은 언제나 Chunk Type 0인 데이터이다.
4.1.1. Chunk Type 0인 데이터의 Message Length가 Payload의 Max Chunk Size를 넘지 않고,
전달받은 데이터의 Header를 제외한 크기가 Message Length보다 크거나 동일한 경우
추가적인 청크가 필요하지 않기 때문에 Message Length를 기준으로 Payload를 할당하면 된다.
4.1.2. Chunk Type 0인 데이터의 Message Length가 Payload의 Max Chunk Size를 넘지 않지만,
전달받은 데이터의 Header를 제외한 크기가 Message Length보다 작은 경우
4.1.2.1. 전달받은 데이터를 전부 Payload에 할당하고 다음 청크로 넘어간다.
4.1.2.2. 이후 청크가 Chunk Type 2 또는 3인 경우 Message Length - 현재 Payload Length 만큼의 데이터를 Payload에 할당한다.
4.1.2.2.1. Chunk Type 1인 경우 Message Length가 존재하는 경우
4.1.3. Chunk Type 0인 데이터의 Message Length가 Payload의 Max Chunk Size를 넘고,
전달받은 데이터의 Header를 제외한 크기가 Message Length보다 크거나 동일한 경우
...중략
T0과 T1은 Stream ID가 같다.
T1은 Message Length 또는 Message Type이 달라지는 경우에 (ex- 오디오가 올 때도 있고, 비디오가 올 때도 있을 때)
그럼 T0의 Message Length는 어떤 의미인가. T1이 오지 않을 경우에?
오디오를 보낼 때 T1과 부속 T2, T3를 보낸다. 비디오가 올 때는 T1과 비디오의 T2, T3
*/
...생략
basic header is { chunkType: 0, chunkStreamId: 4 }
message parser receive: 0 0 63 9 4
into timestamp and data is <Buffer 00 00 00 01 00 a3 09 04 00 00 00>
this data timestmap delta is 0
this data messageLength is 65699
this data typeId is 9
minval, availLength, existDataLength, chunksize
4096, 4096, 65449, 4096
chunk intergrated is {
basicHeader: { chunkType: 0, chunkStreamId: 4 },
messageHeader: {
timestamp: 0,
timestampDelta: 0,
messageLength: 65699,
typeId: 9,
messageStreamId: 4
},
extendedTimestamp: 0,
payload: <Buffer 17 01 00 00 21 00 00 00 27 67 64 00 2a ac 2c ac 07 80 22 7e 5c 05 a8 08 08 0a 00 00 07 d0 00 03 a9 81 c0 00 00 3d 09 00 00 07 a1 20 dd e5 c1 40 00 00 ... 4046 more bytes>
}
basic header is { chunkType: 3, chunkStreamId: 4 }
minval, availLength, existDataLength, chunksize
4096, 4096, 61352, 4096
chunk intergrated is {
basicHeader: { chunkType: 3, chunkStreamId: 4 },
messageHeader: {
timestamp: 0,
timestampDelta: 0,
messageLength: 65699,
typeId: 9,
messageStreamId: 4
},
extendedTimestamp: 0,
payload: <Buffer 17 01 00 00 21 00 00 00 27 67 64 00 2a ac 2c ac 07 80 22 7e 5c 05 a8 08 08 0a 00 00 07 d0 00 03 a9 81 c0 00 00 3d 09 00 00 07 a1 20 dd e5 c1 40 00 00 ... 8142 more bytes>
}
...중략
basic header is { chunkType: 3, chunkStreamId: 4 }
minval, availLength, existDataLength, chunksize
3994, 4096, 3994, 4096
chunk intergrated is {
basicHeader: { chunkType: 3, chunkStreamId: 4 },
messageHeader: {
timestamp: 0,
timestampDelta: 0,
messageLength: 65699,
typeId: 9,
messageStreamId: 4
},
extendedTimestamp: 0,
payload: <Buffer 17 01 00 00 21 00 00 00 27 67 64 00 2a ac 2c ac 07 80 22 7e 5c 05 a8 08 08 0a 00 00 07 d0 00 03 a9 81 c0 00 00 3d 09 00 00 07 a1 20 dd e5 c1 40 00 00 ... 65384 more bytes>
}
---------data length is 43226--------
<Buffer d3 48 99 4b 93 c9 56 8e 80 80 fa 85 d0 02 97 a8 dd 2c 80 e6 c3 ea d3 f0 8e 99 d2 0a 75 ec e3 31 b2 fb eb 79 83 0d 2c 09 e9 76 20 a8 9b 24 28 d7 1a a8 ... 43176 more bytes>
---------------------------------------------
basic header is { chunkType: 3, chunkStreamId: 19 } //여기 타입이 이런식으로 존재하면 안됨!
minval, availLength, existDataLength, chunksize
0, 0, 43225, 4096
chunk intergrated is {
basicHeader: { chunkType: 3, chunkStreamId: 19 },
messageHeader: {
timestamp: 0,
timestampDelta: 0,
messageLength: 0,
typeId: 0,
messageStreamId: 0
},
extendedTimestamp: 0,
payload: <Buffer >
}

기존 로직은 위와 같은 상황을 생각하고 구현되었었다. 요청으로 오는 데이터가 반드시 헤더 + 페이로드 형태로 이루어져 있는 청크들의 집합이고, 이 청크들은 각각 청크 스트림 id, 즉 cid가 다를 수 있다고 생각했다.