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

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


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

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

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