remi icon indicating copy to clipboard operation
remi copied to clipboard

[Question/Enhancement] Integration with PyGame

Open MikeTheWatchGuy opened this issue 5 years ago • 15 comments

Is it possible to show a PyGame window embedded in a Remi window?

I managed to do this with tkinter so that now a PyGame program is capable of running inside of PySimpleGUI. It would be amazing to see it run in a browser using Remi!

Here is my basic integration between PySimpleGUI (tkinter version) and PyGame https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_PyGame_Integration.py

There's really only 3 important lines of code and it only works on Windows for me so far.

embed = graph.TKCanvas
os.environ['SDL_WINDOWID'] = str(embed.winfo_id())
os.environ['SDL_VIDEODRIVER'] = 'windib'

I'm also trying to integrate the pyxel project into tkinter and Remi.

MikeTheWatchGuy avatar Apr 28 '19 18:04 MikeTheWatchGuy

Hey! I managed to get this Super Mario Brothers game to run in my PySimpleGUI (tkinter) window!

It would be amazing to get it piped out to a Remi window...

Super Mario in PySimpleGUI

MikeTheWatchGuy avatar Apr 28 '19 20:04 MikeTheWatchGuy

Hello @MikeTheWatchGuy ,

here is an example for you:

import remi.gui as gui
from remi import start, App
import os

import io
from PIL import Image
import threading

import random, math, pygame
from pygame.locals import *

#constants
WINSIZE = [640, 480]
WINCENTER = [320, 240]
NUMSTARS = 150


def init_star():
    "creates new star values"
    dir = random.randrange(100000)
    velmult = random.random()*.6+.4
    vel = [math.sin(dir) * velmult, math.cos(dir) * velmult]
    return vel, WINCENTER[:]


def initialize_stars():
    "creates a new starfield"
    stars = []
    for x in range(NUMSTARS):
        star = init_star()
        vel, pos = star
        steps = random.randint(0, WINCENTER[0])
        pos[0] = pos[0] + (vel[0] * steps)
        pos[1] = pos[1] + (vel[1] * steps)
        vel[0] = vel[0] * (steps * .09)
        vel[1] = vel[1] * (steps * .09)
        stars.append(star)
    move_stars(stars)
    return stars


def draw_stars(surface, stars, color):
    "used to draw (and clear) the stars"
    for vel, pos in stars:
        pos = (int(pos[0]), int(pos[1]))
        surface.set_at(pos, color)


def move_stars(stars):
    "animate the star values"
    for vel, pos in stars:
        pos[0] = pos[0] + vel[0]
        pos[1] = pos[1] + vel[1]
        if not 0 <= pos[0] <= WINSIZE[0] or not 0 <= pos[1] <= WINSIZE[1]:
            vel[:], pos[:] = init_star()
        else:
            vel[0] = vel[0] * 1.05
            vel[1] = vel[1] * 1.05


class PILImageViewverWidget(gui.Image):
    def __init__(self, pil_image=None, **kwargs):
        super(PILImageViewverWidget, self).__init__("/res:logo.png", **kwargs)
        self._buf = None
        self.index = 0
        self.render_done = True

    def set_image(self, pil_image):
        if not self.render_done:
            return
        self.render_done = False
        self._buf = io.BytesIO()
        pil_image.save(self._buf, format='png')
        self.refresh()

    def refresh(self):
        self.attributes['src'] = "/%s/get_image_data?update_index=%d" % (id(self), self.index)
        self.index = self.index+1
        self.style['background-image'] = "url('/%s/get_image_data?update_index=%d')" % (id(self), self.index)

    def get_image_data(self, update_index):
        if self._buf is None:
            return None
        self.render_done = True
        self._buf.seek(0)
        headers = {'Content-type': 'image/png'}
        return [self._buf.read(), headers]


