上面我们已经生成了录屏的视频,然而这个视频并不是理想中的那样,随时时间的增加,音视频会越来越不同步。
原因就是因为保存视频的方式采用的是固定帧率的方式,既时间戳间隔也是固定的。
举个栗子:假如视频的帧率是10,就是每秒钟10张图像,那么这十张图像是平均分布的,位置分别是:0.1s、0.2s...0.9s、1s。
然而我们每秒钟采集到的屏幕图像是不固定的,这一秒15张,下一秒有可能只有8张。
当我们用这23张图片用上面的方式去合成视频,产生的视频时长就是2.3秒,而实际明明是2秒。
这样时间一久和音频的差距就出来了。
更重要的是,第一秒获取到的15张图像中的第10~15张会被放到视频的第2秒中去,这样也是有很大的问题。
所以在采集到图像之后需要处理下再保存到视频,处理的方法其实也很简单:
1.第一秒的15张图像 多出了5张 只需要筛选出5张 丢掉即可。
2.第二秒的8张不足2张,只需要找2张图片,重复一下即可。
说白了就是多的丢掉,少了重复一下上一张。
这种方式虽然不是非常完美,但是勉强可以了。
代码实现:
因此我们在采集图像的时候,就需要记录下时间,然后再保存视频的时候,才能根据时间来判断这张图像要不要,代码如下:
long time = 0; if (m_saveVideoFileThread) { if (m_getFirst) { qint64 secondTime = QDateTime::currentMSecsSinceEpoch(); time = secondTime - firstTime + timeIndex;//计算相对时间 } else { firstTime = QDateTime::currentMSecsSinceEpoch(); timeIndex = m_saveVideoFileThread->getVideoPts()*1000; m_getFirst = true; } } ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_frame, packet); if(ret < 0) { printf("video Decode Error.(解码错误) "); return; } if(got_frame && pCodecCtx) { sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize); if (m_saveVideoFileThread) { uint8_t * picture_buf = (uint8_t *)av_malloc(size); memcpy(picture_buf,pFrameYUV->data[0],y_size); memcpy(picture_buf+y_size,pFrameYUV->data[1],y_size/4); memcpy(picture_buf+y_size+y_size/4,pFrameYUV->data[2],y_size/4); uint8_t * yuv_buf = (uint8_t *)av_malloc(size); ///将YUV图像裁剪成目标大小 Yuv420Cut(pic_x,pic_y,pic_w,pic_h,pCodecCtx->width,pCodecCtx->height,picture_buf,yuv_buf); m_saveVideoFileThread->videoDataQuene_Input(yuv_buf,yuvSize*3/2,time); av_free(picture_buf); } }
上面采集的线程将图像放到了一个队列中,保存视频的线程只需要从队列中取出图像,并根据时间判断怎么处理即可,代码大致如下:
BufferDataNode *SaveVideoFileThread::videoDataQuene_get(double time) { BufferDataNode * node = NULL; SDL_LockMutex(videoMutex); if (videoDataQueneHead != NULL) { node = videoDataQueneHead; if (time >= node->time) { while(node != NULL) { if (node->next == NULL) { if (isStop) { break; } else { //队列里面才一帧数据 先不处理 SDL_UnlockMutex(videoMutex); return NULL; } } if (time < node->next->time) { break; } BufferDataNode * tmp = node; node = node->next; videoDataQueneHead = node; videoBufferCount--; av_free(tmp->buffer); free(tmp); } } else { node = lastVideoNode; } if (videoDataQueneTail == node) { videoDataQueneTail = NULL; } if (node != NULL && node != lastVideoNode) { videoDataQueneHead = node->next; videoBufferCount--; } } SDL_UnlockMutex(videoMutex); return node; }
这样便可解决不同步的问题了。。
既然是录屏软件,那么当然需要录制屏幕局部区域的功能了。
可以再采集的时候设置参数让他直接获取局部区域,然后没找到方法,也不想研究了。
还可以将采集到的YUV420图像直接裁剪出需要的部分,果断用这个方法了,可以学习新技术,又可以装逼,何乐而不为呢。
那就开始执行YUV420P图像的裁剪吧:
首先先来看顾下YUV420P图像格式:
YUV420p数据格式图
在YUV420中,一个像素点对应一个Y,一个2X2的小方块对应一个U和V。
以下理论是本人自己总结的,没有找到相关文档,不确定准确性,但已经过实测了。
YUV420P的一个U分量是对应4个Y分量的,同样一个V分量也是对应4个Y分量。
所以裁剪的Y分量 必须得是4的倍数,也就是说想要裁掉图中的Y1,那么就需要连带Y2、Y9、Y10也一并裁剪了。这就意味着裁剪掉的部分必须是偶数的大小,不能是奇数,比如想把图像的左边裁掉1个像素是不允许的,只能裁剪2个像素。
现在就以裁掉图像的左边2个像素为例,则对应上图中就是,
去掉Y1 Y2 Y9 Y10 Y17 Y18 Y25 Y26和U1 U5 V1 V5
如下图紫色圈圈所示:
裁剪掉 上、下、左、右都是类似的原理,请自行推理。
理论知识掌握了之后,剩下的就是写代码实现了:
void Yuv420Cut(int x,int y,int desW,int desH,int srcW,int srcH,uint8_t *srcBuffer,uint8_t *desBuffer) { int tmpRange; int bufferIndex; int yIndex = 0; bufferIndex = 0 + x + y*srcW; tmpRange = srcW * desH; for (int i=0;i<tmpRange;) //逐行拷贝Y分量数据 { memcpy(desBuffer+yIndex,srcBuffer+bufferIndex+i,desW); i += srcW; yIndex += desW; } int uIndex = desW * desH; int uIndexStep = srcW/2; int uWidthCopy = desW/2; bufferIndex = srcW * srcH+x/2 + y /2 *srcW / 2; tmpRange = srcW * desH / 4; for (int i=0;i<tmpRange;) //逐行拷贝U分量数据 { memcpy(desBuffer+uIndex,srcBuffer+bufferIndex+i,uWidthCopy); i += uIndexStep; uIndex += uWidthCopy; } int vIndex = desW * desH + desW * desH /4; int vIndexStep = srcW/2; int vWidthCopy = desW/2; bufferIndex = srcW*srcH + srcW*srcH/4 + x/2 + y /2 *srcW / 2; tmpRange = srcW * desH / 4; for (int i=0;i<tmpRange;) //逐行拷贝V分量数据 { memcpy(desBuffer+vIndex,srcBuffer+bufferIndex+i,vWidthCopy); i += vIndexStep; vIndex += vWidthCopy; } }
本例子中,我们加入了一个选择屏幕录屏区域的控件。
刚刚说了,裁剪的部分必须是偶数,同时传给ffmpeg编码的图像数据,宽高也必须是偶数。
而我们手动选择录屏区域的时候还是会选到奇数位置的,因此选择区域完毕后需要手动处理一下。
void MainWindow::slotSelectRectFinished(QRect re) { /// 1.传给ffmpeg编码的图像宽高必须是偶数。 /// 2.图像裁剪的起始位置和结束位置也必须是偶数 /// 而手动选择的区域很有可能会是奇数,因此需要处理一下 给他弄成偶数 /// 处理的方法很简答:其实就是往前或者往后移一个像素 /// 一个像素的大小肉眼基本也看不出来啥区别。 int x = re.x(); int y = re.y(); int w = re.width(); int h = re.height(); if (x % 2 != 0) { x--; w++; } if (y % 2 != 0) { y--; h++; } if (w % 2 != 0) { w++; } if (h % 2 != 0) { h++; } rect = QRect(x,y,w,h); QString str = QString("==当前区域== 起点(%1,%2) 大小(%3 x %4)") .arg(rect.left()).arg(rect.left()).arg(rect.width()).arg(rect.height()); ui->showRectInfoLabel->setText(str); ui->startButton->setEnabled(true); ui->editRectButton->setEnabled(true); ui->hideRectButton->setEnabled(true); ui->hideRectButton->setText("隐藏"); saveFile(); }
到这录屏软件已经很完美了。
别的部分就不解释了,自行下载代码查看吧。
完整工程下载地址:http://download.csdn.net/detail/qq214517703/9827493
学习音视频技术欢迎访问 http://blog.yundiantech.com
音视频技术交流讨论欢迎加 QQ群 121376426