音视频骚操作,FFmpeg 如何播放带「图片」的 M3U8 视频,IJKPlyaer 适配非标 TS 文件

news/2024/5/6 2:12:31/文章来源:https://blog.csdn.net/ZuoYueLiang/article/details/129995675

如果看到一个需要播放的视频链接显示是一张图片,你会不会感觉有点懵?如果这张图片写着 png,然后实际格式是 bmp ,你会不会更懵了?如果这个 bmp 还做了加密篡改呢?今天我们要聊的就是这样一个充满骚操作的音视频故事。

本篇主要是想通过这个「故事」,更直观地给大家普及 M3U8 里的一些基础常识

前言

如果你经常接触音视频,那么对于 M3U8 应该不会陌生, M3U8 简单来说就是 HLS(HTTP Live Streaming) ,指的是苹果开发的基于 HTTP 协议的流媒体解决方案,它可以在普通的 HTTP 的应用上直接提供点播和直播的能力。

在 HLS 里会将视流文件切分成小片(ts)并建立索引文件(M3U8),一般如下图所示,首先会有一个 M3U8 文件,然后对应在 #EXTINF 的 tag 下会有很多 TS 格式切片,这应该是我们认知中的 M3U8 文件的标准。

详细讲解 M3U8 的这里就不详细展开,感兴趣的可以看之前分享过的探索移动端音视频与GSYVideoPlayer之旅 。

那么,如果在 M3U8 里的不是 TS 链接,而是 png 链接或者 bmp 链接会是什么情况?今天的主题就是探讨如何适配带有图片的非标准 M3U8 视频。

我们不鼓励「非标」,而是通过「非标」的适配来做科普

为什么 M3U8 里会有图片?

首先,标准的 HLS 协议里 M3U8 文件内肯定是 TS 的切片链接,那么为什么会有 png/bmp 之类的图片链接存在?或者说,为什么会是图片链接

这就不得不说「劳动人民的智慧」,众所周知,如果想让一个视频加载更快,那么最简单的办法就是给视频上 CDN ,但是碍于某些团队或者个人「囊中羞涩」,所以开始有人瞄上了公共图床的 CDN ,然后再结合 M3U8 的特性,一套民间的「免费」 视频 CDN 潜规则就这样悄然流行起来。

如果你想将一个完整视频伪装成图片上传公共图床明显不现实,因为体积太大,很多图床也会对图片的大小做限制,但是如果是 M3U8 ,那么就可以把视频分解成无数 TS ,再把 TS 伪装成图片分批上传,这样就可以给视频「依附」上 CDN 的能力

TS & 图片

如下图所示,这就是一个「非标」的 M3U8 视频链接,可以看到 #EXTINF tag 下会的链接都是 png 格式的后缀,那么这种 png 后缀,会不会影响视频播放呢

答案是不会,因为 FFmpeg 里播放并不是认定后缀,而是通过读取每个 #EXTINF tag 链接的二进制 Header,最终匹配它们的封装和编解码格式。

所以其实在 M3U8 里 #EXTINF tag 下的链接后缀并不重要,可以是 png 或者 bmp ,甚至你写 txt 也是可以的,重点其实是包本身的编码。

那么如果这个视频链接,真的是一个图片呢?如下图所示,可以看到这个 png 本身就是一个完整的图片,不过这个图片的大小和它本身的质量并不匹配,毕竟这样一个图片不可能高达 1.9 MB 。

如下图所示,我们查看这张图片的二进制,可以看到文件的 Header 确实是 PNG ,但是后面还有类似 FFmpeg Service 这样的描述,可以确实这就是一个伪装成真实 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表,Video PID,Audio PID 等等,这些也是 TS 流播放的重要依据。

而在 mpegts.c 里最重要的 read_packet 函数也是,读取的时候会读取 TS_PACKET_SIZE(188)的大小,然后判断包的首字节是不是 0x47 ,如果不是就通过 mpegts_resync 重新同步一下去尝试寻找 0x47

