当前位置:   article > 正文

fruncm server sql 无法生成 线程_FreeRDP(4/5):Server处理图像

sql server 无法生成 fruncm 线程

在windows,编译freerdp会生成两个server:wfreerdp-server.exe和freerdp-shadow-cli.exe。

  • wfreerdp-server.exe。抓屏支持dxgi和Mirage Driver,Mirage Driver是较早技术,win10已不再支持,因而实际可用的是dxgi。编码图像固定使用RemoteFX,不支持使用自定义h264压缩,像openh264。由于RemoteFX有非常好性能,client连接它时画面流畅,即使server桌面是2560x1600这种高分辨率。
  • freerdp-shadow-cli.exe。抓屏支持dxgi和wds(Windows Desktop Sharing)。如不改源码,默认使用wds。压缩图像可使用h264,传输编码使码gfx([MS-RDPEGFX])。由于压缩效率不如RemoteFX,server高分辨率时,client显示会有较大延时,感觉是个实验室产品。

从流畅角度,windows自然推荐使用wfreerdp-server.exe,但考虑到有人学FreeRDP是为在其它平台上写rdp server,像Android,那些平台很难使用RemoteFX,为此本文介绍能使用H264压缩的freerdp-shadow-cli.exe。有说到该exe抓屏支持dxgi或wds,本文不深入抓屏技术,但考虑到wds要比dxgi复杂很多,如果只是想搞清抓屏后面逻辑、又不在乎抓到图像是否正确,做以下修改可让工作在dxgi。

  1. 1<FreeRDP>/server/shadow/Win/win_dxgi.h,去掉注释WITH_DXGI_1_2宏。
  2. 2<FreeRDP>/server/shadow/Win/win_dxgi.c。
  3. textureDesc.Width = subsystem->width;
  4. textureDesc.Height = subsystem->height;
  5. 改为
  6. textureDesc.Width = subsystem->base.monitors[0].right - subsystem->base.monitors[0].left;
  7. textureDesc.Height = subsystem->base.monitors[0].bottom - subsystem->base.monitors[0].top;
  8. 注:估计freerdp已不再用subsystem->width、subsystem->height,它们总是0。monitors[0]存放着屏幕尺寸。width、height不对时,接下的CreateTexture2D会返回失败。
  9. 3、不编译<FreeRDP>/server/shadow/Win/win_rdp.c、<FreeRDP>/server/shadow/Win/win_wds.c
  10. 4<FreeRDP>/server/shadow/Win/win_wds.h。注释宏WITH_WDS_API。

或许,我们很在乎怎么在rdp中使用h264。这里先说rdp协议是怎么确定此次会话能使用h264,主要关心client发来的两个pdu。

Client MCS Connect Initial PDU with GCC Conference Create Request。这是在连接阶段client发来的一个pdu。包含Client Core Data(TS_UD_CS_CORE),该数据中有个2字节的earlyCapabilityFlags,如果设了RNS_UD_CS_SUPPORT_DYNVC_GFX_PROTOCOL标记,表示client支持gfx(SupportGraphicsPipeline)。一旦发现这标记,server可考虑使用h264压缩了。至于h264要压缩哪些数据,像需要一个还是两个h264流,这取决于第二个字段。

RDPGFX_CAPS_ADVERTISE_PDU。[MS-RDPEGFX]定义的pdu,也是client发出,内中包含若干个指示客户端能力的RDPGFX_CAPSET。server检查逻辑是这样的:每个RDPGFX_CAPSET有版本字段,检查从高版本到低版本,一旦某个RDPGFX_CAPSET可以用了,就以它为准,不再检查更低版本。假设要使用的版本是RDPGFX_CAPVERSION_105,RDPGFX_CAPSET有个字段叫flags,flags支持的标记中RDPGFX_CAPS_FLAG_AVC_DISABLED,一旦设置该标记,表示client不支持MPEG-4 AVC/H.264解码,针对该标记,freerdp server会执行以下赋值。

  1. settings->GfxSmallCache = (flags & RDPGFX_CAPS_FLAG_SMALL_CACHE);
  2. settings->GfxAVC444v2 = settings->GfxAVC444 = settings->GfxH264 = !(flags & RDPGFX_CAPS_FLAG_AVC_DISABLED);

下文认为client没有设置RDPGFX_CAPS_FLAG_AVC_DISABLED标志,即GfxAVC444v2、GfxAVC444、GfxH264都是true。

注:为强制使用openh264,config.h不要定义WITH_MEDIA_FOUNDATION宏,因为MF的优先级要比openh264高。

接下分析server如何处理图像,把过程分三个阶段:抓屏、编码、发送。

一、抓屏

8f52ad8f24ff3fbfb7ddff1844b3b4e7.png
图1 抓屏

图1显示的抓拍使用wds技术。dxgi不存左侧的抓拍线程,没有RdpUpdateEnterEvent、RdpUpdateLeaveEvent这两个同步变量,子系统线程抓到一帧后立即调用win_shadow_surface_copy。也就是说,无论哪种技术都会调用win_shadow_surface_copy。

总的来说,子系统线程抓到一帧,图像放在server->surface->data,设置event->event有信号,死等event->doneEvent。因为event->event有信号了,所有会话线程去处理新帧,shadow_client_send_surface_update是处理新帧的函数。所有会话线程处理完这帧后,最后一个处理完的会话线程负责设置event->doneEvent有信号,触发子系统线程去抓下一帧。当中存在两个同步,1)子系统线程必须等到所有会话线程处理完后才能去抓下一帧;2)所有client要等到其它client都处理完才会执行下一个操作。

rdp_shadow_multiclient_event是抓帧同步中一个重要结构,当中光事件就有三个,event、barrierEvent和doneEvent,为什么会这么复杂,——因为会存在多个client。各线程如何使用这三个事件分两种场景,一种是当前只有一个client,一种是当前至少有两个client。

只一个client。会话线程发现event->event有信号,调用shadow_client_send_surface_update执行处理。处理完后,因为只一个,它自然是最后一个处理屏幕帧的client,于是把doneEvent置为有信号。doneEvent有信号触发子线程去抓下一帧。整个过程不涉及到barrierEvent。

