赞
踩
在windows,编译freerdp会生成两个server:wfreerdp-server.exe和freerdp-shadow-cli.exe。
从流畅角度,windows自然推荐使用wfreerdp-server.exe,但考虑到有人学FreeRDP是为在其它平台上写rdp server,像Android,那些平台很难使用RemoteFX,为此本文介绍能使用H264压缩的freerdp-shadow-cli.exe。有说到该exe抓屏支持dxgi或wds,本文不深入抓屏技术,但考虑到wds要比dxgi复杂很多,如果只是想搞清抓屏后面逻辑、又不在乎抓到图像是否正确,做以下修改可让工作在dxgi。
- 1、<FreeRDP>/server/shadow/Win/win_dxgi.h,去掉注释WITH_DXGI_1_2宏。
- 2、<FreeRDP>/server/shadow/Win/win_dxgi.c。
- textureDesc.Width = subsystem->width;
- textureDesc.Height = subsystem->height;
- 改为
- textureDesc.Width = subsystem->base.monitors[0].right - subsystem->base.monitors[0].left;
- textureDesc.Height = subsystem->base.monitors[0].bottom - subsystem->base.monitors[0].top;
- 注:估计freerdp已不再用subsystem->width、subsystem->height,它们总是0。monitors[0]存放着屏幕尺寸。width、height不对时,接下的CreateTexture2D会返回失败。
- 3、不编译<FreeRDP>/server/shadow/Win/win_rdp.c、<FreeRDP>/server/shadow/Win/win_wds.c
- 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会执行以下赋值。
- settings->GfxSmallCache = (flags & RDPGFX_CAPS_FLAG_SMALL_CACHE);
- 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如何处理图像,把过程分三个阶段:抓屏、编码、发送。
一、抓屏
图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数。
二、编码
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压缩
RGB转成YUV420P、h264压缩都是在avc444_compress执行的,以下是参数。
- INT32 avc444_compress(H264_CONTEXT* h264, const BYTE* pSrcData, DWORD SrcFormat, UINT32 nSrcStep,
- UINT32 nSrcWidth, UINT32 nSrcHeight, BYTE version, BYTE* op, BYTE** ppDstData,
- UINT32* pDstSize, BYTE** ppAuxDstData, UINT32* pAuxDstSize)
- {
- prim_size_t roi;
- primitives_t* prims = primitives_get();
- BYTE* coded;
- UINT32 codedSize;
-
- if (!h264)
- return -1;
-
- if (!h264->subsystem->Compress)
- return -1;
-
- if (!avc420_ensure_buffer(h264, nSrcStep, nSrcWidth, nSrcHeight))
- 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后会解答这个疑问。
- if (!avc444_ensure_buffer(h264, nSrcHeight))
- 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块独立内存。
- roi.width = nSrcWidth;
- roi.height = nSrcHeight;
-
- switch (version)
- {
- case 1:
- if (prims->RGBToAVC444YUV(pSrcData, SrcFormat, nSrcStep, h264->pYUV444Data,
- h264->iStride, h264->pYUVData, h264->iStride,
- &roi) != PRIMITIVES_SUCCESS)
- return -1;
-
- break;
-
- case 2:
- if (prims->RGBToAVC444YUVv2(pSrcData, SrcFormat, nSrcStep, h264->pYUV444Data,
- h264->iStride, h264->pYUVData, h264->iStride,
- &roi) != PRIMITIVES_SUCCESS)
- 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时以两列为一次计算单位。
- break;
-
- default:
- return -1;
- }
-
- {
- const BYTE* pYUV444Data[3] = { h264->pYUV444Data[0], h264->pYUV444Data[1],
- h264->pYUV444Data[2] };
-
- if (h264->subsystem->Compress(h264, pYUV444Data, h264->iStride, &coded, &codedSize) < 0)
- return -1;
Main View的h264压缩。注意此处行距用的是h264->iStride,而不是h264->iYUV444Stride。对YUV420P,以h264->iStride开出的内存尺寸已足够了。
- }
-
- memcpy(h264->lumaData, coded, codedSize);
- *ppDstData = h264->lumaData;
- *pDstSize = codedSize;
- {
- const BYTE* pYUVData[3] = { h264->pYUVData[0], h264->pYUVData[1], h264->pYUVData[2] };
-
- if (h264->subsystem->Compress(h264, pYUVData, h264->iStride, &coded, &codedSize) < 0)
- return -1;
- Auxillary View的h264压缩。
- }
- *ppAuxDstData = coded;
- *pAuxDstSize = codedSize;
- *op = 0;
- return 0;
- }
经过avc444_compress后,压缩后的数据放在函数内变量avc444,类型RDPGFX_AVC444_BITMAP_STREAM。shadow_client_send_surface_gfx再以avc444生成RDPGFX_SURFACE_COMMAND类型的cmd,至此cmd存放着处理此次屏幕帧需要的数据。
2.2 封装、投递到drdynvc_channel->vcm->queue
此过程要经过多次封装,以下是大致逻辑。
rdpgfx_send_surface_frame_command
- static UINT rdpgfx_send_surface_frame_command(RdpgfxServerContext* context,
- const RDPGFX_SURFACE_COMMAND* cmd,
- const RDPGFX_START_FRAME_PDU* startFrame,
- const RDPGFX_END_FRAME_PDU* endFrame)
-
- {
- UINT error = CHANNEL_RC_OK;
- wStream* s;
- UINT32 position = 0;
- 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的整个长度。
- if (startFrame)
- {
- size += rdpgfx_pdu_length(RDPGFX_START_FRAME_PDU_SIZE);
- }
总会发送一整帧,startFrame一定会在的。RDPGFX_START_FRAME_PDU_SIZE是RDPGFX_START_FRAME_PDU中除header外的字节数8。经过计算,size将加16。
- if (endFrame)
- {
- size += rdpgfx_pdu_length(RDPGFX_END_FRAME_PDU_SIZE);
- }
总会发送一整帧,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,为什么这次序,后面会看到。
- s = Stream_New(NULL, size);
- 创建一条wStream,它将用于产生此次要发送的3 pdu。
- if (!s)
- {
- WLog_ERR(TAG, "Stream_New failed!");
- return CHANNEL_RC_NO_MEMORY;
- }
-
- /* Write start frame if exists */
- if (startFrame)
- {
- position = Stream_GetPosition(s);
- error = rdpgfx_server_packet_init_header(s, RDPGFX_CMDID_STARTFRAME, 0);
-
- if (error != CHANNEL_RC_OK)
- {
- WLog_ERR(TAG, "Failed to init header with error %" PRIu32 "!", error);
- goto error;
- }
-
- rdpgfx_write_start_frame_pdu(s, startFrame);
- rdpgfx_server_packet_complete_header(s, position);
- }
生成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。
- /* Write RDPGFX_CMDID_WIRETOSURFACE_1 or RDPGFX_CMDID_WIRETOSURFACE_2 */
- position = Stream_GetPosition(s);
- error = rdpgfx_server_packet_init_header(s, rdpgfx_surface_command_cmdid(cmd),
- 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用的,事实正是如此。
- if (error != CHANNEL_RC_OK)
- {
- WLog_ERR(TAG, "Failed to init header with error %" PRIu32 "!", error);
- goto error;
- }
-
- error = rdpgfx_write_surface_command(s, cmd);
根据cmd(RDPGFX_SURFACE_COMMAND)数据,将RDPGFX_CMDID_WIRETOSURFACE_1或RDPGFX_CMDID_WIRETOSURFACE_2写入流s。
- if (error != CHANNEL_RC_OK)
- {
- WLog_ERR(TAG, "rdpgfx_write_surface_command failed!");
- goto error;
- }
-
- rdpgfx_server_packet_complete_header(s, position);
补写RDPGFX_WIRE_TO_SURFACE_PDU_1中pduLength。
- /* Write end frame if exists */
- if (endFrame)
- {
- position = Stream_GetPosition(s);
- error = rdpgfx_server_packet_init_header(s, RDPGFX_CMDID_ENDFRAME, 0);
-
- if (error != CHANNEL_RC_OK)
- {
- WLog_ERR(TAG, "Failed to init header with error %" PRIu32 "!", error);
- goto error;
- }
-
- rdpgfx_write_end_frame_pdu(s, endFrame);
- rdpgfx_server_packet_complete_header(s, position);
- }
生成RDPGFX_END_FRAME_PDU。至此s依次包含RDPGFX_START_FRAME_PDU、RDPGFX_WIRE_TO_SURFACE_PDU_1、RDPGFX_END_FRAME_PDU这三个pdu的完整数据。
- return rdpgfx_server_packet_send(context, s);
- 封装3pdu负载、投递到drdynvc_channel->vcm->queue。
- error:
- Stream_Free(s, TRUE);
- return error;
- }
rdpgfx_write_surface_command
根据cmd(RDPGFX_SURFACE_COMMAND)数据,将RDPGFX_CMDID_WIRETOSURFACE_1或RDPGFX_CMDID_WIRETOSURFACE_2写入流s。
- static UINT rdpgfx_write_surface_command(wStream* s, const RDPGFX_SURFACE_COMMAND* cmd)
- {
- UINT error = CHANNEL_RC_OK;
- RDPGFX_AVC420_BITMAP_STREAM* havc420 = NULL;
- RDPGFX_AVC444_BITMAP_STREAM* havc444 = NULL;
- UINT32 bitmapDataStart = 0;
- UINT32 bitmapDataLength = 0;
- {
cmd->codecId(=RDPGFX_CODECID_AVC444V2)不是RDPGFX_CODECID_CAPROGRESSIVE、RDPGFX_CODECID_CAPROGRESSIVE_V2时入口。
- /* Write RDPGFX_CMDID_WIRETOSURFACE_1 format for others */
- Stream_Write_UINT16(s, cmd->surfaceId); /* surfaceId (2 bytes) */
- freerdp对cmd->surfaceId总是置0。
- Stream_Write_UINT16(s, cmd->codecId); /* codecId (2 bytes) */
- RDPGFX_CODECID_AVC444V2。表示工作在YUV444v2模式,使用MPEG-4 AVC/H.264压缩图像数据,压缩后的数据封装在bitmapData字段。
- Stream_Write_UINT8(s, pixelFormat); /* pixelFormat (1 byte) */
- pixelFormat值GFX_PIXEL_FORMAT_XRGB_8888(0x20),对应[MS-RDPEGFX]中的PIXEL_FORMAT_XRGB_8888。
- Stream_Write_UINT16(s, cmd->left); /* left (2 bytes) */
- Stream_Write_UINT16(s, cmd->top); /* top (2 bytes) */
- Stream_Write_UINT16(s, cmd->right); /* right (2 bytes) */
- Stream_Write_UINT16(s, cmd->bottom); /* bottom (2 bytes) */
- 屏幕矩形,它总是屏尺寸。对2560x1600时,值分别是0、0、2560、1600。
- Stream_Write_UINT32(s, cmd->length); /* bitmapDataLength (4 bytes) */
- 此时cmd->length值是0,对应[MS-RDPEGFX]中的bitmapDataLength。
- bitmapDataStart = Stream_GetPosition(s);
要开始填充bitmapData字段了,记住此刻的s偏移,后面回填正确的bitmapDataLength值要用到。
- if (cmd->codecId == RDPGFX_CODECID_AVC420)
- {
- havc420 = (RDPGFX_AVC420_BITMAP_STREAM*)cmd->extra;
- error = rdpgfx_write_h264_avc420(s, havc420);
-
- if (error != CHANNEL_RC_OK)
- {
- WLog_ERR(TAG, "rdpgfx_write_h264_avc420 failed!");
- return error;
- }
- }
- else if ((cmd->codecId == RDPGFX_CODECID_AVC444) ||
- (cmd->codecId == RDPGFX_CODECID_AVC444v2))
- {
cmd->codecId是RDPGFX_CODECID_AVC444v2,此模式下bitmapData字段语法在“[MS-RDPEGFX] 2.2.4.6 RFX_AVC444V2_BITMAP_STREAM”。
- havc444 = (RDPGFX_AVC444_BITMAP_STREAM*)cmd->extra;
- 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字段则包含次视图。
- /* avc420EncodedBitstream1 */
- error = rdpgfx_write_h264_avc420(s, havc420);
- 写入第一个子帧avc420EncodedBitstream1:主视图(YUV420)
- if (error != CHANNEL_RC_OK)
- {
- WLog_ERR(TAG, "rdpgfx_write_h264_avc420 failed!");
- return error;
- }
-
- /* avc420EncodedBitstream2 */
- if (havc444->LC == 0)
- {
- havc420 = &(havc444->bitstream[1]);
- error = rdpgfx_write_h264_avc420(s, havc420);
- 写入第二个子帧avc420EncodedBitstream2:次视图(Chroma420)
- if (error != CHANNEL_RC_OK)
- {
- WLog_ERR(TAG, "rdpgfx_write_h264_avc420 failed!");
- return error;
- }
- }
- }
- else
- {
- Stream_Write(s, cmd->data, cmd->length);
- }
-
- /* Fill actual bitmap data length */
- bitmapDataLength = Stream_GetPosition(s) - bitmapDataStart;
- Stream_SetPosition(s, bitmapDataStart - sizeof(UINT32));
- Stream_Write_UINT32(s, bitmapDataLength); /* bitmapDataLength (4 bytes) */
- Stream_Seek(s, bitmapDataLength);
回填正确的bitmapDataLength值,写完后要把偏移加上bitmapDataLength,即回到写完bitmapData字段后的当前位置。
- }
-
- return error;
- }
写入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。
- static INLINE UINT rdpgfx_write_h264_avc420(wStream* s, RDPGFX_AVC420_BITMAP_STREAM* havc420)
- {
- UINT error = CHANNEL_RC_OK;
- if ((error = rdpgfx_write_h264_metablock(s, &(havc420->meta))))
- {
- WLog_ERR(TAG, "rdpgfx_write_h264_metablock failed with error %" PRIu32 "!", error);
- return error;
- }
avc420EncodedBitstream的第一部分:avc420MetaData。avc420MetaData是帧概述信息,包括有多少个矩形(因为总是编码整个帧,值总是1个),每个矩形的4位置分量,还有quantQualityVals。qp是量化值,像137,来自encoder->h264->QP。meta部分的字节数:4 + meta->numRegionRects * 10,meta->numRegionRects是1,后面10包括概述矩形4分量的8字节、quantQualityVals的2字节。
- if (!Stream_EnsureRemainingCapacity(s, havc420->length))
- return ERROR_OUTOFMEMORY;
-
- Stream_Write(s, havc420->data, havc420->length);
avc420EncodedBitstream的第二部分:avc420EncodedBitstream。终于到了在s上生成h264压缩后数据。
- return error;
- }
rdpgfx_server_packet_send
3pdu负载存放在了s,接下rdpgfx_server_packet_send要封装3pdu负载,并投递到drdynvc_channel->vcm->queue。
- static UINT rdpgfx_server_packet_send(RdpgfxServerContext* context, wStream* s)
- {
- UINT error;
- UINT32 flags = 0;
- ULONG written;
- BYTE* pSrcData = Stream_Buffer(s);
- UINT32 SrcSize = Stream_GetPosition(s);
- wStream* fs;
- /* Allocate new stream with enough capacity. Additional overhead is
- * descriptor (1 bytes) + segmentCount (2 bytes) + uncompressedSize (4 bytes)
- * + segmentCount * size (4 bytes) */
- 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个”。
- if (!fs)
- {
- WLog_ERR(TAG, "Stream_New failed!");
- error = CHANNEL_RC_NO_MEMORY;
- goto out;
- }
-
- if (zgfx_compress_to_stream(context->priv->zgfx, fs, pSrcData, SrcSize, &flags) < 0)
- {
- WLog_ERR(TAG, "zgfx_compress_to_stream failed!");
- error = ERROR_INTERNAL_ERROR;
- goto out;
- }
对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负载。
- if (!WTSVirtualChannelWrite(context->priv->rdpgfx_channel, (PCHAR)Stream_Buffer(fs),
- Stream_GetPosition(fs), &written))
- {
- WLog_ERR(TAG, "WTSVirtualChannelWrite failed!");
- error = ERROR_INTERNAL_ERROR;
- goto out;
- }
把SEGMENTED负载封装成数个适合Virtual Channel PDU发送的virtualChannelData数块块。每virtualChannelData生成一个wMessage,投递到drdynvc_channel->vcm->queue。
- if (written < Stream_GetPosition(fs))
- {
- WLog_WARN(TAG, "Unexpected bytes written: %" PRIu32 "/%" PRIuz "", written,
- Stream_GetPosition(fs));
- }
-
- error = CHANNEL_RC_OK;
- out:
- Stream_Free(fs, TRUE);
- Stream_Free(s, TRUE);
- return error;
- }
zgfx_compress_to_stream
pUncompressed存储着3pdu负载,uncompressedSize是负载字节数,zgfx_compress_to_stream要把3pdu换载封装成一个SEGMENTED负载,放在sDst这个wStream中。
- int zgfx_compress_to_stream(ZGFX_CONTEXT* zgfx, wStream* sDst, const BYTE* pUncompressed,
- UINT32 uncompressedSize, UINT32* pFlags)
- {
- int fragment;
- UINT16 maxLength;
- UINT32 totalLength;
- size_t posSegmentCount = 0;
- const BYTE* pSrcData;
- int status = 0;
- maxLength = ZGFX_SEGMENTED_MAXSIZE;
- totalLength = uncompressedSize;
- pSrcData = pUncompressed;
-
- for (fragment = 0; (totalLength > 0) || (fragment == 0); fragment++)
- {
- UINT32 SrcSize;
- size_t posDstSize;
- size_t posDataStart;
- UINT32 DstSize;
- SrcSize = (totalLength > maxLength) ? maxLength : totalLength;
- posDstSize = 0;
- totalLength -= SrcSize;
-
- /* Ensure we have enough space for headers */
- if (!Stream_EnsureRemainingCapacity(sDst, 12))
- {
- WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!");
- return -1;
- }
- SEGMENTED负载语法在“[MS-RDPEGFX] 2.2.5.1 RDP_SEGMENTED_DATA”。
- if (fragment == 0)
- {
- /* First fragment */
- /* descriptor (1 byte) */
- Stream_Write_UINT8(sDst, (totalLength == 0) ? ZGFX_SEGMENTED_SINGLE
- : ZGFX_SEGMENTED_MULTIPART);
- 这是第一分段,totalLength == 0意味着此次3pdu负载一个分段就够了。
- if (totalLength > 0)
- {
- posSegmentCount = Stream_GetPosition(sDst); /* segmentCount (2 bytes) */
- Stream_Seek(sDst, 2);
- Stream_Write_UINT32(sDst, uncompressedSize); /* uncompressedSize (4 bytes) */
将存在多个分段,多个分段时要多出segmentCount、uncompressedSize字段。对segmentCount,此时我们不知道值会是多少,先记住偏移,后面会补写。uncompressedSize填的就是3pdu负载字节数。
- }
- }
-
- if (fragment > 0 || totalLength > 0)
- {
- /* Multipart */
- posDstSize = Stream_GetPosition(sDst); /* size (4 bytes) */
- 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)。
- }
-
- posDataStart = Stream_GetPosition(sDst);
-
- if (!zgfx_compress_segment(zgfx, sDst, pSrcData, SrcSize, pFlags))
- 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负载。
- if (posDstSize)
- {
- /* Fill segment data size */
- DstSize = Stream_GetPosition(sDst) - posDataStart;
- Stream_SetPosition(sDst, posDstSize);
- Stream_Write_UINT32(sDst, DstSize);
- Stream_SetPosition(sDst, posDataStart + DstSize);
修正RDP_DATA_SEGMENT中的size字段,它指示紧跟的bulkData字段的字节数。再说下,只有第二个或之后分段才有RDP_DATA_SEGMENT。
- }
- pSrcData += SrcSize;
- }
- Stream_SealLength(sDst);
- /* fill back segmentCount */
- if (posSegmentCount)
- {
- Stream_SetPosition(sDst, posSegmentCount);
- Stream_Write_UINT16(sDst, fragment);
- Stream_SetPosition(sDst, Stream_Length(sDst));
- }
- return status;
- }
FreeRDP_WTSVirtualChannelWrite
Buffer存储着SEGMENTED负载,Length是负载字节数,FreeRDP_WTSVirtualChannelWrite要把pUncompressed封装成数个适合Virtual Channel PDU发送的virtualChannelData数据块。每virtualChannelData生成一个wMessage,投递到drdynvc_channel->vcm->queue。
- BOOL WINAPI FreeRDP_WTSVirtualChannelWrite(HANDLE hChannelHandle, PCHAR Buffer, ULONG Length,
- PULONG pBytesWritten)
- {
- wStream* s;
- int cbLen;
- int cbChId;
- int first;
- BYTE* buffer;
- UINT32 length;
- UINT32 written;
- UINT32 totalWritten = 0;
- rdpPeerChannel* channel = (rdpPeerChannel*)hChannelHandle;
- BOOL ret = TRUE;
-
- ...
- {
- first = TRUE;
-
- while (Length > 0)
- {
- 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)字节。
- if (!s)
- {
- WLog_ERR(TAG, "Stream_New failed!");
- SetLastError(E_OUTOFMEMORY);
- return FALSE;
- }
此处生成的整个buffer只对应Virtual Channel PDU中VirtualChannelChunkSize字段。当中的格式是DVC(Dynamic Channel Virtual Channel)消息([MS-RDPEDYC])。
- buffer = Stream_Buffer(s);
- Stream_Seek_UINT8(s);
- cbChId = wts_write_variable_uint(s, channel->channelId);
-
- if (first && (Length > (UINT32)Stream_GetRemainingLength(s)))
- {
- cbLen = wts_write_variable_uint(s, Length);
- 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)”。
- }
- else
- {
- buffer[0] = (DATA_PDU << 4) | cbChId;
- }
-
- first = FALSE;
- written = Stream_GetRemainingLength(s);
-
- if (written > Length)
- written = Length;
-
- Stream_Write(s, Buffer, written);
- written是此次取出的SEGMENTED负载长度,因为有DVC消息头,它略小于1600。
- length = Stream_GetPosition(s);
- Stream_Free(s, FALSE);
- Stream_Free第二个参数是FALSE,表示只释放wStream这个struct,不释放当中的buffer。
- Length -= written;
- Buffer += written;
- totalWritten += written;
- ret = wts_queue_send_item(channel->vcm->drdynvc_channel, buffer, length);
- 把此次written字节数的SEGMENTED负载投递到drdynvc_channel的队列。
- }
- }
-
- if (pBytesWritten)
- *pBytesWritten = totalWritten;
- 要是不出错,pBytesWritten值应该等于此次要发送的SEGMENTED负载字节数Length。
-
- return ret;
- }
-
- static BOOL wts_queue_send_item(rdpPeerChannel* channel, BYTE* Buffer, UINT32 Length)
- {
- BYTE* buffer;
- UINT32 length;
- UINT16 channelId;
- buffer = Buffer;
- length = Length;
- channelId = channel->channelId;
- return MessageQueue_Post(channel->vcm->queue, (void*)(UINT_PTR)channelId, 0, (void*)buffer,
- (void*)(UINT_PTR)length);
- }
此个virtualChannelData投递到channel->vcm->drdynvc_channel->vcm->queue,同时queue->event将被置为有信号。至此virtualChannelData只是被放到drdynvc_channel->vcm下的queue,没有生成Virtual Channel PDU,更没有通过网络发送出去。
三、发送
- static DWORD WINAPI shadow_client_thread(LPVOID arg) {
- ...
- ChannelEvent = WTSVirtualChannelManagerGetEventHandle(client->vcm);
- while (1) {
- if (WaitForSingleObject(ChannelEvent, 0) == WAIT_OBJECT_0)
- {
- if (!WTSVirtualChannelManagerCheckFileDescriptor(client->vcm))
- {
- WLog_ERR(TAG, "WTSVirtualChannelManagerCheckFileDescriptor failure");
- goto fail;
- }
- }
- ...
- }
- }
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反映正确值。
- <freerdp>/server/shadow/shadow_client.c
- static INLINE UINT32 rdpgfx_estimate_bitmapdata_avc444v2(RDPGFX_SURFACE_COMMAND* cmd)
- {
- RDPGFX_AVC420_BITMAP_STREAM* havc420 = NULL;
- RDPGFX_AVC444_BITMAP_STREAM* havc444 = NULL;
- UINT32 h264Size = 0;
-
- if (cmd->codecId == RDPGFX_CODECID_AVC444v2) {
- havc444 = (RDPGFX_AVC444_BITMAP_STREAM*)cmd->extra;
- h264Size = sizeof(UINT32); /* cbAvc420EncodedBitstream1 */
- /* avc420EncodedBitstream1 */
- havc420 = &(havc444->bitstream[0]);
- h264Size += rdpgfx_estimate_h264_avc420(havc420);
-
- /* avc420EncodedBitstream2 */
- if (havc444->LC == 0)
- {
- havc420 = &(havc444->bitstream[1]);
- h264Size += rdpgfx_estimate_h264_avc420(havc420);
- }
- }
- return h264Size;
- }
- static BOOL shadow_client_send_surface_gfx(...)
- {
- ...
- if (settings->GfxAVC444 || settings->GfxAVC444v2) {
- cmd.extra = (void*)&avc444;
- cmd.length = rdpgfx_estimate_bitmapdata_avc444v2(&cmd);
- ...
- }
- ...
- }
修改都在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)。
- 因为cmd->length=0,rdpgfx_send_surface_command内变量size=53,即s->capability = 53。
- 41字节。start_frame+SURFACE_PDU_1除bitmapData外的字节数。
- + 18字节。4字节cbAvc420EncodedBitstream1 + (4字节矩形数 + numRegionRects*10)。其中numRegionRects=1。
- + 40字节。bitstream[0].length。由于53不够存了,此步骤s->capability被加大一倍到106。
- + 12字节。end_frame。之前累计99字节,加上12字节就超过106了,而写end_frame时不会调用Stream_EnsureRemainingCapacity。
4.2 rdpgfx_server_packet_send时,让每个分段附加字节数是5个
若不改,应该不致于造成非法问题,但会导致后绪的Stream_EnsureRemainingCapacity会重分配buffer。
- static UINT rdpgfx_server_packet_send(RdpgfxServerContext* context, wStream* s) {
- fs = Stream_New(NULL, SrcSize + 7 + (SrcSize / ZGFX_SEGMENTED_MAXSIZE + 1) * 4);
- 改为
- fs = Stream_New(NULL, SrcSize + 7 + (SrcSize / ZGFX_SEGMENTED_MAXSIZE + 1) * 5);
- }
每个RDP_DATA_SEGMENT除3pdu负载外,会有4字节的size和1字节的bulkData.header。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。