class MyApp(App):
    def main(self):
        main_container = gui.VBox(width="100%", height="100%", style={'margin':'0px auto'})

        #the following image widget will be used to show the PYGAME window content
        self.pil_image_viewer = PILImageViewverWidget()
        main_container.append(self.pil_image_viewer)

        #here we start the thread for pygame
        self.t = threading.Thread(target=self.pygame_main)
        self.t.start()

        return main_container

    def pygame_main(self):
        "This is the starfield code"
        #create our starfield
        random.seed()
        stars = initialize_stars()
        clock = pygame.time.Clock()
        #initialize and prepare screen
        pygame.init()
        screen = pygame.display.set_mode(WINSIZE)
        pygame.display.set_caption('pygame Stars Example')
        white = 255, 240, 200
        black = 20, 20, 40
        screen.fill(black)

        #main game loop
        done = 0
        while not done:
            draw_stars(screen, stars, black)
            move_stars(stars)
            draw_stars(screen, stars, white)
            pygame.display.update()
            for e in pygame.event.get():
                if e.type == QUIT or (e.type == KEYUP and e.key == K_ESCAPE):
                    done = 1
                    break
                elif e.type == MOUSEBUTTONDOWN and e.button == 1:
                    WINCENTER[:] = list(e.pos)
            clock.tick(50)

            #>>>>>>>>> here the pygame window gets shown in the REMI widget <<<<<<<<<
            surf = pygame.display.get_surface()
            data = pygame.image.tostring(surf, 'RGBA')
            img = Image.frombytes('RGBA', surf.get_size(), data)
            with self.update_lock: #thread sync
                self.pil_image_viewer.set_image(img)


if __name__ == "__main__":
    start(MyApp, address='0.0.0.0', port=0, start_browser=True, update_interval=0.1, enable_file_cache=True)

dddomodossola avatar Apr 28 '19 22:04 dddomodossola

WOW you're quick!

That's amazing. Is there a way of not showing the PyGame window. I managed to do it using tkinter in a way that embeds the window and doesn't open the PyGame window. I forgot what line of code I took out that stopped the other window from appearing.

Thank you so much for this code!!

MikeTheWatchGuy avatar Apr 28 '19 22:04 MikeTheWatchGuy

I managed to not display the PyGame window!

I added 2 os.environ calls and after that, only the Remi window shows the game!!!

THIS IS EPIC!!

Here is where I inserted those 2 lines

    def pygame_main(self):
        "This is the starfield code"
        #create our starfield
        random.seed()
        stars = initialize_stars()
        clock = pygame.time.Clock()
        #initialize and prepare screen

        os.environ['SDL_WINDOWID'] = str(1)
        os.environ['SDL_VIDEODRIVER'] = 'windib'

Now "all I have to do" is take your Remi code and translate it into PySimpleGUIWeb code, perhaps using the Widgets I've already got designed, or maybe having to add something new. I don't know yet. Need more studying of how you did this!

MikeTheWatchGuy avatar Apr 29 '19 15:04 MikeTheWatchGuy

Hmmmm.... unlike when I integrated with tkinter, key presses are not being returned to the application. With tkinter, I was able to do another level of integration. This statement: os.environ['SDL_WINDOWID'] = str(1) looks like this for the tkinter version: os.environ['SDL_WINDOWID'] = str(embed.winfo_id()) where embed is a tkinter Canvas widget.

MikeTheWatchGuy avatar Apr 29 '19 15:04 MikeTheWatchGuy

Well done @MikeTheWatchGuy and thank you for sharing. In the example I provided you there is really few remi code. The most of the code is from a pygame example.

To integrate keyboard events you should put a listener in the remi window. I will show you soon with an example. ;-)

dddomodossola avatar Apr 29 '19 15:04 dddomodossola

I figured something like that needed to happen. I already have in PySimpleGUIWeb the provision to return all key presses to the application (it's a flag that's set when the window is created). So that part I'm in good shape for, I think. I don't know how to then route the key presses Remi gives me over to PyGame.

If we show the Super Mario Brothers game running in a browser, perhaps even on Android, it'll be HUGE!

It's crazy this is a possibility!

You've REALLY opened up new possibilities.

MikeTheWatchGuy avatar Apr 29 '19 15:04 MikeTheWatchGuy

