Android使用FFmpeg动态库播放视频

FFmpeg环境的搭建在前面一篇博客中已经写了,详情参照:AndroidStudio3.5.1下搭建FFmpeg环境

本文仅实现将mp4的视频部分渲染到SurfaceView中, 不包含音频,不包含播放控制。文中的视频是在SD卡根目录中有一个input.flv文件,需要手动导入,AndroidManifest.xml中需要声明读取权限

UI界面

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!--视频播放在此控件-->
    <SurfaceView
        android:id="@+id/surface"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <!--播放按钮-->
    <Button
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:onClick="play"
        app:layout_constraintBottom_toBottomOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>
public class MainActivity extends AppCompatActivity {


    Player player;
    private SurfaceView mSurface;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();

    }

    public void play(View view) {
        File file = new File(Environment.getExternalStorageDirectory(), "input.flv");
        player.play(file.getAbsolutePath());
    }

    private void initView() {
        player = new Player();
        mSurface = (SurfaceView) findViewById(R.id.surface);
        player.setSurfaceView(mSurface);
    }
}

player是个工具类,文章末尾会提供完整player类的代码

 

这样UI的部分就算完成了。 我们是要通过FFmpeg的库将视频渲染到SurfaceView,虽然native层没有SurfaceView这个类的,但是native层是有Surface的,而且可以用Surface来创建一个ANativeWindow,从而将视频渲染到界面上。 本质就是数据的copy

 

native播放代码

 

导入的库有

#include <jni.h>
#include <string>
#include <android/native_window_jni.h>
#include <unistd.h>
// 没有用extern "C"包括, 会提示undefined reference to 'xxx'
// extern "C"的作用是 标识库是用C写的,
extern "C" {
#include "include/libavutil/avutil.h"
#include "include/libavformat/avformat.h"
#include "include/libswscale/swscale.h"
#include "include/libavutil/imgutils.h"
#include "include/libswresample/swresample.h"

}

 


