FLV 및 RTMP 포맷 변환과 데이터 구조 분석

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바이트 실제 페이로드

청크 타입

  1. Type 0 (11바이트): 완전한 메시지 헤더, 새로운 스트림 시작시 사용
  2. Type 1 (7바이트): 타임스탬프 델타만 변경
  3. Type 2 (3바이트): 길이와 타입 유지, 타임스탬프 델타만 포함
  4. 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);
}

태그: flv rtmp streaming media-format video-encoding

7월 4일 22:07에 게시됨