基于 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.mzph.cn/news/783379.shtml

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

相关文章

Linux: 进程优先级

Linux: 进程优先级 一、进程优先级概念二、如何查看进程优先级三、如何修改进程的优先级&#xff08;PRL vs NI&#xff09;四、为何优先级PRL必须限定范围五、进程其他特性 一、进程优先级概念 优先级的本质就是排队&#xff0c;而排队则是资源不足所引起的。在计算机中&#…

鸿蒙ARKTS--简易的购物网站

目录 一、media 二、string.json文件 三、pages 3.1 登录页面&#xff1a;gouwuPage.ets 3.2 PageResource.ets 3.3 商品页面&#xff1a;shangpinPage.ets 3.4 我的页面&#xff1a;wodePage.ets 3.5 注册页面&#xff1a;zhucePage.ets 3. 购物网站主页面&#xff…

STM32学习笔记(10_2)- I2C通信协议MPU6050简介

无人问津也好&#xff0c;技不如人也罢&#xff0c;都应静下心来&#xff0c;去做该做的事。 最近在学STM32&#xff0c;所以也开贴记录一下主要内容&#xff0c;省的过目即忘。视频教程为江科大&#xff08;改名江协科技&#xff09;&#xff0c;网站jiangxiekeji.com 本期开…

[Linux_IMX6ULL驱动开发]-基础驱动

驱动的含义 如何理解嵌入式的驱动呢&#xff0c;我个人认为&#xff0c;驱动就是嵌入式上层应用操控底层硬件的桥梁。因为上层应用是在用户态&#xff0c;是无法直接操控底层的硬件的。我们需要利用系统调用&#xff08;open、read、write等&#xff09;&#xff0c;进入内核态…

Java编程实战:疫情物资分配系统的设计与实现

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

Qt 富文本处理 (字体颜色大小加粗等)

Qt中支持HTML的控件有textEdit 、label 、textBrowser 。 接口&#xff1a;setHtml("Qt"); toHtml(). 文本样式设置 : 可分字设置 &#xff0c;主要使用QTextCharFormat类进行文本样式设置。 示例&#xff1a; QTextCharFormat fmt; //粗体 fmt.setFontWeight…

查找算法及查找常用数据结构总结

1.顺序表查找 基本方法&#xff1a; 设查找表以一维数组来存储&#xff0c;要求在此表中查找出关键字的值为x的元素的位置&#xff0c;若查找成功&#xff0c;则返回其位置&#xff08;即下标&#xff09;&#xff0c;否则&#xff0c;返回一个表示元素不存在的下标&#xff0…

VMware虚拟机共享主机v2rayN

目录 &#x1f33c;前言 &#x1f33c;解释 &#x1f6a9;操作 1&#xff09;VMware -- 虚拟网络编辑器 2&#xff09;VMware -- 网络适配器 3&#xff09;主机 IP 地址 4&#xff09;v2rayN 代理端口 5&#xff09;VMware -- 网络代理(Network proxy) &#x1f382;结…

红黑树介绍及插入操作的实现

&#x1f389;个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名乐于分享在学习道路上收获的大二在校生 &#x1f648;个人主页&#x1f389;&#xff1a;GOTXX &#x1f43c;个人WeChat&#xff1a;ILXOXVJE &#x1f43c;本文由GOTXX原创&#xff0c;首发CSDN&…

windows平台虚拟机安装

windows平台虚拟机安装 1. 安装VMwareWorkstationPro 1.1 软件下载 官网下载 官网 百度网盘下载 版本 VMwareWorkstationPro16 链接&#xff1a;https://pan.baidu.com/s/1LidMxoM9e4a4CANixyRoyg?pwd1157 提取码&#xff1a;1157 1.2 软件安装 软件安装注意事项 软件…

代码学习第32天---动态规划

随想录日记part32 t i m e &#xff1a; time&#xff1a; time&#xff1a; 2024.03.30 主要内容&#xff1a;今天开始要学习动态规划的相关知识了&#xff0c;今天的内容主要涉及两个方面&#xff1a; 不同路径 &#xff1b; 不同路径 II。 62.不同路径 63. 不同路径 II 动态…

Canvas实现圆点动画

示例效果图&#xff1a; 话不多说直接上代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><t…

Unity类银河恶魔城学习记录11-10 p112 Items drop源代码

Alex教程每一P的教程原代码加上我自己的理解初步理解写的注释&#xff0c;可供学习Alex教程的人参考 此代码仅为较上一P有所改变的代码 【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili ItemObject_Trigger.cs using System.Collections; using System.Collecti…

P28—P31:变量

P28-变量的定义 什么是变量&#xff1f; 从本质上来说&#xff0c;变量就是一块内存空间&#xff0c;而这块内存空间有数据类型、名字、字面值。变量包含三部分&#xff1a;数据类型、名字、字面值&#xff08;数据&#xff09;变量是内存中存储的基本单元。 数据类型的作用&a…

Linux 查看磁盘信息:df与du命令详解

一、df 1.简介 df 是 disk free的缩写&#xff0c;从UNIX和类UNIX操作系统的早期开始&#xff0c;它就是UNIX和类UNIX操作系统的一部分。它被设计为一种工具&#xff0c;用于监视系统上已使用和可用的磁盘空间数量。 df 命令主要用于需要检查文件系统上已使用和可用的磁盘空…

第十三届蓝桥杯JavaA组省赛真题 - GCD

解题思路&#xff1a; 找规律 最大的最小公因数就是两数的差值 5 7 gcd2 1 3 gcd2 1 4 gcd3 import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner scan new Scanner(System.in);long a scan.nextLong();long b scan.ne…

查找--二分查找(Binary Search)

二分查找属于静态查找表&#xff0c;当以有序表表示静态查找表时&#xff0c;查找函数可用折半查找来实现。 查找过程&#xff1a;先确定待查记录所在的范围&#xff08;区间&#xff09;&#xff0c;然后逐步缩小范围直到找到或找不到该记录为止。 以处于区间中间位置记录的…

Linux appimage如何正确打开

在之前的文章中&#xff0c;提到使用appimage软件非常方便。 但是首次使用会遇到这样的问题&#xff1a; 1. 双击打不开 2. 在终端打开提示&#xff1a; /home/roy/software/appimage/Obsidian-1.5.11.AppImage dlopen(): error loading libfuse.so.2 AppImages require …

Python下载bing每日壁纸并实现win11 壁纸自动切换

前言: 爬虫哪家强,当然是python 我是属于啥语言都用,都懂点,不精通,实际工作中能能够顶上就可以。去年写的抓取bing每日的壁纸&#xff0c;保存到本地&#xff0c;并上传到阿里云oss&#xff0c;如果只是本地壁纸切换&#xff0c;存下来就行&#xff0c;一直想做个壁纸站点&…

vuees6新语法

vue的学习网站&#xff1a; https://www.runoob.com/vue2/vue-tutorial.html1.Vue的介绍 学习目标 说出什么是Vue能够说出Vue的好处能够说出Vue的特点 内容讲解 【1】Vue介绍 1.vue属于一个前端框架&#xff0c;底层使用原生js编写的。主要用来进行前端和后台服务器之间的…