前言

随着全场景多设备生活方式的不断深入,用户拥有的设备越来越多,每个设备都能在适合的场景下提供良好的体验,例如:手表可以提供及时的信息查看能力,电视可以带来沉浸的观影体验。但是,每个设备也有使用场景的局限,例如:在电视上输入文本相对手机来说是非常糟糕的体验。当多个设备通过分布式操作系统能够相互感知、进而整合成一个超级终端时,设备与设备之间就可以取长补短、相互帮助,为用户提供更加自然流畅的分布式体验。分布式流转即可解决此类问题。本课题将从屏幕、视频和控制三个方面介绍分布式数据流转。

分布式屏幕数据流转

涉及模块

分布式屏幕组件,窗口子系统,图形图像子系统,媒体子系统,分布式软总线子系统

 

简介

使用分布式设备中的分布式屏幕的接口来实现分布式屏幕的能力,并通过该模块的一套数据流转来传输屏幕数据。

主控端(source):控制端,通过调用分布式屏幕能力,使用被控端的屏幕用于显示主控端设备的屏幕内容。

被控端(sink):被控制端,通过分布式屏幕接收主控端的控制,在本地对接窗口用于接收和显示主控端设备的屏幕内容。

模块介绍

屏幕区域管理(ScreenRegionManager):管理主控端映射在被控端屏幕上的显示区域的状态,包括为显示区域指定显示的display,设置显示区域的宽高,解码类型等参数。

分布式屏幕管理(DScreenManager):管理被控端屏幕的参数和状态,负责主控端相关对象的创建和销毁。

屏幕服务(ScreenService):分布式屏幕主控端SA服务和分布式屏幕被控端SA服务,负责处理分布式硬件管理框架的IPC调用。

软总线适配器(SoftbusAdapter):对接软总线传输接口,为屏幕图像、输入事件等提供封装的统一调用接口,实现设备间的流数据、字节数据传输和交互。

屏幕传输组件(ScreenTransport):分布式屏幕传输模块,实现屏幕图像数据编码、解码、发送、接收。

代理客户端(ScreenClient):屏幕图像显示代理客户端,用于在设备上显示其他设备投射过来的屏幕图像数据。

服务流程介绍

1.设备认证绑定组网,调用分布式组件提供的ets接口,通过生成和输入pin码的方式认证两端设备。

2.调用窗口子系统提供的接口,获取对端设备的Screen的指针。

3.调用窗口子系统提供的MakeMirror接口创建本端设备的Screen到对端设备Screen的投屏,将本端Screen的RSDisplayNode配置给对端设备的Screen。

4.分布式屏幕组件监听到Screen群组发生变化,调用分布式屏幕的服务。

5.分布式屏幕source端和sink端初始化完成后,创建会话,打开会话,并通过媒体子系统提供的视频编码器和解码器,分别在source端编码Screen的surface数据,通过已打开的会话把编码后的数据发送到sink端,sink端解码后输出到sink端创建的Window的surface中,最后通过图形图像子系统渲染展示。

//获取对端屏幕信息,返回Screen的指针vector vector<sptr<Screen>> QueryRemoteScreenInfo() {     vector<sptr<Screen>> allScreens = ScreenManager::GetInstance().GetAllScreens();     sptr<Display> defaultDisplay = DisplayManager::GetInstance().GetDefaultDisplay();     vector<sptr<Screen>> remoteScreens;     for (const auto &screen : allScreens) {         if (screen == nullptr) {             continue;        }         if (!screen->IsReal() && screen->GetWidth() > 0) {             remoteScreens.push_back(screen);        }    } } //调用窗口子系统ScreenManager提供的接口StartMirror将本端的Screen投屏到对端设备 static void StartMirror() {     vector<sptr<Screen>> remoteScreens = QueryRemoteScreenInfo();     if (remoteScreens.size() == 0) {         cout << "Error: no remote screens enabled" << endl;         return;    }      sptr<Display> defaultDisplay = DisplayManager::GetInstance().GetDefaultDisplay();     cout << "------------start mirror----------" <<endl;     cout << "mirror screen Id is " << mirrorId << endl;     vector<uint64_t> mirrorIds;     mirrorIds.push_back(mirrorId);     ScreenManager::GetInstance().MakeMirror(defaultDisplay->GetScreenId(), mirrorIds);                  }

屏幕数据流转

1.主控端图形子系统将需要发送屏幕数据保存在编码器创建的输入Surface中。

2.主控端编码器将输入数据进行编码,并将编码结果返回传输组件screensourcetrans。

3.主控端传输组件screensourcetrans将编码后的数据通过传输通道screendatachannel发送到softbusadapter,并经由软总线子系统发送到被控端端设备。

