2 * GDI video grab interface
4 * This file is part of FFmpeg.
6 * Copyright (C) 2013 Calvin Walton <calvin.walton@kepstin.ca>
7 * Copyright (C) 2007-2010 Christophe Gisquet <word1.word2@gmail.com>
9 * FFmpeg is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU Lesser General Public License
11 * as published by the Free Software Foundation; either version 2.1
12 * of the License, or (at your option) any later version.
14 * FFmpeg is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Lesser General Public License for more details.
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with FFmpeg; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
26 * GDI frame device demuxer
27 * @author Calvin Walton <calvin.walton@kepstin.ca>
28 * @author Christophe Gisquet <word1.word2@gmail.com>
32 #include "libavformat/internal.h"
33 #include "libavutil/opt.h"
34 #include "libavutil/time.h"
38 * GDI Device Demuxer context
41 const AVClass
*class; /**< Class for private options */
43 int frame_size
; /**< Size in bytes of the frame pixel data */
44 int header_size
; /**< Size in bytes of the DIB header */
45 AVRational time_base
; /**< Time base */
46 int64_t time_frame
; /**< Current time */
48 int draw_mouse
; /**< Draw mouse cursor (private option) */
49 int show_region
; /**< Draw border (private option) */
50 AVRational framerate
; /**< Capture framerate (private option) */
51 int width
; /**< Width of the grab frame (private option) */
52 int height
; /**< Height of the grab frame (private option) */
53 int offset_x
; /**< Capture x offset (private option) */
54 int offset_y
; /**< Capture y offset (private option) */
56 HWND hwnd
; /**< Handle of the window for the grab */
57 HDC source_hdc
; /**< Source device context */
58 HDC dest_hdc
; /**< Destination, source-compatible DC */
59 BITMAPINFO bmi
; /**< Information describing DIB format */
60 HBITMAP hbmp
; /**< Information on the bitmap captured */
61 void *buffer
; /**< The buffer containing the bitmap image data */
62 RECT clip_rect
; /**< The subarea of the screen or window to clip */
64 HWND region_hwnd
; /**< Handle of the region border window */
66 int cursor_error_printed
;
69 #define WIN32_API_ERROR(str) \
70 av_log(s1, AV_LOG_ERROR, str " (error %li)\n", GetLastError())
72 #define REGION_WND_BORDER 3
75 * Callback to handle Windows messages for the region outline window.
77 * In particular, this handles painting the frame rectangle.
79 * @param hwnd The region outline window handle.
80 * @param msg The Windows message.
81 * @param wparam First Windows message parameter.
82 * @param lparam Second Windows message parameter.
83 * @return 0 success, !0 failure
85 static LRESULT CALLBACK
86 gdigrab_region_wnd_proc(HWND hwnd
, UINT msg
, WPARAM wparam
, LPARAM lparam
)
94 hdc
= BeginPaint(hwnd
, &ps
);
96 GetClientRect(hwnd
, &rect
);
97 FrameRect(hdc
, &rect
, GetStockObject(BLACK_BRUSH
));
99 rect
.left
++; rect
.top
++; rect
.right
--; rect
.bottom
--;
100 FrameRect(hdc
, &rect
, GetStockObject(WHITE_BRUSH
));
102 rect
.left
++; rect
.top
++; rect
.right
--; rect
.bottom
--;
103 FrameRect(hdc
, &rect
, GetStockObject(BLACK_BRUSH
));
108 return DefWindowProc(hwnd
, msg
, wparam
, lparam
);
114 * Initialize the region outline window.
116 * @param s1 The format context.
117 * @param gdigrab gdigrab context.
118 * @return 0 success, !0 failure
121 gdigrab_region_wnd_init(AVFormatContext
*s1
, struct gdigrab
*gdigrab
)
124 RECT rect
= gdigrab
->clip_rect
;
126 HRGN region_interior
= NULL
;
128 DWORD style
= WS_POPUP
| WS_VISIBLE
;
129 DWORD ex
= WS_EX_TOOLWINDOW
| WS_EX_TOPMOST
| WS_EX_TRANSPARENT
;
131 rect
.left
-= REGION_WND_BORDER
; rect
.top
-= REGION_WND_BORDER
;
132 rect
.right
+= REGION_WND_BORDER
; rect
.bottom
+= REGION_WND_BORDER
;
134 AdjustWindowRectEx(&rect
, style
, FALSE
, ex
);
136 // Create a window with no owner; use WC_DIALOG instead of writing a custom
138 hwnd
= CreateWindowEx(ex
, WC_DIALOG
, NULL
, style
, rect
.left
, rect
.top
,
139 rect
.right
- rect
.left
, rect
.bottom
- rect
.top
,
140 NULL
, NULL
, NULL
, NULL
);
142 WIN32_API_ERROR("Could not create region display window");
146 // Set the window shape to only include the border area
147 GetClientRect(hwnd
, &rect
);
148 region
= CreateRectRgn(0, 0,
149 rect
.right
- rect
.left
, rect
.bottom
- rect
.top
);
150 region_interior
= CreateRectRgn(REGION_WND_BORDER
, REGION_WND_BORDER
,
151 rect
.right
- rect
.left
- REGION_WND_BORDER
,
152 rect
.bottom
- rect
.top
- REGION_WND_BORDER
);
153 CombineRgn(region
, region
, region_interior
, RGN_DIFF
);
154 if (!SetWindowRgn(hwnd
, region
, FALSE
)) {
155 WIN32_API_ERROR("Could not set window region");
158 // The "region" memory is now owned by the window
160 DeleteObject(region_interior
);
162 SetWindowLongPtr(hwnd
, GWLP_WNDPROC
, (LONG_PTR
) gdigrab_region_wnd_proc
);
164 ShowWindow(hwnd
, SW_SHOW
);
166 gdigrab
->region_hwnd
= hwnd
;
172 DeleteObject(region
);
174 DeleteObject(region_interior
);
181 * Cleanup/free the region outline window.
183 * @param s1 The format context.
184 * @param gdigrab gdigrab context.
187 gdigrab_region_wnd_destroy(AVFormatContext
*s1
, struct gdigrab
*gdigrab
)
189 if (gdigrab
->region_hwnd
)
190 DestroyWindow(gdigrab
->region_hwnd
);
191 gdigrab
->region_hwnd
= NULL
;
195 * Process the Windows message queue.
197 * This is important to prevent Windows from thinking the window has become
198 * unresponsive. As well, things like WM_PAINT (to actually draw the window
199 * contents) are handled from the message queue context.
201 * @param s1 The format context.
202 * @param gdigrab gdigrab context.
205 gdigrab_region_wnd_update(AVFormatContext
*s1
, struct gdigrab
*gdigrab
)
207 HWND hwnd
= gdigrab
->region_hwnd
;
210 while (PeekMessage(&msg
, hwnd
, 0, 0, PM_REMOVE
)) {
211 DispatchMessage(&msg
);
216 * Initializes the gdi grab device demuxer (public device demuxer API).
218 * @param s1 Context from avformat core
219 * @return AVERROR_IO error, 0 success
222 gdigrab_read_header(AVFormatContext
*s1
)
224 struct gdigrab
*gdigrab
= s1
->priv_data
;
227 HDC source_hdc
= NULL
;
233 const char *filename
= s1
->url
;
234 const char *name
= NULL
;
247 if (!strncmp(filename
, "title=", 6)) {
249 hwnd
= FindWindow(NULL
, name
);
251 av_log(s1
, AV_LOG_ERROR
,
252 "Can't find window '%s', aborting.\n", name
);
256 if (gdigrab
->show_region
) {
257 av_log(s1
, AV_LOG_WARNING
,
258 "Can't show region when grabbing a window.\n");
259 gdigrab
->show_region
= 0;
261 } else if (!strcmp(filename
, "desktop")) {
264 av_log(s1
, AV_LOG_ERROR
,
265 "Please use \"desktop\" or \"title=<windowname>\" to specify your target.\n");
270 /* This will get the device context for the selected window, or if
271 * none, the primary screen */
272 source_hdc
= GetDC(hwnd
);
274 WIN32_API_ERROR("Couldn't get window device context");
278 bpp
= GetDeviceCaps(source_hdc
, BITSPIXEL
);
280 horzres
= GetDeviceCaps(source_hdc
, HORZRES
);
281 vertres
= GetDeviceCaps(source_hdc
, VERTRES
);
282 desktophorzres
= GetDeviceCaps(source_hdc
, DESKTOPHORZRES
);
283 desktopvertres
= GetDeviceCaps(source_hdc
, DESKTOPVERTRES
);
286 GetClientRect(hwnd
, &virtual_rect
);
287 /* window -- get the right height and width for scaling DPI */
288 virtual_rect
.left
= virtual_rect
.left
* desktophorzres
/ horzres
;
289 virtual_rect
.right
= virtual_rect
.right
* desktophorzres
/ horzres
;
290 virtual_rect
.top
= virtual_rect
.top
* desktopvertres
/ vertres
;
291 virtual_rect
.bottom
= virtual_rect
.bottom
* desktopvertres
/ vertres
;
293 /* desktop -- get the right height and width for scaling DPI */
294 virtual_rect
.left
= GetSystemMetrics(SM_XVIRTUALSCREEN
);
295 virtual_rect
.top
= GetSystemMetrics(SM_YVIRTUALSCREEN
);
296 virtual_rect
.right
= (virtual_rect
.left
+ GetSystemMetrics(SM_CXVIRTUALSCREEN
)) * desktophorzres
/ horzres
;
297 virtual_rect
.bottom
= (virtual_rect
.top
+ GetSystemMetrics(SM_CYVIRTUALSCREEN
)) * desktopvertres
/ vertres
;
300 /* If no width or height set, use full screen/window area */
301 if (!gdigrab
->width
|| !gdigrab
->height
) {
302 clip_rect
.left
= virtual_rect
.left
;
303 clip_rect
.top
= virtual_rect
.top
;
304 clip_rect
.right
= virtual_rect
.right
;
305 clip_rect
.bottom
= virtual_rect
.bottom
;
307 clip_rect
.left
= gdigrab
->offset_x
;
308 clip_rect
.top
= gdigrab
->offset_y
;
309 clip_rect
.right
= gdigrab
->width
+ gdigrab
->offset_x
;
310 clip_rect
.bottom
= gdigrab
->height
+ gdigrab
->offset_y
;
313 if (clip_rect
.left
< virtual_rect
.left
||
314 clip_rect
.top
< virtual_rect
.top
||
315 clip_rect
.right
> virtual_rect
.right
||
316 clip_rect
.bottom
> virtual_rect
.bottom
) {
317 av_log(s1
, AV_LOG_ERROR
,
318 "Capture area (%li,%li),(%li,%li) extends outside window area (%li,%li),(%li,%li)",
319 clip_rect
.left
, clip_rect
.top
,
320 clip_rect
.right
, clip_rect
.bottom
,
321 virtual_rect
.left
, virtual_rect
.top
,
322 virtual_rect
.right
, virtual_rect
.bottom
);
329 av_log(s1
, AV_LOG_INFO
,
330 "Found window %s, capturing %lix%lix%i at (%li,%li)\n",
332 clip_rect
.right
- clip_rect
.left
,
333 clip_rect
.bottom
- clip_rect
.top
,
334 bpp
, clip_rect
.left
, clip_rect
.top
);
336 av_log(s1
, AV_LOG_INFO
,
337 "Capturing whole desktop as %lix%lix%i at (%li,%li)\n",
338 clip_rect
.right
- clip_rect
.left
,
339 clip_rect
.bottom
- clip_rect
.top
,
340 bpp
, clip_rect
.left
, clip_rect
.top
);
343 if (clip_rect
.right
- clip_rect
.left
<= 0 ||
344 clip_rect
.bottom
- clip_rect
.top
<= 0 || bpp
%8) {
345 av_log(s1
, AV_LOG_ERROR
, "Invalid properties, aborting\n");
350 dest_hdc
= CreateCompatibleDC(source_hdc
);
352 WIN32_API_ERROR("Screen DC CreateCompatibleDC");
357 /* Create a DIB and select it into the dest_hdc */
358 bmi
.bmiHeader
.biSize
= sizeof(BITMAPINFOHEADER
);
359 bmi
.bmiHeader
.biWidth
= clip_rect
.right
- clip_rect
.left
;
360 bmi
.bmiHeader
.biHeight
= -(clip_rect
.bottom
- clip_rect
.top
);
361 bmi
.bmiHeader
.biPlanes
= 1;
362 bmi
.bmiHeader
.biBitCount
= bpp
;
363 bmi
.bmiHeader
.biCompression
= BI_RGB
;
364 bmi
.bmiHeader
.biSizeImage
= 0;
365 bmi
.bmiHeader
.biXPelsPerMeter
= 0;
366 bmi
.bmiHeader
.biYPelsPerMeter
= 0;
367 bmi
.bmiHeader
.biClrUsed
= 0;
368 bmi
.bmiHeader
.biClrImportant
= 0;
369 hbmp
= CreateDIBSection(dest_hdc
, &bmi
, DIB_RGB_COLORS
,
372 WIN32_API_ERROR("Creating DIB Section");
377 if (!SelectObject(dest_hdc
, hbmp
)) {
378 WIN32_API_ERROR("SelectObject");
383 /* Get info from the bitmap */
384 GetObject(hbmp
, sizeof(BITMAP
), &bmp
);
386 st
= avformat_new_stream(s1
, NULL
);
388 ret
= AVERROR(ENOMEM
);
391 avpriv_set_pts_info(st
, 64, 1, 1000000); /* 64 bits pts in us */
393 gdigrab
->frame_size
= bmp
.bmWidthBytes
* bmp
.bmHeight
* bmp
.bmPlanes
;
394 gdigrab
->header_size
= sizeof(BITMAPFILEHEADER
) + sizeof(BITMAPINFOHEADER
) +
395 (bpp
<= 8 ? (1 << bpp
) : 0) * sizeof(RGBQUAD
) /* palette size */;
396 gdigrab
->time_base
= av_inv_q(gdigrab
->framerate
);
397 gdigrab
->time_frame
= av_gettime() / av_q2d(gdigrab
->time_base
);
399 gdigrab
->hwnd
= hwnd
;
400 gdigrab
->source_hdc
= source_hdc
;
401 gdigrab
->dest_hdc
= dest_hdc
;
402 gdigrab
->hbmp
= hbmp
;
404 gdigrab
->buffer
= buffer
;
405 gdigrab
->clip_rect
= clip_rect
;
407 gdigrab
->cursor_error_printed
= 0;
409 if (gdigrab
->show_region
) {
410 if (gdigrab_region_wnd_init(s1
, gdigrab
)) {
416 st
->avg_frame_rate
= av_inv_q(gdigrab
->time_base
);
418 st
->codecpar
->codec_type
= AVMEDIA_TYPE_VIDEO
;
419 st
->codecpar
->codec_id
= AV_CODEC_ID_BMP
;
420 st
->codecpar
->bit_rate
= (gdigrab
->header_size
+ gdigrab
->frame_size
) * 1/av_q2d(gdigrab
->time_base
) * 8;
426 ReleaseDC(hwnd
, source_hdc
);
432 DeleteDC(source_hdc
);
437 * Paints a mouse pointer in a Win32 image.
439 * @param s1 Context of the log information
440 * @param s Current grad structure
442 static void paint_mouse_pointer(AVFormatContext
*s1
, struct gdigrab
*gdigrab
)
446 #define CURSOR_ERROR(str) \
447 if (!gdigrab->cursor_error_printed) { \
448 WIN32_API_ERROR(str); \
449 gdigrab->cursor_error_printed = 1; \
452 ci
.cbSize
= sizeof(ci
);
454 if (GetCursorInfo(&ci
)) {
455 HCURSOR icon
= CopyCursor(ci
.hCursor
);
458 RECT clip_rect
= gdigrab
->clip_rect
;
459 HWND hwnd
= gdigrab
->hwnd
;
460 int horzres
= GetDeviceCaps(gdigrab
->source_hdc
, HORZRES
);
461 int vertres
= GetDeviceCaps(gdigrab
->source_hdc
, VERTRES
);
462 int desktophorzres
= GetDeviceCaps(gdigrab
->source_hdc
, DESKTOPHORZRES
);
463 int desktopvertres
= GetDeviceCaps(gdigrab
->source_hdc
, DESKTOPVERTRES
);
465 info
.hbmColor
= NULL
;
467 if (ci
.flags
!= CURSOR_SHOWING
)
471 /* Use the standard arrow cursor as a fallback.
472 * You'll probably only hit this in Wine, which can't fetch
473 * the current system cursor. */
474 icon
= CopyCursor(LoadCursor(NULL
, IDC_ARROW
));
477 if (!GetIconInfo(icon
, &info
)) {
478 CURSOR_ERROR("Could not get icon info");
485 if (GetWindowRect(hwnd
, &rect
)) {
486 pos
.x
= ci
.ptScreenPos
.x
- clip_rect
.left
- info
.xHotspot
- rect
.left
;
487 pos
.y
= ci
.ptScreenPos
.y
- clip_rect
.top
- info
.yHotspot
- rect
.top
;
489 //that would keep the correct location of mouse with hidpi screens
490 pos
.x
= pos
.x
* desktophorzres
/ horzres
;
491 pos
.y
= pos
.y
* desktopvertres
/ vertres
;
493 CURSOR_ERROR("Couldn't get window rectangle");
497 //that would keep the correct location of mouse with hidpi screens
498 pos
.x
= ci
.ptScreenPos
.x
* desktophorzres
/ horzres
- clip_rect
.left
- info
.xHotspot
;
499 pos
.y
= ci
.ptScreenPos
.y
* desktopvertres
/ vertres
- clip_rect
.top
- info
.yHotspot
;
502 av_log(s1
, AV_LOG_DEBUG
, "Cursor pos (%li,%li) -> (%li,%li)\n",
503 ci
.ptScreenPos
.x
, ci
.ptScreenPos
.y
, pos
.x
, pos
.y
);
505 if (pos
.x
>= 0 && pos
.x
<= clip_rect
.right
- clip_rect
.left
&&
506 pos
.y
>= 0 && pos
.y
<= clip_rect
.bottom
- clip_rect
.top
) {
507 if (!DrawIcon(gdigrab
->dest_hdc
, pos
.x
, pos
.y
, icon
))
508 CURSOR_ERROR("Couldn't draw icon");
513 DeleteObject(info
.hbmMask
);
515 DeleteObject(info
.hbmColor
);
519 CURSOR_ERROR("Couldn't get cursor info");
524 * Grabs a frame from gdi (public device demuxer API).
526 * @param s1 Context from avformat core
527 * @param pkt Packet holding the grabbed frame
528 * @return frame size in bytes
530 static int gdigrab_read_packet(AVFormatContext
*s1
, AVPacket
*pkt
)
532 struct gdigrab
*gdigrab
= s1
->priv_data
;
534 HDC dest_hdc
= gdigrab
->dest_hdc
;
535 HDC source_hdc
= gdigrab
->source_hdc
;
536 RECT clip_rect
= gdigrab
->clip_rect
;
537 AVRational time_base
= gdigrab
->time_base
;
538 int64_t time_frame
= gdigrab
->time_frame
;
540 BITMAPFILEHEADER bfh
;
541 int file_size
= gdigrab
->header_size
+ gdigrab
->frame_size
;
543 int64_t curtime
, delay
;
545 /* Calculate the time of the next frame */
546 time_frame
+= INT64_C(1000000);
548 /* Run Window message processing queue */
549 if (gdigrab
->show_region
)
550 gdigrab_region_wnd_update(s1
, gdigrab
);
552 /* wait based on the frame rate */
554 curtime
= av_gettime();
555 delay
= time_frame
* av_q2d(time_base
) - curtime
;
557 if (delay
< INT64_C(-1000000) * av_q2d(time_base
)) {
558 time_frame
+= INT64_C(1000000);
562 if (s1
->flags
& AVFMT_FLAG_NONBLOCK
) {
563 return AVERROR(EAGAIN
);
569 if (av_new_packet(pkt
, file_size
) < 0)
570 return AVERROR(ENOMEM
);
573 /* Blit screen grab */
574 if (!BitBlt(dest_hdc
, 0, 0,
575 clip_rect
.right
- clip_rect
.left
,
576 clip_rect
.bottom
- clip_rect
.top
,
578 clip_rect
.left
, clip_rect
.top
, SRCCOPY
| CAPTUREBLT
)) {
579 WIN32_API_ERROR("Failed to capture image");
582 if (gdigrab
->draw_mouse
)
583 paint_mouse_pointer(s1
, gdigrab
);
585 /* Copy bits to packet data */
587 bfh
.bfType
= 0x4d42; /* "BM" in little-endian */
588 bfh
.bfSize
= file_size
;
591 bfh
.bfOffBits
= gdigrab
->header_size
;
593 memcpy(pkt
->data
, &bfh
, sizeof(bfh
));
595 memcpy(pkt
->data
+ sizeof(bfh
), &gdigrab
->bmi
.bmiHeader
, sizeof(gdigrab
->bmi
.bmiHeader
));
597 if (gdigrab
->bmi
.bmiHeader
.biBitCount
<= 8)
598 GetDIBColorTable(dest_hdc
, 0, 1 << gdigrab
->bmi
.bmiHeader
.biBitCount
,
599 (RGBQUAD
*) (pkt
->data
+ sizeof(bfh
) + sizeof(gdigrab
->bmi
.bmiHeader
)));
601 memcpy(pkt
->data
+ gdigrab
->header_size
, gdigrab
->buffer
, gdigrab
->frame_size
);
603 gdigrab
->time_frame
= time_frame
;
605 return gdigrab
->header_size
+ gdigrab
->frame_size
;
609 * Closes gdi frame grabber (public device demuxer API).
611 * @param s1 Context from avformat core
612 * @return 0 success, !0 failure
614 static int gdigrab_read_close(AVFormatContext
*s1
)
616 struct gdigrab
*s
= s1
->priv_data
;
619 gdigrab_region_wnd_destroy(s1
, s
);
622 ReleaseDC(s
->hwnd
, s
->source_hdc
);
624 DeleteDC(s
->dest_hdc
);
626 DeleteObject(s
->hbmp
);
628 DeleteDC(s
->source_hdc
);
633 #define OFFSET(x) offsetof(struct gdigrab, x)
634 #define DEC AV_OPT_FLAG_DECODING_PARAM
635 static const AVOption options
[] = {
636 { "draw_mouse", "draw the mouse pointer", OFFSET(draw_mouse
), AV_OPT_TYPE_INT
, {.i64
= 1}, 0, 1, DEC
},
637 { "show_region", "draw border around capture area", OFFSET(show_region
), AV_OPT_TYPE_INT
, {.i64
= 0}, 0, 1, DEC
},
638 { "framerate", "set video frame rate", OFFSET(framerate
), AV_OPT_TYPE_VIDEO_RATE
, {.str
= "ntsc"}, 0, INT_MAX
, DEC
},
639 { "video_size", "set video frame size", OFFSET(width
), AV_OPT_TYPE_IMAGE_SIZE
, {.str
= NULL
}, 0, 0, DEC
},
640 { "offset_x", "capture area x offset", OFFSET(offset_x
), AV_OPT_TYPE_INT
, {.i64
= 0}, INT_MIN
, INT_MAX
, DEC
},
641 { "offset_y", "capture area y offset", OFFSET(offset_y
), AV_OPT_TYPE_INT
, {.i64
= 0}, INT_MIN
, INT_MAX
, DEC
},
645 static const AVClass gdigrab_class
= {
646 .class_name
= "GDIgrab indev",
647 .item_name
= av_default_item_name
,
649 .version
= LIBAVUTIL_VERSION_INT
,
650 .category
= AV_CLASS_CATEGORY_DEVICE_VIDEO_INPUT
,
653 /** gdi grabber device demuxer declaration */
654 AVInputFormat ff_gdigrab_demuxer
= {
656 .long_name
= NULL_IF_CONFIG_SMALL("GDI API Windows frame grabber"),
657 .priv_data_size
= sizeof(struct gdigrab
),
658 .read_header
= gdigrab_read_header
,
659 .read_packet
= gdigrab_read_packet
,
660 .read_close
= gdigrab_read_close
,
661 .flags
= AVFMT_NOFILE
,
662 .priv_class
= &gdigrab_class
,