美文网首页
android ffmpeg播放本地视频

android ffmpeg播放本地视频

作者: 小明叔叔_乐 | 来源:发表于2020-04-18 16:51 被阅读0次

    一、思路:

    Java层:初始化一个surfaceView,赋值给Jni层作为播放载体,设置播放路径,在ffmpeg初始化完成后,开始正式播放本地视频。
    Jni层:通过Java层赋值的surface获取到window对象(真正显示播放的对象窗体),在获取到视频路径后,初始化ffmpeg,初始化成功后通过反射通知Java层,开始解码视频,把读包线程得到的packet统一放到packet queue中,解码线程不停地从packet queue中获取packet,并进行解码,解码得到的frame对象统一放到frame queue中,显示线程不停地从frame queue中获取frame对象,并显示在window对象中。

    二、上代码:

    Java层:
        private void initView() {
            // Java层 第一步: 拿到一个surfaceView,作为播放载体
            surfaceView = findViewById(R.id.surfaceView);
        }
    
        private void initPlayHelper() {
            // Java层 第二步: 把surfaceView设置给playerHelper类
            playerHelper = new PlayerHelper();
            playerHelper.setSurfaceView(surfaceView);
            // 设置相应的监听
            playerHelper.setOnPrepareListener(this);
            playerHelper.setOnProgressListener(this);
            playerHelper.setOnErrorListener(this);
        }
    
        /**
         * 开始播放画面
         *
         * @param view
         */
        public void startPlay(View view) {
            // Java层 第三步: 把播放路径设置给playerHelper类,(路径可以是本地路径,也可以是远端路径)
            File inputMp4 = new File(Environment.getExternalStorageDirectory(), "ffmpeg-test.mp4");    //opensldemo.mp3
            playerHelper.setDataSource(inputMp4.getAbsolutePath());
        }
    
    Jni层:
    FfmpegHelper.cpp
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_hele_zandroid_ffmpeg_PlayerHelper_native_1set_1surface(JNIEnv *env, jobject thiz,
                                                                    jobject surface) {
        // Jni层 第一步:通过Java层传递的surface,创建一个全局的window对象
        // 创建window
        if (window) {
            ANativeWindow_release(window);
            window = 0;
        }
        LOGD("native set window");
        window = ANativeWindow_fromSurface(env, surface);
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_hele_zandroid_ffmpeg_PlayerHelper_native_1prepare(JNIEnv *env, jobject instance,
                                                               jstring dataSource_) {
        const char *dataSource = env->GetStringUTFChars(dataSource_, 0);
        // Jni层 第二步:初始化ffmpeg
        // 定义一个c层回调java层的辅助对象,使c层可以主动调用java层的方法
        javaCallHelper = new JavaCallHelper(javaVM, env,
                                            instance);     // 这个instance就是PlayHelper.java对象
    
        // 注意框架,交给控制层来处理
        ffmpegHelper = new FfmpegHelper(javaCallHelper, dataSource);
        // 设置渲染的回调,window对象在哪,就回调到哪
        ffmpegHelper->setRenderFrame(renderFrame);
        // 初始化ffmpeg
        ffmpegHelper->prepare();
    
        env->ReleaseStringUTFChars(dataSource_, dataSource);
    }
    
    /**
     * 初始化ffmpeg
     */
    void FfmpegHelper::initFfmpeg() {
        // 4.0.5 ffmpeg版本,与3.4的版本的写法还有点不一样
        // 初始化网络模块
        avformat_network_init();
        // 针对当前视频的总上下文 --- 第一个上下文
        formatContext = avformat_alloc_context();
    
        // 设置打开文件的超时时间
        AVDictionary *avDictionary = NULL;
        av_dict_set(&avDictionary, "timeout", "3000000", 0);
        // 打开文件
        int rst = avformat_open_input(&formatContext, dataSource, NULL, &avDictionary); //&avDictionary
        // 如果打开失败,return
        if (rst) {
            // 回调给java层
            if (javaCallHelper) {
                javaCallHelper->onError(THREAD_CHILD, ERROR_OPEN_FILE_FAILED);
            }
            return;
        }
    
        // 查找流
        rst = avformat_find_stream_info(formatContext, 0);
        LOGD("avformat_find_stream_info rst = %d", rst);
        if (rst < 0) {
            if (javaCallHelper) {
                javaCallHelper->onError(THREAD_CHILD, ERROR_FIND_STREAM_FAILED);
            }
            return;
        }
    
        videoChannel = NULL;
        audioChannel = NULL;
    
        for (int i = 0; i < formatContext->nb_streams; i++) {
            AVCodecParameters *codecParameters = formatContext->streams[i]->codecpar;
            // 找到解码器
            AVCodec *decoder = avcodec_find_decoder(codecParameters->codec_id);
            if (!decoder) {
                if (javaCallHelper) {
                    javaCallHelper->onError(THREAD_CHILD, ERROR_FIND_DECODER_FAILED);
                }
                return;
            }
    
            // 创建解码器上下文
            AVCodecContext *decoderContext = avcodec_alloc_context3(decoder);
            if (!decoderContext) {
                if (javaCallHelper) {
                    javaCallHelper->onError(THREAD_CHILD, ERROR_ALLOC_CODEC_CONTEXT_FAILED);
                }
                return;
            }
    
            // 把解码器信息赋值给上下文
            avcodec_parameters_to_context(decoderContext, codecParameters);
    
            // 重要,设置解码线程的个数,提升软解码速度
            if (codecParameters->codec_type == AVMEDIA_TYPE_VIDEO) {
                // 视频解码,多线程软解码提高解码效率
                decoderContext->thread_count = 4;       // 软解码线程,
            }
    
            // 打开解码器
            if (avcodec_open2(decoderContext, decoder, NULL) != 0) {
                if (javaCallHelper) {
                    javaCallHelper->onError(THREAD_CHILD, ERROR_OPEN_DECODER_FAILED);
                }
                return;
            }
    
            // 视频
            if (codecParameters->codec_type == AVMEDIA_TYPE_VIDEO) {
                // 这里就拿到了帧率
                AVRational rational = formatContext->streams[i]->avg_frame_rate;
                int fps = av_q2d(rational);
    
                AVRational time_base = formatContext->streams[i]->time_base;  // 时间显示的单位
    
                // 视频通道的初始化 与 帧率设置(播放控制时用得到)
                videoChannel = new VideoChannel(i, decoderContext, javaCallHelper, time_base);
                videoChannel->setFps(fps);
    
                // 把回调设置过去
                if (renderFrame) {
                    videoChannel->setRenderFrame(renderFrame);
                }
            }
                // 音频
            else if (codecParameters->codec_type == AVMEDIA_TYPE_AUDIO) {
                AVRational time_base = formatContext->streams[i]->time_base;  // 时间显示的单位
                audioChannel = new AudioChannel(i, decoderContext, javaCallHelper, time_base);
            } else {
                avcodec_parameters_free(&codecParameters);
            }
        }
    
        // 把audioChannel对象 注入到 videoChannel中,是为了做音视频同步
        if (videoChannel && audioChannel) {
            videoChannel->setAudioChannel(audioChannel);
        }
    
        // 再判断一下音频/视频是否找到了
        if (!videoChannel && !audioChannel) {
            if (javaCallHelper) {
                javaCallHelper->onError(THREAD_CHILD, ERROR_FIND_NO_MEDIA);
            }
            return;
        }
    
        // 回调java层,ffmpeg已经初始化好了,可以开始播放了
        javaCallHelper->onPrepare(THREAD_CHILD);
    }
    
    /**
     * 开始解码,把packet放到对应的packet queue中
     */
    void FfmpegHelper::startDecode() {
        int ret = 0;
        AVPacket *packet = NULL;
    
        while (isPlaying) {
    
            // 防止解码的packet太多,导致内存溢出
            ......
    
            // 相当于new 一个packet对象
            packet = av_packet_alloc();
            // 从媒体中读取到音频、视频包
            ret = av_read_frame(formatContext, packet);
    
            // 读取成功
            if (ret == 0) {
                // 把数据包放入队列
                // 视频包
                if (videoChannel && packet->stream_index == videoChannel->channelId) {
                    // 把pkt丢给videoChannel中的安全队列,剩下的由videoChannel来单独处理即可,责任清晰
                    videoChannel->pkt_queue.put(packet);
                }
                    // 音频包
                else if (audioChannel && packet->stream_index == audioChannel->channelId) {
                    audioChannel->pkt_queue.put(packet);
                }
            } else if (ret == AVERROR_EOF) {
                // 读取完毕,但是不一定播放完毕
                if (videoChannel->pkt_queue.empty() && videoChannel->frame_queue.empty()
                    && audioChannel->pkt_queue.empty() && audioChannel->frame_queue.empty()) {
                    LOGD("播放完毕");
                    break;
                }
                // 因为seek的存在,就算读取完毕,依然要循环去执行av_read_frame,否则seek无效
            } else {
                break;
            }
        }
        packet = NULL;
        isPlaying = false;
    
        pthread_exit(0);
    }
    
    VideoChannel.cpp
    /**
     * 解码packet并放到frame queue中
     */
    void VideoChannel::decodePacket() {
        LOGD("decodePacket start");
        // 子线程
        AVPacket *packet = 0;
        AVFrame *frame;
        int ret;
    
        while (isPlaying) {
            // 把队列中的数据读取到pkt_queue中
            ret = pkt_queue.get(packet);
            if (!isPlaying) {
                break;
            }
    
            if (!ret) {
                continue;
            }
            // 解码
            avcodec_send_packet(decoderContext, packet);
            frame = av_frame_alloc();
            ret = avcodec_receive_frame(decoderContext, frame);
            // packet资源释放
            releasePacket(packet);
    
            if (ret) {
                // 没有读到帧数据
                continue;
            }
            // 解码成功后,把frame对象放到frame队列中
            frame_queue.put(frame);
    
            // 如果frame队列大于100帧,则等一等
            while (isPlaying && frame_queue.size() >= 100) {
                av_usleep(10 * 1000);
                continue;
            }
        }
        // 释放packet
        releasePacket(packet);
        LOGD("decodePacket end");
    }
    
    /**
     * 从frame queue中获取frame对象,并传给window对象真正显示
     */
    void VideoChannel::showFrame() {
        LOGD("show frame start");
        // 转换器上下文
        swsContext = sws_getContext(decoderContext->width, decoderContext->height,
                                    decoderContext->pix_fmt, decoderContext->width,
                                    decoderContext->height, AV_PIX_FMT_RGBA,
                                    SWS_BILINEAR, 0, 0, 0);
    
        AVFrame *frame = NULL;
        int ret;
    
        long long start, end;
    
        double extra_delay, videoClock, timeDif;
    
        uint8_t *dst_data[4];
        // 每一行的首地址
        int dst_linesize[4];
        // 申请一张width * height, RGBA的图片内存大小 --- av_image_alloc不能放在while循环里,会内存溢出
        av_image_alloc(dst_data, dst_linesize,
                       decoderContext->width, decoderContext->height,
                       AV_PIX_FMT_RGBA, 1); // 1代码左对齐
        double delay = 0;
        while (isPlaying) {
            if (!isPlaying) {
                break;
            }
    
            ret = frame_queue.get(frame);
            if (!isPlaying) {
                break;
            }
    
            if (!ret) {
                continue;
            }
    
            // frame->data --- yuv数据
            // frame->linesize --- yuv 每一行的首地址
            // 0 --- y轴方向的偏移量
            // frame->height --- 帧高度
            // dst_data --- 转换后的rgba数据数组
            // dst_linesize --- rgba数据数组的每一行的首地址
            // yuv 转 rgba, 并把转换后的rgba数据,放在dst_data中
    
            start = getCurrentTimeUs();
            // 把frame中的数据,迁移到image对象中
            sws_scale(swsContext, frame->data, frame->linesize, 0, frame->height,
                      dst_data, dst_linesize);
    
            // 把数据回调出去,在持有window处,真正来显示
            renderFrame(dst_data[0], dst_linesize[0], decoderContext->width, decoderContext->height);
    
            // 视频 与 音频 的时间戳 的差异, timeDif单位为 秒
            videoClock = frame->pts * av_q2d(timeBase);
            if (audioChannel) {
                timeDif = videoClock - audioChannel->colock;
            }
    
            end = getCurrentTimeUs();
            extra_delay =
                    frame->repeat_pict / (double) (2 * fps);   // 如果没有重复帧,这个值为0;为重复帧时,导致的额外延时,单位 秒
    
            // 统一转换为单位 秒
            delay = (1.0f / fps) + extra_delay - (end - start) / 1000000.0f;
    //        LOGD("YUV 2 RGBA cost = %lld, delay show = %d", (end - start), delay);
            delay = delay < 0 ? 0 : delay;
    
            // 根据timeDif来动态调整休眠时间
            adjustSleepTime(timeDif, delay);
    
            // 释放 frame, RGBA数据都出来了,frame就没有用了
            releaseFrame(frame);
        }
    
        releaseFrame(frame);
        av_freep(&dst_data[0]);
        isPlaying = false;
    
        LOGD("show frame end");
    }
    
    /**
     * 正在显示视频的处理方法,因为这个对象持有window对象
     * @param data
     * @param lineSize
     * @param w
     * @param h
     */
    void renderFrame(uint8_t *data, int lineSize, int w, int h) {
    //    LOGD("native-lib renderFrame");
        pthread_mutex_lock(&mutex);
    
        if (!window) {
            pthread_mutex_unlock(&mutex);
            return;;
        }
    
        // 渲染
        // nativeWindow缓冲区大小设置
        ANativeWindow_setBuffersGeometry(window, w, h,
                                         WINDOW_FORMAT_RGBA_8888);
        ANativeWindow_Buffer outBuffer;
    
        // 锁住window ---- 把window 和 outBuffer 绑定
        if (ANativeWindow_lock(window, &outBuffer, 0)) {
            // 如果锁定失败的处理
            ANativeWindow_release(window);
            window = 0;
            pthread_mutex_unlock(&mutex);
            return;
        }
    
        // 进行渲染
        uint8_t *firstWindows = static_cast<uint8_t *>(outBuffer.bits);
        // 输入源Rgba的
        uint8_t *src_data = data;
        // 拿到一行有多少个字节,需要 *4(stride 代表的是像素数量,一个像素4个字节)
        int dstStride = outBuffer.stride * 4;
        int srcStride = lineSize;
    
        // 一行一行copy的原因,是因为window窗体大小,与image buffer的大小,有可能不一致。
        // 是一个数据对齐的问题。
        for (int y = 0; y < outBuffer.height; y++) {
            // 内存拷贝,来进行渲染, 一行一行地copy, 长度以目的地为准
            memcpy(firstWindows + y * dstStride, src_data + y * srcStride, dstStride);
        }
        // 解锁
        ANativeWindow_unlockAndPost(window);
        pthread_mutex_unlock(&mutex);
    }
    

    相关文章

      网友评论

          本文标题:android ffmpeg播放本地视频

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