FLV 포맷 개요
FLV(Flash Video)는 Adobe에서 개발한 스트리밍 미디어 형식으로, 파일 크기가 작고 구조가 단순하여 웹 환경에 적합하다. 현재 대부분의 주요 비디오 플랫폼이 지원하며, 확장자는 .flv이다.
FLV 파일 구조
FLV 파일은 헤더와 본문으로 구성된다:
- 파일 헤더(File Header): 9바이트로 파일 타입 식별 및 미디어 유형 정보 포함
- 파일 본문(File Body): 이전 태그 크기(Previous Tag Size)와 태그(Tag)의 연속으로 구성
각 태그 앞에는 4바이트의 Previous Tag Size 필드가 위치하며, 이 값은 이전 태그의 크기를 나타내어 역방향 탐색을 용이하게 한다. 첫 번째 Previous Tag Size는 항상 0이다.
FLV 헤더 구조
FLV 헤더는 다음과 같은 구조를 가진다:
#pragma pack(push, 1)
class FlvFileHeader {
public:
static constexpr uint8_t kVersion = 1;
static constexpr uint8_t kHeaderSize = 9;
char signature[3]; // 'F', 'L', 'V'
uint8_t version; // 버전 정보
#if __BYTE_ORDER == __BIG_ENDIAN
uint8_t reserved : 5; // 예약 비트
uint8_t has_audio : 1; // 오디오 존재 여부
uint8_t reserved2 : 1; // 예약 비트
uint8_t has_video : 1; // 비디오 존재 여부
#else
uint8_t has_video : 1;
uint8_t reserved2 : 1;
uint8_t has_audio : 1;
uint8_t reserved : 5;
#endif
uint32_t header_length; // 헤더 길이 (항상 9)
uint32_t first_prev_tag_size; // 첫 Previous Tag Size (항상 0)
};
#pragma pack(pop)
FLV 태그 구조
태그는 헤더와 데이터로 구성되며, 총 11바이트의 헤더를 가진다:
class FlvTagHeader {
public:
uint8_t tag_type = 0;
uint8_t data_length[3] = {0};
uint8_t time_stamp[3] = {0};
uint8_t time_stamp_ext = 0;
uint8_t stream_id[3] = {0};
};
타임스탬프는 3바이트 기본값과 1바이트 확장값으로 구성되어 32비트 값을 형성한다. 비디오 프레임의 경우 B-프레임이 존재하면 PTS와 DTS가 다르며, CTS(Composition Time Stamp)를 통해 시간 오프셋을 계산한다.
오디오 태그
오디오 태그는 1바이트 헤더와 데이터로 구성된다:
- 비트 0-3: 오디오 포맷 (AAC는 10)
- 비트 4-5: 샘플링 레이트
- 비트 6: 샘플링 정밀도
- 비트 7: 채널 수
AAC 포맷의 경우 두 번째 바이트에 AACPacketType이 추가되며, 0은 시퀀스 헤더, 1은 원시 데이터를 의미한다.
비디오 태그
비디오 태그는 첫 바이트에 프레임 타입과 코덱 정보를 포함하고, 이후 데이터가 비디오 스트림을 구성한다.
FLV 생성 예제
void writeBigEndian24(void* ptr, uint32_t value) {
uint8_t* data = static_cast<uint8_t*>(ptr);
data[0] = (value >> 16) & 0xFF;
data[1] = (value >> 8) & 0xFF;
data[2] = value & 0xFF;
}
void createFlvTag(uint8_t type, const Buffer::Ptr& data, uint32_t timestamp) {
FlvTagHeader header;
header.tag_type = type;
writeBigEndian24(header.data_length, static_cast<uint32_t>(data->size()));
header.time_stamp_ext = (timestamp >> 24) & 0xFF;
writeBigEndian24(header.time_stamp, timestamp & 0xFFFFFF);
// 태그 헤더 기록
writeData(reinterpret_cast<char*>(&header), sizeof(header));
// 태그 데이터 기록
writeData(data);
// Previous Tag Size 기록
uint32_t prevSize = htonl(static_cast<uint32_t>(sizeof(header) + data->size()));
writeData(reinterpret_cast<char*>(&prevSize), 4);
}
RTMP 프로토콜 구조
메시지(Message) 구조
RTMP 메시지는 헤더와 페이로드로 구성되며, 각 메시지는 하나의 프레임 데이터를 나타낸다:
| 필드 | 크기 | 설명 |
|---|---|---|
| 길이 | 3바이트 | 페이로드 길이 |
| 타임스탬프 | 4바이트 | PTS/DTS 값 |
| 타입 ID | 1바이트 | 메시지 유형 |
| 스트림 ID | 3바이트 | 메시지 스트림 식별자 |
청크(Chunk) 구조
네트워크 전송을 위해 메시지는 청크로 분할되며, 기본 크기는 128바이트이다:
| 구성 요소 | 크기 | 설명 |
|---|---|---|
| 기본 헤더 | 1-3바이트 | FMT 및 CSID 포함 |
| 메시지 헤더 | 0/3/7/11바이트 | FMT에 따라 다름 |
| 확장 타임스탬프 | 0/4바이트 | 필요시 사용 |
| 데이터 | n바이트 | 실제 페이로드 |
청크 타입
- Type 0 (11바이트): 완전한 메시지 헤더, 새로운 스트림 시작시 사용
- Type 1 (7바이트): 타임스탬프 델타만 변경
- Type 2 (3바이트): 길이와 타입 유지, 타임스탬프 델타만 포함
- Type 3 (0바이트): 모든 필드가 이전 청크와 동일
RTMP 메시지 구조체
#pragma pack(1)
struct RtmpMsgHeader {
uint8_t timestamp[3];
uint8_t length[3];
uint8_t type_id;
uint8_t stream_id[4]; // 리틀 엔디안
};
class RtmpMessagePacket {
public:
uint8_t chunk_stream_id = 0;
uint8_t message_type = 0;
uint32_t message_length = 0;
uint32_t timestamp = 0;
uint32_t extended_timestamp = 0;
Buffer::Ptr payload_data;
bool isComplete() const {
return payload_data && payload_data->size() == message_length;
}
bool isKeyFrame() const {
if (!payload_data || payload_data->size() == 0) return false;
uint8_t frame_info = payload_data->data()[0];
uint8_t frame_type = (frame_info >> 4) & 0x0F;
return message_type == 9 && frame_type == 1; // 비디오 키 프레임
}
};
#pragma pack()
FLV에서 RTMP로 변환 예제
void processFlvVideoData(const uint8_t* data, size_t length) {
if (length < 15) return; // 최소 헤더 크기 확인
// FLV 태그 헤더 파싱
uint8_t frame_info = data[11];
uint8_t frame_type = (frame_info >> 4) & 0x0F;
uint8_t codec_id = frame_info & 0x0F;
// 타임스탬프 추출
uint32_t timestamp = (static_cast<uint32_t>(data[4]) << 16) |
(static_cast<uint32_t>(data[5]) << 8) |
static_cast<uint32_t>(data[6]) |
(static_cast<uint32_t>(data[7]) << 24);
// RTMP 메시지 생성
auto rtmp_msg = std::make_shared<RtmpMessagePacket>();
rtmp_msg->message_type = 9; // 비디오
rtmp_msg->timestamp = timestamp;
rtmp_msg->message_length = static_cast<uint32_t>(length - 15);
// 페이로드 데이터 복사
rtmp_msg->payload_data = std::make_shared<Buffer>(length - 15);
std::memcpy(rtmp_msg->payload_data->data(), data + 15, length - 15);
// 키 프레임인 경우 설정 데이터 처리
if (frame_type == 1 && codec_id == 7) { // H.264 키 프레임
if (data[12] == 0) { // AVC 시퀀스 헤더
handleAvcSequenceHeader(rtmp_msg);
}
}
// 디코딩 처리
decodeRtmpMessage(rtmp_msg);
}