至少有两个client。这时要用到barrierEvent、waiting。如果它不是处理完帧的最后一个client,那不去设置event->doneEvent有信号,而是依次执行把waiting+1、等待barrierEvent有信号。当最后一个client处理完成后,发现waiting不是0,改为设置barrierEvent有信号。之前那些处理好的、正等barrierEvent的client会被唤醒,唤醒后waiting-1,-1后waiting为0表示已没有client在等了,于是置event->doneEvent有信号。

waiting表示正等待barrierEvent有信号的client个数。什么样的client会去等待barrierEvent有信号?那些处理完新帧、但又不是最后一个处理完新帧的client。consuming指的是系统中还没处理新帧的client数。

二、编码

574517ed8d139592943645671a54d516.png
图2 [MS-RDPEGFX] RFX_AVC444V2_BITMAP_STREAM中LC

surface->data存着图像数据,格式是RGB,进入h264压缩前须要转成YUV420P。和惯常h264压缩不一样,rdp有可能进行两次h264压缩(图2中LC=0时),当然,两次压缩用的是两份不同YUV420P数据。一次是惯常认为的,生成的YUV420P数据放在h264->pYUV444Data,[MS-RDPEGFX]称为Main View,LC(图2)中称YUV420 frame,经h264压缩后放在avc444.bistream[0],gfx编码时放在avc420EncodedBitstream1。第二次用另一种方法生成,生成的YUV420P数据放在h264->pYUVData,[MS-RDPEGFX]称为Auxillary View,LC(图2)中称Chroma420 frame,经h264压缩后放在avc444.bitstream[1],gfx编码时放在avc420EncodedBitstream2。

2.1 RGB转成YUV420P、h264压缩

02a899098a4a36ee3ff72a049bc055f2.png
图3 RGB转成YUV420P

RGB转成YUV420P、h264压缩都是在avc444_compress执行的,以下是参数。

  • pSrcData[IN]。来自surface->data的RGB格式数据。
  • SrcFormat[IN]。pSrcData的RGB格式,BGRA32或BGRX32。
  • nSrcStep[IN]。行距。不论BGRA32还是BGRX32,每像素4字节,nSrcStep都是4*nSrcWidth。
  • nSrcWidth[IN]、nSrcHeight[IN]。屏幕尺寸,像2560x1600。
  • version[IN]。“version = settings->GfxAVC444v2? 2: 1”,本文GfxAVC444v2是ture,因而值是2。
  • op[OUT]。就是图2中的LC。因为总支持两条h264流,值会固定等于0。
  • ppDstData[OUT]、pDstSize[OUT]。指示Main View经h264压缩后数据的存放地址,即avc444.bistream[0]。pDstSize是对应的字节数。
  • ppAuxDstData[OUT]、pAuxDstSize[OUT]。指示Auxillary View经h264压缩后数据的存放地址,即avc444.bistream[1]。pAuxDstSize是对应的字节数。
  1. INT32 avc444_compress(H264_CONTEXT* h264, const BYTE* pSrcData, DWORD SrcFormat, UINT32 nSrcStep,
  2. UINT32 nSrcWidth, UINT32 nSrcHeight, BYTE version, BYTE* op, BYTE** ppDstData,
  3. UINT32* pDstSize, BYTE** ppAuxDstData, UINT32* pAuxDstSize)
  4. {
  5. prim_size_t roi;
  6. primitives_t* prims = primitives_get();
  7. BYTE* coded;
  8. UINT32 codedSize;
  9. if (!h264)
  10. return -1;
  11. if (!h264->subsystem->Compress)
  12. return -1;
  13. if (!avc420_ensure_buffer(h264, nSrcStep, nSrcWidth, nSrcHeight))
  14. return -1;

设置h264->iStride[0:2]、h264->width、h264->heght,分配h264->pYUVData[0:2]的三块内存,每内存块字节数是对应iStride[i]*height。iStride[0:2]指示YUV三分量行距,分别是nSrcStep、nSrcStep/2、nSrcStep/2。这会让人疑惑,对YUV420P,Y分量只占1字节,Y行距不是应该width?对UV分量,四个Y共用一组U、V,它们各自平面的行距不是应该width/2?——avc444_ensure_buffer后会解答这个疑问。

  1. if (!avc444_ensure_buffer(h264, nSrcHeight))
  2. return -1;

设置h264->iYUV444Stride[0:2]、h264->iYUV444Size[0:2],分配h264->pYUV444Data[0:2]的三块内存,每内存块字节数是对应iYUV444Size[i]。iYUV444Stride[0:2]三个单元值都是h264->iStride[0],iYUV444Size是iYUV444Stride*height。

h264->iYUV444Stride[0:2]值和h264->iStride[0:2]有着一样的疑问,YUV三分量的行距是否设得过大了?——是的。对图3中红色标示的YUV和四个像素,Ya、Yb分别是第一行a、b的Y值,Yc、Yd分别是第二行c、d的Y值。当Y内存块的行距是4*width时,有3*width是不使用的。U是第1、2行Ua、Ub、Uc、Ud的平均值。当U内存块的行距是2*width时,有3/2是不使用的。V是第1、2行Va、Vb、Vc、Vd的平均值。当V内存块的行距是2*width时,有3/2是不使用的。虽然YUV行距没严格遵从YUV420P要求的width、width/2、width/2,但只要write和read时都以一样值作为行距,就不会出错。创建时内存块比read/write用更大的行距,无非就是浪费了内存。

经过avc420_ensure_buffer、avc444_ensure_buffer后,肯定已存在pYUVData[0:2]、pYUV444Data[0:2]这两处、共6块独立内存。

  1. roi.width = nSrcWidth;
  2. roi.height = nSrcHeight;
  3. switch (version)
  4. {
  5. case 1:
  6. if (prims->RGBToAVC444YUV(pSrcData, SrcFormat, nSrcStep, h264->pYUV444Data,
  7. h264->iStride, h264->pYUVData, h264->iStride,
  8. &roi) != PRIMITIVES_SUCCESS)
  9. return -1;
  10. break;
  11. case 2:
  12. if (prims->RGBToAVC444YUVv2(pSrcData, SrcFormat, nSrcStep, h264->pYUV444Data,
  13. h264->iStride, h264->pYUVData, h264->iStride,
  14. &roi) != PRIMITIVES_SUCCESS)
  15. return -1;

RGB到YUV420P,生成的YUV420P数据填充向上面创建的6块独立内存。RGBToAVC444YUVv2是个函数指针,会根据当前正在用的cpu支持哪指令集执行特定函数,像intel cpu,极可能会映射到ssse3_RGBToAVC444YUVv2。因为涉及到SSSE3指令,代码有点难懂,为理解方便可让强制映射到general_RGBToAVC444YUVv2。

回看图3,对YUV420P,4个Y对应一组UV,这4个Y涉及到毗邻的两行,因而general_RGBToAVC444YUVv2是以两行为一次计算单位。假设当前行是y,会先计算出数个指示数据地址的变量,之后调用general_RGBToAVC444YUVv2_BGRX_DOUBLE_ROW计算每像素的Y、U、V,后者计算Y、U、V时以两列为一次计算单位。

  • srcEvent: (读)RGB偶数行数据起始地址pSrc + y * srcStep
  • srcOdd: (读)RGB奇数行数据起始地址srcEven + srcStep
  • dstLumaYEven: (写)Main View中Y分量偶行数据起始地址pDst1[0] + y * dst1Step[0]。
  • dstLumaYOdd: (写)Main View中Y分量偶行数据起始地址dstLumaYEven + dst1Step[0]。
  • dstLumaU: (写)Main View中U分量数据起始地址pDst1[1] + (y / 2) * dst1Step[1]。
  • dstLumaV: (写)Main View中V分量数据起始地址pDst1[2] + (y / 2) * dst1Step[2]。Y、U、V是存在三块独立的内存。
  1. break;
  2. default:
  3. return -1;
  4. }
  5. {
  6. const BYTE* pYUV444Data[3] = { h264->pYUV444Data[0], h264->pYUV444Data[1],
  7. h264->pYUV444Data[2] };
  8. if (h264->subsystem->Compress(h264, pYUV444Data, h264->iStride, &coded, &codedSize) < 0)
  9. return -1;

Main View的h264压缩。注意此处行距用的是h264->iStride,而不是h264->iYUV444Stride。对YUV420P,以h264->iStride开出的内存尺寸已足够了。

  1. }
  2. memcpy(h264->lumaData, coded, codedSize);
  3. *ppDstData = h264->lumaData;
  4. *pDstSize = codedSize;
  5. {
  6. const BYTE* pYUVData[3] = { h264->pYUVData[0], h264->pYUVData[1], h264->pYUVData[2] };
  7. if (h264->subsystem->Compress(h264, pYUVData, h264->iStride, &coded, &codedSize) < 0)
  8. return -1;
  9. Auxillary View的h264压缩。
  10. }
  11. *ppAuxDstData = coded;
  12. *pAuxDstSize = codedSize;
  13. *op = 0;
  14. return 0;
  15. }

