美文网首页
AndroidNDK FFmpeg解码播放mp4文件

AndroidNDK FFmpeg解码播放mp4文件

作者: Analyas | 来源:发表于2022-05-05 19:22 被阅读0次

    在使用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

    相关文章

      网友评论

          本文标题:AndroidNDK FFmpeg解码播放mp4文件

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