Package: pygame-screen-record
A Simple PyGame ScreenRecorderWhy you should use this module?Relatively high accuracyNo (noticable) performance issuesRecording FPS is not bound to game FPSStraightforward usageCodebase is well commented and typedWhy you shouldn't use this module?The library is written in pure Python and therefore cannot compare with any lower level library.Everything is still in develompment.DependenciesApart from pygame and python >=3.8opencv-python (includes numpy)FFmpeg if you want to save videosInstallpip install pygame-screen-recordFAQSee the FAQin the wikiTo-DosEvent recording (mouse, key, quit, etc.) ✔️Sound recording (Either event based with function hooking or as numpy arrays)A proper video playerAdd a wikiContributingFile any bugs or feature requests as GitHub issues. After your idea was approved, we can start working on a solution and make a pull request. The codebase is pretty simple and easy to get ahold of.How To UseMost users just want to make a recording of their game and save it in a video file. Here comes how.Note: I am usingpgas an alias forpygamein the following code snippets and Iimport ScreenRecorderinstead ofpygame_screen_record.ScreenRecorder.A typical game script might look like this:importpygameaspgpg.init()init_code()try:whileTrue:event_handling()updating()drawing()finally:clean_up()pg.quit()Thetry - finally - statementis very important to catch any exceptions and clean up whether the game ended naturely or not.Adding a recorder is very simple:importpygameaspgfromScreenRecorderimportScreenRecorder,save_recordingspg.init()init_code()recorder=ScreenRecorder(60)# Pass your desired fpsrecorder.start_rec()# Start recordingtry:whileTrue:event_handling()updating()drawing()finally:recorder.stop_rec()# stop recordingrecording=recorder.get_single_recording()# returns a Recordingsave_recordings(recording,("my_recording","mp4"))# save the recording as mp4clean_up()pg.quit()This code will record your screen the whole game and then save it in the current working directory asmy_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:recorder=ScreenRecorder(60)recorder.start_rec()is equivalent torecorder=ScreenRecorder(60).start_rec()Andrecorder.stop_rec()recording=recorder.get_single_recording()save_recording(recording,("my_recording","mp4"))is equivalent torecorder.stop_rec().get_single_recording().save("my_recording","mp4")Short word on typesThe whole codebase is typed -> If you want to contribute use types tooAnyPath means any type that can be a file or directorystr | int | bytesnormally.For me,pg.Surfaceis equivalent topg.surface.Surface. (mypy thinks differently)Advanced Recording OptionsSome of the options available:Record any surfaceCompressScreenRecordersStream recordings (maybe add frame streaming too?)Apply effects on recordingsRecord any SurfaceIn most cases you want to record the whole screen. But, you can also pass an optional argumentsurfto aScreenRecorder.my_surface=pg.surface.Surface((900,600))recorder=ScreenRecorder(60,my_surface)Compress recordingsYou can choose to compress your recordings like thisrecorder=ScreenRecorder(compress=2)# fps defaults to 60What does that mean? It means that every frame will be scaled down2-times by two. This reduces the total memory consumption by2^(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 recordingsA stream in this sense is any object that implements asendfunction that can take a recording. To set a stream pass it to theScreenRecorderconstructor.classStream:defsend(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)Nowrecorder.stop_rec()will send to that stream and also save the recording internally. Withrecorder.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 fpsScreenRecorder(60).start_rec(30)will record at 30 fps for this one recording.Abort a running recordingrecorder.abort_rec()Get all recordings of a recorderall_recordings=recorder.get_recordings()Attributes of a recorderThese are the attributes of aScreenRecorderinstance. Don't change any of these if you don't have a reason! Create a new recorder instead.fps: floatselfexp.surf: pg.Surfaceselfexp.compress: intselfexp.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: floatThe time between (delta time) two frames in ms.dt = 1000/fpsrecordings: List[Recording]selfexp. Same asget_recordings()Post-Processing a RecordingAdd framesResizeApply effectsAdding framesYou can always append frames to a recording:recording.add_frame(frame: pg.Surface)Resize a RecordingYou might need to rescale a whole recording to a specific size:recording.resize(size: Tuple[int,int])Apply effectsIf 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 recordingThese are pretty much the same as the attributes of a ScreenRecorderfps: floatselfexp.surf: pg.Surfaceselfexp.compress: intselfexp.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: floatselfexp.frames: List[pg.Surface]selfexp.Advanced Saving OptionsIntroducing the RecordingSaverRecordingSaver(recordings: List[Recording], key: str | SupportsIndex | Callable[[int], Optional[Tuple[str,str]]], save_dir: AnyPath = None, blocking: bool = True)recordings=recorder.get_recordings()saver=RecordingSaver(recordings,"mp4","saved_files")saver.save()Saves the given recordings asmp4files in./saved_filesWe learned that there is a convenient way to do anything. Just callsaved_recordings=recorder.save_recordings("mp4","saved_files")ExplanationkeyYou can either give a str, a list or a functionIf 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 importavailable_formatsfromScreenRecorder.py. You can add/update FFmpeg-supported formats by callingadd_codec(format:str, codec: int | str).An interesting option to mention is thenpzfile 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. This library has built-in support for replaying these files.If key is a list then the ith recording will be saved as the ith element of the recording. This should be a(filename,format)tuple. If an element isNonethe recording will be skipped.It is a very similar case if you give a function. The function gets an int passed and should returnNoneor a(filename,format)tuple.An example for such a key iskey=lambdax:ifx%2==0thenNoneelse("recording_{x}","mp4")This will returnNone(and skip the save) for every recording with an even index.save_dirThe directory where the recordings will be saved. Defaults to the current working directoryblockingWhether the save should block. Defaults to TrueThe save returns a list of paths to the recordings in the given directory. This list will notalwaysbe the same length as the recordings in theRecorderbut will only return a list of recordings that were actually saved.However, if you setblock = Falsethe 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`importtime#to measure how long the saves took# A naive approach is thisstart=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) codestart=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 savejoin1()join2()print("Second save took:",time.time()-start)The second option is favorable because it takes less time than the first. Unfortunately, I haven't yet implemented a contextmanager for this so you will have to manually join or write your own and contribute to the project.Memory ManagementWe 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 usingrecording.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.recording[0:300]#This will actually mutate the recording. If you have issues with this then just share your concernsLastly, you can reduce the depth of the recording by reducing the depth of the recorded screenpg.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 recordingsFirst noteI 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 soundMemoryLags/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#easiestplayer=RecordingPlayer(recording).play()# with an on_stop callbackdefon_stop():print("Playing finished")player=RecordingPlayer(recording,on_stop).play()# with a different surfacemy_surface=pg.Surface((900,600))player=RecordingPlayer(recording,None,my_surface).play()Make sure that you are not blitting anything else to the surface. 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 alwaysstopa player in yourfinally-clauseeven if you normally wait for the player to end. Here a contextmanager might make sense too but I'm tending to rather no.AdvancedRecordingPlayerOptionsPausingYou already know how to start a player. You can also pause the player withpause. Playing is also unpausing.player.pause()StoppingThis stops the player. As said above, always stop the player if in doubt. However, don't even try to reuse a manually stopped player. As with other objects just make a new player. It's really simple.player.stop()SeekingSeeking is known from files and means going to a certain position.player.seek(300)# goes to frame 300 / the 301th frameplayer.seekms(3000.0)# goes to second 3 of the recordingTellingSimilarly telling is also known from files and means getting the current position.player.tell()# Gets the current positionplayer.tellms()# Gets the current position in millisecondsRestartingThis method is a mixture between reviving the player after it stopped and just seeking the very first frame and playing.player.restart()In the future, restart might take a new recording to play. But that is only a thought.Getting state informationThe player has ais_function. There are two reasons for the nameIt resolves the conflict with the python keywordisIt might make the code more readable, readingplayer.is_("playing")is easy to understand and nicer to implement than making individual function for every possible stateplayer.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")isnotequal tonot is_("playing")Making use of theon_stopTheon_stopis a very powerful tool because callbacks are always cool. Optionally,on_stopwill 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_indefinitewill 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_timesplays the playerntimes.player=RecordingPlayer(recording,play_n_times(5)).play()# plays 5 timesplay_n_wrapperplays the playerntimes but it wraps another function to call each time.To come back to our very firston_stop.@play_n_wrapper(5)defon_stop():print("Playing finished")player=RecordingPlayer(recording,on_stop).play()Playing saved npz filesI showed you how to save your recordings asnpzfiles. However, you also need to know how to play them back.
For this there is aNPZPlayerclass. It takes a path to a file and extra parameters just like theRecordingPlayerplayer=NPZPlayer("my_npz_file.npz",on_stop=my_on_stop).play()It implements all the methods aRecordingPlayerimplements tooEvent RegisterOne 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 EventRegisterCreate a newEventRegisterobjectGet your events from the objectFinally, save the registered events.ExampleimportEventRegisterfromEventRegisterpg.init()reg=EventRegister("in","events.json")# save as jsontry:running=Truewhilerunning:time.sleep(0.0099)# 100 fpsforeventinreg.get_events():# instead of pg.event.get()ifevent.type==pg.QUIT:running=Falseelifevent.type==pg.MOUSEBUTTONDOWN:print(event.button,event.pos)finally:pg.quit()reg.save()Now to replay that exact recorded game. Just swapinwithoutwhen instanciating theregobject and everything should work exactly as expected.Random SeedsIf 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()