经过avc444_compress后,压缩后的数据放在函数内变量avc444,类型RDPGFX_AVC444_BITMAP_STREAM。shadow_client_send_surface_gfx再以avc444生成RDPGFX_SURFACE_COMMAND类型的cmd,至此cmd存放着处理此次屏幕帧需要的数据。

2.2 封装、投递到drdynvc_channel->vcm->queue

此过程要经过多次封装,以下是大致逻辑。

  1. 由cmd生成3个dpu,依次为RDPGFX_START_FRAME_PDU、RDPGFX_WIRE_TO_SURFACE_PDU_1、RDPGFX_END_FRAME_PDU,称3pdu负载。
  2. 3pdu负载封装成一个RDP_SEGMENTED_DATA格式的数据块(注:只一个)。3pdu负载将被拆分成数个分段,一个分段存储在一个bulkData中。假设有N个分段,前面N-1个bulkData存65535字节的3pdu负载,第N个存储剩余的字节。称RDP_SEGMENTED_DATA格式的数据块为SEGMENTED负载。
  3. SEGMENTED负载被封装成数个适合Virtual Channel PDU发送的virtualChannelData数块块。注意,它没有生成pdu,只是生成该pdu的virtualChannelData字段。一个virtualChannelData数据块最大1600字节,这1600字节并不全是存着SEGMENTED负载,而是DVC(Dynamic Channel Virtual Channel)格式,因为要前缀DVC消息头,分到每个virtualChannelData的SEGMENTED负载要略小于1600字节。
  4. 从virtualChannelData生成wMessage,每生成一个wMessage就投递到drdynvc_channel->vcm->queue。这里一个对一个了,virtualChannelData地址放在wMessage的wParam,virtualChannelData字节数放在lParam。假设要分成M个virtualChannelData,之前有说virtualChannelData最大1600字节,那前面M-1个的lParam是1600,第M个是剩余字节数。每生成一个wMessage,把它投递到drdynvc_channel->vcm->queue。
  5. 最终数据将被以wMessage的格式存放在drdynvc_channel->vcm->queue。

