Search code examples
pythonpygamemedia-player

How to play video in Pygame currently?


I am looking to add videos in my game (such as cut scenes or an animated menu screen).

I looked around for a bit and it seems pygame doesn't support video playing anymore, so I was wondering if there was another way to get videos playing while being well integrated in my game, such as having the video playing in the background and having pygame elements (start button etc.) in the foreground.


Solution

  • There's an example somewhere (I was unable to find an original link) of using FFMPEG and another python module to decode the frames via a pipe and read these through into PyGame for displaying. I copied a code snippet (I thought from SO), and forgot about it.

    I have now adapted that technique to make a VideoSprite. It uses FFMPEG to decode (and rescale) the video stream, where it's read during the sprites update() to get the next frame.

    This is a very rough implementation, but I hope it gives you an idea of what's possible. While it would be nice if PyGame would just play the videos on its own, at least this method hands the video decoding and rescaling off to a subprocess where it hopefully runs on another CPU.

    (EDIT: added handler for video ending, and proper FPS control)

    import pygame
    import subprocess
    
    # Window size
    WINDOW_WIDTH    = 600
    WINDOW_HEIGHT   = 400
    WINDOW_SURFACE  = pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE
    
    DARK_BLUE = (   3,   5,  54)
    
    ### initialisation
    pygame.init()
    pygame.mixer.init()
    window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), WINDOW_SURFACE )
    pygame.display.set_caption("Video Sprite")
    
    
    class VideoSprite( pygame.sprite.Sprite ):
        FFMPEG_BIN = "/usr/bin/ffmpeg"   # Full path to ffmpeg executable
    
        def __init__(self, rect, filename, FPS=25 ):
            pygame.sprite.Sprite.__init__(self)
            command = [ self.FFMPEG_BIN,
                        '-loglevel', 'quiet',
                        '-i', filename,
                        '-f', 'image2pipe',
                        '-s', '%dx%d' % (rect.width, rect.height),
                        '-pix_fmt', 'rgb24',
                        '-vcodec', 'rawvideo', '-' ]
            self.bytes_per_frame = rect.width * rect.height * 3
            self.proc   = subprocess.Popen( command, stdout=subprocess.PIPE, bufsize=self.bytes_per_frame*3 )
            self.image  = pygame.Surface( ( rect.width, rect.height ), pygame.HWSURFACE )
            self.rect   = self.image.get_rect()
            self.rect.x = rect.x
            self.rect.y = rect.y
            # Used to maintain frame-rate
            self.last_at     = 0           # time frame starts to show
            self.frame_delay = 1000 / FPS  # milliseconds duration to show frame
            self.video_stop  = False
    
        def update( self ):
            if ( not self.video_stop ):
                time_now = pygame.time.get_ticks()
                if ( time_now > self.last_at + self.frame_delay ):   # has the frame shown for long enough
                    self.last_at = time_now
                    try:
                        raw_image = self.proc.stdout.read( self.bytes_per_frame )
                        self.image = pygame.image.frombuffer(raw_image, (self.rect.width, self.rect.height), 'RGB')
                        #self.proc.stdout.flush()  - doesn't seem to be necessary
                    except:
                        # error getting data, end of file?  Black Screen it
                        self.image = pygame.Surface( ( self.rect.width, self.rect.height ), pygame.HWSURFACE )
                        self.image.fill( ( 0,0,0 ) )
                        self.video_stop = True
    
    
    ### Create Video Area
    video_sprite1 = VideoSprite( pygame.Rect( 100, 100, 320, 240 ), '1975_test_pattern.mp4' )
    video_sprite2 = VideoSprite( pygame.Rect( 100, 100, 160,  90 ), '/home/kingsley/Videos/rocket.avi' )  # 640x360
    #sprite_group = pygame.sprite.GroupSingle()
    sprite_group = pygame.sprite.Group()
    sprite_group.add( video_sprite1 )
    sprite_group.add( video_sprite2 )
    
    ### Main Loop
    clock = pygame.time.Clock()
    done = False
    while not done:
    
        # Handle user-input
        for event in pygame.event.get():
            if ( event.type == pygame.QUIT ):
                done = True
            elif ( event.type == pygame.MOUSEBUTTONUP ):
                # On mouse-click
                pass
    
        # Movement keys
        keys = pygame.key.get_pressed()
        if ( keys[pygame.K_UP] ):
            video_sprite2.rect.y -= 10
        if ( keys[pygame.K_DOWN] ):
            video_sprite2.rect.y += 10
        if ( keys[pygame.K_LEFT] ):
            video_sprite2.rect.x -= 10
        if ( keys[pygame.K_RIGHT] ):
            video_sprite2.rect.x += 10
        
    
        # Update the window, but not more than 60fps
        sprite_group.update()
        window.fill( DARK_BLUE )
        sprite_group.draw( window )
        pygame.display.flip()
    
        # Clamp FPS
        clock.tick_busy_loop(25)  # matching my video file
    
    pygame.quit()
    

    Obviously, since it's just the video stream, there's no sound. But for all intents and purposes it's just another sprite. When the video runs out we catch the error and go black.

    example output

    NOTE: Sometimes when I run this, it breaks the terminal echoing in Linux. I suspect it's something to do with the subprocess and/or pipe. Running reset fixes this. It seems a common problem with subprocesses.