[066]Camera360录像预览滞后

前言

Camera360应用录像预览在我们的设备上存在滞后的问题。
具体现象在你快速摄像头角度的时候,预览画面不能及时更新到当前摄像头拍摄的角度的画面,
或者你拍你自己的手,快速握拳展开,预览画面需要延迟一些时间才能显示展开的手

一、程序员的直觉

线索

一:原生Camera应用没有问题,只有Camera360的应用有问题。
二:降低Camera的输出帧率到15帧左右,可以缓解Camera360的问题

1.1 Camera360应用自身问题

很多工程师相信肯定有这个直觉,而且可能用以下这个句话就可以给领导一个交代

原生Camera应用没有问题,只有Camera360的应用有问题,判断是应用自己的问题。
由于Camera360没有源码,暂时无法进一步分析这个问题。

1.2 Camera360无法及时处理一帧画面

这个是我的第一直觉,虽然没有源码我也很好奇为什么,我希望通过Trace来验证我的直觉。

二、Camera APP架构

虽然我对Camera不是很熟悉,但是利用我掌握的知识,推测出Camera预览应用有两种架构。

2.1 架构A


CameraServer直接输出buffer给Camera APP创建的SurfaceView,然后SurfaceFlinger可以直接显示Camera的buffer。
优点:预览延迟少,因为整个预览帧到显示屏幕的过程,其实是跳过APP。
缺点:无法进一步对预览的效果进行加工处理

2.2架构B


CameraServer直接输出buffer给Camera APP创建的SurfaceTexture,APP再通过OpenGL等手段加工,然后在将加工后的buffer通过SurfaceView交给了SurfaceFlinger,SurfaceFlinger显示Camera的buffer。
优点:应用可以对预览的效果进行加工处理,例如美颜效果。
缺点:一旦加工超时,就会导致预览帧无法及时的显示到屏幕上,导致预览延迟。

进一步的直觉

看完上面的架构描述,加上上面的两个线索,再考考你的直觉,Camera360采用的是架构A还是架构B。
答案呼之欲出,Camera360采用的是架构B,因为架构B的缺点和线索完全吻合,而且大概率是Camera360的加工超时导致的这个问题。

以上所有推测是在我没有看Trace和代码之前完成,我比较喜欢提前推测问题的可能性,因为顺着推测更有利于分析Trace和代码。

三、一帧预览buffer到屏幕显示

接下来我们通过Trace来分析一帧预览buffer到屏幕显示,看看导致这个问题的终极元凶是谁。

3.1 CameraProvider->CameraServer->Camera360


箭头1:CameraProvider(HAL) 回调CallBack,然后通过Binder调用告诉CameraServer一帧已经准备好。
箭头2:CameraServer在3.1.1的Binder Reply阶段通过Binder调用将buffer传递到Camera360的SurfaceTexture。

3.2 Camera360->SurfaceFlinger


第一步:
Camera360的8233线程,acquireBuffer拿到3.1中CameraServer通过SurfaceTexture传递的buffer

第二步:
对buffer加工一下,例如美颜一下,处理一下饱和度,色温什么的。

第三步:
通过queuebuffer到SurfaceView,但是发现queuebuffer的时候被同一个bufferqueue的上一帧的GPU绘制卡主了,卡了接近40ms,直到上一帧完成渲染才完成了queuebuffer。
为什么会卡,我们后面分析,先把流程走完 ?

第四步:
GPU开始工作,完成渲染,消耗了接近60ms。

从第3步到第4步,这一帧完全完成GPU绘制就浪费了100ms以上,这还不算加工时间,还有Camera回调到APP的时间,最后SurfaceView显示到屏幕的时间,真正摄像头旋转到拍摄到第一帧到显示到屏幕就远远大于100ms。

四、新增知识点

通过这个Trace我发现两个之前没有掌握的知识点,请看下图。


4.1 Trace分析

知识点1

queuebuffer没有完成,SurfaceView的buffer数量就会增加1,但是实际上这一个buffer对于SurfaceFlinger是不可用。

知识点2

queuebuffer的过程会因为同一个bufferqueue的上一帧GPU绘制未完成而block。

4.2 源码分析

知识点1和2分别对应下列代码中注释的那行代码。
frameworks/native/libs/gui/BufferQueueProducer.cpp

status_t BufferQueueProducer::queueBuffer(int slot,
        const QueueBufferInput &input, QueueBufferOutput *output) {
    ATRACE_CALL();
    ...
    BufferItem item;
    { // Autolock scope
    ...
        output->width = mCore->mDefaultWidth;
        output->height = mCore->mDefaultHeight;
        output->transformHint = mCore->mTransformHintInUse = mCore->mTransformHint;
        output->numPendingBuffers = static_cast<uint32_t>(mCore->mQueue.size());
        output->nextFrameNumber = mCore->mFrameCounter + 1;

        ATRACE_INT(mCore->mConsumerName.string(),
                static_cast<int32_t>(mCore->mQueue.size()));//知识点1
        ...
    } // Autolock scope

    // Wait without lock held
    if (connectedApi == NATIVE_WINDOW_API_EGL) {
        // Waiting here allows for two full buffers to be queued but not a
        // third. In the event that frames take varying time, this makes a
        // small trade-off in favor of latency rather than throughput.
        lastQueuedFence->waitForever("Throttling EGL Production");//知识点2
    }

    return NO_ERROR;
}