When I get code from you, my task is to translate your Remi solution into a PySimpleGUIWeb solution.

This one was a little difficult with the new Image class. I think I managed to use my Image Element to get to the SuperImage Widget that implements it.

When I try to run my code, I only see multiple copies of my logo, the initial image I used to create my Image Element. You did something similar with your logo.

Another difference for me is that the window does not turn black.

There is a method, get_image_data, that isn't getting called directly by the code. That makes me think that Remi is calling it. Perhaps I need to add a "get_image_data" to my SuperImage class that I can replace with the method you supplied?

Here's my code at the moment. I also removed the multithreading part since I multithread Remi already.

import os

import io
from PIL import Image
import threading

import random, math, pygame
from pygame.locals import *
import PySimpleGUIWeb as sg


#constants
WINSIZE = [640, 480]
WINCENTER = [320, 240]
NUMSTARS = 150


def init_star():
    "creates new star values"
    dir = random.randrange(100000)
    velmult = random.random()*.6+.4
    vel = [math.sin(dir) * velmult, math.cos(dir) * velmult]
    return vel, WINCENTER[:]


def initialize_stars():
    "creates a new starfield"
    stars = []
    for x in range(NUMSTARS):
        star = init_star()
        vel, pos = star
        steps = random.randint(0, WINCENTER[0])
        pos[0] = pos[0] + (vel[0] * steps)
        pos[1] = pos[1] + (vel[1] * steps)
        vel[0] = vel[0] * (steps * .09)
        vel[1] = vel[1] * (steps * .09)
        stars.append(star)
    move_stars(stars)
    return stars


def draw_stars(surface, stars, color):
    "used to draw (and clear) the stars"
    for vel, pos in stars:
        pos = (int(pos[0]), int(pos[1]))
        surface.set_at(pos, color)


def move_stars(stars):
    "animate the star values"
    for vel, pos in stars:
        pos[0] = pos[0] + vel[0]
        pos[1] = pos[1] + vel[1]
        if not 0 <= pos[0] <= WINSIZE[0] or not 0 <= pos[1] <= WINSIZE[1]:
            vel[:], pos[:] = init_star()
        else:
            vel[0] = vel[0] * 1.05
            vel[1] = vel[1] * 1.05


class PILImageViewverWidget():
    def __init__(self, image_elem=None, pil_image=None, **kwargs):
        self._buf = None
        self.index = 0
        self.render_done = True
        self.image_elem = image_elem        # type: sg.Image
        self.image_widget = image_elem.Widget # type: sg.SuperImage

    def set_image(self, pil_image):
        if not self.render_done:
            return
        self.render_done = False
        self._buf = io.BytesIO()
        pil_image.save(self._buf, format='png')
        self.refresh()

    def refresh(self):
        self.image_widget.attributes['src'] = "/%s/get_image_data?update_index=%d" % (id(self.image_widget), self.index)
        self.index = self.index+1
        self.image_widget.style['background-image'] = "url('/%s/get_image_data?update_index=%d')" % (id(self.image_widget), self.index)

    def get_image_data(self, update_index):
        if self._buf is None:
            return None
        self.render_done = True
        self._buf.seek(0)
        headers = {'Content-type': 'image/png'}
        return [self._buf.read(), headers]


