class VideoPlayer: def terminate(self): self.ctx.free() self.mpv.terminate() def __init__(self, filename): self.filename = filename self.open = True self.fbo = gl.glGenFramebuffers(1) gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.fbo) self.texture = gl.glGenTextures(1) gl.glBindTexture(gl.GL_TEXTURE_2D, self.texture) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, self.texture, 0) gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, 100, 100, 0, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, None) gl.glBindTexture(gl.GL_TEXTURE_2D, 0) gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) self.playbackPos = (0, ) self.volume = (0, ) self.loop = 'inf' self.mpv = MPV(log_handler=print, loglevel='debug') def get_process_address(_, name): print(name) address = glfw.get_proc_address(name.decode('utf8')) return ctypes.cast(address, ctypes.c_void_p).value proc_addr_wrapper = OpenGlCbGetProcAddrFn(get_process_address) self.ctx = MpvRenderContext( self.mpv, 'opengl', opengl_init_params={'get_proc_address': proc_addr_wrapper}) self.mpv.play(self.filename) self.mpv.volume = 0 def render(self): videowindow, self.open = imgui.begin("Video window {}".format( self.filename), self.open, flags=imgui.WINDOW_NO_SCROLLBAR) w, h = imgui.get_window_size() if imgui.APPEARING: imgui.set_window_size(400, 300) w, h = imgui.core.get_content_region_available() w = int(max(w, 0)) h = int(max(h - 85, 0)) if not self.open: imgui.end() self.terminate() return if self.ctx.update() and w > 0 and h > 0: gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.fbo) gl.glBindTexture(gl.GL_TEXTURE_2D, self.texture) gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB, w, h, 0, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, None) gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) gl.glBindTexture(gl.GL_TEXTURE_2D, 0) self.ctx.render(flip_y=False, opengl_fbo={ 'w': w, 'h': h, 'fbo': self.fbo }) try: imgui.text("Filename: {} fbo: {} tex: {}".format( self.mpv.filename, self.fbo, self.texture)) except: imgui.text("Filename: {} fbo: {} tex: {}".format( self.mpv.filename, self.fbo, self.texture)) try: imgui.text("{0:.2f}s/{1:.2f}s ({2:.2f}s remaining)".format( self.mpv.time_pos, self.mpv.duration, self.mpv.playtime_remaining)) except: imgui.text("Loading...") imgui.image(self.texture, w, h) imgui.push_item_width(-1) changed, values = imgui.slider_float("##Playback Percentage", *self.playbackPos, min_value=0.0, max_value=100.0, format="Playback Percentage %.0f", power=1.0) if changed and values: try: self.mpv.command('seek', values, 'absolute-percent') except: pass self.playbackPos = (values, ) elif self.mpv.percent_pos: self.playbackPos = (self.mpv.percent_pos, ) changed, values = imgui.slider_float("##Volume", *self.volume, min_value=0.0, max_value=100.0, format="Volume %.0f", power=1.0) if changed: self.mpv.volume = values self.volume = (values, ) elif self.mpv.volume: self.volume = (self.mpv.volume, ) imgui.end()
class MpvWidget(QOpenGLWidget): _schedule_update = pyqtSignal() def __init__(self, api: Api, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self._api = api locale.setlocale(locale.LC_NUMERIC, "C") self._destroyed = False self._need_subs_refresh = False self.mpv = MPV(vo="opengl-cb", ytdl=False, loglevel="info", log_handler=print) self.mpv_gl = _mpv_get_sub_api(self.mpv.handle, MpvSubApi.MPV_SUB_API_OPENGL_CB) self.on_update_c = OpenGlCbUpdateFn(self.on_update) self.on_update_fake_c = OpenGlCbUpdateFn(self.on_update_fake) self.get_proc_addr_c = OpenGlCbGetProcAddrFn(get_proc_addr) _mpv_opengl_cb_set_update_callback(self.mpv_gl, self.on_update_c, None) self.frameSwapped.connect(self.swapped, Qt.ConnectionType.DirectConnection) for key, value in { # "config": False, "quiet": False, "msg-level": "all=info", "osc": False, "osd-bar": False, "input-cursor": False, "input-vo-keyboard": False, "input-default-bindings": False, "ytdl": False, "sub-auto": False, "audio-file-auto": False, "vo": "null" if api.args.no_video else "libmpv", "hwdec": "no", "pause": True, "idle": True, "blend-subtitles": "video", "video-sync": "display-vdrop", "keepaspect": True, "stop-playback-on-init-failure": False, "keep-open": True, "track-auto-selection": False, }.items(): setattr(self.mpv, key, value) self._opengl = None self._timer = QTimer(parent=None) self._timer.setInterval(api.cfg.opt["video"]["subs_sync_interval"]) self._timer.timeout.connect(self._refresh_subs_if_needed) api.subs.loaded.connect(self._on_subs_load) api.video.stream_created.connect(self._on_video_state_change) api.video.stream_unloaded.connect(self._on_video_state_change) api.video.current_stream_switched.connect(self._on_video_state_change) api.audio.stream_created.connect(self._on_audio_state_change) api.audio.stream_unloaded.connect(self._on_audio_state_change) api.audio.current_stream_switched.connect(self._on_audio_state_change) api.playback.request_seek.connect(self._on_request_seek, Qt.ConnectionType.DirectConnection) api.playback.request_playback.connect(self._on_request_playback) api.playback.playback_speed_changed.connect( self._on_playback_speed_change) api.playback.volume_changed.connect(self._on_volume_change) api.playback.mute_changed.connect(self._on_mute_change) api.playback.pause_changed.connect(self._on_pause_change) api.video.view.zoom_changed.connect(self._on_video_zoom_change) api.video.view.pan_changed.connect(self._on_video_pan_change) api.gui.terminated.connect(self.shutdown) self._schedule_update.connect(self.update) self.mpv.observe_property("time-pos", self._on_mpv_time_pos_change) self.mpv.observe_property("track-list", self._on_mpv_track_list_change) self.mpv.observe_property("pause", self._on_mpv_pause_change) self._timer.start() def initializeGL(self) -> None: _mpv_opengl_cb_init_gl(self.mpv_gl, None, self.get_proc_addr_c, None) def paintGL(self) -> None: ratio = self.devicePixelRatioF() w = int(self.width() * ratio) h = int(self.height() * ratio) _mpv_opengl_cb_draw(self.mpv_gl, self.defaultFramebufferObject(), w, -h) @pyqtSlot() def maybe_update(self) -> None: if self._destroyed: return if self.window().isMinimized(): self.makeCurrent() self.paintGL() self.context().swapBuffers(self.context().surface()) self.swapped() self.doneCurrent() else: self.update() def on_update(self, ctx: Any = None) -> None: # maybe_update method should run on the thread that creates the # OpenGLContext, which in general is the main thread. # QMetaObject.invokeMethod can do this trick. QMetaObject.invokeMethod(self, "maybe_update") def on_update_fake(self, ctx: Any = None) -> None: pass def swapped(self) -> None: _mpv_opengl_cb_report_flip(self.mpv_gl, 0) def closeEvent(self, _: Any) -> None: self.makeCurrent() if self.mpv_gl: _mpv_opengl_cb_set_update_callback(self.mpv_gl, self.on_update_fake_c, None) _mpv_opengl_cb_uninit_gl(self.mpv_gl) def _on_subs_load(self) -> None: self._api.subs.script_info.changed.subscribe( lambda _event: self._on_subs_change()) self._api.subs.events.changed.subscribe( lambda _event: self._on_subs_change()) self._api.subs.styles.changed.subscribe( lambda _event: self._on_subs_change()) def _on_video_state_change(self, stream: VideoStream) -> None: self._sync_media() self._need_subs_refresh = True def _on_audio_state_change(self, stream: AudioStream) -> None: self._sync_media() def _sync_media(self) -> None: self.mpv.pause = True self.mpv.loadfile("null://") external_files: set[str] = set() for video_stream in self._api.video.streams: external_files.add(str(video_stream.path)) for audio_stream in self._api.audio.streams: external_files.add(str(audio_stream.path)) self.mpv.external_files = list(external_files) if not external_files: self._api.playback.state = PlaybackFrontendState.NOT_READY else: self._api.playback.state = PlaybackFrontendState.LOADING def shutdown(self) -> None: self._destroyed = True self.makeCurrent() if self._opengl: self._opengl.set_update_callback(lambda: None) self._opengl.close() self.deleteLater() self._timer.stop() def _refresh_subs_if_needed(self) -> None: if self._need_subs_refresh: self._refresh_subs() def _refresh_subs(self) -> None: if not self._api.playback.is_ready: return if self.mpv.sub: try: self.mpv.command("sub_remove") except mpv.MPVError: pass with io.StringIO() as handle: write_ass(self._api.subs.ass_file, handle) self.mpv.command("sub_add", "memory://" + handle.getvalue()) self._need_subs_refresh = False def _set_end(self, end: Optional[int]) -> None: if not self._api.playback.is_ready: return if end is None: ret = "none" else: end = max(0, end - 1) ret = ms_to_str(end) if self.mpv.end != ret: self.mpv.end = ret def _on_request_seek(self, pts: int, precise: bool) -> None: self._set_end(None) # mpv refuses to seek beyond --end self.mpv.seek(ms_to_str(pts), "absolute", "exact") def _on_request_playback(self, start: Optional[int], end: Optional[int]) -> None: if start is not None: self.mpv.seek(ms_to_str(start), "absolute") self._set_end(end) self.mpv.pause = False def _on_playback_speed_change(self) -> None: self.mpv.speed = float(self._api.playback.playback_speed) def _on_volume_change(self) -> None: self.mpv.volume = float(self._api.playback.volume) def _on_mute_change(self) -> None: self.mpv.mute = self._api.playback.is_muted def _on_pause_change(self, is_paused: bool) -> None: self._set_end(None) self.mpv.pause = is_paused def _on_video_zoom_change(self) -> None: # ignore errors coming from setting extreme values try: self.mpv.video_zoom = float(self._api.video.view.zoom) except mpv.MPVError: pass def _on_video_pan_change(self) -> None: # ignore errors coming from setting extreme values try: self.mpv.video_pan_x = float(self._api.video.view.pan_x) self.mpv.video_pan_y = float(self._api.video.view.pan_y) except mpv.MPVError: pass def _on_subs_change(self) -> None: self._need_subs_refresh = True def _on_mpv_unload(self) -> None: self._api.playback.state = PlaybackFrontendState.NOT_READY def _on_mpv_load(self) -> None: self._api.playback.state = PlaybackFrontendState.READY self._need_subs_refresh = True def _on_track_list_ready(self, track_list: Any) -> None: # self._api.log.debug(json.dumps(track_list, indent=4)) vid: Optional[int] = None aid: Optional[int] = None current_audio_stream: Optional[AudioStream] try: current_audio_stream = self._api.audio.current_stream except ResourceUnavailable: current_audio_stream = None current_video_stream: Optional[VideoStream] try: current_video_stream = self._api.video.current_stream except ResourceUnavailable: current_video_stream = None for track in track_list: track_type = track["type"] track_path = track.get("external-filename") if (track_type == "video" and current_video_stream and current_video_stream.path.samefile(track_path)): vid = track["id"] if (track_type == "audio" and current_audio_stream and current_audio_stream.path.samefile(track_path)): aid = track["id"] if self.mpv.vid != vid: self.mpv.vid = vid if vid is not None else "no" self._api.log.debug(f"playback: changing vid to {vid}") if self.mpv.aid != aid: self.mpv.aid = aid if aid is not None else "no" self._api.log.debug(f"playback: changing aid to {aid}") delay = (current_audio_stream.delay if current_audio_stream else 0) / 1000.0 if self.mpv.audio_delay != delay: self.mpv.audio_delay = delay if vid is not None or aid is not None: self._api.playback.state = PlaybackFrontendState.READY else: self._api.playback.state = PlaybackFrontendState.NOT_READY def _on_mpv_time_pos_change(self, prop_name: str, new_value: Any) -> None: pts = round((new_value or 0) * 1000) self._api.playback.receive_current_pts_change.emit(pts) def _on_mpv_pause_change(self, prop_name: str, new_value: Any) -> None: self._api.playback.pause_changed.disconnect(self._on_pause_change) self._api.playback.is_paused = new_value self._api.playback.pause_changed.connect(self._on_pause_change) def _on_mpv_track_list_change(self, prop_name: str, new_value: Any) -> None: self._on_track_list_ready(new_value)
class OpenGlArea(Gtk.GLArea): def __init__(self, mpv_commands_async: bool, **properties): super().__init__(**properties) self.mpv_commands_async = mpv_commands_async self._proc_addr_wrapper = OpenGlCbGetProcAddrFn(get_process_address) self.ctx = None self.mpv = MPV(input_default_bindings=True, input_vo_keyboard=True, osc=True # log_handler=print, # loglevel='debug' ) self.connect("realize", self.on_realize) self.connect("render", self.on_render) self.connect("unrealize", self.on_unrealize) self.add_events(Gdk.EventMask.POINTER_MOTION_MASK) self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) self.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK) # self.add_events(Gdk.EventMask.KEY_PRESS_MASK) # self.add_events(Gdk.EventMask.STRUCTURE_MASK) # self.add_events(Gdk.EventMask.SCROLL_MASK) self.connect("motion-notify-event", self.on_mouse_move_event) self.connect("button-press-event", self.on_button_press_event) self.connect("button-release-event", self.on_button_release_event) def on_realize(self, area): self.make_current() self.ctx = MpvRenderContext( self.mpv, 'opengl', opengl_init_params={'get_proc_address': self._proc_addr_wrapper}) self.ctx.update_cb = self.wrapped_c_render_func def on_unrealize(self, arg): self.ctx.free() self.mpv.terminate() def wrapped_c_render_func(self): GLib.idle_add(self.call_frame_ready, None, GLib.PRIORITY_HIGH) def call_frame_ready(self, *args): if self.ctx.update(): self.queue_render() def on_render(self, arg1, arg2): if self.ctx: factor = self.get_scale_factor() rect = self.get_allocated_size()[0] width = rect.width * factor height = rect.height * factor fbo = GL.glGetIntegerv(GL.GL_DRAW_FRAMEBUFFER_BINDING) self.ctx.render(flip_y=True, opengl_fbo={ 'w': width, 'h': height, 'fbo': fbo }) return True return False def play(self, media): self.mpv.command_async("script-binding", "stats/display-stats-toggle") self.mpv.play(media) def on_mouse_move_event(self, _, event) -> bool: scale_factor = self.get_scale_factor() if self.mpv_commands_async: self.mpv.command_async("mouse", int(event.x * scale_factor), int(event.y * scale_factor)) else: self.mpv.command("mouse", int(event.x * scale_factor), int(event.y * scale_factor)) return True def on_button_press_event(self, _, event) -> bool: btn = event.button # PRESS = "keypress" # DOWN = "keydown" # UP = "keyup" if btn == 1: # MouseButton LEFT: if self.mpv_commands_async: self.mpv.command_async("keydown", "MOUSE_BTN" + str(0)) else: self.mpv.command("keydown", "MOUSE_BTN" + str(0)) return True return False def on_button_release_event(self, _: Gtk.Widget, event) -> bool: btn = event.button if btn == 1: if self.mpv_commands_async: self.mpv.command_async("keyup", "MOUSE_BTN" + str(0)) else: self.mpv.command("keyup", "MOUSE_BTN" + str(0)) return True return False