可能这时候细心的你已经发现了「盲点」,前面 PNG 的 Header 二进制里不就是 89 50 4E 47 吗?这里不也是有 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: try to find a better synchro over several packets (use* get_packet_size() ?) */
static int mpegts_resync(AVFormatContext *s, int seekback, const uint8_t *current_packet)
{MpegTSContext *ts = s->priv_data;AVIOContext *pb = s->pb;int c, i;uint64_t pos = avio_tell(pb);avio_seek(pb, -FFMIN(seekback, pos), SEEK_CUR);//Special case for files like 01c56b0dc1.tsif (current_packet[0] == 0x80 && current_packet[12] == 0x47) {avio_seek(pb, 12, SEEK_CUR);return 0;}for (i = 0; i < ts->resync_size; i++) {c = avio_r8(pb);if (avio_feof(pb))return AVERROR_EOF;if (c == 0x47) {avio_seek(pb, -1, SEEK_CUR);reanalyze(s->priv_data);return 0;}}av_log(s, AV_LOG_ERROR,"max resync size reached, could not find sync byte\n");/* no sync found */return AVERROR_INVALIDDATA;
}

所以上述这个 PNG 图片尽管会有一点「冗余」的错误数据,但是最终还是可以被 mpegts.c 正常解析,从而播放。

所以 M3U8 里有图片链接,是因为「劳动人民」需要「免费 CDN」,而链接后缀和前置格式不大会影响视 TS 封装的播放,现有的 IJKPlayer 封装的 FFmpeg 就支持播放伪装成图片的 TS 视频链接。

正文

对,这里开始才是正文,前面的 png 操作还算是比较「常规」,但是接下来的一些特殊案例,就是如果你不适配,大概就播放不了的场景。

因为把 TS 伪装成图片是一种「非标准」的做法,自然就存在各式各样的「骚操作」,例如下面这个 M3U8,就包含有 bmp、png、ts 三种格式的链接。

最有趣的事,尽管链接上写的时候 png ,但是实际这个链接的 header 描述里也是一个 bmp ,然后这个 bmp 的数据还是还被 AES-128 加密。

我们下载这个 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 包,并且是很重要的一个包,也是因为它没被正确读取,所以导致了播放出现没有画面的情况。

那么这个包是什么,为什么它会这么重要?

TS & PAT & PMT

我们前面会出现画面无法被解析,其实就是因为我们说被「丢失」的包导致的,它恰好就是 TS 里的 PAT 包 :

  • PAT (Program Association Table)主要的作用就是指明了 PMT 表的 PID 值
  • PMT(Program Map Table)主要的作用就是指明了音视频流的 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。

比如前面被我们忽略的 47 40 00 10 它对应二进制是 0100 0111 0100 0000 0000 0000 0001 0000 ,按照上面拆分:

sync_byte(1B)0x47 / 0100 0111
transport_error_indicator (1b)传输错误指示符,通常都为 0,这里也是 0
payload_unit_start_indicator(1b)负载单元起始标示符,一个完整的数据包开始时标记为1,这里恰好是 1
transport_priority(1b)传输优先级,0为低优先级,1为高优先级,通常取 0,这里恰好是 0
PID(13b)这里恰好就是 0 0000 0000 0000,也就是 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

所以可以看到,被我们忽略的 47 40 00 10 开头的包,恰好就是最重要的 PAT 包,这也是为什么这个视频播放是会没有画面的原因,因为最终对应视频留的 PID 没有被解析出来。

然后我们再去看这个 PAT 表里的数据,如下图所示是 PAT 的内容部分的结构示意图,我们主要需要的是 Program Number(PMT) 的 PID ,在 N loop 部分前有 64b ,也就是 8 个字节,后面 N loop 部分才是开始循环的实际节目表,其中一个节目是 32b ,也就是 4 个字节,最后 CRC 结束标志为 32b ,也就是 4 个字节。

