美文网首页Android视频播放器
视频播放器之解封装

视频播放器之解封装

作者: arvinljw | 来源:发表于2019-01-31 17:14 被阅读94次

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

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

相关文章

  • iOS-FFmpeg学习笔记(二)

    视频播放器原理 视频播放器播放一个网上文件主要进过以下几个流程: 解协议 封装格式数据 解封装 视/音频压缩数据 ...

  • iOS-视频播放器的简单封装

    iOS-视频播放器的简单封装 封装视频播放器,首先需要了解视频播放器的实现,iOS9之前可以使用MediaPlay...

  • 视频播放及FFmpeg学习笔记

    播放流程 视频播放器播放网络视频,需要经过以下步骤:解协议,解封装,解码视音频,视音频同步。如果播放本地视频则不需...

  • iOS视频相关技术初探

    一.视频播放 简介 视频播放器播放一个互联网上的视频文件,需要经过以下几个步骤:解协议,解封装,解码视音频,视...

  • 05.视频播放器内核切换封装

    05.视频播放器内核切换封装 目录介绍 01.视频播放器内核封装需求 02.播放器内核架构图 03.如何兼容不同内...

  • FFmpeg 音视频同步

    音视频播放器的工作的具体流程如下图所示: 简单的来说包括:解协议,解封装,对音频和视频分别进行解码,音视频同步播放...

  • Android 多媒体 -- 播放器的封装

    边缓存边播放的视频器封装 模仿原生的提供的视频播放器VideoView,封装一个仿微信的视频播放器。 原生的Vid...

  • Android音乐播放器封装

    推荐一个音乐播放器封装库的演示项目:iMusic音乐播放器 若你对视频播放器有需求请移步至Android视频播放器...

  • Android视频播放器封装

    推荐一个视频播放器封装库的演示项目:iMusic视频播放器 若你对音乐播放器有需求请移步至Android音频播放器...

  • iOS视频边下边播之滑动TableView自动播放

    [iOS]仿微博视频边下边播之封装播放器](http://bbs.520it.com/target.php?tar...

网友评论

    本文标题:视频播放器之解封装

    本文链接:https://www.haomeiwen.com/subject/dchlsqtx.html