Android SurfaceView播放RTMP流

2020-06-17  本文已影响0人  星云春风
1.编译ffmpeg 和rtmp 静态库,并配置环境
2.JAVA层实现SurfaceHolder.Callback
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);
}

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. 开始准备工作
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);
}
this->avFormatContext = avformat_alloc_context();
    //此字典 能决定打开的需求
    AVDictionary *dictionary = 0;
    //打开文件花的时长 单位是微秒
    av_dict_set(&dictionary, "timeout", "5000000", 0);
    // 初始化网络
    avformat_network_init();
 /**
     * 媒体上下文
     * 数据源
     * 数据格式
     * 数据字典
     *  判断是不是包裹,如果是石头(被损坏的数据),就无法解析
     */
    int ret = avformat_open_input(&avFormatContext, this->data_source, 0, &dictionary);

    av_dict_free(&dictionary); // 释放字典
    //返回 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;
    }
    //1.寻找媒体格式中的(音频 视频 字幕) ,不给字典是因为不需要设置额外的配置  2.给媒体上下文赋值
    ret = avformat_find_stream_info(this->avFormatContext, 0);
    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;
    }
 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);
            }
        }


    }
  if (!audioChannel && !videoChannel) {
        // 把错误信息,告诉给Java层去(回调给Java)
        this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_NOMEDIA);
        return;
    }
    //告诉java层 ,准备OK了
    if (jniCallback) {
        this->jniCallback->onPrepared(THREAD_CHILD);
    }
5. 视频播放操作,包括解码和播放两步
  //未解码的格式,保存在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.解码 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);
}
//异步线程,开始解码
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);
}
//异步线程 开始播放
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);
}
/**
 * 专门渲染的函数
 * @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);
}

    // 注意:由于是父类,析构函数,必须是虚函数
    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传送门...

上一篇下一篇

猜你喜欢

热点阅读