From aa72b4ce9c599f05ec1fd855dcf94397d5eb1f40 Mon Sep 17 00:00:00 2001 From: angelblue05 Date: Thu, 7 May 2015 01:11:20 -0500 Subject: [PATCH] Reworked playback Supports local path, network path, direct streaming, transcoding. --- resources/lib/PlayUtils.py | 378 ++++++++++++++++++++++----------- resources/lib/PlaybackUtils.py | 4 + resources/lib/Player.py | 13 +- 3 files changed, 259 insertions(+), 136 deletions(-) diff --git a/resources/lib/PlayUtils.py b/resources/lib/PlayUtils.py index a09260f0..4d2afb34 100644 --- a/resources/lib/PlayUtils.py +++ b/resources/lib/PlayUtils.py @@ -6,182 +6,302 @@ import xbmc import xbmcgui import xbmcaddon -import json -import threading -from datetime import datetime -from DownloadUtils import DownloadUtils from ClientInformation import ClientInformation -import urllib -import sys +import Utils as utils import os -#define our global download utils -downloadUtils = DownloadUtils() -clientInfo = ClientInformation() - ########################################################################### + 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): - WINDOW = xbmcgui.Window(10000) - username = WINDOW.getProperty('currUser') - server = WINDOW.getProperty('server%s' % username) + addon = self.addon + WINDOW = xbmcgui.Window(10000) + username = WINDOW.getProperty('currUser') + server = WINDOW.getProperty('server%s' % username) + + if self.isDirectPlay(result): + try: + # Try direct play + playurl = self.directPlay(result) + if not playurl: + # Let user know that direct play failed + 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 + + elif self.isDirectStream(result): + try: + # Try direct stream + playurl = self.directStream(result, server, id) + 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) + except: + return False + + elif self.isTranscoding(result): + try: + # Try transcoding + playurl = self.transcoding(result, server, id) + WINDOW.setProperty("transcoding%s" % id, "true") + self.logMsg("File is transcoding.", 1) + except: + return False + + return playurl - addonSettings = xbmcaddon.Addon(id='plugin.video.emby') - # if the path is local and depending on the video quality play we can direct play it do so- - if self.isDirectPlay(result) == True: - playurl = result.get("Path") - if playurl != None: - #We have a path to play so play it - USER_AGENT = 'QuickTime/7.7.4' - - # If the file it is not a media stub - if (result.get("IsPlaceHolder") != True): - if (result.get("VideoType") == "Dvd"): - playurl = playurl + "/VIDEO_TS/VIDEO_TS.IFO" - elif (result.get("VideoType") == "BluRay"): - playurl = playurl + "/BDMV/index.bdmv" - if addonSettings.getSetting('smbusername') == '': - playurl = playurl.replace("\\\\", "smb://") - else: - playurl = playurl.replace("\\\\", "smb://" + addonSettings.getSetting('smbusername') + ':' + addonSettings.getSetting('smbpassword') + '@') - playurl = playurl.replace("\\", "/") - - if ("apple.com" in playurl): - playurl += '?|User-Agent=%s' % USER_AGENT - if addonSettings.getSetting('playFromStream') == "true": - playurl = "%s/mediabrowser/Videos/%s/stream?static=true" % (server, id) - 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('DefaultAudioStreamIndex')) - - 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): - 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 - forceTranscodingCodecs = addonSettings.getSetting('forceTranscodingCodecs') if forceTranscodingCodecs: forceTranscodingCodecsSet = frozenset(forceTranscodingCodecs.lower().split(',')) codecs = frozenset([mediaStream.get('Codec', None) for mediaStream in result.get('MediaStreams', [])]) commonCodecs = forceTranscodingCodecsSet & codecs #xbmc.log("emby isDirectPlay MediaStreams codecs: %s forceTranscodingCodecs: %s, common: %s" % (codecs, forceTranscodingCodecsSet, commonCodecs)) if commonCodecs: - 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 + return False''' - # Works out if the network quality can play directly or if transcoding is needed def isNetworkQualitySufficient(self, result): + settingsVideoBitRate = self.getVideoBitRate() - settingsVideoBitRate = int(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 - #xbmc.log("emby isNetworkQualitySufficient -> TRUE default") - return True + settingsVideoBitRate = settingsVideoBitRate * 1000 + + try: + mediaSources = result[u'MediaSources'] + 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): - addonSettings = xbmcaddon.Addon(id='plugin.video.emby') - videoQuality = addonSettings.getSetting('videoBitRate') + # get the addon video quality + videoQuality = self.addon.getSetting('videoBitRate') + if (videoQuality == "0"): - return '664' + return 664 elif (videoQuality == "1"): - return '996' + return 996 elif (videoQuality == "2"): - return '1320' + return 1320 elif (videoQuality == "3"): - return '2000' + return 2000 elif (videoQuality == "4"): - return '3200' + return 3200 elif (videoQuality == "5"): - return '4700' + return 4700 elif (videoQuality == "6"): - return '6200' + return 6200 elif (videoQuality == "7"): - return '7700' + return 7700 elif (videoQuality == "8"): - return '9200' + return 9200 elif (videoQuality == "9"): - return '10700' + return 10700 elif (videoQuality == "10"): - return '12200' + return 12200 elif (videoQuality == "11"): - return '13700' + return 13700 elif (videoQuality == "12"): - return '15200' + return 15200 elif (videoQuality == "13"): - return '16700' + return 16700 elif (videoQuality == "14"): - return '18200' + return 18200 elif (videoQuality == "15"): - return '20000' + return 20000 elif (videoQuality == "16"): - return '40000' + return 40000 elif (videoQuality == "17"): - return '100000' + return 100000 elif (videoQuality == "18"): - return '1000000' + return 1000000 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): - if not result.has_key("Path"): + + if u'Path' not in result: + # File has no path in server 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 else: - 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 \ No newline at end of file + return False \ No newline at end of file diff --git a/resources/lib/PlaybackUtils.py b/resources/lib/PlaybackUtils.py index 8b4a2e0c..c870d162 100644 --- a/resources/lib/PlaybackUtils.py +++ b/resources/lib/PlaybackUtils.py @@ -57,6 +57,10 @@ class PlaybackUtils(): seekTime = reasonableTicks / 10000 playurl = PlayUtils().getPlayUrl(server, id, result) + if playurl == False: + xbmc.log("Failed to retrieve the playback path/url.") + return + thumbPath = API().getArtwork(result, "Primary") #if the file is a virtual strm file, we need to override the path by reading it's contents diff --git a/resources/lib/Player.py b/resources/lib/Player.py index 5025400b..0f6a95c1 100644 --- a/resources/lib/Player.py +++ b/resources/lib/Player.py @@ -95,15 +95,14 @@ class Player( xbmc.Player ): #report updates playcount and resume status to Kodi and MB3 #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() - - # 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):