forked from cztomczak/cefpython
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pysdl2.py
534 lines (499 loc) · 18.7 KB
/
pysdl2.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
"""
Example of embedding CEF browser using PySDL2 library.
Requires PySDL2 and SDL2 libraries, see install instructions further
down.
This example is incomplete and has some issues, see the "Known issues"
section further down. Pull requests with fixes are welcome.
Usage:
python pysdl2.py [-v] [-h] [-r {software|hardware}]
-v turn on debug messages
-r specify hardware (default) or software rendering
-h display help info
Tested configurations:
- Windows 7: SDL 2.0.7 and PySDL2 0.9.6
- Mac 10.9: SDL 2.0.7 and PySDL2 0.9.6
- Fedora 26: SDL2 2.0.7 with PySDL2 0.9.6
- Ubuntu 14.04: SDL2 with PySDL2 0.9.6
Install instructions:
1. Install SDL libraries for your OS, e.g:
- Windows: Download SDL2.dll from http://www.libsdl.org/download-2.0.php
and put SDL2.dll in C:\Python27\ (where you've installed Python)
- Mac: Install Homebrew from https://brew.sh/
and then type "brew install sdl2"
- Fedora: sudo dnf install SDL2 SDL2_ttf SDL2_image SDL2_gfx SDL2_mixer
- Ubuntu: sudo apt-get install libsdl2-dev
2. Install PySDL2 using pip package manager:
pip install PySDL2
Known issues (pull requests are welcome):
- There are issues when running on slow machine - key events are being
lost (noticed on Mac only), see Issue #324 for more details
- Performance is still not perfect, see Issue #324 for further details
- Keyboard modifiers that are not yet handled in this example:
ctrl, marking text inputs with the shift key.
- Backspace key doesn't work on Mac
- Dragging with mouse not implemented
- Window size is fixed, cannot be resized
GUI controls:
Due to SDL2's lack of GUI widgets there are no GUI controls
for the user. However, as an exercise this example could
be extended by create some simple SDL2 widgets. An example of
widgets made using PySDL2 can be found as part of the Pi
Entertainment System at:
https://github.com/neilmunday/pes/blob/master/lib/pes/ui.py
"""
import argparse
import logging
import sys
def die(msg):
"""
Helper function to exit application on failed imports etc.
"""
sys.stderr.write("%s\n" % msg)
sys.exit(1)
try:
# noinspection PyUnresolvedReferences
from cefpython3 import cefpython as cef
except ImportError:
die("ERROR: cefpython3 package not found\n"
" To install type: pip install cefpython3")
try:
# noinspection PyUnresolvedReferences
import sdl2
# noinspection PyUnresolvedReferences
import sdl2.ext
except ImportError as exc:
excstr = repr(exc)
if "No module named sdl2" in excstr:
die("ERROR: PySDL2 package not found\n"
" To install type: pip install PySDL2")
elif ("could not find any library for SDL2"
" (PYSDL2_DLL_PATH: unset)" in excstr):
die("ERROR: SDL2 package not found.\n"
" See install instructions in top comment in sources.")
else:
die(excstr)
try:
# noinspection PyUnresolvedReferences
from PIL import Image
except ImportError:
die("ERROR: PIL package not found\n"
" To install type: pip install Pillow")
def main():
parser = argparse.ArgumentParser(
description='PySDL2 / cefpython example',
add_help=True
)
parser.add_argument(
'-v',
'--verbose',
help='Turn on debug info',
dest='verbose',
action='store_true'
)
parser.add_argument(
'-r',
'--renderer',
help='Specify hardware or software rendering',
default='hardware',
dest='renderer',
choices=['software', 'hardware']
)
args = parser.parse_args()
logLevel = logging.INFO
if args.verbose:
logLevel = logging.DEBUG
logging.basicConfig(
format='[%(filename)s %(levelname)s]: %(message)s',
level=logLevel
)
logging.info("Using PySDL2 %s" % sdl2.__version__)
version = sdl2.SDL_version()
sdl2.SDL_GetVersion(version)
logging.info(
"Using SDL2 %s.%s.%s" % (version.major, version.minor, version.patch)
)
# The following variables control the dimensions of the window
# and browser display area
width = 800
height = 600
# headerHeight is useful for leaving space for controls
# at the top of the window (future implementation?)
headerHeight = 0
browserHeight = height - headerHeight
browserWidth = width
# Mouse wheel fudge to enhance scrolling
scrollEnhance = 40
# desired frame rate
frameRate = 100
# Initialise CEF for offscreen rendering
sys.excepthook = cef.ExceptHook
switches = {
# Tweaking OSR performance by setting the same Chromium flags
# as in upstream cefclient (Issue #240).
"disable-surfaces": "",
"disable-gpu": "",
"disable-gpu-compositing": "",
"enable-begin-frame-scheduling": "",
}
browser_settings = {
# Tweaking OSR performance (Issue #240)
"windowless_frame_rate": frameRate
}
cef.Initialize(settings={"windowless_rendering_enabled": True},
switches=switches)
logging.debug("cef initialised")
window_info = cef.WindowInfo()
window_info.SetAsOffscreen(0)
# Initialise SDL2 for video (add other init constants if you
# require other SDL2 functionality e.g. mixer,
# TTF, joystick etc.
sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO)
logging.debug("SDL2 initialised")
# Create the window
window = sdl2.video.SDL_CreateWindow(
'cefpython3 SDL2 Demo',
sdl2.video.SDL_WINDOWPOS_UNDEFINED,
sdl2.video.SDL_WINDOWPOS_UNDEFINED,
width,
height,
0
)
# Define default background colour (black in this case)
backgroundColour = sdl2.SDL_Color(0, 0, 0)
renderer = None
if args.renderer == 'hardware':
# Create the renderer using hardware acceleration
logging.info("Using hardware rendering")
renderer = sdl2.SDL_CreateRenderer(
window,
-1,
sdl2.render.SDL_RENDERER_ACCELERATED
)
else:
# Create the renderer using software acceleration
logging.info("Using software rendering")
renderer = sdl2.SDL_CreateRenderer(
window,
-1,
sdl2.render.SDL_RENDERER_SOFTWARE
)
# Set-up the RenderHandler, passing in the SDL2 renderer
renderHandler = RenderHandler(renderer, width, height - headerHeight)
# Create the browser instance
browser = cef.CreateBrowserSync(window_info,
url="https://www.google.com/",
settings=browser_settings)
browser.SetClientHandler(LoadHandler())
browser.SetClientHandler(renderHandler)
# Must call WasResized at least once to let know CEF that
# viewport size is available and that OnPaint may be called.
browser.SendFocusEvent(True)
browser.WasResized()
# Begin the main rendering loop
running = True
# FPS debug variables
frames = 0
logging.debug("beginning rendering loop")
resetFpsTime = True
fpsTime = 0
while running:
# record when we started drawing this frame
startTime = sdl2.timer.SDL_GetTicks()
if resetFpsTime:
fpsTime = sdl2.timer.SDL_GetTicks()
resetFpsTime = False
# Convert SDL2 events into CEF events (where appropriate)
events = sdl2.ext.get_events()
for event in events:
if (event.type == sdl2.SDL_QUIT
or (event.type == sdl2.SDL_KEYDOWN
and event.key.keysym.sym == sdl2.SDLK_ESCAPE)):
running = False
logging.debug("SDL2 QUIT event")
break
if event.type == sdl2.SDL_MOUSEBUTTONDOWN:
if event.button.button == sdl2.SDL_BUTTON_LEFT:
if event.button.y > headerHeight:
logging.debug(
"SDL2 MOUSEBUTTONDOWN event (left button)"
)
# Mouse click triggered in browser region
browser.SendMouseClickEvent(
event.button.x,
event.button.y - headerHeight,
cef.MOUSEBUTTON_LEFT,
False,
1
)
elif event.type == sdl2.SDL_MOUSEBUTTONUP:
if event.button.button == sdl2.SDL_BUTTON_LEFT:
if event.button.y > headerHeight:
logging.debug("SDL2 MOUSEBUTTONUP event (left button)")
# Mouse click triggered in browser region
browser.SendMouseClickEvent(
event.button.x,
event.button.y - headerHeight,
cef.MOUSEBUTTON_LEFT,
True,
1
)
elif event.type == sdl2.SDL_MOUSEMOTION:
if event.motion.y > headerHeight:
# Mouse move triggered in browser region
browser.SendMouseMoveEvent(event.motion.x,
event.motion.y - headerHeight,
False)
elif event.type == sdl2.SDL_MOUSEWHEEL:
logging.debug("SDL2 MOUSEWHEEL event")
# Mouse wheel event
x = event.wheel.x
if x < 0:
x -= scrollEnhance
else:
x += scrollEnhance
y = event.wheel.y
if y < 0:
y -= scrollEnhance
else:
y += scrollEnhance
browser.SendMouseWheelEvent(0, 0, x, y)
elif event.type == sdl2.SDL_TEXTINPUT:
# Handle text events to get actual characters typed rather
# than the key pressed.
logging.debug("SDL2 TEXTINPUT event: %s" % event.text.text)
keycode = ord(event.text.text)
key_event = {
"type": cef.KEYEVENT_CHAR,
"windows_key_code": keycode,
"character": keycode,
"unmodified_character": keycode,
"modifiers": cef.EVENTFLAG_NONE
}
browser.SendKeyEvent(key_event)
key_event = {
"type": cef.KEYEVENT_KEYUP,
"windows_key_code": keycode,
"character": keycode,
"unmodified_character": keycode,
"modifiers": cef.EVENTFLAG_NONE
}
browser.SendKeyEvent(key_event)
elif event.type == sdl2.SDL_KEYDOWN:
# Handle key down events for non-text keys
logging.debug("SDL2 KEYDOWN event")
if event.key.keysym.sym == sdl2.SDLK_RETURN:
keycode = event.key.keysym.sym
key_event = {
"type": cef.KEYEVENT_CHAR,
"windows_key_code": keycode,
"character": keycode,
"unmodified_character": keycode,
"modifiers": cef.EVENTFLAG_NONE
}
browser.SendKeyEvent(key_event)
elif event.key.keysym.sym in [
sdl2.SDLK_BACKSPACE,
sdl2.SDLK_DELETE,
sdl2.SDLK_LEFT,
sdl2.SDLK_RIGHT,
sdl2.SDLK_UP,
sdl2.SDLK_DOWN,
sdl2.SDLK_HOME,
sdl2.SDLK_END
]:
keycode = get_key_code(event.key.keysym.sym)
if keycode is not None:
key_event = {
"type": cef.KEYEVENT_RAWKEYDOWN,
"windows_key_code": keycode,
"character": keycode,
"unmodified_character": keycode,
"modifiers": cef.EVENTFLAG_NONE
}
browser.SendKeyEvent(key_event)
elif event.type == sdl2.SDL_KEYUP:
# Handle key up events for non-text keys
logging.debug("SDL2 KEYUP event")
if event.key.keysym.sym in [
sdl2.SDLK_RETURN,
sdl2.SDLK_BACKSPACE,
sdl2.SDLK_DELETE,
sdl2.SDLK_LEFT,
sdl2.SDLK_RIGHT,
sdl2.SDLK_UP,
sdl2.SDLK_DOWN,
sdl2.SDLK_HOME,
sdl2.SDLK_END
]:
keycode = get_key_code(event.key.keysym.sym)
if keycode is not None:
key_event = {
"type": cef.KEYEVENT_KEYUP,
"windows_key_code": keycode,
"character": keycode,
"unmodified_character": keycode,
"modifiers": cef.EVENTFLAG_NONE
}
browser.SendKeyEvent(key_event)
# Clear the renderer
sdl2.SDL_SetRenderDrawColor(
renderer,
backgroundColour.r,
backgroundColour.g,
backgroundColour.b,
255
)
sdl2.SDL_RenderClear(renderer)
# Tell CEF to update which will trigger the OnPaint
# method of the RenderHandler instance
cef.MessageLoopWork()
# Update display
sdl2.SDL_RenderCopy(
renderer,
renderHandler.texture,
None,
sdl2.SDL_Rect(0, headerHeight, browserWidth, browserHeight)
)
sdl2.SDL_RenderPresent(renderer)
# FPS debug code
frames += 1
if sdl2.timer.SDL_GetTicks() - fpsTime > 1000:
logging.debug("FPS: %d" % frames)
frames = 0
resetFpsTime = True
# regulate frame rate
if sdl2.timer.SDL_GetTicks() - startTime < 1000.0 / frameRate:
sdl2.timer.SDL_Delay(
(1000 / frameRate) - (sdl2.timer.SDL_GetTicks() - startTime)
)
# User exited
exit_app()
def get_key_code(key):
"""Helper function to convert SDL2 key codes to cef ones"""
key_map = {
sdl2.SDLK_RETURN: 13,
sdl2.SDLK_DELETE: 46,
sdl2.SDLK_BACKSPACE: 8,
sdl2.SDLK_LEFT: 37,
sdl2.SDLK_RIGHT: 39,
sdl2.SDLK_UP: 38,
sdl2.SDLK_DOWN: 40,
sdl2.SDLK_HOME: 36,
sdl2.SDLK_END: 35,
}
if key in key_map:
return key_map[key]
# Key not mapped, raise exception
logging.error(
"""
Keyboard mapping incomplete: unsupported SDL key %d.
See https://wiki.libsdl.org/SDLKeycodeLookup for mapping.
""" % key
)
return None
class LoadHandler(object):
"""Simple handler for loading URLs."""
def OnLoadingStateChange(self, is_loading, **_):
if not is_loading:
logging.info("Page loading complete")
def OnLoadError(self, frame, failed_url, **_):
if not frame.IsMain():
return
logging.error("Failed to load %s" % failed_url)
class RenderHandler(object):
"""
Handler for rendering web pages to the
screen via SDL2.
The object's texture property is exposed
to allow the main rendering loop to access
the SDL2 texture.
"""
def __init__(self, renderer, width, height):
self.__width = width
self.__height = height
self.__renderer = renderer
self.texture = None
def GetViewRect(self, rect_out, **_):
rect_out.extend([0, 0, self.__width, self.__height])
return True
def OnPaint(self, element_type, paint_buffer, **_):
"""
Using the pixel data from CEF's offscreen rendering
the data is converted by PIL into a SDL2 surface
which can then be rendered as a SDL2 texture.
"""
if element_type == cef.PET_VIEW:
image = Image.frombuffer(
'RGBA',
(self.__width, self.__height),
paint_buffer.GetString(mode="rgba", origin="top-left"),
'raw',
'BGRA'
)
# Following PIL to SDL2 surface code from pysdl2 source.
mode = image.mode
rmask = gmask = bmask = amask = 0
depth = None
pitch = None
if mode == "RGB":
# 3x8-bit, 24bpp
if sdl2.endian.SDL_BYTEORDER == sdl2.endian.SDL_LIL_ENDIAN:
rmask = 0x0000FF
gmask = 0x00FF00
bmask = 0xFF0000
else:
rmask = 0xFF0000
gmask = 0x00FF00
bmask = 0x0000FF
depth = 24
pitch = self.__width * 3
elif mode in ("RGBA", "RGBX"):
# RGBX: 4x8-bit, no alpha
# RGBA: 4x8-bit, alpha
if sdl2.endian.SDL_BYTEORDER == sdl2.endian.SDL_LIL_ENDIAN:
rmask = 0x00000000
gmask = 0x0000FF00
bmask = 0x00FF0000
if mode == "RGBA":
amask = 0xFF000000
else:
rmask = 0xFF000000
gmask = 0x00FF0000
bmask = 0x0000FF00
if mode == "RGBA":
amask = 0x000000FF
depth = 32
pitch = self.__width * 4
else:
logging.error("ERROR: Unsupported mode: %s" % mode)
exit_app()
pxbuf = image.tobytes()
# Create surface
surface = sdl2.SDL_CreateRGBSurfaceFrom(
pxbuf,
self.__width,
self.__height,
depth,
pitch,
rmask,
gmask,
bmask,
amask
)
if self.texture:
# free memory used by previous texture
sdl2.SDL_DestroyTexture(self.texture)
# Create texture
self.texture = sdl2.SDL_CreateTextureFromSurface(self.__renderer,
surface)
# Free the surface
sdl2.SDL_FreeSurface(surface)
else:
logging.warning("Unsupport element_type in OnPaint")
def exit_app():
"""Tidy up SDL2 and CEF before exiting."""
sdl2.SDL_Quit()
cef.Shutdown()
logging.info("Exited gracefully")
if __name__ == "__main__":
main()