流媒体服务-传输延时(SEI插帧)

news2024/11/18 13:41:49

什么是延时

很多小伙伴认为,当推流端和拉流端显示的时间不一致,即为延时。

其实这种看法是比较片面的,不同的播放器,对同一路流进行测试,可能会得到不同的结果。

一般来说,延时为以下几个部分的累加组成

  • 采集延时

在采集摄像头或显卡画面时,由于fps的限制和cpu性能、内存拷贝速度等客观限制,采集画面成YUV/RGB等数据时会有一定的延时,一般延时为毫秒级别。由于一般编码器对输入数据格式存在限制,譬如要求统一输入YUV420P,这样在做RGB->YUV420P转换时,也会有转换计算延时(这个可以通过libyuv库来降低)。总而言之,采集延时大概为毫秒级别,如果fps为30,那么一般采集延时会有30毫秒以上的延时,在内存拷贝和颜色转换时,又可能增加若干毫秒的延时。

  • 编码延时

在把原始画面输入到编码器时,并不会立即输出编码后的数据,特别是在开启B帧时,由于需要参考后面的P帧,那么延时会更大,所以延时敏感的情况下一般不开启B帧,这种情况下编码延时应该是毫秒级别,不是很大。

  • 网络上行传输延时

编码后的数据,要经过一定的协议打包才能写入socket,然后传输给推流服务器或拉流代理服务器,协议打包会有一定的内存拷贝和计算量,那么会增加延时,不过这个延时很小,基本忽略不计。数据在上传到服务器时,这个延时可大可小,取决于网络质量。

  • 服务器转协议延时

服务器在收到数据后,要读socket缓存、协议解析、解复用、重新打包等操作,不过总体而言,这个延时比较小,基本没什么影响。有时,服务器为了提高性能,会采取合并写的机制,也就是收到一定量的数据后才会一并转发,这个延时一般为几百毫秒。不过一般服务器会默认不会打开此机制

  • 网络下行延时

流媒体在把视频数据转发给播放器时,会存在网络发送,这个延时大小取决于网络质量。

  • 播放器延时

播放器延时主要有网络接收延时、协议解析解复用延时、解码延时、缓存延时、渲染延时组成,这些延时中缓存延时最大,因为一般的播放器为了保证在网络抖动情况下视频播放的流畅性,会以增加延时为代价,增加播放缓存,这样在网络变差时,不至于播放缓冲卡顿。而且为了音视频同步,也必须确保一定的缓存量。这种延时一般都是秒级别,一般5秒左右。

  • 播放器GOP缓存延时

流媒体服务器为了能让播放器立即出画面,往往会缓存最近的一个I帧,这个I帧往后的所有音视频数据被称作为GOP缓存。如果不缓存GOP,那么播放器要等下一个I帧才能解码成功或不花屏,显然为了提高播放体验,这个GOP缓存是不能去掉的。而一般GOP短则1~3秒,长则10几秒,这个跟采集端编码器设置有关,服务器改变不了。但是由于一般的播放器收到缓存后,并不会丢弃过多的画面来确保低延时。况且播放器还希望有一定的缓存来确保播放的流畅性,所以这个GOP缓存将会增大播放器的延时。

  • 综合延时

以上所有的延时累加,就是你观看到的直观延时。通常大部分延时可能是由播放器造成,如果对播放器缓冲区感兴趣的同学可以参考这篇文章:https://zhuanlan.zhihu.com/p/51582357

如何计算延时

本文所讨论的延时为网络传输延时,也就是经过采集编码后的数据,经由推流端通过网络发送到到流媒体服务器流媒体服务器将数据通过网络推送到到拉流端的延时。

本文推荐使用在码流中混入SEI帧的方式来计算传输延时,具体步骤如下

  • 推流端在I帧之前插入SEI帧,内容为推流端时间戳
  • 拉流端在接收到SEI帧之后,解码出推流端时间戳,与拉流端时间戳对比,计算出延时

在这里插入图片描述

SEI 帧

先复习一下H264码流结构

  • H.264 原始码流组成结构

