¡Esta es una revisión vieja del documento!
DirectShow es la tecnología de Microsoft para el desarrollo de aplicaciones que reproducen audio y video.
Una aplicación DirectShow se programa a partir de bloques denominados filters. Un filter tiene una función específica en la ejecución de la tarea (existen filters source, parsers…), reciben una entrada y producen una salida. Los filters se unen a partir de pins. El conjunto de filters unidos se denomina graph.
Tenemos, por ejemplo, el siguiente graph:
Este graph contiene una estructura básica:
Para que una aplicación utilice graphs únicamente deberá instanciar un Filter Graph Manager. Tanto este como los demás componentes de una aplicación DirectShow son objetos COM, es decir, pueden añadirse a cualquier aplicación en plataforma Windows.
Para registrar un filtro en una .dll se ha de hacer utilizando el comando regsvr32 (pasándole el parámetro /u para desinstalar):
\> regsvr32 myfilter.dll \> regsvr32 /u myfilter.dll
Este comando se ha de hacer desde el modo administrador. Para lanzarlo desde el menú de inicio haremos ejecutar cmd pero, en vez de dar a enter haremos ctrl+shift+enter (o botón derecho sobre el icono y “ejecutar como administrador”).
GraphEdit es una herramienta con interface de usuario para crear graphs y ver su funcionalidad. Un ejemplo podría ser, teniendo un filtro DirectShow, unir los pins de salida de un filtro con los de entrada de otro. El último, haciendo click derecho y “Render” sobre el pin de salida permitiría ver el pipeline.
Para programar un filtro tendrá que ser implementado como un objeto COM. Es decir, deberá ser implementada la interface IUnknown.
IUnknown consta de tres métodos:
QueryInterface: Para obtener una referencia al objeto que implementa dicha interface (como un cast de la interface).AddRef: Se utiliza para incrementar un contador de referencias de este objeto (devuelve el nuevo número).Release: Se utiliza para decrementar el número de referencias de este objeto (devuelve el nuevo número).interface IUnknown { virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0; virtual ULONG AddRef () = 0; virtual ULONG Release () = 0; };
Para realizar una QueryInterface el objeto que la llama ha de enviar dos parámetros: un indentificador de interface (IID) y la dirección de un puntero. Si el componente desarrollado identifica la IID asignará el puntero y devolverá S_OK. Si no, asignará el puntero a NULL y retornará E_NOINTERFACE.
AddRef y Release son otras formas de crear y liberar instancias de la interface entre threads.
Para iniciar a desarrollar necesitarás la SDK de Microsoft (para este artículo se utilizó la versión 7.1) ya que los ejemplos y documentación de la plataforma se encuentran ahí.
Deberás compilar la solución baseclasses como Debug. El resultado de esto será strmbase.lib (en versión retail) o strmbasd.lib (en versión debug). Librerías (prescindibles) que facilitan el uso de DirectShow.
Concretamente ese directorio (baseclasses) y la librería strmbasd.lib tendrán que ser agregadas como directorio directorio adicional de includes y como dependencias del proyecto.
Más detalles en Building DirectShow Filters.
CBaseObject: Objeto base para todo objeto DirectShow (facilita el debug y el desarrollo).CUnknown: Clase que implementa la interface IUnkNown y deriva de CBaseObject.CBasePin: Clase base para pins.CBaseInputPin y CBaseOutputPin clases base para pins de entrada y de salida.CBaseFilter: Clase base para filtros.CSource: Para filtros source push (no es adecuada para pull, como lectores de ficheros), para crear pins de output la clase adecuada es CSourceStream.CTransformFilter: Para filtros transform de copia de datos. Los pins adecuados son CTransformInputPin y CTransformOutputPin.CTransInPlaceFilter: Para filtros transform que procesan los datos (no los copian). Los pins para esta serían CTransInPlaceInputPin y CTransInPlaceOutputPin.CVideoTransformFilter: Para filtros transform de video.CBaseRenderer: para filtros de render, el pin de entrada es CRendererInputPin y una clase base específica para renderizadores de video sería CBaseVideoRenderer.La mayoría de estas no necesitan ser derivadas (copiado de la SDK):
Un filtro ha de crear uno o más pins, estos generalmente derivan de la clase CBasePin (o de alguna derivada como CBaseInputPin) y han de ser declarados como variables miembro dentro de la clase. Algunas clases proveidas por la SDK ya definen sus los pins necesarios.
Cuando se conectan dos filtros, los pins de estos han de casar con…
Un source push tiene un thread que va rellenando continuamente los samples para liberarlos al próximo filtro (downstream) en el graph (típico de una fuente de datos en realtime, como una webcam). Un source pull espera a que el siguiente filtro haga peticiones de datos, el primero escribirá los datos para el downstream dirigido por este (generalmente una lectura de fichero).
Un filtro de transformación recibe los samples del ubstream y los procesa y traslada al downstream.
Un filtro de render recibirá los samples del upstream y organizará el rendering a partir de los time stamps.
Los datos son almacenados en buffers (simples arrays de bytes), están dentro de un objeto COM denominado media sample (implementa IMediaSample) y son creados por los allocators (IMemAllocator). Un allocator se asigna a una conexión entre pines (dos o más conexiones de pines pueden compartir el mismo allocator).
Cuando un filtro necesita rellenar un buffer con datos le pide un sample al allocator llamando a IMemAllocator::GetBuffer. Si los samples están libres retornará un puntero a uno de ellos, si están en uso el método se bloqueará hasta que quede uno disponible. Un sample queda libre cuando el filtro downstream ha dejado de utilizarlo (p.ej. renderizandolo), de esa forma los samples no se sobreescriben.
El stride (o pitch) es el número de bytes de una fila de pixels que se reservan para agregar información adicional. Esto afecta al tamaño de la imagen en memoria pero no pero no a como esta se muestra.
Artículo de MSDN
Video no comprimido es una secuencia de bitmaps mostrados en sucesión con ratio de unos 30 frames por segundo. Para descomprimir un frame se utilizará el método CTransformFilter::Transform; este recibe un puntero a IMediaSample donde están encapsulados los datos, IMediaSample::GetPointer retornará un puntero al primer byte de los datos.
Información sacada de la MSDN, en ese artículo también hay ejemplos de…
Cuando produzcamos vídeo no comprimido rellenaremos el IMFMediaType con los siguientes valores:
Si se conocen se añadirán los siguientes valores: MF_MT_VIDEO_PRIMARIES, MF_MT_TRANSFER_FUNCTION, MF_MT_VIDEO_CHROMA_SITINGMF_MT_YUV_MATRIX y MF_MT_VIDEO_NOMINAL_RANGE.
Existen las siguientes funciones que pueden ser de utilidad:
En el filtro upstream:
Muchas clases base de DirectShow derivan de CUnknown, esta es por tanto la base a partir de la cual crear un filtro. CUnknown hereda de INonDelegatingUnknown, la cual realiza automáticamente el conteo de referencias; has de asegurarte que CUnknown se elimina cuando dicho conteo llega a cero, sino tendrás que sobreescribir CUnknown::NonDelegatingQueryInterface.
Hay que recordar también, al heredar de CUnknown, de definir los método de IUnknown (macro DCLARE_IUNKNOWN).
Tampoco podemos olvidar en el constructor llamar al constructor de la clase base. Éste ha de recibir por parámetros el nombre del componente (tszName), un puntero al IUnknown (pUnk) y un puntero al resultado HRESULT (pHr)
La estructura de código sería algo así:
class CMyComponent : public CUnknown, public ISomeInterface { public: DECLARE_IUNKNOWN; STDMETHODIMP NonDelegatingQueryInterface(REFIID riid, void **ppv) { if( riid == IID_ISomeInterface ) { return GetInterface((ISomeInterface*)this, ppv); } return CUnknown::NonDelegatingQueryInterface(riid, ppv); } CMyComponent(TCHAR *tszName, LPUNKNOWN pUnk, HRESULT *phr) : CUnknown(tszName, pUnk, phr) { /* Other initializations */ }; // More declarations will be added later. };
Los filtros tienen tres posibles estados: stopped, paused y running. El Filter Graph Manager controla las transiciones de estados a partir de IMediaControl::Run, IMediaControl::Pause e IMediaControl::Stop. El Filter Graph Manager siempre hará una llamada a IMediaControl::Pause antes de llamar a los otros dos.
Los live-sources filters (como las cámaras) no producirán samples cuando están en pausa, únicamente cuando están ejecutándose, para ello se consultará al IMediaFilter::GetState retornará VFW_S_CANT_CUE que indicará si está en pausa o en stop.
DirectShow implementa el DllGetClassObject por ti, pero aún así delega en el código ciertas funciones. Cada clase factoría es una instancia de CClassFactory.
La factory template contiene los campos m_Name (nombre), m_ClsID (CLSID), m_lpfnNew (función para crear una instancia del componente), m_lpfnInit (función opcional de inicialización), m_pAMovieSetup_Filter (información de set-up). m_lpfnNew y m_lpfnInit usan las siguientes definiciones:
typedef CUnknown *(CALLBACK *LPFNNewCOMObject)(LPUNKNOWN pUnkOuter, HRESULT *phr); typedef void (CALLBACK *LPFNInitRoutine)(BOOL bLoading, const CLSID *rclsid);
Un ejemplo, imaginemos que creamos una dll que contiene un componente CMyComponent que hereda de CUnknown. Tenemos pues que agregar…
// Public method that returns a new instance. CUnknown * WINAPI CMyComponent::CreateInstance(LPUNKNOWN pUnk, HRESULT *pHr) { CMyComponent *pNewObject = new CMyComponent(NAME("My Component"), pUnk, pHr ); if (pNewObject == NULL) { *pHr = E_OUTOFMEMORY; } return pNewObject; } CFactoryTemplate g_Templates[1] = { { L"My Component", // Name &CLSID_MyComponent, // CLSID CMyComponent::CreateInstance, // Method to create an instance of MyComponent NULL, // Initialization function NULL // Set-up information (for filters) } }; int g_cTemplates = sizeof(g_Templates) / sizeof(g_Templates[0]);
Para programar para DirectShow se tendrán que implementar los siguientes métodos:
Si se define el m_lpfnInit los tres primeros no serán necesarios. Además DirectShow da la función AMovieDllRegisterServer2 que permite hacer un wrap del registro:
STDAPI DllRegisterServer() { return AMovieDllRegisterServer2( TRUE ); } STDAPI DllUnregisterServer() { return AMovieDllRegisterServer2( FALSE ); }
En el fichero .def se tendrán que exportar las funciones:
EXPORTS
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
Un filtro se registra como servidor COM al llamar a la función CoCreateInstance de la DLL. No es necesario agregar más info, pero si se desea que sea visible para el System Device Enumerator o el Filter Mapper sí que será necesario.
Para declarar información adicional se utilizarán las estructuras AMOVEIESETUP_FILTER, AMOVIESETUP_PIN y AMOVEIESETUP_MEDIATYPE para proveer al IFilterMapper2 de lo necesario para localizar el filtro.
Para más información: How to Register DirectShow Filters.
Para indicar los formatos de captura y compresión que el filtro produce se utiliza el método IAMStreamConfig::GetStreamCaps. Este retorna un array de media type (estructura AM_MEDIA_TYPE) y structuras de capacidad (estructuras AUDIO_STREAM_CONFIG_CAPS o VIDEO_STREAM_CONFIG_CAPS). De VIDEO_STREAM_CONFIG_CAPS sólo se utilizan los campos guid, VideoStandard, MinFrameInterval y MaxFrameInterval, estos dos últimos son necesarios para obtener el rango de los frame rates soportados por el dispositivo de captura (tiempo mínimo y máximo de duración de un frame).
Por ejemplo podríamos tener los siguientes valores:
MinCroppingSize = (160, 120) MaxCroppingSize = (320, 240) CropGranularityX = 4 CropGranularityY = 8 CropAlignX = 2 CropAlignY = 4
Estos definen un grupo de rectángulos válidos para el rcSource del VIDEOINFOHEADER (o VIDEOINFOHEADER2). En este caso la estructura mínima es de 160×120 píxels. El tamaño en ancho puede incrementar en 4 hasta 320 y la altura en 8 hasta 240. CropAlignX y CropAlignY definen el crop para la ventana. Los siguientes valores serían válidos pues:
(0, 0, 160, 120) (2, 0, 162, 120) (2, 8, 162, 128)
El método IAMStreamConfig::SetFormat devuelve el formato antes de la conexión entre pines.
Supongamos que una captura se lleva a cabo en formato JPEG en todas las resoluciones entre 160×120 y 320×240; dicha diferencia entre las posibles resoluciones es denominada granularity. Si se permiten todas, la granularity es de 1, si por ejemplo fuese 8 las resoluciones serían: 160×120, 168×120, 168×128, 176×128… 312×232, 320×240.
Se usará GetStreamCaps para indicar el formato de color (16b, 24b…), las resoluciones y la granularity (esta podría ser 0 también). Se utiliza la estructura VIDEOINFOHEADER, ésta contiene las siguientes propiedades:
~ entonces el Filter Graph Manager no renderizará automáticamente (pero sí que podrán ser accedidos manualmente) su salida mediante IGraphBuilder::RenderFile (para pins de captura y no de preview, para pins de datos de información que no han de ser renderizados o de propiedades). Ejemplo para ver cómo implementar un IKsPropertySet para un pin de captura:
// Set: Cannot set any properties. HRESULT CMyCapturePin::Set(REFGUID guidPropSet, DWORD dwID, void *pInstanceData, DWORD cbInstanceData, void *pPropData, DWORD cbPropData) { return E_NOTIMPL; } // Get: Return the pin category (our only property). HRESULT CMyCapturePin::Get( REFGUID guidPropSet, // Which property set. DWORD dwPropID, // Which property in that set. void *pInstanceData, // Instance data (ignore). DWORD cbInstanceData, // Size of the instance data (ignore). void *pPropData, // Buffer to receive the property data. DWORD cbPropData, // Size of the buffer. DWORD *pcbReturned // Return the size of the property. ) { if (guidPropSet != AMPROPSETID_Pin) return E_PROP_SET_UNSUPPORTED; if (dwPropID != AMPROPERTY_PIN_CATEGORY) return E_PROP_ID_UNSUPPORTED; if (pPropData == NULL && pcbReturned == NULL) return E_POINTER; if (pcbReturned) *pcbReturned = sizeof(GUID); if (pPropData == NULL) // Caller just wants to know the size. return S_OK; if (cbPropData < sizeof(GUID)) // The buffer is too small. return E_UNEXPECTED; *(GUID *)pPropData = PIN_CATEGORY_CAPTURE; return S_OK; } // QuerySupported: Query whether the pin supports the specified property. HRESULT CMyCapturePin::QuerySupported(REFGUID guidPropSet, DWORD dwPropID, DWORD *pTypeSupport) { if (guidPropSet != AMPROPSETID_Pin) return E_PROP_SET_UNSUPPORTED; if (dwPropID != AMPROPERTY_PIN_CATEGORY) return E_PROP_ID_UNSUPPORTED; if (pTypeSupport) // We support getting this property, but not setting it. *pTypeSupport = KSPROPERTY_SUPPORT_GET; return S_OK; }
El pin de preview ha de enviar una copia de datos del pin de captura pero sin la posibilidad que el pin de captura pierda frames ya que éste ha de tener prioridad sobre el de preview.
Los dos pins (captura y preview) han de enviar los datos con el mismo formato (media types idénticos). Para saber qué formatos son los adecuados se utilizará IPin::QueryAccept.
Más sobre ello en Implementing a Preview Pin.
Un filtro de captura únicamente debe producir datos cuando el filtro está siendo ejecutado y no pausado. Cuando esté pausado deberá devolver VFW_S_CANT_CUE desde el método CBaseFilter::GetState, lo que informa al Filter Graph Manager que no debería esperar por más datos mientras el filtro está pausado.
CMyVidcapFilter::GetState(DWORD dw, FILTER_STATE *pState) { CheckPointer(pState, E_POINTER); *pState = m_State; if (m_State == State_Paused) { return VFW_S_CANT_CUE; } else { return S_OK; } }
El pin de un filtro de captura (ya sea pin de preview o captura) ha de soportar la interface IAMStreamControl. De esta forma la aplicación puede utilizar el pin de preview y pasar al de captura sin reconstruir el graph. Para que esto sea más sencillo puede usarse la clase CBaseStreamControl.
Cuando el filtro captura un sample se le ha de hacer un time stamp. El end-time es el start-time mas la duración (For example, if the filter is capturing at ten samples per second, and the stream time is 200,000,000 units when the filter captures the sample, the time stamps should be 200000000 and 201000000. (There are 10,000,000 units per second)).
Para calcular dicho tiempo se llamará a IReferenceClock::GetTime y se le restará el start-time. CBaseFilter::StreamTime realiza el mismo cálculo; para asignarlo se llamará a IMediaSample::SetTime. Los time stamps deberían ir incrementando entre samples (incluso cuando el filtro pausa).
Los samples de un pin de preview, en cambio, no deberían contener time stamps.
Una live-source toma los datos de una fuente externa (camara o stream de red) y no puede controlar el ratio de los datos. Si el filtro downstream (el siguiente) no consume los datos el filtro source tendrá que desacerse de ellos.
Una live-sorce devolverá AM_FILTER_MISC_FLAGS_IS_SOURCE en el método IAMFilterMiscFlags::GetMiscFlags y uno de sus pins deberá ser IAMPushSource. El filtro expondrá un IKsPropertySet y un pin de captura (PIN_CATEGORY_CAPTURE).
La latencia del filtro es la cantidad de tiempo que le toma procesar un sample, en los live-source se determina por el tamaño del buffer.
Los live-sources se sincronizarán por la interface IAMPushSource mediante el método IAMGraphStreams::SyncUsingStreamOffset.
Un source push no será siempre una live-source, también podría, por ejemplo, leer de un fichero local. En ese caso el downstream determinará cuan rápido lee los datos según el clock y los time stamps.
Cada pin de output del source filter crea un thread llamado streaming thread. Ahí será donde el pin deje los datos y en dicho thread donde el decoding, el processing y el rendering ocurra (aunque los downstream filters pueden crear threads adicionales). Este thread sigue la siguiente estructura (hasta el final):
Las clases para los push sources son CSource y CSourceStream. CSource es la clase base para el filtro y CSourceStream lo es para los pins de output.
CSourceStream crea un thread de streaming para un pin, su motor se implementa en el CSourceStream::DoFufferProcessingLoop el cual llama a CSourceStream::FillBuffer que ha de ser sobreescrito, aquí es donde se definen los datos del buffer. También contiene otros métodos que pueden ser sobreescritos (CSourceStream::OnThreadCreate, CSourceStream::OnThreadDestroy y CSourceStream::OnThreadStartPlay), si no lo son se devolverá S_OK por defecto.
Los pins de output se crearán en el constructor (uno por output stream); no será necesario guardar sus direcciones ya que ellos se agregan al filtro automáticamente al crearse. La negociación de dichos pins se realiza mediante los métodos:
GetMediaType puede contener únicamente un puntero a CMediaType (cuando soporta un solo media type) o un puntero y una variable de índice (cuando soporta varios). Uno (y sólo uno) de los dos se ha de sobreescribir, en el caso de tener varios media types también deberá ser sobreescrito el CheckMediaType.
Para los formatos de video sin comprimir el filtro downstream podrá proponer cualquier stride value (valor de salto, en cada fila de la imagen los valores adicionales que se añaden).
También de deberá sobreescribir el método CBaseOutputPin::DecideBufferSize para indicar el tamaño de los buffers.
Para definir las propiedades de un filtro se utiliza la clase CBasePropertyPage. Siguiendo el tutorial en Creating a Filter Property Page, los pasos son:
GetInterface: Asigna el puntero a la referencia, incrementa el contador (thread-safe) y devuelve S_OK si todo ha ido bien.DECLARE_IUNKNOWN, código que declara los método de IUnknown.
Antes de crear un objeto COM se crea un clase factoría de estos (función CoGetClassObject). IClassFactory::CreateInstance es la que creará y devolverá el componente.
Dicha función (CoGetClassObject) internamente llama a DllGetClassObject que ha de estar definida en la DLL. Esta función crea la clase factoría y retorna una interface a esta.
Una clase factoría es un objeto COM que se encarga de crear un tipo concreto de objeto COM. Estas utilizan una CFactoryTemplate (plantilla de factorías) la cual contiene información sobre un componente específico (como por ejemplo el identificador CLSID o la función que crea el componente).
La DLL ha de declarar un array de “factory templates” (una por cada componente registrado en esta). Cuando la función DllGetClassObject crea una nueva factoría lo hace buscando en el array por el CLSID y llama al IClassFactory::CreateInstance.
Para ello utilizaremos la macro DEFINE_GUID, se utiliza de la siguiente forma:
DEFINE_GUID(CLSID_MyObject, 0x00000000, 0x0000, 0x0000, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
Podemos obtener una GUID mediante la herramienta Guidgen.exe o con el mismo Visual Studio (tools → create guid).
Cuando en tu proyecto varios archivos de código usan una GUID ésta tendrá que estar declara una vez (única y exclusivamente) en el proyecto y todos los ficheros hacer referencia a ella. Añadiremos dicha GUID un .h y dicho .h deberá ser incluido en los .cpp que la requieran, además, en uno (y sólo en uno) deberá incluirse (antes que dicho .h) el fichero Initguid.h. Algo así…
// Src1.cpp #include <initguid.h> #include "MyGuids.h" // Src2.cpp #include "MyGuids.h" // Src3.cpp #include "MyGuids.h"