AndroidNDK FFmpeg解码播放mp4文件

2022-05-05  本文已影响0人  Analyas

在使用FFmpeg解码播放视频时,遇到了不少坑,如果你参照官方decode_video.c的demo非常容易越陷越深,一不小心就走错方向了,比如av_parser_parse2的作用是将编码后的裸流数据(如H264, H265等)数据合并成一个packet,再由packet如解析出一个AVFrame帧数据,你不能使用av_parser_parse2去解析一个mp4媒体文件,因为他不仅包含视频流,还包含着其它如音频之类的流媒体信息,我也是查了好久才发现以下文章有提到过的,大家可以去了解一下
https://blog.actorsfit.com/a?ID=01050-8e62efbd-a0c3-4e98-aca2-8cc648be9be7

话不多说,直接上代码,已经简化不必要的代码
1.activity_mp4_decode.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:id="@+id/iv_display_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="#000"
        />
</androidx.constraintlayout.widget.ConstraintLayout>
2.Mp4DecodeActivity.java
package com.qmel.surfacework;

import android.Manifest;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

public class Mp4DecodeActivity extends AppCompatActivity {
    //JNI解码接口
    static {
        System.loadLibrary("avformat");
        System.loadLibrary("avcodec");
        System.loadLibrary("avutil");
        System.loadLibrary("swscale");
        System.loadLibrary("swresample");
        System.loadLibrary("avfilter");
        //decodeMp4 JNI实现方法所在的so库
        System.loadLibrary("surfacework");
    }
    public native void decodeMp4(String filePath, CallBack callBack);

    private ImageView displayImageView;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mp4_decode);
        displayImageView = findViewById(R.id.iv_display_image);
        //检查文件访问权限,如果给了权限,需要退出重新打开应用
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
            Toast.makeText(getApplicationContext(), "请授权存储空间访问权限后再次打开应用!", Toast.LENGTH_LONG).show();
            return;
        }

        //开启线程去解码文件,Demo目前只解码视频,音频的需要自己处理了,PTS没做处理,现在是加速播放,正常播放请自己手动去处理PTS之类的东西
        new Thread(){
            @Override
            public void run() {
                //要解码的视频文件路径、解码回调接口
                decodeMp4("/storage/emulated/0/Pictures/aging_video_high.mp4", callBack);
            }
        }.start();
    }

    //解码视频后的回调接口
    public interface CallBack {
        void onGetDecodeData(int[] pixels, int width, int height);
    }
    private final CallBack callBack = new CallBack() {
        Bitmap lastBitmap;
        @Override
        public void onGetDecodeData(int[] pixels, int width, int height) {
            //得到RGB像素数据后生成Bitmap,并回收之前的图片,避免内存泄漏
            Bitmap bitmap = Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888);
            runOnUiThread(()->{
                displayImageView.setImageBitmap(bitmap);
                if (lastBitmap != null && !lastBitmap.isRecycled()) {
                    lastBitmap.recycle();
                }
                lastBitmap = bitmap;
            });
        }
    };
}

3.decode_mp4.cpp
//
// Created by Administrator on 5/5/2022.
//

#include <jni.h>
#include <android/log.h>

