Android万能音频播放器01--多线程解码音频数据

1、FFmpeg解码流程(图解)

2、FFmpeg解码流程(代码)

3、实现步骤

  1. 注册解码器并初始化网络
av_register_all();
avformat_network_init();
  1. 打开文件或网络流
AVFormatContext *pFormatCtx = avformat_alloc_context();
avformat_open_input(&pFormatCtx, url, NULL, NULL)
  1. 获取流信息
avformat_find_stream_info(pFormatCtx, NULL)
  1. 获取音频流
pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO
  1. 获取解码器
AVCodec *dec = avcodec_find_decoder(audio->codecpar->codec_id);
  1. 利用解码器创建解码器上下文
AVCodecContext *avCodecContext = avcodec_alloc_context3(dec);
avcodec_parameters_to_context(audio->avCodecContext, audio->codecpar)
  1. 打开解码器
avcodec_open2(audio->avCodecContext, dec, 0)
  1. 读取音频帧
AVPacket *packet = av_packet_alloc();
av_read_frame(pFormatCtx, packet);

4、代码结构

4.1、做解码前的准备

解码前的准备工作是在C++层做的,所以设置一个准备工作完成以后的回调方法,其实是由C++层通知Java层:

public interface JfOnPreparedListener {
    void onPrepared();
}

所以要为C++层提供一个方法来回调上面的方法:

public void onCallPrepared(){
    if (jfOnPreparedListener != null)
    {
        jfOnPreparedListener.onPrepared();
    }
}

Java层代码:

public class JfPlayer {
    static {
        System.loadLibrary("avutil-55");
        System.loadLibrary("avcodec-57");
        System.loadLibrary("avformat-57");
        System.loadLibrary("avdevice-57");
        System.loadLibrary("swresample-2");
        System.loadLibrary("swscale-4");
        System.loadLibrary("postproc-54");
        System.loadLibrary("avfilter-6");
        System.loadLibrary("native-lib");
    }

    private String source;
    private JfOnPreparedListener jfOnPreparedListener;
    public JfPlayer(){

    }

    /**
     * 设置数据源
     * @param source
     */
    public void setSource(String source) {
        this.source = source;
    }

    public void setJfOnPreparedListener(JfOnPreparedListener jfOnPreparedListener) {
        this.jfOnPreparedListener = jfOnPreparedListener;
    }

    public void prepared(){
        if (TextUtils.isEmpty(source)){
            JfLog.w("SOURCE IS EMPTY");
            return;
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                n_prepared(source);
            }
        }).start();
    }

    /**
     * C++层n_prepare()完成后要调用JfOnPreparedListener
     */
    public void onCallPrepared(){
        if (jfOnPreparedListener != null)
        {
            jfOnPreparedListener.onPrepared();
        }
    }
    public native void n_prepared(String source);
}

因为C++层要调用Java层代码,所以先在C++层实现这个功能,新建一个C++类-JfCallJava:
JfCallJava.h

#define MAIN_THREAD 0
#define CHILD_THREAD 1
/**
 * C++层调用Java层的类
 */
class JfCallJava {

public:
    JavaVM *javaVM = NULL;
    JNIEnv *jniEnv = NULL;
    jobject jobj;

    jmethodID jmid_prepared;
public:
    JfCallJava(JavaVM *vm,JNIEnv *env,jobject *obj);
    ~JfCallJava();

    void onCallPrepared(int threadType);//这里调用Java层的onCallPrepare方法,因为可能在主线程或者子线程中调用,所以加了这个方法
};

JfCallJava.cpp

#include "JfCallJava.h"

JfCallJava::JfCallJava(JavaVM *vm, JNIEnv *env, jobject *obj) {
    this->javaVM = vm;
    this->jniEnv = env;
    this->jobj = env->NewGlobalRef(*obj);//设为全局

    jclass jclz = env->GetObjectClass(jobj);
    if (!jclz) {
        LOGE("get jclass error");
        return ;
    }

    jmid_prepared = env->GetMethodID(jclz,"onCallPrepared","()V");
}

JfCallJava::~JfCallJava() {

}

void JfCallJava::onCallPrepared(int threadType) {
    if (threadType == MAIN_THREAD){
        jniEnv->CallVoidMethod(jobj,jmid_prepared);
    } else if (threadType == CHILD_THREAD){
        JNIEnv *jniEnv;
        if (javaVM->AttachCurrentThread(&jniEnv,0) != JNI_OK){
            if (LOG_DEBUG) {
                LOGE("GET CHILD THREAD JNIENV ERROR");
                return;
            }
        }

        jniEnv->CallVoidMethod(jobj,jmid_prepared);
        javaVM->DetachCurrentThread();
    }
}

编码的过程由FFmpeg在一个子线程中完成,创建一个C++类-JfFFmpeg,将source的路径传进去
JfFFmpeg.h

class JfFFmpeg {

public:
    JfCallJava *callJava = NULL;//初始化回调java方法封装
    const char *url = NULL;//文件的url
    pthread_t decodeThread = NULL;//解码的子线程


    /**
     * 解码相关
     */
    AVFormatContext *pAFmtCtx = NULL; //封装上下文
    JfAudio *audio = NULL;//封装Audio信息
public:
    JfFFmpeg(JfCallJava *callJava,const char *url);//参数都是从外面传进来的
    ~JfFFmpeg();

    void prepare();
    void decodeAudioThread();
};

