鴻蒙開源全場景應用開發(fā)—通訊協(xié)議
前言
前文提到過,已開發(fā)的家庭合影美顏相機應用是同時基于鴻蒙和安卓設備的,我們將對其包含的4個功能模塊即視頻編解碼、視頻渲染、通訊協(xié)議和美顏濾鏡進行拆分講解。
前幾期內(nèi)容中,我們對視頻編解碼和視頻渲染模塊的實現(xiàn)原理進行了解析。本期將繼續(xù)為大家講解通訊協(xié)議并簡要概述安卓美顏濾鏡的實現(xiàn)原理。
背景
RTP是用于Internet上針對流媒體傳輸?shù)囊环N基礎協(xié)議,在一對一或一對多的傳輸情況下工作,其目的是提供時間信息和實現(xiàn)流同步。它可以建立在底層的面向連接和非連接的傳輸協(xié)議上,一般使用UDP協(xié)議進行傳輸。從一個同步源發(fā)出的RTP分組序列稱為流,一個RTP會話可能包含多個RTP流。
應用效果展示
1.家庭合影美顏相機應用效果回顧
先來帶大家一起回顧下上期內(nèi)容講解的家庭合影美顏相機應用。
此應用能夠?qū)Ⅷ櫭纱笃僚臄z的視頻數(shù)據(jù)實時傳輸?shù)桨沧渴謾C上;并在安卓端為其添加濾鏡,再將處理后的視頻數(shù)據(jù)傳回到鴻蒙大屏進行渲染顯示,從而實現(xiàn)鴻蒙大屏美顏拍照的功能,應用運行后的動態(tài)場景效果可以參考圖1。
圖中下方豎屏顯示的是安卓手機,上方橫屏顯示的是鴻蒙手機(由于實驗環(huán)境缺少搭載鴻蒙系統(tǒng)的大屏設備,因此我們使用鴻蒙手機替代大屏設備模擬實驗場景),其顯示的是視頻解碼后渲染的效果。

圖1 家庭合影美顏相機應用運行效果圖
2.RTP傳輸Demo效果
為了更清晰地講解通訊協(xié)議,我們將家庭合影美顏相機應用中數(shù)據(jù)傳輸部分拆分出來,形成了一個RTP傳輸Demo,并進行了功能整理和優(yōu)化,將原本的視頻傳輸改為了圖像傳輸,視頻是由多幀圖像構成,傳輸數(shù)據(jù)類型的改變不會影響RTP傳輸原理和步驟。RTP傳輸Demo的運行效果圖如圖2所示,上圖為發(fā)送端效果,下圖為接收端效果。
成功安裝并打開應用后,在發(fā)送端點擊藍色按鈕,發(fā)送開發(fā)者選中的特定區(qū)域的圖片數(shù)據(jù);在接收端點擊粉色按鈕,接收發(fā)送端剛發(fā)送的圖片數(shù)據(jù),并在按鈕下方顯示。


圖2 RTP傳輸Demo運行效果圖(上發(fā)送端,下接收端)
RTP傳輸原理及步驟解析
接下來為大家重點解析RTP傳輸?shù)膶崿F(xiàn)原理和步驟。
RTP傳輸Demo的原理流程可參考圖3。在鴻蒙發(fā)送端(服務端),設置需要傳輸?shù)膱D像數(shù)據(jù),通過無線網(wǎng)絡,使用RTP協(xié)議和Socket點對點的數(shù)據(jù)通信方式,發(fā)送到鴻蒙接收端(客戶端)。
在鴻蒙接收端(客戶端),接收到發(fā)送端發(fā)來的圖像數(shù)據(jù)后,進行圖像繪制。接下來將針對RTP傳輸Demo的實現(xiàn)步驟進行解析。

圖3 RTP傳輸原理流程圖
服務端數(shù)據(jù)發(fā)送
在服務端,將待發(fā)送的圖像置于resources->base->media文件夾下,如圖4所示。然后對待發(fā)送的圖像數(shù)據(jù)進行格式轉(zhuǎn)換。通過無線網(wǎng)絡,使用RTP協(xié)議和Socket點對點的數(shù)據(jù)通信方式,將圖像數(shù)據(jù)傳輸至鴻蒙接收端。

