2025-11-28 00:35:46 +09:00

822 lines
21 KiB
C++

// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved
#include "stdafx.h"
#include "tedplayer.h"
#include <mferror.h>
#include "assert.h"
#include "intsafe.h"
#include <initguid.h>
EXTERN_GUID(CLSID_CResamplerMediaObject, 0xf447b69e, 0x1884, 0x4a7e, 0x80, 0x55, 0x34, 0x6f, 0x74, 0xd6, 0xed, 0xb3);
////////////////////////////////////////////////////////////////////////////////
//
CTedMediaFileRenderer::CTedMediaFileRenderer(LPCWSTR szFileName, ITedVideoWindowHandler* pVideoWindowHandler, HRESULT& hr)
: m_szFileName(szFileName)
, m_spVideoWindowHandler(pVideoWindowHandler)
{
hr = MFCreateTopoLoader(&m_spTopoLoader);
}
HRESULT CTedMediaFileRenderer::Load(IMFTopology** ppOutputTopology)
{
HRESULT hr;
CComPtr<IMFTopology> spPartialTopology;
IFC( CreatePartialTopology(&spPartialTopology) );
IFC( m_spTopoLoader->Load(spPartialTopology, ppOutputTopology, NULL) );
Cleanup:
return hr;
}
HRESULT CTedMediaFileRenderer::CreatePartialTopology(IMFTopology** ppPartialTopology)
{
HRESULT hr;
IMFTopology* pPartialTopology;
CComPtr<IMFMediaSource> spSource;
IFC( MFCreateTopology(&pPartialTopology) );
IFC( CreateSource(&spSource) );
IFC( BuildTopologyFromSource(pPartialTopology, spSource) );
*ppPartialTopology = pPartialTopology;
Cleanup:
return hr;
}
HRESULT CTedMediaFileRenderer::CreateSource(IMFMediaSource** ppSource)
{
HRESULT hr;
CComPtr<IMFSourceResolver> spSourceResolver;
CComPtr<IUnknown> spSourceUnk;
IMFMediaSource* pSource;
IFC( MFCreateSourceResolver(&spSourceResolver) );
MF_OBJECT_TYPE ObjectType;
IFC( spSourceResolver->CreateObjectFromURL(m_szFileName, MF_RESOLUTION_MEDIASOURCE,
NULL, &ObjectType, &spSourceUnk) );
hr = spSourceUnk->QueryInterface(IID_IMFMediaSource, (void**) &pSource);
if(E_NOINTERFACE == hr)
{
hr = MF_E_UNSUPPORTED_BYTESTREAM_TYPE;
}
IFC( hr );
*ppSource = pSource;
Cleanup:
return hr;
}
// Given a source, connect each stream to a renderer for its media type
HRESULT CTedMediaFileRenderer::BuildTopologyFromSource(IMFTopology* pTopology, IMFMediaSource* pSource)
{
HRESULT hr;
CComPtr<IMFPresentationDescriptor> spPD;
IFC( pSource->CreatePresentationDescriptor(&spPD) );
DWORD cSourceStreams = 0;
IFC( spPD->GetStreamDescriptorCount(&cSourceStreams) );
for(DWORD i = 0; i < cSourceStreams; i++)
{
CComPtr<IMFStreamDescriptor> spSD;
CComPtr<IMFTopologyNode> spNode;
CComPtr<IMFTopologyNode> spRendererNode;
BOOL fSelected = FALSE;
IFC( spPD->GetStreamDescriptorByIndex(i, &fSelected, &spSD) );
IFC( MFCreateTopologyNode(MF_TOPOLOGY_SOURCESTREAM_NODE, &spNode) );
IFC( spNode->SetUnknown(MF_TOPONODE_SOURCE, pSource) );
IFC( spNode->SetUnknown(MF_TOPONODE_PRESENTATION_DESCRIPTOR, spPD) );
IFC( spNode->SetUnknown(MF_TOPONODE_STREAM_DESCRIPTOR, spSD) );
IFC( pTopology->AddNode(spNode) );
IFC( CreateRendererForStream(spSD, &spRendererNode) );
IFC( spNode->ConnectOutput(0, spRendererNode, 0) );
IFC( pTopology->AddNode(spRendererNode) );
}
Cleanup:
return hr;
}
// Create a renderer for the media type on the given stream descriptor
HRESULT CTedMediaFileRenderer::CreateRendererForStream(IMFStreamDescriptor* pSD, IMFTopologyNode** ppRendererNode)
{
HRESULT hr;
CComPtr<IMFMediaTypeHandler> spMediaTypeHandler;
CComPtr<IMFActivate> spRendererActivate;
CComPtr<IMFMediaSink> spRendererSink;
CComPtr<IMFStreamSink> spRendererStreamSink;
IMFTopologyNode* pRendererNode;
GUID gidMajorType;
IFC( MFCreateTopologyNode(MF_TOPOLOGY_OUTPUT_NODE, &pRendererNode) );
IFC( pSD->GetMediaTypeHandler( &spMediaTypeHandler ) );
IFC( spMediaTypeHandler->GetMajorType( &gidMajorType ) );
if(MFMediaType_Audio == gidMajorType)
{
IFC( MFCreateAudioRendererActivate(&spRendererActivate) );
IFC( spRendererActivate->ActivateObject(IID_IMFMediaSink, (void**) &spRendererSink) );
IFC( spRendererSink->GetStreamSinkById(0, &spRendererStreamSink) );
IFC( pRendererNode->SetObject(spRendererStreamSink) );
}
else if(MFMediaType_Video == gidMajorType)
{
HWND hVideoWindow;
IFC( m_spVideoWindowHandler->GetVideoWindow((LONG_PTR*) &hVideoWindow) );
IFC( MFCreateVideoRendererActivate(hVideoWindow, &spRendererActivate) );
IFC( spRendererActivate->ActivateObject(IID_IMFMediaSink, (void**) &spRendererSink) );
IFC( spRendererSink->GetStreamSinkById(0, &spRendererStreamSink) );
IFC( pRendererNode->SetObject(spRendererStreamSink) );
}
else
{
// Do not have renderers for any other major types
}
*ppRendererNode = pRendererNode;
Cleanup:
return hr;
}
///////////////////////////////////////////////////////////////////////////////
//
CTedPlayer::CTedPlayer(CTedMediaEventHandler* pMediaEventHandler, IMFContentProtectionManager* pCPM)
: m_cRef(1)
, m_pMediaEventHandler(pMediaEventHandler)
, m_pCPM(pCPM)
, m_bIsPlaying(false)
, m_bIsPaused(false)
, m_fTopologySet(false)
, m_LastSeqID(0)
, m_hnsMediastartOffset(0)
, m_fPendingClearCustomTopoloader(false)
, m_fPendingProtectedCustomTopoloader(false)
{
m_pCPM->AddRef();
}
CTedPlayer::~CTedPlayer()
{
HRESULT hr;
if(m_pCPM)
{
m_pCPM->Release();
}
for(size_t i = 0; i < m_aTopologies.GetCount(); i++)
{
CComPtr<IMFCollection> spSourceNodeCollection;
IMFTopology* pTopology = m_aTopologies.GetAt(i);
hr = pTopology->GetSourceNodeCollection(&spSourceNodeCollection);
if(FAILED(hr))
{
pTopology->Release();
continue;
}
DWORD cElements = 0;
spSourceNodeCollection->GetElementCount(&cElements);
for(DWORD j = 0; j < cElements; j++)
{
CComPtr<IUnknown> spSourceNodeUnk;
CComPtr<IMFTopologyNode> spSourceNode;
CComPtr<IMFMediaSource> spSource;
hr = spSourceNodeCollection->GetElement(j, &spSourceNodeUnk);
if(FAILED(hr)) continue;
hr = spSourceNodeUnk->QueryInterface(IID_IMFTopologyNode, (void**) &spSourceNode);
if(FAILED(hr)) continue;
hr = spSourceNode->GetUnknown(MF_TOPONODE_SOURCE, IID_IMFMediaSource, (void**) &spSource);
if(FAILED(hr)) continue;
spSource->Shutdown();
}
CComPtr<IMFCollection> spOutputNodeCollection;
hr = pTopology->GetOutputNodeCollection(&spOutputNodeCollection);
if(FAILED(hr))
{
pTopology->Release();
continue;
}
cElements = 0;
spOutputNodeCollection->GetElementCount(&cElements);
for(DWORD j = 0; j < cElements; j++)
{
CComPtr<IUnknown> spSinkNodeUnk;
CComPtr<IMFTopologyNode> spSinkNode;
CComPtr<IUnknown> spStreamSinkUnk;
CComPtr<IMFStreamSink> spStreamSink;
CComPtr<IMFMediaSink> spSink;
hr = spOutputNodeCollection->GetElement(j, &spSinkNodeUnk);
if(FAILED(hr)) continue;
hr = spSinkNodeUnk->QueryInterface(IID_IMFTopologyNode, (void**) &spSinkNode);
if(FAILED(hr)) continue;
hr = spSinkNode->GetObject(&spStreamSinkUnk);
if(FAILED(hr)) continue;
hr = spStreamSinkUnk->QueryInterface(IID_IMFStreamSink, (void**) &spStreamSink);
if(FAILED(hr)) continue;
hr = spStreamSink->GetMediaSink(&spSink);
if(FAILED(hr)) continue;
spSink->Shutdown();
}
pTopology->Release();
}
if(m_spClearSession)
{
m_spClearSession->Shutdown();
}
if(m_spProtectedSession)
{
m_spProtectedSession->Shutdown();
}
}
HRESULT CTedPlayer::InitClear()
{
HRESULT hr;
if(m_spSession)
{
if(m_bIsPlaying)
{
m_bIsPlaying = false;
m_spSession->Stop();
}
m_spSession.Release();
}
if(m_fPendingClearCustomTopoloader && m_spClearSession.p)
{
m_spClearSession->Shutdown();
m_spClearSession.Release();
}
if(m_spClearSession.p == NULL)
{
CComPtr<IMFAttributes> spConfiguration = NULL;
if(m_fPendingClearCustomTopoloader && GUID_NULL != m_gidCustomTopoloader)
{
IFC( MFCreateAttributes(&spConfiguration, 1) );
IFC( spConfiguration->SetGUID(MF_SESSION_TOPOLOADER, m_gidCustomTopoloader) );
}
IFC( MFCreateMediaSession(spConfiguration, &m_spClearSession) );
IFC( m_spClearSession->BeginGetEvent(&m_xOnClearSessionEvent, NULL) );
m_fPendingClearCustomTopoloader = false;
}
m_spSession = m_spClearSession;
IFC( InitFromSession() );
Cleanup:
return hr;
}
HRESULT CTedPlayer::InitProtected()
{
HRESULT hr;
if(m_spSession)
{
if(m_bIsPlaying)
{
m_bIsPlaying = false;
m_spSession->Stop();
}
m_spSession.Release();
}
if(m_fPendingProtectedCustomTopoloader && m_spProtectedSession.p)
{
m_spProtectedSession->Shutdown();
m_spProtectedSession.Release();
}
if(m_spProtectedSession.p == NULL)
{
CComPtr<IMFAttributes> spAttr;
IFC( MFCreateAttributes(&spAttr, 1) );
IFC( spAttr->SetUnknown(MF_SESSION_CONTENT_PROTECTION_MANAGER, m_pCPM) );
if(m_fPendingProtectedCustomTopoloader && GUID_NULL != m_gidCustomTopoloader)
{
IFC( spAttr->SetGUID(MF_SESSION_TOPOLOADER, m_gidCustomTopoloader) );
}
IFC( MFCreatePMPMediaSession( 0, spAttr, &m_spProtectedSession, NULL ) );
IFC( m_spProtectedSession->BeginGetEvent(&m_xOnProtectedSessionEvent, NULL) );
m_fPendingProtectedCustomTopoloader = false;
}
m_spSession = m_spProtectedSession;
IFC( InitFromSession() );
Cleanup:
return hr;
}
// AddRef and Release for callbacks only; not functional
LONG CTedPlayer::AddRef()
{
return m_cRef;
}
LONG CTedPlayer::Release()
{
return m_cRef;
}
HRESULT CTedPlayer::Reset()
{
HRESULT hr;
IFC( m_spSession->ClearTopologies() );
Cleanup:
return hr;
}
HRESULT CTedPlayer::SetTopology(CComPtr<IMFTopology> pPartialTopo, BOOL fTranscode)
{
HRESULT hr;
assert(pPartialTopo != NULL);
assert(m_spSession != NULL);
m_aTopologies.Add(pPartialTopo);
m_aTopologies.GetAt(m_aTopologies.GetCount() - 1)->AddRef();
// Due to a bug, the topoloader cannot correctly resolve a resampler already existing
// in the topology, and will just insert a new one. Work around this by removing
// the resampler
IFC( RemoveResamplerNode(pPartialTopo) );
if(NULL != m_spSource.p)
{
m_spSource.Release();
}
m_hnsMediastartOffset = INT64_MAX;
WORD cNodes;
IFC( pPartialTopo->GetNodeCount(&cNodes) );
for(WORD i = 0; i < cNodes; i++)
{
CComPtr<IMFTopologyNode> spNode;
MF_TOPOLOGY_TYPE tidType;
IFC( pPartialTopo->GetNode(i, &spNode) );
IFC( spNode->GetNodeType(&tidType) );
// We need to find the source node so we can get the IMFMediaSource for this playback
if(MF_TOPOLOGY_SOURCESTREAM_NODE == tidType)
{
if(!m_spSource.p)
{
IFC( spNode->GetUnknown(MF_TOPONODE_SOURCE, IID_IMFMediaSource, (void**) &m_spSource) );
}
MFTIME hnsMediastart = 0;
(void)spNode->GetUINT64(MF_TOPONODE_MEDIASTART, (UINT64*) &hnsMediastart);
if(hnsMediastart < m_hnsMediastartOffset)
{
m_hnsMediastartOffset = hnsMediastart;
}
}
}
// Set topology attributes to enable new MF features. HWMODE_USE_HARDWARE allows topoedit
// to pick up hardware MFTs for decoding. DXVA_FULL allows topoedit to enable full
// DXVA resolution for the topology -- in MFv1, only decoders automatically inserted
// by the topoloader and directly connected to the EVR received a D3D manager message.
IFC( pPartialTopo->SetUINT32(MF_TOPOLOGY_HARDWARE_MODE, MFTOPOLOGY_HWMODE_USE_HARDWARE) );
IFC( pPartialTopo->SetUINT32(MF_TOPOLOGY_DXVA_MODE, MFTOPOLOGY_DXVA_FULL) );
if(NULL == m_spSource.p)
{
IFC( MF_E_NOT_FOUND );
}
else
{
m_fIsTranscoding = fTranscode;
IFC( m_spSession->SetTopology(MFSESSION_SETTOPOLOGY_IMMEDIATE, pPartialTopo) );
}
m_fReceivedTime = false;
Cleanup:
return hr;
}
HRESULT CTedPlayer::Start()
{
HRESULT hr;
PROPVARIANT var;
PropVariantInit( &var );
LONGLONG hnsSeek = (m_bIsPaused) ? PRESENTATION_CURRENT_POSITION : 0;
if ( hnsSeek == PRESENTATION_CURRENT_POSITION )
{
var.vt = VT_EMPTY;
}
else
{
var.vt = VT_I8;
var.hVal.QuadPart = hnsSeek;
}
hr = m_spSession->Start( NULL, &var );
m_bIsPaused = false;
PropVariantClear( &var );
return hr;
}
HRESULT CTedPlayer::Stop()
{
HRESULT hr;
IFC( m_spSession->Stop() );
m_bIsPlaying = false;
Cleanup:
return hr;
}
HRESULT CTedPlayer::Pause()
{
HRESULT hr;
m_bIsPaused = true;
IFC( m_spSession->Pause() );
Cleanup:
return hr;
}
HRESULT CTedPlayer::PlayFrom(MFTIME time)
{
HRESULT hr = S_OK;
PROPVARIANT var;
PropVariantInit( &var );
if ( PRESENTATION_CURRENT_POSITION == time )
{
var.vt = VT_EMPTY;
}
else
{
var.vt = VT_I8;
var.hVal.QuadPart = time;
}
hr = m_spSession->Start( NULL, &var );
PropVariantClear( &var );
return( hr );
}
HRESULT CTedPlayer::GetDuration(MFTIME& hnsTime)
{
HRESULT hr = S_OK;
IMFPresentationDescriptor *pPD = NULL;
if(NULL != m_spSource.p)
{
IFC( m_spSource->CreatePresentationDescriptor( &pPD ) );
IFC( pPD->GetUINT64( MF_PD_DURATION, (UINT64*) &hnsTime ) );
}
else
{
hr = E_POINTER;
}
Cleanup:
if(pPD) pPD->Release();
return( hr );
}
HRESULT CTedPlayer::GetFullTopology(IMFTopology** ppFullTopo)
{
HRESULT hr = S_OK;
CComPtr<IMFGetService> spGetService;
if(m_spFullTopology)
{
*ppFullTopo = m_spFullTopology;
(*ppFullTopo)->AddRef();
}
else
{
hr = m_spSession->GetFullTopology(MFSESSION_GETFULLTOPOLOGY_CURRENT, 0, ppFullTopo);
}
return hr;
}
void CTedPlayer::OnClearSessionEvent(IMFAsyncResult* pResult)
{
HRESULT hr;
CComPtr<IMFMediaEvent> spEvent;
IFC( m_spClearSession->EndGetEvent(pResult, &spEvent) );
if(m_spSession.p == m_spClearSession.p)
{
IFC( HandleEvent(spEvent) );
}
IFC( m_spClearSession->BeginGetEvent(&m_xOnClearSessionEvent, NULL) );
Cleanup:
if(FAILED(hr))
{
m_pMediaEventHandler->NotifyEventError(hr);
}
}
void CTedPlayer::OnProtectedSessionEvent(IMFAsyncResult* pResult)
{
HRESULT hr;
CComPtr<IMFMediaEvent> spEvent;
IFC( m_spProtectedSession->EndGetEvent(pResult, &spEvent) );
if(m_spSession.p == m_spProtectedSession.p)
{
IFC( HandleEvent(spEvent) );
}
IFC( m_spProtectedSession->BeginGetEvent(&m_xOnProtectedSessionEvent, NULL) );
Cleanup:
if(FAILED(hr))
{
m_pMediaEventHandler->NotifyEventError(hr);
}
}
HRESULT CTedPlayer::HandleEvent(IMFMediaEvent * pEvent)
{
HRESULT hr = S_OK;
HRESULT hrEvent = S_OK;
MediaEventType met;
PROPVARIANT var;
PropVariantInit(&var);
IFC(pEvent->GetType(&met));
IFC(pEvent->GetStatus(&hrEvent));
IFC(pEvent->GetValue(&var));
switch(met)
{
case MESessionStarted:
IFC( HandleSessionStarted( pEvent ) );
m_bIsPlaying = true;
break;
case MESessionEnded:
m_bIsPlaying = false;
if(m_fIsTranscoding)
{
m_spSession->Close();
}
break;
case MESessionTopologySet:
if(SUCCEEDED(hrEvent))
{
m_fTopologySet = true;
m_bIsPaused = false;
}
m_spFullTopology.Release();
var.punkVal->QueryInterface(IID_IMFTopology, (void**) &m_spFullTopology);
break;
case MESessionNotifyPresentationTime:
IFC( HandleNotifyPresentationTime( pEvent ) );
break;
}
m_pMediaEventHandler->HandleMediaEvent(pEvent);
Cleanup:
PropVariantClear(&var);
return hr;
}
HRESULT CTedPlayer::HandleSessionStarted(IMFMediaEvent* pEvent)
{
MFTIME hnsTopologyPresentationOffset;
if(SUCCEEDED( pEvent->GetUINT64(MF_EVENT_PRESENTATION_TIME_OFFSET,
(UINT64*)&hnsTopologyPresentationOffset) ))
{
m_hnsOffsetTime = hnsTopologyPresentationOffset;
}
return S_OK;
}
HRESULT CTedPlayer::HandleNotifyPresentationTime(IMFMediaEvent* pEvent)
{
HRESULT hr = S_OK;
IFC( pEvent->GetUINT64(MF_EVENT_START_PRESENTATION_TIME, (UINT64*) &m_hnsStartTime) );
IFC( pEvent->GetUINT64(MF_EVENT_PRESENTATION_TIME_OFFSET, (UINT64*) &m_hnsOffsetTime) );
IFC( pEvent->GetUINT64(MF_EVENT_START_PRESENTATION_TIME_AT_OUTPUT, (UINT64*) &m_hnsStartTimeAtOutput) );
m_fReceivedTime = true;
Cleanup:
return( hr );
}
bool CTedPlayer::IsPlaying() const
{
return m_bIsPlaying;
}
bool CTedPlayer::IsPaused() const
{
return m_bIsPaused;
}
bool CTedPlayer::IsTopologySet() const
{
return m_fTopologySet;
}
void CTedPlayer::SetCustomTopoloader(GUID gidTopoloader)
{
m_gidCustomTopoloader = gidTopoloader;
m_fPendingClearCustomTopoloader = true;
m_fPendingProtectedCustomTopoloader = true;
}
HRESULT CTedPlayer::GetTime( MFTIME *phnsTime )
{
HRESULT hr = S_OK;
IFC( m_spSessionClock->GetTime( phnsTime ) );
//
// To get UI time, we need to apply the appropriate adjustment
// to Presentation Clock time.
//
if (m_fReceivedTime)
{
*phnsTime -= m_hnsOffsetTime;
}
*phnsTime += m_hnsMediastartOffset;
Cleanup:
return( hr );
}
HRESULT CTedPlayer::GetRateBounds(MFRATE_DIRECTION eDirection, float* pflSlowest, float* pflFastest)
{
HRESULT hr = S_OK;
CComPtr<IMFRateSupport> spRateSupport;
assert(pflSlowest != NULL);
assert(pflFastest != NULL);
IFC( MFGetService( m_spSession, MF_RATE_CONTROL_SERVICE, IID_IMFRateSupport, (void**) &spRateSupport ) );
IFC( spRateSupport->GetSlowestRate( eDirection, FALSE, pflSlowest ) );
IFC( spRateSupport->GetFastestRate( eDirection, FALSE, pflFastest ) );
Cleanup:
return( hr );
}
HRESULT CTedPlayer::SetRate(float flRate)
{
HRESULT hr = S_OK;
CComPtr<IMFRateControl> spRateControl = NULL;
IFC( MFGetService( m_spSession,
MF_RATE_CONTROL_SERVICE,
IID_IMFRateControl,
(void**) &spRateControl ));
IFC( spRateControl->SetRate( FALSE, flRate ) );
Cleanup:
return( hr );
}
HRESULT CTedPlayer::GetCapabilities(DWORD* pdwCaps)
{
return m_spSession->GetSessionCapabilities(pdwCaps);
}
HRESULT CTedPlayer::InitFromSession()
{
HRESULT hr = S_OK;
CComPtr<IMFClock> spClock;
if(m_spSessionClock.p)
{
m_spSessionClock.Release();
}
IFC( Reset() );
IFC( m_spSession->GetClock(&spClock) );
IFC( spClock->QueryInterface(IID_IMFPresentationClock, (void**) &m_spSessionClock) );
m_bIsPaused = false;
Cleanup:
return hr;
}
HRESULT CTedPlayer::RemoveResamplerNode(IMFTopology* pTopology)
{
HRESULT hr = S_OK;
WORD cNodes;
IFC( pTopology->GetNodeCount(&cNodes) );
for(WORD i = 0; i < cNodes; i++)
{
CComPtr<IMFTopologyNode> spNode;
IFC( pTopology->GetNode(i, &spNode) );
GUID gidTransformID;
hr = spNode->GetGUID(MF_TOPONODE_TRANSFORM_OBJECTID, &gidTransformID);
if(SUCCEEDED(hr) && CLSID_CResamplerMediaObject == gidTransformID)
{
CComPtr<IMFTopologyNode> spUpstreamNode;
DWORD dwUpstreamIndex;
CComPtr<IMFTopologyNode> spDownstreamNode;
DWORD dwDownstreamIndex;
IFC( spNode->GetInput(0, &spUpstreamNode, &dwUpstreamIndex) );
IFC( spNode->GetOutput(0, &spDownstreamNode, &dwDownstreamIndex) );
IFC( spUpstreamNode->ConnectOutput(dwUpstreamIndex, spDownstreamNode, dwDownstreamIndex) );
IFC( pTopology->RemoveNode(spNode) );
IFC( pTopology->GetNodeCount(&cNodes) );
i--;
}
hr = S_OK;
}
Cleanup:
return hr;
}