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

DirectShow 音视频剪辑

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.44/5 (6投票s)

2008年3月19日

CPOL

4分钟阅读

viewsIcon

108504

downloadIcon

3241

如何使用 DirectShow 剪切媒体文件的指定部分

引言

有时,用户会问有关视频和音频剪辑的问题,然后搜索能够完成此任务的实用工具。但是,对于此任务的程序化实现又如何呢?本文将从程序员的角度展示它是如何实现的。首先,我将向您介绍平台和必要的媒体接口。它将是Win32和核心Windows媒体技术DirectShow。此外,我们还需要安装带有Media Player Classic的“K-Lite Codec Pack”,它用于播放媒体文件。

我们的应用程序由两部分组成

  1. 一个DirectShow转换过滤器,它实际上执行必要的媒体分割
  2. 一个控制台应用程序,演示如何使用此过滤器

本文展示的代码代表了此过程的思想,并非完整的应用程序。但是,它可以成为各种流行应用程序(如不同的媒体(视频和音频)分割器)的坚实基础。

背景

让我们来看看DirectShow转换过滤器——它们是什么?首先,它们允许程序员对流经这些过滤器的媒体样本进行任何操作。但是,我们需要从媒体流中排除指定的间隔吗?答案可能是:我们需要丢弃所有我们不想在输出中看到的样本。如果我们在这里停止,我们将得到一个不包含指定数据间隔的媒体文件,但被剪切的部分将被之前播放的最后一个样本填充,该样本在剪切间隔开始之前。这并不是我们想要的结果。为了避免这种情况,我们不仅需要简单地从媒体流中排除时间块,还需要获取下一个样本并更正它们的时间,以便它们从剪切点开始。这将使我们的过滤器在没有暂停和中断效果的情况下通过样本。为了使其更通用,我们需要将前面的规则应用于所有排除的间隔。我们需要将下一个样本块向前推进,其时间表示被剪切间隔时间的总和。要更详细地了解这一点,请查看应用程序代码。

Using the Code

要运行应用程序代码,您需要将chicken.wmv文件复制到C:\。这是必要的,因为我们使用手动构建的图用于渲染,该图从指定位置获取输入文件。我们不使用代码图构建,因为这是一个演示应用程序,旨在展示核心原理,而不是所有DirectShow应用程序的特定需求。您需要启动演示代码的第二件事是使用类似regsvr32 DShowMediaSplitFilt.ax的shell命令将过滤器注册到您的系统中。然后启动Demo.exe,执行过程完成后,您将在C:\中看到proceed.wmv文件,该文件代表时间块排除过程的结果。

此处使用的图如下所示

DShowMediaSplit

您可以看到我们使用了两个过滤器实例来同步视频和音频的排除。我们还在应用程序中为所有图过滤器使用了IReferenceClock同步。请查看过滤器代码

 // DirectShow Media Split Filter header file by Kovalev Maxim
#ifndef __DSHOWMEDIASPLITFILT_H__
#define __DSHOWMEDIASPLITFILT_H__
#pragma warning (disable:4312)
#include <streams.h> // DirectShow (includes windows.h)
#include <initguid.h> // Declares DEFINE_GUID to declare an EXTERN_C const
#include <vector>
using std::vector;
// {1A21958D-8E93-45de-92DA-DD9544297B41}
DEFINE_GUID (CLSID_DShowMediaSplit, 0x1a21958d, 0x8e93, 0x45de, 0x92, 0xda,
    0xdd, 0x95, 0x44, 0x29, 0x7b, 0x41);
struct TimeBlock
{
 TimeBlock (LONGLONG startTime, LONGLONG endTime)
 {
  this->startTime = startTime;
  this->endTime = endTime;
 }
 
 LONGLONG startTime;
 LONGLONG endTime;
};
// {EC634576-4C86-464e-99D7-AC7AFEFB2FF0}
DEFINE_GUID (IID_IDShowMediaSplitFilt, 0xec634576, 0x4c86, 0x464e, 0x99, 0xd7,
    0xac, 0x7a, 0xfe, 0xfb, 0x2f, 0xf0);
