项目创建
- 权限 读写 网络
- ABI armeabi-v7a
- JNI 库路径
- CMake 代码和头文件、导入导出库
我选择的是Android 4.0冰激凌三明治版本,C++11
从昨天起,Android studio的项目突然出来点问题,即把appcompat-v7:29.+ 的"29."去掉就OK了,可是下次新建项目有需要这样修改,为了抓紧干活,先暂时这样做
像之前的项目,就没什么问题,29版本以上就会出错
代码:
FFDemux.cpp
#include "FFDemux.h"
#include "XLog.h"
extern "C"{
#include <libavformat/avformat.h> //封装器
}
//.cpp实现
//打开文件,或者流媒体 rtmp http rtsp ,通过FFDemux::调用该类下的函数
bool FFDemux::Open(const char *url)//去掉“=0”纯虚函数,(实现是在继承类当中的)
{
XLOGI("Open file %s begin", url);
int re = avformat_open_input(&ic, url, 0, 0);
if (re != 0)
{
char buf[1024];
av_strerror(re, buf, sizeof(buf));
XLOGE("FFDemux open %s failed!可能:"
"1.网络模块未初始化,"
"2.路径下没有对应文件,"
"3.没有网络和读取权限Manifest及代码中动态权限",url);
return false;//先不加锁(需要释放)
}
XLOGI("FFDemux open %s success",url);
//读取文件信息(对于mp4文件不调用也能获取音频信息,有些格式需要调用)
re = avformat_find_stream_info(ic, 0);
if (re != 0)
{
char buf[1024];
av_strerror(re, buf, sizeof(buf));
XLOGE("avformat_find_stream_info %s failed!可能:",url);
return false;//先不加锁(需要释放)
}
this->totalMs = ic->duration / (AV_TIME_BASE / 1000);
XLOGI("total ms = %d",totalMs);
return true;
}
//读取一帧数据,数据由调用者清理
XData FFDemux::Read()
{
XData xd;
return xd;
}
//初始化这个函数只会被调用一次
FFDemux::FFDemux()
{
//为了防止它被静态调用,使用静态变量
static bool isFirst = true;//第一次进来(不是线程安全的)
//如果同时创建2个FFDemux对象,可能会出现多次创建问题
if (isFirst) {//如果是第一次
isFirst = false;
//注册所有封装器
av_register_all();
//注册所有的解码器
avcodec_register_all();
//初始化所有网络
avformat_network_init();
XLOGI("register ffmpeg!");
}
}
FFDemux.h
#ifndef XPLAY_FFDEMUX_H
#define XPLAY_FFDEMUX_H
//#include <libavformat/avformat.h> IDE自动引用了,这里不需要头文件,是自定义声明
#include "XData.h"
#include "IDemux.h"
struct AVFormatContext;
class FFDemux :public IDemux{
public:
//打开文件,或者流媒体 rtmp http rtsp
virtual bool Open(const char *url);//去掉“=0”纯虚函数,(实现是在继承类当中的)
//读取一帧数据,数据由调用者清理
virtual XData Read();
FFDemux();//构造函数,只要创建这个接口肯定会被调用
private: //成员
//只在无参下生效,如果有参将不会被赋值(有参需要放到构造函数里面去初始化)
AVFormatContext *ic = 0; //指针是一种类型不用管它的实现
};
#endif //XPLAY_FFDEMUX_H
IDemux.h
#ifndef XPLAY_IDEMUX_H
#define XPLAY_IDEMUX_H
#include "XData.h"
//解封装接口
class IDemux {
public:
//打开文件,或者流媒体 rtmp http rtsp
virtual bool Open(const char *url) = 0;//定义纯虚函数,(实现是在继承类当中的)
//读取一帧数据,数据由调用者清理
virtual XData Read() = 0;
//总时长(毫秒)
int totalMs = 0;
};
#endif //XPLAY_IDEMUX_H
XLog.h
#ifndef XPLAY_XLOG_H
#define XPLAY_XLOG_H
class XLog {
};
//通过宏区分不同系统
#ifdef ANDROID
#include <android/log.h>
//DEBUG级别
#define XLOGD(...) __android_log_print(ANDROID_LOG_DEBUG,"XPlay",__VA_ARGS__)
#define XLOGI(...) __android_log_print(ANDROID_LOG_INFO,"XPlay",__VA_ARGS__)
//ERROR级别以上的错误都会爆红
#define XLOGE(...) __android_log_print(ANDROID_LOG_ERROR,"XPlay",__VA_ARGS__)
#else
//DEBUG级别
#define XLOGD(...) print("XPlay",__VA_ARGS__)
#define XLOGI(...) print("XPlay",__VA_ARGS__)
//ERROR级别以上的错误都会爆红
#define XLOGE(...) print("XPlay",__VA_ARGS__)
#endif
#endif //XPLAY_XLOG_H
入口文件
在接口之前必须先初始化,首先不能放到IDemux里面,可以对外提供统一初始化接口来实现,还有就是在构造函数里面初始化
出现动态权限问题,导入第三块库 https://github.com/getActivity/XXPermissions
无法引用插件,参考链接https://blog.csdn.net/qq_34829270/article/details/80481209
测试运行结果:
文件到此就成功打开了
FFDemux.cpp修改代码
//读取一帧数据,数据由调用者清理
XData FFDemux::Read()
{
if (!ic) //互斥量
return XData(); //返回空(XData里面就是一个空类)
XData xd;
//读取一帧的接口
AVPacket *pkt = av_packet_alloc();//需要时刻关注空间清理和释放问题
int re = av_read_frame(ic, pkt);//pkt为数据包
if (re != 0)
{
av_packet_free(&pkt);//指针地址,空间释放掉(不然空间泄露)
return XData();
}
//每写一个点(段)做一下测试,
XLOGI("pack size is %d ptss %lld",pkt->size,pkt->pts);
return xd;
}
入口文件中添加 XData
///测试用代码
IDemux *de = new FFDemux;
de->Open("/sdcard/lizhi.mp4");
for (;;) //无须循环
{
XData xd = de->Read();
}
XData.h
#ifndef XPLAY_XDATA_H
#define XPLAY_XDATA_H
//接口(改成结构体)
struct XData {
unsigned char *data = 0;
int size = 0;
void Drop();//清理函数
};
#endif //XPLAY_XDATA_H
XData.cpp
#include "XData.h"
extern "C"{
#include <libavformat/avformat.h>
}
void XData::Drop()
{
if(!data) return;//如果是null
//这个释放函数,来自自定义
av_packet_free((AVPacket **)&data);//指向指针的指针就是指针地址(这里需强转),unsigned char *data = 0;
//把空间释放掉后,一定要置零
data = 0;
size = 0;
}
FFDemux.cpp下Read()添加
//每写一个点(段)做一下测试,
XLOGI("pack size is %d ptss %lld",pkt->size,pkt->pts);
xd.data = (unsigned char *) pkt;//强制转换unsigned char *data = 0;
xd.size = pkt->size; //这个空间出去之后是要被销毁的
测试运行
线程和观察者模式(IDemux作为主体),除了开启停止线程,还需要做个接口关闭线程在安卓退出时(当主线程退出,子线程不一定退出(因为有个主循环))
新建XThread.cpp
#include "XThread.h"
#include "XLog.h"
#include <thread>
//命名空间
using namespace std;
//启动线程
void XThread::Start() //防止客户对Start()做重载
{
//线程就启动了,对象还在堆栈当中,我们放弃线程控制
thread th(&XThread::ThreadMain,this);//1.函数的地址2.指针
th.detach(); //如果不放弃对线程的控制,当对象被清空时,线程出错
}
//定义这个函数的目的:可以公共的预处理,直接用成员函数也可以,为了方便控制
void XThread::ThreadMain()
{
XLOGI("线程函数进入");
//用户做重载的
Main();
XLOGI("线程函数退出");
}
//通过控制isExit()安全停止线程(不一定成功)
void XThread::Stop()
{}
新建XThread.h
#ifndef XPLAY_XTHREAD_H
#define XPLAY_XTHREAD_H
//C++ 11 线程库
class XThread {
public:
/**
* 根据业务值,判断要不要添加返回值
* windows下,设了返回值,不会当,会有错误提示。Linux下会直接当掉
*/
//启动进程时还可能做其他事情,调用同一个接口启动这个线程,但是要作为其他的操作完之后,再来启动线程
//调用父类void XThread()::Start(){}
virtual void Start(); //防止客户对Start()做重载
//通过控制isExit()安全停止线程(不一定成功)
virtual void Stop();
//入口主函数,纯虚函数“=0”要求继承者必须实现这个Main()函数,这里不需要
virtual void Main(){}
private: //不需要外部知道
void ThreadMain();
};
#endif //XPLAY_XTHREAD_H
修改IDemux.h 继承XThread ,写Main()
#include "XData.h"
#include "XThread.h"
//解封装接口
class IDemux :public XThread{
public:
//打开文件,或者流媒体 rtmp http rtsp
virtual bool Open(const char *url) = 0;//定义纯虚函数,(实现是在继承类当中的)
//读取一帧数据,数据由调用者清理
virtual XData Read() = 0;
//总时长(毫秒)
int totalMs = 0;
protected: //不让用户去访问
//继承Main()
virtual void Main();
};
IDemux.cpp
void IDemux::Main()
{
//不断的读,让它打印出来
for(;;)
{
XData xd = Read();//Read()是由子类做的,class FFDemux :public IDemux
XLOGI("IDemux Read %d",xd.size);
if(xd.size<=0) break;
}
}
测试运行结果
读取日志需要耗时,注释掉
修改XThread.cpp
using namespace std;
void XSleep(int mis)
{
chrono::milliseconds duration(mis); //毫秒时间对象
this_thread::sleep_for(duration);
}
//启动线程
void XThread::Start() //防止客户对Start()做重载
{
//给它去掉,不然一旦停止就再也启动不了了
isExit = false;
//线程就启动了,对象还在堆栈当中,我们放弃线程控制
thread th(&XThread::ThreadMain,this);//1.函数的地址2.指针
th.detach(); //如果不放弃对线程的控制,当对象被清空时,线程出错
}
//定义这个函数的目的:可以公共的预处理,直接用成员函数也可以,为了方便控制
void XThread::ThreadMain()
{
isRunning = true;
XLOGI("线程函数进入");
//用户做重载的
Main();
XLOGI("线程函数退出");
isRunning = false;
}
//通过控制isExit()安全停止线程(不一定成功)
void XThread::Stop()
{
isExit = true;
//最多等200ms
for (int i = 0; i <200 ; i++)
{
if (!isRunning) //不等于true
{
XLOGI("Stop 停止线程成功!");
return;
}
XSleep(1);//1s
}
XLOGI("Stop 停止线程超时");
}
虽然这里报错,但似乎没有影响,奇怪,可能是级别设置太小的原因
XThread.h 添加
protected:
bool isExit = false;
bool isRunning = false;
延时停止线程测试运行结果
新建IObserver.h
#include <vector> //C++容器,遍历的更快
#include <mutex>
#include "XData.h"
#include "XThread.h"
//观察者 和 主体 共用一个类
class IObserver :public XThread
{
public: //对外接口
/**
* 观察者接收数据函数
* 由主体调用观察者Update,来通知观察者已经接收数据了
* @param data
*/
virtual void Update(XData data) {}
//主体函数 添加观察者(因为加了锁,线程安全)
void AddObs(IObserver *obs);
//通知所有观察者(会调用Update())(因为加了锁,线程安全)
void Notify(XData data);
protected: //需要一系列的成员来存储观察者
std::vector<IObserver *> obss; //不放对象,放指针,观察者队列
//考虑线程安全
std::mutex mutex;
};
新建IObserver.cpp
#include "IObserver.h"
//主体函数 添加观察者
void IObserver::AddObs(IObserver *obs)
{
if(!obs)return; //参数为空,什么都不需要处理(不加锁)
mutex.lock(); //加锁,调试程度会加大(锁死)
//添加一个观察者
obss.push_back(obs);
mutex.unlock();
}
//通知所有观察者
void IObserver::Notify(XData data)
{
mutex.lock(); //加锁,调试程度会加大(锁死)
//遍历注册了多少个观察者,注意“obss.size()效率很低,每次都需要统计一下size()”
for (int i = 0; i <obss.size() ; i++)
{
obss[i]->Update(data); //目前空数据也一起发送了
}
mutex.unlock();
}
因为基本所有模块都需要到线程,IDemux继承IObserver
//解封装接口
class IDemux :public IObserver{
由公共类IObserver继承多线程
//观察者 和 主体 共用一个类
class IObserver :public XThread
IDemux.cpp 添加Notify()
void IDemux::Main()
{
//for(;;)//不断的读,让它打印出来
while (!isExit)
{
XData xd = Read();//Read()是由子类做的,class FFDemux :public IDemux
if(xd.size>0)
Notify(xd); //(继承IObserver类)数据发送出去
//XLOGI("IDemux Read %d",xd.size);
//防止线程退出if(xd.size<=0) break;
}
}
入口文件native-lib.cpp 添加
class TestObs:public IObserver
{
public:
void Update(XData xd)
{
XLOGI("TestObs Update data size is %d", xd.size);
}
};
测试收到的数据
新建XParameter.h
/**
* 配置项参数
*/
//#include <libavcodec/avcodec.h> //头文件不对
struct AVCodecParameters;//这是个指针,直接给个声明就行,不用管它的实现
class XParameter
{
public:
AVCodecParameters *para= 0; //默认值为0
};
新建FFDecode.h
#include "XParameter.h"
struct AVCodecContext; //只做声明不做定义
class FFDecode
{
//做实现.h声明函数
virtual bool Open(XParameter xParameter);
protected:
AVCodecContext *codec = 0;
};
新建FFDecode.cpp
extern "C"{ //需要加extern "C",不然链接的时候出错,编译的时候在代码里找的到,库里面找不到
#include <libavcodec/avcodec.h>
}
#include "FFDecode.h"
#include "XLog.h"
virtual bool FFDecode::Open(XParameter xParameter)
{
//先不管线程安全和硬解码
if(!xParameter.para)
return false;
AVCodecParameters *p = xParameter.para;
//1.查找解码器
//做封装,要考虑哪些参数需要做成员,哪些不需要做成员的
AVCodec *cd = avcodec_find_decoder(p->codec_id);
if (!cd)
{
XLOGE("avcodec_find_decoder %d failed!",p->codec_id);
return false;
}
XLOGE("avcodec_find_decoder success!");
//2.创建解码上下文,并复制参数
codec = avcodec_alloc_context3(cd);
avcodec_parameters_to_context(codec,p);//复制
//3.打开解码器
int re = avcodec_open2(
codec, //上下文
0, //解码器,第二步已经包含,所以这里传null
0 //对应的配置项,不需要
);
if (re != 0) //C语言当中=0表示成功,!=0表示失败
{
char buf[1024] = {0}; //失败内容
av_strerror(re, buf, sizeof(buf) - 1); //失败编号写入buf
XLOGE("%s",buf);
return false;
}
XLOGE("avcodec_open2 success!");
return true;
}
新建 IDecode.h
#include "XParameter.h"
#include "IObserver.h"
//解码接口,支持硬解码
class IDecode:public IObserver
{
public:
//打开解码器
virtual bool Open(XParameter xParameter) = 0;//定义个参数传递接口,设置成纯虚函数
};
修改IDemux.h
//获取视频参数,放到FFDemux做实现
virtual XParameter GetVPara()=0;
修改FFDemux.h添加
//获取视频参数
virtual XParameter GetVPara();
FFDemux.cpp修改
/**
* 获取视频参数
* @return
*/
XParameter FFDemux::GetVPara()
{
if (!ic)
{
XLOGE("GetVPara failed! ic is NULL!");
return XParameter();
}
//获取了视频流索引
int re = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, 0,0);
if (re < 0)
{
XLOGE("av_find_best_stream failed!");
return XParameter();
}
//返回值
XParameter para;
para.para = ic->streams[re]->codecpar;
return para;
}
FFDecode继承IDecode
注意!去掉virtual
测试视频参数获取运行结果
开发解码模块 (IDecode.h内写接口"纯虚函数"->FFDecode.h中声明->FFDecode.cpp实现“去掉virtual")
获取参数模块 (IDemux.h内写获取音视频参数接口->)
IDemux.h添加 (继承IObserver)
//获取视频参数,放到FFDemux做实现
virtual XParameter GetVPara()=0;
//获取音频参数
virtual XParameter GetAPara() = 0;
FFDemux.h
//获取视频参数
virtual XParameter GetVPara();
//获取音频参数
virtual XParameter GetAPara();
FFDemux.cpp
/**
* 获取视频参数
* @return
*/
XParameter FFDemux::GetVPara()
{
if (!ic)
{
XLOGE("GetVPara failed! ic is NULL!");
return XParameter();
}
//获取了视频流索引
int re = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, 0,0);
if (re < 0)
{
XLOGE("av_find_best_stream failed!");
return XParameter();
}
//返回值
XParameter para;
para.para = ic->streams[re]->codecpar;
return para;
}
XParameter FFDemux::GetAPara() //和视频的差不多
{
if (!ic)
{
XLOGE("GetAPara failed! ic is NULL!");
return XParameter();
}
//获取了音频流索引
int re = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, 0,0);
if (re < 0)
{
XLOGE("av_find_best_stream failed!");
return XParameter();
}
//返回值
XParameter para;
para.para = ic->streams[re]->codecpar;
return para;
}
XData.h 添加
bool isAudio = false;
FFDemux.h加入索引流
FFDemux.cpp
Open()下
Read()中添加
//每写一个点(段)做一下测试,
//XLOGI("pack size is %d ptss %lld",pkt->size,pkt->pts);
xd.data = (unsigned char *) pkt;//强制转换unsigned char *data = 0;
xd.size = pkt->size; //这个空间出去之后是要被销毁的
if (pkt->stream_index == audioStream)
{
xd.isAudio = true;
}
else if (pkt->stream_index == videoStream)
{
xd.isAudio = false;
}
else //既不是音频又不是视频,销毁掉,不然会内存泄露
{
av_packet_free(&pkt);//指针地址,空间释放掉(不然空间泄露)
return XData();
}
native-lib入口文件
//用它的接口
IDecode *vdecode = new FFDecode();
vdecode->Open(de->GetVPara()); //视频参数的获取
IDecode *adecode = new FFDecode();
adecode->Open(de->GetAPara()); //音频参数的获取
IObserver.h添加
/**
* 观察者接收数据函数
* 由主体调用观察者Update,来通知观察者已经接收数据了
* @param data
*/
virtual void Update(XData data) {}
IDecode.h添加
//判断当前是否是音频
bool isAudio = false;
FFDemux.cpp 添加
/**
* 这样就知道当前是音频还是视频了
*/
if (codec->codec_type==AVMEDIA_TYPE_VIDEO)
{
this->isAudio = false;
}
else
{
this->isAudio = true;
}
IDecode.h添加
IDecode.cpp
#include "IDecode.h"
//接收数据 有主体notify的数据(通知观察者)
void IDecode::Update(XData pkt)
{
if (pkt.isAudio != isAudio)
{
return;
}
packsMutex.lock();
//插入队列
packs.push_back(pkt);
packsMutex.unlock();
}
void IDecode::Main()
{
while (!isExit)
{
packsMutex.lock();
packsMutex.unlock();
}
}
修改 IDecode.cpp
#include "IDecode.h"
//接收数据 有主体notify的数据(通知观察者)
void IDecode::Update(XData pkt)
{
if (pkt.isAudio != isAudio)
{
return;
}
while (!isExit) //这样就可以整个控制播放速度
{
packsMutex.lock();
//阻塞
if (packs.size()<maxList)
{
//往后插入队列
packs.push_back(pkt);
packsMutex.unlock(); //解锁
break;
}
packsMutex.unlock();
XSleep(1);
}
}
void IDecode::Main()
{
while (!isExit)
{
packsMutex.lock();
//=>这里就是“消费者”,判断里面有没有数据
if (packs.empty())
{
packsMutex.unlock(); //先解锁
//sleep()的作用释放当前CPU的资源(防止占用“没有休息的时间”)
XSleep(1); //一定要加sleep(),不然一旦为空,这个循环就不断进行,cpu耗尽
continue;
}
//从前面取出packet
XData pack = packs.front();
packs.pop_front(); //然后从链表中删掉
//扔给编码,发送数据到解码线程(并不是真正的解码),一个数据包,可能解码多个数据结果
if (this->SendPacket(pack))
{
while (!isExit)
{
//获取解码数据
XData frame = RecvFrame();
if(!frame.data) break; //读不到数据就结束了
//读到数据就继续往下发给观察者
this->Notify(frame); //读到数据就继续往下发
}
}
pack.Drop(); //清理
packsMutex.unlock();
}
}
代码测试
打印测试
运行
设置音频大小