构建私有播放器
前言
关于如何编译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程序员最后的执著了吧。
先在头文件里面理思路:
- 传入视频源。
- 多线程处理音频。
#include <libavformat/avformat.h>
因为这个解包处理的过程是个耗时过程。需要引入<pthread.h>
。其实我感觉这个玩意对于Android程序员算知识盲区,我也搞了好久,后续会把我的藏货笔记粘出来(这些东西其实我早该整理了,临时抱佛脚~) - 准备过程。准备在这个函数里面做一些初始化操作。
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。