rdpgfx_send_surface_frame_command

  1. static UINT rdpgfx_send_surface_frame_command(RdpgfxServerContext* context,
  2. const RDPGFX_SURFACE_COMMAND* cmd,
  3. const RDPGFX_START_FRAME_PDU* startFrame,
  4. const RDPGFX_END_FRAME_PDU* endFrame)
  5. {
  6. UINT error = CHANNEL_RC_OK;
  7. wStream* s;
  8. UINT32 position = 0;
  9. UINT32 size = rdpgfx_pdu_length(rdpgfx_estimate_surface_command(cmd));

rdpgfx_estimate_surface_command作用是根据cmd->codecId计算pdu长度。RDPGFX_CODECID_AVC444v2对应要发送的pdu是RDPGFX_WIRE_TO_SURFACE_PDU_1,除header外长度的计算方法是“RDPGFX_WIRE_TO_SURFACE_PDU_1_SIZE + cmd->length”,其中RDPGFX_WIRE_TO_SURFACE_PDU_1_SIZE是除bitmapData外的17字节,cmd->length是bitmapData字节数。但下载的freerdp(2020-4-16),这个值是0!虽然后面在编码bitmapData时会用Stream_EnsureRemainingCapacity增加长度,但这应该是个BUG,解决方法见“4.1 让rdpgfx_estimate_surface_command返回包含bitmapData字段的长度”。

rdpgfx_pdu_length作用是再加上header的8字节,因而返回的size是25+bytes(bitmapData)。它是RDPGFX_WIRE_TO_SURFACE_PDU_1这个pdu的整个长度。

  1. if (startFrame)
  2. {
  3. size += rdpgfx_pdu_length(RDPGFX_START_FRAME_PDU_SIZE);
  4. }

总会发送一整帧,startFrame一定会在的。RDPGFX_START_FRAME_PDU_SIZE是RDPGFX_START_FRAME_PDU中除header外的字节数8。经过计算,size将加16。

  1. if (endFrame)
  2. {
  3. size += rdpgfx_pdu_length(RDPGFX_END_FRAME_PDU_SIZE);
  4. }

总会发送一整帧,endFrame一定会在的。RDPGFX_END_FRAME_PDU_SIZE是RDPGFX_START_FRAME_PDU中除header外的字节数4。经过计算,size将加12。至此size值是53+bytes(bitmapData),内中包括了三个pdu,依次将是RDPGFX_START_FRAME_PDU、RDPGFX_WIRE_TO_SURFACE_PDU_1、RDPGFX_END_FRAME_PDU,为什么这次序,后面会看到。

  1. s = Stream_New(NULL, size);
  2. 创建一条wStream,它将用于产生此次要发送的3 pdu。
  3. if (!s)
  4. {
  5. WLog_ERR(TAG, "Stream_New failed!");
  6. return CHANNEL_RC_NO_MEMORY;
  7. }
  8. /* Write start frame if exists */
  9. if (startFrame)
  10. {
  11. position = Stream_GetPosition(s);
  12. error = rdpgfx_server_packet_init_header(s, RDPGFX_CMDID_STARTFRAME, 0);
  13. if (error != CHANNEL_RC_OK)
  14. {
  15. WLog_ERR(TAG, "Failed to init header with error %" PRIu32 "!", error);
  16. goto error;
  17. }
  18. rdpgfx_write_start_frame_pdu(s, startFrame);
  19. rdpgfx_server_packet_complete_header(s, position);
  20. }

生成RDPGFX_START_FRAME_PDU。rdpgfx_server_packet_init_header写入header,参数RDPGFX_CMDID_STARTFRAME赋值给cmdId,参数0赋值给pduLength,flags固定置0。pduLength=0自然不是正确的结果,后面rdpgfx_server_packet_complete_header会补写上正确值。rdpgfx_write_start_frame_pdu写入RDPGFX_START_FRAME_PDU除header外的部分。rdpgfx_server_packet_complete_header补写正确pduLength,注意pduLength是包括header长度8字节的。内中计算pduLength靠wStream的两次偏移值,开始写header时的偏移position,和此刻写完RDPGFX_START_FRAME_PDU后的偏移,两次偏移值之差正是pduLength。写入pduLength后,要用Stream_SetPosition把偏移设回到当前值,后面要在s继续生成后两个pdu。

  1. /* Write RDPGFX_CMDID_WIRETOSURFACE_1 or RDPGFX_CMDID_WIRETOSURFACE_2 */
  2. position = Stream_GetPosition(s);
  3. error = rdpgfx_server_packet_init_header(s, rdpgfx_surface_command_cmdid(cmd),
  4. 0); // Actual length will be filled later

写入RDPGFX_CMDID_WIRETOSURFACE_1的header。老规矩,pduLength先置0,等后面整个PDU写入了,靠rdpgfx_server_packet_complete_header补写。有了写RDPGFX_START_FRAME_PDU经验,直觉会认为这个position是补写RDPGFX_CMDID_WIRETOSURFACE_1的pduLength用的,事实正是如此。

  1. if (error != CHANNEL_RC_OK)
  2. {
  3. WLog_ERR(TAG, "Failed to init header with error %" PRIu32 "!", error);
  4. goto error;
  5. }
  6. error = rdpgfx_write_surface_command(s, cmd);

根据cmd(RDPGFX_SURFACE_COMMAND)数据,将RDPGFX_CMDID_WIRETOSURFACE_1或RDPGFX_CMDID_WIRETOSURFACE_2写入流s。

  1. if (error != CHANNEL_RC_OK)
  2. {
  3. WLog_ERR(TAG, "rdpgfx_write_surface_command failed!");
  4. goto error;
  5. }
  6. rdpgfx_server_packet_complete_header(s, position);

补写RDPGFX_WIRE_TO_SURFACE_PDU_1中pduLength。

  1. /* Write end frame if exists */
  2. if (endFrame)
  3. {
  4. position = Stream_GetPosition(s);
  5. error = rdpgfx_server_packet_init_header(s, RDPGFX_CMDID_ENDFRAME, 0);
  6. if (error != CHANNEL_RC_OK)
  7. {
  8. WLog_ERR(TAG, "Failed to init header with error %" PRIu32 "!", error);
  9. goto error;
  10. }
  11. rdpgfx_write_end_frame_pdu(s, endFrame);
  12. rdpgfx_server_packet_complete_header(s, position);
  13. }

生成RDPGFX_END_FRAME_PDU。至此s依次包含RDPGFX_START_FRAME_PDU、RDPGFX_WIRE_TO_SURFACE_PDU_1、RDPGFX_END_FRAME_PDU这三个pdu的完整数据。

  1. return rdpgfx_server_packet_send(context, s);
  2. 封装3pdu负载、投递到drdynvc_channel->vcm->queue。
  3. error:
  4. Stream_Free(s, TRUE);
  5. return error;
  6. }

rdpgfx_write_surface_command

