NDK FFmpeg音视频播放器四

NDK前期基础知识终于学完了,现在开始进入项目实战学习,通过FFmpeg实现一个简单的音视频播放器。

音视频播放器系列:

NDK FFmpeg音视频播放器一

NDK FFmpeg音视频播放器二

NDK FFmpeg音视频播放器三

NDK FFmpeg音视频播放器四

NDK FFmpeg音视频播放器五

NDK FFmpeg音视频播放器六

音视频一二三节已经实现了音视频播放,本节主要是通过Profiler来检测工程存在的内存泄漏问题。

主要内容如下:
1.项目native层内存泄漏全面分析。
2.项目native层内存泄漏各个隐患补救方案。

用到的ffmpeg、rtmp等库资源:
https://wwgl.lanzout.com/iN21C0qiiija

一、项目native层内存泄漏全面分析

通过Profiler可以清楚的看出,视频在播放时,native层消耗的内存不断增加,最终达到1.5G,java层内存几乎没有变化,可以直观的分析出native层存在严重的内存泄漏问题。进一步定位native层代码,涉及到循环、线程的主要有:NdkPlayer.cpp、VideoChannel.cpp、AudioChannel.cpp;
接下来将从这几个类开始,将代码一行行进行分析定位问题。

 NdkPlayer.cpp:

问题1:循环中存在明显的生产者生产速度远大于,消费者的消费速度,导致队列撑爆,段时间内存急速增到;
优化方案:放慢生产速度,等待消费者将队列数据消费差不多再生产。

// TODO 1.内存泄漏关键点(控制packet队列大小,等待队列中的数据被消费)
while (isPlaying) {
	// 1.1内存泄漏点 解决方案:音频 不丢弃数据,等待队列中压缩包AVPacket被消费,再添加到队列
	if (audio_channel && audio_channel->packets.size() > 100) {
		av_usleep(10 * 1000); // 单位 :microseconds 微妙 10毫秒
		continue;
	}
	// 1.2内存泄漏点 解决方案:视频 不丢弃数据,等待队列中压缩包AVPacket被消费,再添加到队列
	if (video_channel && video_channel->packets.size() > 100) {
		av_usleep(10 * 1000); // 单位 :microseconds 微妙 10毫秒
		continue;
	}
}

问题2:数据使用完后未及时释放;
优化方案:数据使用完后立即释放。

if (result == AVERROR_EOF) {
	// end of file == 读到文件末尾了 == AVERROR_EOF
	// 表示读完了,要考虑释放播放完成,并不代表播放完毕
	LOGI("NdkPlayer::start_() end");
	// 1.3内存泄漏点 解决方案:队列的数据被音频 视频 全部播放完毕了,退出
	if (video_channel->packets.empty() && audio_channel->packets.empty()) {
		break;
	}
}

完整优化代码:

/**
 * 循环获取压缩包AVPacket,并push压缩包到队列
 */
void NdkPlayer::start_() {
    LOGI("NdkPlayer::start_()");
    // TODO 1.内存泄漏关键点(控制packet队列大小,等待队列中的数据被消费)
    while (isPlaying) {
        // 1.1内存泄漏点 解决方案:音频 不丢弃数据,等待队列中压缩包AVPacket被消费,再添加到队列
        if (audio_channel && audio_channel->packets.size() > 100) {
            av_usleep(10 * 1000); // 单位 :microseconds 微妙 10毫秒
            continue;
        }
        // 1.2内存泄漏点 解决方案:视频 不丢弃数据,等待队列中压缩包AVPacket被消费,再添加到队列
        if (video_channel && video_channel->packets.size() > 100) {
            av_usleep(10 * 1000); // 单位 :microseconds 微妙 10毫秒
            continue;
        }
        // AVPacket 可能是音频 也可能是视频(压缩包)
        AVPacket *packet = av_packet_alloc();
        int result = av_read_frame(format_context, packet);
        // @return 0 if OK
        if (!result) {
            // 把压缩包AVPacket 分别加入音频 和 视频队列
            if (audio_channel && audio_channel->stream_index == packet->stream_index) {
                // 音频
                audio_channel->packets.insertToQueue(packet);
            } else if (video_channel && video_channel->stream_index == packet->stream_index) {
                // 视频
                video_channel->packets.insertToQueue(packet);
            }
        } else if (result == AVERROR_EOF) {
            // end of file == 读到文件末尾了 == AVERROR_EOF
            // 表示读完了,要考虑释放播放完成,并不代表播放完毕
            LOGI("NdkPlayer::start_() end");
            // 1.3内存泄漏点 解决方案:队列的数据被音频 视频 全部播放完毕了,退出
            if (video_channel->packets.empty() && audio_channel->packets.empty()) {
                break;
            }
        } else {
            // av_read_frame 出现了错误,结束当前循环
            break;
        }
    } // end while
    isPlaying = 0;
    audio_channel->stop();
    video_channel->stop();
}

