ZFX
ZFX Neu
Home
Community
Neueste Posts
Chat
FAQ
IOTW
Tutorials
Bücher
zfxCON
ZFXCE
Mathlib
ASSIMP
NES
Wir über uns
Impressum
Regeln
Suchen
Mitgliederliste
Membername:
Passwort:
Besucher:
4440681
Jetzt (Chat):
17 (0)
Mitglieder:
5239
Themen:
24223
Nachrichten:
234554
Neuestes Mitglied:
-insane-
ZFX - TutorialsDruckversion

DVD-Player mit DShow
Das Tutorials von Andrew Kerkel aka Marshal

© Copyright 2004 [whole tutorial] by Andrew Kerkel aka Marshal
Die Texte der Tutorials unterliegen dem Copyright und dürfen ohne schriftliche Genehmigung vom Author weder komplett noch auszugsweise vervielfältigt, auf einer anderen Homepage verwendet oder in sonst einer Form veröffentlicht werden. Die zur Verfügung gestellten Quelltexte hingegen stehen zur freien Weiterverwendung in Software Projekten zur Verfügung.
Die Quelltexte dieses Tutorials werden auf einer "as is" Basis bereit gestellt. Der Autor übernimmt keinerlei Garantie für die Lauffähigkeit der Quelltexte. Für eventuelle Schäden die sich aus der Anwendung der Quelltexte ergeben wird keinerlei Haftung übernommen.


Programmierung eines DVD-Players (Frontend) mit DirectShow®

Dieses Tutorial ist für all jene gedacht, die sich etwas (mehr) mit DirectShow® auseinandersetzen wollen, aber nie so recht die Motivation hatten, sich durch die SDK-Beispiele zu kämpfen... und natürlich auch für alle sonstigen Interessierten.

Download Source!
(incl. ausführbarer Datei)

 

0. Prolog

Gleich vorneweg weise ich nochmal deutlich auf den Ausdruck "Frontend" in der Überschrift hin. Das bedeutet, daß es sich hier rein um die Software handelt, mit der der Nutzer arbeiten kann. Genauer: Es handelt sich hier nicht auch noch um den Decoder, deswegen braucht man, um auch mit diesem Player DVDs anschauen zu können, einen 3rd Party Decoder. Jeder der WinDVD, PowerDVD o.ä. bei sich installiert hat, sollte diese Software benutzen können. Am Schluß des Tutorials werde ich noch etwas zu möglichen Fehlermeldungen sagen, die trotz installiertem Decoder evtl. auftreten könnten.

OK, was genau ist das Ziel dieses Tutorials?
Unser Ziel ist hier, eine kleine Software zu entwickeln, die vollständig über ein Popup-Menu (über die rechte Maustaste) steuerbar ist.

Hierbei hab ich mich etwas an den beiden SDK Beispielen orientiert.

Da es sich hier um ein fortgeschritteneres Tutorial handelt, gehe ich davon aus, daß die meisten bereits Grundlagen in der Windows-Programmierung haben und werde deshalb nicht alles und jedes Windows-Bezogene im Detail besprechen.
Bei der Formatierung des Codes habe ich mich nahe am Syntaxhighlighting von VisualAssist orientiert.

Bei den C++-only-Fanatikern entschuldige ich mich jetzt schon mal für die Verwendung der Funktion sprintf(). ;)

 

1. Rahmen

Bevor wir uns an das eigentliche Thema machen, werfen wir noch schnell einen Blick auf die Rahmenanwendung und die Dateien.

Das gesamte Projekt habe ich auf 3 Dateien verteilt:

  1. App.cpp (der Rahmen des Projekts - WinMain())
  2. DVDPlayer.h
  3. DVDPlayer.cpp

Ausnahmsweise und der Einfachheit halber zeige ich die Datei App.cpp als Ganzes, da sie wirklich sehr kurz ist:

#include "DVDPlayer.h"

// ================================================================================================
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
    CDVDPlayer DVDPlayer;
    if (!DVDPlayer.Init()) return -1;

    MSG msg;
    while(::GetMessage(&msg, 0, 0, 0))
    {
        ::TranslateMessage(&msg);
        ::DispatchMessage(&msg);
    };

    DVDPlayer.ShutDown();

    return static_cast<int>(msg.wParam);
};

Als erstes inkludieren wir die Header-Datei für unseren DVD-Player - "DVDPlayer.h".

Im Rumpf der WinMain() besorgen wir uns als erstes ein CDVDPlayer-Objekt (sehen wir uns in Kürze näher an), das wir im nächsten Schritt initialisieren. Sollte das fehlschlagen (boolscher Rückgabewert), brechen wir das Ganze ab, wobei in der Initialisierung (die wir uns gleich im Detail ansehen werden) auch eine Fehlermeldung erscheint. Als nächstes haben wir die wohl einfachste Form der Nachrichtenschleife. Zum Abschluß wird noch eine ShutDown()-Funktion aufgerufen und das wars dann auch schon wieder.

Falls sich jemand über den global Namespace operator ("::") wundert, der vor jeder Windows-Funktion steht... ich verwende diesen lediglich als Kennzeichnung von Windows-API Funktionen. Nötig ist er selbstverständlich nicht. Ebenso setze ich nach jeder schließenden Klammer einen Strichpunkt. Beides lediglich Elemente meiner eigenen Coding-Conventions. Stört euch nicht dran.

 

2. "DVDPlayer.h"

Sehen wir uns gleich mal die ersten Zeilen der Header-Datei an:

#define WIN32_LEAN_AND_MEAN
#define _WIN32_WINNT 0x0400

#include <dshow.h>

Zuerst benutzen wir das übliche #define, um unnötiges MFC-Zeug von uns fernzuhalten. Das nächste #define benötigen wir, um die entsprechenden COM-Funktionen zum Laufen zu bringen.

Achtung:
COM steht für Component Object Model und ist eine Technik, die z.B. von DirectX® verwendet wird. Wer darüber mehr wissen will, soll bitte ins Internet schauen oder in die DirectX® Dokumentation. Hier sei nur so viel gesagt, daß über dieses strikte (!) Programmiermodell dem User Interfaces angeboten werden, mit denen er arbeiten kann. Was dahinter implementativ steht, weiß er (leider?) nicht. Libraries, die auf der COM Technik aufbauen, sind Programmiersprachen unabhängig! D.h. das eine in C++ geschriebene Library auch in Visual Basic verwendet werden kann.
Hat man noch kein einziges Interface, benutzt man die CoCreateInstance() Funktion um eines zu bekommen. Weitere Interfaces kann man (meistens) über die Methode QueryInterface() des bereits besorgten Objektes bereitstellen.

Die einzige include-Datei ist hier dshow.h und sorgt dafür (wer hätte das gedacht), daß wir DirectShow® verwenden können. Tatsächlich müssen wir allerdings auch noch die statische Library strmiids.lib einbinden, aber das sehen wir bald, wenn wir zur eigentlichen Implementierung kommen.

So, jetzt kommen wir zur DVD-Player Klasse, die ich, wir ihr schon gesehen habt, in einem Anfall von Einfallsreichtum CDVDPlayer genannt habe. Werfen wir also einen kurzen Blick auf den Anfang der Klasse:

class CDVDPlayer
{
private:
    enum EMenuIDs
    {
        ID_Play         = 0x0001,
        ID_Stop         = 0x0002,
        ID_Pause        = 0x0004,
        ID_SubPicAct    = 0x0008,
        ID_Menu         = 0x0010,
        ID_Chapter_Prev = 0x0020,
        ID_Chapter_Next = 0x0030,
        ID_Resume       = 0x0040,
        ID_Chapter      = 0x1000,
        ID_Title        = 0x2000,
        ID_SubPic       = 0x3000,
        ID_Audio        = 0x4000,
        ID_MAXIDS       = 0x5000
    };


    [...]

Gleich zu Anfangs definiere ich hier einen enum-Typ, damit wir die Elemente des Menus auch ansprechen können:

Die Namen dürften alle selbsterklärend sein. Anzumerken ist hierbei jedoch, daß die vier letzten IDs vor ID_MAXIDS selbst jeweils wieder Popup-Menus darstellen, die wir dynamisch beim Einlesen der DVD und auch während des Betriebes entsprechend auffüllen werden. Der (Werte-)Abstand zwischen den SubMenus dürfte wirklich ausreichen (1000hex = 4096dez) oder hat schon mal jemand eine DVD mit mehreren Tausend Kapiteln gesehen?! ;)

In der Header Datei kommen jetzt die interessantesten Deklarationen, doch die hebe ich gleich mal ganz fies für das Ende der Erklärung der Header-Datei auf, da es hier am Meisten zu sagen gibt.

Schauen wir uns zuerst alle restlichen Deklarationen an:

    [...]

    bool  m_bPaused;
    bool  m_bRunning;
    bool  m_bRunningTimeScaled;
    bool  m_bFullscreen;
    bool  m_bSubpicture;

    HWND  m_hWnd;

    HMENU m_hMenu;
    HMENU m_hMenuChapters;
    HMENU m_hMenuTitles;
    HMENU m_hMenuAudios;
    HMENU m_hMenuSubpictures;

    ULONG m_ulNumTitles;
    ULONG m_ulNumChapters;
    ULONG m_ulNumAudios;
    ULONG m_ulNumSubpictures;

    ULONG m_ulCurTitle;
    ULONG m_ulCurChapter;
    ULONG m_ulCurAudio;
    ULONG m_ulCurSubpicture;

    [...]

Die ersten Fünf sind lediglich Statusvariablen:
m_bPaused, m_bRunning und m_bFullscreen sind selbsterklärend.
m_bRunningTimeScaled gibt an, ob wir uns gerade im schnellen Vor- oder Rücklauf befinden und m_bSubpicture, ob Untertitel gerade aktiviert sind oder eben nicht.

m_hWnd ist natürlich das Handle auf unser Hauptfenster.

m_hMenu ist das Handle auf das eigentliche Menu, das erscheint, wenn die rechte Maustaste innerhalb des Fensters gedrückt wurde.
Die nächsten vier Menuhandles sind die (Sub-)Popup-Menus, die ich beim Enum-Typ EMenuIDs schon erwähnte. Wie das genau aussieht, sehen wir bald.

Weiterhin kommen jetzt vier Variablen, die einfach die Anzahl der Titel, der Kapitel, der Audiospuren und der Untertitel angeben. Man muß sich dabei im Klaren darüber sein, daß die Titel hierarchisch über allem Anderen stehen. Die Wahl eines Titels erfordert das (Neu-)Bestimmen der Kapitel, der Audiospuren und der Untertitel. Jeder der mit Software-DVD-Playern hantiert, hat das ohnehin wahrscheinlich schon registriert.

Zu guter Letzt noch vier Variablen, die angeben, in welchem Kapitel, etc... wir uns gerade befinden.