根据cmd(RDPGFX_SURFACE_COMMAND)数据,将RDPGFX_CMDID_WIRETOSURFACE_1或RDPGFX_CMDID_WIRETOSURFACE_2写入流s。

  1. static UINT rdpgfx_write_surface_command(wStream* s, const RDPGFX_SURFACE_COMMAND* cmd)
  2. {
  3. UINT error = CHANNEL_RC_OK;
  4. RDPGFX_AVC420_BITMAP_STREAM* havc420 = NULL;
  5. RDPGFX_AVC444_BITMAP_STREAM* havc444 = NULL;
  6. UINT32 bitmapDataStart = 0;
  7. UINT32 bitmapDataLength = 0;
  8. {

cmd->codecId(=RDPGFX_CODECID_AVC444V2)不是RDPGFX_CODECID_CAPROGRESSIVE、RDPGFX_CODECID_CAPROGRESSIVE_V2时入口。

  1. /* Write RDPGFX_CMDID_WIRETOSURFACE_1 format for others */
  2. Stream_Write_UINT16(s, cmd->surfaceId); /* surfaceId (2 bytes) */
  3. freerdp对cmd->surfaceId总是置0
  4. Stream_Write_UINT16(s, cmd->codecId); /* codecId (2 bytes) */
  5. RDPGFX_CODECID_AVC444V2。表示工作在YUV444v2模式,使用MPEG-4 AVC/H.264压缩图像数据,压缩后的数据封装在bitmapData字段。
  6. Stream_Write_UINT8(s, pixelFormat); /* pixelFormat (1 byte) */
  7. pixelFormat值GFX_PIXEL_FORMAT_XRGB_8888(0x20),对应[MS-RDPEGFX]中的PIXEL_FORMAT_XRGB_8888
  8. Stream_Write_UINT16(s, cmd->left); /* left (2 bytes) */
  9. Stream_Write_UINT16(s, cmd->top); /* top (2 bytes) */
  10. Stream_Write_UINT16(s, cmd->right); /* right (2 bytes) */
  11. Stream_Write_UINT16(s, cmd->bottom); /* bottom (2 bytes) */
  12. 屏幕矩形,它总是屏尺寸。对2560x1600时,值分别是0025601600
  13. Stream_Write_UINT32(s, cmd->length); /* bitmapDataLength (4 bytes) */
  14. 此时cmd->length值是0,对应[MS-RDPEGFX]中的bitmapDataLength。
  15. bitmapDataStart = Stream_GetPosition(s);

要开始填充bitmapData字段了,记住此刻的s偏移,后面回填正确的bitmapDataLength值要用到。

  1. if (cmd->codecId == RDPGFX_CODECID_AVC420)
  2. {
  3. havc420 = (RDPGFX_AVC420_BITMAP_STREAM*)cmd->extra;
  4. error = rdpgfx_write_h264_avc420(s, havc420);
  5. if (error != CHANNEL_RC_OK)
  6. {
  7. WLog_ERR(TAG, "rdpgfx_write_h264_avc420 failed!");
  8. return error;
  9. }
  10. }
  11. else if ((cmd->codecId == RDPGFX_CODECID_AVC444) ||
  12. (cmd->codecId == RDPGFX_CODECID_AVC444v2))
  13. {

cmd->codecId是RDPGFX_CODECID_AVC444v2,此模式下bitmapData字段语法在“[MS-RDPEGFX] 2.2.4.6 RFX_AVC444V2_BITMAP_STREAM”。

  1. havc444 = (RDPGFX_AVC444_BITMAP_STREAM*)cmd->extra;
  2. havc420 = &(havc444->bitstream[0]); /* avc420EncodedBitstreamInfo (4 bytes) */

havc444->bitstream[0]存储的是h264压缩后的主视图数据。

			Stream_Write_UINT32(s, havc444->cbAvc420EncodedBitstream1 | (havc444->LC << 30UL));

先写入4字节的avc420EncodedBitstreamInfo。一个32位无符号整数,由两部分组成,cbAvc420EncodedBitstream1用于指定avc420EncodedBitstream1字段中存在的数据大小,LC描述码流中要存在哪些子帧(LC)。freerdp设的LC值是0,表示有两个子帧,avc420EncodedBitstream1包含主视图,avc420EncodedBitstream2字段则包含次视图。

  1. /* avc420EncodedBitstream1 */
  2. error = rdpgfx_write_h264_avc420(s, havc420);
  3. 写入第一个子帧avc420EncodedBitstream1:主视图(YUV420)
  4. if (error != CHANNEL_RC_OK)
  5. {
  6. WLog_ERR(TAG, "rdpgfx_write_h264_avc420 failed!");
  7. return error;
  8. }
  9. /* avc420EncodedBitstream2 */
  10. if (havc444->LC == 0)
  11. {
  12. havc420 = &(havc444->bitstream[1]);
  13. error = rdpgfx_write_h264_avc420(s, havc420);
  14. 写入第二个子帧avc420EncodedBitstream2:次视图(Chroma420)
  15. if (error != CHANNEL_RC_OK)
  16. {
  17. WLog_ERR(TAG, "rdpgfx_write_h264_avc420 failed!");
  18. return error;
  19. }
  20. }
  21. }
  22. else
  23. {
  24. Stream_Write(s, cmd->data, cmd->length);
  25. }
  26. /* Fill actual bitmap data length */
  27. bitmapDataLength = Stream_GetPosition(s) - bitmapDataStart;
  28. Stream_SetPosition(s, bitmapDataStart - sizeof(UINT32));
  29. Stream_Write_UINT32(s, bitmapDataLength); /* bitmapDataLength (4 bytes) */
  30. Stream_Seek(s, bitmapDataLength);

回填正确的bitmapDataLength值,写完后要把偏移加上bitmapDataLength,即回到写完bitmapData字段后的当前位置。

  1. }
  2. return error;
  3. }

写入264子帧,此函数对应的语法在“[MS-RDPEGFX] 2.2.4.4 RFX_AVC420_BITMAP_STREAM”。一个avc420EncodedBitstream分两部分,avc420MetaData和avc420EncodedBitstream。

rdpgfx_write_h264_avc420

havc420存储着经过h264压缩后的数据,rdpgfx_write_h264_avc420把这h264流写入s。

  1. static INLINE UINT rdpgfx_write_h264_avc420(wStream* s, RDPGFX_AVC420_BITMAP_STREAM* havc420)
  2. {
  3. UINT error = CHANNEL_RC_OK;
  4. if ((error = rdpgfx_write_h264_metablock(s, &(havc420->meta))))
  5. {
  6. WLog_ERR(TAG, "rdpgfx_write_h264_metablock failed with error %" PRIu32 "!", error);
  7. return error;
  8. }

avc420EncodedBitstream的第一部分:avc420MetaData。avc420MetaData是帧概述信息,包括有多少个矩形(因为总是编码整个帧,值总是1个),每个矩形的4位置分量,还有quantQualityVals。qp是量化值,像137,来自encoder->h264->QP。meta部分的字节数:4 + meta->numRegionRects * 10,meta->numRegionRects是1,后面10包括概述矩形4分量的8字节、quantQualityVals的2字节。

  1. if (!Stream_EnsureRemainingCapacity(s, havc420->length))
  2. return ERROR_OUTOFMEMORY;
  3. Stream_Write(s, havc420->data, havc420->length);

avc420EncodedBitstream的第二部分:avc420EncodedBitstream。终于到了在s上生成h264压缩后数据。

  1. return error;
  2. }

