基于 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.hqwc.cn/news/577183.html

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

相关文章

Python 多线程同步锁实战

大家好&#xff0c;今天我们要聊聊Python中的多线程世界&#xff0c;你知道吗&#xff1f;在并行处理任务时&#xff0c;多线程就像厨房里的大厨们同时烹饪多个菜品&#xff0c;但得保证每道菜都能完美出锅。这就需要我们引入一个神秘的角色——同步锁&#xff08;Lock&#xf…

win10配置CLion2022+ubuntu20.04远程部署

背景 在博文ubunut搭建aarch64 cuda交叉编译环境记录中&#xff0c;使用的ubuntu20.04虚拟机安装eclipse来交叉编译aarch64的程序&#xff0c;然后发送到jetson板子上执行。开发一段时间后发现eclipse IDE使用起来不太便捷&#xff0c;因此&#xff0c;考虑使用CLion IDE&…

设计模式-设配器模式

目录 &#x1f38a;1.适配器模式介绍 &#x1f383;2.适配器类型 &#x1f38f;3.接口适配器 &#x1f390;4.类的适配器 &#x1f38e;5.优缺点 1.适配器模式介绍 适配器模式&#xff08;Adapter Pattern&#xff09;是作为两个不兼容的接口之间的桥梁。这种类型的设…

Kibana操作Elasticsearch教程

文章目录 简介ES文档操作创建索引查看索引创建映射字段查看映射关系字段属性详解typeindexstore 字段映射设置流程 新增数据新增会随机生成id新增自定义id智能判断 修改数据删除数据查询基本查询查询所有&#xff08;match_all&#xff09;匹配查询多字段查询词条匹配多词条精确…

Android 开发 OCR Tesseract4Android图片文字识别 巨详细全部代码教程

下面是整个详解步骤过程 效果图一、OCR的含义二、前提准备二、使用步骤详情1、将 JitPack 存储库添加到存储库末尾的项目根文件2、将依赖项添加到应用模块文件3、数据文件下载路径4、详情代码案例Demo如下&#xff1a;Main.xmlMain.java 效果图 流程&#xff1a;获取assets中的…

数据分析之POWER Piovt的KPI设置

内容总结&#xff1a; 1.两个表格关联不上&#xff1a;需要添加辅助列&#xff0c;建立关联 2.添加辅助列后还关联不上&#xff1a;将虚线变为实线 3.根据需求要增加一些度量值 4.设置KPI后&#xff0c;绝对值选1后设定百分比 5.在透视表里面加入KPI状态 导入所关联的数据后建立…

从关键词到上下文:GPT 如何重新定义 SEO 策略

如何利用GPT技术革新SEO内容创建&#xff1f; 新的 SEO 格局 探索 SEO 的快速变化&#xff0c;重点关注从以关键字为中心的策略到更深入地了解用户意图和上下文的转变。 GPT 简介及其对内容创建、用户参与和搜索引擎优化 (SEO) 的革命性影响。 了解 GPT&#xff1a;技术范式转…

基于单片机的便携式瓦斯检测仪系统设计

**单片机设计介绍&#xff0c;基于单片机的便携式瓦斯检测仪系统设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机的便携式瓦斯检测仪系统设计是一个针对煤矿等工业环境中瓦斯气体浓度检测的重要项目。以下是该设计…

HarmonyOS实战开发-使用List组件实现导航与内容联动的效果。

1 卡片介绍 使用ArkTS语言&#xff0c;实现一个导航与内容二级联动的效果。 2 标题 二级联动&#xff08;ArkTS&#xff09; 3 介绍 本篇Codelab是主要介绍了如何基于List组件实现一个导航和内容的二级联动效果。样例主要包含以下功能&#xff1a; 切换左侧导航&#xff…

2.2.1.3-移动平均线

跳转到根目录&#xff1a;知行合一&#xff1a;投资篇 已完成&#xff1a; 1、投资&技术   1.1.1 投资-编程基础-numpy   1.1.2 投资-编程基础-pandas   1.2 金融数据处理   1.3 金融数据可视化 2、投资方法论   2.1.1 预期年化收益率   2.1.2 一个关于yaxb的…

8722ES安捷伦8722ES网络分析仪

181/2461/8938产品概述&#xff1a; Agilent 8722ES 包括集成合成源、测试装置和调谐接收器。内置 S 参数测试仪可提供正向和反向的全范围幅度和相位测量。内置矢量精度增强技术包括完整的两端口、适配器移除和可选的 TRL 校准。 其他功能和规格包括&#xff1a; 频率&#x…

【八大排序】一篇文章搞定所有排序

文章目录 1.排序的概念2.常见排序算法的实现2.1 插入排序2.1.1直接插入排序2.1.2希尔排序 2.2选择排序2.2.1直接选择排序:2.2.2堆排序 2.3交换排序2.3.1冒泡排序2.3.2快速排序Hoare法前后指针法挖坑法非递归版本 2.4归并排序递归版本非递归版本 2.5计数排序3.排序的比较 1.排序…