HLS流媒体服务器基础


前言

本篇博客记录了学习HLS流媒体协议的原理、工作流程以及搭建最简单的HLS服务器的过程。通过学习HLS协议,将能够理解流媒体技术的核心概念,并具备搭建自己的流媒体服务的基础知识。


一、HLS协议是什么?

在这里插入图片描述
HLS协议一般只用作拉流观看,但是严格意义上讲,HLS并不是流式协议,工作原理就是通过HTTP协议下载静态文件。不同的是,HLS协议的文件由两部分组成,一是多个只有几秒长度的ts碎片视频文件,另一个是记录这些视频文件地址的m3u8索引文件,且这些静态文件都是直接写入磁盘的。
HLS协议可以用于点播和直播观看,其适配多种播放场景,一般加入插件就可以播放了,如网页加入HLS的js插件就可以播放了,苹果设备是原生支持HLS协议的。

在这里插入图片描述
点播的场景下,也即普通网络视频观看的场景下,m3u8索引文件会记录所有的碎片视频文件地址,HLS在点播场景下优势是更加明显的,由于HLS的相关文件是无状态的静态文件且每个文件的大小是有限的,所以负载均衡、CDN加速的效果更加明显。HLS协议的点播视频会比mp4、flv的视频更快地播放出来,且在加载中跳转视频也会更加顺滑。

在这里插入图片描述直播场景下的HLS相关文件与点播是有些不同的,视频流数据每几秒会打包成一个以ts为后缀的碎片视频文件,每生成一个新的视频文件都会同步更新m3u8索引文件,且碎片视频文件的个数是有上限的,当达到上限后,默认会将最旧的视频文件删除且更新m3u8索引文件,所以在直播的场景下,客户端也需要不断定时重新获取m3u8索引文件。
在这里插入图片描述
HLS协议优缺点。

二、实现一个最简单的HLS服务器

1.ffmpeg命令行生成m3u8切片

如下(示例):

ffmpeg -i input.mp4 -c:v libx264 -c:a copy -f hls -hls_time 10 -hls_list_size 0 -hls_start_number 0 input/index.m3u8

备注:-hls_time n: 设置每片的长度,默认值为2,单位为秒
-hls_list_size n:设置播放列表保存的最多条目,设置为0会保存有所片信息,默认值为5
-hls_start_number n:设置播放列表中sequence number的值为number,默认值为0
-hls_wrap n:设置多少片之后开始覆盖,如果设置为0则不会覆盖,默认值为0
这个选项能够避免在磁盘上存储过多的片,而且能够限制写入磁盘的最多的片的数量

test目录:
第一个是m3u8索引文件,之后都是序号从0开始的ts文件
在这里插入图片描述

2.实现

HLS主要就是将流媒体数据分成一个一个小的ts分片,通过m3u8索引文件维持的分片序号实现播放。

现在文件已经生成了,现在只需要实现服务即可。

以下是main函数的具体实现流程:

#include <stdint.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <iostream>
#include "Utils/Log.h"
#pragma comment(lib, "ws2_32.lib")
#include "Connection.h"
#include <thread>

int main() {

	
	//ffplay -i http://127.0.0.1:8080/index.m3u8


	int port = 8080;
	LOGI("1-hlsServer http://127.0.0.1:%d/index.m3u8", port);

	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		LOGE("WSAStartup error");
		return -1;
	}

	SOCKET serverFd;
	SOCKADDR_IN server_addr;
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	//server_addr.sin_addr.s_addr = inet_addr("192.168.2.61");
	server_addr.sin_port = htons(port);
	serverFd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (bind(serverFd, (SOCKADDR*)&server_addr, sizeof(SOCKADDR)) == SOCKET_ERROR) {
		LOGE("socket bind error");
		return -1;
	}
	if (listen(serverFd, SOMAXCONN) < 0) {
		LOGE("socket listen error");
		return -1;
	}


	while (true)
	{
		LOGI("等待新连接...");
		int len = sizeof(SOCKADDR);
		SOCKADDR_IN accept_addr;
		int clientFd = accept(serverFd, (SOCKADDR*)&accept_addr, &len);
		//const char* clientIp = inet_ntoa(accept_addr.sin_addr);

		if (clientFd == SOCKET_ERROR) {
			LOGE("accept connection error");
			break;
		}
		LOGI("发现新连接 clientFd=%d", clientFd);

		//std::thread t([&]() {
			Connection conn(clientFd);
			conn.start();
		//});

		//t.detach();


	}

	closesocket(serverFd);
	return 0;
}

以下是Connection.cpp:


#include "Connection.h"
#include <iostream>
#include <string>
#include <thread>
#include "Utils/Log.h"
#include "Utils/Utils.h"

#pragma warning(disable: 4996)
#include <WinSock2.h>
#include <WS2tcpip.h>

char buf[1500000];

