基于 FFmpeg 和 SDL 的音视频同步播放器

基于 FFmpeg 和 SDL 的音视频同步播放器

  • 基于 FFmpeg 和 SDL 的音视频同步播放器
    • 前置知识
    • 音视频同步
      • 简介
      • 复习DTS、PTS和时间基
    • 程序框架
      • 主线程
      • 解复用线程
      • 音频解码播放线程
      • 视频解码播放线程
    • 音视频同步逻辑
    • 源程序
    • 结果
    • 工程文件下载
    • 参考链接

基于 FFmpeg 和 SDL 的音视频同步播放器

前置知识

前情提要:

  1. 基于 FFmpeg+SDL 的视频播放器的制作
  2. 最简单的基于 SDL2 的音频播放器

前两篇文章分别基于 FFmpeg+SDL2 实现了音频和视频的播放,要实现一个完整的简易播放器就必须要做到音视频同步播放了,而音视频同步在音视频开发中又是非常重要的知识点,所以在这里记录下音视频同步相关知识的理解。

音视频同步

简介

从前面的学习可以知道,在一个视频文件中,音频和视频都是单独以一条流的形式存在,互不干扰。那么在播放时根据视频的帧率(Frame Rate)和音频的采样率(Sample Rate)通过简单的计算得到其在某一Frame(Sample)的播放时间分别播放,理论上应该是同步的。但是由于机器运行速度,解码效率等等因素影响,很有可能出现音频和视频不同步,且音视频时间差将会呈现线性增长。例如出现视频中人在说话,却只能看到人物嘴动却没有声音,非常影响用户观看体验。

如何做到音视频同步?要知道音视频同步是一个动态的过程,同步是暂时的,不同步才是常态,需要一种随着时间会线性增长的量,视频和音频的播放速度都以该量为标准,播放快了就减慢播放速度;播放慢了就加快播放的速度,在你追我赶中达到同步的状态。

目前主要有三种方式实现同步:

  1. 将视频和音频同步外部的时钟上,选择一个外部时钟为基准,视频和音频的播放速度都以该时钟为标准。
  2. 将音频同步到视频上,就是以视频的播放速度为基准来同步音频。
  3. 将视频同步到音频上,就是以音频的播放速度为基准来同步视频。

比较主流的是第三种,将视频同步到音频上。具体做法是以音频时间为基准,判断视频快了还是慢了,从而调整视频速度。其实是一个动态的追赶与等待的过程。

一般来说,由于某些生物学的原理,人对于声音的敏感度更高,如果频繁地去调整音频会产生杂音让人感觉到刺耳不舒服,而人对图像的敏感度就低很多了,所以一般都会采用第三种方式。

复习DTS、PTS和时间基

  • PTS(Presentation Time Stamp):显示时间戳,指示从packet中解码出来的数据的显示顺序。

  • DTS(Decode Time Stamp):解码时间戳,告诉解码器packet的解码顺序。

音频中二者是相同的,但是视频由于B帧(双向预测)的存在,会造成解码顺序与显示顺序并不相同,也就是视频中DTS与PTS不一定相同。

实例:

实际帧顺序:I B B P
存放帧顺序:I P B B
解码时间戳:1 4 2 3
展示时间戳:1 2 3 4

时间基 FFmpeg 源码:

/*** This is the fundamental unit of time (in seconds) in terms* of which frame timestamps are represented. For fixed-fps content,* timebase should be 1/framerate and timestamp increments should be* identically 1.* This often, but not always is the inverse of the frame rate or field rate* for video.* - encoding: MUST be set by user.* - decoding: the use of this field for decoding is deprecated.*             Use framerate instead.*/
AVRational time_base;
/**
* rational number numerator/denominator
*/
typedef struct AVRational{int num; ///< numeratorint den; ///< denominator
} AVRational;

时间基是一个分数,以秒为单位,num为分子,den为分母。

那它到底表示的是什么意思呢?以帧率为例,如果它的时间基是1/50秒,那么就表示每隔1/50秒显示一帧数据,也就是每1秒显示50帧,帧率为50FPS。

FFmpeg 提供了时间基的计算方法:

