diff --git a/resources/lib/PlexAPI.py b/resources/lib/PlexAPI.py index 3045f616..1973daad 100644 --- a/resources/lib/PlexAPI.py +++ b/resources/lib/PlexAPI.py @@ -619,6 +619,8 @@ class PlexAPI(): 'X-Plex-Version': self.plexversion, 'X-Plex-Client-Identifier': self.clientId, 'machineIdentifier': self.machineIdentifier, + 'Connection': 'keep-alive', + 'X-Plex-Provides': 'player', 'Accept': 'application/xml' } @@ -1781,16 +1783,73 @@ class API(): def getBitrate(self): """ - Returns the bitrate as an int or None + Returns the bitrate as an int. The Part bitrate is returned; if not + available in the Plex XML, the Media bitrate is returned """ item = self.item try: - bitrate = item[self.child][0].attrib['bitrate'] - bitrate = int(bitrate) + bitrate = item[self.child][0][self.part].attrib['bitrate'] except KeyError: - bitrate = None + bitrate = item[self.child][0].attrib['bitrate'] + bitrate = int(bitrate) return bitrate + def getDataFromPartOrMedia(self, key): + """ + Retrieves XML data 'key' first from the active part. If unsuccessful, + tries to retrieve the data from the Media response part. + + If all fails, None is returned. + """ + media = self.item[self.child][0].attrib + part = self.item[self.child][0][self.part].attrib + try: + try: + value = part[key] + except KeyError: + value = media[key] + except KeyError: + value = None + return value + + def getVideoCodec(self): + """ + Returns the video codec and resolution for the child and part selected. + If any data is not found on a part-level, the Media-level data is + returned. + If that also fails (e.g. for old trailers, None is returned) + + Output: + { + 'videocodec': xxx, e.g. 'h264' + 'resolution': xxx, e.g. '720' or '1080' + 'height': xxx, e.g. '816' + 'width': xxx, e.g. '1920' + 'aspectratio': xxx, e.g. '1.78' + 'bitrate': xxx, e.g. 10642 (an int!) + 'container': xxx e.g. 'mkv' + } + """ + + videocodec = self.getDataFromPartOrMedia('videoCodec') + resolution = self.getDataFromPartOrMedia('videoResolution') + height = self.getDataFromPartOrMedia('height') + width = self.getDataFromPartOrMedia('width') + aspectratio = self.getDataFromPartOrMedia('aspectratio') + bitrate = self.getDataFromPartOrMedia('bitrate') + container = self.getDataFromPartOrMedia('container') + + videoCodec = { + 'videocodec': videocodec, + 'resolution': resolution, + 'height': height, + 'width': width, + 'aspectratio': aspectratio, + 'bitrate': bitrate, + 'container': container + } + return videoCodec + def getMediaStreams(self): """ Returns the media streams @@ -1798,7 +1857,7 @@ class API(): Output: each track contains a dictionaries { 'video': videotrack-list, 'videocodec', 'height', 'width', - 'aspectratio', video3DFormat' + 'aspectratio', 'video3DFormat' 'audio': audiotrack-list, 'audiocodec', 'channels', 'audiolanguage' 'subtitle': list of subtitle languages (or "Unknown") @@ -1980,95 +2039,142 @@ class API(): allartworks['Primary'] = artwork return allartworks - def getTranscodeVideoPath(self, action, quality={}, subtitle={}, audioboost=None, partIndex=None, options={}): + def getTranscodeVideoPath(self, action, quality={}, subtitle={}, audioboost=None, options={}): """ Transcode Video support; returns the URL to get a media started Input: - action 'Transcode' OR any other string + action 'DirectPlay', 'DirectStream' or 'Transcode' quality: { 'videoResolution': 'resolution', 'videoQuality': 'quality', 'maxVideoBitrate': 'bitrate' } + (one or several of these options) subtitle {'selected', 'dontBurnIn', 'size'} audioboost e.g. 100 - partIndex Index number of media part, starting with 0 options dict() of PlexConnect-options as received from aTV Output: - final path to pull in PMS transcoder + final URL to pull in PMS transcoder TODO: mediaIndex """ - # path to item - transcodePath = self.server + \ - '/video/:/transcode/universal/start.m3u8?' - - ID = self.getKey() - if partIndex is not None: - path = self.server + '/library/metadata/' + ID - else: - path = self.item[self.child][0][self.part].attrib['key'] - args = { - 'session': self.clientId, - 'protocol': 'hls', # also seen: 'dash' - 'fastSeek': '1', - 'path': path, - 'mediaIndex': 0, # Probably refering to XML reply sheme - 'X-Plex-Client-Capabilities': "protocols=http-live-streaming," - "http-streaming-video," - "http-streaming-video-720p," - "http-streaming-video-1080p," - "http-mp4-streaming," - "http-mp4-video," - "http-mp4-video-720p," - "http-mp4-video-1080p;" + # Set Client capabilities + clientArgs = { + 'X-Plex-Client-Capabilities': + "protocols=shoutcast," + "http-live-streaming," + "http-streaming-video," + "http-streaming-video-720p," + "http-streaming-video-1080p," + "http-mp4-streaming," + "http-mp4-video," + "http-mp4-video-720p," + "http-mp4-video-1080p;" "videoDecoders=" - "h264{profile:high&resolution:1080&level:51};" + "h264{profile:high&resolution:1080&level:51}," + "h265{profile:high&resolution:1080&level:51}," + "mpeg1video," + "mpeg2video," + "mpeg4," + "msmpeg4," + "mjpeg," + "wmv2," + "wmv3," + "vc1," + "cinepak," + "h263;" "audioDecoders=" "mp3," - "aac{bitrate:160000}," - "ac3{channels:6}," - "dts{channels:6}" - # 'offset': 0 # Resume point - # 'directPlay': 0 # 1 if Kodi can also handle the container + "aac," + "ac3{bitrate:800000&channels:8}," + "dts{bitrate:800000&channels:8}," + "truehd," + "eac3," + "dca," + "mp2," + "pcm," + "wmapro," + "wmav2," + "wmavoice," + "wmalossless;" + } + xargs = PlexAPI().getXArgsDeviceInfo(options=options) + # For Direct Playing + if action == "DirectPlay": + path = self.item[self.child][0][self.part].attrib['key'] + transcodePath = self.server + path + # Be sure to have exactly ONE '?' in the path (might already have + # been returned, e.g. trailers!) + if '?' not in path: + transcodePath = transcodePath + '?' + url = transcodePath + \ + urlencode(clientArgs) + '&' + \ + urlencode(xargs) + return url + + # For Direct Streaming or Transcoding + transcodePath = self.server + \ + '/video/:/transcode/universal/start.m3u8?' + partCount = 0 + for parts in self.item[self.child][0]: + partCount = partCount + 1 + # Movie consists of several parts; grap one part + if partCount > 1: + path = self.item[self.child][0][self.part].attrib['key'] + # Movie consists of only one part + else: + path = self.item[self.child].attrib['key'] + args = { + 'path': path, + 'mediaIndex': 0, # Probably refering to XML reply sheme + 'partIndex': self.part, + 'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls' + 'offset': 0, # Resume point + 'fastSeek': 1 } # All the settings - if partIndex is not None: - args['partIndex'] = partIndex if subtitle: - args_update = { + argsUpdate = { 'subtitles': 'burn', 'subtitleSize': subtitle['size'], # E.g. 100 'skipSubtitles': subtitle['dontBurnIn'] # '1': shut off PMS } - args.update(args_update) self.logMsg( "Subtitle: selected %s, dontBurnIn %s, size %s" % (subtitle['selected'], subtitle['dontBurnIn'], subtitle['size']), 1 ) + args.update(argsUpdate) if audioboost: - args_update = { + argsUpdate = { 'audioBoost': audioboost } - args.update(args_update) self.logMsg("audioboost: %s" % audioboost, 1) - if action == 'Transcode': - # Possible Plex settings: - # 'videoResolution': vRes, - # 'maxVideoBitrate': mVB, - # : vQ - self.logMsg("Setting transcode quality to: %s" % quality, 1) - args['directStream'] = '0' - args.update(quality) - else: - args['directStream'] = '1' + args.update(argsUpdate) - xargs = PlexAPI().getXArgsDeviceInfo(options=options) - return transcodePath + urlencode(args) + '&' + urlencode(xargs) + if action == "DirectStream": + argsUpdate = { + 'directPlay': '0', + 'directStream': '1', + } + args.update(argsUpdate) + elif action == 'Transcode': + argsUpdate = { + 'directPlay': '0', + 'directStream': '0' + } + self.logMsg("Setting transcode quality to: %s" % quality, 1) + args.update(quality) + args.update(argsUpdate) + + url = transcodePath + \ + urlencode(clientArgs) + '&' + \ + urlencode(xargs) + '&' + \ + urlencode(args) + return url def adjustResume(self, resume_seconds): resume = 0 @@ -2150,4 +2256,4 @@ class API(): """ Returns the parts of the specified video child in the XML response """ - return self.item[self.child][0] \ No newline at end of file + return self.item[self.child][0] diff --git a/resources/lib/playbackutils.py b/resources/lib/playbackutils.py index 68c8ec6e..edc6f41f 100644 --- a/resources/lib/playbackutils.py +++ b/resources/lib/playbackutils.py @@ -80,6 +80,7 @@ class PlaybackUtils(): propertiesPlayback = utils.window('emby_playbackProps', windowid=10101) == "true" introsPlaylist = False + partsPlaylist = False dummyPlaylist = False self.logMsg("Playlist start position: %s" % startPos, 1) @@ -117,7 +118,7 @@ class PlaybackUtils(): if playListSize > 1: getTrailers = True if utils.settings('askCinema') == "true": - resp = xbmcgui.Dialog().yesno("Emby Cinema Mode", "Play trailers?") + resp = xbmcgui.Dialog().yesno(self.addonName, "Play trailers?") if not resp: # User selected to not play trailers getTrailers = False @@ -158,6 +159,7 @@ class PlaybackUtils(): partcount = len(parts) if partcount > 1: # Only add to the playlist after intros have played + partsPlaylist = True i = 0 for part in parts: API.setPartNumber(i) @@ -176,6 +178,7 @@ class PlaybackUtils(): self.pl.verifyPlaylist() currentPosition += 1 i = i + 1 + API.setPartNumber(0) if dummyPlaylist: # Added a dummy file to the playlist, @@ -201,14 +204,15 @@ class PlaybackUtils(): self.setProperties(playurl, listitem) ############### PLAYBACK ################ - + customPlaylist = utils.window('emby_customPlaylist', windowid=10101) if homeScreen and seektime: self.logMsg("Play as a widget item.", 1) self.setListItem(listitem) xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, listitem) - elif ((introsPlaylist and utils.window('emby_customPlaylist', windowid=10101) == "true") or - (homeScreen and not sizePlaylist)): + elif ((introsPlaylist and customPlaylist == "true") or + (homeScreen and not sizePlaylist) or + (partsPlaylist and customPlaylist == "true")): # Playlist was created just now, play it. self.logMsg("Play playlist.", 1) xbmc.Player().play(playlist, startpos=startPos) diff --git a/resources/lib/playutils.py b/resources/lib/playutils.py index bb0118e3..a755f5e1 100644 --- a/resources/lib/playutils.py +++ b/resources/lib/playutils.py @@ -38,7 +38,6 @@ class PlayUtils(): def getPlayUrl(self, child=0, partIndex=None): - item = self.item # NO, I am not very fond of this construct! self.API.setChildNumber(child) if partIndex is not None: @@ -51,34 +50,27 @@ class PlayUtils(): # playurl = self.httpPlay() # utils.window('emby_%s.playmethod' % playurl, value="DirectStream") - # elif self.isDirectPlay(): + if self.isDirectPlay(): + self.logMsg("File is direct playing.", 1) + playurl = self.API.getTranscodeVideoPath('DirectPlay') + playurl = playurl.encode('utf-8') + # Set playmethod property + utils.window('emby_%s.playmethod' % playurl, value="DirectPlay") - # self.logMsg("File is direct playing.", 1) - # playurl = self.directPlay() - # playurl = playurl.encode('utf-8') - # # Set playmethod property - # utils.window('emby_%s.playmethod' % playurl, value="DirectPlay") - - if self.isDirectStream(): - + elif self.isDirectStream(): self.logMsg("File is direct streaming.", 1) - playurl = self.API.getTranscodeVideoPath( - 'direct', - partIndex=partIndex - ) + playurl = self.API.getTranscodeVideoPath('DirectStream') # Set playmethod property utils.window('emby_%s.playmethod' % playurl, value="DirectStream") elif self.isTranscoding(): - self.logMsg("File is transcoding.", 1) quality = { - 'bitrate': self.getBitrate() + 'maxVideoBitrate': self.getBitrate() } playurl = self.API.getTranscodeVideoPath( 'Transcode', - quality=quality, - partIndex=partIndex + quality=quality ) # Set playmethod property utils.window('emby_%s.playmethod' % playurl, value="Transcode") @@ -102,57 +94,19 @@ class PlayUtils(): def isDirectPlay(self): - item = self.item - # Requirement: Filesystem, Accessible path if utils.settings('playFromStream') == "true": # User forcing to play via HTTP - self.logMsg("Can't direct play, play from HTTP enabled.", 1) + self.logMsg("Can't direct play, user enabled play from HTTP.", 1) return False - if (utils.settings('transcodeH265') == "true" and - item['MediaSources'][0]['Name'].startswith("1080P/H265")): - # Avoid H265 1080p - self.logMsg("Option to transcode 1080P/H265 enabled.", 1) + if not self.h265enabled(): return False - canDirectPlay = item['MediaSources'][0]['SupportsDirectPlay'] - # Make sure direct play is supported by the server - if not canDirectPlay: - self.logMsg("Can't direct play, server doesn't allow/support it.", 1) + # Found with e.g. trailers + if self.API.getDataFromPartOrMedia('optimizedForStreaming') == '1': return False - location = item['LocationType'] - if location == "FileSystem": - # Verify the path - if not self.fileExists(): - self.logMsg("Unable to direct play.") - try: - count = int(utils.settings('failCount')) - except ValueError: - count = 0 - self.logMsg("Direct play failed: %s times." % count, 1) - - if count < 2: - # Let the user know that direct play failed - utils.settings('failCount', value=str(count+1)) - xbmcgui.Dialog().notification( - heading="Emby server", - message="Unable to direct play.", - icon="special://home/addons/plugin.video.plexkodiconnect/icon.png", - sound=False) - elif utils.settings('playFromStream') != "true": - # Permanently set direct stream as true - utils.settings('playFromStream', value="true") - utils.settings('failCount', value="0") - xbmcgui.Dialog().notification( - heading="Emby server", - message=("Direct play failed 3 times. Enabled play " - "from HTTP in the add-on settings."), - icon="special://home/addons/plugin.video.plexkodiconnect/icon.png", - sound=False) - return False - return True def directPlay(self): @@ -164,15 +118,6 @@ class PlayUtils(): except (IndexError, KeyError): playurl = item['Path'] - if item.get('VideoType'): - # Specific format modification - type = item['VideoType'] - - if type == "Dvd": - playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl - elif type == "Bluray": - playurl = "%s/BDMV/index.bdmv" % playurl - # Assign network protocol if playurl.startswith('\\\\'): playurl = playurl.replace("\\\\", "smb://") @@ -206,15 +151,21 @@ class PlayUtils(): self.logMsg("Failed to find file.") return False - def isDirectStream(self): - - item = self.item - - if (utils.settings('transcodeH265') == "true" and - item[0][0].attrib('videoCodec').startswith("h265") and - item[0][0].attrib('videoResolution').startswith("1080")): + def h265enabled(self): + videoCodec = self.API.getVideoCodec() + codec = videoCodec['videocodec'] + resolution = videoCodec['resolution'] + if ((utils.settings('transcodeH265') == "true") and + ("h265" in codec) and + (resolution == "1080")): # Avoid H265 1080p - self.logMsg("Option to transcode 1080P/H265 enabled.", 1) + self.logMsg("Option to transcode 1080P/H265 enabled.", 0) + return False + else: + return True + + def isDirectStream(self): + if not self.h265enabled(): return False # Requirement: BitRate, supported encoding @@ -229,7 +180,6 @@ class PlayUtils(): if not self.isNetworkSufficient(): self.logMsg("The network speed is insufficient to direct stream file.", 1) return False - return True def directStream(self): @@ -256,11 +206,7 @@ class PlayUtils(): settings = self.getBitrate() sourceBitrate = self.API.getBitrate() - if not sourceBitrate: - self.logMsg("Bitrate value is missing.", 0) - return True - self.logMsg("The add-on settings bitrate is: %s, the video bitrate required is: %s" - % (settings, sourceBitrate), 1) + self.logMsg("The add-on settings bitrate is: %s, the video bitrate required is: %s" % (settings, sourceBitrate), 1) if settings < sourceBitrate: return False return True