extern "C" {
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libavutil/avutil.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#include "libavfilter/avfilter.h"
#include "libavutil/imgutils.h"

    JNIEXPORT void JNICALL
    Java_com_qmel_surfacework_Mp4DecodeActivity_decodeMp4(JNIEnv *env, jobject thiz, jstring filePath,
                                                          jobject callback) {
        //1.打开视频文件,获取视频信息,如果失败,检查程序是否有访问权限(必要)
        AVFormatContext *avFormatContext = NULL;
        const char *filepath = env->GetStringUTFChars(filePath, nullptr);
        int open_status = avformat_open_input(&avFormatContext, filepath, nullptr, nullptr);
        if (open_status != 0) {
            char message[256];
            sprintf(message, "无法打开视频文件,错误代码:%d:%s", open_status, av_err2str(open_status));
            env->ThrowNew(env->FindClass("java/lang/Exception"), message);
            return;
        }
        //获取视频轨道在stream中的index
        int videoIndex = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
        //根据视频信息,查找指定解码器,请注意,如果AVCodec指针为0x0,表示没有这样的解码器,mp4一般是h264
        const AVCodec *avCodec = avcodec_find_decoder(avFormatContext->streams[videoIndex]->codecpar->codec_id);
        __android_log_print(ANDROID_LOG_ERROR, "FFMpeg调试信息", "AVCodec指针:%p", avCodec);
        //拿到视频的宽高信息
        jint width = avFormatContext->streams[videoIndex]->codecpar->width;
        jint height = avFormatContext->streams[videoIndex]->codecpar->height;


        //2.创建解码器,并初始化解码器上下文
        AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);
        //解码线程数
        avCodecContext->thread_count = 16;
        //将视频流的基本信息传递到解码器上下文中(看codec_par.c源码其实主要传递了PIX_FMT,宽高,色彩信息以及视频的extradata信息)
        avcodec_parameters_to_context(avCodecContext, avFormatContext->streams[videoIndex]->codecpar);
        //打开解码器,注意:在open前请先调用avcodec_parameters_to_context把视频参数附加到上下文,不然可能会解码出错
        int codecIsOpen = avcodec_open2(avCodecContext, avCodec, nullptr);
        if (codecIsOpen < 0) {
            char message[256];
            sprintf(message, "解码器打开失败: %d==%d", codecIsOpen, avCodec->id);
            env->ThrowNew(env->FindClass("java/lang/Exception"), message);
            return;
        }


        //3.初始化解码的一些配置(通常来说必要)
        //创建swsContext,用于转换解码出来的视频帧格式,因为解析mp4出来的是yuv420p格式,但我需要RGB888格式(这里看自己需要)
        SwsContext *swsContext = sws_getContext(width, height, AV_PIX_FMT_YUV420P, width, height,
                                                AV_PIX_FMT_RGB24, SWS_BILINEAR,
                                                NULL, NULL, NULL);
        //解码后的YUV帧
        AVFrame *avDecodeFrame = av_frame_alloc();
        //转成后的RGB帧
        AVFrame *rgbFrame = av_frame_alloc();
        //编码的packet数据缓存
        AVPacket *avDecodePacket = av_packet_alloc();
        //分配RGB帧的缓存大小,有于YUV在解码时会自动帮我们设置,但使用sws转换的不会帮我们去设置,所以这里是必要的
        int allocImageSize = av_image_alloc(rgbFrame->data, rgbFrame->linesize, width, height, AV_PIX_FMT_RGB24, 1);
        //分配空间对于RGB来说一般返回结果是width*height*3,如果小于等于0,那么失败,最后一个参数align一般是1,测试0是分配失败的
        if (allocImageSize <= 0){
            char message[256];
            sprintf(message, "无法分配RGB缓存空间,av_image_alloc返回值:%d", allocImageSize);
            env->ThrowNew(env->FindClass("java/lang/Exception"), message);
            return;
        }

        //4.初始化Java层的回调
        jclass javaClazz = env->FindClass("com/qmel/surfacework/Mp4DecodeActivity$CallBack");
        jmethodID methodId = env->GetMethodID(javaClazz, "onGetDecodeData", "([III)V");
        //存储rgb pixel像素的java数组
        jintArray pixelArray = env->NewIntArray(width * height);
        jint pixelCount = width * height;
        jint *toPixels = (jint *) malloc(pixelCount * sizeof(jint));

        //5.开始解码
        while (true) {
            //读取一帧Packet, 注意:该Packet可能是视频帧也可能是音频帧,注意根据stream_index做判断
            int readFrameStatus = av_read_frame(avFormatContext, avDecodePacket);
            if (readFrameStatus == AVERROR_EOF) {
                __android_log_print(ANDROID_LOG_ERROR, "FFMpeg调试信息", "读取完毕");
                break;
            } else if (avDecodePacket->stream_index != videoIndex){
                continue;
            }
            //发送编码的Packet到解码器
            int sendPacketStatus = avcodec_send_packet(avCodecContext, avDecodePacket);
            if (sendPacketStatus < 0 || sendPacketStatus == AVERROR(EAGAIN)) {
                char message[256];
                sprintf(message, "发送数据到解码器失败,错误代码:%s", av_err2str(sendPacketStatus));
                __android_log_print(ANDROID_LOG_ERROR, "FFMpeg调试信息", "%s", message);
                continue;
            }
            //接收解码后的数据
            int receiveFrameStatus = avcodec_receive_frame(avCodecContext, avDecodeFrame);
            if (receiveFrameStatus == AVERROR(EAGAIN)) {
                continue;
            } else if (receiveFrameStatus < 0) {
                char message[256];
                sprintf(message, "获取解码数据失败,错误信息:%s", av_err2str(receiveFrameStatus));
                env->ThrowNew(env->FindClass("java/lang/Exception"), message);
                return;
            }

            __android_log_print(ANDROID_LOG_ERROR, "FFmpeg解码", "解码视频帧宽高:%d x %d, linesize:%d, Format:%d",
                                avDecodeFrame->width,
                                avDecodeFrame->height,
                                avDecodeFrame->linesize[0],
                                avDecodeFrame->format);

            //将YUV视频帧转换成RGB视频帧
            sws_scale(swsContext, avDecodeFrame->data, avDecodeFrame->linesize, 0, height
                      , rgbFrame->data, rgbFrame->linesize);

            ///////////////////////将数据转换成Bitmap需要的int[] RGB数组/////////////////////////////
            //源数据
            uint8_t *rgbBytes = rgbFrame->data[0];
            //开始进行像素的运算
            int startIndex = 0;
            for (int he = 0; he < height; ++he) {
                for (int wi = 0; wi < width; ++wi) {
                    //由于opengl的第一个像素是从左下角开始数起,所以第一个像素应该从左上角开始设置
                    jint pixel = ((int32_t) 0xff000000) | ((rgbBytes[startIndex] & 0xff) << 16)
                                 | ((rgbBytes[startIndex + 1] & 0xff) << 8)
                                 | ((rgbBytes[startIndex + 2] & 0xff));
                    toPixels[(he * width + wi)] = pixel;
                    startIndex = startIndex + 3;
                }
            }
            //将像素数据放入java层的数组中
            env->SetIntArrayRegion(pixelArray, 0, pixelCount, toPixels);
            //释放内存

            if (callback != nullptr) {
                env->CallVoidMethod(callback, methodId, pixelArray, width, height);
            }
        }

        //释放资源
        free(toPixels);
        av_packet_free(&avDecodePacket);
        av_frame_free(&avDecodeFrame);
        av_frame_free(&rgbFrame);
        avcodec_close(avCodecContext);
        avformat_close_input(&avFormatContext);
        sws_freeContext(swsContext);
        env->DeleteLocalRef(javaClazz);
        env->DeleteLocalRef(pixelArray);
    }
}

关于FFmpeg源码的导入这里不过多解释,大家有兴趣的话可以看我之前的文章
(1) Android NDK编译和导入FFmpeg源码

欢迎加入Android开发学习群:202253703

上一篇下一篇

猜你喜欢

热点阅读