在之前的文章中FFmpeg的编译和集成也完成了,这一篇开始视频播放器处理的第一步:解封装
解封装
在解封装的代码开始以前,我们需要引入Log机制,虽然现在ndk开发中也能debug了但是有log会更方便。具体怎么引入在我的Android JNI开发系列之Java与C相互调用一文最后有方法,这里就不再赘述了。
解封装这一步是处理视频数据的开始,需要处理以下几步:
解封装.jpgAndroid的界面比较简单这里就不写了,样子是这样的:
ui.png就两个按钮,第一个按钮把初始化和打开数据集成在一步了。
准备工作
为了方便测试,之后都把测试的方法定义在FFmpegUtil.java文件中,例如这里有初始化,打开数据文件和读取数据文件三个方法,写出来就是:
public class FFmpegUtil {
static {
System.loadLibrary("native-lib");
}
public static native void init();
public static native void open(String url);
public static native void read();
}
然后实现都在native-lib.cpp文件中
extern "C"
JNIEXPORT void JNICALL
Java_net_arvin_ffmpegtest_FFmpegUtil_init(JNIEnv *env, jclass type) {
//TODO
}
extern "C"
JNIEXPORT void JNICALL
Java_net_arvin_ffmpegtest_FFmpegUtil_open(JNIEnv *env, jclass type, jstring url_) {
const char *url = env->GetStringUTFChars(url_, 0);
//TODO
env->ReleaseStringUTFChars(url_, url);
}
extern "C"
JNIEXPORT void JNICALL
Java_net_arvin_ffmpegtest_FFmpegUtil_read(JNIEnv *env, jclass type) {
//TODO
}
当然实现还没有写。为了把每块的代码分开,所以我们把解封装的代码放到一个叫做Demux的cpp文件中,新建c++class Demux,然后在CMakeLists文件中添加到库中(不然找不到文件)。
然后在Demux.h文件中定义三个方法:
class Demux {
public:
virtual void init();
virtual void open(const char *url);
virtual bool read();
}
准备工作到这里就基本结束了,这三个方法的实现肯定都在Demux.cpp文件中。
初始化
其实没啥说的,就是调用FFmpeg的api,首先需要注册各种封装器和初始化网络;当然对于网络模块的初始化,是对在线视频才需要的。
void Demux::init() {
//注册所有封装器
av_register_all();
//初始化网络
avformat_network_init();
LOG_I("Register FFmpeg!");
}
这样写了,肯定会提示找不到方法,所以需要引入头文件,记住ffmpeg的库引入都需要加入extern "C"
,当然还有Log文件,如下:
#include "Log.h"
extern "C" {
#include <libavformat/avformat.h>
}
这样初始化就完成了。
打开数据文件
核心方法就是avformat_open_input
,需要传入AVFormatContext,这个上下文对象和文件的url以及其他配置信息,返回值是int,0表示成功,非0可以通过av_strerror转成对应的str信息提示。
这一步就能把上下文对象初始化,然后再调用avformat_find_stream_info
方法就能把常见的文件信息都获取到,参数就是传入上下文和配置字典(可不传)。
然后带回读数据要区分是音频还是视频,所以可以通过av_find_best_stream
方法获取到音频流的索引和视频流的索引。
打开数据文件基本就这三个重要的方法,因为获取音频和视频信息的时候也能获取到对应的音视频参数,所以再在Demux.h中定义了两个方法,获取音频和视频的参数:
virtual void getVideoParams();
virtual void getAudioParams();
当然里边我们用到的上下文和音视频流索引也在Demux.h中定义好:
protected:
AVFormatContext *ic;
int videoStream = 0;
int audioStream = 1;
其中AVFormatContext肯定是找不到的,这时候也不要引用FFmpeg的头文件,避免耦合,可以定义成struct AVFormatContext;
。
然后实现的方法如下:
void Demux::open(const char *url) {
LOG_I("open file %s begin", url);
//打开文件
int re = avformat_open_input(&ic, url, 0, 0);
if (re != 0) {
char buff[1024] = {0};
av_strerror(re, buff, sizeof(buff));
LOG_E("Demux open %s failed! error is %s", url, buff);
return;
}
LOG_I("Demux open %s success", url);
//读取文件信息
re = avformat_find_stream_info(ic, 0);
if (re != 0) {
char buff[1024] = {0};
av_strerror(re, buff, sizeof(buff));
LOG_E("avformat_find_stream_info failed! error is %s", buff);
return;
}
//读取总时长
int64_t totalMs = ic->duration / (AV_TIME_BASE / 1000);
LOG_I("total ms = %lld", totalMs);
getVideoParams();
getAudioParams();
}
void Demux::getVideoParams() {
if (!ic) {
return;
}
int re = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, 0, 0);
if (re < 0) {
LOG_E("av_find_best_stream video failed");
return;
}
videoStream = re;
}
void Demux::getAudioParams() {
if (!ic) {
return;
}
int re = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, 0, 0);
if (re < 0) {
LOG_E("av_find_best_stream audio failed");
return;
}
audioStream = re;
}
代码很简单,可以看到我们就能获取到总时长了和音视频的索引了。
读取数据
这一步是解封装这一步最核心的,因为通过这一步才获取到每一帧的数据;核心方法是av_read_frame
,需要传入上下文ic,和AVPacket指针;而AVPacket指针的空间需要手动申请和释放,不然很容易造成内存泄露,所以这一点一定要注意,自己申请的数据一定要清理。
还有一点就是packet返回帧信息中的pts和dps是有一个基数的,我们把它转成毫秒就好了,方便之后使用,在转换的过程中会涉及到一个AVRational类,是一个分数,但是包含分子和分母的,这样数据就更准确,一般这个基数是1000000。当然我们使用packet中提供的基数更准确,需要一个将AVRational转成double的方法:
//分数转为浮点数
static double r2d(AVRational r) {
return r.num == 0 || r.den == 0 ? 0 : (double) r.num / (double) r.den;
}
很简单,就是判断分母不能为0,open方法的实现方式如下:
bool Demux::read() {
if (!ic) {
return false;
}
AVPacket *pkt = av_packet_alloc();
int re = av_read_frame(ic, pkt);
if (re != 0) {
av_packet_free(&pkt);
return false;
}
pkt->pts = (long long) (pkt->pts * (1000 * r2d(ic->streams[pkt->stream_index]->time_base)));
if (pkt->stream_index == audioStream) {
LOG_I("read audio size = %d,pts = %lld", pkt->size, pkt->pts);
} else if (pkt->stream_index == videoStream) {
LOG_I("read video size = %d,pts = %lld", pkt->size, pkt->pts);
} else {
av_packet_free(&pkt);
return false;
}
av_packet_free(&pkt);
return true;
}
其中获取帧数据成功之后,转化pts和dps的时间基数,单位编程毫秒,然后再区分音频和视频去打印帧数据的大小和pts。当然其中av_packet_free
是对AVPacket对象申请空间的释放。
这样这三个方法的实现就基本完成了,然后我们再回到最开始,把native-lib中的方法实现一下,其实就是调用demux的方法。最后再在MainActivity中在点击不同按钮调用FFmpegUtil中的方法即可。native-lib.cpp代码如下:
static Demux *demux;
extern "C"
JNIEXPORT void JNICALL
Java_net_arvin_ffmpegtest_FFmpegUtil_init(JNIEnv *env, jclass type) {
if (!demux) {
demux = new Demux();
demux->init();
}
}
extern "C"
JNIEXPORT void JNICALL
Java_net_arvin_ffmpegtest_FFmpegUtil_open(JNIEnv *env, jclass type, jstring url_) {
const char *url = env->GetStringUTFChars(url_, 0);
if (demux) {
demux->open(url);
}
env->ReleaseStringUTFChars(url_, url);
}
extern "C"
JNIEXPORT void JNICALL
Java_net_arvin_ffmpegtest_FFmpegUtil_read(JNIEnv *env, jclass type) {
if (!demux) {
return;
}
bool re = true;
while (re) {
re = demux->read();
}
}
MainActivity中的代码如下:
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_init:
initAndOpen();
break;
case R.id.btn_read_data:
readData();
break;
}
}
private void initAndOpen() {
permissionUtil.request("需要读取读写文件权限", Manifest.permission.WRITE_EXTERNAL_STORAGE,
new PermissionUtil.RequestPermissionListener() {
@Override
public void callback(boolean granted, boolean isAlwaysDenied) {
FFmpegUtil.init();
FFmpegUtil.open("/sdcard/1080.mp4");
}
});
}
private void readData() {
FFmpegUtil.read();
}
这里的permissionUtil是我封装的对Android6.0以上动态申请权限库,方便使用。
这里要打开数据文件所以需要文件读写权限,所以在AndroidManifest文件中也要申请权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
如果是在线视频文件需要再添加网络权限:
<uses-permission android:name="android.permission.INTERNET"/>
当然在初始化部分网络初始化就一定要加上。
上边的代码比较简单,就是FFmpegUtil.open的时候传入的url是自己本地的文件或者在线的视频才行。
到这里解封装的基本内容就完了,还是比较简单的,当然如果有不正确的地方请不吝赐教。
网友评论