FFmpeg入门:最简单的音视频播放器
FFmpeg入门:最简单的音视频播放器
前两章,我们已经了解了分别如何构建一个简单和音频播放器和视频播放器。
FFmpeg入门:最简单的音频播放器
FFmpeg入门:最简单的视频播放器
本章我们将结合上述两章的知识,看看如何融合成一个完整的音视频播放器,跟上我的节奏,本章将是咱们后续完成一个完整的音视频播放器的起点。
整体流程图
话不多说,先上图
这个图似乎有点复杂了,这里我会分别将每个模块拿出来讲述,方便大家一步一步分析整个流程。
第一步:初始化
我们首先关注整个流程图的最上面一部分,这部分其实和之前的流程一样,主要就是将做一些前置的初始化工作:
1:打开文件,获取文件上下文
2:找到对应的音频/视频流,获取到Codec上下文
3:打开解码器
4:分配输出空间缓存,用于后续存储解码的输出数据
5:音频/视频帧的格式转化上下文
6:初始化SDL组件,主要是视频的播放窗口和音频播放器
代码(省略了部分校验和参数初始化,方便阅读,原码见文章末尾):
/** 初始化函数 */init_video_state(&video_state);audio_param = video_state->audioParam;video_param = video_state->videoParam;avformat_network_init();// 1. 打开视频文件,获取格式上下文if(avformat_open_input(&video_state->formatCtx, argv[1], NULL, NULL)!=0){printf("Couldn't open input stream.\n");return -1;}// 2. 对文件探测流信息if(avformat_find_stream_info(video_state->formatCtx, NULL) < 0){printf("Couldn't find stream information.\n");return -1;}// 3. 找到对应的 音频流/视频流 索引video_state->audioStream=-1;video_state->videoStream=-1;for(int i=0; i < video_state->formatCtx->nb_streams; i++) {if(video_state->formatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_AUDIO){video_state->audioStream=i;}if (video_state->formatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_VIDEO) {video_state->videoStream=i;}}// 4. 将 音频流/视频流 编码参数写入上下文AVCodecParameters* aCodecParam = video_state->formatCtx->streams[video_state->audioStream]->codecpar;avcodec_parameters_to_context(video_state->aCodecCtx, aCodecParam);AVCodecParameters* vCodecParam = video_state->formatCtx->streams[video_state->videoStream]->codecpar;avcodec_parameters_to_context(video_state->vCodecCtx, vCodecParam);// 5. 查找流的编码器video_state->aCodec = avcodec_find_decoder(video_state->aCodecCtx->codec_id);video_state->vCodec = avcodec_find_decoder(video_state->vCodecCtx->codec_id);// 6. 打开流的编解码器if(avcodec_open2(video_state->aCodecCtx, video_state->aCodec, NULL)<0){printf("Could not open audio codec.\n");return -1;}if(avcodec_open2(video_state->vCodecCtx, video_state->vCodec, NULL)<0){printf("Could not open video codec.\n");return -1;}/** 音频输出信息构建 */audio_output_set(video_state);/** 视频输出信息构建 */video_output_set(video_state);// SDL 初始化
#if USE_SDLif(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {printf( "Could not initialize SDL - %s\n", SDL_GetError());return -1;}/** 初始化音频SDL设备 */SDL_AudioSpec wanted_spec;wanted_spec.freq = audio_param->out_sample_rate; // 采样率wanted_spec.format = AUDIO_S16SYS; // 采样格式 16bitwanted_spec.channels = audio_param->out_channels; // 通道数wanted_spec.silence = 0;wanted_spec.samples = audio_param->out_nb_samples; // 单帧处理的采样点wanted_spec.callback = fill_audio; // 回调函数wanted_spec.userdata = video_state->aCodecCtx; // 回调函数的参数/** 初始化视频SDL设备 */SDL_Window* window = NULL;SDL_Renderer* renderer = NULL;SDL_Texture* texture= NULL;/** 窗口 */window = SDL_CreateWindow("SDL2 window",SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,video_state->vCodecCtx->width,video_state->vCodecCtx->height,SDL_WINDOW_SHOWN);/** 渲染 */renderer = SDL_CreateRenderer(window,-1,SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);/** 纹理 */texture = SDL_CreateTexture(renderer,SDL_PIXELFORMAT_YV12,SDL_TEXTUREACCESS_STREAMING,video_state->vCodecCtx->width,video_state->vCodecCtx->height);// 打开音频播放器if (SDL_OpenAudio(&wanted_spec, NULL)<0) {printf("can't open audio.\n");return -1;}#endif// 音频上下文格式转换swr_alloc_set_opts2(&video_state->swrCtx,&audio_param->out_channel_layout, // 输出layoutaudio_param->out_sample_fmt, // 输出格式audio_param->out_sample_rate, // 输出采样率&video_state->aCodecCtx->ch_layout, // 输入layoutvideo_state->aCodecCtx->sample_fmt, // 输入格式video_state->aCodecCtx->sample_rate, // 输入采样率0, NULL);swr_init(video_state->swrCtx);// 视频上下文格式转换video_state->swsCtx = sws_getContext(video_state->vCodecCtx->width, // src 宽video_state->vCodecCtx->height, // src 高video_state->vCodecCtx->pix_fmt, // src 格式video_param->width, // dst 宽video_param->height, // dst 高video_param->pix_fmt, // dst 格式SWS_BILINEAR,NULL,NULL,NULL);// 开始播放SDL_PauseAudio(0);
第二步:packet队列写入
做完准备工作之后,我们就将源文件中的输入packet都取出来,放入到对应的音频packet队列和视频packet队列中,方便后续使用。然后然后分别启动音频解码进程和视频解码进程同时进行解码。
- 读出packet,判断packet类型
- 根据类型放入音频和视频packet队列
- 创建解码进程
// 循环1: 从文件中读取packetwhile(av_read_frame(video_state->formatCtx, packet)>=0){/** 写入音频pkt队列 */if(packet->stream_index==video_state->audioStream){packet_queue_push(video_state->aQueue, packet);}/** 写入视频pkt队列 */if (packet->stream_index==video_state->videoStream) {packet_queue_push(video_state->vQueue, packet);}av_packet_unref(packet);SDL_PollEvent(&event);switch(event.type) {case SDL_QUIT:SDL_Quit();exit(0);break;default:break;}}printf("audio queue.size=%d\n", video_state->aQueue->size);// 创建一个线程并启动SDL_CreateThread(audio_thread, "audio_thread", video_state);SDL_CreateThread(video_thread, "video_thread", video_state);
第三步:音频解码+播放
接下来会分别讲一下音频和视频解码进程,这两个进程是同时开始的。
首先音频解码进程的步骤可以参考之前的音频播放器文章。我简单说一下步骤
- 从音频packet队列中取出packet
- 对packet进行解码得到frame
- 按音频输出格式进行swr_convert转换得到输出值,并写入buffer。
- SDL音频播放器通过回调函数从buffer不断读取数据播放。
代码如下:
/**音频线程*/
int audio_thread(void *arg) {/**1. 从packet_queue队列中取出packet2. 将packet进行解码3. 写入到sdl的缓冲区中*/ VideoState* video_state = (VideoState*) arg;AudioParam* audio_param = video_state->audioParam;PacketQueue* queue = video_state->aQueue;audio_param->index = 0;AVRational time_base = video_state->formatCtx->streams[video_state->audioStream]->time_base;int64_t av_start_time = av_gettime(); // 播放开始时间戳AVPacket packet;int ret;AVFrame* pFrame = av_frame_alloc();for(;;) {if (queue->size > 0) {packet_queue_pop(queue, &packet);// 将packet写入编解码器ret = avcodec_send_packet(video_state->aCodecCtx, &packet);// 获取解码后的帧while (!avcodec_receive_frame(video_state->aCodecCtx, pFrame)) {// 格式转化swr_convert(video_state->swrCtx, &audio_param->out_buffer, audio_param->out_buffer_size,(const uint8_t **)pFrame->data, pFrame->nb_samples);audio_param->index++;printf("第%d帧 | pts:%lld | 帧大小(采样点):%d | 实际播放点%.2fs | 预期播放点%.2fs\n",audio_param->index,packet.pts,packet.size,(double)(av_gettime() - av_start_time)/AV_TIME_BASE,pFrame->pts * av_q2d(time_base));#if USE_SDL// 设置读取的音频数据audio_info.audio_len = audio_param->out_buffer_size;audio_info.audio_pos = (Uint8 *) audio_param->out_buffer;// 等待SDL播放完成while(audio_info.audio_len > 0)SDL_Delay(0.5);
#endif}av_packet_unref(&packet);}else {break;}}av_frame_free(&pFrame);// 结束video_state->isEnd = 1;return 0;
}
第四步:视频解码(子线程)+播放(主线程)
说视频的解码和播放之前,先提一点:SDL的主窗口操作是需要在主线程中进行的。因此我们不能再解码子线程中直接渲染SDL窗口,否则会造成内存泄漏。知道这个知识之后,更能理解接下来的流程分析。
我们视频解码播放拆成两个部分:解码+播放
第一部分:解码子线程,在子线程中完成解码,通过标识符的方式通知到主线程帧已更新,并渲染出来。
第二部分:主线程播放,循环监听子线程的通知标识,并更新窗口帧进行显示。
代码如下:
视频解码子线程
/**视频线程*/
int video_thread(void *arg) {/**1. 从视频pkt队列中读出packet2. 送入解码器解码并取出3. 使用SDL进行渲染4. 根据pts计算延迟SDL_DELAY*/VideoState* video_state = (VideoState*) arg;PacketQueue* video_queue = video_state->vQueue;AVCodecContext* pCodecCtx = video_state->vCodecCtx;AVFrame* out_frame = video_state->videoParam->out_frame;AVPacket packet;AVFrame* pFrame = av_frame_alloc();AVRational time_base = video_state->formatCtx->streams[video_state->videoStream]->time_base;int64_t av_start_time = av_gettime(); // 开始播放时间(ms*1000)int64_t frame_delay = av_q2d(time_base) * AV_TIME_BASE; // pts单位(ms*1000)int64_t frame_start_time = av_gettime();for (;;) {if (video_queue->size > 0) {packet_queue_pop(video_queue, &packet);// 将packet写入编解码器int ret = avcodec_send_packet(pCodecCtx, &packet);// 从解码器中取出原始帧while (!avcodec_receive_frame(pCodecCtx, pFrame)) {// 帧格式转化,转为YUV420Psws_scale(video_state->swsCtx, // sws_context转换(uint8_t const * const *)pFrame->data, // 输入 datapFrame->linesize, // 输入 每行数据的大小(对齐)0, // 输入 Y轴位置pCodecCtx->height, // 输入 heightout_frame->data, // 输出 dataout_frame->linesize); // 输出 linesize// 帧更新video_state->videoParam->frame_update = 1;// 计算延迟int64_t pts = pFrame->pts; // ptsint64_t actual_playback_time = av_start_time + pts * frame_delay; // 实际播放时间int64_t current_time = av_gettime();if (actual_playback_time > current_time) {SDL_Delay((Uint32)(actual_playback_time-current_time)/1000); // 延迟当前时间和实际播放时间}video_state->videoParam->index++;printf("第%i帧 | 属于%s | pts为%d | 时长为%.2fms | 实际播放点为%.2fs | 预期播放点为%.2fs\n ",video_state->videoParam->index,get_frame_type(pFrame),(int)pFrame->pts,(double)(av_gettime() - frame_start_time)/1000,(double)(av_gettime() - av_start_time)/AV_TIME_BASE,pFrame->pts * av_q2d(time_base));frame_start_time = av_gettime();}av_packet_unref(&packet);} else {break;}}av_frame_free(&pFrame);// 结束video_state->isEnd = 1;return 1;
}
渲染主线程
while (!video_state->isEnd) {// 处理事件(必须由主线程执行)while (SDL_PollEvent(&event)) {if (event.type == SDL_QUIT) {video_state->isEnd = 1;}}if (video_state->videoParam->frame_update) {// 将AVFrame的数据写入到texture中,然后渲染后windows上rect.x = 0;rect.y = 0;rect.w = video_state->vCodecCtx->width;rect.h = video_state->vCodecCtx->height;out_frame = video_state->videoParam->out_frame;// 更新纹理SDL_UpdateYUVTexture(texture, &rect,out_frame->data[0], out_frame->linesize[0], // Yout_frame->data[1], out_frame->linesize[1], // Uout_frame->data[2], out_frame->linesize[2]); // V// 渲染页面SDL_RenderClear(renderer);SDL_RenderCopy(renderer, texture, NULL, NULL);SDL_RenderPresent(renderer);// 重置标志video_state->videoParam->frame_update = 0;}}
完整代码
sample_player.h
//
// sample_player.h
// learning
//
// Created by chenhuaiyi on 2025/2/26.
//#ifndef sample_player_h
#define sample_player_h#include <stdio.h>
// ffmpeg
#include "libavcodec/avcodec.h"
#include "libswresample/swresample.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
#include "libavutil/time.h"
#include "libavutil/fifo.h"
#include "libavutil/channel_layout.h"
// SDL
#include "SDL.h"
#include "SDL_thread.h"/**宏定义*/
#define USE_SDL 1typedef struct MyAVPacketList {AVPacket *pkt;int serial;
} MyAVPacketList;/**packet队列*/
typedef struct PacketQueue {AVFifo* pkt_list; // fifo队列int size; // 队列大小SDL_mutex* mutex; // 互斥信号量SDL_cond* cond; // 条件变量,阻塞线程
} PacketQueue;/**数据类型定义*/
typedef struct AudioInfo{Uint32 audio_len; // 缓冲区长度Uint8* audio_pos; // 缓冲区起始地址指针
} AudioInfo;/**语音输出参数*/
typedef struct AudioParam {AVChannelLayout out_channel_layout; // layoutint out_nb_samples; // 每一帧的样本数enum AVSampleFormat out_sample_fmt; // 格式int out_sample_rate; // 采样率int out_channels; // 输出通道数int index; // 音频帧总数int out_buffer_size; // 音频输出缓冲区大小uint8_t* out_buffer; // 音频输出缓冲区
} AudioParam;/**视频输出参数*/
typedef struct VideoParam {int width; // 宽int height; // 高enum AVPixelFormat pix_fmt; // 格式 YUV420Pint num_bytes; // 单帧字节数int index; // 视频帧总数AVFrame* out_frame; // 输出帧int frame_update; // 帧更新标识
} VideoParam;/**全局参数*/
typedef struct VideoState {AVFormatContext* formatCtx; // format上下文int audioStream; // 音频流索引AVCodecContext* aCodecCtx; // 音频codec上下文const AVCodec* aCodec; // 音频解码器AudioParam* audioParam; // 音频参数SwrContext* swrCtx; // 音频上线文转换int videoStream; // 视频流索引AVCodecContext* vCodecCtx; // 视频codec上新闻const AVCodec* vCodec; // 视频解码器VideoParam* videoParam; // 视频参数struct SwsContext* swsCtx; // 视频上下文转换PacketQueue* aQueue; // 音频pkt队列PacketQueue* vQueue; // 视频pkt队列int isEnd; // 结束标志
} VideoState;/**全局变量*/
extern AudioInfo audio_info;#endif /* sample_player_h */
utils.h
//
// utils.h
// sample_player
//
// Created by chenhuaiyi on 2025/2/27.
//#ifndef utils_h
#define utils_h#include "sample_player.h"int init_video_state(VideoState** video_state);int destory_video_state(VideoState** video_state);int packet_queue_push(PacketQueue* q, AVPacket* pkt);int packet_queue_init(PacketQueue** q, size_t max_size);int packet_queue_pop(PacketQueue* q, AVPacket* pkt);void packet_queue_destroy(PacketQueue** q);char* get_frame_type(AVFrame* frame);#endif /* utils_h */
manager.h
//
// manager.h
// sample_player
//
// Created by chenhuaiyi on 2025/2/27.
//#ifndef manager_h
#define manager_h#include "sample_player.h"/**音频输出信息设置*/
int audio_output_set(VideoState* video_state);/**视频输出信息设置*/
int video_output_set(VideoState* video_state);/**音频SDL初始化*/
int audio_sdl_set(VideoState* video_state, SDL_AudioSpec* wanted_spec, void (*fn)(void*, Uint8*, int));/**视频SDL初始化*/
int video_sdl_set(VideoState* video_state, SDL_Window** window, SDL_Renderer** renderer, SDL_Texture** texture);#endif /* manager_h */
utils.c
//
// utils.c
// sample_player
//
// Created by chenhuaiyi on 2025/2/27.
//#include "utils.h"/**初始化VideoState*/
int init_video_state(VideoState** video_state) {*video_state = av_malloc(sizeof(VideoState));(*video_state)->formatCtx = avformat_alloc_context();(*video_state)->audioStream = 0;(*video_state)->aCodecCtx = avcodec_alloc_context3(NULL);(*video_state)->audioParam = av_malloc(sizeof(AudioParam));(*video_state)->videoStream = 0;(*video_state)->vCodecCtx = avcodec_alloc_context3(NULL);(*video_state)->videoParam = av_malloc(sizeof(VideoParam));(*video_state)->videoParam->frame_update = 0;/** pkt队列初始化 */(*video_state)->aQueue = av_malloc(sizeof(PacketQueue));packet_queue_init(&(*video_state)->aQueue, 1);(*video_state)->vQueue = av_malloc(sizeof(PacketQueue));packet_queue_init(&(*video_state)->vQueue, 1);(*video_state)->isEnd = 0;return 1;
}/**销毁VideoState*/
int destory_video_state(VideoState** video_state){swr_free(&(*video_state)->swrCtx);avcodec_free_context(&(*video_state)->aCodecCtx);av_free((*video_state)->audioParam->out_buffer);av_free((*video_state)->audioParam);sws_freeContext((*video_state)->swsCtx);avcodec_free_context(&(*video_state)->vCodecCtx);av_frame_free(&(*video_state)->videoParam->out_frame);av_free((*video_state)->videoParam);/** 队列释放 */packet_queue_destroy(&(*video_state)->aQueue);packet_queue_destroy(&(*video_state)->vQueue);if ((*video_state)->formatCtx != NULL) {avformat_close_input(&(*video_state)->formatCtx);(*video_state)->formatCtx = NULL;}av_free(*video_state);return 1;
}/**初始化队列*/
int packet_queue_init(PacketQueue** q, size_t max_size) {// 创建一个 AVFifo 队列,每个元素的大小为 sizeof(AVPacket)*q = av_malloc(sizeof(PacketQueue));(*q)->pkt_list = av_fifo_alloc2(max_size, sizeof(MyAVPacketList), AV_FIFO_FLAG_AUTO_GROW);(*q)->size = 0;(*q)->mutex = SDL_CreateMutex();(*q)->cond = SDL_CreateCond();if (!(*q)->pkt_list) {return -1;}return 0;
}/**写入队列*/
int packet_queue_push(PacketQueue* q, AVPacket* pkt) {MyAVPacketList pNode;if (!q || !pkt) {return -1;}AVPacket* pkt1 = av_packet_alloc();if (!pkt1) {av_packet_unref(pkt);return -1;}SDL_LockMutex(q->mutex);av_packet_ref(pkt1, pkt);pNode.pkt = pkt1;// 将 pkt 压入队列if (av_fifo_write(q->pkt_list, &pNode, 1) < 0) {SDL_UnlockMutex(q->mutex);return -1;}q->size++;SDL_CondSignal(q->cond);SDL_UnlockMutex(q->mutex);return 0;
}/**弹出队列*/
int packet_queue_pop(PacketQueue* q, AVPacket* pkt) {if (!q || !pkt) {return -1;}SDL_LockMutex(q->mutex);MyAVPacketList pNode;// 从队列中弹出一个元素, 没找到则阻塞线程,等待生产者释放if (av_fifo_read(q->pkt_list, &pNode, 1) < 0) {SDL_CondWait(q->cond, q->mutex);}q->size--;av_packet_move_ref(pkt, pNode.pkt);av_packet_free(&pNode.pkt);SDL_UnlockMutex(q->mutex);return 0;
}/**销毁队列*/
void packet_queue_destroy(PacketQueue** q) {if ((*q) && (*q)->pkt_list) {// 释放队列中的所有 AVPacketMyAVPacketList pNode;SDL_LockMutex((*q)->mutex);while (av_fifo_read((*q)->pkt_list, &pNode, 1) >= 0) {av_packet_free(&pNode.pkt); // 释放 AVPacket 的资源}SDL_UnlockMutex((*q)->mutex);// 释放 AVFifo 队列(*q)->size = 0;av_fifo_freep2(&(*q)->pkt_list);SDL_DestroyMutex((*q)->mutex);SDL_DestroyCond((*q)->cond);av_free(*q);}
}/**获取帧类型*/
char* get_frame_type(AVFrame* frame) {switch (frame->pict_type) {case AV_PICTURE_TYPE_I:return "I";break;case AV_PICTURE_TYPE_P:return "P";break;case AV_PICTURE_TYPE_B:return "B";break;case AV_PICTURE_TYPE_S:return "S";break;case AV_PICTURE_TYPE_SI:return "SI";break;case AV_PICTURE_TYPE_SP:return "SP";break;case AV_PICTURE_TYPE_BI:return "BI";break;default:return "N";break;}
}
manager.c
//
// manager.c
// sample_player
//
// Created by chenhuaiyi on 2025/2/27.
//#include "manager.h"/**音频输出信息构建*/
int audio_output_set(VideoState* video_state) {AudioParam* audio_param = video_state->audioParam;// 输出用到的信息av_channel_layout_default(&audio_param->out_channel_layout, 2);audio_param->out_nb_samples = video_state->aCodecCtx->frame_size; // 编解码器每个帧需要处理或者输出的采样点的大小 AAC:1024 MP3:1152audio_param->out_sample_fmt = AV_SAMPLE_FMT_S16; // 采样格式audio_param->out_sample_rate = 44100; // 采样率audio_param->out_channels = audio_param->out_channel_layout.nb_channels; // 通道数// 获取需要使用的缓冲区大小 -> 通道数,单通道样本数,位深 1024(单帧处理的采样点)*2(双通道)*2(16bit对应2字节)audio_param->out_buffer_size = av_samples_get_buffer_size(NULL, audio_param->out_channels,audio_param->out_nb_samples,audio_param->out_sample_fmt, 1);// 分配缓冲区空间audio_param->out_buffer = NULL;av_samples_alloc(&audio_param->out_buffer, NULL, audio_param->out_channels,audio_param->out_nb_samples, audio_param->out_sample_fmt, 1);return 1;
}/**视频输出信息构建*/
int video_output_set(VideoState* video_state) {VideoParam* video_param = video_state->videoParam;// 基础信息video_param->width = video_state->vCodecCtx->width;video_param->height = video_state->vCodecCtx->height;video_param->pix_fmt = AV_PIX_FMT_YUV420P;// 计算单帧大小, 分配单帧内存video_param->num_bytes = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, video_param->width, video_param->height, 1);video_param->out_frame = av_frame_alloc();av_image_alloc(video_param->out_frame->data, video_param->out_frame->linesize,video_param->width, video_param->height, AV_PIX_FMT_YUV420P, 1);return 1;
}/**音频SDL初始化*/
int audio_sdl_set(VideoState* video_state, SDL_AudioSpec* wanted_spec, void (*fn)(void*, Uint8*, int)) {AudioParam* audio_param = video_state->audioParam;wanted_spec->freq = audio_param->out_sample_rate; // 采样率wanted_spec->format = AUDIO_S16SYS; // 采样格式 16bitwanted_spec->channels = audio_param->out_channels; // 通道数wanted_spec->silence = 0;wanted_spec->samples = audio_param->out_nb_samples; // 单帧处理的采样点wanted_spec->callback = fn; // 回调函数wanted_spec->userdata = video_state->aCodecCtx; // 回调函数的参数return 1;
}/**视频SDL初始化*/
int video_sdl_set(VideoState* video_state, SDL_Window** window, SDL_Renderer** renderer, SDL_Texture** texture){AVCodecContext* pCodecCtx = video_state->vCodecCtx;/** 窗口 */*window = SDL_CreateWindow("SDL2 window",SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,pCodecCtx->width,pCodecCtx->height,SDL_WINDOW_SHOWN);if (!*window) {printf("SDL_CreateWindow Error: %s\n", SDL_GetError());SDL_Quit();return 1;}/** 渲染 */*renderer = SDL_CreateRenderer(*window,-1,SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);if (!*renderer) {printf("SDL_CreateRenderer Error: %s\n", SDL_GetError());SDL_DestroyWindow(*window);SDL_Quit();return 1;}/** 纹理 */*texture = SDL_CreateTexture(*renderer,SDL_PIXELFORMAT_YV12,SDL_TEXTUREACCESS_STREAMING,pCodecCtx->width,pCodecCtx->height);return 1;
}
main.c
//
// main.c
// sample_player
//
// Created by chenhuaiyi on 2025/2/26.
//#include "utils.h"
#include "manager.h"AudioInfo audio_info;/* udata: 传入的参数* stream: SDL音频缓冲区* len: SDL音频缓冲区大小* 回调函数*/
void fill_audio(void *udata, Uint8 *stream, int len){SDL_memset(stream, 0, len); // 必须重置,不然全是电音!!!if(audio_info.audio_len==0){ // 有音频数据时才调用return;}len = (len>audio_info.audio_len ? audio_info.audio_len : len); // 最多填充缓冲区大小的数据SDL_MixAudio(stream, audio_info.audio_pos, len, SDL_MIX_MAXVOLUME);audio_info.audio_pos += len;audio_info.audio_len -= len;
}/**音频线程*/
int audio_thread(void *arg) {/**1. 从packet_queue队列中取出packet2. 将packet进行解码3. 写入到sdl的缓冲区中*/ VideoState* video_state = (VideoState*) arg;AudioParam* audio_param = video_state->audioParam;PacketQueue* queue = video_state->aQueue;audio_param->index = 0;AVRational time_base = video_state->formatCtx->streams[video_state->audioStream]->time_base;int64_t av_start_time = av_gettime(); // 播放开始时间戳AVPacket packet;int ret;AVFrame* pFrame = av_frame_alloc();for(;;) {if (queue->size > 0) {packet_queue_pop(queue, &packet);// 将packet写入编解码器ret = avcodec_send_packet(video_state->aCodecCtx, &packet);if ( ret < 0 ) {printf("send packet error\n");return -1;}// 获取解码后的帧while (!avcodec_receive_frame(video_state->aCodecCtx, pFrame)) {// 格式转化swr_convert(video_state->swrCtx, &audio_param->out_buffer, audio_param->out_buffer_size,(const uint8_t **)pFrame->data, pFrame->nb_samples);audio_param->index++;printf("第%d帧 | pts:%lld | 帧大小(采样点):%d | 实际播放点%.2fs | 预期播放点%.2fs\n",audio_param->index,packet.pts,packet.size,(double)(av_gettime() - av_start_time)/AV_TIME_BASE,pFrame->pts * av_q2d(time_base));#if USE_SDL// 设置读取的音频数据audio_info.audio_len = audio_param->out_buffer_size;audio_info.audio_pos = (Uint8 *) audio_param->out_buffer;// 等待SDL播放完成while(audio_info.audio_len > 0)SDL_Delay(0.5);
#endif}av_packet_unref(&packet);}else {break;}}av_frame_free(&pFrame);// 结束video_state->isEnd = 1;return 0;
}/**视频线程*/
int video_thread(void *arg) {/**1. 从视频pkt队列中读出packet2. 送入解码器解码并取出3. 使用SDL进行渲染4. 根据pts计算延迟SDL_DELAY*/VideoState* video_state = (VideoState*) arg;PacketQueue* video_queue = video_state->vQueue;AVCodecContext* pCodecCtx = video_state->vCodecCtx;AVFrame* out_frame = video_state->videoParam->out_frame;AVPacket packet;AVFrame* pFrame = av_frame_alloc();AVRational time_base = video_state->formatCtx->streams[video_state->videoStream]->time_base;int64_t av_start_time = av_gettime(); // 开始播放时间(ms*1000)int64_t frame_delay = av_q2d(time_base) * AV_TIME_BASE; // pts单位(ms*1000)int64_t frame_start_time = av_gettime();for (;;) {if (video_queue->size > 0) {packet_queue_pop(video_queue, &packet);// 将packet写入编解码器int ret = avcodec_send_packet(pCodecCtx, &packet);if (ret < 0) {printf("packet resolve error!");break;}// 从解码器中取出原始帧while (!avcodec_receive_frame(pCodecCtx, pFrame)) {// 帧格式转化,转为YUV420Psws_scale(video_state->swsCtx, // sws_context转换(uint8_t const * const *)pFrame->data, // 输入 datapFrame->linesize, // 输入 每行数据的大小(对齐)0, // 输入 Y轴位置pCodecCtx->height, // 输入 heightout_frame->data, // 输出 dataout_frame->linesize); // 输出 linesize// 帧更新video_state->videoParam->frame_update = 1;// 计算延迟int64_t pts = pFrame->pts; // ptsint64_t actual_playback_time = av_start_time + pts * frame_delay; // 实际播放时间int64_t current_time = av_gettime();if (actual_playback_time > current_time) {SDL_Delay((Uint32)(actual_playback_time-current_time)/1000); // 延迟当前时间和实际播放时间}video_state->videoParam->index++;printf("第%i帧 | 属于%s | pts为%d | 时长为%.2fms | 实际播放点为%.2fs | 预期播放点为%.2fs\n ",video_state->videoParam->index,get_frame_type(pFrame),(int)pFrame->pts,(double)(av_gettime() - frame_start_time)/1000,(double)(av_gettime() - av_start_time)/AV_TIME_BASE,pFrame->pts * av_q2d(time_base));frame_start_time = av_gettime();}av_packet_unref(&packet);} else {break;}}av_frame_free(&pFrame);// 结束video_state->isEnd = 1;return 1;
}int main(int argc, char* argv[])
{VideoState* video_state;AudioParam* audio_param;VideoParam* video_param;SDL_Event event;SDL_Rect rect;if(argc < 2) {fprintf(stderr, "Usage: test <file>\n");exit(1);}/** 初始化函数 */init_video_state(&video_state);audio_param = video_state->audioParam;video_param = video_state->videoParam;avformat_network_init();// 1. 打开视频文件,获取格式上下文if(avformat_open_input(&video_state->formatCtx, argv[1], NULL, NULL)!=0){printf("Couldn't open input stream.\n");return -1;}// 2. 对文件探测流信息if(avformat_find_stream_info(video_state->formatCtx, NULL) < 0){printf("Couldn't find stream information.\n");return -1;}// 打印信息av_dump_format(video_state->formatCtx, 0, argv[1], 0);// 3. 找到对应的 音频流/视频流 索引video_state->audioStream=-1;video_state->videoStream=-1;for(int i=0; i < video_state->formatCtx->nb_streams; i++) {if(video_state->formatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_AUDIO){video_state->audioStream=i;}if (video_state->formatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_VIDEO) {video_state->videoStream=i;}}if(video_state->audioStream==-1){printf("Didn't find a audio stream.\n");return -1;}if (video_state->videoStream==-1) {printf("Didn't find a video stream.\n");return -1;}// 4. 将 音频流/视频流 编码参数写入上下文AVCodecParameters* aCodecParam = video_state->formatCtx->streams[video_state->audioStream]->codecpar;avcodec_parameters_to_context(video_state->aCodecCtx, aCodecParam);
// avcodec_parameters_free(&aCodecParam); 这个是不需要手动释放的AVCodecParameters* vCodecParam = video_state->formatCtx->streams[video_state->videoStream]->codecpar;avcodec_parameters_to_context(video_state->vCodecCtx, vCodecParam);
// avcodec_parameters_free(&vCodecParam);// 5. 查找流的编码器video_state->aCodec = avcodec_find_decoder(video_state->aCodecCtx->codec_id);if(video_state->aCodec==NULL){printf("Audio codec not found.\n");return -1;}video_state->vCodec = avcodec_find_decoder(video_state->vCodecCtx->codec_id);if(video_state->vCodec==NULL){printf("Video codec not found.\n");return -1;}// 6. 打开流的编解码器if(avcodec_open2(video_state->aCodecCtx, video_state->aCodec, NULL)<0){printf("Could not open audio codec.\n");return -1;}if(avcodec_open2(video_state->vCodecCtx, video_state->vCodec, NULL)<0){printf("Could not open video codec.\n");return -1;}/** 音频输出信息构建 */audio_output_set(video_state);/** 视频输出信息构建 */video_output_set(video_state);// SDL 初始化
#if USE_SDLif(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {printf( "Could not initialize SDL - %s\n", SDL_GetError());return -1;}// 在 main 函数开始处添加SDL_SetHint(SDL_HINT_VIDEO_MAC_FULLSCREEN_SPACES, "0");SDL_SetHint(SDL_HINT_MAC_BACKGROUND_APP, "1");/** 初始化音频SDL设备 */SDL_AudioSpec wanted_spec;// audio_sdl_set(video_state, &wanted_spec, fill_audio);wanted_spec.freq = audio_param->out_sample_rate; // 采样率wanted_spec.format = AUDIO_S16SYS; // 采样格式 16bitwanted_spec.channels = audio_param->out_channels; // 通道数wanted_spec.silence = 0;wanted_spec.samples = audio_param->out_nb_samples; // 单帧处理的采样点wanted_spec.callback = fill_audio; // 回调函数wanted_spec.userdata = video_state->aCodecCtx; // 回调函数的参数/** 初始化视频SDL设备 */SDL_Window* window = NULL;SDL_Renderer* renderer = NULL;SDL_Texture* texture= NULL;// video_sdl_set(video_state, &window, &renderer, &texture);/** 窗口 */window = SDL_CreateWindow("SDL2 window",SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,video_state->vCodecCtx->width,video_state->vCodecCtx->height,SDL_WINDOW_SHOWN);if (!window) {printf("SDL_CreateWindow Error: %s\n", SDL_GetError());SDL_Quit();return 1;}/** 渲染 */renderer = SDL_CreateRenderer(window,-1,SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);if (!renderer) {printf("SDL_CreateRenderer Error: %s\n", SDL_GetError());SDL_DestroyWindow(window);SDL_Quit();return 1;}/** 纹理 */texture = SDL_CreateTexture(renderer,SDL_PIXELFORMAT_YV12,SDL_TEXTUREACCESS_STREAMING,video_state->vCodecCtx->width,video_state->vCodecCtx->height);// 打开音频播放器if (SDL_OpenAudio(&wanted_spec, NULL)<0) {printf("can't open audio.\n");return -1;}#endif// 音频上下文格式转换swr_alloc_set_opts2(&video_state->swrCtx,&audio_param->out_channel_layout, // 输出layoutaudio_param->out_sample_fmt, // 输出格式audio_param->out_sample_rate, // 输出采样率&video_state->aCodecCtx->ch_layout, // 输入layoutvideo_state->aCodecCtx->sample_fmt, // 输入格式video_state->aCodecCtx->sample_rate, // 输入采样率0, NULL);swr_init(video_state->swrCtx);// 视频上下文格式转换video_state->swsCtx = sws_getContext(video_state->vCodecCtx->width, // src 宽video_state->vCodecCtx->height, // src 高video_state->vCodecCtx->pix_fmt, // src 格式video_param->width, // dst 宽video_param->height, // dst 高video_param->pix_fmt, // dst 格式SWS_BILINEAR,NULL,NULL,NULL);// 开始播放SDL_PauseAudio(0);int64_t av_start_time = av_gettime(); // 播放开始时间戳AVPacket* packet = av_packet_alloc(); // packet初始化// 循环1: 从文件中读取packetwhile(av_read_frame(video_state->formatCtx, packet)>=0){/** 写入音频pkt队列 */if(packet->stream_index==video_state->audioStream){packet_queue_push(video_state->aQueue, packet);}/** 写入视频pkt队列 */if (packet->stream_index==video_state->videoStream) {packet_queue_push(video_state->vQueue, packet);}av_packet_unref(packet);SDL_PollEvent(&event);switch(event.type) {case SDL_QUIT:SDL_Quit();exit(0);break;default:break;}}printf("audio queue.size=%d\n", video_state->aQueue->size);// 创建一个线程并启动SDL_CreateThread(audio_thread, "audio_thread", video_state);SDL_CreateThread(video_thread, "video_thread", video_state);// video_thread(video_state);AVFrame* out_frame = NULL;while (!video_state->isEnd) {// 处理事件(必须由主线程执行)while (SDL_PollEvent(&event)) {if (event.type == SDL_QUIT) {video_state->isEnd = 1;}}if (video_state->videoParam->frame_update) {// 将AVFrame的数据写入到texture中,然后渲染后windows上rect.x = 0;rect.y = 0;rect.w = video_state->vCodecCtx->width;rect.h = video_state->vCodecCtx->height;out_frame = video_state->videoParam->out_frame;// 更新纹理SDL_UpdateYUVTexture(texture, &rect,out_frame->data[0], out_frame->linesize[0], // Yout_frame->data[1], out_frame->linesize[1], // Uout_frame->data[2], out_frame->linesize[2]); // V// 渲染页面SDL_RenderClear(renderer);SDL_RenderCopy(renderer, texture, NULL, NULL);SDL_RenderPresent(renderer);// 重置标志video_state->videoParam->frame_update = 0;}}// 打印参数printf("格式: %s\n", video_state->formatCtx->iformat->name);printf("时长: %lld us\n", video_state->formatCtx->duration);printf("音频持续时长为 %.2f,音频帧总数为 %d\n", (double)(av_gettime()-av_start_time)/AV_TIME_BASE, audio_param->index);printf("码率: %lld\n", video_state->formatCtx->bit_rate);printf("编码器: %s (%s)\n", video_state->aCodecCtx->codec->long_name, avcodec_get_name(video_state->aCodecCtx->codec_id));printf("通道数: %d\n", video_state->aCodecCtx->ch_layout.nb_channels);printf("采样率: %d \n", video_state->aCodecCtx->sample_rate);printf("单通道每帧的采样点数目: %d\n", video_state->aCodecCtx->frame_size);printf("pts单位(ms*1000): %.2f\n", av_q2d(video_state->formatCtx->streams[video_state->audioStream]->time_base) * AV_TIME_BASE);// 释放空间av_packet_free(&packet);#if USE_SDLSDL_CloseAudio();SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();
#endifdestory_video_state(&video_state);return 0;
}
结语和展望
终于做完了,恭喜你,完成了一个非常粗糙,而且有很多问题的简单音视频播放器。接下来几期,我们跟着大家一起对这个简单的播放器进行优化。当然我也是个小萌新,所以一步一步来嘛哈哈。先抛出几个问题:
- 时钟同步怎么做
- 如何边读出packet,边解码frame并播放
- 我们如何对输出的解码帧进行转化
ps. 鼓励大家阅读ffplay源码,所有的问题都能迎刃而解,哈哈哈!
相关文章:

FFmpeg入门:最简单的音视频播放器
FFmpeg入门:最简单的音视频播放器 前两章,我们已经了解了分别如何构建一个简单和音频播放器和视频播放器。 FFmpeg入门:最简单的音频播放器 FFmpeg入门:最简单的视频播放器 本章我们将结合上述两章的知识,看看如何融…...

java 查找两个集合的交集部分数据
利用了Java 8的Stream API,代码简洁且效率高 import java.util.stream.Collectors; import java.util.List; import java.util.HashSet; import java.util.Set;public class ListIntersection {public static List<Long> findIntersection(List<Long> …...

【系统架构设计师】以数据为中心的体系结构风格
目录 1. 说明2. 仓库体系结构风格3. 黑板体系结构风格 1. 说明 1.以数据为中心的体系结构风格主要包括仓库体系结构风格和黑板体系结构风格。 2. 仓库体系结构风格 1.仓库(Repository)是存储和维护数据的中心场所。2.在仓库风格中,有两种不…...

通过HTML有序列表(ol/li)实现自动递增编号的完整解决方案
以下是通过HTML有序列表(ol/li)实现自动递增编号的完整解决方案: <!DOCTYPE html> <html> <head> <style> /* 基础样式 */ ol {margin: 1em 0;padding-left: 2em; }/* 方案1:默认数字编号 */ ol.default {list-style-type: dec…...

【Python 数据结构 4.单向链表】
目录 一、单向链表的基本概念 1.单向链表的概念 2.单向链表的元素插入 元素插入的步骤 3.单向链表的元素删除 元素删除的步骤 4.单向链表的元素查找 元素查找的步骤 5.单向链表的元素索引 元素索引的步骤 6.单向链表的元素修改 元素修改的步骤 二、Python中的单向链表 编辑 三…...

基于 vLLM 部署 LSTM 时序预测模型的“下饭”(智能告警预测与根因分析部署)指南
Alright,各位看官老爷们,准备好迎接史上最爆笑、最通俗易懂的 “基于 vLLM 部署 LSTM 时序预测模型的智能告警预测与根因分析部署指南” 吗? 保证让你笑出猪叫,看完直接变身技术大咖!🚀😂 咱们今天的主题,就像是要打造一个“智能运维小管家”! 这个小管家,不仅能提…...

Java多线程与高并发专题——ConcurrentHashMap 在 Java7 和 8 有何不同?
引入 上一篇我们提到HashMap 是线程不安全的,并推荐使用线程安全同时性能比较好的 ConcurrentHashMap。 而在 Java 8 中,对于 ConcurrentHashMap 这个常用的工具类进行了很大的升级,对比之前 Java 7 版本在诸多方面都进行了调整和变化。不过…...

NL2SQL-基于Dify+阿里通义千问大模型,实现自然语音自动生产SQL语句
本文基于Dify阿里通义千问大模型,实现自然语音自动生产SQL语句功能,话不多说直接上效果图 我们可以试着问他几个问题 查询每个部门的员工数量SELECT d.dept_name, COUNT(e.emp_no) AS employee_count FROM employees e JOIN dept_emp de ON e.emp_no d…...

LeetCode 1328.破坏回文串:贪心
【LetMeFly】1328.破坏回文串:贪心 力扣题目链接:https://leetcode.cn/problems/break-a-palindrome/ 给你一个由小写英文字母组成的回文字符串 palindrome ,请你将其中 一个 字符用任意小写英文字母替换,使得结果字符串的 字典…...

计算机视觉|ViT详解:打破视觉与语言界限
一、ViT 的诞生背景 在计算机视觉领域的发展中,卷积神经网络(CNN)一直占据重要地位。自 2012 年 AlexNet 在 ImageNet 大赛中取得优异成绩后,CNN 在图像分类任务中显示出强大能力。随后,VGG、ResNet 等深度网络架构不…...

//定义一个方法,把int数组中的数据按照指定的格式拼接成一个字符串返回,调用该方法,并在控制台输出结果
import java.util.Scanner; public class cha{ public static void main(String[] args){//定义一个方法,把int数组中的数据按照指定的格式拼接成一个字符串返回,调用该方法,并在控制台输出结果//eg: 数组为:int[] arr…...

Python快捷手册
Python快捷手册 后续会陆续更新Python对应的依赖或者工具使用方法 文章目录 Python快捷手册[toc]1-依赖1-词云小工具2-图片添加文字3-BeautifulSoup网络爬虫4-Tkinter界面绘制5-PDF转Word 2-开发1-多线程和队列 3-运维1-Requirement依赖2-波尔实验室3-Anaconda3使用教程4-CentO…...

QT5 GPU使用
一、问题1 1、现象 2、原因分析 出现上图错误,无法创建EGL表面,错误=0x300b。申请不上native window有可能是缺少libqeglfs-mali-integration.so 这个库 3、解决方法 需要将其adb push 到小机端的/usr/lib/qt5/plugins/egldeviceintegrat…...

如何在Spring Boot中读取JAR包内resources目录下文件
精心整理了最新的面试资料和简历模板,有需要的可以自行获取 点击前往百度网盘获取 点击前往夸克网盘获取 以下是如何在Spring Boot中读取JAR包内resources目录下文件的教程,分为多种方法及详细说明: 方法1:使用 ClassPathResour…...

《张一鸣,创业心路与算法思维》
张一鸣,多年如一日的阅读习惯。 爱读人物传记,称教科书式人类知识最浓缩的书,也爱看心理学,创业以及商业管理类的书。 冯仑,王石,联想,杰克韦尔奇,思科。 《乔布斯传》《埃隆马斯…...

SSE 和 WebSocket 的对比
SSE 和 WebSocket 的对比 在现代Web开发中,实时通信是提升用户体验的重要手段。Server-Sent Events(SSE)和WebSocket是两种实现服务器与客户端之间实时数据传输的技术,但它们在功能、适用场景以及实现方式上有所不同。 1. 基本概…...

es如何进行refresh?
在 Elasticsearch 中,refresh 操作的作用是让最近写入的数据可以被搜索到。以下为你介绍几种常见的执行 refresh 操作的方式: 1. 使用 RESTful API 手动刷新 你可以通过向 Elasticsearch 发送 HTTP 请求来手动触发 refresh 操作。可以针对单个索引、多个索引或者所有索引进…...

Kubespray部署企业级高可用K8S指南
目录 前言1 K8S集群节点准备1.1 主机列表1.2 kubespray节点python3及pip3准备1.2.1. 更新系统1.2.2. 安装依赖1.2.3. 下载Python 3.12源码1.2.4. 解压源码包1.2.5. 编译和安装Python1.2.6. 验证安装1.2.7. 设置Python 3.12为默认版本(可选)1.2.8. 安装pi…...

【实战篇】【深度解析DeepSeek:从机器学习到深度学习的全场景落地指南】
一、机器学习模型:DeepSeek的降维打击 1.1 监督学习与无监督学习的"左右互搏" 监督学习就像学霸刷题——给标注数据(参考答案)训练模型。DeepSeek在信贷风控场景中,用逻辑回归模型分析百万级用户数据,通过特征工程挖掘出"凌晨3点频繁申请贷款"这类魔…...

优选算法的智慧之光:滑动窗口专题(二)
专栏:算法的魔法世界 个人主页:手握风云 目录 一、例题讲解 1.1. 最大连续1的个数 III 1.2. 找到字符串中所有字母异位词 1.3. 串联所有单词的子串 1.4. 最小覆盖子串 一、例题讲解 1.1. 最大连续1的个数 III 题目要求是二进制数组&am…...

Kylin麒麟操作系统服务部署 | NFS服务部署
以下所使用的环境为: 虚拟化软件:VMware Workstation 17 Pro 麒麟系统版本:Kylin-Server-V10-SP3-2403-Release-20240426-x86_64 一、 NFS服务概述 NFS(Network File System),即网络文件系统。是一种使用于…...

7.1.2 计算机网络的分类
文章目录 分布范围交换方式 分布范围 计算机网络按照分布范围可分为局域网、广域网、城域网。局域网的范围在10m~1km,例如校园网,网速高,主要用于共享网络资源,拓扑结构简单,约束少。广域网的范围在100km,例…...

Spring Cloud Alibaba 实战:轻松实现 Nacos 服务发现与动态配置管理
1. Nacos 介绍 1.1 什么是 Nacos? Nacos(Naming and Configuration Service)是阿里巴巴开源的一个服务注册中心和配置管理中心。它支持动态服务发现、配置管理和服务治理,适用于微服务架构,尤其是基于 Spring Cloud …...

【数据结构】LRUCache|并查集
目录 一、LRUCache 1.概念 2.实现:哈希表双向链表 3.JDK中类似LRUCahe的数据结构LinkedHashMap 🔥4.OJ练习 二、并查集 1. 并查集原理 2.并查集代码实现 3.并查集OJ 一、LRUCache 1.概念 最近最少使用的,一直Cache替换算法 LRU是Least Recent…...

智能合约中权限管理不当
权限管理不当 : 权限管理不当是智能合约中常见的安全问题之一,尤其是在管理员或特定账户被过度赋予权限的情况下。如果合约中的关键功能,如转移资产、修改合约状态或升级合约逻辑,可以被未经授权的实体随意操作,这将构…...

MariaDB Galera 原理及用例说明
一、底层原理 MariaDB Galera 集群是一种基于同步多主架构的高可用数据库解决方案,适合需要高并发、低延迟和数据强一致性的场景。以下是部署和配置 MariaDB Galera 集群的简明步骤: 1. 环境准备 节点要求:至少 3 个节点(奇数节点…...

【RAG 篇】万字长文:向量数据库选型指南 —— Milvus 与 FAISS/Pinecone/Weaviate 等工具深度对比
大家好,我是大 F,深耕AI算法十余年,互联网大厂技术岗。分享AI算法干货、技术心得。 欢迎关注《大模型理论和实战》、《DeepSeek技术解析和实战》,一起探索技术的无限可能! 文章目录 向量数据库的核心价值主流工具横向对比 FAISS:Meta 的高效检索引擎Pinecone:全托管商业…...

关于服务器cpu过高的问题排查
1.定位是哪个程序造成的cpu过高 如果有云服务器,就用云服务器自带的监控功能,查时间段 如果没有,则使用: ps -eo pid,comm,pcpu,pmem,cputime --sort-cputime | head -n 100 2.定位到问题 发现是uwsgi的cpu消耗过高࿰…...

Gpt翻译完整版
上一篇文章收到了很多小伙伴的反馈,总结了一下主要以下几点: 1. 说不知道怎么调api 2. 目前只是把所有的中文变成了英文,如果想要做多语言还需要把这些关键字提炼出来成放到message_zh.properties和message_en.properties文件中,…...

雷池WAF的为什么选择基于Docker
Docker 是一种开源的容器化平台,可以帮助开发人员将应用程序及其所有依赖项打包到一个称为容器的独立、可移植的环境中。Docker 的核心概念包括以下几点: 容器:Docker 使用容器来封装应用程序及其依赖项,使其能够在任何环境中都能…...