rdpgfx_server_packet_send

3pdu负载存放在了s,接下rdpgfx_server_packet_send要封装3pdu负载,并投递到drdynvc_channel->vcm->queue。

  1. static UINT rdpgfx_server_packet_send(RdpgfxServerContext* context, wStream* s)
  2. {
  3. UINT error;
  4. UINT32 flags = 0;
  5. ULONG written;
  6. BYTE* pSrcData = Stream_Buffer(s);
  7. UINT32 SrcSize = Stream_GetPosition(s);
  8. wStream* fs;
  9. /* Allocate new stream with enough capacity. Additional overhead is
  10. * descriptor (1 bytes) + segmentCount (2 bytes) + uncompressedSize (4 bytes)
  11. * + segmentCount * size (4 bytes) */
  12. fs = Stream_New(NULL, SrcSize + 7 + (SrcSize / ZGFX_SEGMENTED_MAXSIZE + 1) * 4);

7字节是RDP_SEGMENTED_DATA中的descriptor(1) + segmentCount(2) + uncompressedSize(4)。ZGFX_SEGMENTED_MAXSIZE是65535。4是每个RDP_DATA_SEGMENT除3pdu负载外还有4字节的size。注:这里没包括1字节的bulkData.header,等不够时是可由Stream_EnsureRemainingCapacity扩展。但个人强烈建议修正,见“4.2 rdpgfx_server_packet_send时,让每个分段附加字节数是5个”。

  1. if (!fs)
  2. {
  3. WLog_ERR(TAG, "Stream_New failed!");
  4. error = CHANNEL_RC_NO_MEMORY;
  5. goto out;
  6. }
  7. if (zgfx_compress_to_stream(context->priv->zgfx, fs, pSrcData, SrcSize, &flags) < 0)
  8. {
  9. WLog_ERR(TAG, "zgfx_compress_to_stream failed!");
  10. error = ERROR_INTERNAL_ERROR;
  11. goto out;
  12. }

对s中的数据进行分段,封装到一个RDP_SEGMENTED_DATA结构的块中,语法对应“[MS-RDPEGFX] 2.2.5.1 RDP_SEGMENTED_DATA”。假设要分N段,0到N-1段承担的3pdu负载字数节都是65535,第N段是剩余字节。以SrcSize=322354为例,将分为5段(N=5),前4段负载65535字节,第5段60214字节。经过此函数,fs存储着SEGMENTED负载。

  1. if (!WTSVirtualChannelWrite(context->priv->rdpgfx_channel, (PCHAR)Stream_Buffer(fs),
  2. Stream_GetPosition(fs), &written))
  3. {
  4. WLog_ERR(TAG, "WTSVirtualChannelWrite failed!");
  5. error = ERROR_INTERNAL_ERROR;
  6. goto out;
  7. }

把SEGMENTED负载封装成数个适合Virtual Channel PDU发送的virtualChannelData数块块。每virtualChannelData生成一个wMessage,投递到drdynvc_channel->vcm->queue。

  1. if (written < Stream_GetPosition(fs))
  2. {
  3. WLog_WARN(TAG, "Unexpected bytes written: %" PRIu32 "/%" PRIuz "", written,
  4. Stream_GetPosition(fs));
  5. }
  6. error = CHANNEL_RC_OK;
  7. out:
  8. Stream_Free(fs, TRUE);
  9. Stream_Free(s, TRUE);
  10. return error;
  11. }

zgfx_compress_to_stream

