当前位置: 首页 > news >正文

基于 FFmpeg 的跨平台视频播放器简明教程(八):音画同步

系列文章目录

  1. 基于 FFmpeg 的跨平台视频播放器简明教程(一):FFMPEG + Conan 环境集成
  2. 基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux)
  3. 基于 FFmpeg 的跨平台视频播放器简明教程(三):视频解码
  4. 基于 FFmpeg 的跨平台视频播放器简明教程(四):像素格式与格式转换
  5. 基于 FFmpeg 的跨平台视频播放器简明教程(五):使用 SDL 播放视频
  6. 基于 FFmpeg 的跨平台视频播放器简明教程(六):使用 SDL 播放音频和视频
  7. 基于 FFmpeg 的跨平台视频播放器简明教程(七):使用多线程解码视频和音频

文章目录

  • 系列文章目录
  • 前言
  • I 帧,P 帧,B帧
  • PTS 与 DTS
  • Timebase,时间基
    • Timebase 转换
  • 音画同步
    • 精确地纪录和获取时间
    • 纪录时间的时机
    • 音画同步具体算法
  • 总结
  • 参考


前言

在上篇文章 基于 FFmpeg 的跨平台视频播放器简明教程(七):使用多线程解码视频和音频 中,我们使用多个线程来做不同的事情,让整个播放器更加的模块化。

我们的播放器现在能够同时视频和音频了,但还不够,你会发现视频画面和音频会对不上,这是因为我们没有做音画同步。在现有的代码中,我们每隔 30ms 去播放一帧画面,而音频则是由系统的音频线程来负责驱动调用。这等于说,视频和音频各播各的,画面与声音失去了同步。因此,本章将讨论如何进行音画同步。

本文参考文章来自 An ffmpeg and SDL Tutorial - Tutorial 05: Synching Video 和 An ffmpeg and SDL Tutorial - Tutorial 06: Synching Audio。这个系列对新手较为友好,但 2015 后就不再更新了,以至于文章中的 ffmpeg api 已经被弃用了。幸运的是,有人对该教程的代码进行重写,使用了较新的 api,你可以在 rambodrahmani/ffmpeg-video-player 找到这些代码。

本文的代码在 ffmpeg_video_player_tutorial-my_tutorial05_01_clock.cpp。

I 帧,P 帧,B帧

I 帧(关键帧):也被称为内插帧。每一帧都是独立的图像,也就是说它不依赖于其他任何帧的图像信息。I帧类似于完整的图像,编解码器只需要本帧数据就可以完成解码。在视频播放时,I帧也是快进、快退、拖动的定位点,通常情况下,视频中的第一帧就是I帧。

P 帧(预测帧):P帧的数据包含与前一个I帧或P帧的差别,在编码时仅考虑了前向预测,也就是只与前一帧比较,来找出两帧之间的区别,然后只记录下这个差别。P帧依赖于前面的I帧或P帧。

B 帧(双向预测帧):B帧记录的是该帧与前后帧的差别,即考虑了双向的预测,可以理解为,B帧被插在I、P帧之间,通过前后帧进行预测、记录、解压。B帧依赖于前后的I帧或P帧。

需要所有这些帧的主要原因是为了压缩视频。I帧需要的数据量最多,但是不可能所有帧都为I帧,这样压缩率就很低,所以P帧和B帧诞生了,P帧和B帧只记录和参考帧的差异信息,所以数据量小,压缩率高,缺点是如果参考帧丢失,那么将导致无法解压出真实图像。总的来说,所有这些帧的组合能够使视频数据得以有效的压缩,同时保持视频质量。

PTS 与 DTS

假设现在有 4 帧画面,且没有 B 帧的情况,一种最常见的编码情况是:

I P0 P1 P0

在 “I P P P” 这种编码模式下,视频帧的解码过程较为简单,播放顺序和解码顺序是一样的,因为这种情况下没有B帧存在,P帧只依赖前面的I帧或P帧。