def pygame_main():
    "This is the starfield code"
    #create our starfield
    random.seed()
    stars = initialize_stars()
    clock = pygame.time.Clock()
    #initialize and prepare screen

    # --------------------- PySimpleGUI window layout and creation --------------------
    layout = [[sg.T('Test of PySimpleGUI with PyGame')],
              [sg.Image(filename=r'C:\Python\PycharmProjects\GooeyGUI\logo200.png', size=(640, 480), background_color='lightblue', key='_IMAGE_')],
              [sg.B('Draw'), sg.Exit()]]

    window = sg.Window('PySimpleGUI + PyGame', layout).Finalize()
    image_elem = window.Element('_IMAGE_')
    pil_image_viewer = PILImageViewverWidget(image_elem)

    # -------------- Magic code to integrate PyGame with tkinter -------
    os.environ['SDL_WINDOWID'] = str(1)
    os.environ['SDL_VIDEODRIVER'] = 'windib'

    # pygame.init()

    screen = pygame.display.set_mode(WINSIZE)
    screen.fill(pygame.Color(255, 255, 255))

    pygame.display.init()
    pygame.display.update()

    pygame.display.set_caption('pygame Stars Example')
    white = 255, 240, 200
    black = 20, 20, 40
    # screen.fill(black)
    #main game loop
    done = 0
    count = 0
    while not done:
        event, values = window.Read(timeout=10)
        print(event, values) if event != sg.TIMEOUT_KEY else None
        if event in (None, 'Exit'):
            break
        draw_stars(screen, stars, black)
        move_stars(stars)
        draw_stars(screen, stars, white)
        pygame.display.update()
        for e in pygame.event.get():
            print(f'Event = {e}')
            if e.type == QUIT or (e.type == KEYUP and e.key == K_ESCAPE):
                print('**** FOUND  A KEY PRESS!')
                done = 1
                break
            elif e.type == MOUSEBUTTONDOWN and e.button == 1:
                print('**** FOUND  A MOUSE PRESS!')
                WINCENTER[:] = list(e.pos)
        clock.tick(50)

        #>>>>>>>>> here the pygame window gets shown in the REMI widget <<<<<<<<<
        surf = pygame.display.get_surface()
        data = pygame.image.tostring(surf, 'RGBA')
        img = Image.frombytes('RGBA', surf.get_size(), data)
        # with update_lock: #thread sync
        pil_image_viewer.set_image(img)
        # pil_image_viewer.get_image_data(count)
        count +=1
    window.Close()

if __name__ == "__main__":
    pygame_main()
    # t = threading.Thread(target=pygame_main)
    # t.start()

Here is what's output:

image

MikeTheWatchGuy avatar Apr 29 '19 16:04 MikeTheWatchGuy

Oh!! I needed to replace my get_image_data with the one supplied by the app:

image_elem.Widget.get_image_data = pil_image_viewer.get_image_data

After that I got this image!

image

It looks slow because I'm getting all this logging output. I still haven't figured out how to turn it off all...

image

MikeTheWatchGuy avatar Apr 29 '19 16:04 MikeTheWatchGuy

IT WORKS!

PyGame Space

It runs just as fast as your version :-)

And I do not see a second window!

THIS IS AMAZING!

MikeTheWatchGuy avatar Apr 29 '19 16:04 MikeTheWatchGuy

Check this out.... in my web browser!

image

Now I need help with 2 things....

  1. Turning off the warnings that are constantly scrolling. image
  2. Get the keyboard routed to PyGame. With my tkinter port, all that was required was clicking on the image, giving it focus. At the moment, no keys are being sent to PyGame.

This is getting to be really really cool!

MikeTheWatchGuy avatar Apr 29 '19 23:04 MikeTheWatchGuy

@MikeTheWatchGuy here is the version with events (Mouse position is correct) and sound redirection:

import remi.gui as gui
from remi import start, App
import os

import io
from PIL import Image
import threading

import random, math, pygame
from pygame.locals import *

from pygame.mixer import *

from time import sleep
import base64
import wave

#constants
WINSIZE = [640, 480]
WINCENTER = [320, 240]
NUMSTARS = 150


def init_star():
    "creates new star values"
    dir = random.randrange(100000)
    velmult = random.random()*.6+.4
    vel = [math.sin(dir) * velmult, math.cos(dir) * velmult]
    return vel, WINCENTER[:]


def initialize_stars():
    "creates a new starfield"
    stars = []
    for x in range(NUMSTARS):
        star = init_star()
        vel, pos = star
        steps = random.randint(0, WINCENTER[0])
        pos[0] = pos[0] + (vel[0] * steps)
        pos[1] = pos[1] + (vel[1] * steps)
        vel[0] = vel[0] * (steps * .09)
        vel[1] = vel[1] * (steps * .09)
        stars.append(star)
    move_stars(stars)
    return stars


def draw_stars(surface, stars, color):
    "used to draw (and clear) the stars"
    for vel, pos in stars:
        pos = (int(pos[0]), int(pos[1]))
        surface.set_at(pos, color)


def move_stars(stars):
    "animate the star values"
    for vel, pos in stars:
        pos[0] = pos[0] + vel[0]
        pos[1] = pos[1] + vel[1]
        if not 0 <= pos[0] <= WINSIZE[0] or not 0 <= pos[1] <= WINSIZE[1]:
            vel[:], pos[:] = init_star()
        else:
            vel[0] = vel[0] * 1.05
            vel[1] = vel[1] * 1.05


class PILImageViewverWidget(gui.Image):
    def __init__(self, pil_image=None, **kwargs):
        super(PILImageViewverWidget, self).__init__("/res:logo.png", **kwargs)
        self._buf = None
        self.index = 0
        self.render_done = True

    def set_image(self, pil_image):
        if not self.render_done:
            return
        self.render_done = False
        self._buf = io.BytesIO()
        pil_image.save(self._buf, format='png')
        self.refresh()

    def refresh(self):
        self.attributes['src'] = "/%s/get_image_data?update_index=%d" % (id(self), self.index)
        self.index = self.index+1
        self.style['background-image'] = "url('/%s/get_image_data?update_index=%d')" % (id(self), self.index)

    def get_image_data(self, update_index):
        if self._buf is None:
            return None
        self.render_done = True
        self._buf.seek(0)
        headers = {'Content-type': 'image/png'}
        return [self._buf.read(), headers]


app_instance = None

class MyApp(App):
    def idle(self):
        #THIS CODE ALLOWS TO REDIRECT VIDEO TO REMI
        try:
            surf = pygame.display.get_surface()
            data = pygame.image.tostring(surf, 'RGBA')
            img = Image.frombytes('RGBA', surf.get_size(), data)
            with self.update_lock: #thread sync
                self.pil_image_viewer.set_image(img)
        except:
            pass

    def main(self):
        global app_instance
        app_instance = self
        main_container = gui.VBox(width="100%", height="100%", style={'margin':'0px auto'})

        #the following image widget will be used to show the PYGAME window content
        self.pil_image_viewer = PILImageViewverWidget(style={})
        main_container.append(self.pil_image_viewer)
        
        #I connect keyboard events to the body (the entire page) because the main_container can't get focus in this case, and can't receive these events
        self.page.children['body'].onkeydown.connect(self.onkeydown)
        self.page.children['body'].onkeyup.connect(self.onkeyup)
        #I connect mouse events to the main_container in order to get mouse position relative to this widget
        self.pil_image_viewer.onmousemove.connect(self.onmousemove)
        self.pil_image_viewer.onmousedown.connect(self.onmousedown)
        self.pil_image_viewer.onmouseup.connect(self.onmouseup)
        
        return main_container
    
    def onkeydown(self, emitter, key, keycode, ctrl, shift, alt):
        #print("keydown: %s"%keycode)
        """ PYGAME COMMON EVENTS
        QUIT             none
        ACTIVEEVENT      gain, state
        KEYDOWN          unicode, key, mod
        KEYUP            key, mod
        MOUSEMOTION      pos, rel, buttons
        MOUSEBUTTONUP    pos, button
        MOUSEBUTTONDOWN  pos, button
        JOYAXISMOTION    joy, axis, value
        JOYBALLMOTION    joy, ball, rel
        JOYHATMOTION     joy, hat, value
        JOYBUTTONUP      joy, button
        JOYBUTTONDOWN    joy, button
        VIDEORESIZE      size, w, h
        VIDEOEXPOSE      none
        USEREVENT        code
        """
        #sending event to pygame
        ee = pygame.event.Event(KEYDOWN, {'key':int(keycode), 'mod':0})
        pygame.event.post(ee)
    
    def onkeyup(self, emitter, key, keycode, ctrl, shift, alt):
        #sending event to pygame
        ee = pygame.event.Event(KEYUP, {'key':int(keycode), 'mod':0})
        pygame.event.post(ee)

    def onmousemove(self, emitter, x, y):
        ee = pygame.event.Event(MOUSEMOTION, {'pos':(int(float(x)),int(float(y))), 'buttons':(False, False, False)})
        pygame.event.post(ee)

    def onmousedown(self, emitter, x, y):
        ee = pygame.event.Event(MOUSEBUTTONDOWN, {'pos':(int(float(x)),int(float(y))), 'button':1})
        pygame.event.post(ee)

    def onmouseup(self, emitter, x, y):
        ee = pygame.event.Event(MOUSEBUTTONUP, {'pos':(int(float(x)),int(float(y))), 'button':1})
        pygame.event.post(ee)

