diff --git a/app/tools/media.py b/app/tools/media.py index a5055ca8..a2b757a4 100644 --- a/app/tools/media.py +++ b/app/tools/media.py @@ -28,57 +28,95 @@ import os import sys -from abc import ABC, abstractmethod from datetime import datetime -from gi.repository import Gdk, Gtk +from gi.repository import Gdk, Gtk, GObject from app.commons import run_task, log, _DATE_FORMAT, run_with_delay -class Player(ABC): +class Player(Gtk.DrawingArea): """ Base player class. Also used as a factory. """ - @abstractmethod + def __init__(self, mode, widget, **kwargs): + super().__init__(**kwargs) + + GObject.signal_new("error", self, GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + GObject.signal_new("message", self, GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + GObject.signal_new("position", self, GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + GObject.signal_new("played", self, GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + GObject.signal_new("audio-track", self, GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + GObject.signal_new("subtitle-track", self, GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + + self.connect("draw", self.on_draw) + self.connect("motion-notify-event", self.on_mouse_motion) + self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK) + widget.add(self) + + parent = widget.get_parent() + parent.connect("play", self.on_play) + parent.connect("stop", self.on_stop) + self.show() + def get_play_mode(self): pass - @abstractmethod def play(self, mrl=None): pass - @abstractmethod def stop(self): pass - @abstractmethod def pause(self): pass - @abstractmethod def set_time(self, time): pass - @abstractmethod def release(self): pass - @abstractmethod def is_playing(self): pass - @abstractmethod - def get_instance(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb): + def set_audio_track(self, track): pass - def get_window_handle(self, widget): + def get_audio_track(self): + pass + + def set_subtitle_track(self, track): + pass + + def set_aspect_ratio(self, ratio): + pass + + def get_instance(self, mode, widget): + pass + + def on_play(self, widget, url): + self.play(url) + + def on_stop(self, widget, state): + self.stop() + + def on_release(self, widget, state): + self.release() + + def get_window_handle(self): """ Returns the identifier [pointer] for the window. Based on gtkvlc.py[get_window_pointer] example from here: https://github.com/oaubert/python-vlc/tree/master/examples """ if sys.platform == "linux": - return widget.get_window().get_xid() + return self.get_window().get_xid() else: is_darwin = sys.platform == "darwin" try: @@ -91,23 +129,14 @@ class Player(ABC): # https://gitlab.gnome.org/GNOME/pygobject/-/issues/112 ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object] - gpointer = ctypes.pythonapi.PyCapsule_GetPointer(widget.get_window().__gpointer__, None) + gpointer = ctypes.pythonapi.PyCapsule_GetPointer(self.get_window().__gpointer__, None) get_pointer = libgdk.gdk_quartz_window_get_nsview if is_darwin else libgdk.gdk_win32_window_get_handle get_pointer.restype = ctypes.c_void_p get_pointer.argtypes = [ctypes.c_void_p] return get_pointer(gpointer) - def get_video_widget(self, widget): - area = Gtk.DrawingArea(visible=True) - area.connect("draw", self.on_drawing_area_draw) - area.connect("motion-notify-event", self.on_mouse_motion) - area.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK) - widget.add(area) - - return area - - def on_drawing_area_draw(self, widget, cr): + def on_draw(self, widget, cr): """ Used for black background drawing in the player drawing area. """ cr.set_source_rgb(0, 0, 0) cr.paint() @@ -126,25 +155,21 @@ class Player(ABC): window.set_cursor(cursor) @staticmethod - def make(name, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None): + def make(name, mode, widget): """ Factory method. We will not use a separate factory to return a specific implementation. @param name: implementation name. @param mode: current player mode [Built-in or windowed]. @param widget: parent of video widget. - @param buf_cb: buffering callback. - @param position_cb: time (position) callback. - @param error_cb: error callback. - @param playing_cb: playing state callback. Throws a NameError if there is no implementation for the given name. """ if name == "mpv": - return MpvPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb) + return MpvPlayer.get_instance(mode, widget) elif name == "gst": - return GstPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb) + return GstPlayer.get_instance(mode, widget) elif name == "vlc": - return VlcPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb) + return VlcPlayer.get_instance(mode, widget) else: raise NameError("There is no such [{}] implementation.".format(name)) @@ -156,11 +181,12 @@ class MpvPlayer(Player): """ __INSTANCE = None - def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb): + def __init__(self, mode, widget): + super().__init__(mode, widget) try: from app.tools import mpv - self._player = mpv.MPV(wid=str(self.get_window_handle(self.get_video_widget(widget), )), + self._player = mpv.MPV(wid=str(self.get_window_handle()), input_default_bindings=False, input_cursor=False, cursor_autohide="no") @@ -174,25 +200,24 @@ class MpvPlayer(Player): @self._player.event_callback(mpv.MpvEventID.FILE_LOADED) def on_open(event): log("Starting playback...") - playing_cb() + self.emit("played", 0) @self._player.event_callback(mpv.MpvEventID.END_FILE) def on_end(event): event = event.get("event", {}) if event.get("reason", mpv.MpvEventEndFile.ERROR) == mpv.MpvEventEndFile.ERROR: log("Stream playback error: {}".format(event.get("error", mpv.ErrorCode.GENERIC))) - error_cb() + self.emit("error", "Can't Playback!") @classmethod - def get_instance(cls, mode, widget, buf_cb, position_cb, error_cb, playing_cb): + def get_instance(cls, mode, widget): if not cls.__INSTANCE: - cls.__INSTANCE = MpvPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb) + cls.__INSTANCE = MpvPlayer(mode, widget) return cls.__INSTANCE def get_play_mode(self): return self._mode - @run_task def play(self, mrl=None): if not mrl: return @@ -200,7 +225,6 @@ class MpvPlayer(Player): self._player.play(mrl) self._is_playing = True - @run_task def stop(self): self._player.stop() self._is_playing = True @@ -225,7 +249,8 @@ class GstPlayer(Player): __INSTANCE = None - def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb): + def __init__(self, mode, widget): + super().__init__(mode, widget) try: import gi @@ -234,30 +259,17 @@ class GstPlayer(Player): from gi.repository import Gst, GstVideo # Initialization of GStreamer. Gst.init(sys.argv) - gtk_sink = Gst.ElementFactory.make("gtksink") - if not gtk_sink: - msg = "GStreamer error: gtksink plugin not installed!" - log(msg) - raise ImportError(msg) except (OSError, ValueError) as e: log("{}: Load library error: {}".format(__class__.__name__, e)) raise ImportError("No GStreamer is found. Check that it is installed!") else: - self._error_cb = error_cb - self._playing_cb = playing_cb - self.STATE = Gst.State self.STAT_RETURN = Gst.StateChangeReturn self._mode = mode self._is_playing = False self._player = Gst.ElementFactory.make("playbin", "player") - # Initialization of the playback widget. - self._player.set_property("video-sink", gtk_sink) - vid_widget = gtk_sink.get_property("widget") - vid_widget.connect("motion-notify-event", self.on_mouse_motion) - widget.add(vid_widget) - vid_widget.show() + self._player.set_window_handle(self.get_window_handle()) bus = self._player.get_bus() bus.add_signal_watch() @@ -266,9 +278,9 @@ class GstPlayer(Player): bus.connect("message::eos", self.on_eos) @classmethod - def get_instance(cls, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None): + def get_instance(cls, mode, widget): if not cls.__INSTANCE: - cls.__INSTANCE = GstPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb) + cls.__INSTANCE = GstPlayer(mode, widget) return cls.__INSTANCE def get_play_mode(self): @@ -285,8 +297,11 @@ class GstPlayer(Player): ret = self._player.set_state(self.STATE.PLAYING) if ret == self.STAT_RETURN.FAILURE: - log("ERROR: Unable to set the 'PLAYING' state for '{}'.".format(mrl)) + msg = "ERROR: Unable to set the 'PLAYING' state for '{}'.".format(mrl) + log(msg) + self.emit("error", msg) else: + self.emit("played", 0) self._is_playing = True def stop(self): @@ -315,7 +330,7 @@ class GstPlayer(Player): def on_error(self, bus, msg): err, dbg = msg.parse_error() log(err) - self._error_cb() + self.emit("error", "Can't Playback!") def on_state_changed(self, bus, msg): if not msg.src == self._player: @@ -325,7 +340,7 @@ class GstPlayer(Player): old_state, new_state, pending = msg.parse_state_changed() if new_state is self.STATE.PLAYING: log("Starting playback...") - self._playing_cb() + self.emit("played", 0) self.get_stream_info() def on_eos(self, bus, msg): @@ -360,8 +375,12 @@ class VlcPlayer(Player): __VLC_INSTANCE = None - def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb): + def __init__(self, mode, widget): + super().__init__(mode, widget) try: + if sys.platform == "win32": + os.add_dll_directory(r"C:\Program Files\VideoLAN\VLC") + from app.tools import vlc from app.tools.vlc import EventType @@ -377,45 +396,28 @@ class VlcPlayer(Player): self._is_playing = False ev_mgr = self._player.event_manager() - - if buf_cb: - # TODO look other EventType options - ev_mgr.event_attach(EventType.MediaPlayerBuffering, - lambda et, p: buf_cb(p.get_media().get_duration()), - self._player) - if position_cb: - ev_mgr.event_attach(EventType.MediaPlayerTimeChanged, - lambda et, p: position_cb(p.get_time()), - self._player) - - if error_cb: - ev_mgr.event_attach(EventType.MediaPlayerEncounteredError, - lambda et, p: error_cb(), - self._player) - if playing_cb: - ev_mgr.event_attach(EventType.MediaPlayerPlaying, - lambda et, p: playing_cb(), - self._player) + ev_mgr.event_attach(EventType.MediaPlayerVout, self.on_playback_start) + ev_mgr.event_attach(EventType.MediaPlayerTimeChanged, + lambda et: self.emit("position", self._player.get_time())) + ev_mgr.event_attach(EventType.MediaPlayerEncounteredError, lambda et: self.emit("error", "Can't Playback!")) self.init_video_widget(widget) @classmethod - def get_instance(cls, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None): + def get_instance(cls, mode, widget): if not cls.__VLC_INSTANCE: - cls.__VLC_INSTANCE = VlcPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb) + cls.__VLC_INSTANCE = VlcPlayer(mode, widget) return cls.__VLC_INSTANCE def get_play_mode(self): return self._mode - @run_task def play(self, mrl=None): if mrl: self._player.set_mrl(mrl) self._player.play() self._is_playing = True - @run_task def stop(self): if self._is_playing: self._player.stop() @@ -441,14 +443,34 @@ class VlcPlayer(Player): def is_playing(self): return self._is_playing + def set_audio_track(self, track): + self._player.audio_set_track(track) + + def get_audio_track(self): + return self._player.audio_get_track() + + def set_subtitle_track(self, track): + self._player.video_set_spu(track) + + def set_aspect_ratio(self, ratio): + self._player.video_set_aspect_ratio(ratio) + + def on_playback_start(self, event): + self.emit("played", self._player.get_media().get_duration()) + # Audio tracks + a_desc = self._player.audio_get_track_description() + self.emit("audio-track", [(t[0], t[1].decode(encoding="utf-8", errors="ignore")) for t in a_desc]) + # Subtitle + s_desc = self._player.video_get_spu_description() + self.emit("subtitle-track", [(s[0], s[1].decode(encoding="utf-8", errors="ignore")) for s in s_desc]) + def init_video_widget(self, widget): - video_widget = self.get_video_widget(widget) if sys.platform == "linux": - self._player.set_xwindow(video_widget.get_window().get_xid()) + self._player.set_xwindow(self.get_window_handle()) elif sys.platform == "darwin": - self._player.set_nsobject(self.get_window_handle(video_widget)) + self._player.set_nsobject(self.get_window_handle()) else: - self._player.set_hwnd(self.get_window_handle(video_widget)) + self._player.set_hwnd(self.get_window_handle()) class Recorder: diff --git a/app/ui/app_menu.ui b/app/ui/app_menu.ui index ad28d5ca..ed92fd74 100644 --- a/app/ui/app_menu.ui +++ b/app/ui/app_menu.ui @@ -1,6 +1,17 @@ + + Playback + app.hide_media_bar + action-disabled +
+ + Close + app.on_playback_close + +
+
File app.hide_menu_bar @@ -176,6 +187,17 @@
+ + Playback + app.hide_media_bar + action-disabled +
+ + Close + app.on_playback_close + +
+
File app.hide_menu_bar diff --git a/app/ui/control.glade b/app/ui/control.glade index cb867eeb..9c8bbc22 100644 --- a/app/ui/control.glade +++ b/app/ui/control.glade @@ -1201,6 +1201,7 @@ Author: Dmitriy Yefremov 5 + end 1 @@ -1276,7 +1277,7 @@ Author: Dmitriy Yefremov True - False + True diff --git a/app/ui/control.py b/app/ui/control.py index 5888d54d..26c808ef 100644 --- a/app/ui/control.py +++ b/app/ui/control.py @@ -35,9 +35,10 @@ from urllib.parse import quote from gi.repository import GLib from .dialogs import get_builder, show_dialog, DialogType -from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Page +from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Page, Column from ..commons import run_task, run_with_delay, log, run_idle from ..connections import HttpAPI, UtfFTP +from ..settings import IS_DARWIN, PlayStreamsMode class EpgBox(Gtk.Box): @@ -131,15 +132,14 @@ class RecordingsBox(Gtk.Box): def __init__(self, app, http_api, settings, *args, **kwargs): super().__init__(*args, **kwargs) - self._http_api = http_api self._app = app self._app.connect("profile-changed", self.init) self._settings = settings self._ftp = None # Icon. theme = Gtk.IconTheme.get_default() - icon = "folder-symbolic" - self._icon = theme.load_icon(icon, 32, 0) if theme.lookup_icon(icon, 32, 0) else None + icon = "folder-symbolic" if IS_DARWIN else "folder" + self._icon = theme.load_icon(icon, 24, 0) if theme.lookup_icon(icon, 24, 0) else None handlers = {"on_path_press": self.on_path_press, "on_path_activated": self.on_path_activated, @@ -150,6 +150,7 @@ class RecordingsBox(Gtk.Box): objects=("recordings_frame", "recordings_model", "rec_paths_model")) self._rec_view = builder.get_object("recordings_view") self._paths_view = builder.get_object("recordings_paths_view") + self._paned = builder.get_object("recordings_paned") self.pack_start(builder.get_object("recordings_frame"), True, True, 0) self.init() @@ -209,7 +210,7 @@ class RecordingsBox(Gtk.Box): def on_path_activated(self, view, path, column): row = view.get_model()[path][:] path = "{}/{}".format(row[-1], row[1]) - self._http_api.send(HttpAPI.Request.RECORDINGS, quote(path), self.update_recordings_data) + self._app.http_api.send(HttpAPI.Request.RECORDINGS, quote(path), self.update_recordings_data) def on_path_press(self, view, event): target = view.get_path_at_pos(event.x, event.y) @@ -237,12 +238,12 @@ class RecordingsBox(Gtk.Box): def on_recordings_activated(self, view, path, column): rec = view.get_model()[path][-1] - self._http_api.send(HttpAPI.Request.STREAM_TS, rec.get("e2filename", ""), self.on_play_recording) + self._app.http_api.send(HttpAPI.Request.STREAM_TS, rec.get("e2filename", ""), self.on_play_recording) def on_play_recording(self, m3u): url = self._app.get_url_from_m3u(m3u) if url: - self._app.play(url) + self._app.emit("play-recording", url) def on_recording_remove(self, action, value=None): """ Removes recordings via FTP. """ @@ -259,6 +260,21 @@ class RecordingsBox(Gtk.Box): self._app.show_error_message(resp) break + def on_playback(self, box, state): + """ Updates state of the UI elements for playback mode. """ + if self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN: + self._paned.set_orientation(Gtk.Orientation.VERTICAL) + self.update_rec_columns_visibility(False) + + def on_playback_close(self, box, state): + """ Restores UI elements state after playback mode. """ + self._paned.set_orientation(Gtk.Orientation.HORIZONTAL) + self.update_rec_columns_visibility(True) + + def update_rec_columns_visibility(self, state): + for c in (Column.REC_SERVICE, Column.REC_TIME, Column.REC_LEN, Column.REC_FILE, Column.REC_DESC): + self._rec_view.get_column(c).set_visible(state) + class ControlBox(Gtk.Box): diff --git a/app/ui/main.glade b/app/ui/main.glade index c5de3a0e..ad7aa962 100644 --- a/app/ui/main.glade +++ b/app/ui/main.glade @@ -1,5 +1,5 @@ - + + + + True + False + + + + + + + + True + False + + + True + False + Previous stream in the list + True + gtk-media-previous + + + + False + False + + + + + True + False + Play + gtk-media-play + + + + False + False + + + + + True + False + Stop playback + True + gtk-media-stop + + + + False + False + + + + + True + False + Next stream in the list + True + gtk-media-next + + + + False + False + + + + + True + False + + + False + 2 + + + True + False + 0 + + + False + True + 0 + + + + + True + True + False + 0 + False + False + + + + True + True + 1 + + + + + True + False + 0 + + + False + True + 2 + + + + + + + True + True + + + + + True + False + Toggle in fullscreen + True + gtk-fullscreen + + + + False + False + + + + + True + False + Close playback + True + gtk-close + + + + False + False + + + + diff --git a/app/ui/playback.py b/app/ui/playback.py new file mode 100644 index 00000000..c6c32dc7 --- /dev/null +++ b/app/ui/playback.py @@ -0,0 +1,331 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2018-2021 Dmitriy Yefremov +# +# 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. +# +# Author: Dmitriy Yefremov +# + + +""" Additional module for playback. """ +from functools import lru_cache + +from gi.repository import GLib, GObject + +from app.commons import run_idle, run_with_delay +from app.connections import HttpAPI +from app.eparser.ecommons import BqServiceType +from app.settings import PlayStreamsMode, IS_DARWIN +from app.tools.media import Player +from app.ui.dialogs import get_builder +from app.ui.main_helper import get_iptv_url +from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, Column + + +class PlayerBox(Gtk.Box): + + def __init__(self, app, *args, **kwargs): + super().__init__(*args, **kwargs) + # Signals. + GObject.signal_new("playback-full-screen", self, GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + GObject.signal_new("playback-close", self, GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + GObject.signal_new("play", self, GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + GObject.signal_new("stop", self, GObject.SIGNAL_RUN_LAST, + GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,)) + + self._app = app + self._app.connect("fav-clicked", self.on_fav_clicked) + self._app.connect("page-changed", self.on_page_changed) + self._app.connect("play-current", self.on_play_current) + self._app.connect("play-recording", self.on_play_recording) + self._fav_view = app.fav_view + self._player = None + self._current_mrl = None + self._full_screen = False + self._playback_window = None + self._play_mode = self._app.app_settings.play_streams_mode + + handlers = {"on_realize": self.on_realize, + "on_press": self.on_press, + "on_play": self.on_play, + "on_stop": self.on_stop, + "on_next": self.on_next, + "on_previous": self.on_previous, + "on_rewind": self.on_rewind, + "on_full_screen": self.on_full_screen, + "on_close": self.on_close} + + builder = get_builder(UI_RESOURCES_PATH + "playback.glade", handlers) + self.set_spacing(5) + self.set_orientation(Gtk.Orientation.VERTICAL) + self._event_box = builder.get_object("event_box") + self.pack_start(self._event_box, True, True, 0) + self.pack_end(builder.get_object("tool_bar"), False, True, 0) + + self._scale = builder.get_object("scale") + self._full_time_label = builder.get_object("full_time_label") + self._current_time_label = builder.get_object("current_time_label") + self._rewind_box = builder.get_object("rewind_box") + self._tool_bar = builder.get_object("tool_bar") + self._prev_button = builder.get_object("prev_button") + self._next_button = builder.get_object("next_button") + self._play_button = builder.get_object("play_button") + self._fav_view.bind_property("sensitive", self._prev_button, "sensitive") + self._fav_view.bind_property("sensitive", self._next_button, "sensitive") + + self.connect("delete-event", self.on_delete) + self.connect("show", self.set_player_area_size) + + def on_fav_clicked(self, app, mode): + if mode is not FavClickMode.STREAM and not self._app.http_api: + return + + self._fav_view.set_sensitive(False) + if mode is FavClickMode.STREAM: + self.on_play_stream() + elif mode is FavClickMode.ZAP_PLAY: + self._app.on_zap(self.on_watch) + elif mode is FavClickMode.PLAY: + self.on_play_service() + + def on_play_current(self, app, url): + self.on_watch() + + def on_play_recording(self, app, url): + self.play(url) + + def on_page_changed(self, app, page): + self.on_close() + self.set_visible(False) + + def on_realize(self, box): + if not self._player: + settings = self._app.app_settings + try: + self._player = Player.make(settings.stream_lib, settings.play_streams_mode, self._event_box) + self._player.connect("error", self.on_error) + self._player.connect("played", self.on_played) + self._player.connect("position", self.on_time_changed) + except (ImportError, NameError) as e: + self._app.show_error_message(str(e)) + return True + else: + self._app.app_window.connect("key-press-event", self.on_key_press) + self.emit("play", self._current_mrl) + finally: + if settings.play_streams_mode is PlayStreamsMode.BUILT_IN: + self.set_player_area_size(box) + + def on_play(self, button=None): + self.emit("play", None) + + def on_stop(self, button=None): + self.emit("stop", None) + + def on_next(self, button): + if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, 1): + self.set_player_action() + + def on_previous(self, button): + if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, -1): + self.set_player_action() + + def on_rewind(self, scale, scroll_type, value): + self._player.set_time(int(value)) + + def on_full_screen(self, item=None): + self._full_screen = not self._full_screen + if self._play_mode is PlayStreamsMode.BUILT_IN: + self._tool_bar.set_visible(not self._full_screen) + self.emit("playback-full-screen", not self._full_screen) + elif self._playback_window: + self._tool_bar.set_visible(not self._full_screen) + self._playback_window.fullscreen() if self._full_screen else self._playback_window.unfullscreen() + + def on_close(self, action=None, value=None): + if self._playback_window: + self._app.app_settings.add("playback_window_size", self._playback_window.get_size()) + self._playback_window.hide() + + self.on_stop() + self.hide() + self.emit("playback-close", None) + + return True + + def on_press(self, area, event): + if event.button == Gdk.BUTTON_PRIMARY: + if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS: + self.on_full_screen() + + def on_key_press(self, widget, event): + if self._player and self.get_visible(): + key = event.keyval + if any((key == Gdk.KEY_F11, key == Gdk.KEY_f, self._full_screen and key == Gdk.KEY_Escape)): + self.on_full_screen() + + def on_delete(self, box): + if self._player: + self._player.release() + + @run_with_delay(1) + def set_player_action(self): + self._fav_view.set_sensitive(False) + if self._fav_click_mode is FavClickMode.PLAY: + self.on_play_service() + elif self._fav_click_mode is FavClickMode.ZAP_PLAY: + self.on_zap(self.on_watch) + elif self._fav_click_mode is FavClickMode.STREAM: + self.on_play_stream() + + def update_buttons(self): + if self._player: + path, column = self._fav_view.get_cursor() + current_index = path[0] + self._player_prev_button.set_sensitive(current_index != 0) + self._player_next_button.set_sensitive(len(self._fav_model) != current_index + 1) + + @lru_cache(maxsize=1) + def on_duration_changed(self, duration): + self._scale.set_value(0) + self._scale.get_adjustment().set_upper(duration) + GLib.idle_add(self._rewind_box.set_visible, duration > 0, priority=GLib.PRIORITY_LOW) + GLib.idle_add(self._current_time_label.set_text, "0", priority=GLib.PRIORITY_LOW) + GLib.idle_add(self._full_time_label.set_text, self.get_time_str(duration), + priority=GLib.PRIORITY_LOW) + + def on_time_changed(self, widget, t): + if not self._full_screen and self._rewind_box.get_visible(): + GLib.idle_add(self._current_time_label.set_text, self.get_time_str(t), + priority=GLib.PRIORITY_LOW) + + def get_time_str(self, duration): + """ Returns a string representation of time from duration in milliseconds """ + m, s = divmod(duration // 1000, 60) + h, m = divmod(m, 60) + return "{}{:02d}:{:02d}".format(str(h) + ":" if h else "", m, s) + + def set_player_area_size(self, widget): + w, h = self._app.app_window.get_size() + widget.set_size_request(w * 0.6, -1) + + @run_idle + def show_playback_window(self): + width, height = 480, 240 + size = self._app.app_settings.get("playback_window_size") + if size: + width, height = size + + if self._playback_window: + self._playback_window.show() + else: + self._playback_window = Gtk.Window(title=self.get_playback_title(), + window_position=Gtk.WindowPosition.CENTER, + icon_name="demon-editor") + + self._playback_window.connect("delete-event", self.on_close) + self._playback_window.connect("key-press-event", self.on_key_press) + self._playback_window.bind_property("visible", self._event_box, "visible") + + if not IS_DARWIN: + self._prev_button.set_visible(False) + self._next_button.set_visible(False) + + self.reparent(self._playback_window) + self._playback_window.set_application(self._app) + + self.show() + self._playback_window.resize(width, height) + self._playback_window.show() + + def get_playback_title(self): + path, column = self._fav_view.get_cursor() + if path: + return "DemonEditor [{}]".format(self._app.fav_view.get_model()[path][:][Column.FAV_SERVICE]) + return "DemonEditor [Playback]" + + def on_play_stream(self): + path, column = self._fav_view.get_cursor() + if path: + row = self._fav_view.get_model()[path][:] + if row[Column.FAV_TYPE] != BqServiceType.IPTV.name: + self.on_error(None, "Not allowed in this context!") + return + + url = get_iptv_url(row, self._app.app_settings.setting_type) + self.play(url) if url else self.on_error(None, "No reference is present!") + + def on_play_service(self, item=None): + path, column = self._fav_view.get_cursor() + if not path or not self._app.http_api: + return + + ref = self._app.get_service_ref(path) + if not ref: + return + + if self._player and self._player.is_playing(): + self.emit("stop", None) + + self._app.http_api.send(HttpAPI.Request.STREAM, ref, self.watch) + + def on_watch(self, item=None): + """ Switch to the channel and watch in the player. """ + self._app.http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.watch) + + def watch(self, data): + url = self._app.get_url_from_m3u(data) + GLib.timeout_add_seconds(1, self.play, url) if url else self.on_error(None, "Can't Playback!") + + def play(self, url): + if self._play_mode is PlayStreamsMode.M3U: + self._app.save_stream_to_m3u(url) + return + + if self._play_mode is not self._app.app_settings.play_streams_mode: + self.on_error(None, "Play mode has been changed!\nRestart the program to apply the settings.") + return + + if self._play_mode is PlayStreamsMode.BUILT_IN: + self.show() + elif self._play_mode is PlayStreamsMode.WINDOW: + self.show_playback_window() + + if self._player: + self.emit("play", url) + else: + self._current_mrl = url + + def on_played(self, player, duration): + GLib.idle_add(self._fav_view.set_sensitive, True) + self.on_duration_changed(duration) + + def on_error(self, player, msg): + self._app.show_error_message(msg) + self._fav_view.set_sensitive(True) + + +if __name__ == "__main__": + pass diff --git a/app/ui/uicommons.py b/app/ui/uicommons.py index 5d2f8795..8e7ef591 100644 --- a/app/ui/uicommons.py +++ b/app/ui/uicommons.py @@ -125,7 +125,6 @@ class Page(Enum): SERVICES = "services" SATELLITE = "satellite" PICONS = "picons" - PLAYBACK = "playback" EPG = "epg" TIMERS = "timers" RECORDINGS = "recordings" @@ -210,6 +209,13 @@ class Column(IntEnum): ALT_FAV_ID = 5 ALT_ID = 6 ALT_ITER = 7 + # Recordings view + REC_SERVICE = 0 + REC_TITLE = 1 + REC_TIME = 2 + REC_LEN = 3 + REC_FILE = 4 + REC_DESC = 5 def __index__(self): """ Overridden to get the index in slices directly """