Ren'Py Need help understanding the play_callback function, real world examples are appreciated

siletris

New Member
Dec 20, 2022
11
4
I want a smooth transition (aka start second and third video exactly after the previous one ends) between 3 videos depending of player choices, the first one is a looped video, second one is either a choice between two non looped videos and the third one is also choice between two looped video.

You don't have permission to view the spoiler content. Log in or register now.

I have the videos already made and each one ends exactly one frame before the next one starts, to make it look smooth (already tested on a video player and the transition is smooth)

I had been trying to do it with a counter, see example code below, but for some reason it never sync and always show a hicup whenever it jumps to the next video, I guess its because the screen function needs time to load the next video

Python:
screen video_wait():
    modal True
    zorder 100  
    default ready = False  
    timer vid_timer:
        repeat (not ready)
        action If(ready, Jump(next_vid))
    textbutton "Continue" action SetLocalVariable("ready", True) xalign 0.99 yalign 0.99
I have seen several people and even in the renpy official documentation that in order to made smooth transitions between videos you need to use the play_callback argument and a function, but there are no real world examples anywhere.

Renpy really needs to add a feature were you can queue videos in memory and depending of user choice then play them in order, like why can't we just do something easy like this below?

video block_name:
video1.mp4 loop #will run until player makes a choice and once the choice is made will wait until video ends before showing selected video
"yes":
video2.mp4
"no":
video3.mp4
video4.mp4 loop #will loop until player advances

Another idea I had was to load the two possible second videos paused in a different layer and then depending of the choice hide the other layers and play the desired video, but I can't find any instructions on how to pause/start a video.

if anyone has a good way to do what i need please let me know.

For those wondering if the hardware is to blame my specs are I7 11th gen, nvidia 4070 and im running everything from an nvme, also videos aren't too big, 720p mp4 at 2mb bitrate, I have also increased the renpy cache.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
11,093
16,543
I have seen several people and even in the renpy official documentation that in order to made smooth transitions between videos you need to use the play_callback argument and a function, but there are no real world examples anywhere.
It's not what the play_callback argument is for.
It's used only once, when the video, or audio, starts, and it's designed to starts two videos/audios at the exact same time.

It can be distracted from this purpose, but there will never be a clean transition since, outside of screens, Ren'Py can only operate at around 20Hz, therefore once every 1/20th second, while movies usually have at least 25 images/second.
Anyway, doing so through play_callback wouldn't give something better than using directly config.periodic_callback or a screen timer.



Python:
screen video_wait():
    modal True
    zorder 100
    default ready = False
    timer vid_timer:
        repeat (not ready)
        action If(ready, Jump(next_vid))
    textbutton "Continue" action SetLocalVariable("ready", True) xalign 0.99 yalign 0.99
It's the idea, but there's many flaws:

Firstly, you don't need a variable for the timer delay, and what is its value?

Secondly, you don't take into account the looping video.

Thirdly, using Jump to change the video will add processing time (the jump itself, then all the label processing) before the next video can be started.