def pygame_main():
    "This is the starfield code"
    #create our starfield
    random.seed()
    stars = initialize_stars()
    clock = pygame.time.Clock()
    #initialize and prepare screen
    pygame.init()
    screen = pygame.display.set_mode(WINSIZE)
    pygame.display.set_caption('pygame Stars Example')
    white = 255, 240, 200
    black = 20, 20, 40
    screen.fill(black)

    #main game loop
    done = 0
    while not done:
        draw_stars(screen, stars, black)
        move_stars(stars)
        draw_stars(screen, stars, white)
        pygame.display.update()
        for e in pygame.event.get():
            if e.type == QUIT or (e.type == KEYUP and e.key == K_ESCAPE):
                #>>>>>>>>>>sound<<<<<<<<<<<
                pre_init(44100, -16, 1, 1024)
                pygame.init()
                
                snd = pygame.mixer.Sound("./Alarm01.wav")
                snd.play(-1)
                sleep(5)
                
                done = 1
                break
            elif e.type == MOUSEBUTTONDOWN and e.button == 1:
                WINCENTER[:] = list(e.pos)
        clock.tick(50)

#THIS CODE ALLOWS TO REDIRECT AUDIO TO REMI, place this after the App class
SND = pygame.mixer.Sound
class SoundProxy(pygame.mixer.Sound):
    def play(self, *args, **kwargs):
        global app_instance
        raw = self.get_raw()
        sio = io.BytesIO()
        
        sfile = wave.open(sio, 'w')
        frequency, _format, channels = pygame.mixer.get_init()
        # write raw PyGame sound buffer to wave file
        sfile.setframerate(frequency)
        sfile.setnchannels(channels)
        sfile.setsampwidth(2)
        sfile.writeframesraw(raw)
        sfile.close()
        sio.seek(0)

        data = "function beep(data) {var snd = new Audio(data); snd.play();}; beep('%s');"%("data:audio/wav;base64,%s"%base64.b64encode(sio.read()))
        with app_instance.update_lock:
            app_instance.execute_javascript(data)
        return SND.play(self, *args, **kwargs)
pygame.mixer.Sound = SoundProxy
Sound = SoundProxy


if __name__ == "__main__":
    #here we start the thread for pygame
    t = threading.Thread(target=pygame_main)
    t.start()
    start(MyApp, address='0.0.0.0', port=8081, start_browser=True, update_interval=0.01, enable_file_cache=True, multiple_instance=True)

have a good development ;-)

dddomodossola avatar May 03 '19 09:05 dddomodossola

Man, I just saw this thread, and I can only say that your support @dddomodossola is amazing and the work you are doing here is super cool!

awesomebytes avatar Jun 21 '19 07:06 awesomebytes

@awesomebytes Thank you a lot Sam :D

dddomodossola avatar Jun 21 '19 09:06 dddomodossola

@dddomodossola please format code samples on your comments. like

| ```python
| Your code here...
| ```
import remi.gui as gui
from remi import start, App
import os

import io
from PIL import Image
import threading

import random, math, pygame
from pygame.locals import *

from pygame.mixer import *

from time import sleep
import base64
import wave

#constants
WINSIZE = [640, 480]
WINCENTER = [320, 240]
NUMSTARS = 150