interface IDShowMediaSplitFilt : public IUnknown
{
 STDMETHOD (SetIntervalList) (const vector <TimeBlock> &lst) = 0;
};
#endif
// DirectShow Media Split Filter source file by Kovalev Maxim
#include "DShowMediaSplitFilt.h"
// Setup data - allows the self-registration to work
const AMOVIESETUP_MEDIATYPE sudPinTypes =
 { &MEDIATYPE_NULL     // clsMajorType
 , &MEDIASUBTYPE_NULL  // clsMinorType
 };
const AMOVIESETUP_PIN psudPins [] = {
 { L"Input"            // strName
   , FALSE             // bRendered
   , FALSE             // bOutput
   , FALSE             // bZero
   , FALSE             // bMany
   , &CLSID_NULL       // clsConnectsToFilter
   , L""               // strConnectsToPin
   , 1                 // nTypes
   , &sudPinTypes      // lpTypes
 }
 ,
 { L"Output"           // strName
 , FALSE               // bRendered
 , TRUE                // bOutput
 , FALSE               // bZero
 , FALSE               // bMany
 , &CLSID_NULL         // clsConnectsToFilter
 , L""                 // strConnectsToPin
 , 1                   // nTypes
 , &sudPinTypes        // lpTypes
 }
 };
const AMOVIESETUP_FILTER sudTransformSample =
 { &CLSID_DShowMediaSplit           // clsID
 , L"DirectShow Media Split Filter" // strName
 , MERIT_DO_NOT_USE                 // dwMerit
 , 2                                // nPins
 , psudPins                         // lpPin
 };
struct TimeBlockEx : public TimeBlock
{
 TimeBlockEx (LONGLONG startTime, LONGLONG endTime,
  LONGLONG timeAtSkipped, LONGLONG timeAtNonSkipped)
  : TimeBlock (startTime, endTime)
 {
  this->timeAtSkipped = timeAtSkipped;
  this->timeAtNonSkipped = timeAtNonSkipped;
  skippedFound = false;
  nonSkippedFound = false;
  needToSkip = false;
 }
 
 LONGLONG timeAtSkipped;
 LONGLONG timeAtNonSkipped;
 bool skippedFound;
 bool nonSkippedFound;
 bool needToSkip;
};
class CDShowMediaSplitFilt : public CTransInPlaceFilter, IDShowMediaSplitFilt
{
public:
 // CreateInstance. Provide the way for COM to create a "CDShowMediaSplitFilt" object
 static CUnknown *WINAPI CreateInstance (LPUNKNOWN pUnk, HRESULT *pHr);
 DECLARE_IUNKNOWN;
 
 // Method, which provides specified interface querying
 STDMETHODIMP NonDelegatingQueryInterface (REFIID riid, void **ppv);
 // Method for storing time blocks list into class members
 STDMETHODIMP SetIntervalList (const vector <TimeBlock> &lst);
private:
 CDShowMediaSplitFilt (TCHAR *tszName, LPUNKNOWN pUnk, HRESULT *pHr)
  : CTransInPlaceFilter (tszName, pUnk, CLSID_DShowMediaSplit, pHr) {}
 // Filter's transform method - where core work done
 HRESULT Transform (IMediaSample *pSample);
 // We accept any input type. We'd return S_FALSE for any we didn't like
 HRESULT CheckInputType (const CMediaType *pMediaTypeIn);
 // Time block's array, which stores split intervals
 vector <TimeBlockEx> timeBlockList_;
};
// Needed for the CreateInstance mechanism
CFactoryTemplate g_Templates [] = {
 {
  L"DirectShow Media Split Filter",
  &CLSID_DShowMediaSplit,
  CDShowMediaSplitFilt::CreateInstance,
  NULL,
  &sudTransformSample
 }
 };
