65.9K
CodeProject 正在变化。 阅读更多。
Home

DirectShow 下的多媒体流同步机制

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (17投票s)

2000年2月2日

viewsIcon

523268

downloadIcon

2593

介绍执行多媒体流的三种方法。

引言

本文讨论了 DirectShow(DirectX 媒体 SDK 的一部分)中的同步机制,以便将视频流与音频流同步播放。本文使用 Microsoft 基础类框架提供了自定义解决方案。

在标准 PC 计算机上播放视频流并非易事。如果视频流以每秒 25 帧的帧率编码,那么可以说,给定的 PC 实际上无法以该速率在屏幕上播放该流。无论我们谈论的是旧式 PC、奔腾 PC 还是最新 PC,拥有更强的处理能力只会将问题转移,而不会解决它。事实上,基于 PIII 的 PC 很可能能够以该速率播放视频流,但如果您同时进行其他操作,例如编译源代码、在后台处理某些内容等,计算机的性能将急剧下降。DirectShow 的核心机制旨在解决这个问题。更符合政治正确且实际有效的解决方案是尝试尽快播放视频流,使其与音频流同步,并在视频流与音频流不同步时丢弃任何视频帧。

在本文中,我们将从不同角度探讨同步机制,并提供自定义解决方案,包括或不包括多线程。

解决方案 #1:使用 MFC 框架的 CWinApp::OnIdle。

解决方案 #2:通过自定义视频渲染器过滤器使用异步通知

解决方案 #3:使用多线程进行异步流式传输


为什么视频会与音频不同步

众所周知,音频解码比视频解码需要更少的 CPU 时间。这是因为在特定的时间段内,视频流需要解码的数据量远大于音频流。因此,每当视频流和音频流之间出现同步问题时,视频流很可能正在努力追赶丢失的音频流时间戳。

因此,如果计算机或软件必须在两者之间选择参考流,那么它将是音频流。原因如下:

  • 音频流延迟会导致产生令人不悦的糟糕声音

  • 视频流的带宽远大于音频流

SDK 中的 GraphEditor 工具很好地说明了这一点。让我们启动它,向视频文件插入一个源过滤器,然后渲染其输出引脚。您可以看到,一旦过滤器图构建完成,音频渲染器中就会出现一个黄色的圆圈,表明它拥有整个流的参考时钟。这样做之后,当您播放视频文件时,视频流将尽最大努力接近音频流,从而只会发生听不见的微小延迟,并立即丢弃视频帧。

您可能不想使用任何参考时钟,这样两个流将各自独立运行。让我们试试:转到 Graph 菜单,取消选中“Use Clock”,然后运行视频文件。两个流都将尽可能快地运行,而不尝试同步。



音频参考时钟有助于同步流

在未使用参考时钟自定义的多媒体流式传输代码中,音频渲染器充当参考时钟。


多媒体流式传输

DirectShow 提供了一组接口,可通过调用 Update() 方法(尽可能频繁地调用)来播放视频/音频流,直到流完成。这些是多媒体流式传输接口。请查看以下节点中的文档:DirectShow / MultimediaStreaming / MultimediaStreaming reference。

这些接口可以被视为位于过滤器图接口之上的接口。但这并非完全属实,但有助于理解 DirectShow 不需要您只使用一套接口。多媒体流式传输接口有助于播放流,只需调用 OpenFile、Run,然后只要流未完成就调用 Update,然后 Stop,即可完成。它也可以用于输出到磁盘的流。它使用与过滤器图不同的函数集和比喻,并且适合您控制下的渐进式渲染。



MultimediaStreaming 是 DirectShow 的一部分,可简化流的播放/编码


有关丢帧的信息

首先请注意,尝试同步是内部工作。除非您自己编写一个或多个过滤器,否则您无法自定义它,在这些过滤器中,某个时钟将与某些其他时钟同步,或将其本身认证为参考时钟。



“Sample video renderer”过滤器的属性页确实提供了信息