def init_star():
    "creates new star values"
    dir = random.randrange(100000)
    velmult = random.random()*.6+.4
    vel = [math.sin(dir) * velmult, math.cos(dir) * velmult]
    return vel, WINCENTER[:]


def initialize_stars():
    "creates a new starfield"
    stars = []
    for x in range(NUMSTARS):
        star = init_star()
        vel, pos = star
        steps = random.randint(0, WINCENTER[0])
        pos[0] = pos[0] + (vel[0] * steps)
        pos[1] = pos[1] + (vel[1] * steps)
        vel[0] = vel[0] * (steps * .09)
        vel[1] = vel[1] * (steps * .09)
        stars.append(star)
    move_stars(stars)
    return stars


def draw_stars(surface, stars, color):
    "used to draw (and clear) the stars"
    for vel, pos in stars:
        pos = (int(pos[0]), int(pos[1]))
        surface.set_at(pos, color)


def move_stars(stars):
    "animate the star values"
    for vel, pos in stars:
        pos[0] = pos[0] + vel[0]
        pos[1] = pos[1] + vel[1]
        if not 0 <= pos[0] <= WINSIZE[0] or not 0 <= pos[1] <= WINSIZE[1]:
            vel[:], pos[:] = init_star()
        else:
            vel[0] = vel[0] * 1.05
            vel[1] = vel[1] * 1.05


class PILImageViewverWidget(gui.Image):
    def __init__(self, pil_image=None, **kwargs):
        super(PILImageViewverWidget, self).__init__("/res:logo.png", **kwargs)
        self._buf = None
        self.index = 0
        self.render_done = True

    def set_image(self, pil_image):
        if not self.render_done:
            return
        self.render_done = False
        self._buf = io.BytesIO()
        pil_image.save(self._buf, format='png')
        self.refresh()

    def refresh(self):
        self.attributes['src'] = "/%s/get_image_data?update_index=%d" % (id(self), self.index)
        self.index = self.index+1
        self.style['background-image'] = "url('/%s/get_image_data?update_index=%d')" % (id(self), self.index)

    def get_image_data(self, update_index):
        if self._buf is None:
            return None
        self.render_done = True
        self._buf.seek(0)
        headers = {'Content-type': 'image/png'}
        return [self._buf.read(), headers]


app_instance = None