Und damit sind wir am Ende der Header-Datei:

    [...]

    bool         RaiseFailureMessage(const char* const p_pcMsg);
    bool         RaiseFailureMessage(const AM_DVD_RENDERSTATUS* const p_pStatus);
    bool         CreateBaseWindow();
    bool         AcquireInterfaces();
    void         CreateMenu();
    void         ClearMenu(const HMENU p_hMenu) const;

    void         ReSize() const;
    bool         ToggleScreenMode();

    void         OnUserCommand(const WPARAM p_wParam);
    void         OnTitleSelect(const int p_iTitle);
    void         OnSubpictureSelect(const int p_iSubpicture);
    void         OnAudioSelect(const int p_iAudio);
    void         OnChapterSelect(const int p_iChapter);
    void         OnMouseEvent(const UINT p_iMsg, const LPARAM p_lParam) const;
    void         OnKeyboardEvent(const UINT p_iMsg, const WPARAM p_wParam);
    void         OnDVDEvent();

    void         Play(bool p_bPlay = true);

    void         TogglePause();

public:
    CDVDPlayer() { memset(this, 0, sizeof(*this)); };
    bool         Init();
    void         ShutDown();
    LRESULT      MsgProc(HWND p_hwnd, UINT p_uiMsg, WPARAM p_wParam, LPARAM p_lParam);
};

Sämtliche Methoden werden wir uns noch im Detail ansehen, zunächst aber die Grobübersicht:

  • Die beiden RaiseFailureMessage() Methoden sind lediglich dafür da, dem User eine MessageBox an den Kopf zu schmeissen, falls etwas schiefgelaufen ist und geben immer false zurück.
  • CreateBaseWindow() macht nichts anderes als die Fensterklasse zu registrieren und das Basisfenster zu erstellen. Deswegen spare ich es mir einfach sie noch extra abzudrucken, da hier wirklich nichts Neues passiert.
  • AquireInterfaces() sorgt für die Bereitstellung sämtlicher Interfaces, die wir brauchen, um mit unserer Software sinnvoll arbeiten zu können.
  • CreateMenu() erstellt das Popup-Menu, das bei rechtem Maustastendruck erscheint.
  • ClearMenu() ist nur eine kleine Hilfsfunktion, um den Inhalt eines Submenus zu löschen.
  • ReSize() passt das eigentliche Videofenster an das Hauptfenster an (was das bedeutet werden wir noch sehen).
  • ToggleScreenMode() schaltet einfach zwischen Fenster- und Vollbildmodus um.
  • OnUserCommand() ist eine unserer wichtigsten Methoden. Sie sorgt für die Ausführung, wenn der User irgendwas im Popup-Menu angeklickt hat.
  • Die nächsten vier Methoden sind wieder kleine Hilfsfunktionen (ok, die erste ist eigentlich keine Hilfsfunktion) die wir allerdings nie direkt ausführen, sondern von OnDVDEvent() ausführen lassen.
  • OnMouseEvent() und OnKeyboardEvent() kommen natürlich immer dann zum Tragen, wenn entweder die Maus oder die Tastatur verwendet wurde.
  • OnDVDEvent() ist die zweite entscheidende Funktion, denn hier reagieren wir quasi auf die Anweisungen unseres DVD-Laufwerks, die es durch unsere OnUserCommand() Methode bekommen hat.
  • Ich trau's mich kaum zu sagen: Die Play() Methode spielt unsere DVD ab... kann das Abspielen aber auch stoppen, wenn wir ihr false als Parameter übergeben (ist also zugleich auch Stop-Funktion).
  • Jetzt kommt der Destruktor, der lediglich dafür sorgt, alles auf "0" zu setzen.
  • Das mußte ja noch kommen: Eine Init() Methode, die alles initialisiert und zusätzlich auch gleich Play() aufruft, wenn eine DVD im Laufwerk ist.
  • Mit TogglePause() unterbrechen wir das Abspielen oder die Pause.
  • Und wo initialisiert wird, wird normalerweise auch noch heruntergefahren und deswegen gibt es die ShutDown() Methode.
  • Zu guter Letzt brauchen wir natürlich auch noch eine Nachrichtenfunktion.

Ich hatte euch noch versprochen, daß wir uns die vorher ausgelassen Interfaces noch näher anschauen und genau das werden wir jetzt tun:

    [...]

    // DirectShow interfaces
    IGraphBuilder*    m_pGraphB;
    IMediaControl*    m_pMediaCtrl;
    IMediaEventEx*    m_pMediaEvent;
    IVideoWindow*     m_pVideoWin;

    // DirectShow (DVD) interfaces
    IDvdGraphBuilder* m_pDvdGraphB;
    IDvdControl2*     m_pDvdCtrl;
    IDvdInfo2*        m_pDvdInfo;

    [...]

Aha, hier haben wir nun ein paar Variablen, die nicht alle selbsterklärend sind - angefangen mit dem ersten: IGraphBuilder.
Dieses Interface wird über das IDvdGraphBuilder-Interface besorgt und dient im Prinzip nur dazu, seinerseits jeweils das IMediaControl-, IMediaEventEx- und IVideoWindow-Interface zu besorgen.
Hm, mächtig interessant, aber schlauer sind wir jetzt immer noch nicht.

Also ganz von vorne:
Zunächst werden wir uns nachher in der AquireInterfaces() Methode das IDvdGraphBuilder-Interface besorgen und zwar über die Funktion CoCreateInstance(). Dabei ist dieses Interface lediglich ein Helfer-Interface speziell für DVD-Anwendungen (läßt ja das "Dvd" im Namen irgendwoher auch vermuten). Von diesem Interface aus werden wir uns das IDvdControl2-, das IDvdInfo2- und eben das IGraphBuilder-Interface besorgen. Fein, aber wozu ist nötig?
Aus folgendem Grund: Das IDvdGraphBuilder-Interface erstellt uns einen so genannten Filtergraphen, der letztendlich das DVD-Abspielen durch die Konkatenation mehrer verschiedener Filter ermöglicht. Für unsere Anwendung sieht der tatsächliche Filtergraph so aus (dargestellt mit GraphEdit aus dem DirectX® SDK):

Jede DVD wird über den Eingangsfilter eines Filtergraphen (nochmal: Ein Filtergraph ist eine Aneinanderreihung von Filtern) ausgelesen. Diese Daten gehen über den A/V-Splitter, der die Audio- und Videodaten trennt, schließlich zu den jeweiligen Decodern (die mit einer kommerziellen Software (WinDVD, PowerDVD, etc...) daherkommen) und von dort aus zu den entsprechenden Ausgängen. Wie wir hier sehen wird das Splitten von Sound- und Videodaten (außerdem noch die Untertiteldaten), in einem DVD-Graphen (immer!) vom so genannten DVD-Navigator übernommen. Die Filter sind dabei untereinander mit so genannten Pins verbunden. Das aber nur am Rande.
Was ist nun aber eigentlich ein Filter? Wie wir aus der obigen Grafik entnehmen, kann ein Filter alles mögliche sein.
Ein Filter ist z.B. nötig, um die Daten von der DVD zu lesen und die gelesenen Daten zu splitten. Wieder andere Filter sind für das Decodieren zuständig und letztendlich werden irgendwelche Ausgabefilter sich eben um die Ausgabe kümmern, bzw. die Daten entsprechend aufbereitet an andere Geräte weitergeben.

So, nach all dem Gedöns gleich mal die gute Nachricht: Darum müssen wir uns gar nicht kümmern! DirectShow® nimmt uns die Erstellung des Filtergraphen nämlich vollständig ab. Aber es ist eben möglich, diverse Graphen von Hand zu erstellen und das ist auch genau das, was die Hersteller professioneller Software normalerweise auch machen (und ihren Player damit auch etwas besser unter Kontrolle haben). Wir begnügen uns hier aber mit dieser Variante und werden sehn, daß sich auch damit sehr wohl was anfangen läßt.

Nun haben wir also in der m_pDvdGraphB-Variable einen solchen Graphen besorgt. Allerdings war das IDvdGraphBuilder-Interface ja bloß ein Hilfsinterface und deswegen müssen wir uns den eigentlichen Graphen IGraphBuilder über eine entsprechende Methode besorgen.

Bevor ich aber an dieser Stelle noch weiter einsteige wechseln wir lieber gleich in die DVDPlayer.cpp und sehen uns die Implementierung an. Hier werden wir die kurz angesprochenen Interfaces etwas genauer betrachten.

 

3. Eigentliche Implementierung

Bevor wir zu den Interfaces zurückkommen, werfen wir noch schnell einen Blick auf den Kopf der DVDPlayer.cpp und steigen dann voll ein:

#include "DVDPlayer.h"
#include <string>

#pragma comment(lib,"strmiids.lib")

#define SaveRelease(p)  { if(p){ (p)->Release(); (p)=0; }; }
#define WM_GRAPHNOTIFY  (WM_USER+1)

CDVDPlayer* g_pDVD;     // nur für DVDMsgProc(...)

// ================================================================================================

LRESULT CALLBACK DVDMsgProc(HWND p_hwnd, UINT p_uiMsg, WPARAM p_wParam, LPARAM p_lParam)
{
    
g_pDVD->MsgProc(p_hwnd, p_uiMsg, p_wParam, p_lParam);
};

OK, als erstes binden wir natürlich unsere Header-Datei ein, sowie noch die string-Klasse der STL um die Ausgabe der RaiseFailureMessage() Methode etwas angenehmer formatieren zu können.

Über #pragma comment(lib,"strmiids.lib") binden wir die für DirectShow® benötigte Library ein.
Das SaveRelease()-Makro ist nur dafür da, in der ShutDown() Methode das Freigeben der Interfaces zu vereinfachen.
Das nächste #define ist nötig, um Windows mitzuteilen, welche Message es bei einem DVD-Event verschicken soll. Dabei ist der Name selbstverständlich frei wählbar. Wichtig ist dabei lediglich, daß der Wert auf etwas über WM_USER gesetzt wird (oder alternativ auf etwas über WM_APP).
Die globale Variable ist nur dafür notwendig, unserer Fensterklasse in der CreateBaseWindow() Methode die globale Messageprozedur übergeben zu können.

So, damit verlassen wir endgültig den funktionalen Teil und steigen in die AcquireInterfaces() Methode ein:

bool CDVDPlayer::AcquireInterfaces()
{
    if (S_OK != ::CoCreateInstance( CLSID_DvdGraphBuilder,
                                    
NULL,
                                    CLSCTX_INPROC_SERVER,
                                    IID_IDvdGraphBuilder,
                                    reinterpret_cast<void**>(&m_pDvdGraphB)))
        return RaiseFailureMessage("CoCreateInstance (CLSID_DvdGraphBuilder) failed!");

    if (S_OK != m_pDvdGraphB->GetFiltergraph(&m_pGraphB))
        return RaiseFailureMessage("GetFiltergraph failed!");

    if (S_OK != m_pGraphB->QueryInterface(IID_IMediaEventEx, reinterpret_cast<void**>(&m_pMediaEvent)))
        return RaiseFailureMessage("QueryInterface (IID_IMediaEventEx) failed!");

    if (S_OK != m_pGraphB->QueryInterface(IID_IMediaControl, reinterpret_cast<void**>(&m_pMediaCtrl)))
        return RaiseFailureMessage("QueryInterface (IID_IMediaControl) failed!");

    if (S_OK != m_pGraphB->QueryInterface(IID_IVideoWindow, reinterpret_cast<void**>(&m_pVideoWin)))
        return RaiseFailureMessage("QueryInterface (IID_IVideoWindow) failed!");

    AM_DVD_RENDERSTATUS status;
    if (S_OK != m_pDvdGraphB->RenderDvdVideoVolume(NULL, AM_DVD_HWDEC_PREFER, &status))
        return RaiseFailureMessage(&status);

    if (S_OK != m_pDvdGraphB->GetDvdInterface(IID_IDvdInfo2, reinterpret_cast<void**>(&m_pDvdInfo)))
        return RaiseFailureMessage("GetDvdInterface (IID_IDvdInfo2) failed!");

    if (S_OK != m_pDvdGraphB->GetDvdInterface(IID_IDvdControl2, reinterpret_cast<void**>(&m_pDvdCtrl)))
        return RaiseFailureMessage("GetDvdInterface (IID_IDvdControl2) failed!");

    return true;
};

Als Allererstes besorgen wir uns mit der CoCreateInstance() Funktion das IDvdGraphBuilder-Interface (hat das geklappt, wird S_OK zurückgegeben).
Im ersten Parameter muß dazu der so genannte Class-Identifier übergeben werden, der wie das Interface selbst heißt, doch anstatt dem führenden "I" ein "CLSID_" (CLasSIDentifier) stehen hat. Parameter Nummer zwei muß hier NULL sein und Parameter Nummer drei gibt den Klassenkontext an und muß bei uns CLSCTX_INPROC_SERVER lauten. Wird diese Funktion evtl. in einem anderen Thread ausgeführt, muß hier ein anderer Kontext angegeben werden (steht alles im Windows SDK).
Der vierte Parameter ist ein so genannter Interface-Identifier (IID), der uns gleich noch öfter begegnen wird. Alle Interfaces, die über COMs verwendet werden, müssen einen solchen eindeutig identifizierbaren Bezeichner besitzen. Dabei heißt der Identifier immer wie das Interface selbst mit dem Unterschied, daß das Prefix "IID_" noch davor gesetzt wird.
Zu guter Letzt müssen wir unsere IDvdGraphBuilder-Variable aus der Header-Datei übergeben, mit der wir ja schließlich weiterarbeiten wollen.

Als nächstes besorgen wir uns den eigentlichen Filtergraphen über die Methode GetFiltergraph() (wir erinnern uns: Das IDvdGraphBuilder-Interface ist nur ein Helfer!).
Übergeben wird dabei lediglich unsere passende Variable, da IDvdGraphBuilder bereits weiß, welchen Filtergraphen es verwendet.
Läuft hier etwas schief, wird die RaiseFailureMessage()-Methode aufgerufen. Beide Versionen davon werde ich hier nicht abdrucken, da sie lediglich eine MessageBox ausgeben, die ShutDown()-Methode aufrufen und false zurückgeben..

Die nächsten drei Interfaces werden alle mit dem gleichen Schema besorgt. (Achtung: Diesmal jedoch über den eigentlichen Filtergraphen!).
Dazu wird die QueryInterface()-Methode aufgerufen, der wieder der entsprechende Identifier und natürlich unser Interface-Objekt übergeben wird.
Schön, aber wofür bauchen wir die eigentlich?
Also das IMediaEventEx-Interface benötigen wir, damit wir u.a. unserer Anwendung mitteilen können, wohin die DVD-Event-Nachrichten eigentlich geschickt werden. Das sehen wir uns anschließend in der Init() Methode an.
Das IMediaControl-Interface wird dafür gebraucht, den kompletten Filtergraphen zu starten, sprich: Die DVD abspielen zu lassen, zu stoppen oder zu pausieren.
Mit dem IVideoWindow-Interface bekommen wir das eigentliche Videofenster (das ich schon öfter erwähnt habe), in dem die DVD abläuft. Tatsächlich haben wir es nämlich mit zwei Fenstern zu tun und wenn wir nicht entsprechende Vorkehrungen treffen würden (Init() Methode), dann würde tatsächlich ein zweites Fenster (ein ActiveMovie-Window) erscheinen. Das wollen wir natürlich nicht, und u.a. deswegen dieses Interface.

Mit der RenderDvdVideoVolume()-Methode (Achtung: Diesmal wieder über den DVDFiltergraphen!) besorgen wir uns ausnahmsweise kein Interface, sondern machen zunächst etwas ganz entscheidendes: Wir schließen den Aufbau des Filtergraphen ab! Tatsächlich hatten wir vorher einen unfertigen Graphen, der noch keine DVD abspielen kann. Mit dieser Methode werden sozusagen die Decoder-Filter des Graphen eingebaut und das Auslesen der DVD überhaupt erst ermöglicht. An dieser Stelle zeigt es sich dann auch, ob solche Filter schon vorhanden sind (z.B. durch WinDVD oder andere). Ist das nicht der Fall oder ist überhaupt keine DVD im Laufwerk eingelegt, wird eine entsprechende Meldung ausgegeben und der Einfachheit halber das Programm beendet. Hier sollte man in einer ordentlicheren Anwendung natürlich etwas anders vorgehen aber für unsere Zwecke reicht das locker.

Zu den Parametern:
Zunächst erwartet die Methode eine Pfadangabe - für das Standard-DVD-Laufwerk kann man aber NULL übergeben, was wir auch tun.
Parameter Nummer zwei gibt an, wie das Dekodieren der Daten von Statten gehen soll. Sinnvoller Weise sollte das natürlich die Hardware übernehmen, weswegen wir hier AM_DVD_HWDEC_PREFER übergeben. Für weitere Möglichkeiten einfach einen Blick ins DirectX® SDK werfen.
Als letzter Parameter muß eine AM_DVD_RENDERSTATUS-Instanz übergeben werden, mit der wir anschließend im Fehlerfall etwas genauer prüfen können, was schief gelaufen ist. Und hier springt die zweite Version der RaiseFailureMessage()-Methode ein, die im Gegensatz zur ersten Methode die Fehler, mitgeteilt in der AM_DVD_RENDERSTATUS-Instanz, speziell formatiert ausgibt. Da ist nichts besonderes dran und kann einfach im Quelltext nachvollzogen werden. Die Bedeutung der Komponenten der Struktur kann natürlich im DirectX® SDK nachgeschlagen werden.

Die letzten beiden Interfaces werden ganz äquivalent zu den oben verwendeten QueryInterface()-Methoden verwendet, nur das diese Methode hier GetDvdInterface() heißt.
Fein, und wofür brauchen wir diese nun?
OK, das IDvdInfo2-Interface brauchen wir, um die Titel-, Kapitel-, Audio- und Untertitelinformationen zu bekommen, sowie noch ein paar andere Sachen.
Das IDvdControl2-Interface ist für das Umschalten der Kapitel, Aktivierung der Untertitel und noch einiges mehr zuständig.
Definitiv also zwei Interfaces auf die wir auf keinen Fall verzichten wollen (können).

Für DirectShow® -Neulinge mag das alles ziemlich umständlich aussehen (teilweise ist es das sogar), doch eigentlich ist das, wie so vieles, einfach nur Gewöhnungssache. Aber halten wir uns nicht lange auf und sehen uns an, wofür diese Methode eigentlich implementiert haben. Aufgerufen wird sie in der Init()-Methode:

bool CDVDPlayer::Init()
{
    g_pDVD = this;

    if (!CreateBaseWindow()) return false;

    if (S_OK != ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED))
        return RaiseFailureMessage("CoInitializeEx failed!");

    if (!AcquireInterfaces()) return false;

    if (S_OK != m_pMediaEvent->SetNotifyWindow(reinterpret_cast<OAHWND>(m_hWnd), WM_GRAPHNOTIFY, 0))
        return RaiseFailureMessage("SetNotifyWindow failed!");

    if (S_OK != m_pDvdCtrl->SetOption(DVD_HMSF_TimeCodeEvents, TRUE))
        return RaiseFailureMessage("SetOption failed!");

    if (S_OK != m_pVideoWin->put_MessageDrain(reinterpret_cast<OAHWND>(m_hWnd)))
        return RaiseFailureMessage("put_MessageDrain failed!");

    if (S_OK != m_pVideoWin->put_Owner(reinterpret_cast<OAHWND>(m_hWnd)))
        return RaiseFailureMessage("put_Owner failed!");

    if (S_OK != m_pVideoWin->put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN))
        return RaiseFailureMessage("put_WindowStyle failed!");

    ReSize();
    CreateMenu();

    ::ShowWindow(m_hWnd, SW_SHOW);

    Play();

    return true;
};

Damit die globale DVDMsgProc() arbeiten kann übergeben wir hier unserem globalen Pointer den this-Zeiger.

Unmittelbar danach erstellen wir das Basisfenster. Im Positiv-Fall gehen wir gleich weiter zur CoInitializeEx()-Funktion, womit die Verwendung der COM-Funktion CoCreateInstance() erst ermöglicht wird.
Als erster Parameter muß hier NULL übergeben werden! Für den zweiten Parameter stehen vier Möglichkeiten zur Verfügung. Da unsere Anwendung nur einen Thread für sich beansprucht, ist die einzig richtige Wahl für uns COINIT_APARTMENTTHREADED.

Achtung:
Wie bei dieser Funktion gilt auch für alles andere hier: Werft bei näherem Interesse oder auch nur zur reinen Information einen Blick in das Windows SDK, bzw. in das DirectX® SDK!

Hat alles funktioniert, besorgen wir uns jetzt alle benötigten Interfaces mit der bereits implementierten AcquireInterfaces()-Methode.

Haben wir erfolgreich alles beschafft, müssen wir unserem Filtergraphen mitteilen, an welches Fenster er seine Nachrichten schicken soll. Das erreichen wir, indem wir dem IMediaEventEx-Interface (das wir ja über den Filtergraphen besorgt haben) über die Methode SetNotifyWindow()das Handle unseres Hauptfensters (Achtung: Muß nach OAHWND gecastet werden), sowie die von uns definierte Nachricht übergeben, die wir in unserer MessageProc später als ganz normale Nachricht behandeln können. Im dritten Parameter der Methode könnten wir noch einen Übergabe-Parameter mitgeben, der der Nachricht im LPARAM-Parameter mit angehängt werden würde. Allerdings ist das für unsere Zwecke nicht nötig und deswegen uninteressant.

