移动 前端 Python Android Java

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

2020-10-25  本文已影响0人  zcwfeng

播放器播放基本方法

  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. 播放器:视频,音频,音视频同步
上一篇下一篇

猜你喜欢

热点阅读