class MyApp(App):
    def idle(self):
        #THIS CODE ALLOWS TO REDIRECT VIDEO TO REMI
        try:
            surf = pygame.display.get_surface()
            data = pygame.image.tostring(surf, 'RGBA')
            img = Image.frombytes('RGBA', surf.get_size(), data)
            with self.update_lock: #thread sync
                self.pil_image_viewer.set_image(img)
        except:
            pass

    def main(self):
        global app_instance
        app_instance = self
        main_container = gui.VBox(width="100%", height="100%", style={'margin':'0px auto'})

        #the following image widget will be used to show the PYGAME window content
        self.pil_image_viewer = PILImageViewverWidget(style={})
        main_container.append(self.pil_image_viewer)
        
        #I connect keyboard events to the body (the entire page) because the main_container can't get focus in this case, and can't receive these events
        self.page.children['body'].onkeydown.connect(self.onkeydown)
        self.page.children['body'].onkeyup.connect(self.onkeyup)
        #I connect mouse events to the main_container in order to get mouse position relative to this widget
        self.pil_image_viewer.onmousemove.connect(self.onmousemove)
        self.pil_image_viewer.onmousedown.connect(self.onmousedown)
        self.pil_image_viewer.onmouseup.connect(self.onmouseup)
        
        return main_container
    
    def onkeydown(self, emitter, key, keycode, ctrl, shift, alt):
        #print("keydown: %s"%keycode)
        """ PYGAME COMMON EVENTS
        QUIT             none
        ACTIVEEVENT      gain, state
        KEYDOWN          unicode, key, mod
        KEYUP            key, mod
        MOUSEMOTION      pos, rel, buttons
        MOUSEBUTTONUP    pos, button
        MOUSEBUTTONDOWN  pos, button
        JOYAXISMOTION    joy, axis, value
        JOYBALLMOTION    joy, ball, rel
        JOYHATMOTION     joy, hat, value
        JOYBUTTONUP      joy, button
        JOYBUTTONDOWN    joy, button
        VIDEORESIZE      size, w, h
        VIDEOEXPOSE      none
        USEREVENT        code
        """
        #sending event to pygame
        ee = pygame.event.Event(KEYDOWN, {'key':int(keycode), 'mod':0})
        pygame.event.post(ee)
    
    def onkeyup(self, emitter, key, keycode, ctrl, shift, alt):
        #sending event to pygame
        ee = pygame.event.Event(KEYUP, {'key':int(keycode), 'mod':0})
        pygame.event.post(ee)

    def onmousemove(self, emitter, x, y):
        ee = pygame.event.Event(MOUSEMOTION, {'pos':(int(float(x)),int(float(y))), 'buttons':(False, False, False)})
        pygame.event.post(ee)

     def onmousedown(self, emitter, x, y):
         ee = pygame.event.Event(MOUSEBUTTONDOWN, {'pos':(int(float(x)),int(float(y))), 'button':1})
         pygame.event.post(ee)
 
     def onmouseup(self, emitter, x, y):
         ee = pygame.event.Event(MOUSEBUTTONUP, {'pos':(int(float(x)),int(float(y))), 'button':1})
         pygame.event.post(ee)
 
 def pygame_main():
     "This is the starfield code"
     #create our starfield
     random.seed()
     stars = initialize_stars()
     clock = pygame.time.Clock()
     #initialize and prepare screen
     pygame.init()
     screen = pygame.display.set_mode(WINSIZE)
     pygame.display.set_caption('pygame Stars Example')
     white = 255, 240, 200
     black = 20, 20, 40
     screen.fill(black)
 
     #main game loop
     done = 0
     while not done:
         draw_stars(screen, stars, black)
         move_stars(stars)
         draw_stars(screen, stars, white)
         pygame.display.update()
         for e in pygame.event.get():
             if e.type == QUIT or (e.type == KEYUP and e.key == K_ESCAPE):
                 #>>>>>>>>>>sound<<<<<<<<<<<
                 pre_init(44100, -16, 1, 1024)
                 pygame.init()
                 
                 snd = pygame.mixer.Sound("./Alarm01.wav")
                 snd.play(-1)
                 sleep(5)
                 
                 done = 1
                 break
             elif e.type == MOUSEBUTTONDOWN and e.button == 1:
                 WINCENTER[:] = list(e.pos)
         clock.tick(50)
 
 #THIS CODE ALLOWS TO REDIRECT AUDIO TO REMI, place this after the App class
 SND = pygame.mixer.Sound
 class SoundProxy(pygame.mixer.Sound):
     def play(self, *args, **kwargs):
         global app_instance
         raw = self.get_raw()
         sio = io.BytesIO()
         
         sfile = wave.open(sio, 'w')
         frequency, _format, channels = pygame.mixer.get_init()
         # write raw PyGame sound buffer to wave file
         sfile.setframerate(frequency)
         sfile.setnchannels(channels)
         sfile.setsampwidth(2)
         sfile.writeframesraw(raw)
         sfile.close()
         sio.seek(0)
 
         data = "function beep(data) {var snd = new Audio(data); snd.play();}; beep('%s');"%("data:audio/wav;base64,%s"%base64.b64encode(sio.read()))
         with app_instance.update_lock:
             app_instance.execute_javascript(data)
         return SND.play(self, *args, **kwargs)
 pygame.mixer.Sound = SoundProxy
 Sound = SoundProxy
 
 
 if __name__ == "__main__":
     #here we start the thread for pygame
     t = threading.Thread(target=pygame_main)
     t.start()
     start(MyApp, address='0.0.0.0', port=8081, start_browser=True, update_interval=0.01, enable_file_cache=True, multiple_instance=True)

Ksengine avatar Dec 07 '20 15:12 Ksengine