要获取每秒丢帧数的统计信息,您可以使用另一个更详细的视频渲染器过滤器。假设 SampVid.ax 视频渲染器过滤器在 DirectShow SDK 的 /bin 目录中可用。让我们启动 GraphEditor,然后在 DirectShow 过滤器的列表中插入名为“Sample video renderer”的过滤器,然后插入一个源过滤器,并渲染其输出引脚。过滤器图管理器将使用您插入的视频渲染器过滤器来构建过滤器图。它具有一个属性页,提供了有关我们正在处理的许多信息。顺便说一句,该过滤器的完整源代码在 samples 目录中可用。

例如,渲染器的帧率可以与编码在视频流本身中的帧率进行比较,后者可以通过以下代码检索:

REFTIME CMovieView::getTimePerFrame()
{
   STREAM_TIME result=0;

   if (m_pDDStream != NULL) // from IDirectDrawMediaStream *m_pDDStream
   {
      m_pDDStream->GetTimePerFrame(&result);
   }

   return ((REFTIME)result) / 10000000; // returns the time duration of one frame, in seconds unit
}

请注意,帧率是 1 / getTimePerFrame()。getTimePerFrame() 通常返回 0.040 秒(40 毫秒),即每秒 25 帧。


我也可以没有音频

对于一组视频应用程序,没有必要渲染音频流。在这种情况下,不再需要同步。例如,一个仅基于图像细节自动将视频流剪辑成短镜头序列的应用程序根本不需要音频。

为此,我们首先要参考以下代码片段(不含错误处理程序):

CComPtr <IAMMultiMediaStream> pAMStream;

CoCreateInstance( CLSID_AMMultiMediaStream, NULL, CLSCTX_INPROC_SERVER,
                  IID_IAMMultiMediaStream, (void **)&pAMStream);

//Initialize stream
pAMStream->Initialize(STREAMTYPE_READ, 0, NULL);

//Add primary video stream
pAMStream->AddMediaStream(m_pDD3, &MSPID_PrimaryVideo, 0, NULL);

//Add primary audio stream
pAMStream->AddMediaStream(NULL, &MSPID_PrimaryAudio, AMMSF_ADDDEFAULTRENDERER, NULL);

//Opens and automatically creates a filter graph for the specified media file
pAMStream->OpenFile(wFile, 0);  // wFile : fully qualified filepath (Wide Char format)

这会打开一个标准的视频/音频流。现在让我们用以下代码替换最后四行代码:

pAMStream->OpenFile(wFile, AMMSF_NOCLOCK);

现在没有音频了,也没有同步。视频帧不会被丢弃。这是适用于要求渲染所有视频帧的关键应用程序的代码。请注意,由于没有参考时钟,渲染器不会计算帧率,因此上述代码在此上下文中不起作用。您必须检索给定媒体样本的时间戳,并例如计算开始和结束时间之间的中点。这些数据有助于计算平均帧率。


视频尺寸和颜色深度如何?

我在此并未过多强调视频是在窗口中还是全屏渲染的事实,无论哪种情况都覆盖了桌面窗口的给定表面。将视频帧从核心视频大小blit到最终大小所需的 CPU 时间与 X 和 Y 的尺寸以及颜色深度成正比。因此,建议尽可能使用硬件加速。多媒体流式传输接口集允许您创建一个带有自定义 DirectDraw 表面的视频流,因此您可以对其进行任何操作。标准做法是拉伸blit该表面,使其填充给定工作视频窗口的客户端矩形。或者,在全屏模式下。请查看示例。

许多基于视频的应用程序只是将视频渲染到其初始大小。这当然是为了避免与音频的同步问题,但请告诉我,为什么我所有的窗口都可以拉伸,而这个该死的昂贵的第三方视频应用程序却不允许拉伸?所有第三方开发人员,Windows 合规性如何?

关于颜色深度的一点说明:由于 DirectDraw 不允许即时颜色映射,您必须插入一个额外的 **颜色空间转换器** 过滤器,或者更糟的是,使用 GDI 的标准 Blit/StretchBlit 方法。


DirectShow SDK 中的示例

在处理视频/音频同步方面,DirectShow SDK 提供了三个主要示例值得一看。

第一个在 sample 目录中。名为 **ShowStrm**。它使用多媒体流式传输接口集播放视频/音频流。它很小且不言自明,但缺少一些窗口功能。