所以回到二进制里,黄色部分就是需要固定字节,然后红色下划线的 00 01 EF FF 就是节目表,该 PAT 里只有一个节目单,其中 00 01 是 number ,也就是节目 number 为 01 , PID 是 FFF, 也就是该节目的 PID 是 4095

黄色前还有一个 00 属于 adapter 区的,因为前面 Adaptation_field_control 是 01。

然后我们在看下一个 TS 包,如下图红色部分,它的 Header 是 47 4F FF 01 ,对应的二进制就是 0100 0111 0100 1111 1111 1111 000 0001 ,那么它的 PID 就是 0 1111 1111 1111 这 13 位,也就是 FFF (4095)。

所以,到这里一切都清晰了,因为忽略的是 PAT 包,所以会导致后面这个 PMT ID 4095 不被解析为特殊的 TS 包,从而获取不到对应的节目数据

那 PMT 如何读取出流信息?如下图所示是一个 PMT 的 TS 包结构,我们直接看 N loop 部分,一个 loop 大概要 40b ,也就是 5 个字节,其中我们主要是 stream type 和 elementary PID。

其中 stream type 对应的字节代表了流的具体类型,比如 0x0f 就是 aac 音频, 0x1b 就是 h264 的视频,所以 TS 里可以通过 PMT 得到需要当然封装具体的音视频解码格式。

那么回到二进制里,如下图所示,结合 PMT 的结构,可以看到有两个 stream ,其中 stream_type h.264 编码对应 0x1b,aac 编码对应 0x0f ,而 E100 : 111[0 0001 0000 0000] ,后 13 位也就是 256,所以视频的 PID 是 256 ,也就是 h264 的视频 pid 是 256 ,而 acc 的音频的 PID 是 257。

我们再看下一个包,可以看到这个包里有 264 的描述,它的 header 是 47 41 00 31 ,也就是 0100 0111 0100 0001 0000 0000 0011 0001 ,对应的 PID 就是 0 0001 0000 0000 ,也就是 256,这就和前面的 PMT 继续对应上了。

image-20230331160122249

更直观一点,我们简单写一个 python 脚本,输出下所有的 PID ,可以看到除了 0 和 4095 ,剩下的就都是 256 和 257 这样的流数据包,所以到这里就可以完全对应上: PID 为 0 的包是 PAT ,通过 PAT 得到 PMT 的 PID ,找到 PID 就可以得到 stream type 和 stream pid ,然后就可以找到对应的 stream pid 的 TS 包去读取音视频流数据

# 导入需要的模块
import sys# 定义常量
TS_PACKET_SIZE = 188# 打开 TS 文件
with open(sys.argv[1], 'rb') as ts_file:pids = []# 循环读取 TS 数据包while True:ts_packet = ts_file.read(TS_PACKET_SIZE)if not ts_packet:break# 提取 PID 并输出pid = (ts_packet[1] & 0x1F) << 8 | ts_packet[2]pids.append(pid) print(pids)

所以前面的的「奇奇怪怪」的编码,恰好会让 FFmpeg 忽略掉 PAT 数据,从而导致加载到节目表而导致没有画面。

开始适配

基于这个逻辑,我觉得应该是首先解决 PAT 包被忽略的问题,所以如下代码所示,在 mpegts.cread_packet 里我添加了 if((*data)[0] == 0x47 && (*data)[188] != 0x47) 的判断,如果包是以 0x47 开头,但是下一个包不是 0x47 ,那么就在包内重新去寻找一个能「首尾相接」的 0x47 TS 包,然后重新 ffio_read_indirect

