Von Haus aus gibt es keine Möglichkeit eine mit PyGame erstellte App als Video aufzuzeichnen. Natürlich könnte man den gesamten Bildschirm aufzeichnen, aber da das für meinen Anwendungsfall nicht infrage kam, habe ich mich nach anderen Möglichkeiten umgesehen.

Mein erster Ansatz war jeden einzelnen Frame als PNG-Datei zu speichern und die Bilder anschließend zu einem Video zusammenzufügen. Da dies – je nach länge der Aufzeichnung – aber viel Speicherplatz kostet und die Schreibzugriffe auf die SSD das Spiel zusätzlich verlangsamt haben, habe ich eine Variante gesucht, bei der man die Frames “streamen” kann.

Auf Github hat jemand eine beispiel App veröffentlich, die jeden Frame mit Hilfe von OpenCV konvertiert und an FFMPEG streamt. Ein besserer Ansatz, da hierdurch die Festplattenzugriffe entfallen. Leider litt die Performance dennoch unter der Konvertierung was zum Einbrechen der Framerate führte.

Statt OpenCV, habe ich probiert den PyGame Screen ausschließlich mit NumPy zu konvertieren. Dies hat den gewünschten Erfolg gebracht: Das Video wird aufgenommen ohne spürbare Drops in der Framerate.

Video aufnehmen mit FFMPEG

Zunächst installieren wir FFMPEG, ein mächtiges CLI-Tool für jegliche Art von Videokodierung. Auch das zusammensetzen einzelner Bilder zu einem Video ist damit möglich.

Linux:

sudo apt install ffmpeg

Mac (mit Homebrew):

brew install ffmpeg

Wir können nun FFMPEG als Subprozess innerhalb unserer PyGame App nutzen. Zuerst importieren wir die benötigten Module:

import pygame
import numpy as np
import subprocess as sp

Starten des Subprozesses:

proc = sp.Popen(['ffmpeg',
    '-y',
    '-f', 'rawvideo',
    '-vcodec', 'rawvideo',
    '-s', str(WIDTH)+'x'+str(HEIGHT),
    '-pix_fmt', 'rgb24',
    '-r', str(FPS),
    '-i', '-',
    '-an',
    '-pix_fmt', 'yuv420p',
    '-vcodec', 'libx264',
    '-crf', '10',
    '-v', 'warning',
    'video.mp4'],
    stdin=sp.PIPE, stdout=sp.PIPE, stderr=None)

Die Parameter WIDTH, HEIGHT und FPS sollten mit den für PyGame eingestellten Werten übereinstimmen. Der für die Enkodierung genutzte Kodex und Qualitätseinstellungen (vcodec, crf) können nach Belieben angepasst werden, siehe dazu die FFMPEG-Dokumentation.

Für jeden einzelnen Frame führen den folgenden Code aus, um den PyGame Screen in das von FFMPEG erwartete Format “RGB24” zu bringen und senden den Frame anschließend in binärer Form an den Subprozess:

r = pygame.surfarray.pixels_red(screen)
g = pygame.surfarray.pixels_green(screen)
b = pygame.surfarray.pixels_blue(screen)
bb = np.dstack((r,g,b)) # Combine RGB channels

# Delete to unblock surface
del r
del g
del b

bb = np.rot90(bb) # Rotate
bb = np.flipud(bb) # Mirror
proc.stdin.write(bb.tobytes())

In den Zeilen 1-3 rufen wir die 2D-Arrays der einzelnen Farbkanäle des PyGame Screens ab. In Zeile 4 fügen wir die Kanäle mit Hilfe von NumPy zu einem 3D-Array zusammen. In Zeile 7-9 löschen wir die nun nicht mehr benötigten 2D-Arrays. Dies ist notwending, da PyGame den Screen durch die Abfragen gelockt hat und erst wieder freigibt wenn die Variablen dereferenziert wurden. Nun müssen wir unseren Array noch um 90° drehen und spiegeln (Zeile 11-12), damit er kompatibel zu FFMPEG wird. In Zeile 13 senden wir die Bytes des Arrays an den FFMPEG Prozess.

Wenn wir fertig mit der Aufnahme sind, schließen wir den FFMPEG Prozess, damit das Video final gespeichert werden kann:

proc.stdin.close()
proc.wait()

Alternative: Imageio

Eine Alternative bietet die grafische Python Library Imageio. Auch diese kann (durch ein Zusatzmodul) mit FFMPEG umgehen, wodurch sich ein wenig Code einsparen lässt (siehe Beispiele).

Installieren der Module:

pip install imageio imageio-ffmpeg

Öffnen des Videos:

import imageio
writer = imageio.get_writer('video.mp4', fps=FPS)

Für jeden Frame führen wir die Zeilen 1-12 genau wie oben aus. Anschließend senden wir die Daten an FFMPEG. Da diese Funktion auch ein NumPy-Array akzeptiert, brauchen wir nicht die tobytes()-Funktion aufrufen:

writer.append_data(bb)

Video speichern:

writer.close()

Fazit

Sowohl das direkte Einbinden von FFMPEG, als auch die Nutzung von Imageio ermöglichen ein problemloses und schnelles Aufzeichnen von Videos mit PyGame. Wer sich mit FFMPEG auskennt und genaue Kontrolle über den CLI-Befehl haben möchte, sollte die erstere Variante nutzen. Wer weniger Codezeilen bevorzugt und kein Problem damit hat zwei zusätzliche Module zu installieren, kann auf Imageio zurückgreifen.