具体来说,解码过程如下:

  1. 首先解码第一帧I帧,I帧是关键帧,可以独立解码,不依赖任何其他帧。
  2. 然后解码第二帧P帧,此P帧只依赖于前一帧,前一帧是I帧,已经解码。
  3. 接着解码第三帧P帧,此P帧也只依赖于前一帧,前一帧是P帧,已经解码。
  4. 最后解码第四帧P帧,此P帧只依赖于前一帧,前一帧是P帧,已经解码。

所以,解码顺序和播放顺序一致,均为:I P P P。

如果有 B 帧呢?一种最常见的编码情况是:

I B0 B1 P0

在 “I B B P” 这种编码模式下,视频帧的解码顺序和播放顺序也是不一样的。B帧依赖前面和后面的帧,因此需要等待后一个参考帧(I或P帧)解析完成后,才能开始 B 帧的解码。

  1. 首先解码第一帧I帧,I帧是关键帧,可以独立解码,不依赖任何其他帧。
  2. 然后解码第四帧P帧,P帧只依赖于之前的I帧或P帧,这一步可以顺利进行。
  3. 解码完第四帧P帧后,才能开始解码第二帧和第三帧B帧。B帧需要依赖前一帧和后一帧,也就是需要依赖第一帧I帧和第四帧P帧。

所以,解码顺序为:I P B B。然而,播放顺序仍然为:I B B P。

因此,类似的,在包含B帧的情况下,解码顺序和播放顺序是有所不同的,这也是为什么在视频解码过程中需要对帧进行重新排序的原因。

我们使用 DTS(Decoding Time Stamp;解码时间戳)来表示解码的顺序,PTS(Presentation Time Stamp;呈现时间戳)来表示播放的顺序。

在 FFmpeg 中,解封装得到 AVPacket 后,每个 AVPacket 都有个成员变量叫做 dts;解码得到 AVFrame 后,每个 AVFrame 都有一个成员变量叫 pts。这两个变量对应着 DTS 和 PTS 的概念。

仍然以上面的 4 帧画面为例,在没有 B 帧的情况,经过编码后,它的码流应该是 Stream: I P0 P1 P2,解码顺序与播放顺序一致,因此:

		PTS: 1  2  3  4DTS: 1  2  3  4Stream: I P0 P1 P2

在有 B 帧的情况,经过编码后码流为 Stream: I P B0 B1,解码顺序是 DTS: 1 2 3 4,播放顺序是 PTS: 1 4 2 3,因此:

		PTS: 1  4  2  3DTS: 1  2  3  4Stream: I  P  B0 B1

Timebase,时间基

DTS 和 PTS 都是时间戳,那么时间的单位是什么?是秒(s)还是毫秒(ms)呢?其实都不是,DTS 和 PTS 是以时间基(timebase)为单位的,时间基是由编码格式决定的,通常它是一个如1/90000这样的分数。

为什么要引入 timebase,而不是固定使用 秒(s)或者毫秒(ms) 呢?引入timebase主要是为了提供更为灵活和精细的时间表示方法。虽然可以选择只用秒或毫秒作为单位,但这可能会在一些特殊的情况下引入不必要的误差或者损失精度。

在多媒体处理中,每种编码格式或者媒体流可能有其特定的帧率或者时钟频率,用一个固定的时间单位如秒或毫秒可能难以精确地表示这些特定的时间点或时长。比如有些视频可能是24帧/秒,有的可能是30帧/秒,有的可能是29.97帧/秒等。

而timebase是一个分数形式的时间单位,它的分子和分母都可以根据具体的编码格式或者媒体流来自由设定,可以精确适配不同的帧率和时钟频率,使得时间表示既可以非常精细,也可以非常灵活。例如,对于一个帧率为29.97fps的视频,我们可以设置timebase为1/30000,这样就能够非常精确地表示每一帧的时间。所以,引入timebase主要是为了在多媒体处理中提供一个既灵活又精确的时间表示方法。

Timebase 不如常见的秒或者毫秒那样直观。对于绝大多数人来说,"1/30000秒"涉及到的分数运算可能会比较难以理解,相比之下,"33.33毫秒"可能会更直观一些。

