基于 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; ///< 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.mfbz.cn/a/498971.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue 元素拖动,复制,已复制元素可移动,快捷方便,已解决

注意&#xff1a;使用当前组件时&#xff0c;请先了解组件代码逻辑 下方组件根据自己的需求来更改响应的元素id&#xff0c;调整代码实现逻辑&#xff0c;这里不过多解释 import Vue from "vue";/*** 拖拽*/ Vue.directive("Drag", (el) > {const move…

MySQL---函数

目录 一、概述 二、字符串函数 三、数值函数 四、日期函数 五、流程函数 一、概述 函数 是指一段可以直接被另一段程序调用的程序或代码。 也就意味着&#xff0c;这一段程序或代码在MySQL 中 已经给我们提供了&#xff0c;我们要做的就是在合适的业务场景调用对应的函数完…

课堂练习:环境体验——Linux 文件操作命令

任务描述 第二个任务就是了解Linxu的文件查看命令&#xff0c;文件编辑基本命令。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a; 1.文件查看命令。 2.文件编辑基本命令。 文件查看命令 我们要查看一些文本文件的内容时&#xff0c;要使用文本编辑器来查看…

vue3+ts白屏问题解决

文章目录 打开白屏解决方法可能出现问题使用base导致的使用baseUrl导致的 注意点vue3ts白屏问题知识分享 打开白屏 解决方法 在vue.config.js页面 添加publicPath:./, const { defineConfig } require(vue/cli-service)module.exports defineConfig({ transpileDependenci…

MATLAB:优化与规划问题

一、线性规划 % 线性规划&#xff08;Linear programming, 简称LP&#xff09; fcoff -[75 120 90 105]; % 目标函数系数向量 A [9 4 7 54 5 6 105 10 8 53 8 9 77 6 4 8]; % 约束不等式系数矩阵 b [3600 2900 3000 2800 2200]; % 约束不等式右端向量 Aeq []; % 约束等式系…

搭建本地局域网域名并配置本地的mqtt服务器

1. 第一步&#xff1a; 首先准备一台windows电脑&#xff0c;安装 Technitium DNS Server 链接如下&#xff1a; Technitium DNS Server | An Open Source DNS Server For Privacy & Security 启动 start 然后进入 http://localhost:5380/ 下载完成之后&#xff0c;需要…

数字三角形 Number Triangles

题目描述 观察下面的数字金字塔。 写一个程序来查找从最高点到底部任意处结束的路径&#xff0c;使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。 在上面的样例中&#xff0c;从 7 → 3 → 8 → 7 → 5 7 \to 3 \to 8 \to 7 \to 5 7→3→8→7→5 的…

【JAVAEE学习】探究Java中多线程的使用和重点及考点

˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱ ʕ̯•͡˔•̯᷅ʔ大家好&#xff0c;我是xiaoxie.希望你看完之后,有不足之处请多多谅解&#xff0c;让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客 本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如…

vue 内嵌第三方网页

需要将另一个系统嵌套到当前网页中 一、frame 方法一就是通过html的标签 iframe 实现网页中嵌入其他网站 标签属性 属性含义src嵌套的网页地址width设置嵌套网页的宽度&#xff0c;单位为像素height设置嵌套网页的高度&#xff0c;单位为像素frameborder控制嵌套的网页是否…

【CC工具箱1.2.5】更新_免费无套路,70+个工具

CC工具箱目前已经更新到1.2.5版本&#xff0c;完全免费无套路。 适用版本ArcGIS Pro 3.0及以上。 欢迎大家使用&#xff0c;反馈bug&#xff0c;以及提出需求和意见&#xff0c;时间和能力允许的话我会尽量满足要求。 如有关于工具的使用问题和需求建议&#xff0c;可以加下…

使用unplugin-auto-import页面不引入api飘红

解决方案&#xff1a;. tsconfig.json文件夹加上 {"compilerOptions": {"target": "ES2020","useDefineForClassFields": true,"module": "ESNext","lib": ["ES2020", "DOM", &q…

使用Python进行微服务架构的设计与实现【第159篇—微服务架构】

&#x1f47d;发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 使用Python进行微服务架构的设计与实现 在当今软件开发领域中&#xff0c;微服务架构已经成…

结构体,联合体,枚举( 1 )

目录 前言 1.结构体 1.1结构体的声明 1.2结构体变量的创建和初始化 1.3结构体成员的访问字符 1.4结构体的内存大小 1.4.1对齐规则 1.5结构体传参 前言 在编程的世界里&#xff0c;数据结构的选择对于程序的效率和可读性有着至关重要的影响。不同的数据结构适用于不同的…

华为WATCH 4是怎么监测我们健康的?真的有用吗?

最近&#xff0c;总听到身边的朋友说手表帮他们发现了不少健康的问题&#xff0c;所以我也想整一个来试试看。看了很多款手表后&#xff0c;发现华为WATCH 4还挺符合我的需求&#xff0c;它有一系列超实用的健康监测功能&#xff0c;可以说是随身的健康小助手。 先来说说心脏…

企微侧边栏开发(内部应用内嵌H5)

一、背景 公司的业务需要用企业微信和客户进行沟通&#xff0c;而客户的个人信息基本都存储在内部CRM系统中&#xff0c;对于销售来说需要一边看企微&#xff0c;一边去内部CRM系统查询&#xff0c;比较麻烦&#xff0c;希望能在企微增加一个侧边栏展示客户的详细信息&#xf…

电脑最高可以装多少内存?电脑内存怎么装?

大家好&#xff0c;我是来自兼容性之家的&#xff01; 通常我们的家用电脑主机有8到16GB的运行内存。 极少数高端用户会使用32至64GB内存。 比较高端的工作站的内存在128GB左右。 同时&#xff0c;家用电脑的硬盘容量约为1TB。 那么你有没有想过一台电脑可以拥有的最大内存量…

网站业务对接DDoS高防

准备需要接入的网站域名清单&#xff0c;包含网站的源站服务器IP&#xff08;仅支持公网IP的防护&#xff09;、端口信息等。所接入的网站域名必须已完成ICP备案。如果您的网站支持HTTPS协议访问&#xff0c;您需要准备相应的证书和私钥信息&#xff0c;一般包含格式为.crt的公…

Kafka入门到实战-第二弹

Kafka入门到实战 Kafka快速开始官网地址Kafka概述Kafka术语Kafka初体验更新计划 Kafka快速开始 官网地址 声明: 由于操作系统, 版本更新等原因, 文章所列内容不一定100%复现, 还要以官方信息为准 https://kafka.apache.org/Kafka概述 Apache Kafka 是一个开源的分布式事件流…

AI写作工具哪家强?推荐11款AI写作生成器

AI写作工具近年来在技术的不断进步和应用的不断拓展下&#xff0c;逐渐成为人们创作、撰写的得力助手。然而&#xff0c;市面上涌现出的众多AI写作生成器&#xff0c;究竟哪家强呢&#xff1f;以下是11款方便实用的AI写作生成器&#xff0c;它们提供了快速、智能的写作功能&…

今天是我和 “代码” 分手的第100天...

smardaten迎来了一位特殊用户——杨钏&#xff0c;一名去年刚毕业的Java开发工程师。 进入的第一家ISV公司在与数睿数据达成长期合作的战略背景下&#xff0c;从现有开发人马中抽出成员&#xff0c;创建了由smardaten无代码工具完成交付的项目团队。 杨钏作为公司近年入职的新…
最新文章