Android音视频——构建私有播放器(FFmpeg&Rtmp)

前言

关于如何编译FFmpeg,前面的博客已经讲过。Linux+NDK编译FFmpeg
编译好文件结构如下:

  • include 待引入头文件
  • lib so库文件
  • share C语言示例

强烈建议进入share文件夹下,浏览一下调用示例的C文件,对下一步的理解的使用将会有巨大帮助

一. 引入Library与检查

1.配置

新建CPP的Android工程,配置Cmake,引入事先构建好的so文件和头文件。工程路径如下:
在这里插入图片描述
cmake.list脚本:

cmake_minimum_required(VERSION 3.4.1)

set(NAME hm-player)
file(GLOB allCpp *.cpp)

add_library(${NAME} SHARED ${allCpp})

find_library(log-lib log)

# 导入FFmpeg头文件
include_directories(${CMAKE_SOURCE_DIR}/ffmpeg/include)

# 导入FFmpeg库文件
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/ffmpeg/libs/${CMAKE_ANDROID_ARCH_ABI}")

# RTMP的库文件
set(CMAKE_CXX_FLAGS  "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/rtmp/libs/${CMAKE_ANDROID_ARCH_ABI}")

target_link_libraries(
        ${NAME}
        -Wl,--start-group # 忽略顺序引发的问题
        avcodec avfilter avformat avutil swresample swscale
        -Wl,--end-group
        ${log-lib}
        z
        rtmp
)

1.注意System.loadLibrary("hm-player")中的引用库的名字一定跟自己cmake配置的一样。
2.注意库平台类型的筛选,因为我只用了armeabi-v7a的库文件,所有就需要再app级别的gradle里面配置abiFilters "armeabi-v7a"
在这里插入图片描述
构建成功即ready,进入代码环节。

2. 调用验证

首先在函数中调用一下版本号,看是否成功。我们就用native-lib.cpp系统生成的函数。

#include <jni.h>
#include <string>
extern "C" {
    #include <libavutil/avutil.h>
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_heima_ffmpegtest_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "ffmpeg version : ";
    hello.append(av_version_info());
    return env->NewStringUTF(hello.c_str());
}

千万注意那个包裹头文件引用的extern "C",这个牵扯到语法问题,cpp调用C的库需要加上。我是因为工作中经常调用C的算法库,错的多了也就习惯顺手加上了,不熟悉的小伙伴一定要注意下。
安装过后,手机页面显示版本号4.2.2,调用成功。
PS:我不知道是自己编译器问题还是怎么回事,我build app后,查看.apk格式文件,路径下有lib/XXso,但是我点击安装,IDE自动构建新APP时候,又会丢掉lib,导致我每次用IDE安装都会报错,couldn't find "libnative-lib.so"。如果我build app后 ,直接命令行adb install ...XX...就成功运行,真的见了鬼了。

二. Java/Kotlin层工程化构建

从这里开始就要搭框架写代码了,我认为这部分是也是最难的部分,往往难以下手 。说实话,这个东西我demo看了没有十几个,也有七八个,我就凭着自己思路写了。开始构思MyPlayer.class

1. 视频播放的载体

首先考虑到的是视频在哪里播放? SurfaceView这个是不容置疑的。

  • 相信有过camera开发经验的同学对这个一定不熟悉,因为最终camera的预览设置全部跟他有关。我们直接让Player继承相关接口就好。
  • 需要在类里面添加一个方法,可以让承载SurfaceView界面Activity可以吧SurfaceView传递进来,在此类中进行操作绘制。
  • 我们需要一个视频源,可以是rtmp,也可以是本地的视频,暂且拿个字符串先糊弄着吧。

整理下来,我就先随便写了下面的类,后面再有什么再说添加吧,要不这代码我真的敲不下去了。

class MyPlayer : SurfaceHolder.Callback {
    var dataSource: String? = null // rtmp流/本地流
    var surfaceHolder: SurfaceHolder? = null
    var surfaceHolder: SurfaceHolder? = null

    fun setSurfaceView(surfaceView: SurfaceView) { // 每次设置SurfaceView的时候,判断Holder释放被清除
        if (null != surfaceHolder) {
            surfaceHolder!!.removeCallback(this)//每次设置释放旧Holder
        }
        surfaceHolder = surfaceView.holder
        this.surfaceHolder?.addCallback(this)
    }
    override fun surfaceChanged(p0: SurfaceHolder?, p1: Int, p2: Int, p3: Int) {
        setSurfaceNative(p0!!.surface)
    }

