安卓流媒体播放器实战代码

项目创建

  • 权限 读写 网络
  • 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();
    }
}

代码测试

打印测试

运行

设置音频大小

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alex-panda

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值