第二个,我将以此为基础进行所有解释,是文档本身的纯文本。名为 **MovieWin**。让我们在以下节点查看它:DirectShow / Application developer's guide / How to / Play a movie in a window using DirectDrawEx and multimedia streaming / Entire MovieWin example code。

MovieWin 不在 sample 目录中。

另一个示例是 sample 目录中的 **VidClip**。它使用多媒体流式传输接口同时读取和写入,将视频/音频流编码为 AVI 视频/音频流。对于 DirectShow 中的编码器来说,这是一个很好的起点。

由于 MovieWin 是用纯 WIN32 编写的,我提供了一个该示例的 MFC 版本,因此我们可以轻松地使用 MFC 框架提供的一种简化 OnIdle 机制。其源代码在此网站上提供。


自定义解决方案

接下来,我将展示几种管理同步视频/音频流的自定义解决方案。

  • 第一个是基础的。它使用 MFC 类 CWinApp 的标准 OnIdle 机制。

  • 第二个使用了自定义渲染器过滤器通知机制。

  • 第三个使用了多线程

所有解决方案都是可行的。我之所以想同时展示这两者,是因为第一个方案虽然简单直接,但存在潜在的缺点。缺点是 CPU 不断在 OnIdle() 方法中调用 RenderToSurface(),导致应用程序运行时 CPU 始终满负荷运行。这很奇怪。另外两种解决方案没有这个缺点,但运行起来要复杂得多。


解决方案 #1:使用 MFC 框架的 CWinApp::OnIdle。


基本框架

我使用多媒体流式传输接口集来构建过滤器图并运行视频/音频流。这是标准操作。

我没有使用简单的 FilterGraph 比喻(例如 SDK 中的 MFCPlay 示例),因为它没有提供您可能需要的对渲染表面控制。例如,MFCPlay 将多媒体流渲染到默认的 ActiveMovie 窗口。如果您想将帧缓冲区blit到其他地方怎么办?事实上,关于通过简单的 FilterGraph Play() 渲染多媒体流,没有什么可说的,因为所有同步都在隐藏级别处理,因此在演示时会自动进行,但对于实际的视频应用程序来说也不合适。

我们知道,依赖于隐藏的控制措施,并非所有帧都会显示。有些帧会根据 CPU 处理视频/音频流、解码和渲染的能力而丢弃。

但现在有趣的是要检查哪个进程实际上会告诉视频引擎渲染每个新可用的帧缓冲区(*图像媒体样本*),以便进行最终的屏幕blit。

事实上,我们只需要渲染每个新帧缓冲区,然后等待新的帧缓冲区到来。看起来我们将调用 Update() 方法,并且该方法在帧缓冲区准备好进行最终blit之前不会返回。此时,应该注意的是,我们可以选择使用 ASYNC 标志调用 Update(),这样该方法会立即返回,而由我们来知道何时准备好新帧缓冲区。本文稍后将详细介绍。

让我们回到正题。由我们的视频引擎(CMovieView 类)调用 Update() 方法。

void CMovieView::RenderToSurface()
{
   ...
   if (!bPaused)
   {
      //update each frame
      if (pMediaSample->Update(0, NULL, NULL, 0) != S_OK)
      {
         bAppactive = FALSE;
         pMMStream->SetState(STREAMSTATE_STOP); // finished!
      }
   }
   ...
   // now blit the surface somewhere so that we see it (the surface itself is 
   // known & associated to the pMediaSample at init time)
   ...
}
那么哪个类/组件会调用 RenderToSurface() 方法,以及调用频率如何?

而且,既然我们在这里,为什么不直接在视频引擎本身中循环调用 RenderToSurface() 直到完成?这很简单:那么谁来处理此应用程序的窗口消息。通常,CView 会处理一条消息,然后将控制权交还给核心 Afx 库,后者或多或少地调用 CWinApp 入口点类(继承自 CWinThread)中的 Run/OnIdle 机制。

现在是主循环

