Movies now direct playing. Trailers playing but always transcoding
This commit is contained in:
parent
93ad4ae0cb
commit
cef41188e0
3 changed files with 200 additions and 144 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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():
|
||||
|
||||
# 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():
|
||||
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")
|
||||
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue