Pull to refresh

Скриншот видеоплеера без «черных» дыр

Reading time9 min
Views6.5K
На фоне нынешних рассуждений о том, «справится ли мой телефон/калькулятор/часы с проигрыванием HD-видео» отошел на задний план интересный исторический факт: немногим более 10 лет назад проблемой была даже не скорость декодирования видео (теплый ламповый MPEG декодировался тогда отдельными аппаратными декодерами; остальные перебивались возможностями имеющегося ЦП), а скорость его вывода на экран. Многоэкранная среда убивает на корню идею переноса блока памяти с декодированным изображением в видеопамять, ведь окна могут перекрывать друг друга самым причудливым образом.

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

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

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

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

Метод, предложенный ниже решает данную задачу для плееров, выводящих видео через DirectShow.

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

Первая задача решается несколькими способами, я выбрал самый простой: через установку хука (hook). Тип хука не так важен, главное — попасть в адресное пространство плеера. Например, так:

void WINAPI SetHook(HWND hWnd)
{
  if (!g_hhHook)
  {
    g_hhHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)HookProc, hInst, 0);
  }
}


* This source code was highlighted with Source Code Highlighter.


Процедура, получающая управление при срабатывании хука стандартная «ничегонеделающая».

LRESULT WINAPI HookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
  if (nCode < 0)
  {
return CallNextHookEx(g_hhHook, nCode, wParam, lParam);
  }
// Здесь мы можем что-нибудь сделать, но не будем - незачем
  return CallNextHookEx(g_hhHook, nCode, wParam, lParam);
}


* This source code was highlighted with Source Code Highlighter.


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

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

Та самая область памяти, в которую осуществляется копирование, выделяется объектом IDirectDrawSurface (она же «DirectDraw-поверхность»), которая создана с флагом DDSCAPS_OVERLAY. Переносом же изображения на экран, упрощенно говоря, занимается метод IDirectDrawSurface::Flip. Значит, перехватив вызов данного метода, можно получить доступ к данным с изображением видеокадра. О перехвате вызовов сказано немало, например. В свое время я воспользовался методом номер один из этой статьи, как наиболее простым. Единственное препятствие: получить адрес метода COM-объекта вызовом GetProcAddress не удастся. Это не так страшно, ведь можно создать DirectDraw-поверхность, и узнать положение метода Flip относительно базового адреса исполняемого файла, в который было произведено внедрение. Сделать это можно следующим образом:

// Создадим объект IDirectDraw

LPDIRECTDRAW pDirectDraw;
hr = DirectDrawCreate(NULL, (&pDirectDraw), NULL);

hr = pDirectDraw->SetCooperativeLevel(NULL, DDSCL_NORMAL);

DDSURFACEDESC desc = {0};
desc.dwSize = sizeof(desc);
desc.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH;
desc.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
desc.dwWidth = 100;
desc.dwHeight = 100;

// И вспомогательную поверхность IDirectDrawSurface

LPDIRECTDRAWSURFACE Surf;
hr = pDirectDraw->CreateSurface(&desc, &Surf, NULL);

void *pFlip = (*reinterpret_cast<void***>(Surf))[11]; // 11 - номер метода IDirectDrawSurface::Flip в vtable
ptrdiff_t pDDSFlipDiff = reinterpret_cast<ptrdiff_t>(pFlip) - reinterpret_cast<ptrdiff_t>(GetModuleHandle(_T("ddraw.dll")));


* This source code was highlighted with Source Code Highlighter.


Для определения положения и размера оверлейной поверхности понадобится метод IDirectDrawSurface::UpdateOverlay, который имеет номер метода 33.
После этого можно произвести перехват методов Flip и UpdateOverlay, указав адреса оригинального и модифицированного метода в процедуре установки перехватчика. Прототипы методов выглядят так:

typedef HRESULT (WINAPI *FUNC_IDIRECTDRAWSURFACEFLIP)(IDirectDrawSurface *This, LPDIRECTDRAWSURFACE lpDDSurfTargetOverride, DWORD dwFlags);

typedef HRESULT (WINAPI *FUNC_IDIRECTDRAWSURFACEUPDATEOVERLAY)(IDirectDrawSurface *This, LPRECT lpSrcRect, LPDIRECTDRAWSURFACE lpDDDestSurface, LPRECT lpDestRect, DWORD dwFlags, LPDDOVERLAYFX lpDDOverlayFx);


* This source code was highlighted with Source Code Highlighter.


Обратите внимание на то, что в отличие от списка параметров методов Flip и UpdateOverlay в прототипах первым в списке аргументов записан указатель на COM-объект, содержащий данный метод. Это связано с тем, что методы COM-объектов имеют тип вызова thiscall.

Аналогичные действия следует предпринять и для методов IDirectDrawSurface7::Flip и IDirectDrawSurface7::UpdateOverlay, так как некоторые рендереры используют интерфейсы седьмой версии DirectDraw.

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

HRESULT WINAPI Patch_UO(IDirectDrawSurface *This, LPRECT lpSrcRect, LPDIRECTDRAWSURFACE lpDDDestSurface, LPRECT lpDestRect, DWORD dwFlags, LPDDOVERLAYFX lpDDOverlayFx)
{
  DetourRestore(True_UO); // Восстанавливаем оригинальное состояние метода
  HRESULT res = True_UO(This, lpSrcRect, lpDDDestSurface, lpDestRect, dwFlags, lpDDOverlayFx);
  DetourRenew(True_UO); // Устанавливаем перехватчик

  DDCOLORKEY ck = {0};
  if (dwFlags & DDOVER_KEYDEST) // Значение цвета фона хранится в поверхности назначения
  {
    DDCOLORKEY ck2 = {0};
    lpDDDestSurface->GetColorKey(DDCKEY_DESTOVERLAY, &ck2);
    g_ColorKey = ck2.dwColorSpaceHighValue;
  }
  if (dwFlags & DDOVER_KEYDESTOVERRIDE) // Значение цвета фона хранится в поверхности эффектов
  {
    if (lpDDOverlayFx != NULL)
    {
      ck = lpDDOverlayFx->dckDestColorkey;
      g_ColorKey = ck.dwColorSpaceHighValue;
    }
  }

  if (lpDestRect != NULL)
  {
    CopyMemory(&g_OverlayRect, lpDestRect, sizeof(RECT));
  }
  
  return res;
}


