Example #1
0
 def initializeGL(self) -> None:
     self.mpv_gl = MpvRenderContext(
         self.mpv,
         "opengl",
         opengl_init_params={"get_proc_address": self.get_proc_addr_c},
     )
     self.mpv_gl.update_cb = self.on_update
 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
Example #3
0
class MpvOpenGLWidget(VideoOpenGLWidget):
    """Mpv 视频输出窗口

    销毁时,应该调用 shutdown 方法来释放资源。

    该 Widget 是模仿一些 C++ 程序编写而成(详见项目 research 目录下的例子),
    我们对背后的原理不太理解,目前测试是可以正常工作的。

    目前主要的疑问有:

    - [ ] shutdown 方法期望是用来释放资源的,目前的写法不知是否合理?
          应该在什么时候调用 shutdown 方法?
    """
    def __init__(self, app, parent=None):
        super().__init__(app=app, parent=parent)
        self._app = app
        self.mpv = self._app.player._mpv  # noqa
        self.ctx = None
        self.get_proc_addr_c = OpenGlCbGetProcAddrFn(get_proc_addr)

    def initializeGL(self):
        params = {'get_proc_address': self.get_proc_addr_c}
        self.ctx = MpvRenderContext(self.mpv,
                                    'opengl',
                                    opengl_init_params=params)
        self.ctx.update_cb = self.on_update

    def shutdown(self):
        if self.ctx is not None:
            self.ctx.free()
            self.ctx = None

    def paintGL(self):
        # HELP: It seems that `initializeGL` is called by Qt on
        # old version (<= v5.15.2)
        if self.ctx is None:
            self.initializeGL()
        # compatible with HiDPI display
        ratio = self._app.devicePixelRatio()
        w = int(self.width() * ratio)
        h = int(self.height() * ratio)
        opengl_fbo = {'w': w, 'h': h, 'fbo': self.defaultFramebufferObject()}
        self.ctx.render(flip_y=True, opengl_fbo=opengl_fbo)

    @pyqtSlot()
    def maybe_update(self):
        if self.window().isMinimized():
            self.makeCurrent()
            self.paintGL()
            self.context().swapBuffers(self.context().surface())
            self.doneCurrent()
        else:
            self.update()

    def on_update(self, ctx=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')
Example #4
0
class MpvWidget(QOpenGLWidget):
    def __init__(self, parent=None):
        super().__init__(parent=parent)
        self.mpv = MPV(ytdl=True)
        self.ctx = None
        self.get_proc_addr_c = OpenGlCbGetProcAddrFn(get_proc_addr)

    def initializeGL(self):
        params = {'get_proc_address': self.get_proc_addr_c}
        self.ctx = MpvRenderContext(self.mpv,
                                    'opengl',
                                    opengl_init_params=params)
        self.ctx.update_cb = self.on_update

    def paintGL(self):
        # compatible with HiDPI display
        ratio = self.windowHandle().devicePixelRatio()
        w = int(self.width() * ratio)
        h = int(self.height() * ratio)
        opengl_fbo = {'w': w, 'h': h, 'fbo': self.defaultFramebufferObject()}
        self.ctx.render(flip_y=True, opengl_fbo=opengl_fbo)

    @pyqtSlot()
    def maybe_update(self):
        if self.window().isMinimized():
            self.makeCurrent()
            self.paintGL()
            self.context().swapBuffers(self.context().surface())
            self.doneCurrent()
        else:
            self.update()

    def on_update(self, ctx=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=None):
        pass

    def closeEvent(self, _):
        # self.makeCurrent()
        # self.mpv.terminate()
        pass
Example #5
0
    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
Example #6
0
 def initializeGL(self):
     params = {'get_proc_address': self.get_proc_addr_c}
     self.ctx = MpvRenderContext(self.mpv,
                                 'opengl',
                                 opengl_init_params=params)
     self.ctx.update_cb = self.on_update
Example #7
0
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()
Example #8
0
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(ytdl=False, loglevel="info", log_handler=print)
        self.mpv_gl = None
        self.get_proc_addr_c = OpenGlCbGetProcAddrFn(get_proc_addr)
        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:
        self.mpv_gl = MpvRenderContext(
            self.mpv,
            "opengl",
            opengl_init_params={"get_proc_address": self.get_proc_addr_c},
        )
        self.mpv_gl.update_cb = self.on_update

    def paintGL(self) -> None:
        if self.mpv_gl:
            ratio = self.devicePixelRatioF()
            w = int(self.width() * ratio)
            h = int(self.height() * ratio)
            self.mpv_gl.render(
                flip_y=True,
                opengl_fbo={
                    "fbo": self.defaultFramebufferObject(),
                    "w": w,
                    "h": 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) -> None:
        self._schedule_update.emit()

    def on_update_fake(self) -> None:
        pass

    def swapped(self) -> None:
        if self.mpv_gl:
            self.mpv_gl.report_swap()

    def closeEvent(self, _: Any) -> None:
        self.makeCurrent()
        if self.mpv_gl:
            self.mpv_gl.update_cb = self.on_update_fake
            self.mpv_gl.free()

    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