]> git.sesse.net Git - vlc/blob - modules/video_output/xcb/window.c
GLX: uses Xlib, needs XInitThreads()
[vlc] / modules / video_output / xcb / window.c
1 /**
2  * @file window.c
3  * @brief X C Bindings window provider module for VLC media player
4  */
5 /*****************************************************************************
6  * Copyright © 2009 Rémi Denis-Courmont
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU General Public License
10  * as published by the Free Software Foundation; either version 2
11  * of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public
19  * License along with this library; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
21  ****************************************************************************/
22
23 #ifdef HAVE_CONFIG_H
24 # include <config.h>
25 #endif
26
27 #include <stdarg.h>
28 #include <assert.h>
29 #include <poll.h>
30 #include <unistd.h> /* gethostname() and sysconf() */
31 #include <limits.h> /* _POSIX_HOST_NAME_MAX */
32
33 #include <xcb/xcb.h>
34 typedef xcb_atom_t Atom;
35 #include <X11/Xatom.h> /* XA_WM_NAME */
36
37 #include <vlc_common.h>
38 #include <vlc_plugin.h>
39 #include <vlc_vout_window.h>
40
41 #include "xcb_vlc.h"
42
43 #define XID_TEXT N_("ID of the video output X window")
44 #define XID_LONGTEXT N_( \
45     "VLC can embed its video output in an existing X11 window. " \
46     "This is the X identifier of that window (0 means none).")
47
48 static int  Open (vlc_object_t *);
49 static void Close (vlc_object_t *);
50 static int  EmOpen (vlc_object_t *);
51 static void EmClose (vlc_object_t *);
52
53 /*
54  * Module descriptor
55  */
56 vlc_module_begin ()
57     set_shortname (N_("X window"))
58     set_description (N_("X11 video window (XCB)"))
59     set_category (CAT_VIDEO)
60     set_subcategory (SUBCAT_VIDEO_VOUT)
61     set_capability ("vout window xid", 10)
62     set_callbacks (Open, Close)
63
64     /* Obsolete since 1.1.0: */
65     add_obsolete_bool ("x11-altfullscreen")
66     add_obsolete_bool ("xvideo-altfullscreen")
67     add_obsolete_bool ("xvmc-altfullscreen")
68     add_obsolete_bool ("glx-altfullscreen")
69
70     add_submodule ()
71     set_shortname (N_("Drawable"))
72     set_description (N_("Embedded window video"))
73     set_category (CAT_VIDEO)
74     set_subcategory (SUBCAT_VIDEO_VOUT)
75     set_capability ("vout window xid", 70)
76     set_callbacks (EmOpen, EmClose)
77
78     add_integer ("drawable-xid", 0, NULL, XID_TEXT, XID_LONGTEXT, true)
79         change_unsaveable ()
80
81 vlc_module_end ()
82
83 static int Control (vout_window_t *, int, va_list ap);
84 static void *Thread (void *);
85
86 #define MATCHBOX_HACK 1 /* Matchbox focus hack */
87
88 struct vout_window_sys_t
89 {
90     xcb_connection_t *conn;
91     key_handler_t *keys;
92     vlc_thread_t thread;
93
94     xcb_window_t root;
95     xcb_atom_t wm_state;
96     xcb_atom_t wm_state_above;
97     xcb_atom_t wm_state_below;
98     xcb_atom_t wm_state_fullscreen;
99 #ifdef MATCHBOX_HACK
100     xcb_atom_t mb_current_app_window;
101 #endif
102 };
103
104 /** Set an X window property from a nul-terminated string */
105 static inline
106 void set_string (xcb_connection_t *conn, xcb_window_t window,
107                  xcb_atom_t type, xcb_atom_t atom, const char *str)
108 {
109     xcb_change_property (conn, XCB_PROP_MODE_REPLACE, window, atom, type,
110                          /* format */ 8, strlen (str), str);
111 }
112
113 /** Set an X window string property */
114 static inline
115 void set_ascii_prop (xcb_connection_t *conn, xcb_window_t window,
116                      xcb_atom_t atom, const char *value)
117 {
118     set_string (conn, window, XA_STRING, atom, value);
119 }
120
121 static inline
122 void set_wm_hints (xcb_connection_t *conn, xcb_window_t window)
123 {
124     static const uint32_t wm_hints[8] = {
125         3, /* flags: Input, Initial state */
126         1, /* input: True */
127         1, /* initial state: Normal */
128         0, 0, 0, 0, 0, /* Icon */
129     };
130     xcb_change_property (conn, XCB_PROP_MODE_REPLACE, window, XA_WM_HINTS,
131                          XA_WM_HINTS, 32, 8, wm_hints);
132 }
133
134 /** Set the Window ICCCM client machine property */
135 static inline
136 void set_hostname_prop (xcb_connection_t *conn, xcb_window_t window)
137 {
138     char* hostname;
139     long host_name_max = sysconf (_SC_HOST_NAME_MAX);
140     if (host_name_max <= 0) host_name_max = _POSIX_HOST_NAME_MAX;
141     hostname = malloc (host_name_max);
142     if(!hostname) return;
143
144     if (gethostname (hostname, host_name_max) == 0)
145     {
146         hostname[host_name_max - 1] = '\0';
147         set_ascii_prop (conn, window, XA_WM_CLIENT_MACHINE, hostname);
148     }
149     free(hostname);
150 }
151
152 /** Request the X11 server to internalize a string into an atom */
153 static inline
154 xcb_intern_atom_cookie_t intern_string (xcb_connection_t *c, const char *s)
155 {
156     return xcb_intern_atom (c, 0, strlen (s), s);
157 }
158
159 /** Extract the X11 atom from an intern request cookie */
160 static
161 xcb_atom_t get_atom (xcb_connection_t *conn, xcb_intern_atom_cookie_t ck)
162 {
163     xcb_intern_atom_reply_t *reply;
164     xcb_atom_t atom;
165
166     reply = xcb_intern_atom_reply (conn, ck, NULL);
167     if (reply == NULL)
168         return 0;
169
170     atom = reply->atom;
171     free (reply);
172     return atom;
173 }
174
175 #define NET_WM_STATE_REMOVE 0
176 #define NET_WM_STATE_ADD    1
177 #define NET_WM_STATE_TOGGLE 2
178
179 static void CacheAtoms (vout_window_sys_t *p_sys)
180 {
181     xcb_connection_t *conn = p_sys->conn;
182     xcb_intern_atom_cookie_t wm_state_ck, wm_state_above_ck,
183                              wm_state_below_ck, wm_state_fs_ck;
184
185     wm_state_ck = intern_string (conn, "_NET_WM_STATE");
186     wm_state_above_ck = intern_string (conn, "_NET_WM_STATE_ABOVE");
187     wm_state_below_ck = intern_string (conn, "_NET_WM_STATE_BELOW");
188     wm_state_fs_ck = intern_string (conn, "_NET_WM_STATE_FULLSCREEN");
189 #ifdef MATCHBOX_HACK
190     xcb_intern_atom_cookie_t mb_current_app_window;
191     mb_current_app_window = xcb_intern_atom (conn, true,
192                                              strlen ("_MB_CURRENT_APP_WINDOW"),
193                                              "_MB_CURRENT_APP_WINDOW");
194 #endif
195
196     p_sys->wm_state = get_atom (conn, wm_state_ck);
197     p_sys->wm_state_above = get_atom (conn, wm_state_above_ck);
198     p_sys->wm_state_below = get_atom (conn, wm_state_below_ck);
199     p_sys->wm_state_fullscreen = get_atom (conn, wm_state_fs_ck);
200 #ifdef MATCHBOX_HACK
201     p_sys->mb_current_app_window = get_atom (conn, mb_current_app_window);
202 #endif
203 }
204
205 /**
206  * Create an X11 window.
207  */
208 static int Open (vlc_object_t *obj)
209 {
210     vout_window_t *wnd = (vout_window_t *)obj;
211     xcb_generic_error_t *err;
212     xcb_void_cookie_t ck;
213
214     vout_window_sys_t *p_sys = malloc (sizeof (*p_sys));
215     if (p_sys == NULL)
216         return VLC_ENOMEM;
217
218     /* Connect to X */
219     char *display = var_CreateGetNonEmptyString (wnd, "x11-display");
220     int snum;
221
222     xcb_connection_t *conn = xcb_connect (display, &snum);
223     if (xcb_connection_has_error (conn) /*== NULL*/)
224         goto error;
225
226     /* Find configured screen */
227     const xcb_setup_t *setup = xcb_get_setup (conn);
228     const xcb_screen_t *scr = NULL;
229     for (xcb_screen_iterator_t i = xcb_setup_roots_iterator (setup);
230          i.rem > 0; xcb_screen_next (&i))
231     {
232         if (snum == 0)
233         {
234             scr = i.data;
235             break;
236         }
237         snum--;
238     }
239     if (scr == NULL)
240     {
241         msg_Err (wnd, "bad X11 screen number");
242         goto error;
243     }
244
245     /* Create window */
246     const uint32_t mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK;
247     uint32_t values[2] = {
248         /* XCB_CW_BACK_PIXEL */
249         scr->black_pixel,
250         /* XCB_CW_EVENT_MASK */
251         XCB_EVENT_MASK_KEY_PRESS,
252     };
253
254     xcb_window_t window = xcb_generate_id (conn);
255     ck = xcb_create_window_checked (conn, scr->root_depth, window, scr->root,
256                                     0, 0, wnd->cfg->width, wnd->cfg->height, 0,
257                                     XCB_WINDOW_CLASS_INPUT_OUTPUT,
258                                     scr->root_visual, mask, values);
259     err = xcb_request_check (conn, ck);
260     if (err)
261     {
262         msg_Err (wnd, "creating window: X11 error %d", err->error_code);
263         free (err);
264         goto error;
265     }
266
267     wnd->handle.xid = window;
268     wnd->display.x11 = display;
269     wnd->control = Control;
270     wnd->sys = p_sys;
271
272     p_sys->conn = conn;
273     if (var_CreateGetBool (obj, "keyboard-events"))
274         p_sys->keys = CreateKeyHandler (obj, conn);
275     else
276         p_sys->keys = NULL;
277     p_sys->root = scr->root;
278
279     /* ICCCM
280      * No cut&paste nor drag&drop, only Window Manager communication. */
281     /* xgettext:
282        Plain ASCII of "VLC media player" for the ICCCM window name.
283        This must be ASCII. The limitation is partially with ICCCM
284        and partially with VLC.
285        For Latin script languages, you may need to strip accents.
286        For other scripts, you will need to transliterate into Latin. */
287     set_ascii_prop (conn, window, XA_WM_NAME,
288                   vlc_pgettext ("ASCII", "VLC media player"));
289     /* xgettext:
290        Plain ASCII of "VLC" for the ICCCM window name. */
291     set_ascii_prop (conn, window, XA_WM_ICON_NAME,
292                     vlc_pgettext ("ASCII", "VLC"));
293     set_wm_hints (conn, window);
294     xcb_change_property (conn, XCB_PROP_MODE_REPLACE, window, XA_WM_CLASS,
295                          XA_STRING, 8, 8, "vlc\0Vlc");
296     set_hostname_prop (conn, window);
297
298     /* EWMH */
299     xcb_intern_atom_cookie_t utf8_string_ck
300         = intern_string (conn, "UTF8_STRING");;
301     xcb_intern_atom_cookie_t net_wm_name_ck
302         = intern_string (conn, "_NET_WM_NAME");
303     xcb_intern_atom_cookie_t net_wm_icon_name_ck
304         = intern_string (conn, "_NET_WM_ICON_NAME");
305     xcb_intern_atom_cookie_t wm_window_role_ck
306         = intern_string (conn, "WM_WINDOW_ROLE");
307
308     xcb_atom_t utf8 = get_atom (conn, utf8_string_ck);
309
310     xcb_atom_t net_wm_name = get_atom (conn, net_wm_name_ck);
311     char *title = var_CreateGetNonEmptyString (wnd, "video-title");
312     if (title)
313     {
314         set_string (conn, window, utf8, net_wm_name, title);
315         free (title);
316     }
317     else
318         set_string (conn, window, utf8, net_wm_name, _("VLC media player"));
319
320     xcb_atom_t net_wm_icon_name = get_atom (conn, net_wm_icon_name_ck);
321     set_string (conn, window, utf8, net_wm_icon_name, _("VLC"));
322
323     xcb_atom_t wm_window_role = get_atom (conn, wm_window_role_ck);
324     set_ascii_prop (conn, window, wm_window_role, "vlc-video");
325
326     /* Cache any EWMH atom we may need later */
327     CacheAtoms (p_sys);
328 #ifdef MATCHBOX_HACK
329     if (p_sys->mb_current_app_window)
330     {
331         uint32_t value = XCB_EVENT_MASK_PROPERTY_CHANGE;
332         xcb_change_window_attributes (conn, scr->root,
333                                       XCB_CW_EVENT_MASK, &value);
334     }
335 #endif
336
337     /* Make the window visible */
338     xcb_map_window (conn, window);
339
340     if (var_CreateGetBool (obj, "video-wallpaper"))
341     {
342         vout_window_SetState (wnd, VOUT_WINDOW_STATE_BELOW);
343         vout_window_SetFullScreen (wnd, true);
344     }
345
346     /* Create the event thread. It will dequeue all events, so any checked
347      * request from this thread must be completed at this point. */
348     if ((p_sys->keys != NULL)
349      && vlc_clone (&p_sys->thread, Thread, wnd, VLC_THREAD_PRIORITY_LOW))
350         DestroyKeyHandler (p_sys->keys);
351
352 #ifdef MATCHBOX_HACK
353     if (p_sys->mb_current_app_window)
354         xcb_set_input_focus (p_sys->conn, XCB_INPUT_FOCUS_POINTER_ROOT,
355                              wnd->handle.xid, XCB_CURRENT_TIME);
356 #endif
357     xcb_flush (conn); /* Make sure map_window is sent (should be useless) */
358     return VLC_SUCCESS;
359
360 error:
361     xcb_disconnect (conn);
362     free (display);
363     free (p_sys);
364     return VLC_EGENERIC;
365 }
366
367
368 /**
369  * Destroys the X11 window.
370  */
371 static void Close (vlc_object_t *obj)
372 {
373     vout_window_t *wnd = (vout_window_t *)obj;
374     vout_window_sys_t *p_sys = wnd->sys;
375     xcb_connection_t *conn = p_sys->conn;
376
377     if (p_sys->keys)
378     {
379         vlc_cancel (p_sys->thread);
380         vlc_join (p_sys->thread, NULL);
381         DestroyKeyHandler (p_sys->keys);
382     }
383     xcb_disconnect (conn);
384     free (wnd->display.x11);
385     free (p_sys);
386 }
387
388
389 /** Background thread for X11 events handling */
390 static void *Thread (void *data)
391 {
392     vout_window_t *wnd = data;
393     vout_window_sys_t *p_sys = wnd->sys;
394     xcb_connection_t *conn = p_sys->conn;
395
396     int fd = xcb_get_file_descriptor (conn);
397     if (fd == -1)
398         return NULL;
399
400     for (;;)
401     {
402         xcb_generic_event_t *ev;
403         struct pollfd ufd = { .fd = fd, .events = POLLIN, };
404
405         poll (&ufd, 1, -1);
406
407         int canc = vlc_savecancel ();
408         while ((ev = xcb_poll_for_event (conn)) != NULL)
409         {
410             if (ProcessKeyEvent (p_sys->keys, ev) == 0)
411                 continue;
412 #ifdef MATCHBOX_HACK
413             if (p_sys->mb_current_app_window
414              && (ev->response_type & 0x7f) == XCB_PROPERTY_NOTIFY)
415             {
416                 const xcb_property_notify_event_t *pne =
417                     (xcb_property_notify_event_t *)ev;
418                 if (pne->atom == p_sys->mb_current_app_window
419                  && pne->state == XCB_PROPERTY_NEW_VALUE)
420                 {
421                     xcb_get_property_reply_t *r =
422                         xcb_get_property_reply (conn,
423                             xcb_get_property (conn, 0, pne->window, pne->atom,
424                                               XA_WINDOW, 0, 4), NULL);
425                     if (r != NULL
426                      && !memcmp (xcb_get_property_value (r), &wnd->handle.xid,
427                                  4))
428                     {
429                         msg_Dbg (wnd, "asking Matchbox for input focus");
430                         xcb_set_input_focus (conn,
431                                              XCB_INPUT_FOCUS_POINTER_ROOT,
432                                              wnd->handle.xid, pne->time);
433                         xcb_flush (conn);
434                     }
435                     free (r);
436                 }
437             }
438             else
439 #endif
440                 msg_Dbg (wnd, "unhandled event: %"PRIu8, ev->response_type);
441             free (ev);
442         }
443         vlc_restorecancel (canc);
444
445         if (xcb_connection_has_error (conn))
446         {
447             msg_Err (wnd, "X server failure");
448             break;
449         }
450     }
451     return NULL;
452 }
453
454 /** Changes the EWMH state of the window */
455 static void set_wm_state (vout_window_t *wnd, bool on, xcb_atom_t state)
456 {
457     vout_window_sys_t *sys = wnd->sys;
458     /* From EWMH "_WM_STATE" */
459     xcb_client_message_event_t ev = {
460          .response_type = XCB_CLIENT_MESSAGE,
461          .format = 32,
462          .window = wnd->handle.xid,
463          .type = sys->wm_state,
464     };
465
466     ev.data.data32[0] = on ? NET_WM_STATE_ADD : NET_WM_STATE_REMOVE;
467     ev.data.data32[1] = state;
468     ev.data.data32[2] = 0;
469     ev.data.data32[3] = 1;
470
471     /* From ICCCM "Changing Window State" */
472     xcb_send_event (sys->conn, 0, sys->root,
473                     XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY |
474                     XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT,
475                     (const char *)&ev);
476 }
477
478
479 static int Control (vout_window_t *wnd, int cmd, va_list ap)
480 {
481     vout_window_sys_t *p_sys = wnd->sys;
482     xcb_connection_t *conn = p_sys->conn;
483
484     switch (cmd)
485     {
486         case VOUT_WINDOW_SET_SIZE:
487         {
488             unsigned width = va_arg (ap, unsigned);
489             unsigned height = va_arg (ap, unsigned);
490             const uint32_t values[] = { width, height, };
491
492             xcb_configure_window (conn, wnd->handle.xid,
493                                   XCB_CONFIG_WINDOW_WIDTH |
494                                   XCB_CONFIG_WINDOW_HEIGHT, values);
495             break;
496         }
497
498         case VOUT_WINDOW_SET_STATE:
499         {
500             unsigned state = va_arg (ap, unsigned);
501             bool above = (state & VOUT_WINDOW_STATE_ABOVE) != 0;
502             bool below = (state & VOUT_WINDOW_STATE_BELOW) != 0;
503
504             set_wm_state (wnd, above, p_sys->wm_state_above);
505             set_wm_state (wnd, below, p_sys->wm_state_below);
506             break;
507         }
508
509         case VOUT_WINDOW_SET_FULLSCREEN:
510         {
511             bool fs = va_arg (ap, int);
512             if (!fs && var_GetBool (wnd, "video-wallpaper"))
513                 return VLC_EGENERIC;
514             set_wm_state (wnd, fs, p_sys->wm_state_fullscreen);
515             break;
516         }
517
518         default:
519             msg_Err (wnd, "request %d not implemented", cmd);
520             return VLC_EGENERIC;
521     }
522     xcb_flush (p_sys->conn);
523     return VLC_SUCCESS;
524 }
525
526 /*** Embedded drawable support ***/
527
528 static vlc_mutex_t serializer = VLC_STATIC_MUTEX;
529
530 /** Acquire a drawable */
531 static int AcquireDrawable (vlc_object_t *obj, xcb_window_t window)
532 {
533     xcb_window_t *used;
534     size_t n = 0;
535
536     if (var_Create (obj->p_libvlc, "xid-in-use", VLC_VAR_ADDRESS))
537         return VLC_ENOMEM;
538
539     /* Keep a list of busy drawables, so we don't overlap videos if there are
540      * more than one video track in the stream. */
541     vlc_mutex_lock (&serializer);
542     used = var_GetAddress (obj->p_libvlc, "xid-in-use");
543     if (used != NULL)
544     {
545         while (used[n])
546         {
547             if (used[n] == window)
548                 goto skip;
549             n++;
550         }
551     }
552
553     used = realloc (used, sizeof (*used) * (n + 2));
554     if (used != NULL)
555     {
556         used[n] = window;
557         used[n + 1] = 0;
558         var_SetAddress (obj->p_libvlc, "xid-in-use", used);
559     }
560     else
561     {
562 skip:
563         msg_Warn (obj, "X11 drawable 0x%08"PRIx8" is busy", window);
564         window = 0;
565     }
566     vlc_mutex_unlock (&serializer);
567
568     return (window == 0) ? VLC_EGENERIC : VLC_SUCCESS;
569 }
570
571 /** Remove this drawable from the list of busy ones */
572 static void ReleaseDrawable (vlc_object_t *obj, xcb_window_t window)
573 {
574     xcb_window_t *used;
575     size_t n = 0;
576
577     vlc_mutex_lock (&serializer);
578     used = var_GetAddress (obj->p_libvlc, "xid-in-use");
579     assert (used);
580     while (used[n] != window)
581     {
582         assert (used[n]);
583         n++;
584     }
585     do
586         used[n] = used[n + 1];
587     while (used[++n]);
588
589     if (n == 0)
590          var_SetAddress (obj->p_libvlc, "xid-in-use", NULL);
591     vlc_mutex_unlock (&serializer);
592
593     if (n == 0)
594         free (used);
595     /* Variables are reference-counted... */
596     var_Destroy (obj->p_libvlc, "xid-in-use");
597 }
598
599 /**
600  * Wrap an existing X11 window to embed the video.
601  */
602 static int EmOpen (vlc_object_t *obj)
603 {
604     vout_window_t *wnd = (vout_window_t *)obj;
605
606     xcb_window_t window = var_CreateGetInteger (obj, "drawable-xid");
607     if (window == 0)
608         return VLC_EGENERIC;
609     var_Destroy (obj, "drawable-xid");
610
611     if (AcquireDrawable (obj, window))
612         return VLC_EGENERIC;
613
614     vout_window_sys_t *p_sys = malloc (sizeof (*p_sys));
615     xcb_connection_t *conn = xcb_connect (NULL, NULL);
616     if (p_sys == NULL || xcb_connection_has_error (conn))
617         goto error;
618
619     wnd->handle.xid = window;
620     wnd->control = Control;
621     wnd->sys = p_sys;
622
623     p_sys->conn = conn;
624
625     xcb_get_geometry_reply_t *geo =
626         xcb_get_geometry_reply (conn, xcb_get_geometry (conn, window), NULL);
627     if (geo == NULL)
628     {
629         msg_Err (obj, "bad X11 window 0x%08"PRIx8, window);
630         goto error;
631     }
632     p_sys->root = geo->root;
633     free (geo);
634
635     if (var_CreateGetBool (obj, "keyboard-events"))
636     {
637         p_sys->keys = CreateKeyHandler (obj, conn);
638         if (p_sys->keys != NULL)
639         {
640             const uint32_t mask = XCB_CW_EVENT_MASK;
641             const uint32_t values[1] = {
642                 XCB_EVENT_MASK_KEY_PRESS,
643             };
644             xcb_change_window_attributes (conn, window, mask, values);
645         }
646     }
647
648     CacheAtoms (p_sys);
649     if ((p_sys->keys != NULL)
650      && vlc_clone (&p_sys->thread, Thread, wnd, VLC_THREAD_PRIORITY_LOW))
651         DestroyKeyHandler (p_sys->keys);
652
653     xcb_flush (conn);
654
655     return VLC_SUCCESS;
656
657 error:
658     xcb_disconnect (conn);
659     free (p_sys);
660     ReleaseDrawable (obj, window);
661     return VLC_EGENERIC;
662 }
663
664 static void EmClose (vlc_object_t *obj)
665 {
666     vout_window_t *wnd = (vout_window_t *)obj;
667     xcb_window_t window = wnd->handle.xid;
668
669     Close (obj);
670     ReleaseDrawable (obj, window);
671 }