在 ffplay 中获得绿屏:使用 Live555 通过 RTP 流将桌面(DirectX 表面)流式传输为 H264 视频

Posted

技术标签:

【中文标题】在 ffplay 中获得绿屏:使用 Live555 通过 RTP 流将桌面(DirectX 表面)流式传输为 H264 视频【英文标题】:Getting green screen in ffplay: Streaming desktop (DirectX surface) as H264 video over RTP stream using Live555 【发布时间】:2020-02-18 13:38:04 【问题描述】:

我正在尝试在 Windows10 上使用 Live555 和 Windows 媒体基金会的硬件编码器通过 RTP 流将桌面(NV12 格式的 DirectX 表面)流式传输为 H264 视频,并期望它由 ffplay(ffmpeg 4.2)呈现。但只会得到如下所示的绿屏,

我参考了 MFWebCamToRTP mediafoundation-sample 和 Encoding DirectX surface using hardware MFT 来实现 live555 的 FramedSource 并将输入源更改为 DirectX Surface 而不是 webCam。

这是我对 Live555 的 doGetNextFrame 回调的实现的摘录,以从 directX 表面提供输入样本:

virtual void doGetNextFrame()

    if (!_isInitialised)
    
        if (!initialise()) 
            printf("Video device initialisation failed, stopping.");
            return;
        
        else 
            _isInitialised = true;
        
    

    //if (!isCurrentlyAwaitingData()) return;

    DWORD processOutputStatus = 0;
    HRESULT mftProcessOutput = S_OK;
    MFT_OUTPUT_STREAM_INFO StreamInfo;
    IMFMediaBuffer *pBuffer = NULL;
    IMFSample *mftOutSample = NULL;
    DWORD mftOutFlags;
    bool frameSent = false;
    bool bTimeout = false;

    // Create sample
    CComPtr<IMFSample> videoSample = NULL;

    // Create buffer
    CComPtr<IMFMediaBuffer> inputBuffer;
    // Get next event
    CComPtr<IMFMediaEvent> event;
    HRESULT hr = eventGen->GetEvent(0, &event);
    CHECK_HR(hr, "Failed to get next event");

    MediaEventType eventType;
    hr = event->GetType(&eventType);
    CHECK_HR(hr, "Failed to get event type");


    switch (eventType)
    
    case METransformNeedInput:
        
            hr = MFCreateDXGISurfaceBuffer(__uuidof(ID3D11Texture2D), surface, 0, FALSE, &inputBuffer);
            CHECK_HR(hr, "Failed to create IMFMediaBuffer");

            hr = MFCreateSample(&videoSample);
            CHECK_HR(hr, "Failed to create IMFSample");
            hr = videoSample->AddBuffer(inputBuffer);
            CHECK_HR(hr, "Failed to add buffer to IMFSample");

            if (videoSample)
            
                _frameCount++;

                CHECK_HR(videoSample->SetSampleTime(mTimeStamp), "Error setting the video sample time.\n");
                CHECK_HR(videoSample->SetSampleDuration(VIDEO_FRAME_DURATION), "Error getting video sample duration.\n");

                // Pass the video sample to the H.264 transform.

                hr = _pTransform->ProcessInput(inputStreamID, videoSample, 0);
                CHECK_HR(hr, "The resampler H264 ProcessInput call failed.\n");

                mTimeStamp += VIDEO_FRAME_DURATION;
            
        

        break;

    case METransformHaveOutput:

        
            CHECK_HR(_pTransform->GetOutputStatus(&mftOutFlags), "H264 MFT GetOutputStatus failed.\n");

            if (mftOutFlags == MFT_OUTPUT_STATUS_SAMPLE_READY)
            
                MFT_OUTPUT_DATA_BUFFER _outputDataBuffer;
                memset(&_outputDataBuffer, 0, sizeof _outputDataBuffer);
                _outputDataBuffer.dwStreamID = outputStreamID;
                _outputDataBuffer.dwStatus = 0;
                _outputDataBuffer.pEvents = NULL;
                _outputDataBuffer.pSample = nullptr;

                mftProcessOutput = _pTransform->ProcessOutput(0, 1, &_outputDataBuffer, &processOutputStatus);

                if (mftProcessOutput != MF_E_TRANSFORM_NEED_MORE_INPUT)
                
                    if (_outputDataBuffer.pSample) 

                        //CHECK_HR(_outputDataBuffer.pSample->SetSampleTime(mTimeStamp), "Error setting MFT sample time.\n");
                        //CHECK_HR(_outputDataBuffer.pSample->SetSampleDuration(VIDEO_FRAME_DURATION), "Error setting MFT sample duration.\n");

                        IMFMediaBuffer *buf = NULL;
                        DWORD bufLength;
                        CHECK_HR(_outputDataBuffer.pSample->ConvertToContiguousBuffer(&buf), "ConvertToContiguousBuffer failed.\n");
                        CHECK_HR(buf->GetCurrentLength(&bufLength), "Get buffer length failed.\n");
                        BYTE * rawBuffer = NULL;

                        fFrameSize = bufLength;
                        fDurationInMicroseconds = 0;
                        gettimeofday(&fPresentationTime, NULL);

                        buf->Lock(&rawBuffer, NULL, NULL);
                        memmove(fTo, rawBuffer, fFrameSize);

                        FramedSource::afterGetting(this);

                        buf->Unlock();
                        SafeRelease(&buf);

                        frameSent = true;
                        _lastSendAt = GetTickCount();

                        _outputDataBuffer.pSample->Release();
                    

                    if (_outputDataBuffer.pEvents)
                        _outputDataBuffer.pEvents->Release();
                

                //SafeRelease(&pBuffer);
                //SafeRelease(&mftOutSample);

                break;
            
        

        break;
    

    if (!frameSent)
    
        envir().taskScheduler().triggerEvent(eventTriggerId, this);
    

    return;