extern "C"
JNIEXPORT void JNICALL
Java_com_mobcb_ffmpegdemo_Player_native_1play(JNIEnv *env, jobject thiz, jstring path_,
                                              jobject surface) {

    const char *path = env->GetStringUTFChars(path_, 0);
    // 初始化网络
    avformat_network_init();

    // 初始化总上下文
    AVFormatContext *avFormatContext = avformat_alloc_context();

    // 1.打开url/或者本地文件路径
    // 打开时的参数设置
    AVDictionary *opt = NULL;
    // 设置超时参数值为3000000,这里单位是微秒,3000000微秒=3s
    av_dict_set(&opt, "timeout", "3000000", 0);

    // 打开url,ret=0标识成功,否则失败
    int ret = avformat_open_input(&avFormatContext, path, NULL, &opt);
    if (ret) {
        return;
    }
    // 发送指令,让avFormatContext获取stream信息
    // 一段视频中一般有两个流,视频流,音频流,有的还包含字幕流,这里就是要获取这些流信息
    avformat_find_stream_info(avFormatContext, &opt);
    // 视频流的id
    int stream_index;
    for (int i = 0; i < avFormatContext->nb_streams; ++i) {
        // codecpar 是stream的参数信息, 包括 宽,高, 类型等
        if (avFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            // 是视频流
            stream_index = i;
            break;
        }
    }

    // 获取流的参数信息
    AVCodecParameters *avCodecParameters = avFormatContext->streams[stream_index]->codecpar;

    // 根据流的参数信息取得解码器,相当于从map中以codec_id为key取value
    AVCodec *avCodec = avcodec_find_decoder(avCodecParameters->codec_id);

    // 根据解码器,创建解码器上下文
    AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);

    // 将流的参数信息数据填充到解码器上下文
    avcodec_parameters_to_context(avCodecContext, avCodecParameters);

    // 使用解码器初始化解码器上下文
    avcodec_open2(avCodecContext, avCodec, NULL);

    // 声明包
    AVPacket *avPacket = av_packet_alloc();

    // 创建视频帧到RGBA的转换上下文
    SwsContext *swsContext = sws_getContext(
            avCodecContext->width,
            avCodecContext->height,
            avCodecContext->pix_fmt,
            avCodecContext->width,
            avCodecContext->height,
            AV_PIX_FMT_RGBA,
            SWS_BILINEAR, 0, 0, 0
    );

    // 创建渲染的窗体
    ANativeWindow *aNativeWindow = ANativeWindow_fromSurface(env, surface);

    // 窗体的缓冲区,绘制实际是上就是改变此缓冲区的内容
    ANativeWindow_Buffer buffer;


    // 设置缓冲区大小
    ANativeWindow_setBuffersGeometry(aNativeWindow, avCodecContext->width, avCodecContext->height,
                                     WINDOW_FORMAT_RGBA_8888);


    // 以一个package为单位读取帧信息
    while ((av_read_frame(avFormatContext, avPacket) >= 0)) {
        // 向解码器输入pack 数据
        avcodec_send_packet(avCodecContext, avPacket);
        // 解码之后的帧数据
        AVFrame *avFrame = av_frame_alloc();
        // 读取帧数据信息
        int ret1 = avcodec_receive_frame(avCodecContext, avFrame);
        if (ret1 == AVERROR(EAGAIN)) {
            //output is not available in this state - user must try
            // *                         to send new input
            // 需要再发送数据,队列里已没有数据可取
            continue;
        } else if (ret1 < 0) {
            // 无数据, 退出循环
            break;
        }

        uint8_t *dst_data[0];
        int dst_linesize[0];

        // 申请一块画布,并且数据和行数都用宽高和pix_fmt声明好
        av_image_alloc(dst_data, dst_linesize, avCodecContext->width, avCodecContext->height,
                       AV_PIX_FMT_RGBA, 1);

        if (avPacket->stream_index == stream_index) {
            // 是视频流
            if (ret1 == 0) {
                // 读取帧信息成功
                // 绘制

                // 锁缓冲区
                ANativeWindow_lock(aNativeWindow, &buffer, NULL);

                // 将frame转换成统一格式,格式为av_image_alloc申请的AV_PIX_FMT_RGBA
                sws_scale(swsContext,
                          avFrame->data,
                          avFrame->linesize,
                          0,
                          avFrame->height,
                          dst_data, dst_linesize);


                // 将转换格式之后的frame 拷贝到ANativeWindow的缓冲区

                // 输出到屏幕的输入源
                uint8_t *srcFirstLine = dst_data[0];
                // 输出到屏幕的输入源的行数
                int srcStride = dst_linesize[0];
                // 输出到屏幕时的跨度 是 缓冲区的跨度 * 4 , 因为一个pixel是4个字节,包含RGBA四个变量
                int windowStride = buffer.stride * 4;
                // 屏幕的首行地址
                uint8_t *windowFirstLine = (uint8_t *) buffer.bits;
                for (int i = 0; i < buffer.height; ++i) {
                    // 逐行拷贝数据
                    memcpy(windowFirstLine + i * windowStride, srcFirstLine + i * srcStride,
                           windowStride);
                }
                // 解锁缓冲区
                ANativeWindow_unlockAndPost(aNativeWindow);
                usleep(1000 * 16);
                // 释放帧数据
                av_frame_free(&avFrame);
            }
        }
        av_packet_unref(avPacket);
        // 这个地方必须要释放, 否则内存会飙升,已copy的帧内存释放掉
        av_freep(&dst_data[0]);
    }

    ANativeWindow_release(aNativeWindow);
    av_freep(avPacket);
    sws_freeContext(swsContext);
    avcodec_close(avCodecContext);
    avformat_free_context(avFormatContext);
    // 下面三个不可写,否则会闪退,上面的代码会将下面三个指针都释放掉
//    av_freep(avCodec);
//    av_freep(swsContext);
//    av_freep(avCodecContext);
    // 播放完要释放,否则会内存泄漏
    av_freep(avFormatContext);

    // 释放字符串
    env->ReleaseStringUTFChars(path_, path);
}

 

player类完整代码

public class Player implements SurfaceHolder.Callback {
    static {
        System.loadLibrary("native-lib");
    }


    private SurfaceHolder surfaceHolder;

    public void setSurfaceView(SurfaceView surfaceView) {
        if (null != this.surfaceHolder) {
            this.surfaceHolder.removeCallback(this);
        }
        this.surfaceHolder = surfaceView.getHolder();
        this.surfaceHolder.addCallback(this);
    }

    public void play(String path) {
        native_play(path, surfaceHolder.getSurface());
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {

    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        this.surfaceHolder = holder;
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {

    }

    public native void native_play(String path, Surface surface);

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值