/**
* Convert rational to double.
* @param a rational to convert
* @return (double) a
*/
static inline double av_q2d(AVRational a){return a.num / (double) a.den;
}

每一帧数据都有对应的PTS,在播放视频或音频的时候我们需要将PTS时间戳转化为以秒为单位的时间,用来最后的展示,视频中某帧的显示时间的计算方式为:

time = pts * av_q2d(time_base);

程序框架

在这里插入图片描述

主线程

  1. 加载视频文件,查找音视频流信息
  2. 初始化音视频解码器
  3. 初始化SDL并设置相关的音视频参数
  4. 创建解复用线程,音频解码播放线程,视频解码播放线程
  5. 然后进入SDL窗口的事件循环,等待退出事件

解复用线程

  1. 循环读文件流,每次从文件流中读取一帧数据
  2. 根据帧类型放入相应的队列中

音频解码播放线程

  1. 从音频队列中取出一帧
  2. 将取到的数据送至音频解码器中
  3. 循环从解码器中取解码音频帧
  4. 将解码数据转换成packed形式,也就是LRLRLR…
  5. 等待SDL音频回调播放音频完成,回到1

视频解码播放线程

  1. 从视频队列中取出一帧
  2. 将取到的数据送至视频解码器中
  3. 循环从解码器中取解码视频帧
  4. 渲染视频帧到SDL窗口中
  5. 计算视频帧的pts和持续时间
  6. 根据音频帧和视频帧的差值计算延时
  7. 延时计算的时长后回到1

音视频同步逻辑

  1. 如果当前视频帧与音频帧的播放时间差值小于或等于视频帧持续时间,则表示音视频同步,正常延时。delay = duration。
  2. 如果视频帧比音频帧快,且大于视频帧一帧的时长,延时2倍的正常延时。delay = 2 * delay。
  3. 如果视频帧比音频帧慢,且大于视频帧一帧的时长,则立即播放下一帧。delay = 0。

源程序

环境:

  1. ffmpeg-win32-4.2.2
  2. SDL2
  3. Visual Studio 2015

下载地址:

  1. ffmpeg-win32-4.2.2.zip
  2. SDL2 库 - from 雷霄骅.zip

完整程序:

// Simplest FFmpeg Sync Player.cpp : 定义控制台应用程序的入口点。
//#include "stdafx.h"#pragma warning(disable:4996)#include <stdio.h>#define __STDC_CONSTANT_MACROSextern "C"
{
#include "libavformat/avformat.h"
#include "libavutil/time.h"
#include "SDL2/SDL.h"
}// 报错:
// LNK2019 无法解析的外部符号 __imp__fprintf,该符号在函数 _ShowError 中被引用
// LNK2019 无法解析的外部符号 __imp____iob_func,该符号在函数 _ShowError 中被引用// 解决办法:
// 包含库的编译器版本低于当前编译版本,需要将包含库源码用vs2017重新编译,由于没有包含库的源码,此路不通。
// 然后查到说是stdin, stderr, stdout 这几个函数vs2015和以前的定义得不一样,所以报错。
// 解决方法呢,就是使用{ *stdin,*stdout,*stderr }数组自己定义__iob_func()
#pragma comment(lib,"legacy_stdio_definitions.lib")
extern "C"
{FILE __iob_func[3] = { *stdin, *stdout, *stderr };
}char av_error[AV_ERROR_MAX_STRING_SIZE] = { 0 };
#define av_err2str(errnum) av_make_error_string(av_error, AV_ERROR_MAX_STRING_SIZE, errnum)#define MAX_VIDEO_PIC_NUM  1 // 最大缓存解码图片数#define AV_SYNC_THRESHOLD 0.01 // 同步最小阈值
#define AV_NOSYNC_THRESHOLD 10.0 //  不同步阈值// Packet 队列
typedef struct PacketQueue
{AVPacketList* first_pkt, *last_pkt; // 头、尾指针int nb_packets; // packet 计数器SDL_mutex* mutex; // SDL 互斥量
} PacketQueue;// 音视频同步时钟模式
enum {AV_SYNC_AUDIO_MASTER, // 设置音频为主时钟,将视频同步到音频上,默认选项AV_SYNC_VIDEO_MASTER, // 设置视频为主时钟,将音频同步到视频上,不推荐AV_SYNC_EXTERNAL_CLOCK, // 选择一个外部时钟为基准,不推荐
};// Buffer:
// |-----------|-------------|
// chunk-------pos---len-----|
static Uint8* audio_chunk;
static Uint32 audio_len;
static Uint8* audio_pos;SDL_Window* sdlWindow = nullptr; // 窗口
SDL_Renderer* sdlRenderer = nullptr; // 渲染器
SDL_Texture* sdlTexture = nullptr; // 纹理
SDL_Rect sdlRect; // 渲染显示面积AVFormatContext* pFormatCtx = NULL;
AVPacket* pkt;
AVFrame* video_frame, *audio_frame;
int ret;
int video_index = -1, audio_index = -1;// 输入文件路径
char in_filename[] = "cuc_ieschool.mp4";int frame_width = 1280;
int frame_height = 720;// 视频解码
AVCodec* video_pCodec = nullptr;
AVCodecContext* video_pCodecCtx = nullptr;typedef struct video_pic
{AVFrame frame;float clock; // 显示时钟float duration; // 持续时间int frame_NUM; // 帧号
} video_pic;video_pic v_pic[MAX_VIDEO_PIC_NUM]; // 视频解码最多保存四帧数据
int pic_count = 0; // 已存储图片数量// 音频解码
AVCodec* audio_pCodec = nullptr;
AVCodecContext* audio_pCodecCtx = nullptr;PacketQueue video_pkt_queue; // 视频帧队列
PacketQueue audio_pkt_queue; // 音频帧队列// 同步时钟,设置音频为主时钟
int av_sync_type = AV_SYNC_AUDIO_MASTER;int64_t audio_callback_time;double video_clock; // 视频时钟
double audio_clock; // 音频时钟// SDL 音频参数结构
SDL_AudioSpec audio_spec;// 初始化 SDL 并设置相关的音视频参数
int initSDL();
// 关闭 SDL 并释放资源
void closeSDL();
// SDL 音频回调函数
void fill_audio_pcm2(void* udata, Uint8* stream, int len);// fltp 转为 packed 形式
void fltp_convert_to_f32le(float* f32le, float* fltp_l, float* fltp_r, int nb_samples, int channels)
{for (int i = 0; i < nb_samples; i++){f32le[i * channels] = fltp_l[i];f32le[i * channels + 1] = fltp_r[i];}
}// 将一个 AVPacket 放入相应的队列中
void put_AVPacket_into_queue(PacketQueue *q, AVPacket* packet)
{SDL_LockMutex(q->mutex); // 上锁AVPacketList* temp = nullptr;temp = (AVPacketList*)av_malloc(sizeof(AVPacketList));if (!temp){printf("Malloc an AVPacketList error.\n");return;}temp->pkt = *packet;temp->next = nullptr;if (!q->last_pkt)q->first_pkt = temp;elseq->last_pkt->next = temp;q->last_pkt = temp;q->nb_packets++;SDL_UnlockMutex(q->mutex); // 解锁
}// 从 AVPacket 队列中取出第一个帧
static void packet_queue_get(PacketQueue* q, AVPacket *pkt2)
{while (true){AVPacketList* pkt1 = nullptr;// 一直取,直到队列中有数据,就返回pkt1 = q->first_pkt;if (pkt1){SDL_LockMutex(q->mutex); // 上锁q->first_pkt = pkt1->next;if (!q->first_pkt)q->last_pkt = nullptr;q->nb_packets--;SDL_UnlockMutex(q->mutex); // 解锁// pkt2 指向我们取的帧*pkt2 = pkt1->pkt;// 释放帧av_free(pkt1);break;}else{// 队列里暂时没有帧,等待SDL_Delay(1);}}return;
}// 视频解码播放线程
int video_play_thread(void * data)
{AVPacket video_pkt = { 0 };// 取数据while (true){// 从视频帧队列中取出一个 AVPacketpacket_queue_get(&video_pkt_queue, &video_pkt);// Send packet to decoderret = avcodec_send_packet(video_pCodecCtx, &video_pkt);if (ret < 0){fprintf(stderr, "Error sending a packet to video decoder.\n", av_err2str(ret));return -1;}while (ret >= 0){// Receive frame from decoderret = avcodec_receive_frame(video_pCodecCtx, video_frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0){fprintf(stderr, "Error receiving frame from video decoder.\n");break;}// printf("帧数:%3d\n", video_pCodecCtx->frame_number);fflush(stdout); // 清空输出缓冲区,并把缓冲区内容输出// video_clock = video_pCodecCtx->frame_number * durationvideo_clock = av_q2d(video_pCodecCtx->time_base) * video_pCodecCtx->ticks_per_frame * 1000 * video_pCodecCtx->frame_number;// printf("视频时钟:%f ms\n", video_clock);double duration = av_q2d(video_pCodecCtx->time_base) * video_pCodecCtx->ticks_per_frame * 1000;// 设置纹理的数据SDL_UpdateYUVTexture(sdlTexture, nullptr, // 矩形区域 rect,为 nullptr 表示全部区域video_frame->data[0], video_frame->linesize[0],video_frame->data[1], video_frame->linesize[1],video_frame->data[2], video_frame->linesize[2]);sdlRect.x = 0;sdlRect.y = 0;sdlRect.w = frame_width;sdlRect.h = frame_height;// 清理渲染器缓冲区SDL_RenderClear(sdlRenderer);// 将纹理拷贝到窗口渲染平面上SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect);// 翻转缓冲区,前台显示SDL_RenderPresent(sdlRenderer);// 调整播放下一帧的延迟时间,以实现同步double delay = duration;double diff = video_clock - audio_clock; // 时间差if (fabs(diff) <= duration) // 时间差在一帧范围内表示正常,延时正常时间delay = duration;else if (diff > duration) // 视频时钟比音频时钟快,且大于一帧的时间,延时 2 倍delay *= 2;else if (diff < -duration) // 视频时钟比音频时钟慢,且超出一帧时间,立即播放当前帧delay = 0;printf("frame: %d, delay: %lf ms\n", video_pCodecCtx->frame_number, delay);SDL_Delay(delay);}}return 0;
}// 音频解码播放线程
int audio_play_thread(void* data)
{AVPacket audio_pkt = { 0 };// 取数据while (true){// 从音频帧队列中取出一个 AVPacketpacket_queue_get(&audio_pkt_queue, &audio_pkt);// Send packet to decoderret = avcodec_send_packet(audio_pCodecCtx, &audio_pkt);if (ret < 0){fprintf(stderr, "Error sending a packet to audio decoder.\n", av_err2str(ret));return -1;}while (ret >= 0){// Receive frame from decoderret = avcodec_receive_frame(audio_pCodecCtx, audio_frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)break;else if (ret < 0){fprintf(stderr, "Error receiving frame from audio decoder.\n");break;}/** 下面是得到解码后的裸流数据进行处理,根据裸流数据的特征做相应的处理,* 如 AAC 解码后是 PCM ,H.264 解码后是 YUV,等等。*/// 根据采样格式,获取每个采样所占的字节数int data_size = av_get_bytes_per_sample(audio_pCodecCtx->sample_fmt);if (data_size < 0){// This should not occur, checking just for paranoiafprintf(stderr, "Failed to calculate data size.\n");break;}// nb_samples: AVFrame 的音频帧个数,channels: 通道数int pcm_buffer_size = data_size * audio_frame->nb_samples * audio_pCodecCtx->channels;uint8_t* pcm_buffer = (uint8_t*)malloc(pcm_buffer_size);memset(pcm_buffer, 0, pcm_buffer_size);// 转换为 packed 模式fltp_convert_to_f32le((float*)pcm_buffer, (float*)audio_frame->data[0], (float*)audio_frame->data[1],audio_frame->nb_samples, audio_pCodecCtx->channels);// 使用 SDL 播放// Set audio buffer (PCM data)audio_chunk = pcm_buffer;audio_len = pcm_buffer_size;audio_pos = audio_chunk;audio_clock = audio_frame->pts * av_q2d(audio_pCodecCtx->time_base) * 1000;// printf("音频时钟: %f ms\n", audio_clock);// Wait until finishwhile (audio_len > 0){// 使用 SDL_Delay 进行 1ms 的延迟,用当前缓存区剩余未播放的长度大于 0 结合前面的延迟进行等待SDL_Delay(1);}free(pcm_buffer);}}return 0;
}// 解复用线程
int open_file_thread(void* data)
{// 读取一个 AVPacketwhile (av_read_frame(pFormatCtx, pkt) >= 0){if (pkt->stream_index == video_index){// 加入视频队列put_AVPacket_into_queue(&video_pkt_queue, pkt);}else if (pkt->stream_index == audio_index){// 加入音频队列put_AVPacket_into_queue(&audio_pkt_queue, pkt);}else{// 当我们从数据队列中取出数据使用完后,需要释放空间(AVPacket)// 否则被导致内存泄漏,导致程序占用内存越来越大av_packet_unref(pkt);}}return 0;
}int main(int argc, char * argv[])
{// 打开媒体文件ret = avformat_open_input(&pFormatCtx, in_filename, 0, 0);if (ret < 0){printf("Couldn't open input file.\n");return -1;}// 读取媒体文件信息,给 pFormatCtx 赋值ret = avformat_find_stream_info(pFormatCtx, 0);if (ret < 0){printf("Couldn't find stream information.\n");return -1;}video_index = -1;for (int i = 0; i < pFormatCtx->nb_streams; i++){if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){video_index = i;break;}}if (video_index == -1){printf("Didn't find a video stream.\n");return -1;}audio_index = -1;for (size_t i = 0; i < pFormatCtx->nb_streams; i++){if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){audio_index = i;break;}}if (audio_index == -1){printf("Didn't find an audio stream.\n");return -1;}// Output Infoprintf("--------------- File Information ----------------\n");av_dump_format(pFormatCtx, 0, in_filename, 0); // 打印输入文件信息printf("-------------------------------------------------\n");// 根据视频流信息的 codec_id 找到对应的解码器video_pCodec = avcodec_find_decoder(pFormatCtx->streams[video_index]->codecpar->codec_id);if (!video_pCodec){printf("Video codec not found.\n");return -1;}// 分配视频解码器上下文video_pCodecCtx = avcodec_alloc_context3(video_pCodec);// 拷贝视频流信息到视频解码器上下文中avcodec_parameters_to_context(video_pCodecCtx, pFormatCtx->streams[video_index]->codecpar);// 得到视频的宽度和高度frame_width = pFormatCtx->streams[video_index]->codecpar->width;frame_height = pFormatCtx->streams[video_index]->codecpar->height;// 打开视频解码器和关联解码器上下文if (avcodec_open2(video_pCodecCtx, video_pCodec, nullptr)){printf("Could not open video codec.\n");return -1;}// 根据音频流信息的 codec_id 找到对应的解码器audio_pCodec = avcodec_find_decoder(pFormatCtx->streams[audio_index]->codecpar->codec_id);if (!audio_pCodec){printf("Audio codec not found.\n");return -1;}// 分配音频解码器上下文audio_pCodecCtx = avcodec_alloc_context3(audio_pCodec);// 拷贝音频流信息到音频解码器上下文中avcodec_parameters_to_context(audio_pCodecCtx, pFormatCtx->streams[audio_index]->codecpar);// 打开音频解码器和关联解码器上下文if (avcodec_open2(audio_pCodecCtx, audio_pCodec, nullptr)){printf("Could not open audio codec.\n");return -1;}// 申请一个 AVPacket 结构pkt = av_packet_alloc();// 申请一个 AVFrame 结构用来存放解码后的数据video_frame = av_frame_alloc();audio_frame = av_frame_alloc();// 初始化 SDLinitSDL();// 创建互斥量video_pkt_queue.mutex = SDL_CreateMutex();audio_pkt_queue.mutex = SDL_CreateMutex();// 设置 SDL 音频播放参数audio_spec.freq = audio_pCodecCtx->sample_rate; // 采样率audio_spec.format = AUDIO_F32LSB; // 音频数据采样格式audio_spec.channels = audio_pCodecCtx->channels; // 通道数audio_spec.silence = 0; // 音频缓冲静音值audio_spec.samples = audio_pCodecCtx->frame_size; // 每一帧的采样点数量,基本是 512、1024,设置不合适可能会导致卡顿audio_spec.callback = fill_audio_pcm2; // 音频播放回调// 打开系统音频设备if (SDL_OpenAudio(&audio_spec, NULL) < 0){printf("Can't open audio.\n");return -1;}// 开始播放SDL_PauseAudio(0);// 创建 SDL 线程SDL_CreateThread(open_file_thread, "open_file", nullptr);SDL_CreateThread(video_play_thread, "video_play", nullptr);SDL_CreateThread(audio_play_thread, "audio_play", nullptr);bool quit = false;SDL_Event e;while (quit == false){while (SDL_PollEvent(&e) != 0){if (e.type == SDL_QUIT){quit = true;break;}}}// 销毁互斥量SDL_DestroyMutex(video_pkt_queue.mutex);SDL_DestroyMutex(audio_pkt_queue.mutex);// 关闭 SDLcloseSDL();// 释放 FFmpeg 相关资源avcodec_close(video_pCodecCtx);avcodec_free_context(&video_pCodecCtx);avcodec_close(audio_pCodecCtx);avcodec_free_context(&audio_pCodecCtx);av_packet_free(&pkt);av_frame_free(&audio_frame);av_frame_free(&video_frame);avformat_close_input(&pFormatCtx);return 0;
}// SDL 初始化
int initSDL()
{if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){printf("Could not initialize SDL - %s\n", SDL_GetError());return -1;}// 创建窗口 SDL_WindowsdlWindow = SDL_CreateWindow("Simplest FFmpeg Sync Player", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,frame_width, frame_height, SDL_WINDOW_SHOWN);if (sdlWindow == nullptr){printf("SDL: Could not create window - exiting:%s\n", SDL_GetError());return -1;}// 创建渲染器 SDL_RenderersdlRenderer = SDL_CreateRenderer(sdlWindow, -1, 0);if (sdlRenderer == nullptr){printf("SDL: Could not create renderer - exiting:%s\n", SDL_GetError());return -1;}// 创建纹理 SDL_Texture// IYUV: Y + U + V  (3 planes)// YV12: Y + V + U  (3 planes)sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, frame_width, frame_height);if (sdlTexture == nullptr){printf("SDL: Could not create texture - exiting:%s\n", SDL_GetError());return -1;}sdlRect.x = 0;sdlRect.y = 0;sdlRect.w = frame_width;sdlRect.h = frame_height;return 0;
}/* SDL 音频回调函数
*
* 开始播放后,会有音频其他子线程来调用回调函数,进行音频数据的补充,经过测试每次补充 4096 个字节
* The audio function callback takes the following parameters:
* stream: A pointer to the audio buffer to be filled
* len: The length (in bytes) of the audio buffer
*
*/
void fill_audio_pcm2(void* udata, Uint8* stream, int len)
{// 获取当前系统时钟audio_callback_time = av_gettime();// SDL 2.0SDL_memset(stream, 0, len);if (audio_len == 0) /* Only play if we have data left */return;/* Mix as much data as possible */len = ((Uint32)len > audio_len ? audio_len : len);/* 混音播放函数* dst: 目标数据,这个是回调函数里面的 stream 指针指向的,直接使用回调的 stream 指针即可* src: 音频数据,这个是将需要播放的音频数据混到 stream 里面去,那么这里就是我们需要填充的播放的数据* len: 音频数据的长度* volume: 音量,范围 0~128 ,SAL_MIX_MAXVOLUME 为 128,设置的是软音量,不是硬件的音响*/SDL_MixAudio(stream, audio_pos, len, SDL_MIX_MAXVOLUME / 2);audio_pos += len;audio_len -= len;
}// 关闭 SDL
void closeSDL()
{// 关闭音频设备SDL_CloseAudio();// 释放 SDL 资源SDL_DestroyWindow(sdlWindow);sdlWindow = nullptr;SDL_DestroyRenderer(sdlRenderer);sdlRenderer = nullptr;SDL_DestroyTexture(sdlTexture);sdlTexture = nullptr;// 退出 SDL 系统SDL_Quit();
}