pUncompressed存储着3pdu负载,uncompressedSize是负载字节数,zgfx_compress_to_stream要把3pdu换载封装成一个SEGMENTED负载,放在sDst这个wStream中。

  1. int zgfx_compress_to_stream(ZGFX_CONTEXT* zgfx, wStream* sDst, const BYTE* pUncompressed,
  2. UINT32 uncompressedSize, UINT32* pFlags)
  3. {
  4. int fragment;
  5. UINT16 maxLength;
  6. UINT32 totalLength;
  7. size_t posSegmentCount = 0;
  8. const BYTE* pSrcData;
  9. int status = 0;
  10. maxLength = ZGFX_SEGMENTED_MAXSIZE;
  11. totalLength = uncompressedSize;
  12. pSrcData = pUncompressed;
  13. for (fragment = 0; (totalLength > 0) || (fragment == 0); fragment++)
  14. {
  15. UINT32 SrcSize;
  16. size_t posDstSize;
  17. size_t posDataStart;
  18. UINT32 DstSize;
  19. SrcSize = (totalLength > maxLength) ? maxLength : totalLength;
  20. posDstSize = 0;
  21. totalLength -= SrcSize;
  22. /* Ensure we have enough space for headers */
  23. if (!Stream_EnsureRemainingCapacity(sDst, 12))
  24. {
  25. WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!");
  26. return -1;
  27. }
  28. SEGMENTED负载语法在“[MS-RDPEGFX] 2.2.5.1 RDP_SEGMENTED_DATA”。
  29. if (fragment == 0)
  30. {
  31. /* First fragment */
  32. /* descriptor (1 byte) */
  33. Stream_Write_UINT8(sDst, (totalLength == 0) ? ZGFX_SEGMENTED_SINGLE
  34. : ZGFX_SEGMENTED_MULTIPART);
  35. 这是第一分段,totalLength == 0意味着此次3pdu负载一个分段就够了。
  36. if (totalLength > 0)
  37. {
  38. posSegmentCount = Stream_GetPosition(sDst); /* segmentCount (2 bytes) */
  39. Stream_Seek(sDst, 2);
  40. Stream_Write_UINT32(sDst, uncompressedSize); /* uncompressedSize (4 bytes) */

将存在多个分段,多个分段时要多出segmentCount、uncompressedSize字段。对segmentCount,此时我们不知道值会是多少,先记住偏移,后面会补写。uncompressedSize填的就是3pdu负载字节数。

  1. }
  2. }
  3. if (fragment > 0 || totalLength > 0)
  4. {
  5. /* Multipart */
  6. posDstSize = Stream_GetPosition(sDst); /* size (4 bytes) */
  7. Stream_Seek(sDst, 4);

fragment>0表示第二个或更后分段,totalLength>0表示后面还有分段,这意味多分段时的第一个分段也会进入这里。“[MS-RDPEFGX] 2.2.5.1 RDP_SEGMENTED_DATA”中的RDP_SEGMENTED_DATA语法,segmentArray之前是bulkData字段。当只有一个分段,存在bulkData。一旦多个分段它就不存在了,后直接跟segmentArray,多个分段时,segmentArray中RDP_SEGMENTED_DATA个数正是分段数(The number of elements in this array is specified by the segmentCount field)。

  1. }
  2. posDataStart = Stream_GetPosition(sDst);
  3. if (!zgfx_compress_segment(zgfx, sDst, pSrcData, SrcSize, pFlags))
  4. return -1;

向fs写入RDP_SEGMENTED_DATA(fragment=0时)/RDP_DATA_SEGMENT(fragment>=1时)中的bulkData字段,bulkData语法在“[MS-RDPEGFX] 2.2.5.3 RDP8_BULK_ENCODED_DATA”。会写1字节header、此个分段要承担的SrcSize字节的3pdu负载。

  1. if (posDstSize)
  2. {
  3. /* Fill segment data size */
  4. DstSize = Stream_GetPosition(sDst) - posDataStart;
  5. Stream_SetPosition(sDst, posDstSize);
  6. Stream_Write_UINT32(sDst, DstSize);
  7. Stream_SetPosition(sDst, posDataStart + DstSize);

修正RDP_DATA_SEGMENT中的size字段,它指示紧跟的bulkData字段的字节数。再说下,只有第二个或之后分段才有RDP_DATA_SEGMENT。

  1. }
  2. pSrcData += SrcSize;
  3. }
  4. Stream_SealLength(sDst);
  5. /* fill back segmentCount */
  6. if (posSegmentCount)
  7. {
  8. Stream_SetPosition(sDst, posSegmentCount);
  9. Stream_Write_UINT16(sDst, fragment);
  10. Stream_SetPosition(sDst, Stream_Length(sDst));
  11. }
  12. return status;
  13. }

FreeRDP_WTSVirtualChannelWrite

Buffer存储着SEGMENTED负载,Length是负载字节数,FreeRDP_WTSVirtualChannelWrite要把pUncompressed封装成数个适合Virtual Channel PDU发送的virtualChannelData数据块。每virtualChannelData生成一个wMessage,投递到drdynvc_channel->vcm->queue。

  1. BOOL WINAPI FreeRDP_WTSVirtualChannelWrite(HANDLE hChannelHandle, PCHAR Buffer, ULONG Length,
  2. PULONG pBytesWritten)
  3. {
  4. wStream* s;
  5. int cbLen;
  6. int cbChId;
  7. int first;
  8. BYTE* buffer;
  9. UINT32 length;
  10. UINT32 written;
  11. UINT32 totalWritten = 0;
  12. rdpPeerChannel* channel = (rdpPeerChannel*)hChannelHandle;
  13. BOOL ret = TRUE;
  14. ...
  15. {
  16. first = TRUE;
  17. while (Length > 0)
  18. {
  19. s = Stream_New(NULL, channel->client->settings->VirtualChannelChunkSize);

VirtualChannelChunkSize的值是1600。“[MS-RDPPBCGR] 2.2.6.1 Virtual Channel PDU CHANNEL_CHUNK_LENGTH”限制了virtualChannelData不能超过CHANNEL_CHUNK_LENGTH(1600)字节。

  1. if (!s)
  2. {
  3. WLog_ERR(TAG, "Stream_New failed!");
  4. SetLastError(E_OUTOFMEMORY);
  5. return FALSE;
  6. }

此处生成的整个buffer只对应Virtual Channel PDU中VirtualChannelChunkSize字段。当中的格式是DVC(Dynamic Channel Virtual Channel)消息([MS-RDPEDYC])。

  1. buffer = Stream_Buffer(s);
  2. Stream_Seek_UINT8(s);
  3. cbChId = wts_write_variable_uint(s, channel->channelId);
  4. if (first && (Length > (UINT32)Stream_GetRemainingLength(s)))
  5. {
  6. cbLen = wts_write_variable_uint(s, Length);
  7. buffer[0] = (DATA_FIRST_PDU << 4) | (cbLen << 2) | cbChId;

DVC Data First PDU的语法在“[MS-RDPEDYC] 2.2.3.1 DVC Data First PDU (DYNVC_DATA_FIRST)”。

  1. }
  2. else
  3. {
  4. buffer[0] = (DATA_PDU << 4) | cbChId;
  5. }
  6. first = FALSE;
  7. written = Stream_GetRemainingLength(s);
  8. if (written > Length)
  9. written = Length;
  10. Stream_Write(s, Buffer, written);
  11. written是此次取出的SEGMENTED负载长度,因为有DVC消息头,它略小于1600
  12. length = Stream_GetPosition(s);
  13. Stream_Free(s, FALSE);
  14. Stream_Free第二个参数是FALSE,表示只释放wStream这个struct,不释放当中的buffer。
  15. Length -= written;
  16. Buffer += written;
  17. totalWritten += written;
  18. ret = wts_queue_send_item(channel->vcm->drdynvc_channel, buffer, length);
  19. 把此次written字节数的SEGMENTED负载投递到drdynvc_channel的队列。
  20. }
  21. }
  22. if (pBytesWritten)
  23. *pBytesWritten = totalWritten;
  24. 要是不出错,pBytesWritten值应该等于此次要发送的SEGMENTED负载字节数Length
  25. return ret;
  26. }
  27. static BOOL wts_queue_send_item(rdpPeerChannel* channel, BYTE* Buffer, UINT32 Length)
  28. {
  29. BYTE* buffer;
  30. UINT32 length;
  31. UINT16 channelId;
  32. buffer = Buffer;
  33. length = Length;
  34. channelId = channel->channelId;
  35. return MessageQueue_Post(channel->vcm->queue, (void*)(UINT_PTR)channelId, 0, (void*)buffer,
  36. (void*)(UINT_PTR)length);
  37. }
  • message.context。通道号(channelId)。像1007。
  • message.id。固定0
  • message.wParam。virtualChannelData数据。
  • message.lParam。virtualChannelData数据的字节数,当要拆成多个pdu时,前面的pdu这个值是1600。

