本章主要介绍如何使用FFmpeg来将一个音频文件和一个视频文件合成一个MP4文件,以及在这个过程中我们如何对编码过程进行封装以及sample_rate 重采样的过程(由于提供的音频文件的编码类型为S16,所以我们需要转化为MP4支持的FLTP浮点类型)。

Muxer

首先我们来介绍如何封装MP4的封装器,就是我们将视频流和音频流输入封装器,封装器输出MP4文件。下面是封装器的头文件,里面有一些封装器必要的成员函数。

#ifndef MUXER_H #define MUXER_H #include <iostream> 
// 在C++文件中中导入C库需要使用extern关键字 
extern "C" { #include "libavcodec/avcodec.h" #include "libavformat/avformat.h" 
} class Muxer { public: Muxer(); ~Muxer(); // 初始化 int Init(const char* url); // 释放资源 void DeInit(); // 将一条视频/音频流添加到封装器 int AddStream(AVCodecContext* codec_ctx); // 将头发送到封装器中 int SendHeader(); // 将数据帧发送到封装器中 int SendPacket(AVPacket* packet); // 将尾发送到封装器中 int SendTrailer(); // 打开输入源url int Open(); private: // format上下文 AVFormatContext* fmt_ctx_ = NULL; // 输入源url,这里可能是url也可以是一个文件路径 std::string url_ = ""; // 视频流复用器上下文 AVCodecContext* vid_codec_ctx_ = NULL; AVCodecContext* aud_codec_ctx_ = NULL; // 视频流 AVStream* vid_st_ = NULL; AVStream* aud_st_ = NULL; // 有没有对应的流 int video_index_ = -1; int audio_index_ = -1; 
}; 
#endif // MUXER_H

接下来是封装器的具体时间,我们暂时只实现最基础的功能:

int Muxer::Init(const char *url) { int ret = avformat_alloc_output_context2(&fmt_ctx_, NULL, NULL,url); if(ret < 0) { char errbuf[1024] = {0}; av_strerror(ret, errbuf, sizeof(errbuf) - 1);printf("avformat_alloc_output_context2 failed:%s\n", errbuf); return -1; } url_ = url; return 0; 
}

由于这是第一个具体的函数实现,所以我就放上了获取错误的函数,后面我就不说了。avformat_alloc_output_context2用来初始化输出格式上下文。最后是将传入的url参数赋值给类成员。

void Muxer::DeInit() { if(fmt_ctx_) { avformat_close_input(&fmt_ctx_); } url_ = ""; aud_codec_ctx_ = NULL; aud_stream_ = NULL; audio_index_ = -1; vid_codec_ctx_ = NULL; vid_stream_ = NULL; video_index_ = -1; 
}

这里主要的功能就是关闭输出格式上下文,然后将其他的类成员设置为初始状态。

int Muxer::AddStream(AVCodecContext *codec_ctx) { if(!fmt_ctx_) { printf("fmt ctx is NULL\n"); return -1; } if(!codec_ctx) { printf("codec ctx is NULL\n"); return -1; } AVStream *st = avformat_new_stream(fmt_ctx_, NULL); if(!st) { printf("avformat_new_stream failed\n"); return -1; } // st->codecpar->codec_tag = 0; // 从编码器上下文复制 avcodec_parameters_from_context(st->codecpar, codec_ctx);av_dump_format(fmt_ctx_, 0, url_.c_str(), 1); // 判断当前的是视频流还是音频流 if(codec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) { aud_codec_ctx_ = codec_ctx; aud_stream_ = st; audio_index_ = st->index; } else if(codec_ctx->codec_type == AVMEDIA_TYPE_VIDEO) { vid_codec_ctx_ = codec_ctx; vid_stream_ = st; video_index_ = st->index; } return 0; 
}
  • avformat_new_stream的第二个参数通常是NULL,自动分配流。但是如果是已知编码器,可以直接传入AVCodec*。
  • 在判断当前传入的流的种类后,初始化对应的类成员。
