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

news2024/11/25 4:22:49

基于 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; ///< numerator
   int 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_MACROS

extern "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;
	else
		q->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)
	{
		// 从视频帧队列中取出一个 AVPacket
		packet_queue_get(&video_pkt_queue, &video_pkt);
		// Send packet to decoder
		ret = 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 decoder
			ret = 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 * duration
			video_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)
	{
		// 从音频帧队列中取出一个 AVPacket
		packet_queue_get(&audio_pkt_queue, &audio_pkt);
		// Send packet to decoder
		ret = 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 decoder
			ret = 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 paranoia
				fprintf(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 finish
			while (audio_len > 0)
			{
				// 使用 SDL_Delay 进行 1ms 的延迟,用当前缓存区剩余未播放的长度大于 0 结合前面的延迟进行等待
				SDL_Delay(1);
			}

			free(pcm_buffer);
		}
	}
	return 0;
}

// 解复用线程
int open_file_thread(void* data)
{
	// 读取一个 AVPacket
	while (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 Info
	printf("--------------- 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();

	// 初始化 SDL
	initSDL();

	// 创建互斥量
	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);

	// 关闭 SDL
	closeSDL();

	// 释放 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_Window
	sdlWindow = 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_Renderer
	sdlRenderer = 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.0
	SDL_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.coloradmin.cn/o/1559312.html

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

相关文章

js垃圾回收新生代和老生代以及堆栈内存详细

js 堆栈内存、新生代和老生代、垃圾回收详聊 要想了解JS内存管理就必须明白存这些js数据的内存又分为&#xff1a;栈内存和堆内存 一、 栈|堆内存(Stack|Heap) 栈(Stack)内存 原始值&#xff1a;Number、String、Boolean、Null、Undefined、Symbol和BigInt 栈内存主要存储原始…

Mac使用“Workstation”安装双系统

## 选择虚拟机 Mac推荐使用“VMware” 优点 1.个人版是免费的 2.界面清晰&#xff0c;运行流程 3.使用人群广&#xff0c;遇到问题容易解决 版本比较 VMware Workstation Pro 和 VMware Workstation Player 个人使用推荐 VMware Workstation Player &#xff0c;因为个人的…

安卓玩机工具推荐----MTK芯片读写分区 备份分区 恢复分区 制作线刷包 从0开始 工具操作解析【三】

同类博文; 安卓玩机工具推荐----MTK芯片读写分区 备份分区 恢复分区 制作线刷包 工具操作解析 安卓玩机工具推荐----MTK芯片读写分区 备份分区 恢复分区 制作线刷包 工具操作解析【二】-CSDN博客 回顾以往 在以前的博文简单介绍了这款工具的rom制作全程。今天针对这款工具的…

【博弈论3——二人博弈的纳什均衡】

1.俾斯麦海之战 2. 零和博弈的定义 零和博弈&#xff08;Zero-Sum Game&#xff09;是一种博弈论的基本概念&#xff0c;指的是在博弈过程中&#xff0c;博弈参与者之间的收益和损失之和总是一个常数&#xff0c;特别是总和为零。即博弈一方的收益必然等于另一方的损失&#x…

Chatgpt掘金之旅—有爱AI商业实战篇(二)

演示站点&#xff1a; https://ai.uaai.cn 对话模块 官方论坛&#xff1a; www.jingyuai.com 京娱AI 一、前言&#xff1a; 成为一名商业作者是一个蕴含着无限可能的职业选择。在当下数字化的时代&#xff0c;作家们有着众多的平台可以展示和推广自己的作品。无论您是对写书、文…

hcip综合实验2

目录 实验拓扑&#xff1a; 实验要求&#xff1a; 实验思路&#xff1a; 实验步骤&#xff1a; 1.配置设备接口IP 2.通过配置缺省路由让公网互通 3.配置ppp 1.R1和R5间的ppp的PAP认证&#xff1b; 2.R2与R5之间的ppp的CHAP认证; 3. R3与R5之间的HDLC封装; 4.构建R1、…

mysql安装遇到的问题

最近mysql安装遇到了许多问题 这个界面是下载器界面&#xff0c;reconfigure是重新配置这个版本&#xff0c;要新安装要点add 进入这个界面选择对应的版本下载

向量点积的推导

1、余弦定理 2、二维点积 三、三维点积 用到第一个余弦定理

9.处理消息边界

网络编程中消息的长度是不太确定的&#xff0c;read方法读取字节数据到ByteBuffer中&#xff0c;ByteBuffer会有一个固定容量&#xff0c;单次超出容量的部分字节数据将会在下一次的ByteBuffer中&#xff0c;这样消息就会按照字节截断&#xff0c;出现消息边界问题。 Http 2.0 …

UE4_碰撞_碰撞蓝图节点——Line Trace For Objects(对象的线条检测)

一、Line Trace For Objects&#xff08;对象的线条检测&#xff09;&#xff1a;沿给定线条执行碰撞检测并返回遭遇的首个命中&#xff0c;这只会找到由Object types指定类型的对象。注意他与Line Trace By Channel(由通道检测线条&#xff09;的区别&#xff0c;一个通过Obje…

解决AD使用交互式BOM插件时,插入make点导致显示异常的问题

记得上次写了一篇关于使用这个插件时出现这个问题的解决方法&#xff0c;具体可查看&#xff1a;AD使用交互式BOM插件时应该注意到的一个问题_ad的bom插件-CSDN博客 当时的解决办法就是删除后再运行脚本生成&#xff0c;这些天经过多次实验&#xff0c;发现是当时那个封装有问…

ES的RestClient相关操作

ES的RestClient相关操作 Elasticsearch使用Java操作。 本文仅介绍CURD索引库和文档&#xff01;&#xff01;&#xff01; Elasticsearch基础&#xff1a;https://blog.csdn.net/weixin_46533577/article/details/137207222 Elasticsearch Clients官网&#xff1a;https://ww…

实例、构造函数、原型、原型对象、prototype、__proto__、原型链……

学习原型链和原型对象&#xff0c;不需要说太多话&#xff0c;只需要给你看看几张图&#xff0c;你自然就懂了。 prototype 表示原型对象__proto__ 表示原型 实例、构造函数和原型对象 以 error 举例 图中的 error 表示 axios 抛出的一个错误对象&#xff08;实例&#xff0…

Makefile:动态库的编译链接与使用(六)

1、动态链接库 动态链接库&#xff1a;不会把代码编译到二进制文件中&#xff0c;而是运行时才去加载&#xff0c;所以只需要维护一个地址 动态&#xff1a;运行时才去加载&#xff0c;即所谓的动态加载连接&#xff1a;指库文件和二进制程序分离&#xff0c;用某种特殊的手段…

字符串的遍历,统计,反转.java

题目&#xff1a;键盘输入字符串&#xff0c;统计字符串所包含的大小写字母个数&#xff0c;及数字个数 分析&#xff1a;键盘输入字符串需next&#xff08;&#xff09;方法&#xff0c;利用fot循环遍历每个字符&#xff0c;返回字符串上的字符用charAt&#xff08;index&…

图论-最短路

一、不存在负权边-dijkstra算法 dijkstra算法适用于这样一类问题&#xff1a; 从起点 start 到所有其他节点的最短路径。 其实求解最短路径最暴力的方法就是使用bfs广搜一下&#xff0c;但是要一次求得所有点的最短距离我们不可能循环n次&#xff0c;这样复杂度太高&#xf…

Mac air 个人免费版VMWare Fusion安装及配置教程

Mac air 安装免费版VMWare Fusion教程及问题解决 1、下载VMWare Fusion2、下载wins镜像文件3、开始配置4、出现的问题及解决方法4.1 如何跳过启动时的网络连接4.2 启动后&#xff0c;无法连接网络怎么办4.3 怎么实现将文件拖拽到虚拟机中 当你手上是一台Mac电脑&#xff0c;却需…

薪酬、人数上不封顶,这家互联网大厂正在疯抢超级毕业生

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了人工智能中文站https://ai.weoknow.com 每天给大家更新可用的国内可用chatGPT资源 发布在https://it.weoknow.com 更多资源欢迎关注 又是一年一度校园春招季。在生成式 AI 一路狂飙的时代浪潮下&#xff0c;人工…

Linux: 进程地址空间究竟是什么?进程地址空间存在意义何在?

Linux: 进程地址空间究竟是什么&#xff1f; 一、内存究竟是什么&#xff1f;分为哪些&#xff1f;二、内存是真实物理空间&#xff1f;三、进程地址空间&#xff08;虚拟地址&#xff09;3.1 为何同一个变量地址相同&#xff0c;保存的数据却不同&#xff1f; 四、为什么需要地…

ssm012医院住院管理系统+vue

医院住院管理关系 摘 要 随着时代的发展&#xff0c;医疗设备愈来愈完善&#xff0c;医院也变成人们生活中必不可少的场所。如今&#xff0c;已经2021年了&#xff0c;虽然医院的数量和设备愈加完善&#xff0c;但是老龄人口也越来越多。在如此大的人口压力下&#xff0c;医院…