Connection::Connection(int clientFd):mClientFd(clientFd) {
    LOGI("clientFd=%d", clientFd);
}
Connection::~Connection() {
    LOGI("clientFd=%d", mClientFd);
    closesocket(mClientFd);

}
int Connection::start() {

    char bufRecv[2000] = { 0 };         // 用于接收从客户端发来的数据
    int bufRecvSize = recv(mClientFd, bufRecv, 2000, 0);        // 将数据存储到 bufRecv 中
    LOGI("bufRecvSize=%d,bufRecv=%s", bufRecvSize, bufRecv);    // 用于存储从 HTTP 请求中解析出的 URI

    char uri[100] = { 0 };// /, /index0.ts, /index1.ts ,,,

    const char* sep = "\n";             // 用于分割 HTTP 请求的不同行
    char* line = strtok(bufRecv, sep);  // 将接收到的 HTTP 请求按照 \n 分割成不同的行,并从中提取出第一行
    while (line) {
        if (strstr(line, "GET")) {      // 检查当前行是否包含 "GET"
            if (sscanf(line, "GET %s HTTP/1.1\r\n", &uri) != 1) {   // 解析出 HTTP 请求中的 URI,并将其存储到 uri 中
                LOGE("parse uri error");

            }
        }
        line = strtok(NULL, sep);
    }
    printf("uri=%s\n", uri);

    std::string filename = "../data/test" + std::string(uri);       // 根据解析出的 URI 构建文件路径
    FILE* fp;
    fp = fopen(filename.data(), "rb");  // 打开相应的文件,准备读取数据
    if (!fp) {
        LOGE("fopen %s error", filename.data());
        return -1;
    }

    int bufLen = fread(buf, 1, sizeof(buf), fp);    // 从文件中读取数据到缓冲区 buf 中,并记录读取的数据长度

    LOGI("bufLen=%d", bufLen);
    if (fp) {
        fclose(fp);
    }

    char http_headers[2000];             // 构建 HTTP 响应头部

    if (0 == strcmp("/index.m3u8", uri)) {
        sprintf(http_headers, "HTTP/1.1 200 OK\r\n"
            "Access-Control-Allow-Origin: * \r\n"   // 让客户端能够跨域访问资源
            "Connection: keep-alive\r\n"
            "Content-Length: %d\r\n"
            "Content-Type: application/vnd.apple.mpegurl; charset=utf-8\r\n"
            "Keep-Alive: timeout=30, max=100\r\n"
            "Server: hlsServer\r\n"
            "\r\n",
            bufLen);
    }
    else {
        sprintf(http_headers, "HTTP/1.1 200 OK\r\n"
            "Access-Control-Allow-Origin: * \r\n"
            "Connection: close\r\n"
            "Content-Length: %d\r\n"
            "Content-Type: video/mp2t; charset=utf-8\r\n"
            "Keep-Alive: timeout=30, max=100\r\n"
            "Server: hlsServer\r\n"
            "\r\n",
            bufLen);
    }

    int http_headers_len = strlen(http_headers);
    LOGI("http_headers_len=%d", http_headers_len);

    send(mClientFd, http_headers, http_headers_len, 0);
    send(mClientFd, buf, bufLen, 0);

    Sleep(10);

    return 0;

}


这个服务启动以后,实际上就是一个最简单的http请求,请求和读取的是m3u8文件,第一次返回头文件,第二次返回值。如果不是index.m3u8,那就是对应的ts分片,读取分片文件并返回。这里的区别在Content-Type部分体现!
这部分通过检查 URI 是否为 “/index.m3u8”,来区分对不同类型资源的处理,主要分为两种情况:

  1. 如果 URI 为 “/index.m3u8”,则构建的 HTTP 响应头中的 Content-Type 设置为 “application/vnd.apple.mpegurl”,表示返回的是 M3U8 文件,同时使用 “Connection: keep-alive” 保持连接。然后将该响应头和文件内容发送给客户端。
  2. 如果 URI 不是 “/index.m3u8”,则构建的 HTTP 响应头中的 Content-Type 设置为 “video/mp2t”,表示返回的是 TS 文件,同时使用 “Connection: close” 关闭连接。然后将该响应头和文件内容发送给客户端。

整体流程就是根据 URI 的不同,设置不同的 Content-Type,并选择是否保持连接,然后将响应头和文件内容发送给客户端。

以上是针对点播服务,如果是直播服务的话,m3u8索引文件是会动态变化的,比如每隔5秒变化一次,这种情况下就得每隔5秒重新请求一下,重新请求后会获得更新后的ts名称,然后再根据ts名称逐个请求。后续会继续学习针对直播服务的动态变化的hls服务器。

3.抓包分析:

在这里插入图片描述


总结

希望通过本文的学习,能够对HLS流媒体协议有一个全面的了解,并且能够运用这些知识搭建自己的流媒体服务,为用户提供更优质的视频播放体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值