Mit SetOption() können wir unserem Filtergraphen zusätzliche Anweisungen geben und mit dem angegebenen Parameter im Speziellen, eine Zeit-Nachricht zu senden, damit wir auch wissen, an welcher Stelle (zeitlich gesehen) wir uns auf der DVD befinden. Diese Aktion ist nicht essentiell aber eine Zeitanzeige gehört meines Erachtens einfach dazu. Über andere Parameter kann man z.B. auch angeben, daß der Sound beim Vor- und/oder Rückwärtsspulen mit ausgegeben wird, was standardmäßig nicht der Fall ist.

Jedes DVD-Menu hat normalerweise die Möglichkeit der Navigation, d.h. daß der User mit dem Cursor verschiedene Menupunkte auswählen und aktivieren kann. Damit das funktioniert, müssen wir der Anwendung aber mitteilen, wohin die Maus- und Keyboardaktionen geschickt werden sollen, denn die Navigation läuft natürlich im Video- und nicht in unserem Hauptfenster ab! Genau diese Funktion erreichen wir mit der Methode put_MessageDrain(), der einfach das Handle unseres Hauptfensters übergeben wird (wieder nach OAHWND gecastet!).

OK, jetzt hab ich ja schon etliche Male erwähnt, daß wir es mit zwei Fenstern zu tun haben. Damit wir aber nicht auch zwei verschiedene Fenster sehen, wird dem Videofenster über die Methode put_Owner() mitgeteilt, daß es zu unserem Hauptfenster gehört (wieder wird unser Window-Handle, nach OAHWND gecastet, übergeben).

Damit das Videofenster aber auch weiß, daß wir es bitte ohne Rand und Kopfzeile haben wollen, müssen wir ihm mit put_WindowStyle() noch sagen, daß es ein Child-Window ist! Das erreichen wir ganz einfach durch die Übergabe des WS_CHILD-Flags der WindowsAPI.

So, damit ist die eigentliche Initialisierung so gut wie abgeschlossen. Was jetzt noch fehlt, ist lediglich die Anpassung der Videofenstergröße an unser Hauptfenster, was wir mit der ReSize()-Methode erreichen:

void CDVDPlayer::ReSize() const
{
    if (!m_pVideoWin) return;

    RECT rect;
    ::GetClientRect(m_hWnd, &rect);
    m_pVideoWin->SetWindowPosition(0, 0, rect.right, rect.bottom);
};

Sollte das Videofenster aus irgendeinem Grund 0 sein, brechen wir gleich ab. Ansonsten besorgen wir uns einfach die Größe des Clientbereichs unseres Hauptfenster mit GetClientRect() und übergeben sie unserem Videofenster mit der SetWindowPosition()-Methode. Da das Videofenster bereits ein Child-Window unseres eigentlichen Fenster ist, ist die x- und y-Koordinate der linken oberen Ecke sowieso "0". GetClientRect() besorgt im übrigen eh nur die relativen Koordinaten des Clientbereichs eines Fensters, weshalb rect.left und rect.top ebenfalls immer "0" sind.

Damit ist die Konfiguration des Videofensters abgeschlossen und wir machen uns gleich an die Erstellung des Menus:

void CDVDPlayer::CreateMenu()
{
    m_hMenu            = ::CreatePopupMenu();
    m_hMenuChapters    = ::CreatePopupMenu();
    m_hMenuTitles      = ::CreatePopupMenu();
    m_hMenuSubpictures = ::CreatePopupMenu();
    m_hMenuAudios      = ::CreatePopupMenu();
    ::AppendMenu(m_hMenu, MF_STRING,    ID_Play,                                        "Wiedergabe");
    ::AppendMenu(m_hMenu, MF_STRING,    ID_Stop,                                        "Stop");
    ::AppendMenu(m_hMenu, MF_STRING,    ID_Pause,                                       "Pause");
    ::AppendMenu(m_hMenu, MF_STRING,    ID_SubPicAct,                                   "Untertitel aktivieren");
    ::AppendMenu(m_hMenu, MF_SEPARATOR, 0,                                              0);
    ::AppendMenu(m_hMenu, MF_STRING,    ID_Menu,                                        "Menu");
    ::AppendMenu(m_hMenu, MF_STRING,    ID_Chapter_Prev,                                "Vorheriges Kapitel");
    ::AppendMenu(m_hMenu, MF_STRING,    ID_Chapter_Next,                                "Nächstes Kapitel");
    ::AppendMenu(m_hMenu, MF_POPUP,     reinterpret_cast<UINT_PTR>(m_hMenuChapters),    "Kapitel");
    ::AppendMenu(m_hMenu, MF_POPUP,     reinterpret_cast<UINT_PTR>(m_hMenuTitles),      "Titel");
    ::AppendMenu(m_hMenu, MF_POPUP,     reinterpret_cast<UINT_PTR>(m_hMenuSubpictures), "Untertitel");
    ::AppendMenu(m_hMenu, MF_POPUP,     reinterpret_cast<UINT_PTR>(m_hMenuAudios),      "Sprache");
    ::AppendMenu(m_hMenu, MF_SEPARATOR, 0,                                              0);
    ::AppendMenu(m_hMenu, MF_STRING,    ID_Resume,                                      "Resume");
};

Da gibt es nicht viel zu sagen. Zuerst erstellen wir das eigentliche Menu mit der CreatePopupMenu()-Funktion. Ebenso erstellen wir auch gleich die Submenus. Anschließend füllen wir das Hauptmenu mit den Items auf, die wir eben haben wollen. Die Handles der Untermenus werden dabei nach UINT_PTR gecastet übergeben. Das wars dann auch schon wieder fürs erste. Wie die Untermenus gefüllt und das eigentliche Menu auf den Bildschirm gebracht wird, werden wir später sehen.

Jetzt sind wir endgültig fertig mit der Initialisierung und weil alles was irgendwann initialisiert worden ist auch wieder freigegeben werden muß, hier gleich die ShutDown()-Methode:

void CDVDPlayer::ShutDown()
{
    Play(false);
    if(m_bFullscreen) ToggleScreenMode();
    if(m_pVideoWin)
    {
        m_pVideoWin->put_Visible(OAFALSE);
        m_pVideoWin->put_Owner(0);
    };

    ::DestroyMenu(m_hMenu);

    SaveRelease(m_pVideoWin);
    SaveRelease(m_pMediaCtrl);
    SaveRelease(m_pDvdCtrl);
    SaveRelease(m_pDvdInfo);
    SaveRelease(m_pMediaEvent);
    SaveRelease(m_pGraphB);
    SaveRelease(m_pDvdGraphB);

    ::CoUninitialize();
};

Sollte eigentlich auch verständlich sein:

  • Läuft die DVD noch, dann beenden wir das Abspielen.
  • Befinden wir uns im Fullscreen-Modus, wechseln wir natürlich noch in den Fenstermodus.
  • Falls das Videofenster existiert (wir also nicht wegen eines Fehlers abbrechen), machen wir es unsichtbar und entfernen mit put_Owner(0) die Bindung an das Besitzerfenster.
  • Über DestroyMenu() löschen wir das Popup-Menu und somit auch alles Untermenus, da die Funktion rekursiv arbeitet und automatisch weiß, ob es noch Submenus hat.
  • Alle besorgten Interfaces werden mit unserem SaveRelease()-Makro freigegeben.
  • Schlußendlich teilen wir Windows mit CoUninitialize() noch mit, daß wir keine COM-Funktionen mehr brauchen und es somit alle diesbezüglich geladenen Libraries wieder aus dem Speicher entfernen kann.

Aha, da hatten wir eben noch die ToggleScreenMode()-Methode, die wir noch nicht besprochen hatten:

bool CDVDPlayer::ToggleScreenMode()
{
    if (!m_pVideoWin) return false;

    static LONG lOrigStyle, lOrigStyleEx;

    if (!m_bFullscreen)
    {
        m_pVideoWin->get_WindowStyle(&lOrigStyle);
        m_pVideoWin->get_WindowStyleEx(&lOrigStyleEx);
        m_pVideoWin->put_WindowStyle(lOrigStyle & ~(WS_BORDER | WS_CAPTION | WS_THICKFRAME));
        m_pVideoWin->put_WindowStyleEx(lOrigStyleEx & ~(WS_EX_CLIENTEDGE |
                                                        WS_EX_STATICEDGE |
                                                        WS_EX_WINDOWEDGE |
                                                        WS_EX_DLGMODALFRAME) | WS_EX_TOPMOST);
        m_pVideoWin->put_Owner(0);
        m_pVideoWin->SetWindowPosition(0,
                                       0,
                                       ::GetSystemMetrics(SM_CXSCREEN),
                                       ::GetSystemMetrics(SM_CYSCREEN));
        m_bFullscreen = true;
        ::SetTimer(m_hWnd, 1, 3000, 0);
    }
    else
    {
        RECT rct;
        ::GetClientRect(m_hWnd, &rct);
        m_pVideoWin->put_Owner(reinterpret_cast<OAHWND>(m_hWnd));
        m_pVideoWin->SetWindowPosition(0, 0, rct.right, rct.bottom);
        m_pVideoWin->put_WindowStyle(lOrigStyle);
        m_pVideoWin->put_WindowStyleEx(lOrigStyleEx);
        ::ShowCursor(TRUE);
        m_bFullscreen = false;
    };
    return true;
};

Das erste, was man dazu sagen sollte ist, daß es zwei Möglichkeiten gibt, um in den Vollbildmodus zu schalten. Die eine und auch einfachere Möglichkeit ist die Verwendung der put_FullScreenMode()-Methode des IVideoWindow-Interfaces und die andere (sowohl umständlicher als auch etwas langwierigere) Methode ist die hier vorgestellte. Und das aus gutem Grund: Im ersten Fall wird der Cursor automatisch ausgeblendet und ward nicht mehr gesehen. Das macht die Navigation der Menus ziemlich umständlich... auch wenn es noch funktioniert. Besser ist es, sich selber um das Erscheinen und Verschwinden des Cursors zu kümmern, was aber nur auf diesem Weg möglich ist.

Zunächst brauchen wir zwei statische Variablen um die Fenstereigenschaften zu sichern.
Befindet sich das Fenster noch nicht im Fullscreen-Modus besorgen wir uns mit get_WindowStyle() und get_WindowStyleEx() die entsprechenden Eigenschaften, um sie beim Zurückschalten in den Fenstermodus wieder herstellen zu können. Mit put_WindowStyle() und put_WindowStyleEx() setzen wir den neuen Stil so, daß wir weder Ränder, Titelleiste noch sonstigen Schnickschnack mehr haben. Im "Ex"-Fall geben wir mit WS_EX_TOPMOST zusätzlich noch an, daß unser Fenster jetzt vor allen anderen ganz oben stehen soll.
Außerdem ist es wichtig dem Videofenster (Achtung: Wir vergrößern nur dieses, und NICHT unser Hauptfenster) noch mit put_Owner(0) mitzuteilen, daß es kein Child-Window mehr ist, sondern sein eigener Herr.
Jetzt können wir das Videofenster mit der bereits bekannten SetWindowPosition()-Methode auf unsere Bildschirmgröße aufblasen.
Nachdem wir unsere bFullscreen-Variable auf true gesetzt haben, stellen wir noch einen Timer auf 3 Sekunden, da der Cursor ja nach wie vor sichtbar ist. Wie wir die ausgelöste WM_TIMER-Nachricht handhaben, sehen wir uns gleich im Anschluß an.