A smoother code could be:
/!\ Wrote in the fly, there's possibly some typos or errors /!\
Python:
screen videoHandler( videos = [] ):

    #  Flag raised when the handler is ready to change the video.
    default switchFlag = False
    #  Index of the currently played movie
    default currentIndex = 0
    #  Video currently played
    default currentMovie = videos[currentIndex][0]
    #  Cache the duration of the currently playing movie, for a bit of speed optimization.
    default duration = 0

    #  If there's still video that haven't been played
    If currentIndex < len( videos ) - 1:
        #  0.01 is generally the best you can get with a screen.
        # Yet it will often in fact be between 0.015 and 0.01 seconds.
        timer 0.01:
            #  You don't need to have a fluctuating value here
            repeat True
            #  IF the handler is ready to switch
            # AND the current movie is between -0.02 and 0 seconds from its end
            # THEN
           #    1) Pass to the next video
            #   2) Lower the flag, since we starts a new video.
            #   3) Set the duration at 0 to force a new computation
            #   4) Pass to the next movie
            # /!\ I assume that the video will be played on the 'movie' channel, change if needed.
            action if( switchFlag and duration - 0.02 < renpy.music.get_pos( "movie" ),
                          #  A little trick here. If the video do not loop, /switchFlag/ will be set to /True/
                          # this will permit to automatically change the video. And if it loop, then
                          # /switchFlag/ will be set to /False/, waiting for player's input to change.
                          [ SetLocalVariable( "currentIndex", currentIndex + 1 ),
                            SetLocalVariable( "switchFlag", not videos[currentIndex][1] ),
                            SetLocalVariable( "duration", 0 ),
                            SetLocalVariable( "currentMovie", videos[currentIndex][0] ) ],
                          NullAction() )
    #  If it's the last video, we need a different timer.
    # /!\ This imply that the last video do *not* and can *never* loop.
    else:
       timer 0.01:
       repeat True
       # It we are at least than 0.02 second to the end of the movie, quit the screen.
       # /!\ I assume that the video will be played on the 'movie' channel, change if needed.
       action If( duration - 0.02 < renpy.music.get_pos( "movie" ), Return(), NullAction() )

    #  Include the /currentMovie/ video into the screen and play it.
    #  Adapt the /xpos/ and /ypos/ value as you want.
    add currentMovie  xpos 100 ypos 100
    #  If the current video duration is unknown, cache it.
    if  duration ==0:
        # /!\ I assume that the video will be played on the 'movie' channel, change if needed.
        $ duration = renpy.music.get_duration( "movie" )
        if duration is None:
            $ duration = 0

    #  As long as we don't know if we need to change the video at the end of the current one,
    # ask for the player input.
    #  For none looping video, this will never be displayed, and for looping one, it will be displayed
    # until the player hit the button. Hiding it after that will explicitly show the player that his
    # action have been take into account.
    if switchFlag:
        vbox:
            #  Adapt the /xpos/ and /ypos/ value as you want.
            xpos 0 xpos 0
            textbutton "Next" action SetLocalVariable( "switchFlag", True )


image movie1 = Movie( play="path/to/movie/name.ext" )
image movie2 = Movie( play="path/to/movie/name.ext" )
image movie3 = Movie( play="path/to/movie/name.ext" )

label whatever:
    # movie 1 do not loop, movie2 loop, movie3 do not loop
    call screen videoHandler( [ ( movie1, False ), ( movie2, True) , ( movie3, False ) ] )
    [...]
Normally this should works.
Like the movie is changed directly in the screen, during it's refresh loop (that will happen every [0.015 - 0.01] seconds), there will be not latency, whatever how small it can be, between one movie and the other.
Like the movie is changed only when there's less than 0.021 second before the end of the current movie, the transition should be relatively smooth.
And with the switchFlag trick, it handle looping and none looping videos, asking for a player input only when necessary.

But keep in mind that I'm a works, writing this during my lunch time. Therefore I can't test it.


Renpy really needs to add a feature were you can queue videos in memory and depending of user choice then play them in order, like why can't we just do something easy like this below?
Ren'Py already have a feature to queue videos. It's just that when you've looping videos, you can't obviously not know when to pass to the next one.
This being said, it's probably possible to hack your way through this by defining a queue with many occurrence of the looped video (but defining the video as none looping), and dirtily updating the queue after each video change, to add more looped video if needed, as well as dirty updating the queue, this time to remove all occurrence of the looped video, when the player press the "next" button.

But I'll not try this, because it's dirty and useless.


For those wondering if the hardware is to blame [...]
Those persons do not understand that a game, especially one as (relatively) basic as a Ren'Py one, is intended to be played on an extra large variety of computers, and therefore that the specs are totally irrelevant here. This simply because a Ren'Py game that works fine on a high end computer, but not on a low end one, is a shitty game.


Edit: A bit of bug fix in the code
 
Last edited:

Turning Tricks

Rendering Fantasies
Game Developer
Apr 9, 2022
1,406
2,665
Like Anne mentioned above, that play_callback function is to set up when a movie, sprite or audio starts. It's not for transitions between movies.

As far as I understand the issue, Ren'py (as of yet) does not have the ability to predict movies. Renpy Tom has and mentioned that even running multiple movies can , which I presume to mean you might get weird things happening even with regular image prediction at that point.

