這篇文章主要分下面幾點(diǎn)來展開講解:
1)Android 最新Camera 整體框架;
2)Android Camera2 和HAL3 的基本了解凌箕;
3)Camera2 介紹;
(本文所寫的內(nèi)容基于Android 9.0)
一粪牲、Android最新Camera 整體框架
Android Camera整體框架主要包括三個(gè)進(jìn)程:app進(jìn)程玖雁、camera server進(jìn)程组贺、hal進(jìn)程(provider進(jìn)程)灶伊。進(jìn)程之間的通信都是通過binder實(shí)現(xiàn)援制,其中app和camera server通信使用 AIDL(Android Interface Definition Language) 赠摇,camera server和hal(provider進(jìn)程)通信使用HIDL(HAL interface definition language) 超燃。
Android上面的框架分級(jí)区拳,基本都是類似的,應(yīng)用層-> framework層->Hal層意乓,我們ps看下設(shè)備上實(shí)際的進(jìn)程情況樱调,如下圖所示,可以看到有cameraserver和provider進(jìn)程届良。cameraservice是負(fù)責(zé)app和framework層的通信笆凌,而provider進(jìn)程則是負(fù)責(zé)framework和hal層之間的通信。
(附:Android 8.0 重新設(shè)計(jì)了 Android 操作系統(tǒng)框架(在一個(gè)名為“Treble”的項(xiàng)目中)伙窃,以便讓制造商能夠以更低的成本更輕松菩颖、更快速地將設(shè)備更新到新版 Android 系統(tǒng)。
Android O之后使用Treble的架構(gòu)为障,為了解決Android系統(tǒng)的碎片化問題和提高系統(tǒng)更新的效率晦闰,減少了framework 和HAL 的耦合性放祟,進(jìn)而引出了HIDL 的概念。
HIDL 全稱為HAL interface definition language(發(fā)音為“hide-l”)是用于指定 HAL 和其用戶之間的接口的一種接口描述語言 (IDL)呻右。
HIDL 的目標(biāo)是跪妥,框架可以在無需重新構(gòu)建 HAL 的情況下進(jìn)行替換。HAL 將由供應(yīng)商或 SOC 制造商構(gòu)建声滥,放置在設(shè)備的 /vendor 分區(qū)中眉撵,這樣一來,框架就可以在其自己的分區(qū)中通過 OTA 進(jìn)行替換落塑,而無需重新編譯 HAL纽疟,這也是Project Treble框架設(shè)計(jì)而誕生的。)
如下圖所示憾赁,展示了Android Camera的最新框架污朽,我們先大概看下圖片流程,對(duì)整體框架有個(gè)基本了解龙考。
二蟆肆、Android Camera2 和HAL3 的基本了解
1) Camera2 接口什么時(shí)候開始引入的?
從Android 5.0開始晦款,Google 引入了一套全新的相機(jī)框架 Camera2(android.hardware.camera2)并且廢棄了舊的相機(jī)框架 Camera1(android.hardware.Camera)
不了解的同學(xué)炎功,可能會(huì)有疑問,為啥要廢棄Camera1接口缓溅?基本原因是蛇损,camera1接口過于簡單,沒法滿足更加復(fù)雜的相機(jī)應(yīng)用場景肛宋。為了給應(yīng)用層提供更多的相機(jī)控制權(quán)限州藕,從而構(gòu)建出更高質(zhì)量的相機(jī)應(yīng)用程序,Google才推出了Camera2 接口酝陈。下面可以看下和Camera1比較床玻,Camera2有哪些高級(jí)特性。
2)一些只有 Camera2 才支持的高級(jí)特性
-
在開啟相機(jī)之前檢查相機(jī)信息
出于某些原因沉帮,你可能需要先檢查相機(jī)信息再?zèng)Q定是否開啟相機(jī)锈死,例如檢查閃光燈是否可用。在 Caemra1 上穆壕,你無法在開機(jī)相機(jī)之前檢查詳細(xì)的相機(jī)信息待牵,因?yàn)檫@些信息都是通過一個(gè)已經(jīng)開啟的相機(jī)實(shí)例提供的。在 Camera2 上喇勋,我們有了和相機(jī)實(shí)例完全剝離的 CameraCharacteristics 實(shí)例專門提供相機(jī)信息缨该,所以我們可以在不開啟相機(jī)的前提下檢查幾乎所有的相機(jī)信息。 -
在不開啟預(yù)覽的情況下拍照
在 Camera1 上川背,開啟預(yù)覽是一個(gè)很重要的環(huán)節(jié)贰拿,因?yàn)橹挥性陂_啟預(yù)覽之后才能進(jìn)行拍照蛤袒,因此即使顯示預(yù)覽畫面與實(shí)際業(yè)務(wù)需求相違背的時(shí)候,你也不得不開啟預(yù)覽膨更。而 Camera2 則不強(qiáng)制要求你必須先開啟預(yù)覽才能拍照妙真。 -
一次拍攝多張不同格式和尺寸的圖片
在 Camera1 上,一次只能拍攝一張圖片荚守,更不同談多張不同格式和尺寸的圖片了珍德。而 Camera2 則支持一次拍攝多張圖片,甚至是多張格式和尺寸都不同的圖片矗漾。例如你可以同時(shí)拍攝一張 1440x1080 的 JPEG 圖片和一張全尺寸的 RAW 圖片锈候。 -
控制曝光時(shí)間
在暗環(huán)境下拍照的時(shí)候,如果能夠適當(dāng)延長曝光時(shí)間缩功,就可以讓圖像畫面的亮度得到提高晴及。在 Camera2 上,你可以在規(guī)定的曝光時(shí)長范圍內(nèi)配置拍照的曝光時(shí)間嫡锌,從而實(shí)現(xiàn)拍攝長曝光圖片,你甚至可以延長每一幀預(yù)覽畫面的曝光時(shí)間讓整個(gè)預(yù)覽畫面在暗環(huán)境下也能保證一定的亮度琳钉。而在 Camera1 上你只能 YY 一下势木。 -
連拍
連拍 30 張圖片這樣的功能在 Camera2 出現(xiàn)之前恐怕只有系統(tǒng)相機(jī)才能做到了(通過 OpenGL 截取預(yù)覽畫面的做法除外),也可能是出于這個(gè)原因歌懒,市面上的第三方相機(jī)無一例外都不支持連拍啦桌。有了 Camera2,你完全可以讓你的相機(jī)應(yīng)用程序支持連拍功能及皂,甚至是連續(xù)拍 30 張使用不同曝光時(shí)間的圖片甫男。 -
靈活的 3A 控制
3A(AF、AE验烧、AWB)的控制在 Camera2 上得到了最大化的放權(quán)板驳,應(yīng)用層可以根據(jù)業(yè)務(wù)需求靈活配置 3A 流程并且實(shí)時(shí)獲取 3A 狀態(tài),而 Camera1 在 3A 的控制和監(jiān)控方面提供的接口則要少了很多碍拆。例如你可以在拍照前進(jìn)行 AE 操作若治,并且監(jiān)聽本這次拍照是否點(diǎn)亮閃光燈。
3)何為HAL3感混?
為了配合Camera2 的使用端幼,Android Hal層Camera框架也做了相對(duì)應(yīng)的改動(dòng),也就是HAL3弧满。Camera1接口對(duì)應(yīng)的是調(diào)用的HAL1框架婆跑。
4)一些概念
關(guān)于Camera2 和Hal3,有些基本概念我們得了解下~~
我們先來看下Camera2 API涉及到哪些類庭呜,下面會(huì)對(duì)各個(gè)類的使用進(jìn)行講解~~
1) Pipeline
Camera2 的 API 模型被設(shè)計(jì)成一個(gè) Pipeline(管道)滑进,它按順序處理每一幀的請(qǐng)求并返回請(qǐng)求結(jié)果給客戶端犀忱。下面這張來自官方的圖展示了 Pipeline 的工作流程,我們會(huì)通過一個(gè)簡單的例子詳細(xì)解釋這張圖郊供。
為了解釋上面的示意圖峡碉,假設(shè)我們想要同時(shí)拍攝兩張不同尺寸的圖片,并且在拍攝的過程中閃光燈必須亮起來驮审。整個(gè)拍攝流程如下:
- 創(chuàng)建一個(gè)用于從 Pipeline 獲取圖片的 CaptureRequest鲫寄。
- 修改 CaptureRequest 的閃光燈配置,讓閃光燈在拍照過程中亮起來疯淫。
- 創(chuàng)建兩個(gè)不同尺寸的 Surface 用于接收?qǐng)D片數(shù)據(jù)地来,并且將它們添加到 CaptureRequest 中。
- 發(fā)送配置好的 CaptureRequest 到 Pipeline 中等待它返回拍照結(jié)果熙掺。
一個(gè)新的 CaptureRequest 會(huì)被放入一個(gè)被稱作 Pending Request Queue 的隊(duì)列中等待被執(zhí)行未斑,當(dāng) In-Flight Capture Queue 隊(duì)列空閑的時(shí)候就會(huì)從 Pending Request Queue 獲取若干個(gè)待處理的 CaptureRequest,并且根據(jù)每一個(gè) CaptureRequest 的配置進(jìn)行 Capture 操作币绩。最后我們從不同尺寸的 Surface 中獲取圖片數(shù)據(jù)并且還會(huì)得到一個(gè)包含了很多與本次拍照相關(guān)的信息的 CaptureResult蜡秽,流程結(jié)束。
2 )Supported Hardware Level
相機(jī)功能的強(qiáng)大與否和硬件息息相關(guān)缆镣,不同廠商對(duì) Camera2 的支持程度也不同芽突,所以 Camera2 定義了一個(gè)叫做 Supported Hardware Level 的重要概念,其作用是將不同設(shè)備上的 Camera2 根據(jù)功能的支持情況劃分成多個(gè)不同級(jí)別以便開發(fā)者能夠大概了解當(dāng)前設(shè)備上 Camera2 的支持情況董瞻。截止到 Android P 為止寞蚌,從低到高一共有 LEGACY、LIMITED钠糊、FULL 和 LEVEL_3 四個(gè)級(jí)別:
- LEGACY:向后兼容的級(jí)別挟秤,處于該級(jí)別的設(shè)備意味著它只支持 Camera1 的功能,不具備任何 Camera2 高級(jí)特性抄伍。
- LIMITED:除了支持 Camera1 的基礎(chǔ)功能之外艘刚,還支持部分 Camera2 高級(jí)特性的級(jí)別。
- FULL:支持所有 Camera2 的高級(jí)特性逝慧。
- LEVEL_3:新增更多 Camera2 高級(jí)特性昔脯,例如 YUV 數(shù)據(jù)的后處理等。
3 )Capture
相機(jī)的所有操作和參數(shù)配置最終都是服務(wù)于圖像捕獲笛臣,例如對(duì)焦是為了讓某一個(gè)區(qū)域的圖像更加清晰云稚,調(diào)節(jié)曝光補(bǔ)償是為了調(diào)節(jié)圖像的亮度。因此沈堡,在 Camera2 里面所有的相機(jī)操作和參數(shù)配置都被抽象成 Capture(捕獲)静陈,所以不要簡單的把 Capture 直接理解成是拍照,因?yàn)?Capture 操作可能僅僅是為了讓預(yù)覽畫面更清晰而進(jìn)行對(duì)焦而已。如果你熟悉 Camera1鲸拥,那你可能會(huì)問 setFlashMode()
在哪拐格?setFocusMode()
在哪?takePicture()
在哪刑赶?告訴你捏浊,它們都是通過 Capture 來實(shí)現(xiàn)的。
Capture 從執(zhí)行方式上又被細(xì)分為【單次模式】撞叨、【多次模式】和【重復(fù)模式】三種金踪,我們來一一解釋下:
- 單次模式(One-shot):指的是只執(zhí)行一次的 Capture 操作,例如設(shè)置閃光燈模式牵敷、對(duì)焦模式和拍一張照片等胡岔。多個(gè)一次性模式的 Capture 會(huì)進(jìn)入隊(duì)列按順序執(zhí)行。
- 多次模式(Burst):指的是連續(xù)多次執(zhí)行指定的 Capture 操作枷餐,該模式和多次執(zhí)行單次模式的最大區(qū)別是連續(xù)多次 Capture 期間不允許插入其他任何 Capture 操作靶瘸,例如連續(xù)拍攝 100 張照片,在拍攝這 100 張照片期間任何新的 Capture 請(qǐng)求都會(huì)排隊(duì)等待毛肋,直到拍完 100 張照片怨咪。多組多次模式的 Capture 會(huì)進(jìn)入隊(duì)列按順序執(zhí)行。
- 重復(fù)模式(Repeating):指的是不斷重復(fù)執(zhí)行指定的 Capture 操作润匙,當(dāng)有其他模式的 Capture 提交時(shí)會(huì)暫停該模式惊暴,轉(zhuǎn)而執(zhí)行其他被模式的 Capture,當(dāng)其他模式的 Capture 執(zhí)行完畢后又會(huì)自動(dòng)恢復(fù)繼續(xù)執(zhí)行該模式的 Capture趁桃,例如顯示預(yù)覽畫面就是不斷 Capture 獲取每一幀畫面。該模式的 Capture 是全局唯一的肄鸽,也就是新提交的重復(fù)模式 Capture 會(huì)覆蓋舊的重復(fù)模式 Capture卫病。
我們舉個(gè)例子來進(jìn)一步說明上面三種模式,假設(shè)我們的相機(jī)應(yīng)用程序開啟了預(yù)覽典徘,所以會(huì)提交一個(gè)重復(fù)模式的 Capture 用于不斷獲取預(yù)覽畫面蟀苛,然后我們提交一個(gè)單次模式的 Capture,接著我們又提交了一組連續(xù)三次的多次模式的 Capture逮诲,這些不同模式的 Capture 會(huì)按照下圖所示被執(zhí)行:
下面是幾個(gè)重要的注意事項(xiàng):
- 無論 Capture 以何種模式被提交帜平,它們都是按順序串行執(zhí)行的,不存在并行執(zhí)行的情況梅鹦。
- 重復(fù)模式是一個(gè)比較特殊的模式裆甩,因?yàn)樗鼤?huì)保留我們提交的 CaptureRequest 對(duì)象用于不斷重復(fù)執(zhí)行 Capture 操作,所以大多數(shù)情況下重復(fù)模式的 CaptureRequest 和其他模式的 CaptureRequest 是獨(dú)立的齐唆,這就會(huì)導(dǎo)致重復(fù)模式的參數(shù)和其他模式的參數(shù)會(huì)有一定的差異嗤栓,例如重復(fù)模式不會(huì)配置
CaptureRequest.AF_TRIGGER_START
,因?yàn)檫@會(huì)導(dǎo)致相機(jī)不斷觸發(fā)對(duì)焦的操作。 - 如果某一次的 Capture 沒有配置預(yù)覽的 Surface茉帅,例如拍照的時(shí)候叨叙,就會(huì)導(dǎo)致本次 Capture 不會(huì)將畫面輸出到預(yù)覽的 Surface 上,進(jìn)而導(dǎo)致預(yù)覽畫面卡頓的情況堪澎,所以大部分情況下我們都會(huì)將預(yù)覽的 Surface 添加到所有的 CaptureRequest 里擂错。
4) CameraManager
CameraManager 是一個(gè)負(fù)責(zé)查詢和建立相機(jī)連接的系統(tǒng)服務(wù),它的功能不多樱蛤,這里列出幾個(gè) CameraManager 的關(guān)鍵功能:
- 將相機(jī)信息封裝到 CameraCharacteristics 中钮呀,并提獲取 CameraCharacteristics 實(shí)例的方式。
- 根據(jù)指定的相機(jī) ID 連接相機(jī)設(shè)備刹悴。
- 提供將閃光燈設(shè)置成手電筒模式的快捷方式行楞。
5 )CameraCharacteristics
CameraCharacteristics 是一個(gè)只讀的相機(jī)信息提供者,其內(nèi)部攜帶大量的相機(jī)信息土匀,包括代表相機(jī)朝向的 LENS_FACING
子房;判斷閃光燈是否可用的 FLASH_INFO_AVAILABLE
;獲取所有可用 AE 模式的 CONTROL_AE_AVAILABLE_MODES
等等就轧。如果你對(duì) Camera1 比較熟悉证杭,那么 CameraCharacteristics 有點(diǎn)像 Camera1 的 Camera.CameraInfo
或者 Camera.Parameters
。
6 ) CameraDevice
CameraDevice 代表當(dāng)前連接的相機(jī)設(shè)備妒御,它的職責(zé)有以下四個(gè):
- 根據(jù)指定的參數(shù)創(chuàng)建 CameraCaptureSession解愤。
- 根據(jù)指定的模板創(chuàng)建 CaptureRequest。
- 關(guān)閉相機(jī)設(shè)備乎莉。
- 監(jiān)聽相機(jī)設(shè)備的狀態(tài)送讲,例如斷開連接、開啟成功和開啟失敗等惋啃。
熟悉 Camera1 的人可能會(huì)說 CameraDevice 就是 Camera1 的 Camera 類哼鬓,實(shí)則不是,Camera 類幾乎負(fù)責(zé)了所有相機(jī)的操作边灭,而 CameraDevice 的功能則十分的單一异希,就是只負(fù)責(zé)建立相機(jī)連接的事務(wù),而更加細(xì)化的相機(jī)操作則交給了稍后會(huì)介紹的 CameraCaptureSession绒瘦。
7) Surface
Surface 是一塊用于填充圖像數(shù)據(jù)的內(nèi)存空間称簿,例如你可以使用 SurfaceView 的 Surface 接收每一幀預(yù)覽數(shù)據(jù)用于顯示預(yù)覽畫面,也可以使用 ImageReader 的 Surface 接收 JPEG 或 YUV 數(shù)據(jù)惰帽。每一個(gè) Surface 都可以有自己的尺寸和數(shù)據(jù)格式憨降,你可以從 CameraCharacteristics 獲取某一個(gè)數(shù)據(jù)格式支持的尺寸列表。
8) CameraCaptureSession
CameraCaptureSession 實(shí)際上就是配置了目標(biāo) Surface 的 Pipeline 實(shí)例善茎,我們?cè)谑褂孟鄼C(jī)功能之前必須先創(chuàng)建 CameraCaptureSession 實(shí)例券册。一個(gè) CameraDevice 一次只能開啟一個(gè) CameraCaptureSession,絕大部分的相機(jī)操作都是通過向 CameraCaptureSession 提交一個(gè) Capture 請(qǐng)求實(shí)現(xiàn)的,例如拍照烁焙、連拍航邢、設(shè)置閃光燈模式、觸摸對(duì)焦骄蝇、顯示預(yù)覽畫面等等膳殷。
9 ) CaptureRequest
CaptureRequest 是向 CameraCaptureSession 提交 Capture 請(qǐng)求時(shí)的信息載體,其內(nèi)部包括了本次 Capture 的參數(shù)配置和接收?qǐng)D像數(shù)據(jù)的 Surface九火。CaptureRequest 可以配置的信息非常多赚窃,包括圖像格式、圖像分辨率岔激、傳感器控制勒极、閃光燈控制、3A 控制等等虑鼎,可以說絕大部分的相機(jī)參數(shù)都是通過 CaptureRequest 配置的辱匿。值得注意的是每一個(gè) CaptureRequest 表示一幀畫面的操作,這意味著你可以精確控制每一幀的 Capture 操作炫彩。
10) CaptureResult
CaptureResult 是每一次 Capture 操作的結(jié)果松嘶,里面包括了很多狀態(tài)信息体谒,包括閃光燈狀態(tài)坑雅、對(duì)焦?fàn)顟B(tài)读规、時(shí)間戳等等。例如你可以在拍照完成的時(shí)候杉允,通過 CaptureResult 獲取本次拍照時(shí)的對(duì)焦?fàn)顟B(tài)和時(shí)間戳邑贴。需要注意的是,CaptureResult 并不包含任何圖像數(shù)據(jù)叔磷,前面我們?cè)诮榻B Surface 的時(shí)候說了痢缎,圖像數(shù)據(jù)都是從 Surface 獲取的。
11) Request的整體處理流程
三世澜、 代碼實(shí)戰(zhàn):如何拍攝單張照片
拍攝單張照片是最簡單的拍照模式,它使用的就是單次模式的 Capture署穗,我們會(huì)使用 ImageReader 創(chuàng)建一個(gè)接收照片的 Surface寥裂,并且把它添加到 CaptureRequest 里提交給相機(jī)進(jìn)行拍照,最后通過 ImageReader 的回調(diào)獲取 Image 對(duì)象案疲,進(jìn)而獲取 JPEG 圖像數(shù)據(jù)進(jìn)行保存封恰。
1) 定義回調(diào)接口
當(dāng)拍照完成的時(shí)候我們會(huì)得到兩個(gè)數(shù)據(jù)對(duì)象,一個(gè)是通過 onImageAvailable()
回調(diào)給我們的存儲(chǔ)圖像數(shù)據(jù)的 Image褐啡,一個(gè)是通過 onCaptureCompleted()
回調(diào)給我們的存儲(chǔ)拍照信息的 CaptureResult诺舔,它們是一一對(duì)應(yīng)的,所以我們定義了如下兩個(gè)回調(diào)接口:
private val captureResults: BlockingQueue<CaptureResult> = LinkedBlockingDeque()
private inner class CaptureImageStateCallback : CameraCaptureSession.CaptureCallback() {
@MainThread
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
super.onCaptureCompleted(session, request, result)
captureResults.put(result)
}
}
private inner class OnJpegImageAvailableListener : ImageReader.OnImageAvailableListener {
@WorkerThread
override fun onImageAvailable(imageReader: ImageReader) {
val image = imageReader.acquireNextImage()
val captureResult = captureResults.take()
if (image != null && captureResult != null) {
// Save image into sdcard.
}
}
}
2) 創(chuàng)建 ImageReader
創(chuàng)建 ImageReader 需要我們指定照片的大小,所以首先我們要獲取支持的照片尺寸列表低飒,并且從中篩選出合適的尺寸许昨,假設(shè)我們要求照片的尺寸最大不能超過 4032x3024,并且比例必須是 4:3褥赊,所以會(huì)有如下篩選尺寸的代碼片段:
@WorkerThread
private fun getOptimalSize(cameraCharacteristics: CameraCharacteristics, clazz: Class<*>, maxWidth: Int, maxHeight: Int): Size? {
val streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val supportedSizes = streamConfigurationMap?.getOutputSizes(clazz)
return getOptimalSize(supportedSizes, maxWidth, maxHeight)
}
@AnyThread
private fun getOptimalSize(supportedSizes: Array<Size>?, maxWidth: Int, maxHeight: Int): Size? {
val aspectRatio = maxWidth.toFloat() / maxHeight
if (supportedSizes != null) {
for (size in supportedSizes) {
if (size.width.toFloat() / size.height == aspectRatio && size.height <= maxHeight && size.width <= maxWidth) {
return size
}
}
}
return null
}
接著我們就可以篩選出合適的尺寸糕档,然后創(chuàng)建一個(gè)圖像格式是 JPEG 的 ImageReader 對(duì)象,并且獲取它的 Surface:
val imageSize = getOptimalSize(cameraCharacteristics, ImageReader::class.java, maxWidth, maxHeight)!!
jpegImageReader = ImageReader.newInstance(imageSize.width, imageSize.height, ImageFormat.JPEG, 5)
jpegImageReader?.setOnImageAvailableListener(OnJpegImageAvailableListener(), cameraHandler)
jpegSurface = jpegImageReader?.surface
3) 創(chuàng)建 CaptureRequest
接下來我們使用 TEMPLATE_STILL_CAPTURE
模板創(chuàng)建一個(gè)用于拍照的 CaptureRequest.Builder 對(duì)象拌喉,并且添加拍照的 Surface 和預(yù)覽的 Surface 到其中:
captureImageRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captureImageRequestBuilder.addTarget(previewDataSurface)
captureImageRequestBuilder.addTarget(jpegSurface)
附:
- Google Android Camera Demo 代碼:
https://github.com/android/camera-samples - Google文檔速那,Android Camera 版本介紹
https://source.android.google.cn/devices/camera/versioning - Google 開發(fā)者文檔,Camera2 API 介紹
https://developer.android.google.cn/reference/android/hardware/camera2/package-summary.html
*本人從事Android Camera相關(guān)開發(fā)已有5年尿背,
*目前在深圳上班端仰,
*小伙伴記得點(diǎn)我頭像,看【個(gè)人介紹】進(jìn)行關(guān)注哦田藐,希望和更多的小伙伴一起交流 ~
-------- 2021.03.10 深圳 18:08