FFmpeg(三)自定义播放器基本知识点
播放器播放基本方法
- 打开媒体
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;
}
- 查找媒体流
根据前面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
- 视频数据刷新到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
- 调试没有输出的ndk,用debug模式不用打断点,报错代码就会自动定位到c++代码栈并暂停
11,c++ 指针类型成员变量一定初始化,避免bug
- 播放器:视频,音频,音视频同步