视频播放器之解封装

在之前的文章中FFmpeg的编译集成也完成了,这一篇开始视频播放器处理的第一步:解封装

解封装

在解封装的代码开始以前,我们需要引入Log机制,虽然现在ndk开发中也能debug了但是有log会更方便。具体怎么引入在我的Android JNI开发系列之Java与C相互调用一文最后有方法,这里就不再赘述了。

解封装这一步是处理视频数据的开始,需要处理以下几步:

解封装.jpg

Android的界面比较简单这里就不写了,样子是这样的:

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是自己本地的文件或者在线的视频才行。

到这里解封装的基本内容就完了,还是比较简单的,当然如果有不正确的地方请不吝赐教。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容