视频解封装之后就会得到音频流和视频流,解封状得到的数据是AVPackage类型数据,需要进一步解码成AVFrame一帧一帧数据才能进行播放。
1.从AVPackage队列获取数据进行解码操作
pthread_create(&pid_video_decode, nullptr, task_video_decode, this);
void *task_video_decode(void *args) {
auto *video_channel = static_cast<VideoChannel *>(args);
video_channel->video_decode();
return nullptr;
}
/**
* 视频解码操作
* 把队列里面的压缩包(AVPacket *)取出来,然后解码成(AVFrame * )原始包,保存到视频帧队列
*/
void VideoChannel::video_decode() {
AVPacket *pkt = 0;
while (isPlaying) {
if (isPlaying&& frames.size() > AV_MAX_SIZE){
av_usleep(10*000);
continue;
}
/*阻塞式函数*/
int ret = packets.getQueueAndDel(pkt);
if (!isPlaying) {
/*如果关闭了播放跳出循环,releaseAVPacket(&pkt);*/
break;
}
if (!ret) {
/* 可能是生产数据慢没拿到数据*/
continue;
}
/*
* 直接将拿到的 AVPackage* 数据通过avcodec_send_packet 函数丢给ffmpeg缓冲区 解码操作
* */
ret = avcodec_send_packet(codecContext, pkt);
if (ret) {
break;
}
AVFrame *frame = av_frame_alloc();
/*下面是从 Fmpeg缓冲区 获取原始包AVFrame数据,解码后的视频原始数据包yuv420数据*/
ret = avcodec_receive_frame(codecContext, frame);
if (ret == AVERROR(EAGAIN)) {
continue;
} else if (ret != 0) {
if (frame){
releaseAVFrame(&frame);
}
break;
}
/*将原始包放到 帧队列 */
frames.insertToQueue(frame);
/*使用完记得释放 否则会造成内容泄漏*/
av_packet_unref(pkt);
releaseAVPacket(&pkt);
}
/*发生异常释放 指针*/
av_packet_unref(pkt);
releaseAVPacket(&pkt);
}
- 由于是耗时操作,所以先pthread_create创建线程
- 开启循环从AVPackage队列获取AVPackage数据进行解码操作
- frames 是视频帧队列,队列的阈值是AV_MAX_SIZE,这个值可以自己设置,不要太大,否则在队列保存的数据太大
- packets.getQueueAndDel(pkt):从队列获取AVPackage类型数据,阻塞队列,如果队列为空会进行阻塞等待。
- avcodec_send_packet(codecContext, pkt):将获取到的 AVPackage* 数据通过avcodec_send_packet 函数丢给ffmpeg缓冲区 解码操作
- avcodec_receive_frame:从ffmpeg缓冲区 获取原始包AVFrame数据,原始数据包yuv420数据,获取帧数据后,insertToQueue加入到视频帧队列。
2.从视频帧队列获取数据进行播放
pthread_create(&pid_video_play, nullptr, task_video_play, this);
/*线程回调函数*/
void *task_video_play(void *args) {
auto *video_channel = static_cast<VideoChannel *>(args);
video_channel->video_play();
return nullptr;
}
/**
* 把队列里面的原始包(AVFrame *)取出来播放
*/
void VideoChannel::video_play() {
AVFrame *frame = nullptr;
/*接收RGBA数据*/
uint8_t *dst_data[4];
/*接收RGBA 数据长度*/
int dst_linesize[4];
/*
* 原始包(YUV数据) 通过libswscale方法进行数据转换 Android屏幕(RGBA数据)
* dst_data 申请内存 width * height * 4
* */
av_image_alloc(dst_data, dst_linesize,
codecContext->width, codecContext->height,
AV_PIX_FMT_RGBA, 1);
/*
* 初始化转换上下文:SwsContext
* 第七个参数:yuv 转rgba flag :SWS_FAST_BILINEAR,快速双线性,速度快可能会模糊,
* 所以选择SWS_BILINEAR普通双线性就好
* */
SwsContext *sws_ctx = sws_getContext(
/*输入环节*/
codecContext->width,
codecContext->height,
/*自动获取 xxx.mp4 的像素格式 AV_PIX_FMT_YUV420P*/
codecContext->pix_fmt,
/*输出环节*/
codecContext->width,
codecContext->height,
AV_PIX_FMT_RGBA,
SWS_BILINEAR, NULL, NULL, NULL);
while (isPlaying) {
int ret = frames.getQueueAndDel(frame);
if (!isPlaying) {
/*如果关闭了播放跳出循环,releaseAVPacket(&pkt);*/
break;
}
if (!ret) {
/* 压缩包加入队列慢,继续*/
continue;
}
/*
* 将yuv格式数据转换成rgba数据
* */
sws_scale(sws_ctx,
/*输入环节 YUV的数据*/
frame->data, frame->linesize,
0, codecContext->height,
/*输出环节:RGBA数据*/
dst_data,
dst_linesize
);
/*
* 将得到的rgba数据进行回调,回调给ANativeWindow进行渲染播放
* */
renderCallback(dst_data[0], codecContext->width, codecContext->height, dst_linesize[0]);
/*释放原始包,已经被渲染完了*/
releaseAVFrame(&frame);
}
/*出现错误,所退出的循环,都要释放frame*/
releaseAVFrame(&frame);
isPlaying = false;
av_free(&dst_data[0]);
sws_freeContext(sws_ctx);
}
- 播放会涉及到 视频帧yuv转rgba耗时操作,pthread_create创建线程处理。
- sws_getContext:初始化转换上下文SwsContext,第七个参数是yuv 转rgba flag :SWS_FAST_BILINEAR,快速双线性,速度快可能会模糊, 所以选择SWS_BILINEAR普通双线性就好。
- sws_scale:将获取到的AVFrame数据进行数据转换,转成RGBA数据
- renderCallback:将RGBA数据回调,通过ANativeWindow进行渲染播放
3.RGBA数据通过AnativeWindow渲染播放
3.1.初始化ANativeWindow
pthread_mutex_lock(&mutex);
/*先释放之前的显示窗口*/
if (window) {
ANativeWindow_release(window);
window = 0;
}
/*创建新的窗口用于视频显示*/
window = ANativeWindow_fromSurface(env, surface);
pthread_mutex_unlock(&mutex);
- pthread_mutex_lock(&mutex):为了线程安全,加锁
- ANativeWindow_release(window):如果之前有初始化,先释放
- ANativeWindow_fromSurface(env, surface):从SurfaceView中获取ANativeWindow
3.2.将rgba数据渲染到ANativeWindow
/*为了线程安全加锁*/
pthread_mutex_lock(&mutex);
if (!window) {
/*出现了问题后,释放锁,避免出现死锁问题*/
pthread_mutex_unlock(&mutex);
}
/*设置窗口的大小,各个属性*/
ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);
/*声明ANativeWindow缓冲区*/
ANativeWindow_Buffer window_buffer;
/*如果在渲染的时候被锁住的,就无法渲染需要释放 ,防止出现死锁*/
if (ANativeWindow_lock(window, &window_buffer, 0)) {
ANativeWindow_release(window);
window = 0;
pthread_mutex_unlock(&mutex);
return;
}
/*
* 把rgba数据进行字节对齐 将数据丢给window_buffer画面就出来了
* */
uint8_t *dst_data = static_cast<uint8_t *>(window_buffer.bits);
/*目标行大小*/
int dst_linesize = window_buffer.stride * 4;
/*
* 图:一行一行显示 循环遍历高度
* 视频分辨率:426 * 240
* 视频分辨率:宽 426
* 一行数据的大小 426 * 4(rgba8888) = 1704
* memcpy(dst_data + i * 1704, src_data + i * 1704, 1704); 直接这样处理会花屏幕
* 花屏原因:ANativeWindow_Buffer 64字节对齐的算法, 1704无法以64位字节对齐
* FFmpeg是默认采用8字节对齐的,他就认为没有问题, 但是ANativeWindow_Buffer他是64字节对齐的,就有问题
* */
for (int i = 0; i < window_buffer.height; ++i) {
/*
*C库函数 void *memcpy(void *str1, const void *str2, size_t n) 从存储区 str2 复制 n 个字节到存储区 str1。
* 将src_data + i * src_lineSize存储区的数据 复制dst_linesize个字节 到 dst_data + i * dst_linesize存储区
* */
memcpy(dst_data + i * dst_linesize, src_data + i * src_lineSize, dst_linesize);
}
/*
* 解锁后并且刷新window_buffer的数据显示画面
* */
ANativeWindow_unlockAndPost(window);
pthread_mutex_unlock(&mutex);
- ANativeWindow_setBuffersGeometry:设置ANativeWindow 属性,宽、高、数据格式。
- window_buffer.stride * 4:得到目标行大小,不能直接使用src_lineSize大小,因为FFmpeg是默认采用8字节对齐的,ANativeWindow是64字节对齐的,直接使用src_lineSize会导致花屏。
- memcpy:循环按照高度遍历将数据进行拷贝window_buffer中,然后刷新就能显示画面了。
总结:视频封装格式解码到播放流程:从队列获取AVPackage数据->解码成AVFrame数据->数据格式转化成rgba数据->将rgba数据循环遍历拷贝到ANativeWindow缓冲区->刷新就能显示出画面了。