在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、<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执行的,以下是参数。
- 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是对应的字节数。
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时以两列为一次计算单位。
- 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是存在三块独立的内存。
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
此过程要经过多次封装,以下是大致逻辑。
- 由cmd生成3个dpu,依次为RDPGFX_START_FRAME_PDU、RDPGFX_WIRE_TO_SURFACE_PDU_1、RDPGFX_END_FRAME_PDU,称3pdu负载。
- 3pdu负载封装成一个RDP_SEGMENTED_DATA格式的数据块(注:只一个)。3pdu负载将被拆分成数个分段,一个分段存储在一个bulkData中。假设有N个分段,前面N-1个bulkData存65535字节的3pdu负载,第N个存储剩余的字节。称RDP_SEGMENTED_DATA格式的数据块为SEGMENTED负载。
- SEGMENTED负载被封装成数个适合Virtual Channel PDU发送的virtualChannelData数块块。注意,它没有生成pdu,只是生成该pdu的virtualChannelData字段。一个virtualChannelData数据块最大1600字节,这1600字节并不全是存着SEGMENTED负载,而是DVC(Dynamic Channel Virtual Channel)格式,因为要前缀DVC消息头,分到每个virtualChannelData的SEGMENTED负载要略小于1600字节。
- 从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。
- 最终数据将被以wMessage的格式存放在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); }
- 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,更没有通过网络发送出去。
三、发送
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。