但是在多媒体编程中,由于需要处理各种不同的编码格式和媒体流,每种可能有其特定的帧率或者时钟频率,这就需要一个更灵活和精细的时间表示方法来适应这些差异,这就是引入timebase的原因。

对于程序来说,处理timebase只不过是一些简单的乘法和除法运算,而对于熟悉多媒体编程的开发者来说,他们也已经习惯了timebase这样的时间表示方法。

在FFmpeg中,timebase是一个分数,表示的是时间单位,用于进行时间戳与实际时间的转换。它由一个分子和一个分母构成。分子通常为1,分母通常等于媒体的帧率或者时钟频率。

  • 分子(numerator):通常为1,在方程中根据需要设定
  • 分母(denominator):通常将帧率或者时钟频率设定为分母
    以数字视频为例,如果一个视频的帧率是每秒30帧,那么它的timebase就是1/30,这个值表示的是每帧的时间长度。换算成秒,就是约0.0333秒。如果你有一个时间戳值,例如100,那么这个时间戳对应的实际时间就是100 * (1/30)= 约3.33秒。

再举一个数字音频的例子,如果音频的采样率是44100Hz,表示每秒钟有44100个音频样本,那么它的timebase就是1/44100。如果你有一个时间戳值,例如22050,那么这个时间戳对应的实际时间就是22050 * (1/44100)= 0.5秒。

需要注意的是,不同的流可能有不同的time_base。例如一个包含视频和音频的媒体文件,它的视频流的time_base可能是1/30(对应30fps的帧率),而音频流的time_base可能是1/44100(对应44100Hz的采样率)。所以在计算时间戳对应的实际时间时,需要使用正确的time_base。

或者换一种理解,在FFmpeg中,timebase以一个分数形式存在,分母表示将1秒拆分成的份额,而分子表示每一帧(或采样,或其他计数单位)占用的份额。例如,如果一个时间基准(timebase)是7/30,那么这意味着1秒被拆分成了30份,每一帧占用了7份。所以,每一帧对应的时间就是7 * 1/30 = 0.2333秒。

在大多数的实际应用中,timebase的分子通常为1,这是因为在大多数媒体格式中,每一帧(或其他的计数单位)通常都对应于一个单位的时间。这样的话,如1/24、1/30、1/60这样的timebase也就表示了每帧的时间,直接是1/24秒、1/30秒、1/60秒。

Timebase 转换

由于视频流和音频流使用的 timebase 是不同的,为了能够进行音画同步,我们首先要做的是将视频帧的 PTS 与音频帧的 PTS 都转换到同一个 Timebase 上,以便我们进行时间上的比较。

在FFmpeg中,AV_TIME_BASE是一个宏定义,其值为1000000。这是作为FFmpeg内部处理时间戳时的一个共同参考,用于统一不同的时间基准。

这个宏定义的原理是以微秒作为基础时间单位。这样,你可以用这个AV_TIME_BASE来将时间戳从微秒转换到FFmpeg内部使用的时间单位,反之亦然。例如,如果你有一个以秒为单位的时间量,你可以乘以AV_TIME_BASE将其转换为基于FFmpeg的时间单位。

例如,1秒 = 1 * AV_TIME_BASE,0.5秒 = 0.5 * AV_TIME_BASE。多了解这一点,可以帮助你更好地理解FFmpeg在处理时间戳时的一些机制和单位转换等问题。

需要注意的是,虽然FFmpeg内部用AV_TIME_BASE作为统一的时间处理标准,但是不同的流还是可能有各自的时间基准,这需要我们在处理时对时间戳进行相应的转换。

假设我们有一个视频流,帧率是30fps,所以视频流的timebase是1/30;我们还有一个音频流,样本率是44100Hz,所以音频流的timebase是1/44100。

假设我们现在有一个视频帧和一个音频样本,它们的PTS(显示时间戳)分别是v_pts和a_pts。首先,我们需要把v_pts和a_pts转换到以AV_TIME_BASE为单位的时间戳,也就是以微秒为单位的时间戳:

v_pts_us = v_pts * (1 / 30) * AV_TIME_BASE 
a_pts_us = a_pts * (1 / 44100) * AV_TIME_BASE

