# Copyright 2004-2019 Tom Rothamel # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import print_function import renpy.display import renpy.audio import collections # The movie displayable that's currently being shown on the screen. current_movie = None # True if the movie that is currently displaying is in fullscreen mode, # False if it's a smaller size. fullscreen = False # The size of a Movie object that hasn't had an explicit size set. default_size = (400, 300) # The file we allocated the surface for. surface_file = None # The surface to display the movie on, if not fullscreen. surface = None def movie_stop(clear=True, only_fullscreen=False): """ Stops the currently playing movie. """ if (not fullscreen) and only_fullscreen: return renpy.audio.music.stop(channel='movie') def movie_start(filename, size=None, loops=0): """ This starts a movie playing. """ if renpy.game.less_updates: return global default_size if size is not None: default_size = size filename = [ filename ] if loops == -1: loop = True else: loop = False filename = filename * (loops + 1) renpy.audio.music.play(filename, channel='movie', loop=loop) movie_start_fullscreen = movie_start movie_start_displayable = movie_start # A map from a channel name to the movie texture that is being displayed # on that channel. texture = { } # The set of channels that are being displayed in Movie objects. displayable_channels = collections.defaultdict(list) # A map from a channel to the topmost Movie being displayed on # that channel. (Or None if no such movie exists.) channel_movie = { } # Is there a video being displayed fullscreen? fullscreen = False # Movie channels that had a hide operation since the last interaction took # place. reset_channels = set() def early_interact(): """ Called early in the interact process, to clear out the fullscreen flag. """ displayable_channels.clear() channel_movie.clear() def interact(): """ This is called each time the screen is drawn, and should return True if the movie should display fullscreen. """ global fullscreen for i in list(texture.keys()): if not renpy.audio.music.get_playing(i): del texture[i] if renpy.audio.music.get_playing("movie"): for i in displayable_channels.keys(): if i[0] == "movie": fullscreen = False break else: fullscreen = True else: fullscreen = False return fullscreen def get_movie_texture(channel, mask_channel=None, side_mask=False): if not renpy.audio.music.get_playing(channel): return None, False c = renpy.audio.music.get_channel(channel) surf = c.read_video() if side_mask: if surf is not None: w, h = surf.get_size() w //= 2 mask_surf = surf.subsurface((w, 0, w, h)) surf = surf.subsurface((0, 0, w, h)) else: mask_surf = None elif mask_channel: mc = renpy.audio.music.get_channel(mask_channel) mask_surf = mc.read_video() else: mask_surf = None if mask_surf is not None: # Something went wrong with the mask video. if surf: renpy.display.module.alpha_munge(mask_surf, surf, renpy.display.im.identity) else: surf = None if surf is not None: renpy.display.render.mutated_surface(surf) tex = renpy.display.draw.load_texture(surf, True) texture[channel] = tex new = True else: tex = texture.get(channel, None) new = False return tex, new def render_movie(channel, width, height): tex, _new = get_movie_texture(channel) if tex is None: return None sw, sh = tex.get_size() scale = min(1.0 * width / sw, 1.0 * height / sh) dw = scale * sw dh = scale * sh rv = renpy.display.render.Render(width, height) rv.forward = renpy.display.render.Matrix2D(1.0 / scale, 0.0, 0.0, 1.0 / scale) rv.reverse = renpy.display.render.Matrix2D(scale, 0.0, 0.0, scale) rv.blit(tex, (int((width - dw) / 2), int((height - dh) / 2))) return rv def default_play_callback(old, new): # @UnusedVariable renpy.audio.music.play(new._play, channel=new.channel, loop=new.loop, synchro_start=True) if new.mask: renpy.audio.music.play(new.mask, channel=new.mask_channel, loop=new.loop, synchro_start=True) class Movie(renpy.display.core.Displayable): """ :doc: movie This is a displayable that shows the current movie. `fps` The framerate that the movie should be shown at. (This is currently ignored, but the parameter is kept for backwards compatibility. The framerate is auto-detected.) `size` This should be specified as either a tuple giving the width and height of the movie, or None to automatically adjust to the size of the playing movie. (If None, the displayable will be (0, 0) when the movie is not playing.) `channel` The audio channel associated with this movie. When a movie file is played on that channel, it will be displayed in this Movie displayable. If this is not given, and the `play` is provided, a channel name is automatically selected. `play` If given, this should be the path to a movie file. The movie file will be automatically played on `channel` when the Movie is shown, and automatically stopped when the movie is hidden. `side_mask` If true, this tells Ren'Py to use the side-by-side mask mode for the Movie. In this case, the movie is divided in half. The left half is used for color information, while the right half is used for alpha information. The width of the displayable is half the width of the movie file. Where possible, `side_mask` should be used over `mask` as it has no chance of frames going out of sync. `mask` If given, this should be the path to a movie file that is used as the alpha channel of this displayable. The movie file will be automatically played on `movie_channel` when the Movie is shown, and automatically stopped when the movie is hidden. `mask_channel` The channel the alpha mask video is played on. If not given, defaults to `channel`\ _mask. (For example, if `channel` is "sprite", `mask_channel` defaults to "sprite_mask".) `start_image` An image that is displayed when playback has started, but the first frame has not yet been decoded. `image` An image that is displayed when `play` has been given, but the file it refers to does not exist. (For example, this can be used to create a slimmed-down mobile version that does not use movie sprites.) Users can also choose to fall back to this image as a preference if video is too taxing for their system. The image will also be used if the video plays, and then the movie ends. ``play_callback`` If not None, a function that's used to start the movies playing. (This may do things like queue a transition between sprites, if desired.) It's called with the following arguments: `old` The old Movie object, or None if the movie is not playing. `new` The new Movie object. A movie object has the `play` parameter available as ``_play``, while the ``channel``, ``loop``, ``mask``, and ``mask_channel`` fields correspond to the given parameters. Generally, this will want to use :func:`renpy.music.play` to start the movie playing on the given channel, with synchro_start=True. A minimal implementation is:: def play_callback(old, new): renpy.music.play(new._play, channel=new.channel, loop=new.loop, synchro_start=True) if new.mask: renpy.music.play(new.mask, channel=new.mask_channel, loop=new.loop, synchro_start=True) `loop` If False, the movie will not loop. If `image` is defined, the image will be displayed when the movie ends. Otherwise, the movie will become transparent. This displayable will be transparent when the movie is not playing. """ fullscreen = False channel = "movie" _play = None mask = None mask_channel = None side_mask = False image = None start_image = None play_callback = None loop = True def ensure_channel(self, name): if name is None: return if renpy.audio.music.channel_defined(name): return if self.mask: framedrop = True else: framedrop = False renpy.audio.music.register_channel(name, renpy.config.movie_mixer, loop=True, stop_on_mute=False, movie=True, framedrop=framedrop) def __init__(self, fps=24, size=None, channel="movie", play=None, mask=None, mask_channel=None, image=None, play_callback=None, side_mask=False, loop=True, start_image=None, **properties): super(Movie, self).__init__(**properties) global auto_channel_serial if channel == "movie" and play and renpy.config.auto_movie_channel: channel = "movie_{}_{}".format(play, mask) self.size = size self.channel = channel self._play = play self.loop = loop if side_mask: mask = None self.mask = mask if mask is None: self.mask_channel = None elif mask_channel is None: self.mask_channel = channel + "_mask" else: self.mask_channel = mask_channel self.side_mask = side_mask self.ensure_channel(self.channel) self.ensure_channel(self.mask_channel) self.image = renpy.easy.displayable_or_none(image) self.start_image = renpy.easy.displayable_or_none(start_image) self.play_callback = play_callback if (self.channel == "movie") and (renpy.config.hw_video) and renpy.mobile: raise Exception("Movie(channel='movie') doesn't work on mobile when config.hw_video is true. (Use a different channel argument.)") def render(self, width, height, st, at): if self._play and not (renpy.game.preferences.video_image_fallback is True): channel_movie[self.channel] = self if st == 0: reset_channels.add(self.channel) playing = renpy.audio.music.get_playing(self.channel) not_playing = not playing if self.channel in reset_channels: not_playing = False if (self.image is not None) and not_playing: surf = renpy.display.render.render(self.image, width, height, st, at) w, h = surf.get_size() rv = renpy.display.render.Render(w, h) rv.blit(surf, (0, 0)) return rv if self.size is None: tex, _ = get_movie_texture(self.channel, self.mask_channel, self.side_mask) if (not not_playing) and (tex is not None): width, height = tex.get_size() rv = renpy.display.render.Render(width, height) rv.blit(tex, (0, 0)) elif (not not_playing) and (self.start_image is not None): surf = renpy.display.render.render(self.start_image, width, height, st, at) w, h = surf.get_size() rv = renpy.display.render.Render(w, h) rv.blit(surf, (0, 0)) else: rv = renpy.display.render.Render(0, 0) else: w, h = self.size if not playing: rv = None else: rv = render_movie(self.channel, w, h) if rv is None: rv = renpy.display.render.Render(w, h) # Usually we get redrawn when the frame is ready - but we want # the movie to disappear if it's ended, or if it hasn't started # yet. renpy.display.render.redraw(self, 0.1) return rv def play(self, old): if old is None: old_play = None else: old_play = old._play if (self._play != old_play) or renpy.config.replay_movie_sprites: if self._play: if self.play_callback is not None: self.play_callback(old, self) else: default_play_callback(old, self) else: renpy.audio.music.stop(channel=self.channel) if self.mask: renpy.audio.music.stop(channel=self.mask_channel) def stop(self): if self._play: renpy.audio.music.stop(channel=self.channel) if self.mask: renpy.audio.music.stop(channel=self.mask_channel) def per_interact(self): displayable_channels[(self.channel, self.mask_channel)].append(self) renpy.display.render.redraw(self, 0) def visit(self): return [ self.image, self.start_image ] def playing(): if renpy.audio.music.get_playing("movie"): return True for i in displayable_channels: channel, _mask_channel = i if renpy.audio.music.get_playing(channel): return True return def update_playing(): """ Calls play/stop on Movie displayables. """ old_channel_movie = renpy.game.context().movie for c, m in channel_movie.items(): old = old_channel_movie.get(c, None) if (c in reset_channels) and renpy.config.replay_movie_sprites: m.play(old) elif old is not m: m.play(old) for c, m in old_channel_movie.items(): if c not in channel_movie: m.stop() renpy.game.context().movie = dict(channel_movie) reset_channels.clear() def frequent(): """ Called to update the video playback. Returns true if a video refresh is needed, false otherwise. """ update_playing() renpy.audio.audio.advance_time() if displayable_channels: update = True for i in displayable_channels: channel, mask_channel = i c = renpy.audio.audio.get_channel(channel) if not c.video_ready(): update = False break if mask_channel: c = renpy.audio.audio.get_channel(mask_channel) if not c.video_ready(): update = False break if update: for v in displayable_channels.values(): for j in v: renpy.display.render.redraw(j, 0.0) return False elif fullscreen and not ((renpy.android or renpy.ios) and renpy.config.hw_video): c = renpy.audio.audio.get_channel("movie") if c.video_ready(): return True else: return False return False