int Muxer::SendHeader()
{if(!fmt_ctx_) {printf("fmt ctx is NULL\n");return -1;}/** 这里其实可以选择封装参数(如mp4的faststart)* AVDictionary* option = NULL;* av_dict_set(&options, "movflags", "faststart", 0);*/int ret = avformat_write_header(fmt_ctx_, NULL);if (ret != 0) {char errbuf[1024] = {0};av_strerror(ret, errbuf, sizeof(errbuf) - 1);printf("avformat_write_header failed:%s\n", errbuf);return -1;}return 0;
}
  • 需要注意的是这个函数必须在所有流添加完成后调用,因为avformat_write_header必须在所有流都添加完毕后调用.
  • 如果后续还要修改参数,需要在调用前完成。
int Muxer::SendPacket(AVPacket *packet)
{int stream_index = packet->stream_index;if (!packet || packet->size <=0 || packet->data) {printf("packet is null\n");if (packet) {av_packet_free(&packet);}return -1;}AVRational src_time_base; // 编码后的包AVRational dst_time_base; // mp4输出文件对应流的time_baseif (vid_st_ && vid_codec_ctx_ && stream_index == video_index_) {src_time_base = vid_codec_ctx_->time_base;dst_time_base = vid_st_->time_base;}else if (aud_st_ && aud_codec_ctx_ && stream_index == audio_index_) {src_time_base = aud_codec_ctx_->time_base;dst_time_base = aud_st_->time_base;}packet->pts = av_rescale_q(packet->pts, src_time_base, dst_time_base);packet->dts = av_rescale_q(packet->dts, src_time_base, dst_time_base);packet->duration = av_rescale_q(packet->duration, src_time_base, dst_time_base);int ret = 0;ret = av_interleaved_write_frame(fmt_ctx_, packet);// ret = av_write_frame(fmt_ctx_, packet);av_packet_free(&packet);if (ret == 0) {return 0;}else {char errbuf[1024] = {0};av_strerror(ret, errbuf, sizeof(errbuf) - 1);printf("avformat_write_header failed:%s\n", errbuf);return -1;}
}
  • av_interleaved_write_frame和av_write_frame的功能其实差不多,不过前者会有一些缓存,而后者是直接写入到文件。前者的缓存目的是根据pts对帧进行排序。
  • 这里比较重要的就是时间基的转化问题。为什么要进行时间基转化呢:不同的音视频流都有自己的时间基,也就是fps,但是当我们合成的时候,就要统一这些时间基,把他们统一到新编码格式上。
int Muxer::SendTrailer()
{if(!fmt_ctx_) {printf("fmt ctx is NULL\n");return -1;}// 写入尾部信息int ret = av_write_trailer(fmt_ctx_);if (ret != 0) {char errbuf[1024] = {0};av_strerror(ret, errbuf, sizeof(errbuf) - 1);printf("av_write_trailer failed:%s\n", errbuf);return -1;}return 0;
}
  • 这里的主要函数是av_write_trailer,它做了以下几件事:
    • 写入文件尾部信息(如MP4,MKV中的索引表);
    • 刷新内部缓冲区;
    • 调用每个AVStream的codec相关清理代码;
    • 确保生成的文件可被播放器正确读取;
    • 释放部分资源(这里还需要手动关闭avio_close()和avformat_free_context())

AudioEncoder

接下来是音频编码器,用来编码输入的音频流数据。

#ifndef AUDIOENCODER_H
#define AUDIOENCODER_Hextern "C"
{
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
}
class AudioEncoder
{
public:AudioEncoder();~AudioEncoder();// 这里使用的AAC音频流,如果要适配更多的流,可以自己添加int InitAAC(int channels, int sample_rate, int bit_rate);
//    int InitMP3(/*int channels, int sample_rate, int bit_rate*/);void DeInit();  // 释放资源AVPacket *Encode(AVFrame *farme, int stream_index, int64_t pts, int64_t time_base);int GetFrameSize(); // 获取一帧数据 每个通道需要多少个采样点int GetSampleFormat();  // 编码器需要的采样格式AVCodecContext *GetCodecContext();int GetChannels();int GetSampleRate();
private:// 默认值int channels_ = 2; // 双声道int sample_rate_ = 44100; // 采样率int bit_rate_ = 128*1024; // 比特率int64_t pts_ = 0; // 显示时间:显示的时间  dts是解码时间:开始解码当前帧的时间AVCodecContext * codec_ctx_ = NULL;
};#endif // AUDIOENCODER_H

