Tabla de Contenidos

DirectShow

Conceptos

DirectShow es la tecnología de Microsoft para el desarrollo de aplicaciones que reproducen audio y video.

Graphs, pins & filters

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.

Registro y prueba de filtros

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.

COM y la interface IUnknown

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:

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.

Desarrollo

Inicio

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.

Clases en la SDK

Clases COM

Clases pin

Clases de filtros

Otras clases

La mayoría de estas no necesitan ser derivadas (copiado de la SDK):

Pins y traslado de datos

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…

  1. Transporte, el mecanismo que los filtros usarán para proveer datos del output al input. Por ejemplo IMemInputPin (push model) o IAsyncReader (pull model).
  2. Media type. Qué tipo de datos se comunicarán.
  3. Allocator, el objeto que crea los buffers que contienen los datos. Los pins han de coincidir cual de los dos lo provee, en el tamaño del buffer, el número se estos y sus propiedades.

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.

Flujo de datos

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.

Formato de los datos

Stride de la imagen

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

Leer video no comprimido

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.

Producir video no comprimido

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:

Cambio del formato de datos

En el filtro upstream:

  1. El filtro downstream comprueba en la conexión quién debe ser propietario del allocator, si lo es el upstream no podrá cambiar el formato de los datos.
  2. El filtro downstream llama a IPin::QueryAccept del pin de output del upstream.
  3. Si QueryAccept retorna S_OK, entonce el filtro downstream llama al método privado del allocator para asignar el media type. Dentro de este método se llama a IMediaSample::SetMediaType.
  4. El filtro upstream llama a GetBuffer para tomar un nuevo sample y a IMediaSample::GetMediaType para obtener el media type.
  5. Cuando el filtro upstream deja el sample debe indicar el media type, de esa forma el filtro de downstream podrá conformar que el media type a cambiado.

Explicación en MSDN

Creación de un filtro

CUnknown

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 estados de un filtro

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.

Definición de componentes en una DLL

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…

  1. Una función que devuelva una instancia de CMyComponent.
  2. Un array global (denominado g_Templates) que contenga su la factory template adecuada.
  3. El tamaño del array en la variable g_cTemplates.
// 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]);   

La DLL

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

El registro de filtros

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.

Formatos de captura y compresión

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:

Ampliar filtros

Creación de un filtro de captura

Requisitos para los pins

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;
}

Pin de preview

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.

Generar datos de salida

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.

Control de tiempo

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.

Creación de un filtro source

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.

Live-sources

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.

Streaming de datos

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):

  1. Tomar un sample del allocator (si no hay disponibles se bloqueará hasta que los haya).
  2. Llenar el sample de datos.
  3. Time stamp el sample.
  4. Dejar el sample al downstream.

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.

Formato de la imagen

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.

Definición de propiedades del filtro

Para definir las propiedades de un filtro se utiliza la clase CBasePropertyPage. Siguiendo el tutorial en Creating a Filter Property Page, los pasos son:

  1. Se ha de indicar un método por el que el filtro se comunique con la página de propiedades, los mecanismos posibles son:
    1. A partir de una interface COM desarrollada para ello.
    2. Automatización de propiedades a partir de IDispatch.
    3. A partir de la interface IPropertyBag y de definir un conjunto de propiedades.
  2. Se utilizará la interface ISpecifyPropertyPages, la cual tiene un único método (GetPages) que retorna un array de CLSIDs para las páginas de propiedades.
  3. Recuerda utilizar en el filtro la macro DECLARE_IUNKNOWN, luego tendrás que comprobar las IIDs de las dos interfaces sobreescribiendo CUnknown::NonDelegatingQueryInterface.
  4. Se implementará la página de propiedades a partir de CBasePropertyPage.
  5. Se utilizará CBasePropertyPage::OnConnect para almacenar un puntero al filtro.
  6. En el método CBasePropertyPage::OnActivate se inicializará la clase de la ventana de propiedades con sus controles.
  7. Se sobreescribirá CBasePropertyPage::OnReceiveMessage para actualizar la ventana según la entrada de datos del usuario (si no se trata ningún mensaje llama al OnReceiveMessage de la clase padre).
  8. El método CBasePropertyPage::OnApplyChanges debe aceptar los cambios, asignando los nuevos valores sobre las variables.
  9. Usa CBasePropertyPage::OnDisconnect para cuando se cierre la ventana sin aceptar los cambios.
  10. Si lo has hecho por COM, agrega otra CFactoryTemplate al array g_Templates que indica los objetos COM que tiene tu dll.

Notas

@echo off
SET filterfile=VCamD.ax
AT > NUL
IF NOT %ERRORLEVEL% EQU 0 (
    ECHO Necesitas ser administrador para instalar el filtro
    pause
    EXIT /B 1
)
regsvr32 %~dp0%filterfile%

Notas de programación COM

Funciones de ayuda

Instanciación de los objetos COM

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.

Definir GUID

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"

Recursos