结果

测试发现,该程序能成功解码各种格式的视频,但只能正确播放 AAC 音频。

在这里插入图片描述

工程文件下载

GitHub:UestcXiye / Simplest-FFmpeg-Sync-Player

CSDN:Simplest FFmpeg Sync Player.zip

参考链接

  1. 《 100行代码实现最简单的基于FFMPEG+SDL的视频播放器(SDL1.x)》
  2. FFmpeg音视频同步
  3. 使用FFMPEG和SDL2实现音视频同步的简易视频播放器

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

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

相关文章

【Java 多线程】从源码出发,剖析Threadlocal的数据结构

文章目录 exampleset(T value)createMap(t, value);set(ThreadLocal<?> key, Object value)ThreadLocalMap和Thread的关系 全貌 ThreadLocal是个很重要的多线程类&#xff0c;里面数据结构的设计很有意思&#xff0c;很巧妙。但是我们平时使用它的时候常常容易对它的使用…

24年大一训练一(东北林业大学)

前言&#xff1a; 周五晚上的训练赛&#xff0c;以后应该每两周都会有一次。 正文&#xff1a; Problem:A矩阵翻转&#xff1a; #include<bits/stdc.h> using namespace std; int a[55][55]; int main(){int n,m;while(cin>>n>>m){for(int i1;i<n;i){for…