    override fun surfaceDestroyed(p0: SurfaceHolder?) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun surfaceCreated(p0: SurfaceHolder?) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }


    fun prepare() {
        prepareNative(this.dataSource)
    }

    fun start() {
        startNative()
    }

    fun stop() {
        stopNative()
    }

    fun release() {
        releaseNative()
    }

    /**
     * native待实现
     */
    external fun setSurfaceNative(surface: Surface?) // 其实底层是操控 Surface对象(绘制.渲染)
    external fun prepareNative(dataSource: String?)//视频流的传递
}

2. 基础播放器动作

思考一下,一个正常的播放器有哪些动作呢?我对着XX视频思考很久。
准备,播放,暂停,结束。

 	/**
     * native待实现
     */
    fun prepare() {}
    fun start() {}
    fun stop() {}
    fun release() {}

因为我们是ndk加载,所以我们需要把这些方法映射到Native层里面去。

二. Native实现

这里牵扯到编码思路了,讲道理,我很迷。。。秉承着犹豫不决先思考的作死原则,我至少思考了半天时间来整理思路。23:35:22,这个熬夜点最适合码字了。GO~

1. 播放类→MyPlayer 构思

我们在上层建立了MyPlayer类,这个是想到面向对象单一职责原则,同样的我们在native层也要建一个同样的类进行呼应。同时这也是我在垃圾C++编码水平下,扔选择C++的原因,用C写不了类,也就无法面向对象。PS:这可能是我作为Java程序员最后的执著了吧。
先在头文件里面理思路:

  1. 传入视频源。
  2. 多线程处理音频。#include <libavformat/avformat.h>
    因为这个解包处理的过程是个耗时过程。需要引入<pthread.h> 。其实我感觉这个玩意对于Android程序员算知识盲区,我也搞了好久,后续会把我的藏货笔记粘出来(这些东西其实我早该整理了,临时抱佛脚~)
  3. 准备过程。准备在这个函数里面做一些初始化操作。

MyPlayer.h构思的代码如下:

#include <pthread.h>
#include <libavformat/avformat.h>
class MyPlayer {
public:
    MyPlayer(const char *dataSource);
    virtual ~MyPlayer();//析构函数释放资源
    void prepare();//准备工作(主线程中创建线程)
    void prepareAsy();//真正的异步准备工作
    void start();//播放前的主线程操作
    void startAsy();//播放时的异步操作

private:
    const char * dataSource;
    pthread_t pid_prepare = 0;
};

JB公司对我们还是很人性的,其实构造函数和析构函数可以按照Java的快捷键 alt+insert → con.../de...一样自动生成的,并且.cpp里面也会自动把格式搞好,这里不得不吹一波JB公司。
接下来就是实现了,我会把文字写到代码注释里面。
我的MyPlayer.c实现代码如下:

//
// Created by heima on 2019/6/10.
//

#include "MyPlayer.h"

MyPlayer::MyPlayer(const char *dataSource) {
    //this->dataSource= const_cast<char *>(dataSource);//这是错的,注意指针的复制需要新开辟空间(变量)
    /**
     * 为什么+1???
     * 在 C 语言中,字符串是以空字符做为终止标记(结束符)的
     */
    this->dataSource = new char[strlen(dataSource) + 1];
    //第一个坑  这里一直爆红,类型不匹配。const关键字修饰变量不可修改,全局变量删掉这个关键字
    strcpy(this->dataSource, dataSource);
}

// 函数指针传入 →  pthread执行的方法
// void* (*__start_routine)(void*)
void * customTaskPrepareThread(void * pVoid) {
    MyPlayer * myPlayer = /*new MyPlayer();*/ static_cast<MyPlayer *>(pVoid);
    return 0; // 必须返回,要不就报错
}


void MyPlayer::init() {
        // 创建异步线程 → 最后一个参数传入对象本身,巧妙的异步函数
        pthread_create(&this->pid_prepare, 0, customTaskPrepareThread, this);
}

/**
 * 视频流解包流程
 */
