1:功能介紹
????????在視頻這類大文件的傳輸過程中,經常會因為文件太大而受到網絡帶寬的限制。比如在實現視頻預覽功能時,常常會出現長時間加載、緩存卡頓的問題。我在項目中也遇到了類似的情況,于是采用了這個解決方案。
????????我們可以利用 FFmpeg 這個強大的工具,把體積較大的 MP4 視頻文件轉換成 HLS 格式。HLS 會將視頻切分成多個小片段:一個個 .ts
文件,同時生成一個 .m3u8
播放列表文件。
????????你可以把 .m3u8
文件理解成一個“目錄”,它告訴播放器一共有多少個視頻片段、按什么順序播放。而 .ts
文件就是按固定時長(比如每10秒一段)切出來的視頻小片段。
????????播放時,客戶端不再需要一次性加載整個視頻,而是根據 .m3u8
目錄,一個片段一個片段地按需加載。這樣即使網絡帶寬有限,也能快速開始播放,邊下邊播,大大減少了等待緩存的時間,顯著提升了用戶體驗。
????????這個方案特別適合用于在線視頻播放、課程平臺、監控回放等需要快速預覽大視頻的場景。
優點:
-
漸進式加載:客戶端按需加載小片段,無需等待整個文件下載
-
自適應碼率:支持不同網絡條件下的流暢播放
-
斷點續傳:客戶端可以從中斷處繼續播放
-
CDN 友好:便于內容分發網絡緩存
2:使用FFmpeg實現格式轉換
將 MP4 轉換為 HLS 格式轉換指令:
ffmpeg -i input.mp4 \-c:v copy -c:a copy \ # 保持原始編碼-hls_time 10 \ # 每個切片10秒-hls_list_size 0 \ # 播放列表包含所有分段-hls_segment_filename "output_%03d.ts" \ # 分段文件名output.m3u8 # 播放列表
程序實現轉換功能:
int convert_to_hls(const char *mp4_path) {// 直接從完整路徑提取文件名(不含路徑和擴展名)const char *base_name = strrchr(mp4_path, '/');base_name = base_name ? base_name + 1 : mp4_path;char file_name[256];strncpy(file_name, base_name, sizeof(file_name)-1);file_name[sizeof(file_name)-1] = '\0';// 移除擴展名char *ext = strrchr(file_name, '.');if (ext) *ext = '\0';// 確保HLS目錄存在ensure_directory(HLS_DIR);char playlist_path[512];snprintf(playlist_path, sizeof(playlist_path), "%s/%s.m3u8", HLS_DIR, file_name);// 檢查是否已轉換struct stat st;if (stat(playlist_path, &st) == 0) {printf("HLS already exists: %s\n", playlist_path);return 0;}// 獲取文件大小if (stat(mp4_path, &st)) {perror("Failed to get file size");return -1;}off_t file_size = st.st_size;// 動態計算切片時間int segment_time = 10; // 默認10秒if (file_size > 100 * 1024 * 1024) { // >100MBsegment_time = 20;}if (file_size > 500 * 1024 * 1024) { // >500MBsegment_time = 30;}if (file_size > 1024 * 1024 * 1024) { // >1GBsegment_time = 60;}printf("File size: %.2f MB, using segment time: %d seconds\n", (double)file_size/(1024*1024), segment_time);char command[4096];snprintf(command, sizeof(command),"%s -i '%s' -c:v copy -c:a copy -hls_time %d -hls_list_size 0 ""-threads 4 " // 使用4個線程加速轉換"-hls_segment_filename '%s/%s_%%03d.ts' ""'%s/%s.m3u8'", FFMPEG_PATH, mp4_path, segment_time, HLS_DIR, file_name, HLS_DIR, file_name);printf("Converting to HLS: %s\n", command);int ret = system(command);if (ret != 0) {fprintf(stderr, "FFmpeg conversion failed with code %d\n", ret);return -1;}return 0;
}
其中采用動態的切片操作根據要傳輸的文件大小來選擇執行對應的切片大小,這樣可以優化一點由于視頻文件過長而導致切片過多的現象。
3:構建嵌入式http服務器
http協議屬于應用層協議,其中使用的傳輸層是基于TCP協議進行傳輸,在c語言中創建TCP服務器采用的是socket編程。其中相關的協議就不過多介紹,附上源碼。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <poll.h>
#include <stdarg.h>
#include <dirent.h>
#include <errno.h>
#include <signal.h>
#include <sched.h>
#include "Function.h"
#include "record_management_app.h"#define OPEN_MAX 512
#define SERVER_PORT 1001typedef struct ClientInfo {struct pollfd client_fds[OPEN_MAX];
} ClientInfo; //定義客戶端總結構體ClientInfo client_info;volatile sig_atomic_t keep_running = 1;void signal_handler(int signal) {if (signal == SIGINT || signal == SIGTERM) {printf("Caught signal %d, shutting down gracefully...\n", signal);keep_running = 0;}
}void handle_connection(int num, struct sockaddr_in *client);int main(int argc, char const *argv[])
{int ret; int socket_fd; int client_fd;struct sockaddr_in server;struct sockaddr_in client;socklen_t client_len = sizeof(client);// 設置退出信號處理器struct sigaction term_sa;memset(&term_sa, 0, sizeof(term_sa));term_sa.sa_handler = signal_handler;sigemptyset(&term_sa.sa_mask);term_sa.sa_flags = 0; // 關鍵:不自動重啟系統調用if (sigaction(SIGINT, &term_sa, NULL) == -1) {perror("sigaction(SIGINT) failed");exit(EXIT_FAILURE);}if (sigaction(SIGTERM, &term_sa, NULL) == -1) {perror("sigaction(SIGTERM) failed");exit(EXIT_FAILURE);}// 設置 SIGCHLD 信號處理器struct sigaction sa;memset(&sa, 0, sizeof(sa));sa.sa_handler = sigchld_handler;sigemptyset(&sa.sa_mask);sa.sa_flags = SA_RESTART;sigaction(SIGTERM, &sa, NULL); // kill 命令或系統關機信號if (sigaction(SIGCHLD, &sa, NULL) == -1) {perror("sigaction failed");exit(EXIT_FAILURE);}// 創建socket 對象socket_fd = socket(AF_INET, SOCK_STREAM, 0);if (socket_fd < 0) {perror("socket");return -1;}printf("create socket success, socket = %d\n", socket_fd);//端口復用int optval = 1;if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) {perror("setsockopt(SO_REUSEADDR) failed");exit(EXIT_FAILURE);}// 給服務器綁定 net_info.ipmemset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(SERVER_PORT);server.sin_addr.s_addr = INADDR_ANY;printf("Port: %d\n", SERVER_PORT);ret = bind(socket_fd, (struct sockaddr *)&server, sizeof(server));if (ret < 0) {perror("bind");return -1;}printf("bind success\n");// 創建最大連接數量ret = listen(socket_fd, 10);if (ret < 0) {perror("listen");}//添加監聽描述符client_info.client_fds[0].fd = socket_fd;client_info.client_fds[0].events = POLLIN; // 監聽讀事件//初始化客戶連接描述符for (int i = 1; i < OPEN_MAX; i++) {client_info.client_fds[i].fd = -1;}int nready = 0; // 可以描述符個數int i = 1; // 存儲下一個要添加的描述符的下標// 主循環,監聽并處理客戶端的連接while (keep_running) {//獲取可用描述符的個數nready = poll(client_info.client_fds, OPEN_MAX, 1000); if (nready == -1) {if (errno == EINTR) {// 如果是被信號中斷,則繼續循環continue;} else {perror("poll error:");continue; // 繼續循環而不是退出}}//測試監聽描述符是否準備好if (client_info.client_fds[0].revents & POLLIN){client_fd = accept(socket_fd, (struct sockaddr *)&client, &client_len);if (client_fd == -1){perror("accept error:");exit(1);} printf("one client coming, net_info.ip = %s\n", inet_ntoa(client.sin_addr));//將新的連接描述符添加到數組中for (i = 0; i < OPEN_MAX; i++){if (client_info.client_fds[i].fd < 0){client_info.client_fds[i].fd = client_fd;break;}}if (i == OPEN_MAX){printf("too many clients\n");exit(1);}//將新的描述符添加到讀描述符集合中client_info.client_fds[i].events = POLLIN;// 主線程不再監聽新的連接if (--nready <= 0){continue;}}//處理客戶連接handle_connection(OPEN_MAX, &client);}return 0;
}
//接口處理函數
void handle_connection(int num, struct sockaddr_in *client)
{int i = 0;size_t cnt = 0;uint8_t rbuf[65535] = {0}; // 增大緩沖區大小為64kbfor (i = 0; i < num; i++){if (client_info.client_fds[i].fd < 0) continue;//測試客戶端描述符是否準備好if(client_info.client_fds[i].revents & POLLIN){cnt = read(client_info.client_fds[i].fd, rbuf, sizeof(rbuf));if (cnt == 0){close(client_info.client_fds[i].fd);printf("client %s disconnect\n", inet_ntoa(client->sin_addr));client_info.client_fds[i].fd = -1;continue;}if (cnt < 0){if (errno == EAGAIN || errno == EWOULDBLOCK) continue; // 非阻塞模式下,沒有數據可讀perror("read error:");continue;}printf("rbuf: \r%s\n", rbuf);ApiPath api_path = {0};if (parse_api_path((char*)rbuf, &api_path) != 0) {printf("Invalid API path format\n");continue;}printf("Parsed Topic: %s, Method: %s\n", api_path.topic, api_path.method);if (strncmp((const char*)rbuf, "GET", 3) == 0) { if (strncmp(api_path.topic, "record_management", 17) == 0) {printf("進入視頻預覽功能\n");// 清理 method,去掉空格之后的內容char *space = strchr(api_path.method, ' ');if (space) {*space = '\0';}char decoded_method[200];url_decode(api_path.method, decoded_method, sizeof(decoded_method));printf("Method: %s\n", decoded_method);// 處理HLS文件請求(.m3u8或.ts)if (strstr(decoded_method, ".m3u8") || strstr(decoded_method, ".ts")) {char file_path[512];// 直接定位到HLS目錄snprintf(file_path, sizeof(file_path), "%s/hls/%s", MOUNT_POINT, decoded_method);const char *content_type = strstr(decoded_method, ".m3u8") ? "application/x-mpegURL" : "video/MP2T";send_file(client_info.client_fds[i].fd, file_path, content_type);continue;}// 啟動HLS流char *video_name = strdup(decoded_method);if (!video_name) {perror("strdup failed");continue;}// 移除可能的文件擴展名char *ext = strrchr(video_name, '.');if (ext) *ext = '\0';// 準備線程參數size_t arg_size = sizeof(int) + strlen(video_name) + 1;void *thread_arg = malloc(arg_size);if (!thread_arg) {perror("malloc for thread_arg failed");free(video_name);continue;}// 復制客戶端文件描述符和視頻名int client_fd = client_info.client_fds[i].fd;memcpy(thread_arg, &client_fd, sizeof(int));memcpy(thread_arg + sizeof(int), video_name, strlen(video_name) + 1);pthread_t hls_thread;pthread_create(&hls_thread, NULL, send_hls_stream, thread_arg);pthread_detach(hls_thread);free(video_name);}}}}
}
4:編譯與運行
Makefile
# 設置SDK根目錄
SYSROOT := /home/qingwu007/aarch64-buildroot-linux-gnu_sdk-buildroot# 設置工具鏈前綴
BUILD_TOOL_DIR := $(SYSROOT)
BUILD_TOOL_PREFIX := $(BUILD_TOOL_DIR)/bin/aarch64-buildroot-linux-gnu-# 定義工具鏈
CC := $(BUILD_TOOL_PREFIX)gcc
AR := $(BUILD_TOOL_PREFIX)ar
LD := $(BUILD_TOOL_PREFIX)gcc# 編譯參數
CFLAGS := -g -Wall \--sysroot=$(SYSROOT) \-I$(SYSROOT)/include \-I$(SYSROOT)/usr/include \-I$(SYSROOT)/cjson \-I$(SYSROOT)/usr/include/aarch64-buildroot-linux-gnu \-I./include# 鏈接參數
LDFLAGS := --sysroot=$(SYSROOT) \-L$(SYSROOT)/lib64 \-L$(SYSROOT)/usr/lib64 \-Wl,-rpath-link,$(SYSROOT)/lib64 \-Wl,-rpath-link,$(SYSROOT)/usr/lib64 \-Wl,-rpath,/opt/app/bin # 添加這一行,指定運行時庫路徑-Wl,--dynamic-linker=/lib64/ld-linux-aarch64.so.1 \-fPIC# 需要鏈接的庫
LIBS := -lavcodec -lavdevice -lavfilter -lavformat -lavutil -lc -lcjson -lpthread# 目標設置
TARGET := hettp_save# 源文件處理 - 自動查找src目錄下的所有.c文件
SRC_DIR := src
SRCS := $(wildcard $(SRC_DIR)/*.c)
OBJS := $(patsubst $(SRC_DIR)/%.c,%.o,$(SRCS)).PHONY: all cleanall: $(TARGET)$(TARGET): $(OBJS)$(LD) -o $@ $^ $(LDFLAGS) $(LIBS)# 模式規則:編譯源文件
%.o: $(SRC_DIR)/%.c@echo "Compiling $<..."$(CC) $(CFLAGS) -c $< -o $@# 靜態庫目標示例
libexample.a: $(OBJS)$(AR) rcs $@ $^clean:rm -f $(TARGET) $(OBJS) libexample.a# 安裝目標
install: $(TARGET)cp $(TARGET) /usr/local/bin# 調試目標
debug: CFLAGS += -DDEBUG -O0
debug: clean all.PHONY: install debug
我在http服務器上面寫的接口是:
http://IP:port/_api/app/record_management/xxxxx
測試的接口根據自己的環境來確定。
我的視頻文件是放在開發板里面的,然后通過搭建的http服務器加上ffmpeg就可以實現本地視頻的預覽和播放了。
我使用VLC來進行測試:
可以看到上面的請求數據,就按照這個視頻的一個個切片請求這樣就可以實現大視頻文件的預覽傳輸。但是這樣也會有一個問題當要傳輸的文件過于大的話要進行切片的時間也就越長,但是相比較與直接進行視頻文件的傳輸還是較為好用的。完整的程序放在了我的資源中有需要自取,我是在rk3588上面跑的環境,根據自己的環境跟換Makefile即可。
【免費】在rk3588上面基于FFmpeg和HLS的大文件分片傳輸方案,以實現大視頻文件高效預覽效果資源-CSDN下載