done:

    printf("MediaFoundationH264LiveSource doGetNextFrame failed.\n");
    envir().taskScheduler().triggerEvent(eventTriggerId, this);

初始化方法:

bool initialise()

    HRESULT hr;
    D3D11_TEXTURE2D_DESC desc =  0 ;

    HDESK CurrentDesktop = nullptr;
    CurrentDesktop = OpenInputDesktop(0, FALSE, GENERIC_ALL);
    if (!CurrentDesktop)
    
        // We do not have access to the desktop so request a retry
        return false;
    

    // Attach desktop to this thread
    bool DesktopAttached = SetThreadDesktop(CurrentDesktop) != 0;
    CloseDesktop(CurrentDesktop);
    CurrentDesktop = nullptr;
    if (!DesktopAttached)
    
        printf("SetThreadDesktop failed\n");
    

    UINT32 activateCount = 0;

    // h264 output
    MFT_REGISTER_TYPE_INFO info =  MFMediaType_Video, MFVideoFormat_H264 ;

    UINT32 flags =
        MFT_ENUM_FLAG_HARDWARE |
        MFT_ENUM_FLAG_SORTANDFILTER;

    // ------------------------------------------------------------------------
    // Initialize D3D11
    // ------------------------------------------------------------------------

    // Driver types supported
    D3D_DRIVER_TYPE DriverTypes[] =
    
        D3D_DRIVER_TYPE_HARDWARE,
        D3D_DRIVER_TYPE_WARP,
        D3D_DRIVER_TYPE_REFERENCE,
    ;
    UINT NumDriverTypes = ARRAYSIZE(DriverTypes);

    // Feature levels supported
    D3D_FEATURE_LEVEL FeatureLevels[] =
    
        D3D_FEATURE_LEVEL_11_0,
        D3D_FEATURE_LEVEL_10_1,
        D3D_FEATURE_LEVEL_10_0,
        D3D_FEATURE_LEVEL_9_1
    ;
    UINT NumFeatureLevels = ARRAYSIZE(FeatureLevels);

    D3D_FEATURE_LEVEL FeatureLevel;

    // Create device
    for (UINT DriverTypeIndex = 0; DriverTypeIndex < NumDriverTypes; ++DriverTypeIndex)
    
        hr = D3D11CreateDevice(nullptr, DriverTypes[DriverTypeIndex], nullptr,
            D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
            FeatureLevels, NumFeatureLevels, D3D11_SDK_VERSION, &device, &FeatureLevel, &context);
        if (SUCCEEDED(hr))
        
            // Device creation success, no need to loop anymore
            break;
        
    

    CHECK_HR(hr, "Failed to create device");

    // Create device manager
    UINT resetToken;
    hr = MFCreateDXGIDeviceManager(&resetToken, &deviceManager);
    CHECK_HR(hr, "Failed to create DXGIDeviceManager");

    hr = deviceManager->ResetDevice(device, resetToken);
    CHECK_HR(hr, "Failed to assign D3D device to device manager");


    // ------------------------------------------------------------------------
    // Create surface
    // ------------------------------------------------------------------------
    desc.Format = DXGI_FORMAT_NV12;
    desc.Width = surfaceWidth;
    desc.Height = surfaceHeight;
    desc.MipLevels = 1;
    desc.ArraySize = 1;
    desc.SampleDesc.Count = 1;

    hr = device->CreateTexture2D(&desc, NULL, &surface);
    CHECK_HR(hr, "Could not create surface");

    hr = MFTEnumEx(
        MFT_CATEGORY_VIDEO_ENCODER,
        flags,
        NULL,
        &info,
        &activateRaw,
        &activateCount
    );
    CHECK_HR(hr, "Failed to enumerate MFTs");

    CHECK(activateCount, "No MFTs found");

    // Choose the first available encoder
    activate = activateRaw[0];

    for (UINT32 i = 0; i < activateCount; i++)
        activateRaw[i]->Release();

    // Activate
    hr = activate->ActivateObject(IID_PPV_ARGS(&_pTransform));
    CHECK_HR(hr, "Failed to activate MFT");

    // Get attributes
    hr = _pTransform->GetAttributes(&attributes);
    CHECK_HR(hr, "Failed to get MFT attributes");

    // Unlock the transform for async use and get event generator
    hr = attributes->SetUINT32(MF_TRANSFORM_ASYNC_UNLOCK, TRUE);
    CHECK_HR(hr, "Failed to unlock MFT");

    eventGen = _pTransform;
    CHECK(eventGen, "Failed to QI for event generator");

    // Get stream IDs (expect 1 input and 1 output stream)
    hr = _pTransform->GetStreamIDs(1, &inputStreamID, 1, &outputStreamID);
    if (hr == E_NOTIMPL)
    
        inputStreamID = 0;
        outputStreamID = 0;
        hr = S_OK;
    
    CHECK_HR(hr, "Failed to get stream IDs");

     // ------------------------------------------------------------------------
    // Configure hardware encoder MFT
   // ------------------------------------------------------------------------
    CHECK_HR(_pTransform->ProcessMessage(MFT_MESSAGE_SET_D3D_MANAGER, reinterpret_cast<ULONG_PTR>(deviceManager.p)), "Failed to set device manager.\n");

    // Set low latency hint
    hr = attributes->SetUINT32(MF_LOW_LATENCY, TRUE);
    CHECK_HR(hr, "Failed to set MF_LOW_LATENCY");

    hr = MFCreateMediaType(&outputType);
    CHECK_HR(hr, "Failed to create media type");

    hr = outputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
    CHECK_HR(hr, "Failed to set MF_MT_MAJOR_TYPE on H264 output media type");

    hr = outputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_H264);
    CHECK_HR(hr, "Failed to set MF_MT_SUBTYPE on H264 output media type");

    hr = outputType->SetUINT32(MF_MT_AVG_BITRATE, TARGET_AVERAGE_BIT_RATE);
    CHECK_HR(hr, "Failed to set average bit rate on H264 output media type");

    hr = MFSetAttributeSize(outputType, MF_MT_FRAME_SIZE, desc.Width, desc.Height);
    CHECK_HR(hr, "Failed to set frame size on H264 MFT out type");

    hr = MFSetAttributeRatio(outputType, MF_MT_FRAME_RATE, TARGET_FRAME_RATE, 1);
    CHECK_HR(hr, "Failed to set frame rate on H264 MFT out type");

    hr = outputType->SetUINT32(MF_MT_INTERLACE_MODE, 2);
    CHECK_HR(hr, "Failed to set MF_MT_INTERLACE_MODE on H.264 encoder MFT");

    hr = outputType->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, TRUE);
    CHECK_HR(hr, "Failed to set MF_MT_ALL_SAMPLES_INDEPENDENT on H.264 encoder MFT");

    hr = _pTransform->SetOutputType(outputStreamID, outputType, 0);
    CHECK_HR(hr, "Failed to set output media type on H.264 encoder MFT");

    hr = MFCreateMediaType(&inputType);
    CHECK_HR(hr, "Failed to create media type");

    for (DWORD i = 0;; i++)
    
        inputType = nullptr;
        hr = _pTransform->GetInputAvailableType(inputStreamID, i, &inputType);
        CHECK_HR(hr, "Failed to get input type");

        hr = inputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
        CHECK_HR(hr, "Failed to set MF_MT_MAJOR_TYPE on H264 MFT input type");

        hr = inputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_NV12);
        CHECK_HR(hr, "Failed to set MF_MT_SUBTYPE on H264 MFT input type");

        hr = MFSetAttributeSize(inputType, MF_MT_FRAME_SIZE, desc.Width, desc.Height);
        CHECK_HR(hr, "Failed to set MF_MT_FRAME_SIZE on H264 MFT input type");

        hr = MFSetAttributeRatio(inputType, MF_MT_FRAME_RATE, TARGET_FRAME_RATE, 1);
        CHECK_HR(hr, "Failed to set MF_MT_FRAME_RATE on H264 MFT input type");

        hr = _pTransform->SetInputType(inputStreamID, inputType, 0);
        CHECK_HR(hr, "Failed to set input type");

        break;
    

    CheckHardwareSupport();

    CHECK_HR(_pTransform->ProcessMessage(MFT_MESSAGE_COMMAND_FLUSH, NULL), "Failed to process FLUSH command on H.264 MFT.\n");
    CHECK_HR(_pTransform->ProcessMessage(MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, NULL), "Failed to process BEGIN_STREAMING command on H.264 MFT.\n");
    CHECK_HR(_pTransform->ProcessMessage(MFT_MESSAGE_NOTIFY_START_OF_STREAM, NULL), "Failed to process START_OF_STREAM command on H.264 MFT.\n");

    return true;