static int read_packet(AVFormatContext *s, uint8_t *buf, int raw_packet_size,const uint8_t **data)
{AVIOContext *pb = s->pb;int len;len = ffio_read_indirect(pb, buf, TS_PACKET_SIZE, data);if (len != TS_PACKET_SIZE)return len < 0 ? len : AVERROR_EOFfor (;;) {len = ffio_read_indirect(pb, buf, TS_PACKET_SIZE, data);if (len != TS_PACKET_SIZE)return len < 0 ? len : AVERROR_EOF;/* check packet sync byte */if ((*data)[0] != 0x47) {/* find a new packet start */if (mpegts_resync(s, raw_packet_size, *data) < 0)return AVERROR(EAGAIN);elsecontinue;} else {/  / / 添加的部分  /  /  /if((*data)[0] == 0x47 && (*data)[188] != 0x47) {for(int i = 0; i < TS_PACKET_SIZE; i++) {if((*data)[i] == 0x47 && (*data)[i+188] == 0x47) {avio_seek(pb, i, SEEK_CUR);avio_seek(pb, -TS_PACKET_SIZE, SEEK_CUR);reanalyze(s->priv_data);len = ffio_read_indirect(pb, buf, TS_PACKET_SIZE, data);if (len != TS_PACKET_SIZE)return len < 0 ? len : AVERROR_EOF;return 0;}}} else {break;}}}return 0;
}

重新打包之后,它确实可以解析出画面了,而画面却出现花屏,花屏肯定是播放过程中出现了丢包,导致 IBP 帧解析出现异常,所以我们上面的写法存在问题。

而恰巧这时候我们发现,前面 bmp 填充部分的长度,恰好也是 188 字节,这种巧合让我不禁怀疑,是不是其实我们不需要忽略这个前置字节?

所以我们直接无视 0x47 的开头,直接读取解析(因为后续一些解析逻辑也会判断 0x47 ,所以这里我们强行无视),然后重新打包之后,我们惊喜的发现可以播放了,也不会花屏了,但是又有新的问题出现:一个 TS 播放完了它不会切换到下一个 TS 。

然后我们再去看这个 TS 文件的末尾,原来文件末尾填充了大量的 0x00 字节,从而导致读取时无法正常触发结束标识。

所以我们再次简单修改下,当遇到 0x00 开头的包时,我们用 mpegts_resync 函数处理一下,如果找不到正常的包,我们就可以直接返回 AVERROR(EAGAIN) 结束这个 TS 的播放。

到这里我们可以看到这个 M3U8 可以正常播放了,对应的 stream 也可以被解析出现,虽然这里的修改很简单粗暴,但是这样的修改,就可以在兼容正规协议的情况下,也可以适配到这种「民间非标」支持,重点是通过这个例子,可以形象的普及 TS 封装里的基础概念。

以上修改需要调整 FFmpeg 的源码,然后重新构建动态库。

另外这个格式的文件的 ExoPlayer 下也是无法被播放,主要是因为前面说的读错了 0x47 的包头位置,因为后面 0xFF 太多,会导致超过两个 TS_PACKET_SIZE 的判断,从而抛出异常,如果要想 ExoPlayer 也支持播放,可以从这点切入去修改源码。

另外你会发现浏览器是可以播放这类链接,因为如 hls.js 在这方面的检测没有那么严谨。

最后

上述播放调整是基于 IJKPlayer 上的 FFmpeg 版本进行,虽然如今 IJKPlayer 已经没有维护,但是基于 IJKPlayer 做一些调整优化还是很方便。

当然,因为 IJKPlayer 整体构建环境比较老,所以如果你重新构建编译,可以参考 GSYVideoPlayer 下的 编译 IJKPlayer so 相关支持 ,目前文档已经支持到 Mac M1/M2 下的环境 。

还有调试这类 TS 文件,个人建议使用本地播放 M3U8 来进行测试,这样我们可以更方便在播放时动态修改本地的 TS 二进制字节,例如可以修改 M3U8 为下面的文件格式。

当然,如果播放本地 M3U8 遇到了下方类似的错误提示,可以参考下方代码添加 "allowed_extensions", "ALL" 到 IJKPlayer 里来临时允许播放。

