NDK前期基础知识终于学完了,现在开始进入项目实战学习,通过FFmpeg实现一个简单的音视频播放器。
音视频播放器系列:
NDK FFmpeg音视频播放器一
NDK FFmpeg音视频播放器二
NDK FFmpeg音视频播放器三
NDK FFmpeg音视频播放器四
NDK FFmpeg音视频播放器五
NDK FFmpeg音视频播放器六
本文主要内容如下:
阻塞式队列SafeQueue。
音视频BaseChannel基础通道。
音视频压缩包加入队列。
视频解码与播放。
ANativeWindow渲染
用到的ffmpeg、rtmp等库资源:
https://wwgl.lanzout.com/iN21C0qiiija
音视频播放流程:
1.准备工作完成,音视频解封装后,
通过音视频媒体上下文AVFormatContext获取到具体的音视频压缩包AVPacket
2.将音视频压缩包AVPacket解压,得到音视频原始包AVFrame(可播放的文件包)
3.拿到音视频原始包AVFrame,进行播放。
代码逻辑:
1.获取压缩包AVPacket、获取原始包AVFrame、播放;是个生产消费,重复并发进行的过程,可以通过队列queue来完成。
2.创建两个队列queue,压缩包AVPacket队列和原始包AVFrame队列;
3.创建循环获取压缩包AVPacket,并push压缩包到AVPacket队列;
4.创建循环去AVPacket队列中获取压缩包AVPacket,解压得到原始包AVFrame,并push原始包到AVFrame队列;
5.创建循环去AVFrame队列中获取原始包AVFrame,进行播放;
6.音频和视频都有相同的解压、原始包、播放动作,故创建分别创建音频和视频队列,并封装到音频AudioChannel通道和视频VideoChannel通道中去处理;音频AudioChannel通道和视频VideoChannel通道,重复部分封装BaseChannel通道去。
一、阻塞式队列SafeQueue
封装线程安全队列SafeQueue,通过pthread_mutex_t互斥锁和pthread_cond_t条件变量来实现数据入队,出队,等待和唤醒工作。
#ifndef NDKPLAYER_SAFEQUEUE_H
#define NDKPLAYER_SAFEQUEUE_H
#include
#include
using namespace std;
/**
* 线程安全队列
* @tparam T 泛型:存放任意类型
*/
template
class SafeQueue {
private:
typedef void (*ReleaseCallback)(T *); // 函数指针定义 做回调 用来释放T里面的内容的
private:
queue queue;
pthread_mutex_t mutex; // 互斥锁 安全
pthread_cond_t cond; // 等待 和 唤醒
int work; // 标记队列是否工作
ReleaseCallback releaseCallback;
public:
SafeQueue() {
pthread_mutex_init(&mutex, 0); // 初始化互斥锁
pthread_cond_init(&cond, 0); // 初始化条件变量
}
virtual ~SafeQueue() {
pthread_mutex_destroy(&mutex); // 释放互斥锁
pthread_cond_destroy(&cond); // 释放条件变量
}
/**
* 入队 [ AVPacket * 压缩包] [ AVFrame * 原始包]
*/
void insertToQueue(T value) {
pthread_mutex_lock(&mutex); // 多线程的访问(先锁住)
if (work) {
// 工作状态,入队
queue.push(value);
// 当插入数据包 进队列后,发出通知唤醒
pthread_cond_signal(&cond);
} else {
//非工作状态,释放value
if (releaseCallback) {
releaseCallback(&value);
}
}
pthread_mutex_unlock(&mutex); // 多线程的访问(要解锁)
}
/**
* 出队 [ AVPacket * 压缩包] [ AVFrame * 原始包]
*/
int getQueueAndDel(T &value) {
int result = 0;
pthread_mutex_lock(&mutex); // 多线程的访问(先锁住)
while (work && queue.empty()) {
// 如果是工作 并且 队列里面没有数据,就阻塞在这里
pthread_cond_wait(&cond, &mutex);
}
if (!queue.empty()) {
// 取出队列的数据包 给外界,并删除队列数据包
value = queue.front();
// 删除队列中的数据
queue.pop();
// 成功 return true
result = 1;
}
pthread_mutex_unlock(&mutex); // 多线程的访问(要解锁)
return result;
}
/**
* 设置工作状态,设置队列是否工作
* @param work
*/
void setWork(int work) {
pthread_mutex_lock(&mutex); // 多线程的访问(先锁住)
this->work = work;
// 每次设置状态后,就去唤醒
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex); // 多线程的访问(要解锁)
}
int empty() {
return queue.empty();
}
int size() {
return queue.size();
}
/**
* 清空队列中所有的数据,循环一个一个的删除
*/
void clear() {
pthread_mutex_lock(&mutex); // 多线程的访问(先锁住)
unsigned int size = queue.size();
for (int i = 0; i < size; ++i) {
//循环释放队列中的数据
T value = queue.front();
if (releaseCallback) {
releaseCallback(&value); // 让外界去释放堆区空间
}
queue.pop(); // 删除队列中的数据,让队列为0
}
pthread_mutex_unlock(&mutex); // 多线程的访问(要解锁)
}
/**
* 设置此函数指针的回调,让外界去释放
* @param releaseCallback
*/
void setReleaseCallback(ReleaseCallback releaseCallback) {
this->releaseCallback = releaseCallback;
}
};
#endif //NDKPLAYER_SAFEQUEUE_H
二、音视频BaseChannel基础通道
BaseChannel封装压缩包和原始包队列
#ifndef NDKPLAYER_BASECHANNEL_H
#define NDKPLAYER_BASECHANNEL_H
extern "C" {
#include "ffmpeg/include/libavcodec/avcodec.h"
};
#include "SafeQueue.h"
#include
// log宏
#define TAG "NDK"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
class BaseChannel {
public:
int stream_index; // 音频 或 视频 的下标
SafeQueue packets; // 压缩的 数据包
SafeQueue frames; // 原始的 数据包
bool isPlaying; // 音频 和 视频 都会有的标记 是否播放
AVCodecContext *codecContext = 0; // 音频 视频 都需要的 解码器上下文
BaseChannel(int streamIndex, AVCodecContext *codecContext)
: stream_index(streamIndex), codecContext(codecContext) {
packets.setReleaseCallback(releaseAVPacket);
frames.setReleaseCallback(releaseAVFrame);
}
// 父类析构一定要加virtual
virtual ~BaseChannel() {
// 清空队列
packets.clear();
frames.clear();
}
/**
* 释放 队列中 所有的 AVPacket *
* typedef void (*ReleaseCallback)(T *);
*/
static void releaseAVPacket(AVPacket **pPacket) {
if (pPacket) {
// 释放队列里面的 T == AVPacket
av_packet_free(pPacket);
*pPacket = 0;
}
}
/**
* 释放 队列中 所有的 AVFrame *
* typedef void (*ReleaseCallback)(T *);
*/
static void releaseAVFrame(AVFrame **pFrame) {
if (pFrame) {
// 释放队列里面的 T == AVFrame
av_frame_free(pFrame);
*pFrame = 0;
}
}
};
#endif //NDKPLAYER_BASECHANNEL_H
三、音视频压缩包加入队列
创建子线程,把音频和视频 压缩包 加入队列里面去
/**
* 函数指针
* 此函数和NdkPlayer这个对象没有关系,你没法拿NdkPlayer的私有成员
* @return
*/
void *task_start(void *ndk_player) {
NdkPlayer *ndk_player_ = static_cast(ndk_player);
ndk_player_->start_();
return 0; // 必须返回,否则报错
}
void NdkPlayer::start() {
// 开始播放
isPlaying = 1;
// 音视频通道开始
if (audio_channel) {
audio_channel->start();
}
if (video_channel) {
video_channel->start();
}
// 创建子线程,把音频和视频 压缩包 加入队列里面去
pthread_create(&pid_start, 0, task_start, this);
}
/**
* 循环获取压缩包AVPacket,并push压缩包到队列
*/
void NdkPlayer::start_() {
LOGI("NdkPlayer::start_()");
while (isPlaying) {
// AVPacket 可能是音频 也可能是视频(压缩包)
AVPacket *packet = av_packet_alloc();
int result = av_read_frame(format_context, packet);
// @return 0 if OK
if (!result) {
// 把压缩包AVPacket 分别加入音频 和 视频队列
if (audio_channel && audio_channel->stream_index == packet->stream_index) {
// 音频
audio_channel->packets.insertToQueue(packet);
} else if (video_channel && video_channel->stream_index == packet->stream_index) {
// 视频
video_channel->packets.insertToQueue(packet);
}
} else if (result == AVERROR_EOF) {
// end of file == 读到文件末尾了 == AVERROR_EOF
// 表示读完了,要考虑释放播放完成,并不代表播放完毕
isPlaying = 0;
LOGI("NdkPlayer::start_() end");
} else {
// av_read_frame 出现了错误,结束当前循环
break;
}
} // end while
isPlaying = 0;
audio_channel->stop();
video_channel->stop();
}
四、视频解码与播放
第一个线程: 视频:取出队列的压缩包 进行编码 编码后的原始包 再push队列中去;
第二线线程:视频:从队列取出原始包,播放
#include "VideoChannel.h"
VideoChannel::VideoChannel(int streamIndex, AVCodecContext *codecContext)
: BaseChannel(streamIndex, codecContext) {
}
VideoChannel::~VideoChannel() {
}
void VideoChannel::stop() {
}
/**
* 函数指针 解码
* @param video_channel
* @return
*/
void *task_video_decode(void *video_channel) {
VideoChannel *video_channel_ = static_cast(video_channel);
video_channel_->video_decode();
return 0;
}
/**
* 函数指针 播放
* @param video_channel
* @return
*/
void *task_video_play(void *video_channel) {
VideoChannel *video_channel_ = static_cast(video_channel);
video_channel_->video_play();
return 0;
}
void VideoChannel::start() {
LOGI("VideoChannel::start()");
isPlaying = 1;
// 队列开始工作了
packets.setWork(1);
frames.setWork(1);
// 第一个线程: 视频:取出队列的压缩包 进行编码 编码后的原始包 再push队列中去
pthread_create(&pid_video_decode, 0, task_video_decode, this);
// 第二线线程:视频:从队列取出原始包,播放
pthread_create(&pid_video_play, 0, task_video_play, this);
}
/**
* 第一个线程: 视频:取出队列的压缩包 进行编码 编码后的原始包 再push队列中去
*/
void VideoChannel::video_decode() {
LOGI("VideoChannel::video_decode()");
AVPacket *pkt = 0;
while (isPlaying) {
// 获取AVPacket * 压缩包
int result = packets.getQueueAndDel(pkt);
if (!isPlaying) {
// 获取压缩包是耗时操作,获取完,如果关闭了播放,跳出循环
break;
}
if (!result) {
// 获取失败,可能是压缩包数据还没有加入队列,继续获取
continue;
}
// 1.发送pkt(压缩包)给缓冲区,@return 0 on success
result = avcodec_send_packet(codecContext, pkt);
// FFmpeg源码缓存一份pkt,释放即可
releaseAVPacket(&pkt);
if (result) {
// avcodec_send_packet 出现了错误
break;
}
AVFrame *frame = av_frame_alloc();
// 2.从缓冲区拿出来(原始包),@return 0: success
result = avcodec_receive_frame(codecContext, frame);
if (result == AVERROR(EAGAIN)) {
// B帧 B帧参考前面成功 B帧参考后面失败 可能是P帧没有出来,再拿一次就行了
continue;
} else if (result != 0) {
// avcodec_receive_frame 出现了错误
break;
}
// 拿到了原始包,并将原始包push到队列
frames.insertToQueue(frame);
}
// 解码获取原始包后,释放压缩包
releaseAVPacket(&pkt);
}
/**
* 第二线线程:视频:从队列取出原始包,播放
*/
void VideoChannel::video_play() {
LOGI("VideoChannel::video_play()");
AVFrame *frame = 0;
uint8_t *dst_data[4]; // RGBA 播放文件
int dst_linesize[4]; // RGBA
//给 dst_data 申请内存 width * height * 4 xxxx
av_image_alloc(dst_data, dst_linesize,
codecContext->width, codecContext->height, AV_PIX_FMT_RGBA, 1);
// SWS_BILINEAR 适中算法
SwsContext *sws_ctx = sws_getContext(
// 下面是输入环节
codecContext->width,
codecContext->height,
codecContext->pix_fmt, // 自动获取 xxx.mp4 的像素格式 AV_PIX_FMT_YUV420P // 写死的
// 下面是输出环节
codecContext->width,
codecContext->height,
AV_PIX_FMT_RGBA,
SWS_BILINEAR, NULL, NULL, NULL);
while (isPlaying) {
int result = frames.getQueueAndDel(frame);
if (!isPlaying) {
break; // 如果关闭了播放,跳出循环,releaseAVFrame(&frame);
}
if (!result) { // ret == 0
continue; // 哪怕是没有成功,也要继续(假设:你生产太慢(原始包加入队列),我消费就等一下你)
}
// 格式转换 yuv ---> rgba
sws_scale(sws_ctx,
// 下面是输入环节 YUV的数据
frame->data, frame->linesize,
0, codecContext->height,
// 下面是输出环节 成果:RGBA数据 dst_data
dst_data,
dst_linesize
);
/**
* ANatvieWindows 渲染工作
* SurfaceView ----- ANatvieWindows
* 这里拿不到Surface,只能函数指针renderCallback()将RGBA数据 dst_data 回调给 native-lib.cpp,显示
* 函数指针renderCallback()
* 参数1:RGBA数据 dst_data 数组被传递会退化成指针,默认就是取第1元素
* 参数2:视频宽
* 参数3:视频高
* 参数4:数据长度
*/
this->renderCallback(dst_data[0], codecContext->width, codecContext->height,
dst_linesize[0]);
// 释放原始包,因为已经被渲染完了,没用了
releaseAVFrame(&frame);
}
releaseAVFrame(&frame);
isPlaying = 0;
av_free(&dst_data[0]);
// free(sws_ctx); FFmpeg必须使用人家的函数释放,直接崩溃
sws_freeContext(sws_ctx);
}
void VideoChannel::setRenderCallback(RenderCallback renderCallback) {
this->renderCallback = renderCallback;
}
五、ANativeWindow渲染
1)初始化surfaceView
private SurfaceView surfaceView;
surfaceView = findViewById(R.id.surfaceView);
mNdkPlayer = new NdkPlayer(dataSource);
mNdkPlayer.setSurfaceHolder(surfaceView);
2)绑定surfaceHolder
public class NdkPlayer implements SurfaceHolder.Callback {
private SurfaceHolder surfaceHolder;
public void setSurfaceHolder(SurfaceView surfaceView) {
if (surfaceHolder != null) {
// 清除上一次数据
surfaceHolder.removeCallback(this);
}
this.surfaceHolder = surfaceView.getHolder();
// 添加监听
surfaceHolder.addCallback(this);
}
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
setSurfaceNative(holder.getSurface());
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
}
/**
* native函数区域
*/
private native void setSurfaceNative(Surface surface);
}
3)关联Native层ANativeWindow
ANativeWindow *window = 0;
/**
* 实例化播放window 关联 surfaceView
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_player_NdkPlayer_setSurfaceNative(JNIEnv *env, jobject thiz, jobject surface) {
// 线程安全,锁住
pthread_mutex_lock(&mutex);
// 先释放之前的显示窗口
if (window) {
ANativeWindow_release(window);
window = 0;
}
// 创建新的窗口用于视频显示
window = ANativeWindow_fromSurface(env, surface);
pthread_mutex_unlock(&mutex);
}
4)VideoChannel将解析完的RGBA数据(可播放数据)回调给 native-lib.cpp,进行渲染显示。
/**
* 定义函数指针 实现渲染工作,this->renderCallback()回调到这里来
*/
void renderCallback(uint8_t *dst_data, int width, int height, int dst_linesize) {
LOGI("native-lib::renderCallback playing");
pthread_mutex_lock(&mutex);
// 播放窗口为空,释放锁,小概率出现
if (!window) {
pthread_mutex_unlock(&mutex);
return;
}
// 设置窗口的大小,各个属性
ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);
// 定义缓冲区 buffer
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(window_buffer.bits);
// ANativeWindow_Buffer 64字节对齐的数据长度
int dst_linesize_ = window_buffer.stride * 4;
for (int i = 0; i < window_buffer.height; ++i) {
/**
* 参数1:接收播放数据容器
* 参数2:RGBA播放数据
* 参数3:64字节对齐的数据长度
*/
memcpy(dst_data_ + i * dst_linesize_, dst_data + i * dst_linesize, dst_linesize_);
}
// 解锁并且刷新 window_buffer的数据显示画面
ANativeWindow_unlockAndPost(window);
pthread_mutex_unlock(&mutex);
}
音视频--视频解码与播放渲染功能完成,接下来。。。