void MyPlayer::prepare() {
    //todo   PS:点开结构体看注释,写的很清楚

    // 1.获取 → 媒体总上下文
    AVFormatContext * avFormatContext=avformat_alloc_context();
    // 媒体字典 key/value存储 → 设置或读取内部参数
    AVDictionary * dictionary = 0;
    /**
     * 1.字典地址
     * 2.添加的键(配置的名称:探测时间/超时时间/最大延时/支持的协议的白名单等,点开结构体可以看到)
     * 3.配置的值(当前为超时时间 微妙)
     * return :0 on success otherwise an error code <0
     */
    av_dict_set(&dictionary, "timeout", "5000000", 0);
    // 释放字典
    av_dict_free(&dictionary);

    // 是不是包裹,如果对方寄来的是,石头(被损坏的数据),那就没法玩了
    int ret = avformat_open_input(&avFormatContext, this->dataSource, 0, &dictionary);
    if (ret) {//0 on success, a negative AVERROR on failure.
        // todo 播放流error的操作
        return;
    }
    // 寻找(媒体流格式数字:0音频,1视频,2字幕)
    ret = avformat_find_stream_info(avFormatContext, 0);
    if(ret < 0) {
        // ....通知用户,xxxx
        return;
    }

    // 循环遍历,媒体格式里面的 流1==音频 流2==字幕 流0==视频
    for (int i = 0; i < avFormatContext->nb_streams; ++i) {
      //todo 1 視頻处理
      //todo 2 视频处理
    }
}

MyPlayer::~MyPlayer() {
    //new的东西一定要释放
    if (this->dataSource) {
        delete this->dataSource;
        this->dataSource = 0;
    }
}

2. 媒体流解析

先挂上媒体流处理的大致流程:
在这里插入图片描述
先有个流程的概念,然后继续继续专注代码。
注意前面提到的avFormatContext->nb_streams,我们可以理解为从整体视频流解析出来的数组长度,长度一般为3(0视频 1音频 2字幕 )。直接带进去看注释:

    /**
     * Number of elements in AVFormatContext.streams.
     *
     * Set by avformat_new_stream(), must not be modified by any other code.
     */
    unsigned int nb_streams;

这里注意提示的avformat_new_stream()函数返回一个指针AVStream **streams就是我们需要遍历的流。

    /**
     * A list of all streams in the file. New streams are created with
     * avformat_new_stream().
     *
     * - demuxing: streams are created by libavformat in avformat_open_input().
     *             If AVFMTCTX_NOHEADER is set in ctx_flags, then new streams may also
     *             appear in av_read_frame().
     * - muxing: streams are created by the user before avformat_write_header().
     *
     * Freed by libavformat in avformat_free_context().
     */
    AVStream **streams;

OK,基本上按照这个思路才开始进入正题,媒体流处理流程开始。

 // 循环遍历,媒体格式里面的 流1==音频 流2==字幕 流0==视频
    for (int i = 0; i < avFormatContext->nb_streams; ++i) {
        //获取媒体流
        AVStream *stream = avFormatContext->streams[i];
        //获取指定流的 解码器ID
        AVCodecParameters *codecParameters = stream->codecpar;
        enum AVCodecID codecId = codecParameters->codec_id;
        //获取解码器
        AVCodec *codec = avcodec_find_decoder(codecId);
        if (!codec) {
            //获取解码器失败动作
            return;
        }
        //AVCodecContext → 解码Context
        AVCodecContext *codecContext = avcodec_alloc_context3(codec);
        if (!codecContext) {
            //获取解码器失败动作
            return;
        }
        //解码器Context 设置参数
        ret = avcodec_parameters_to_context(codecContext, codecParameters);
        if (ret < 0) {
            //设置失败动作
            return;
        }
        //打开解码器   第三参数为字典,我暂时不会设置额外参数,所以空了。。。
        avcodec_open2(codecContext, codec, 0);
        if (ret < 0) {
            //打开失败动作
            return;
        }
        //打开解码器成功后,动态的判断
        if (codecParameters->codec_type == AVMEDIA_TYPE_AUDIO) {
            //todo 2 音频处理

        } else if (codecParameters->codec_type == AVMEDIA_TYPE_VIDEO) {
            //todo 2 视频处理
        }
    }

预测俩个流的处理过程类似,可以进行简单的封装,我们新建个BaseChannel,AudioChannel,VideoChannel的h/cpp,在我们的MyPlayer. h引入这些头文件。大体的工程结构就如下所示了:
在这里插入图片描述

3. 上下层交互

