https://www.cnblogs.com/dyan1024/p/10224538.html
简单实现h264转ts
转载注明出处:https://www.cnblogs.com/dyan1024/p/10224538.html
最近一个项目中需要在项目中临时嵌入h264裸流编码成ts的代码,但是以前从没接触过视频,先是在网上搜索了2~3天(主要是想找个能用demo看看编码流程借鉴下)。只找到ffmpeg命令行转码的,ffmpeg也没接触过没那时间去分析怎么转的,而且我们不想使用ffmpeg这个库,之后还要将编码过程从ffmpeg中提取出来,又是一个比较大的难点。总之没头苍蝇一样搜索心累,耐心用完了,算了算了看看格式自己编好了。
然后又用了2~3天初步完成了转码,因为网上很难找到这样的demo可以参考,所以有了这篇文章。
以下是参考博客及规范链接:
hls之m3u8、ts、h264、AAC流格式详解
将H264与AAC打包Ipad可播放的TS流的总结
TS协议解析第一部分(PAT)
TS协议解析第二部分(PMT)
TS协议解析第三部分(PES)
TS协议解析第四部分(adaptation field)
ISO.IEC-13818-1-中文版规范
ts crc32 验证与计算
参考TS协议解析4连是因为手头没有ts格式的二进制文件的实际参考,主要是为了对比实际生成的二进制文件与自己理解的文件格式是否有差别。
其中第二个参考博客那张图对于直观理解hls的ts包格式很有帮助,因此将这张图片复制了过来,还读了一些其他博客,以上只是个人感觉对于完成手动编码比较关键的部分。
下面直接上demo代码,注释已经比较详细(令人发指)。需要特别注意的是这个demo没用到PCR
1 /*2 这个demo实现的是将h264裸流封装成ts文件(也可以是m3u8和ts文件)3 由于我们的摄像头数据只含有以下帧类型4 PPS (0x00 00 00 01 68),5 SPS (0x00 00 00 01 67),6 SEI (0x00 00 00 01 66),7 IDR (0x00 00 00 01 65),8 非IDR(0x00 00 00 01 41),9 帧只有帧开始(0x00 00 00 01)没有帧中(0x00 00 01),10 因此这个demo不会处理其他帧情况,如果输入文件含有其他帧会有问题。11 重要:这个demo没有PCR信息,其他信息能省则省,毕竟规范文件内容太多。12 测试1.——————————————————————————————————13 输出结果.ts文件通过VLC播放器本地播放有跳帧的情况,VLC播放器给出的信息是:14 main error: Invalid PCR value in ES_OUT_SET_(GROUP_)PCR !15 ......16 main error: Invalid PCR value in ES_OUT_SET_(GROUP_)PCR !17 direct3d9 error: SetThumbNailClip failed: 0x800706f418 avcodec info: Using DXVA2 (Intel(R) HD Graphics, vendor Intel(8086), device 402, revision 6) for hardware decoding19 direct3d9 error: SetThumbNailClip failed: 0x800706f420 这应当是我们忽略了PCR导致的,不过使用android(版本8.1.0,手头没有其他版本的android机测试)和ios手机浏览器或视频播放器可以正常播放。21 造成VLC播放异常的主要是PCR,这个PCR没怎么去了解。22 测试2.——————————————————————————————————23 .m3u8文件在本地貌似播放不了(试过一些播放器都不行)24 然后用nginx的rtmp模块搭了一个hls点播服务器25 1.使用pc端的VLC播放器打开网络串流,画面一闪而过,看似只有前面的1帧或几帧播放成功,后面都是黑屏26 2.使用手机的浏览器打开能够正常播放27 综述.——————————————————————————————————28 由于是极简封装,存在一些发现或未发现的小问题,比如PCR29 PC端VLC不能正常播放。30 手机浏览器和视频播放器可以正常的播放。(貌似现在手机都兼容了safari浏览器?)31 html5没有测试过。32 —————————————————————————————————————33 written by dyan102434 date:2019-1-235 */36 37 #include "stdafx.h" //在linux下注释掉这一行38 #include <stdio.h>39 #include <stdlib.h>40 #include <string.h>41 #include <list>42 #include <math.h>43 #ifdef _WIN3244 #include <winsock.h>45 #pragma comment(lib,"ws2_32.lib")46 #else 47 #include <arpa/inet.h>48 #endif /*_WIN32*/49 using namespace std;50 FILE *bits = NULL; //.h264输入文件51 FILE *fw = NULL; //.ts输出文件52 FILE *m3u8 = NULL; //.m3u8输出文件53 static int FindStartCode2(unsigned char *Buf);//查找开始字符0x00000154 static int FindStartCode3(unsigned char *Buf);//查找开始字符0x0000000155 56 static int info2 = 0, info3 = 0;57 58 #define BUF_SIZE 1024*1024*359 #define Program_map_PID 0x0003 //pmt节目pid,也会关联到pat表60 #define Elementary_PID 0x0012 //pes流类型对应的pid,也会关联到pmt表61 #define TS_PACKET_LEN 188 //ts包长度固定188字节62 #define M3U8_TS_LEN 32 //m3u8文件每行最大长度63 #define IDR_PER_TSFILE 10 //生成hls时每多少个关键帧一个.ts文件(ts包和.ts文件不同,.ts包含很多ts包,而一个ts包固定188字节)64 /*用于存一帧h264数据*/65 struct frame_buf {66 unsigned char * p_buf;67 int len;68 int max;69 frame_buf() {70 p_buf = new unsigned char[BUF_SIZE];//我们摄像头图像大小是640*480*3,就算不压缩,这个数组完全足够存一帧71 len = 0;72 max = BUF_SIZE;73 }74 ~frame_buf()75 {76 delete[] p_buf;77 p_buf = NULL;78 }79 };80 /*用于存一个ts包(188字节)*/81 struct pes_packet {82 unsigned char *p_buf;83 int max;84 pes_packet() {85 p_buf = new unsigned char[TS_PACKET_LEN];86 max = TS_PACKET_LEN;87 }88 ~pes_packet()89 {90 delete[] p_buf;91 p_buf = NULL;92 }93 };94 /*用于存m3u8文件中标记.ts文件时长及文件名*/95 struct m3u8_ts {96 float ts_time;97 char ts_name[M3U8_TS_LEN];98 m3u8_ts() {99 memset(&ts_name, 0, M3U8_TS_LEN); 100 } 101 }; 102 /*用于缓存m3u8文件内容*/ 103 struct m3u8_text { 104 char extm3u[M3U8_TS_LEN]; 105 char ext_x_version[M3U8_TS_LEN]; 106 char ext_x_allow_cache[M3U8_TS_LEN]; 107 char ext_x_targetduration[M3U8_TS_LEN]; 108 char ext_x_media_sequence[M3U8_TS_LEN]; 109 char ext_x_playlist_type[M3U8_TS_LEN]; 110 list<m3u8_ts> l_m3u8_ts; 111 char extinf[M3U8_TS_LEN]; 112 char ext_x_endlist[M3U8_TS_LEN]; 113 m3u8_text() { 114 memset(extm3u, 0, M3U8_TS_LEN); 115 memcpy(extm3u, "#EXTM3U\n", strlen("#EXTM3U\n")); 116 memset(ext_x_version, 0, M3U8_TS_LEN); 117 memcpy(ext_x_version, "#EXT-X-VERSION:3\n", strlen("#EXT-X-VERSION:3\n")); 118 memset(ext_x_allow_cache, 0, M3U8_TS_LEN); 119 memcpy(ext_x_allow_cache, "#EXT-X-ALLOW-CACHE:YES\n", strlen("#EXT-X-ALLOW-CACHE:YES\n")); 120 memset(ext_x_targetduration, 0, M3U8_TS_LEN); 121 memcpy(ext_x_targetduration, "#EXT-X-TARGETDURATION:%d\n", strlen("#EXT-X-TARGETDURATION:%d\n")); 122 memset(ext_x_media_sequence, 0, M3U8_TS_LEN); 123 memcpy(ext_x_media_sequence, "#EXT-X-MEDIA-SEQUENCE:%d\n", strlen("#EXT-X-MEDIA-SEQUENCE:%d\n")); 124 memset(ext_x_playlist_type, 0, M3U8_TS_LEN); 125 memcpy(ext_x_playlist_type, "#EXT-X-PLAYLIST-TYPE:VOD\n", strlen("#EXT-X-PLAYLIST-TYPE:VOD\n")); //点播 126 memset(extinf, 0, M3U8_TS_LEN); 127 memcpy(extinf, "#EXTINF:%f,\n", strlen("#EXTINF:%f,\n")); 128 memset(ext_x_endlist, 0, M3U8_TS_LEN); 129 memcpy(ext_x_endlist, "#EXT-X-ENDLIST\n", strlen("#EXT-X-ENDLIST\n")); 130 } 131 }; 132 133 frame_buf *p_pesframe = NULL; 134 unsigned long long pts = 1; //初始值似乎是随便定义的,第一帧最好不要为0,这个初始值pts+pts_per_frame不要等于0就行 135 unsigned int frame_rate = 90; //视频帧率不太清楚,随便给个值在这里,只是影响播放时快慢的问题(在VLC播放器上还会影响播放效果,比如设置为20,播放时不是跳帧,而是从头到尾只有1帧,估计还是PCR的问题) 136 unsigned int pts_per_frame = 90000 / frame_rate; 137 unsigned long long pts_max = pow(2, 33); 138 139 unsigned int crc32Table[256] = { 0 }; 140 int MakeTable(unsigned int *crc32_table) 141 { 142 for (unsigned int i = 0; i < 256; i++) { 143 unsigned int k = 0; 144 for (unsigned int j = (i << 24) | 0x800000; j != 0x80000000; j <<= 1) { 145 k = (k << 1) ^ (((k ^ j) & 0x80000000) ? 0x04c11db7 : 0); 146 } 147 148 crc32_table[i] = k; 149 } 150 return 0; 151 } 152 unsigned int Crc32Calculate(u_char *buffer, unsigned int size, unsigned int *crc32_table) 153 { 154 unsigned int crc32_reg = 0xFFFFFFFF; 155 for (unsigned int i = 0; i < size; i++) { 156 crc32_reg = (crc32_reg << 8) ^ crc32_table[((crc32_reg >> 24) ^ *buffer++) & 0xFF]; 157 } 158 return crc32_reg; 159 } 160 161 void OpenBitstreamFile(char *fn) 162 { 163 if (NULL == (bits = fopen(fn, "rb"))) 164 { 165 printf("open file error\n"); 166 exit(0); 167 } 168 } 169 void CloseBitstreamFile() { 170 fclose(bits); 171 bits = NULL; 172 } 173 void OpenWriteFile(char *fn) { 174 if (NULL == (fw = fopen(fn, "wb"))) 175 { 176 printf("open write file error\n"); 177 exit(0); 178 } 179 } 180 void CloseWriteFile() { 181 fclose(fw); 182 fw = NULL; 183 } 184 185 /*从文件中读取一帧h264裸流数据(I帧或P帧,没有B帧) 186 @Buf, [out]已分配内存空间的指针,用于存储一帧图像 187 @len, [out]返回Buf的有效长度 188 return, -1(异常),0(IDR),1(非IDR),2(最后一帧,不区分帧类型)*/ 189 int GetOneFrame(unsigned char* Buf, int &len) { 190 int StartCodeFound = 0; 191 int rewind; 192 int nal_unit_type; 193 int forbidden_bit; 194 int nal_reference_idc; 195 len = 0; 196 197 int startcodeprefix_len = 3;//初始化码流序列的开始字符为3个字节 198 199 //从我们摄像头截取的h264数据获知只有帧开始(0x00000001)没有帧中(0x000001),不处理帧中。 200 if (4 != fread(Buf+len, 1, 4, bits))//从码流中读4个字节 201 { 202 return -1; 203 } 204 info3 = FindStartCode3(Buf+len);//判断是否为0x00000001 205 if (info3 != 1)//如果不是,返回-1 206 { 207 return -1; 208 } 209 else {//如果是0x00000001,得到开始字符为4个字节 210 len += 4; 211 startcodeprefix_len = 4; 212 } 213 while (!StartCodeFound) { 214 //到文件末尾,认为当前帧结束 215 if (feof(bits)) { 216 break; 217 } 218 //再读一个字节,知道找到下一个帧开始0x00000001 219 Buf[len++] = fgetc(bits); 220 info3 = FindStartCode3(&Buf[len - 4]);//判断是否为0x00000001 221 //if (info3 != 1) 222 // info2 = FindStartCode2(&Buf[pos - 3]);//判断是否为0x000001 223 StartCodeFound = (/*info2 == 1 || */info3 == 1); 224 if (StartCodeFound) { 225 //到文件末尾,认为当前帧结束 226 if (feof(bits)) { 227 break; 228 } 229 //再读一个字节判断帧类型 230 Buf[len++] = fgetc(bits); 231 //第1位h.264裸流肯定为0,不用管它 232 forbidden_bit = Buf[len-1] & 0x80; //(nalu->buf[0]>>7) & 1; 233 /*接下来2位,取值0~3,指示这个nalu的重要性,I帧、sps、pps通常取3,P帧通常取2,B帧通常取0. 234 对于封装pes包帧重要性关系不大,因为我们实际情况的帧类型比较简单(只有IDR(关键帧),非IDR,SPS,PPS,SEI),主要还是看帧类型,因为我们不管什么帧只要在帧前加个pes头就行了*/ 235 nal_reference_idc = Buf[len-1] & 0x60; //(nalu->buf[0]>>5) & 3; 236 /*最后5位,帧类型:IDR(关键帧),非IDR,SPS,PPS,SEI,分解符,片分区A/B/C,序列结束,码流结束,填充等。 237 截取一段摄像头h264裸流分析后发现对于我们摄像头的实时h264裸流只有前5种。 238 而封装pes包sps,pps,sei,IDR封装为一个pes包,各个非IDR分别一个pes包,因此检测到下一个IDR或非IDR才返回。*/ 239 nal_unit_type = (Buf[len-1]) & 0x1f; 240 //只要不是非IDR或IDR(SEI,SPS,PPS),不要截断数据,继续读取 241 if (1 != nal_unit_type && 5 != nal_unit_type) { 242 StartCodeFound = 0; 243 continue; 244 } 245 //检测到下一帧的开始,可以截取当前帧了 246 else { 247 rewind = (info3 == 1) ? -5 : -4; //回退一个h264头(4字节(或3字节,帧中我们不考虑))和一个帧类型(1字节) 248 if (0 != fseek(bits, rewind, SEEK_CUR))//把文件指针向后退开始字节的字节数 249 { 250 printf("Cannot fseek in the bit stream file\n"); 251 return -1; 252 } 253 len += rewind;//帧长减下一帧头0x00000001(或0x000001)的长度 254 //区分一下IDR和非IDR,可能要用 255 int front_nal_unit_type = Buf[startcodeprefix_len] & 0x1f; 256 //非IDR返回1,IDR返回0 257 if (1 == front_nal_unit_type) { 258 return 1; 259 } 260 else if (5 == front_nal_unit_type) { 261 return 0; 262 } 263 } 264 } 265 } 266 return 2; 267 } 268 269 /*为一帧h264裸流加pes头(pts) 270 @p_pes, [out]已提前分配存储空间的指针,用于存储加pes头的h264一帧数据 271 @Buf, [int]一帧h264数据 272 @len, [in]Buf数据的长度 273 return, -1(异常),0(成功)*/ 274 int PesHead_p(frame_buf *p_pes, unsigned char* Buf, int len) { 275 int temp_len = 0; 276 p_pes->len = 0; 277 memset(p_pes->p_buf, 0, BUF_SIZE); 278 /*最后我们是要按字节写的,为了避免大小端的问题最好一个字节一个字节存。或者转为网络字节序 279 比如pes start code存成*((unsigned int *)p_pes->p_buf) |= 0x00000100,看上去符合pes的开始标记 00 00 01 00,事实上写到本地后确是00 01 00 00,这是不对的 280 */ 281 *(p_pes->p_buf + 2) = 0x01; //pes start code,3字节 0x000001 282 *(p_pes->p_buf + 3) = 0xe0; //stream id,1字节 0xe0 283 //*(p_pes->p_buf + 4) = 0x00; 284 //*(p_pes->p_buf + 5) = 0x00; //pes packet length,2字节 视频数据长度0x0000表示pes包不限制长度 285 *(p_pes->p_buf + 6) = 0x80; //通常取值0x80,表示数据不加密、无优先级、备份的数据等(自己封装怎么简单怎么来) 286 *(p_pes->p_buf + 7) = 0x80; //pts_dts_flags取值0x80(10 00 00 00)表示只含有pts(其中的前2bit(10)),取值0xc0(11 00 00 00)表示含有pts和dts,(前2bit(11))这个序列1越多后面描述越长(自己封装怎么简单怎么来)。 287 *(p_pes->p_buf + 8) = 0x05; //因为我们只有pts,按协议就是5字节 288 temp_len += 9; 289 pts += pts_per_frame; /*按协议pts/dts有效位33bit,不知道为什么33bit(看来需要长整形存储),就算我们只用33bit,可以长达约26.512((2^33-1)/ 90000 / 3600)个小时*/ 290 pts %= pts_max; /*如果超出33bit根据协议超过33bit记录后应该重新从0开始*/ 291 /*取33bit的pts值的前3位(本来应该右移30,但又需要给最后的maker_bit预留一个位子,所以最后右移29位和0x0e(00 00 11 10)与运算)*/ 292 char ptsn1_temp = 0x0e & pts >> 29; //0x0e(00 00 11 10) 293 /*我们前面pts_dts_flags是0x80(前2位为10),则按协议这个字节前4位为0010,后跟pts的前3位,最后接一个maker_bit(1)*/ 294 char ptsn1 = 0x21 | ptsn1_temp; //0x21(00 10 00 01) 295 unsigned short ptsn2_temp = 0x0001 | (pts >> 14); //按协议取接下来pts的15bit,后接一个位maker_bit(1),同上。额外注意的是这不是单字节数据类型,最后要转网络字节序 296 unsigned short ptsn2 = htons(ptsn2_temp); //转网络字节序 297 unsigned short ptsn3_temp = 0x0001 | (pts << 1); //按协议取最后pts的15bit,后接一个位maker_bit(1),同上。额外注意的是这不是单字节数据类型,最后要转网络字节序 298 unsigned short ptsn3 = htons(ptsn3_temp); //转网络字节序 299 //将pts写入 300 *(p_pes->p_buf + temp_len) = ptsn1; 301 temp_len++; 302 *(unsigned short*)(p_pes->p_buf + temp_len) = ptsn2; 303 temp_len += 2; 304 *(unsigned short*)(p_pes->p_buf + temp_len) = ptsn3; 305 temp_len += 2; 306 //pes头结束,网上传言要在每个pes头后加个0x00000001 0x09(按h264格式这是B帧分解符?) 0x**(最后的**不是00就行) 307 unsigned int h264_head = htonl(1); 308 *(unsigned int*)(p_pes->p_buf + temp_len) = h264_head; 309 temp_len += 4; 310 *(p_pes->p_buf + temp_len) = 0x09; 311 temp_len++; 312 *(p_pes->p_buf + temp_len) = 0xff; 313 temp_len++; 314 //加上h264裸流,注意数组别越界 315 if (p_pes->max < (temp_len + len)) { 316 //TODO 317 printf("pes buf size allocated is too small\n"); 318 return -1; 319 } 320 memcpy(p_pes->p_buf + temp_len, Buf, len); 321 p_pes->len = len + temp_len; 322 return 0; 323 } 324 325 /*为一帧h264裸流加pes头(pts和dts) 326 @p_pes, [out]已提前分配存储空间的指针,用于存储加pes头的h264一帧数据 327 @Buf, [int]一帧h264数据 328 @len, [in]Buf数据的长度 329 return, -1(异常),0(成功)*/ 330 int PesHead_pd(frame_buf *p_pes, unsigned char* Buf, int len) { 331 int temp_len = 0; 332 p_pes->len = 0; 333 memset(p_pes->p_buf, 0, BUF_SIZE); 334 /*最后我们是要按字节写的,为了避免大小端的问题最好一个字节一个字节存。或者转为网络字节序 335 比如pes start code存成*((unsigned int *)p_pes->p_buf) |= 0x00000100,看上去符合pes的开始标记 00 00 01 00,事实上写到本地后确是00 01 00 00,这是不对的 336 */ 337 *(p_pes->p_buf + 2) = 0x01; //pes start code,3字节 0x000001 338 *(p_pes->p_buf + 3) = 0xe0; //stream id,1字节 0xe0 339 //*(p_pes->p_buf + 4) = 0x00; 340 //*(p_pes->p_buf + 5) = 0x00; //pes packet length,2字节 视频数据长度0x0000表示pes包不限制长度 341 *(p_pes->p_buf + 6) = 0x80; //通常取值0x80,表示数据不加密、无优先级、备份的数据等(自己封装怎么简单怎么来) 342 /*这里主要注意pts_dts_flag不同后面的pts和dts起始标识也会不同*/ 343 *(p_pes->p_buf + 7) = 0xc0; //pts_dts_flags取值0xc0(11 00 00 00)表示含有pts和dts(其中前2bit(11)),取值0x80表示只含有pts(其中的前2bit(10)),(前2bit(11))这个序列1越多后面描述越长(自己封装怎么简单怎么来,我们不要其他的信息了)。 344 *(p_pes->p_buf + 8) = 0x0a; //因为我们有pts和dts,按协议就是10字节,我们这里将pts和dts设置为一样就行 345 temp_len += 9; 346 pts += pts_per_frame; /*按协议pts/dts有效位33bit,不知道为什么33bit(看来需要长整形存储),就算我们只用33bit,可以长达约26.512((2^33-1)/ 90000 / 3600)个小时*/ 347 pts %= pts_max; /*如果超出33bit根据协议超过33bit记录后应该重新从0开始*/ 348 /*取33bit的pts值的前3位(本来应该右移30,但又需要给最后的maker_bit预留一个位子,所以最后右移29位和0x0e(00 00 11 10)与运算)*/ 349 char ptsn1_temp = 0x0e & pts >> 29; //0x0e(00 00 11 10) 350 /*我们前面pts_dts_flags是0xc0(前2位为11),则按协议这个字节前4位为0011,后跟pts的前3位,最后接一个maker_bit(1)*/ 351 char ptsn1 = 0x31 | ptsn1_temp; //0x31(00 11 00 01) 352 unsigned short ptsn2_temp = 0x0001 | (pts >> 14); //取接下来的15bit,后接一个位maker_bit(1),同上。额外注意的是这不是单字节数据类型,最后要转网络字节序 353 unsigned short ptsn2 = htons(ptsn2_temp); //转网络字节序 354 unsigned short ptsn3_temp = 0x0001 | (pts << 1); //取最后的15bit,后接一个位maker_bit(1),同上。额外注意的是这不是单字节数据类型,最后要转网络字节序 355 unsigned short ptsn3 = htons(ptsn3_temp); //转网络字节序 356 //将pts写入 357 *(p_pes->p_buf + temp_len) = ptsn1; 358 temp_len++; 359 *(unsigned short*)(p_pes->p_buf + temp_len) = ptsn2; 360 temp_len += 2; 361 *(unsigned short*)(p_pes->p_buf + temp_len) = ptsn3; 362 temp_len += 2; 363 /*将dts设置为与pts相同*/ 364 /*pts -= pts_per_frame;*/ 365 ptsn1 = 0x11 | pts >> 29; /*取前2bit,我们前面pts_dts_flags是0xc0(前2位为11),则按协议这个字节前4位为0001,后跟pts的前3位(由于我们是32位的unsigned int,最高位是没有的所以只取前2位,前一位置为0,应该右移30,但又需要给最后的maker_bit预留一个位子,所以最后右移29位),最后接一个位maker_bit(1) 366 0x11(00 01 00 01)*/ 367 ptsn2_temp = 0x0001 | (pts >> 14); //取接下来的15bit,后接一个位maker_bit(1),同上。额外注意的是这不是单字节数据类型,最后要转网络字节序 368 ptsn2 = htons(ptsn2_temp); //转网络字节序 369 ptsn3_temp = 0x0001 | (pts << 1); //取最后的15bit,后接一个位maker_bit(1),同上。额外注意的是这不是单字节数据类型,最后要转网络字节序 370 ptsn3 = htons(ptsn3_temp); //转网络字节序 371 //将dts写入 372 *(p_pes->p_buf + temp_len) = ptsn1; 373 temp_len++; 374 *(unsigned short*)(p_pes->p_buf + temp_len) = ptsn2; 375 temp_len += 2; 376 *(unsigned short*)(p_pes->p_buf + temp_len) = ptsn3; 377 temp_len += 2; 378 379 //pes头结束,网上传言要在每个pes头后加个0x00000001 0x09(按h264格式这是B帧分解符?) 0x**(最后的**不是00就行) 380 unsigned int h264_head = htonl(1); 381 *(unsigned int*)(p_pes->p_buf + temp_len) = h264_head; 382 temp_len += 4; 383 *(p_pes->p_buf + temp_len) = 0x09; 384 temp_len++; 385 *(p_pes->p_buf + temp_len) = 0xff; 386 temp_len++; 387 //加上h264裸流,注意数组别越界 388 if (p_pes->max < (temp_len + len)) { 389 //TODO 390 printf("pes buf size allocated is too small\n"); 391 return -1; 392 } 393 memcpy(p_pes->p_buf + temp_len, Buf, len); 394 p_pes->len = len + temp_len; 395 return 0; 396 } 397 398 /*将加了pes头的h264拆分成一个个ts包(每个pes包使用独立递增计数器) 399 @l_pes, [out]list引用,元素存储空间函数内部分配,用完后需要在外部释放 400 @Buf, [in]加pes头的h264数据 401 return, -1(异常),0(成功)*/ 402 int PesPacket(list<struct pes_packet*> &l_pes,frame_buf *Buf) { 403 if (NULL == Buf || NULL == Buf->p_buf) { 404 printf(" frame_buf or frame_buf->p_buf is NULL when pespacket\n"); 405 return -1; 406 } 407 unsigned char packet_pes = TS_PACKET_LEN - 4; //每个ts包除包头后的长度 408 unsigned char packet_remain = Buf->len % packet_pes; //每个ts包长度规定188字节,再去掉ts头长度 409 unsigned char packet_num = Buf->len / packet_pes; //可以剩余可封装出的不含自适应区的包数 410 411 while (!l_pes.empty()) { 412 l_pes.pop_front(); 413 } 414 unsigned short temp_1, temp_2, temp_3; 415 unsigned int temp_len = 0; 416 unsigned int pos = 0; //pes数据偏移量 417 unsigned char pes_count = 0; //递增计数器(ts头最后4bit,0~f增加,到f后下一个为0)(这里定义一个局部变量,意思是每个pes包都重置递增计数器) 418 419 pes_packet *p = new pes_packet; 420 *(p->p_buf) = 0x47; 421 temp_len++; 422 temp_1 = 0x4000 & 0xe000; 423 temp_2 = Elementary_PID & 0x1fff; 424 temp_3 = htons(temp_1 | temp_2); 425 *(unsigned short*)(p->p_buf + temp_len) = temp_3; 426 temp_len += 2; 427 428 /*如果pes包长度对184取余数大于0,需要填充,第一个包既含有自适应区又含有有效载荷*/ 429 if(packet_remain > 0){ 430 /*2bit加扰控制(00,无加密),2bit带自适应域(11,带自适应区和有效负载),4bit递增计数器(0000)*/ 431 *(p->p_buf + temp_len) = 0x30; //00 11 00 00 432 temp_len++; 433 /**/ 434 unsigned char stuff_num = TS_PACKET_LEN - 4 - 1 - packet_remain; //填充字节数 = ts包长(188字节) - ts头(4字节) - 自适应域长度(1字节) - 有效载荷长度 435 *(p->p_buf + temp_len) = stuff_num; 436 temp_len++; 437 /*如果正好余下183字节,还只有一个调整位长度,前面我们已经设为0,接下来不要再填充*/ 438 if (stuff_num == 0) { 439 // 440 } 441 else { 442 *(p->p_buf + temp_len) = 0x00; //8bit填充信息flag,我们不要额外的设定信息,填充字节全部0xff 443 temp_len++; 444 memset(p->p_buf + temp_len, 0xff, stuff_num - 1); //填充字节数还要再减去填充信息flag的长度 445 temp_len += (stuff_num - 1); 446 } 447 memcpy(p->p_buf + temp_len, Buf->p_buf + pos, packet_remain); 448 pos += packet_remain; 449 } 450 /*第一个包仅含有效载荷.仅含有效载荷,没有自适应区和自适应长度,ts头后直接跟pes数据*/ 451 else { 452 /*2bit加扰控制(00,无加密),2bit带自适应域(01,有效负载),4bit递增计数器(0000)*/ 453 *(p->p_buf + temp_len) = 0x10; //00 01 00 00 454 temp_len++; 455 memcpy(p->p_buf + temp_len, Buf->p_buf + pos, packet_pes); 456 pos += packet_pes; 457 packet_num--; //这个包不含自适应,剩余不含自适应区的包数量减一 458 } 459 /*将第一个包插入list链表*/ 460 l_pes.push_back(p); 461 while (packet_num > 0) { 462 pes_packet *p_temp = new pes_packet; 463 unsigned int len = 0; 464 /*因为第一个包用了0000,所以这里递增计数器先自增*/ 465 pes_count++; 466 pes_count &= 0x0f; //0~f增加,到f后下一个为0 467 *(p_temp->p_buf) = 0x47; 468 len++; 469 temp_1 = 0x0000 & 0xe000; 470 temp_2 = Elementary_PID & 0x1fff; 471 temp_3 = htons(temp_1 | temp_2); 472 *(unsigned short*)(p_temp->p_buf + len) = temp_3; 473 len += 2; 474 unsigned char c_temp = 0x10; //00 01 00 00 475 *(p_temp->p_buf + len) = c_temp | pes_count; 476 len++; 477 memcpy(p_temp->p_buf + len, Buf->p_buf + pos, packet_pes); 478 pos += packet_pes; 479 packet_num--; //剩余不含自适应区的包数量减一 480 l_pes.push_back(p_temp); 481 } 482 483 return 0; 484 } 485 486 /*ts头+PAT表 487 return, 加ts头的pat表,固定188字节,需要在外部释放*/ 488 unsigned char* PatPacket() { 489 int temp_len = 0; 490 int pat_len = 0; 491 int pat_before_len = 0; 492 unsigned char *p_pat = new unsigned char[188]; 493 memset(p_pat, 0xff, 188); 494 *p_pat = 0x47; //ts包起始字节 495 /*接下来1bit传输错误标识(0),1bit负载单元开始标识(1),1bit传输优先级(1),13bit为PID(pat表限定0),共计2字节*/ 496 *(p_pat + 1) = 0x60; 497 *(p_pat + 2) = 0x00; 498 /*接下来2bit传输扰乱控制(00未加密),2bit是否包含自适应区(01只含有效载荷),4bit递增计数器(0000),共计1字节*/ 499 *(p_pat + 3) = 0x10; 500 temp_len += 4; 501 /*因为前面负载开始标识为1,这里有一个调整字节,一般这个字节为0x00*/ 502 *(p_pat + temp_len) = 0x00; 503 temp_len++; 504 pat_before_len = temp_len; 505 /*接下来是PAT表*/ 506 /*table_id,对于PAT只能是0x00*/ 507 *(p_pat + temp_len) = 0x00; 508 temp_len++; 509 /*固定4bit(1011),2bit必定为00,后10bit表示后续长度(9+4*PMT表数,注意其中也包括了crc_32校验码的长度,我们的节目只有视频所以这个长度为13:0x000d),共计2字节*/ 510 unsigned short temp_1 = 0xb000; //10 11 00 00 00 00 00 00 511 unsigned short temp_2 = 0x000d & 0x03ff; //10bit有效 512 unsigned short temp_3 = temp_1 | temp_2; 513 *(unsigned short*)(p_pat + temp_len) = htons(temp_3); 514 temp_len += 2; 515 *(unsigned short*)(p_pat + temp_len) = htons(0x0000);/*传输流ID,用户自定义(那就随便定义个0x0000)*/ 516 temp_len += 2; 517 /*接下来2bit固定(11),5bit(00000,一旦PAT有变化,版本号加1),1bit(1,表示传送的PAT当前可以使用,若为0表示下一个表有效)*/ 518 *(p_pat + temp_len) = 0xc1; //11 00 00 01 519 temp_len++; 520 /*接下来一个字节(0x00,我们只有一个视频节目,不存在分段)给出了该分段的数目。在PAT中的第一个分段的section_number为0x00,PAT中每一分段将加1。*/ 521 *(p_pat + temp_len) = 0x00; 522 temp_len++; 523 /*接下来一个字节(0x00,我们只有一个视频节目,不存在分段)给出了该分段的数目。该字段指出了最后一个分段号。在整个PAT中即分段的最大数目。*/ 524 *(p_pat + temp_len) = 0x00; 525 temp_len++; 526 527 /*开始循环*/ 528 /*接下来开始循环记录节目即PMT表(我们只有一个视频节目),每次循环4字节*/ 529 /*循环的前2个字节program_number。0x0001:这个为PMT。该字段指出了节目对于那个Program_map_PID是可以使用的。如果是0x0000,那么后面的PID是网络PID,否则其他值由用户定义。*/ 530 *(unsigned short*)(p_pat + temp_len) = htons(0x0001); 531 temp_len += 2; 532 /*循环的后2个字节,3bit为固定值(111),13bit为节目号对应内容的PID值(即Program_map_PID,反正只有这一个节目,在全局中随便定义一个id,等会PMT表还要用到)*/ 533 temp_1 = 0xe000; //11 10 00 00 00 00 00 00 534 temp_2 = Program_map_PID; 535 temp_3 = temp_1 | temp_2; 536 *(unsigned short*)(p_pat + temp_len) = htons(temp_3); 537 temp_len += 2; 538 /*循环到这里就结束了*/ 539 540 /*最后是4字节的PAT表crc_32校验码。这个校验码从PAT表(不含ts头及调整字节)开始到crc_32校验码前的数据*/ 541 //TODO 如果pat分表了,要考虑,我们这里不会分表不要考虑 542 pat_len = temp_len - pat_before_len; 543 unsigned int crc32_res = Crc32Calculate(p_pat + pat_before_len, pat_len, crc32Table); 544 *(unsigned int *)(p_pat + temp_len) = htonl(crc32_res); 545 temp_len += 4; 546 /*后续用0xff填充,我们在初始化时已经初始化为0xff了*/ 547 return p_pat; 548 } 549 550 /*ts头+PMT表 551 return, 加ts头的pmt表,固定188字节,需要在外部释放*/ 552 unsigned char* PmtPacket() { 553 int temp_len = 0; 554 int pmt_len = 0; 555 int pmt_before_len = 0; 556 unsigned short temp_1, temp_2, temp_3; 557 unsigned char *p_pmt = new unsigned char[188]; 558 memset(p_pmt, 0xff, 188); 559 *p_pmt = 0x47; //ts包起始字节 560 temp_len++; 561 /*接下来1bit传输错误标识(0),1bit负载单元开始标识(1),1bit传输优先级(1),13bit为PID(用pat表中记录的Program_map_PID:0x0003),共计2字节*/ 562 temp_1 = 0x6000 & 0xe000; 563 temp_2 = 0x0003 & 0x1fff; 564 temp_3 = htons(temp_1 | temp_2); 565 *(unsigned short*)(p_pmt + temp_len) = temp_3; 566 //*(p_pmt + 1) = 0x60; 567 //*(p_pmt + 2) = 0x03; 568 temp_len += 2; 569 /*接下来2bit传输扰乱控制(00未加密),2bit是否包含自适应区(01只含有效载荷),4bit递增计数器(0000),共计1字节*/ 570 *(p_pmt + temp_len) = 0x10; //00 01 00 00 571 temp_len++; 572 /*因为前面负载开始标识为1,这里有一个调整字节,一般这个字节为0x00*/ 573 *(p_pmt + temp_len) = 0x00; 574 temp_len++; 575 pmt_before_len = temp_len; 576 /*接下来是PMT表*/ 577 /*table_id,对于PMT表固定为0x02*/ 578 *(p_pmt + temp_len) = 0x02; 579 temp_len++; 580 /*固定4bit(1011),2bit必定为00,后10bit表示后续长度(13+5*流类型数,我们的节目只有视频流所以这个长度为18:0x0012),共计2字节*/ 581 temp_1 = 0xb000 & 0xf000; //10 11 00 00 00 00 00 582 temp_2 = 0x0012 & 0x03ff; //00 00 00 00 01 00 10 583 temp_3 = htons(temp_1 | temp_2); 584 *(unsigned short*)(p_pmt + temp_len) = temp_3; 585 temp_len += 2; 586 /*对应PAT中的program_number(我们的PAT表中只有一个PMT的program_number:0x0001),共计2字节*/ 587 *(unsigned short*)(p_pmt + temp_len) = htons(0x0001); 588 temp_len += 2; 589 /*接下来2bit固定(11),5bit该字段指出了TS中program_map_section的版本号(00000,如果PAT有变化则版本号加1),1bit(1,当该字段置为1时,表示当前传送的program_map_section可用;当该字段置0时,表示当前传送的program_map_section不可用,下一个TS的program_map_section有效)*/ 590 *(p_pmt + temp_len) = 0xc1; //11 00 00 01 591 temp_len++; 592 /*接下来section_number,该字段总是置为0x00*/ 593 *(p_pmt + temp_len) = 0x00; 594 temp_len++; 595 /*接下来last_section_number,该字段总是置为0x00*/ 596 *(p_pmt + temp_len) = 0x00; 597 temp_len++; 598 /*接下来3bit固定(111),13bitPCR(节目参考时钟)所在TS分组的PID(指定为视频PID),我们暂时不想要pcr(ISO/IEC-13818-1中2.4.4.9对于PMT表中PCR_PID有一段描述“若任何PCR均与专用流的节目定义无关,则此字段应取值0x1fff”,不是太懂PCR,但是我们貌似可以忽略它),指定它为0x1fff,总计2字节*/ 599 temp_1 = 0xe000 & 0xe000; //11 10 00 00 00 00 00 00 600 temp_2 = 0x1fff & 0x1fff; //00 01 11 11 11 11 11 11 601 temp_3 = htons(temp_1 | temp_2); 602 *(unsigned short*)(p_pmt + temp_len) = temp_3; 603 temp_len += 2; 604 /*接下来4bit固定(1111),12bit节目描述信息(指定为0x000表示没有),共计2字节*/ 605 temp_1 = 0xf000 & 0xf000; //11 11 00 00 00 00 00 00 606 temp_2 = 0x0000 & 0x0fff; //00 00 00 00 00 00 00 00 607 temp_3 = htons(temp_1 | temp_2); 608 *(unsigned short*)(p_pmt + temp_len) = temp_3; 609 temp_len += 2; 610 611 /*开始循环*/ 612 /*接下来开始循环记录流类型、PID及描述信息,每次循环5字节*/ 613 /*流类型,8bit(0x1B:表示这个流时h264格式的,通俗点就是视频,我们只有h264裸流)*/ 614 *(p_pmt + temp_len) = 0x1b; 615 temp_len++; 616 /*3bit固定(111),13bit流类型对应的PID,表示该pid的ts包就是用来装该流类型的数据的(对应这里就是我们在pes封包中的为h264裸流指定的Elementary_PID),总计2字节*/ 617 temp_1 = 0xe000 & 0xe000; 618 temp_2 = Elementary_PID & 0x1fff; 619 temp_3 = htons(temp_1 | temp_2); 620 *(unsigned short*)(p_pmt + temp_len) = temp_3; 621 temp_len += 2; 622 /*4bit固定(1111),12bit节目描述信息,指定为0x000表示没有,共计2字节*/ 623 temp_1 = 0xf000 & 0xf000; 624 temp_2 = 0x0000 & 0x0fff; 625 temp_3 = htons(temp_1 | temp_2); 626 *(unsigned short*)(p_pmt + temp_len) = temp_3; 627 temp_len += 2; 628 /*循环结束*/ 629 630 /*最后是4字节的PMT表crc_32校验码。这个校验码从PMT表(不含ts头及调整字节)开始到crc_32校验码前的数据*/ 631 pmt_len = temp_len - pmt_before_len; 632 unsigned int crc32_res = Crc32Calculate(p_pmt + pmt_before_len, pmt_len, crc32Table); 633 *(unsigned int *)(p_pmt + temp_len) = htonl(crc32_res); 634 temp_len += 4; 635 /*后续用0xff填充,我们在初始化时已经初始化为0xff了*/ 636 return p_pmt; 637 } 638 639 static int FindStartCode2(unsigned char *Buf) 640 { 641 if (Buf[0] != 0 || Buf[1] != 0 || Buf[2] != 1) return 0; //判断是否为0x000001,如果是返回1 642 else return 1; 643 } 644 645 static int FindStartCode3(unsigned char *Buf) 646 { 647 if (Buf[0] != 0 || Buf[1] != 0 || Buf[2] != 0 || Buf[3] != 1) return 0;//判断是否为0x00000001,如果是返回1 648 else return 1; 649 } 650 651 void write2file(unsigned char* buf, int len) { 652 fwrite(buf, 1, len, fw); 653 } 654 655 void writeM3u8(m3u8_text text,int ts_time_max, int ts_start_num) { 656 char file_m3u8[] = "stream.m3u8"; 657 OpenWriteFile(file_m3u8); 658 char targetduration[M3U8_TS_LEN] = { 0 }; 659 memcpy(targetduration, text.ext_x_targetduration, M3U8_TS_LEN); 660 char media_sequence[M3U8_TS_LEN] = { 0 }; 661 memcpy(media_sequence, text.ext_x_media_sequence, M3U8_TS_LEN); 662 sprintf(text.ext_x_targetduration, targetduration, ts_time_max); 663 sprintf(text.ext_x_media_sequence, media_sequence, ts_start_num); 664 write2file((unsigned char*)text.extm3u, strlen(text.extm3u)); 665 write2file((unsigned char*)text.ext_x_version, strlen(text.ext_x_version)); 666 write2file((unsigned char*)text.ext_x_allow_cache, strlen(text.ext_x_allow_cache)); 667 write2file((unsigned char*)text.ext_x_targetduration, strlen(text.ext_x_targetduration)); 668 write2file((unsigned char*)text.ext_x_media_sequence, strlen(text.ext_x_media_sequence)); 669 write2file((unsigned char*)text.ext_x_playlist_type, strlen(text.ext_x_playlist_type)); 670 while (!text.l_m3u8_ts.empty()) { 671 m3u8_ts temp = text.l_m3u8_ts.front(); 672 text.l_m3u8_ts.pop_front(); 673 char extinf[M3U8_TS_LEN] = { 0 }; 674 sprintf(extinf, text.extinf, temp.ts_time); 675 write2file((unsigned char*)extinf, strlen(extinf)); 676 write2file((unsigned char*)temp.ts_name, strlen(temp.ts_name)); 677 write2file((unsigned char*)"\n", strlen("\n")); //ts_name后是没有换行符的,这里要添加换行符 678 } 679 write2file((unsigned char*)text.ext_x_endlist, strlen(text.ext_x_endlist)); 680 CloseWriteFile(); 681 } 682 683 /*输入h264裸流文件,输出ts文件*/ 684 int H264ToTs() 685 { 686 char file_in[] = "./stream.h264"; 687 OpenBitstreamFile(file_in); 688 MakeTable(crc32Table); 689 char file_out[] = "./stream.ts"; 690 OpenWriteFile(file_out); 691 unsigned char* p_pat_res = PatPacket(); 692 write2file(p_pat_res, TS_PACKET_LEN); 693 fflush(fw); 694 unsigned char* p_pmt_res = PmtPacket(); 695 write2file(p_pmt_res, TS_PACKET_LEN); 696 fflush(fw); 697 698 unsigned char* temp = new unsigned char[BUF_SIZE]; 699 int temp_len = 0; 700 frame_buf *p_frame = new frame_buf; 701 list<pes_packet*> list_pes; 702 int frame_count = 0; 703 while (!feof(bits)) { 704 int read_res = GetOneFrame(temp,temp_len); 705 if (read_res < 0) { 706 continue; 707 } 708 frame_count++; 709 如果需要实时播放(直播)而不是每次都是都是从文件开始到结束,可以考虑在中间插入pat和pmt表,暂时我们也用不上 710 //else if (read_res == 0) { 711 // write2file(p_pat_res, TS_PACKET_LEN); 712 // fflush(fw); 713 // write2file(p_pmt_res, TS_PACKET_LEN); 714 // fflush(fw); 715 //} 716 int res = PesHead_pd(p_frame, temp, temp_len); 717 //int res = PesHead_pd_1(p_frame, temp, temp_len); 718 if (res != 0) { 719 printf("pesHead failed\n"); 720 return -1; 721 } 722 res = PesPacket(list_pes, p_frame); 723 if (res != 0) { 724 printf("pesPacket failed\n"); 725 return -1; 726 } 727 while (!list_pes.empty()) { 728 pes_packet* p = list_pes.front(); 729 write2file(p->p_buf, TS_PACKET_LEN); 730 fflush(fw); 731 list_pes.pop_front(); 732 delete p; 733 734 } 735 } 736 delete[] p_pat_res; 737 delete[] p_pmt_res; 738 delete[] temp; 739 delete p_frame; 740 fclose(bits); 741 fclose(fw); 742 return 0; 743 } 744 745 /*输入h264裸流文件,输出m3u8及多个ts文件*/ 746 int H264ToM3u8() 747 { 748 char file_in[] = "./stream.h264"; 749 OpenBitstreamFile(file_in); 750 MakeTable(crc32Table); 751 int ts_file_num = 100; 752 int ts_start_num = ts_file_num; 753 int ts_time_max = 0; 754 char ts_file_temp[M3U8_TS_LEN] = "./stream%d.ts"; 755 char ts_file_current[M3U8_TS_LEN] = { 0 }; 756 sprintf(ts_file_current, ts_file_temp, ts_file_num); 757 758 OpenWriteFile(ts_file_current); 759 unsigned char* p_pat_res = PatPacket(); 760 write2file(p_pat_res, TS_PACKET_LEN); 761 fflush(fw); 762 unsigned char* p_pmt_res = PmtPacket(); 763 write2file(p_pmt_res, TS_PACKET_LEN); 764 fflush(fw); 765 766 unsigned char* temp_frame = new unsigned char[BUF_SIZE]; 767 int temp_frame_len = 0; 768 frame_buf *p_frame = new frame_buf; 769 list<pes_packet*> list_pes; 770 int IDR_count = 0; 771 int frame_count = 0; //用来计算一个.ts文件的时间 772 m3u8_text text; 773 while (!feof(bits)) { 774 int read_res = GetOneFrame(temp_frame, temp_frame_len); 775 776 if (read_res < 0) { 777 continue; 778 } 779 //将一个长ts文件分成多个ts短文件,我们尽量让第一个帧为关键帧(没试第一帧为非关键帧的情况) 780 else if (0 == read_res) { 781 IDR_count++; 782 frame_count++; 783 if (0 == (IDR_count%IDR_PER_TSFILE)) { 784 CloseWriteFile(); 785 /*将这个ts文件插入到m3u8_text中*/ 786 m3u8_ts temp; 787 temp.ts_time = 1.0 * frame_count / frame_rate; 788 frame_count = 0; //ts包计数清零 789 memcpy(temp.ts_name, ts_file_current, M3U8_TS_LEN); 790 text.l_m3u8_ts.push_back(temp); 791 int temp_time = ceil(temp.ts_time); 792 if (ts_time_max < temp_time) { 793 ts_time_max = temp_time; 794 } 795 /*开始下一个ts文件,文件号连续*/ 796 ts_file_num++; 797 memset(ts_file_current, 0, M3U8_TS_LEN); 798 sprintf(ts_file_current, ts_file_temp, ts_file_num); 799 OpenWriteFile(ts_file_current); 800 write2file(p_pat_res, TS_PACKET_LEN); 801 fflush(fw); 802 write2file(p_pmt_res, TS_PACKET_LEN); 803 fflush(fw); 804 } 805 } 806 else { 807 frame_count++; 808 } 809 int res = PesHead_pd(p_frame, temp_frame, temp_frame_len); 810 //int res = PesHead_pd_1(p_frame, temp, temp_len); 811 if (res != 0) { 812 printf("pesHead failed\n"); 813 return -1; 814 } 815 res = PesPacket(list_pes, p_frame); 816 if (res != 0) { 817 printf("pesPacket failed\n"); 818 return -1; 819 } 820 while (!list_pes.empty()) { 821 pes_packet* p = list_pes.front(); 822 write2file(p->p_buf, TS_PACKET_LEN); 823 fflush(fw); 824 list_pes.pop_front(); 825 delete p; 826 827 } 828 } 829 /*将最后一帧写入m3u8_text*/ 830 CloseWriteFile(); 831 m3u8_ts temp; 832 temp.ts_time = 1.0 * frame_count / frame_rate; 833 memcpy(temp.ts_name, ts_file_current, M3U8_TS_LEN); 834 text.l_m3u8_ts.push_back(temp); 835 int temp_time = ceil(temp.ts_time); 836 if (ts_time_max < temp_time) { 837 ts_time_max = temp_time; 838 } 839 /*使用m3u8_text完成最后的m3u8文件*/ 840 writeM3u8(text, ts_time_max, ts_start_num); 841 /*释放内存*/ 842 delete[] p_pat_res; 843 delete[] p_pmt_res; 844 delete[] temp_frame; 845 delete p_frame; 846 CloseBitstreamFile(); 847 848 return 0; 849 } 850 851 int main() { 852 //return H264ToM3u8(); 853 return H264ToTs(); 854 }
最后附上测试的h264数据
链接: https://pan.baidu.com/s/1TsmmuWtixhU5YEcUpNCg3A
提取码: 4a4b