此个virtualChannelData投递到channel->vcm->drdynvc_channel->vcm->queue,同时queue->event将被置为有信号。至此virtualChannelData只是被放到drdynvc_channel->vcm下的queue,没有生成Virtual Channel PDU,更没有通过网络发送出去。

三、发送

  1. static DWORD WINAPI shadow_client_thread(LPVOID arg) {
  2. ...
  3. ChannelEvent = WTSVirtualChannelManagerGetEventHandle(client->vcm);
  4. while (1) {
  5. if (WaitForSingleObject(ChannelEvent, 0) == WAIT_OBJECT_0)
  6. {
  7. if (!WTSVirtualChannelManagerCheckFileDescriptor(client->vcm))
  8. {
  9. WLog_ERR(TAG, "WTSVirtualChannelManagerCheckFileDescriptor failure");
  10. goto fail;
  11. }
  12. }
  13. ...
  14. }
  15. }

ChannelEvent就是drdynvc_channel->vcm->queue下的event。一旦有virtualChannelData投递到queue,ChannelEvent被触发,于是执行WTSVirtualChannelManagerCheckFileDescriptor。后者取出wMessage,wParam得到数据、lParam得到数据长度,生成一个Virtual Channel PDU,数据放在virtualChannelData、长度放在channelPduHeader.length,发向网络。

既然virtualChannelData投递到queue,和从queue取出、通过网络发送都是在同一线程,为什么不投递就直接发呢?——猜测可能是freerdp希望做到让会话线程的网络发送和子系统线程的抓帧可以并发执行。要是把网络发送放在rdpgfx_send_surface_frame_command,执行这函数时会阻塞子系统线程的,那意味着子系统线程将阻塞更长时间,从而影响抓屏。

四、几处修改

4.1 让rdpgfx_estimate_surface_command返回包含bitmapData字段的长度。

return RDPGFX_WIRE_TO_SURFACE_PDU_1_SIZE + cmd->length;

上面是cmd->codecId是RDPGFX_CODECID_AVC444v2时,计算长度会用的公式,按语义,cmd->length应该返回bitmapData字段字节数,但实际是0。改法是让cmd->length反映正确值。

  1. <freerdp>/server/shadow/shadow_client.c
  2. static INLINE UINT32 rdpgfx_estimate_bitmapdata_avc444v2(RDPGFX_SURFACE_COMMAND* cmd)
  3. {
  4. RDPGFX_AVC420_BITMAP_STREAM* havc420 = NULL;
  5. RDPGFX_AVC444_BITMAP_STREAM* havc444 = NULL;
  6. UINT32 h264Size = 0;
  7. if (cmd->codecId == RDPGFX_CODECID_AVC444v2) {
  8. havc444 = (RDPGFX_AVC444_BITMAP_STREAM*)cmd->extra;
  9. h264Size = sizeof(UINT32); /* cbAvc420EncodedBitstream1 */
  10. /* avc420EncodedBitstream1 */
  11. havc420 = &(havc444->bitstream[0]);
  12. h264Size += rdpgfx_estimate_h264_avc420(havc420);
  13. /* avc420EncodedBitstream2 */
  14. if (havc444->LC == 0)
  15. {
  16. havc420 = &(havc444->bitstream[1]);
  17. h264Size += rdpgfx_estimate_h264_avc420(havc420);
  18. }
  19. }
  20. return h264Size;
  21. }
  22. static BOOL shadow_client_send_surface_gfx(...)
  23. {
  24. ...
  25. if (settings->GfxAVC444 || settings->GfxAVC444v2) {
  26. cmd.extra = (void*)&avc444;
  27. cmd.length = rdpgfx_estimate_bitmapdata_avc444v2(&cmd);
  28. ...
  29. }
  30. ...
  31. }

修改都在shadow_client.c。1)增加函数rdpgfx_estimate_bitmapdata_avc444v2,用于计算AVC444v2时bitmapData长度。2)在“cmd.extra = (void*)&avc444”后,增加一条cmd.length赋值。

要是不修改此处,有可能造成非法内存访问;而且预先计入bitmapData,可让之后Stream_EnsureRemainingCapacity不会触发buffer重分配。以下是一个造成非法内存访问示例,假设bitstream[0]是40字节,不传bitstream[1](LC=1)。

  1. 因为cmd->length=0,rdpgfx_send_surface_command内变量size=53,即s->capability = 53
  2. 41字节。start_frame+SURFACE_PDU_1除bitmapData外的字节数。
  3. + 18字节。4字节cbAvc420EncodedBitstream1 + (4字节矩形数 + numRegionRects*10)。其中numRegionRects=1
  4. + 40字节。bitstream[0].length。由于53不够存了,此步骤s->capability被加大一倍到106
  5. + 12字节。end_frame。之前累计99字节,加上12字节就超过106了,而写end_frame时不会调用Stream_EnsureRemainingCapacity。

4.2 rdpgfx_server_packet_send时,让每个分段附加字节数是5个

若不改,应该不致于造成非法问题,但会导致后绪的Stream_EnsureRemainingCapacity会重分配buffer。

  1. static UINT rdpgfx_server_packet_send(RdpgfxServerContext* context, wStream* s) {
  2. fs = Stream_New(NULL, SrcSize + 7 + (SrcSize / ZGFX_SEGMENTED_MAXSIZE + 1) * 4);
  3. 改为
  4. fs = Stream_New(NULL, SrcSize + 7 + (SrcSize / ZGFX_SEGMENTED_MAXSIZE + 1) * 5);
  5. }

每个RDP_DATA_SEGMENT除3pdu负载外,会有4字节的size和1字节的bulkData.header。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/402789
推荐阅读
相关标签
  

闽ICP备14008679号