BOOL CMovieApp::OnIdle( LONG lCount )
{
   CWinApp::OnIdle(lCount);
   if (lCount>10)
   if (m_viewChild)
   {
      // update the frame buffer on screen
      m_viewChild->RenderToSurface();
      if (!m_viewChild->IsPaused())
      {
         // evaluate the new cursor position (we have a slider control to update too in fact)
         STREAM_TIME total = m_viewChild->getDuration();
         STREAM_TIME current = m_viewChild->getCurrentPosition();
         if (total>0)
         {
            long pos = long( current * MAXFRAME / total );
            GetDialogBar()->setFramePos(pos);
         }
      }
   }
   return TRUE;
}
m_viewChild 是 CMovieView 类(视频引擎)的指针。

OnIdle() 方法在 CWinApp 的主 Run() 方法中被调用。只要没有更多要处理和分派给子窗口(包括 CMovieView)的窗口消息,它就会被调用。

一方面,这种实现非常简单,我们不需要定时器来通知我们应该更新屏幕上的缓冲区,但另一方面,CPU 始终在工作,因此没有真正的空闲时间。如果您启动性能监视器,您会立即注意到这一点。

下载解决方案 #1 的源代码:使用 OnIdle 的渲染机制。


解决方案 #2:通过自定义视频渲染器过滤器使用异步通知

这里我们深入一些。有关构建自定义视频渲染器过滤器的完整详细信息,请查看我另一篇关于此主题的文章。它包含并解释了所有内容。尽管 OnIdle() 技术确实效果很好,但它并不完全适合同时进行其他处理的应用程序,即需要 CPU 时间来处理其他事情,例如在当前帧缓冲区的某个部分应用实时过滤器。

想法是进入异步比喻。我们告诉 DirectShow 引擎渲染新帧缓冲区,同时,我们将自己注册为接收新帧缓冲区已准备好(或流已完成或发生任何错误)的通知的人。这样,就不会有同步的 Update() 调用。一旦流运行,它就会通知我们任何新帧缓冲区,我们甚至可以忽略它们。但是,流是通过 Pause()/Stop()/Run()/Seek(nFrame) 等方法控制的。这就是过滤器图比喻。

实际上,我们正在使用过滤器图管理器。自定义部分是一个渲染器过滤器,它能够使用通知接收器接口并与过滤器图组件通信,以告知新帧缓冲区已准备好。并且,由于过滤器图位于应用程序级别,我们注册为任何发送到它的新消息的侦听器,并按我们想要的方式进行处理。这就是魔力。现在来看代码。

以下是过滤器图构建机制。我们使用标准的过滤器图管理器方法调用。即,我们创建一个空的过滤器图,然后手动添加我们的渲染器过滤器(我们知道 CLSID),然后添加一个源过滤器并调用 Render 它的输出引脚,就像我们在 GraphEditor 中所做的那样。

HRESULT CMovieView::RenderFile(LPCTSTR szFilename)
{
   DeleteCurrentFilterGraph();

   if ( !CreateFilterGraph() )
   {  AfxMessageBox(IDS_CANT_INIT_QUARTZ);
      return FALSE;
   }

   WCHAR wPath[MAX_PATH];
   MultiByteToWideChar( CP_ACP, 0, szFilename,
                        -1, wPath, MAX_PATH );

   // add our custom renderer filter
   // this tells the filter graph manager to use this filter
   // instead of the default noisy ActiveMovie renderer filter
   AddNewVideoRendererToGraph();

   if (FAILED( m_pGraph->RenderFile(wPath, NULL) ))
   {  AfxMessageBox(IDS_CANT_RENDER_FILE);
      return FALSE;
   }
}
HRESULT CMovieView::AddNewVideoRendererToGraph()
{
   HRESULT  hr;

   IBaseFilter *pFilter=NULL;

   if (m_pGraph==NULL)
   {  AfxMessageBox(IDS_NO_FILTERGRAPH_AVAILABLE);
      return E_FAIL;
   }

   hr = CoCreateInstance(CLSID_ARST_DirectDrawVIDEORenderer,
                         NULL, CLSCTX_INPROC_SERVER,
                         IID_IBaseFilter, (LPVOID *)&pFilter);
   if (FAILED(hr))
   {   AfxMessageBox(IDS_CANT_ADD_FILTER);
       return E_FAIL;
   }

   hr = m_pGraph->AddFilter(pFilter, L"ARST DirectDraw Video Renderer");
   if (FAILED(hr))
   {   pFilter->Release();
       return E_FAIL;
   }

   return S_OK;
}
然后,我们创建一个侦听器来接收发送到过滤器图的所有通知消息。如您所见,我们还注册了 EC_REPAINT 消息作为我们将处理的特定消息(通常由过滤器图处理)。以下代码段可嵌入任何 init() 例程中:

// get hold of the event notification handle so we can wait for completion or other messages
IMediaEvent *pME;
hr = m_pGraph->QueryInterface(IID_IMediaEvent, (void **) &pME);
if (FAILED(hr))
{  DeleteCourrentFilterGraph();
   return FALSE;
}

hr = pME->GetEventHandle((OAEVENT*) &m_hGraphNotifyEvent);

// the EC_REPAINT message has a special meaning to us
pME->CancelDefaultHandling(EC_REPAINT);

pME->Release();
EC_REPAINT 消息会发送新帧缓冲区准备好进行最终屏幕渲染的次数。我们在主活动循环中所做的是检查收到的任何消息,并按以下方式处理:

GetGraphEventHandle() 方法检索我们刚刚创建的侦听器的句柄。

int CMovieApp::Run()
{
   // Overridden to check for Graph events as well as messages

   if (m_pMainWnd == NULL && AfxOleGetUserCtrl())
   {
      // Not launched /Embedding or /Automation, but has no main window!
      TRACE0("Warning: m_pMainWnd is NULL in CMovieApp::Run - quitting application.\n");
      AfxPostQuitMessage(0);
   }

   BOOL bIdle = TRUE;
   LONG lIdleCount = 0;
   HANDLE  ahObjects[1];               // handles that need to be waited on
   const int cObjects = 1;             // no of objects that we are waiting on

   // message loop lasts until we get a WM_QUIT message
   // upon which we shall return from the function
   while (TRUE)
   {
      // If we don't have an event handle then process idle
      // routines until a message arrives or until the idle routines
      // stop (when we block until a message arrives). The graph event
      // handle can only be created in response to a message
      if( (ahObjects[ 0 ] = GetView()->GetGraphEventHandle()) == NULL )
      {
         while ( bIdle && !::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))
         {
            // call OnIdle while in bIdle state
            if (!OnIdle(lIdleCount++))
            {
               bIdle = FALSE;
               WaitMessage();
            }
         }
      }
      else
      {
         // wait for any message sent or posted to this queue
         // or for a graph notification. If there is no message or event
         // and we are idling then we process the idle time routines
         DWORD result;

         result = MsgWaitForMultipleObjects( cObjects, ahObjects,
                                             FALSE, (bIdle ? 0 : INFINITE),
                                             QS_ALLINPUT);
         if( result != (WAIT_OBJECT_0 + cObjects) )
         {
            // not a Windows message... GREAT !! The buffer is ready
            if( result == WAIT_OBJECT_0 )  GetView()->OnGraphNotify();

            else if( result == WAIT_TIMEOUT )
                    if(!OnIdle(lIdleCount++))
                        bIdle = FALSE;

            continue;
         }
      }

      // When here, we either have a message or no event handle
      // has been created yet.

      // read all of the messages in this next loop
      // removing each message as we read it
      do
      {
         // pump message, but quit on WM_QUIT
         if (!PumpMessage())
            return ExitInstance();

         // reset "no idle" state after pumping "normal" message
         if (IsIdleMessage(&m_msgCur))
         {
            bIdle = TRUE;
            lIdleCount = 0;
         }

      } while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));

   } // end of the always while-loop

}
该方法的实现是这样的,它会休眠并在 EC_REPAINT(或任何其他事件通知代码)到达时唤醒,然后进行处理,然后处理所有窗口消息,最后调用 OnIdle 并再次循环。总而言之,CPU 执行了主动休眠。当您的应用程序暂停时,CPU 百分比几乎为 0,当它正在流式传输时,它只占用最终屏幕渲染的处理时间。与之前的解决方案完全不同。

