1. FFmpeg+SDL播放音频
1.1. 一、代码实现
1.1.1. 增加头文件
#include <SDL_thread.h>
1.1.2. 定义一
// 定义一
// SDL读音频缓存的大小
#define SDL_AUDIO_BUFFER_SIZE 1024
#define MAX_AUDIO_FRAME_SIZE 192000
int quit = 0;// 全局退出进程标识,在界面上点了退出后,告诉线程退出
1.1.3. 定义二:数据包队列(链表)结构体
// 定义二:数据包队列(链表)结构体
/*-------链表节点结构体-------
typedef struct AVPacketList {
AVPacket pkt;//链表数据
struct AVPacketList *next;//链表后继节点
} AVPacketList;
---------------------------*/
typedef struct PacketQueue {
AVPacketList *first_pkt, *last_pkt;// 队列首尾节点指针
int nb_packets;// 队列长度
int size;// 保存编码数据的缓存长度,size=packet->size
SDL_mutex *qlock;// 队列互斥量,保护队列数据
SDL_cond *qready;// 队列就绪条件变量
} PacketQueue;
PacketQueue audioq;// 定义全局队列对象
1.1.4. 定义三:队列初始化函数
// 定义三:队列初始化函数
void packet_queue_init(PacketQueue *q) {
memset(q, 0, sizeof(PacketQueue));//全零初始化队列结构体对象
q->qlock = SDL_CreateMutex();//创建互斥量对象
q->qready = SDL_CreateCond();//创建条件变量对象
}
1.1.5. 定义四:向队列中插入数据包
// 定义四:向队列中插入数据包
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
/*-------准备队列(链表)节点对象------*/
AVPacketList *pktlist;// 创建链表节点对象指针
pktlist = static_cast<AVPacketList *>(av_malloc(sizeof(AVPacketList)));// 在堆上创建链表节点对象
if (!pktlist) {// 检查链表节点对象是否创建成功
return -1;
}
pktlist->pkt = *pkt;// 将输入数据包赋值给新建链表节点对象中的数据包对象
pktlist->next = NULL;// 链表后继指针为空
// if (av_packet_ref(pkt, pkt)<0) {// 增加pkt编码数据的引用计数(输入参数中的pkt与新建链表节点中的pkt共享同一缓存空间)
// return -1;
// }
/*---------将新建节点插入队列-------*/
SDL_LockMutex(q->qlock);// 队列互斥量加锁,保护队列数据
if (!q->last_pkt) {// 检查队列尾节点是否存在(检查队列是否为空)
q->first_pkt = pktlist;// 若不存在(队列尾空),则将当前节点作队列为首节点
}
else {
q->last_pkt->next = pktlist;// 若已存在尾节点,则将当前节点挂到尾节点的后继指针上,并作为新的尾节点
}
q->last_pkt = pktlist;// 将当前节点作为新的尾节点
q->nb_packets++;// 队列长度+1
q->size += pktlist->pkt.size;// 更新队列编码数据的缓存长度
SDL_CondSignal(q->qready);// 给等待线程发出消息,通知队列已就绪
SDL_UnlockMutex(q->qlock);// 释放互斥量
return 0;
}
1.1.6. 定义五:从队列中提取数据包,并将提取的数据包出队列
// 定义五:从队列中提取数据包,并将提取的数据包出队列
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) {
AVPacketList *pktlist;// 临时链表节点对象指针
int ret;// 操作结果
SDL_LockMutex(q->qlock);// 队列互斥量加锁,保护队列数据
for (;;) {
if (quit) {// 检查退出进程标识
ret = -1;// 操作失败
break;
}
pktlist = q->first_pkt;// 传递将队列首个数据包指针
if (pktlist) {// 检查数据包是否为空(队列是否有数据)
q->first_pkt = pktlist->next;// 队列首节点指针后移
if (!q->first_pkt) {// 检查首节点的后继节点是否存在
q->last_pkt = NULL;// 若不存在,则将尾节点指针置空
}
q->nb_packets--;// 队列长度-1
q->size -= pktlist->pkt.size;// 更新队列编码数据的缓存长度
*pkt = pktlist->pkt;// 将队列首节点数据返回
av_free(pktlist);// 清空临时节点数据(清空首节点数据,首节点出队列)
ret = 1;// 操作成功
break;
} else if (!block) {
ret = 0;
break;
} else {// 队列处于未就绪状态,此时通过SDL_CondWait函数等待qready就绪信号,并暂时对互斥量解锁
/*---------------------
* 等待队列就绪信号qready,并对互斥量暂时解锁
* 此时线程处于阻塞状态,并置于等待条件就绪的线程列表上
* 使得该线程只在临界区资源就绪后才被唤醒,而不至于线程被频繁切换
* 该函数返回时,互斥量再次被锁住,并执行后续操作
--------------------*/
SDL_CondWait(q->qready, q->qlock);// 暂时解锁互斥量并将自己阻塞,等待临界区资源就绪(等待SDL_CondSignal发出临界区资源就绪的信号)
}
}// end for for-loop
SDL_UnlockMutex(q->qlock);// 释放互斥量
return ret;
}
1.1.7. 定义六:音频解码
/*---------------------------
* 定义六:音频解码
* 从缓存队列中提取数据包、解码,并返回解码后的数据长度(对一个完整的packet解码,将解码数据写入audio_buf缓存,并返回多帧解码数据的总长度)
* aCodecCtx:音频解码器上下文
* audio_buf:保存解码一个完整的packe后的原始音频数据(缓存中可能包含多帧解码后的音频数据)
* buf_size:解码后的音频数据长度,未使用
--------------------------*/
int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf, int buf_size) {
static AVPacket pkt;// 保存从队列中提取的数据包
static AVFrame frame;// 保存从数据包中解码的音频数据
static uint8_t *audio_pkt_data = NULL;// 保存数据包编码数据缓存指针
static int audio_pkt_size = 0;// 数据包中剩余的编码数据长度
int coded_consumed_size, data_size = 0;// 每次消耗的编码数据长度[input](len1),输出原始音频数据的缓存长度[output]
for (;;) {
while(audio_pkt_size>0) {// 检查缓存中剩余的编码数据长度(是否已完成一个完整的pakcet包的解码,一个数据包中可能包含多个音频编码帧)
int got_frame = 0;// 解码操作成功标识,成功返回非零值
coded_consumed_size=avcodec_decode_audio4(aCodecCtx,&frame,&got_frame,&pkt);//解码一帧音频数据,并返回消耗的编码数据长度
if (coded_consumed_size < 0) {// 检查是否执行了解码操作
// if error, skip frame.
audio_pkt_size = 0;// 更新编码数据缓存长度
break;
}
audio_pkt_data += coded_consumed_size;// 更新编码数据缓存指针位置
audio_pkt_size -= coded_consumed_size;// 更新缓存中剩余的编码数据长度
if (got_frame) {// 检查解码操作是否成功
// 计算解码后音频数据长度[output]
data_size=av_samples_get_buffer_size(NULL,aCodecCtx->channels,frame.nb_samples,aCodecCtx->sample_fmt,1);
memcpy(audio_buf, frame.data[0], data_size);// 将解码数据复制到输出缓存
}
if (data_size <= 0) {// 检查输出解码数据缓存长度
// No data yet, get more frames.
continue;
}
// We have data, return it and come back for more later.
return data_size;// 返回解码数据缓存长度
}// end for while
if (pkt.data) {// 检查数据包是否已从队列中提取
av_packet_unref(&pkt);// 释放pkt中保存的编码数据
}
if (quit) {// 检查退出进程标识
return -1;
}
// 从队列中提取数据包到pkt
if (packet_queue_get(&audioq, &pkt,1)<0) {
return -1;
}
audio_pkt_data = pkt.data;// 传递编码数据缓存指针
audio_pkt_size = pkt.size;// 传递编码数据缓存长度
}// end for for-loop
}
1.1.8. 定义七:音频输出回调函数
/*------Audio Callback-------
* 定义七:音频输出回调函数
* sdl通过该回调函数将解码后的pcm数据送入声卡播放,
* sdl通常一次会准备一组缓存pcm数据,通过该回调送入声卡,声卡根据音频pts依次播放pcm数据
* 待送入缓存的pcm数据完成播放后,再载入一组新的pcm缓存数据(每次音频输出缓存为空时,sdl就调用此函数填充音频输出缓存,并送入声卡播放)
* When we begin playing audio, SDL will continually call this callback function
* and ask it to fill the audio buffer with a certain number of bytes
* 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,缓存长度wanted_spec.samples=SDL_AUDIO_BUFFER_SIZE(1024)
--------------------------*/
void audio_callback(void *userdata, Uint8 *stream, int len) {
AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;// 传递用户数据
int wt_stream_len, audio_size;// 每次写入stream的数据长度,解码后的数据长度
static uint8_t audio_buf[(MAX_AUDIO_FRAME_SIZE*3)/2];// 保存解码一个packet后的多帧原始音频数据
static unsigned int audio_buf_size = 0;// 解码后的多帧音频数据长度
static unsigned int audio_buf_index = 0;// 累计写入stream的长度
while (len>0) {// 检查音频缓存的剩余长度
if (audio_buf_index >= audio_buf_size) {// 检查是否需要执行解码操作
// We have already sent all our data; get more,从缓存队列中提取数据包、解码,并返回解码后的数据长度,audio_buf缓存中可能包含多帧解码后的音频数据
audio_size = audio_decode_frame(aCodecCtx, audio_buf, audio_buf_size);
if (audio_size < 0) {// 检查解码操作是否成功
// If error, output silence.
audio_buf_size = 1024; // arbitrary?
memset(audio_buf, 0, audio_buf_size);// 全零重置缓冲区
} else {
audio_buf_size = audio_size;// 返回packet中包含的原始音频数据长度(多帧)
}
audio_buf_index = 0;// 初始化累计写入缓存长度
}// end for if
wt_stream_len = audio_buf_size-audio_buf_index;// 计算解码缓存剩余长度
if (wt_stream_len > len) {// 检查每次写入缓存的数据长度是否超过指定长度(1024)
wt_stream_len = len;// 指定长度从解码的缓存中取数据
}
// 每次从解码的缓存数据中以指定长度抽取数据并写入stream传递给声卡
memcpy(stream,(uint8_t*)audio_buf+audio_buf_index,wt_stream_len);
len -= wt_stream_len;// 更新解码音频缓存的剩余长度
stream += wt_stream_len;// 更新缓存写入位置
audio_buf_index += wt_stream_len;// 更新累计写入缓存数据长度
}// end for while
}
1.1.9. 第一步:注册组件
/*-------------------------
* 第一步:注册组件
* 注册所有ffmpeg支持的多媒体格式及编解码器
-------------------------*/
av_register_all();
1.1.10. 第二步:打开封装格式
/*-------------------------
* 第二步:打开封装格式
* 打开视频文件,读文件头内容,取得文件容器的封装信息及码流参数并存储在avformat_context中
* 参数一:封装格式上下文
* 参数二:视频路径
* 参数三:指定输入的格式
* 参数四:设置默认参数
--------------------------*/
AVFormatContext* avformat_context = avformat_alloc_context();// 参数一:封装格式上下文
const char *url = "/storage/emulated/0/Download/test.mov";// 参数二:视频路径
int avformat_open_input_result = avformat_open_input(&avformat_context, url, NULL, NULL);
if (avformat_open_input_result != 0){
__android_log_print(ANDROID_LOG_INFO, "main", "查找音视频流\n");
return -1;
}
1.1.11. 第三步:查找码流
/*-------------------------
* 第三步:查找码流
* 取得文件中保存的码流信息,并填充到avformat_context->stream 字段
* 参数一:封装格式上下文
* 参数二:指定默认配置
-------------------------*/
int avformat_find_stream_info_result = avformat_find_stream_info(avformat_context, NULL);
if (avformat_find_stream_info_result < 0){
__android_log_print(ANDROID_LOG_INFO, "main", "查找音视频流失败\n");
return -1;
}
1.1.12. 第四步:查找解码器
// 第四步:查找解码器
// 视频流类型标号初始化为-1
int av_video_stream_index = -1;
// 音频流类型标号初始化为-1
int av_audio_stream_index = -1;
for (int i = 0; i < avformat_context->nb_streams; ++i) {
// 若文件中包含有视频流
if (avformat_context->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO){
av_video_stream_index = i;
}
// 若文件中包含有音频流
if (avformat_context->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO){
av_audio_stream_index = i;
}
}
// 检查文件中是否存在视频流
if (av_video_stream_index == -1) {
__android_log_print(ANDROID_LOG_INFO, "main", "没有找到视频流\n");
return -1;
}
// 检查文件中是否存在音频流
if (av_audio_stream_index == -1) {
__android_log_print(ANDROID_LOG_INFO, "main", "没有找到音频流\n");
return -1;
}
// 根据流类型标号从avformat_context->streams中取得流对应的解码器上下文
AVCodecContext *video_avcodec_context = avformat_context->streams[av_video_stream_index]->codec;
AVCodecContext *audio_avcodec_context = avformat_context->streams[av_audio_stream_index]->codec;
// 根据流对应的解码器上下文查找对应的解码器,返回对应的解码器(信息结构体)
AVCodec *video_avcodec = avcodec_find_decoder(video_avcodec_context->codec_id);
AVCodec *audio_avcodec = avcodec_find_decoder(audio_avcodec_context->codec_id);
// 检查视频解码器
if (!video_avcodec) {
__android_log_print(ANDROID_LOG_INFO, "main", "没找到视频解码器\n");
return -1;
}
// 检查音频解码器
if (!audio_avcodec) {
__android_log_print(ANDROID_LOG_INFO, "main", "没找到音频解码器\n");
return -1;
}
1.1.13. 第五步:打开解码器
// 第五步:打开解码器
// 打开视频解码器
int avcodec_open2_result = avcodec_open2(video_avcodec_context, video_avcodec, NULL);
if (avcodec_open2_result != 0){
__android_log_print(ANDROID_LOG_INFO, "main", "打开视频解码器失败\n");
return -1;
}
// 打开音频解码器
avcodec_open2_result = avcodec_open2(audio_avcodec_context, audio_avcodec, NULL);
if (avcodec_open2_result != 0){
__android_log_print(ANDROID_LOG_INFO, "main", "打开音频解码器失败\n");
return -1;
}
// 打印解码器信息
__android_log_print(ANDROID_LOG_INFO, "main", "视频解码器:%s\n", video_avcodec->name);
__android_log_print(ANDROID_LOG_INFO, "main", "音频解码器:%s\n", audio_avcodec->name);
1.1.14. 第六步:定义类型转换参数
/*-------------------------
* 第六步:定义类型转换参数
* 参数一:原始视频像素数据格式宽
* 参数二:原始视频像素数据格式高
* 参数三:原始视频像素数据格式类型
* 参数四:目标视频像素数据格式宽
* 参数五:目标视频像素数据格式高
* 参数六:目标视频像素数据格式类型
-------------------------*/
// 设置图像转换像素格式为AV_PIX_FMT_YUV420P
SwsContext *swscontext = sws_getContext(video_avcodec_context->width,
video_avcodec_context->height,
video_avcodec_context->pix_fmt,
video_avcodec_context->width,
video_avcodec_context->height,
AV_PIX_FMT_YUV420P,
SWS_BICUBIC,
NULL,
NULL,
NULL);
// 保存音视频解码后的数据,如状态信息、编解码器信息、宏块类型表,QP表,运动矢量表等数据
AVFrame* avframe_in = av_frame_alloc();
// 定义解码结果
int decode_result = 0;
// AV_PIX_FMT_YUV420P格式的视频帧
AVFrame* avframe_yuv420p = av_frame_alloc();
// 给缓冲区设置类型
int buffer_size =av_image_get_buffer_size(AV_PIX_FMT_YUV420P,// 视频像素数据格式类型
video_avcodec_context->width,// 一帧视频像素数据宽 = 视频宽
video_avcodec_context->height,// 一帧视频像素数据高 = 视频高
1);// 字节对齐方式,默认是1
// 开辟一块内存空间
uint8_t *out_buffer = (uint8_t *)av_malloc(buffer_size);
// 向avframe_yuv420p填充数据
av_image_fill_arrays(avframe_yuv420p->data,// 目标视频帧数据
avframe_yuv420p->linesize,// 目标视频帧行大小
out_buffer,// 原始数据
AV_PIX_FMT_YUV420P,// 视频像素数据格式类型
video_avcodec_context->width,// 视频宽
video_avcodec_context->height,//视频高
1);// 字节对齐方式
1.1.15. 第七步:初始化SDL多媒体框架
// 第七步:初始化SDL多媒体框架
if (SDL_Init( SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER ) == -1) {
__android_log_print(ANDROID_LOG_INFO, "main", "初始化失败:%s", SDL_GetError());
// Mac使用
// printf("初始化失败:%s", SDL_GetError());
return -1;
}
1.1.16. 第八步:缓存队列初始化
// 第八步:缓存队列初始化
packet_queue_init(&audioq);
1.1.17. 第九步:设置音频播放参数
// 第九步:设置音频播放参数
// SDL_AudioSpec a structure that contains the audio output format,创建 SDL_AudioSpec 结构体,设置音频播放数据
SDL_AudioSpec wanted_spec, spec;
// 创建SDL_AudioSpec结构体,设置音频播放参数
// 采样频率 DSP frequency -- samples per second
wanted_spec.freq = audio_avcodec_context->sample_rate;
// 采样格式 Audio data format
wanted_spec.format = AUDIO_S16SYS;
// 声道数 Number of channels: 1 mono, 2 stereo
wanted_spec.channels = audio_avcodec_context->channels;
wanted_spec.silence = 0;// 无输出时是否静音
// 默认每次读音频缓存的大小,推荐值为 512~8192,ffplay使用的是1024
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
// 设置取音频数据的回调接口函数 the function to call when the audio device needs more data
wanted_spec.callback = audio_callback;
// 传递用户数据
wanted_spec.userdata = audio_avcodec_context;
1.1.18. 第十步:打开音频设备
/*--------------------------
* 第十步:打开音频设备
* 以指定参数打开音频设备,并返回与指定参数最为接近的参数,该参数为设备实际支持的音频参数
* Opens the audio device with the desired parameters(wanted_spec)
* return another specs we actually be using
* and not guaranteed to get what we asked for
--------------------------*/
if (SDL_OpenAudio(&wanted_spec, &spec)<0) {
fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
return -1;
}
// 开启音频设备,如果这时候没有获得数据那么它就静音
SDL_PauseAudio(0);
1.1.19. 第十一步:初始化SDL窗口
// 第十一步:初始化SDL窗口
SDL_Window* sdl_window = SDL_CreateWindow("FFmpeg+SDL播放视频",// 参数一:窗口名称
SDL_WINDOWPOS_CENTERED,// 参数二:窗口在屏幕上的x坐标
SDL_WINDOWPOS_CENTERED,// 参数三:窗口在屏幕上的y坐标
video_avcodec_context->width,// 参数四:窗口在屏幕上宽
video_avcodec_context->height,// 参数五:窗口在屏幕上高
SDL_WINDOW_OPENGL);// 参数六:窗口状态(打开)
if (sdl_window == NULL){
__android_log_print(ANDROID_LOG_INFO, "main", "窗口创建失败:%s", SDL_GetError());
// Mac使用
// printf("窗口创建失败: %s\n", SDL_GetError());
// 退出程序
SDL_Quit();
return -1;
}
__android_log_print(ANDROID_LOG_INFO, "main", "窗口创建成功,width:%d,height:%d", video_avcodec_context->width, video_avcodec_context->height);
1.1.20. 第十二步:创建渲染器
// 第十二步:创建渲染器
// 定义渲染器区域
SDL_Rect sdl_rect;
SDL_Renderer* sdl_renderer = SDL_CreateRenderer(sdl_window,// 渲染目标创建
-1, // 从那里开始渲染(-1:表示从第一个位置开始)
0);// 渲染类型(软件渲染)
if (sdl_renderer == NULL){
__android_log_print(ANDROID_LOG_INFO, "main", "渲染器创建失败:%s", SDL_GetError());
// Mac使用
// printf("渲染器创建失败: %s\n", SDL_GetError());
// 退出程序
SDL_Quit();
return -1;
}
1.1.21. 第十三步:创建纹理
// 第十三步:创建纹理
SDL_Texture* sdl_texture = SDL_CreateTexture(sdl_renderer,// 渲染器
SDL_PIXELFORMAT_IYUV,// 像素数据格式
SDL_TEXTUREACCESS_STREAMING,// 绘制方式:频繁绘制-
video_avcodec_context->width,// 纹理宽
video_avcodec_context->height);// 纹理高
if (sdl_texture == NULL) {
__android_log_print(ANDROID_LOG_INFO, "main", "纹理创建失败:%s", SDL_GetError());
// Mac使用
// printf("纹理创建失败: %s\n", SDL_GetError());
// 退出程序
SDL_Quit();
return -1;
}
1.1.22. 第十四步:读取视频压缩数据帧
// 第十四步:读取视频压缩数据帧
int video_current_index = 0;
// 负责保存压缩编码数据相关信息的结构体,每帧图像由一到多个packet包组成
AVPacket* packet = (AVPacket*)av_malloc(sizeof(AVPacket));
// 从文件中依次读取每个图像编码数据包,并存储在AVPacket数据结构中,>=:读取到了,<0:读取错误或者读取完毕
while (av_read_frame(avformat_context, packet) >= 0) {
// 检查数据包类型是否是视频流
if (packet->stream_index == av_video_stream_index) {
/*-----------------------
* 第十五步:视频解码
* 解码完整的一帧数据,decode_result返回true
* 可能无法通过只解码一个packet就获得一个完整的视频帧frame,可能需要读取多个packet才行
* avcodec_receive_frame()会在解码到完整的一帧时,decode_result为true
-----------------------*/
// ...
video_current_index++;
__android_log_print(ANDROID_LOG_INFO, "main", "当前解码视频第%d帧", video_current_index);
}
// 检查数据包类型是否是音频流
else if (packet->stream_index == av_audio_stream_index) {
// 第二十一步:向缓存队列中填充编码数据包
// ...
}
// 字幕流类型标识
else {
// 释放AVPacket数据结构中编码数据指针
av_packet_free(&packet);
}
/*------------------------
* 第二十二步:获取SDL事件
* 在每次循环中从SDL后台队列取事件并填充到SDL_Event对象中
* SDL的事件系统使得你可以接收用户的输入,从而完成一些控制操作
------------------------*/
// ...
}
1.1.23. 第十五步:视频解码
/*-----------------------
* 第十五步:视频解码
* 解码完整的一帧数据,decode_result返回true
* 可能无法通过只解码一个packet就获得一个完整的视频帧frame,可能需要读取多个packet才行
* avcodec_receive_frame()会在解码到完整的一帧时,decode_result为true
-----------------------*/
// 发送一帧视频压缩数据
avcodec_send_packet(video_avcodec_context, packet);
// 解码一帧视频数据
decode_result = avcodec_receive_frame(video_avcodec_context, avframe_in);
if (decode_result == 0) {
// 视频解码成功
// 第十六步:开始类型转换
// ...
// 第十七步:设置纹理数据
// ...
// 第十八步:将纹理数据拷贝给渲染器
// ...
// 第十九步:呈现画面帧
// ...
// 第二十步:渲染每一帧直接间隔时间
// ...
}
1.1.24. 第十六步:开始类型转换
// 第十六步:开始类型转换
// 将解码出来的视频像素点数据格式统一转类型为yuv420P
sws_scale(swscontext,// 视频像素数据格式上下文
(const uint8_t *const *)avframe_in->data,// 输入数据
avframe_in->linesize,// 输入画面每一行大小
0,// 输入画面每一行开始位置(0表示从原点开始读取)
video_avcodec_context->height,// 输入数据行数
avframe_yuv420p->data,// 输出数据
avframe_yuv420p->linesize);// 输出画面每一行大小
1.1.25. 第十七步:设置纹理数据
// 第十七步:设置纹理数据
SDL_UpdateTexture(sdl_texture, // 纹理
NULL,// 渲染区域
avframe_yuv420p->data[0],// 需要渲染数据:视频像素数据帧
avframe_yuv420p->linesize[0]);// 帧宽
1.1.26. 第十八步:将纹理数据拷贝给渲染器
// 第十八步:将纹理数据拷贝给渲染器
// 设置左上角位置(全屏)
sdl_rect.x = 100;
sdl_rect.y = 100;
sdl_rect.w = video_avcodec_context->width;
sdl_rect.h = video_avcodec_context->height;
SDL_RenderClear(sdl_renderer);
SDL_RenderCopy(sdl_renderer, sdl_texture, NULL, &sdl_rect);
1.1.27. 第十九步:呈现画面帧
// 第十九步:呈现画面帧
SDL_RenderPresent(sdl_renderer);
1.1.28. 第二十步:渲染每一帧直接间隔时间
// 第二十步:渲染每一帧直接间隔时间
SDL_Delay(30);
1.1.29. 第二十一步:向缓存队列中填充编码数据包
// 第二十一步:向缓存队列中填充编码数据包
packet_queue_put(&audioq, packet);
1.1.30. 第二十二步:获取SDL事件
/*------------------------
* 第二十二步:获取SDL事件
* 在每次循环中从SDL后台队列取事件并填充到SDL_Event对象中
* SDL的事件系统使得你可以接收用户的输入,从而完成一些控制操作
------------------------*/
SDL_Event event;//SDL事件对象
SDL_PollEvent(&event);
switch (event.type) {//检查SDL事件对象
case SDL_QUIT://退出事件
quit = 1;//退出进程标识置1
SDL_Quit();//退出操作
exit(0);//结束进程
break;
default:
break;
}// end for switch
1.1.31. 第二十三步:释放资源,退出程序
// 第二十三步:释放资源,退出程序
av_packet_free(&packet);
av_frame_free(&avframe_in);
av_frame_free(&avframe_yuv420p);
free(out_buffer);
avcodec_close(video_avcodec_context);
avformat_free_context(avformat_context);
SDL_DestroyTexture(sdl_texture);
SDL_DestroyRenderer(sdl_renderer);
SDL_Quit();
1.2. 一、Android实现
1.2.1. 第一步:Android集成SDL
参考:http://www.1221.site/FFmpeg/08_SDL%E6%92%AD%E6%94%BEYUV.html
新建工程名称为AndroidDisplayVideoWhileDecoding
,按上面文章配置,能正常播放YUV文件就算成功完成了第一步。
1.2.2. 第二步:Android集成FFmpeg
参考:http://www.1221.site/FFmpeg/02_FFmpeg%E9%9B%86%E6%88%90.html
1.2.3. 第三步:修改native-lib.cpp
#include <jni.h>
#include <string>
#include <android/log.h>
#include <errno.h>
#include "SDL.h"
#include <SDL_thread.h>
extern "C" {
// 引入头文件
// 核心库->音视频编解码库
#include <libavcodec/avcodec.h>
#include "libavformat/avformat.h"
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
// 定义一
// SDL读音频缓存的大小
#define SDL_AUDIO_BUFFER_SIZE 1024
#define MAX_AUDIO_FRAME_SIZE 192000
int quit = 0;// 全局退出进程标识,在界面上点了退出后,告诉线程退出
// SDL入口
extern "C"
int main(int argc, char *argv[]) {
// 边解码边播放音视频实现
// 复制代码实现
}