Im umgekehrten Fall, also wenn wir vom Fullscreen- in den Fenstermodus wechseln, besorgen wir uns die Maße unseres Hauptfenster, sagen dem Videofenster, daß es wieder zu unserem Hauptfenster gehört und restaurieren die Fenstereigenschaften.
Nachdem wir den Cursor wieder sichtbar gemacht haben, setzen wir m_bFullscreen natürlich noch auf false.

Achtung: Ein Aufruf der ShowCursor()-Funktion reicht aus! Warum, das sehen wir jetzt gleich in der MsgProc()-Methode:

LRESULT CDVDPlayer::MsgProc(HWND p_hWnd, UINT p_uiMsg, WPARAM p_wParam, LPARAM p_lParam)
{
    switch(p_uiMsg)
    {
        case WM_SIZE:          if (!m_bFullscreen) ReSize(); return 0;
        case WM_CLOSE:         ::PostQuitMessage(0); return 0;
        case WM_KEYDOWN:
        case WM_KEYUP:         OnKeyboardEvent(p_uiMsg, p_wParam); return 0;
        case WM_COMMAND:       OnUserCommand(p_wParam); return 0;
        case WM_SYSCOMMAND:    switch(p_wParam)
                               {
                                   case SC_CLOSE:        ::PostQuitMessage(0); return 0;
                                   case SC_SCREENSAVE:
                                   case SC_MONITORPOWER: return 0;
                               };
                               break;

        case WM_TIMER:         ::KillTimer(m_hWnd, 1);
                               if (m_bFullscreen) while (::ShowCursor(FALSE) > -1);
                               break;

        case WM_MOUSEMOVE:
        case WM_LBUTTONUP:
        case WM_RBUTTONUP:     OnMouseEvent(p_uiMsg, p_lParam); return 0;
        case WM_ENTERMENULOOP: ::KillTimer(m_hWnd, 1); ::ShowCursor(TRUE); return 0;
        case WM_EXITMENULOOP:  ::SetTimer(m_hWnd, 1, 1000, 0); return 0;
        case WM_GRAPHNOTIFY:   OnDVDEvent(); return 0;
    };

    return ::DefWindowProc(p_hWnd, p_uiMsg, p_wParam, p_lParam);
};

Ganz klar - auf eine WM_SIZE-Nachricht reagieren wir natürlich mit unserer ReSize()-Methode.
WM_CLOSE ist ebenso klar.
Falls der User im Fenstermodus das kleine "X" anklickt erwartet er, daß unsere Anwendung beendet wird und genau das wird im WM_SYSCOMMAND-Fall mit der SC_CLOSE-Nachricht abgefangen. Sollte sich ein Screensaver oder eine Stromsparfunktion einschalten, fangen wir das einfach nichtstuend ab.
Befinden wir uns im Vollbildmodus und bewegen die Maus 3 Sekunden nicht mehr, kommt schließlich die Timer-Nachricht an, in der wir den Timer erst löschen und anschließend solange ShowCursor(FALSE) aufrufen, bis der Rückgabewert kleiner 0 ist. Das müssen wir machen, weil in der OnMouseEvent()-Methode jedesmal im Vollbildmodus, wenn wir die Maus bewegen ShowCursor(TRUE) aufgerufen wird. Wem dieses Vorgehen jetzt nicht klar ist, sollte ganz dringend mal im Windows SDK nachschauen, wie diese Funktion eigentlich arbeitet!!!
Dann haben wir außerdem noch WM_ENTERMENULOOP und WM_EXITMENULOOP. Hier wird dafür gesorgt, daß der Timer beim Betreten der Menuschleife (jedes Menu ist ja wieder ein eigenes Fenster und hat außerdem eine eigene Nachrichtenschleife, um die wir uns aber nicht kümmern müssen) zerstört wird. Einfach aus dem Grund, daß beim Navigieren im Popup-Menu der Mauszeiger nicht verschwindet. Genau das würde aber sonst passieren, da das Menu ein eigenes Fenster ist und die Mousemove-Nachrichten nicht mehr an unser Hauptfenster weitergeleitet würden und der Timer somit schließlich auslaufen würde. Zusätzlich machen wir den Cursor sicherheitshalber auch noch sichtbar. Bewegt man nämlich die Maus vorher nicht und betätigt die rechte Maustaste, würde der Mauszeiger natürlich nach wie vor verdeckt bleiben.
Beim Verlassen der Menuschleife ziehen wir einfach einen neuen Timer (etwas kürzer) auf und das wars.
Alle anderen Nachrichten geben wir unbearbeitet an unsere entsprechenden Methoden weiter. Hier sehen wir, daß auch unsere eigens definierte WM_GRAPHNOTIFY-Nachricht ganz normal behandelt wird.

Bevor wir uns also an alle On...()-Methoden machen sehen wir uns noch schnell die Play()-Methode an, die ja bereits am Ende der Initialisierung aufgerufen wird:

void CDVDPlayer::Play(bool p_bPlay)
{
    if (p_bPlay)
    {
        if (m_bRunning) return;

        m_pVideoWin->put_Visible(OATRUE);
        m_pMediaCtrl->Run();
        m_bRunning = true;

    }
    else
    {
        if (!m_bRunning) return;

        m_pMediaCtrl->Stop();
        m_pVideoWin->put_Visible(OAFALSE);
        m_bRunning = false;
    };
};

Zuerst überprüfen wir mit dem Parameter p_bPlay, ob wir die DVD starten oder stoppen wollen. Läuft die DVD schon (m_bRunning), dann brechen wir gleich ab. Anderenfalls machen wir das Videofenster mit put_Visible(OATRUE) sichtbar. Das wäre zwar nicht nötig, da das Fenster standardmäßig sichtbar ist aber am Ende der Methode (im Stop-Fall) übergeben wir hier OAFALSE, damit beim Stoppen der DVD das Fenster unsichtbar wird und der schwarzen Hintergrund zu sehen ist. Ansonsten würde nämlich das letzte Bild angezeigt, was zwar nicht schlimm aber auch nicht ganz richtig wäre. Wird eine DVD mehrfach gestartet und gestoppt, muß hier put_Visible(OATRUE) gesetzt werden.
Mit der Run()-Methode des IMediaControl-Interfaces starten wir bequem den kompletten Filtergraphen (und somit die DVD) und setzen unsere m_bRunning-Variable auf true.

Im Stop-Fall überprüfen wir zuerst, ob die DVD überhaupt läuft. Falls ja, halten wir den Filterraphen mit der Stop()-Methode an, machen das Videofenster unsichtbar und setzen m_bRunning auf false.

Kommen wir jetzt zu den On...()-Methoden und beginnen zuerst mit denen, die wir vorher in der MsgProc() gesehen haben, allen voran OnMouseEvent():

void CDVDPlayer::OnMouseEvent(const UINT p_iMsg, const LPARAM p_lParam) const
{
    POINT pt;
    pt.x = GET_X_LPARAM(p_lParam);
    pt.y = GET_Y_LPARAM(p_lParam);

    switch(p_iMsg)
    {
    case WM_MOUSEMOVE:  m_pDvdCtrl->SelectAtPosition(pt);
                        if (m_bFullscreen)
                        {
                            ::ShowCursor(TRUE);
                            ::SetTimer(m_hWnd, 1, 3000, 0);
                        };
                        break;

    case WM_LBUTTONUP:  m_pDvdCtrl->ActivateAtPosition(pt); break;
    case WM_RBUTTONUP:  ::GetCursorPos(&pt);
                        ::TrackPopupMenu(m_hMenu,
                                         TPM_LEFTALIGN | TPM_TOPALIGN,
                                         pt.x,
                                         pt.y,
                                         0,
                                         m_hWnd,
                                         
0);
                        break;
    };
};

Hm, da passiert wirklich nicht viel.
Zuerst holen wir uns die Mauskoordinaten im Client-Space.
Wird die Maus bewegt, soll im Menu der entsprechende Punkt angewählt werden. Das geschieht supersimpel durch die SelectAtPosition()-Methode des IDvdControl2-Interfaces. Befinden wir uns im Fullscreen-Modus machen wir natürlich noch den Mauszeiger sichtbar und ziehen jedesmal den Timer neu auf. Unser Timer mit der Nummer "1" wird dabei jedesmal automatisch ersetzt, wenn er noch nicht abgelaufen ist.
Angenommen ein Menupunkt ist ausgewählt, dann soll bei einem Linksklick natürlich auch etwas passieren und genau das regelt die ActivateAtPosition()-Methode des IDvdControl2-Interfaces. Einfacher gehts wirklich nicht mehr!
Mit der TrackPopupMenu()-Funktion wird ganz einfach bei Rechtsklick das Popup-Menu angezeigt. Mit den beiden Flags TPM_LEFTALIGN und TPM_TOPALIGN sorgen wir dafür, daß der linke obere Rand des Menus an der Position der Mauszeigerspitze erscheint. Hier müssen wir allerdings zuerst mit GetCursorPos() die absoluten Mauskoordinaten besorgen, da TrackPopupMenu() von absoluten Koordinaten ausgeht.