2024.3.30学习笔记

今日学习韩顺平java0200_韩顺平Java_对象机制练习_哔哩哔哩_bilibili 今日学习p295-p314 super关键字 super代表父类的引用&#xff0c;用于访问父类的属性、方法、构造器 super细节和语法 访问父类的属性&#xff0c;但不能访问父类的private属性 super.属性名 访问父类的…

CubeIDE 下如何将版本号和日期关联。

1. 使用__DATE__ 和__TIME__获取编译日期和时间。 2. 将__DATE__ 和__TIME__转换成UINT 3. 将转换后的数赋值给版本号。 4. 设置工程保证每次都会重新编译对应文件。 对应函数如下&#xff1a; uint8_t VER_MAIN; uint8_t VER_SUB; uint8_t VER_MIN; #include <stdlib.…

蓝桥杯刷题第四天

思路&#xff1a; 这道题很容易即可发现就是简单的暴力即可完成题目&#xff0c;我们只需满足所有数的和为偶数即可保证有满足条件的分法&#xff0c;同时也不需要存下每个输入的数据&#xff0c;只需要知道他是偶数还是奇数即可&#xff0c;因为我们只需要偶数个奇数搭配在一块…

使用通用内部函数对代码进行矢量化

返回&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;OpenCV 如何使用 XML 和 YAML 文件的文件输入和输出 下一篇&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; ​ 目标 本教程的目标是提供使用通用内…