(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "protocol_whitelist", "crypto,file,http,https,tcp,tls,udp,rtmp,rtsp");
(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_extensions", "ALL");
(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "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 函数可以帮助你重新同步到 TS packet 的包头

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

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

css三角和css 用户见面样式,vertical-align 属性应用,溢出的文字省略号显示,常见布局技巧

目录 3.CSS三角 4.CSS 用户界面样式 4.1什么是界面样式 4.2轮廓线 outline 4.3 防止拖拽文本域 resize 5.vertical-align 属性 5.1图片,表单都属于行内块元素&#xff0c;默认的vertical-align 是基线对齐。 5.2解决图片底部默认空白缝隙问题 6.溢出的文字省略号显示 1.单…

linux centos7 查看端口占用命令netstat 报错提示 –bash:netstat:未找到命令解决方法

今天在一台centos7上用netstat命令看端口占用情况&#xff0c;提示 –bash:netstat:未找到命令&#xff1a; 解决方法&#xff1a; 输入 yum search ifconfig 查看这个命令是在 net-tools.x86_64里的&#xff1a; 然后安装这个包&#xff0c;输入 yum install net-tools 安装&…

ERTEC200P-2 PROFINET设备完全开发手册(2-1)

2. 入门指导&#xff1a;第一个PN IO设备 开发之前的准备&#xff0c;需要的软件&#xff1a; TIA Portal V16、V17串口终端软件 (MobaXterm或Putty或TeraTerm)Win10 并且安装64位JAVA运行环境J-Link的驱动软件Proneta&#xff08;推荐使用&#xff09; 需要准备的硬件 性能…

通信算法之130:软件无线电-接收机架构

1. 超外差式接收机 2.零中频接收机 3.数字中频接收机

洛谷B2033A*B问题

洛谷B2033 题目描述 输入两个正整数A 和B&#xff0c;求 AB 的值。注意乘积的范围和数据类型的选择。 输入格式 一行&#xff0c;包含两个正整数 A 和B&#xff0c;中间用单个空格隔开。1≤A,B≤50000。 输出格式 一个整数&#xff0c;即AB 的值 代码&#xff1a; #include&…

MySQL-双主高可用

目录 &#x1f341;拓扑环境 &#x1f341;配置两台MySQL主主同步 &#x1f343;修改MySQL配置文件 &#x1f343;配置主从关系 &#x1f343;测试主主同步 &#x1f341;keepalived高可用 &#x1f343;keepalived的安装配置 &#x1f343;master配置 &#x1f343;slave配置 …

Aurora 64B/66B 协议介绍

简介 Aurora 是一个用于在点对点串行链路间移动数据的可扩展轻量级链路层协议。这为物理层提供透明接口&#xff0c;让专有协议或业界标准协议上层能方便地使用高速收发器。虽然使用的逻辑资源非常少&#xff0c;但 Aurora 能提供低延迟高带宽和高度可配置的特性集。 特性&…

凹凸/法线/移位贴图的区别

你是否在掌握 3D 资产纹理的道路上遇到过障碍&#xff1f; 不要难过&#xff01; 许多刚接触纹理或 3D 的艺术家在第一次遇到凹凸贴图&#xff08;Bump Map&#xff09;、法线贴图&#xff08;Normal Map&#xff09;和移位贴图&#xff08;Displacement Map&#xff09;时通常…

React class组件和hooks setState异步更新数据详解

一、 class组件setState详解 1.class组件setState异步更新数据详解 class Father extends React.Component{state {num:0}addHandler () > { this.setState({num: 100})console.log(state中的值,this.state.num)}render() { return (<div><button onClick{this…

DBC数据库中定义信号时采用的两种字节顺序:Intel、Motorola(深度好文)

我之前写过好几篇文章介绍大端小端的存储、显示和读取。在介绍DBC的文章中,也有信号在CAN消息数据中如何定义的顺序,它和大端小端采用的原理相同,但是不能带入数据大端小端存储的方法。这里千万要注意! DBC数据库中定义信号时采用的字节顺序,如果想讲明白,很简单。但是如…

「解析」Jetson 安装 CUDA/cuDNN

注意&#xff1a;自从JetPack 升级到 5.0版本之后&#xff0c;可以&#xff0c;JetPack 官方教程 官方教程提供了三种方法&#xff1a;SD卡、SDK Manager 以及 apt安装Jetpack。前两种主要用于Orin系列之前的 Jetson开发板&#xff0c;主要针对还没有烧录系统的空机。而从 Jets…

手机也可以3D沙发建模

3D沙发建模是当今室内设计领域中必不可少的一种技术。通过此技术&#xff0c;我们可以使用虚拟设计软件创建高质量的3D沙发模型。这些模型具有极高的精度和逼真度&#xff0c;可以帮助设计师更好地展示他们的创意&#xff0c;并有效地促进设计过程。 在进行3D沙发建模时&#…

洛谷B2038奇偶ASCII值判断

洛谷B2038 题目描述 任意输入一个字符&#xff0c;判断其 ASCII 是否是奇数&#xff0c;若是&#xff0c;输出 YES&#xff0c;否则&#xff0c;输出 NO 。 例如&#xff0c;字符 A 的 ASCII 值是 65&#xff0c;则输出 YES&#xff0c;若输入字符 B(ASCII 值是 66)&#xff0…

shell脚本基础之详解结构化命令(一)

详解结构化命令使用if-then语句注意&#xff1a;if-then-else语句嵌套if语句elif语句注意&#xff1a;test语句注意&#xff1a;数值比较字符串比较字符串相等性字符串顺序字符串大小文件比较检查目录检查对象是否存在检查文件检查是否可读检查非空文件复合条件测试if-then高级…

怎么选购邮件营销工具?

据可靠数据统计&#xff0c;邮件营销得投资回报比达1&#xff1a;44&#xff0c;他高性价比的特性在众多营销方式中脱颖而出。他促使企业能够以较低的成本&#xff0c;和客户建立联系并维持长期联系。邮件营销对企业来讲无疑是极佳的获客渠道和营销方式。 想要做好邮件营销通常…

API 优先级和公平性(APF)

1. 概述 目前apiserver默认的限流方式太过简单 目前k8s缺少客户端业务请求隔离&#xff0c;一个错误的客户端发送大量请求可能造成其他客户端请求异常&#xff0c;也不支持突发流量。 2. 开启APF APF测试 开启APF&#xff0c;需要在apiserver配置 --feature-gatesAPIPrior…

乐观锁的作用(php代码实现)

非乐观锁场景时序图&#xff1a; 乐观锁场景示意图&#xff1a; 假设有一个账户余额表 user_balance&#xff0c;其中有两个字段&#xff1a;user_id 和 balance&#xff0c;分别表示用户 ID 和账户余额。现在有两个用户同时进行充值操作&#xff0c;充值金额分别为 100 元…

Zotero安装教程

一、下载 可以直接通过Zotero | Your personal research assistant下载安装包。 根据对应的系统选择下载包。 二、安装 安装过程简单&#xff0c;一路next直到出现下图为安装成功。 三、注册账号 安装完成后&#xff0c;打开zotero&#xff0c;选择编辑->首选项->同步…

【Python_Selenium学习笔记(五)】基于Selenium模块实现鼠标操作

基于Selenium模块实现鼠标操作 前言 为了模拟鼠标操作&#xff0c;Selenium 模块提供了 Actionchains 类&#xff0c;可以模仿人的几乎任何鼠标行为操作&#xff1b; 在此篇文章主要介绍 Actionchains类 的常用方法&#xff0c;使用流程&#xff0c;并以具体的示例进行展示。…

ERROR: No matching distribution found for subprocess

安装python包时出现了 ERROR: Could not find a version that satisfies the requirement subprocess (from versions: none) ERROR: No matching distribution found for subprocess 这里我们使用的指令是&#xff1a; 尝试使用特定版本的库。如果pip无法找到最新版本的库&a…