A package that allows you to record your pygame game
Project description
A Simple pygame ScreenRecorder
Why you should use pygame ScreenRecorder?
- Relatively high accuracy
- No (noticable) performance issues
- Recording FPS is not bound to game FPS
- Straightforward usage
- The codebase is well commented and typed
Dependencies
Apart from pygame and python >=3.8
- opencv-python (includes numpy)
- FFmpeg if you want to save videos
Install
pip install pygame-screen-record
FAQ
See the FAQ in the wiki
To-Dos
- Event recording (mouse, key, quit, etc.) ✔️
- Sound recording (Either event based with function hooking or as numpy arrays)
- A proper video player
- Add a wiki ✔️
Contributing
File any bugs or feature requests as GitHub issues. Any comments are always welcome.
How To Use
Probably you just want to make a recording of their game and save it in a video file. Here comes how.
Note: I am using
pg
as an alias forpygame
in the following code snippets
A typical game script might look like this:
import pygame as pg
pg.init()
init_code()
try:
while True:
event_handling()
updating()
drawing()
finally:
clean_up()
pg.quit()
The try - finally - statement
is very important to catch any exceptions and clean up whether the game ended naturely or not.
Adding a recorder is very simple:
import pygame as pg
from pygame_screen_record import ScreenRecorder
pg.init()
init_code()
recorder = ScreenRecorder(60) # pass your desired fps
recorder.start_rec() # start recording
try:
while True:
event_handling()
updating()
drawing()
finally:
recorder.stop_rec() # stop recording
recorder.save_recording("my_recording.mp4") # saves the last recording
clean_up()
pg.quit()
This code will record your screen the whole game and then save it in the current working directory as my_recording.mp4
.
Typical values for the frames per second (fps) are 24 (for slow games), 30, 60 and 120 (Most displays only refresh at 60 Hertz, so most users won't see a difference upwards of 60 fps). Don't forget that the fps value is (at least in theory) proportional to the memory consumption of your recording.
One cool thing of many is that you can chain functions. So for example
recorder = ScreenRecorder(60)
recorder.start_rec()
is equivalent to
recorder = ScreenRecorder(60).start_rec()
and you can also just write: recorder.stop_rec().save_recording("my_recording.mp4")
Advanced Recording Options
Some of the options available:
- Record multiple recordings
- Record any surface
- Compress
ScreenRecorders
- Stream recordings (maybe add frame streaming too?)
- Apply effects on recordings
Record multiple recordings
In the example, we only recorded a single recording. However, you can record as many recordings as you like with a single ScreenRecorder
. To stop and start recordings use stop_rec
and start_rec
. You can also abort recordings using abort_rec
.
Finally, when you want to save your recordings call save_recordings("mp4")
. More detailed saving options are given in the next chapter Advanced Saving Options.
Record any Surface
In most cases you want to record the whole screen. But, you can also pass an optional argument surf
to a ScreenRecorder
.
my_surface = pg.surface.Surface((900, 600))
recorder = ScreenRecorder(60, my_surface)
Compress recordings
You can choose to compress your recordings like this
recorder = ScreenRecorder(compress=2) # fps defaults to 60
What does that mean? It means that every frame will be scaled down 2
-times by two. This reduces the total memory consumption by 2^(2^2) = 16
! But normally you will only compress by one or not compress at all. The recordings will automatically be decompressed when played or saved. So don't worry about that. Just try out whether the loss in resolution is okay for your needs.
Stream recordings
A stream in this sense is any object that implements a send
function that can take a recording. To set a stream pass it to the ScreenRecorder
constructor.
class Stream:
def send(self, rec):
print(f"Recording received with {rec.frame_number} frames, a size of {rec.size} and a total length of {rec.length} s")
my_stream = Stream()
recorder = ScreenRecorder(stream=my_stream)
Now recorder.stop_rec()
will send to that stream and also save the recording internally. With recorder.stop_rec_to_stream(stream = None)
you send to the stream without saving and can optionally specify a stream that will override the recorders default stream.
Set individual recordings fps
ScreenRecorder(60).start_rec(30)
will record at 30 fps for this one recording.
Get all recordings of a recorder
all_recordings = recorder.get_recordings()
Attributes of a recorder
These are the attributes of a ScreenRecorder
instance. Don't change any of these if you don't have a reason! Create a new recorder instead.
fps: float
selfexp.
surf: pg.Surface
selfexp.
compress: int
selfexp.
size: Tuple[int,int]
The size (width, height) of the recorded surface. Change this attribute only if you are also manually changing the surface at the same time.
dt: float
The time between (delta time) two frames in ms.
dt = 1000/fps
recordings: List[Recording]
selfexp. Same as get_recordings()
Post-Processing a recording
- Add frames
- Resize
- Apply effects
Adding frames
You can always append frames to a recording:
recording.add_frame(frame: pg.Surface)
Resize a recording
You might need to rescale a whole recording to a specific size:
recording.resize(size: Tuple[int,int])
Apply effects
If there is more to do than just resizing:
recording.apply(effect: Callable[[pg.Surface], pg.Surface])
Will apply the effect on every frame of the recording.
Attributes of a recording
These are pretty much the same as the attributes of a ScreenRecorder
fps: float
selfexp.
surf: pg.Surface
selfexp.
compress: int
selfexp.
size: Tuple[int, int]
selfexp. Change this attribute only if you are also manually changing the frames at the same time (e.g. Applying a resizing filter).
dt: float
selfexp.
frames: List[pg.Surface]
selfexp.
Advanced Saving Options
Introducing the RecordingSaver
RecordingSaver(recordings: List[Recording], key: str | Sequence[str] | Callable[[int], Optional[str]], save_dir: AnyPath = None, blocking: bool = True)
recordings = recorder.get_recordings()
saver = RecordingSaver(recordings, "mp4", "saved_files")
saver.save()
Saves the given recordings as mp4
files in ./saved_files
. But you can also just call
saved_recordings = recorder.save_recordings("mp4", "saved_files")
Explanation
-
key
You can either give a str, a list or a function
If key is a str that determines the format of the recordings and they will be saved asrecording_0.{key}
,recording_1.{key}
, etc.Valid formats are listed if you call
available_formats()
. You can add/update FFmpeg-supported formats by callingadd_codec(format:str, codec: int | str)
.Maybe its worth to mention the
npz
file format. It is not a classical video format but actually a way to save numpy arrays (npz = numpy zipped). If you don't need to share the recording in the internet or so, this is an efficient alternative. Also this library has built-in support for replaying these files.If key is a sequence then the ith recording will be saved as the ith element of the recording. If an element is
None
the according recording will be skipped.It is a very similar case if you give a function. The function gets an int passed and should return
None
or a filename.An example for such a key is
key = lambda x: if x%2 == 0 then None else ("recording_{x}","mp4")
This will return
None
(and skip the save) for every recording with an even index. -
save_dir
The directory where the recordings will be saved. Defaults to the current working directory -
blocking
Whether the save should block. Defaults toTrue
The save returns a list of paths to the recordings in the given directory. This list will not always be the same length as the recordings in the Recorder
but will only return a list of recordings that were actually saved.
However, if you set block = False
the function will return another function that returns the list of paths and must be called before the script ends! Now you might ask yourself why that makes any sense. Here is an example
# At this point we have a recorder that recorded some recordings
# Now we want to save the recordings as `mp4` and also as `npz`
import time #to measure how long the saves took
# A naive approach is this
start = time.time()
recorder.save_recordings("mp4", "saved_files1")
recorder.save_recordings("npz", "saved_files1")
print("First save took:", time.time() - start)
# Now we use non-blocking (asynchronous) code
start = time.time()
join1 = recorder.save_recordings("mp4", "saved_files2", False)
join2 = recorder.save_recordings("npz", "saved_files2", False)
print("This message doen't have to wait for the save. Instead it comes almost instantly")
time.sleep(2) # We add some more virtual io with time.sleep
# Finally we join the save
join1()
join2()
print("Second save took:", time.time() - start)
The second option is favorable because it takes less time than the first.
Memory Cosniderations
We talked about how to efficiently save your recordings (from a time aspect). But now we talk about how you can reduce memory consumption. Generally, all video recordings will be automatically compressed by FFmpeg/numpy. However, there are three ways you can reduce memory consumption:
-
Reduce fps. One cool thing about this ScreenRecorder is that you can record at a different framerate than you play the game. For example you can have a game frame rate of 60 fps but only record at 30 fps. This would halve the memory usage in comparison to if you recorded at 60 fps.
-
Resize the recording. We already established that you can halve the recording size as often as you like. But this will only reduce memory usage while the program runs. The result will still be saved in the original size. However, you can save the recording in a smaller size by using
recording.resize(pg.Vector2(recording.size)/your_scale_factor)
This will actually save and play the recording in that size, which might look very weird. So you might not actually want to do that. -
Shorten the recording length. You can cut out parts of a recording like this. Lets say you only want the first 300 frames.
# This will actually mutate the recording. recording[0:300]
-
Lastly, you can reduce the depth of the recording by reducing the depth of the recorded screen
pg.display.set_mode((900,600), depth=your_depth)
. This will definitely reduce the memory usage while the program is running and it might also reduce the memory usage on the disk. However, decreasing the depth of the screen will also decrease the variety in color. But in most amateur applications this might just not matter anyway because you are not using very nuanced colors.
Replay recordings
First note
I had already implemented a VideoPlayer that could play a Full HD video (1920x816) pretty well (Thats over 6 MB per frame at 24 fps). However, there were several issues (without much detail):
- Missing sound
- Memory
- Lags/Preloading (combined with Memory)
Finally, I came to the conclusion that it makes no sense to write a VideoPlayer in pure Python.
Easiest way to replay a recording
# easiest
player = RecordingPlayer(recording).play()
# with an on_stop callback
def on_stop():
print("Playing finished")
player = RecordingPlayer(recording, on_stop).play()
# with a different surface
my_surface = pg.Surface((900,600))
player = RecordingPlayer(recording, None, my_surface).play()
Make sure that you are not blitting anything else to the surface. However, you still have to do the flipping/updating yourself (I figured it would be weird if the player did that for you).
Very important is that you always stop
a player in your finally-clause
even if you normally wait for the player to end. Here a contextmanager might make sense too but I'm tending to rather no.
Advanced RecordingPlayer
Options
Pausing
You already know how to start a player. You can also pause the player with pause
. Playing is also unpausing.
player.pause()
Stopping
This stops the player. As said above, always stop the player if in doubt. However, don't even try to reuse a manually stopped player. Just make a new player instead, it's really simple.
player.stop()
Seeking
Seeking is known from files and means going to a certain position.
player.seek(300) # goes to frame 300 / the 301th frame
player.seekms(3000.0) # goes to second 3 of the recording
Telling
Similarly telling is also known from files and means getting the current position.
player.tell() # Gets the current position
player.tellms() # Gets the current position in milliseconds
Restarting
This method is a mixture between reviving the player after it stopped and just seeking the very first frame and playing.
player.restart()
Getting state information
The player has a is_
function. There are two reasons for the name
- It resolves the conflict with the python keyword
is
- It might make the code more readable, reading
player.is_("playing")
is easy to understand and nicer to implement than making individual function for every possible state
player.is_("started")
player.is_("stopped")
player.is_("playing")
player.is_("paused")
They are all pretty self explanatory. But remember two things:
player.is_("stopped")
might be the most important state because you shouldn't call any other function when the player is stopped (Except restart and stop).is_("paused")
is not equal tonot is_("playing")
(In the beginning the player is neither playing nor paused)
Making use of the on_stop
The on_stop
is a very powerful tool because callbacks are always cool. Optionally, on_stop
will be passed the player object itself. So, you can really do anything you want. I wrote three example callbacks which are very likely to be useful to the API user. Don't forget to import them before you use them (They are not included in *
).
-
play_indefinite
will restart the player indefinitely as long as the player is not stopped manually (Stopping the player manually will overwrite the on_stop).player = RecordingPlayer(recording,play_indefinite).play()
-
play_n_times
plays the playern
times.player = RecordingPlayer(recording,play_n_times(5)).play() # plays 5 times
-
play_n_wrapper
plays the playern
times but it wraps another function to call each time. To come back to our very firston_stop
:@play_n_wrapper(5) def on_stop(): print("Playing finished") player = RecordingPlayer(recording,on_stop).play()
Playing saved npz files
I showed you how to save your recordings as npz
files. However, you also need to know how to play them back. For this there is a NPZPlayer
class. It takes a path to a file and extra parameters just like the RecordingPlayer
player = NPZPlayer("my_npz_file.npz", on_stop=my_on_stop).play()
In all other regards it is the same as the RecprdingPlayer
Event Register
One of my to-dos was an event register. This task is accomplished. Here comes the tutorial for this.
Let's suppose you are using events and have a deterministic game (No randomness/randomness with a seed). You just need to do four things to record your game.
import EventRegister from EventRegister
- Create a new
EventRegister
object - Get your events from the object
- Finally, save the registered events.
Example
import EventRegister from EventRegister
pg.init()
reg = EventRegister("in","events.json") # save as json
try:
running = True
while running:
time.sleep(0.0099) # 100 fps
for event in reg.get_events(): # instead of pg.event.get()
if event.type == pg.QUIT:
running = False
elif event.type == pg.MOUSEBUTTONDOWN:
print(event.button,event.pos)
finally:
pg.quit()
reg.save()
Now to replay that exact recorded game. Just swap in
with out
when instanciating the reg
object and everything should work exactly as expected.
Random Seeds
If your game uses randomness - which most games should - it's very simple. This will automatically load or save the seed depending on the mode.
reg = EventRegister("in","events.json").seed()
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file pygame_screen_record-0.1.1.tar.gz
.
File metadata
- Download URL: pygame_screen_record-0.1.1.tar.gz
- Upload date:
- Size: 25.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/5.0.0 CPython/3.9.19
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 5d620f60ccc3579fe009f7f48f76ea3ac447a957c1539d059c4295332f25ba3d |
|
MD5 | a2549f3f3a9f9ba63cdd345737e45515 |
|
BLAKE2b-256 | 380619e22ca92a13c981f94c05bed53722abd1beb2a6b6165b28847ec6af248f |
File details
Details for the file pygame_screen_record-0.1.1-py3-none-any.whl
.
File metadata
- Download URL: pygame_screen_record-0.1.1-py3-none-any.whl
- Upload date:
- Size: 19.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/5.0.0 CPython/3.9.19
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | dd99c6e42d515b52de4db520514f20fad172423d45afd677f4b4132f90c535c7 |
|
MD5 | 29f76c5c1f5c6edae3a9e5aefed287c4 |
|
BLAKE2b-256 | 11dadafe0d52aa833b5f92f02e1148469632b2db7d69645cb885ddd180398167 |