[flask]请求全局钩子

flask从入门到精通之钩子、异常、context、jinjia模板、过滤器 - 异步非阻塞 - 博客园 (cnblogs.com) 参考的这个博客&#xff0c;但有一个需要注意的是&#xff0c;最新版本的flask不知道是不是更新了还是怎么了&#xff0c;他的before_first_request不见了&#xff0c;如果继…

《极客时间TonyBai go语言第一课》学习笔记

文章目录 前置篇显式组合 大纲 前置篇 显式 在 C 语言中&#xff0c;下面这段代码可以正常编译并输出正确结果&#xff1a; #include <stdio.h> int main() { short int a 5; int b 8; long c 0; c a b; printf("%ld\n", c); }我们看到在上面这段代码中…

【git】git使用手册

目录 一 初始化 1.1 账号配置 1.2 ssh生成 1.2.1 配置ssh 1.2.2 测试SSH 1.3 初始化本地仓库并关联远程仓库 二 使用 2.1 上传 2.2 拉取 三 问题 3.1 关联失败 一 初始化 git的安装很简单,下载后大部分进行下一步完成即可----->地址: git工具下载 1.1 账号配置…

UE4_碰撞_自定义碰撞检测通道

效果如图&#xff1a; 1、项目设置中新建追踪检测通道weapon&#xff0c;默认值为忽略。 2、新建几个actor作为枪&#xff0c;碰撞预设全部设为自定义&#xff0c;把新建的检测响应weapon设为阻挡。 3、角色进行射线检测 运行效果如下&#xff1a; 发现有些物体碰不到&#xff…

