DirectShow 音视频剪辑






4.44/5 (6投票s)
如何使用 DirectShow 剪切媒体文件的指定部分
引言
有时,用户会问有关视频和音频剪辑的问题,然后搜索能够完成此任务的实用工具。但是,对于此任务的程序化实现又如何呢?本文将从程序员的角度展示它是如何实现的。首先,我将向您介绍平台和必要的媒体接口。它将是Win32和核心Windows媒体技术DirectShow。此外,我们还需要安装带有Media Player Classic的“K-Lite Codec Pack”,它用于播放媒体文件。
我们的应用程序由两部分组成
- 一个DirectShow转换过滤器,它实际上执行必要的媒体分割
- 一个控制台应用程序,演示如何使用此过滤器
本文展示的代码代表了此过程的思想,并非完整的应用程序。但是,它可以成为各种流行应用程序(如不同的媒体(视频和音频)分割器)的坚实基础。
背景
让我们来看看DirectShow转换过滤器——它们是什么?首先,它们允许程序员对流经这些过滤器的媒体样本进行任何操作。但是,我们需要从媒体流中排除指定的间隔吗?答案可能是:我们需要丢弃所有我们不想在输出中看到的样本。如果我们在这里停止,我们将得到一个不包含指定数据间隔的媒体文件,但被剪切的部分将被之前播放的最后一个样本填充,该样本在剪切间隔开始之前。这并不是我们想要的结果。为了避免这种情况,我们不仅需要简单地从媒体流中排除时间块,还需要获取下一个样本并更正它们的时间,以便它们从剪切点开始。这将使我们的过滤器在没有暂停和中断效果的情况下通过样本。为了使其更通用,我们需要将前面的规则应用于所有排除的间隔。我们需要将下一个样本块向前推进,其时间表示被剪切间隔时间的总和。要更详细地了解这一点,请查看应用程序代码。
Using the Code
要运行应用程序代码,您需要将chicken.wmv文件复制到C:\。这是必要的,因为我们使用手动构建的图用于渲染,该图从指定位置获取输入文件。我们不使用代码图构建,因为这是一个演示应用程序,旨在展示核心原理,而不是所有DirectShow应用程序的特定需求。您需要启动演示代码的第二件事是使用类似regsvr32 DShowMediaSplitFilt.ax
的shell命令将过滤器注册到您的系统中。然后启动Demo.exe,执行过程完成后,您将在C:\中看到proceed.wmv文件,该文件代表时间块排除过程的结果。
此处使用的图如下所示

您可以看到我们使用了两个过滤器实例来同步视频和音频的排除。我们还在应用程序中为所有图过滤器使用了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的程序化实现来实现视频剪辑过程。我们可以看到它的基础已经成功建立。