美文网首页
Android SurfaceView播放RTMP流

Android SurfaceView播放RTMP流

作者: 星云春风 | 来源:发表于2020-06-17 23:46 被阅读0次
    1.编译ffmpeg 和rtmp 静态库,并配置环境
    2.JAVA层实现SurfaceHolder.Callback
    • surfaceChanged回调中传递Surface给Native层去操作
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_xia_ndk_1player_1code_MyPlayer_setSurfaceNative(JNIEnv *env, jobject thiz,
                                                             jobject surface) {
        // TODO: implement setSurfaceNative()
        pthread_mutex_lock(&mutex);
        if (nativeWindow) {
            ANativeWindow_release(nativeWindow);
            nativeWindow = 0;
        }
        // 创建新的窗口用于视频显示
        nativeWindow = ANativeWindow_fromSurface(env, surface);
        pthread_mutex_unlock(&mutex);
    }
    
    
    • 定义prepareNative(String dataSource)方法给Native层传递流地址或者流文件
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_xia_ndk_1player_1code_MyPlayer_prepareNative(JNIEnv *env, jobject thiz,
                                                          jstring data_source) {
        // TODO: implement prepareNative()
        const char *dataSource = env->GetStringUTFChars(data_source, NULL);
        JNICallback *jniCallback = new JNICallback(::myJavaVm, env, thiz);
        //面向对象 创建MyPlayer
        myPlayer = new MyPlayer(dataSource, jniCallback);
        myPlayer->setRenderCallback(renderFrame);
        myPlayer->prepare();
        env->ReleaseStringUTFChars(data_source, dataSource);
    }
    
    3.Native中获取流地址或者流文件,JNICallback回调用于跟Java层进行传递信息
    MyPlayer::MyPlayer(const char *data_source, JNICallback *pCallback) {
        //拿到的是悬空
        //this->data_source = data_source;
        //长度不对,需要加1  c和C++的区别
        this->data_source = new char[strlen(data_source) + 1];
        strcpy(this->data_source, data_source);
        this->jniCallback = pCallback;
    }
    
    4. 开始准备工作
      1. 创建子线程进行准备
    void *customTaskPrepareThread(void *pVoid) {
        MyPlayer *myPlayer = static_cast<MyPlayer *>(pVoid);
        //创建异步函数
        myPlayer->prepare_();
        return 0;
    }
    //准备工作 ,(拆包裹 解码 解封装 解复用)(音频流 视频流 字幕流。。。等) 主线程
    void MyPlayer::prepare() {
        //创建异步线程
        pthread_create(&this->pid_prepare, 0, customTaskPrepareThread, this);
    }
    
    • 2 .获取媒体上下文
    this->avFormatContext = avformat_alloc_context();
    
      1. 设置字典
        //此字典 能决定打开的需求
        AVDictionary *dictionary = 0;
        //打开文件花的时长 单位是微秒
        av_dict_set(&dictionary, "timeout", "5000000", 0);
    
      1. 初始化网络
        // 初始化网络
        avformat_network_init();
    
    • 5.打开流媒体文件
     /**
         * 媒体上下文
         * 数据源
         * 数据格式
         * 数据字典
         *  判断是不是包裹,如果是石头(被损坏的数据),就无法解析
         */
        int ret = avformat_open_input(&avFormatContext, this->data_source, 0, &dictionary);
    
    
      1. 释放字典
        av_dict_free(&dictionary); // 释放字典
    
    • 7.判断打开流媒体文件是否成功
        //返回 0是成功
        if (ret) {
            // TODO xia chen hui 2020/6/13 19:36 写JNI回调,通知java层,数据流被损坏了
            if (jniCallback) {
                this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL);
            }
            return;
        }
    
    • 8.寻找媒体格式中的(音频 视频 字幕)
        //1.寻找媒体格式中的(音频 视频 字幕) ,不给字典是因为不需要设置额外的配置  2.给媒体上下文赋值
        ret = avformat_find_stream_info(this->avFormatContext, 0);
    
    • 9.判断是否有找到流媒体
        if (ret < 0) {
            // TODO xia chen hui 2020/6/13 19:36 写JNI回调,通知java层,找不到流媒体
            if (jniCallback) {
                this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_CAN_NOT_FIND_STREAMS);
            }
            return;
        }
    
      1. 循环遍历流媒体上下文中的流,获取流数据, 流0(视频) 流1 (音频) 流2(字幕) 等等
     for (int i = 0; i < this->avFormatContext->nb_streams; ++i) {
            //获取媒体流
            AVStream *stream = this->avFormatContext->streams[i];
            //获取编解码器
            AVCodecParameters *codecParameters = stream->codecpar;
            //编解码器ID
            enum AVCodecID codecId = codecParameters->codec_id;
            //拿到编解码器
            AVCodec *codec = avcodec_find_decoder(codecId);
            if (!codec) {
                // TODO xia chen hui 2020/6/13 19:36 写JNI回调,通知java层  找不到解码器
                if (jniCallback) {
                    this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_FIND_DECODER_FAIL);
                }
                return;
            }
            //为了解码,需要解码器上下文h
            AVCodecContext *codecContext = avcodec_alloc_context3(codec);
            if (!codecContext) {
                // TODO xia chen hui 2020/6/13 19:36 写JNI回调,通知java层, 获取解码器上下文失败
                if (jniCallback) {
                    this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);
                }
                return;
            }
            //解码器上下文 设置参数
            ret = avcodec_parameters_to_context(codecContext, codecParameters);
            if (ret < 0) {
                // TODO xia chen hui 2020/6/13 19:36 写JNI回调,通知java层, 解码器上下文设置失败
                if (jniCallback) {
                    this->jniCallback->onErrorAction(THREAD_CHILD,
                                                     FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL);
                }
                return;
            }
            //打开解码器
            ret = avcodec_open2(codecContext, codec, 0);
            if (ret) {
                //0是成功,非0为true
                // TODO xia chen hui 2020/6/13 19:36 写JNI回调,通知java层, 打开解码器失败
                if (jniCallback) {
                    this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_OPEN_DECODER_FAIL);
                }
                return;
            }
            //使用解码器
            //区分音频 视频 通道
            if (codecParameters->codec_type == AVMEDIA_TYPE_AUDIO) {
                this->audioChannel = new AudioChannel(i, codecContext);
            } else if (codecParameters->codec_type == AVMEDIA_TYPE_VIDEO) {
                //现在很多字幕流放在视频轨里一起了。
                this->videoChannel = new VideoChannel(i, codecContext);
                videoChannel->setRenderCallback(renderCallback);
            } else {
                // TODO xia chen hui 2020/6/13 19:36 写JNI回调,通知java层, 没有音视频
                if (jniCallback) {
                    this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_NOMEDIA);
                }
            }
    
    
        }
    
    • 11 .准备完毕,进行播放操作
      if (!audioChannel && !videoChannel) {
            // 把错误信息,告诉给Java层去(回调给Java)
            this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_NOMEDIA);
            return;
        }
        //告诉java层 ,准备OK了
        if (jniCallback) {
            this->jniCallback->onPrepared(THREAD_CHILD);
        }
    
    5. 视频播放操作,包括解码和播放两步
      1. 开启子线把未解码的数据放入队列packets中
      //未解码的格式,保存在AVPacket中
            AVPacket *packet = av_packet_alloc();
            //执行这个 ,packet就可以获取值了
            int ret = av_read_frame(this->avFormatContext, packet);
            if (!ret) {  //ret  == 0
                //把已经得到的Packet 放入队列中
                //先要判断是否是音频 还是视频
                if (videoChannel && videoChannel->stream_index == packet->stream_index) {
                    // 说明是视频
                    this->videoChannel->packets.push(packet);
                } else if (audioChannel && audioChannel->stream_index == packet->stream_index) {
                    //音频
                    this->audioChannel->packets.push(packet);
                }
            } else if (ret == AVERROR_EOF) {
                //文件末尾 ,流读完了
    
            } else {
                //代表失败了
                break;
            }
    
      1. 队列中取出数据,进行解码以及播放
    // 从队列中取出 1.解码 2. 播放
    void VideoChannel::start() {
        this->isPlaying = 1;
        // 存放未解锁的队列 开始工作。
        this->packets.setFlag(1);
        //存放已解码的队列,开始工作
        this->frames.setFlag(1);
        //解码的线程
        pthread_create(&pid_video_decode, 0, task_video_decode, this);
        // 播放的线程
        pthread_create(&pid_video_player, 0, task_video_player, this);
    }
    
    • 3 解码操作
    //异步线程,开始解码
    void VideoChannel::video_decode() {
        //取出未解码的队列数据
        AVPacket *packet;
        while (isPlaying) {
            // 消费速度比生成速度慢(生成100,只消费10个,这样队列会爆)
            // 内存泄漏点2,解决方案:控制队列大小
            if (isPlaying && frames.queueSize() > 100) {
                // 休眠 等待队列中的数据被消费
                av_usleep(10 * 1000);
                continue;
            }
    
            int ret = this->packets.pop(packet);
            if (!isPlaying) {
                break;
            }
            if (!ret) {
                continue;
            }
            //可以未解码的视频数据包
            ret = avcodec_send_packet(this->avCodecContext, packet);
            if (ret) {
                //失败了
                break;
            }
            //创建原始数据 ,开始把音频AAC 视频H264转变成音频PCM 视频YUV了
            AVFrame *avFrame = av_frame_alloc();
            ret = avcodec_receive_frame(this->avCodecContext, avFrame);
            if (ret == AVERROR(EAGAIN)) {
                //代表帧取的不完整 如果是I帧(完整的一帧)就不会进入,如果是P帧  B帧 就会进入这里
                continue; //重新取,直到取到完整的帧为止
            } else if (ret != 0) {
                // TODO xia chen hui 2020/6/14 23:38 做释放工作
                break;
            }
            //取到了原始数据
            this->frames.push(avFrame);
        }
    
        // TODO xia chen hui 2020/6/14 23:39 出了循环 ,需要释放
        releaseAVPacket(&packet);
    }
    
    • 4 .播放操作
    //异步线程 开始播放
    void VideoChannel::video_player() {
        // yuv最原始的视频帧数据,但是不能显示在屏幕上(Android IOS SDL等)
        //yuv 转变成rgba 才能显示
        // TODO xia chen hui 2020/6/15 0:04 1.转换的上下文 ,把yunv转为rgba   ,SwsContext是视频
        SwsContext *swsContext = sws_getContext(
                //原始的一层 宽  高  格式
                this->avCodecContext->width, this->avCodecContext->height,
                this->avCodecContext->pix_fmt,
                //目标最终要显示到屏幕的信息 ,最好和原始的保持一致
                this->avCodecContext->width, this->avCodecContext->height, AV_PIX_FMT_RGBA,
                //渲染的速率  ,这个是比较常用的
                SWS_BILINEAR,
                NULL, NULL, NULL
        );
        // TODO xia chen hui 2020/6/15 0:04 2.给转换后的数据 rgba 这种申请内存
        uint8_t *dst_data[4];
        int dst_linesize[4];
        AVFrame *avFrame = 0;
        av_image_alloc(dst_data, dst_linesize,
                       avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA, 1);
        // TODO xia chen hui 2020/6/15 0:07 3. 原始数据转换 (队列中拿到原始数据,一帧一帧的转换未rgba,一帧一帧渲染到屏幕)
        int ret;
        while (isPlaying) {
            ret = frames.pop(avFrame);
            if (!isPlaying) {
                //停止播放,就跳出循环,出了循环就需要释放
            }
            if (!ret) {
                //失败了就继续
                continue;
            }
            //avFrame->data是yuv原始数据 dst_data是rgba格式的数据
            sws_scale(swsContext, avFrame->data,
                      avFrame->linesize, 0, avCodecContext->height, dst_data, dst_linesize);
            //渲染 方式有2种,1 渲染一帧图像(宽  高  数据) 这个会回调native-lib中的renderFrame函数
            this->renderCallback(dst_data[0], avCodecContext->width, avCodecContext->height,
                                 dst_linesize[0]);
    
            releaseAVFrame(&avFrame);
        }
        releaseAVFrame(&avFrame);
        isPlaying = 0;
        av_freep(dst_data[0]);
        sws_freeContext(swsContext);
    }
    
      1. 渲染屏幕
    /**
     * 专门渲染的函数
     * @param src_data 解码后的视频 rgba 数据
     * @param width 宽信息
     * @param height 高信息
     * @param src_liinesize 行数size相关信息
     */
    void renderFrame(uint8_t *src_data, int width, int height, int src_liinesize) {
        pthread_mutex_lock(&mutex);
    
        if (!nativeWindow) {
            pthread_mutex_unlock(&mutex);
        }
    
        // 设置窗口属性
        ANativeWindow_setBuffersGeometry(nativeWindow, width, height, WINDOW_FORMAT_RGBA_8888);
        ANativeWindow_Buffer windowBuffer;
        if (ANativeWindow_lock(nativeWindow, &windowBuffer, 0)) {
            ANativeWindow_release(nativeWindow);
            nativeWindow = 0;
            pthread_mutex_unlock(&mutex);
            return;
        }
    
        // 填数据到buffer,其实就是修改数据
        uint8_t *dst_data = static_cast<uint8_t *>(windowBuffer.bits);
        int lineSize = windowBuffer.stride * 4; // RGBA ,每一个像素点xRGBA
        // 下面就是逐行Copy了
        for (int i = 0; i < windowBuffer.height; ++i) {
              // 一行一行的copy到Android 屏幕上
            memcpy(dst_data + i * lineSize, src_data + i * src_liinesize, lineSize);
        }
    
        ANativeWindow_unlockAndPost(nativeWindow);
        pthread_mutex_unlock(&mutex);
    }
    
    
      1. 释放队列
        // 注意:由于是父类,析构函数,必须是虚函数
        virtual ~BaseChannel() {
            packets.clearQueue();
            frames.clearQueue();
        }
        /**
         * 释放AVFrame 队列
         * @param avFrame
         */
        static void releaseAVFrame(AVFrame **avFrame) {
            if (avFrame) {
                av_frame_free(avFrame);
                *avFrame = 0;
            }
        }
      /**
            * 释放AVPacket 队列
            * @param avPacket
            */
        static void releaseAVPacket(AVPacket **avPacket) {
            if (avPacket) {
                av_packet_free(avPacket);
                *avPacket = 0;
            }
        }
    
    6. 音频播放操作

    DEMO传送门...

    相关文章

      网友评论

          本文标题:Android SurfaceView播放RTMP流

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