Последняя версия - 1.14 от 03.08.2023: Исходники
readme.md
Creates the list thumbnails of the video file in console. Based on ffmpeg and QT. About 60 parameters for setting.
Last compile on Fedora 41 with ffmpeg 7.0.2 and QT5.15.15-1.
Example video - https://youtu.be/FGfhkvHVTcQ
Parameters: --preset Use section in config file (default - "Default") --presetsList Show all presets in config file (all other parameters ignored) --presetInfo Show all values in preset (all other parameters ignored, example: --presetInfo MyPreset) --frames Frames count in screenlist (default - 16) --width Width screenlist (default - 1200) --horCount Frames in horisontal (default - 4) --offsetBegin Time offset from begin (default - "00:01:00") --offsetEnd Time offset from end (default - "00:01:00") --listBorder Depth border screenlist (default - 10) --frameBorder Depth border frame (default - 5) --shadowOffset Offset shadow of frame (default - 5) --shadowInt Intensity shadow of frame (default - 10, range 0-100) --shadowColor R,G,B color shadow of frame (default - "0,0,0" - black, "255,255,255" - white) --background Picture file for background (if empty, fill white) --backgroundColor R,G,B[,A] background color (default - "255,255,255") --picType Type of picture file (default - "jpg") --header Header with information (default - true) --timeFrame Time stamp on frame (default - true) --fontFrameName Name font on frame (default - "Liberation Sans") --fontFrameSize Size font on frame (default - 10) --fontFrameWeight Weight font on frame (default - Normal, (Light,DemiBold,Bold,Black)) --fontFrameItalic Italic font on frame (default - false) --fontFrameColor R,G,B[,A] color font on frame (default - "255,255,255") --fontFrameShadowOffset Offset shadow of time stamp (default - 0) --fontFrameShadowInt Intensity shadow of time stamp (default - 2, range 0-100) --fontFrameShadowColor R,G,B color shadow of time stamp (default - "0,0,0") --stampPos Position time stamp (default - "rb" (RightBottom), (rb, rt, lb, lr)) --stampOffset Offset from the edge of the frame to the timestamp (default - 3) --stampStart Time offset for timestamp (default - "00:00:00") --fontHeaderName Name font on header (default - "Liberation Sans") --fontHeaderSize Size font on header (default - 13) --fontHeaderWeight Weight font on header (default - Bold, (Normal,Light,DemiBold,Black)) --fontHeaderItalic Italic font on header (default - false) --fontHeaderColor R,G,B[,A] color font on header (default - "0,0,0") --headerBackground Picture file for header background (if empty, default color for header) --headerColor R,G,B[,A] color header background (default - "255,255,255") --headerBorderColor R,G,B[,A] color header border (default - "0,0,0") --headerBorder Depth border header (default - 2) --infoShadowOffset Offset shadow of information text (default - 5) --infoShadowInt Intensity shadow of information text (default - 10, range 0-100) --infoShadowColor R,G,B color shadow of information text (default - "100,100,100") --logoFile Picture file for logotype (it is recommended PNG image with alpha channel) --logoShadowOffset Offset shadow of logotype (default - 5) --logoShadowInt Intensity shadow of logotype (default - 10, range 0-100) --logoShadowColor R,G,B color shadow of logotype (default - "100,100,100") --foreground Picture file for foreground (it is recommended PNG image with alpha channel) --aspect Aspect ratio (if 0.0 - source aspect (DAR), default - 0.0) --descr Custom text, located at the top of the header --outFolder Folder for result files --frameBoxColor R,G,B[,A] color frame border (default - "120,120,120") --frameBox Depth border frame (default - 0) --stampDescr Custom text, located on frame --stampDescrPos Position custom text (default - "lt" (LeftTop), (rb, rt, lb, lr)) --timeStep Time between frames. If not equal to "00:00:00", the parameter "frames" are ignored. (default - "00:00:00") --fpm Frames per minute. If not equal to "0", the parameter "frames" and "timeStep" are ignored. (default - "0") --maxRows Maximum rows if use "timeStep" or "fpm". (default - 50) --version Version string. (default - false) --logLevel FFMPEG log level. (default - -8 (QUIET))
CHANGELOG
2012-08-15 (1.0) * First release. 2012-08-16 (1.1) * Change backend from xine-lib to ffmpeg-libs. 2012-08-16 (1.2) * Fix errors, clean code. * Added parameter "stampPos". * Delete option --video. * Added the ability translate header. * Added russian translate for header. 2012-08-23 (1.3) * Fix errors. * Change algorithm shadow create. * Added parameters "infoShadowOffset", "infoShadowInt", "infoShadowColor" for text in header. * Added parameters "logoFile", "logoShadowOffset", "logoShadowInt", logoShadowColor" for logotype in header. * Added parameter "descr" - Custom text, located at the top of the header. * Added parameter "stampOffset" - Offset from the edge of the frame to the timestamp. 2012-08-23 (1.3.2) * Added parameter "outFolder". * Change logic for save output file - saved in current folder or in folder from parameter "outFolder". 2012-08-31 (1.4) * Disable log messages from ffmpeg-libs. * Added parameters "fontFrameShadowOffset", "fontFrameShadowInt", "fontFrameShadowColor" for timestamp shadow. 2012-09-19 (1.5) * Fix errors. * Added parameters "frameBox", "frameBoxColor", "stampDescr", "stampDescrPos". * All color parameters (except shadows) support alpha channel (maybe set to "R,G,B,A"). 2012-11-22 (1.6) * Fix segmentation fault if audio not present in video file. * Added parameters "presetsList" and "presetInfo". 2013-09-01 (1.7) * Fix segmentation fault if open error videofile, if not exists video stream or video codec. * Video Codec names fixed. * Added parameter "timeStep". 2014-09-09 (1.8) * Change console output. * Change default aspect ratio to DAR (Display Aspect Ratio). * Added parameter "maxRows" if use parameter "timeStep". * Added parameter "version". * Added parameter "stampStart". * Added example scripts. 2015-08-03 (1.9) * Fixed deprecated call of FFMpeg (2.4.3). 2017-09-12 (1.10) * Fixed deprecated call of libAVCodec 57.63.103. * Fixed for H264 codec. * Added parameter (from console only) "logLevel" - FFMpeg log. Available: -8 QUIET (default) 0 PANIC 8 FATAL 16 ERROR 24 WARNING 32 INFO 40 VERBOSE 48 DEBUG 56 TRACE * Added video bitrate info if available. 2019-02-17 (1.11) * Fixed offsetBegin and offsetEnd parameters 2023-08-02 (1.12) * Fixed to ffmpeg 6.0 2023-08-03 (1.13) * Add --fpm parameter 2023-08-03 (1.14) * Fix errors
Спасибо всем, кто разместил информацию о программе:
https://youtu.be/FGfhkvHVTcQ
https://github.com/Svarkovsky/screengen
https://www.debugpoint.com/create-video-thumbnail-image-using-screengen-in-ubuntu/
https://zenway.ru/page/screengen
https://linuxaria.com/pills/screengen-how-to-easily-create-a-thumbnail-from-a-video-with-linux
https://astapm.github.io/soft/screengen.html
Показывает трафик на сервере.
https://kochkin.tk/ifaces/
Исходник
<!doctype html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>Скорость на интерфейсах</title> <script src="raphael-min.js" type="text/javascript" charset="utf-8"></script> </head> <body> <script> // By Oleg Kochkin 2010. <kochkin.tk> // https://github.com/OlegKochkin/ifaces // Данное ПО распространяется под лицензией GPL // // Использованы: // // - Библиотека Raphaël для JavaScript. Copyright (c) 2008 - 2010 Dmitry Baranovskiy <raphaeljs.com> // - Части кода примеров Raphaël. Copyright (c) 2008 - 2010 Dmitry Baranovskiy <raphaeljs.com> // - Пример по Ajax - http://forum.eugen.su/showthread.php?t=132&p=599 function createRequest() { if (request != null) return; try { request = new XMLHttpRequest(); } catch (failed) { request = null; } if (request == null) alert("Error creating request object!"); } // Посылка запроса на сервер function getIfacesSpeed() { createRequest(); var url = "getIfacesSpeed.php"; request.onreadystatechange = viewSpeed(); request.open("GET", url, true); request.send(null); } function paintIndicator(nc, left, top, nameText, scaleIndex, diameter, width, border){ var scale=ind[nc].scaleValuesDigits[scaleIndex]; var valueText=ind[nc].scaleValuesText[scaleIndex]; ind[nc].currentIdScale=scaleIndex; ind[nc].scale=scale; ind[nc].diameter=diameter; // Диаметр ind[nc].width=width; ind[nc].border=border; ind[nc].digits = []; ind[nc].scale10 = []; ind[nc].scale5 = []; ind[nc].max=0; ind[nc].min=10; var r2=ind[nc].diameter/2, r1=r2-r2/20, r5=r2-r2/3, r4=r2-r2/13.33, r3=r2-r2/6.67, r5=r2-r2/3; cx=r2+width+border, cy=r2+width+border, beta=2*Math.PI/130, pathParams={stroke: "#0f0","stroke-width": width,"stroke-linecap": "round"}; ind[nc].canvas=Raphael (left, top, r2*2+width*2+border*2, r2*2+width*2+border*2); // Подложка bg1 = ind[nc].canvas.circle(r2+width+border, r2+width+border, r2+width+border-4); bg1.attr("fill", "black"); // Центр стрелки bg2 = ind[nc].canvas.circle(r2+width+border, r2+width+border, r2/10); bg2.attr("fill", "blue"); for (var i=0; i<130; i++){ var alpha=beta*i-Math.PI/2, cos=Math.cos(alpha), sin=Math.sin(alpha); if (i==52){ // Индикатор переполнения ind[nc].over = ind[nc].canvas.circle(cx+r1*cos,cy+r1*sin, r2/20); ind[nc].over.attr({gradient: "300-#fff-#f00:50-#900"}); } // Мелкие риски шкалы if (i<50 || i>80){ var sector=ind[nc].canvas.path ("M"+(cx+r1*cos)+" "+(cy+r1*sin)+"L"+(cx+r2*cos)+" "+(cy+r2*sin)).attr (pathParams); } } // Единицы измерения ind[nc].valueTextObj=ind[nc].canvas.text (r2+ind[nc].width+ind[nc].border, r2+r2/2.86, valueText); ind[nc].valueTextObj.attr ({fill: "gray", "font-size": r2/9, "font-style": "italic", "font-weight": "bold"}); for (var j=-5;j<=5;j++){ var beta2=2*Math.PI/13, alpha2=beta2*j-Math.PI/2, cos2=Math.cos(alpha2), sin2=Math.sin(alpha2), dig=(j+5)*scale/10; // Цифры ind[nc].digits[j+5]=ind[nc].canvas.text (cx+r5*cos2,cy+r5*sin2,dig+""); ind[nc].digits[j+5].attr ({fill: "#0ff", "font-size": r2/8.33, "font-weight": "bold"}); // Риски шкалы 1/10 ind[nc].scale10[j+5]=ind[nc].canvas.path ("M"+(cx+r3*cos2)+" "+(cy+r3*sin2)+"L"+(cx+r2*cos2)+" "+(cy+r2*sin2)).attr ({stroke: "yellow","stroke-width": r2/40,"stroke-linecap": "round"}) // Риски шкалы 1/5 if (j<5){ var alpha3=beta2*(j+0.5)-Math.PI/2, cos3=Math.cos(alpha3), sin3=Math.sin(alpha3); ind[nc].scale5[j+5]=ind[nc].canvas.path ("M"+(cx+r4*cos3)+" "+(cy+r4*sin3)+"L"+(cx+r2*cos3)+" "+(cy+r2*sin3)).attr({stroke: "yellow","stroke-width": r2/66.66,"stroke-linecap": "round"}) } } // Сообщение ind[nc].message=ind[nc].canvas.text (r2+width+border, r2+r2/1.2, "Запуск"); ind[nc].message.attr ({fill: "red", "font-size": r2/9, "font-style": "italic", "font-weight": "bold"}); ind[nc].message.animate ({opacity: 0},3000); setScale (nc); // Название индикатора var nameTextIn=ind[nc].canvas.text (r2+width+border, r2-r2/4, nameText); nameTextIn.attr ({fill: "gray", "font-size": r2/9, "font-style": "italic", "font-weight": "bold"}); // Стрелка ind[nc].arrow=ind[nc].canvas.path ("M"+(r2+width+border)+" "+(r2+width+border+r2/6.5)+"L"+(r2+width+border)+" "+(r2-r2/1.35)).attr({stroke: "white","stroke-width": r2/28.5,"stroke-linecap": "round"}) // Верхушка центра стрелки bg3 = ind[nc].canvas.circle(r2+width+border, r2+width+border, r2/20); bg3.attr("fill", "red"); // Стекло bg4 = ind[nc].canvas.image("glass.png", 0, 0, (r2+width+border)*2, (r2+width+border)*2); // Поворот стрелки в нулевое положение ind[nc].arrow.rotate (360*8/13, r2+width+border, r2+width+border); var angle=360*8/13; ind[nc].arrow.animate({rotation: [angle, ind[nc].diameter/2+ind[nc].width+ind[nc].border, ind[nc].diameter/2+ind[nc].width+ind[nc].border]}, 10); } function setScale (nc){ var scale=ind[nc].scaleValuesDigits[ind[nc].currentIdScale]; var valueText=ind[nc].scaleValuesText[ind[nc].currentIdScale]; ind[nc].scale=scale; ind[nc].valueText=valueText; ind[nc].valueTextObj.attr ("text",valueText); for (var j=-5;j<=5;j++){ var dig=(j+5)*scale/10; dig=dig+""; ind[nc].digits[j+5].attr ("text", dig); } ind[nc].message.attr ({"opacity": 1, "text": "Смена шкалы"}); ind[nc].message.animate ({opacity: 0},5000); } function arrowRotate (nc, aind){ var sc=ind[nc].scaleValues[ind[nc].currentIdScale]; if (ind[nc].firstTeek){ var curSc=0; for (var i=0;i<ind[nc].scaleValues.length;i++){ curSc=ind[nc].scaleValues[i]; if (aind<curSc){ ind[nc].currentIdScale=i; sc=curSc; setScale (nc); ind[nc].firstTeek=false; break; } } } // Если переполнение -> включить индикатор и "упереть" стрелку if (aind>sc){ // Если значение больше шкалы aind=ind[nc].scale; ind[nc].over.attr({gradient: "300-#fff-#f00:50-#900"}); ind[nc].max++; if (ind[nc].max>scaleChangeTics){ // Если значение зашкаливает scaleChangeTics тиков подряд ind[nc].currentIdScale++; if (ind[nc].currentIdScale > ind[nc].scaleValues.length-1) ind[nc].currentIdScale = ind[nc].scaleValues.length-1; else setScale (nc); ind[nc].max=0; } } else { // Если значение меньше шкалы ind[nc].over.attr({gradient: "300-#666-#600:50-#300"}); ind[nc].max--; if (ind[nc].max<0) ind[nc].max=0; } if (aind<sc/10){ // Если значение меньше 1/10 шкалы ind[nc].min--; if (ind[nc].min<0){ ind[nc].currentIdScale--; if (ind[nc].currentIdScale < 0) ind[nc].currentIdScale = 0; else setScale (nc); ind[nc].min=scaleChangeTics; } } else{ ind[nc].min++; if (ind[nc].min>scaleChangeTics) ind[nc].min=scaleChangeTics; } // Пересчёт килобайтов в градусы var angle=360*80/130+aind*360/(sc+sc*3/10); // Поворот стрелки на нужный угол ind[nc].arrow.animate({rotation: [angle, ind[nc].diameter/2+ind[nc].width+ind[nc].border, ind[nc].diameter/2+ind[nc].width+ind[nc].border]}, 5000); } // При получении данных от сервера function viewSpeed() { if (request.readyState == 4) { var str=request.responseText; // Получаемые данные: values=str.split ("|"); current_rx = values[0] - prev_rx; prev_rx = values[0]; current_tx = values[1] - prev_tx; prev_tx = values[1]; // alert (current_tx); arrowRotate ("IFaceRx", Number(current_rx / 2048)); arrowRotate ("IFaceTx", Number(current_tx / 2048)); request = null; } } var ind=[]; var request = null; var scaleChangeTics=5; var prev_rx = 0, prev_tx = 0, current_rx = 0, current_tx = 0; window.onload = function () { var vaules=[]; ind["default"]=[]; ind["default"].scaleValuesDigits=[1,10,20,50,100,200,500,2000,10,20]; ind["default"].scaleValues=[1,10,20,50,100,200,500,2048, 10240,20480]; ind["default"].scaleValuesText=["КБайт/сек","КБайт/сек","КБайт/сек","КБайт/сек","КБайт/сек","КБайт/сек","КБайт/сек","КБайт/сек","МБайт/сек","МБайт/сек"]; ind["default"].firstTeek=true; ind["IFaceRx"]=[]; ind["IFaceTx"]=[]; // Шкалы ind["IFaceRx"].scaleValuesDigits=ind["default"].scaleValuesDigits.slice() ind["IFaceRx"].scaleValues=ind["default"].scaleValues.slice(); ind["IFaceRx"].scaleValuesText=ind["default"].scaleValuesText.slice(); ind["IFaceRx"].firstTeek=ind["default"].firstTeek; ind["IFaceTx"].scaleValuesDigits=ind["default"].scaleValuesDigits.slice() ind["IFaceTx"].scaleValues=ind["default"].scaleValues.slice(); ind["IFaceTx"].scaleValuesText=ind["default"].scaleValuesText.slice(); ind["IFaceTx"].firstTeek=ind["default"].firstTeek; var cols=2,diam=400; var px = (window.outerWidth - (diam*cols+10)) / 2; var py = (window.outerHeight - (11+diam)) / 2; var bg=Raphael (px, py, diam*cols+10, 11+diam); var bgrect=bg.rect (4, 5, cols*diam+1, diam, 20); bgrect.attr ("fill", "black"); paintIndicator ("IFaceRx", px+10, py+10, "Приём", 5, diam-30, 2, 7); paintIndicator ("IFaceTx", px+diam+10, py+10, "Передача", 5, diam-30, 2, 7); // Таймер. Каждые 1000 мс запускает getIfacesSpeed() var timerGetSpeed=setInterval ("getIfacesSpeed();",1000); } </script> </body> </html>
Простой IPTV плеер с источниками потоков в m3u файлах.
Исходник
#!/usr/bin/python3 # Simple IPTV player for sources in m3u files # TVOK. Version 0.6.3 (27.07.2023). By Oleg Kochkin. License GPL. import vlc import sys, os, time, xml.etree.ElementTree as ET, datetime, textwrap from PyQt5.QtWidgets import QApplication,QWidget,QMainWindow,QMenu,QAction,QLabel,QSystemTrayIcon,QFrame,QGridLayout,QBoxLayout from PyQt5.QtGui import QIcon from PyQt5.QtCore import QSettings, Qt, pyqtSlot, QTimer from PyQt5.QtDBus import QDBusConnection, QDBusMessage, QDBusInterface, QDBusReply, QDBus WINDOW_DECORATION_HEIGHT_TITLE = 25 WINDOW_DECORATION_WIDTH_BORDER = 4 VOLUME_CHANGE=5 VOLUME_LEVEL=80 # Get folder with script scriptDir = os.path.dirname(os.path.realpath(__file__)) # Config file init cfg = QSettings('tvok','tvok') # Load playlist pl = [] try: list = sys.argv[1] except: try: list = scriptDir+os.path.sep+'tvok.m3u' except: print("Run example:\n\ttvok.py ChannelsFile.m3u") exit(1) f = open(list) line = f.readline() while line: if "#EXTINF:" in line: tvg='' if ('tvg-id=' in line): tvg=line.split('tvg-id="')[1].split('"')[0] ch = line.split(',')[1].strip() url = f.readline().strip() pl.append([ch,url,tvg]) line = f.readline() f.close() class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() def createUI(self): self.home = os.getenv("HOME") # vlc player init self.instance = vlc.Instance('--ipv4-timeout=1000 --config='+self.home+'/.config/tvok/vlcrc -q') self.mediaplayer = self.instance.media_player_new() # Main window settings self.gridLayout = QGridLayout(self) self.gridLayout.setObjectName("gridLayout") self.videoFrame = QFrame(self) self.videoFrame.setObjectName("videoFrame") self.gridLayout.addWidget(self.videoFrame, 0, 0, 1, 1) self.mediaplayer.set_xwindow(int(self.winId())) self.setWindowIcon(QIcon(scriptDir+os.path.sep+'pics'+os.path.sep+'logo.png')) self.resize(cfg.value('Width',456,type=int),cfg.value('Height',256,type=int)) self.setGeometry(cfg.value(os.path.basename(list)+'/Left',456,type=int)+WINDOW_DECORATION_WIDTH_BORDER, cfg.value(os.path.basename(list)+'/Top',256,type=int)+WINDOW_DECORATION_HEIGHT_TITLE, cfg.value(os.path.basename(list)+'/Width',456,type=int), cfg.value(os.path.basename(list)+'/Height',256,type=int)) pal = self.palette() pal.setColor(self.backgroundRole(), Qt.darkBlue) self.setPalette(pal) self.currentCursor = self.cursor() # Save status audio mute self.AudioMuteOnStart = self.mediaplayer.audio_get_mute() self.AudioVolumeOnStart = self.mediaplayer.audio_get_volume() self.Volume = cfg.value('Volume',VOLUME_LEVEL,type=int) self.EpgInMenu = cfg.value(os.path.basename(list)+'/EpgInMenu',False,type=bool) self.EPGOk = True try: self.EPG = ET.parse(scriptDir+os.path.sep+'epg.xml') except: self.EPGOk = False # Registered DBUS service DBUSName = 'tv.ok' DBUSConn = QDBusConnection.connectToBus(QDBusConnection.SessionBus, DBUSName) DBUSConn.registerService(DBUSName) DBUSConn.registerObject("/", self, QDBusConnection.ExportAllContents) self.ChMenu = QMenu() self.ChMenu.setToolTipsVisible(True) self.ChMenu.setStyleSheet("font:14pt") self.ChMenu.setToolTipDuration(1) for chs in pl: action = self.ChMenu.addAction(chs[0]) self.ChMenu.addSeparator() self.quitAction = self.ChMenu.addAction(self.tr("Quit")) # Timer 1 second init. Once second call function t1secEvent self.t1sec = QTimer(self) self.t1sec.timeout.connect(self.t1secEvent) self.t1sec.start(1000) self.t1min = QTimer(self) self.t1min.timeout.connect(self.t1minEvent) self.t1min.start(60000) self.trayIcon = QSystemTrayIcon() self.trayIcon.setToolTip('TVOK '+os.path.basename(list)) self.trayIcon.activated.connect (self.ToggleMute) # print ("MUTE:",self.mediaplayer.audio_get_mute()) self.swapIcon() # Select channel saved previous run self.chNum = cfg.value(os.path.basename(list)+"/Channel",1,type=int) self.chPrev = self.chNum + 1 self.chChange() self.selectChannel = '' self.tChSelect = QTimer(self) self.tChSelect.timeout.connect(self.tChSelectTimeout) self.tLongStartJob = QTimer(self) self.tLongStartJob.timeout.connect(self.ChMenuToolTipRefresh) self.tLongStartJob.start(1) def ChMenuToolTipRefresh(self): self.tLongStartJob.stop() for index,chs in enumerate(pl): self.ChMenu.actions()[index].setToolTip(chs[0]+'\n'+self.GetChannelProg(index)) def osdView(self,mess): # Send OSD # If DBUS daemon org.kochkin.okindd is running dbus_interface = QDBusInterface("org.kochkin.okindd", "/Text") if dbus_interface.isValid(): dbus_interface.call('printText', 'Tvok', mess, 5000) @pyqtSlot(int) def channelNum(self,digit): if (digit >= 0) and (digit <= 9): self.selectChannel = self.selectChannel+str(digit) if int(self.selectChannel) > len(pl): self.selectChannel = self.selectChannel[:-1] if int(self.selectChannel) < 1: self.selectChannel = self.selectChannel[:-1] self.osdView(self.selectChannel+': '+pl[int(self.selectChannel)-1][0]) self.tChSelect.start(2000) @pyqtSlot() def tChSelectTimeout(self): self.tChSelect.stop() self.chNum = int (self.selectChannel) self.selectChannel = '' self.chChange() def swapIcon(self): picture = scriptDir+os.path.sep+'pics'+os.path.sep+'din-on.png' if not self.mute(): picture = scriptDir+os.path.sep+'pics'+os.path.sep+'din-off.png' self.trayIcon.setIcon (QIcon (picture)) self.trayIcon.show() @pyqtSlot(result=bool) def mute(self): return self.mediaplayer.audio_get_mute() @pyqtSlot(result=int) def GetChannelNum(self): return self.chNum @pyqtSlot(result=str) def GetChannelName(self): return pl[self.chNum-1][0] @pyqtSlot(int,result=str) def GetChannelProg(self,chNumber): Title = "" if (self.EPGOk): rootEPG = self.EPG.getroot() ChName = pl[chNumber][0] import cr if (ChName in cr.ChReplace): ChName = cr.ChReplace[ChName] # print(ChName) Now = (datetime.datetime.now()).strftime("%Y%m%d%H%M%S") ChId=pl[chNumber][2] for prog in rootEPG.findall("./programme/[@channel='"+ChId+"']"): Start=prog.get('start').split()[0] Stop=prog.get('stop').split()[0] if ((Start < Now) and (Stop > Now)): Title=prog.find('title').text.split(' [')[0] return Title @pyqtSlot(result=int) def GetVolume(self): return self.mediaplayer.audio_get_volume() @pyqtSlot() def VolumeIncrease(self): self.mediaplayer.audio_set_volume(self.mediaplayer.audio_get_volume()+VOLUME_CHANGE) @pyqtSlot() def VolumeDecrease(self): self.mediaplayer.audio_set_volume(self.mediaplayer.audio_get_volume()-VOLUME_CHANGE) # Once second def t1secEvent(self): if self.isFullScreen(): self.CursorOff() # Once minute def t1minEvent(self): ChPr=self.GetChannelProg(self.chNum-1) DelimWin=". " DelimOsd="\n" if (ChPr == ""): DelimWin="" DelimOsd="" NewWindowTitle = str(self.chNum)+". "+pl[self.chNum-1][0]+DelimWin+ChPr if (NewWindowTitle != self.windowTitle()): print(self.windowTitle()+" -> "+NewWindowTitle) self.setWindowTitle(NewWindowTitle) self.osdView(str(self.chNum)+': '+pl[self.chNum-1][0]+DelimOsd+textwrap.fill(ChPr,40)) self.tLongStartJob.start(1) @pyqtSlot() def ToggleMute(self): self.mediaplayer.audio_set_mute(not self.mediaplayer.audio_get_mute()) cfg.setValue(os.path.basename(list)+'/AudioMute',self.mute()) self.swapIcon() @pyqtSlot() def ChannelNext(self): self.chNum += 1 self.chChange() @pyqtSlot() def ChannelPrev(self): self.chNum -= 1 self.chChange() def keyPressEvent(self,event): if event.key() == Qt.Key_PageUp: self.ChannelNext() if event.key() == Qt.Key_PageDown: self.ChannelPrev() event.accept() # On mouse wheel change def wheelEvent(self,event): if event.angleDelta().y() > 0: self.ChannelNext() if event.angleDelta().y() < 0: self.ChannelPrev() event.accept() @pyqtSlot() def ChannelRestart(self): self.chChange() @pyqtSlot(result=str) def getCrop(self): return self.mediaplayer.video_get_crop_geometry() @pyqtSlot(str) def setCrop(self,crop): self.mediaplayer.video_set_crop_geometry(crop) cfg.setValue(self.GetChannelName()+'/Crop',crop) cfg.sync() @pyqtSlot(result=str) def getAspect(self): return self.mediaplayer.video_get_aspect_ratio() @pyqtSlot(str) def setAspect(self,aspect): self.mediaplayer.video_set_aspect_ratio(aspect) @pyqtSlot() def chReconnect(self): self.chPrev = self.chNum + 1 self.chChange() # Stop current channel and start chNum channel def chChange(self): if self.chNum != self.chPrev: if self.chNum > len(pl): self.chNum = 1 if self.chNum < 1: self.chNum = len(pl) ChPr=self.GetChannelProg(self.chNum-1) DelimWin=". " DelimOsd="\n" if (ChPr == ""): DelimWin="" DelimOsd="" self.setWindowTitle(str(self.chNum)+'. '+pl[self.chNum-1][0]+DelimWin+ChPr) self.osdView(str(self.chNum)+': '+pl[self.chNum-1][0]+DelimOsd+textwrap.fill(ChPr,40)) self.mediaplayer.stop() self.media = self.instance.media_new(pl[self.chNum-1][1]) self.mediaplayer.set_media(self.media) playerError = self.mediaplayer.play() self.setVolume = QTimer(self) self.setVolume.timeout.connect(self.setVolumeMax) self.setVolume.start() # print ("playerError = "+str(playerError)) if playerError != 0: sys.exit() cfg.setValue('Channel',self.chNum) cfg.setValue(os.path.basename(list)+"/Channel",self.chNum) self.chPrev = self.chNum if self.isFullScreen(): self.CursorOff() def setVolumeMax(self): self.mediaplayer.audio_set_volume(VOLUME_LEVEL) if self.mediaplayer.audio_get_volume() >= VOLUME_LEVEL: self.setVolume.stop() # If double click left button mouse - toggle full screen def mouseDoubleClickEvent(self,event): if event.button() == Qt.MouseButton.LeftButton: self.ToggleFullScreen() event.accept() # If middle click mouse - toggle audio mute def mousePressEvent(self,event): if event.button() == Qt.MouseButton.MiddleButton: self.ToggleMute() event.accept() def CursorOff(self): self.setCursor(Qt.BlankCursor) QApplication.setOverrideCursor(Qt.BlankCursor) def CursorOn(self): self.setCursor(self.currentCursor) QApplication.setOverrideCursor(self.currentCursor) @pyqtSlot() def ToggleFullScreen(self): if self.isFullScreen(): self.showNormal() self.CursorOn() else: self.showFullScreen() self.CursorOff() # Mouse pressed for context menu def contextMenuEvent(self,event): self.CursorOn() action = self.ChMenu.exec_(self.mapToGlobal(event.pos())) if action: value_index=1 for value in pl: if value[0] == action.iconText(): if self.chNum != value_index: self.chNum = value_index self.chChange() break else: value_index += 1 if action == self.quitAction: self.close() event.accept() def closeEvent(self, event): self.mediaplayer.stop() if not self.isFullScreen(): cfg.setValue(os.path.basename(list)+'/Left',self.x()) cfg.setValue(os.path.basename(list)+'/Top',self.y()) cfg.setValue(os.path.basename(list)+'/Width',self.width()) cfg.setValue(os.path.basename(list)+'/Height',self.height()) cfg.sync() cfg.setValue(os.path.basename(list)+'/AudioMute',self.mute()) self.mediaplayer.audio_set_mute(self.AudioMuteOnStart) event.accept() exit() app = QApplication(sys.argv) tvok = MainWindow() tvok.createUI() tvok.show() sys.exit(app.exec_())