请注意,我们重写了 CWinApp::Run() 方法,而不是 OnIdle(),OnIdle() 保留其默认实现,即 CPU 可以说进入空闲状态。

每次收到新消息时,不一定是 EC_REPAINT,它可能是 EC_COMPLETE(请参阅官方文档中以下节点以获取事件代码的完整列表:DirectShow / C++ reference / Event Notification codes),然后我们回调视频引擎实现的 OnGraphNotify() 方法。

对于收到的任何 EC_REPAINT 消息,我们获取随消息本身传递的参数,并处理屏幕上的最终渲染。简而言之,因为这在网站上的另一篇文章中已经解释过,另一个基本参数是指向由我们的自定义渲染器过滤器完全处理的核心 DirectDraw 表面的指针。因此,此消息确实为我们提供了最终blit所需的所有数据。

OnGraphNotify() 实现还必须处理其他事件代码,例如 EC_COMPLETE,否则程序可能在流结束时崩溃。

// If the event handle is valid, ask the graph
// if anything has happened. eg the graph has stopped...
void CMovieView::OnGraphNotify()
{
   IMediaEvent *pME;
   long lEventCode, lParam1, lParam2;
   static long count=0;

   ASSERT( m_hGraphNotifyEvent != NULL );
   ASSERT( m_pGraph != NULL);

   if( SUCCEEDED(m_pGraph->QueryInterface(IID_IMediaEvent, (void **) &pME)))
   {
      if( SUCCEEDED(pME->GetEvent(&lEventCode, &lParam1, &lParam2, 0)))
      {
         // Free parameters only if this is something else than EC_REPAINT
         // because we have an important paramter to extract from the message structure
         if (lEventCode!=EC_REPAINT)
         {
            HRESULT hrTmp = pME->FreeEventParams(lEventCode, lParam1, lParam2);
            ASSERT(SUCCEEDED(hrTmp));
         }
         else
         {
            //TRACE("EC_REPAINT %ld:A repaint is required.\n",count++);
            // get a pointer to the core DirectDraw surface
            m_pDDSOffscreen2 = (IDirectDrawSurface*)lParam1;
            // render to the screen immediately
            RenderToSurface();
         }

         ...
      }
   }
}
那么同步怎么办?它有效且被隐藏了!!!级联过滤器管理视频和音频流。音频渲染器有一个参考时钟,当您使用过滤器图管理器时,这是默认行为,因此任何无法由过滤器处理的帧都会被丢弃,而我们收到的帧是与音频流同步的帧,因此是用于在听到姐妹音频数据时进行一致的即时屏幕渲染的候选帧。


解决方案 #3:使用多线程进行异步流式传输

这次我们回到多媒体流式传输接口。我们还没有完全利用 IDirectDrawStreamSample 接口的 Update() 方法的全部功能。事实上,当不传递特定标志时,Update() 方法调用会尽快返回,但不会早于下一个帧缓冲区准备好进行最终渲染。但 Update 还有更多功能。我们可以使用 SSUPDATE_ASYNC 标志,并传递一个 WIN32 事件来在下一个帧缓冲区准备好时发出信号,或者甚至传递一个指向回调函数的指针,该函数在该时自动调用。

我们建议的实现使用 WIN32 的事件模型。在初始化时,我们创建一个 Apartment 线程(即带有窗口消息循环的线程(这有助于序列化方法调用)),然后创建一个触发事件的实例,并将该事件的句柄传递给线程。

那么,我们在该线程中所做的事情很简单:我们等待一个信号,然后向视频引擎发送一条消息,通知现在应该渲染帧缓冲区。否则,我们将休眠。

以下代码显示了辅助线程的代码。

///////////////////////////////////////////////////////////
//
// CStreamUpdater
//   - A simple C++ class that encapsulates creating a
//     component on an apartment thread.
//
class CStreamUpdater
{
public:
   // Constructor
   CStreamUpdater();

   // Destructor
   virtual ~CStreamUpdater();

