Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

В этом выпуске описан процесс разработки и приведен исходный код приложения, производящего запись изображения с экрана в


Visual C++ - расширенное программирование

Выпуск      : 21
Подписчиков : 6281
Cайт        : SoftMaker.com.ru
Архив       : Visual C++ - расширенное программирование (архив)
В этом выпуске
Программирование на C++ глазами хакера (+ CD-ROM)

Программирование на C++ глазами ХАКЕРА (+ CD-ROM)

От автора

Здравствуйте уважаемые подписчики !

Как всегда, рад приветствовть вас на страницах этой рассылки.

Как обычно, напомню, что если Вы хотите:

  • - опубликовать здесь статью
  • - создать и вести какой либо раздел в этой рассылке
  • - предложить интересную идею, тему для публикации, и.т.д.
просто напишите мне.

Я также буду рад любым пожеланиям, замечаниям, предложениям и сообщениям об ошибках по поводу рассылки и сайта. Их можно отправлять по тому же адресу.

И, как всегда, вы можете задать свои вопросы по программированию в форуме.
Или обсудить их в дискуссионном листе "Программирование. Форум !!!".

Многим может быть также интересна рассылка: C/C++ Вопрос-Ответ, формируемая из писем читателей. Здесь Вы можете задать вопрос по программированию на C/C++ и ответить на вопросы других подписчиков. А также рассылка Программирование на JavaScript, в которой публикуются и описываются оригинальные скрипты, решения многих практических задач, готовые для применения на сайте.

С уважением, Вахтуров Виктор.

Статьи
Господа, на момент выпуска этого номера рассылки, исходник к статье будет находиться здесь: ScreenToAVI.zip (35kb).
Однако, в будущем его адрес может поменяться.


Программа захвата видео с экрана

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

Вступление

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

Хотя приложение, процесс разработки которого мы рассмотрим далее, достаточно простое, оно интересно тем, что его разработка охватывает несколько вопросов прикладного программирования в Windows: снятие скриншотов, преобразование аппаратно-зависимых растров в аппаратно-независимые, получение изображения текущего курсора, работа с AVI-файлами, работа с кодеками. А данная статья как минимум дает развернутый ответ на бесконечные вопросы, часто звучащие на разных форумах, типа: "Как сделать скриншот ?", "Как записать видео с экрана ?", "Как создать AVI-файл ?".

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

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

Пути реализации

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

Метод первый (с использованием mirror driver)

Данный метод применим только в Windows 2000 и выше. Он основан на написании и использовании так называемого "зеркального" драйвера (mirror driver). Такому драйверу поступают те же самые команды графического вывода, генерируемые подсистемой GDI, что и основному display-драйверу. Иными словами, с помощью зеркального драйвера можно поток этих команд (и данных, передаваемых с ними) перехватить.

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

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

Недостатки данного метода заключаются в невозможности его использования в Windows линейки 9x, а также в возможности перехвата графического вывода, генерируемого только подсистемой GDI.

Примечание
К слову, в Windows 95/98 имеется аналогичная возможность. Пакет Microsoft Active Accessibility SDK 1.0 включает API функцию SetDDIHook, позволяющую осуществить перехват операций графического вывода.

Метод второй (простой, но очень ресурсоемкий)

Заключается в периодическом снятии скриншотов (screen shots) и, либо немедленном сжатии полученных растров видео кодеком (собственно, во время записи будет формироваться видеофайл), либо - сжатии растров каким то "легким" алгоритмом без потери качества с последующим формированием видео файла (распаковкой и сжатием растров уже кодеком).

В любом случае, данный метод, как и было указано выше, чрезвычайно ресурсоемкий (из за необходимости компрессии очень больших объемов информации в единицу времени). Однако он имеет и преимущества: одинаково работает как в Windows 9x, так и в Windows NT/2K/XP, захватывается все изображение на первичной графической поверхности, выведенное туда любым способом.

Примечание
Как можно заметить, оба этих метода имеют существенные ограничения (например, не позволяют захватывать изображение с оверлейных поверхностей).

Ввиду сложности реализации перехвата графического вывода, выберем второй метод.

Реализация

Итак, перед нами стоят две основные задачи:

  • Снятие скриншотов
  • Запись видео файла, кадрами которого будут являться эти скриншоты

Снятие скриншотов

Стоит сказать, что снятие скриншотов можно произвести как минимум двумя способами:

  • Средствами DirectX (копирование изображения с первичной графической поверхности (primary surface))
  • Средствами GDI (копирование растра с экранного контекста устройства на совместимый контекст устройства (Compatible Device Context или Memory Device Context))
Поскольку использование DirectX в нашем случае практически не даст прибавки в скорости (основное время будет тратиться на сжатие растров кодеком), для снятия скриншотов будем использовать GDI.