Anstatt an dieser Stelle die komplette OnKeyboardEvent()-Methode zu zeigen, picke ich lediglich die interessanten Teile heraus. Die ganze Methode ist natürlich nicht notwendig aber genau hier kann man sich als Programmierer so richtig auslassen und nette Gimicks einbauen. Ich habe mich für folgende Tastendruckaktionen entschieden:

  • Tab:
    Wechseln zwischen Vollbild- und Fenstermodus.
  • Escape:
    Beenden der Anwendung.
  • Pfeil rechts halten:
    Vorspulen der Anwendung mit der PlayForwards(8.0, dwFlags, 0)-Methode des IDvdControl2-Interfaces. Im ersten Parameter wird die Geschwindigkeit als double übergeben, wobei 1.0 für normale, 2.0 für doppelte Geschwindigkeit, usw. steht. Im zweiten Parameter gibt man ein oder mehrere Synchronisations-Flags mit an - ich verwende hier DVD_CMD_FLAG_Block | DVD_CMD_FLAG_Flush. Wer genauer wissen möchte, was diese bewirken und welche Möglichkeiten es sonst noch gibt, soll bitte das DirectX® SDK konsultieren. Im dritten Parameter kann man einen Funktionspointer auf eine Synchronisationsfunktion mitgeben. Ich hoffe ihr verzeiht mir (nochmal), wenn ich auch hier einfach auf das SDK verweise. Für unsere Zwecke hier reicht ohnehin NULL aus. ;)
  • Pfeil links halten:
    Zurückspulen mit der entsprechenden PlayBackwards()-Methode.
  • Pfeil rechts loslassen:
    Zurückschalten auf normale Abspielgeschwindigkeit mit derselben Methode (1.0 als erster Parameter).
  • Pfeil links loslassen:
    Wie eben.
  • Pfeil hoch:
    Zum nächsten Kapitel mit PlayNextChapter(dwFlags, 0) springen. Natürlich wieder mit dem IDvdControl2-Interface ausgeführt. Parameter 1 und 2 entsprechen den Parametern 2 und 3 der eben vorgestellten Methoden.
  • Pfeil runter:
    Ich klatsch' euch jetzt ganz frech PlayPrevChapter(dwFlags, 0) hin und weiß, daß ihr sofort wisst, was Sache ist.
  • Space:
    Pausieren. Mit der Pause()-Methode wird pausiert und mit der bereits bekannten Run()-Methode wieder weitergespielt. Außerdem wird dabei im Popup-Menu noch entsprechend ein Häckchen gesetzt bzw. entfernt. Das Ganze wird allerdings in TogglePause() gehandhabt.
void CDVDPlayer::TogglePause()
{
    m_bPaused = !m_bPaused;
    m_bPaused ? m_pMediaCtrl->Pause() : m_pMediaCtrl->Run();
    ::CheckMenuItem(m_hMenu, ID_Pause, m_bPaused ? MF_CHECKED : MF_UNCHECKED);
};

Hier ist natürlich noch viel machbar und es bleibt jedem selbst überlassen, wie er seinen DVD-Player steuern will.

Eine der ganz entscheidenden Funktionen sehen wir uns jetzt an:

void CDVDPlayer::OnUserCommand(const WPARAM p_wParam)
{
    const DWORD dwFlags = DVD_CMD_FLAG_Block | DVD_CMD_FLAG_Flush;

    switch(p_wParam)
    {
    case ID_Play:     Play(); break;
    case ID_Stop:     Play(false); break;
    case ID_Pause:    TogglePause(); break;
    case ID_SubPicAct:     
        m_bSubpicture = !m_bSubpicture;
        m_pDvdCtrl->SetSubpictureState(m_bSubpicture, dwFlags, 0);
        ::CheckMenuItem(m_hMenu, ID_SubPicAct, m_bSubpicture ? MF_CHECKED : MF_UNCHECKED);
        break;

    case ID_Menu:         m_pDvdCtrl->ShowMenu(DVD_MENU_Root, dwFlags, 0); break;
    case ID_Chapter_Prev: m_pDvdCtrl->PlayPrevChapter(dwFlags, 0); break;
    case ID_Chapter_Next: m_pDvdCtrl->PlayNextChapter(dwFlags, 0); break;
    case ID_Resume:       m_pDvdCtrl->Resume(dwFlags, 0); break;
    default:
        if (p_wParam >= ID_Chapter + 1 && p_wParam < ID_Title)
        {
            m_pDvdCtrl->PlayChapter(static_cast<int>(p_wParam) - ID_Chapter, dwFlags, 0);
        }
        else if (p_wParam >= ID_Title + 1 && p_wParam < ID_SubPic)
        {
            m_pDvdCtrl->PlayTitle(static_cast<int>(p_wParam) - ID_Title, dwFlags, 0);
        }
        else if (p_wParam >= ID_SubPic + 1 && p_wParam < ID_Audio)
        {
            m_pDvdCtrl->SelectSubpictureStream(static_cast<int>(p_wParam) - ID_SubPic - 1, dwFlags, 0);
        }
        else if (p_wParam >= ID_Audio + 1 && p_wParam < ID_MAXIDS)
        {
            m_pDvdCtrl->SelectAudioStream(static_cast<int>(p_wParam) - ID_Audio - 1, dwFlags, 0);
        };
    };
};

Wie ich schon am Anfang erwähnte wird OnUserCommand() immer dann aufgerufen, wenn der User im Popup-Menu einen der Menupunkte anklickt (wird ja über WM_COMMAND an diese Methode weitergeleitet).
Gleich am Anfang definiere ich wieder diese ominösen Flags, die hier alle Methoden des IDvdControl2-Interface nunmal brauchen.
Das Starten, Stoppen und Pausieren kennen wir ja mittlerweile schon und rufen einfach die entsprechenden Methoden auf.
Das Aktivieren der Untertitel sieht vom Ablauf ganz ähnlich der TogglePause()-Methode aus. Die eigentliche Aktivierung (oder eben Deaktivierung) läuft wieder über das Controlinterface über die Methode SetSubpictureState(). Erster Parameter ist dabei true oder false und Parameter zwei und drei wieder die, die wir vorher schon hatten. Entsprechend wird natürlich auch das richtige Häckchen im Menu gesetzt oder eben entfernt.

Manche DVDs erlauben das Umschalten der Audio- und/oder Untertitelspur nicht über das Popup-Menu, deswegen wechselt man hier in das DVD-Stammmenu und stellt alles entsprechend ein. Anschließend kann man mit einem Klick auf "Weiter" oder "Resume" im Popup-Menu den Film von der Stelle weiterschauen, wo man gerade unterbrochen hat. Genau dafür ist der Punkt ID_Menu gedacht. Erreicht wird das über die Methode ShowMenu(), der man, um das Stammmenu aufzurufen, DVD_MENU_Root übergibt. Das habe ich hier zur Vereinfachung gemacht, da dieses Tutorial wirklich lang genug ist. Normalerweise sollte man ein extra Untermenu im Popup-Menu haben und dort die verschiedenen Menuarten (Kapitel-, Audio-, Untertitelmenu, ...) dem User zur Verfügung stellen. Entsprechend wird dann der erste Parameter übergeben. Parameter zwei und drei sind wieder die üblichen (wie auch in den noch folgenden Methoden).
Entsprechend gibt es dann noch die Methode Resume(), die eben wieder zum Film zurückkehrt.

Daß das nächste, bzw. vorhergehende Kapitel auch hier wählbar sein sollte vernachlässigen wir natürlich nicht. Die Methoden dazu kennen wir eh schon.

Und der default-Fall unserer switch()-Anweisung ist jetzt der Clou: Alles was noch nicht behandelt worden ist, muß ein Element der Untermenus sein. Um herauszufinden, zu welchem Untermenu der Eintrag gehört, prüfen wir einfach dessen Wert. Warum das so einfach geht, werden wir nachher in der OnTitleSelect()-Methode sehen, wo wir die Einträge intelligent genug (ausgehend vom Wert des Eintrages im darüberliegenden Popup-Menu) erstellen. Der exakte Eintrag läßt sich dann einfach dadurch bestimmen, daß man von ihm den Wert des SubMenu-Eintrages abzieht.
Das klingt alles wesentlich komplizierter als es ist. Seht euch einfach später in Ruhe beide Methoden an und ihr werdet sehr schnell sehen, daß das wirklich ganz banal ist.
Ein kleiner Stolperstein ist aber noch zu klären: In den Methoden SelectSubpictureStream() und SelectAudioStream() ziehe ich zusätzlich noch eine "1" ab. Das hat einfach den Grund, daß die Untertitel- und Audioinformationen von "0" weg, und die Titel- und Kapitelinformationen von "1" weg durchnummeriert sind.

Und jetzt wirds interessant! In der nächsten Methode behandeln wir die Nachrichten, die von unserem DVD-Laufwerk verschickt werden (der Satz stimmt zwar so nicht ganz, aber ich laß das jetzt einfach mal stehen). Am besten sehen wir uns erstmal nur den Kopf der Methode an und machen anschließend den Rest:

void CDVDPlayer::OnDVDEvent()
{
    if (!m_pMediaEvent) return;

    static DVD_DOMAIN domain;
    char ac[MAX_PATH];
    LONG lEvent, lParam1, lParam2;

    while(SUCCEEDED(m_pMediaEvent->GetEvent(&lEvent, reinterpret_cast<LONG_PTR*>(&lParam1), reinterpret_cast<LONG_PTR*>(&lParam2), 0)))
    {
        [...]

        m_pMediaEvent->FreeEventParams(lEvent, lParam1, lParam2);
    };
};

Zu allererst überprüfen wir, ob wir überhaupt noch das IMediaEventEx-Interface (aha, hier brauchen wir es also) haben. Durch die GetMessage()-Funktion in unserer Hauptschleife (in der WinMain()) kann es nämlich durchaus sein, das nach dem Release aller Interfaces noch eine DVD-Nachricht daherkommt, da GetMessage() ja keine Nachrichten verliert!
Nun werden erst mal ein paar Variablen eingeführt. Allen voran eine statische Variable des merkwürdigen enum-Typs DVD_DOMAIN. Was es damit auf sich hat, sehen wir uns gleich im Rest der Methode an.
Das char-Array benötigen wir zum Einen für das Auffüllen des Titel-Submenus und zum Anderen für das Anzeigen der Zeit in der Titelleiste, was wir uns im zweiten Teil anschauen werden.
Die drei folgenden Varablen brauchen wir für die GetEvent()-Methode des IMediaEventEx-Interfaces. lEvent enthält dabei den so genannten Event-Code, also letztendlich nichts anderes als die Nachricht selbst, und die beiden lParam Variablen beschreiben zugehörige Parameter.

Die eigentliche Nachrichtenabfage geschieht ganz einfach solange in einer while-Schleife, wie die GetEvent()-Methode nichts kleineres als "0" zurückgibt, was sich durch das DirectX®-Standard-Makro SUCCEEDED() bequem überprüfen läßt. Das SDK weißt bei der Verwendung der Methode deutlich darauf hin, daß nach dem Abarbeiten aller Nachrichten in der Event-Abfrage evtl. allokierter Speicher mit der Methode FreeEventParams() wieder freigegeben werden muß (sollte), was wir natürlich am Ende der Schleife auch machen.