int g_cTemplates = sizeof (g_Templates) / sizeof (g_Templates [0]);
CUnknown *WINAPI CDShowMediaSplitFilt::CreateInstance (LPUNKNOWN pUnk, HRESULT *pHr)
{
 CDShowMediaSplitFilt *pNewObject = new CDShowMediaSplitFilt (
  NAME ("DirectShow Media Split Filter"), pUnk, pHr);
 if (pNewObject == NULL)
  *pHr = E_OUTOFMEMORY;
 return pNewObject;
}
STDMETHODIMP CDShowMediaSplitFilt::NonDelegatingQueryInterface (REFIID riid, void **ppv)
{
 if (riid == IID_IDShowMediaSplitFilt)
  return GetInterface (static_cast <IDShowMediaSplitFilt *> (this),
  ppv);
 return CTransInPlaceFilter::NonDelegatingQueryInterface (riid, ppv);
}
STDMETHODIMP CDShowMediaSplitFilt::SetIntervalList (const vector <TimeBlock> &lst)
{
 for (size_t i = 0; i < lst.size (); i++)
 {
  TimeBlockEx timeBlockEx (lst.at (i).startTime, lst.at (i).endTime, 0, 0);
  timeBlockList_.push_back (timeBlockEx);
 }
 return S_OK;
}
HRESULT CDShowMediaSplitFilt::Transform (IMediaSample *pSample)
{
 LONGLONG sampleStartTime = 0;
 LONGLONG sampleEndTime = 0;
 pSample->GetTime (&sampleStartTime, &sampleEndTime);
 // Summary time, for which the samples have to be moved
 LONGLONG totalDelta = 0;
 
 for (size_t i = 0; i < timeBlockList_.size (); i++)
 {
  // Check this sample time - is it needed to skip this
  if (sampleEndTime > timeBlockList_.at (i).startTime &&
   sampleEndTime < timeBlockList_.at (i).endTime)
   timeBlockList_.at (i).needToSkip = true;
  else
   timeBlockList_.at (i).needToSkip = false;
  
  // Search the first sample time, which is needed to be
  // skipped and store it
  if (sampleEndTime >= timeBlockList_.at (i).startTime &&
   !timeBlockList_.at (i).skippedFound)
  {
   timeBlockList_.at (i).timeAtSkipped = sampleEndTime;
   timeBlockList_.at (i).skippedFound = true;
  }
  // Search the first sample time, which need not to be
  // skipped and store it
  if (sampleEndTime >= timeBlockList_.at (i).endTime &&
   !timeBlockList_.at (i).nonSkippedFound)
  {
   timeBlockList_.at (i).timeAtNonSkipped = sampleEndTime;
   timeBlockList_.at (i).nonSkippedFound = true;
  }
  // All necessary times found, so calculate "delta" time
  // as a difference of founded previously times and add
  // calculated value to the summary time for using in the
  // next of cut time blocks
  if (timeBlockList_.at (i).skippedFound &&
   timeBlockList_.at (i).nonSkippedFound)
  {
   LONGLONG delta = timeBlockList_.at (i).timeAtSkipped -
    timeBlockList_.at (i).timeAtNonSkipped;
   totalDelta += delta;
   // New sample times, which makes effects, that media is not broken
   LONGLONG newSampleStartTime = sampleStartTime + totalDelta;
   LONGLONG newSampleEndTime = sampleEndTime + totalDelta;
   // Setting new times
   pSample->SetTime (&newSampleStartTime, &newSampleEndTime);
  }
  // If the current sample time is inside "i"-th interval,
  // we need to skip it
  if (timeBlockList_.at (i).needToSkip)
   return S_FALSE;
 }
 
 return NOERROR;
}
HRESULT CDShowMediaSplitFilt::CheckInputType (const CMediaType *pMediaTypeIn)
{
 return S_OK;
}
// Exported entry points for registration and unregistration
STDAPI DllRegisterServer ()
{
 return AMovieDllRegisterServer2 (TRUE);
}
STDAPI DllUnregisterServer ()
{
 return AMovieDllRegisterServer2 (FALSE);
}