这里可能又点跳,但是你看我代码中好多todo,其实就是native与上层交互的动作,我们要在底层调用上层的一些方法,让上层知道某些信息并进行操作。我以前有文章已经做好只是储备了,想了解的可以看一下。Android JNI手册——Java/Kotlin与Native层的相互调用
直接贴代码,这个玩意后续反正也要用,直接封装好,方便后续粘贴。
JniCallback.h

//
// Created by heima on 2018/5/2.
//

#ifndef FFMPEGTEST_JNICALLBACK_H
#define FFMPEGTEST_JNICALLBACK_H

#include "jni.h"

class JniCallback {
public:
    //如果开多线程,因为JNIEnv无法跨线程,所以JavaVM
    JniCallback(JavaVM *javaVm, JNIEnv *env, jobject instance);
    //某些未知释放操作(预留)
    virtual ~JniCallback();
    // 回调上层(成功)
    void onPrepared(int thread_mode); // thread_mode 主线程/异步线程
    // 回调上层(失败)
    void onErrorAction(int thread_mode, int error_code);

private:
    JavaVM *javaVm = 0;
    JNIEnv *env = 0;
    jobject instance;
    jmethodID jmd_repared;
    jmethodID jmd_error;
};
#endif //FFMPEGTEST_JNICALLBACK_H

JniCallback .cpp

//
// Created by heima on 2018/5/2.
//

#include "JniCallback.h"
#define THREAD_MAIN 1   //主线程
#define THREAD_CHILD 2  //子线程

JniCallback::JniCallback(JavaVM *javaVm, JNIEnv *env, jobject instance) {
    this->javaVm = javaVm;
    this->env = env;
    // 跨线程不能使用JNIEnv *env
    // 升级为全局引用
    this->instance = env->NewGlobalRef(instance);
    // 回调上层
    jclass myPlayerClass = env->GetObjectClass(this->instance);
    const char *sig = "()V";
    this->jmd_repared = env->GetMethodID(myPlayerClass, "onPrepared", sig);
    sig = "(I)V";
    this->jmd_error = env->GetMethodID(myPlayerClass, "onError", sig);
}

JniCallback::~JniCallback() {
    this->javaVm = 0;
    env->DeleteGlobalRef(this->instance);
    this->instance = 0;
    env = 0;
}
// 可能是主线  异步线程
void JniCallback::onPrepared(int thread_mode) {
    if (thread_mode == THREAD_MAIN) {
        // 主线程env可用
        this->env ->CallVoidMethod(this->instance, this->jmd_repared); // 调用上层
    } else if (thread_mode == THREAD_CHILD){
        // 子线程 → javaVm
        JNIEnv * jniEnv = nullptr;
        int ret = this->javaVm->AttachCurrentThread(&jniEnv, 0);
        if (ret != JNI_OK) {
            return;
        }
        jniEnv->CallVoidMethod(this->instance, this->jmd_repared);
        // 释放
        this->javaVm->DetachCurrentThread();
    }
}

同时Myplayer.java也要添加部分代码。因为这里我们的播放显示控件SurfaceView是在Activity中实现与用户交互的,我们的中间件就理所当然的要通知Act一些信息。下面的flag是我随便定义的,同时在native层定义一个头文件,保持同样的含义。你们可以根据上面解包的代码中的todo自己定义文字解释

    var onPreparedListener: OnPreparedListener? = null;
    /**
     * 给native调用的函数
     */
    fun onPrepared() {
        this.onPreparedListener?.onPrepared()
    }

    fun onError(errorCode: Int) {
        var errorText: String? = null
        errorText = when (errorCode) {
            Flags.FFMPEG_ALLOC_CODEC_CONTEXT_FAIL -> "无法根据解码器创建上下文"
            Flags.FFMPEG_CAN_NOT_FIND_STREAMS -> "找不到媒体流信息"
            Flags.FFMPEG_CAN_NOT_OPEN_URL -> "打不开媒体数据源"
            Flags.FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL -> "根据流信息 配置上下文参数失败"
            Flags.FFMPEG_FIND_DECODER_FAIL -> "找不到解码器"
            Flags.FFMPEG_NOMEDIA -> "没有音视频"
            Flags.FFMPEG_READ_PACKETS_FAIL -> "读取媒体数据包失败"
            else -> "垃圾代码..."
            }
          onPreparedListener?.onError(errorText)
    }

注意在你传入jobject的java文件里面写上层待调用函数,如果细节不清楚,看我上面推荐那篇文章

