• 注册
当前位置:1313e > 默认分类 >正文

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 OKif (!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 whileisPlaying = 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 successresult = avcodec_send_packet(codecContext, pkt);// FFmpeg源码缓存一份pkt,释放即可,放到后面释放// releaseAVPacket(&pkt);if (result) {// avcodec_send_packet 出现了错误break;}AVFrame *frame = av_frame_alloc();// 2.从缓冲区拿出来(原始包),@return 0: successresult = 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 xxxxav_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 == 0continue; // 哪怕是没有成功,也要继续(假设:你生产太慢(原始包加入队列),我消费就等一下你)}// 格式转换 yuv ---> rgbasws_scale(sws_ctx,// 下面是输入环节 YUV的数据frame->data, frame->linesize,0, codecContext->height,// 下面是输出环节  成果:RGBA数据 dst_datadst_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 successresult = avcodec_send_packet(codecContext, pkt);// FFmpeg源码缓存一份pkt,释放即可,放到后面释放// releaseAVPacket(&pkt);if (result) {// avcodec_send_packet 出现了错误break;}AVFrame *frame = av_frame_alloc();// 2.从缓冲区拿出来(原始包),@return 0: successresult = 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左右,基本解决内存泄漏问题,接下来。。。 

本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 162202241@qq.com 举报,一经查实,本站将立刻删除。

最新评论

欢迎您发表评论:

请登录之后再进行评论

登录