我们使用了IDShowMediaSplitFilt接口,该接口采用COM风格,允许调用应用程序设置必须从输入媒体文件中排除的时间块。所有核心功能都保留在过滤器Transform方法内部。过滤器代码被大大简化了。我们的过滤器接受任何媒体类型,并且不控制它接收的类型——编码的还是解码的,但是,我们只处理解码的内容。但现在让我们看看下面的演示应用程序源代码

// DirectShow Media Split Filter demo application source file
// by Kovalev Maxim
#include <iostream>
using std::cout;
using std::endl;
#include "../DShowMediaSplitFilt/DShowMediaSplitFilt.h"
// Read manually created graph function with structured exception
// handling to make it more like C++ 
HRESULT LoadGraphFile (IGraphBuilder *pGraph, const WCHAR *wszName)
{
 IStorage *pStorage = NULL;
 IPersistStream *pPersistStream = NULL;
 IStream *pStream = NULL;
 HRESULT hr = S_OK;
 
 try
 {
  hr = StgIsStorageFile (wszName);
  if (FAILED (hr))
   throw "Graph file not found";
  
  hr = StgOpenStorage (wszName, 0,
   STGM_TRANSACTED | STGM_READ | STGM_SHARE_DENY_WRITE,
   0, 0, &pStorage);
  
  if (FAILED (hr))
   throw "Storage opening failed";
  
  hr = pGraph->QueryInterface (IID_IPersistStream,
   reinterpret_cast <void **> (&pPersistStream));
  
  if (FAILED (hr))
   throw "Couldn't query interface";
  
  hr = pStorage->OpenStream (L"ActiveMovieGraph", 0,
   STGM_READ | STGM_SHARE_EXCLUSIVE, 0, &pStream);
  
  if (FAILED (hr))
   throw "Couldn't open ActiveMovieGraph";
  
  hr = pPersistStream->Load (pStream);
  if (FAILED (hr))
   throw "Couldn't load stream data";
 }
 catch (char *str)
 {
  cout << "Error occurred: " << str << endl;
 }
 
 if (pStream) pStream->Release ();
 if (pPersistStream) pPersistStream->Release ();
 if (pStorage) pStorage->Release ();
 
 return hr;
}
// App entry point. Here we demonstrate how to use our "DirectShow Transform Filter"
int main ()
{
 const WCHAR GRAPH_FILE_NAME [] = L"Graph.GRF";
 const LONGLONG SCALE_FACTOR = 10000000;
 
 IGraphBuilder *pGraph = NULL;
 IMediaControl *pControl = NULL;
 IMediaEvent *pEvent = NULL;
 HRESULT hr = S_OK;
 IBaseFilter *pVideoSplitFilt = NULL;
 IDShowMediaSplitFilt *pVideoSplitConfig = NULL;
 IBaseFilter *pAudioSplitFilt = NULL;
 IDShowMediaSplitFilt *pAudioSplitConfig = NULL;
 IReferenceClock *pClock = NULL;
 IEnumFilters *pFiltEnum = NULL;
 
 try
 {
  cout << "Process started" << endl;
  hr = CoInitialize (NULL);
  if (FAILED (hr))
   throw "Could not initialize COM library";
  
  hr = CoCreateInstance (CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
   IID_IGraphBuilder, reinterpret_cast <void **> (&pGraph));
  if (FAILED (hr))
   throw "Could not create the Filter Graph Manager";
  
  hr = LoadGraphFile (pGraph, GRAPH_FILE_NAME);
  if (FAILED (hr))
   throw "Couldn't load graph file";
  
  hr = pGraph->FindFilterByName (L"DirectShow Media Split Filter",
   &pVideoSplitFilt);
  if (FAILED (hr))
   throw """DirectShow Media Split Filter"" not founded in graph";
  
  hr = pVideoSplitFilt->QueryInterface (IID_IDShowMediaSplitFilt,
   reinterpret_cast <void **> (&pVideoSplitConfig));
  if (FAILED (hr))
   throw "IDShowMediaSplitFilt config 4 video splitter couldn't be retrieved";
  
  hr = pGraph->FindFilterByName (L"DirectShow Media Split Filter 0001",
   &pAudioSplitFilt);
  if (FAILED (hr))
   throw """DirectShow Media Split Filter 0001"" not founded in graph";
  
  hr = pAudioSplitFilt->QueryInterface (IID_IDShowMediaSplitFilt,
   reinterpret_cast <void **> (&pAudioSplitConfig));
  if (FAILED (hr))
   throw "IDShowMediaSplitFilt config 4 audio splitter couldn't be retrieved";
  
  vector <TimeBlock> timeBlockList;
  timeBlockList.push_back (TimeBlock (1 * SCALE_FACTOR, 3 * SCALE_FACTOR));
  timeBlockList.push_back (TimeBlock (5 * SCALE_FACTOR, 9 * SCALE_FACTOR));
  
  // Setting equal cutting intervals 4 both filter instances - video/audio
  pVideoSplitConfig->SetIntervalList (timeBlockList);
  pAudioSplitConfig->SetIntervalList (timeBlockList);
  
  hr = CoCreateInstance (CLSID_SystemClock, NULL, CLSCTX_INPROC_SERVER,
   IID_IReferenceClock, reinterpret_cast <void **> (&pClock));
  if (FAILED (hr))
   throw "Failed to create reference clock";
  
  hr = pGraph->EnumFilters (&pFiltEnum);
  if (FAILED (hr))
   throw "Couldn't enumerate graph filters";
  
  IBaseFilter *pCurrFilt = NULL;
  while (pFiltEnum->Next (1, &pCurrFilt, 0) == S_OK)
  {
   pCurrFilt->SetSyncSource (pClock);
   pCurrFilt->Release ();
  }
  
  hr = pGraph->QueryInterface (IID_IMediaControl, reinterpret_cast <
   void **> (&pControl));
  if (FAILED (hr))
   throw "Media control couldn't be retrieved";
  
  hr = pGraph->QueryInterface (IID_IMediaEvent, reinterpret_cast <
   void **> (&pEvent));
  if (FAILED (hr))
   throw "Media event couldn't be retrieved";
  
  hr = pControl->Run ();
  if (FAILED (hr))
   throw "Graph couldn't be started";
  
  long evCode = 0;
  pEvent->WaitForCompletion (INFINITE, &evCode);
 }
 catch (char *str)
 {
  cout << "Error occurred: " << str << endl;
 }
 
 if (pFiltEnum) pFiltEnum->Release ();
 if (pClock) pClock->Release ();
 if (pAudioSplitConfig) pAudioSplitConfig->Release ();
 if (pAudioSplitFilt) pAudioSplitFilt->Release ();
 if (pVideoSplitConfig) pVideoSplitConfig->Release ();
 if (pVideoSplitFilt) pVideoSplitFilt->Release ();
 if (pEvent) pEvent->Release ();
 if (pControl) pControl->Release ();
 if (pGraph) pGraph->Release ();
 
 CoUninitialize ();
 cout << "Done." << endl;
 
 return EXIT_SUCCESS;
}

我们如何看出这是DirectShow特定的代码,它展示了如何通过COM接口设置过滤器属性。在我们的应用程序中,我们需要排除的是时间块。时间块以second为单位设置,并乘以一个因子,该因子允许将这些时间用作DirectShow风格的参考时间。

关注点

当然,有很多方法可以完成类似的任务。例如,我们可以使用DirectShow Editing Services。但在我看来,非标准的方法在实现起来更有趣。

历史

  • 2008年3月19日:初始帖子

    前段时间,我对上面描述的任务的实现非常感兴趣,但找不到任何有用的材料或代码,于是决定自己编写。它旨在通过DirectShow的程序化实现来实现视频剪辑过程。我们可以看到它的基础已经成功建立。

© . All rights reserved.