4.被控端设备软总线子系统收到屏幕数据后,通过softbusadapter返回给被控端传输通道screendatachannel。

5.被控端传输通道screendatachannel将获取到的屏幕数据传递给解码器进行解码。

6.解码器将屏幕数据解码,并将解码后的数据保存到被控端代理显示窗口设置到解码器的Surface中,最终由窗口将画面显示在屏幕上。

 

软总线适配器(SoftbusAdapter):对接软总线传输接口,为屏幕图像、输入事件等提供封装的统一调用接口,实现设备间的流数据、字节数据传输和交互,在这一层调用软总线提供的组网和传输等能力。

控端数据传输组件(ScreenSourceTrans):包含数据传输通道channel和数据处理模块processor。

屏幕数据通道(ScreenDataChannel): 屏幕数据传输通道,用于组件和编解码器之间的数据传输 。

前端应用通过RPC通信传输数据

涉及模块

RPC通信,分布式软总线,需求到的组件和子系统

 

简介

一个前台进程可以通过RPC通信(设备间进程通信)的方式来传输字节数据,流数据和文件数据等数据类型至对端设备的一个前台进程,对端设备的前台进程再调用需要用到的子系统的对应的接口再通过IPC通信的方式与本机运行的服务进程进行通信,获取需要调用到的子系统的能力,实现对应的功能。RPC跨设备通信能力由分布式软总线提供。

分布式涂鸦应用例子

ets应用端:通过分布式硬件和rpc模块提供的接口进行跨设备通信,再通过其他子系统的接口获取其他子系统功能。

操作步骤

1.设备绑定认证。

2.通过分布式硬件的接口获取对端设备id。

3.连接至本地应用,设置RPC通信的RemoteObject。

4.通过对端设备id打开对端应用。

5.通过对端设备id连接至对端应用,获取对端应用的对象。

6.在控件上触摸划动时会通过RPC通信将划动的数据发送给对端,通过之前获取的对端应用的对象来sendRequest发送数据。

7.对端设备通过已注册的监听来将触摸和划动的数据写给控件。

.onTouch((event:TouchEvent)) =>{             if(event.type == TouchType.Down){                 this.context.moveTo(event.touches[0].x,event.touches[0].y);                 this,drawToRemote(event.touches[0].x,event.touches[0].y,TouchType.Down)            }             if(event.type == TouchType.Up){                 this.context.save();                 this.drawToRemote(0,0,TouchType.Up)            }                                                       async public drawToRemote(x:number,y:number,touchType:number) : Promise<void> {                if(mRemote == null){                   return                }                let option = new rpc.MessageOption();                let data = new rpc.MessageParcl();                let reply = new rpc.MessageParcl();                data.writeInt(x);                data.writeInt(y);                data.writeInt(touchType);                await mRemote.sendRequest(CODE_CONNECT_ROMOTE.data,reply,option);          }    }

分布式视频数据流转

C++示例流程图

 

播放器

8F267B61-1715-4FF1-BC75-74B3562DAA21

 

操作步骤

1.设备绑定认证。

2.设备组网。

3.创建会话,打开会话。

4.数据传输。

5.回调函数接收传输数据,进行自定义的处理。

//注册自实现的回调函数 void T_CreateSessionServer(void) {     ISessionListener listener = {        .OnSessionOpened = OnSessionOpened,        .OnSessionClosed = OnSessionClosed,        .OnBytesReceived = OnBytesReceived,        .OnMessageReceived = OnMessageReceived,        .OnStreamReceived = OnStreamReceived,        .OnQosEvent = NULL  }; }      //收到数据的回调函数中可以在调用其他子系统的的接口对收到的data参数进行其他处理 void OnBytesReceived(int sessionId, const void *data, unsigned int dataLen) {     printf("\n>>>OnBytesReceived sessionId = %d, data = %s.\n", sessionId, data); }  void OnMessageReceived(int sessionId, const void *data, unsigned int dataLen) {     printf("\n>>>OnMessageReceived sessionId = %d, data = %s.\n", sessionId, data); }  void OnStreamReceived(int sessionId, const StreamData *data, const StreamData *ext, const StreamFrameInfo *param) {     printf("\n>>>OnStreamReceived sessionId = %d, data = %s, ext = %s\n", sessionId, data->buf, ext->buf); }

在发送数据前对需要发送的数据进行处理。在回调函数中接收数据,对接收到的数据再进行处理。

处理视频数据,encode编码端(发送端)

在设备认证绑定组网并已经打开会话以后,即可进行视频数据的编码操作。

调用媒体子系统提供的接口 multimedia/media_standard/interfaces/innerkits/native/media/include下的avcodec_video_encoder.h

主要分为以下6个步骤:

1.创建视频解码器。

venc_ = VideoDecoderFactory::CreateByMime("video/avc");//根据格式创建编码器,H264格式 signal_ = make_shared<VEncSingal>(); //创建信号指针,主要处理bufferQueue cb_  = make_shared<VencDemoCallback>(signal_); //创建回调指针 venc->SetCallback(cb_);//设置回调

2.设置格式,像素格式,宽高,帧率等,并配置入编码器中。

Format format; format.PutIntValue("width",DEFAULT_WIDTH); format.PutIntValue("height",DEFAULT_HEIGHT); format.PutIntValue("pixel_format",3); format.PutIntValue("frame",DEFAULT_FRAME_RATE); //设置像素格式 venc_->Configure(format);//设置编码器的配置

3.创建surface并获取surface指针,设置编码器参数,开启编码器。

surface_ = vdec_->CreateInputSurface();//创建并获得输入用的Surface的指针 Format format; format.PutIntValue("suspend_input_surface",suspend); format.PutIntValue("max_encoder_fps",maxFps); format.PutIntValue("repeat_frame_after",repeatMs); venc_->SetParameter(format);//设置编码器的参数 venc_->Prepare(); venc_->Start();//开启编码器

4.创建OHOS图形图像的SurfaceBuffer并获取指针,从编码器中申请输入用的surface,通过这个surface获取他的buffer。获取某一个窗口的指针,并通过其指针获取对应的surface,将窗口的buffer赋给申请到的surface的buffer,编码器编码完成,清空buffer,循环加载下一帧。