done:

    printf("MediaFoundationH264LiveSource initialisation failed.\n");
    return false;



    HRESULT CheckHardwareSupport()
    
        IMFAttributes *attributes;
        HRESULT hr = _pTransform->GetAttributes(&attributes);
        UINT32 dxva = 0;

        if (SUCCEEDED(hr))
        
            hr = attributes->GetUINT32(MF_SA_D3D11_AWARE, &dxva);
        

        if (SUCCEEDED(hr))
        
            hr = attributes->SetUINT32(CODECAPI_AVDecVideoAcceleration_H264, TRUE);
        

#if defined(CODECAPI_AVLowLatencyMode) // Win8 only

        hr = _pTransform->QueryInterface(IID_PPV_ARGS(&mpCodecAPI));

        if (SUCCEEDED(hr))
        
            VARIANT var =  0 ;

            // FIXME: encoder only
            var.vt = VT_UI4;
            var.ulVal = 0;

            hr = mpCodecAPI->SetValue(&CODECAPI_AVEncMPVDefaultBPictureCount, &var);

            var.vt = VT_BOOL;
            var.boolVal = VARIANT_TRUE;
            hr = mpCodecAPI->SetValue(&CODECAPI_AVEncCommonLowLatency, &var);
            hr = mpCodecAPI->SetValue(&CODECAPI_AVEncCommonRealTime, &var);

            hr = attributes->SetUINT32(CODECAPI_AVLowLatencyMode, TRUE);

            if (SUCCEEDED(hr))
            
                var.vt = VT_UI4;
                var.ulVal = eAVEncCommonRateControlMode_Quality;
                hr = mpCodecAPI->SetValue(&CODECAPI_AVEncCommonRateControlMode, &var);

                // This property controls the quality level when the encoder is not using a constrained bit rate. The AVEncCommonRateControlMode property determines whether the bit rate is constrained.
                VARIANT quality;
                InitVariantFromUInt32(50, &quality);
                hr = mpCodecAPI->SetValue(&CODECAPI_AVEncCommonQuality, &quality);
            
        