VideoChannel.cpp:

同样存在上面问题。

/**
 * 第一个线程: 视频:取出队列的压缩包 进行解码 解码后的原始包 再push队列中去
 */
void VideoChannel::video_decode() {
    LOGI("VideoChannel::video_decode()");
    // TODO 2.内存泄漏关键点(控制frames队列大小,等待队列中的数据被消费)
    AVPacket *pkt = 0;
    while (isPlaying) {
        // 2.1内存泄漏点 解决方案:不丢弃数据,等待队列中原始包AVFrame被消费,再添加到队列
        if (isPlaying && frames.size() > 100) {
            av_usleep(10 * 1000); // 单位 :microseconds 微妙 10毫秒
            continue;
        }
        // 获取AVPacket *  压缩包
        int result = packets.getQueueAndDel(pkt);
        if (!isPlaying) {
            // 获取压缩包是耗时操作,获取完,如果关闭了播放,跳出循环
            break;
        }
        if (!result) {
            // 获取失败,可能是压缩包数据还没有加入队列,继续获取
            continue;
        }
        // 1.发送pkt(压缩包)给缓冲区,@return 0 on success
        result = avcodec_send_packet(codecContext, pkt);
        // FFmpeg源码缓存一份pkt,释放即可,放到后面释放
        // releaseAVPacket(&pkt);
        if (result) {
            // avcodec_send_packet 出现了错误
            break;
        }
        AVFrame *frame = av_frame_alloc();
        // 2.从缓冲区拿出来(原始包),@return 0: success
        result = avcodec_receive_frame(codecContext, frame);
        if (result == AVERROR(EAGAIN)) {
            // B帧  B帧参考前面成功  B帧参考后面失败   可能是P帧没有出来,再拿一次就行了
            continue;
        } else if (result != 0) {
            // avcodec_receive_frame 出现了错误
            // 2.2内存泄漏点 解决方案:解码视频的frame出错,马上释放,防止在堆区开辟了空间
            if (frame) {
                releaseAVFrame(&frame);
            }
            break;
        }
        // 拿到了原始包,并将原始包push到队列
        frames.insertToQueue(frame);
        // TODO 4.内存泄漏点 原始包已经加入队列,安心释放压缩包pkt本身空间和pkt成员指向的空间
        av_packet_unref(pkt); // 减1 = 0 释放成员指向的堆区
        releaseAVPacket(&pkt); // 释放AVPacket * 本身的堆区空间
    }
    // 解码获取原始包后,释放压缩包
    // 4.1内存泄漏点 原始包已经加入队列,安心释放压缩包pkt本身空间和pkt成员指向的空间
    av_packet_unref(pkt); // 减1 = 0 释放成员指向的堆区
    releaseAVPacket(&pkt); // 释放AVPacket * 本身的堆区空间
}

/**
 * 第二线线程:视频:从队列取出原始包,播放
 */
