2

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.

Max
  • 160
  • 3
  • 15

2 Answers2

4

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.

Kingsley
  • 14,398
  • 5
  • 31
  • 53
  • I've tried to run the code as it is (added shell=True in self.proc, changed my video directory) but I've got some issues, firstly: ffmpeg isn't recognized as an internal command. I think I don't really get the FFMPEG thing, I used this tutorial to install a ffmpeg python module: https://medium.com/@yanweiliu/how-to-use-ffmpeg-on-python-2ba3fa360ba7 though it seemed weird since you hadn't imported this module, either way it doesn't work. Next error is at line 33 "ValueError: Buffer length does not equal format and resolution size" – notproplayer 3 Jul 13 '20 at 08:25
  • @notproplayer3 - It's using the FFMPEG executable directly to decode the video, and the options cause it output thousands of RGB byte-triples representing pixels. So you only need to give it the path to `ffmpeg.exe`, this could be something like `C:\\Downloads\\ffmpeg.exe` or `/usr/bin/ffmpeg` etc. I'll investigate the buffer length and edit if necessary. – Kingsley Jul 13 '20 at 22:02
  • 1
    This is absolutely working now, thank you so much for taking the time to help me, I really appreciate it – notproplayer 3 Jul 15 '20 at 08:50
2

I wrote a code which seems to work very well with moviepy, I was inspired by that of Kingsley.

import moviepy.editor
import moviepy.video.fx.all
import pygame

class VideoSprite(pygame.sprite.Sprite):
    def __init__(self, rect, filename):
        pygame.sprite.Sprite.__init__(self)
        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
        self.video = moviepy.editor.VideoFileClip(filename).resize((self.rect.width, self.rect.height))
        self.video_stop = False

    def update(self, time=pygame.time.get_ticks()):
        if not self.video_stop:
            try:
                raw_image = self.video.get_frame(time / 1000)  # /1000 for time in s
                self.image = pygame.image.frombuffer(raw_image, (self.rect.width, self.rect.height), 'RGB')
            except:
                self.video_stop = True

The advantage here is that the video is at the right speed