大家好,关于图床上传网站源码分享很多朋友都还不太明白,今天小编就来为大家分享关于图床源码 轻量级的知识,希望对各位有所帮助!
1、前言
如果你经常接触音视频,那么对于M3U8应该不会陌生,M3U8简单来说就是HLS(HTTPLiveStreaming),指的是苹果开发的基于HTTP协议的流媒体解决方案,它可以在普通的HTTP的应用上直接提供点播和直播的能力。
在HLS里会将视流文件切分成小片(ts)并建立索引文件(M3U8),一般如下图所示,首先会有一个M3U8文件,然后对应在EXTINFtag下会的链接都是png格式的后缀,「那么这种png后缀,会不会影响视频播放呢」?
「答案是不会,因为FFmpeg里播放并不是认定后缀」,而是通过读取每个EXTINFtag下的链接后缀并不重要,可以是png或者bmp,甚至你写txt也是可以的,重点其实是包本身的编码。
?
那么如果这个视频链接,真的是一个图片呢?如下图所示,可以看到这个png本身就是一个完整的图片,不过这个图片的大小和它本身的质量并不匹配,毕竟这样一个图片不可能高达1.9MB。
如下图所示,我们查看这张图片的二进制,可以看到文件的Header确实是PNG,但是后面还有类似FFmpegService这样的描述,可以确实这就是一个伪装成真实PNG格式的视频文件。
从二进制字节看可以发现这就是一个TS封装的视频文件,因为在它的二进制代码里,有「以0x47开头,长度为188字节,并且通过0xFF进行填充的规律packet存在」。
?
后面我们会详细解释。
?
「那么这个PNG可以正常播放吗?答案是可以的」。那为什么明明是PNG的Header,却可以被解析成视频?
首先FFmpeg在播放前,会根据前面提到的0x47/188这个特征去识别这是一个TS封装的视频,之后在mpegts.c的对应封装处理逻辑里,会针对识别0x47作为包的起始位置去解析,所以PNG包部分会被忽略。
?
「0x47是一个TS包的固定header,一般一个TS包是188字节,不够长度一般会用0xFF填充」,而FFmpeg会针对每个格式去做识别,计算它们的score,根据每种格式的score决定它可能是什么格式,比如mpegts.c里是mpegts_probe函数,它通过analyze函数就会找到0x47起始做一系列的判断。
?
另外还一个叫mpegts_read_header的函数会读取数据头信息,比如解析出TS流当中的数据包大小,节目信息,PMT表,VideoPID,AudioPID等等,这些也是TS流播放的重要依据。
而在mpegts.c里最重要的read_packet函数也是,读取的时候会读取TS_PACKET_SIZE(188)的大小,然后判断包的首字节是不是0x47,如果不是就通过mpegts_resync重新同步一下去尝试寻找0x47。
「可能这时候细心的你已经发现了「盲点」,前面PNG的Header二进制里不就是89504E47吗?这里不也是有0x47?,这种情况下mpegts.c在解包的时候不就会「错乱」了吗」?
如下图所示,因为如果从图片的0x47开始算,以188的包长度计算,下一个包不就找不到0x47了吗?
「答案是会,但是有方法保证它不会」。这就不得不提mpegts_resync函数,在前面截图的代码里有if((*data)[0]!=0x47)时会调用mpegts_resync,如下代码所示,它的关键作用是:
首先通过avio_seek往回移动-FFMIN(seekback,pos)的大小,对应到上面的图片,就是把指针移回读取上图黄色标注的数据开始的FF位置,也就是还没读取这一块数据的时候。在for循环里通过avio_r8让指针往前逐步读字节,当遇到0x47就停下来,让指针回到0x47,然后调用reanalyze重新分析数据。可以看到,在经过mpegts_resync函数同步之后,指针回重新被同步到上图黄色标准里的0x47位置,再重新执行ffio_read_indirect打开一组188的数据,从而让TS包解析回归正常
/*XXX:trytofindabettersynchrooverseveralpackets(use\n*get_packet_size()?)*/\nstaticintmpegts_resync(AVFormatContext*s,intseekback,constuint8_t*current_packet)\n{\nMpegTSContext*ts=s->priv_data;\nAVIOContext*pb=s->pb;\nintc,i;\nuint64_tpos=avio_tell(pb);\n\navio_seek(pb,-FFMIN(seekback,pos),SEEK_CUR);\n\n//Specialcaseforfileslike01c56b0dc1.ts\nif(current_packet[0]==0x80&¤t_packet[12]==0x47){\navio_seek(pb,12,SEEK_CUR);\nreturn0;\n}\n\nfor(i=0;i<ts->resync_size;i++){\nc=avio_r8(pb);\nif(avio_feof(pb))\nreturnAVERROR_EOF;\nif(c==0x47){\navio_seek(pb,-1,SEEK_CUR);\nreanalyze(s->priv_data);\nreturn0;\n}\n}\nav_log(s,AV_LOG_ERROR,\n&34;);\n/*nosyncfound*/\nreturnAVERROR_INVALIDDATA;\n}
所以上述这个PNG图片尽管会有一点「冗余」的错误数据,但是最终还是可以被mpegts.c正常解析,从而播放。
「所以M3U8里有图片链接,是因为「劳动人民」需要「免费CDN」,而链接后缀和前置格式不大会影响视TS封装的播放,现有的IJKPlayer封装的FFmpeg就支持播放伪装成图片的TS视频链接。」
4、正文
对,这里开始才是正文,前面的png操作还算是比较「常规」,但是接下来的一些特殊案例,就是如果你不适配,大概就播放不了的场景。
因为把TS伪装成图片是一种「非标准」的做法,自然就存在各式各样的「骚操作」,例如下面这个M3U8,就包含有bmp、png、ts三种格式的链接。
?
最有趣的事,尽管链接上写的时候png,但是实际这个链接的header描述里也是一个bmp,然后这个bmp的数据还是还被AES-128加密。
?
C++音视频学习资料免费获取方法:关注音视频开发T哥,点击「链接」即可免费获取2023年最新C++音视频开发进阶独家免费学习大礼包!
我们下载这个M3U8里其中一个bmp,如下图所示,通过大小可以很明显看到它也是一个伪装成bmp的视频链接,但是它有点特殊,因为:「它经过了M3U8的AES-128加密,同时它的二进制组成也有些特殊」。
如下图所示,查看这个加密的bmp文件的二进制,可以看到从Header看它确实是bmp格式,同时因为TS视频的数据被AES-128加密了,所以此时我们看不到原始的TS封装信息,但是因为它所在的M3U8里有可用的加密key,所以我们可以直接通过一些工具来下载和解密。
比如我们可以通过开源的M3U8-Downloader来下载得到一个解密后bmp,如下图所示是上面的bmp文件经过下载解密之后的二进制格式,可以看到此时已经可以看到一些我们熟悉的信息,比如H264的描述,比如0x47和大量0xFF填充。
另外可以看到,此时的BMP因为「AES-128」的解密作用下,此时的bmp已经不是一个正常的图片格式,无法以图片的形式打开查看。
?
因为Header没了。
?
同时,此时的伪装TS封装在解密后依然不是0x47开头,所以如下图所示,视频在播放时,会找到我们蓝色选中第二行里的0x47的位置,然后开始往后读取一个188长度的TS包进行解析播放。
「但是问题来了,此时播放出来的视频,会出现没有画面的情况」。为什么会有这种情况?这就要说到前面提到的mpegts_resync。
因为从第一个0x47开始读取,那么第二个包就会是上图画出来的红色部分,因为不是0x47开头,所以会通过mpegts_resync函数找到绿色的0x47,然后继续往后读取。
「这样乍一看没有什么问题,但是其实忽略了黄色部分的0x47」,如果仔细去数,你就会发现黄色部分的0x47到绿色的0x47,恰好就是188的长度,所以其实这部分应该是一个完整的TS包,并且是很重要的一个包,也是因为它没被正确读取,所以导致了播放出现没有画面的情况。
那么这个包是什么,为什么它会这么重要?
5、TS&PAT&PMT
我们前面会出现画面无法被解析,其实就是因为我们说被「丢失」的包导致的,它恰好就是TS里的PAT包:
PAT(ProgramAssociationTable)主要的作用就是指明了PMT表的PID值PMT(ProgramMapTable)主要的作用就是指明了音视频流的PID值PID确定TS包中的数据属于什么类型
「所以由于PAT没有被正确的解析,所以没有得到正确PMT,从而没有找到正确的视频编码包的PID,所以出现了没有画面的情况」。
这也是为什么PAT包那么重要,简单来说,正常情况下解析一个TS封装的流程为:
?
TS流里每个packet一般都是188个字节,解析TS需要先解析每个packet,然后需要从一个packet中解析出PAT的PID,PAT的PID一般为0,然后从PAT包中解析出PMT的PID,再根据PMT的PID找到PMT包,在从PMT包中解析出Video和Audio的PID,然后根据PID找出相应的音视频包。
?
如下图所示,一般TS包的header主要由4个字节组成,其中sync_byte是一个字节(8b),固定为0x47,而PID是一个13b的二进制,一般PID为0的packet就会被认定为是PAT。
比如前面被我们忽略的47400010它对应二进制是01000111010000000000000000010000,按照上面拆分:
sync_byte(1B)
0x47/01000111
transport_error_indicator(1b)
传输错误指示符,通常都为0,这里也是0
payload_unit_start_indicator(1b)
负载单元起始标示符,一个完整的数据包开始时标记为1,这里恰好是1
transport_priority(1b)
传输优先级,0为低优先级,1为高优先级,通常取0,这里恰好是0
PID(13b)
这里恰好就是0000000000000,也就是0,「PID为0就说明这个TS包是M3U8里的PAT包」
Transport_scrambling_control(2b)
传输加扰控制,00表示未加密,这里是00
Adaptation_field_control(2b)
00保留;01为无自适应域,这里为01
Continuity_counter(4b)
表示该计数器为0,PID相同的包的计数因该是连续,递增计数器,从0-f,起始值不一定取0,但PID相同的包计数器必须是连续,这里是0000
所以可以看到,被我们忽略的47400010开头的包,恰好就是最重要的PAT包,这也是为什么这个视频播放是会没有画面的原因,因为最终对应视频留的PID没有被解析出来。
然后我们再去看这个PAT表里的数据,如下图所示是PAT的内容部分的结构示意图,我们主要需要的是ProgramNumber(PMT)的PID,在Nloop部分前有64b,也就是8个字节,后面Nloop部分才是开始循环的实际节目表,其中一个节目是32b,也就是4个字节,最后CRC结束标志为32b,也就是4个字节。
所以回到二进制里,黄色部分就是需要固定字节,然后红色下划线的0001EFFF就是节目表,「该PAT里只有一个节目单,其中0001是number,也就是节目number为01,PID是FFF,也就是该节目的PID是4095」。
?
黄色前还有一个00属于adapter区的,因为前面Adaptation_field_control是01。
?
然后我们在看下一个TS包,如下图红色部分,它的Header是474FFF01,对应的二进制就是0100011101001111111111110000001,那么它的PID就是0111111111111这13位,也就是FFF(4095)。
所以,到这里一切都清晰了,「因为忽略的是PAT包,所以会导致后面这个PMTID4095不被解析为特殊的TS包,从而获取不到对应的节目数据」。
那PMT如何读取出流信息?如下图所示是一个PMT的TS包结构,我们直接看Nloop部分,一个loop大概要40b,也就是5个字节,其中我们主要是streamtype和elementaryPID。
其中streamtype对应的字节代表了流的具体类型,比如0x0f就是aac音频,0x1b就是h264的视频,所以TS里可以通过PMT得到需要当然封装具体的音视频解码格式。
那么回到二进制里,如下图所示,结合PMT的结构,可以看到有两个stream,其中stream_typeh.264编码对应0x1b,aac编码对应0x0f,而E100:111[0000100000000],后13位也就是256,所以视频的PID是256,也就是h264的视频pid是256,而acc的音频的PID是257。
我们再看下一个包,可以看到这个包里有264的描述,它的header是47410031,也就是01000111010000010000000000110001,对应的PID就是0000100000000,也就是256,这就和前面的PMT继续对应上了。
image-20230331160122249
更直观一点,我们简单写一个python脚本,输出下所有的PID,可以看到除了0和4095,剩下的就都是256和257这样的流数据包,所以到这里就可以完全对应上:「PID为0的包是PAT,通过PAT得到PMT的PID,找到PID就可以得到streamtype和streampid,然后就可以找到对应的streampid的TS包去读取音视频流数据」。
定义常量\nTS_PACKET_SIZE=188\n\n39;rb&循环读取TS数据包\nwhileTrue:\nts_packet=ts_file.read(TS_PACKET_SIZE)\nifnotts_packet:\nbreak\n\n34;allowed_extensions&34;ALL&34;protocol_whitelist&34;crypto,file,http,https,tcp,tls,udp,rtmp,rtsp&34;allowed_extensions&34;ALL&34;enable-accurate-seek”,1);
好了,本篇到这里就结束了,通过讲解适配对TS封装一系列的骚操作,相信大家对TS的一些基础概念都有了一定的认识,最后总结一下;
「对于TS解码,视频的后缀格式和封装header并不会实际影响播放效果」「TS封装是以0x47开头,188字节长度,会用0xFF做冗余填充的包格式」「TS封装里PID是唯一标识,而PID为0的包是PAT包」「PAT包很重要,因为通过PAT包才能找到PMT包,找到PMT包才能正确获取音视频的PID」「FFmpeg的mpegts.c里,mpegts_resync函数可以帮助你重新同步到TSpacket的包头」。
原文链接:音视频骚操作,FFmpeg如何播放带「图片」的M3U8视频,IJKPlyaer适配非标TS文件
如果你还想了解更多这方面的信息,记得收藏关注本站。