Теперь выясним, что же входит конкретно в нашу задачу снятия скриншотов. Во-первых, это, конечно, получение "снимка экрана". Как уже было сказано, данная операция производится при помощи копирования изображения с экранного контекста на совместимый контекст устройства. При этом в совместимом контексте устройства должен быть выбран аппаратно-зависимый растр, совместимый с исходным контекстом. В результате, текущее изображение экрана будет скопировано на этот растр. Однако, на таком "снимке" будет отсутствовать изображение курсора. Поэтому (во-вторых), необходимо получить текущий курсор и отрисовать его изображение на полученном растре. В-третьих, поскольку видеокомпрессору для обработки надо передавать буфер, содержащий данные кадра изображения (обычно, эти данные представляют собой аппаратно-независимый (DIB) растр), а у нас в распоряжении имеется только дескриптор аппаратно-зависимого растра (DDB), необходимо будет производить формирование DIB растра из DDB.

Для решения всех этих задач был разработан класс CCaptureHolder. Вот листинг его декларации:

Код
class CCaptureHolder
{

public:

    CCaptureHolder();
    virtual ~CCaptureHolder();

public:

    BOOL Create();
    void Close();

    BOOL MakeShot();

    BITMAPINFO* GetDIB(void **ppBits = NULL);

protected:

    BOOL CreateDIB();
    HCURSOR GetCurrentCursorHandle(POINT &pt);

protected:

    CDC        m_dcScreen;
    CDC        m_dcMem;

    CBitmap    m_bmpShot;
    CBitmap    *m_pOldMemDCBmp;

    void       *m_pDIBData;
    void       *m_pDIBits;

};

Применять этот класс крайне просто: при вызове метода MakeShot снимается скриншот и производится формирование аппаратно-независимого растра. После этого можно вызывать метод GetDIB, который возвратит указатель на сам растр (указатель на структуру BITMAPINFO - заголовок, содержащий информацию о растре, после которого следует массив данных растра). Адрес массива данных растра можно получить сразу, при вызове метода GetDIB, передав ему указатель на переменную-указатель, в которую и он и будет записан.

Для минимизации временных издержек, связанных с созданием/удалением объектов GDI, выделением памяти под данные DIB-растра, эти операции производятся не при каждом снятии скриншота. Выделение ресурсов происходит в методе Create, а их освобождение - в методе Close. При этом создается DIB-растр (выделяется память и заполняются поля структуры-заголовка BITMAPINFO) с разрещением, цветовой глубиной пикселя и другими параметрами, идентичными аналогичным параметрам экрана. Таким образом, между вызовами методов Create и Close может следовать серия операций получения скриншотов. Ниже приведен листинг этих методов:

Код
BOOL CCaptureHolder::Create()
{
    HDC hDCScreen = ::GetDC(NULL);

    if(hDCScreen != NULL)
    {
        m_dcScreen.Attach(hDCScreen);

        if(m_dcMem.CreateCompatibleDC(&m_dcScreen))
        {
            if(m_bmpShot.CreateCompatibleBitmap(&m_dcScreen,
                            ::GetSystemMetrics(SM_CXSCREEN),
                            ::GetSystemMetrics(SM_CYSCREEN)))
            {
                m_pOldMemDCBmp = m_dcMem.SelectObject(&m_bmpShot);

                if(CreateDIB())
                    return TRUE;
            }
        }
    }

    Close();

    return FALSE;
}

void CCaptureHolder::Close()
{

    if( m_dcScreen.GetSafeHdc() != NULL)
        ::ReleaseDC(NULL, m_dcScreen.Detach());

    if(m_dcMem.GetSafeHdc() != NULL)
    {
        if(m_pOldMemDCBmp != NULL)
        {
            m_dcMem.SelectObject(m_pOldMemDCBmp);
            m_pOldMemDCBmp = NULL;
        }

        m_dcMem.DeleteDC();
    }

    if(m_bmpShot.GetSafeHandle() != NULL)
        m_bmpShot.DeleteObject();

    if(m_pDIBData != NULL)
        delete [] m_pDIBData;

    m_pDIBData  = NULL;
    m_pDIBits   = NULL;

}

Из кода видно, что метод Create создает: экранный (m_dcScreen), совместимый (m_dcMem) контексты устройства; совместимый с экранным контекстом аппаратно-зависимый растр (m_bmpShot), выбирает этот растр в совместимый контекст, а также вызывает метод CreateDIB. Именно этот метод и производит создание "заготовки" DIB-растра (выделяет память и заполняет структуру его заголовка). Для сокращения объема текста, я не привожу здесь листинг этого метода (он не особо интересен, и вы всегда можете его увидеть, скачав исходники проекта). Гораздо больший интерес представляет непосредственно процесс получения скриншотов (методы MakeShot и GetCurrentCursorHandle).

Уже было сказано, что метод MakeShot производит снятие скриншота. Ниже приведен его листинг:

Код
#if !defined(CAPTUREBLT)
    #define CAPTUREBLT        (DWORD)0x40000000
#endif

