用 C# 编写 DirectShow 解复用器。第二部分 - AVI 文件源示例





5.00/5 (7投票s)
本文介绍了实现自定义文件源 DirectShow 过滤器的基本任务。

引言
关于如何创建 DirectShow 解复用器的第二部分。在这里,我将通过一个 AVI 文件源过滤器在 C# 中的实现示例来回顾如何实现源过滤器。该示例基于我的类库:BaseClasses.NET,该库在文章 Pure .NET DirectShow Filters in C# 中进行了描述。背景
源过滤器类似于分离器(Splitters),但它不包含输入引脚。取而代之的是,它使用 IFileSourceFilter
接口加载文件、流或 URL。一旦用户调用其接口的 Load
方法,过滤器就会执行输入文件的验证并创建输出引脚。在播放过程中,过滤器直接从文件中读取和解析数据并将其传递出去。
FilterGraph 如何决定使用源过滤器?
在上一篇文章中,我们回顾了分离器过滤器的注册。源过滤器也将信息存储在系统注册表中。源过滤器可以按文件扩展名或按协议进行注册。
注册协议
协议注册主要用于流。在调用 RenderFile 时,Filter Graph 会自动决定为指定的协议使用哪个源过滤器。它在以下注册表子键中进行注册:
HKCR\<your protocol>\Extensions
这里 <your protocol> - 是协议名称,例如“http”、“https”、“mms”或您自己的协议。在“Extensions”子键下,将文件扩展名作为名称,将您的源过滤器的 GUID 作为值。如果过滤器适用于任何扩展名,则“Source Filter”值(带有 GUID)直接放置在 <your protocol> 的子键下。这里是 http 协议的截图:

在我创建的类库中,我添加了一个属性来方便执行此类注册。
[ComVisible(false)]
[AttributeUsage(AttributeTargets.Class, AllowMultiple=true)]
public class RegisterProtocolExtension : Attribute
以下构造函数是可用的:
public RegisterProtocolExtension(string _protocol)
public RegisterProtocolExtension(string _protocol, string _extension)
该属性可以放在过滤器类或解析器类上。
注册扩展名
扩展名的工作方式与协议类似,因此 Filter Graph 会检查文件的扩展名,在注册表中查找条目,并在指定了源过滤器时加载它。扩展名在以下注册表子键中注册:
HKCR\Media Type\<your extension>
<your extension> 应以点开头,例如“.mp3”。在该子键下,指定了一个主要值,该值由过滤器的 GUID 组成,并命名为“Source Filter”。另外还有两个可能的 GUID 值,分别名为“Media Type”和“SubType”,这些值在 RenderFile 调用时会传递给过滤器。这是示例:

类库也通过另一个属性处理了这个问题。
[ComVisible(false)]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class RegisterFileExtension : Attribute
以及它的构造函数:
public RegisterFileExtension(string _extension)
public RegisterFileExtension(string _extension,string _MediaType,string _SubType)
如果没有找到协议也没有找到扩展名,那么 Filter Graph 将使用“File Source Async”过滤器作为源。
基本过滤器架构
在当前的示例中,我们将使用另一种模型,其中每个引脚在自己的线程中从文件中读取数据。因此,每个输出都是异步工作的,并尽快传递样本。这看起来就像每个引脚都从文件中读取自己的数据进行传递。

黑色箭头表示从文件中查询数据。这种架构比上一篇文章中的要简单得多,并且易于实现,因为这里可能不需要队列(在当前示例中未使用)。文件加载后即可创建输出引脚。文件源过滤器的所有主要内容与上一篇文章中描述的相同。我们将回顾一些附加功能。
加载文件
在文件源过滤器中,我们需要实现 IFileSourceFilter
接口。它用于向源过滤器指定文件路径、URL 或其他字符串数据。此外,还可以选择性地指定 Media Type。它通常由 Graph Manager 使用注册表为“File Source Async”过滤器设置,如前一篇文章所述。对于“File Source Async”,我们在输出引脚上看到该类型。如果我们使用自己的文件源,则不需要传递 Media Type。一旦用户在此过滤器中调用此方法,我们会检查给定文件并决定是否可以处理它。如果无法处理,我们应返回错误。否则,我们将加载音轨并初始化输出引脚。验证会调用两次:第一次,解析器中的 BitStreamReader
对象未初始化(为 null
) - 这是您直接在解析器中处理文件的情况;如果第一次调用失败,基类将初始化 reader 对象并调用第二次。
打开进度
此外,源过滤器还可以实现 IAMOpenProgress
接口(主要在 File Source 过滤器中实现)。应用程序可以使用此接口来检查打开操作的进度。例如,如果我们从网络打开媒体或在打开文件过程中使用其他可能耗时的事情,那么应用程序可以在单独的线程中查询进度和/或中止操作。类库对该接口有简要实现,您可以根据自己的需要进行修改。
实现流选择
流选择允许应用程序启用或禁用指定的音频输出,以防文件中存在多个音频音轨。此机制可通过 IAMStreamSelect
接口获得。该接口允许提取音频音轨信息、文件中可用音轨的数量,并启用或禁用它们以供播放。在我的类库中,BaseSplitter
实现该接口,因此它被自动支持,并且可以被应用程序使用。在音轨实现中,您可以指定音频音轨的名称和 LCID - 以显示音频的语言。大多数播放器应用程序支持使用此接口。启用或禁用实现中的棘手之处在于,我们需要向前搜索一小段时长,然后向后搜索 - 这是必需的,因为选定的搜索引脚可能已被禁用,我们需要在过滤器图中重新选择它。
if (IsActive && dwFlags != AMSTREAMSELECTENABLEFLAGS.DISABLE)
{
try
{
IMediaSeeking _seeking = (IMediaSeeking)FilterGraph;
if (_seeking != null)
{
long _current;
_seeking.GetCurrentPosition(out _current);
_current -= UNITS / 10;
_seeking.SetPositions(_current, AMSeekingSeekingFlags.AbsolutePositioning, null, AMSeekingSeekingFlags.NoPositioning);
_current += UNITS / 10;
_seeking.SetPositions(_current, AMSeekingSeekingFlags.AbsolutePositioning, null, AMSeekingSeekingFlags.NoPositioning);
}
}
catch
{
}
}
在 GraphStudio 应用程序中,我们的示例中流选择的外观如下:

AVI 源过滤器实现
使用上面描述的模型,我们可以轻松实现 AVI 源过滤器。我们将使用 VFW API,因为它足以满足我们的示例。AVI 有一个索引表,可以非常快速地访问音轨中的样本,并且可以在单独的线程中完成,因此我们的模型应该可以工作。让我们看看我们的过滤器外观:
[ComVisible(true)]
[Guid("C55996E5-89C1-4a03-93FB-2985EC8CBC0E")]
[AMovieSetup(true)]
[PropPageSetup(typeof(AboutForm))]
[RegisterFileExtension(".avx")]
public class AVISourceFilter : BaseSourceFilterTemplate<AVIParser>
{
public AVISourceFilter()
: base("CSharp AVI Source")
{
}
}
我为注册文件扩展名并将其与我们的过滤器关联添加了属性,但代替“.avi”,我指定了“.avx”。因此,如果您将 AVI 文件的扩展名更改为“.avx”,并在 GraphEdit 中尝试渲染它,或者在任何其他应用程序中播放,都会使用我们的过滤器。与上一篇文章一样,我们专注于实现文件格式支持。
实现解析器
由于在当前实现中,我们不使用通用的解复用线程,并且每个音轨独立工作,所以在解析器中,我们只进行文件检查和音轨初始化。数据包的读取和搜寻都在音轨中直接进行。
打开文件
我之前提到我们使用 IFileSourceFilter
接口加载文件。所有操作都在基类中完成,在解析器中,我们只需要覆盖与以前相同的方法:基解析器类的 CheckFile
。在此方法中,我们检查文件而不是上一篇文章中的流。
protected override HRESULT CheckFile()
{
if (String.IsNullOrEmpty(m_sFileName)) return E_INVALIDARG;
return (HRESULT)AVIFileOpen(out m_pAviFile, m_sFileName, AVIOpenFlags.Read, null);
}
这非常简单:如果 AVIFileOpen
API 成功 - 我们可以播放文件,否则 - 不能。注意:我们应该检查 m_sFileName
变量是否已初始化(因为初始化可以执行两次,方式不同)。
加载音轨
文件检查后的下一步是加载音轨。我们有一个由调用 CheckFile
初始化 AVI 文件句柄。在这里,我们使用 AVIFileInfo
API 来获取文件和其中所有流的信息。然后,我们使用 AVIFileGetStream
基于流创建音轨。
AVIFILEINFO _info = new AVIFILEINFO();
hr = (HRESULT)AVIFileInfo(m_pAviFile, _info, Marshal.SizeOf(_info));
if (hr.Failed) return hr;
if (_info.dwStreams == 0)
{
return E_UNEXPECTED;
}
for (int i = 0; i < (int)_info.dwStreams; i++)
{
IntPtr _stream;
if (S_OK == AVIFileGetStream(m_pAviFile, out _stream, 0, i))
{
AVITrack _track = new AVITrack(this, _stream);
if (S_OK == _track.InitTrack())
{
m_Tracks.Add(_track);
}
else
{
_track.Dispose();
_track = null;
}
}
}
与上一个示例一样,我们将其他初始化放在音轨自己的方法中。在这里,我们只检查结果,如果失败则丢弃流的播放。此外,我们计算媒体的持续时间。
m_rtDuration = 0;
if (_info.dwRate != 0)
{
m_rtDuration = _info.dwLength * _info.dwScale / _info.dwRate;
}
实现音轨
在当前实现中,所有主要功能都位于音轨中。每个音轨执行以下任务:
- 初始化
- 提供用于连接的媒体类型
- 跳转
- 从文件读取数据并准备数据包。
音轨初始化
在额外的音轨初始化中,我们准备了有关音轨持续时间的信息。此外,我们查询推荐的缓冲区大小。根据流类型 FOURCC,我们初始化输出类型:音频或视频。
if (m_pAviStream == IntPtr.Zero) return E_UNEXPECTED;
AVISTREAMINFO _info = new AVISTREAMINFO();
HRESULT hr = (HRESULT)AVIStreamInfo(m_pAviStream, _info, Marshal.SizeOf(_info));
if (hr.Failed) return hr;
m_iBufferSize = (int)_info.dwSuggestedBufferSize;
m_nCurrentSample = AVIStreamStart(m_pAviStream);
if (_info.dwRate != 0)
{
m_rtOffset = (long)((double)(UNITS * _info.dwStart * _info.dwScale) / _info.dwRate);
m_rtDuration = (long)((double)(UNITS * _info.dwLength * _info.dwScale) / _info.dwRate);
}
if (_info.fccType == streamtypeVIDEO)
{
m_Type = TrackType.Video;
}
if (_info.fccType == streamtypeAUDIO)
{
m_Type = TrackType.Audio;
}
if ((int)(_info.dwFlags & AVIStreamInfoFlags.DISABLED) != 0)
{
m_bEnabled = false;
}
if (_info.wLanguage != 0)
{
m_lcid = MAKELCID(MAKELANGID(_info.wLanguage, SUBLANG_NEUTRAL), SORT_DEFAULT);
}
我决定将输出媒体类型的初始化放在这里,以避免每次都调用 VFW API。
int cbFormat = 0;
hr = (HRESULT)AVIStreamReadFormat(m_pAviStream, _info.dwStart, IntPtr.Zero, ref cbFormat);
if (hr.Failed) return hr;
IntPtr pFormat = Marshal.AllocCoTaskMem(cbFormat);
hr = (HRESULT)AVIStreamReadFormat(m_pAviStream, _info.dwStart, pFormat, ref cbFormat);
if (hr.Succeeded)
{
if (m_Type == TrackType.Audio)
{
m_mt.majorType = MediaType.Audio;
WaveFormatEx _wfx = (WaveFormatEx)Marshal.PtrToStructure(pFormat, typeof(WaveFormatEx));
m_mt.SetFormat(pFormat,cbFormat);
m_mt.formatType = FormatType.WaveEx;
m_mt.sampleSize = _wfx.nBlockAlign;
m_mt.subType = new FOURCC(_wfx.wFormatTag);
if (m_iBufferSize < _wfx.nAvgBytesPerSec)
{
m_iBufferSize = _wfx.nAvgBytesPerSec;
}
if (m_iBufferSize < _wfx.nSamplesPerSec)
{
m_iBufferSize = _wfx.nSamplesPerSec << 3;
}
}
if (m_Type == TrackType.Video)
{
m_mt.majorType = MediaType.Video;
VideoInfoHeader _vih = new VideoInfoHeader();
_vih.BmiHeader = (BitmapInfoHeader)Marshal.PtrToStructure(pFormat, typeof(BitmapInfoHeader));
_vih.SrcRect.bottom = _vih.BmiHeader.Height;
_vih.TargetRect.bottom = _vih.BmiHeader.Height;
_vih.SrcRect.right = _vih.BmiHeader.Width;
_vih.TargetRect.right = _vih.BmiHeader.Width;
if (_info.dwRate != 0)
{
_vih.AvgTimePerFrame = UNITS * _info.dwScale / _info.dwRate;
}
else
{
_vih.AvgTimePerFrame = 1;
}
if (_vih.BmiHeader.ImageSize == 0)
{
_vih.BmiHeader.ImageSize = _vih.BmiHeader.GetBitmapSize();
}
if (_vih.BmiHeader.Compression == 0)
{
switch (_vih.BmiHeader.BitCount)
{
case 1:
m_mt.subType = MediaSubType.RGB1;
break;
case 4:
m_mt.subType = MediaSubType.RGB4;
break;
case 8:
m_mt.subType = MediaSubType.RGB8;
break;
case 16:
m_mt.subType = MediaSubType.RGB565;
break;
case 32:
m_mt.subType = MediaSubType.RGB32;
break;
default:
case 24:
m_mt.subType = MediaSubType.RGB24;
break;
}
}
else
{
m_mt.fixedSizeSamples = false;
m_mt.subType = new FOURCC(_vih.BmiHeader.Compression);
}
m_mt.SetFormat(_vih);
int nExtraSize = cbFormat - Marshal.SizeOf(_vih.BmiHeader);
if (nExtraSize > 0)
{
IntPtr _ptr = new IntPtr(pFormat.ToInt32() + Marshal.SizeOf(_vih.BmiHeader));
m_mt.AddFormatExtraData(_ptr,nExtraSize);
}
m_mt.sampleSize = _vih.BmiHeader.ImageSize;
if (m_iBufferSize < _vih.BmiHeader.ImageSize)
{
m_iBufferSize = _vih.BmiHeader.ImageSize;
}
}
}
Marshal.FreeCoTaskMem(pFormat);
我们通过调用 AVIStreamReadFormat
读取视频的 BitmapInfoHeader
结构或音频的 WaveFormatEx
结构,并根据它准备所有媒体类型信息。音频类型初始化相当简单。但是对于视频,我们应该处理 RGB 颜色空间,因为 AVI 可以包含 RGB 视频,但压缩 FOURCC 与子类型 Guid
不同。最重要的事情是解码器可能需要的额外数据,它位于 BitmapInfoHeader
结构之外,但我们需要将其单独复制到媒体类型中,因为我们将 VideoInfoHeader 结构用作格式。
实现 GetMediaType
这里我们只返回在音轨初始化期间保存的 AMMediaType
结构的副本。
public override HRESULT GetMediaType(int iPosition, ref AMMediaType pmt)
{
if (iPosition < 0) return E_INVALIDARG;
if (iPosition > 0) return VFW_S_NO_MORE_ITEMS;
pmt.Set(m_mt);
return NOERROR;
}
跳转
每个音轨都应该实现搜寻功能,而不是解析器,这也因为我们使用了不同的模型。在基类解析器中,每个音轨都会收到搜寻通知,并且我们知道在此方法调用期间所有线程都已停止。在 VFW 的 AVI 文件中,我们可以通过给定的样本索引读取媒体数据。因此,我们有一个变量来存储当前播放的样本索引。由于搜寻是按时间值执行的,因此我们使用 AVIStreamTimeToSample
将时间转换为样本索引。
public override HRESULT SeekTrack(long _time)
{
if (_time <= 0)
{
m_nCurrentSample = AVIStreamStart(m_pAviStream);
}
else
{
m_nCurrentSample = AVIStreamTimeToSample(m_pAviStream,(int)(_time / 10000));
int _temp = AVIStreamFindSample(m_pAviStream, m_nCurrentSample, AVIStreamFindFlags.FIND_PREV | AVIStreamFindFlags.FIND_KEY);
if (_temp != 0)
{
m_nCurrentSample = _temp;
}
long _current = AVIStreamSampleToTime(m_pAviStream, m_nCurrentSample);
if (_current != -1)
{
_current *= 10000;
_time = _current;
}
}
return base.SeekTrack(_time);
}
搜寻后,我们应该始终从关键帧开始,所以我们应该调整结果的样本索引。为此,我们使用 AVIStreamFindSample
API。
读取数据并准备数据包
准备数据包并输出它们比上一个示例简单得多。因为在这里我们只是读取数据并将其传递出去,而无需将数据包放入队列。因此,我们需要覆盖 GetNextPacket
方法,该方法在基类实现中从队列中提取要传递的数据。
public override PacketData GetNextPacket()
{
PacketData _data = new PacketData();
_data.Buffer = new byte[m_iBufferSize];
int nSamples = 0;
HRESULT hr = (HRESULT)AVIStreamRead(m_pAviStream, m_nCurrentSample, -1, _data.Buffer, m_iBufferSize, out _data.Size, out nSamples);
if (hr.Failed) return null;
int _temp = AVIStreamFindSample(m_pAviStream, m_nCurrentSample, AVIStreamFindFlags.FIND_NEXT | AVIStreamFindFlags.FIND_KEY);
if (_temp != -1 && _temp < m_nCurrentSample + nSamples)
{
_data.SyncPoint = true;
}
_data.Start = AVIStreamSampleToTime(m_pAviStream, m_nCurrentSample);
if (_data.Start != -1) _data.Start *= 10000;
m_nCurrentSample += nSamples;
_data.Stop = AVIStreamSampleToTime(m_pAviStream, m_nCurrentSample);
if (_data.Stop != -1) _data.Stop *= 10000;
return _data;
}
我们使用 AVIStreamRead
API 来读取数据。我们传入当前样本索引和准备好的缓冲区。然后,我们将当前样本变量增加已读取的样本数。最主要的是正确设置关键帧标志,因为这是传递压缩数据的关键。为此,我们使用与搜寻相同的 API:AVIStreamFindSample
。
过滤器说明
结果过滤器执行 AVI 文件读取,并通过其各自的线程在每个输出引脚上传递数据。它展示了如何轻松创建此类过滤器以及如何在没有通用解复用线程的情况下使用该模型。注意:这样的实现并不意味着所有文件源过滤器都以相同的方式工作,大多数使用的模型在上一篇文章中已描述,但这里只是另一种方式的用例。您可以尝试修改过滤器以传递未压缩的样本。