然后,我们就可以直接比较v_pts_us和a_pts_us,这样就能知道音频和视频哪个应该先播放。

如果v_pts_us < a_pts_us,那么这个视频帧应该先播放; 如果a_pts_us < v_pts_us,那么这个音频样本应该先播放。这样,就实现了音画同步。当然这是一个简化的示例,在真实的应用中,可能还需要更复杂的逻辑来处理AV同步,例如处理拖动播放条引起的seek操作,处理音频和视频的解码延迟等。

在 FFmpeg API 中,转换时间戳的函数是 av_rescale_q。该函数通过给定的 AVRational 结构体(表示时间基数的分数结构体)来重新缩放时间戳。你可以像下面这样使用它:

int64_t v_pts_us = av_rescale_q(v_pts, video_stream->time_base, AV_TIME_BASE_Q);

这样 av_rescale_q 就会将 v_pts 从 video_stream->time_base (视频流的时间基数)转换为 AV_TIME_BASE_Q(AV_TIME_BASE的时间基数,值为1/1000000)。同样的,你可以用这个函数转换音频时间戳:

int64_t a_pts_us = av_rescale_q(a_pts, audio_stream->time_base, AV_TIME_BASE_Q);

音画同步

有了前面的知识铺垫,相信你已经对 FFmpeg 中关于时间概念有所了解,这是进行音画同步编程的前提。

就目前的程序而言,视频和音频正在愉快地运行,根本不需要同步。如果一切正常,我们就不必担心这个问题。但你的电脑并不完美,很多视频文件也不完美。因此,我们有三种选择:将音频同步到视频、将视频同步到音频,或者将两者同步到外部时钟(如电脑)。现在,我们介绍的是将视频同步到音频。

精确地纪录和获取时间

我们首先要解决的第一个问题是:如何精确的纪录当前视频/音频流的时间。只有拿到了精确的时间,我们才能知道视频与音频之间的快慢关系。

在 ffmpeg_video_player_tutorial-my_tutorial05_01_clock.cpp 中我们封装了一个叫 Clock 的类,它负责纪录时间。

class Clock {
public:std::atomic<double> pts{0};          // clock base, secondsstd::atomic<double> last_updated{0}; // last pts updated timestd::atomic<double> pre_pts{0};std::atomic<double> pre_frame_delay{0};
};
  • pts,当前流的播放时间,单位 s
  • last_updated,最近 pts 更新的时间,单位 s
  • pre_pts,上次的 pts
  • pre_frame_delay,上次帧的延迟。在某些情况我们将使用这个变量作为当前帧的延迟。

使用下面这些函数来更新/获取时钟:


void setClockAt(Clock &clock, double pts, double time) {clock.pts = pts;clock.last_updated = time;
}void setClock(Clock &clock, double pts) {setClockAt(clock, pts, (double)av_gettime() / 1000000.0);
}double getClock(const Clock &c) const {double time = (double)av_gettime() / 1000000.0;return c.pts + time - c.last_updated;
}

setClock(Clock &clock, double pts) 更新当前 clock 的 pts,同时更新 last_updated 值,last_updated 也就是调用该函数时的系统时间。为什么需要 last_updated ?getClock 给出了答案。

getClock 获取当前流的播放时间。下图解释了 getClock 的计算逻辑

	     pts=30				  pts=60			   pts=90	  		   pts=120
last_updated=1000	 last_updated=1030	  last_updated=1090	  last_updated=1120
Thread0: |--------------------|--------------------|--------------------| |                                             ||                                             |
Thread1: |----------|---------------------------------------------|-----| time=1015                                     time=1110t0=30+(1015-1000)							    t1=90+(1110-1090)=45                                           =110 

想象有两个线程,Thread 0(视频播放线程)和Thread 1。Thread 0定期播放视频帧,并更新视频流的时间。Thread 1在任何时间都可能访问视频流时钟以获取当前播放时间。

在时间点 t0,系统时间 time 为 1015,视频流时钟的 pts=30,last_updated=1000;经过的时间是 (time - last_updated) = 15,这表明更新 pts 距现在过去了15个时间单位,所以当前播放时间是 pts + 15 = 45。