Personally, I think your best option is to use a transition of some sort between the looping video and the non-looping cumshot ends. Ren'py is basically built from the ground up to do smooth transitions for visual novels, so why not use it's strongest tools? Be artistic... have a close up of the LI's Ahegao face or the MC's "I'm cumming!" face and then transition to the movie end.

I mean, when you think about it... this is not much different then a production company making real movies. Rarely do you see a scene that is completely linear and uncut from beginning to end. Instead, various transitions are used to hide when changes or cuts are made.

I've also had good results transitioning between multiple looping videos by starting them all at the same time and using the show statement to put them on different layers. Then you can just hide and show the different layers using a nice Alpha transition and the looping videos will always be in sync, until you basically hide them and move on to a new scene.

By all means, if you want to code a python function to queue and play these movies the way you want, then do so. You run the risk though, of your custom python code interacting in some unknown way with Ren'py's core functions. And the results of your script will probably vary a lot depending on the end user's computer hardware (or mobile). As an engineer, I am a firm believer in the KISS principle. Keep it Simple Stupid ;)

After all, webm's are very light weight, and I could stitch several together to make alternate versions from other segments in minutes, using FFMPEG.
 

Leny99

New Member
Mar 4, 2017
1
0
It's not what the play_callback argument is for.
It's used only once, when the video, or audio, starts, and it's designed to starts two videos/audios at the exact same time.

It can be distracted from this purpose, but there will never be a clean transition since, outside of screens, Ren'Py can only operate at around 20Hz, therefore once every 1/20th second, while movies usually have at least 25 images/second.
Anyway, doing so through play_callback wouldn't give something better than using directly config.periodic_callback or a screen timer.





It's the idea, but there's many flaws:

Firstly, you don't need a variable for the timer delay, and what is its value?

Secondly, you don't take into account the looping video.

Thirdly, using Jump to change the video will add processing time (the jump itself, then all the label processing) before the next video can be started.


A smoother code could be:
/!\ Wrote in the fly, there's possibly some typos or errors /!\
Python:
screen videoHandler( videos = [] ):

    #  Flag raised when the handler is ready to change the video.
    default switchFlag = False
    #  Index of the currently played movie
    default currentIndex = 0
    #  Video currently played
    default currentMovie = videos[currentIndex][0]
    #  Cache the duration of the currently playing movie, for a bit of speed optimization.
    default duration = 0

    #  If there's still video that haven't been played
    If currentIndex < len( videos ) - 1:
        #  0.01 is generally the best you can get with a screen.
        # Yet it will often in fact be between 0.015 and 0.01 seconds.
        timer 0.01:
            #  You don't need to have a fluctuating value here
            repeat True
            #  IF the handler is ready to switch
            # AND the current movie is between -0.02 and 0 seconds from its end
            # THEN
            #   1) Lower the flag, since we starts a new video.
            #   2) Set the duration at 0 to force a new computation
            #   3) Pass to the next movie
            # /!\ I assume that the video will be played on the 'movie' channel, change if needed.
            action if( switchFlag and duration - 0.02 < renpy.music.get_pos( "movie" ) < duration,
                          #  A little trick here. If the video do not loop, /switchFlag/ will be set to /True/
                          # this will permit to automatically change the video. And if it loop, then
                          # /switchFlag/ will be set to /False/, waiting for player's input to change.
                          [ SetLocalVariable( "switchFlag", not videos[currentIndex][1] ),
                            SetLocalVariable( "duration", 0 ),
                            SetLocalVariable( "currentMovie", videos[currentIndex][0] ) ],
                          NullAction() )
    #  If it's the last video, we need a different timer.
    # /!\ This imply that the last video do *not* and can *never* loop.
    else:
       timer 0.01:
       repeat True
       # It we are at least than 0.02 second to the end of the movie, quit the screen.
       # /!\ I assume that the video will be played on the 'movie' channel, change if needed.
       action If( duration - 0.02 < renpy.music.get_pos( "movie" ) < duration, Return(), NullAction() )

    #  Include the /currentMovie/ video into the screen and play it.
    #  Adapt the /xpos/ and /ypos/ value as you want.
    add currentMovie  xpos 100 ypos 100
    #  If the current video duration is unknown, cache it.
    if duration == 0:
        # /!\ I assume that the video will be played on the 'movie' channel, change if needed.
        $ duration = renpy.music.get_duration( "movie" )

    #  As long as we don't know if we need to change the video at the end of the current one,
    # ask for the player input.
    #  For none looping video, this will never be displayed, and for looping one, it will be displayed
    # until the player hit the button. Hiding it after that will explicitly show the player that his
    # action have been take into account.
    if switchFlag:
        vbox:
            #  Adapt the /xpos/ and /ypos/ value as you want.
            xpos 0 xpos 0
            textbutton "Next" action SetLocalVariable( "switchFlag", True )