算法系列--动态规划--背包问题(4)--完全背包拓展题目

&#x1f495;"这种低水平质量的攻击根本就不值得我躲&#xff01;"&#x1f495; 作者&#xff1a;Lvzi 文章主要内容&#xff1a;算法系列–动态规划–背包问题(4)–完全背包拓展题目 大家好,今天为大家带来的是算法系列--动态规划--背包问题(4)--完全背包拓展题目…

Linux 常见性能分析方法论介绍(业务负载画像、下钻分析、USE方法论,检查清单)

写在前面 博文内容为 《BPF Performance Tools》 读书笔记整理内容涉及常用的性能调优方法论介绍&#xff1a;业务负载画像下钻分析USE方法论检查清单理解不足小伙伴帮忙指正 不必太纠结于当下&#xff0c;也不必太忧虑未来&#xff0c;当你经历过一些事情的时候&#xff0c;眼…

时序预测 | Matlab实现OOA-BP鱼鹰算法优化BP神经网络时间序列预测

时序预测 | Matlab实现OOA-BP鱼鹰算法优化BP神经网络时间序列预测 目录 时序预测 | Matlab实现OOA-BP鱼鹰算法优化BP神经网络时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现OOA-BP鱼鹰算法优化BP神经网络时间序列预测&#xff08;完整源码和数据…

