streams playback rework

This commit is contained in:
DYefremov
2021-09-13 16:52:19 +03:00
parent a034d0476d
commit 9a2a2b49f6
10 changed files with 2423 additions and 2281 deletions

View File

@@ -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:

View File

@@ -1,6 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="menu_bar">
<submenu>
<attribute name="label" translatable="yes">Playback</attribute>
<attribute name="action">app.hide_media_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Close</attribute>
<attribute name="action">app.on_playback_close</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">File</attribute>
<attribute name="action">app.hide_menu_bar</attribute>
@@ -176,6 +187,17 @@
</section>
</menu>
<menu id="mac_menu_bar">
<submenu>
<attribute name="label" translatable="yes">Playback</attribute>
<attribute name="action">app.hide_media_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Close</attribute>
<attribute name="action">app.on_playback_close</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">File</attribute>
<attribute name="action">app.hide_menu_bar</attribute>

View File

@@ -1201,6 +1201,7 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkCellRendererText" id="rec_title_renderer">
<property name="xpad">5</property>
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">1</attribute>
@@ -1276,7 +1277,7 @@ Author: Dmitriy Yefremov
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">False</property>
<property name="shrink">True</property>
</packing>
</child>
<child>

View File

@@ -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):

File diff suppressed because it is too large Load Diff

View File