圖4 圖片在項目結構中的位置
服務端的數(shù)據(jù)發(fā)送流程包含以下三個步驟:
步驟1. 通過資源ID獲取位圖對象;
步驟2. 將位圖指定區(qū)域像素進行格式轉(zhuǎn)換;
步驟3. 數(shù)據(jù)傳輸;
(1)通過資源ID獲取位圖對象
通過getResource()方法,以資源IDdrawableID對象作為入?yún)?,獲取資源輸入流drawableInputStream;實例化圖像設置類ImageSource.SourceOptions對象,并設置圖像源格式為png;創(chuàng)建圖像源,參數(shù)為資源輸入流和圖像源ImageSource類對象;實例化圖像參數(shù)類DecodingOptions的對象,為其初始化圖像尺寸、區(qū)域并設置位圖格式;根據(jù)像參數(shù)類對象decodingOptions,通過圖像源ImageSource類對象創(chuàng)建位圖對象;返回位圖對象。
- //通過資源ID獲取位圖對象
- private PixelMap getPixelMap(int drawableId) {
- InputStream drawableInputStream = null;
- try {
- //以資源ID作為入?yún)?,獲取資源輸入流
- drawableInputStream=this.getResourceManager().getResource(drawableId);
- //實例化圖像源ImageSource類對象
- ImageSource.SourceOptions sourceOptions = new ImageSource.SourceOptions();
- sourceOptions.formatHint = "image/png";//設置圖像源格式
- //創(chuàng)建圖像源,參數(shù)為資源輸入流和圖像源ImageSource類對象
- ImageSource imageSource = ImageSource.create(drawableInputStream, sourceOptions);
- //實例化圖像源解碼操作DecodingOptions類對象
- ImageSource.DecodingOptions decodingOptions = new ImageSource.DecodingOptions();
- decodingOptions.desiredSize = new Size(0, 0);//設置圖像尺寸
- decodingOptions.desiredRegion = new Rect(0, 0, 0, 0);
- decodingOptions.desiredPixelFormat = PixelFormat.ARGB_8888;//設置位圖格式
- PixelMap pixelMap = imageSource.createPixelmap(decodingOptions);//根據(jù)解碼操作類對象,創(chuàng)建位圖
- return pixelMap;//返回位圖
- }
- ...
- }
(2)將位圖指定區(qū)域像素進行格式轉(zhuǎn)換
在得到位圖對象后,實例化矩形Rect矩形類對象,用于為開發(fā)者選中特定的圖像區(qū)域(該區(qū)域應不大于resources->base->media路徑下圖像的大小);通過位圖對象pixelMap調(diào)用readPixels()方法將指定區(qū)域像素轉(zhuǎn)換為int[]類型數(shù)據(jù);調(diào)用intToBytes()方法再將int[]類型數(shù)據(jù)格式轉(zhuǎn)換為byte類型數(shù)據(jù)。
- // 讀取指定區(qū)域像素
- Rect region = new Rect(0, 0, 30, 30);//實例化舉行類對象,規(guī)定指定區(qū)域
- pixelMap.readPixels(pixelArray,0,30,region);//將指定區(qū)域像素轉(zhuǎn)換為int[]類型數(shù)據(jù)
- pic = intToBytes(pixelArray);//將int[]類型數(shù)據(jù)昂虎子你換位byte類型數(shù)據(jù)
(3)數(shù)據(jù)傳輸
實例化RTP發(fā)送類對象RtpSenderWrapper,將IP地址設置為接收端手機IP地址,端口號設置為5005;調(diào)用sendAvcPacket()方法發(fā)送圖像數(shù)據(jù)。
由于對RTP傳輸?shù)臄?shù)據(jù)類型做了簡化,因此圖像RTP傳輸會相對容易,而如果是原應用中的視頻RTP傳輸,則需要逐幀對視頻數(shù)據(jù)進行格式轉(zhuǎn)換,并將從攝像頭獲取的YUV類型的原始視頻數(shù)據(jù)壓縮為h264類型的視頻數(shù)據(jù),以方便Socket進行傳輸。
- mRtpSenderWrapper = new RtpSenderWrapper("192.168.31.12", 5005, false);
- mRtpSenderWrapper.sendAvcPacket(pic, 0, pic.length, 0);//發(fā)送數(shù)據(jù)
客戶端接收數(shù)據(jù)
在發(fā)送端通過RTP協(xié)議成功發(fā)送數(shù)據(jù)后,接收端就可以正常開始接收了。發(fā)送端接收數(shù)據(jù)的流程主要分為以下5個步驟:
步驟1. 創(chuàng)建數(shù)據(jù)接收線程;
步驟2. 接收數(shù)據(jù);
步驟3. 在線程間進行數(shù)據(jù)傳遞;
步驟4. 處理位圖數(shù)據(jù)得到pixelMapHolder;
步驟5. 繪制圖像。
(1)創(chuàng)建數(shù)據(jù)接收線程
創(chuàng)建子線程作為數(shù)據(jù)接收線程。
- new Thread(new Runnable())//新開一個數(shù)據(jù)接收線程
(2)接收數(shù)據(jù)
在子線程接收線程中,實例化數(shù)據(jù)包DatagramPacket;通過Socket類對象調(diào)用receive()方法,接收發(fā)送端的數(shù)據(jù)到數(shù)據(jù)包DatagramPacket中;通過數(shù)據(jù)包DatagramPacket調(diào)用getData()方法獲取數(shù)據(jù)包中的RTP數(shù)據(jù)。
- datagramPacket = new DatagramPacket(data,data.length);//實例化數(shù)據(jù)包
- socket.receive(datagramPacket);//接收數(shù)據(jù)到數(shù)據(jù)包中
- rtpData = datagramPacket.getData();獲取數(shù)據(jù)包中的RTP數(shù)據(jù)
(3)在線程間進行數(shù)據(jù)傳遞
待子線程拿到RTP發(fā)送數(shù)據(jù)后,需要將RTP數(shù)據(jù)從子線程傳遞到主線程。這就涉及到線程間的數(shù)據(jù)傳遞。在此應用中,我們使用了Java類的SynchronousQueue并發(fā)隊列來實現(xiàn)子線程和主線程間的數(shù)據(jù)傳遞。先實例化一個byte[]類型的并發(fā)隊列SynchronousQueue類對象;將h264類型的數(shù)據(jù)放入并發(fā)隊列中;再從隊列中獲取數(shù)據(jù)。
- SynchronousQueue<byte[]> queue = new SynchronousQueue<byte[]>();//實例化byte[]類型的并發(fā)隊列
- queue.put(h264Data);//將h264類型的數(shù)據(jù)放入并發(fā)隊列中
- rgbData = queue.take();//從隊列中獲取數(shù)據(jù)
(4)處理解碼后的位圖數(shù)據(jù)得到PixelMapHolder
主線程從隊列中拿到圖像RGB數(shù)據(jù)后即可進行圖像繪制。PixelMap是接收得到的位圖數(shù)據(jù),PixelMapHolder 使用 PixelMap 生成渲染后端所需的數(shù)據(jù),并提供數(shù)據(jù)作為 Canvas 中方法的輸入?yún)?shù)。因此為了后續(xù)能夠?qū)ξ粓D進行渲染,需要在圖像數(shù)據(jù)從子線程傳遞到主線程后,將圖像數(shù)據(jù)pixelmap轉(zhuǎn)換為pixelMapHolder類對象,即在實例化pixelMapHolder類對象時,將pixelmap位圖數(shù)據(jù)作為入?yún)魅雽嵗椒ㄖ小?/p>
- public void putPixelMap(PixelMap pixelMap){
- if (pixelMap != null) {//判斷接收到的位圖數(shù)據(jù)是否為空
- rectSrc = new RectFloat(0, 0, pixelMap.getImageInfo().size.width, pixelMap.getImageInfo().size.height);
- pixelMapHolder = new PixelMapHolder(pixelMap);//實例化PixelMapHolder類對象
- }else{
- pixelMapHolder = null;//若接收到的位圖為空,則全部置為空
- setPixelMap(null);
- }
- }
(5)繪制圖像
實例化一個矩形Rect類對象,設置圖像信息并規(guī)定指定的區(qū)域如寬和高;添加一個同步繪制任務,先判斷pixelMapHolder是否為空,若為空則直接返回,不為空則開始繪制任務;在繪制任務中,調(diào)用drawPixelMapHolderRoundRectShape()方法將PixelMapHolder類對象繪制到實例化得到的矩形Rect類對象中,并設置其為圓角效果;其位置由rectDst指定;繪制完成后釋放pixelMapHolder,將其置為空。
- private void onDraw(){
- this.addDrawTask((view, canvas) -> { //添加繪制任務
- if (pixelMapHolder == null){//判斷pixelMapHolder是否為空
- return;
- }
- synchronized (pixelMapHolder) {//在同步任務中繪制圖像
- canvas.drawPixelMapHolderRoundRectShape(pixelMapHolder, rectSrc, rectDst, radius, radius);//繪制圖像為圓角效果
- pixelMapHolder = null;//繪制完成后將pixelMapHolder釋放
- }
- });
- }
安卓端美顏濾鏡效果實現(xiàn)
美顏濾鏡的部分我們參考了GitHub上的開源項目(https://github.com/google/grafika、https://github.com/cats-oss/android-gpuimage、https://github.com/wuhaoyu1990/MagicCamera),使用GPU著色器實現(xiàn)添加濾鏡和切換濾鏡的效果。由于不涉及鴻蒙的能力,此部分不作為重點講述,只簡要概括下其實現(xiàn)流程,可分為如下5個步驟:
(1)設置不同的濾鏡
使用著色器語言,設置所需的多種代碼。

圖5 美顏相機使用的濾鏡
(2)opengl繪制;
- import android.opengl.GLES20;
- ...
- // add the vertex shader to program
- GLES20.glAttachShader(mProgram, vertexShader);
- // add the fragment shader to program
- GLES20.glAttachShader(mProgram, fragmentShader);
- // creates OpenGL ES program executables
- GLES20.glLinkProgram(mProgram);
(3)添加濾鏡;
- private List<FilterFactory.FilterType>filters = new ArrayList<>();
- ...
- filters.add(FilterFactory.FilterType.Original);
- filters.add(FilterFactory.FilterType.Sunrise);
- ...
(4)開啟或關閉美顏濾鏡;
- mCameraView.enableBeauty(true);
(5)設置美顏程度;
- mCameraView.setBeautyLevel(0.5f);
(6)設置切換濾鏡和切換鏡頭,再設置相機拍攝和拍攝完成后的回調(diào)即可。
- mCameraView.updateFilter(filters.get(pos));//切花濾鏡
- mCameraView.switchCamera();//切換鏡頭