Reworked playback
Supports local path, network path, direct streaming, transcoding.
This commit is contained in:
parent
22c62d9727
commit
aa72b4ce9c
3 changed files with 259 additions and 136 deletions
|
@ -6,182 +6,302 @@ import xbmc
|
||||||
import xbmcgui
|
import xbmcgui
|
||||||
import xbmcaddon
|
import xbmcaddon
|
||||||
|
|
||||||
import json
|
|
||||||
import threading
|
|
||||||
from datetime import datetime
|
|
||||||
from DownloadUtils import DownloadUtils
|
|
||||||
from ClientInformation import ClientInformation
|
from ClientInformation import ClientInformation
|
||||||
import urllib
|
import Utils as utils
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
#define our global download utils
|
|
||||||
downloadUtils = DownloadUtils()
|
|
||||||
clientInfo = ClientInformation()
|
|
||||||
|
|
||||||
###########################################################################
|
###########################################################################
|
||||||
|
|
||||||
class PlayUtils():
|
class PlayUtils():
|
||||||
|
|
||||||
|
_shared_state = {}
|
||||||
|
|
||||||
|
clientInfo = ClientInformation()
|
||||||
|
|
||||||
|
addonName = clientInfo.getAddonName()
|
||||||
|
addonId = clientInfo.getAddonId()
|
||||||
|
addon = xbmcaddon.Addon(id=addonId)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.__dict__ = self._shared_state
|
||||||
|
|
||||||
|
def logMsg(self, msg, lvl=1):
|
||||||
|
|
||||||
|
className = self.__class__.__name__
|
||||||
|
utils.logMsg("%s %s" % (self.addonName, className), str(msg), int(lvl))
|
||||||
|
|
||||||
def getPlayUrl(self, server, id, result):
|
def getPlayUrl(self, server, id, result):
|
||||||
|
|
||||||
WINDOW = xbmcgui.Window(10000)
|
addon = self.addon
|
||||||
username = WINDOW.getProperty('currUser')
|
WINDOW = xbmcgui.Window(10000)
|
||||||
server = WINDOW.getProperty('server%s' % username)
|
username = WINDOW.getProperty('currUser')
|
||||||
|
server = WINDOW.getProperty('server%s' % username)
|
||||||
|
|
||||||
addonSettings = xbmcaddon.Addon(id='plugin.video.emby')
|
if self.isDirectPlay(result):
|
||||||
# if the path is local and depending on the video quality play we can direct play it do so-
|
try:
|
||||||
if self.isDirectPlay(result) == True:
|
# Try direct play
|
||||||
playurl = result.get("Path")
|
playurl = self.directPlay(result)
|
||||||
if playurl != None:
|
if not playurl:
|
||||||
#We have a path to play so play it
|
# Let user know that direct play failed
|
||||||
USER_AGENT = 'QuickTime/7.7.4'
|
resp = xbmcgui.Dialog().yesno('Warning', 'Unable to direct play. Try direct stream or transcoding instead? By selecting yes, it will also switch your playback to HTTP for future playback.')
|
||||||
|
if resp == True:
|
||||||
|
# Try direct stream
|
||||||
|
playurl = self.directStream(result, server, id)
|
||||||
|
addon.setSetting('playFromStream', "true")
|
||||||
|
if not playurl:
|
||||||
|
# Try transcoding
|
||||||
|
playurl = self.transcoding(result, server, id)
|
||||||
|
WINDOW.setProperty("transcoding%s" % id, "true")
|
||||||
|
self.logMsg("File is transcoding.", 1)
|
||||||
|
else:
|
||||||
|
self.logMsg("File is direct streaming.", 1)
|
||||||
|
else:
|
||||||
|
# User decided not to proceed.
|
||||||
|
self.logMsg("Unable to direct play. Verify the following path is accessible by the device: %s. You might also need to add SMB credentials in the addon settings." % result[u'MediaSources'][0][u'Path'])
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.logMsg("File is direct playing.", 1)
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
# If the file it is not a media stub
|
elif self.isDirectStream(result):
|
||||||
if (result.get("IsPlaceHolder") != True):
|
try:
|
||||||
if (result.get("VideoType") == "Dvd"):
|
# Try direct stream
|
||||||
playurl = playurl + "/VIDEO_TS/VIDEO_TS.IFO"
|
playurl = self.directStream(result, server, id)
|
||||||
elif (result.get("VideoType") == "BluRay"):
|
if not playurl:
|
||||||
playurl = playurl + "/BDMV/index.bdmv"
|
# Try transcoding
|
||||||
if addonSettings.getSetting('smbusername') == '':
|
playurl = self.transcoding(result, server, id)
|
||||||
playurl = playurl.replace("\\\\", "smb://")
|
WINDOW.setProperty("transcoding%s" % id, "true")
|
||||||
else:
|
self.logMsg("File is transcoding.", 1)
|
||||||
playurl = playurl.replace("\\\\", "smb://" + addonSettings.getSetting('smbusername') + ':' + addonSettings.getSetting('smbpassword') + '@')
|
else:
|
||||||
playurl = playurl.replace("\\", "/")
|
self.logMsg("File is direct streaming.", 1)
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
if ("apple.com" in playurl):
|
elif self.isTranscoding(result):
|
||||||
playurl += '?|User-Agent=%s' % USER_AGENT
|
try:
|
||||||
if addonSettings.getSetting('playFromStream') == "true":
|
# Try transcoding
|
||||||
playurl = "%s/mediabrowser/Videos/%s/stream?static=true" % (server, id)
|
playurl = self.transcoding(result, server, id)
|
||||||
mediaSources = result.get("MediaSources")
|
WINDOW.setProperty("transcoding%s" % id, "true")
|
||||||
if(mediaSources != None):
|
self.logMsg("File is transcoding.", 1)
|
||||||
if mediaSources[0].get('DefaultAudioStreamIndex') != None:
|
except:
|
||||||
playurl = playurl + "&AudioStreamIndex=" +str(mediaSources[0].get('DefaultAudioStreamIndex'))
|
return False
|
||||||
if mediaSources[0].get('DefaultSubtitleStreamIndex') != None:
|
|
||||||
playurl = playurl + "&SubtitleStreamIndex=" + str(mediaSources[0].get('DefaultAudioStreamIndex'))
|
return playurl
|
||||||
|
|
||||||
else:
|
|
||||||
#No path or has a path but not sufficient network so transcode
|
|
||||||
if result.get("Type") == "Audio":
|
|
||||||
playurl = "%s/mediabrowser/Audio/%s/stream.mp3" % (server, id)
|
|
||||||
else:
|
|
||||||
txt_mac = clientInfo.getMachineId()
|
|
||||||
playurl = "%s/mediabrowser/Videos/%s/master.m3u8?mediaSourceId=%s" % (server, id, id)
|
|
||||||
playurl = playurl + '&videoCodec=h264'
|
|
||||||
playurl = playurl + '&AudioCodec=aac,ac3'
|
|
||||||
playurl = playurl + '&deviceId=' + txt_mac
|
|
||||||
playurl = playurl + '&VideoBitrate=' + str(int(self.getVideoBitRate()) * 1000)
|
|
||||||
mediaSources = result.get("MediaSources")
|
|
||||||
if(mediaSources != None):
|
|
||||||
if mediaSources[0].get('DefaultAudioStreamIndex') != None:
|
|
||||||
playurl = playurl + "&AudioStreamIndex=" +str(mediaSources[0].get('DefaultAudioStreamIndex'))
|
|
||||||
if mediaSources[0].get('DefaultSubtitleStreamIndex') != None:
|
|
||||||
playurl = playurl + "&SubtitleStreamIndex=" + str(mediaSources[0].get('DefaultSubtitleStreamIndex'))
|
|
||||||
return playurl.encode('utf-8')
|
|
||||||
|
|
||||||
# Works out if we are direct playing or not
|
|
||||||
def isDirectPlay(self, result):
|
def isDirectPlay(self, result):
|
||||||
addonSettings = xbmcaddon.Addon(id='plugin.video.emby')
|
# Requirements for Direct play:
|
||||||
|
# FileSystem, Accessible path
|
||||||
|
addon = self.addon
|
||||||
|
|
||||||
|
playhttp = addon.getSetting('playFromStream')
|
||||||
|
# User forcing to play via HTTP instead of SMB
|
||||||
|
if playhttp == "true":
|
||||||
|
return False
|
||||||
|
|
||||||
|
canDirectPlay = result[u'MediaSources'][0][u'SupportsDirectPlay']
|
||||||
|
# Make sure it's supported by server
|
||||||
|
if not canDirectPlay:
|
||||||
|
return False
|
||||||
|
|
||||||
|
location = result[u'LocationType']
|
||||||
|
# File needs to be "FileSystem"
|
||||||
|
if u'FileSystem' in location:
|
||||||
|
# Verify if path is accessible
|
||||||
|
if self.fileExists(result):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def directPlay(self, result):
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Item can be played directly
|
||||||
|
playurl = result[u'MediaSources'][0][u'Path']
|
||||||
|
|
||||||
|
if u'VideoType' in result:
|
||||||
|
# Specific format modification
|
||||||
|
if u'Dvd' in result[u'VideoType']:
|
||||||
|
playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl
|
||||||
|
elif u'BluRay' in result[u'VideoType']:
|
||||||
|
playurl = "%s/BDMV/index.bdmv" % playurl
|
||||||
|
|
||||||
|
# Network - SMB protocol
|
||||||
|
if "\\\\" in playurl:
|
||||||
|
smbuser = addon.getSetting('smbusername')
|
||||||
|
smbpass = addon.getSetting('smbpassword')
|
||||||
|
# Network share
|
||||||
|
if smbuser:
|
||||||
|
playurl = playurl.replace("\\\\", "smb://%s:%s@" % (smbuser, smbpass))
|
||||||
|
else:
|
||||||
|
playurl = playurl.replace("\\\\", "smb://")
|
||||||
|
playurl = playurl.replace("\\", "/")
|
||||||
|
|
||||||
|
if "apple.com" in playurl:
|
||||||
|
USER_AGENT = 'QuickTime/7.7.4'
|
||||||
|
playurl += "?|User-Agent=%s" % USER_AGENT
|
||||||
|
|
||||||
|
return playurl
|
||||||
|
|
||||||
|
except:
|
||||||
|
self.logMsg("Direct play failed. Trying Direct stream.", 1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isDirectStream(self, result):
|
||||||
|
# Requirements for Direct stream:
|
||||||
|
# FileSystem or Remote, BitRate, supported encoding
|
||||||
|
canDirectStream = result[u'MediaSources'][0][u'SupportsDirectStream']
|
||||||
|
# Make sure it's supported by server
|
||||||
|
if not canDirectStream:
|
||||||
|
return False
|
||||||
|
|
||||||
|
location = result[u'LocationType']
|
||||||
|
# File can be FileSystem or Remote, not Virtual
|
||||||
|
if u'Virtual' in location:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Verify BitRate
|
||||||
|
if not self.isNetworkQualitySufficient(result):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def directStream(self, result, server, id):
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Play with Direct Stream
|
||||||
|
playurl = "%s/mediabrowser/Videos/%s/stream?static=true" % (server, id)
|
||||||
|
|
||||||
|
mediaSources = result[u'MediaSources']
|
||||||
|
if mediaSources[0].get('DefaultAudioStreamIndex') != None:
|
||||||
|
playurl = "%s&AudioStreamIndex=%s" % (playurl, mediaSources[0].get('DefaultAudioStreamIndex'))
|
||||||
|
if mediaSources[0].get('DefaultSubtitleStreamIndex') != None:
|
||||||
|
playurl = "%s&SubtitleStreamIndex=%s" % (playurl, mediaSources[0].get('DefaultSubtitleStreamIndex'))
|
||||||
|
|
||||||
|
self.logMsg("Playurl: %s" % playurl)
|
||||||
|
return playurl
|
||||||
|
|
||||||
|
except:
|
||||||
|
self.logMsg("Direct stream failed. Trying transcoding.", 1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def isTranscoding(self, result):
|
||||||
|
# Last resort, no requirements
|
||||||
|
# BitRate
|
||||||
|
canTranscode = result[u'MediaSources'][0][u'SupportsTranscoding']
|
||||||
|
# Make sure it's supported by server
|
||||||
|
if not canTranscode:
|
||||||
|
return False
|
||||||
|
|
||||||
|
location = result[u'LocationType']
|
||||||
|
# File can be FileSystem or Remote, not Virtual
|
||||||
|
if u'Virtual' in location:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def transcoding(self, result, server, id):
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Play transcoding
|
||||||
|
deviceId = self.clientInfo.getMachineId()
|
||||||
|
playurl = "%s/mediabrowser/Videos/%s/master.m3u8?mediaSourceId=%s" % (server, id, id)
|
||||||
|
playurl = "%s&VideoCodec=h264&AudioCodec=aac,ac3&deviceId=%s&VideoBitrate=%s" % (playurl, deviceId, self.getVideoBitRate()*1000)
|
||||||
|
|
||||||
|
mediaSources = result[u'MediaSources']
|
||||||
|
if mediaSources[0].get('DefaultAudioStreamIndex') != None:
|
||||||
|
playurl = "%s&AudioStreamIndex=%s" % (playurl, mediaSources[0][u'DefaultAudioStreamIndex'])
|
||||||
|
if mediaSources[0].get('DefaultSubtitleStreamIndex') != None:
|
||||||
|
playurl = "%s&SubtitleStreamIndex=%s" % (playurl, mediaSources[0][u'DefaultSubtitleStreamIndex'])
|
||||||
|
|
||||||
|
self.logMsg("Playurl: %s" % playurl)
|
||||||
|
return playurl
|
||||||
|
|
||||||
|
except:
|
||||||
|
self.logMsg("Transcoding failed.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
'''forceTranscodingCodecs = self.addon.getSetting('forceTranscodingCodecs')
|
||||||
# check if we should force encoding due to the forceTranscodingCodecs setting
|
# check if we should force encoding due to the forceTranscodingCodecs setting
|
||||||
forceTranscodingCodecs = addonSettings.getSetting('forceTranscodingCodecs')
|
|
||||||
if forceTranscodingCodecs:
|
if forceTranscodingCodecs:
|
||||||
forceTranscodingCodecsSet = frozenset(forceTranscodingCodecs.lower().split(','))
|
forceTranscodingCodecsSet = frozenset(forceTranscodingCodecs.lower().split(','))
|
||||||
codecs = frozenset([mediaStream.get('Codec', None) for mediaStream in result.get('MediaStreams', [])])
|
codecs = frozenset([mediaStream.get('Codec', None) for mediaStream in result.get('MediaStreams', [])])
|
||||||
commonCodecs = forceTranscodingCodecsSet & codecs
|
commonCodecs = forceTranscodingCodecsSet & codecs
|
||||||
#xbmc.log("emby isDirectPlay MediaStreams codecs: %s forceTranscodingCodecs: %s, common: %s" % (codecs, forceTranscodingCodecsSet, commonCodecs))
|
#xbmc.log("emby isDirectPlay MediaStreams codecs: %s forceTranscodingCodecs: %s, common: %s" % (codecs, forceTranscodingCodecsSet, commonCodecs))
|
||||||
if commonCodecs:
|
if commonCodecs:
|
||||||
return False
|
return False'''
|
||||||
|
|
||||||
if (self.fileExists(result) or (result.get("LocationType") == "FileSystem" and self.isNetworkQualitySufficient(result) == True and self.isLocalPath(result) == False)):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
# Works out if the network quality can play directly or if transcoding is needed
|
# Works out if the network quality can play directly or if transcoding is needed
|
||||||
def isNetworkQualitySufficient(self, result):
|
def isNetworkQualitySufficient(self, result):
|
||||||
|
|
||||||
settingsVideoBitRate = self.getVideoBitRate()
|
settingsVideoBitRate = self.getVideoBitRate()
|
||||||
settingsVideoBitRate = int(settingsVideoBitRate) * 1000
|
settingsVideoBitRate = settingsVideoBitRate * 1000
|
||||||
mediaSources = result.get("MediaSources")
|
|
||||||
if(mediaSources != None):
|
|
||||||
if mediaSources[0].get('Bitrate') != None:
|
|
||||||
if settingsVideoBitRate < int(mediaSources[0].get('Bitrate')):
|
|
||||||
#xbmc.log("emby isNetworkQualitySufficient -> FALSE bit rate - settingsVideoBitRate: " + str(settingsVideoBitRate) + " mediasource bitrate: " + str(mediaSources[0].get('Bitrate')))
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
#xbmc.log("emby isNetworkQualitySufficient -> TRUE bit rate")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Any thing else is ok
|
try:
|
||||||
#xbmc.log("emby isNetworkQualitySufficient -> TRUE default")
|
mediaSources = result[u'MediaSources']
|
||||||
return True
|
sourceBitRate = int(mediaSources[0][u'Bitrate'])
|
||||||
|
|
||||||
|
if settingsVideoBitRate > sourceBitRate:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
except:
|
||||||
|
return True
|
||||||
|
|
||||||
# get the addon video quality
|
|
||||||
def getVideoBitRate(self):
|
def getVideoBitRate(self):
|
||||||
addonSettings = xbmcaddon.Addon(id='plugin.video.emby')
|
# get the addon video quality
|
||||||
videoQuality = addonSettings.getSetting('videoBitRate')
|
videoQuality = self.addon.getSetting('videoBitRate')
|
||||||
|
|
||||||
if (videoQuality == "0"):
|
if (videoQuality == "0"):
|
||||||
return '664'
|
return 664
|
||||||
elif (videoQuality == "1"):
|
elif (videoQuality == "1"):
|
||||||
return '996'
|
return 996
|
||||||
elif (videoQuality == "2"):
|
elif (videoQuality == "2"):
|
||||||
return '1320'
|
return 1320
|
||||||
elif (videoQuality == "3"):
|
elif (videoQuality == "3"):
|
||||||
return '2000'
|
return 2000
|
||||||
elif (videoQuality == "4"):
|
elif (videoQuality == "4"):
|
||||||
return '3200'
|
return 3200
|
||||||
elif (videoQuality == "5"):
|
elif (videoQuality == "5"):
|
||||||
return '4700'
|
return 4700
|
||||||
elif (videoQuality == "6"):
|
elif (videoQuality == "6"):
|
||||||
return '6200'
|
return 6200
|
||||||
elif (videoQuality == "7"):
|
elif (videoQuality == "7"):
|
||||||
return '7700'
|
return 7700
|
||||||
elif (videoQuality == "8"):
|
elif (videoQuality == "8"):
|
||||||
return '9200'
|
return 9200
|
||||||
elif (videoQuality == "9"):
|
elif (videoQuality == "9"):
|
||||||
return '10700'
|
return 10700
|
||||||
elif (videoQuality == "10"):
|
elif (videoQuality == "10"):
|
||||||
return '12200'
|
return 12200
|
||||||
elif (videoQuality == "11"):
|
elif (videoQuality == "11"):
|
||||||
return '13700'
|
return 13700
|
||||||
elif (videoQuality == "12"):
|
elif (videoQuality == "12"):
|
||||||
return '15200'
|
return 15200
|
||||||
elif (videoQuality == "13"):
|
elif (videoQuality == "13"):
|
||||||
return '16700'
|
return 16700
|
||||||
elif (videoQuality == "14"):
|
elif (videoQuality == "14"):
|
||||||
return '18200'
|
return 18200
|
||||||
elif (videoQuality == "15"):
|
elif (videoQuality == "15"):
|
||||||
return '20000'
|
return 20000
|
||||||
elif (videoQuality == "16"):
|
elif (videoQuality == "16"):
|
||||||
return '40000'
|
return 40000
|
||||||
elif (videoQuality == "17"):
|
elif (videoQuality == "17"):
|
||||||
return '100000'
|
return 100000
|
||||||
elif (videoQuality == "18"):
|
elif (videoQuality == "18"):
|
||||||
return '1000000'
|
return 1000000
|
||||||
else:
|
else:
|
||||||
return '2147483' # max bit rate supported by server (max signed 32bit integer)
|
return 2147483 # max bit rate supported by server (max signed 32bit integer)
|
||||||
|
|
||||||
def fileExists(self, result):
|
def fileExists(self, result):
|
||||||
if not result.has_key("Path"):
|
|
||||||
|
if u'Path' not in result:
|
||||||
|
# File has no path in server
|
||||||
return False
|
return False
|
||||||
path=result.get("Path").encode('utf-8')
|
|
||||||
if os.path.exists(path) == True:
|
path = result[u'Path']
|
||||||
|
# Verify the device has access to the direct path
|
||||||
|
if os.path.exists(path):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Works out if the network quality can play directly or if transcoding is needed
|
|
||||||
def isLocalPath(self, result):
|
|
||||||
path=result.get("Path").encode('utf-8')
|
|
||||||
playurl = path
|
|
||||||
if playurl != None:
|
|
||||||
#We have a path to play so play it
|
|
||||||
if ":\\" in playurl:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# default to not local
|
|
||||||
return False
|
|
|
@ -57,6 +57,10 @@ class PlaybackUtils():
|
||||||
seekTime = reasonableTicks / 10000
|
seekTime = reasonableTicks / 10000
|
||||||
|
|
||||||
playurl = PlayUtils().getPlayUrl(server, id, result)
|
playurl = PlayUtils().getPlayUrl(server, id, result)
|
||||||
|
if playurl == False:
|
||||||
|
xbmc.log("Failed to retrieve the playback path/url.")
|
||||||
|
return
|
||||||
|
|
||||||
thumbPath = API().getArtwork(result, "Primary")
|
thumbPath = API().getArtwork(result, "Primary")
|
||||||
|
|
||||||
#if the file is a virtual strm file, we need to override the path by reading it's contents
|
#if the file is a virtual strm file, we need to override the path by reading it's contents
|
||||||
|
|
|
@ -95,16 +95,15 @@ class Player( xbmc.Player ):
|
||||||
#report updates playcount and resume status to Kodi and MB3
|
#report updates playcount and resume status to Kodi and MB3
|
||||||
#librarySync.updatePlayCount(item_id)
|
#librarySync.updatePlayCount(item_id)
|
||||||
|
|
||||||
|
# Stop transcoding
|
||||||
|
if self.WINDOW.getProperty("transcoding%s" % item_id) == "true":
|
||||||
|
deviceId = self.clientInfo.getMachineId()
|
||||||
|
url = "{server}/mediabrowser/Videos/ActiveEncodings?DeviceId=%s" % deviceId
|
||||||
|
self.doUtils.downloadUrl(url, type="DELETE")
|
||||||
|
self.WINDOW.clearProperty("transcoding%s" % item_id)
|
||||||
|
|
||||||
self.played_information.clear()
|
self.played_information.clear()
|
||||||
|
|
||||||
# stop transcoding - todo check we are actually transcoding?
|
|
||||||
clientInfo = ClientInformation()
|
|
||||||
txt_mac = clientInfo.getMachineId()
|
|
||||||
url = "{server}/mediabrowser/Videos/ActiveEncodings"
|
|
||||||
url = url + '?DeviceId=' + txt_mac
|
|
||||||
self.doUtils.downloadUrl(url, type="DELETE")
|
|
||||||
|
|
||||||
def stopPlayback(self, data):
|
def stopPlayback(self, data):
|
||||||
|
|
||||||
self.logMsg("stopPlayback called", 2)
|
self.logMsg("stopPlayback called", 2)
|
||||||
|
|
Loading…
Reference in a new issue