4.3 小知识

以后看Trace的之后注意,就算buffer size+1了,不代表这帧准备好了,因为可能会等上一帧的GPU渲染完成。就算queuebuffer的方法执行完了,也不代表这帧准备好了,需要等到这帧的GPU渲染完成。
这里都出现了一个相同的词,等待GPU渲染完成,GPU渲染完成就是通过Fence机制通知的。Fence机制,这里暂时不展开说了。

五、不对劲的点

问题还远没有结束,Camera360预览滞后的真实体验远大于我前面分析的100ms极限,滞后的感觉至少有500ms以上,继续跟踪。

六、显示的那一帧是什么时候拍的?

回过头来看这个架构图,大家会发现,其实我只是分析到了从CameraServer的buffer到SurfaceFlinger显示的流程,但是我没有跟踪这个buffer创建的时间点。
也就是从CameraServer将buffer传给CameraHal之后,CameraHal回传给CameraServer的时间


七、跟踪一帧拍摄

我随便选中一个 SurfaceTexture-0-11745-1: 9 跟踪一下。

7.1 从buffer传递给HAL拍摄到HAL回调CameraServer

不看不知道,一看吓一跳,没想到时间间隔竟然有600ms


7.2 buffer传递给HAL拍摄

这一步可以理解为摄像头转向某个角度时候拍摄到的画面。


7.3 HAL回调CameraServer

这一步可以理解为7.2中拍摄的buffer回传给应用用于显示


7.4 小结

没想到一帧拍摄buffer到回调竟然需要600多毫秒,加上显示需要消耗的100多ms,真实当摄像头转到某个角度,这个角度拍的照片到显示到屏幕上保守估计就需要700ms。
这才是真正导致Camera360录像预览滞后的原因

八、为什么每次滞后8帧

发现问题稳定后,看下面的trace从buffer给hal,再到hal回传buffer,中间正好有8个prepareHalRequests


8.1 SurfaceTexture的buffer size

通过搜索SurfaceTexture-0-11745-1: 0~SurfaceTexture-0-11745-1: 9发现SurfaceTexture的数量是10,我一开始是以为SurfaceTexture的上限,导致了buffer的堆积,但是我搜了一下源码SurfaceTexture的buffer的上限是64,而且10和8也没有太大关系,排除了这个可能性。

8.2 MAX_INFLIGHT_REQUESTS

cameraserver中MAX_INFLIGHT_REQUESTS是8,正好和trace吻合。

//我们平台上是8,不同的平台可能会设置不同的数值
#define MAX_INFLIGHT_REQUESTS  8

看到prepareHalRequests的过程中调用getBuffer的时候会去判断requests的数量是不是等于8,如果等于8的话,就会用mOutputBufferReturnedSignal休眠,等释放一个requests,就会唤醒。

frameworks/av/services/camera/libcameraservice/device3/Camera3Device.cpp


status_t Camera3Device::RequestThread::prepareHalRequests() {
    ...
    res = outputStream->getBuffer(&outputBuffers->editItemAt(j),
                        waitDuration,
                        captureRequest->mOutputSurfaces[streamId]);//跳转到下面代码
    ...
}

frameworks/av/services/camera/libcameraservice/device3/Camera3Stream.cpp

status_t Camera3Stream::getBuffer(camera3_stream_buffer *buffer,
        nsecs_t waitBufferTimeout,
        const std::vector<size_t>& surface_ids) {
        ...
    // Wait for new buffer returned back if we are running into the limit.
    if (getHandoutOutputBufferCountLocked() == camera3_stream::max_buffers) {//判断是不是等于max,也就是8
        ALOGV("%s: Already dequeued max output buffers (%d), wait for next returned one.",
                        __FUNCTION__, camera3_stream::max_buffers);
        nsecs_t waitStart = systemTime(SYSTEM_TIME_MONOTONIC);
        if (waitBufferTimeout < kWaitForBufferDuration) {
            waitBufferTimeout = kWaitForBufferDuration;
        }
        res = mOutputBufferReturnedSignal.waitRelative(mLock, waitBufferTimeout);//休眠等待唤醒。
        nsecs_t waitEnd = systemTime(SYSTEM_TIME_MONOTONIC);
        mBufferLimitLatency.add(waitStart, waitEnd);
        if (res != OK) {
            if (res == TIMED_OUT) {
                ALOGE("%s: wait for output buffer return timed out after %lldms (max_buffers %d)",
                        __FUNCTION__, waitBufferTimeout / 1000000LL,
                        camera3_stream::max_buffers);
            }
            return res;
        }
    }
}