#endif

        return hr;
    

ffplay 命令:

ffplay -protocol_whitelist file,udp,rtp -i test.sdp -x 800 -y 600 -profile:v baseline

SDP:

v=0
o=- 0 0 IN IP4 127.0.0.1
s=No Name
t=0 0
c=IN IP4 127.0.0.1
m=video 1234 RTP/AVP 96
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1

我不知道我错过了什么,我已经尝试修复这个问题将近一个星期,但没有任何进展,并且尝试了几乎所有我能做的事情。此外,用于将 DirectX 表面编码为视频的在线资源非常有限。

任何帮助将不胜感激。

【问题讨论】:

我认为您错误地期望在 METransformNeedInput 之后再次调用 doGetNextFrame。也许你应该在里面循环,直到你得到一个有效的 ProcessOutput 调用。 hr = event->GetType(&eventType); switch(eventType) .... if (!frameSent) envir().taskScheduler().triggerEvent(eventTriggerId, this);上面的 2 个块很好地处理了调用 ProcessInput 直到我们从编码器获得输出。我已经验证了相同的。 @VuVirt 那么当 frameSent 为真时会发生什么?在这种情况下你会触发一个新事件吗?之后你有一个“return”声明。 @VuVirt 由底层live555库循环自动调用。 “ProcessInput”和“ProcessOutput”根据switch语句中的事件交替调用。我从 ProcessOut 获得了一个连续的流,但不仅仅是能够查看它。我确定我正确设置了采样时间和持续时间。 您可能需要检查是否收到来自 ProcessOutput 的 MF_E_TRANSFORM_STREAM_CHANGE 并相应地处理格式更改。 【参考方案1】:

这比看起来更难。

如果你想像你一样使用编码器,直接调用IMFTransform接口,你必须将RGB帧转换为NV12。如果你想要好的性能,你应该在 GPU 上做。可以使用像素着色器,渲染 2 帧,全尺寸一到 DXGI_FORMAT_R8_UNORM 渲染目标的亮度,半尺寸到 DXGI_FORMAT_R8G8_UNORM 目标的颜色,并写入两个像素着色器以产生 NV12 值。两个渲染目标都可以渲染到具有相同 NV12 纹理的 2 个平面中,但仅从 Windows 8 开始。

其他方法是使用sink writer。它可以同时托管多个 MFT,因此您可以在 VRAM 中提供 RGB 纹理,接收器编写器将首先使用一个 MFT 将它们转换为 NV12(这可能是由 GPU 驱动程序实现的专有硬件,就像编码器一样),然后传递给编码器 MFT。编码成 mp4 文件相对容易,使用MFCreateSinkWriterFromURL API 创建编写器。从接收器写入器中获取原始样本要困难得多,但是,您必须实现自定义媒体接收器,为其视频流自定义流接收器,并调用 MFCreateSinkWriterFromMediaSink 来创建写入器。

还有更多。

无论编码方法如何,您都不能重复使用帧纹理。从 DD 获得的每一帧,您都应该创建一个新纹理并将其传递给 MF。

视频编码器需要恒定的帧速率。 DD 不会给你那个,它每次屏幕上发生变化时都会给你一个框架。如果您有游戏显示器,则可以为 144 FPS,如果唯一的变化是闪烁的光标,则可以为 2 FPS。理想情况下,您应该以视频媒体类型中指定的恒定帧速率向 MF 提交帧。

如果您想流式传输到网络,通常您还必须提供参数集。除非您使用的是broken with no comments from Intel 的英特尔硬件 h265 编码器,否则 MF 通过调用 IMFMediaTypeHandler 接口上的 SetCurrentMediaType 为您提供媒体类型的 MF_MT_MPEG_SEQUENCE_HEADER attribute 中的数据。您可以实现该接口以获得通知。您只有在开始编码后才能获得该数据。如果你使用 sink writer,IMFTransform 方法更简单,你应该从 ProcessOutput 方法中获取 MF_E_TRANSFORM_STREAM_CHANGE 代码,然后调用 GetOutputAvailableType 以获取具有该魔法 blob 的更新媒体类型。

【讨论】:

您的意思是 DirectX(桌面复制)即使使用 D3D11_CREATE_DEVICE_VIDEO_SUPPORT 和表面描述符初始化为 DXGI_FORMAT_NV12 并在转换中设置 MFT_MESSAGE_SET_D3D_MANAGER 也不会提供 NV12 格式的帧?我也认为我们必须将 RGB 缓冲区显式转换为 NV12 或任何支持的输入格式(主要是 YUV 的变体)或使用 SinkWriter 。但是,这个人能够通过我的方法本身以某种方式实现这一目标。 ***.com/questions/43432670/… ***.com/questions/43424229/… & ***.com/questions/56406825/… @Ram 桌面复制始终以DXGI_FORMAT_B8G8R8A8_UNORM 格式提供 RGB 帧。 H264 和 h265 编码器 MFT 仅支持 NV12 和其他一些,同样奇怪的。必须有人转换。您使用桌面复制;你已经不能用它来支持 Windows 7。使用接收器编写器。我很确定这些用于将 RGB 转换为 NV12 的 nVidia / Intel 硬件 MFT 比像素着色器 ALU 更节能,它们可能纯粹在硬件中实现。 你是对的。颜色转换必须明确完成。 github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/92。我正朝着那个方向前进。 @Ram 它应该可以工作,我以前做过。当 DD 因为没有更新而拒绝给你一个新的帧时,你可以通过再次向编码器提交相同的纹理来节省大量的 VRAM。仅当 DD 为您提供新框架时才创建新纹理。但是检测何时应该提交帧以及等待多长时间的代码并非易事。我使用 QueryPerformanceCounter 来测量时间,并在最后几帧中使用某种滚动平均值来确定我应该捕获还是应该睡觉。顺便说一句,正确的睡眠方式是 IDXGIOutput::WaitForVBlank 方法。【参考方案2】:

由于ffplay 抱怨流参数,我认为它无法获取 SPS/PPS。您尚未在硬编码的 SDP 中设置它们 - 请参阅 RFC-3984 并查找 sprop-parameter-sets。来自 RFC 的一个示例:

m=视频 49170 RTP/AVP 98 a=rtpmap:98 H264/90000 a=fmtp:98 profile-level-id=42A01E;sprop-parameter-sets=Z0IACpZTBYmI,aMljiA==

我强烈认为ffplay 在 SDP 中期待这些。我不记得如何从媒体基础编码器中获取 SPS/PPS,但是它们都在示例有效负载中,您需要通过查找正确的 NAL 单元来提取它们,或者谷歌如何从编码器 - 第一个 hit 我看起来很有前途。

【讨论】:

这是一个有效的点。我也怀疑 SPS/PPS。我还没有验证它。感谢您将我引导到 MSDN 线程,这给了我一些希望。 @Ram 很有可能 SPS/PPS 在示例有效负载中,所以我先检查一下。 是的,我明白了。当我尝试通过 Mpeg4MediaSink 将样本写入文件时,我已经掌握了一些直接从媒体基础编码器中检索和解析 SPS/PPS 的知识。我会朝着这个方向前进。【参考方案3】:

Soonts 为您提供解决问题所需的一切。

首先要做的是DXGI_FORMAT_B8G8R8A8_UNORM和MFVideoFormat_NV12之间的格式转换:

format conversion information

我觉得还是用shader来做格式转换比较好,因为所有的贴图都会留在GPU里(性能更好)。

这是您需要做的第一步。您将有其他人来改进您的程序。

【讨论】:

2x4 图像在 NV12 中占用 12 个字节而不是 24: 8 个亮度值,但彩色图像是两倍小,1x2 像素,因此颜色信息总共只有 4 个字节2x4 图像,U 2 字节,V 2 字节。 是的,你是对的,我省略了 NV12 格式的下采样到 4.2.0。我会尝试制作一个更合适的图表。

以上是关于在 ffplay 中获得绿屏:使用 Live555 通过 RTP 流将桌面(DirectX 表面)流式传输为 H264 视频的主要内容,如果未能解决你的问题,请参考以下文章

live555的使用(转载)

建立live555海思编码推流服务

live555 client 接收rtp数据

Live555流媒体服务器编译(Windows下)

LIVE555研究之三:LIVE555基础

Windows7上用VS编译本地使用的live555