4. 处理流程 →准备

这里开始写Native的prepareNative函数。
因为上面说到需要多线程,我们需要添点料在native-lib.cpp

JavaVM *javaVm;
MyPlayer *myPlayer;
int Jni_OnLoad(JavaVM *javaVm, void *pVoid) {
    ::javaVm = javaVm;
    return JNI_VERSION_1_6;
}

因为Myplayer.cpp中需要回调上层的操作,所以添加全局变量JniCallback * jniCallback = 0;
同时修改了构造函数,在new的时候就把JniCallback传进入。

extern "C"
JNIEXPORT void JNICALL
Java_com_heima_ffmpegtest_MyPlayer_prepareNative(JNIEnv *env, jobject thiz, jstring data_source) {

    JniCallback* jniCallback = new JniCallback(::javaVm, env, thiz);

    const char * dataSource = env->GetStringUTFChars(data_source, NULL);

    // 创建MyPlayer,传入上下层交互的callback
    myPlayer = new MyPlayer(dataSource, jniCallback);
    myPlayer->prepare();

    env->ReleaseStringUTFChars(data_source, dataSource);
}

注意其中的JavaVM,这个我们需要在native.cpp中对这个全局变量进行补全,这个动态注册那部门我记得我有粘上代码Android JNI中巧妙的使用动态注册
突然感觉这些东西其实挺难的,一环扣一环。等我思路理好,代码全部会上传github,以供大家参考批评,相信我。

JavaVM *javaVm;
MyPlayer *myPlayer;
int Jni_OnLoad(JavaVM *javaVm, void *pVoid) {
    ::javaVm = javaVm;
    return JNI_VERSION_1_6;
}

传入之后,补全todo中的通知回调

    // 是不是包裹(被损坏的数据),
    int ret = avformat_open_input(&avFormatContext, this->dataSource, 0, &dictionary);
    if (ret) {//0 on success, a negative AVERROR on failure.
        // .... 写JNI回调,告诉Java层,通知用户,你的播放流损坏的
        /*传到上层的俩个int值,根据自己喜好定义好*/
        this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL);
        return;
    }

基本上前置流程完成50%

5.播放与解析中的多线程

因为我们要用到多线程,这里我就找个一个很好的多线程相关的队列.h,直接用到我们的代码里面去。

#include <queue>
#include <pthread.h>

template <typename T>
class SafeQueue
{
    // Java的回调   ===  C语言的函数指针
    typedef void (*ReleaseCallback) (T *);

private:
    std::queue<T> q;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    int flag; // 标记队列释放工作[true=工作状态,false=非工作状态]
    ReleaseCallback releaseCallback;

public:
    SafeQueue()
    {
        pthread_mutex_init(&mutex, 0);
        pthread_cond_init(&cond, 0);
    }

    ~SafeQueue()
    {
        pthread_mutex_destroy(&mutex);
        pthread_cond_destroy(&cond);
    }

    /**
     * 入队
     */
     void push(T value) {
        pthread_mutex_lock(&mutex); // 为了线程的安全性,锁上

        if (flag) {
            q.push(value);
            pthread_cond_signal(&cond); // 通知
        } else {
            // 释放操作(不知道value是什么类型,int, 对象 ,等等,怎么办?)
            // 我们不知道,交给用户来处理
            if (releaseCallback) {
                releaseCallback(&value);
            }
        }

        pthread_mutex_unlock(&mutex); // 为了让其他线程可以进来,解锁
     }

    /**
     * 出队
     */
    int pop(T & t) {
        int ret = 0;

        pthread_mutex_lock(&mutex); // 为了线程的安全性,锁上

        while (flag && q.empty()) {
            // 如果工作状态 并且 队列中没有数据,就等待)(排队)
            pthread_cond_wait(&cond, &mutex);
        }

        if (!q.empty()) {
            t = q.front();
            q.pop();
            ret = 1;
        }

        pthread_mutex_unlock(&mutex); // 为了让其他线程可以进来,解锁

        return ret;
     }

     void setFlag(int flag) {
         pthread_mutex_lock(&mutex); // 为了线程的安全性,锁上

         this->flag = flag;
         pthread_cond_signal(&cond); // 通知

         pthread_mutex_unlock(&mutex); // 为了让其他线程可以进来,解锁
    }

    // 队列释放为空
    int isEmpty() {
        return q.empty();
    }

    // 队列的大小
    int queueSize() {
        return q.size();
    }

