基于FFmpeg和HLS的大文件分片传输方案

发布于:2025-08-07 ⋅ 阅读:(16) ⋅ 点赞:(0)

1:功能介绍

        在视频这类大文件的传输过程中,经常会因为文件太大而受到网络带宽的限制。比如在实现视频预览功能时,常常会出现长时间加载、缓存卡顿的问题。我在项目中也遇到了类似的情况,于是采用了这个解决方案。

        我们可以利用 FFmpeg 这个强大的工具,把体积较大的 MP4 视频文件转换成 HLS 格式。HLS 会将视频切分成多个小片段:一个个 .ts 文件,同时生成一个 .m3u8 播放列表文件。

        你可以把 .m3u8 文件理解成一个“目录”,它告诉播放器一共有多少个视频片段、按什么顺序播放。而 .ts 文件就是按固定时长(比如每10秒一段)切出来的视频小片段。

        播放时,客户端不再需要一次性加载整个视频,而是根据 .m3u8 目录,一个片段一个片段地按需加载。这样即使网络带宽有限,也能快速开始播放,边下边播,大大减少了等待缓存的时间,显著提升了用户体验。

        这个方案特别适合用于在线视频播放、课程平台、监控回放等需要快速预览大视频的场景。

优点:

  1. 渐进式加载:客户端按需加载小片段,无需等待整个文件下载

  2. 自适应码率:支持不同网络条件下的流畅播放

  3. 断点续传:客户端可以从中断处继续播放

  4. 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) { // >100MB
        segment_time = 20;
    }
    if (file_size > 500 * 1024 * 1024) { // >500MB
        segment_time = 30;
    }
    if (file_size > 1024 * 1024 * 1024) { // >1GB
        segment_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 1001

typedef 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.ip
    memset(&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};   // 增大缓冲区大小为64kb

    for (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 clean

all: $(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下载


网站公告

今日签到

点亮在社区的每一天
去签到