@@ -45,12 +45,13 @@ from app.eparser.ecommons import CAS, Flag, BouquetService
from app.eparser.enigma.bouquets import BqServiceType
from app.eparser.iptv import export_to_m3u
from app.eparser.neutrino.bouquets import BqType
from app.settings import (SettingsType, Settings, SettingsException, PlayStreamsMode, SettingsReadException,
IS_DARWIN)
from app.tools.media import Player, Recorder
from app.settings import (SettingsType, Settings, SettingsException, SettingsReadException,
IS_DARWIN, PlayStreamsMode)
from app.tools.media import Recorder
from app.ui.control import ControlBox, EpgBox, TimersBox, RecordingsBox
from app.ui.epg_dialog import EpgDialog
from app.ui.ftp import FtpClientBox
from app.ui.playback import PlayerBox
from app.ui.transmitter import LinksTransmitter
from .backup import BackupDialog, backup_data, clear_data_path
from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog, get_message, get_builder
@@ -59,8 +60,9 @@ from .imports import ImportDialog, import_bouquet
from .iptv import IptvDialog, SearchUnavailableDialog, IptvListConfigurationDialog, YtListImportDialog, M3uImportDialog
from .main_helper import (insert_marker, move_items, rename, ViewTarget, set_flags, locate_in_services,
scroll_to, get_base_model, update_picons_data, copy_picon_reference, assign_picons,
remove_picon, is_only_one_item_selected, gen_bouquets, BqGenType, get_iptv_url, append_picons,
get_selection, get_model_data, remove_all_unused_picons, get_picon_pixbuf, get_base_itrs)
remove_picon, is_only_one_item_selected, gen_bouquets, BqGenType, append_picons,
get_selection, get_model_data, remove_all_unused_picons, get_picon_pixbuf, get_base_itrs,
get_iptv_url)
from .picons import PiconManager
from .satellites import SatellitesTool, ServicesUpdateDialog
from .search import SearchProvider
@@ -174,18 +176,8 @@ class Application(Gtk.Application):
"on_epg_list_configuration": self.on_epg_list_configuration,
"on_iptv_list_configuration": self.on_iptv_list_configuration,
"on_play_stream": self.on_play_stream,
"on_watch": self.on_watch,
"on_player_play": self.on_player_play,
"on_player_stop": self.on_player_stop,
"on_player_previous": self.on_player_previous,
"on_player_next": self.on_player_next,
"on_player_rewind": self.on_player_rewind,
"on_player_close": self.on_player_close,
"on_player_press": self.on_player_press,
"on_full_screen": self.on_full_screen,
"on_play_current": self.on_play_current,
"on_main_window_state": self.on_main_window_state,
"on_player_box_realize": self.on_player_box_realize,
"on_player_box_visibility": self.on_player_box_visibility,
"on_record": self.on_record,
"on_remove_all_unavailable": self.on_remove_all_unavailable,
"on_new_bouquet": self.on_new_bouquet,
@@ -240,11 +232,6 @@ class Application(Gtk.Application):
self._recordings_box = None
self._control_box = None
self._ftp_client = None
# Player
self._player = None
self._full_screen = False
self._current_mrl = None
self._playback_window = None
# Record
self._recorder = None
# http api
@@ -258,14 +245,20 @@ class Application(Gtk.Application):
self._EXTRA_COLOR = None # Color for services with a extra name for the bouquet
# Current page.
self._page = Page.INFO
self._fav_pages = {Page.SERVICES, Page.PICONS, Page.PLAYBACK, Page.EPG, Page.TIMERS}
self._fav_pages = {Page.SERVICES, Page.PICONS, Page.EPG, Page.TIMERS}
# Signals.
GObject.signal_new("profile-changed", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("fav-changed", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("fav-clicked", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("page-changed", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("play-recording", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("play-current", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
builder = get_builder(UI_RESOURCES_PATH + "main.glade", handlers)
self._main_window = builder.get_object("main_window")
@@ -288,7 +281,7 @@ class Application(Gtk.Application):
self._services_view.get_model().set_sort_func(Column.SRV_POS, self.position_sort_func, Column.SRV_POS)
# App info
self._app_info_box = builder.get_object("app_info_box")
self._app_info_box.bind_property("visible", builder.get_object("main_paned"), "visible", 4)
self._app_info_box.bind_property("visible", builder.get_object("data_paned"), "visible", 4)
# Info bar.
self._info_bar = builder.get_object("info_bar")
self._info_label = builder.get_object("info_label")
@@ -338,23 +331,20 @@ class Application(Gtk.Application):
self._filter_only_free_button = builder.get_object("filter_only_free_button")
self._services_load_spinner.bind_property("active", self._filter_header_button, "sensitive", 4)
self._services_load_spinner.bind_property("active", self._filter_box, "sensitive", 4)
# Player
self._player_box = builder.get_object("player_box")
self._player_event_box = builder.get_object("player_event_box")
self._player_scale = builder.get_object("player_scale")
self._player_full_time_label = builder.get_object("player_full_time_label")
self._player_current_time_label = builder.get_object("player_current_time_label")
self._player_rewind_box = builder.get_object("player_rewind_box")
self._player_tool_bar = builder.get_object("player_tool_bar")
self._player_prev_button = builder.get_object("player_prev_button")
self._player_next_button = builder.get_object("player_next_button")
self._player_play_button = builder.get_object("player_play_button")
# Playback.
self._player_box = PlayerBox(self)
paned = builder.get_object("main_paned")
data_paned = paned.get_child1()
paned.remove(data_paned)
self._player_box.bind_property("visible", self._profile_combo_box, "visible", 4)
self._player_box.bind_property("visible", self._player_event_box, "visible")
self._fav_view.bind_property("sensitive", self._player_prev_button, "sensitive")
self._fav_view.bind_property("sensitive", self._player_next_button, "sensitive")
self._fav_view.bind_property("sensitive", self._bouquets_view, "sensitive")
self._player_tool_bar.bind_property("visible", builder.get_object("fs_box"), "visible")
paned.pack1(self._player_box, True, True)
paned.pack2(data_paned, True, False)
self._player_box.connect("show", self.on_playback_show)
self._player_box.connect("playback-close", self.on_playback_close)
self._player_box.connect("playback-full-screen", self.on_playback_full_screen)
self._data_paned = builder.get_object("data_paned")
self._data_paned.bind_property("visible", self._status_bar_box, "visible")
self._data_paned.bind_property("visible", builder.get_object("fs_box"), "visible")
# Record
self._record_image = builder.get_object("record_button_image")
# Search
@@ -380,27 +370,32 @@ class Application(Gtk.Application):
self._stack_ftp_box = builder.get_object("ftp_box")
self._stack_control_box = builder.get_object("control_box")
# Header bar.
profile_box = builder.get_object("profile_combo_box")
toolbar_box = builder.get_object("toolbar_main_box")
if IS_GNOME_SESSION:
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True)
header_bar.pack_start(builder.get_object("file_header_button"))
header_bar.pack_start(Gtk.Separator(visible=True))
header_bar.pack_start(builder.get_object("profile_combo_box"))
header_bar.pack_start(builder.get_object("toolbar_main_box"))
header_bar.pack_start(profile_box)
header_bar.pack_start(toolbar_box)
header_bar.set_custom_title(builder.get_object("stack_switcher"))
self._player_box.bind_property("visible", builder.get_object("main_popover_menu_box"), "visible", 4)
self._player_box.bind_property("visible", builder.get_object("close_player_menu_button"), "visible")
self._main_window.set_titlebar(header_bar)
else:
tool_bar = Gtk.Box(visible=True, spacing=6, margin=6, valign=Gtk.Align.CENTER)
tool_bar.add(builder.get_object("profile_combo_box"))
tool_bar.add(builder.get_object("toolbar_main_box"))
tool_bar.add(profile_box)
tool_bar.add(toolbar_box)
tool_bar.set_center_widget(builder.get_object("stack_switcher"))
main_header_box = Gtk.Box(visible=True, spacing=6)
main_header_box.get_style_context().add_class(Gtk.STYLE_CLASS_PRIMARY_TOOLBAR)
main_header_box.pack_start(tool_bar, True, True, 0)
main_box = builder.get_object("main_window_box")
main_box.add(main_header_box)
main_box.reorder_child(main_header_box, 0)
self._player_tool_bar.bind_property("visible", main_header_box, "visible")
self._data_paned.bind_property("visible", main_header_box, "visible")
self._player_box.bind_property("visible", profile_box, "visible", 4)
self._player_box.bind_property("visible", toolbar_box, "visible", 4)
# Style
style_provider = Gtk.CssProvider()
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
@@ -499,11 +494,17 @@ class Application(Gtk.Application):
sa = self.set_state_action("show_control", self.on_page_show, self._settings.get("show_control", True))
sa.connect("change-state", lambda a, v: self._stack_control_box.set_visible(v))
self.bind_property("is-enigma", sa, "enabled")
# Menu bar.
# Menu bar and playback.
self.set_action("on_playback_close", self._player_box.on_close)
if not IS_GNOME_SESSION:
# We are working with the "hidden-when" submenu attribute. See 'app_menu_.ui' file.
hide_bar_action = Gio.SimpleAction.new("hide_menu_bar", None)
self._player_box.bind_property("visible", hide_bar_action, "enabled", 4)
self.add_action(hide_bar_action)
hide_media_bar = Gio.SimpleAction.new("hide_media_bar", None)
hide_media_bar.set_enabled(False)
self._player_box.bind_property("visible", hide_media_bar, "enabled")
self.add_action(hide_media_bar)
def set_action(self, name, fun, enabled=True):
ac = Gio.SimpleAction.new(name, None)
@@ -546,9 +547,6 @@ class Application(Gtk.Application):
"last_bouquet": self._current_bq_name})
self._settings.save() # storing current settings
if self._player:
self._player.release()
if self._http_api:
self._http_api.close()
@@ -714,6 +712,11 @@ class Application(Gtk.Application):
else:
GLib.idle_add(self.quit)
def on_main_window_state(self, window, event):
if event.new_window_state & Gdk.WindowState.FULLSCREEN or event.new_window_state & Gdk.WindowState.MAXIMIZED:
# Saving the current size of the application window.
self._settings.add("window_size", self._main_window.get_size())
@run_idle
def on_about_app(self, action, value=None):
show_dialog(DialogType.ABOUT, self._main_window)
@@ -752,6 +755,8 @@ class Application(Gtk.Application):
def on_recordings_realize(self, box):
self._recordings_box = RecordingsBox(self, self._http_api, self._settings)
box.pack_start(self._recordings_box, True, True, 0)
self._player_box.connect("play", self._recordings_box.on_playback)
self._player_box.connect("playback-close", self._recordings_box.on_playback_close)
def on_ftp_realize(self, box):
self._ftp_client = FtpClientBox(self, self._settings)
@@ -2237,9 +2242,9 @@ class Application(Gtk.Application):
view.do_unselect_all(view)
elif ctrl and model_name == self.FAV_MODEL_NAME:
if key is KeyboardKey.P:
self.on_play_stream()
self.emit("fav-clicked", FavClickMode.STREAM)
if key is KeyboardKey.W:
self.on_zap(self.on_watch)
self.emit("fav-clicked", FavClickMode.ZAP_PLAY)
if key is KeyboardKey.Z:
self.on_zap()
elif key is KeyboardKey.CTRL_L or key is KeyboardKey.CTRL_R:
@@ -2347,16 +2352,10 @@ class Application(Gtk.Application):
if self._fav_click_mode is FavClickMode.DISABLED:
return
self._fav_view.set_sensitive(False)
if self._fav_click_mode is FavClickMode.STREAM:
self.on_play_stream()
elif self._fav_click_mode is FavClickMode.ZAP_PLAY:
self.on_zap(self.on_watch)
elif self._fav_click_mode is FavClickMode.ZAP:
if self._fav_click_mode is FavClickMode.ZAP:
self.on_zap()
elif self._fav_click_mode is FavClickMode.PLAY:
self.on_stream()
else:
self.emit("fav-clicked", self._fav_click_mode)
else:
return self.on_view_popup_menu(menu, event)
@@ -2597,237 +2596,31 @@ class Application(Gtk.Application):
""" Shows backup tool dialog """
BackupDialog(self._main_window, self._settings, self.open_data).show()
# ***************** Player ********************* #
# ************************* Streams ***************************** #
def on_play_stream(self, item=None):
self.on_player_play()
self.emit("fav-clicked", FavClickMode.STREAM)
def on_player_play(self, item=None):
path, column = self._fav_view.get_cursor()
if path:
row = self._fav_model[path][:]
if row[Column.FAV_TYPE] != BqServiceType.IPTV.name:
self.show_error_message("Not allowed in this context!")
self.set_playback_elms_active()
return
def on_play_current(self, item=None):
""" starts playback of the current channel. """
self.emit("play-current", None)
url = get_iptv_url(row, self._s_type)
self.update_player_buttons()
if not url:
self.show_error_message("No reference is present!")
self.set_playback_elms_active()
return
self.play(url)
def play(self, url):
mode = self._settings.play_streams_mode
if mode is PlayStreamsMode.M3U:
self.save_stream_to_m3u(url)
return
if self._player and self._player.get_play_mode() is not mode:
self.show_error_message("Play mode has been changed!\nRestart the program to apply the settings.")
self.set_playback_elms_active()
return
if mode is PlayStreamsMode.WINDOW:
try:
if not self._player:
self._current_mrl = url
self.show_playback_window()
elif self._playback_window:
title = self.get_playback_title()
self._playback_window.set_title(title)
self._playback_window.show()
GLib.idle_add(self._player.play, url)
else:
self.show_error_message("Init player error!")
finally:
self.set_playback_elms_active()
else:
if not self._player:
self._current_mrl = url
else:
if not self._player_box.get_visible():
self.set_player_area_size(self._player_box)
GLib.idle_add(self._player.play, url)
self._player_box.set_visible(True)
self._stack.set_visible_child(self._player_box)
def on_player_stop(self, item=None):
if self._player:
self._fav_view.set_sensitive(True)
self._player.stop()
def on_player_previous(self, item):
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, -1):
self.set_player_action()
def on_player_next(self, item):
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, 1):
self.set_player_action()
@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_stream()
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 on_player_rewind(self, scale, scroll_type, value):
self._player.set_time(int(value))
def update_player_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)
def on_player_close(self, window=None, event=None):
if self._player:
GLib.idle_add(self._player.stop)
self.set_playback_elms_active()
if self._playback_window:
self._settings.add("playback_window_size", self._playback_window.get_size())
self._playback_window.hide()
else:
GLib.idle_add(self._player_box.set_visible, False, priority=GLib.PRIORITY_LOW)
return True
@lru_cache(maxsize=1)
def on_player_duration_changed(self, duration):
self._player_scale.set_value(0)
self._player_scale.get_adjustment().set_upper(duration)
GLib.idle_add(self._player_rewind_box.set_visible, duration > 0, priority=GLib.PRIORITY_LOW)
GLib.idle_add(self._player_current_time_label.set_text, "0", priority=GLib.PRIORITY_LOW)
GLib.idle_add(self._player_full_time_label.set_text, self.get_time_str(duration),
priority=GLib.PRIORITY_LOW)
def on_player_time_changed(self, t):
if not self._full_screen and self._player_rewind_box.get_visible():
GLib.idle_add(self._player_current_time_label.set_text, self.get_time_str(t),
priority=GLib.PRIORITY_LOW)
@run_with_delay(2)
def on_player_error(self):
self.set_playback_elms_active()
self.show_error_message("Can't Playback!")
@run_idle
def set_playback_elms_active(self):
self._fav_view.set_sensitive(True)
self._fav_view.do_grab_focus(self._fav_view)
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 on_player_box_realize(self, widget):
if not self._player:
try:
self._player = Player.make(name=self._settings.stream_lib,
mode=self._settings.play_streams_mode,
widget=widget,
buf_cb=self.on_player_duration_changed,
position_cb=self.on_player_time_changed,
error_cb=self.on_player_error,
playing_cb=self.set_playback_elms_active)
except (ImportError, NameError) as e:
self.show_error_message(str(e))
return True
else:
self._main_window.connect("key-press-event", self.on_player_key_press)
self._player.play(self._current_mrl)
finally:
if self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
self.set_player_area_size(widget)
def on_player_box_visibility(self, box):
visible = box.get_visible()
self._fav_paned.set_orientation(Gtk.Orientation.VERTICAL if visible else Gtk.Orientation.HORIZONTAL)
@run_idle
def set_player_area_size(self, widget):
w, h = self._main_window.get_size()
widget.set_size_request(w * 0.6, -1)
def on_player_press(self, area, event):
if event.button == Gdk.BUTTON_PRIMARY:
if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
self.on_full_screen()
def on_player_key_press(self, widget, event):
if self._player and self._player_event_box.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_full_screen(self, item=None):
self._full_screen = not self._full_screen
if self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
self.update_state_on_full_screen(not self._full_screen)
self._main_window.fullscreen() if self._full_screen else self._main_window.unfullscreen()
elif self._playback_window:
self._player_tool_bar.set_visible(not self._full_screen)
self._playback_window.fullscreen() if self._full_screen else self._playback_window.unfullscreen()
def update_state_on_full_screen(self, visible):
self._player_tool_bar.set_visible(visible)
self._fav_paned.set_visible(visible)
self._status_bar_box.set_visible(visible and not self._app_info_box.get_visible())
def on_playback_full_screen(self, box, state):
self._data_paned.set_visible(state)
self._main_window.unfullscreen() if state else self._main_window.fullscreen()
if not IS_GNOME_SESSION:
self._main_window.set_show_menubar(visible)
self._main_window.set_show_menubar(state)
def on_main_window_state(self, window, event):
if event.new_window_state & Gdk.WindowState.FULLSCREEN or event.new_window_state & Gdk.WindowState.MAXIMIZED:
# Saving the current size of the application window.
self._settings.add("window_size", self._main_window.get_size())
def on_playback_show(self, box):
if self._page is not Page.RECORDINGS and self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
self._stack.set_visible(False)
self._fav_paned.set_orientation(Gtk.Orientation.VERTICAL)
@run_idle
def show_playback_window(self):
width, height = 480, 240
size = self._settings.get("playback_window_size")
if size:
width, height = size
self._playback_window = Gtk.Window(title=self.get_playback_title(),
window_position=Gtk.WindowPosition.CENTER,
gravity=Gdk.Gravity.CENTER,
icon_name="demon-editor")
self._playback_window.resize(width, height)
self._playback_window.connect("delete-event", self.on_player_close)
self._playback_window.connect("key-press-event", self.on_player_key_press)
box = Gtk.HBox(visible=True, orientation="vertical")
self._player_event_box.reparent(box)
self._playback_window.bind_property("visible", self._player_event_box, "visible")
if not self._settings.is_darwin:
self._player_prev_button.set_visible(False)
self._player_next_button.set_visible(False)
self._player_box.remove(self._player_tool_bar)
box.pack_end(self._player_tool_bar, False, False, 0)
self._playback_window.add(box)
self._playback_window.set_application(self)
self._playback_window.show()
def get_playback_title(self):
path, column = self._fav_view.get_cursor()
if path:
return "DemonEditor [{}]".format(self._fav_model[path][:][Column.FAV_SERVICE])
return "DemonEditor [Playback]"
# ************************* Record ***************************** #
def on_playback_close(self, box, state):
self._fav_view.set_sensitive(True)
self._stack.set_visible(True)
self._fav_paned.set_orientation(Gtk.Orientation.HORIZONTAL)
def on_record(self, button):
if show_dialog(DialogType.QUESTION, self._main_window) == Gtk.ResponseType.CANCEL:
@@ -2845,7 +2638,7 @@ class Application(Gtk.Application):
if is_record:
self._recorder.stop()
else:
self._http_api.send(HttpAPI.Request.STREAM_CURRENT, None, self.record)
self._http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.record)
def record(self, data):
url = self.get_url_from_m3u(data)
@@ -2898,39 +2691,10 @@ class Application(Gtk.Application):
elif self._links_transmitter:
self._links_transmitter.show(enable)
def on_stream(self, item=None):
path, column = self._fav_view.get_cursor()
if not path or not self._http_api:
return
ref = self.get_service_ref(path)
if not ref:
return
if self._player and self._player.is_playing():
self._player.stop()
self._http_api.send(HttpAPI.Request.STREAM, ref, self.watch)
def on_watch(self, item=None):
""" Switch to the channel and watch in the player """
if not self._app_info_box.get_visible() and self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
self.set_player_area_size(self._player_box)
GLib.idle_add(self._player_box.set_visible, True)
GLib.idle_add(self._app_info_box.set_visible, False)
self._http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.watch)
def watch(self, data):
url = self.get_url_from_m3u(data)
if url:
GLib.timeout_add_seconds(1, self.play, url)
def get_url_from_m3u(self, data):
error_code = data.get("error_code", 0)
if error_code or self._http_status_image.get_visible():
self.show_error_message("No connection to the receiver!")
self.set_playback_elms_active()
return
m3u = data.get("m3u", None)
@@ -2938,6 +2702,10 @@ class Application(Gtk.Application):
return [s for s in m3u.split("\n") if not s.startswith("#")][0]
def save_stream_to_m3u(self, url):
if self._page not in self._fav_pages:
self.show_error_message("Not allowed in this context!")
return
path, column = self._fav_view.get_cursor()
s_name = self._fav_model.get_value(self._fav_model.get_iter(path), Column.FAV_SERVICE) if path else "stream"
@@ -2960,20 +2728,18 @@ class Application(Gtk.Application):
""" Switch(zap) the channel """
path, column = self._fav_view.get_cursor()
if not path or not self._http_api:
self.set_playback_elms_active()
return
ref = self.get_service_ref(path)
if not ref:
return
if self._player and self._player.is_playing():
self._player.stop()
self._player_box.on_stop()
# IPTV type checking
row = self._fav_model[path][:]
if row[Column.FAV_TYPE] == BqServiceType.IPTV.name and callback:
callback = self.play(get_iptv_url(row, self._s_type))
callback = self._player_box.play(get_iptv_url(row, self._s_type))
def zap(rq):
if rq and rq.get("e2state", False):
@@ -2982,7 +2748,6 @@ class Application(Gtk.Application):
callback()
else:
self.show_error_message("No connection to the receiver!")
self.set_playback_elms_active()
self._http_api.send(HttpAPI.Request.ZAP, ref, zap)
@@ -2992,7 +2757,6 @@ class Application(Gtk.Application):
if srv_type in self._marker_types:
self.show_error_message("Not allowed in this context!")
self.set_playback_elms_active()
return
srv = self._services.get(fav_id, None)
@@ -3666,6 +3430,10 @@ class Application(Gtk.Application):
def app_settings(self):
return self._settings
@property
def http_api(self):
return self._http_api
@GObject.Property(type=bool, default=True)
def is_enigma(self):
return self._is_enigma