status_t Camera3Stream::returnBuffer(const camera3_stream_buffer &buffer,
        nsecs_t timestamp, bool timestampIncreasing,
         const std::vector<size_t>& surface_ids, uint64_t frameNumber) {
    ...
    // Even if returning the buffer failed, we still want to signal whoever is waiting for the
    // buffer to be returned.
    mOutputBufferReturnedSignal.signal();//唤醒getbuffer中的休眠

    return res;
}

也就是说就算SurfaceTexture的buffer再多,必须等一个request空闲出来,才能继续向cameraprovider发起请求,这一切有就都说的通了。

九、总结

用一个比喻来总结整个问题的过程

你:代表Camera360 App
一叠空杯:代表一个SurfaceTexture,一个杯子代表一个buffer
一个服务员:代表CameraServer
一个饮料机:代表CameraProvider
8个餐盘:一个餐盘代表CameraServer的一个request,为什么是8个,因为代码设置是8个,这个数值可以改成。

服务员的生产者流程,首先看有没有空餐盘,如果有空餐盘就向你要一个空杯子,然后拿着餐盘和空杯子去倒饮料,也就是代表我们正常的向camera hal请求一帧画面,杯子饮料倒满了,就放到桌子上,服务员每秒最多可以倒30杯饮料,代表手机最大的预览输出帧率是30帧,没有空餐盘就休息。

你的消费流程,服务员向你要空杯子,你就给他空杯子,然后你只能挑台面上时间最早的饮料,然后先把空餐盘还给服务员,然后拿起杯子,喝饮料,喝饮料也就代表app把一帧画面显示倒屏幕上,喝完以后空杯子留着,继续用

假设你喝饮料的速度大于等于每秒30杯,这样子整个环节服务员不需要用到所有8个餐盘,最多也就用2个餐盘,你呢最多也就用2个空杯,你和服务员就可以很顺畅的流水协作起来,甚至你会等服务员出饮料,你喝的饮料永远都是新鲜的

但是假如你喝饮料的速度小于每秒30杯,这样子慢慢的桌子上就会堆积饮料,直到8个餐盘和8个杯子都倒满饮料,而且你还喝着第9个杯子的饮料,服务员一看没有空盘子了,就开始休息了,当你喝完第9个杯子的饮料,你拿起最早的那个第一个餐盘,把餐盘给服务员,开始喝第1个杯子的饮料,服务员一看有餐盘了就问你要了你刚喝完的第9个杯子,然后去倒饮料了,倒完又得等你去喝第2个杯子时候,归还的餐盘,周而复始,你只能喝最老的那杯饮料了,你中间永远隔着7杯饮料

最坑爹的就是这个服务员喜欢的工作流程就是一开始的时候不断向你要空杯子,非得等到他的餐盘用完了,才能允许你开始喝第一杯,所以假如你喝的比服务员出饮料速度慢,也就是慢于每秒30杯,你就永远只能喝不新鲜的饮料,而且这个不新鲜的时长由你喝的速度决定,你永远只能喝8乘n毫秒之前打的饮料,n代表你喝一杯饮料的时间。

总结假设
camera app处理一帧的时间是t毫秒
camera hal提供了i个request
camera hal的出帧频率是每秒n帧
如果1000/t<n,最后app会达到的一个预览延迟的时间T约等于(i-1)t+t,也就是it,为什么要加t,因为一帧图像显示到屏幕上也需要t的时间

解决延迟的办法有三个方向
减小t,治本
减少i ,治标不治本,仅仅是减少延迟的时间
减小n,牺牲了录像的出帧的帧率

尾巴

其实Trace只是一个辅助工具,展现的是一段时间内代码的调用的流程,要学会看trace,首先你要了解Android系统,当你了解Android系统中跨进程,跨线程的机制,UI绘制机制,Input事件机制,你看Trace才能在各种线程进程之前自由的穿梭。

不是你没有掌握看Trace的技巧,而是你还没有彻底了解Android系统。

当然当你彻底了解Android系统之后,如何看Trace,还是需要掌握一些技巧的,推荐以下教程:
https://www.androidperformance.com/2020/02/14/Android-Systrace-SurfaceFlinger/

通过分析这个问题,我对bufferqueue的生产者消费者模型,还有fence机制有了更加深入的理解,给大家推荐一些这方面写的比较好的博客。
https://www.jianshu.com/p/dca7c4d9495c
http://tangzm.com/blog/?p=167
https://blog.csdn.net/w401229755/article/details/39228535
https://www.cnblogs.com/brucemengbm/p/6881925.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容