liudianwu |
2022-12-06 09:26 |
Qt音视频开发05-保存视频文件(yuv/h264/mp4)
## 一、前言 和音频存储类似,视频的存储也对应三种格式,视频最原始的数据是yuv(音频对应pcm),视频压缩后的数据是h264(音频对应aac),由于很多播放器或者早期的播放器不支持直接播放h264文件,所以需要用编码器编码成mp4格式,这块就需要用到ffmpeg里面一整套的编码流程,对yuv数据进行编码成MP4格式存储。 在经过对各种视频文件或者视频流保存的过程中,发现rtsp这类的视频流可以直接编码打包存储,不需要经过 avcodec_send_frame avcodec_receive_packet 这两个步骤对每个包编码,这样可以极大的降低CPU占用,猜测可能是rtsp视频流收到的数据包packet就已经是标准的h264裸流带了各种pps啥的。所以视频监控领域如果要同时存储16路32路视频,采用这个策略是最稳妥的,相当于一直写文件。很多人会觉得编码流程繁琐,其实只要静下心来,挨个测试,把流程搞懂,基本上都是水到渠成的事情。包括之前遇到的保存的文件鼠标右键属性中看不到分辨率等参数信息,原来是调用写入文件头 avformat_write_header 写入的时机不对,一定要在打开打开视频编码器 avcodec_open2 以及打开输出文件 avio_open 以后再写入。 编码保存的大致流程: 01. 查找编码器 avcodec_find_encoder 02. 创建编码器 avcodec_alloc_context3 03. 设置编码器 pix_fmt/time_base/framerate/width/height 04. 打开编码器 avcodec_open2 06. 创建上下文 avformat_alloc_output_context2 07. 创建输出流 avformat_new_stream 08. 设置流参数 avcodec_parameters_from_context 09. 写入开始符 avformat_write_header 10. 发送数据帧 avcodec_send_frame 11. 打包数据帧 avcodec_receive_packet 12. 写入数据帧 av_interleaved_write_frame 13. 写入结尾符 av_write_trailer 14. 释放各资源 avcodec_free_context/avio_close/avformat_free_context ## 二、效果图 [attachment=23193][attachment=23194] ## 三、体验地址 1. 国内站点:[https://gitee.com/feiyangqingyun](https://gitee.com/feiyangqingyun) 2. 国际站点:[https://github.com/feiyangqingyun](https://github.com/feiyangqingyun) 3. 个人作品:[https://blog.csdn.net/feiyangqingyun/article/details/97565652](https://blog.csdn.net/feiyangqingyun/article/details/97565652) 4. 体验地址:[https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g](https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g) 提取码:01jf 文件名:bin_video_demo/bin_linux_video。 ## 四、相关代码 ```cpp bool FFmpegSave::initVideoH264() { //查找视频编码器(如果源头是H265则采用HEVC作为编码器) AVCodecID codecID = FFmpegHelper::getCodecID(videoStreamIn); if (codecID == AV_CODEC_ID_HEVC) { videoCodec = avcodec_find_encoder(AV_CODEC_ID_HEVC); } else { videoCodec = avcodec_find_encoder(AV_CODEC_ID_H264); } if (!videoCodec) { debug("编码失败", QString("错误: 查找视频编码器失败")); return false; } //创建视频编码器上下文 videoCodecCtx = avcodec_alloc_context3(videoCodec); if (!videoCodecCtx) { debug("编码失败", QString("错误: 创建视频编码器上下文失败")); return false; } //为了兼容低版本的编译器推荐选择第一种方式 #if 1 //放大系数是为了小数位能够正确放大到整型 int ratio = 10000; videoCodecCtx->time_base.num = 1 * ratio; videoCodecCtx->time_base.den = frameRate * ratio; videoCodecCtx->framerate.num = frameRate * ratio; videoCodecCtx->framerate.den = 1 * ratio; #elif 0 videoCodecCtx->time_base = {1, frameRate}; videoCodecCtx->framerate = {frameRate, 1}; #else videoCodecCtx->time_base = videoStreamIn->codec->time_base; videoCodecCtx->framerate = videoStreamIn->codec->framerate; #endif #if 0 videoCodecCtx->qmin = 10; videoCodecCtx->qmax = 51; videoCodecCtx->me_range = 16; videoCodecCtx->max_qdiff = 4; videoCodecCtx->qcompress = 0.6; #endif //初始化视频编码器参数(如果要文件体积小一些画质差一些可以调整码率) //参数说明 https://blog.csdn.net/qq_40179458/article/details/110449653 videoCodecCtx->bit_rate = FFmpegHelper::getBitRate(videoWidth, videoHeight); videoCodecCtx->width = videoWidth; videoCodecCtx->height = videoHeight; videoCodecCtx->gop_size = 25; videoCodecCtx->max_b_frames = 3; videoCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P; videoCodecCtx->level = 50; videoCodecCtx->profile = FF_PROFILE_H264_MAIN; //加上下面这个才能在文件属性中看到分辨率等信息 https://www.cnblogs.com/lidabo/p/15754031.html if (saveVideoType == SaveVideoType_Mp4) { videoCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; } //加载预设 https://blog.csdn.net/JineD/article/details/125304570 if (videoCodecCtx->codec_id == AV_CODEC_ID_H264) { av_opt_set(videoCodecCtx->priv_data, "preset", "slow", 0); //设置零延迟(本地摄像头视频流保存如果不设置则播放的时候会越来越模糊) av_opt_set(videoCodecCtx->priv_data, "tune", "zerolatency", 0); } else if (videoCodecCtx->codec_id == AV_CODEC_ID_HEVC) { av_opt_set(videoCodecCtx->priv_data, "x265-params", "qp=20", 0); av_opt_set(videoCodecCtx->priv_data, "preset", "ultrafast", 0); av_opt_set(videoCodecCtx->priv_data, "tune", "zero-latency", 0); } //打开视频编码器 int result = avcodec_open2(videoCodecCtx, videoCodec, NULL); if (result < 0) { debug("打开编码", QString("错误: 打开视频编码器失败 %1").arg(FFmpegHelper::getError(result))); return false; } videoPacket = FFmpegHelper::creatPacket(NULL); return true; } bool FFmpegSave::initVideoMp4() { //必须先设置过输入视频流 if (!videoStreamIn || fileName.isEmpty()) { return false; } //有部分视频参数不正确保存不了 http://tv.netxt.cc:1998/live/y.flv if (videoStreamIn->time_base.num == 0) { return false; } QByteArray fileData = fileName.toUtf8(); const char *filename = fileData.data(); //开辟一个格式上下文用来处理视频流输出 int result = avformat_alloc_output_context2(&formatCtx, NULL, "mp4", filename); if (result < 0) { debug("创建格式", QString("错误: %1").arg(FFmpegHelper::getError(result))); return false; } //创建视频流用来输出视频数据到文件 videoStreamOut = avformat_new_stream(formatCtx, NULL); result = FFmpegHelper::copyContext(videoCodecCtx, videoStreamOut, true); if (result < 0) { debug("创建视频", QString("错误: %1").arg(FFmpegHelper::getError(result))); goto end; } //打开输出文件 result = avio_open(&formatCtx->pb, filename, AVIO_FLAG_WRITE); if (result < 0) { debug("打开输出", QString("错误: %1").arg(FFmpegHelper::getError(result))); goto end; } //写入文件开始符 result = avformat_write_header(formatCtx, NULL); if (result < 0) { debug("写入失败", QString("错误: %1").arg(FFmpegHelper::getError(result))); goto end; } return true; end: //关闭释放并清理文件 this->close(); this->deleteFile(fileName); return false; } void FFmpegSave::writePacket(AVPacket *packet) { packetCount++; if (saveVideoType == SaveVideoType_H264) { file.write((char *)packet->data, packet->size); } else if (saveVideoType == SaveVideoType_Mp4) { AVRational timeBaseIn = videoStreamIn->time_base; AVRational timeBaseOut = videoStreamOut->time_base; //没有下面这段判断在遇到不连续的帧的时候就会错位(相当于每次重新计算时间基准保证时间正确) //不连续帧的情况有暂停录制以及切换播放进度导致中间有些帧不需要录制 double fps = frameRate;//av_q2d(videoStreamIn->r_frame_rate); double duration = AV_TIME_BASE / fps; packet->pts = (packetCount * duration) / (av_q2d(timeBaseIn) * AV_TIME_BASE); packet->dts = packet->pts; packet->duration = duration / (av_q2d(timeBaseIn) * AV_TIME_BASE); //重新调整时间基准 packet->pts = av_rescale_q_rnd(packet->pts, timeBaseIn, timeBaseOut, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)); packet->dts = packet->pts; //packet->dts = av_rescale_q_rnd(packet->dts, timeBaseIn, timeBaseOut, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)); packet->duration = av_rescale_q(packet->duration, timeBaseIn, timeBaseOut); packet->pos = -1; //写入一帧数据 int result = av_interleaved_write_frame(formatCtx, packet); if (result < 0) { debug("写入失败", QString("错误: %1").arg(FFmpegHelper::getError(result))); } } av_packet_unref(packet); } ```
|
|