void VideoChannel::video_play() {
    LOGI("VideoChannel::video_play()");
    AVFrame *frame = 0;
    uint8_t *dst_data[4]; // RGBA 播放文件
    int dst_linesize[4]; // RGBA
    //给 dst_data 申请内存   width * height * 4 xxxx
    av_image_alloc(dst_data, dst_linesize,
                   codecContext->width, codecContext->height, AV_PIX_FMT_RGBA, 1);
    // SWS_BILINEAR 适中算法
    SwsContext *sws_ctx = sws_getContext(
            // 下面是输入环节
            codecContext->width,
            codecContext->height,
            codecContext->pix_fmt, // 自动获取 xxx.mp4 的像素格式  AV_PIX_FMT_YUV420P // 写死的
            // 下面是输出环节
            codecContext->width,
            codecContext->height,
            AV_PIX_FMT_RGBA,
            SWS_BILINEAR, NULL, NULL, NULL);
    while (isPlaying) {
        int result = frames.getQueueAndDel(frame);
        if (!isPlaying) {
            break; // 如果关闭了播放,跳出循环,releaseAVFrame(&frame);
        }
        if (!result) { // ret == 0
            continue; // 哪怕是没有成功,也要继续(假设:你生产太慢(原始包加入队列),我消费就等一下你)
        }
        // 格式转换 yuv ---> rgba
        sws_scale(sws_ctx,
                // 下面是输入环节 YUV的数据
                  frame->data, frame->linesize,
                  0, codecContext->height,

                // 下面是输出环节  成果:RGBA数据 dst_data
                  dst_data,
                  dst_linesize
        );
        /**
         * ANatvieWindows 渲染工作
         * SurfaceView ----- ANatvieWindows
         * 这里拿不到Surface,只能函数指针renderCallback()将RGBA数据 dst_data 回调给 native-lib.cpp,显示
         * 函数指针renderCallback()
         * 参数1:RGBA数据 dst_data 数组被传递会退化成指针,默认就是取第1元素
         * 参数2:视频宽
         * 参数3:视频高
         * 参数4:数据长度
         */
        this->renderCallback(dst_data[0], codecContext->width, codecContext->height,
                             dst_linesize[0]);
        // 释放原始包,因为已经被渲染完了,没用了
        // TODO 6.内存泄漏点 原始包已经被播放了,安心释放原始包frame本身空间和frame成员指向的空间
        av_frame_unref(frame); // 减1 = 0 释放成员指向的堆区
        releaseAVFrame(&frame); // 释放AVFrame * 本身的堆区空间
    }
    // 6.1内存泄漏点 原始包已经被播放了,安心释放原始包frame本身空间和frame成员指向的空间
    av_frame_unref(frame); // 减1 = 0 释放成员指向的堆区
    releaseAVFrame(&frame); // 释放AVFrame * 本身的堆区空间
    isPlaying = 0;
    av_free(&dst_data[0]);
    // free(sws_ctx); FFmpeg必须使用人家的函数释放,直接崩溃
    sws_freeContext(sws_ctx);
}

AudioChannel.cpp:

同样存在上面问题。

/**
 * 第一个线程: 音频:取出队列的压缩包 进行编码 编码后的原始包 再push队列中去(音频:PCM数据)
 */
void AudioChannel::audio_decode() {
    LOGI("AudioChannel::audio_decode()");
    // TODO 3.内存泄漏关键点(控制frames队列大小,等待队列中的数据被消费)
    AVPacket *pkt = 0;
    while (isPlaying) {
        // 3.1内存泄漏点 解决方案:不丢弃数据,等待队列中原始包AVFrame被消费,再添加到队列
        if (isPlaying && frames.size() > 100) {
            av_usleep(10 * 1000); // 单位 :microseconds 微妙 10毫秒
            continue;
        }
        // 获取AVPacket *  压缩包
        int result = packets.getQueueAndDel(pkt);
        if (!isPlaying) {
            // 获取压缩包是耗时操作,获取完,如果关闭了播放,跳出循环
            break;
        }
        if (!result) {
            // 获取失败,可能是压缩包数据还没有加入队列,继续获取
            continue;
        }
        // 1.发送pkt(压缩包)给缓冲区,@return 0 on success
        result = avcodec_send_packet(codecContext, pkt);
        // FFmpeg源码缓存一份pkt,释放即可,放到后面释放
        // releaseAVPacket(&pkt);
        if (result) {
            // avcodec_send_packet 出现了错误
            break;
        }
        AVFrame *frame = av_frame_alloc();
        // 2.从缓冲区拿出来(原始包),@return 0: success
        result = avcodec_receive_frame(codecContext, frame);
        if (result == AVERROR(EAGAIN)) {
            // 有可能音频帧,也会获取失败,重新拿一次
            continue;
        } else if (result != 0) {
            // avcodec_receive_frame 出现了错误
            // 3.2内存泄漏点 解决方案:解码视频的frame出错,马上释放,防止在堆区开辟了空间
            if (frame) {
                releaseAVFrame(&frame);
            }
            break;
        }
        // 拿到了原始包,并将原始包push到队列 PCM数据
        frames.insertToQueue(frame);
        // TODO 5.内存泄漏点 原始包已经加入队列,安心释放压缩包pkt本身空间和pkt成员指向的空间
        av_packet_unref(pkt); // 减1 = 0 释放成员指向的堆区
        releaseAVPacket(&pkt); // 释放AVPacket * 本身的堆区空间
    }
    // 解码获取原始包后,释放压缩包
    // 5.1内存泄漏点 原始包已经加入队列,安心释放压缩包pkt本身空间和pkt成员指向的空间
    av_packet_unref(pkt); // 减1 = 0 释放成员指向的堆区
    releaseAVPacket(&pkt); // 释放AVPacket * 本身的堆区空间
}