image movie1 = Movie( play="path/to/movie/name.ext" )
image movie2 = Movie( play="path/to/movie/name.ext" )
image movie3 = Movie( play="path/to/movie/name.ext" )

label whatever:
    # movie 1 do not loop, movie2 loop, movie3 do not loop
    call screen videoHandler( [ ( movie1, False ), ( movie2, True) , ( movie3, False ) ] )
    [...]
Normally this should works.
Like the movie is changed directly in the screen, during it's refresh loop (that will happen every [0.015 - 0.01] seconds), there will be not latency, whatever how small it can be, between one movie and the other.
Like the movie is changed only when there's less than 0.021 second before the end of the current movie, the transition should be relatively smooth.
And with the switchFlag trick, it handle looping and none looping videos, asking for a player input only when necessary.

But keep in mind that I'm a works, writing this during my lunch time. Therefore I can't test it.




Ren'Py already have a feature to queue videos. It's just that when you've looping videos, you can't obviously not know when to pass to the next one.
This being said, it's probably possible to hack your way through this by defining a queue with many occurrence of the looped video (but defining the video as none looping), and dirtily updating the queue after each video change, to add more looped video if needed, as well as dirty updating the queue, this time to remove all occurrence of the looped video, when the player press the "next" button.

But I'll not try this, because it's dirty and useless.




Those persons do not understand that a game, especially one as (relatively) basic as a Ren'Py one, is intended to be played on an extra large variety of computers, and therefore that the specs are totally irrelevant here. This simply because a Ren'Py game that works fine on a high end computer, but not on a low end one, is a shitty game.
ran this through Cody ai.... how doess this look




screen videoHandler(videos=[], channel="movie", button_xpos=0, button_ypos=0, end_threshold=0.02):
# Flag raised when the handler is ready to change the video.
default switchFlag = False
# Index of the currently played movie
default currentIndex = 0
# Video currently played
default currentMovie = None
# Cache the duration of the currently playing movie, for a bit of speed optimization.
default duration = 0

if videos:
if currentMovie is None:
$ currentMovie = videos[currentIndex][0]
timer 0.01:
repeat True
# Check if the current movie is near the end
if duration > 0 and duration - end_threshold <= renpy.music.get_pos(channel) <= duration:
if currentIndex < len(videos) - 1:
# If the video doesn't loop, set switchFlag to True to wait for user input
if not videos[currentIndex][1]:
$ switchFlag = True
else:
# If the video loops, move to the next video
$ currentIndex += 1
$ currentMovie = videos[currentIndex][0]
$ duration = 0
$ switchFlag = False
else:
# If it's the last video, return from the screen
return

# Include the /currentMovie/ video into the screen and play it.
add currentMovie xpos 100 ypos 100
# If the current video duration is unknown, cache it.
if duration == 0:
$ duration = renpy.music.get_duration(channel)
if duration is None:
$ duration = 0

# As long as we don't know if we need to change the video at the end of the current one,
# ask for the player input.
if switchFlag:
vbox:
xpos button_xpos
ypos button_ypos
textbutton "Next" action [SetVariable("currentIndex", currentIndex + 1), SetVariable("currentMovie", videos[currentIndex + 1][0]), SetVariable("duration", 0), SetVariable("switchFlag", False)]



lastly


