从零开始学习音视频编程技术(二十一) 录屏软件开发之最终完善

原文地址:http://blog.yundiantech.com/?log=blog&id=28

上面我们已经生成了录屏的视频,然而这个视频并不是理想中的那样,随时时间的增加,音视频会越来越不同步。

原因就是因为保存视频的方式采用的是固定帧率的方式,既时间戳间隔也是固定的。

举个栗子:假如视频的帧率是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张图片,重复一下即可。


说白了就是多的丢掉,少了重复一下上一张。

这种方式虽然不是非常完美,但是勉强可以了。



代码实现:

因此我们在采集图像的时候,就需要记录下时间,然后再保存视频的时候,才能根据时间来判断这张图像要不要,代码如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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);
     }
}


上面采集的线程将图像放到了一个队列中,保存视频的线程只需要从队列中取出图像,并根据时间判断怎么处理即可,代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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

如下图紫色圈圈所示:

裁剪掉 上、下、左、右都是类似的原理,请自行推理。


理论知识掌握了之后,剩下的就是写代码实现了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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编码的图像数据,宽高也必须是偶数。

而我们手动选择录屏区域的时候还是会选到奇数位置的,因此选择区域完毕后需要手动处理一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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


音视频技术交流讨论欢迎加 QQ群 121376426  


原文地址:http://blog.yundiantech.com/?log=blog&id=28


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值