Damit kommen wir zum zweiten Teil (bei [...] eingefügt), wo wir die Nachrichten auswerten:

        switch(lEvent)
        {
        case EC_DVD_DOMAIN_CHANGE:
            switch (lParam1)
            {
            case DVD_DOMAIN_FirstPlay:
                ULONG ulTmp;
                DVD_DISC_SIDE dvdDiscSide;
                m_pDvdInfo->GetDVDVolumeInfo(&ulTmp, &ulTmp, &dvdDiscSide, &m_ulNumTitles);

                ClearMenu(m_hMenuTitles);

                for (ULONG i=1; i<=m_ulNumTitles; ++i)
                {
                    sprintf(ac, "Titel %i", i);
                    ::AppendMenu(m_hMenuTitles, MF_STRING, ID_Title + i, ac);
                };
                break;
            };

            domain = static_cast<DVD_DOMAIN>(lParam1);
            break;

        case EC_DVD_TITLE_CHANGE:             OnTitleSelect(lParam1); break;
        case EC_DVD_CHAPTER_START:            OnChapterSelect(lParam1); break;
        case EC_DVD_AUDIO_STREAM_CHANGE:      OnAudioSelect(lParam1); break;
        case EC_DVD_SUBPICTURE_STREAM_CHANGE: OnSubpictureSelect(lParam1); break;
        case EC_DVD_CURRENT_HMSF_TIME:
            
if (domain == DVD_DOMAIN_Title)
            {
                DVD_HMSF_TIMECODE time;
                time = *reinterpret_cast<DVD_HMSF_TIMECODE*>(&lParam1);
                sprintf(ac, "DVD-Player - Titel: %i | Kapitel: %i | Position: [%02i:%02i:%02i]",
                        m_ulCurTitle, m_ulCurChapter, time.bHours, time.bMinutes, time.bSeconds);
                ::SetWindowText(m_hWnd, ac);

                break;
            };

            ::SetWindowText(m_hWnd, "DVD-Player");
            break;
        };

Ich fang einfach mal von unten an:
In der EC_DVD_CURRENT_HMSF_TIME-Nachricht finden wir im lParam1-Parameter die Zeit in Form der DVD_HMSF_TIMECODE-Struktur.
Zwar benötigen wir diese lediglich zur Information aber es gehört doch einfach dazu, daß wenigsten während des Films die aktuelle Zeit angezeigt wird (im Fenstermodus). Der Einfachheit halber machen wir das auch nur in der Titelleiste.
OK, was ich noch nicht angesprochen habe ist, daß wir das alles nur machen, wenn wir uns in einer Titel-Domäne befinden, was wir mit der domain-Variable überprüfen. Eine Domäne ist nichts anderes als eine Bezeichnung dafür, wo man sich gerade auf der DVD befindet. In Domänen gesprochen bedeutet das entweder ganz am Anfang der DVD, in einem der Menus, in einem bestimmten Titel (eigentlich die tatsächlichen Film-Kapitel) oder im Stop Modus (Blick ins SDK!). Dazu werde ich gleich noch was sagen.

Zuerst werfen wir ganz schnell einen Blick auf die Nachrichten, die ankommen, wenn der Titel, das Kapitel, die Untertitel oder die Audiospur umgeschaltet wurde. Diese werden lediglich an die entsprechenden On...()-Methoden weitergeleitet, die wir uns im Anschluß ansehen werden.

Achtung: Um es nochmal deutlich zu machen: Die eben genannten Events sind nur die Reaktionen auf die OnUserCommand()-Aktionen. Deswegen wird in den entsprechenden Methoden (die Ausnahme ist die OnTitleSelect()-Methode) nicht mehr passieren als die Häckchen in den entsprechenden Untermenus zu setzen.

Und jetzt sehen wir uns die eigentliche Nachricht für Domänen Angelegenheiten an - EC_DVD_DOMAIN_CHANGE:
Wie ich vorher schon gesagt habe, gibt es mehrere Domänen. Mit welcher man es gerade zu tun hat, steht im ersten Event-Parameter, wenn sich die Domäne ändert!. Anstatt das mit einer if-Abfrage zu erfahren, habe ich zur Verdeutlichung eine switch/case-Anweisung hier platziert. Zur Vereinfachung werden wir hier aber nur die DVD_DOMAIN_FirstPlay-Domäne behandeln, in der man sich beim ersten Starten der DVD befindet. Hier sehen wir auch endlich, wie eigentlich das Titel-Untermenu gefüllt wird. Dazu befragen wir erstmal das IDvdInfo2-Interface mit der GetDVDVolumeInfo()-Methode um die relevanten Informationen zu bekommen. Dazu gehört im ersten und zweiten Parameter die Anzahl der DVDs und auf welcher man sich gerade befindet (für mehrfach-Laufwerke). Das interessiert uns aber nicht und wir übergeben bloß Dummy-Variablen (man kann leider nicht NULL übergeben). Da man im dritten Parameter auch nicht NULL übergeben kann, setzen wir hier ebenfalls eine Dummy-Variable, diesmal jedoch des enum-Typs DVD_DISC_SIDE an. Es dürfte klar sein, daß es hier nur zwei Seiten gibt... ;)
Haben wir im letzten Parameter die Anzahl der Titel endlich bekommen (die Anzahl der Titel ändert sich nicht mehr), können wir das entsprechende Untermenu auffüllen (nachdem wir es mit ClearMenu() geleert haben). Und da sehen wir den ganzen Trick an der Sache: Wir konstruieren den String einfach aus dem Namen "Titel" und der entsprechenden Zahl und hängen den Menupunkt ganz simpel addiert im Submenu an. Das wars auch schon.

Zum Schluß speichern wir die aktuelle Domain noch in der statischen Variable (um in der EC_DVD_CURRENT_HMSF_TIME-Nachricht zu wissen, wo wir uns gerade befinden).

Und damit sind wir endlich bei der letzten größeren Methode:

void CDVDPlayer::OnTitleSelect(const int p_iTitle)
{
    ULONG i;
    char ac[96];
    LCID LanguageID;

    m_ulCurTitle = p_iTitle;

    for (i=1; i<=m_ulNumTitles; ++i)
    {
        ::CheckMenuItem(m_hMenuTitles, i-1, MF_BYPOSITION | (m_ulCurTitle == i ? MF_CHECKED : MF_UNCHECKED));
    };

    // chapter update -------------------------------------------------
    ClearMenu(m_hMenuChapters);
    m_pDvdInfo->GetNumberOfChapters(m_ulCurTitle, &m_ulNumChapters);

    for (i=1; i<=m_ulNumChapters; ++i)
    {
        sprintf(ac, "Kapitel %i", i);
        ::AppendMenu(m_hMenuChapters, MF_STRING, ID_Chapter + i, ac);
    };

    // subpicture update --------------------------------------------
    ClearMenu(m_hMenuSubpictures);
    BOOL bSubPicDisabled;
    m_pDvdInfo->GetCurrentSubpicture(&m_ulNumSubpictures, &m_ulCurSubpicture, &bSubPicDisabled);
    m_bSubpicture = (bool)!bSubPicDisabled;
    m_pDvdCtrl->SetSubpictureState(m_bSubpicture, DVD_CMD_FLAG_Block, 0);
    ::CheckMenuItem(m_hMenu, ID_SubPicAct, m_bSubpicture ? MF_CHECKED : MF_UNCHECKED);

    for (i=0; i<m_ulNumSubpictures; ++i)
    {
        m_pDvdInfo->GetSubpictureLanguage(i, &LanguageID);
        if (!LanguageID) continue;
        ::GetLocaleInfo(LanguageID, LOCALE_SLANGUAGE, ac, sizeof(ac));
        ::AppendMenu(m_hMenuSubpictures, MF_STRING, ID_SubPic + i + 1, ac);
    };
    ::CheckMenuItem(m_hMenuSubpictures, m_ulCurSubpicture, MF_BYPOSITION | MF_CHECKED);

    // audio update -------------------------------------------------
    ClearMenu(m_hMenuAudios);
    m_pDvdInfo->GetCurrentAudio(&m_ulNumAudios, &m_ulCurAudio);

    for (i=0; i<m_ulNumAudios; ++i)
    {

        m_pDvdInfo->GetAudioLanguage(i, &LanguageID);
        if (!LanguageID) continue;
        ::GetLocaleInfo(LanguageID, LOCALE_SLANGUAGE, ac, sizeof(ac));
        ::AppendMenu(m_hMenuAudios, MF_STRING, ID_Audio + i + 1, ac);
    };
    ::CheckMenuItem(m_hMenuAudios, m_ulCurAudio, MF_BYPOSITION | MF_CHECKED);
};

Laßt euch nicht erschrecken! Das sieht zwar viel aus, ist aber letztendlich immer nur dasselbe.
Nachdem wir eine Laufvariable, ein Array in das wir die Bezeichnungen schreiben und noch eine Sprachen-ID (wir werden gleich sehen für was wir das brauchen) definiert haben, halten wir in m_ulCurTitle erstmal fest, welcher Titel gerade läuft. Anschließend haken wir den entsprechenden Eintrag ab. Achtung: Wir beginnen bei Titeln und Kapiteln immer mit 1!

Nachdem wir das Kapitelmenu geleert haben, füllen wir es, nachdem wir mit GetNumberOfChapters() die Anzahl der Kapitel für den aktuellen Titel erfragt haben, wieder auf und zwar genau so, wie wir das bereits in der zuletzt besprochenen Methode für die Titel gemacht haben. Das aktuelle Kapitel (wissen wir hier eh noch nicht... naja, eigentlich schon - es ist das erste des Titels) brauchen wir hier nicht markieren, da nach der Titel-Change-Message auch eine Kapitel-Change-Message erfolgt, die wir ja sowieso noch bearbeiten (sehen wir im Anschluß).

Im nächsten Schritt leeren und füllen wir das Untertitelmenu auf. Dazu fragen wir mit GetCurrentSubpicture() ab, wieviele Untertitel es eigentlich gibt, welcher gerade ausgewählt ist und ob er aktiviert ist. Im letzten Parameter steht die Information, ob der Untertitel gerade deaktiviert ist, oder eben nicht. Damit die Untertitel aber auch sichtbar werden, muß das über SetSubpictureState()-dem Controlinterface mitgeteilt werden. Zusätzlich haken wir den entsprechenden Menueintrag auch ab oder löschen das Häckchen eben.
Etwas interessanter ist dabei das Herausfinden des echten Sprachennamens. Dazu loopen wir alle Sprachen durch und erfragen mit GetSubpictureLanguage() die Sprachen-ID. Ist die Sprache unbekannt, wird die ID auf NULL gesetzt und wir übergehen sie einfach. Ansonsten wandeln wir mit der WinAPI-Funktion GetLocaleInfo() die ID in einen von uns lesbaren String um. Mit der Übergabe des LOCALE_SLANGUAGE-Parameters weiß die Funktion, daß der Sprachenname in derselben Sprache wie Windows selbst erscheinen soll, also in einem deutschen Windows "Deutsch" statt "German" in einem englischen Windows.
Achtung: Der Stolperstein hier ist, daß wir bei der Übergabe des Identifiers noch eine "1" addieren müssen, da die Untertitelinformationen ja genau wie die Audioinformationen bei "0" anfangen.
Jetzt setzen wir noch an der entsprechenden Stelle das Häckchen und sind fertig mit dem Untertitelmenu.