H.264 原始码流(裸流)是由一个接一个 NALU 组成。它的功能分为两层,VCL(视频编码层)和 NAL(网络提取层)。

为了方便从字节流中提取出 NALU,协议规定,在每个 NALU 的前面加上起始码(StartCode): 0x000001 或 0x00000001。

  • NALU 组成结构

NALU(NAL Unit)= 一组对应于视频编码的 NALU 头部信息(NAL header)+ 一个 RBSP(Raw Byte Sequence Payload,原始字节序列负荷)

NAL Unit Type 常⻅类型如下:

NAL Unit TypeNAL Unit Content
1非 IDR 图像,且不采用数据划分的片段。
5IDR 图像。
6补充增强信息(SEI)。
7序列参数集(SPS)。
8图像参数集(PPS)。
11流结束符。

那么NAL Unit Type6时,即为SEI帧。

SEI payload type 计算方式

当开始解析类型为 SEI 的 NAL 时,在 RBSP 中持续读取 8 bit,直到非 0xff 为止,然后把读取的数值累加,累加值即为 SEI payload type。

SEI RBSP 结构图如下:

SEI payload size 计算方式

读取 SEI payload size 的逻辑与 SEI payload type 类似,即读取到非 0xff 为止,这样可以支持任意⻓度的 SEI payload 添加。假设 SEI payload type 后面的字符序列是 FF FF AA BB …,则 FF FF AA 将会解析成 SEI payload size,为 255 + 255 + 170 = 680。

实例代码

// @brief: 将时间戳写入sei frame,将sei frame写入文件
#include <iostream>
#include <vector>
#include <cassert>
#include <fstream>
#include <string>
#include <chrono>

std::vector<uint8_t> MakeSei(const std::string& data)
{
    // 使用1个字节存储payload
    assert(data.size() + 16 < 255);

    std::vector<uint8_t> seiFrame;
    std::vector<uint8_t> uuid(16, 0x41);
    uint8_t              payloadSize = 16 + data.size();
    seiFrame.insert(seiFrame.end(), {0x00, 0x00, 0x00, 0x01}); // start code
    seiFrame.insert(seiFrame.end(), {0x06});                   // nalu type
    seiFrame.insert(seiFrame.end(), {0x05});                   // sei unregister data type
    seiFrame.push_back(payloadSize);                           // sei payload size
    seiFrame.insert(seiFrame.end(), uuid.begin(), uuid.end()); // uuid,这里可以替换成你自己的
    for (auto& ch : data)
        seiFrame.push_back(ch); // custom message
    seiFrame.push_back(0x80);   // rbsp trailing bits

    return seiFrame;
}

int main()
{
    std::ofstream file("sei.h264", std::ios::binary);
    std::string   data =
        "ts:" + std::to_string(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count());
    auto sei = MakeSei(data);
    file.write(reinterpret_cast<char*>(sei.data()), sei.size());
    file.close();
    return 0;
}

这里在SEI中写入的数据格式为ts:{timestamp},你也可以定义为你希望的数据格式,如json,注意不要超过255 - 16个字节。

生成的帧信息如下:

推流端和拉流端如何进行时钟对齐

在拉流端拿到SEI frame之后,解码出推流端时间戳,计算delay = {拉流端时间戳} - {推流端时间戳}

这里存在一个问题是,拉流端系统时钟可能与推流端系统时钟不一致(如人为调整过系统时间),导致延时计算不准确,甚至是拉流端时间戳早于推流端时间戳。那么这时候就需要将推流端和拉流端的时间戳进行对齐

一般选择流媒体服务器时间戳进行对齐。

计算方法如下:

在这里插入图片描述

  • 参考文章

https://github.com/ZLMediaKit/ZLMediaKit/wiki/%E6%80%8E%E4%B9%88%E6%B5%8B%E8%AF%95ZLMediaKit%E7%9A%84%E5%BB%B6%E6%97%B6%EF%BC%9F

https://doc-zh.zego.im/faq/sei?product=ExpressAudio&platform=macos

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/886677.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

最近抖音很火的情侣飞行棋