View File

@@ -218,6 +218,9 @@ class PiconManager(Gtk.Box):
if index % factor == 0:
yield True
if not os.path.isdir(path):
return
for file in os.listdir(path):
if self._terminate:
return

157
app/ui/playback.glade Normal file
View File

@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkEventBox" id="event_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="button-press-event" handler="on_press" swapped="no"/>
<signal name="realize" handler="on_realize" swapped="no"/>
<child>
<placeholder/>
</child>
</object>
<object class="GtkToolbar" id="tool_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkToolButton" id="prev_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Previous stream in the list</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-media-previous</property>
<signal name="clicked" handler="on_previous" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="play_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Play</property>
<property name="stock_id">gtk-media-play</property>
<signal name="clicked" handler="on_play" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="stop_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Stop playback</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-media-stop</property>
<signal name="clicked" handler="on_stop" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="next_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Next stream in the list</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-media-next</property>
<signal name="clicked" handler="on_next" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolItem" id="player_item">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox" id="rewind_box">
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="current_time_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScale" id="scale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="restrict_to_fill_level">False</property>
<property name="fill_level">0</property>
<property name="draw_value">False</property>
<property name="has_origin">False</property>
<signal name="change-value" handler="on_rewind" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="full_time_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="full_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Toggle in fullscreen</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-fullscreen</property>
<signal name="clicked" handler="on_full_screen" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="close_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Close playback</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-close</property>
<signal name="clicked" handler="on_close" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
</object>
</interface>

331
app/ui/playback.py Normal file
View File

@@ -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

View File

@@ -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 """