Github profile Readme实现小游戏[github自述游戏]

Github profile Readme常用于个人主页介绍&#xff0c;将它与action自动化流程结合&#xff0c;可以实现一些小游戏 例如&#xff1a;2048、五子棋 2048实现 losehu (RUBO) GitHub 五子棋 https://github.com/losehu/losehu/tree/main 通过python/C编写可执行文件&#xf…

相机标定学习记录

相机标定是计算机视觉和机器视觉领域中的一项基本技术&#xff0c;它的主要目的是通过获取相机的内部参数&#xff08;内参&#xff09;和外部参数&#xff08;外参&#xff09;&#xff0c;以及镜头畸变参数&#xff0c;建立起现实世界中的点与相机成像平面上对应像素点之间准…

[linux初阶][vim-gcc-gdb] TwoCharter: gcc编译器

目录 一.Linux中gcc编译器的下载与安装 二.使用gcc编译器来翻译 C语言程序 ①.编写C语言代码 ②翻译C语言代码 a.预处理 b.编译 c.汇编 d.链接 ③.执行Main 二进制可执行程序(.exe文件) 三.总结 一.Linux中gcc编译器的下载与安装 使用yum命令(相当于手机上的应用…

2024年AI大模型基础设施栈市场地图

2024年大模型(LLM)基础架构的组件和工具,最近看到国外的一篇深度分析,技术负责人可以重点关注:附带图谱: 一、现代LLM基础设施栈定义 根据Menlo Ventures的数据,2023年企业在现代AI基础设施栈上的支出超过11亿美元,成为生成式AI中最大的新市场,为初创企业提供了巨大的…

Spring Web MVC的入门学习(一)

目录 一、什么是 Spring Web MVC 1、MVC 定义 二、学习Spring MVC 1、项目准备 2、建立连接 2.1 RequestMapping 注解的学习 2.2 RequestMapping 使用 3、请求 3.1 传递单个参数 3.2 传递多个参数 3.3 传递对象 3.4 后端参数重命名&#xff08;后端参数映射&#xf…

Cortex-A7 常用汇编指令

一、处理器内部数据传输指令 使用处理器做的最多事情就是在处理器内部来回的传递数据&#xff0c;常见的操作有&#xff1a; ①、将数据从一个寄存器传递到另外一个寄存器。 ②、将数据从一个寄存器传递到特殊寄存器&#xff0c;如 CPSR 和 SPSR 寄存器。 ③、将立即数传递到寄…

暴力破解pdf文档密码

首先安装pdfcrack工具包 apt install pdfcrack 默认密码字典存储在/usr/share/wordlists里&#xff0c;是gz文件&#xff0c;将它解压并copy到pdf目录 然后使用pdfcrack破解 密码在最后一行user-password的单引号里