BOOL CCaptureHolder::MakeShot()
{
    ASSERT(m_pDIBData != NULL);

    BITMAPINFOHEADER &refBmHeader = *((BITMAPINFOHEADER *) m_pDIBData);

    m_dcMem.BitBlt( 0, 0, refBmHeader.biWidth, refBmHeader.biHeight,
                    &m_dcScreen, 0, 0, SRCCOPY | CAPTUREBLT);

    // --

    POINT ptCursor;
    HCURSOR hCursor = GetCurrentCursorHandle(ptCursor);

    if(hCursor != NULL)
    {
        ICONINFO info;

        if(::GetIconInfo(hCursor, &info))
        {
            ::DrawIconEx(   m_dcMem.GetSafeHdc(),
                            ptCursor.x - info.xHotspot,
                            ptCursor.y - info.yHotspot,
                            hCursor,
                            0,
                            0,
                            0,
                            NULL,
                            DI_DEFAULTSIZE | DI_NORMAL);
        }
    }
    
    if(::GetDIBits( m_dcMem.GetSafeHdc(), (HBITMAP) m_bmpShot, 0,
                    refBmHeader.biHeight, m_pDIBits,
                    (LPBITMAPINFO) &refBmHeader, DIB_RGB_COLORS))
    {
        refBmHeader.biClrUsed = (refBmHeader.biBitCount <= 8) ?
                                    1 << refBmHeader.biBitCount : 0;

        return TRUE;
    }

    return FALSE;
}

Как можно видеть, получение снимка экрана в аппаратно-зависимый растр производится очень просто (копирование изображения с экранного контекста устройства на совместимый контекст при помощи функции BitBlt). Однако, здесь надо указать на одну немаловажную деталь - добавление флага CAPTUREBLT в код растровой операции, передаваемый в BitBlt. Это необходимо для того, чтобы в изображение при копировании включались "полупрозрачные" окна (layered windows).

Аппаратно-зависимый растр конвертируется в аппаратно-независимый при помощи функции GetDIBits. Перед конвертированием DDB растра в DIB, производится получение дескриптора текущего курсора мыши, и его отрисовка на DDB растр в позиции, соответствующей его экранным координатам. С отрисовкой, думаю, все ясно. Она производится простейшим образом (функция DrawIconEx) при этом, если курсор анимационный, всегда будет отрисовываться только первый кадр (об этом недостатке будет подробнее написано ниже). ). А о получении дескриптора курсора мыши (оно производится методом GetCurrentCursorHandle) стоит сказать несколько слов отдельно.

Существует API-функция для получения дескриптора курсора мыши, отображаемого в данный момент: GetCursor. Однако, эта функция возвращает дескриптор реально отображаемого курсора только в том случае, если курсор "принадлежит" вызывающему функцию GetCursor процессу. В противном случае возвращается дескриптор стандартного курсора (идентификатор IDC_ARROW). Начиная с Windows 98 и Windows NT 4.0 SP6, библиотека user32.dll экспортирует функцию GetCursorInfo, позволяющую получать информацию о глобальном курсоре. Эта функция описана в заголовочных файлах, поставляемых с MS Visual Studio 6.0 (приложение разрабатывалось именно в этой среде), однако, она декларируется только в случае определения макроса WINVER >= 0x0500.

Для того, чтобы осветить возможность получения глобального курсора мыши без использования функции GetCursorInfo (это актуально в Windows 95), я включил соответствующий код в метод GetCurrentCursorHandle:

Код
HCURSOR CCaptureHolder::GetCurrentCursorHandle(POINT &pt)
{

#if (WINVER >= 0x0500)

    CURSORINFO curInfo;

    curInfo.cbSize = sizeof(CURSORINFO);

    if(::GetCursorInfo(&curInfo))
    {
        pt = curInfo.ptScreenPos;
        return curInfo.hCursor;
    }

    return NULL;

#else

    HCURSOR hCursor = NULL;

    // --

    ::GetCursorPos(&pt);

    HWND hCurWnd = ::WindowFromPoint(pt);

    if(hCurWnd != NULL)
    {
        DWORD   dwThreadID          = ::GetWindowThreadProcessId(hCurWnd, NULL);
        DWORD   dwCurrentThreadID   = ::GetCurrentThreadId();
        
        if(dwCurrentThreadID != dwThreadID)
        {
            if(::AttachThreadInput(dwCurrentThreadID, dwThreadID, TRUE))
            {
                hCursor = ::GetCursor();
                ::AttachThreadInput(dwCurrentThreadID, dwThreadID, FALSE);
            }
        }
    }
    
    if(hCursor == NULL)
        hCursor = ::GetCursor();
    
    return hCursor;

#endif

}

Как видите, в случае, если значение WINVER >= 0x0500, информация о курсоре получается при помощи GetCursorInfo, иначе – используется GetCursor (при этом требуется произвести несколько дополнительных действий).

