貌似最近都是在写业余研究的东西,今天也偶尔写写工作上的东西:-D
最近两周因为项目需要都在研究 server 和 Windows Media Format SDK,今天就记录一下研究 wmvcopy 这个示例程序时如何把 wmvcopy 改写成让 reader 读 WMV 文件,decode 后拿到 raw data,然后再让 writer encode 这些数据得到一个新的 WMV 的方法。
原始的 wmvcopy 为了仅仅实现原样复制 WMV,所以让 reader 从 WMV 中读出 compressed data,然后交给 writer 写入另外一个 WMV 文件中,它旁路了 reader 的 decode 和 writer 的 encode 过程,这是通过下面步骤实现的:
1) 在对 reader 进行设置时,调 IWMReaderAdvanced::SetReceiveStreamSample() 的时候第二个参数给的是 TRUE,指示 reader 应用程序想要接收 stream sample/compressed sample,这就旁路了 reader 的 decoder
2) 在 WMF SDK 中,compressed sample 是通过 IWMReaderCallbackAdvanced::OnStreamSample() 回调得到的,uncompressed sample 是通过 IWMReaderCallback::OnSample() 回调得到的。由于 wmvcopy 只是原样复制,所以它只把核心处理逻辑放到了 OnStreamSample() 里
3) 在对 writer 进行设置时,调 IWMWriter::SetInputProps() 的时候第二个参数是 NULL,指示 writer 不用为输入数据配置编码器,这就旁路了 writer 的 encoder
4) 在收到 sample 后,wmvcopy 通过调用 IWMWriterAdvanced::WriteStreamSample() 把 compressed sample 写至最终去向,而查阅 WMF SDK 文档可知,向 writer 写入 uncopressed sample 需要调用 IWMWriter::WriteSample()
5) 由于是原样复制 WMV audio/video 内容,所以 wmvcopy 需要尽可能快的数据处理速度,为了做到这一点,在配置 reader 的时候,它启用了 user provided clock。如果想让 reader 以播放速度来取数据,只把 IWMReaderAdvanced::SetUserProvidedClock() 这个调用传的值由 TRUE 改成 FALSE 是不够的,IWMReaderAdvanced::SetUserProvidedClock() 的文档里有这样的话:
User-provided clocks are only supported when the source file is a local file.
This method can fail if the current source does not support user-provided clocks.
To drive a clock, an application must call DeliverTime, and then wait for IWMReaderCallbackAdvanced::OnTime to reach the time specified.
果然在 CWMVCopy::OnStatus() 的 case WMT_STARTED 里发现确实这里调用了 IWMReaderAdvanced::DeliverTime(),把这里相关的代码注释掉,不用管 OnTime(),然后就可以让 reader 按播放速度去 decode sample 了。
6) 由于只是想要原样复制 WMV 内容,所以 wmvcopy 给 writer 设置了跟从原始文件中读出后取得的 peofile 一模一样的 peofile。由于使用的 profile 一致,所以 reader 的 output 的 audio stream index 可以对应 writer input 的 audio stream index,reader output 的 video stream index 也可以对应 writer input 的 video stream index。这从 wmvcopy 对 OnStreamSample() 的处理上也能看出来:
- HRESULT
- CWMVCopy::OnStreamSample(
- WORD wStreamNum,
- QWORD cnsSampleTime,
- QWORD cnsSampleDuration,
- DWORD dwFlags,
- INSSBuffer __RPC_FAR *pSample,
- void __RPC_FAR *pvContext)
- {
- ... ...
- hr = m_pWriterAdvanced->WriteStreamSample(
- wStreamNum,
- cnsSampleTime,
- 0,
- cnsSampleDuration,
- dwFlags,
- pSample );
- ... ...
- }
OnStreamSample() 的 wStreamNum 参数被直接用于对 WriteStreamSample() 的调用。这就表示——比如 reader output 的第 0 个 stream 是 audio stream,那么 writer input 的第 0 个输入也是用来接收 audio stream 的。
但是对于 writer 使用了跟 reader 不一样的 profile 这种情况,writer 的第 0 个 input 不一定是用来接收 audio stream 的,所以需要应用程序自己去检查。
下面是检查 reader output stream index 和 audio/video 对应情况的示例代码:
- IWMProfile *pReaderProfile = NULL;
- pReader->QueryInterface( IID_IWMProfile, (void **)&pReaderProfile );
-
- DWORD dwStreamCount = 0;
- pReaderProfile->GetStreamCount( &dwStreamCount );
-
- IWMStreamConfig *pStreamConfig = NULL;
- for( DWORD i = 0; i < dwStreamCount; i++ )
- {
- m_pReaderProfile->GetStream( i, &pStreamConfig );
-
- WORD wStreamNumber = 0;
- pStreamConfig->GetStreamNumber( &wStreamNumber );
- pReaderAdvanced->SetReceiveStreamSamples( wStreamNumber, FALSE ); // we want to receive uncompressed sample
-
- IWMMediaProps *pMediaProperty = NULL;
- pStreamConfig->QueryInterface( IID_IWMMediaProps, (void **)&pMediaProperty );
-
- GUID type;
- pMediaProperty->GetType( &type );
- if( WMMEDIATYPE_Audio == type )
- {
- dwReaderOutputStreamIndex_Audio = i;
- }
- else if( WMMEDIATYPE_Video == type )
- {
- dwReaderOutputStreamIndex_Video = i;
- }
- }
下面是检查 writer input stream index 和 audio/video 对应情况的示例代码:
- DWORD dwInputCount = 0;
- pWriter->GetInputCount( &dwInputCount );
-
- IWMInputMediaProps *pInputMediaProps = NULL;
- for( DWORD i = 0; i < dwInputNum; ++i )
- {
- hr = pWriter->GetInputProps( i, &pInputMediaProps );
-
- GUID type;
- pInputMediaProps->GetType( &type );
-
- if( WMMEDIATYPE_Audio == type )
- {
- dwWriterInputIndex_Audio = i;
- }
- else if( WMMEDIATYPE_Video == type )
- {
- dwWriterInputIndex_Video = i;
- }
- }
拿到了对应关系之后,在处理 OnSample() 时就可以以这样:
- HRESULT
- CWMVCopy::OnSample(
- DWORD dwOutputNum,
- QWORD qwSampleTime,
- QWORD qwSampleDuration,
- DWORD dwFlags,
- INSSBuffer __RPC_FAR *pSample,
- void __RPC_FAR *pvContext )
- {
- DWORD dwInputIndex = 0;
- if( dwReaderOutputStreamIndex_Audio == dwOutputNum )
- {
- dwInputInput = dwWriterInputIndex_Audio;
- }
- else if( dwReaderOutputStreamIndex_Video == dwOutputNum )
- {
- dwInputIndex = dwWriterInputIndex_Video;
- }
- else
- {
- return( S_OK );
- }
-
- m_pWriter->WriteSample( dwInputIndex, qwSampleTime, dwFlags, pSample );
- return( S_OK );
- }
《Windows Media 编程导向》上讲到对接 reader output 和 writer input 时用的方法是比较 input 和 output 的 connection name,但是我在调试中发现 writer input 的 connection name 始终拿不到,只能拿到 reader output 的(类似“0”,“1”这类字串值),所以我就用了上面的方法。
7) 如果想以不同的参数对输入 WMV 重新进行编码,那么就需要给 writer 设置一个跟 reader 不一样的 profile。这个目标的正规做法是创建一个空 profile,然后给这个 profile 添加几个 stream,然后设置 stream 的格式,但是过程非常复杂。其实还有近路,那就是直接使用 system profile 或者其它现成的 profile。
使用 system profile 的方法是调用 IWMWriter::SetProfileByID(),参数是 system profile 的 GUID,比如 WMProfile_V80_56Video。
profile 也可以保存成 .prx 文件,其实就是一个 profile 的 XML 描述,这就方便了我们直接使用现成的 profile 来设置 writer,下面是把一个存在于磁盘上的 .prx 文件加载,获得一个 profile 实例的代码:
- HRESULT
- LoadProfileFromFile(
- IWMProfileManager *pManager,
- IWMProfile **ppProfile,
- const char *pFilename )
- {
- HRESULT hr = S_OK;
- HANDLE hFile = INVALID_HANDLE_VALUE;
- LPWSTR pProfileData = NULL;
-
- do
- {
- hFile = CreateFile(
- pFilename,
- GENERIC_READ,
- FILE_SHARE_READ,
- NULL,
- OPEN_EXISTING,
- FILE_ATTRIBUTE_NORMAL,
- NULL );
- if( INVALID_HANDLE_VALUE == hFile )
- {
- hr = HRESULT_FROM_WIN32( GetLastError() );
- break;
- }
- if( FILE_TYPE_DISK != GetFileType( hFile ) )
- {
- hr = NS_E_INVALID_NAME;
- break;
- }
- DWORD dwLength = GetFileSize( hFile, NULL );
- if( -1 == dwLength )
- {
- hr = HRESULT_FROM_WIN32( GetLastError() );
- break;
- }
- pProfileData = (WCHAR *)new BYTE[dwLength + sizeof( WCHAR )];
- if( NULL == pProfileData )
- {
- hr = E_OUTOFMEMORY;
- break;
- }
- memset( pProfileData, 0, dwLength + sizeof( WCHAR ) );
- DWORD dwBytesRead = 0;
- if( !ReadFile( hFile, pProfileData, dwLength, &dwBytesRead, NULL ) )
- {
- hr = HRESULT_FROM_WIN32( GetLastError() );
- break;
- }
- hr = pManager->LoadProfileByData( pProfileData, ppProfile );
- if( FAILED( hr ) )
- {
- break;
- }
- }
- while( false );
-
- if( pProfileData )
- delete []pProfileData;
- if( hFile )
- CloseHandle( hFile );
- return( hr );
- }
至于现成的 .prx,可以使用 WMF SDK 的 wmgenprofile,也可以用《Windows Media 编程导向》随书光盘里的一个小工具 ShowProfile。