    // 清空队列
    void clearQueue() {
        pthread_mutex_lock(&mutex); // 为了线程的安全性,锁上

        unsigned int  size = q.size();
        for (int i = 0; i < size; ++i) {
            // 循环 一个个的释放
            T value = q.front();
            if (releaseCallback) {
                releaseCallback(&value);
            }
            q.pop();
        }
        pthread_mutex_unlock(&mutex); // 为了让其他线程可以进来,解锁
    }

    void setReleaseCallback(ReleaseCallback releaseCallback) {
        this->releaseCallback = releaseCallback;
    }
};

接下来我们重新考虑跟实现MyPlayer.cpp中我们空下的准备/开始播放函数,这理跟上面写的稍微有些出入。我这里修改的原因是,某些先前定义的局部变量,可能后面还有的用,所以就修改成全局变量了。我直接把完整的代码粘贴进来。
MyPlayer.h ↓↓↓

#include <pthread.h>
#include "VideoChannel.h"
#include "AudioChannel.h"
#include "JniCallback.h"
#include "flags.h"
extern "C" {
#include <libavformat/avformat.h>
};

class MyPlayer {
public:
    MyPlayer(const char *dataSource, JniCallback *pCallback);
    virtual ~MyPlayer();//析构函数释放资源
    void prepare();//准备工作(主线程中创建线程)
    void prepareAsy();//真正的异步准备工作
    void start();//播放前的主线程操作
    void startAsy();//播放时的异步操作

private:
    char *dataSource;
    pthread_t pid_prepare = 0;
    AVFormatContext * avFormatContext = 0; // 媒体的总上下文,prepare/start均需要
    JniCallback * jniCallback = 0;
    VideoChannel *videoChannel=0;
    AudioChannel *audioChannel=0;
    int isPlayer = 0;//播放状态 
    SafeQueue<AVPacket *> packets; // 音频:aac,  视频:h264  
    SafeQueue<AVFrame *> frames; // 音频:pcm    视频:yuv
};

注意点1:俩个安全队列盛放的泛型类型不一样,因为流中直接取包出来的原始数据跟我们解码后的数据格式不一样。 你问我为啥知道?因为这个基础理论还是要提前看一看的,特别是现在多媒体这么火的。。。

接下来关于MyPlayer.cpp的实现如下代码:

#include "MyPlayer.h"
MyPlayer::MyPlayer(const char *dataSource, JniCallback *pCallback) {
    //this->dataSource= const_cast<char *>(dataSource);//这是错的,注意指针的复制需要新开辟空间(变量)
    /**
     * 为什么+1???
     * 在 C 语言中,字符串是以空字符做为终止标记(结束符)的
     */
    this->dataSource = new char[strlen(dataSource) + 1];
    //第一个坑  这里一直爆红,类型不匹配。const关键字修饰变量不可修改,全局变量删掉这个关键字
    strcpy(this->dataSource, dataSource);
}

// 函数指针传入 →  pthread执行的方法
// void* (*__start_routine)(void*)
void *customTaskPrepareThread(void *pVoid) {
    MyPlayer *myPlayer = /*new MyPlayer();*/ static_cast<MyPlayer *>(pVoid);
    myPlayer->prepareAsy();
    return 0; // 必须返回,有坑
}

// 函数指针 线程运行处
// void* (*__start_routine)(void*)
void * customTaskStartThread(void * pVoid) {
    MyPlayer * myPlayer = static_cast<MyPlayer *>(pVoid);
    myPlayer->startAsy();
    return 0;
}


void MyPlayer::prepare() {
    // 创建异步线程 → 最后一个参数传入对象本身,巧妙的异步函数
    pthread_create(&this->pid_prepare, 0, customTaskPrepareThread, this);
}

/**
 * 媒体流解包流程
 */