Для получения дескриптора курсора, созданного другим процессом, при помощи функции GetCursor, поток вызывающий GetCursor, может "прикинуться своим", "прицепившись" к очереди сообщений потока, создавшего окно, принимающее на данный момент ввод мыши. Для этого производится получение текущих координат курсора мыши, затем с помощью функции WindowFromPoint ищется дескриптор окна, находящегося под курсором, потом получается дескриптор потока, обслуживающего очередь сообщений данного окна и, если это - не текущий поток - производится подключение к его очереди сообщений, вызывается GetCursor и производится отключение от очереди сообщений. Если в ходе данных манипуляций дескриптор курсора не найден (в случае ошибок, либо, если окно, над которым находится курсор, принадлежит "нашему" потоку), снова вызывается GetCursor.

Примечание
Поддержка Windows 95 и Windows NT уже давно прекращена Microsoft. Поддержка Windows 98 прекратится 11 июля сего (2006-го) года (ура, товарищи !), и код, получающий дескриптор курсора через AttachThreadInput/GetCursor не особо актуален (однако сам принцип, думаю, интересен), поэтому в файле проекта StdAfx.h я определил константу WINVER как 0x0500. Таким образом, если кто то захочет скомпилировать приложение для работы в Windows 95, надо просто закомментировать это определение.

Запись AVI файлов

Для работы с видеофайлами формата AVI мы будем использовать AVIFile API функции Эта группа функций является составной частью Microsoft Video for Windows появившегося еще в 16-ти битных версиях Windows. Большинство возможностей Video for Windows впоследствии было реализовано в DirectX. Но для наших целей вышеназванные функции вполне подходят. Перед рассмотрением реализации записи AVI файлов в создаваемом приложении, надо сказать несколько слов о принципах работы с AVIFile API функциями.

Принципы работы с библиотекой AVIFile

Нашей задачей является лишь запись файлов AVI, поэтому вдаваться в детальное описание всех функций группы AVI API не имеет смысла. Здесь будет описано только то, что необходимо для решения нашей задачи. Дополнительно стоит отметить лишь, тот момент, что эти функции позволяют не только программно создавать/записывать AVI файлы. С их помощью можно производить чтение, редактирование видео данных, помещать их в буфер обмена, и получать их оттуда.

Инициализация / деинициализация библиотеки AVIFile функций

Функции AVIFile API содержатся в DLL библиотеке. Для ее инициализации необходимо вызвать функцию AVIFileInit. После инициализации можно будет производить вызовы любых функций данной группы. Для освобждения ресурсов библиотеки необходимо произвести вызов функции AVIFileExit. Библиотека AVIFile API ведет подсчет вызовов AVIFileInit и AVIFileExit (фактически это аналогично подсчету ссылок в COM). Поэтому, каждому вызову AVIFileInit должен соответствовать парный вызов AVIFileExit.

Принципы работы с AVIFile функциями

Audio-Video Interleaved (AVI) файлы представляют собой файлы в формате RIFF (Resource Information File Format). Данные в них содержатся в виде последовательности блоков (chunks). AVI-файлы обычно содержат данные нескольких типов (видео, аудио, текстовые субтитры на разных языках). Библиотека AVIFile API представляет эти данные как отдельные потоки (streams). Методы работы с потоками доступны через COM интерфейс IAVIStream. Большинство AVIFile-функций принимают указатели на интерфейсы потоков в качестве параметров. Потоки создаются (возвращаются интерфейсы IAVIStream) такими функциями как: AVIFileGetStream, AVIStreamOpenFromFile, AVIFileCreateStream, AVIFileOpen, AVIMakeCompressedStream. Для закрытия потока используется API функция AVIStreamClose.

Реализация класса работы с AVI файлами

Для записи AVI файлов был создан класс CAVIFile. Вот листинг его декларации:

Код
class CAVIFile  
{

public:

    CAVIFile();
    virtual ~CAVIFile();

public:

    BOOL Create(LPCTSTR lpszFileName, DWORD dwFrameRate, BITMAPINFO *pBmp,
                AVICOMPRESSOPTIONS *pOptions);

    void Close();

    BOOL WriteFrame(BITMAPINFO *pBmp, DWORD dwFrameNum);

protected:

    PAVIFILE m_pFile;
    PAVISTREAM m_pStream;
    PAVISTREAM m_pCompressedStream;

};

Метод Create создает AVI-файл с указанным в параметре lpszFileName именем, частотой следования кадров dwFrameRate, форматом кадра, совпадающим с форматом DIB-растра, указатель на заголовок которого передан в параметре pBmp и опциями сжатия, передаваемыми в структуре AVICOMPRESSOPTIONS через параметр pOptions.

