Tổng quan về kiến trúc hệ thống
NymphCast vận hành theo mô hình Client-Server, trong đó phía Client đóng vai trò nguồn phát và điều khiển, còn Server chịu trách nhiệm tiếp nhận, giải mã và xuất tín hiệu ra các thiết bị ngoại vi như loa hoặc màn hình. Sự tách biệt này giúp tối ưu hóa việc truyền tải dữ liệu và quản lý tài nguyên hệ thống.
Chi tiết kỹ thuật về DataBuffer (Bộ đệm dữ liệu)
Trái tim của khả năng xử lý luồng là lớp DataBuffer, được định nghĩa trong src/server/databuffer.h. Đây là một cấu trúc dữ liệu dạng Ring Buffer (bộ đệm vòng), cho phép ghi và đọc dữ liệu liên tục mà không cần sao chép bộ nhớ dư thừa hay cấp phát động thường xuyên.
Các thuộc tính quản lý trạng thái
Lớp DataBuffer sử dụng các con trỏ và chỉ số để duy trì tính nhất quán của dữ liệu:
buffer_ptr: Con trỏ gốc tới vùng nhớ đã cấp phát.write_head(tương đương back): Vị trí hiện tại để ghi dữ liệu mới vào.read_head(tương đương front): Vị trí hiện tại để lấy dữ liệu ra.total_capacity: Kích thước tối đa của bộ đệm tính bằng byte.current_fill_level: Số lượng byte đang chứa dữ liệu hợp lệ.available_space: Số byte còn trống có thể ghi thêm.
Cơ chế hoạt động FIFO
Dữ liệu được xử lý theo nguyên tắc First-In-First-Out. Khi write_head di chuyển đến cuối mảng bộ nhớ, nó sẽ tự động quay lại đầu mảng nếu chưa đầy, tạo thành một vòng lặp liên tục. Tương tự với read_head. Điều này đảm bảo hiệu suất O(1) cho cả thao tác ghi và đọc.
Các hàm API chính
Dưới đây là cách triển khai logic cốt lõi, với tên biến và cấu trúc được tái cấu trúc để rõ ràng hơn:
// Khởi tạo bộ đệm với dung lượng cụ thể
bool DataBuffer::allocate(uint32_t maxSize) {
if (maxSize == 0) return false;
// Cấp phát bộ nhớ và reset các con trỏ
buffer_memory = new uint8_t[maxSize];
write_index = 0;
read_index = 0;
capacity = maxSize;
data_size = 0;
return true;
}
// Ghi dữ liệu vào bộ đệm
uint32_t DataBuffer::push(const uint8_t* source_data, uint32_t length) {
uint32_t bytes_written = 0;
for (uint32_t i = 0; i < length; ++i) {
// Kiểm tra xem bộ đệm có đầy không
if (data_size >= capacity) break;
// Ghi byte hiện tại vào vị trí write_index
buffer_memory[write_index] = source_data[i];
// Di chuyển con trỏ ghi theo kiểu vòng lặp
write_index = (write_index + 1) % capacity;
data_size++;
bytes_written++;
}
return bytes_written;
}
// Đọc dữ liệu từ bộ đệm
uint32_t DataBuffer::pop(uint8_t* dest_buffer, uint32_t max_read_len) {
uint32_t bytes_read = 0;
for (uint32_t i = 0; i < max_read_len; ++i) {
// Nếu không còn dữ liệu để đọc thì dừng
if (data_size == 0) break;
// Sao chép byte từ vị trí read_index sang đích
dest_buffer[i] = buffer_memory[read_index];
// Di chuyển con trỏ đọc theo kiểu vòng lặp
read_index = (read_index + 1) % capacity;
data_size--;
bytes_read++;
}
return bytes_read;
}
// Tìm kiếm vị trí trong bộ đệm (cho chức năng tua video/audio)
int64_t DataBuffer::seek(int64_t offset, SeekOrigin origin) {
int64_t new_pos = 0;
switch (origin) {
case SEEK_SET:
new_pos = offset;
break;
case SEEK_CUR:
new_pos = read_index + offset;
break;
case SEEK_END:
new_pos = (capacity - data_size) + offset;
break;
}
// Đảm bảo vị trí tìm kiếm nằm trong phạm vi dữ liệu hợp lệ
if (new_pos < 0 || new_pos >= capacity) return -1;
read_index = new_pos % capacity;
return read_index;
}
Quy trình xử lý luồng phương tiện (Media Pipeline)
Dòng chảy dữ liệu đi qua ba giai đoạn chính: Tiếp nhận (Ingestion), Giải mã (Decoding) và Hiển thị/Phát (Rendering). Các thành phần tương ứng bao gồm StreamHandler, DecoderC và DataBuffer.
1. StreamHandler: Quản lý phiên kết nối
Tọa lạc tại src/server/ffplay/stream_handler.h, module này chịu trách nhiệm thiết lập kết nối với nguồn phát và đẩy dữ liệu thô vào bộ đệm.
// Mở luồng và khởi tạo context
VideoState* StreamHandler::open_stream(const char* uri, AVFormatContext* fmt_ctx) {
VideoState* state = init_video_state();
// Thiết lập callback để đổ dữ liệu vào DataBuffer
state->buffer_feed_callback = [](const uint8_t* pkt_data, size_t len) {
server_data_buffer.push(pkt_data, len);
};
// Bắt đầu quá trình nạp dữ liệu bất đồng bộ
start_ingestion_thread(state);
return state;
}
// Đóng luồng và giải phóng tài nguyên
void StreamHandler::close_stream(VideoState* state) {
stop_ingestion_thread(state);
destroy_video_state(state);
}
2. DecoderC: Giải mã dữ liệu
Trong src/server/ffplay/decoder.h, bộ giải mã lấy dữ liệu đã nén từ DataBuffer, giải mã chúng thành khung hình video (frame) hoặc mẫu âm thanh (sample).
// Khởi tạo bộ giải mã gắn với hàng đợi gói tin
int DecoderC::setup(AVCodecContext* codec_ctx, PacketQueue* queue) {
this->context = codec_ctx;
this->packet_queue = queue;
// Đăng ký hàm xử lý khi hàng đợi rỗng
SDL_cond_wait_for_empty(queue->mutex, queue->cond);
return 0;
}
// Vòng lặp giải mã chính
int DecoderC::process_frames() {
AVFrame* frame = av_frame_alloc();
int ret;
while (running) {
// Lấy gói tin từ queue
Packet packet;
if (!packet_queue_get(packet)) break;
// Gửi gói tin vào decoder
ret = avcodec_send_packet(context, &packet);
// Nhận khung hình đã giải mã
while (ret >= 0) {
ret = avcodec_receive_frame(context, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break;
// Đưa frame vào pipeline hiển thị
push_to_render_pipeline(frame);
}
}
av_frame_free(&frame);
return 0;
}
3. Luồng dữ liệu tổng thể
- Thu thập: Client gửi các chunk media qua mạng TCP/UDP. Server nhận và gọi
DataBuffer::push(). - Bộ đệm: Dữ liệu tạm thời nằm trong Ring Buffer, decouple tốc độ mạng khỏi tốc độ xử lý CPU.
- Giải mã: Thread giải mã liên tục gọi
DataBuffer::pop()để lấy dữ liệu nén, sau đó dùng FFmpeg để giải mã thành raw data. - Xuất: Raw data được đưa vào SDL Audio/Video output để phát trên thiết bị vật lý.
Chiến lược tối ưu hóa hiệu năng
Để đảm bảo trải nghiệm mượt mà ngay cả khi mạng yếu, NymphCast áp dụng các kỹ thuật sau:
- Quản lý bộ đệm thích ứng (Adaptive Buffering): Hệ thống giám sát tỷ lệ fill của
DataBuffer. Nếu buffer gần đầy, tốc độ đọc tăng lên; nếu buffer cạn kiệt, cơ chế pre-buffering sẽ kích hoạt để nạp trước dữ liệu. - Tái sử dụng bộ nhớ: Việc sử dụng Ring Buffer loại bỏ hoàn toàn các thao tác
malloc/freelặp đi lặp lại trong vòng lặp nóng (hot loop), giảm đáng kể overhead của CPU và tránh fragmentation. - Pre-roll Mechanism: Trước khi bắt đầu phát, hệ thống yêu cầu một lượng dữ liệu tối thiểu (ví dụ: 5-10 giây audio) phải được load vào buffer để tránh tình trạng "stutter" ở đầu bài hát/video.