Für das Audiomenu gehen wir ganz analog vor, mit dem Unterschied, daß wir die benötigten Informationen mit GetCurrentAudio() abfragen und keinen dritten Parameter übergeben müssen, da ein Audiostream immer ausgewählt ist . Beim Eintragen der Menupunkte ist der einzige Unterschied die GetAudioLanguage()-Funktion. Der Rest ist wieder gleich.

So, daß wars eigentlich schon (schon?). Die restlichen Methoden sind nur noch kleine Hilfen und die haben wir schnell abgehandelt:

void CDVDPlayer::OnChapterSelect(const int p_iChapter)
{
    m_ulCurChapter = p_iChapter;
    for (ULONG i=1; i<=m_ulNumChapters; ++i)
    {
        ::CheckMenuItem(m_hMenuChapters, i-1, MF_BYPOSITION | (m_ulCurChapter == i ? MF_CHECKED : MF_UNCHECKED));
    };
};

// ================================================================================================
void CDVDPlayer::OnSubpictureSelect(const int p_iSubpicture)
{
    m_ulCurSubpicture = p_iSubpicture;
    for (ULONG i=0; i<m_ulNumSubpictures; ++i)
    {
        ::CheckMenuItem(m_hMenuSubpictures, i, MF_BYPOSITION | (m_ulCurSubpicture == i ? MF_CHECKED : MF_UNCHECKED));
    };
};

// ================================================================================================
void CDVDPlayer::OnAudioSelect(const int p_iAudio)
{
    m_ulCurAudio= p_iAudio;
    for (ULONG i=0; i<m_ulNumAudios; ++i)
    {
        ::CheckMenuItem(m_hMenuAudios, i, MF_BYPOSITION | (m_ulCurAudio == i ? MF_CHECKED : MF_UNCHECKED));
    };
};

Da gibts eigentlich wirklich nichts zu sagen. Im entsprechenden Untermenu wird der aktuelle Eintrag abgehakt, alle anderen Haken entfernt. Punkt.

Damit bleibt nur noch diese Helfermethode übrig:

void CDVDPlayer::ClearMenu(const HMENU p_hMenu) const
{
    int iNumItems = ::GetMenuItemCount(p_hMenu)-1;
    for (int i=iNumItems; i>=0; --i)
    {
        ::DeleteMenu(p_hMenu, i, MF_BYPOSITION);
    };
};

Hier wird einfach das entsprechende Submenu gesäubert. Damit intern nichts umgeordnet werden muß, lassen wir die Schleife von hinten nach vorne durchlaufen.

Damit haben wir endgültig alle Methoden durch und haben jetzt (hoffentlich) einen lauffähigen DVD-Player.

 

4. Epilog

Ja, es gibt noch einige Sachen zu sagen.
Einige werden sich sicher noch die eine oder andere Grundfunktion wünschen - z.B. Bookmarks. Diese können aber wirklich selber ganz einfach eingebaut werden. Das DirectX® SDK gibt dazu alle nötigen Informationen.
Wieder andere hätten sich vielleicht lieber eine Buttonleiste anstatt einem Popup-Menu gewünscht und ganz andere lieber ein extra Steuerfenster. Das alles sind aber lediglich reine WinAPI Angelegenheiten, die nicht wirklich in dieses Tutorial gehören. Unsere einfache Version eines Popup-Menus reicht hier (denke ich) wirklich aus. Das Tutorial ist ohnehin schon ziemlich lang geworden, obwohl wir eh fast nur die Standardfeatures behandelt haben.
Aber sind wir doch mal ehrlich: Knappe 600 Codezeilen (in der DVDPlayer.cpp - inkl. "Kommentarzeilen") für einen vollwertigen DVD-Player (mit 56kb für die ausführbare Datei) sind doch wirklich nicht schlecht. Das Wort "Kommentarzeilen" hab ich jetzt bewußt in Gänsefüßchen gesetzt, da im Projekt nicht mehr viel kommentiert ist - schließlich habe ich das hoffentlich ausreichend hier getan.

Ich hatte ganz am Anfang gesagt, daß es trotzdem zu Fehlermeldungen kommen könnte, wenn mehrere Decoder installiert sind (oder auch ein NimoCodec-Pack installiert ist). Eine könnte z.B. so aussehen:

Diese Fehlermeldung wird in der AcquireInterfaces()-Methode durch RaiseFailureMessage() erzeugt, wenn die RenderDvdVideoVolume()-Methode einen Fehler zurückmeldet. Der Fehler liegt dabei jedoch nicht an unserer Software hier. Das liegt vielmehr daran, daß sich in diesem Fall mehrere Decoder in die Quere kommen. Tatsächlich hatte ich diesen Fehler zwischendrin sogar mal. Die Lösung sah so aus, daß ich mehrere Filter im System von Hand entregistrierte und schlußendlich die zwei Filter von WinDVD wieder registrierte. Das sehen wir uns jetzt natürlich noch etwas genauer an.

Mein WinDVD kam mit zwei Filtern daher:

  • ivivideo.ax - Videodecoder (ist der nicht vorhanden oder kollidiert er mit anderen Decodern, kommt die obere Fehlermeldung)
  • iviaudio.ax - Audiodecoder

PowerDVD verwendet dagegen die folgenden Filter:

  • clvsd.ax - Videodecoder
  • claud.ax - Audiodecoder

Der Cinemaster 4 DVD-Player z.B, bringt die Filter in Form von .dll-Dateien mit:

  • DSCinemVideoDecoder.dll
  • DSCinemAudioDecoder.dll

Sind ein oder mehrere DVD-Player (mit Decodern) installiert worden, kann es sein, daß sich die Filter trotzdem niemals in die Quere kommen und wunderbar harmonieren. Genau das war bei mir leider (natürlich) nicht der Fall und irgendwann kam halt mal die obige Fehlermeldung.
Der Grund dafür ist einfach: Die eben genannten Produkte verwenden, wie ich zu Anfangs schon erwähnte, normalerweise immer eigene Filtergraphen und müssen sich nicht auf die im System registrierten Filter verlassen (deswegen funktionieren sie auch noch, wenn man alle Filter entregistriert). In unserem Fall sind wir leider auf registrierte Filter angewiesen, da wir in dem von DirectShow® erzeugten Graphen nicht mehr eingreifen.

Die Fehlermeldung kann übrigens auch kommen, wenn anderweitige Geräte gerade decodierenden Zugriff auf das DVD-Laufwerk haben.

Dazu gleich noch ein paar Erfahrungen meinerseits:
Bei der Verwendung älterer PowerDVD Filter war das Format (auch im Menu) immer korrekt aber dafür die Bildqualität insgesamt ziemlich schwach und der Sound war ziemlich leise.
Die Verwendung der (auch älteren) Filter von WinDVD brachte zwar eine gute Filmqualität und guten Sound, doch das Erscheinungsbild in den Menus (aber nur in den Menus) war bei manchen DVDs etwas in die Höhe verzogen - offensichtlich hätte hier auch das 16:9 Format zum Tragen kommen sollen. Aus irgendeinem Grund wurde es aber im 4:3 Format dargestellt. Achtung: Für die Filme selbst passte aber alles!.
Des weiteren schaltete mein Verstärker automatisch zu anfangs in den AC3-Modus... sobald aber das Menu kam, stellte sich wieder der normale Stereomodus ein und blieb auch im Film so (hängt mit den Einstellungen in der WinDVD-Software zusammen).
Etwas, was ebenfalls nicht sonderlich toll war, war das Vorwärtsspulen. Während das Rückwärtsspulen so ablief, wie man es sich vorstellt, kam das Vorwärtsspulen innerhalb von ein paar Sekunden ins Stocken aber diesen "Fehler" konnte ich auch bei anderen Playern feststellen (liegt vermutlich einfach an der Decodiergeschwindigkeit des Systems im Zusammenhang mit dem vorrausschauenden Zugriff bei den Videodaten - 2 Sekunden normalerweise).
Das meiste liegt wie gesagt an den registrierten Filtern und unserem unbearbeiteten Zugriff darauf und läßt sich eben besser in den Griff bekommen, wenn man den Graphen von Hand erstellen (programmieren), bzw. wenigstens ein paar Eingriffe von Hand vornehmen würde.

Ganz kurz noch für diejenigen, die nicht wissen, wie man überhaupt bestimmte Sachen von Hand (ent-)registrieren kann:
Dazu geht ihr einfach auf "Start" - "Ausführen" und (ent-)registriert den entsprechenden Filter mit der regsvr32.exe (ist bei Windows dabei).

Beispiel (registrieren): regsvr32      C:\Programme\InterVideo\WinDVD\iviaudio.ax
Beispiel (entregistrieren): regsvr32 \u   C:\WINNT\system32\mpeg2Decoder.ax

Mehr ist es nicht!

Ein weiterer Kritikpunkt an dieser Software ist, daß man nicht von Hand in den AC3-Modus (6-Kanal) und zurück umschalten kann. Das liegt u.a. aber ehrlich gesagt auch daran, daß ich das nicht wirklich hinbekommen habe. Um das zu bewerkstelligen müßte wahrscheinlich "nur" der entsprechende Ausgangspin (Interface IPin) des Audiodecoders gesetzt werden. Vielleicht geht das aber auch einfacher über das IAMStreamSelect- oder das IAMStreamConfig-Interface, wo man den passenden Audiostrom anhand der AM_MEDIA_TYPE-Struktur identifiziert. Vielleicht werde ich mich damit nochmal auseinandersetzen aber sollte das jemand bereits wissen, wäre ich ihm dankbar, mir eine kurze Mail diesbezüglich zukommen zu lassen.

Auch für jegliche andere konstruktive Kritik bin ich natürlich offen. Wer Verbesserungsvorschläge oder andere Ideen hat, kann mir gerne schreiben.


Abschließend sehen wir uns nochmal die Anforderungen und die Steuerung für den DVD-Player an:

Anforderungen:

  • Es muß bereits ein Decoder installiert sein, um diesen DVD-Player benutzen zu können.
  • Bevor man die Software startet, muß eine DVD im Laufwerk liegen

Steuerung:

Esc: Beenden
Tab: Wechseln zwischen Vollbild- und Fenstermodus
Space: Pausieren/Weiterspielen
Pfeil hoch: nächstes Kapitel
Pfeil runter: vorheriges Kapitel
Pfeil rechts halten: vorspulen
Pfeil links halten: zurückspulen
rechte Maustaste Popup-Menu aufrufen

Copyright © by Andrew Kerkel - 2004
Ich übernehme keinerlei Verantwortung oder Haftung für das Funktionieren der Software und auch nicht für den Inhalt angegebener Links.
Die Verwendung der Software erfolgt ausdrücklich auf eigene Gefahr!



Informationen: " ); ?> " ); ?> " ); ?> " ); ?>
WWW :$wwwtitel
Data:Projektdateien
Mail:$author ($email)
Nick:$nick