美文网首页 移动 前端 Python Android Java
FFmpeg(三)自定义播放器基本知识点

FFmpeg(三)自定义播放器基本知识点

作者: zcwfeng | 来源:发表于2020-10-25 13:18 被阅读0次

    播放器播放基本方法

    1. 打开媒体

    start,stop,pause,play,prepare,setDataSource
    这是上层调用的基本方法,和播放器基本功能定义。
    prepare 主要是播放前准备好相关上下文等环境,
    setDataSource 设置播放源
    我们主要看下FFmpeg中的相关知识

    prepare

    AVFormatContext 获取上下文

    AVFormatContext *avFormatContext = avformat_alloc_context();
    

    打开媒体,avformat_open_input

        /**
         * 1.打开媒体文件
         */
    //参数3,文件的封装格式,传null表示自动检测格式 avi/flv
    //参数4,map集合,如打开网络文件
        AVDictionary **opts;
        av_dict_set(opts, "timeout", "300000", 0);
        int ret = avformat_open_input(&avFormatContext, path, 0, 0);
        if (ret != 0) {
            LOGE("打开%s失败,返回:%d,错误描述%s", path, ret, av_err2str(ret));
            helper->onError(FFMPEG_CAN_NOT_OPEN_URL,THREAD_CHILD);
            return;
        }
    
    1. 查找媒体流

    根据前面FFmpeg(一),FFmpeg(二)流程介绍

    查找媒体->得到视频时长->循环媒体几道流->查找解码器->
    ->打开解码器(给成员赋值)->判断处理视频流和音频流计算帧率->

    帧率结构体,av_q2d 函数可以帮我们把结构体转换成帧率

    typedef struct AVRational{
        int num; ///< Numerator
        int den; ///< Denominator
    } AVRational;
    

    封装channel

    BaseChannel 封装公用的操作
    VideoChannel

    播放放在播放的线程里面
    解码放在解码的线程里面处理
    自定义安全队列,思路用互斥量实现

    JavaCallHelper JNI 回调java的一个辅助

    想要c/c++ 子线程回调java主线程,需要JavaVM,是JNI定义的结构体,JNIEnv 与线程绑定

    我们看下Java里面的定义回调

    private native long nativeInit();
    
        private native void setDataSource(long nativeHandl,String path);
    
        private native void prepare(long nativeHandl);
    
    
        //-------------C++ 给Java 的各种回调,类似MediaPlayer.OnErrorListener等--
    
        private void onError(int code){
            if(onErrorListener != null){
                onErrorListener.onError(code);
            }
        }
    
    
        private void onProgress(int progress){
            if(onProgressListener != null){
                onProgressListener.onProgress(progress);
            }
        }
    
        private void onPrepare(){
            if(onPrepareListener != null){
                onPrepareListener.onPrepare();
            }
        }
    
        public interface OnErrorListener{
            void onError(int err);
        }
        public interface OnProgressListener{
            void onProgress(int progress);
        }
        public interface OnPrepareListener{
            void onPrepare();
        }
    
        private OnErrorListener onErrorListener;
        private OnProgressListener onProgressListener;
        private OnPrepareListener onPrepareListener;
    
        public void setOnErrorListener(OnErrorListener onErrorListener) {
            this.onErrorListener = onErrorListener;
        }
    
        public void setOnProgressListener(OnProgressListener onProgressListener) {
            this.onProgressListener = onProgressListener;
        }
    
        public void setOnPrepareListener(OnPrepareListener onPrepareListener) {
            this.onPrepareListener = onPrepareListener;
        }
    

    C++ 中的回调绑定

    JavaCallHelper 定义

    #ifndef ZCWPLAYER_JAVACALLHELPER_H
    #define ZCWPLAYER_JAVACALLHELPER_H
    #include <jni.h>
    
    //标记线程 因为子线程需要attach
    #define THREAD_MAIN 1
    #define THREAD_CHILD 2
    
    //错误代码
    //打不开视频
    #define FFMPEG_CAN_NOT_OPEN_URL 1
    //找不到流媒体
    #define FFMPEG_CAN_NOT_FIND_STREAMS 2
    //找不到解码器
    #define FFMPEG_FIND_DECODER_FAIL 3
    //无法根据解码器创建上下文
    #define FFMPEG_ALLOC_CODEC_CONTEXT_FAIL 4
    //根据流信息 配置上下文参数失败
    #define FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL 6
    //打开解码器失败
    #define FFMPEG_OPEN_DECODER_FAIL 7
    //没有音视频
    #define FFMPEG_NOMEDIA 8
    class JavaCallHelper {
    public:
        JavaCallHelper(JavaVM *_javaVm, JNIEnv *_env, jobject &_jobj);
        ~JavaCallHelper();
    
        void onError(int code,int thread = THREAD_MAIN);
        void onPrepare(int thread = THREAD_MAIN);
        void onProgress(int progress,int thread = THREAD_MAIN);
    
    public:
        JavaVM *javaVM;
        JNIEnv *env;
        jobject jobj;
        jmethodID jmid_error;
        jmethodID jmid_prepare;
        jmethodID jmid_progress;
    };
    
    
    #endif //ZCWPLAYER_JAVACALLHELPER_H
    

    ---------------------------------
    实现

    #include "JavaCallHelper.h"
    
    
    JavaCallHelper::JavaCallHelper(JavaVM *_javaVm, JNIEnv *_env, jobject &_jobj): javaVM(_javaVm), env(_env){
        jobj = env->NewGlobalRef(_jobj);
        jclass jclazz = env->GetObjectClass(jobj);
    
        jmid_error = env->GetMethodID(jclazz,"onError","(I)V");
        jmid_prepare = env->GetMethodID(jclazz,"onError","(I)V");
        jmid_progress = env->GetMethodID(jclazz,"onError","(I)V");
    
    }
    
    JavaCallHelper::~JavaCallHelper() {
        env->DeleteGlobalRef(jobj);
        jobj = 0;
    }
    // 如果是主线程直接调用,如果是子线程,必须AttachCurrentThread当前线程的env绑定
    void JavaCallHelper::onError(int code, int thread) {
        if (thread == THREAD_CHILD) {
            //子线程
            JNIEnv *jniEnv;
            if (javaVM->AttachCurrentThread(&jniEnv, 0) != JNI_OK) {
                return;
            }
            jniEnv->CallVoidMethod(jobj, jmid_error, code);
            javaVM->DetachCurrentThread();
        } else {
            env->CallVoidMethod(jobj, jmid_error, code);
        }
    
    }
    
    void JavaCallHelper::onPrepare(int thread) {
        if (thread == THREAD_CHILD) {
            JNIEnv *jniEnv;
            if (javaVM->AttachCurrentThread(&jniEnv, 0) != JNI_OK) {
                return;
            }
            jniEnv->CallVoidMethod(jobj, jmid_prepare);
            javaVM->DetachCurrentThread();
        } else {
            env->CallVoidMethod(jobj, jmid_prepare);
        }
    }
    
    void JavaCallHelper::onProgress(int progress, int thread) {
        if (thread == THREAD_CHILD) {
            JNIEnv *jniEnv;
            if (javaVM->AttachCurrentThread(&jniEnv, 0) != JNI_OK) {
                return;
            }
            jniEnv->CallVoidMethod(jobj, jmid_progress, progress);
            javaVM->DetachCurrentThread();
        } else {
            env->CallVoidMethod(jobj, jmid_progress, progress);
        }
    }
    

    ANativeWindow

    ANativeWindow代表的是本地窗口。通过 ANativeWindow_fromSurface 由surface得到ANativeWindow窗口 , ANativeWindow_release 进行释放。类似Java,可以对它进行lock、unlockAndPost以及通过 ANativeWindow_Buffer 进行图像数据的修改。

     #include <android/native_window_jni.h>
    //根据Surface获得 ANativeWindow
    window = ANativeWindow_fromSurface(env, surface); 
    //设置 ANativeWindow 属性
    ANativeWindow_setBuffersGeometry(window, w,
    h,
    WINDOW_FORMAT_RGBA_8888);
     // lock获得 ANativeWindow 需要显示的数据缓存
    ANativeWindow_Buffer window_buffer;
    if (ANativeWindow_lock(window, &window_buffer, 0)) {
        ANativeWindow_release(window); window = 0;
        return;
    }
    //填充rgb数据给dst_data
    uint8_t *dst_data = static_cast<uint8_t *>(window_buffer.bits);
     //......
    ANativeWindow_unlockAndPost(window);
    

    在NDK中使用ANativeWindow编译时需要链接NDK中的 libandroid.so 库

    #编译链接NDK/platforms/android-X/usr/lib/libandroid.so target_link_libraries(XXX android )
    

    由于FFmpeg在解码视频时一般情况而言视频数据会被解码为YUV数据,而ANativeWindow并不能直接显示YUV数 据的图像,所以需要将YUV转换为RGB进行显示。而FFmpeg的swscale模块就提供了颜色空间转换的功能。

    FFmpeg的swscale转换效率可能存在问题,如ijkPlayer中使用的是google的libyuv库进行的转换。

     extern "C"{
    #include <libswscale/swscale.h>
    }
    // 参数分别为:转换前宽高与格式,转换后宽高与格式,转换使用的算法,输入/输出图像滤波器,特定缩放算法需要的 参数
    SwsContext *sws_ctx = 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];
    int dst_linesize[4];
    //根据格式申请内存
    av_image_alloc(dst_data, dst_linesize,
    avCodecContext->width, avCodecContext->height, 
    AV_PIX_FMT_RGBA, 1); AVFrame *frame = 解码后待转换的结构体;
    sws_scale(sws_ctx,
                      reinterpret_cast<const uint8_t *const *>(frame->data),       
                      frame->linesize, 0, frame->height,
                      dst_data, dst_linesize);
    

    在得到了RGBA格式的时候后就可以向ANativeWindow填充。但是在数据填充时,需要根据 window_buffer.stride 来一行行拷贝,如:

     uint8_t *dst_data = static_cast<uint8_t *>(window_buffer.bits); 
    //一行需要多少像素 * 4(RGBA)
    int32_t dst_linesize = window_buffer.stride * 4;
    uint8_t *src_data = data; //需要显示的数据
    int32_t src_linesize = linesize; //数据每行字节数
    //一次拷贝一行
    for (int i = 0; i < window_buffer.height; ++i) {
         memcpy(dst_data + i * dst_linesize, src_data + i * src_linesize, src_linesize);
    }
    

    以我们播放的852x480视频为例,在将ANativeWindow的格式设置为同样大小后,得到的window_buffer.stride为 864,则每行需要864*4 = 3456个字节数据。而将视频解码数据转换为RGBA之后获得的linesize为3408。window 与图像数据的每行数据数不同,所以需要一行行拷贝。
    为什么会出现不同?

    无论是window的stride还是ffmpeg的linesize只会出现比widget大的情况,这意味着不可能出现图像数据缺失的情 况,但是为什么会比widget大呢?这是由于字节对齐不同导致的。在编译FFmpeg时,会在FFmpeg源码根目录下 生成一个config.h文件,这个文件中根据编译目标平台的特性定义了一些列的宏,其中

     #define HAVE_SIMD_ALIGN_16 0
    #define HAVE_SIMD_ALIGN_32 0
    #define HAVE_SIMD_ALIGN_64 0
    

    这三个宏表示的就是FFmpeg中数据的以几字节对齐。在目标为android arm架构下,均为0。则FFmpeg使用8字 节对齐( libavcodec/internal.h )

     #if HAVE_SIMD_ALIGN_64
    # define STRIDE_ALIGN 64 /* AVX-512 */ #elif HAVE_SIMD_ALIGN_32
    # define STRIDE_ALIGN 32
    #elif HAVE_SIMD_ALIGN_16
    # define STRIDE_ALIGN 16
    #else
    # define STRIDE_ALIGN 8
    #endif
    

    那么图像宽为852,即数据为852*4=3408的情况下,3408%8=0。则不需要占位字节用于对齐,因此linesize为 3408。

    而ANativeWindow中的stride计算出来结果为3456。这是因为ANativeWindow在此处是以64字节对齐,若stride 为宽度的852,数据为3408的情况下,3408/16=53.25,此时需要占位字节将其补充为54,则54*64=3456,所以 stride为3456以便于64字节对齐。

    字节对齐就好像是一个放肥皂的盒子,每行可放10盒肥皂,即以10字节对齐,若有一行不足10盒,为了保证整齐 度,你可以放入一些无意义的空盒子让他补充至10盒。我们能够经常在一些结构体定义中看到这些占位用的空数 据。如需要完成微信资源混淆时,需要学习Resources.arsc格式,android源码中定义有结构体:

    struct ResTable_type
    {
    //......
    // Must be 0. uint16_t reserved;
     //......
    };
    

    其中 reserved 字段就是无意义的, must be 0 只用于占位以满足字节对齐。

    知识点

    1. C++ 全局引用

    NewGlobalRef 全局引用不要忘记释放

    JavaCallHelper::JavaCallHelper(JavaVM *_javaVm, JNIEnv *_env, jobject &_jobj): javaVM(_javaVm), env(_env){
        jobj = env->NewGlobalRef(_jobj);
        jclass jclazz = env->GetObjectClass(jobj);
    
        jmid_error = env->GetMethodID(jclazz,"onError","(I)V");
        jmid_prepare = env->GetMethodID(jclazz,"onPrepare","()V");
        jmid_progress = env->GetMethodID(jclazz,"onProgress","(I)V");
    
    }
    
    JavaCallHelper::~JavaCallHelper() {
        env->DeleteGlobalRef(jobj);
        jobj = 0;
    }
    

    2.互斥量,类似java的syncronized

    3.安全队列自己实现

    #ifndef ZCWPLAYER_SAFE_QUEUE_H
    #define ZCWPLAYER_SAFE_QUEUE_H
    
    #include <pthread.h>
    #include <queue>
    
    using namespace std;
    template<typename T>
    
    class SafeQueue {
        typedef void (*ReleaseHandle)(T &);
    
        typedef void (*SyncHandle)(queue<T> &);
    public:
        SafeQueue() {
            pthread_mutex_init(&mutex, 0);
            pthread_cond_init(&cond, 0);
        }
    
        virtual ~SafeQueue() {
            pthread_cond_destroy(&cond);
            pthread_mutex_destroy(&mutex);
        }
    
        void enQueue(T new_value) {
            pthread_mutex_lock(&mutex);
            if (mEnable) {
                q.push(new_value);
                pthread_cond_signal(&cond);
            } else {
                releaseHandle(new_value);
            }
            pthread_mutex_unlock(&mutex);
    
        }
    
    
        int deQueue(T &value) {
            int ret = 0;
            pthread_mutex_lock(&mutex);
            //在多核处理器下 由于竞争可能虚假唤醒 包括jdk也说明了
            while (mEnable && q.empty()) {
                pthread_cond_wait(&cond, &mutex);
            }
            if (!q.empty()) {
                value = q.front();
                q.pop();
                ret = 1;
            }
            pthread_mutex_unlock(&mutex);
    
            return ret;
        }
    
        void setEnable(bool enable) {
            pthread_mutex_lock(&mutex);
            this->mEnable = enable;
            pthread_cond_signal(&cond);
            pthread_mutex_unlock(&mutex);
    
        }
    
        int empty() {
            return q.empty();
        }
    
        int size() {
            return q.size();
        }
    
        void clear() {
            pthread_mutex_lock(&mutex);
            int size = q.size();
            for (int i = 0; i < size; ++i) {
                T value = q.front();
                releaseHandle(value);
                q.pop();
            }
            pthread_mutex_unlock(&mutex);
        }
    
        void sync() {
            pthread_mutex_lock(&mutex);
            syncHandle(q);
            pthread_mutex_unlock(&mutex);
        }
    
        void setReleaseHandle(ReleaseHandle r) {
            releaseHandle = r;
        }
    
        void setSyncHandle(SyncHandle s) {
            syncHandle = s;
        }
    
    private:
        pthread_cond_t cond;
        pthread_mutex_t mutex;
        queue <T> q;
        bool mEnable;
        ReleaseHandle releaseHandle;
        SyncHandle syncHandle;
    };
    
    #endif //ZCWPLAYER_SAFE_QUEUE_H
    

    4.小的设计while循环阻塞队列

    5.swscale
    sws_getContext:YUV 转换 RGB 才能播放,缩放,sws_getContext

    sws_scale:AVFrame 存储 RGBA的个是 数组的数组
    //R
    //G
    //B
    //A
    byte[][4]
    sws_scale 参数 是个指针数组unit8_t * const[]

    av_image_alloc()

    6 技巧:设置 宽高,让视频比例看起不奇怪,
    设置的是视频内部排列像素的宽高,不是物理尺寸
    ANativeWindow_setBuffersGeometry

    1. 视频数据刷新到buffer和window

    8. 字节对齐 和 步长

    各个硬件平台对存储空间的处理上有很大的不同。

    一些平台对某些特定类型的数据只能从某些特定地址开始存取。

    比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对 齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对 数据存放进行对齐,会在存取效率 上带来损失。

    比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始 的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出 的结果的高低字节进行拼凑才能得到该32bit数据。

    我们看一个6字节对齐
    加入我们有这样一个结构体

    struct{
        int i;//4字节
        short j://2字节
        int k;//四字节
    -》看这里
      uint8_t  a[2];
    }
    

    有时候我们可能会看到开源代码或者别人代码结构体有一个占位
    例如:·apk的字节对齐,优化解析速度

    举个栗子:

    0x0       0x1             0x2        0x3       0x4      0x5     0x6      0x7
                x              x         x         x 
    

    地址从奇数0x1开始,但是我们只能从偶地址读取,那么从0x0-0x3 对去三个数据,在从0x4 读取一个。把他们拼接读取了两次

    9 集成需要的细节 libz ffmpeg依赖libz

    1. 调试没有输出的ndk,用debug模式不用打断点,报错代码就会自动定位到c++代码栈并暂停

    11,c++ 指针类型成员变量一定初始化,避免bug

    1. 播放器:视频,音频,音视频同步

    相关文章

      网友评论

        本文标题:FFmpeg(三)自定义播放器基本知识点

        本文链接:https://www.haomeiwen.com/subject/pmpumktx.html