* This source code was highlighted with Source Code Highlighter.


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

Теперь можно «украсть» само изображение.
HRESULT WINAPI Patch_Flip(IDirectDrawSurface *This, LPDIRECTDRAWSURFACE lpDDSurfTargetOverride, DWORD dwFlags)
{
  DDSURFACEDESC desc = {0};
  desc.dwSize = sizeof(desc);
  This->GetSurfaceDesc(&desc);

  // Проверяем, помечена ли поверхность как оверлейная
  if ((desc.ddsCaps.dwCaps & DDSCAPS_OVERLAY) > 0)
  {
    lpOverlaySurf = This;
  }

  // Если прошло успешно, значит пора забирать картинку
  if (lpOverlaySurf != NULL)
  {
    GetPic();
  }

  // Восстановим оригинальный метод
  DetourRestore(True_Flip);
  // Сделаем вид, что нас тут не было
  HRESULT res = True_Flip(This, lpDDSurfTargetOverride, dwFlags);
  // Но это был только вид
  DetourRenew(True_Flip);
  
  return res;
}


* This source code was highlighted with Source Code Highlighter.


Итак, после проверки поверхности на «оверлейность» можно приступать к выуживанию изображения, этим занимается функция GetPic(), которая в укороченном варианте выглядит примерно так:

void WINAPI GetPic(void)
{  
  DDSURFACEDESC2 desc = {0};
  desc.dwSize = sizeof(desc);

  DDSURFACEDESC desc1 = {0};
  desc1.dwSize = sizeof(desc1);

  // Вызов метода Lock предоставит нам доступ к памяти, которая и содержит вожделенную картинку
  ((LPDIRECTDRAWSURFACE)lpOverlaySurf)->Lock(NULL, &desc1, DDLOCK_WAIT | DDLOCK_READONLY | DDLOCK_SURFACEMEMORYPTR | DDLOCK_NOSYSLOCK, NULL);
    desc.ddsCaps.dwCaps = desc1.ddsCaps.dwCaps;
    desc.lpSurface = desc1.lpSurface;
    desc.dwWidth = desc1.dwWidth;
    desc.dwHeight = desc1.dwHeight;
    desc.lPitch = desc1.lPitch;
    desc.ddpfPixelFormat = desc1.ddpfPixelFormat;
    desc.ddckCKDestOverlay = desc1.ddckCKDestOverlay;
    desc.ddckCKSrcOverlay = desc1.ddckCKSrcOverlay;

  // Перестанем быть эгоистами, отдадим поверхность другим
  ((LPDIRECTDRAWSURFACE)lpOverlaySurf)->Unlock(NULL);
}


* This source code was highlighted with Source Code Highlighter.


Метод Lock блокирует поверхность для записи, и заодно возвращает указатель на изображение. Оттуда его следует как можно быстрее забрать и разблокировать поверхность, чтобы не смущать видеоплеер своим присутствием. Метод IDirectDrawSurface::Lock возвращает описание поверхности в структуре DDSURFACEDESC, в то время как IDirectDrawSurface7::Lock — в DDSURFACEDESC2. Отсюда некоторая чехарда с копированием данных из одной структуры в другую.
Данные находятся по указателю desc.lpSurface, а размер этих данных вычисляется в зависимости от того, в какой цветовой модели эти самые данные хранятся.

      if (desc.ddpfPixelFormat.dwFourCC == 0x0)
      {
        DataLen = desc.dwHeight * desc.lPitch * desc.ddpfPixelFormat.dwRGBBitCount >> 3;
      }
      if (desc.ddpfPixelFormat.dwFourCC == MAKEFOURCC('Y', 'V', '1', '2'))
      {
        DataLen = desc.dwHeight * desc.lPitch * desc.ddpfPixelFormat.dwYUVBitCount >> 3;
      }
      if (desc.ddpfPixelFormat.dwFourCC == MAKEFOURCC('Y', 'U', 'Y', '2'))
      {
        DataLen = desc.dwHeight * desc.lPitch * desc.ddpfPixelFormat.dwYUVBitCount >> 4;
      }
      if (desc.ddpfPixelFormat.dwFourCC == MAKEFOURCC('Y', 'V', 'Y', 'U'))
      {
        DataLen = desc.dwHeight * desc.lPitch * desc.ddpfPixelFormat.dwYUVBitCount >> 4;
      }
      if (desc.ddpfPixelFormat.dwFourCC == MAKEFOURCC('U', 'Y', 'V', 'Y'))
      {
        DataLen = desc.dwHeight * desc.lPitch * desc.ddpfPixelFormat.dwYUVBitCount >> 4;
      }


* This source code was highlighted with Source Code Highlighter.


Здесь dwHeight — высота изображения, lPitch — сдвиг в байтах до начала следующей строки, dwYUVBitCount — число бит на пиксел изображения.

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

В заключительной части статьи речь пойдет о преобразованиях цветовых моделей и композиции скриншота из двух изображений: скриншота с «дыркой» и кадра из видео.
Tags:
Hubs:
Total votes 117: ↑105 and ↓12+93
Comments28

Articles