最近抖音很火的情侣飞行棋 最近抖音很火的情侣飞行棋&#xff0c;这款情侣飞行棋提供了丰富的游戏玩法&#xff0c;可以为情侣、朋友或家人带来欢乐的游戏体验。扫码进行体验识别 无论是在家中&#xff0c;还是在聚会、旅行等场合&#xff0c;都可以轻松启动该网站&#xff0c…

为何千万别学网络安全专业(网络安全小白避坑的建议解析)

前言&#xff1a; 近年来&#xff0c;随着国家对网络安全的战略关注和新基建的持续投入&#xff0c;网络安全专业成为一个热门话题。然而&#xff0c;好专业不一定就能找到好工作&#xff0c;对于想从事网络安全专业的小白们&#xff0c;需要持谨慎态度&#xff0c;避免走一些…

案例:用户登录/注册

文章目录 技术框架说明登录案例1.需求分析2.环境准备2.1 前端页面2.2 创建数据表及对应的实体类2.3 导入mybatis坐标&#xff0c;MySQL坐标2.4 配置文件及接口 3. 用户名密码校验4. 前端配置5.Servlet编写 注册案例1.需求分析2.配置用户接口3. 测试添加用户4. 前端配置5. servl…

【概念理解】STM32中的sprintf()函数

sprintf()函数 这个函数在 stdio.h中&#xff1b;可以将格式化的数据写入到一个字符串缓冲区中。 int sprintf(char *str, const char *format, ...);str&#xff1a;指向字符数组的指针&#xff0c;即用于存储格式化后字符串的缓冲区。format&#xff1a;格式化字符串&#…

通过nvm切换nodejs版本

下载&#xff1a; 1.下载nvm地址&#xff1a; https://github.com/coreybutler/nvm-windows/releases 下载该安装包&#xff0c;下载后无需配置就可以使用&#xff0c;十分方便。 简单说明一些包&#xff1a; nvm - noinstall.zip &#xff1a; 这个是绿色免安装版本&#…

c++ std::shared_ptr的线程安全问题(race condition)

有 3 个 shared_ptr 对象 x、g、n; 两个工作线程&#xff1a; void main(){shared_ptr g(new Foo); // 线程之间共享的 shared_ptr shared_ptr x; // 线程 A 的局部变量 shared_ptr n(new Foo); // 线程 B 的局部变量std::thread([&]{x g; }).detach();std::thread([&…

分布式 - 消息队列Kafka:Kafka 消费者的消费位移

文章目录 01. Kafka 分区位移02. Kafka 消费位移03. kafka 消费位移的作用04. Kafka 消费位移的提交05. kafka 消费位移的存储位置06. Kafka 消费位移与消费者提交的位移07. kafka 消费位移的提交时机08. Kafka 维护消费状态跟踪的方法 01. Kafka 分区位移 对于Kafka中的分区而…

每日一题——移动零

移动零 题目链接 思路——双指针 如果可以开辟额外的空间&#xff0c;那这题十分好做。我们开辟和nums同样大小的空间&#xff0c;将遍历数组&#xff0c;将非零元素从头放置&#xff0c;将零从后往前放置&#xff0c;这样就可以将所有的零放到后面&#xff0c;同时保证非零元…

安全狗获批成为算网融合产业及标准推进委员会伙伴单位

近日&#xff0c;安全狗获批成为中国通信标准化协会算网融合产业及标准推进委员会伙伴单位。 据悉&#xff0c;中国通信标准化协会算网融合产业及标准推进委员会&#xff0c;致力于算网融合、数字化转型、SDN/NFV、SD-WAN、新基建、信息安全、边缘计算、高性能计算领域及典型应…

品牌营销|所有产品都值得用 AI 再做一遍

微软 CEO Satya Nadella 曾经说过&#xff1a;“所有的产品都值得用 AI 重做一遍。” AI 大模型的出现&#xff0c;开启了一个全新的智能化时代&#xff0c;重新定义了人机交互。这让生成式 AI 技术变得「触手可得」&#xff0c;也让各行业看到 AGI 驱动商业增长的更大可能性。…

基于注册中心如何实现全链路灰度

