当前位置: 首页 > news >正文

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

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) { // >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下载

http://www.dtcms.com/a/317113.html

相关文章:

  • SRS简介及简单demo
  • 豆包新模型与PromptPilot工具深度测评:AI应用开发的全流程突破
  • 神经网络搭建对CIFAR10数据集分类
  • 生成式AI如何颠覆我们的工作和生活
  • 深度学习(pytorch版)前言:环境安装和书籍框架介绍
  • 【Canvas与三角形】黑底回环金片三角形
  • 如何解决网页视频课程进度条禁止拖动?
  • DHCP 服务器与DNS服务器
  • QML开发:QML中的基本元素
  • JAVA高级编程第六章
  • 深入解析Java NIO在高并发场景下的性能优化实践指南
  • Kubernetes服务发现、名称解析和工作负载
  • 如何根据枚举值,快速方便显示对应枚举含义 js
  • 大疆无人机连接Jetson主板
  • hive专题面试总结2
  • 疯狂星期四文案网第31天运营日记
  • GitHub Spark公共预览版上线
  • Sourcetree GIT 可视化工具安装全攻略
  • Maven补充
  • 【Linux内核系列】:信号(上)
  • HTML应用指南:利用GET请求获取全国OPPO官方授权体验店门店位置信息
  • nflsoi 8.6 题解
  • 【JavaEE】(8) 网络原理 HTTP/HTTPS
  • 使用MatterJs物理2D引擎实现重力和鼠标交互等功能,有点击事件(盒子堆叠效果)
  • GaussDB 数据库架构师修炼(六)-3 集群工具管理-主备倒换
  • CentOS7中Docker的安装与卸载
  • 8.6 CSS3rem布局
  • 聊一聊RPC接口测试工具及方法
  • 基于串口实现可扩展的硬件函数 RPC 框架(附完整 Verilog 源码)
  • 【第5话:相机模型1】针孔相机、鱼眼相机模型的介绍及其在自动驾驶中的作用及使用方法