• Начальные сведения
  • Программа AviPlay
  • Глава 8. Воспроизведение видеороликов

    Видеоролики встречаются в компьютерных играх уже несколько лет, но обычно лишь в простейших заставках или переходах между уровнями. Впрочем, иногда они образуют все содержание самой игры. Например, в игре Phantasmagoria фирмы Sierra On-Line видеотехнология применяется для наложения роликов с живыми актерами на интерьеры, сгенерированные компьютером. В результате возникает на удивление правдоподобный синтез реального и компьютерного окружения. В этом случае видео обеспечивает уровень реализма, которого было бы невозможно добиться с синтезированными актерами.

    В этой главе мы узнаем, как считать AVI-файл и вывести его на поверхности DirectDraw. Хотя материал не ориентирован ни на какие конкретные цели, кроме простого воспроизведения видеороликов, он станет хорошей отправной точкой для самостоятельной работы с видео в DirectDraw. Изложенный материал воплощен в программе AviPlay, предназначенной для воспроизведения AVI-файлов.

    Начальные сведения

    Перед тем как переходить к техническим подробностям, давайте кратко рассмотрим важнейшие понятия и концепции работы с видеороликами. Если вам уже приходилось заниматься воспроизведением видео, этот раздел кратко напомнит основные положения.

    С концептуальной точки зрения видеофайл представляет собой последовательность растровых изображений, каждое из которых содержит очередной кадр. Обычно все растры последовательности обладают одинаковыми размерами и предназначаются для вывода с постоянной скоростью. Нередко растры сопровождаются звуковыми данными — авторским повествованием, диалогом или музыкой.

    Графическая и звуковая (необязательная) часть видеоролика часто называются потоками (streams). Этот термин говорит о том, что отдельные компоненты ролика (в случае видеоданных — растры) связаны между собой и следуют в определенном порядке. Термин «поток» часто встречается в последующих объяснениях.


    AVI-файлы

    Формат AVI (Audio Video Interleave) был разработан Microsoft для хранения и воспроизведения видеороликов. AVI-файл кроме последовательности растров содержит одну или несколько звуковых дорожек, сопровождающих видеоролик. В этой главе нам придется часто пользоваться AVI-файлами.

    Почему для хранения видеоинформации нужен специальный файловый формат? Почему нельзя представить видеоролик в виде последовательности растров и звукового файла? Существует несколько причин, но, видимо, самая важная из них — сжатие. Если кадры видеоролика будут храниться без сжатия (например, в BMP-файлах), то даже относительно короткий ролик будет занимать слишком много места. Например, 1-минутный ролик при разрешении 320×200 и частоте вывода 30 кадров в секунду займет на диске свыше 100 Мбайт.

    Разумеется, для практического применения видеороликов необходимо сжатие. Возникает вопрос: какое именно? Существует много различных алгоритмов сжатия, каждый из которых обладает своими достоинствами и недостатками. Маловероятно, чтобы один алгоритм подошел сразу по всем критериям.

    Поэтому в AVI-файлах была предусмотрена поддержка разнообразных методов сжатия. Конечно, без развитого API чтение AVI-файлов было бы очень сложной задачей. К счастью, в Windows существует специальный API Video For Windows, который мы вскоре рассмотрим.


    Представление данных в видеофайлах

    Форматы видеофайлов (такие как AVI) фактически поддерживают два уровня сжатия. Различные алгоритмы обеспечивают сжатие отдельных кадров; кроме того, некоторые кадры представляются не в виде самостоятельных изображений, а в виде изменений, внесенных в предыдущие кадры.

    Это объясняется тем, что типичный видеоролик состоит из похожих изображений. Например, если в нем показано, как человек идет по комнате, то фигура человека будет изменяться от кадра к кадру, но остальная часть (комната), скорее всего, останется практически неизменной. Следовательно, такой видеоролик можно представить в виде первого кадра и цепочки последующих изменений.

    Кадры видеоролика с полным изображением называются ключевыми кадрами (key frames). Видеоролик может содержать любое количество ключевых кадров, в зависимости от того, насколько часто и сильно меняется изображение. Ключевые кадры особенно часто появляются при переходах между сценами, эффектах «растворения» и перемещении съемочной камеры. Ключевые кадры чрезвычайно важны при чтении видеоролика; без них промежуточные кадры теряют смысл. Неключевой кадр тоже можно вывести, но изображение, скорее всего, будет нарушено, потому что такой кадр представляет собой лишь изменение предыдущего изображения, а не самостоятельное изображение.

    Открытый видеофайл обычно называется потоком. Как мы увидим в следующем разделе, посвященном Video For Windows API, концепция потока тесно интегрирована с Video For Windows.


    Video For Windows

    Для создания и чтения AVI-файлов Windows предоставляет разработчикам Video For Windows (VFW) API. Этот API заметно упрощает задачу воспроизведения видео, поскольку в нем имеются удобные средства для поиска и извлечения кадров из ролика.

    VFW не пытается поддерживать все возможные алгоритмы сжатия, используемые для хранения AVI-данных. Вместо этого VFW допускает установку и удаление компрессоров и декомпрессоров, обеспечивающих работу одного или нескольких алгоритмов сжатия. Таким образом, VFW позволяет выбрать нужные компрессор и декомпрессор в зависимости от потребностей разработчика. Кроме поддержки сжатия и распаковки VFW API предоставляет средства для поиска и извлечения кадров, а также для решения других задач, связанных с потоками.

    VFW API

    Имена многих функций VFW API начинаются с букв AVI. Другие функции (например, относящиеся к сжатию) начинаются с IC. Существуют некоторые исключения, но большинство функций VFW снабжается одним из этих префиксов. Алфавитный список всех функций VFW приведен в табл. 8.1.


    Таблица 8.1. Функции VFW

    AVIBuildFilter()

    AVICLearClipboard()

    AVIFileAddRef()

    AVIFileCreateStream()

    AVIFileEndRecord()

    AVIFileExit()

    AVIFileGetStream()

    AVIFileInfo()

    AVIFileInit()

    AVIFileOpen()

    AVIFileReadData()

    AVIFileRelease()

    AVIFileWriteData()

    AVIGetFromClipboard()

    AVIMakeCompressedStream()

    AVIMakeFileFromStream()

    AVIMakeStreamFromClipboard()

    AVIPutFileOnClipboard()

    AVISave()

    AVISaveOptions()

    AVISaveOptionsFree()

    AVISaveV()

    AVIStreamAddRef()

    AVIStreamBeginStreaming()

    AVIStreamCreate()

    AVIStreamEndStreaming()

    AVIStreamCreate()

    AVIStreamEndStreaming()

    AVIStreamFindSample()

    AVIStreamGetFrame()

    AVIStreamGetFrameClose()

    AVIStreamGetFrameOpen()

    AVIStreamInfo()

    AVIStreamLength()

    AVIStreamOpenFromFile()

    AVIStreamRead()

    AVIStreamReadData()

    AVIStreamReadFormat()

    AVIStreamRelease()

    AVIStreamSampleToTime()

    AVIStreamSetFormat()

    AVIStreamStart()

    AVIStreamTimeToSample()

    AVIStreamWrite()

    AVIStreamWriteData()

    CreateEditableStream()

    EditStreamClone()

    EditStreamCopy()

    EditStreamCut()

    EditStreamPaste()

    EditStreamSetInfo()

    EditStreamSetName()

    ICClose()

    ICCompress()

    ICCompressorChoose()

    ICCompressorFree()

    ICDecompress()

    ICDecompressEx()

    ICDecompressExBegin()

    ICDecompressExQuery()

    ICDraw()

    ICDrawBegin()

    ICDrawSuggestFormat()

    ICGetInfo()

    ICGetDisplayFormat()

    ICImageCompress()

    ICImageDecompress()

    ICInfo()

    ICInstall()

    ICLocate()

    ICOpen()

    ICOpenFunction()

    ICRemove()

    ICSendMessage()

    ICSeqCompressFrame()

    ICCompressFrameEnd()

    ICCompressFrameStart()

    ICGetStatusProc()

    MyStatusProc()


    Конечно, для воспроизведения роликов нужна лишь небольшая часть функций VFW, но чтобы изменить или расширить возможности программы из этой главы, вам, скорее всего, понадобятся и другие функции. Давайте рассмотрим те функции, с которыми нам придется работать.

    СОВЕТ

    Не забудьте включить VFW в проект

    Перед тем как пользоваться функциями Video For Windows, необходимо включить в проект заголовочный файл vfw.h и добавить vfw32.lib в список файлов компоновщика.

    Для правильной работы функций из табл. 8.1 необходимо инициализировать VFW функцией AVIFileInit(). Эта функция не получает аргументов и не возвращает никакого значения, поэтому работать с ней проще простого.

    После инициализации VFW можно создавать поток функцией AVIStreamOpenFromFile(). Эта функция получает в одном из аргументов имя AVI-файла и инициализирует по нему логический номер потока. Полученный логический номер затем используется как аргумент большинства функций VFW и определяет поток, с которым выполняется операция.

    В частности, по логическому номеру, возвращаемому AVIStreamOpenFromFile(), можно получить сведения о видеоролике. Функция AVIStreamReadFormat() сообщает такие данные, как количество кадров, размер изображения и глубина пикселей; для этого она заполняет структуру BITMAPINFOHEADER (возможно, структура BITMAPINFOHEADER покажется вам знакомой — мы уже встречались с ней при описании BMP-файлов). Я снова приведу определение этой структуры, взятое из файла Windows wingdi.h:

    typedef struct tagBITMAPINFOHEADER {

     DWORD biSize;

     LONG  biWidth;

     LONG  biHeight;

     WORD  biPlanes;

     WORD  biBitCount;

     DWORD biCompression;

     DWORD biSizeImage;

     LONG  biXPelsPerMeter;

     LONG  biYPelsPerMeter;

     DWORD biClrUsed;

     DWORD biClrImportant;

    } BITMAPINFOHEADER, FAR *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;

    Некоторые поля структуры (например, biXPelsPerMeter и biYPelsPerMeter) не имеют отношения к AVI-файлам, но таких полей немного. Размеры, глубина пикселей, алгоритм сжатия и количество цветов — все это можно взять из структуры BITMAPINFOHEADER после успешного вызова функции AVIStreamReadFormat().

    Более подробные сведения о потоке можно получить с помощью функции AVIStreamInfo(). Функция AVIStreamInfo(), как и функция AVIStreamReadFormat(), получает в качестве аргумента логический номер и заполняет структуру данными о потоке. Для передачи информации AVIStreamInfo() использует структуру AVISTREAMINFO. Ее определение в файле vfw.h выглядит так:

    typedef struct _AVISTREAMINFOW {

     DWORD fccType;

     DWORD fccHandler;

     DWORD dwFlags;

     DWORD dwCaps;

     WORD  wPriority;

     WORD  wLanguage;

     DWORD dwScale;

     DWORD dwRate;

     DWORD dwStart;

     DWORD dwLength;

     DWORD dwInitialFrames;

     DWORD dwSuggestedBufferSize;

     DWORD dwQuality;

     DWORD dwSampleSize;

     RECT  rcFrame;

     DWORD dwEditCount;

     DWORD dwFormatChangeCount;

     WCHAR szName[64];

    } AVISTREAMINFOW, FAR * LPAVISTREAMINFOW;

    Давайте рассмотрим некоторые ключевые поля этой структуры. Первое поле, fccType, определяет тип потока. В файлах формата AVI поддерживаются четыре типа потоковых данных: видео, аудио, MIDI (музыка) и текст. В этой книге будут рассматриваться только видеопотоки.

    Второе поле структуры AVISTREAMINFO, fccHandler, определяет алгоритм сжатия, примененный при сохранении видеопотока. Как вы вскоре увидите, в Video For Windows это поле можно использовать для создания «декомпрессоров», которые способны восстановить каждый сжатый кадр из потока.

    Поле dwStart определяет индекс первого кадра потока (это важно, потому что первый кадр видеоролика может иметь индекс 0 или 1). Эту же величину можно непосредственно получить функцией AVIStreamStart().В поле dwLength хранится общее количество кадров видеоролика. Его можно либо взять из структуры AVISTREAMINFO, либо получить с помощью функции AVIStreamLength().

    Теперь мы знаем, как открыть видеопоток и получить сведения о нем. Можно переходить к процессу чтения и отображения видеокадров. Чтение кадров выполняется функцией AVIStreamRead(). Эта функция, получив логический номер потока и индекс интересующего нас кадра, помещает данные кадра в буфер, который мы также должны ей передать. Данные кадра хранятся в сжатом виде, поэтому перед отображением их необходимо восстановить.

    Для восстановления кадров применяется функция ICDecompress(). Она получает два буфера: в одном находятся сжатые данные, а в другой будет помещен восстановленный кадр. Функции ICDecompress() также следует передать логический номер декомпрессора, используемого функцией ICDecompress() для обработки сжатых данных.

    Логический номер декомпрессора можно получить функцией ICDecompressOpen(). По описанию видеоданных функция ICDecompressOpen() ищет декомпрессор с поддержкой нужного алгоритма восстановления.

    После завершения работы с потоком необходимо закрыть его функцией AVIStreamRelease(). После вызова функции AVIStreamRelease() логический номер потока становится недействительным (до повторного открытия потока). Наконец, перед завершением программы необходимо вызвать функцию AVIFileExit(), которая освобождает программные модули VFW. 

    Программа AviPlay

    Пора браться за программирование. Программу AviPlay, как и все остальные программы, рассматриваемые в этой книге, можно найти на CD-ROM.

    Программа AviPlay использует Video For Windows для открытия и воспроизведения AVI-файлов на поверхностях DirectDraw. Она позволяет выбрать любой AVI-файл и задать видеорежим для воспроизведения ролика. Диалоговое окно для выбора файла изображено на рис. 8.1.

    Рис. 8.1. Диалоговое окно для выбора AVI-файла в программе AviPlay


    Если вы еще не успели опробовать программу AviPlay и не обнаружили этого сами, я хочу указать на несколько вещей, которые эта программа не умеет делать. Например, она не воспроизводит аудиопотоки из выбранного AVI-файла, а лишь извлекает и воспроизводит графическую часть файла.

    Кроме того, программа не пытается управлять частотой вывода кадров; все кадры извлекаются и воспроизводятся с максимальной скоростью. В каждом AVI-файле присутствуют хронометрические данные, которые используются всеми полноценными программами для воспроизведения AVI-файлов.

    Еще одна полезная возможность, которая бы пригодилась в программе AviPlay, — поддержка 16- и 24-битных видеорежимов. В представленном варианте всегда применяются 8-битные режимы независимо от глубины пикселей воспроизводимого ролика.

    Класс AviPlayWin 

    Большинство возможностей программы AviPlay обеспечивается классом AviPlayWin, который наследует поддержку DirectDraw от класса DirectDrawWin. В отличие от других программ этой книги класс AviPlayWin использует диалоговое окно для выбора файла. Вместо того чтобы создавать поверхности при запуске, программа AviPlay (как и программа BmpView из главы 5) ожидает, пока пользователь выберет файл. Затем программа создает поверхности и настраивает их в соответствии с содержимым выбранного файла. Определение класса AviPlayWin приведено в листинге 8.1.


    Листинг 8.1. Класс AviPlayWin

    class AviPlayWin : public DirectDrawWin {

    public:

     AviPlayWin();

    protected:

     //{{AFX_MSG(AviPlayWin)

     afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);

     afx_msg void OnRButtonDown(UINT nFlags, CPoint point);

     afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);

     afx_msg void OnDestroy();

     //}}AFX_MSG

     DECLARE_MESSAGE_MAP()

    private:

     int SelectInitialDisplayMode();

     BOOL CreateCustomSurfaces() {

      return TRUE;

     }

     void DrawScene();

     void RestoreSurfaces();

     void GetSystemPalette();

     void ShowDialog();

     BOOL LoadAvi();

     BOOL CreateAviSurface();

     BOOL UpdateAviSurface();

     BOOL InstallPalette();

    private:

     AviDialog* avidialog;

     CString fullfilename;

     CString filename;

     CString pathname;

     CRect displayrect;

     LPDIRECTDRAWSURFACE avisurf;

     CRect avirect;

     int x,y;

     DisplayModeArray displaymode;

     LPDIRECTDRAWPALETTE syspal;

     LPDIRECTDRAWPALETTE avipal;

     PAVISTREAM avistream;

     AVISTREAMINFO streaminfo;

     HIC decomp;

     long fmtlen, buflen;

     long startframe, endframe;

     long curframe;

     LPBITMAPINFOHEADER srcfmt;

     LPBITMAPINFOHEADER dstfmt;

     BYTE* rawdata;

     BYTE* finaldata;

    };

    Сначала мы объявляем конструктор класса AviPlayWin, предназначенный только для инициализации переменных класса.

    В классе определены четыре обработчика сообщений: OnKeyDown(), OnRButtonDown(), OnCreate() и OnDestroy(). Функция OnKeyDown() следит за нажатием клавиш Escape и пробела во время воспроизведения, прерывает ролик и отображает диалоговое окно для выбора AVI-файла (мы могли воспользоваться DirectInput, но программа AviPlay не стоит подобных хлопот). Функция OnRButtonDown() тоже вызывает диалоговое окно для выбора AVI-файла, но по щелчку правой кнопки мыши. Функция OnCreate() инициализирует DirectDraw и AVI, а функция OnDestroy() завершает их работу.

    Затем мы объявляем 10 закрытых (private) функций. Первой идет функция SelectInitialDisplayMode(), которая выполняет три задачи: выбор исходного видеорежима (то, для чего предназначена сама функция), построение списка 8-битных режимов для диалогового окна и захват системной палитры. Вскоре мы рассмотрим эту функцию. Функция GetSystemPalette() вызывается функцией SelectInitialDisplayMode(); мы увидим, как она работает, при знакомстве с последней.

    Функция CreateCustomSurfaces() объявлена встроенной (inline). Она всего лишь возвращает TRUE, потому что при запуске приложения поверхности не создаются.

    Следующая функция, ShowDialog(), отображает диалоговое окно и в случае выбора допустимого AVI-файла загружает его функцией LoadAvi(). Основная часть функциональных возможностей программы обеспечивается этими двумя функциями, поэтому мы рассмотрим их подробно.

    За функций LoadAvi() объявляется функция DrawScene(). Мы воспользуется ею для вывода кадров видеоролика. Помимо извлечения и восстановления кадров видеопотока DrawScene() осуществляет блиттинг и переключение страниц, необходимые для отображения кадра.

    Функция RestoreSurfaces() отвечает за восстановление поверхностей, хранящихся в видеопамяти, в случае их потери. Как вы вскоре убедитесь, в нашей программе эта функция выглядит примитивно.

    Функции CreateAviSurface() и UpdateAviSurface() отвечают за создание и обновление поверхности AVI. Размеры поверхности AVI определяются размерами кадров AVI-файла, выбранного пользователем, поэтому при каждом открытии нового AVI-файла создается новая поверхность AVI. Функция UpdateAviSurface() готовит поверхность AVI к отображению, копируя выходные данные функции ICDecompress() в память поверхности.

    Последней объявлена функция InstallPalette(), которая устанавливает палитру AVI перед началом воспроизведения ролика. Однако перед этим она должна извлечь данные палитры из потока AVI.

    Оставшаяся часть класса содержит лишь переменные. Мы познакомимся с ними во время рассмотрения программы.

    Функция OnCreate() 

    Мы будем рассматривать функции примерно в порядке их выполнения. Начнем с функции OnCreate(), которая выглядит так:

    int AviPlayWin::OnCreate(LPCREATESTRUCT lpCreateStruct) {

     if (DirectDrawWin::OnCreate(lpCreateStruct) == -1) return -1;

     AVIFileInit();

     ShowDialog();

     return 0;

    }

    Сначала мы вызываем версию OnCreate() базового класса (а она инициализирует DirectDraw). Если вызов этой функции закончится неудачей, продолжать бессмысленно, и мы возвращаем код неудачного завершения –1.

    Затем мы вызываем функцию AVIFileInit(), которая инициализирует Video For Windows. После этого можно спокойно пользоваться функциями VFW.

    Наконец, функция ShowDialog() выводит диалоговое окно для выбора AVI-файла и ожидает ввод от пользователя. Однако перед тем, как обсуждать ShowDialog(), необходимо рассмотреть функцию SelectInitialDisplayMode(), которая вызывается при использовании функции OnCreate() класса DirectDrawWin.

    Функция SelectInitialDisplayMode() 

    Как упоминалось выше, функция SelectInitialDisplayMode() решает три задачи. Она выглядит так:

    int AviPlayWin::SelectInitialDisplayMode() {

     GetSystemPalette();

     int i, nummodes=GetNumDisplayModes();

     DWORD w,h,d;

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

      DisplayModeDescription desc;

      GetDisplayModeDimensions(i, w, h, d);

      if (d==8) {

       desc.w=w;

       desc.h=h;

       desc.d=d;

       desc.desc.Format("%dx%d", w, h);

       displaymode.Add(desc);

      }

     }

     int curdepth=GetDisplayDepth();

     if (curdepth!=8) ddraw2->SetDisplayMode(640, 480, curdepth, 0, 0);

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

      GetDisplayModeDimensions(i, w, h, d);

      if (w==640 && h==480 && d==8) return i;

     }

     return 1;

    }

    Перед тем как выполнять свою основную задачу (выбор исходного видеорежима), функция SelectInitialDisplayMode() вызывает функцию GetSystemPalette(). В свою очередь GetSystemPalette() создает палитру DirectDraw на базе текущей палитры Windows. Эта палитра обеспечивает правильный вывод диалогового окна независимо от того, какая палитра была установлена для воспроизведения ролика. Вспомните — GDI ничего не знает о DirectDraw и поэтому всегда пытается вывести диалоговое окно с использованием системной палитры, несмотря на то что она могла быть переопределена DirectDraw.

    Затем функция SelectInitialDisplayMode() перебирает список доступных видеорежимов и сохраняет описания 8-битных режимов в массиве displaymodes. Позднее этот массив передается диалоговому окну для вывода списка доступных видеорежимов.

    Наконец, функция ищет 8-битный режим с разрешением 640x480. Этот режим выбран лишь потому, что он поддерживается абсолютным большинством видеокарт (если не всеми). После вывода диалогового окна пользователь сможет выбрать любой другой 8-битный режим.

    Функция ShowDialog() 

    Давайте рассмотрим функцию для вывода диалогового окна. Функция ShowDialog() приведена в листинге 8.2.


    Листинг 8.2. Функция ShowDialog()

    void AviPlayWin::ShowDialog() {

     const CRect& displayrect=GetDisplayRect();

     if (displayrect.Width()<640 || displayrect.Height()>480) ddraw2->SetDisplayMode(640, 480, 8, 0, 0);

     ClearSurface(backsurf, 0);

     ClearSurface(primsurf, 0);

     primsurf->SetPalette(syspal);

     ddraw2->FlipToGDISurface();

     ShowCursor(TRUE);

     if (avidialog==0) {

      avidialog=new AviDialog();

      avidialog->SetArray(&displaymode);

     }

     if (avistream)  AVIStreamRelease(avistream), avistream=0;

     if (avidialog->DoModal()==IDCANCEL) {

      PostMessage(WM_CLOSE);

      return;

     }

     ShowCursor(FALSE);

     fullfilename=avidialog->fullfilename;

     filename=avidialog->filename;

     pathname=avidialog->pathname;

     int index=avidialog->GetIndex();

     DWORD w,h,d;

     w=displaymode[index].w;

     h=displaymode[index].h;

     d=displaymode[index].d;

     ActivateDisplayMode(GetDisplayModeIndex(w, h, d));

     LoadAvi();

     CreateAviSurface();

     InstallPalette();

     curframe=startframe;

    }

    Функция ShowDialog() начинается с проверки текущего разрешения. Если в данный момент установлен видеорежим с разрешением меньше 640x480, он изменяется. Это сделано для того, чтобы диалоговое окно не выводилось в режиме Mode X. Поскольку этот режим не поддерживается Windows, такая попытка, скорее всего, закончится неудачей из-за нелинейной организации пикселей в режимах Mode X.

    Возможно, у вас возник вопрос — а почему может действовать режим Mode X? Вспомните, что эта функция вызывается при каждом нажатии клавиши Escape, пробела или правой кнопки мыши во время воспроизведения видеоролика. Нельзя исключать того, что видеорежим Mode X был установлен для воспроизведения ролика, поэтому перед выводом диалогового окна необходимо проверить эту возможность.

    Далее мы стираем первичную поверхность и вторичный буфер, после чего устанавливаем системную палитру. Строго говоря, стирать поверхности необязательно, но после восстановления системной палитры оставшиеся изображения будут выглядеть довольно странно.

    После установки системной палитры мы вызываем функцию DirectDraw FlipToGDISurface(). Это гарантирует, что диалоговое окно Windows будет отображаться на видимой поверхности, а не во вторичном буфере. Кроме того, мы снова включаем курсор мыши (иначе пользователь не сможет нажимать кнопки диалогового окна и выбрать AVI-файл).

    Если экземпляр класса AviDialog не был создан при предыдущем вызове функции ShowDialog(), мы создаем его. Обратите внимание на то, что при создании диалогового окна ему передается массив 8-битных видеорежимов, подготовленный в функции SelectInitialDisplayMode().

    Затем мы закрываем существующий AVI-поток. Это делается из-за того, что класс AviDialog обладает собственными средствами для работы с файлами, с помощью которых он выводит размеры и количество кадров в выбранном AVI-файле. Если не закрыть ранее открытый файл, то при его повторном выборе диалоговое окно уже не сможет получить эту информацию.

    Функция DoModal() отображает диалоговое окно, в котором пользователь может выбрать нужный файл. При нажатии кнопки Cancel мы посылаем сообщение WM_CLOSE. Если все идет нормально, мы получаем имя выбранного файла (в трех различных формах) вместе с индексом видеорежима (видеорежим необходимо выбрать до нажатия кнопки Play). Размеры выбранного видеорежима, взятые из массива displaymode, передаются функции SetDisplayMode().

    Дальше следует вызов функции LoadAvi(). Как вы вскоре убедитесь, функция LoadAvi() на самом деле не загружает видеоролик — она лишь открывает файл и извлекает сведения о ролике (например, количество кадров и их размеры). Функция CreateAviSurface() по полученным размерам создает поверхность для хранения одного кадра видеопотока.

    Функция InstallPalette() извлекает данные палитры из AVI-файла и строит по ним палитру DirectDraw, которая лучше всего подходит для просмотра. Наконец, переменной curframe, предназначенной для перебора кадров, присваивается значение переменной startframe.

    Функция LoadAvi() 

    Перейдем к функции, которая непосредственно открывает AVI-файл. Функция LoadAvi() приведена в листинге 8.3.


    Листинг 8.3. Функция LoadAvi()

    BOOL AviPlayWin::LoadAvi() {

     long r;

     CWaitCursor cur;

     if (avistream) AVIStreamRelease(avistream), avistream=0;

     r=AVIStreamOpenFromFile(&avistream, filename, streamtypeVIDEO, 0, OF_READ | OF_SHARE_EXCLUSIVE, 0);

     TRACE("AVIStreamOpenFromFile: %s\n", r==0 ? "OK" : "failed");

     r=AVIStreamFormatSize(avistream, 0, &fmtlen);

     TRACE("AVIStreamFormatSize: %s\n", r==0 ? "OK" : "failed");

     int formatsize=fmtlen+sizeof(RGBQUAD)*256;

     if (srcfmt)   delete [] srcfmt;

     srcfmt = (LPBITMAPINFOHEADER)new BYTE[formatsize];

     ZeroMemory(srcfmt, formatsize);

     if (dstfmt)   delete [] dstfmt;

     dstfmt = (LPBITMAPINFOHEADER)new BYTE[formatsize];

     ZeroMemory(dstfmt, formatsize);

     r=AVIStreamReadFormat(avistream, 0, srcfmt, &fmtlen);

     TRACE("AVIStreamReadFormat: %s\n", r==0 ? "OK" : "failed");

     TRACE(" --- %s ---\n", filename);

     TRACE(" biSize: %d\n", srcfmt->biSize);

     TRACE(" biWidth x biHeight: %dx%d\n", srcfmt->biWidth, srcfmt->biHeight);

     if (srcfmt->biPlanes != 1) TRACE(" - biPlanes: %d\n", srcfmt->biPlanes);

     TRACE(" biBitCount: %d\n", srcfmt->biBitCount);

     CString comp;

     switch (srcfmt->biCompression) {

     case BI_RGB:

      comp="BI_RGB";

      break;

     case BI_RLE8:

      comp="BI_RLE8";

      break;

     case BI_RLE4:

      comp="BI_RLE4";

      break;

     case BI_BITFIELDS:

      comp="BI_BITFIELDS";

      break;

     }

     TRACE(" biCompression: %s\n", comp);

     TRACE(" biSizeImage: %d\n", srcfmt->biSizeImage);

     TRACE(" ------------------\n");

     memcpy(dstfmt, srcfmt, fmtlen);

     dstfmt->biBitCount = 8;

     dtfmt->biCompression = BI_RGB;

     dstfmt->biSizeImage = dstfmt->biWidth * dstfmt->biHeight;

     startframe = AVIStreamStart(avistream);

     TRACE("stream start: %d\n", startframe);

     endframe = AVIStreamEnd(avistream);

     TRACE("stream end: %d\n", endframe);

     r=AVIStreamInfo(avistream, &streaminfo, sizeof(streaminfo));

     TRACE("AVIStreamInfo: %s\n", r==0 ? "OK" : "failed" );

     buflen = dstfmt->biSizeImage;

     int finalbuflen=((dstfmt->biWidth+3) & ~3) * dstfmt->biHeight;

     if (streaminfo.dwSuggestedBufferSize) if ((LONG)streaminfo.dwSuggestedBufferSize < buflen) {

      TRACE("adjusting buflen to suggested size\n");

      buflen = (LONG)streaminfo.dwSuggestedBufferSize;

     }

     if (decomp) ICClose(decomp);

     decomp = ICDecompressOpen(ICTYPE_VIDEO, streaminfo.fccHandler, srcfmt, dstfmt);

     TRACE("ICDecompressOpen: %s\n", decomp ? "OK" : "failed");

     if (rawdata) {

      TRACE("delete [] rawdata...\n");

      delete [] rawdata;

     }

     rawdata = new BYTE[buflen];

     if (finaldata) {

      TRACE("delete [] finaldata...\n");

      delete [] finaldata;

     }

     finaldata = new BYTE[finalbuflen];

     return TRUE;

    }

    В функции LoadAvi() используются функции VFW. Сначала LoadAvi() закрывает открытый ранее AVI-поток функцией AVIStreamRelease(), а затем открывает новый поток функцией AVIStreamOpenFromFile(), которой в числе прочих аргументов передается имя открываемого AVI-файла.

    Обратите внимание — третьим аргументом функции AVIStreamOpenFromFile() является флаг, определяющий тип открываемого потока. В нашем случае использован флаг видеопотока streamtypeVIDEO, но с помощью трех оставшихся флагов (streamtypeAUDIO, streamtypeMIDI и streamtypeTEXT) можно открывать и потоки других типов.

    Затем мы получаем данные о формате потока функцией AVIStreamReadFormat() (пользуясь при этом функцией AVIStreamFormatSize()). Я специально оставил в этом фрагменте отладочные макросы TRACE(), чтобы продемонстрировать, какую информацию можно получить об AVI-файле.

    На этой стадии инициализируются некоторые важные переменные класса. Например, мы присваиваем значения переменным startframe и endframe, чтобы во время извлечения кадров были известны допустимые значения их индексов.

    Затем мы получаем доступ к декомпрессору. Функция ICDecompressorOpen() по структуре, описывающей AVI-файл и желательный формат вывода, возвращает логический номер модуля декомпрессии. Позднее этот модуль используется для восстановления кадров. Наконец, мы выделяем память под два буфера: в одном хранятся необработанные (сжатые) данные, извлеченные из AVI-потока, а в другом — итоговый (восстановленный) кадр.

    Функция CreateAviSurface() 

    Теперь у нашего приложения есть открытый AVI-поток и информация в объеме, достаточном для извлечения кадров. Но что же делать с кадром после того, как он будет прочитан и восстановлен? Нам понадобится поверхность для хранения полученных данных, и тогда воспроизведение видеоролика сведется к простому блиттингу содержимого этой поверхности во вторичный буфер приложения с последующим переключением страниц. Эта промежуточная поверхность создается функцией CreateAviSurface():

    BOOL AviPlayWin::CreateAviSurface() {

     if (avisurf) avisurf->Release(), avisurf=0;

     avisurf=CreateSurface(srcfmt->biWidth, srcfmt->biHeight);

     CRect displayrect=GetDisplayRect();

     x=(displayrect.Width()-srcfmt->biWidth)/2;

     y=0;

     return TRUE;

    }

    После освобождения поверхности, созданной ранее, функция CreateAviSurface() с помощью функции CreateSurface() интерфейса DirectDraw создает поверхность, размеры которой совпадают с размерами кадра. Кроме того, функция CreateAviSurface() инициализирует переменные x и y, определяющие положение поверхности AVI на вторичном буфере. В нашем случае кадры будут выравниваться по центру экрана, поэтому в вычислениях применяется функция DirectDrawWin::GetDisplayRect() для определения размеров экрана.

    Функция InstallPalette() 

    С помощью файлового формата AVI и VFW API можно получить палитру, оптимально подходящую для просмотра видеоролика. Функция InstallPalette() извлекает необходимые данные и использует их для конструирования палитры DirectDraw. Функция InstallPalette() выглядит так:

    BOOL AviPlayWin::InstallPalette() {

     ICDecompressGetPalette(decomp, srcfmt, dstfmt);

     PALETTEENTRY pe[256];

     LPBITMAPINFO info=(LPBITMAPINFO)dstfmt;

     for (int i=0; i<256; i++) {

      pe[i].peRed = info->bmiColors[i].rgbRed;

      pe[i].peGreen = info->bmiColors[i].rgbGreen;

      pe[i].peBlue = info->bmiColors[i].rgbBlue;

      pe[i].peFlags = 0;

     }

     if (avipal) avipal->Release();

     ddraw2->CreatePalette(DDPCAPS_8BIT, pe, &avipal, 0);

     primsurf->SetPalette(avipal);

     return TRUE;

    }

    Функция ICDecompressGetPalette() получает данные палитры и в цикле преобразует их в формат, который мы можем использовать. Полученный массив передается при вызове функции CreatePalette() интерфейса DirectDraw. Остается лишь присоединить созданную палитру к первичной поверхности.

    Функция DrawScene() 

    Наконец, все готово к отображению кадров видеоролика. Для этого мы подготавливаем и выводим очередной кадр при каждом вызове функции DrawScene() классом DirectDrawWin. Функция DrawScene() выглядит так:

    void AviPlayWin::DrawScene() {

     long r;

     r=AVIStreamRead(avistream, curframe, 1, rawdata, buflen, 0, 0);

     if (r) {

      TRACE("AVIStreamRead failed: ");

      switch (r)  {

      case AVIERR_BUFFERTOOSMALL:

       TRACE("BUFFERTOOSMALL\n");

       break;

      case AVIERR_MEMORY:

       TRACE("MEMORY\n");

       break;

      case AVIERR_FILEREAD:

       TRACE("FILEREAD\n");

       break;

      }

     }

     r=ICDecompress(decomp, 0, srcfmt, rawdata, dstfmt, finaldata);

     UpdateAviSurface();

     backsurf->BltFast(x, y, avisurf, 0, DDBLTFAST_WAIT);

     curframe=(curframe<endframe) ? curframe+1 : startframe;

     primsurf->Flip(0, DDFLIP_WAIT);

    }

    Функция DrawScene() с помощью функции AVIStreamRead() извлекает очередной кадр из AVI-потока, после чего сохраняет полученные данные в буфере rawdata. Я оставил в ней несколько макросов TRACE(), которые пригодились мне при отладке, но надеюсь, что вам они не понадобятся.

    Затем мы вызываем функцию ICDecompress() и передаем ей логический номер декомпрессора, ранее полученный от функции LoadAvi(). Аргументами функции ICDecompress() являются два буфера — первый содержит необработанные (сжатые) данные, а второй — восстановленное изображение.

    Функция UpdateAviSurface() копирует восстановленный кадр на поверхность AVI. Эта функция рассматривается ниже.

    Подготовленная поверхность AVI копируется во вторичный буфер функцией BltFast() интерфейса DirectDrawSurface. После этого переменная curframe увеличивается или сбрасывается в зависимости от ее значения и количества кадров в ролике. Наконец, функция Flip() интерфейса DirectDrawSurface выводит кадр на экран.

    Функция UpdateAviSurface()

    Перед тем как рассматривать функцию UpdateAviSurface(), я хочу обратить ваше внимание на ее сходство с кодом класса DirectDrawWin, предназначенным для загрузки BMP-файлов на поверхность (см. главу 5). Функция UpdateAviSurface(), как и функции загрузки BMP-файлов DirectDrawWin, блокирует поверхность и затем копирует данные в ее память:

    BOOL AviPlayWin::UpdateAviSurface() {

     HRESULT r;

     if (finaldata==0) return FALSE;

     DWORD dwWidth = (srcfmt->biWidth+3) & ~3;

     DWORD dwHeight = srcfmt->biHeight;

     DDSURFACEDESC desc;

     ZeroMemory(&desc, sizeof(desc));

     desc.dwSize = sizeof(desc);

     r = avisurf->Lock(0, &desc, DDLOCK_WAIT, 0);

     if (r==DD_OK) {

      BYTE* src = finaldata + dwWidth * (dwHeight-1);

      BYTE* dst = (BYTE *)desc.lpSurface;

      for (DWORD y=0; y<dwHeight; y++) {

       memcpy(dst, src, dwWidth);

       dst += desc.lPitch;

       src -= dwWidth;

      }

      avisurf->Unlock(0);

     }

     return TRUE;

    }

    После блокировки поверхности функция UpdateAviSurface() в цикле копирует каждую строку пикселей AVI-данных в память поверхности. В формате AVI, как и в формате BMP, изображения хранятся в перевернутом виде, поэтому мы начинаем с последней строки буфера данных и двигаемся к его началу.

    Функция RestoreSurfaces() 

    Все трудное осталось позади, дальше будет легко. Особенно просто реализуется функция RestoreSurfaces():

    void AviPlayWin::RestoreSurfaces() {

     avisurf->Restore();

    }

    Вспомните — функция RestoreSurfaces() вызывается только при восстановлении потерянных поверхностей, а класс DirectDrawWin автоматически восстанавливает первичную поверхность со вторичным буфером. В программе AviPlay остается лишь восстановить поверхность AVI, а для этого достаточно вызвать функцию Restore() интерфейса DirectDrawSurface.

    В некоторых программах функция RestoreSurfaces() восстанавливала не только область памяти, но и содержимое поверхности. В нашем случае можно ограничиться восстановлением памяти, потому что ее содержимое будет перезаписано следующим кадром. Если вы вдруг засомневаетесь, напомню — вызов функции Restore() для поверхности, которая не была потеряна (например, находящейся в системной памяти), не причинит никакого вреда.

    Обработка пользовательского ввода 

    В программе AviPlay ввод не играет особой роли. Программа реагирует всего на три клавиши, причем одинаково. Ввод с клавиатуры обрабатывается функцией OnKeyDown():

    void AviPlayWin::OnKeyDown(UINT key, UINT nRepCnt, UINT nFlags) {

     switch (key) {

     case VK_ESCAPE:

     case VK_SPACE:

     case VK_RETURN:

      ShowDialog();

      break;

     }

     DirectDrawWin::OnKeyDown(key, nRepCnt, nFlags);

    }

    Все три клавиши вызывают функцию ShowDialog(). Аналогично обрабатывается и ввод от мыши, это происходит в функции OnRButtonDown():

    void AviPlayWin::OnRButtonDown(UINT nFlags, CPoint point) {

     ShowDialog();

     DirectDrawWin::OnRButtonDown(nFlags, point);

    }

    Когда пользователь закрывает диалоговое окно для выбора AVI-файла, функция ShowDialog() посылает сообщение WM_CLOSE, сигнализируя о завершении приложения.

    Функция OnDestroy() 

    Остается лишь завершить приложение. Функция OnDestroy() занимается «уборкой мусора» — она закрывает открытые AVI-потоки, освобождает декомпрессор и буферы данных AVI:

    void AviPlayWin::OnDestroy() {

     DirectDrawWin::OnDestroy();

     if (avistream) AVIStreamRelease(avistream), avistream=0;

     if (decomp)  ICClose(decomp), decomp=0;

     if (srcfmt) delete [] srcfmt, srcfmt=0;

     if (dstfmt) delete [] dstfmt, dstfmt=0;

     if (rawdata) {

      TRACE("delete [] rawdata...\n");

      delete [] rawdata, rawdata=0;

     }

     if (finaldata) {

      TRACE("delete [] finaldata...\n");

      delete [] finaldata, finaldata=0;

     }

     if (avidialog) delete avidialog, avidialog=0;

     AVIFileExit();

    }

    Обратите внимание на вызов функции AviFileExit() в конце OnDestroy(). Это завершает работу VFW и освобождает все используемые им ресурсы.

    Заключение 

    Наше знакомство с воспроизведением видеороликов подходит к концу. Честно говоря, чтобы превратить программу AviPlay в полноценный проигрыватель AVI-файлов, вам придется еще немало потрудиться. Необходимо организовать поддержку звука и хронометраж, не говоря уже о том, что VFW обладает многими странностями и в работе с ним приходится много экспериментировать.

    И последнее замечание. По неизвестным мне причинам VFW отказывается работать с AVI-файлами, сжатыми кодеками IR32 и IR42 (возможно, есть и другие, но я заметил эти два). С другой стороны, AVI-файлы, использующие кодеки MS-CRAM и Cinepak, работают нормально.

    В главе 9 мы возьмемся за проверку столкновений. Наша цель — написать код, который бы обеспечивал точность проверки на уровне пикселей при максимальной эффективности.







     

    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх