美文网首页
NDK--利用FFmpeg进行安卓原生绘制播放

NDK--利用FFmpeg进行安卓原生绘制播放

作者: aruba | 来源:发表于2020-06-30 17:42 被阅读0次

    上次说到视频编码和转码,得到了yuv格式的文件,我们平常看到的视频画面实际都是yuv格式,今天来实现在APP中播放视频,代码和上次差不多,只需将转码文件换成输出到屏幕上

    1.自定义View继承SurfaceView,把SurfaceView传入native层进行绘制
    package com.aruba.ffmpegapplication;
    
    import android.content.Context;
    import android.graphics.PixelFormat;
    import android.util.AttributeSet;
    import android.view.Surface;
    import android.view.SurfaceView;
    
    import java.lang.ref.WeakReference;
    
    /**
     * Created by aruba on 2020/6/30.
     */
    public class YuvPlayView extends SurfaceView {
        static {
            System.loadLibrary("native-lib");
        }
    
        private PlayThread playThread;
    
        public YuvPlayView(Context context) {
            this(context, null);
        }
    
        public YuvPlayView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public YuvPlayView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        private void init() {
            setKeepScreenOn(true);
            //设置一个像素占4字节
            getHolder().setFormat(PixelFormat.RGBA_8888);
    
            playThread = new PlayThread(this);
        }
    
        /**
         * 开始播放
         */
        public void play(String filePath) {
            if (isPlaying()) {
                playThread.stopPlay();
            }
    
            playThread = new PlayThread(this);
            playThread.filePath = filePath;
            playThread.start();
        }
    
        public void stop() {
            if (isPlaying()) {
                playThread.stopPlay();
            }
        }
    
        public boolean isPlaying() {
            return playThread.isPlaying();
        }
    
        @Override
        protected void onDetachedFromWindow() {
            super.onDetachedFromWindow();
            stop();
        }
    
        static class PlayThread extends Thread {
            public boolean isPlaying;
            public String filePath;
            private WeakReference<YuvPlayView> playViewWeakReference;
    
            public PlayThread(YuvPlayView yuvPlayView) {
                this.playViewWeakReference = new WeakReference<>(yuvPlayView);
            }
    
            @Override
            public void run() {
                isPlaying = true;
                //阻塞方法,不断将视频内容绘制到屏幕
                if (playViewWeakReference.get() != null) {
                    int ret = render(filePath, playViewWeakReference.get().getHolder().getSurface());
                }
                isPlaying = false;
            }
    
            public boolean isPlaying() {
                return isPlaying;
            }
    
            public native int render(String filePath, Surface surface);
    
            public native void stopPlay();
        }
    
    }
    
    编写相应的native方法
    #include <jni.h>
    #include <string>
    #include <android/log.h>
    #include <android/native_window_jni.h>
    #include <unistd.h>
    
    extern "C" {
    //编码
    #include "libavcodec/avcodec.h"
    //封装格式处理
    #include "libavformat/avformat.h"
    //像素处理
    #include "libswscale/swscale.h"
    }
    
    #define  LOG_TAG    "aruba"
    #define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
    
    bool is_playing = false;
    
    extern "C"
    JNIEXPORT jint JNICALL
    Java_com_aruba_ffmpegapplication_YuvPlayView_00024PlayThread_render(JNIEnv *env, jobject instance,
                                                                        jstring filePath_,
                                                                        jobject surface) {
        const char *filePath = env->GetStringUTFChars(filePath_, 0);
    
        //注册FFmpeg中各大组件
        av_register_all();
    
        //打开文件
        AVFormatContext *formatContext = avformat_alloc_context();
        if (avformat_open_input(&formatContext, filePath, NULL, NULL) != 0) {
            LOGE("打开失败");
            avformat_free_context(formatContext);
            env->ReleaseStringUTFChars(filePath_, filePath);
            return 1;
        }
    
        //将文件信息填充进AVFormatContext
        if (avformat_find_stream_info(formatContext, NULL) < 0) {
            LOGE("获取文件信息失败");
            avformat_free_context(formatContext);
            env->ReleaseStringUTFChars(filePath_, filePath);
            return 2;
        }
    
        //获取视频流的编解码器上下文
        AVCodecContext *codecContext = NULL;
        int vidio_stream_idx = -1;
        for (int i = 0; i < formatContext->nb_streams; ++i) {
            if (formatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {//如果是视频流
                codecContext = formatContext->streams[i]->codec;
                vidio_stream_idx = i;
                break;
            }
        }
    
        if (codecContext == NULL) {
            LOGE("获取编解码器上下文失败");
            avformat_free_context(formatContext);
            env->ReleaseStringUTFChars(filePath_, filePath);
            return 3;
        }
    
        //根据编解码器上下文的id获取视频流解码器
        AVCodec *codec = avcodec_find_decoder(codecContext->codec_id);
        //打开解码器
        if (avcodec_open2(codecContext, codec, NULL) < 0) {
            LOGE("打开解码失败");
            avformat_free_context(formatContext);
            env->ReleaseStringUTFChars(filePath_, filePath);
            return 4;
        }
    
        //开始读每一帧
        is_playing = true;
    
        //存放压缩数据
        AVPacket *pkt = (AVPacket *) (av_malloc(sizeof(AVPacket)));
        av_init_packet(pkt);
    
        //存放解压数据
        AVFrame *picture = av_frame_alloc();
    
        //存放转码数据
        AVFrame *picture_rgb = av_frame_alloc();
        //为转码数据分配内存
        uint8_t *data_size = (uint8_t *) (av_malloc(
                (size_t) avpicture_get_size(AV_PIX_FMT_RGBA, codecContext->width,
                                            codecContext->height)));
        avpicture_fill((AVPicture *) picture_rgb, data_size, AV_PIX_FMT_RGBA, codecContext->width,
                       codecContext->height);
    
        int picture_ptr = 0;
    
        //获取底层绘制对象
        ANativeWindow *aNativeWindow = ANativeWindow_fromSurface(env, surface);
        //为Window配置长宽和像素编码
        ANativeWindow_setBuffersGeometry(aNativeWindow, codecContext->width, codecContext->height,
                                         AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM);
        //视频缓存区 
        ANativeWindow_Buffer out_buff;
    
        //转码组件上下文,前三个参数为原视频的宽高和编码,后三个为转码后的视频宽高和编码,还可以传入过滤器对视频做处理,这边不做处理
        SwsContext *swsContext = sws_getContext(codecContext->width, codecContext->height,
                                                codecContext->pix_fmt,
                                                codecContext->width, codecContext->height,
                                                AV_PIX_FMT_RGBA, SWS_BILINEAR, NULL, NULL, NULL
        );
    
        while (av_read_frame(formatContext, pkt) == 0 && is_playing) {//读到每一帧的压缩数据存放在AVPacket
            if (pkt->stream_index == vidio_stream_idx) {
                //解码
                avcodec_decode_video2(codecContext, picture, &picture_ptr, pkt);
    
                LOGE("picture_ptr %d", picture_ptr);
                if (picture_ptr > 0) {
                    //转码 data中存放着真实数据,linesize为一行的数据,0为转码起始位置,高度为整个画面高
                    sws_scale(swsContext, picture->data, picture->linesize, 0, picture->height,
                              picture_rgb->data, picture_rgb->linesize);
                    //锁定
                    ANativeWindow_lock(aNativeWindow, &out_buff, NULL);
    
                    //将转码后的frame(picture_rgb)中的数据复制到window的视频缓冲区(out_buff)
                    //picture_rgb中二维数据的首地址
                    uint8_t *frame_data_p = picture_rgb->data[0];
                    //视频缓存区中二维数据的首地址
                    uint8_t *buff_data_p = (uint8_t *) (out_buff.bits);
                    //视频缓存区有多少个字节 RGBA8888占4个字节
                    int destStride = out_buff.stride * 4;
                    for (int i = 0; i < codecContext->height; i++) {
                        memcpy(buff_data_p, frame_data_p, picture_rgb->linesize[0]);
                        buff_data_p += destStride;
                        frame_data_p += picture_rgb->linesize[0];
                    }
    
                    ANativeWindow_unlockAndPost(aNativeWindow);
                    //16ms播放一帧
                    usleep(16 * 1000);
                }
            }
    
            av_free_packet(pkt);
        }
    
        //释放资源
        sws_freeContext(swsContext);
        ANativeWindow_release(aNativeWindow);
        av_frame_free(&picture_rgb);
        av_frame_free(&picture);
        free(data_size);
        avcodec_close(codecContext);
        avformat_free_context(formatContext);
        env->ReleaseStringUTFChars(filePath_, filePath);
    
        return 0;
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_aruba_ffmpegapplication_YuvPlayView_00024PlayThread_stopPlay(JNIEnv *env,
                                                                          jobject instance) {
        is_playing = false;
    }
    
    通过ANativeWindow进行绘制,要注意的是两个缓冲区数据复制时,指针的位移量,思路是:由于每一帧的数据其实就是一个二维数组,将数据进行逐行复制。最后别忘了在cmake中增加ANativeWindow的支持
    target_link_libraries(
            native-lib
            avcodec-56
            avdevice-56
            avfilter-5
            avformat-56
            avutil-54
            postproc-53
            swresample-1
            swscale-3
    
            android
            ${log-lib})
    
    最后效果:
    项目地址:https://gitee.com/aruba/FFmpegApplication.git

    相关文章

      网友评论

          本文标题:NDK--利用FFmpeg进行安卓原生绘制播放

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