天天看點

鴻蒙開源全場景應用開發——通訊協定

前言

前文提到過,已開發的家庭合影美顔相機應用是同時基于鴻蒙和安卓裝置的,我們将對其包含的4個功能子產品即視訊編解碼、視訊渲染、通訊協定和美顔濾鏡進行拆分講解。

前幾期内容中,我們對視訊編解碼和視訊渲染子產品的實作原理進行了解析。本期将繼續為大家講解通訊協定并簡要概述安卓美顔濾鏡的實作原理。相關代碼已經開源到Gitee(https://gitee.com/isrc_ohos/cameraharmony/tree/RTP/ ),歡迎各位下載下傳使用并提出寶貴意見!

背景

RTP是用于Internet上針對流媒體傳輸的一種基礎協定,在一對一或一對多的傳輸情況下工作,其目的是提供時間資訊和實作流同步。它可以建立在底層的面向連接配接和非連接配接的傳輸協定上,一般使用UDP協定進行傳輸。從一個同步源發出的RTP分組序列稱為流,一個RTP會話可能包含多個RTP流。

應用效果展示

1.家庭合影美顔相機應用效果回顧

先來帶大家一起回顧下上期内容講解的家庭合影美顔相機應用。

此應用能夠将鴻蒙大屏拍攝的視訊資料實時傳輸到安卓手機上;并在安卓端為其添加濾鏡,再将處理後的視訊資料傳回到鴻蒙大屏進行渲染顯示,進而實作鴻蒙大屏美顔拍照的功能,應用運作後的動态場景效果可以參考圖1。

圖中下方豎屏顯示的是安卓手機,上方橫屏顯示的是鴻蒙手機(由于實驗環境缺少搭載鴻蒙系統的大屏裝置,是以我們使用鴻蒙手機替代大屏裝置模拟實驗場景),其顯示的是視訊解碼後渲染的效果。

::: hljs-center

鴻蒙開源全場景應用開發——通訊協定

圖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();//切換鏡頭
           

項目貢獻人

蔡志傑 李珂 朱偉 鄭森文 陳美汝

想了解更多關于鴻蒙的内容,請通路:

51CTO和華為官方合作共建的鴻蒙技術社群

https://harmonyos.51cto.com/#bkwz

繼續閱讀