import os
import sys
import requests
import time
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QPushButton, QLabel, QListWidget,
QSlider, QMessageBox, QTextEdit, QListWidgetItem,
QScrollArea, QFrame, QMenuBar, QMenu, QAction,
QSizePolicy, QToolBar, QStatusBar, QInputDialog, QLineEdit, QActionGroup)
from PyQt5.QtCore import Qt, QTimer, QSize, QPoint, QEvent, pyqtSignal
from PyQt5.QtGui import QColor, QIcon, QFont, QPalette, QCloseEvent
import vlc
import subprocess
import ctypes
class EPGDisplay(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setLayout(QVBoxLayout())
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(True)
self.layout().addWidget(self.scroll)
self.content = QWidget()
self.content.setLayout(QHBoxLayout())
self.content.layout().setContentsMargins(0, 0, 0, 0)
self.content.layout().setSpacing(0)
self.scroll.setWidget(self.content)
self.setMinimumHeight(150)
self.setMaximumHeight(200)
def update_epg(self, programs):
# Clear existing widgets
for i in reversed(range(self.content.layout().count())):
self.content.layout().itemAt(i).widget().deleteLater()
# Add program blocks
for program in programs:
frame = QFrame()
frame.setFrameShape(QFrame.Box)
frame.setLineWidth(1)
frame.setStyleSheet("background-color: #f0f0f0;")
layout = QVBoxLayout()
layout.setContentsMargins(5, 5, 5, 5)
time_label = QLabel(program['start_time'].strftime("%H:%M"))
time_label.setStyleSheet("font-weight: bold;")
title_label = QLabel(program['title'])
title_label.setWordWrap(True)
layout.addWidget(time_label)
layout.addWidget(title_label)
frame.setLayout(layout)
# Calculate width based on duration (1 hour = 200px)
duration_hours = (program['end_time'] - program['start_time']).total_seconds() / 3600
frame.setFixedWidth(int(duration_hours * 200))
self.content.layout().addWidget(frame)
class PlaylistWindow(QMainWindow):
"""Window to display playlist and EPG information"""
channel_selected = pyqtSignal(str, str) # channel_name, url
update_requested = pyqtSignal()
visibilityChanged = pyqtSignal(bool)
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("IPTV Player - Playlist")
self.setAttribute(Qt.WA_DeleteOnClose)
# Initialize UI
self.init_ui()
# EPG data storage
self.epg_data = {}
self.current_epg_url = ""
def init_ui(self):
"""Initialize the playlist window UI"""
main_widget = QWidget(self)
layout = QVBoxLayout()
main_widget.setLayout(layout)
# Channel list
self.channel_list = QListWidget()
self.channel_list.setStyleSheet("""
QListWidget {
font-size: 12px;
}
QListWidget::item {
padding: 5px;
}
QListWidget::item:selected {
background-color: #0078d7;
color: white;
}
""")
self.channel_list.itemDoubleClicked.connect(self.on_channel_selected)
self.channel_list.currentItemChanged.connect(self.update_epg_display)
layout.addWidget(self.channel_list, 3)
# EPG display
self.epg_display = EPGDisplay()
layout.addWidget(self.epg_display, 1)
# Controls
control_layout = QHBoxLayout()
self.play_button = QPushButton("Play")
self.play_button.clicked.connect(self.on_channel_selected)
self.reload_button = QPushButton("Update")
self.reload_button.clicked.connect(self.update_requested.emit)
control_layout.addWidget(self.play_button)
control_layout.addWidget(self.reload_button)
layout.addLayout(control_layout)
self.setCentralWidget(main_widget)
def on_channel_selected(self):
"""Emit signal when channel is selected"""
selected = self.channel_list.currentItem()
if selected:
self.channel_selected.emit(selected.text(), selected.data(Qt.UserRole))
def load_playlist(self, playlist_path):
"""Load the playlist with channel names and icons"""
if os.path.exists(playlist_path):
try:
with open(playlist_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
self.channel_list.clear()
current_channel = None
current_id = None
for line in lines:
line = line.strip()
if line.startswith("#EXTINF:"):
parts = line.split(',')
if len(parts) > 1:
current_channel = parts[1].strip()
if 'tvg-id="' in line:
current_id = line.split('tvg-id="')[1].split('"')[0]
elif line and not line.startswith("#"):
if current_channel:
item = QListWidgetItem(current_channel)
item.setData(Qt.UserRole, line) # Store URL
item.setData(Qt.UserRole + 1, current_id) # Store channel ID
self.channel_list.addItem(item)
current_channel = None
current_id = None
if self.channel_list.count() > 0:
self.channel_list.setCurrentRow(0)
except Exception as e:
print(f"Error loading playlist: {str(e)}")
def load_epg(self, epg_url):
"""Load EPG data from XMLTV file"""
try:
self.current_epg_url = epg_url
response = requests.get(epg_url, timeout=10)
root = ET.fromstring(response.content)
self.epg_data = {}
for programme in root.findall('programme'):
channel = programme.get('channel')
start_time = datetime.strptime(programme.get('start'), '%Y%m%d%H%M%S %z')
end_time = datetime.strptime(programme.get('stop'), '%Y%m%d%H%M%S %z')
title = programme.find('title').text if programme.find('title') is not None else "No Title"
if channel not in self.epg_data:
self.epg_data[channel] = []
self.epg_data[channel].append({
'start_time': start_time,
'end_time': end_time,
'title': title
})
except Exception as e:
print(f"Error loading EPG: {str(e)}")
def update_epg_display(self, current, previous):
"""Update EPG display when channel selection changes"""
if current is None:
return
channel_id = current.data(Qt.UserRole + 1) # Channel ID for EPG lookup
now = datetime.now().astimezone()
end_time = now + timedelta(hours=2)
programs = []
if channel_id in self.epg_data:
for program in self.epg_data[channel_id]:
if program['end_time'] > now and program['start_time'] < end_time:
programs.append(program)
self.epg_display.update_epg(programs)
def showEvent(self, event):
"""Emit signal when window is shown"""
self.visibilityChanged.emit(True)
super().showEvent(event)
def hideEvent(self, event):
"""Emit signal when window is hidden"""
self.visibilityChanged.emit(False)
super().hideEvent(event)
def closeEvent(self, event):
"""Handle window close by hiding instead of deleting"""
self.hide()
if self.parent():
self.parent().playlist_visibility_changed(False)
event.ignore()
# Clean up any resources
self.channel_list.clear()
if hasattr(self, 'epg_data'):
del self.epg_data
class MainPlayer(QMainWindow):
"""Main player window with video playback and controls"""
def __init__(self):
super().__init__()
self.setWindowTitle("IPTV Player")
self.setAttribute(Qt.WA_DeleteOnClose)
# Initialize variables
self.php_port = 8888
self.php_dir = os.path.join(os.path.dirname(__file__), "php_files")
os.makedirs(self.php_dir, exist_ok=True)
self.php_process = None
self.playlist_window = None
# Decoder modes
self.decoder_modes = {
"HW+": ["--avcodec-hw=any", "--avcodec-fast", "--avcodec-threads=0"],
"HW": ["--avcodec-hw=dxva2", "--avcodec-fast"],
"SW": ["--avcodec-hw=none", "--no-accelerated-video"]
}
self.current_decoder = "HW+"
# Setup embedded VLC
self.setup_embedded_vlc()
# Create playlist window early
self.create_playlist_window()
# Initialize UI
self.init_ui()
# Start PHP server
self.start_php_server()
# Download and process files
self.download_and_process_files()
# Initialize VLC
self.init_vlc()
# Position windows
self.position_windows()
@staticmethod
def setup_embedded_vlc():
"""Setup embedded VLC paths"""
vlc_dir = os.path.join(os.path.dirname(__file__), "vlc")
if not os.path.exists(vlc_dir):
os.makedirs(vlc_dir)
# Add VLC to DLL search path (Windows)
if sys.platform == "win32":
os.environ['PATH'] = vlc_dir + os.pathsep + os.environ['PATH']
try:
ctypes.windll.kernel32.SetDllDirectoryW(vlc_dir)
except:
pass
def init_ui(self):
"""Initialize the main UI"""
main_widget = QWidget(self)
layout = QVBoxLayout()
main_widget.setLayout(layout)
# Video frame
self.video_frame = QFrame()
self.video_frame.setFrameShape(QFrame.NoFrame)
self.video_frame.setStyleSheet("background-color: black;")
self.video_frame.mouseDoubleClickEvent = self.toggle_fullscreen
self.video_frame.mousePressEvent = self.show_stream_info
layout.addWidget(self.video_frame, 1)
# Info labels
self.channel_label = QLabel(self.video_frame)
self.channel_label.setStyleSheet("color: white; font-size: 14px; background-color: rgba(0, 0, 0, 150);")
self.channel_label.move(10, 10)
self.channel_label.hide()
self.clock_label = QLabel(self.video_frame)
self.clock_label.setStyleSheet("color: white; font-size: 14px; background-color: rgba(0, 0, 0, 150);")
self.clock_label.hide()
self.resolution_label = QLabel(self.video_frame)
self.resolution_label.setStyleSheet("color: white; font-size: 14px; background-color: rgba(0, 0, 0, 150);")
self.resolution_label.hide()
# Debug console
self.debug_console = QTextEdit()
self.debug_console.setReadOnly(True)
self.debug_console.setMaximumHeight(100)
layout.addWidget(self.debug_console)
# Controls
self.create_toolbar()
layout.addWidget(self.toolbar)
# Volume control
self.volume_slider = QSlider(Qt.Horizontal)
self.volume_slider.setRange(0, 100)
self.volume_slider.setValue(50)
self.volume_slider.valueChanged.connect(self.set_volume)
self.volume_slider.setFixedWidth(150)
# Add volume control to a separate toolbar
volume_toolbar = QToolBar()
volume_toolbar.addWidget(QLabel("Volume:"))
volume_toolbar.addWidget(self.volume_slider)
layout.addWidget(volume_toolbar)
self.setCentralWidget(main_widget)
# Create menu
self.create_menu()
# Clock timer
self.clock_timer = QTimer(self)
self.clock_timer.timeout.connect(self.update_clock)
self.clock_timer.start(1000)
# Stream info timer
self.stream_info_timer = QTimer()
self.stream_info_timer.timeout.connect(self.update_stream_info)
# Info display timer
self.info_display_time = 10 # seconds
self.show_info = False
self.info_timer = QTimer()
self.info_timer.timeout.connect(self.hide_info)
# Initialize state
self.is_fullscreen = False
self.current_channel = ""
self.log_message("MainPlayer initialized")
def create_playlist_window(self):
"""Create the playlist window and connect signals"""
if not self.playlist_window:
self.playlist_window = PlaylistWindow(self)
self.playlist_window.channel_selected.connect(self.play_stream)
self.playlist_window.update_requested.connect(self.update_playlist)
self.playlist_window.visibilityChanged.connect(self.playlist_visibility_changed)
def position_windows(self):
"""Position windows at startup"""
screen = QApplication.primaryScreen().availableGeometry()
# Position playlist window (left half of screen)
self.playlist_window.move(screen.left(), screen.top())
self.playlist_window.resize(int(screen.width() * 0.5), screen.height())
self.playlist_window.show()
# Position main player window (top right quadrant)
self.move(screen.left() + int(screen.width() * 0.5), screen.top())
self.resize(int(screen.width() * 0.5), int(screen.height() * 0.5))
self.log_message("Windows positioned")
def playlist_visibility_changed(self, visible):
"""Update toggle button text when playlist visibility changes"""
if visible:
self.show_playlist_action.setText("Hide Playlist")
else:
self.show_playlist_action.setText("Show Playlist")
def create_toolbar(self):
self.toolbar = QToolBar()
self.toolbar.setIconSize(QSize(24, 24))
# Play/pause button
self.play_action = QAction(QIcon.fromTheme("media-playback-start"), "Play", self)
self.play_action.triggered.connect(self.toggle_play_pause)
self.toolbar.addAction(self.play_action)
# Stop button
self.stop_action = QAction(QIcon.fromTheme("media-playback-stop"), "Stop", self)
self.stop_action.triggered.connect(self.stop)
self.toolbar.addAction(self.stop_action)
# Previous channel
self.prev_action = QAction(QIcon.fromTheme("media-skip-backward"), "Previous", self)
self.prev_action.triggered.connect(self.prev_channel)
self.toolbar.addAction(self.prev_action)
# Next channel
self.next_action = QAction(QIcon.fromTheme("media-skip-forward"), "Next", self)
self.next_action.triggered.connect(self.next_channel)
self.toolbar.addAction(self.next_action)
# Fullscreen
self.fullscreen_action = QAction(QIcon.fromTheme("view-fullscreen"), "Fullscreen", self)
self.fullscreen_action.triggered.connect(self.toggle_fullscreen)
self.toolbar.addAction(self.fullscreen_action)
# Show playlist
self.show_playlist_action = QAction(QIcon.fromTheme("view-list-details"), "Playlist", self)
self.show_playlist_action.triggered.connect(self.toggle_playlist)
self.toolbar.addAction(self.show_playlist_action)
def create_menu(self):
menubar = self.menuBar()
# File menu
file_menu = menubar.addMenu("File")
self.update_action = QAction("Update Playlist", self)
self.update_action.triggered.connect(self.update_playlist)
file_menu.addAction(self.update_action)
self.login_action = QAction("Change Login", self)
self.login_action.triggered.connect(self.change_login)
file_menu.addAction(self.login_action)
file_menu.addSeparator()
exit_action = QAction("Exit", self)
exit_action.setShortcut("Ctrl+Q")
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# View menu
view_menu = menubar.addMenu("View")
self.always_on_top_action = QAction("Always on Top", self, checkable=True)
self.always_on_top_action.setChecked(True)
self.always_on_top_action.triggered.connect(self.toggle_always_on_top)
view_menu.addAction(self.always_on_top_action)
self.show_resolution_action = QAction("Show Resolution Info", self, checkable=True)
self.show_resolution_action.setChecked(True)
self.show_resolution_action.triggered.connect(self.toggle_resolution_info)
view_menu.addAction(self.show_resolution_action)
self.show_clock_action = QAction("Show Clock", self, checkable=True)
self.show_clock_action.setChecked(True)
self.show_clock_action.triggered.connect(self.toggle_clock_info)
view_menu.addAction(self.show_clock_action)
# Decoder menu
decoder_menu = menubar.addMenu("Decoder")
self.hw_plus_action = QAction("Hardware+ (Recommended)", self, checkable=True)
self.hw_action = QAction("Hardware Only", self, checkable=True)
self.sw_action = QAction("Software Only", self, checkable=True)
decoder_group = QActionGroup(self)
decoder_group.addAction(self.hw_plus_action)
decoder_group.addAction(self.hw_action)
decoder_group.addAction(self.sw_action)
self.hw_plus_action.triggered.connect(lambda: self.set_decoder_mode("HW+"))
self.hw_action.triggered.connect(lambda: self.set_decoder_mode("HW"))
self.sw_action.triggered.connect(lambda: self.set_decoder_mode("SW"))
decoder_menu.addAction(self.hw_plus_action)
decoder_menu.addAction(self.hw_action)
decoder_menu.addAction(self.sw_action)
self.hw_plus_action.setChecked(True)
# Help menu
help_menu = menubar.addMenu("Help")
about_action = QAction("About", self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
def set_decoder_mode(self, mode):
"""Set the decoder mode (HW+, HW, SW)"""
self.current_decoder = mode
self.log_message(f"Decoder mode set to: {mode}")
# Reinitialize VLC with new settings if already playing
if hasattr(self, 'media_player') and self.media_player.is_playing():
current_url = self.media_player.get_media().get_mrl()
self.stop()
QTimer.singleShot(100, lambda: self.play_stream(self.current_channel, current_url))
def init_vlc(self):
"""Initialize VLC with current decoder settings"""
try:
args = self.decoder_modes[self.current_decoder]
# Platform-specific additions
if sys.platform == "win32":
args.extend(["--directx-hw-yuv"])
elif sys.platform == "linux":
args.extend(["--vout=opengl"])
self.instance = vlc.Instance(args)
self.media_player = self.instance.media_player_new()
if sys.platform == "win32":
self.media_player.set_hwnd(self.video_frame.winId())
else:
self.media_player.set_xwindow(self.video_frame.winId())
self.stream_info_timer.start(1000)
self.log_message(f"VLC initialized with {self.current_decoder} mode")
except Exception as e:
self.log_message(f"VLC initialization failed: {str(e)}", error=True)
# Fallback to software mode
if self.current_decoder != "SW":
self.current_decoder = "SW"
self.init_vlc()
def start_php_server(self):
"""Start the built-in PHP server"""
try:
os.chdir(self.php_dir)
self.php_process = subprocess.Popen(
["php", "-S", f"localhost:{self.php_port}"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
)
self.log_message(f"PHP server started on port {self.php_port}")
except Exception as e:
self.log_message(f"Failed to start PHP server: {str(e)}", error=True)
QMessageBox.critical(self, "PHP Server Error",
"Could not start PHP server.\n"
"Please check if PHP is properly installed.")
def download_and_process_files(self):
"""Download and process all required files"""
files = {
"myportal.php": "https://iptv.nywebforum.com/myportal.php?download=true",
"mac_list.php": "https://iptv.nywebforum.com/mac_list.php?download=true",
"playlist.m3u": "https://iptv.nywebforum.com/playlist.m3u"
}
for filename, url in files.items():
try:
temp_path = os.path.join(self.php_dir, f"temp_{filename}")
response = requests.get(url, timeout=10)
with open(temp_path, 'wb') as f:
f.write(response.content)
if filename == "playlist.m3u":
with open(temp_path, 'r', encoding='utf-8') as f:
content = f.read()
if "x-tvg-url=" in content:
epg_url = content.split('x-tvg-url="')[1].split('"')[0]
self.load_epg(epg_url)
final_path = os.path.join(self.php_dir, filename)
if os.path.exists(final_path):
os.remove(final_path)
os.rename(temp_path, final_path)
self.log_message(f"Downloaded {filename}")
# Load playlist after download
if filename == "playlist.m3u":
self.load_playlist(final_path)
except Exception as e:
self.log_message(f"Error processing {filename}: {str(e)}", error=True)
if os.path.exists(temp_path):
os.remove(temp_path)
def process_url_replacements(self, playlist_path):
"""Replace all server URLs with localhost URLs"""
try:
if os.path.exists(playlist_path):
with open(playlist_path, 'r', encoding='utf-8') as f:
content = f.read()
content = content.replace(
"https://iptv.nywebforum.com",
f"http://localhost:{self.php_port}"
)
with open(playlist_path, 'w', encoding='utf-8') as f:
f.write(content)
self.log_message("URL replacements completed")
except Exception as e:
self.log_message(f"Error during URL replacement: {str(e)}", error=True)
def load_playlist(self, playlist_path):
"""Load the playlist into the playlist window"""
self.process_url_replacements(playlist_path)
self.playlist_window.load_playlist(playlist_path)
# Play the first channel after loading
QTimer.singleShot(500, self.play_first_channel)
if not self.playlist_window.isVisible():
self.playlist_window.show()
def load_epg(self, epg_url):
"""Load EPG data"""
if self.playlist_window:
self.playlist_window.load_epg(epg_url)
def play_first_channel(self):
"""Play the first channel in the playlist"""
if (hasattr(self, 'playlist_window') and self.playlist_window and \
hasattr(self.playlist_window, 'channel_list') and \
self.playlist_window.channel_list.count()) > 0:
self.playlist_window.channel_list.setCurrentRow(0)
item = self.playlist_window.channel_list.currentItem()
if item:
channel_name = item.text()
url = item.data(Qt.UserRole)
self.play_stream(channel_name, url)
def play_stream(self, channel_name, url):
"""Play a stream URL"""
try:
self.current_channel = channel_name
self.setWindowTitle(f"IPTV Player - {channel_name}")
self.log_message(f"Attempting to play: {channel_name} ({url})")
if hasattr(self, 'media_player'):
self.media_player.stop()
media = self.instance.media_new(url)
media.add_option(':network-caching=3000')
media.add_option(':file-caching=3000')
media.add_option(':live-caching=3000')
self.media_player.set_media(media)
self.media_player.play()
self.play_action.setIcon(QIcon.fromTheme("media-playback-pause"))
self.play_action.setText("Pause")
self.log_message(f"Playing stream: {channel_name}")
except Exception as e:
self.log_message(f"Error playing stream: {str(e)}", error=True)
def toggle_play_pause(self):
"""Toggle between play and pause"""
if hasattr(self, 'media_player'):
if self.media_player.is_playing():
self.media_player.pause()
self.play_action.setIcon(QIcon.fromTheme("media-playback-start"))
self.play_action.setText("Play")
else:
self.media_player.play()
self.play_action.setIcon(QIcon.fromTheme("media-playback-pause"))
self.play_action.setText("Pause")
def stop(self):
"""Stop playback"""
if hasattr(self, 'media_player'):
self.media_player.stop()
self.play_action.setIcon(QIcon.fromTheme("media-playback-start"))
self.play_action.setText("Play")
def set_volume(self, value):
"""Set player volume"""
if hasattr(self, 'media_player'):
self.media_player.audio_set_volume(value)
def toggle_fullscreen(self, event=None):
"""Toggle borderless fullscreen mode"""
if self.is_fullscreen:
self.showNormal()
self.toolbar.show()
self.menuBar().show()
self.is_fullscreen = False
else:
self.showFullScreen()
self.toolbar.hide()
self.menuBar().hide()
self.is_fullscreen = True
def keyPressEvent(self, event):
"""Handle key presses"""
if event.key() == Qt.Key_Escape and self.is_fullscreen:
self.toggle_fullscreen()
elif event.key() == Qt.Key_Space:
self.toggle_play_pause()
elif event.key() == Qt.Key_Left:
self.prev_channel()
elif event.key() == Qt.Key_Right:
self.next_channel()
else:
super().keyPressEvent(event)
def toggle_playlist(self):
"""Toggle playlist visibility"""
if self.playlist_window.isVisible():
self.playlist_window.hide()
self.show_playlist_action.setText("Show Playlist")
else:
self.playlist_window.show()
self.show_playlist_action.setText("Hide Playlist")
def prev_channel(self):
"""Switch to previous channel"""
if self.playlist_window and hasattr(self.playlist_window, 'channel_list') and self.playlist_window.channel_list.count() > 0:
current_row = self.playlist_window.channel_list.currentRow()
if current_row > 0:
self.playlist_window.channel_list.setCurrentRow(current_row - 1)
self.playlist_window.on_channel_selected()
def next_channel(self):
"""Switch to next channel"""
if self.playlist_window and hasattr(self.playlist_window, 'channel_list') and self.playlist_window.channel_list.count() > 0:
current_row = self.playlist_window.channel_list.currentRow()
if current_row < self.playlist_window.channel_list.count() - 1:
self.playlist_window.channel_list.setCurrentRow(current_row + 1)
self.playlist_window.on_channel_selected()
def update_playlist(self):
"""Update playlist from server"""
self.download_and_process_files()
def change_login(self):
"""Change login credentials"""
username, ok1 = QInputDialog.getText(self, "Change Login", "Enter new username:")
if ok1:
password, ok2 = QInputDialog.getText(self, "Change Login", "Enter new password:", QLineEdit.Password)
if ok2:
QMessageBox.information(self, "Success", "Login credentials updated")
def toggle_always_on_top(self, state):
"""Toggle always on top"""
if state:
self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint)
else:
self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint)
self.show()
def toggle_resolution_info(self, state):
"""Toggle resolution info display"""
pass
def toggle_clock_info(self, state):
"""Toggle clock display"""
pass
def show_about(self):
"""Show about dialog"""
about_text = """IPTV Player
Copyright © 2025 Home13TV
Contact: home13tvdevice@gmail.com for subscription and technical support.
This software is provided for authorized users only.
Unauthorized distribution or use is prohibited."""
QMessageBox.about(self, "About IPTV Player", about_text)
def show_stream_info(self, event):
"""Show stream information on screen"""
if not self.show_info:
self.show_info = True
self.update_info_labels()
self.info_timer.start(self.info_display_time * 1000)
def hide_info(self):
"""Hide stream information"""
self.show_info = False
self.channel_label.hide()
self.clock_label.hide()
self.resolution_label.hide()
self.info_timer.stop()
def update_info_labels(self):
"""Update the information labels"""
if not self.show_info:
return
# Channel name
self.channel_label.setText(f"Channel: {self.current_channel}")
self.channel_label.adjustSize()
self.channel_label.show()
# Clock
if hasattr(self, 'show_clock_action') and self.show_clock_action.isChecked():
self.update_clock()
self.clock_label.show()
# Resolution info
if hasattr(self, 'media_player') and hasattr(self, 'show_resolution_action') and self.show_resolution_action.isChecked():
try:
width = self.media_player.video_get_width() or 0
height = self.media_player.video_get_height() or 0
fps = self.media_player.get_fps() or 0
if width > 0 and height > 0:
resolution = ""
if width >= 3840 or height >= 2160:
resolution = "4K"
elif width >= 1920 or height >= 1080:
resolution = "FHD"
elif width >= 1280 or height >= 720:
resolution = "HD"
else:
resolution = "SD"
self.resolution_label.setText(f"{width}x{height} {resolution} {int(fps)}fps")
self.resolution_label.adjustSize()
# Position at bottom right
self.resolution_label.move(
self.video_frame.width() - self.resolution_label.width() - 10,
self.video_frame.height() - self.resolution_label.height() - 10
)
self.resolution_label.show()
self.log_message(f"Resolution: {width}x{height} {resolution} {int(fps)}fps")
except Exception as e:
self.log_message(f"Error getting resolution: {str(e)}", error=True)
def update_clock(self):
"""Update the clock display"""
current_time = time.strftime("%H:%M:%S")
self.clock_label.setText(current_time)
self.clock_label.adjustSize()
# Position at top right
self.clock_label.move(
self.video_frame.width() - self.clock_label.width() - 10,
10
)
def update_stream_info(self):
"""Update stream information periodically"""
if self.show_info:
self.update_info_labels()
def log_message(self, message, error=False):
"""Add message to debug console"""
timestamp = time.strftime("%H:%M:%S", time.localtime())
if error:
self.debug_console.setTextColor(Qt.red)
self.debug_console.append(f"[ERROR {timestamp}] {message}")
else:
self.debug_console.setTextColor(Qt.black)
self.debug_console.append(f"[INFO {timestamp}] {message}")
self.debug_console.setTextColor(Qt.black)
self.debug_console.ensureCursorVisible()
def closeEvent(self, event):
"""Handle application closing with proper cleanup sequence"""
reply = QMessageBox.question(
self, 'Exit',
'Are you sure you want to exit?',
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.No:
event.ignore()
return
# Stop any active timers first
self.clock_timer.stop()
self.stream_info_timer.stop()
self.info_timer.stop()
# Stop VLC playback and release resources
if hasattr(self, 'media_player'):
try:
if self.media_player.is_playing():
self.media_player.stop()
self.media_player.release()
del self.media_player
if hasattr(self, 'instance'):
del self.instance
except Exception as e:
self.log_message(f"Error stopping VLC: {str(e)}", error=True)
# Stop PHP server
if hasattr(self, 'php_process') and self.php_process:
try:
self.php_process.terminate()
try:
self.php_process.wait(timeout=1)
except subprocess.TimeoutExpired:
self.php_process.kill()
except Exception as e:
self.log_message(f"Error stopping PHP: {str(e)}", error=True)
# Close playlist window
if hasattr(self, 'playlist_window') and self.playlist_window:
try:
self.playlist_window.close()
self.playlist_window.deleteLater()
except Exception as e:
self.log_message(f"Error closing playlist: {str(e)}", error=True)
# Process any pending events
QApplication.processEvents()
# Accept the event to allow normal closing
event.accept()
if __name__ == "__main__":
# Setup embedded VLC first
MainPlayer.setup_embedded_vlc()
app = QApplication(sys.argv)
# Check dependencies
try:
import requests
import vlc
except ImportError as e:
QMessageBox.critical(None, "Missing Dependencies",
f"Required packages missing:\n{str(e)}\n"
"Please install with: pip install python-vlc requests")
sys.exit(1)
# Check if PHP is available
try:
subprocess.run(["php", "-v"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except:
QMessageBox.critical(None, "PHP Not Found",
"PHP is required but not found in system PATH.\n"
"Please install PHP or place PHP binaries in the application directory.")
sys.exit(1)
player = MainPlayer()
player.show()
sys.exit(app.exec_())