mirror of
https://github.com/DYefremov/DemonEditor.git
synced 2025-12-30 04:19:41 +01:00
462 lines
19 KiB
Python
462 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# The MIT License (MIT)
|
|
#
|
|
# Copyright (c) 2018-2023 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, Gio
|
|
|
|
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, SettingsType, USE_HEADER_BAR
|
|
from app.tools.media import Player
|
|
from app.ui.dialogs import get_builder, get_message
|
|
from app.ui.main_helper import get_iptv_url
|
|
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, Column, Page
|
|
|
|
|
|
class PlayerBox(Gtk.Box):
|
|
|
|
def __init__(self, app, **kwargs):
|
|
super().__init__(**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,))
|
|
GObject.signal_new("pause", 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("srv-clicked", self.on_srv_clicked)
|
|
self._app.connect("iptv-clicked", self.on_iptv_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._audio_track_menu = None
|
|
self._subtitle_track_menu = None
|
|
self._play_mode = self._app.app_settings.play_streams_mode
|
|
|
|
handlers = {"on_realize": self.on_realize,
|
|
"on_press": self.on_press,
|
|
"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)
|
|
if not IS_DARWIN:
|
|
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._audio_menu_button = builder.get_object("audio_menu_button")
|
|
self._video_menu_button = builder.get_object("video_menu_button")
|
|
self._subtitle_menu_button = builder.get_object("subtitle_menu_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
|
|
|
|
if len(self._fav_view.get_model()) == 0:
|
|
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_srv_clicked(self, app, mode):
|
|
if not self._app.http_api:
|
|
return
|
|
|
|
view = self._app.services_view
|
|
path, column = view.get_cursor()
|
|
if path:
|
|
srv = self._app.current_services.get(view.get_model()[path][Column.SRV_FAV_ID], None)
|
|
if not srv or not srv.picon_id:
|
|
return
|
|
|
|
ref = self._app.get_service_ref_data(srv)
|
|
s_type = self._app.app_settings.setting_type
|
|
error_msg = "No connection to the receiver!"
|
|
if s_type is SettingsType.ENIGMA_2:
|
|
def zap(rq):
|
|
self.on_watch() if rq and rq.get("e2state", False) else self.on_error(None, error_msg)
|
|
|
|
self._app.http_api.send(HttpAPI.Request.ZAP, ref, zap)
|
|
elif self._s_type is SettingsType.NEUTRINO_MP:
|
|
def zap(rq):
|
|
self.on_watch() if rq and rq.get("data", None) == "ok" else self.on_error(None, error_msg)
|
|
|
|
self._app.http_api.send(HttpAPI.Request.N_ZAP, f"?{ref}", zap)
|
|
|
|
def on_iptv_clicked(self, app, mode):
|
|
if not self._app.http_api:
|
|
return
|
|
|
|
view = self._app.iptv_services_view
|
|
path, column = view.get_cursor()
|
|
if path:
|
|
row = view.get_model()[path][:]
|
|
url = get_iptv_url(row, self._app.app_settings.setting_type, Column.IPTV_FAV_ID)
|
|
self.play(url, row[Column.IPTV_SERVICE]) if url else self.on_error(None, "No reference is present!")
|
|
|
|
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)
|
|
except (ImportError, NameError) as e:
|
|
self._app.show_error_message(str(e))
|
|
return True
|
|
else:
|
|
self.init_playback_elements()
|
|
self.emit("play", self._current_mrl)
|
|
finally:
|
|
if settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
|
|
self.set_player_area_size(box)
|
|
|
|
def init_playback_elements(self):
|
|
self._player.connect("error", self.on_error)
|
|
self._player.connect("played", self.on_played)
|
|
self._player.connect("audio-track", self.on_audio_track_changed)
|
|
self._player.connect("subtitle-track", self.on_subtitle_track_changed)
|
|
self._app.app_window.connect("key-press-event", self.on_key_press)
|
|
|
|
builder = get_builder(UI_RESOURCES_PATH + "app_menu.ui")
|
|
self._audio_track_menu = builder.get_object("audio_track_menu")
|
|
self._subtitle_track_menu = builder.get_object("subtitle_track_menu")
|
|
audio_menu = builder.get_object("audio_menu")
|
|
video_menu = builder.get_object("video_menu")
|
|
subtitle_menu = builder.get_object("subtitle_menu")
|
|
|
|
if not USE_HEADER_BAR:
|
|
menu_bar = self._app.get_menubar()
|
|
menu_bar.insert_section(1, None, audio_menu)
|
|
menu_bar.insert_section(2, None, video_menu)
|
|
menu_bar.insert_section(3, None, subtitle_menu)
|
|
|
|
if not IS_DARWIN:
|
|
self._player.connect("position", self.on_time_changed)
|
|
self._audio_menu_button.set_menu_model(self._audio_track_menu)
|
|
self._video_menu_button.set_menu_model(builder.get_object("aspect_ratio_menu"))
|
|
self._subtitle_menu_button.set_menu_model(self._subtitle_track_menu)
|
|
# Actions.
|
|
self._app.set_action("on_play", self.on_play)
|
|
self._app.set_action("on_stop", self.on_stop)
|
|
audio_track_action = Gio.SimpleAction.new_stateful("on_set_audio_track", GLib.VariantType.new("i"),
|
|
GLib.Variant("i", 0))
|
|
audio_track_action.connect("activate", self.on_set_audio_track)
|
|
self._app.add_action(audio_track_action)
|
|
aspect_action = Gio.SimpleAction.new_stateful("on_set_aspect_ratio", GLib.VariantType.new("s"),
|
|
GLib.Variant("s", ""))
|
|
aspect_action.connect("activate", self.on_set_aspect_ratio)
|
|
self._app.add_action(aspect_action)
|
|
subtitle_track_action = Gio.SimpleAction.new_stateful("on_set_subtitle_track", GLib.VariantType.new("i"),
|
|
GLib.Variant("i", -1))
|
|
subtitle_track_action.connect("activate", self.on_set_subtitle_track)
|
|
self._app.add_action(subtitle_track_action)
|
|
|
|
def on_play(self, action=None, value=None):
|
|
self.emit("play", None)
|
|
|
|
def on_stop(self, action=None, value=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:
|
|
if not IS_DARWIN:
|
|
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
|
|
|
|
@run_with_delay(1)
|
|
def on_audio_track_changed(self, player, tracks):
|
|
self._audio_track_menu.remove_all()
|
|
for t in tracks:
|
|
item = Gio.MenuItem.new(t[1], None)
|
|
item.set_action_and_target_value("app.on_set_audio_track", GLib.Variant("i", t[0]))
|
|
self._audio_track_menu.append_item(item)
|
|
|
|
@run_with_delay(1)
|
|
def on_subtitle_track_changed(self, player, tracks):
|
|
self._subtitle_track_menu.remove_all()
|
|
for t in tracks:
|
|
item = Gio.MenuItem.new(t[1], None)
|
|
item.set_action_and_target_value("app.on_set_subtitle_track", GLib.Variant("i", t[0]))
|
|
self._subtitle_track_menu.append_item(item)
|
|
|
|
def on_set_audio_track(self, action, value):
|
|
action.set_state(value)
|
|
self._player.set_audio_track(value.get_int32())
|
|
|
|
def on_set_aspect_ratio(self, action, value):
|
|
action.set_state(value)
|
|
self._player.set_aspect_ratio(value.get_string())
|
|
|
|
def on_set_subtitle_track(self, action, value):
|
|
action.set_state(value)
|
|
self._player.set_subtitle_track(value.get_int32())
|
|
|
|
def on_press(self, area, event):
|
|
if event.button == Gdk.BUTTON_PRIMARY:
|
|
if event.type == Gdk.EventType.BUTTON_PRESS:
|
|
self.emit("pause", None)
|
|
elif 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):
|
|
click_mode = self._app.app_settings.fav_click_mode
|
|
self._fav_view.set_sensitive(False)
|
|
if click_mode is FavClickMode.PLAY:
|
|
self.on_play_service()
|
|
elif click_mode is FavClickMode.ZAP_PLAY:
|
|
self._app.on_zap(self.on_watch)
|
|
elif 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 f"{str(h) + ':' if h else ''}{m:02d}:{s:02d}"
|
|
|
|
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, title=None):
|
|
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()
|
|
self._playback_window.set_title(title or self.get_playback_title())
|
|
else:
|
|
self._playback_window = Gtk.Window(title=title or 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):
|
|
if self._app.page is not Page.RECORDINGS:
|
|
path, column = self._fav_view.get_cursor()
|
|
if path:
|
|
return f"DemonEditor [{self._app.fav_view.get_model()[path][:][Column.FAV_SERVICE]}]"
|
|
else:
|
|
return f"DemonEditor [{get_message('Recordings')}]"
|
|
return f"DemonEditor [{get_message('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)
|
|
|
|
s_type = self._app.app_settings.setting_type
|
|
req = HttpAPI.Request.STREAM if s_type is SettingsType.ENIGMA_2 else HttpAPI.Request.N_STREAM
|
|
self._app.http_api.send(req, ref, self.watch)
|
|
|
|
def on_watch(self, item=None):
|
|
""" Switch to the channel and watch in the player. """
|
|
s_type = self._app.app_settings.setting_type
|
|
if s_type is SettingsType.ENIGMA_2:
|
|
self._app.http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.watch)
|
|
elif s_type is SettingsType.NEUTRINO_MP:
|
|
self._app.http_api.send(HttpAPI.Request.N_ZAP, "",
|
|
lambda rf: self._app.http_api.send(HttpAPI.Request.N_STREAM, rf.get("data", ""),
|
|
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, title=None):
|
|
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(title)
|
|
|
|
if self._player:
|
|
self.emit("play", url)
|
|
else:
|
|
self._current_mrl = url
|
|
|
|
@run_idle
|
|
def on_played(self, player, duration):
|
|
self._fav_view.set_sensitive(True)
|
|
if not IS_DARWIN:
|
|
self.on_duration_changed(duration)
|
|
|
|
@run_idle
|
|
def on_error(self, player, msg):
|
|
self._app.show_error_message(msg)
|
|
self._fav_view.set_sensitive(True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pass
|