   // Create and start the thread.
   BOOL StartThread(CMovieView *pView,DWORD WaitTime = 500);

   // Stop the thread.
   void StopThread();

   // Current thread status
   BOOL IsThreadStarted();

   // Member variables
protected:

   // ID of thread
   DWORD m_ThreadId;

   // Handle to thread
   HANDLE m_hThread;

   // Event signals main thread to continue.
   HANDLE m_hComponentReadyEvent;

   // Time to wait before calling WorkerFunction
   DWORD m_WaitTime;

   CMovieView *m_cpView;

   BOOL m_bShouldStopNow;

   // Internal helper functions
private:
   // Thread procedure
   static DWORD WINAPI RealThreadProc(void* pv);

   // Member thread procedure
   DWORD ClassThreadProc();

   // Wait for an event, but process window messages.
   BOOL WaitWithMessageLoop(HANDLE hEvent);
}
///////////////////////////////////////////////////////////
//
// Constructor
//
CStreamUpdater::CStreamUpdater()
{
   m_ThreadId = 0;
   m_hThread  = NULL;
   m_hComponentReadyEvent = NULL;
   m_WaitTime = 500;
}

///////////////////////////////////////////////////////////
//
// Destructor
//
CStreamUpdater::~CStreamUpdater()
{
   // The thread must be stopped before we are deleted
   // because the WorkerFunction is in the derived class.
   StopThread();
}

///////////////////////////////////////////////////////////
//
// StartThread
//   - Create and start the thread.
//
BOOL CStreamUpdater::StartThread(CMovieView *pView,DWORD WaitTime)
{
   m_cpView = pView;
   m_bShouldStopNow=FALSE;

   if (IsThreadStarted())
   {
      return FALSE;
   }

   // Create the thread.
   m_hThread = ::CreateThread(NULL,              // Default security
                              0,                 // Default stack size
                              RealThreadProc,
                              (void*)this,
                              CREATE_SUSPENDED,  // Create the thread suspended.
                              &m_ThreadId);     // Get the Thread ID.
   if (m_hThread == NULL)
   {
      return FALSE;
   }

   // Create an event for the thread to signal when it is finished.
   m_hComponentReadyEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
   if (m_hComponentReadyEvent == NULL)
   {
      return FALSE;
   }

   // Initialize the wait time.
   m_WaitTime = WaitTime;

   // Thread was created suspended; start the thread.
   DWORD r = ResumeThread(m_hThread);
   assert(r != 0xffffffff);

   // Wait for the thread to start up before we continue.
   WaitWithMessageLoop(m_hComponentReadyEvent);

   return TRUE;
}

///////////////////////////////////////////////////////////
//
// Stop Thread
//
void CStreamUpdater::StopThread()
{
   m_bShouldStopNow = TRUE;
}

///////////////////////////////////////////////////////////
//
// Current thread status
//
BOOL CStreamUpdater::IsThreadStarted()
{
   return (m_hThread != NULL);
}

///////////////////////////////////////////////////////////
//
// Thread procedure
//
DWORD WINAPI CStreamUpdater::RealThreadProc(void* pv)
{
   CStreamUpdater* pApartment = reinterpret_cast<CStreamUpdater*>(pv);
   return pApartment->ClassThreadProc();
}

///////////////////////////////////////////////////////////
//
// Thread procedure
//
DWORD CStreamUpdater::ClassThreadProc()
{
   // Signal that we are starting.
   SetEvent(m_hComponentReadyEvent);

   HANDLE hUpdater=m_cpView->getUpdateEvent(); // retrieve the event from the video engine

   // Wait for the signal to create a component.
   BOOL bContinue = TRUE;

   while (bContinue )
   {
      switch( ::WaitForSingleObject( hUpdater,m_WaitTime) )
      {
         // Update the surface becoz now it's ready
         case WAIT_OBJECT_0:
            ::SendMessage(m_cpView->GetSafeHwnd(),WM_USER+100,0,0);
            break;
         // Do background processing.
         case WAIT_TIMEOUT:
            if (m_bShouldStopNow)
               bContinue=FALSE;
            break;

         default:break;
      }
   }

   return 0;
}