1. 为什么需要服务发现? 2. 微服务注册中心 3. 基于注册中心如何实现全链路灰度 4. GRPC 如何结合注册中心 GRPC服务发现与全链路灰度 为什么需要服务发现? 服务拆分 配置调用 如果有很多服务怎么办&#xff1f; 服务注册 服务发现 注册中心的架构 配置与使用 常见的…

西瓜书之神经网络

一&#xff0c;神经元模型 所谓神经网络&#xff0c; 目前用得最广泛的一个定义是“神经网络是由具有适应性的简单单元组成的广泛并行互连的网络&#xff0c;它的组织能够模拟生物神经系统对真实世界物体所做出的交互反应”。 M-P神经元 M-P神经元&#xff1a;接收n个输入(…

存算分离实践:构建轻量、云中立的大数据平台

今天我们将分享社区用户多点 DMALL 的案例。多点 DMALL 是亚洲领先的全渠道数字零售解决方案服务商&#xff0c;目前已与 380 家零售企业达成合作&#xff0c;覆盖 6 个国家和地区。 面对 B 端客户日益增长的企业数据&#xff0c;存算一体的架构显得力不从心。计算资源冗余浪费…

CAS问题汇总

CAS的执行流程&#xff1f; CAS比较比替换的大致流程是这样的: 首先它有三个参数 &#xff1a; V 内存值 A 预期的旧值 B 新值比较V的值与A的值是否相等如果相等的话&#xff0c;则将V的值替换成B&#xff0c;否则就提示修改失败。 一般正常情况的话就是没有其他线程修改内存…

解决内网GitLab 社区版 15.11.13项目拉取失败

问题描述 GitLab 社区版 发布不久&#xff0c;搭建在内网拉取项目报错&#xff0c;可能提示 unable to access https://github.comxxxxxxxxxxx: Failed to connect to xxxxxxxxxxxxxGit clone error - Invalid argument error:14077438:SSL routines:SSL23_GET_S 15.11.13ht…

工业互联网产业联盟发布《2023可信工业数据流通应用案例集》

导读 随着新一代信息技术与制造业的深度融合发展&#xff0c;全球工业数据应用已经进入纵深发展的新阶段&#xff0c;数据作为新型生产要素和重要战略资源&#xff0c;正在制造业数字化转型过程中发挥出更大的作用。在这一进程中&#xff0c;工业数据的流通共享受到广泛关注。…

深入探索JavaEE单体架构、微服务架构与云原生架构

课程链接&#xff1a; 链接: https://pan.baidu.com/s/1xSI1ofwYXfqOchfwszCZnA?pwd4s99 提取码: 4s99 复制这段内容后打开百度网盘手机App&#xff0c;操作更方便哦 --来自百度网盘超级会员v4的分享 课程介绍&#xff1a; &#x1f50d;【00】模块零&#xff1a;开营直播&a…

LeetCode集

目录 1、算法1.1 排序1.1.1 冒泡排序1.1.1.1 简单交换排序1.1.1.2 冒泡排序 1.1.2 简单选择排序1.1.3 直接插入排序1.1.4 希尔排序1.1.5 堆排序1.1.6 归并排序1.1.7 快速排序 1.1 位运算/二进制1.1.1 Java中的正数、负数1.1.2 Java中的位运算1.1.3 比特位计数1.1.4 2的幂1.1.5 …

【腾讯云Cloud Studio实战训练营】Cloud Studio + iPad,让代码之舞飞扬在指尖

Cloud Studio iPad&#xff0c;让代码之舞飞扬在指尖 妙手偶得&#xff0c;开启神奇之旅立即反馈&#xff0c;一切尽在掌握版本控制&#xff0c;简单易用MetaWork 协作&#xff0c;摸鱼变得不再轻松 ​一直以来&#xff0c;开发者大多都习惯在电脑端开发&#xff0c;而iPad只是…

Swift 基础

工程目录 请点击下面工程名称&#xff0c;跳转到代码的仓库页面&#xff0c;将工程 下载下来 Demo Code 里有详细的注释 点击下载代码&#xff1a;swift-01