void MyPlayer::prepareAsy() {
    //todo   PS:点开结构体看注释,写的很清楚
    // 1.获取 → 媒体总Context
    this->avFormatContext = avformat_alloc_context();
    // 媒体字典 key/value存储 → 设置或读取内部参数
    AVDictionary *dictionary = 0;
    /**
     * 1.字典地址
     * 2.添加的键(配置的名称:探测时间/超时时间/最大延时/支持的协议的白名单等,点开结构体可以看到)
     * 3.配置的值(当前为超时时间 微妙)
     * return :0 on success otherwise an error code <0
     */
    av_dict_set(&dictionary, "timeout", "5000000", 0);
    // 释放字典
    av_dict_free(&dictionary);

    // 是不是包裹,(被损坏的数据),
    int ret = avformat_open_input(&avFormatContext, this->dataSource, 0, &dictionary);
    if (ret) {//0 on success, a negative AVERROR on failure.
        // .... 写JNI回调,告诉Java层,通知用户,你的播放流损坏的
        /*传到上层的俩个int值,根据自己喜好定义好*/
        this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL);
        return;
    }
    // 寻找(媒体流格式数字:0音频,1视频,2字幕)
    ret = avformat_find_stream_info(avFormatContext, 0);
    if (ret < 0) {
        // .... 写JNI回调,告诉Java层,通知用户,xxxx
        return;
    }

    // 循环遍历,媒体格式里面的 流1==音频 流2==字幕 流0==视频
    for (int i = 0; i < avFormatContext->nb_streams; ++i) {
        //获取媒体流
        AVStream *stream = avFormatContext->streams[i];
        //获取指定流的 解码器ID
        AVCodecParameters *codecParameters = stream->codecpar;
        enum AVCodecID codecId = codecParameters->codec_id;
        //获取解码器
        AVCodec *codec = avcodec_find_decoder(codecId);
        if (!codec) {
            //获取解码器失败动作
            return;
        }
        //AVCodecContext → 解码Context
        AVCodecContext *codecContext = avcodec_alloc_context3(codec);
        if (!codecContext) {
            //获取解码器失败动作
            return;
        }
        //解码器Context 设置参数
        ret = avcodec_parameters_to_context(codecContext, codecParameters);
        if (ret < 0) {
            //设置失败动作
            return;
        }
        //打开解码器   第三参数为字典,我暂时不会设置额外参数,所以空了。。。
        avcodec_open2(codecContext, codec, 0);
        if (ret < 0) {
            //打开失败动作
            return;
        }
        //打开解码器成功后,动态的判断
        if (codecParameters->codec_type == AVMEDIA_TYPE_AUDIO) {
            //todo 2 音频处理

        } else if (codecParameters->codec_type == AVMEDIA_TYPE_VIDEO) {
            //todo 2 视频处理
            this->videoChannel = new VideoChannel(i, codecContext);
        }

    }
    // 循环结束 → 准备完毕,告诉上层,开始交互
    this->jniCallback->onPrepared(THREAD_CHILD);
}


void MyPlayer::startAsy() {
    // 开始播放,需要异步
    // 让音频 和 视频 两个通道 运行起来
    isPlayer = 1; // 播放的状态
    if (this->videoChannel) {
        //todo Video中多线程处理解码与播放
        this->videoChannel->start();
    }
    pthread_t pdi_start;
    // 开启线程(读包:把未解码的流数据  音频 、 视频) 放入队列
    pthread_create(&pdi_start, 0, customTaskPrepareThread, this);
}

void MyPlayer::start() {
// 循环 读取视频包
    while (this->isPlayer) {
        // 未解码的→保存在AVPacket里面的
        AVPacket * packet = av_packet_alloc();
        int ret = av_read_frame(this->avFormatContext, packet);
        if (!ret) { // ret == 0
            // 把已经得到的Packet 放入队列中
            // 判断是否是音频/视频
            if (videoChannel && videoChannel-> == packet->stream_index) {
                // 视频包放入对应通道
                this->videoChannel->packets.push(packet);
            } /*else if (audioChannel && audioChannel->stream_index == packet->stream_index) {

            }*/
        } else if (ret == AVERROR_EOF) {  // 读到底了
            // TODO 后续维护
        } else {
            // 代表失败了
            break;
        }
    } // end while

    // 释放工作
    isPlayer = 0;
    //todo 关闭视频处理通道
    videoChannel->stop();
}

MyPlayer::~MyPlayer() {
    //new的东西一定要释放
    if (this->dataSource) {
        delete this->dataSource;
        this->dataSource = 0;
    }
}

注意点2:代码有2个todo的关于Channel的stat和stop,作用就跟字面意思一样,细节请看下面的流程。

6. 视频流处理

前面代码已经可以遍历流获视频流,并传入我们定义好的VideoChannel/BaseChannel。我们就来写一下它。无论我们处理音频还是视频,都是需要把遍历媒体流的下表和对应的解码器context传进去的,我们就要封装一个BaseChannel,传入这些抽取的公共内容。