在时间点 t1,系统时间 time 为 1110,视频流时钟的 pts=90,last_updated=1090;经过的时间是 (time - last_updated) = 20,这表明更新 pts 距现在过去了20个时间单位,所以当前播放时间是 pts + 20 = 110。

纪录时间的时机

在代码中,我们使用两个 clock 分别纪录视频和音频的时间

Clock audio_clock_t; // 纪录音频时间
Clock video_clock_t; // 纪录视频时间

那么应该在何时更新时钟的时间呢?

在视频流中,在渲染当前帧时,我们更新 video_clock_t 时钟,具体的,在 videoRefreshTimer 函数使用 ctx->videoSync 进行时钟更新

void videoRefreshTimer(void *userdata) {// ...auto real_delay = ctx->videoSync(video_frame);// ...
}

在音量流中,在播放当前音频帧时,我们更新 audio_clock_t 时钟,具体的,在 audioCallback 函数中使用 ctx->setClock 进行时钟更新:

void audioCallback(void *userdata, Uint8 *stream, int len){// ...play_ctx->setClock(play_ctx->audio_clock_t,frame->pts * av_q2d(audio_stream->time_base));// ...
}

当然,注意我们设置时钟时需要将 pts 转换到以秒(s)为单位,确保视频时钟和音频时钟时间单位一致。

音画同步具体算法

本文主要讲解如何将视频同步到音频,因此,在音频线程中,我们只需更新音频时钟,而无需进行任何同步操作。所有的音频视频同步操作都会在视频播放线程中进行。

简单来说,音视频同步的原理并不复杂。以每秒 25 帧(即每 40 毫秒刷新一帧)的视频为例。如果视频播放速度超过了音频,我们可以适当地延长下一帧的刷新时间,使视频播放速度慢一些,给音频一些“追赶”的时间,比如延长到 45 毫秒刷新一帧。相反,如果音频的播放速度快于视频,我们可以缩短下一帧的刷新时间,使视频播放快一些,以便能跟得上音频的速度。

具体计算视频下一帧刷新时间的代码在 videoSync 函数中,看代码:

int videoSync(AVFrame *video_frame) {auto video_timebase_d = av_q2d(decode_ctx->video_stream->time_base);auto pts = video_frame->pts * video_timebase_d;setClock(video_clock_t, pts);auto pts_delay = pts - video_clock_t.pre_pts;printf("PTS Delay:\t\t\t\t%lf\n", pts_delay);// if the obtained delay is incorrectif (pts_delay <= 0 || pts_delay >= 1.0) {// use the previously calculated delaypts_delay = video_clock_t.pre_frame_delay;}printf("Corrected PTS Delay:\t%f\n", pts_delay);// save delay information for the next timevideo_clock_t.pre_pts = pts;video_clock_t.pre_frame_delay = pts_delay;auto audio_ref_clock = getAudioClock();auto video_clock = getVideoClock();auto diff = video_clock - audio_ref_clock;printf("Audio Ref Clock:\t\t%lf\n", audio_ref_clock);printf("Audio Video Delay:\t\t%lf\n", diff);auto sync_threshold = std::max(pts_delay, AV_SYNC_THRESHOLD);printf("Sync Threshold:\t\t\t%lf\n", sync_threshold);if (fabs(diff) < AV_NOSYNC_THRESHOLD) {if (diff <= -sync_threshold) {pts_delay = 0;} else if (diff >= sync_threshold) {pts_delay = 2 * pts_delay; // [2]}}printf("Corrected PTS delay:\t%lf\n", pts_delay);return (int)std::round(pts_delay * 1000);}
};

