1/* 2 * GDI video grab interface 3 * 4 * This file is part of FFmpeg. 5 * 6 * Copyright (C) 2013 Calvin Walton <calvin.walton@kepstin.ca> 7 * Copyright (C) 2007-2010 Christophe Gisquet <word1.word2@gmail.com> 8 * 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. 13 * 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. 18 * 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 22 */ 23 24/** 25 * @file 26 * GDI frame device demuxer 27 * @author Calvin Walton <calvin.walton@kepstin.ca> 28 * @author Christophe Gisquet <word1.word2@gmail.com> 29 */ 30 31#include "config.h" 32#include "libavformat/internal.h" 33#include "libavutil/opt.h" 34#include "libavutil/time.h" 35#include <windows.h> 36 37/** 38 * GDI Device Demuxer context 39 */ 40struct gdigrab { 41 const AVClass *class; /**< Class for private options */ 42 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 */ 47 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) */ 55 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 */ 63 64 HWND region_hwnd; /**< Handle of the region border window */ 65 66 int cursor_error_printed; 67}; 68 69#define WIN32_API_ERROR(str) \ 70 av_log(s1, AV_LOG_ERROR, str " (error %li)\n", GetLastError()) 71 72#define REGION_WND_BORDER 3 73 74/** 75 * Callback to handle Windows messages for the region outline window. 76 * 77 * In particular, this handles painting the frame rectangle. 78 * 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 84 */ 85static LRESULT CALLBACK 86gdigrab_region_wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) 87{ 88 PAINTSTRUCT ps; 89 HDC hdc; 90 RECT rect; 91 92 switch (msg) { 93 case WM_PAINT: 94 hdc = BeginPaint(hwnd, &ps); 95 96 GetClientRect(hwnd, &rect); 97 FrameRect(hdc, &rect, GetStockObject(BLACK_BRUSH)); 98 99 rect.left++; rect.top++; rect.right--; rect.bottom--; 100 FrameRect(hdc, &rect, GetStockObject(WHITE_BRUSH)); 101 102 rect.left++; rect.top++; rect.right--; rect.bottom--; 103 FrameRect(hdc, &rect, GetStockObject(BLACK_BRUSH)); 104 105 EndPaint(hwnd, &ps); 106 break; 107 default: 108 return DefWindowProc(hwnd, msg, wparam, lparam); 109 } 110 return 0; 111} 112 113/** 114 * Initialize the region outline window. 115 * 116 * @param s1 The format context. 117 * @param gdigrab gdigrab context. 118 * @return 0 success, !0 failure 119 */ 120static int 121gdigrab_region_wnd_init(AVFormatContext *s1, struct gdigrab *gdigrab) 122{ 123 HWND hwnd; 124 RECT rect = gdigrab->clip_rect; 125 HRGN region = NULL; 126 HRGN region_interior = NULL; 127 128 DWORD style = WS_POPUP | WS_VISIBLE; 129 DWORD ex = WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_TRANSPARENT; 130 131 rect.left -= REGION_WND_BORDER; rect.top -= REGION_WND_BORDER; 132 rect.right += REGION_WND_BORDER; rect.bottom += REGION_WND_BORDER; 133 134 AdjustWindowRectEx(&rect, style, FALSE, ex); 135 136 // Create a window with no owner; use WC_DIALOG instead of writing a custom 137 // window class 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); 141 if (!hwnd) { 142 WIN32_API_ERROR("Could not create region display window"); 143 goto error; 144 } 145 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"); 156 goto error; 157 } 158 // The "region" memory is now owned by the window 159 region = NULL; 160 DeleteObject(region_interior); 161 162 SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR) gdigrab_region_wnd_proc); 163 164 ShowWindow(hwnd, SW_SHOW); 165 166 gdigrab->region_hwnd = hwnd; 167 168 return 0; 169 170error: 171 if (region) 172 DeleteObject(region); 173 if (region_interior) 174 DeleteObject(region_interior); 175 if (hwnd) 176 DestroyWindow(hwnd); 177 return 1; 178} 179 180/** 181 * Cleanup/free the region outline window. 182 * 183 * @param s1 The format context. 184 * @param gdigrab gdigrab context. 185 */ 186static void 187gdigrab_region_wnd_destroy(AVFormatContext *s1, struct gdigrab *gdigrab) 188{ 189 if (gdigrab->region_hwnd) 190 DestroyWindow(gdigrab->region_hwnd); 191 gdigrab->region_hwnd = NULL; 192} 193 194/** 195 * Process the Windows message queue. 196 * 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. 200 * 201 * @param s1 The format context. 202 * @param gdigrab gdigrab context. 203 */ 204static void 205gdigrab_region_wnd_update(AVFormatContext *s1, struct gdigrab *gdigrab) 206{ 207 HWND hwnd = gdigrab->region_hwnd; 208 MSG msg; 209 210 while (PeekMessage(&msg, hwnd, 0, 0, PM_REMOVE)) { 211 DispatchMessage(&msg); 212 } 213} 214 215/** 216 * Initializes the gdi grab device demuxer (public device demuxer API). 217 * 218 * @param s1 Context from avformat core 219 * @return AVERROR_IO error, 0 success 220 */ 221static int 222gdigrab_read_header(AVFormatContext *s1) 223{ 224 struct gdigrab *gdigrab = s1->priv_data; 225 226 HWND hwnd; 227 HDC source_hdc = NULL; 228 HDC dest_hdc = NULL; 229 BITMAPINFO bmi; 230 HBITMAP hbmp = NULL; 231 void *buffer = NULL; 232 233 const char *filename = s1->filename; 234 const char *name = NULL; 235 AVStream *st = NULL; 236 237 int bpp; 238 RECT virtual_rect; 239 RECT clip_rect; 240 BITMAP bmp; 241 int ret; 242 243 if (!strncmp(filename, "title=", 6)) { 244 name = filename + 6; 245 hwnd = FindWindow(NULL, name); 246 if (!hwnd) { 247 av_log(s1, AV_LOG_ERROR, 248 "Can't find window '%s', aborting.\n", name); 249 ret = AVERROR(EIO); 250 goto error; 251 } 252 if (gdigrab->show_region) { 253 av_log(s1, AV_LOG_WARNING, 254 "Can't show region when grabbing a window.\n"); 255 gdigrab->show_region = 0; 256 } 257 } else if (!strcmp(filename, "desktop")) { 258 hwnd = NULL; 259 } else { 260 av_log(s1, AV_LOG_ERROR, 261 "Please use \"desktop\" or \"title=<windowname>\" to specify your target.\n"); 262 ret = AVERROR(EIO); 263 goto error; 264 } 265 266 if (hwnd) { 267 GetClientRect(hwnd, &virtual_rect); 268 } else { 269 virtual_rect.left = GetSystemMetrics(SM_XVIRTUALSCREEN); 270 virtual_rect.top = GetSystemMetrics(SM_YVIRTUALSCREEN); 271 virtual_rect.right = virtual_rect.left + GetSystemMetrics(SM_CXVIRTUALSCREEN); 272 virtual_rect.bottom = virtual_rect.top + GetSystemMetrics(SM_CYVIRTUALSCREEN); 273 } 274 275 /* If no width or height set, use full screen/window area */ 276 if (!gdigrab->width || !gdigrab->height) { 277 clip_rect.left = virtual_rect.left; 278 clip_rect.top = virtual_rect.top; 279 clip_rect.right = virtual_rect.right; 280 clip_rect.bottom = virtual_rect.bottom; 281 } else { 282 clip_rect.left = gdigrab->offset_x; 283 clip_rect.top = gdigrab->offset_y; 284 clip_rect.right = gdigrab->width + gdigrab->offset_x; 285 clip_rect.bottom = gdigrab->height + gdigrab->offset_y; 286 } 287 288 if (clip_rect.left < virtual_rect.left || 289 clip_rect.top < virtual_rect.top || 290 clip_rect.right > virtual_rect.right || 291 clip_rect.bottom > virtual_rect.bottom) { 292 av_log(s1, AV_LOG_ERROR, 293 "Capture area (%li,%li),(%li,%li) extends outside window area (%li,%li),(%li,%li)", 294 clip_rect.left, clip_rect.top, 295 clip_rect.right, clip_rect.bottom, 296 virtual_rect.left, virtual_rect.top, 297 virtual_rect.right, virtual_rect.bottom); 298 ret = AVERROR(EIO); 299 goto error; 300 } 301 302 /* This will get the device context for the selected window, or if 303 * none, the primary screen */ 304 source_hdc = GetDC(hwnd); 305 if (!source_hdc) { 306 WIN32_API_ERROR("Couldn't get window device context"); 307 ret = AVERROR(EIO); 308 goto error; 309 } 310 bpp = GetDeviceCaps(source_hdc, BITSPIXEL); 311 312 if (name) { 313 av_log(s1, AV_LOG_INFO, 314 "Found window %s, capturing %lix%lix%i at (%li,%li)\n", 315 name, 316 clip_rect.right - clip_rect.left, 317 clip_rect.bottom - clip_rect.top, 318 bpp, clip_rect.left, clip_rect.top); 319 } else { 320 av_log(s1, AV_LOG_INFO, 321 "Capturing whole desktop as %lix%lix%i at (%li,%li)\n", 322 clip_rect.right - clip_rect.left, 323 clip_rect.bottom - clip_rect.top, 324 bpp, clip_rect.left, clip_rect.top); 325 } 326 327 if (clip_rect.right - clip_rect.left <= 0 || 328 clip_rect.bottom - clip_rect.top <= 0 || bpp%8) { 329 av_log(s1, AV_LOG_ERROR, "Invalid properties, aborting\n"); 330 ret = AVERROR(EIO); 331 goto error; 332 } 333 334 dest_hdc = CreateCompatibleDC(source_hdc); 335 if (!dest_hdc) { 336 WIN32_API_ERROR("Screen DC CreateCompatibleDC"); 337 ret = AVERROR(EIO); 338 goto error; 339 } 340 341 /* Create a DIB and select it into the dest_hdc */ 342 bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); 343 bmi.bmiHeader.biWidth = clip_rect.right - clip_rect.left; 344 bmi.bmiHeader.biHeight = -(clip_rect.bottom - clip_rect.top); 345 bmi.bmiHeader.biPlanes = 1; 346 bmi.bmiHeader.biBitCount = bpp; 347 bmi.bmiHeader.biCompression = BI_RGB; 348 bmi.bmiHeader.biSizeImage = 0; 349 bmi.bmiHeader.biXPelsPerMeter = 0; 350 bmi.bmiHeader.biYPelsPerMeter = 0; 351 bmi.bmiHeader.biClrUsed = 0; 352 bmi.bmiHeader.biClrImportant = 0; 353 hbmp = CreateDIBSection(dest_hdc, &bmi, DIB_RGB_COLORS, 354 &buffer, NULL, 0); 355 if (!hbmp) { 356 WIN32_API_ERROR("Creating DIB Section"); 357 ret = AVERROR(EIO); 358 goto error; 359 } 360 361 if (!SelectObject(dest_hdc, hbmp)) { 362 WIN32_API_ERROR("SelectObject"); 363 ret = AVERROR(EIO); 364 goto error; 365 } 366 367 /* Get info from the bitmap */ 368 GetObject(hbmp, sizeof(BITMAP), &bmp); 369 370 st = avformat_new_stream(s1, NULL); 371 if (!st) { 372 ret = AVERROR(ENOMEM); 373 goto error; 374 } 375 avpriv_set_pts_info(st, 64, 1, 1000000); /* 64 bits pts in us */ 376 377 gdigrab->frame_size = bmp.bmWidthBytes * bmp.bmHeight * bmp.bmPlanes; 378 gdigrab->header_size = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + 379 (bpp <= 8 ? (1 << bpp) : 0) * sizeof(RGBQUAD) /* palette size */; 380 gdigrab->time_base = av_inv_q(gdigrab->framerate); 381 gdigrab->time_frame = av_gettime() / av_q2d(gdigrab->time_base); 382 383 gdigrab->hwnd = hwnd; 384 gdigrab->source_hdc = source_hdc; 385 gdigrab->dest_hdc = dest_hdc; 386 gdigrab->hbmp = hbmp; 387 gdigrab->bmi = bmi; 388 gdigrab->buffer = buffer; 389 gdigrab->clip_rect = clip_rect; 390 391 gdigrab->cursor_error_printed = 0; 392 393 if (gdigrab->show_region) { 394 if (gdigrab_region_wnd_init(s1, gdigrab)) { 395 ret = AVERROR(EIO); 396 goto error; 397 } 398 } 399 400 st->codec->codec_type = AVMEDIA_TYPE_VIDEO; 401 st->codec->codec_id = AV_CODEC_ID_BMP; 402 st->codec->time_base = gdigrab->time_base; 403 st->codec->bit_rate = (gdigrab->header_size + gdigrab->frame_size) * 1/av_q2d(gdigrab->time_base) * 8; 404 405 return 0; 406 407error: 408 if (source_hdc) 409 ReleaseDC(hwnd, source_hdc); 410 if (dest_hdc) 411 DeleteDC(dest_hdc); 412 if (hbmp) 413 DeleteObject(hbmp); 414 if (source_hdc) 415 DeleteDC(source_hdc); 416 return ret; 417} 418 419/** 420 * Paints a mouse pointer in a Win32 image. 421 * 422 * @param s1 Context of the log information 423 * @param s Current grad structure 424 */ 425static void paint_mouse_pointer(AVFormatContext *s1, struct gdigrab *gdigrab) 426{ 427 CURSORINFO ci = {0}; 428 429#define CURSOR_ERROR(str) \ 430 if (!gdigrab->cursor_error_printed) { \ 431 WIN32_API_ERROR(str); \ 432 gdigrab->cursor_error_printed = 1; \ 433 } 434 435 ci.cbSize = sizeof(ci); 436 437 if (GetCursorInfo(&ci)) { 438 HCURSOR icon = CopyCursor(ci.hCursor); 439 ICONINFO info; 440 POINT pos; 441 RECT clip_rect = gdigrab->clip_rect; 442 HWND hwnd = gdigrab->hwnd; 443 444 if (ci.flags != CURSOR_SHOWING) 445 return; 446 447 if (!icon) { 448 /* Use the standard arrow cursor as a fallback. 449 * You'll probably only hit this in Wine, which can't fetch 450 * the current system cursor. */ 451 icon = CopyCursor(LoadCursor(NULL, IDC_ARROW)); 452 } 453 454 if (!GetIconInfo(icon, &info)) { 455 CURSOR_ERROR("Could not get icon info"); 456 goto icon_error; 457 } 458 459 pos.x = ci.ptScreenPos.x - clip_rect.left - info.xHotspot; 460 pos.y = ci.ptScreenPos.y - clip_rect.top - info.yHotspot; 461 462 if (hwnd) { 463 RECT rect; 464 465 if (GetWindowRect(hwnd, &rect)) { 466 pos.x -= rect.left; 467 pos.y -= rect.top; 468 } else { 469 CURSOR_ERROR("Couldn't get window rectangle"); 470 goto icon_error; 471 } 472 } 473 474 av_log(s1, AV_LOG_DEBUG, "Cursor pos (%li,%li) -> (%li,%li)\n", 475 ci.ptScreenPos.x, ci.ptScreenPos.y, pos.x, pos.y); 476 477 if (pos.x >= 0 && pos.x <= clip_rect.right - clip_rect.left && 478 pos.y >= 0 && pos.y <= clip_rect.bottom - clip_rect.top) { 479 if (!DrawIcon(gdigrab->dest_hdc, pos.x, pos.y, icon)) 480 CURSOR_ERROR("Couldn't draw icon"); 481 } 482 483icon_error: 484 if (icon) 485 DestroyCursor(icon); 486 } else { 487 CURSOR_ERROR("Couldn't get cursor info"); 488 } 489} 490 491/** 492 * Grabs a frame from gdi (public device demuxer API). 493 * 494 * @param s1 Context from avformat core 495 * @param pkt Packet holding the grabbed frame 496 * @return frame size in bytes 497 */ 498static int gdigrab_read_packet(AVFormatContext *s1, AVPacket *pkt) 499{ 500 struct gdigrab *gdigrab = s1->priv_data; 501 502 HDC dest_hdc = gdigrab->dest_hdc; 503 HDC source_hdc = gdigrab->source_hdc; 504 RECT clip_rect = gdigrab->clip_rect; 505 AVRational time_base = gdigrab->time_base; 506 int64_t time_frame = gdigrab->time_frame; 507 508 BITMAPFILEHEADER bfh; 509 int file_size = gdigrab->header_size + gdigrab->frame_size; 510 511 int64_t curtime, delay; 512 513 /* Calculate the time of the next frame */ 514 time_frame += INT64_C(1000000); 515 516 /* Run Window message processing queue */ 517 if (gdigrab->show_region) 518 gdigrab_region_wnd_update(s1, gdigrab); 519 520 /* wait based on the frame rate */ 521 for (;;) { 522 curtime = av_gettime(); 523 delay = time_frame * av_q2d(time_base) - curtime; 524 if (delay <= 0) { 525 if (delay < INT64_C(-1000000) * av_q2d(time_base)) { 526 time_frame += INT64_C(1000000); 527 } 528 break; 529 } 530 if (s1->flags & AVFMT_FLAG_NONBLOCK) { 531 return AVERROR(EAGAIN); 532 } else { 533 av_usleep(delay); 534 } 535 } 536 537 if (av_new_packet(pkt, file_size) < 0) 538 return AVERROR(ENOMEM); 539 pkt->pts = curtime; 540 541 /* Blit screen grab */ 542 if (!BitBlt(dest_hdc, 0, 0, 543 clip_rect.right - clip_rect.left, 544 clip_rect.bottom - clip_rect.top, 545 source_hdc, 546 clip_rect.left, clip_rect.top, SRCCOPY | CAPTUREBLT)) { 547 WIN32_API_ERROR("Failed to capture image"); 548 return AVERROR(EIO); 549 } 550 if (gdigrab->draw_mouse) 551 paint_mouse_pointer(s1, gdigrab); 552 553 /* Copy bits to packet data */ 554 555 bfh.bfType = 0x4d42; /* "BM" in little-endian */ 556 bfh.bfSize = file_size; 557 bfh.bfReserved1 = 0; 558 bfh.bfReserved2 = 0; 559 bfh.bfOffBits = gdigrab->header_size; 560 561 memcpy(pkt->data, &bfh, sizeof(bfh)); 562 563 memcpy(pkt->data + sizeof(bfh), &gdigrab->bmi.bmiHeader, sizeof(gdigrab->bmi.bmiHeader)); 564 565 if (gdigrab->bmi.bmiHeader.biBitCount <= 8) 566 GetDIBColorTable(dest_hdc, 0, 1 << gdigrab->bmi.bmiHeader.biBitCount, 567 (RGBQUAD *) (pkt->data + sizeof(bfh) + sizeof(gdigrab->bmi.bmiHeader))); 568 569 memcpy(pkt->data + gdigrab->header_size, gdigrab->buffer, gdigrab->frame_size); 570 571 gdigrab->time_frame = time_frame; 572 573 return gdigrab->header_size + gdigrab->frame_size; 574} 575 576/** 577 * Closes gdi frame grabber (public device demuxer API). 578 * 579 * @param s1 Context from avformat core 580 * @return 0 success, !0 failure 581 */ 582static int gdigrab_read_close(AVFormatContext *s1) 583{ 584 struct gdigrab *s = s1->priv_data; 585 586 if (s->show_region) 587 gdigrab_region_wnd_destroy(s1, s); 588 589 if (s->source_hdc) 590 ReleaseDC(s->hwnd, s->source_hdc); 591 if (s->dest_hdc) 592 DeleteDC(s->dest_hdc); 593 if (s->hbmp) 594 DeleteObject(s->hbmp); 595 if (s->source_hdc) 596 DeleteDC(s->source_hdc); 597 598 return 0; 599} 600 601#define OFFSET(x) offsetof(struct gdigrab, x) 602#define DEC AV_OPT_FLAG_DECODING_PARAM 603static const AVOption options[] = { 604 { "draw_mouse", "draw the mouse pointer", OFFSET(draw_mouse), AV_OPT_TYPE_INT, {.i64 = 1}, 0, 1, DEC }, 605 { "show_region", "draw border around capture area", OFFSET(show_region), AV_OPT_TYPE_INT, {.i64 = 0}, 0, 1, DEC }, 606 { "framerate", "set video frame rate", OFFSET(framerate), AV_OPT_TYPE_VIDEO_RATE, {.str = "ntsc"}, 0, 0, DEC }, 607 { "video_size", "set video frame size", OFFSET(width), AV_OPT_TYPE_IMAGE_SIZE, {.str = NULL}, 0, 0, DEC }, 608 { "offset_x", "capture area x offset", OFFSET(offset_x), AV_OPT_TYPE_INT, {.i64 = 0}, INT_MIN, INT_MAX, DEC }, 609 { "offset_y", "capture area y offset", OFFSET(offset_y), AV_OPT_TYPE_INT, {.i64 = 0}, INT_MIN, INT_MAX, DEC }, 610 { NULL }, 611}; 612 613static const AVClass gdigrab_class = { 614 .class_name = "GDIgrab indev", 615 .item_name = av_default_item_name, 616 .option = options, 617 .version = LIBAVUTIL_VERSION_INT, 618}; 619 620/** gdi grabber device demuxer declaration */ 621AVInputFormat ff_gdigrab_demuxer = { 622 .name = "gdigrab", 623 .long_name = NULL_IF_CONFIG_SMALL("GDI API Windows frame grabber"), 624 .priv_data_size = sizeof(struct gdigrab), 625 .read_header = gdigrab_read_header, 626 .read_packet = gdigrab_read_packet, 627 .read_close = gdigrab_read_close, 628 .flags = AVFMT_NOFILE, 629 .priv_class = &gdigrab_class, 630}; 631