extern "C" {
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
};
class BaseChannel {
public:
    int index;
    AVCodecContext *avCodecContext;
    BaseChannel(int index, AVCodecContext *avCodecContext){
        this->index = index;
        this->avCodecContext = avCodecContext;
    }
	virtual ~BaseChannel(); 
   //队列中原始数据:  音频:aac,  视频:h264
    SafeQueue<AVPacket *> packets;
    // 处理后原始数据: 音频:pcm    视频:yuv
    SafeQueue<AVFrame *> frames;
    bool isPlaying = 1;
};

创建视频通道继承父类就可以使用父类的变量了,这里的stop/start方法就是是在**5.**中提到的操作。直接上代码。
VideoChannel.h

#include "BaseChannel.h"
extern "C" {
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
};

class VideoChannel :public  BaseChannel{
public:
    VideoChannel( int index ,AVCodecContext *avCodecContext);
    virtual ~VideoChannel();
    void start();
    void stop();
    //耗时操作分开→多线程 
    pthread_t pid_video_decode;//解码
    void video_decode();
    pthread_t pid_video_player; //播放
    void video_player();
};

具体实现VideoChannel.cpp

//
// Created by heima on 2020/6/14.
//

#include "VideoChannel.h"
extern "C" {
 #include <libavutil/avutil.h>
}
VideoChannel::VideoChannel( int index ,AVCodecContext *avCodecContext):BaseChannel( index,  avCodecContext) {}

VideoChannel::~VideoChannel() {}

void * task_video_decode(void * pVoid) {
    //解码异步实现
    VideoChannel * videoChannel = static_cast<VideoChannel *>(pVoid);
    videoChannel->video_decode();
    return 0;
}

void * task_video_player(void * pVoid) {
    //播放异步实现
    VideoChannel * videoChannel = static_cast<VideoChannel *>(pVoid);
    videoChannel->video_player();
    return 0;
}



/**
 * 从队列里面取出,未解码的数据
 * 解码 → 播放
 */
void VideoChannel::start() {
    // 未解码的队列开始工作(锁上)
    packets.setFlag(1);
    // 存放已解码的队列开始工作
    frames.setFlag(1);
    // 解码线程
    pthread_create(&pid_video_decode, 0, task_video_decode, this);
    // 播放线程
    pthread_create(&pid_video_player, 0, task_video_player, this);
}

void VideoChannel::stop() {

}

// 异步解码
void VideoChannel::video_decode() {
    // 取出 未解码的 队列数据
    AVPacket * packet;
    while (isPlaying) {
        int ret = this->packets.pop(packet);

        if (!isPlaying) {
            break;
        }

        if (!ret) {
            continue;
        }

        // 取到了未解码的视频数据包
        ret = avcodec_send_packet(this->avCodecContext, packet);
        if (ret) {
            break; // 失败了
        }

        AVFrame * avFrame = av_frame_alloc();
        ret = avcodec_receive_frame(this->avCodecContext, avFrame);
        if (ret == AVERROR(EAGAIN)) {
            // 代表帧取得不完整
            continue; // 直到取到完整的帧为止
        } else if (ret != 0) {
            // TODO 以后做释放工作...
            break;
        }

        // 取到原始数据
        this->frames.push(avFrame);
    }

    // TODO 出了循环,释放
    // ...
}
// 异步播放
void VideoChannel::video_player() {
    // yuv ---> rgba(才能显示到屏幕上)
    // 转化的上下文
    SwsContext * swsContext = sws_getContext(
            // 原始的一层  宽 高 格式
            this->avCodecContext->width, this->avCodecContext->height, this->avCodecContext->pix_fmt,
            // 目标 最终要显示到屏幕的信息,最好和原始的保持一致
            this->avCodecContext->width, this->avCodecContext->height, AV_PIX_FMT_RGBA,
            SWS_BILINEAR, NULL, NULL, NULL
    );
    AVFrame * frame = 0;
    while (isPlaying) {
        int ret = frames.pop(frame);
    }
    
}

其实写到这里,视频流处理一定会差不多了,但是现在代码只是顺着思路捋下来了,我还没有真机试过,因为我还没有找到可用视频流,我相信,加入真的视频流,这个工程一定会报错了!
所以说,未完待续,继续找Bug。

猜你喜欢

转载自blog.csdn.net/ma598214297/article/details/106600852