上述代码片段是一个函数,用于视频同步。用于使用音频时钟作为参考来调整视频帧的展示时间,从而实现音画同步。

  1. 第一部分:计算出当前视频帧的表现时间 pts。影片序数(pts)是一个表示时间基数的特殊单位,表示在给定的时间标度(time base)下的时间。这里av_q2d 函数将 decode_ctx->video_stream->time_base 转化为双精度浮点数。将 pts设为当前音频时钟的时间。
  2. 第二部分:计算 pts_delay,它表示当前帧和上一帧之间的延迟。如果这被认为是无效的(即小于等于0或大于等于1.0),则会使用先前的帧延迟。这些信息存储起来供下一次使用。
  3. 第三部分:获取音频时钟 audio_ref_clock和视频时钟 video_clock,并计算出二者之间的差值diff,这表示音频和视频帧之间的延迟。
  4. 第四部分:计算出调整阈值 sync_threshold,当差值 diff 在阈值范围内时,不调整 pts_delay,音频和视频同步。若差值(音频和视频延时)过大,则根据其正负,使视频速度加快或放慢,实现音视频同步。
  5. 最后,返回 pts_delay 的值(以毫秒为单位),这个返回值将被用作等待时间,来决定何时展示下一帧。

在视频播放线程中,使用 videoSync 计算出下一帧的刷新等待时间后,使用 scheduleRefresh(ctx, real_delay); 设置一个定时器,让定时器在 real_delay 后发送一个事件让 SDL 去渲染下一帧画面。就这样,我们完成了音画同步。

总结

本文介绍了如何实现播放器的音画同步,首先介绍了 I/P/B 帧的区别,引出了 PTS 和 DTS 的概念;接着,介绍了在 FFmpeg 中的 timebase 的概念,让读者了解 FFmpeg 是如何描述时间的;然后,我们详细的描述了音画同步实施的具体要点,包括如何精确的纪录不同流的当前时间,在什么时间节点来更新时钟,以及音画同步的具体算法。

参考

  • An ffmpeg and SDL Tutorial - Tutorial 05: Synching Video
  • An ffmpeg and SDL Tutorial - Tutorial 06: Synching Audio
  • FFmpeg 音视频(DTS / PTS)
  • ffmpeg_video_player_tutorial-my_tutorial05_01_clock.cpp。

相关文章:

基于 FFmpeg 的跨平台视频播放器简明教程(八):音画同步

系列文章目录 基于 FFmpeg 的跨平台视频播放器简明教程&#xff08;一&#xff09;&#xff1a;FFMPEG Conan 环境集成基于 FFmpeg 的跨平台视频播放器简明教程&#xff08;二&#xff09;&#xff1a;基础知识和解封装&#xff08;demux&#xff09;基于 FFmpeg 的跨平台视频…...

【NLP pytorch】基于BiLSTM-CRF模型医疗数据实体识别实战(项目详解)

基于BiLSTM-CRF模型医疗数据实体识别实战 1数据来源与加载1.1 数据来源1.2 数据类别名称和定义1.3 数据介绍2 模型介绍2 数据预处理2.1 数据读取2.2 数据标注2.3 数据集划分2.4 词表和标签的生成3 Dataset和DataLoader3.1 Dataset3.2 DataLoader4 BiLSTM模型定义5 CRF模型6 模型…...

人工智能原理(1)

*请注意&#xff0c;本文仅供学习使用* 目录 一、人工智能发展 1、孕育期 2、摇篮期 3、形成期 4、发展期&#xff08;1970-1979&#xff09; 5、实用期 6、稳步发展期 二、何为人工智能 1、智能的主要观点 2、智能定义 3、人工智能定义 三、人工智能研究方法 1、…...

预测成真,国内传来三个消息,中国年轻人变了,创新力产品崛起

中国的年轻人真的变了&#xff01; 最近&#xff0c;国内传来三个消息&#xff0c;让外媒的预测成真。 第一&#xff0c;奥迪要开始用国产车的平台了。这里需要说明的是新能源汽车&#xff0c;奥迪也曾多次公开表示&#xff0c;承认了当前中国新能源汽车核心技术上的领先。 第…...