///////////////////////////////////////////////////////////
//
// BOOL WaitWithMessageLoop(HANDLE hEvent)
//
BOOL CStreamUpdater::WaitWithMessageLoop(HANDLE hEvent)
{
   while (TRUE)
   {
      // Wait for the event and for messages.
      DWORD dwReturn = ::MsgWaitForMultipleObjects(1,
                                                   &hEvent,
                                                   FALSE,
                                                   INFINITE,
                                                   QS_ALLINPUT);
      if (dwReturn == WAIT_OBJECT_0)
      {
         // Our event happened.
         return TRUE;
      }
      else if (dwReturn == WAIT_OBJECT_0 + 1)
      {
         // Handle message to keep client alive.
         MSG msg;
         while(::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
         {
            ::DispatchMessage(&msg);
         }
      }
      else
      {
         return FALSE;
      }
   }
}
请注意,我们根本不需要自定义 CWinApp::OnIdle() 或 CWinApp::Run()。因此,CPU 不会像解决方案 #1 那样加载到 100%。

每当辅助线程收到信号时,它不会调用 RenderToSurface()(一个执行最终屏幕渲染的方法),因为它如果这样做,将使用此特定执行单元(此线程),尽管渲染步骤应始终由视频引擎本身执行。因此,会向包装视频引擎的窗口发送一条消息,并且实际的渲染步骤是在消息循环在 CWinApp::Run() 实现中处理消息时完成的。由于可能发生许多其他消息,包括 WM_MOUSEMOVE 等,消息 WM_USER+100 可能不会立即处理。最好是没有任何消息需要大量处理时间,否则在帧缓冲区准备好(并听到音频)和实际图像显示在屏幕之间会有延迟。

现在,剩下要看的是处理 WM_USER+100 消息时调用的方法。

BEGIN_MESSAGE_MAP(CMovieView, CView)
   //{{AFX_MSG_MAP(CMovieView)
   ON_MESSAGE( WM_USER+100, OnRefresh )
   ...
   //}}AFX_MSG_MAP
   END_MESSAGE_MAP()
afx_msg void CMovieView::OnRefresh(UINT wparam, LONG lparam)
{
   TRACE ("Refresh msg received\n");
   Update();
}

void CMovieView::Update(BOOL bForceUpdate)
{
   if (!m_bFileLoaded) return;

   ::ResetEvent(m_hUpdateEvent);

   // final rendering on screen
   RenderToSurface();

   if ( bForceUpdate || IsPlaying() )
   {
      // update each frame
      pMediaSample->Update(SSUPDATE_ASYNC, m_hUpdateEvent, NULL, 0);
   }
}
另请注意,当多媒体流运行时,您应该调用一次上述方法,以便点燃信号。

附加说明:Update() 调用还有另一种选择。使用 SSUPDATE_ASYNC 标志,我们可以传递一个事件句柄,或者不传递任何事件句柄,而是迭代地尝试调用 CompletionStatus(),这是一个与 Update() 相同级别的实现的方法。此方法有助于循环并等待新的帧缓冲区状态。

可能的实现是:

pMediaSample->Update(SSUPDATE_ASYNC, NULL, NULL, 0);

do
{
  hr = pMediaSample->CompletionStatus(COMPSTAT_WAIT,10); // wait 10 milliseconds before returning
}
while (hr==MS_S_PENDING);

if (FAILED(hr))
{
   // handling error
}

换句话说,CompletionStatus 的返回值是 MS_S_PENDING,表示一个帧缓冲区正在等待,直到它变为 S_OK 或任何错误。该方法在完成或在下 10 毫秒结束时返回。这种速率适用于大多数情况,因为许多视频流的编码速率为每秒 15 帧或每秒 25 帧,即两帧之间的时间间隔为 40 毫秒。

下载解决方案 #3 的源代码:使用单独的线程进行异步渲染机制。


结论

本文介绍了在 DirectShow 下渲染多媒体流的几种同步技术。源代码没有版权。

Stephane Rodriguez -
1999年9月20日

DirectShow 下的多媒体流同步机制 - CodeProject - 代码之家
© . All rights reserved.