重新压缩一个AVI文件

    技术2022-05-11  121

    这里我们讲解怎样用DirectShow来压缩一个AVI文件。我们重点放到视频压缩,同样的方法可以应用到音频压缩。我们分以下几步来讲解:1、 选择一个压缩过滤器有许多种方法可以压缩视频或者音频,比如:a、 本地DirectShow过滤器b、 视频压缩管理编码器(VCM)c、 音频压缩管理编码器(ACM)d、 DirectX媒体对象(DMOs)在DirectShow中,VCM被AVI Compressor过滤器封装了。同样的,ACM编码器也被ACM Wrapper过滤器封装了。DMOs被DMO Wrapper过滤器封装。系统设备枚举器提供了一个统一的方法来枚举和创建这些压缩器,我们不用考虑底层的操作。枚举设备方法请参照前面的讲述。这里我们只给出代码:void OnInitDialog(HWND hDlg){    HRESULT hr;    ICreateDevEnum *pSysDevEnum = NULL;    IEnumMoniker *pEnum = NULL;    IMoniker *pMoniker = NULL;

        hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL,         CLSCTX_INPROC_SERVER, IID_ICreateDevEnum,         (void**)&pSysDevEnum);    hr = pSysDevEnum->CreateClassEnumerator(             CLSID_VideoCompressorCategory, &pEnum, 0);        while (S_OK == pEnum->Next(1, &pMoniker, NULL))    {        IPropertyBag *pPropBag = NULL;        pMoniker->BindToStorage(0, 0, IID_IPropertyBag,                                 (void **)&pPropBag);        VARIANT var;        VariantInit(&var);        hr = pPropBag->Read(L"FriendlyName", &var, 0);        if (SUCCEEDED(hr))        {            LRESULT iSel = AddString(GetDlgItem(hDlg,                         IDC_CODEC_LIST), var.bstrVal);        }           VariantClear(&var);         pPropBag->Release();        pMoniker->Release();    }

        SendDlgItemMessage(hDlg, IDC_CODEC_LIST,                        LB_SETCURSEL, 0, 0);    pSysDevEnum->Release();    pEnum->Release();}创建一个过滤器的实例,调用IMoniker::BindToObject方法。方法会返回一个IBaseFilter接口指针。就象下面那样:IBaseFilter *pFilter = NULL;hr = pMoniker->BindToObject(NULL, NULL, IID_IBaseFilter,                                        (void**)&pFilter);if (SUCCEEDED(hr)){    // 可以使用过滤器了    // 记着释放IBaseFilter接口指针}2、 设置视频压缩属性视频压缩过滤器可以在它的输出引脚支持IAMVideoCompression接口。使用这个接口可以设置压缩的属性,比如桢率,压缩质量等待。首先,调用IBaseFilter::EnumPins方法找到过滤器的输出引脚,然后为接口查询引脚。一些过滤器不是所有的接口都支持,也有的不支持某个压缩属性。为了决定支持的属性能力,我们调用IAMVideoCompression::GetInfo来确定。这个方法返回一些信息:a、 一个设置性能的标识b、 一个描述字符串和版本字符串c、 默认的桢速率,质量等参数它按照下面的语法调用:hr = pCompress->GetInfo(pszVersion, &cbVersion, pszDesc, &cbDesc,          &lKeyFrame, &lPFrame, &dblQuality, &lCap);

    pszVersion和pszDesc参数是接收版本和描述字符串的宽字符缓冲区。cbVersion和cbDesc参数接收被请求的缓冲区大小。IkeyFrame,lPFrame和dblQuality参数获得默认的桢速率,p桢率和质量。质量是用从0.0到1.0的浮点数来表示的。Icap参数获得一个被或的能力标识,这个被定义为CompressionCaps枚举类型。这些参数的任何一个都可以设置为NULL,。例如如果为了获得版本和描述字符串,第一次调用方法的时候,第一个和第三个参数为NULL。使用返回值cbVersion和cbDesc来分配缓冲区,然后再次调用该方法: int cbVersion, cbDesc; // Size in bytes, not characters! hr = pCompress->GetInfo(0, &cbVersion, 0, &cbDesc, 0, 0, 0, 0); if (SUCCEEDED(hr)) {     WCHAR *pszVersion = new WCHAR[cbVersion/2];       WCHAR *pszDesc = new WCHAR[cbDesc/2];     hr = pCompress->GetInfo(pszVersion, 0, pszDesc, 0, 0, 0, 0, 0); }iCap参数提供了对IAMVideoCompression方法的过滤器的支持能力。例如,如果iCap包含了CompressionCaps_CanKeyFrame标志,你就可以调用IAMVideoCompression::get_KeyFrameRate方法来得到关键桢的速率,调用IAMVideoCompression::put_KeyFrameRate来设置桢速率。如果iCap的值没有包含这些参数,那么就只能使用默认的值了。 if (lCap & CompressionCaps_CanKeyFrame) {     hr = pCompress->get_KeyFrameRate(&lKeyFrame);     if (FAILED(hr) || lKeyFrame < 0)     {         lKeyFrame = lDefaultKeyFrame; // 来自 GetInfo.     } }

    下面的代码试着在输出引脚寻找IAMVideoCompression接口。如果成功,它将会返回默认的和实际的压缩属性值。 HRESULT hr = E_FAIL; IEnumPins *pEnum = NULL; IPin *pPin = NULL; IAMVideoCompression *pCompress = NULL;

     // 寻找支持 IAMVideoCompression的引脚 pFilter->EnumPins(&pEnum); while (S_OK == pEnum->Next(1, &pPin, NULL)) {     hr = pPin->QueryInterface(IID_IAMVideoCompression, (void**)&pCompress);     pPin->Release();     if (SUCCEEDED(hr)) // Found the interface.     {         break;     } } if (SUCCEEDED(hr))  {     long lCap;                     // 性能标识     long lKeyFrame, lPFrame;       // 真实值     double m_Quality;     long lKeyFrameDef, lPFrameDef; //默认值     double QualityDef;         //得到默认值和它的性能     hr = pCompress->GetInfo(0, 0, 0, 0, &KeyFrameDef, &lPFrameDef,              &QualityDef, &lCap);     if (SUCCEEDED(hr))     {         // 得到实际的值.         if (lCap & CompressionCaps_CanKeyFrame)         {             hr = pCompress->get_KeyFrameRate(&lKeyFrame);             if (FAILED(hr) || lKeyFrame < 0)                 lKeyFrame = lKeyFrameDef;         }         if (lCap & CompressionCaps_CanBFrame)         {             hr = pCompress->get_PFramesPerKeyFrame(&lPFrame);             if (FAILED(hr) || lPFrame < 0)                 lPFrame = lPFrameDef;         }         if (lCap & CompressionCaps_CanQuality)         {             hr = pCompress->get_Quality(&Quality);             if (FAILED(hr) || Quality < 0)                 Quality = QualityDef;         }     } }

    注意:如果你使用IcaptureGraphBuilder2接口来创建你的过滤图形,你可以调用IcaptureGraphBuilder2::FindInterface方法来获得IAMVideoCompression接口。3、 建立压缩图形下图是一个典型的AVI文件的过滤图形:

     AVI_Splitter过滤器从文件的源过滤器(File Source(Async))拉数据,然后分解到视频和音频流。视频解压缩过滤器解码被压缩的视频,然后重新被视频压缩器重新压缩。被压缩的视频进入到AVI Mux过滤器。音频流在这个例子中没有被压缩,因此它直接从AVI Splitter传输到AVI Mux。AVI Mux进行隔行扫描,然后使用File Write过滤器将数据输出到磁盘上。注意,就算原始文件里面没有音频流,AVI Mux过滤器也是必须的。最简单的方法创建这种过滤图形就是使用Capture Graph Builder,这是DirectShow里面为了建立捕获图形或者别的定制的过滤图形的一个部件。注意:DirectShow里面包含了两个Capture Graph Builder版本。它们提供了不同的接口和类的标识。早期的版本类标识是CLSID_CaptureGraphBuild,接口是IcaptureGraphBuilder。它兼容存在的应用程序。新版本的类标识是CLSID_CaptureGraphBuilder2新的接口名称是IcaptureGraphBuilder2。新的接口比老的接口有更多的灵活性。创建Capture Graph Builder我们还是使用CoCreateInstance: ICaptureGraphBuilder2 *pBuild = NULL; hr = CoCreateInstance(CLSID_CaptureGraphBuilder2,                          NULL, CLSCTX_INPROC_SERVER,     IID_ICaptureGraphBuilder2, (void **)&pBuild);然后我们使用Capture Graph Builder来建立过滤图形:a、 建立部分渲染的过滤图形,它包含AVI Mux 过滤器和File Writer过滤器。b、 添加源过滤器和压缩过滤器。c、 连接源过滤器到MUX 过滤器。下面逐步的解释每一个细节:建立渲染段为了建立过滤图形的渲染段,调用IcaptureGraphBuilder2::SetOutputFileName方法。它返回一个MUX的过滤器和File Write的指针。MUX是下面建立过滤图形所需要的,但是这个例子不需要File Write,因此,它的参数为NULL。 IBaseFilter *pMux = NULL; pBuild->SetOutputFileName(          &MEDIASUBTYPE_Avi, //文件类型         wszOutputFile,     // 文件名         &pMux,             // 得到一个指向multiplexer的指针        NULL);             // 得到一个指向File Write的指针

    当该方法返回,MUX过滤器有一个很明显的参考计数,所以以后一定要确保释放它。MUX过滤器提供了两个接口来控制AVI格式:IconfigInterleaving接口:设置交错模式IconfigAviMux接口:设置主流和AVI兼容性的索引添加源过滤器和压缩过滤器下一步我们要在过滤图形中添加源过滤器和压缩过滤器。当你调用SetOutputFileName的时候,Capture Graph Builder会自动的创建一个过滤图形管理器的实例。你可以调用IcaptureGraphBuilder::GetFiltergraph方法来获得刚才创建的过滤图形管理器的指针。 IGraphBuilder *pGraph = NULL; pBuild->GetFiltergraph(&pGraph);现在我们该调用IgraphBuilder::AddSourceFilter方法来添加异步文件源过滤器,然后调用IfilterGraph::AddFilter方法来添加视频压缩过滤器: IBaseFilter *pSrc = NULL; pGraph->AddSourceFilter(wszInputFile, L"Source Filter", &pSrc); pGraph->AddFilter(pVComp, L"Compressor");到了这一步我们的状态就象下图那样,源过滤器和压缩过滤器没有和别的任何过滤器连接。 连接源到Mux最后一步就是通过视频压缩过滤器连接源过滤器到AVI Mux过滤器。我们使用IcaptureGraphBuilder2::RenderStream方法来连接源过滤器的输出引脚到指定的过滤器。前两个参数指定了用那个源过滤器的引脚来连接,通过指明引脚的分类和媒体类型来实现。异步文件源过滤器只有一个输出引脚,所以这些参数要设置成NULL。后三个参数指定了源过滤器,压缩过滤器,和Mux过滤器。下面的代码演示了通过视频压缩过滤器来渲染视频流:pBuild->RenderStream(        NULL,       // 输出引脚类型        NULL,       // 媒体类型        pSrc,       // 源过滤器        pVComp,     // 压缩过滤器        pMux);      假定源文件包含了音频流,AVI Splitter过滤器会在输出引脚输出音频流。为了连接这个管脚我们需要再次调用RenderStream: pBuild->RenderStream(NULL, NULL, pSrc, NULL, pMux);这里我们没有指定压缩过滤器。而且源过滤器的输出引脚已经连接了,因此RenderStream方法会搜索一个未连接的输出引脚到Splitter过滤器。它可以直接连接引脚到MUX过滤器。但是如果源文件没有音频流,那么第二次调用会失败。4、 写文件如果想写文件正常的进行,你必须调用ImediaControl::Run方法来运行过滤图形。等到播放完成后调用ImediaControl::Stop。如果要显示文件的写的进度,你可以使用ImediaSeeking来查询Mux过滤器。调用ImediaSeeking::GetDuration方法来获得文件的持续时间。定时的使用ImediaSeeking::GetCurrentPostition方法来获得当前的位置,但必须要是过滤图形运行中。注意,一般情况下我们用ImediaSeeking接口查询过滤图形管理器来定位,如果正在写文件的话,这将是一个特例。这里我们要查询Mux过滤器就可以了。查询过滤图形定位要求回放的时候才可以,而不是写文件的时候。看了下面的代码读者就会更加清楚了。IMediaSeeking *pSeek = NULL;IMediaEventEx *pEvent = NULL;IMediaControl *pControl = NULL;REFERENCE_TIME rtTotal;

    hr = pMux->QueryInterface(IID_IMediaSeeking, (void**)&pSeek);hr = pGraph->QueryInterface(IID_IMediaEventEx, (void**)&pEvent);hr = pGraph->QueryInterface(IID_IMediaControl, (void**)&pControl);

    // 设置DirectShow的时间通报hr = pEvent->SetNotifyWindow((OAHWND)hwnd, WM_GRAPHNOTIFY, 0);

    hr = GetDuration(&rtTotal);SendDlgItemMessage(hwnd, IDC_PROGRESS1, PBM_SETRANGE, 0,    MAKELPARAM(0, rtTotal / 10000000));// 开始时间.UINT_PTR res = SetTimer(hwnd, nIDEvent, 100, NULL);// 运行过滤图形.pControl->Run();当应用程序收到定时器事件,它就更新当前位置:void OnTimer(HWND hDlg, IMediaSeeking *pSeek){    REFERENCE_TIME rtNow;    HRESULT hr = pSeek->GetCurrentPosition(&rtNow);    if (SUCCEEDED(hr))    {        SendDlgItemMessage(hDlg, IDC_PROGRESS1, PBM_SETPOS, rtNow/10000000, 0);    }}当应用程序收到DirectShow结束事件,它就可以停止图形,就象下面那样:LRESULT CALLBACK WndProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam){    switch (msg)    {    /*  ...  */    case WM_GRAPHNOTIFY:        DoHandleEvent();        break;    /*  ...  */    }}

    void DoHandleEvent(){    long evCode, param1, param2;    bool bComplete = false;    if (!pEvent) return;

        //得到所有的事件    while (SUCCEEDED(pEvent->GetEvent(&evCode, ¶m1, ¶m2, 0))    {        pEvent->FreeEventParams(evCode, param1, param2);        switch(evCode)        {            case EC_USERABORT:            case EC_ERRORABORT:            case EC_COMPLETE:                bComplete = true;                break;        }    }    if (bComplete)    {        pControl->Stop(); // Important! You must stop the graph!

            //关掉事件通报.        pEvent->SetNotifyWindow(NULL, 0, 0);        pEvent->Release();        pEvent = NULL;        SendDlgItemMessage(IDC_PROGRESS1, PBM_SETPOS, 0, 0);        KillTimer(hwnd, nIDEvent);    }}

     

     


    最新回复(0)