维深(Wellsenn):2023中国消费端VR内容开发商调研报告(附下载

关于报告的所有内容&#xff0c;公众【营销人星球】获取下载查看 核心观点 国内互联网大厂商入局VR&#xff0c;字节跳动、网易表态明确。字节跳动2021年收购国内头部VR硬件厂商PICO后&#xff0c;加速构建VR内容生态&#xff0c;2021年 成立海南创见未来当前已推出VR视频应用…...

redis事务管理详解

事务管理 事务管理乐观锁与悲观锁watch命令实现乐观锁watch命令示例 事务管理 Redis 提供了事务管理功能&#xff0c;可以通过 Redis 的 MULTI、EXEC、WATCH 和 DISCARD 命令来实现。 开启事务&#xff1a; 使用 MULTI 命令开始一个事务&#xff0c;表示接下来执行的命令都属于…...

国产低功耗蓝牙HS6621CxC/6621Px系列支持Find My网络功能方案芯片

目录 什么是“Find My“&#xff1f;HS6621系列简介 什么是“Find My“&#xff1f; “Find My”是苹果公司于19年前推出的针对失物追踪&#xff0c;Find My iPhone&#xff08;查找我的iPhone&#xff09;和Find My Friends&#xff08;查找朋友&#xff09;的结合体应用。为…...

【openGauss】分区表的介绍与使用

一、openGauss分区表介绍 在openGauss中&#xff0c;数据分区是在一个节点内部对数据按照用户指定的策略做进一步的水平分表&#xff0c;将表中的数据按照指定方式划分为多个互不重叠的部分。 对于大多数用户使用场景&#xff0c;分区表和普通表相比具有以下优点&#xff1a; …...

代码随想录算法训练营day57

文章目录 Day57回文子串题目思路代码 最长回文子序列题目思路代码 Day57 回文子串 647. 回文子串 - 力扣&#xff08;LeetCode&#xff09; 题目 给你一个字符串 s &#xff0c;请你统计并返回这个字符串中 回文子串 的数目。 回文字符串 是正着读和倒过来读一样的字符串。…...

【基础类】—前后端通信类系统性学习

一、什么是同源策略及限制 同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。源&#xff1a;协议、域名和端口&#xff0c; 默认端口是80 三者有一个不同&#xff0c;即源不同&#xff0c;就是跨域 ht…...

vite项目中使用@代表根路径

1.配置vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue import path from pathexport default defineConfig({plugins: [vue()],resolve: {alias:{: path.resolve(__dirname, src) }} })2.报错path和__dirname 找不到模块“path”或其相…...

冶金化工操作VR虚拟仿真实验软件提高员工们协同作业的配合度

对于高风险行业来说&#xff0c;开展安全教育培训是企业的重点工作&#xff0c;传统培训逐渐跟不上时代变化和工人需求&#xff0c;冶金安全VR模拟仿真培训系统作为一种新型的教育和培训工具&#xff0c;借助VR虚拟现实技术为冶金行业的工人提供一个安全、高效的培训环境。 冶金…...

SQL Server数据库 -- 索引与视图

文章目录 一、索引 聚集索引非聚集索引二、视图三、自定义函数 标量函数表值函数四、游标五、总结 前言 在学习完创建库表、查询等知识点后&#xff0c;为了更加方便优化数据库的存储和内容&#xff0c;我们需要学习一系列的方法例如索引与视图等等&#xff0c;从而使我们更加…...

2023 java web面试秘籍

目录 第一章&#xff1a;Java Web基础知识1.介绍3.Java Web基本概念 4.常见面试问题第二章&#xff1a;Java Web核心概念和技术1.介绍3.Servlet和JSP4.Web安全5.常见面试问题 第三章&#xff1a;Java Web高级概念和技术1.介绍3.Spring框架4.安全性5.常见面试问题 第四章&#x…...

2023-08-05力扣今日二题

链接&#xff1a; 剑指 Offer 18. 删除链表的节点 题意&#xff1a; 如题 解&#xff1a; 基础链表操作 实际代码&#xff1a; #include<iostream> using namespace std; struct ListNode {int val;ListNode *next;ListNode(int x) : val(x), next(NULL) {} }; Li…...

stl_list类(使用+实现)(C++)

list 一、list-简单介绍二、list的常用接口1.常见构造2.iterator的使用3.Capacity和Element access4.Modifiers5.list的迭代器失效 三、list实现四、vector 和 list 对比五、迭代器1.迭代器的实现2.迭代器的分类&#xff08;按照功能分类&#xff09;3.反向迭代器(1)、包装逻辑…...

利用hfish反控境外攻击源主机

导师给了7个网络安全课题选题&#xff0c;本想和他聊了下思路&#xff0c;他一挥手让我先做出点东西再来聊就把我打发走了…… 正好前段时间阿里云到校做推广&#xff0c;用优惠卷薅了一台云服务器&#xff0c;装了hfish先看下情况 没想到才装上没两天数据库就爆了&#xff0…...

4、Rocketmq之存储原理

CommitLog ~ MappedFileQueue ~ MappedFile集合...

在线原型设计工具有好用的吗?就是这10个

随着设计工作的不断发展&#xff0c;原型设计在设计工作中越来越重要&#xff0c;而在线原型设计工具在减轻了设计师工作负担的同时也提高了设计师的工作效率&#xff0c;今天本文将为大家推荐10个能在线使用的原型设计工具&#xff0c;一起来看看吧&#xff01; 1、即时设计 …...

Vc - Qt - QPainter translate

QPainter的translate()函数是用来对绘制坐标系统进行平移操作的方法。它可以将绘制的原点&#xff08;坐标轴的起始点&#xff09;在水平和垂直方向上进行平移。以下是一个使用QPainter的translate()方法进行坐标平移的示例代码&#xff1a; QPainter painter(this);// 绘制一个…...

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周&#xff0c;有很多同学在写期末Java web作业时&#xff0c;运行tomcat出现乱码问题&#xff0c;经过多次解决与研究&#xff0c;我做了如下整理&#xff1a; 原因&#xff1a; IDEA本身编码与tomcat的编码与Windows编码不同导致&#xff0c;Windows 系统控制台…...

【Linux】shell脚本忽略错误继续执行

在 shell 脚本中&#xff0c;可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行&#xff0c;可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令&#xff0c;并忽略错误 rm somefile…...

golang循环变量捕获问题​​

在 Go 语言中&#xff0c;当在循环中启动协程&#xff08;goroutine&#xff09;时&#xff0c;如果在协程闭包中直接引用循环变量&#xff0c;可能会遇到一个常见的陷阱 - ​​循环变量捕获问题​​。让我详细解释一下&#xff1a; 问题背景 看这个代码片段&#xff1a; fo…...

CMake基础:构建流程详解

目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...

Leetcode 3577. Count the Number of Computer Unlocking Permutations

Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接&#xff1a;3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯&#xff0c;要想要能够将所有的电脑解锁&#x…...

Python爬虫(一):爬虫伪装

一、网站防爬机制概述 在当今互联网环境中&#xff0c;具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类&#xff1a; 身份验证机制&#xff1a;直接将未经授权的爬虫阻挡在外反爬技术体系&#xff1a;通过各种技术手段增加爬虫获取数据的难度…...

Netty从入门到进阶(二)

二、Netty入门 1. 概述 1.1 Netty是什么 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty是一个异步的、基于事件驱动的网络应用框架&#xff0c;用于…...

GitFlow 工作模式(详解)

今天再学项目的过程中遇到使用gitflow模式管理代码&#xff0c;因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存&#xff0c;无论是github还是gittee&#xff0c;都是一种基于git去保存代码的形式&#xff0c;这样保存代码…...

Git常用命令完全指南:从入门到精通

Git常用命令完全指南&#xff1a;从入门到精通 一、基础配置命令 1. 用户信息配置 # 设置全局用户名 git config --global user.name "你的名字"# 设置全局邮箱 git config --global user.email "你的邮箱example.com"# 查看所有配置 git config --list…...

es6+和css3新增的特性有哪些

一&#xff1a;ECMAScript 新特性&#xff08;ES6&#xff09; ES6 (2015) - 革命性更新 1&#xff0c;记住的方法&#xff0c;从一个方法里面用到了哪些技术 1&#xff0c;let /const块级作用域声明2&#xff0c;**默认参数**&#xff1a;函数参数可以设置默认值。3&#x…...