这边的音频编码器只封装了AAC的音频流,并且设置了一些原始数据,后面可以再拓展。

int AudioEncoder::InitAAC(int channels, int sample_rate, int bit_rate)
{// 初始化当前参数channels_ = channels;sample_rate_ = sample_rate;bit_rate_ = bit_rate;// 根据ID寻找编码器AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_AAC);if(!codec) {printf("avcodec_find_encoder AV_CODEC_ID_AAC failed\n");return -1;}// 为编码器分配上下文codec_ctx_ = avcodec_alloc_context3(codec);if(!codec_ctx_) {printf("avcodec_alloc_context3 AV_CODEC_ID_AAC failed\n");return -1;}// 配置编码器上下文参数codec_ctx_->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // 取消AAC的adts头codec_ctx_->sample_rate = sample_rate;codec_ctx_->bit_rate = bit_rate;// 这是新的写法,这个函数会配置nb_channels和channel_layoutav_channel_layout_default(&codec_ctx_->ch_layout, channels);// 编码采样格式codec_ctx_->sample_fmt = AV_SAMPLE_FMT_FLTP; // 平面浮点数int ret = avcodec_open2(codec_ctx_, NULL, NULL);if (ret != 0) {char errbuf[1024] = {0};av_strerror(ret, errbuf, sizeof(errbuf) - 1);printf("avcodec_open2 failed:%s\n", errbuf);return -1;}printf("InitAAC success\n");return 0;
  • 这里需要注意的是av_channel_layout_default,这是新的写法,之前需要单独分来对channel相关变量进行赋值。
// 这就是编码函数了
AVPacket *AudioEncoder::Encode(AVFrame *frame, int stream_index, int64_t pts, int64_t time_base)
{if (!codec_ctx_) {printf("codec_ctx_ null\n");return NULL;}// 时间基转换pts = av_rescale_q(pts, AVRational{1, (int)time_base}, codec_ctx_->time_base);if (frame) {frame->pts = pts;}int ret = avcodec_send_frame(codec_ctx_, frame);if (ret != 0) {char errbuf[1024] = {0};av_strerror(ret, errbuf, sizeof (errbuf) - 1);printf("avcodec_send_frame failed:%s\n", errbuf);return NULL;}AVPacket*  packet = av_packet_alloc();ret = avcodec_receive_packet(codec_ctx_, packet);if (ret != 0) {char errbuf[1024] = {0};av_strerror(ret, errbuf, sizeof (errbuf) - 1);printf("avcodec_send_frame failed:%s\n", errbuf);return NULL;}packet->stream_index = stream_index;return packet;
}
  • 设置好编码器参数后就是编码了,将数据帧一个一个编码为packet,最后记得设置一下index返回
  • 每一条音频和视频都是分开的,有自己的index(编号)。

Main

主函数的内容还是比较多的,由于涉及到一些常规的操作,比如打开文件等,这里就不都解释了,我们主要看一些比较重要的需要记录的地方。

  • 首先来看一些宏定义,他们定义了我们转化视频的一些参数。
// 视频的宽和高
#define YUV_WIDTH 720
#define YUV_HEIGHT 576
#define YUV_FPS 25
// 比特率
#define VIDEO_BIT_RATE 512*1024
// 采样率
#define PCM_SAMPLE_RATE 44100
#define PCM_CHANNELS 2#define AUDIO_BIT_RATE 128*1024// 基准时间 本例子中的时间是5s 也就是下面的时间*5
#define AUDIO_TIME_BASE 1000000
#define VIDEO_TIME_BASE 1000000
  • 接着这里有一个计算YUV420P编码格式帧大小的地方。
  int y_frame_size = yuv_width * yuv_height;int u_frame_size = yuv_width * yuv_height / 4;int v_frame_size = yuv_width * yuv_height / 4;int yuv_frame_size = y_frame_size * u_frame_size * v_frame_size;
/*
* 可以看到这里YUV三个方向的size计算方式不同
# 这是因为在YUV420中,UV方向的比特率都是Y方向的1/4
*/
  • 最后看一下主循环
while(1) {if (audio_finish && video_finish)break;printf("apts:%0.0lf, vpts:%0.0lf\n", audio_pts/1000, video_pts/1000);if ((video_finish != 1 && audio_pts > video_pts)|| (video_finish != 1 && audio_finish ==1)) {read_len = fread(yuv_frame_buf, 1, yuv_frame_size, in_yuv_fd);// 文件中的视频帧内容已经消耗完if (read_len < yuv_frame_size) {video_finish = 1;printf("fread yuv_frame_buf finish\n");}if (video_finish != 1) {packet = video_encoder.Encode(yuv_frame_buf, yuv_frame_size, video_index,video_pts, video_time_base);}else {// 这里有一个冲刷编码器的过程packet = video_encoder.Encode(NULL, 0, video_index,video_pts, video_time_base);}// 叠加ptsvideo_pts += video_frame_duration; // 叠加ptsif (packet) {mp4_muxer.SendPacket(packet);}}else if (audio_finish != 1) {read_len = fread(pcm_frame_buf, 1, pcm_frame_size, in_pcm_fd);if (read_len < pcm_frame_size) {audio_finish = 1;printf("fread pcm_frame_buf finish\n");}if (audio_finish != 1) {AVFrame* fltp_frame = AllocFltpPcmFrame(pcm_channels, audio_encoder.GetFrameSize());ret = audio_resampler.ResampleFromS16ToFLTP(pcm_frame_buf, fltp_frame);packet = audio_encoder.Encode(fltp_frame, audio_index,audio_pts, audio_time_base);FreePcmFrame(fltp_frame);}else {packet = audio_encoder.Encode(NULL, audio_index,audio_pts, audio_time_base);}audio_pts += audio_frame_duration;if (packet) {mp4_muxer.SendPacket(packet);}}}

下面是主函数的本体:

#include <iostream>
#include "audioencoder.h"
#include "videoencoder.h"
#include "muxer.h"
#include "audioresampler.h"using namespace std;#define YUV_WIDTH 720
#define YUV_HEIGHT 576
#define YUV_FPS 25#define VIDEO_BIT_RATE 512*1024#define PCM_SAMPLE_RATE 44100
#define PCM_CHANNELS 2#define AUDIO_BIT_RATE 128*1024// 基准时间 本例子中的时间是5s 也就是下面的时间*5
#define AUDIO_TIME_BASE 1000000
#define VIDEO_TIME_BASE 1000000int main(int argc, char* argv[])
{if (argc != 4) {printf("usage -> exe in.yuv in.pcm out.mp4");return -1;}const char* in_yuv_name = argv[1];const char* in_pcm_name = argv[2];const char* out_mp4_name = argv[3];FILE* in_yuv_fd = NULL;FILE* in_pcm_fd = NULL;in_yuv_fd = fopen(in_yuv_name, "rb");if (!in_yuv_fd) {printf("Failed to open %s file\n", in_yuv_fd);return -1;}in_pcm_fd = fopen(in_pcm_name, "rb");if (!in_pcm_fd) {printf("Failed to open %s file\n", in_pcm_fd);return -1;}int ret = 0;// 初始化编码器,包括视频,音频编码器int yuv_width = YUV_WIDTH;int yuv_height = YUV_HEIGHT;int yuv_fps = YUV_FPS;int video_bit_rate = VIDEO_BIT_RATE;VideoEncoder video_encoder;ret = video_encoder.InitH264(yuv_width, yuv_height, yuv_fps, video_bit_rate);if (ret < 0) {printf("video_encoder.InitH264 failed\n");return -1;}int y_frame_size = yuv_width * yuv_height;int u_frame_size = yuv_width * yuv_height / 4;int v_frame_size = yuv_width * yuv_height / 4;int yuv_frame_size = y_frame_size * u_frame_size * v_frame_size;uint8_t* yuv_frame_buf = (uint8_t*)malloc(yuv_frame_size);if (!yuv_frame_buf) {printf("malloc(yuv_frame_size\n");return -1;}int pcm_channels = PCM_CHANNELS;int pcm_sample_rate = PCM_SAMPLE_RATE;int pcm_sample_format = AV_SAMPLE_FMT_FLTP;int audio_bit_rate = AUDIO_BIT_RATE;int pcm_frame_size = av_get_bytes_per_sample((AVSampleFormat)pcm_sample_format);AudioEncoder audio_encoder;ret = audio_encoder.InitAAC(pcm_channels, pcm_sample_rate, audio_bit_rate);if (ret < 0) {printf("audio_encoder.InitAAC failed\n");return -1;}uint8_t* pcm_frame_buf = (uint8_t*)malloc(pcm_frame_size);// 这里需要进行一下重采样 将 S16 转化为 FLTPAudioResampler audio_resampler;ret = audio_resampler.InitFromS16ToFLTP(pcm_channels, pcm_sample_rate,audio_encoder.GetChannels(), audio_encoder.GetSampleFormat());if (ret < 0) {printf("audio_resampler.InitFromS16ToFLTP failed\n");return -1;}Muxer mp4_muxer;ret = mp4_muxer.Init(out_mp4_name);if (ret < 0) {printf("mp4_muxer.Init failed\n");return -1;}// 将流添加到封装器中ret = mp4_muxer.AddStream(video_encoder.GetCodecContext());if (ret < 0) {printf("mp4_muxer.AddStream video failed\n");return -1;}ret = mp4_muxer.AddStream(audio_encoder.GetCodecContext());if (ret < 0) {printf("mp4_muxer.AddStream video failed\n");return -1;}ret = mp4_muxer.Open();if (ret < 0) {return -1;}ret = mp4_muxer.SendHeader();if (ret < 0) {return -1;}int64_t audio_time_base = AUDIO_TIME_BASE;int64_t video_time_base = VIDEO_TIME_BASE;double audio_pts = 0;double video_pts = 0;double audio_frame_duration = 1.0 * audio_encoder.GetFrameSize()/pcm_sample_rate*audio_time_base;double video_frame_duration = 1.0/yuv_fps * video_time_base;int audio_finish = 0;int video_finish = 0;size_t read_len = 0;AVPacket* packet = NULL;int audio_index = mp4_muxer.GetAudioStreamIndex();int video_index = mp4_muxer.GetVideoStreamIndex();while(1) {if (audio_finish && video_finish)break;printf("apts:%0.0lf, vpts:%0.0lf\n", audio_pts/1000, video_pts/1000);if ((video_finish != 1 && audio_pts > video_pts)|| (video_finish != 1 && audio_finish ==1)) {read_len = fread(yuv_frame_buf, 1, yuv_frame_size, in_yuv_fd);if (read_len < yuv_frame_size) {video_finish = 1;printf("fread yuv_frame_buf finish\n");}if (video_finish != 1) {packet = video_encoder.Encode(yuv_frame_buf, yuv_frame_size, video_index,video_pts, video_time_base);}else {packet = video_encoder.Encode(NULL, 0, video_index,video_pts, video_time_base);}video_pts += video_frame_duration; // 叠加ptsif (packet) {mp4_muxer.SendPacket(packet);}}else if (audio_finish != 1) {read_len = fread(pcm_frame_buf, 1, pcm_frame_size, in_pcm_fd);if (read_len < pcm_frame_size) {audio_finish = 1;printf("fread pcm_frame_buf finish\n");}if (audio_finish != 1) {AVFrame* fltp_frame = AllocFltpPcmFrame(pcm_channels, audio_encoder.GetFrameSize());ret = audio_resampler.ResampleFromS16ToFLTP(pcm_frame_buf, fltp_frame);packet = audio_encoder.Encode(fltp_frame, audio_index,audio_pts, audio_time_base);FreePcmFrame(fltp_frame);}else {packet = audio_encoder.Encode(NULL, audio_index,audio_pts, audio_time_base);}audio_pts += audio_frame_duration;if (packet) {mp4_muxer.SendPacket(packet);}}}ret = mp4_muxer.SendTrailer();if (ret < 0) {printf("mp4_muxer.SendTrailer failed\n");}printf("write mp4 finish\n");if (yuv_frame_buf)free(yuv_frame_buf);if (pcm_frame_buf)free(pcm_frame_buf);if (in_yuv_fd)fclose(in_yuv_fd);if (in_pcm_fd)fclose(in_pcm_fd);return 0;
}

参考资料:https://github.com/0voice

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

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

相关文章

第十九章 使用LAMP架构部署动态网站环境

第十九章 使用LAMP架构部署动态网站环境 文章目录第十九章 使用LAMP架构部署动态网站环境一、安装Httpd服务1、安装httpd服务2、启动httpd服务3、设置允许通过防火墙4、验证http服务是否成功二、安装Mariadb服务1、安装Mariadb服务2、启动Mariadb服务三、安装PHP服务1、列出可用…

Selenium应用中的核心JavaScript操作技巧

Selenium是一款强大的浏览器自动化测试工具&#xff0c;其操作浏览器的能力部分来自于其内嵌的JavaScript执行引擎。这使得Selenium不仅能够模拟用户在浏览器中的各种操作&#xff0c;还能执行复杂的JavaScript脚本&#xff0c;以实现更为精细的控制。本文将探讨如何通过Seleni…

《Linux 基础指令实战:新手入门的命令行操作核心教程(第一篇)》

前引&#xff1a;当你第一次面对 Linux 系统中那片闪烁着光标、只有黑白字符的终端界面时&#xff0c;或许会和很多初学者一样感到些许茫然&#xff1a;这些由字母和符号组成的 “指令” 究竟该如何输入&#xff1f;它们又能完成哪些神奇的操作&#xff1f;其实&#xff0c;Lin…

03.【Linux系统编程】基础开发工具1(yum软件安装、vim编辑器、编辑器gcc/g++)

目录 1. 软件包管理器 1.1 什么是软件包 1.2 Linux软件生态 1.3 yum具体操作 1.3.1 查看软件包 1.3.2 安装软件 1.3.3 卸载软件 1.3.4 注意事项(测试网络) 1.3.5 yum指令集总结 1.4 yum源目录、安装源 2. Vim编辑器的使用 2.1 Linux编辑器-vim使用 2.2 vim的基本概…

3DMAX自动材质开关插件AutoMaterial安装和使用方法

3DMAX自动材质开关AutoMaterial&#xff0c;是一个3dMax脚本插件&#xff0c;它根据材质编辑器中当前活动的材质自动将材质应用于3dMax中新创建的对象&#xff0c;也适用于您复制的没有材质的对象。它作为一个开关&#xff0c;可以绑定到按钮或菜单来打开和关闭它。该工具的创建…

Linux内核调优实战指南

内核调优通常通过修改内核运行时参数来实现&#xff0c;这些参数的配置文件是 Linux 系统中核心的性能调整点。 内核调优配置文件名称 /etc/sysctl.conf: 这是最传统和主要的内核参数配置文件。系统启动时或手动执行 sysctl -p 命令时会读取并应用其中的设置。/etc/sysctl.d/*.…

Java基础常见知识点

Java 中 和 equals() 的区别详解_java中与equals的区别及理解-CSDN博客https://blog.csdn.net/m0_64432106/article/details/142026852深入理解Java中方法的参数传递机制 - 悟小天 - 博客园https://www.cnblogs.com/sum-41/p/10799555.html浮点型精度是什么意思&#xff1f;为…

OD C卷 -【高效货运】

文章目录高效货运高效货运 货车的额定载货量为wt&#xff1b;货物A单件重量为wa&#xff0c;单件运费利润为pa;货物B单件重量wb&#xff0c;单件运费利润为pb;每次出车必须包含A、B货物&#xff0c;且单件货物都不可分割&#xff0c;总重量达到额定的载货量wt;每次出车能够获取…

手动解压并读取geo 文件 series_matrix_table_begin series_matrix_table_end之间的数据

手动解压并读取geo 文件 series_matrix_table_begin series_matrix_table_end之间的数据 1. 手动解压并读取文件内容 file_path <- “K:/download/geo/raw_data/GEO/GSE32967_series_matrix.txt.gz” 使用latin1编码读取文件所有行 con <- gzfile(file_path, “r”) all_…

主板硬件研发基础--DP/DP++

现在的主板大多数使用的是比DP功能更加强大的DP++。 DisplayPort++(DP++)是 DisplayPort 技术的增强版,旨在提升与多种视频接口的兼容性和连接性能。以下是关于它的详细介绍: 功能特性 多协议兼容:DP++ 接口不仅支持 DisplayPort 标准的信号传输,还可以通过内部的转换电…

科技行业新闻发布平台哪家好?多场景推广专业方案服务商推荐

面对海量得新闻发布平台和碎片化的传播场景&#xff0c;如何精准选择推广方案无疑是企业主面临的一大难题&#xff0c;对于技术迭代迅速的科技行业更是如此。针对复杂的市场环境&#xff0c;一些专业的新闻发布平台往往能够针对性地给出营销方案&#xff0c;并提供一定技术支持…

SystemVerilog 学习之SystemVerilog简介

SystemVerilog简介SystemVerilog是一种硬件描述和验证语言&#xff08;HDVL&#xff09;&#xff0c;由Accellera开发并于2005年成为IEEE标准&#xff08;IEEE 1800&#xff09;。它在传统Verilog基础上扩展了高级验证和设计功能&#xff0c;广泛应用于数字电路设计、验证及系统…

JavaWeb--day3--AjaxElement路由打包部署

&#xff08;以下内容全部来自上述课程及课件&#xff09; Ajax &#xff08;此章节纯粹演示&#xff0c;因服务器端url链接失效&#xff0c;所以无法实战&#xff09; 1. 同步与异步 同步&#xff1a; 浏览器页面在发送请求给服务器&#xff0c;在服务器处理请求的过程…

IMF GDP的bug

IMF GDP 数据底子是官方数字&#xff0c;基本是沿用官方的&#xff0c;虽然经过修订或估算&#xff0c;存在4大“bug”&#xff1a;1. 依赖官方上报&#xff0c;真实性不保证2. PPP GDP 虚高&#xff0c;居民实际消费力低很多ppp gdp高&#xff0c;甚至gdp高的地方&#xff0c;…

第2篇:数据持久化实战

在上一篇中&#xff0c;我们构建了一个基于内存存储的食谱助手。说实话&#xff0c;内存存储虽然简单&#xff0c;但有个致命问题&#xff1a;程序一重启&#xff0c;数据就全没了。 所以这篇我们要解决数据持久化的问题&#xff0c;将食谱助手从内存存储升级到SQLite数据库。 …

Java推荐系统与机器学习实战案例

基于Java的推荐系统与机器学习实例 以下是一些基于Java的推荐系统与机器学习实例的参考方向及开源项目,涵盖协同过滤、矩阵分解、深度学习等常见方法。内容根据实际项目和技术文档整理,可直接用于学习或开发。 协同过滤实现 用户-物品评分预测 使用Apache Mahout的基于用户…

AI生成内容检测的综合方法论与技术路径

一、AI内容检测技术的分类与原理当前AI内容检测技术主要分为四大类&#xff0c;每类都有其独特的原理和应用场景&#xff1a;1. 基于语言特征分析的检测方法这类方法通过挖掘人类写作与AI生成文本之间的统计学差异进行判断&#xff1a;1.1 词汇使用模式分析AI生成的文本在词汇选…

可可图片编辑 HarmonyOS(5)滤镜效果

可可图片编辑 HarmonyOS&#xff08;5&#xff09;滤镜效果 前言 可可图片编辑也实现了滤镜效果&#xff0c;主要是利用 Image组件的 colorFilter 属性实现。滤镜的关键属性 colorFilter colorFilter 的主要作用是给图像设置颜色滤镜效果。 其核心原理是使用一个 4x5 的颜色矩阵…

< JS事件循环系列【二】> 微任务深度解析:从本质到实战避坑

在上一篇关于 JS 事件循环的文章中&#xff0c;我们提到 “微任务优先级高于宏任务” 这一核心结论&#xff0c;但对于微任务本身的细节并未展开。作为事件循环中 “优先级最高的异步任务”&#xff0c;微任务的执行机制直接影响代码逻辑的正确性&#xff0c;比如Promise.then的…

STM32 单片机开发 - SPI 总线

一、SPI 总线概念SPI 总线 --- Serial Peripheral Interface&#xff0c;即串行外设接口SPI 是摩托罗拉公司设计的一款 串行、同步、全双工总线&#xff1b;SPI 总线是三线 / 四线制总线&#xff0c;分别是&#xff1a;SPI_SCK&#xff08;时钟线&#xff09;、S…