while(frameCount <= count) {     sptr<OHOS::SurfaceBuffer> buffer = nullptr //创建一个OHOS的图像指针     surface_->RequestBuffer(buffer,fence,&_request);//通过这个surface获取他的buffer     auto addr = static_cast<uint8_t *>(buffer->GetViraddr())     sptr<Rosen::window>window = Rosen::window::Find("current_window");     sptr<securec>surface = window->GetSourceTNode()->GetSurface();     surface->RequestBuffer(buffer,fence,g_request);     auto addr1 = static_cast<uint8_t*>(buffer->GetViraddr());     surface_->FlushBuffer(buffer,-1,g_flushConfig),//清空buffer,循环每一帧     frameCount++; }

5.buffer处理结束后会通过回调函数通知,可以通过回调函数的index来获取编码完成的buffer,再通过软总线的发送函数来发给对端设备。

void VencDemoCallback::OnOutputBufferAvailable(uint32_t index, AVCodecBufferInfo info,AVCodecBufferFlag flag) {     auto buffer = vdec_GetOutputBuffer(index);     unsigned char*data = (unsigned char*)malloc(sizeof(unsigned char)*buffer->GetSize());     memcpy(data,buffer->GetBase(),buffer->GetSize())     int ret = SendBytes(sessionId,data,datalength); }

6.从本地读视频文件发送到对端窗口显示,由于OHOS暂时没有单独给出demuxer的接口,原本通过demuxer可以将MP4分理处H264视频流和音频流,该能力在第三方库的开源代码ffmpeg中提供,如需要可自行封装,或先单独处理MP4文件压缩为H264格式 。

testFile_ = std::make_unique<std::ifstream>(); testFile_->open("/data/media/video.mp4",std::ios::in | std::ios::binary); const int32_t*frameLen = ES; char*fileBuffer = (char*)malloc(sizeof(char)*(*frameLen)+1); testFile_->read(fileBuffer,*frameLen);

视频数据接收端(decode解码端)

调用媒体子系统提供的接口 multimedia/media_standard/interfaces/innerkits/native/media/include下的avcodec_video_decoder.h

主要分为以下4个步骤:

1.创建视频解码器。

venc_ = VideoDecoderFactory::CreateByMime("video/avc");//根据格式创建解码器,H264格式 signal_ = make_shared<VEncSingal>(); //创建信号指针,主要处理bufferQueue cb_ = make_shared<VencDemoCallback>(signal_); //创建回调指针 venc->SetCallback(cb_);//设置回调

2.设置格式,并配置入解码器中。

Format format; format.PutIntValue("width",DEFAULT_WIDTH); format.PutIntValue("height",DEFAULT_HEIGHT); format.PutIntValue("pixel_format",3); format.PutIntValue("frame",DEFAULT_FRAME_RATE); format.PutIntValue("max_input_size",MAX_INPUT_BUFFER_SIZE); //设置像素格式 venc_->Configure(format);//设置编码器的配置

3.通过窗口子系统的接口创建OHOS窗口子系统的window指针surface,将这个surface设置进解码器的输出中。

spt<Rosen::windowOption> option = mew Rosen::windowOption(); option->SetWindowRect({0,0,DEFAULT_WIDTH,DEFAULT_HEIGHT}); option->SetWindowType(Rosen::windowType,WINDOW_TYPE_APP_LAUNCHING); option->SetWindowMode(Rosen::windowMode,WINDOW_MODE_FLQAING) sptr<Surface>surface = window->GetSourceTNode()->GetSurface(); window->Show(); vdec_->SetOutputSurface(surface);

4.开启解码器,在软总线收到数据的回调被触发后,把收到的buffer交给解码器的输入队列处理。可以通过index编号来获取解码器的buffer做后续的一些操作。

//开启解码器 vdec_->Prepare(); vdec_->Start(); //输入处理 void OnBytesReceived(int sessionId, const void *data, unsigned int dataLen) { printf("\n>>>OnBytesReceived sessionId = %d, data = %s.\n", sessionId, data); uint32_t index = signal_->inQueue_.front(); auto buffer = venc_->GetInputBuffer(index); memcpy_s(buffer->GetBase(),buffer->GetSize(),data,dataLen); info.size = dataLen; info.offset = 0; info.presentationTimeUs=timeStamp_; rer = venc_->QueueInputBuffer(index,info,AVCODEC_BUFFER_FLAG_CODEC_DATA); signal_->inQueue_.pop(); } //buffer处理器后通过回调通知 void VDecDemoCallback::OnInputBufferAvailable(uint32_t index,AVCodecBufferInfo info,AVCodecBufferFlag flag) { signal_->outQueue_.push(index);//吧buffer编号加入输出队例 }

分布式控制数据流转

涉及模块

多模输入子系统,分布式软总线

FE5D45E8-A785-4D8E-9AE2-A3A5AE00E5A9

 

简介

多模输入系统主要用于接收按键,触摸等输入事件,并且会对这些原始输入事件进行处理,之后再对这些事件进行派发。同时多模输入系统还提供了注入事件的接口,应用可以通过调用这个接口产生输入事件,然后将该输入事件注入到输入系统中进行处理。

工作流程

1.多模输入子系统驱动检测到输入事件产生,例如触屏,键鼠输入等事件,图形子系统的。

2.InputManagerService会通过InputEventHub类来接收这个事件。

3.通过InputEventDistributer通过IPC通信的方式来将事件分发到应用层。

触屏事件为例

通过创建一个触屏事件,再通过分布式软总线的能力将该触屏事件内存空间存储的字节数据发送到对端,对端触发软总线回调,收到数据,再将该事件通过接口上报,从而在对端设备产生触屏输入。

操作步骤

1.由应用程序产生一个PointerEvent事件,填充相关的参数,通过分布式软总线的SendByte将该PointerEvent指针所指向的内存空间的数据发送到对端。

2.对端设备可以继续处理相关参数,若不处理则可以直接通过InputManager的接口SimulateInputEvent将该事件上报,会在屏幕上产生一次对应的触屏。

3.通过多模输入子系统的input_manager.h的AddMonitor来注册触屏事件的监听,产生触屏事件后会通过注册的回调函数返回一个PointerEvent的指针,传输方式如上,对端收到后通过SimulateInputEvent上报。

void Touchpadcb (std::shared_ptr<OHOS::MMI::PointerEvent> TouchPadEventMonitor) { int32_t ret; auto pointerEvent = PointerEvent::Create(); int32_t pointerid; int32_t gx; int32_t gy; int32_t sourcetype; int32_t pointeraction; OHOS::MMI::PointerEvent::PointerItem item; OHOS::MMI::PointerEvent::PointerItem item1; pointerid=TouchPadEventMonitor->GetPointerId(); ret=TouchPadEventMonitor->GetPointerItem(pointerid,item1); gx=item1.GetGlobalX(); gy=item1.GetGlobalY(); item.SetPointerId(pointerid); item.SetGlobalX(gx); item.SetGlobalY(gy); sourcetype=TouchPadEventMonitor->GetSourceType(); pointeraction=TouchPadEventMonitor->GetPointerAction(); pointerEvent->SetPointerId(0); pointerEvent->AddPointerItem(item); pointerEvent->SetPointerAction(PointerEvent::POINTER_ACTION_DOWN); pointerEvent->SetSourceType(PointerEvent::SOURCE_TYPE_TOUCHSCREEN); } InputManager* IM =InputManager::GetInstance(); ret=IM->AddMonitor(Touchpadcb);

总结

分布式数据流转在OpenHarmony中泛指多设备分布式操作。流转能力打破设备界限,多设备联动,使用户应用程序可分可合、可流转,实现如多设备协同健身、多屏游戏等分布式业务。流转为开发者提供更广的使用场景和更新的产品视角,强化产品优势,实现体验升级。

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