Explanation of Changes:

  1. Single Timer: The two timer blocks are merged into one. The logic now checks if the current video is near the end and handles both looping and non-looping cases within the same block.
  2. Configurable end_threshold: The 0.02 is now a parameter end_threshold with a default value, making it easier to adjust.
  3. Clearer switchFlag Logic: The switchFlag is now only set to True for non-looping videos when they reach the end, indicating that the user needs to click "Next" to proceed.
  4. Configurable Channel: The channel parameter allows you to specify which channel the video is played on.
  5. Removed NullAction(): The redundant NullAction() calls have been removed.
  6. Correct currentMovie Update: The currentMovie is now updated with the current movie's path.
  7. Correct currentIndex Update: The currentIndex is now updated when moving to the next video.
  8. Race Condition Mitigation: The code now checks if the video duration is greater than 0 before comparing it to the current playback position.
  9. Error Handling: The code now checks if renpy.music.get_duration() returns None and sets the duration to 0 if it does.
  10. Configurable Button Placement: The "Next" button's position is now configurable through button_xpos and button_ypos parameters.
  11. Initial currentMovie Check: The code now checks if currentMovie is None and sets it to the first video if it is.
How to Use the Improved Code:

image movie1 = Movie(play="path/to/movie/name1.webm")
image movie2 = Movie(play="path/to/movie/name2.webm")
image movie3 = Movie(play="path/to/movie/name3.webm")

label whatever:
# movie1 does not loop, movie2 loops, movie3 does not loop
call screen videoHandler(videos=[(movie1, False), (movie2, True), (movie3, False)], channel="movie", button_xpos=100, button_ypos=200)
[...]
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
11,093
16,543
ran this through Cody ai.... how doess this look
Ridiculous, like most code wrote/corrected by AI, and I'll explain why:


Python:
screen videoHandler(videos=[], channel="movie", button_xpos=0, button_ypos=0, end_threshold=0.02):

The channel argument is half useful, half stupid:

Useful because the movie can actually be played on another channel. Having a way to address this without need to edit the screen itself isn't totally a bad idea.
But stupid because this isn't the code for an API. Therefore a dev will copy the code into his own game, and perfectly have the possibility to edit the channel at this time. This because, if a dev can possibly have a reason to not use the "movie" channel for his movies, he have no reason to use more than one channel, especially when using this screen.

A better answer would have be to add a define MOVIE_CHANNEL = "movie" with a long comment block explaining that it need to be changed if he dev use another channel. Then use that constant to address the channel in the screen.
Due to Ren'Py screen optimization, it will be a bit faster, and since it's Python, and for once real time, a "bit faster" is something important.


The button_xpos and button_ypos arguments are useless:

Once again because it's not an API, and also because having such important part of the User Interface that can move during the game is a really bad idea. It's the dev that must adapt its movies to the interface he designed, not the opposite.
Plus, if the button s at a place where the dev don't want it, it will be obvious the first time he test the code. Therefore he'll think about moving it.
This unlike for the movie channel, that can act as a more subtle bug if the dev don't understand what channels are for; yet, if he don't understand this, there's 99% chance that he kept the default one, and therefore that the code would works fine.

It there's position that should be added as argument, it would be for the add statement. Here it would make sense, because the size of the movies can change from one use of the screen to another, and therefore the said add statement would then need to be moved to keep the video centered.


The end_threshold argument is totally useless:

There's absolutely no reasons to have another value than this one. Since Ren'Py frequency is irregular, even if the version of Ren'Py can handle a timer with a interface lower than 0.015, and the player's computer can handle it too, the lower will be the value, the higher will be the risk to miss the end of a looped movie, and therefore for it to starts an unwanted new loop.
This while a higher value would prevent to have the "as smooth as possible" transition between the movies.
The value wasn't choose arbitrary, but because it's the most adapted and the one that should always used.


Python:
    default currentMovie = None

    if videos:
        if currentMovie is None:
            $ currentMovie = videos[currentIndex][0]
There's no need to delay the assignation, it's the opposite:

Yes, it there's no videos provided, it will trigger an error. But once again this is not an API. This error have to happen, to let the dev know that he messed something during his own tests.
It also MUST happen when someone play the game. As designed by the AI, if there's no movies, the screen will display nothing and not return. The game will enter in an endless pause that can absolutely not be stopped by the player...
This will looks like an expected behavior during the few first seconds, then players will loose patience, close the game and never play it again. And neither the player, nor the dev, will understand why it happen, especially if the content of videos is built dynamically, and therefore can be filled during dev's test, but not for few players who followed a rely particular path that lead to it being empty.

I.e: this week I played a game update that limited to 2 lines of dialog. This because all the content was conditioned, and my particular play kept me out of all of it, except those two lines that where common to all contents.
The same can happen to a game using this screen, and both the player and dev NEED to explicitly know that it happened.