/**
 * 1.out_buffers 给予数据
 * 2.out_buffers 给予数据的大小计算工作
 * @return  大小还要计算,因为我们还要做重采样工作,重采样之后,大小不同了
 */
int AudioChannel::getPCM() {
    LOGI("AudioChannel::getPCM");
    int pcm_data_size = 0;
    // 从frames队列中,获取PCM数据,frame->data == PCM数据(待 重采样 32bit)
    AVFrame *frame = 0;
    while (isPlaying) {
        int result = frames.getQueueAndDel(frame);
        if (!isPlaying) {
            break; // 如果关闭了播放,跳出循环,releaseAVPacket(&pkt);
        }
        if (!result) {
            continue; // 哪怕是没有成功,也要继续(假设:你生产太慢(原始包加入队列),我消费就等一下你)
        }
        /**
         * 开始重采样
         * 如:来源:10个48000   ---->  目标:44100  11个44100
         * 获取单通道的样本数 (计算目标样本数: ? 10个48000 --->  48000/44100因为除不尽  11个44100)
         * 参数1:swr_get_delay(swr_ctx, frame->sample_rate) + frame->nb_samples 获取下一个输入样本相对于下一个输出样本将经历的延迟
         * 参数2:out_sample_rate 输出采样率
         * 参数3:frame->sample_rate 输入采样率
         * 参数4:AV_ROUND_UP 先上取 取去11个才能容纳的上
         */
        int dst_nb_samples = av_rescale_rnd(
                swr_get_delay(swr_ctx, frame->sample_rate) + frame->nb_samples,
                out_sample_rate, frame->sample_rate, AV_ROUND_UP);
        /**
         * pcm的处理逻辑
         * 音频播放器的数据格式是我们自己在下面定义的
         * 而原始数据(待播放的音频pcm数据)
         * TODO 重采样工作
         * 返回的结果:每个通道输出的样本数(注意:是转换后的) 重采样实验(通道基本上都是:1024)
         * 参数1:swr_ctx SwrContext
         * TODO 下面是输出区域
         * 参数2:out_buffers 重采样后的成果的buff
         * 参数3:dst_nb_samples 成果的 单通道的样本数 无法与out_buffers对应,所以有下面的pcm_data_size计算
         * TODO 下面是输入区域
         * 参数4:(const uint8_t **) frame->data 队列的AVFrame * 的PCM数据 未重采样的
         * 参数5:frame->nb_samples 输入的样本数
         * 参数6:
         */
        int samples_per_channel = swr_convert(swr_ctx, &out_buffers, dst_nb_samples,
                                              (const uint8_t **) frame->data, frame->nb_samples);
        /**
         * 由于out_buffers 和 dst_nb_samples 无法对应,所以pcm_data_size需要重新计算
         * 941通道样本数  *  2样本格式字节数  *  2声道数  =3764
         */
        pcm_data_size = samples_per_channel * out_sample_size * out_channels;
        break;
    } // while end
    // TODO 7.内存泄漏点 原始包已经被播放了,安心释放原始包frame本身空间和frame成员指向的空间
    av_frame_unref(frame); // 减1 = 0 释放成员指向的堆区
    releaseAVFrame(&frame); // 释放AVFrame * 本身的堆区空间
    /**
     * FFmpeg录制 Mac 麦克风  输出 每一个音频包的size == 4096
     * 4096是单声道的样本数,44100是每秒钟采样的数
     * 单通道样本数:1024 * 2声道 * 2(16bit) = 4,096 == 4096是单声道的样本数
     * 采样率 44100是每秒钟采样的次数
     * 样本数 = 采样率 * 声道数 * 位声
     * 双声道的样本数 = (采样率 * 声道数 * 位声) * 2
     */
    return pcm_data_size;
}

优化后再次使用Profiler检测内存:

内存平稳,保持在177M左右,基本解决内存泄漏问题,接下来。。。 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sziitjin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值