Код
BOOL CAVIFile::Create(  LPCTSTR lpszFileName, DWORD dwFrameRate,
                        BITMAPINFO *pBmp, AVICOMPRESSOPTIONS *pOptions)
{
    if( (lpszFileName == NULL) || (dwFrameRate < 1) ||
        (pBmp == NULL) || (pOptions == NULL))
        return FALSE;

    AVISTREAMINFO sStreamInfo;

    if(AVIFileOpen( &m_pFile, lpszFileName,
                    OF_WRITE | OF_CREATE, NULL) == AVIERR_OK)
    {
        void *pBits = ( (LPBYTE) pBmp) + pBmp->bmiHeader.biSize +
                        pBmp->bmiHeader.biClrUsed * sizeof(RGBQUAD);

        memset(&sStreamInfo, 0, sizeof(sStreamInfo));

        sStreamInfo.fccType                 = streamtypeVIDEO;
        sStreamInfo.fccHandler              = 0;
        sStreamInfo.dwScale                 = 1;
        sStreamInfo.dwRate                  = dwFrameRate;
        sStreamInfo.dwSuggestedBufferSize   = pBmp->bmiHeader.biSizeImage;

        SetRect(&sStreamInfo.rcFrame, 0, 0,
                pBmp->bmiHeader.biWidth,
                pBmp->bmiHeader.biHeight);

        if(AVIFileCreateStream(m_pFile, &m_pStream, &sStreamInfo) == AVIERR_OK)
        {
            if(AVIMakeCompressedStream( &m_pCompressedStream, m_pStream,
                                        pOptions, NULL) == AVIERR_OK)
            {
                if(AVIStreamSetFormat(  m_pCompressedStream, 0, pBmp,
                                        ((LPBYTE) pBits) -
                                        ((LPBYTE) pBmp)) == AVIERR_OK)
                {
                    return TRUE;
                }
            }
        }
    }

    return FALSE;
}

Метод создает AVI-файл с помощью функции AVIFileOpen, затем, на основе информации, занесенной с поля структуры AVISTREAMINFO, создается объект потока видео-данных. Для того, чтобы при записи данные сжимались видеокомпрессором, на основе этого потока и информации из структуры AVICOMPRESSOPTIONS, создается "сжатый поток" (compressed stream) при помощи функции AVIMakeCompressedStream.

Тут стоит отметить одну немаловажную деталь. Дело в том, что структура AVICOMPRESSOPTIONS должна заполняться в процессе выбора кодека. В принципе, произвести выбор и настройку кодека средствами AVIFile API очень просто - при помощи вызова функции AVISaveOptions - она отобразит соответствующий диалог и заполнит поля структуры AVICOMPRESSOPTIONS. Однако, эта функция требует также указателя на интерфейс IAVIStream в качестве входного параметра (т.е. перед ее вызовом необходимо как минимум создать файл и объект потока данных). Однако, хотелось бы производить выбор кодека независимо от существования файла на диске (точнее, не в момент его создания), поэтому, конфигурирование кодека будет производиться другими средствами и в другом месте.

Метод Close, как, наверно, ясно из названия, производит освобождение ресурсов, занятых в методе Create.

Метод WriteFrame производит запись в файл одного кадра. Входными параметрами являются аппаратно-независимый растр (передается указатель на структуру-заголовок pBmp) и номер кадра (номера следующих один за другим кадров, могут отличаться больше чем на 1, если возникнет ситуация, при которой частота их фактической записи оказывается ниже "заявленной" частоты следования кадров). Ниже приведен листинг метода WriteFrame:

Код
BOOL CAVIFile::WriteFrame(BITMAPINFO *pBmp, DWORD dwFrameNum)
{
    if(m_pFile != NULL)
    {
        return (AVIStreamWrite( m_pCompressedStream,
                                dwFrameNum,
                                1,
                                ((LPBYTE) pBmp) + pBmp->bmiHeader.biSize +
                                pBmp->bmiHeader.biClrUsed * sizeof(RGBQUAD),
                                pBmp->bmiHeader.biSizeImage,
                                0,
                                NULL,
                                NULL) == AVIERR_OK);
    }
    
    return FALSE;
}

На этом закончим описание класса CAVIFile и приступим к рассмотрению реализации, собственно, процесса записи видео с экрана.

Программа записи видео с рабочего стола (захватываем скринщоты и записываем их в AVI-файл)

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

Процесс записи инициирует метод CMainDlg::BeginRecord, вызываемый из обработчика клика по кнопке начала записи CMainDlg::OnButtonRecord. Метод CMainDlg::BeginRecord инициализирует объект m_Capture класса CCaptureHolder, объект m_AviFile класса CAVIFile, получает значение количества миллисекунд, прошедших с момента старта Windows в переменную m_dwStartTime (как вы помните, методу CAVIFile::WriteFrame необходимо передавать номер кадра; этот номер можно высчитывать на основе разницы во времени между текущим моментом и моментом начала записи; для получения достаточно точных значений времени, будет использоваться функция timeGetTime из Multimedia SDK) и запускает таймер:

Код
void CMainDlg::BeginRecord()
{
    ...
    if(m_Capture.Create() && m_Capture.MakeShot())
    {
        if(m_AviFile.Create("capture.avi", m_dwFrameRate,
                            m_Capture.GetDIB(), &m_sCompOptions))
        {
            m_dwStartTime = timeGetTime(); // время начала записи

            m_nTimerID = SetTimer(1000, 1000 / m_dwFrameRate, NULL);
    ...

Запись останавливается вызовом метода CMainDlg::EndRecord. В нем убивается таймер, освобождаются ресурсы объектов m_Capture, m_AviFile, и.т.д.

Как уже было сказано, снятие скриншотов и их запись в AVI-файл, производятся в обработчике сообщений таймера CMainDlg::OnTimer (при этом с помощью timeGetTime получается текущее время и на его основе рассчитывается номер кадра):

Код
void (UINT nIDEvent) 
{
    if(nIDEvent == m_nTimerID)
    {
        m_Capture.MakeShot();

        DWORD dwTime = timeGetTime() - m_dwStartTime;

        if(dwTime < 0)
            dwTime = (DWORD) + dwTime;

        m_AviFile.WriteFrame(   m_Capture.GetDIB(),
                                dwTime * m_dwFrameRate / 1000);

        if(dwTime >= m_dwDuration * 1000)
            EndRecord();
    }
 
    CDialog::OnTimer(nIDEvent);
}

Собственно, о процессе записи видео с экрана - все. Единственный момент, оставшийся пока не освещенным - конфигурирование кодека.

Выбор и конфигурирование кодека

Выше указывалось, что метод CAVIFile::Create принимает параметр - указатель на структуру AVICOMPRESSOPTIONS, содержащую информацию о потоке и об опциях его сжатия. Структура AVICOMPRESSOPTIONS содержится как переменная-член m_sCompOptions в классе CMainDlg. Ее заполнение производится в обработчике события клика на кнопке конфигурирования кодека CMainDlg::OnButtonConfigCodec при помощи вызова API-функции ICCompressorChoose (подробнее см. Video Compression Manager Reference). Эта функция отображает диалог выбора и конфигурирования кодека и заносит данные в структуру COMPVARS, назначение многих полей которой совпадает с назначением полей структуры AVICOMPRESSOPTIONS. После вызова ICCompressorChoose, необходимые данные просто переносятся из переменной-структуры типа COMPVARS в структуру m_sCompOptions:

Код
void CMainDlg::OnButtonConfigCodec()
{

    ...

        COMPVARS cv;

        memset(&cv, 0, sizeof(cv));

        cv.cbSize = sizeof(cv);

        m_Capture.MakeShot();

        if(ICCompressorChoose(  GetSafeHwnd(),
                                ICMF_CHOOSE_DATARATE | ICMF_CHOOSE_KEYFRAME,
                                (LPBITMAPINFO) m_Capture.GetDIB(),
                                NULL,
                                &cv,
                                _T("Настройка кодеков")
                                ))
        {

    ...

                m_sCompOptions.fccType      = streamtypeVIDEO;
                m_sCompOptions.fccHandler   = cv.fccHandler;
    ...

На этом описание реализации закончено.

Вместо послесловия

В заключение уместно сказать о существующих недостатках и возможностях улучшения программы. Однако, тут же хотелось бы дать некоторые рекомендации по использованию приложения, позволяющие весьма эффективно применять его по прямому назначению.

Недостатки и возможности улучшения

Итак, сначала - о недостатках и переспективах улучшения.

Недостаток 1

Он, впрочем, уже упоминался. Состоит в том, что вся "запись" (захват скриншотов и компрессия) крутится в интерфейсном потоке. Из-за этого в процессе записи интерфейс может "подтормаживать".

Улучшения

Стоило бы вынести запись видео в отдельный поток с синхронизацией по времени, например, multimedia timer - ом.

Недостаток 2

Состоит в отсутствии отслеживания изменения параметров настройки дисплея.

В процедуре CMainDlg::OnButtonConfigCodec в функцию ICCompressorChoose среди прочих параметров, передается указатель на DIB-растр (скриншот, непосредственно перед этим сформированный). Это необходимо для того, чтобы в диалоге выбора кодеков присутствовали только те кодеки, которые могут обрабатывать растры данного формата. В случае, если после выбора кодека настройки дисплея были изменены, возможно возникновение ошибок при инициализации процесса записи.

Улучшения

Желательно обрабатывать сообщение WM_SETTINGCHANGE и, хотя бы, выдавать предупреждение пользователю при смене параметров настройки монитора.

Недостаток 3

О нем тоже уже упоминалось. Изображение с оверлеев захватываться не будет.

Небольшое пояснение (возможно, кому то это будет интересно). Дело в том, что современные видеокарты поддерживают создание так называемых оверлейных поверхностей (overlay surfaces). Оверлей - это аппаратные поверхность, не связанная с первичной графической поверхностью (primary surface) - ее данные находятся в области видеопамяти, отличной от области, занимаемой первичной графической поверхностью. Для оверлея задаются размеры, положение на экране и цветовой ключ (некоторое значение цвета пикселя). Если оверлей/оверлеи существуют, то при формировании изображения на мониторе, в области, в которой находится оверлей, видеокарта анализирует значение цвета пикселей первичной графической поверхности и, если оно равно значению цветового ключа оверлея, данные для вывода на монитор берутся не с первичной, а с оверлейной поверхности.

У оверлеев есть множество применений. Например, они традиционно используются различными приложениями-видеопроигрывателями. Преимущество использования оверлеев в данном случае заключается в отсутствии необходимости обновления изображения текущего кадра (например, при перекрытии окна видеопроигрывателя другим окном). Изображение просто выводится на оверлей, оверлей позиционируется в соответствии с положением окна проигрывателя, а окно проигрывателя заливается однородным редко используемым цветом (например, RGB(16, 0, 16)), значение которого задано в качестве цветового ключа оверлея.

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

Улучшения

Простого способа захватить изображение с оверлея, созданного чужим процессом, видимо, не существует. Одно из решений - внедрять свой код в процесс, создавший оверлей, а там средствами DirectX производить поиск оверлеев. Затем, если оверлей найден, можно скопировать с него изображение с передать его в наше приложение для добавления к скриншоту. Ввиду того, что неизвестно, какие приложения создали, или создадут оверлеи, внедряться придется в большинство процессов. Одним словом, данный способ сложный и мало приемлемый.

Примечание
О том, как все-таки записать видео с дисплея, включающее изображение проигрываемого фильма, можно прочитать далее (см. Практические советы по использованию приложения).

Недостаток 4

Не поддерживаются анимационные курсоры.

В методе CCaptureHolder::MakeShot для отрисовки курсора на полученном скриншоте, используется функция DrawIconEx. Один их ее параметров - номер кадра, если выводимое изображение является анимационным курсором. В CCaptureHolder::MakeShot этот параметр всегда устанавливается равным 0. Дело в том, что я не нашел ни одной документированной возможности получения полной информации об анимационном курсоре, зная только его дескриптор (количество кадров, частота их следования, индивидуальная задержка для каждого кадра).

Вероятно, недокументированная API функция GetCursorFrameInfo, экспортируемая из user32.dll, возвращает необходимую информацию, но я нигде не нашел ее описания.

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

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

Практические советы по использованию программы

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

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

Далее, следует обратить особое внимание на кодеки. Собственно, именно используемый кодек оказывает определяющее влияние на общую скорость записи.

Большинство кодеков реализуют сжатие изображения с потерей качества. Их алгоритмы кодирования достаточно "тяжеловесны". Поэтому при их использовании не добиться ни нормальной скорости записи, ни нормального качества. Из кодеков данной категории достаточно эффективно мне удалось использовать лишь Microsoft Video 1.

Использование кодека Microsoft Video 1

Выбираем в диалоге выбора кодека "Microsoft Video 1". Открываем диалог "настройка". В нем снимаем отметку "скорость передачи", а параметр "Качество сжатия" устанавливаем на 100.

AVI файл при записи получается большого объема, однако, качество изображения при этом - очень высокое. После записи получившийся файл можно пережать другим кодеком для уменьшения объема.

Существуют также кодеки, осуществляющие сжатие изображения без потерь. Один из таких кодеков - CamStudio Lossless Codec.

Использование кодеков CamStudio Lossless Codec

Помните, в самом начале статьи я писал, что при записи можно производить сжатие "легким" алгоритмом, а потом, после окончания записи, пережимать эти данные кодеком ? Так вот, CamStudio Lossless Codec как раз и реализует один из таких "легких" алгоритмов, но это именно кодек (!), поэтому его можно использовать непосредственно с нашей программой.

Вот его основные характеристики:

  • Реализованы два алгоритма сжатия:
    • "легкий" LZO, приспособленный именно для использования с программами захвата видео
    • GZIP, для последующего "пережатия" видео, записанного с использованием первого кодека
    Каждый из этих алгоритмов сжимают изображение без потери качества (т.к. это, вобщем то, не алгоритмы сжатия изображений, а алгоритмы архивирования данных), поэтому видео получается просто отменное.
  • Кодек "умеет" сжимать 16, 24 и 32-х битные RGB растры.
  • Этот кодек распространяется под лицензией GPL (GNU GENERAL PUBLIC LICENSE). Скачать его можно, например, тут: http://www.camstudio.org/CamStudioCodec10.zip

К слову, есть и другие кодеки с аналогичной функциональностью. Правда, попробовав один из них (MSU Lossless Video Codec 0.6.0) и получив кучу ошибок в процессе записи видео, я остановился на использовании кодека CamStudio.

Еще один момент, на котором хотелось бы остановить внимание - разрешение экрана монитора. Ясно, что чем оно меньше, тем более быстро производится сжатие каждого кадра. На самом деле, при создании демонстрационных клипов, большое разрешение не особо актуально - гораздо важнее качество записи. Наверно, не имеет особого смысла выставлять разрешение больше 1024x768 пикселей (иначе просто не удобно будет впоследствии просматривать клип). Для более ясной картины в таблице 1.1 просто приведу результаты, полученные мной при тестировании программы на моем Athlon XP 1600 (тактовая частота 1400 MHz) с использованием кодека CamStudio, алгоритм LZO.
Таблица 1.1
Разрешение Frame Rate (кадр/сек) Загрузка процессора (%)
800x600 16 45-55
1024x768 12 70-77
1280x1024 8 85-92

Чего не стоит делать в процессе работы приложения, так это - менять настройки дисплея во время записи - ясно, что будут глюки. Если менять разрешение, то при изменении в меньшую сторону получите меняющееся изображение рабочего стола при меньшем разрешении на фоне последнего кадра, снятого при более высоком разрешении. Если менять разрешение в большую сторону, то получим "обрезанную" картинку. При смене глубины цвета (скажем, с 32 бит до 8 бит), скорее всего, начнутся "тормоза" (я пробовал менять с 32 до 16 бит - вроде, ничего особого не произошло).

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

На этом позвольте закончить.

Книги по C/C++
SmogDX - объектно-ориентированная графика для Windows (DirectX и Visual C++)
SmogDX - объектно-ориентированная графика для Windows (DirectX и Visual C++)

Автор: В. А. Дебелов, Ю. А. Ткачев

Книга посвящена вопросам программирования динамических графических приложений в среде MS Windows на базе суперсистемы DirectX фирмы Microsoft.

Авторы разработали и представили объектно-ориентированную оболочку для основных графических частей:
  • DirectDraw - двумерной динамической графики и
  • Direct3D - трехмерной динамической графики.

При написании книги авторы избрали конструктивный подход, они вместе с читателем строят новую систему SmogDX и при этом изучают базовое обеспечение - DirectX. Таким образом, читатель не только знакомится с системой SmogDX, но и изучает наиболее существенные функциональные средства DirectX. В связи с этим данную книгу можно рассматривать и как учебное пособие по введению в программирование на DirectDraw и Direct3D.

Книга заполняет нишу литературы и пособий на русском языке по применению DirectX, ориентирована на широкий круг программистов разного уровня подготовки: школьников, студентов, научных работников.

Страница книги на Озоне
Интерактивная компьютерная графика. Вводный курс на базе OpenGL, 2-е изд.
Интерактивная компьютерная графика. Вводный курс на базе OpenGL, 2-е изд.

Автор: Эдвард Энджел

Книга представляет собой вводный курс компьютерной графики, в котором основной упор сделан на вопросах прикладного программирования. Она включает описание структуры графических систем и обсуждение основных концепции формирования изображений трехмерных объектов и сцен. Рассматривается взаимодействие освещения и материалов, также приводятся основные сведения о методах тонирования освещенных поверхностей, принципах иерархической организации графических моделей и новых возможностях современных аппаратных графических средств. В книгу включены те разделы линейной алгебры и геометрии, которые необходимы для понимания основ компьютерной графики. Обсуждаются методы построения кривых и поверхностей, языковые модели, фракталы и системы частиц, а также методика применения графических средств для визуализации результатов научных расчетов.

Весь теоретический материал в книге иллюстрируется программами на OpenGL.

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

Страница книги на Озоне
Рассылки и дискуссионные листы компьютерной тематики
Рассылки
C/C++ Вопрос-Ответ

Это - интерактивная рассылка !
Здесь Вы можете задать свой вопрос по программированию на C и C++, а также ответить на вопросы других подписчиков.


Программирование на JavaScript

Все аспекты программирования на JavaScript - нестандартные приемы, ОРИГИНАЛЬНЫЕ скрипты, авторские статьи и наработки. "JavaScript solutions" - в каждом выпуске готовый к применению ИНТЕРЕСНЫЙ скрипт (исходный код с комментариями).

Дискуссионные листы
Программирование. Форум !!!

Самый популярный дискуссионный лист по программированию на subscribe.ru, существующий с момента открытия сервиса дискуссионных листов !

Задайте здесь любой вопрос по программированию - и Вы получите ответ. Участвуйте в оживленных дискуссиях, обсуждайте интересные темы. Давайте ответы сами. Ведь это форум !!! Здесь просто интересно ! Присоединяйтесь !

Вебстроительство. Форум !!!

В дискуссионном листе ведется обсуждение различных аспектов создания сайтов - написание скриптов, использование, настройка портальных движков, и многое другое. Присоединяйтесь !

Поисковые системы. Форум !!!

Этот дискуссионный лист посвящен обсуждению поиковых систем, методов индексации сайтов поисковиками, способам оптимизации сайта под поисковые системы.

Хостинг. Обзоры и обсуждения платного и бесплатного хостинга.

Вы ищете хостинг (платный, бесплатный) ? Хотите спросить совета в выборе ? Можете обсудить это здесь. Поделитесь советом, если знаете. Или узнайте больше. Все о хостинге.


Всего доброго. До встречи в следующем номере.

В избранное