Python:
        timer 0.01:
            repeat True
            # Check if the current movie is near the end
            if duration > 0 and duration - end_threshold <= renpy.music.get_pos(channel) <= duration:
                if currentIndex < len(videos) - 1:
                    # If the video doesn't loop, set switchFlag to True to wait for user input
                    if not videos[currentIndex][1]:
                        $ switchFlag = True
                    else:
                        # If the video loops, move to the next video
                        $ currentIndex += 1
                        $ currentMovie = videos[currentIndex][0]
                        $ duration = 0
                        $ switchFlag = False
                else:
                    # If it's the last video, return from the screen
                    return
This is absolutely not how the timer statement works.

It's not totally impossible that a part of this code works, but it would be a pure side effect and still totally broken.
Anyway, the return would have absolutely no effect here. Called screens return through a screen action, not through an instruction.

The original screen could have use only one timer, moving the if inside the timer block:
Python:
    timer 0.01:
        repeat True
        if currentIndex < len( videos ) - 1:
            action if( switchFlag and duration - 0 [...]
        else:
            action [...]
I voluntarily choose not to do it that way, to keep the code at an as low as possible level of complexity.
Having two different timer being easier to understand than having a single timer than can have two different behaviors. This will devs who have an higher knowledge regarding Ren'Py would figure by themselves that it's something they can do.


Now, to address the code itself, there's a big issue: The AI didn't understood its purpose...
Python:
                    # If the video doesn't loop, set switchFlag to True to wait for user input
The screen do the exact opposite, waiting for players input only when a movie loop.

And, of course, the player must have the possibility to press the button at whatever moment when the looped video is playing. Here it's even more ridiculous because the button will only be available once in a while, during at most 0.02 second...

Knowing that the average human reaction time is 0.25 seconds, be ready to face the hardest challenge you ever had with a Ren'Py game...


Python:
        # Include the /currentMovie/ video into the screen and play it.
        add currentMovie xpos 100 ypos 100
        # If the current video duration is unknown, cache it.
        if duration == 0:
            $ duration = renpy.music.get_duration(channel)
            if duration is None:
                $ duration = 0
This is a bug fix.

Effectively, there's risk that the video isn't yet seen as started when the code get the duration. Therefore Ren'Py would return None, what I forgot when writing this code in the fly.
I updated my code to take count of this.


Python:
if switchFlag: 
    [...]
                textbutton "Next" action [SetVariable("currentIndex", currentIndex + 1), SetVariable("currentMovie", videos[currentIndex + 1][0]), SetVariable("duration", 0), SetVariable("switchFlag", False)]
A bug fix and many bullshit.

I totally forgot to update currentIndex and doing it here do no harm.
I also corrected this in my code.

But as I said above, the AI inverted the switchFlag meaning, making the code works at the opposite of what it's intended to do.

As for the update of currentMovie directly here, it force the movie to be changed the instant the player will click. What is, once again, the exact opposite of what the screen is intended to do.


Removed NullAction(): The redundant NullAction() calls have been removed.
It's not redundant, If screen actions expect a condition, a true action, and a false action. When there's no true action, or no false action, you have to explicitly tell Ren'Py that he must do nothing, and it's what NullAction() do.
And anyway, "redundant"? Do this AI know what that word mean?


Correct currentMovie Update: The currentMovie is now updated with the current movie's path.
It have always been... It's currentIndex that wasn't updated. To my defense, as I said I wrote the code in the fly.


Race Condition Mitigation: The code now checks if the video duration is greater than 0 before comparing it to the current playback position.
I'm pretty sure that neither humans, nor AIs, achieved yet to create videos that have a negative duration...
If the video had a negative duration, it would need to be changed immediately, to not create a black hole or destroy the universe, so the code need to not check if this duration is negative; well try AI, but it's not today that you'll end humanity.

Anyway, "Race Condition", with a Ren'Py screen? Wow, now I want to see what that AI can do as marvel with it's multi-threaded screens...



Well, two errors, for a code wrote in the fly (and presented as it) during a works break... Not too bad.
But I can sleep in peace, there's no risk for an AI to take my job before I retire.