一、准备阶段
开始之前,需要将FFmpeg集成到AS中,集成步骤可以参考之前的一一篇文章,链接地址linux如何编译ffmpeg,并集成到AS中。另外,需要额外依赖三个动态库libz.so(使用ffmpeg需要引入,这个软件包提供了用gzip和PKZIP压缩算法进行开发),libandroid.so(提供了视频渲染的窗口类ANativeWindow)和libOpenSLES.so(音频播放相关)。
二、大致流程
1、初始化FFmpeg(子线程中进行)
2、FFmpeg初始化完成以后,通知Activity可以播放了(采用native层反射java层通知)。activity调用native方法开始播放
3、首先开启packet队列和frame队列,然后开始视频播放(子线程),分为解码线程和播放线程
4、首先开启packet队列和frame队列,然后开始音频播放(子线程),分为解码线程和播放线程
5、音视频同步
6、资源释放
为什么要开启队列?一般情况下我们视频解码的速度是要比播放的速度快,这里的解码相当于生产者,播放相当于消费者。如果存放在数组,需要指定数组的大小,但是这个大小不好确定,太小的话这样很容易导致内存。所以使用队列的方式。
三、初始化FFmpeg
1、首先在播放之前需要将surfaceView进行初始化,当surfaceCreated()回调方法执行以后,表示已经准备好了。此时将Surface(可以通过surfaceHolder.getSurface()拿到)传递给native层,因为native层是可以对surface进行操作的。代码如下:
// TODO java层
@Override
public void surfaceCreated(SurfaceHolder holder) {
/**
* 将surface传递个native层
*/
native_set_surface(mSurfaceHolder.getSurface());
}
// TODO native层
Java_com_xinyartech_ffmpegdemo_player_QbPlayer2_native_1set_1surface(JNIEnv *env,
jobject instance,jobject surface) {
//根据surface创建window 在之前需要释放掉window,防止横竖屏切换
if(window){
ANativeWindow_release(window);
window = 0;
}
//初始化ANativeWindow
window = ANativeWindow_fromSurface(env,surface);
}
2、点击播放按钮,会调用native层FFmpeg初始化方法。代码如下:
/** TODO JAVA层
*
* 调用native层初始化
* @paramters dataSource 播放地址
*/
public void prepare() {
native_prepare(dataSource);
}
// TODO native层
Java_com_xinyartech_ffmpegdemo_player_QbPlayer2_native_1prepare(JNIEnv *env, jobject
instance,jstring dataSource_) {
const char *dataSource = env->GetStringUTFChars(dataSource_, 0);
JavaCallHelper* javaCallHelper = new JavaCallHelper(jvm,env,instance);
// TODO ffmpeg初始化
fFmpeg = new QbFFmpeg(javaCallHelper,dataSource);
//回调frame
fFmpeg->setRenderFrameCallBack(renderFrame);
fFmpeg->prepare();
env->ReleaseStringUTFChars(dataSource_, dataSource);
}
3、QbFFmpeg是一个封装实体类,主要负责初始化,解码操作。首先调用prepare()方法,代码如下:
/**
* 在线程中进行ffmpeg初始化
*/
void QbFFmpeg::prepare() {
pthread_create(&prepareThread, NULL, prepareFFmpeg, this);
}
4、开启子线程prepareFFmepg初始化,代码如下:
/**
* 线程执行回调方法 一定要返回0
*/
void *prepareFFmpeg(void *args) {
//强转对象,拿到当前外部操作对象
QbFFmpeg *fFmpeg = static_cast<QbFFmpeg *>(args);
fFmpeg->prepareFFmpegInit();
return 0;
}
5、继续调用prepareFFmpegInit方法,代码如下:
/**
* 这部分初始化是在子线程中
*/
void QbFFmpeg::prepareFFmpegInit() {
//网络初始化
avformat_network_init();
//代表一个音频、视频。包含音频、视频的所有信息
avFormatContext = avformat_alloc_context();
//1、打开URL
AVDictionary *options = NULL;
//设置3秒链接超时
av_dict_set(&options, "timeout", "3000000", 0);
//为0返回成功
int ret = avformat_open_input(&avFormatContext, url, NULL, &options);
if (ret != 0) {
LOGE("打开URL地址失败%s", url);
//反射到java层,告诉UI,打开失败
if (javaCallHelper) {
javaCallHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL);
}
return;
}
//2、查找流
if (avformat_find_stream_info(avFormatContext, NULL) < 0) {
if (javaCallHelper) {
javaCallHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_FIND_STREAMS);
}
return;
}
//3、根据流,找到解码器
for (int i = 0; i < avFormatContext->nb_streams; ++i) {
AVCodecParameters *parameters = avFormatContext->streams[i]->codecpar;
AVCodec *avCodec = avcodec_find_decoder(parameters->codec_id);
if (!avCodec) {
if (javaCallHelper) {
javaCallHelper->onError(THREAD_CHILD, FFMPEG_FIND_DECODER_FAIL);
}
break;
}
//创建解码器上下文
AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);
if (!avCodecContext) {
if (javaCallHelper) {
javaCallHelper->onError(THREAD_CHILD, FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);
}
}
//复制解码器参数
if (avcodec_parameters_to_context(avCodecContext, parameters) < 0) {
if (javaCallHelper) {
javaCallHelper->onError(THREAD_CHILD, FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL);
}
return;
}
//打开解码器
if (avcodec_open2(avCodecContext, avCodec, 0) != 0) {
if (javaCallHelper) {
javaCallHelper->onError(THREAD_CHILD, FFMPEG_OPEN_DECODER_FAIL);
}
return;
}
//判断是音频还是视频
if (parameters->codec_type == AVMEDIA_TYPE_VIDEO) {
//视频
videoChannel2 = new VideoChannel2(i, javaCallHelper, avCodecContext);
//设置回调
videoChannel2->setRenderFrameCallBack(renderFrame);
} else if (parameters->codec_type == AVMEDIA_TYPE_AUDIO) {
//音频
audioChannel = new AudioChannel(i, javaCallHelper, avCodecContext);
}
}
if (!videoChannel2 && !audioChannel) {
if (javaCallHelper) {
javaCallHelper->onError(THREAD_CHILD, FFMPEG_NOMEDIA);
}
return;
}
//表示ffmpeg初始化准备好了
if (javaCallHelper) {
javaCallHelper->onPrepare(THREAD_CHILD);
}
}
这里对视频和音频封装类都进行了初始化,后面会用到。
二、反射JAVA层
1、看上面最后javaCallHelper->onPrepare(THREAD_CHILD)代码,表示已经初始化完成了,这里的javaCallHelper是一个反射封装类,会对视频初始化失败、成功以及播放进度反射给java层。代码如下:
#include "JavaCallHelper.h"
#include "macro.h"
JavaCallHelper::JavaCallHelper(JavaVM *javaVM, JNIEnv *env, jobject &obj) {
this->javaVM = javaVM;
this->env = env;
//设置成全局的obj
this->obj = env->NewGlobalRef(obj) ;
jclass jClazz = env->GetObjectClass(obj);
//对应java层的三个方法
jmid_prepare = env->GetMethodID(jClazz,"onPrepare","()V");
jmid_progress = env->GetMethodID(jClazz,"onProgress","(I)V");
jmid_error = env->GetMethodID(jClazz,"onError","(I)V");
}
JavaCallHelper::~JavaCallHelper() {
}
/**
* 以下方法反射java方法
* @param thread
* @param errorCode
*/
void JavaCallHelper::onError(int thread, int errorCode) {
if(thread == THREAD_CHILD){
JNIEnv* bindEnv = NULL;
//绑定到主线程
if(javaVM->AttachCurrentThread(&bindEnv,0) != JNI_OK){
return;
}
//使用绑定之后的env结构体
bindEnv->CallVoidMethod(obj,jmid_error,errorCode);
//解绑
javaVM->DetachCurrentThread();
}else{
//主线程直接反射
env->CallVoidMethod(obj,jmid_error,errorCode);
}
}
void JavaCallHelper::onProgress(int thread, int progress) {
if(thread == THREAD_CHILD){
JNIEnv* bindEnv = NULL;
//绑定到主线程
if(javaVM->AttachCurrentThread(&bindEnv,0) != JNI_OK){
return;
}
//使用绑定之后的env结构体
bindEnv->CallVoidMethod(obj,jmid_progress,progress);
//解绑
javaVM->DetachCurrentThread();
}else{
//主线程直接反射
env->CallVoidMethod(obj,jmid_progress,progress);
}
}
void JavaCallHelper::onPrepare(int thread) {
if(thread == THREAD_CHILD){
JNIEnv* bindEnv = NULL;
//绑定到主线程
if(javaVM->AttachCurrentThread(&bindEnv,0) != JNI_OK){
return;
}
//使用绑定之后的env结构体
bindEnv->CallVoidMethod(obj,jmid_prepare);
//解绑
javaVM->DetachCurrentThread();
}else{
//主线程直接反射
env->CallVoidMethod(obj,jmid_prepare);
}
}
2、此时会回调java层onPrepare方法
/**
* 接受native层反射的方法 ,再通过接口回调给activity
*/
//准备
public void onPrepare() {
if (mPrepareListener != null) {
mPrepareListener.onPrepared();
}
}
/** TODO activity接收处理代码
* 初始化回调
*/
private void initCallBack() {
mQbPlayer.setPrepareListener(new QbPlayer2.prepareListener() {
@Override
public void onPrepared() {
//ffmpeg初始化完成才能进行播放
Log.e("初始化完成了11-----","11111111111");
mQbPlayer.start();
}
});
mQbPlayer.setProgressListener(new QbPlayer2.progressListener() {
@Override
public void onProgree(int progress) {
}
});
mQbPlayer.setErrorListener(new QbPlayer2.errorListener() {
@Override
public void onError(int errorCode) {
}
});
}
3、start方法调用以后会调用native层,代码如下:
Java_com_xinyartech_ffmpegdemo_player_QbPlayer2_native_1start(JNIEnv *env, jobject instance) {
// TODO ffmpeg配置完成后 开始播放
if(fFmpeg){
fFmpeg->start();
}
}
三、视频解码
1、首先在fFpeng类中会开启一个不断读取packet的子线程,并且把相应packet类型(视频类型,音频类型)存放到不同的音视频frame队列中。
/**
* 解码线程
*/
void *play(void *args) {
QbFFmpeg *fFmpeg = static_cast<QbFFmpeg *>(args);
fFmpeg->startDecode();
return 0;
}
void QbFFmpeg::start() {
isPlaying = true;
//视频开始播放 交给视频对象处理
if (videoChannel2) {
videoChannel2->play();
}
//开始解码线程
pthread_create(&playThread, NULL, play, this);
}
void QbFFmpeg::startDecode() {
int ret = 0;
while (isPlaying) {
//防止队列满了,生产者速度大于消费者速度 100帧 放在前面,此时没有去取一包,防止跳帧
if (audioChannel && audioChannel->pkt_queue.size() > 100) {
av_usleep(10 * 1000);//休眠10ms
continue;
}
if (videoChannel2 && videoChannel2->pkt_queue.size() > 100) {
av_usleep(10 * 1000);
continue;
}
//读取包
/** TODO AVPacket 结构体 相关信息 解码前数据
* uint8_t *data:压缩编码的数据。
例如对于H.264来说。1个AVPacket的data通常对应一个NAL。
注意:在这里只是对应,而不是一模一样。他们之间有微小的差别:使用FFMPEG类库分离出多媒体文件中的H.264码流
因此在使用FFMPEG进行视音频处理的时候,常常可以将得到的AVPacket的data数据直接写成文件,从而得到视音频的码流文件。
int size:data的大小
int64_t pts:显示时间戳
int64_t dts:解码时间戳
int stream_index:标识该AVPacket所属的视频/音频流。
*/
AVPacket *packet = av_packet_alloc();
//从媒体中读取音频、视频包
ret = av_read_frame(avFormatContext, packet);
if (ret == 0) {
//将数据包加入队列
if (audioChannel && packet->stream_index == audioChannel->channelId) {
// audioChannel->pkt_queue.enQueue(packet);
} else if (videoChannel2 && packet->stream_index == videoChannel2->channelId) {
videoChannel2->pkt_queue.enQueue(packet);
}
} else if (ret == AVERROR_EOF) {
//读取完毕了,但不一定是播放完毕
if (videoChannel2->pkt_queue.empty() && videoChannel2->frame_queue.empty() &&
audioChannel->pkt_queue.empty() && audioChannel->frame_queue.empty()) {
break;
}
} else {
break;
}
}
//停止
isPlaying = false;
videoChannel2->isPlaying = false;
audioChannel->stop();
videoChannel2->stop();
}
2、在启动读取packet的过程中,同时调用了videoChannel2->play();方法。代码如下:
#include "VideoChannel2.h"
#include "BaseChannel.h"
extern "C" {
#include <libavutil/time.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
}
/**
* 解码线程执行方法
*/
void *decode(void *args) {
VideoChannel2 *videoChannel2 = static_cast<VideoChannel2 *>(args);
videoChannel2->decodePackets();
return 0;
}
/**
* 播放线程执行方法
*/
void *goPlay(void *args) {
VideoChannel2 *videoChannel2 = static_cast<VideoChannel2 *>(args);
videoChannel2->goPlayFrame();
return 0;
}
VideoChannel2::VideoChannel2(int id, JavaCallHelper *javaCallHelper,
AVCodecContext *avCodecContext) : BaseChannel(id, javaCallHelper,
avCodecContext) {
}
VideoChannel2::~VideoChannel2() {
}
void VideoChannel2::play() {
//使队列开始工作
pkt_queue.setWork(1);
frame_queue.setWork(1);
isPlaying = true;
//解码线程
pthread_create(&pid_video_decode, NULL, decode, this);
//播放线程
pthread_create(&pid_video_play, NULL, goPlay, this);
}
void VideoChannel2::stop() {
}
void VideoChannel2::decodePackets() {
AVPacket *packet = 0;
LOGE("解码一帧11");
while (isPlaying) {
//到QbFfmpeg类中进行解码,
//懂队列中读取packet
int ret = pkt_queue.deQueue(packet);
if (!isPlaying) {
break;
}
if (!ret) {
continue;
}
//开始解压
ret = avcodec_send_packet(avCodecContext, packet);
//得到frame,此时可以释放packet
releaseAvPacket(packet);
//解码器中也维持一个解码队列,满的话就需要等待
if (ret == AVERROR(EAGAIN)) {
//需要更多数据
continue;
} else if (ret < 0) {
//失败 比如断线了
break;
}
//读取frame
AVFrame *frame = av_frame_alloc();
ret = avcodec_receive_frame(avCodecContext, frame);
LOGE("加入一帧");
frame_queue.enQueue(frame);
//同样判断队列是否需要延迟,防止队列爆满
while (frame_queue.size() > 100 && isPlaying) {
av_usleep(1000 * 10);
continue;
}
}
//彻底释放完毕 释放最后一帧
releaseAvPacket(packet);
}
void VideoChannel2::goPlayFrame() {
//初始化转换器上下文 YUV转rgba
SwsContext *swsContext = sws_getContext(avCodecContext->width, avCodecContext->height,
avCodecContext->pix_fmt,
avCodecContext->width, avCodecContext->height,
AV_PIX_FMT_RGBA, SWS_BILINEAR, 0, 0, 0);
uint8_t *dst_data[4];//4个字节argb
int dst_lineSize[4];
//初始化容器,刚好和一帧的大小相等
av_image_alloc(dst_data, dst_lineSize, avCodecContext->width, avCodecContext->height,
AV_PIX_FMT_RGBA, 1);
AVFrame *frame = 0;
while (isPlaying) {
int ret = frame_queue.deQueue(frame);
//播放停止了,退出循环
if (!isPlaying) {
break;
}
//没有取到,继续取
if (!ret) {
continue;
}
//填充容器 frame->data指向一行的首地址 数据转换到dst_data
/**
* 第二个参数可以理解为1帧为N个片(片头+片数据),是数组
* 第三个参数是修饰每个片数据长度,一行的像素数据
* 第四个参数,一帧的高度
*
* 第五个参数 像素总个数 指向首地址
* 第6个参数 每一行的像素个数
*/
sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),
frame->linesize, 0, frame->height, dst_data, dst_lineSize);
//回调给调用者
renderFrame(dst_data[0],dst_lineSize[0],avCodecContext->width,avCodecContext->height);
//16ms
av_usleep(16*1000);
//释放frame
releaseAvFrame(frame);
}
//资源释放
av_free(&dst_data[0]);
isPlaying = false;
releaseAvFrame(frame);
sws_freeContext(swsContext);
}
void VideoChannel2::setRenderFrameCallBack(RenderFrame renderFrame) {
this->renderFrame = renderFrame;
}
3、在play执行以后会启动两个线程:解码线程(将packet转为frame)和播放线程。在转为frame过程中为了防止frame队列爆满,当队列长度超过100时就会延迟一段时间,等待frame被消费。消费frame是调用renderFrame(dst_data[0],dst_lineSize[0],avCodecContext->width,avCodecContext->height);回调给native_lib.cpp类。因为ANativeWindow是仅存在于这个类的,播放的渲染需要在这里执行。回调渲染代码如下:
void renderFrame(uint8_t* data,int lineSize,int w,int h){
// 渲染
//设置窗口属性
ANativeWindow_setBuffersGeometry(window, w,
h,
WINDOW_FORMAT_RGBA_8888);
ANativeWindow_Buffer window_buffer;
if (ANativeWindow_lock(window, &window_buffer, 0)) {
ANativeWindow_release(window);
window = 0;
return;
}
// 缓冲区 window_data[0] =255;
uint8_t *window_data = static_cast<uint8_t *>(window_buffer.bits);
// r g b a int 一行的像素 字节拷贝 需要x4
int window_linesize = window_buffer.stride * 4;
//数据源
uint8_t *src_data = data;
//一个字节拷贝速度太慢
//一行一行的拷贝
//不能整体拷贝原因:缓冲区一行的大小可能会比实际的一行数据大,整体拷贝会导致是在缓存去一行会去填充实际的两行数据,导致视频画面乱码
for (int i = 0; i < window_buffer.height; ++i) {
//大小以左边为准,根据真实为准
memcpy(window_data + i * window_linesize, src_data + i * lineSize, window_linesize);
}
ANativeWindow_unlockAndPost(window);
}
以上就完成了视频的渲染播放。