JfFFmpeg.cpp

JfFFmpeg::JfFFmpeg(JfCallJava *callJava, const char *url) {
    this->callJava = callJava;
    this->url = url;
}


void *decodeFFmpeg(void *data){
    JfFFmpeg *jfFFmpeg = (JfFFmpeg *)(data);
    jfFFmpeg->decodeAudioThread();
    pthread_exit(&jfFFmpeg->decodeThread);//退出线程
}
/**
 * 正式解码的过程,开一个子线程解码
 */
void JfFFmpeg::prepare() {
    pthread_create(&decodeThread,NULL,decodeFFmpeg,this);
}

void JfFFmpeg::decodeAudioThread() {
    av_register_all();
    avformat_network_init();

    pAFmtCtx = avformat_alloc_context();

    if (avformat_open_input(&pAFmtCtx,url,NULL,NULL) != 0){
        if (LOG_DEBUG){
            LOGE("open url file error url === %s",url);
        }
        return;
    }

    if (avformat_find_stream_info(pAFmtCtx,NULL) < 0){
        if (LOG_DEBUG){
            LOGE("find stream info error url === %s",url);
        }
        return;
    }

    for (int i = 0; i < pAFmtCtx->nb_streams; i++) {
        if (pAFmtCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            if (audio == NULL) {
                audio = new JfAudio;
                audio->streamIndex = i;
                audio->codecpar = pAFmtCtx->streams[i]->codecpar;
            }
        }
    }

    AVCodec *dec = avcodec_find_decoder(audio->codecpar->codec_id);
    if (!dec){
        if (LOG_DEBUG){
            LOGE("FIND DECODER ERROR");
        }
        return;
    }

    audio->pACodecCtx = avcodec_alloc_context3(dec);
    if (!audio->pACodecCtx){
        if (LOG_DEBUG){
            LOGE("avcodec_alloc_context3 ERROR");
        }
        return;
    }

    if (avcodec_parameters_to_context(audio->pACodecCtx,audio->codecpar)){//将解码器中信息复制到上下文当中
        if (LOG_DEBUG){
            LOGE("avcodec_parameters_to_context ERROR");
        }
        return;
    }

    if (avcodec_open2(audio->pACodecCtx,dec,NULL) < 0){
        if (LOG_DEBUG){
            LOGE("avcodec_open2 ERROR");
        }
        return;
    }

    callJava->onCallPrepared(CHILD_THREAD);
}

创建一个C++类-JfAudio,保存音频解码过程中要用到的参数:
JfAudio.h

class JfAudio {

public:
    int streamIndex = -1;//stream索引
    AVCodecParameters *codecpar = NULL;//包含音视频参数的结构体。很重要,可以用来获取音视频参数中的宽度、高度、采样率、编码格式等信息。
    AVCodecContext *pACodecCtx = NULL;
public:
    JfAudio();
    ~JfAudio();
};

JfAudio.cpp

#include "JfAudio.h"

JfAudio::JfAudio() {

}

JfAudio::~JfAudio() {

}

到这里,我们已经完成了所有的准备工作,接下来就要开始读取音频帧

Java层添加native方法:

public void start(){
    if (TextUtils.isEmpty(source)){
        JfLog.w("SOURCE IS EMPTY");
        return;
    }

    new Thread(new Runnable() {
        @Override
        public void run() {
            n_start();
        }
    }).start();
}

public native void n_start();

C++层去实现native方法:

extern "C"
JNIEXPORT void JNICALL
Java_com_example_myplayer_player_JfPlayer_n_1start(JNIEnv *env, jobject instance) {
    // TODO
    if (ffmpeg != NULL){
        ffmpeg->start();
    }
}

实现start方法:

void JfFFmpeg::start() {
    if (audio == NULL) {
        if (LOG_DEBUG){
            LOGE("AUDIO == NULL");
        }
    }

    int count;
    while (1) {
        AVPacket *avPacket = av_packet_alloc();
        if (av_read_frame(pAFmtCtx,avPacket) == 0) {
            if (avPacket->stream_index == audio->streamIndex){
                count++;
                if (LOG_DEBUG) {
                    LOGD("解码第%d帧",count);
                }
                av_packet_free(&avPacket);
                av_free(avPacket);
                avPacket = NULL;
            } else {
                av_packet_free(&avPacket);
                av_free(avPacket);
                avPacket = NULL;
            }
        } else {
            av_packet_free(&avPacket);
            av_free(avPacket);
            avPacket = NULL;
        }
    }
}

这里每次取出一帧都要释放缓存。

源码地址:https://github.com/Xiaoben336/SuperAudioPlayer

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 一、简历准备 1、个人技能 (1)自定义控件、UI设计、常用动画特效 自定义控件 ①为什么要自定义控件? Andr...
    lucas777阅读 5,186评论 2 54
  • Android FFmpeg音频播放 本文介绍了使用opensl es和FFmpeg在Android平台上实现音频...
    JasonXiao阅读 4,404评论 9 26
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 古来英雄,世易时移,功业何在?物议纷纭,取舍异道,殊难定论!然彼超拔之英姿,邈远之情怀,如歌如诗,美蕴其中...
    东溪生阅读 617评论 1 2
  • 像这荷塘里的花儿,看轻你时你重,看重你时你轻,看远时你近,看近时你远,看浓时你淡,看淡时你浓……你很轻又很重,很淡...
    天堂里的鱼阅读 318评论 0 0