基于 FFmpeg 的跨平台视频播放器简明教程(九):Seek 策略
系列文章目录
- 基于 FFmpeg 的跨平台视频播放器简明教程(一):FFMPEG + Conan 环境集成
- 基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux)
- 基于 FFmpeg 的跨平台视频播放器简明教程(三):视频解码
- 基于 FFmpeg 的跨平台视频播放器简明教程(四):像素格式与格式转换
- 基于 FFmpeg 的跨平台视频播放器简明教程(五):使用 SDL 播放视频
- 基于 FFmpeg 的跨平台视频播放器简明教程(六):使用 SDL 播放音频和视频
- 基于 FFmpeg 的跨平台视频播放器简明教程(七):使用多线程解码视频和音频
- 基于 FFmpeg 的跨平台视频播放器简明教程(八):音画同步
文章目录
- 系列文章目录
- 前言
- FFmpeg API 中的 Seek 方法
- avformat_seek_file
- Seek 到关键帧
- GOP
- Show me the Code
- 发出 seek 命令
- Demux 线程进行 seek 操作
- 解码线程进行 seek 操作
- 精准 Seek
- Show Me The Code
- Seek 性能优化
- 解码前丢弃非参考帧。
- GOP 内向后 seek 逻辑优化
- 总结
- 参考
前言
经过前面八章的学习与代码实现,我们的播放器已经能够正常播放视频了,接下来我们将加入最常用的 seek 能力,让你能够快进/快退。
本文参考文章来自 An ffmpeg and SDL Tutorial -Tutorial 07: Seeking。这个系列对新手较为友好,但 2015 后就不再更新了,以至于文章中的 ffmpeg api 已经被弃用了。幸运的是,有人对该教程的代码进行重写,使用了较新的 api,你可以在 rambodrahmani/ffmpeg-video-player 找到这些代码。
本文的代码在 ffmpeg_video_player_tutorial-my_tutorial07.cpp 和 ffmpeg_video_player_tutorial-my_tutorial07_01_accurate_seek.cpp。
FFmpeg API 中的 Seek 方法
我们想做的事情很简单:当用户按下左右键时,播放进度快进 5s 或者回退 5s。例如当前播放进度为 1s,当按下右键时,期望播放进度能够跳跃到 6s。
FFmpeg 提供了 seek 的接口,但它没法精确的跳跃到我们期望的位置,它有一些限制。关于精准 seek 我们稍后会进行讨论,目前让我们将目光关注到 FFmpeg 提供的 seek api 上。
avformat_seek_file
FFmpeg 提供了 avformat_seek_file
进行 seek,函数原型:
int avformat_seek_file(AVFormatContext *s, int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags);
函数的参数和返回值说明如下:
- AVFormatContext *s: 媒体文件句柄。它是函数的主要输入,指向处理的媒体文件的上下文。
- int stream_index: 用作时间基准参考的流的索引。如果流索引为-1,则所有的时间戳都以AV_TIME_BASE单位处理。如果此标志包含AVSEEK_FLAG_FRAME,则流索引中的所有时间戳都以帧为单位。
- int64_t min_ts: 可接受的最小时间戳。此时间戳定义了ts可能到达的范围下限。
- int64_t ts: 目标时间戳。这是函数尝试寻找并尽可能接近的时间戳。
- int64_t max_ts: 可接受的最大时间戳。这个时间戳定义了ts可能到达的范围上限。
- int flags: 表示寻址标志,包含 AVSEEK_FLAG_BYTE、AVSEEK_FLAG_FRAME、AVSEEK_FLAG_ANY、AVSEEK_FLAG_BACKWARD 四个选项,用于指定时间戳的单位、是否将非关键帧视为关键帧,等等。
首先明确一点,seek 到目标时间 ts 上,这个 ts 的时间单位是什么?在 avformat_seek_file
注释中对 ts 的时间单位做了说明,总结下来:
- 如果“flags”包含 AVSEEK_FLAG_BYTE,那么所有时间戳都是以字节为单位的,它们代表的是文件位置(不过这种方式可能不会被所有的demuxers所支持)
- 如果“flags”包含 AVSEEK_FLAG_FRAME,那么所有的时间戳都是以帧为单位的,这些帧在由stream_index 参数指定的那个流中(这种方式同样可能不会被所有的demuxers所支持)
- 如果 stream_index 为 -1,则以 AV_TIME_BASE 为单位
- 否则,所有时间戳都以选定的流单元中的时间基为单位表示
在我们的代码实现中并不会使用到 AVSEEK_FLAG_BYTE 或者 AVSEEK_FLAG_FRAME,因此忽略上面的 1、2 点。将 stream_index
设置为 -1 是一种常见的选择,使得 ts 时间单位为 AV_TIME_BASE(也就是 1us)。
注意到 avformat_seek_file
作用的对象是 AVFormatContext
,这也就说它影响的是解封装的结果:经过 seek 之后,av_read_frame
将读取到新位置的 AVPacket。
Seek 到关键帧
虽然在 “flags” 中包含 AVSEEK_FLAG_BYTE 或者 AVSEEK_FLAG_FRAME 使得可以 seek 到任意位置(帧)上,但在实际使用上这两个 flag 很少被使用。我们最常用的还是 flags = 0 或者 flags = AVSEEK_FLAG_BACKWARD,这种情况下 avformat_seek_file 将会跳转至符合要求的关键帧(I帧)位置上。
在跳转时,如果指定了 AVSEEK_FLAG_BACKWARD 标志,则会优先跳转到前一个关键帧,否则会优先跳转到后一个关键帧。如果没有关键帧,则会跳转到最接近的非关键帧。但是,跳转到非关键帧可能会导致解码器出现错误或画面不完整的情况,因此在实际应用中,一般会尽量跳转到关键帧。
由于avformat_seek_file的特性,它会跳转到最接近指定时间戳的关键帧,因此在实际应用中,你可能会发现跳转的位置并不完全符合预期。例如,如果当前播放位置在1秒,你希望快进到6秒,但是在执行avformat_seek_file后,播放位置可能会跳转到8秒。这是因为在8秒处有一个关键帧,而在预期的6秒处没有关键帧。所以,avformat_seek_file会选择最近的关键帧进行跳转。「精准 seek」中将说明如何处理这种情况,此处不表。
GOP
两个关键帧的距离,有一个专业的名词来描述,即 GOP。GOP,全称为Group of Pictures,中文译为“图像组”,是视频编码中的一个重要概念。
在视频编码中,为了提高压缩效率,通常会采用帧间预测的方式,即利用前后帧之间的相关性,只编码和前后帧的差异部分。这样可以大大减少需要编码的数据量,从而提高压缩效率。而GOP就是帧间预测的基本单位。
一个GOP由一个I帧开始,后面跟随若干个P帧和B帧。I帧是关键帧,可以独立解码,而P帧和B帧则需要依赖其他帧进行解码。GOP的长度,即一个GOP中包含的帧数,是可以调整的,它直接影响到视频的压缩效率和错误恢复能力。GOP长度越短,错误恢复能力越强,但压缩效率较低;反之,GOP长度越长,压缩效率越高,但错误恢复能力较弱。
此外 GOP 还与视频画面质量有关,详细说明请参考:
- Back to basics: GOPs explained
- 关于GOP和帧率、码率的关系
Show me the Code
讲解往 ffmpeg 中关于 seek 的 api 后,现在来说明如何在代码中实现 seek 的逻辑。大体步骤为:
- 通过键盘发出 seek 命令,例如按下左右键等
- Demux 线程收到 seek 的请求后,调用
avformat_seek_file
进行 seek 操作;Demux 完成 seek 后,还需要通知视频/音频解码线程发生了 seek 操作 - 解码线程收到 seek 通知后,清理解码器上下文中的缓存信息
接下来对上述步骤做详细的说明
发出 seek 命令
void onEvent(const SDL_Event &event) {switch (event.type) {case SDL_KEYDOWN: {switch (event.key.keysym.sym) {case SDLK_LEFT: {play_ctx->doSeekRelative(-5.0);break;}case SDLK_RIGHT: {play_ctx->doSeekRelative(5.0);break;}case SDLK_DOWN: {play_ctx->doSeekRelative(-60.0);break;}case SDLK_UP: {play_ctx->doSeekRelative(60.0);break;}}break;}// ....
SDL 支持键盘事件,当我们按了某些按键时,通过 SDL_Event
中的信息来判断所按的键是哪个。在上述代码中,支持左右上下键来快进和快退。
void doSeekRelative(double incr) {if (!seek_req) {std::lock_guard lg(seek_mut);auto pos = getAudioClock();pos += incr;if (pos < 0) {pos = 0;}seek_rel = (int64_t)(incr * AV_TIME_BASE);seek_pos = (int64_t)(pos * AV_TIME_BASE);seek_flags = (incr < 0) ? AVSEEK_FLAG_BACKWARD : 0;seek_req = true;}}
doSeekRelative
函数中:
getAudioClock
获取当前的音频时钟,即播放到第几秒了。pos += incr;
计算快进快退的目标位置- 接着计算
seek_rel
和seek_pos
,这里将时间单位转换到 AV_TIME_BASE 是为了后面处理更方面;seek_flags
如果是快退的话设置为AVSEEK_FLAG_BACKWARD
- 最后设置
seek_req = true
等待 demux 线程消费
Demux 线程进行 seek 操作
if (ctx.seek_req) {// seek stuff goes hereint64_t seek_pos = 0;int64_t seek_rel = 0;int seek_flags = 0;{std::lock_guard lg(ctx.seek_mut);seek_pos = ctx.seek_pos;seek_rel = ctx.seek_rel;seek_flags = ctx.seek_flags;}auto min_ts = (seek_rel > 0) ? (seek_pos - seek_rel + 2) : (INT64_MIN);auto max_ts = (seek_rel < 0) ? (seek_pos - seek_rel - 2) : (INT64_MAX);ret = avformat_seek_file(ctx.decode_ctx->demuxer.getFormatContext(), -1,min_ts, seek_pos, max_ts, seek_flags);if (ret < 0) {fprintf(stderr, "%s: error while seeking %s\n",decode_ctx.demuxer.getFormatContext()->url, av_err2str(ret));} else {if (ctx.decode_ctx->video_stream_index >= 0) {decode_ctx.video_packet_sync_que.clear();decode_ctx.video_packet_sync_que.tryPush(&flush_packet);}if (ctx.decode_ctx->audio_stream_index >= 0) {decode_ctx.audio_packet_sync_que.clear();decode_ctx.audio_packet_sync_que.tryPush(&flush_packet);}}ctx.setClock(ctx.audio_clock_t, seek_pos / (double)AV_TIME_BASE);ctx.seek_req = false;
}
上述代码在 demux 线程中,它展示了处理 seek 命令的逻辑操作。
- 在收到 seek 请求后,将
seek_pos
、seek_rel
和seek_flags
保存起来(为了线程安全) - 接着调用
avformat_seek_file
接口,至于min_ts
和max_ts
为啥这样算?是直接抄的 ffplay 中代码(哈哈哈哈)。注意avformat_seek_file
第二个参数是 -1,这也意味着所有输入的时间戳单位是 AV_TIME_BASE 为单位的。 - 接着,清空视频和音频的 packet queue,并向它们都发送了一个 flush packet,通过这个 flush packet 去通知解码线程发生了 seek 操作。
解码线程进行 seek 操作
// seek stuff here
if (std::strcmp((char *)pkt->data, FLUSH_DATA) == 0) {avcodec_flush_buffers(codec.getCodecContext());out_frame_queue.clear();return 0;
}
在解码线程中,要做的事情非常简单:
- 判断当前 packet 是否是 flush packet
- 如果是,那么调用
avcodec_flush_buffers
清理解码器上下文缓存,且清空 out_frame_queue 里的数据
精准 Seek
回到之前的那个问题:avformat_seek_file
只能跳转到关键帧,在实际应用中,你可能会发现跳转的位置并不完全符合预期。例如,如果当前播放位置在1秒,你希望快进到6秒,但是在执行avformat_seek_file后,播放位置可能会跳转到8秒。这是因为在8秒处有一个关键帧,而在预期的6秒处没有关键帧。
如何解决这个问题?要做两件事情:
- 调用
avformat_seek_file
时,确保跳转到目标位置(target position)的前面的关键帧。例如target_pos = 2s
时,跳转到关键帧应该满足pos <= 2s
。调用avformat_seek_file
时,将 flag 设置为 AVSEEK_FLAG_BACKWARD 即可,如果没有设置这个标志,那么找到的关键帧可能会超过目标时间戳。 - 从关键帧开始解码,直到当前位置(current position)大于目标位置。满足该条件后,意味着我们 seek 到了目标位置,可以进行该视频帧的播放。
接下来让我们看具体实现的代码 ffmpeg_video_player_tutorial-my_tutorial07_01_accurate_seek.cpp
Show Me The Code
使用 SDL 处理键盘事件的代码与之前是一样的,此处不再赘述。直接看 demux 线程与解码线程的修改。
Demux Thread:
if (ctx.seek_req) {// seek stuff goes hereint64_t seek_pos = 0;int64_t seek_rel = 0;int seek_flags = 0;{std::lock_guard lg(ctx.seek_mut);seek_pos = ctx.seek_pos;seek_rel = ctx.seek_rel;seek_flags = ctx.seek_flags;seek_flags = AVSEEK_FLAG_BACKWARD;}auto min_ts = (seek_rel > 0) ? (seek_pos - seek_rel + 2) : (INT64_MIN);auto max_ts = (seek_rel < 0) ? (seek_pos - seek_rel - 2) : (seek_pos);ret = avformat_seek_file(ctx.decode_ctx->demuxer.getFormatContext(), -1,min_ts, seek_pos, max_ts, seek_flags);if (ret < 0) {fprintf(stderr, "%s: error while seeking %s\n",decode_ctx.demuxer.getFormatContext()->url, av_err2str(ret));} else {seek_packet.pos = seek_pos;// ...
}
Demux 线程代码与之前有两个差异点:
seek_flags
被设置为AVSEEK_FLAG_BACKWARD
- 设置 seek_packet 的 pos 为目标位置,告诉解码线程目标位置的值
Decode Thread:
// seek stuff here
if (pkt->stream_index == FF_SEEK_PACKET_INDEX) {avcodec_flush_buffers(codec.getCodecContext());out_frame_queue.clear();seeking_flag = true;target_seek_pos_avtimebase = pkt->pos;return 0;
}
解码线程收到 seek packet 后,首先做了两件事情:
- 清理缓存。调用
avcodec_flush_buffers
清理解码器上下文缓存;清理 out_frame_queue 中的缓存帧 - 保存 seek 信息,以便后续的循环解码
while (ret >= 0) {ret = codec.receiveFrame(out_frame);ON_SCOPE_EXIT([&out_frame] { av_frame_unref(out_frame); });// need more packetif (ret == AVERROR(EAGAIN)) {break;} else if (ret == AVERROR_EOF || ret == AVERROR(EINVAL)) {// EOF exit loopbreak;} else if (ret < 0) {printf("Error while decoding.\n");return -1;}if (seeking_flag) {auto cur_frame_pts_avtimebase =av_rescale_q(out_frame->pts, stream_time_base, AV_TIME_BASE_Q);if (cur_frame_pts_avtimebase < target_seek_pos_avtimebase) {break;} else {seeking_flag = false;}}out_frame_queue.waitAndPush(out_frame);
}
在原有的解码逻辑中,我们加入了对 seek 的判断:
- 如果命中
seeking_flag
,判断当前解码帧的 pts 是否小于目标位置。如果是,以为还没有解码到目标帧,继续解码;如果否,那么当前帧已经满足目标位置。 - 满足条件后,将
seeking_flag
设置为false
,将解码的视频帧放入 queue 中,让 sdl 去播放。
Seek 性能优化
实现精准 seek 后,可以发现在某些情况下需要解码很多很多帧,才能到达 seek 的目标位置。举个例子,假设 GOP=100,目标位置在 GOP 的最后一帧,调用 avformat_seek_file
后,seek 到了当前 GOP 的第一帧,于是需要解码 100 帧。有啥优化的办法吗?这里大致描述下,具体代码留给各位自行实现了。
解码前丢弃非参考帧。
AVPacket 的 flags 标志位中可以知道当前的 packet 是否可以被解码器丢弃。以下是对上述标志位的解释:
- AV_PKT_FLAG_KEY:这个标志表示该数据包包含一个关键帧。在视频编码中,关键帧是完整的帧,可以独立于其他帧进行解码。
- AV_PKT_FLAG_CORRUPT:这个标志表示该数据包的内容已经损坏。这可能是由于数据传输错误或者编码错误导致的。
- AV_PKT_FLAG_DISCARD:这个标志表示这个数据包在解码后可以被丢弃。这些数据包对于维持解码器的状态是必要的,但是对于输出来说并不需要。
- AV_PKT_FLAG_TRUSTED:这个标志表示这个数据包来自一个可信的源。这意味着即使数据包中包含一些不安全的结构,如指向数据包外部数据的任意指针,也可以被接受。
- AV_PKT_FLAG_DISPOSABLE:这个标志表示这个数据包包含的帧可以被解码器丢弃,也就是说这些帧不是参考帧。在视频编码中,参考帧是其他帧在预测编码时需要参考的帧,而非参考帧则不需要被其他帧参考。
或者使用 AVDiscard 来丢弃某些帧,具体参考 百倍变速–解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!!
GOP 内向后 seek 逻辑优化
如果 seek 的目标位置与当前位置属于同一个 GOP,且为向后 seek,那么可以优化现有 seek 逻辑,无需做其他操作只需等待解码器解码到目标位置即可。
总结
本文介绍了播放器中如何实现快进、快退功能,并给出了具体的实现代码,还讨论了如何实现精准 seek 逻辑,并在最后给出了一些优化的思路。本文的代码在 ffmpeg_video_player_tutorial-my_tutorial07.cpp 和 ffmpeg_video_player_tutorial-my_tutorial07_01_accurate_seek.cpp。
参考
- An ffmpeg and SDL Tutorial -Tutorial 07: Seeking
- ffmpeg_video_player_tutorial-my_tutorial07.cpp
- ffmpeg_video_player_tutorial-my_tutorial07_01_accurate_seek.cpp。
- Back to basics: GOPs explained
- 关于GOP和帧率、码率的关系
- 百倍变速–解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!!
相关文章:
基于 FFmpeg 的跨平台视频播放器简明教程(九):Seek 策略
系列文章目录 基于 FFmpeg 的跨平台视频播放器简明教程(一):FFMPEG Conan 环境集成基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux)基于 FFmpeg 的跨平台视频…...

设计模式_备忘录模式
备忘录模式 介绍 设计模式定义案例问题堆积在哪里解决办法备忘录模式行为型模式, 保存了数据某一个时间点的状态 在需要的时候进行回档单机游戏的角色 数据保存并且回档保存和回档加一个状态管理类 类图 代码 MomentData using UnityEngine;public class MomentD…...
双势阱模型
双势阱模型 原子钟 传统的原子钟利用氨分子 由于隧道效应,上顶点的氮原子可以贯穿三个氢原子形成的势垒,到达下顶点对体系注入微波能量后,氮原子在上下定点之间振荡,体系的能量在两个稳定态之间交替变换,其振荡频率决…...

文献阅读:The Reversal Curse: LLMs trained on “A is B” fail to learn “B is A”
文献阅读:The Reversal Curse: LLMs trained on “A is B” fail to learn “B is A” 1. 文章简介2. 实验 & 结果考察 1. finetune实验2. 真实知识问答 3. 结论 & 思考 文献链接:https://arxiv.org/abs/2309.12288 1. 文章简介 这篇文章是前…...

真实感受:是智能家居在选择合适的技术!
科技从来都是为了让我们的生活更加的简单、舒适,而智能家居的智能,体现在如何更更更方便的使用我需要控制的家居。 例如:下班躺在床上想休息,房间和大厅的灯还开着,这时你会选择什么产品躺着解决问题? 红外…...
前端 TS 快速入门之二:接口
1. 接口有什么用 通过 interface 定义接口。 检测对象的属性,不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。 interface IPerson {name: string;age: number; } function say(person: IPerson): void {console.log(my name is ${pers…...

论文生成器(论文、文献综述、开题报告……),Java、Python、C++
“让论文生成器为您省时省力,轻松写出高质量的论文!” 2022年,腾讯全球数字生态大会腾讯云智能专场发布。 链接:http://xiezuo.saiertewl.cn/tb/xrWQed?dCodeh1xDrXmuhZbKPKgI&couponCodexiaoweilunwen...

【Java基础面试三十六】、遇到过异常吗,如何处理?
文章底部有个人公众号:热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享? 踩过的坑没必要让别人在再踩,自己复盘也能加深记忆。利己利人、所谓双赢。 面试官:遇到过异常吗࿰…...

DASCTF-CBCTF-2023 Crypto部分复现
文章目录 EzRSACB backpack 这次比赛没打,记错时间了,看了一下,如果去做的话大概也只能做出那两道简单的题,还是太菜啦 EzRSA 题目描述: from Crypto.Util.number import * import random from gmpy2 import * from …...
为什么要做字节对齐 alignment?
下面这段 C 代码的输出是什么?定义的 Type 占用的字节数(下面简称为字节数)是多少呢? #include <iostream>struct Type {char a;int b; };int main(void) {std::cout << sizeof(Type) << \n; }经过编译运行&am…...

(零基础学习)Neo4j+Spring boot 自行定义属性
前置知识 1.Neo4j :属性 节点和关系都可以设置自己的属性。 属性是由Key-Value键值对组成,键名是字符串。属性值是要么是原始值,要么是原始值类型的一个数组。比如String,int和iint[]都是合法的。 注意 null不是一个合法的属性值。 Nulls能…...
【JavaEE】Java的文件IO
文件IO操作 Linux 下的文件操作讲解Java中的文件操作 -- 对文件的增删改查Java中对文件内容的操作 -- 读写操作使用案例 Linux 下的文件操作讲解 在我的Linux栏目下有, 如有需要, 点击下面进行跳转: 内存级文件系统语言级别的文教操作磁盘文件 Java中的文件操作 – 对文件的…...
域名解析与记录
域名解析是将域名转换为IP的过程,使得人们能够直接通过域名访问网站,而不用记繁琐的IP地址信息。而在域名解析中,CNAME记录和A记录是两个不同的记录类型。 A记录(Address Record,地址记录)是指将一个域名解…...
Android 13.0 第三方无源码apk授予QUERY_ALL_PACKAGES等其他权限的方法
1.概述 在13.0的系统产品中,对于内置第三方低版本app时,会有某些权限的冲突,导致在启动app时,崩溃掉,在查询相关日志发现是报权限的问题,所以就需要在安装解析app的时候,授予权限 2.第三方无源码apk授予QUERY_ALL_PACKAGES等其他权限的方法的核心类 /frameworks/base…...
【CSS】gird 网格
网格(Grid)是一种基于列数的布局系统,它可以帮助开发者创建具有水平和垂直分隔的页面布局。在CSS中,Grid是一种非常强大的布局工具,可以轻松地创建复杂的布局结构。Grid的主要属性包括: grid-template-col…...

(未完待续)【Netty专题】Netty实战与核心组件详解
目录 前言阅读对象阅读导航前置知识课程内容一、Netty简介1.1 Netty是什么1.2 Netty有什么优势 二、第一个Netty程序2.1 Netty简单使用示例2.2 代码解读2.3 Netty的特性2.3.1 Netty的事件 2.4 Netty线程模型 三、Netty核心组件详解(未完待续)3.1 EventLo…...

“第四十五天” 数据结构基本概念
目前看的有关数据结构的课,估计这周就看完了,但感觉差很多,还是和c一样,这样过一下吧。但可能比较急,目前是打算争取寒假回家之前把四大件都先大致过一遍。 数据结构里面有很多新的定义和概念,学到现在&am…...

《java 桌面软件开发》swing 以鼠标为中心放大缩小移动图片
swing 使用Graphic2D 绘制图片,要实现对图片进行缩放和自由拖动。 1.以鼠标所在的位置为中心,滚轮控制缩放 2.缩放后再支持鼠标拖动。 基本原理: 利用scale() 函数。进行缩放。但是要注意的地方是,如果是在 public void paintCom…...

浅析人脸活体检测技术的功能及几种分类
在日常生活工作中,出现了人脸验证、人脸支付、人脸乘梯、人脸门禁等等常见的应用场景。这说明人脸识别技术已经在门禁安防、金融行业、教育医疗等领域被广泛地应用,人脸识别技术的高速发展与应用同时也出现不少质疑。其中之一就是人脸识别很容易被照片、…...

【Java基础面试三十五】、谈谈你对面向接口编程的理解
文章底部有个人公众号:热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享? 踩过的坑没必要让别人在再踩,自己复盘也能加深记忆。利己利人、所谓双赢。 面试官:谈谈你对面向接口编程的…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

学校招生小程序源码介绍
基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码,专为学校招生场景量身打造,功能实用且操作便捷。 从技术架构来看,ThinkPHP提供稳定可靠的后台服务,FastAdmin加速开发流程,UniApp则保障小程序在多端有良好的兼…...
【生成模型】视频生成论文调研
工作清单 上游应用方向:控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...
Caliper 负载(Workload)详细解析
Caliper 负载(Workload)详细解析 负载(Workload)是 Caliper 性能测试的核心部分,它定义了测试期间要执行的具体合约调用行为和交易模式。下面我将全面深入地讲解负载的各个方面。 一、负载模块基本结构 一个典型的负载模块(如 workload.js)包含以下基本结构: use strict;/…...

rknn toolkit2搭建和推理
安装Miniconda Miniconda - Anaconda Miniconda 选择一个 新的 版本 ,不用和RKNN的python版本保持一致 使用 ./xxx.sh进行安装 下面配置一下载源 # 清华大学源(最常用) conda config --add channels https://mirrors.tuna.tsinghua.edu.cn…...
人工智能 - 在Dify、Coze、n8n、FastGPT和RAGFlow之间做出技术选型
在Dify、Coze、n8n、FastGPT和RAGFlow之间做出技术选型。这些平台各有侧重,适用场景差异显著。下面我将从核心功能定位、典型应用场景、真实体验痛点、选型决策关键点进行拆解,并提供具体场景下的推荐方案。 一、核心功能定位速览 平台核心定位技术栈亮…...
前端高频面试题2:浏览器/计算机网络
本专栏相关链接 前端高频面试题1:HTML/CSS 前端高频面试题2:浏览器/计算机网络 前端高频面试题3:JavaScript 1.什么是强缓存、协商缓存? 强缓存: 当浏览器请求资源时,首先检查本地缓存是否命中。如果命…...

如何把工业通信协议转换成http websocket
1.现状 工业通信协议多数工作在边缘设备上,比如:PLC、IOT盒子等。上层业务系统需要根据不同的工业协议做对应开发,当设备上用的是modbus从站时,采集设备数据需要开发modbus主站;当设备上用的是西门子PN协议时…...

英国云服务器上安装宝塔面板(BT Panel)
在英国云服务器上安装宝塔面板(BT Panel) 是完全可行的,尤其适合需要远程管理Linux服务器、快速部署网站、数据库、FTP、SSL证书等服务的用户。宝塔面板以其可视化操作界面和强大的功能广受国内用户欢迎,虽然官方主要面向中国大陆…...

SFTrack:面向警务无人机的自适应多目标跟踪算法——突破小尺度高速运动目标的追踪瓶颈
【导读】 本文针对无人机(UAV)视频中目标尺寸小、运动快导致的多目标跟踪难题,提出一种更简单高效的方法。核心创新在于从低置信度检测启动跟踪(贴合无人机场景特性),并改进传统外观匹配算法以关联此类检测…...