超分服务的分量保存
分量说明
分量的概念主要是对于显卡解码,编码和网络传输而言,显卡可以同时进行几个线程,多个显卡可以分布式计算,对分量进行AI识别,比如我们有cuda的显卡,cuda的核心量可以分给不同的分片视频,第一步先将视频减小,第二部分割视频。对于小视频片而言,不同的智能盒子也可以接收网络传输来进行接收数据,进行并行识别服务。这就是我说的分量服务的概念。
采样
在超分服务中,上采样和下采样是两个重要的操作,分别用于增加和减少图像的分辨率。我们在下采样后存储文件和传输,可以显著减少存储的量,同时减少网络的传输量,而接收端收到以后再进行上采样放大,同时进行AI 分析
超分服务说明
实现超分服务,将实时视频能够缩小并且传输和保存,同时需要将文件分成片,同时保证每个文件的最后一帧和下一个文件的第一帧相同,还需要解决两个问题
文件切片是问了能够分布式传输出去,同时本地如果有多个显卡,可以同时进行文件的搜索,比如再多个文件中同时启动AI服务,搜索同一个人脸。
采用rtsp,rtmp服务器接收,rtmp服务器在接收文件时保存为flv文件,为了不污染任何代码,不采用开源的各种服务,使用c++20 去写rtmp和rtsp服务器,这个花了两天时间,协议没啥问题,结果反而时卡在了文件保存上面,下面具体说几个问题,主要体现在时间戳上面。
以下为flv文件保存的要素,首先时flv 头部,在头部中查找0x17 和 0x00 0x01
*/
//0x17 key 00(AVCPacketType ) 00 表示 是AVCDecoderConfigurationRecord
//0x17 key 01(AVCPacketType ) 01 表示 raw data ->nalu
//0x27 not key 01 01 表示 raw data ->nalu
//FLV head 9 bytes
//4 bytes previous tag
//tag data —>11 bytes head -> 5 bytes video head -> nalu data
//4 bytes previous tag
//tag data
熟悉flv文件格式的人一看就懂,无需多言。
1 关键帧问题
2 时间戳问题
关键帧问题
必须保证一个文件第一帧一定为关键帧,所以在分割视频的时候必须能够拿到关键帧的时候才能分割,为了能够保证未丢失文件,上一个文件的最后一帧为下一个文件的第一帧,否则会有很多依靠关键帧解码的p帧b帧无法解码,变成比较难受的绿色,也有可能为绿加黑。
分量保存的时间戳
看下图,
显然除了第一个文件是正确的,但除了第一个分量文件,其他文件第一帧的时间戳是不对的,这是因为AVC sequence header 总是零,vlc播放的时候计算的时间就不正确了,那么就有两个方法:
1 是修改 sequence header的时间戳,
2 是修改每一帧时间戳,
这里有一个问题要说明,就是整体直播出去的时候关键帧的时间戳肯定是对的,为了让文件比较正常,采取修改每一帧时间戳。
总结一下flv头部, 11 个字节头部后,如果是视频,加5个字节的扩展,后面就是nalu数据,如果是音频,加2个字节的扩展,视频5个字节里面第一个就是判别是关键帧和非关键帧的紧要,这里简单一点先用0x17 0x27来判别,注意实际上不是这样,只有h264才是这个值,先找定时间戳,假定我们从协议里面获取的时间戳是正确的,看下面的代码
//11 个字节头部
static void pack_tag_header(uint8_t *buf, uint8_t type, uint32_t data_size, uint32_t timestamp) {
//8 audio ; 9 video ; 18 script
//8 is the most
if (type == 8 || type == 9 || type == 18)
{
*buf++ = type;//one bytes
buf = write_be_ui24(buf, data_size); //three bytes
buf = write_be_ui24(buf, timestamp & 0xffffff);//three bytes
*buf++ = timestamp >> 24; //one bytes
buf = write_be_ui24(buf, 0); //three bytes
}
}
//flv header length is 11
//type 8:audio, 9:video, 18:script meta
static void pack_tag(uint8_t* header, ptr_s_memory mem, uint8_t type, uint32_t timestamp) {
pack_tag_header(header, type, (uint32_t)mem->v_len, timestamp);
uint8_t* p = mem->v_data_r + mem->v_len;
//last write the frame length ,it must include the header length
write_be_ui32(p, FLV_TAG_HEADER_LEN + (uint32_t)mem->v_len);
}
1 2 3 4 5 6 7 8 9 10 11
09 xx xx xx 00 7c 79 00 00 00 00
11 个flv字节头部里面有四个字节包含了时间戳,第5个字节到第8,也是我们自己的代码写入的,我们要做的就是重写时间戳,但是不能修改传入的tag数组,这是外面传输出去要用的
0 和 1 之间无缝衔接,同时每个文件的时间长度和时间戳都保证正确
开头和结尾衔接
相邻两个文件开头和结尾为同一帧
code
主要就是需要重新改写时间戳,直接看代码
#pragma once
#include <stdint.h>
#include <stdio.h>
#include <string>
#include "c_hub.h"
#include "util_flv_pack.h"
//flv 文件读写
class c_flv_writer
{
FILE* v_fp = NULL;
int64_t v_num = 0;
uint32_t v_hash = 0;
std::string v_deviceurl;
uint32_t v_record_timestamp = 0;
public:
ptr_s_memory v_head_video = nullptr;
ptr_s_memory v_head_audio = nullptr;
int v_frame_count = 2000;
int v_frame_record = 0;
int v_inited = 0;
protected:
std::string GetFileName()
{
//判断v_deviceurl是否"/"结尾
std::string name = v_deviceurl + std::to_string(v_hash);
name +="_" + std::to_string(v_num);
name += ".flv";
v_num++;
return name;
}
public:
void initStart(std::string deviceurl, uint32_t hash, ptr_s_memory v, ptr_s_memory a)
{
v_hash = hash;
v_deviceurl = deviceurl;
v_head_video = v;
v_head_audio = a;
v_inited = 1;
}
static void modify_timestamp(uint8_t* buf, uint32_t timestamp) {
buf = buf + 4; // write_be_ui24(buf, data_size); //three bytes
buf = write_be_ui24(buf, timestamp & 0xffffff);//three bytes
*buf++ = timestamp >> 24; //one bytes
}
int writeStart(uint32_t ts)
{
if (v_fp == NULL)
{
v_frame_record = 0;
std::string name = GetFileName();
v_fp = fopen(name.c_str(), "wb+");
if (v_fp == NULL)
return -1;
fwrite(FLV_HEADER_BUF_13, 13, 1, v_fp);
//video head
if (v_head_video != nullptr)
{
uint8_t* data_v = v_head_video->v_data_h; //flvhub->v_cache_hv->v_data_h;
size_t len_v = v_head_video->v_len + 11 + 4;
fwrite(data_v, len_v, 1, v_fp);
}
//audio head
if (v_head_audio != nullptr)
{
uint8_t* data_a = v_head_audio->v_data_h;
size_t len_a = v_head_audio->v_len + 11 + 4;
fwrite(data_a, len_a, 1, v_fp);
}
}
v_record_timestamp = ts;
return 0;
}
void writeData(uint8_t* tag, int taglen, uint8_t* data, size_t len, uint32_t ts)
{
if (v_fp == NULL)
writeStart(ts);
//遇到关键帧才能重新开始
if (v_fp != NULL /*&& v_frame_record < v_frame_count*/)
{
uint8_t a = *data;
uint8_t b = *(data + 1);
if (v_frame_record > v_frame_count && ((a == 0x17) && (b == 0x01)))
{
//需要重复最后一帧放开
uint8_t newtag[11];
memcpy(newtag, tag, 11);
uint32_t nowts = ts - v_record_timestamp;
modify_timestamp(newtag, nowts);
fwrite(newtag, taglen, 1, v_fp);
fwrite(data, len, 1, v_fp);
fclose(v_fp);
v_fp = NULL;
std::cout << "close the file now" << std::endl;
writeStart(ts);
}
uint8_t newtag[11];
memcpy(newtag, tag, 11);
uint32_t nowts = ts - v_record_timestamp;
modify_timestamp(newtag, nowts);
fwrite(newtag, taglen, 1, v_fp);
fwrite(data, len, 1, v_fp);
v_frame_record++;
std::cout << "write the number " << v_frame_record << std::endl;
}
}
};
调用
调用的时候放在音视频接收以后并且下采样结束的地方
if (flvhub->v_flv_w.v_inited == 0)
{
flvhub->v_flv_w.initStart("./", hash, flvhub->v_cache_hv, flvhub->v_cache_ha);
flvhub->v_flv_w.v_inited = 1;
}
flvhub->v_flv_w.writeData(tag,taglen, mem->v_data_r ,len, mem->v_ts);
其他编码
由于rtmp协议已经加入enhanced 扩展,rtmp/flv已经有统一支持H265的国际版本,我后面会修改rtmp server,加入对h265的支持,那么这边存储flv 也必须进行修改,适应编码