jellyfin-kodi/resources/lib/playutils.py

653 lines
23 KiB
Python
Raw Normal View History

2016-03-31 17:41:06 +00:00
# -*- coding: utf-8 -*-
#################################################################################################
import logging
import sys
2016-03-31 17:41:06 +00:00
import xbmc
import xbmcgui
import xbmcvfs
import clientinfo
import downloadutils
from utils import window, settings, language as lang
#################################################################################################
log = logging.getLogger("EMBY."+__name__)
2016-03-31 17:41:06 +00:00
#################################################################################################
class PlayUtils():
def __init__(self, item):
self.item = item
self.clientInfo = clientinfo.ClientInfo()
2016-06-16 21:24:07 +00:00
self.userid = window('emby_currUser')
self.server = window('emby_server%s' % self.userid)
self.doUtils = downloadutils.DownloadUtils().downloadUrl
2016-03-31 17:41:06 +00:00
def getPlayUrlNew(self):
'''
New style to retrieve the best playback method based on sending the profile to the server
Based on capabilities the correct path is returned, including livestreams that need to be opened by the server
'''
playurl = None
pbinfo = self.getPlaybackInfo()
if pbinfo:
xbmc.log("getPlayUrl pbinfo: %s" %(pbinfo))#debug
if pbinfo["SupportsDirectPlay"]:
playmethod = "DirectPlay"
playurl = pbinfo["Path"]
elif pbinfo["SupportsDirectStream"]:
playmethod = "DirectStream"
playurl = self.directStream()
else:
playmethod = "Transcode"
if pbinfo.get("TranscodingUrl"):
playurl = self.server + pbinfo["TranscodingUrl"]
else:
playurl = self.transcoding()
xbmc.log("getPlayUrl playmethod: %s - playurl: %s" %(playmethod, playurl))#debug
window('emby_%s.playmethod' % playurl, value=playmethod)
if pbinfo["RequiresClosing"] and pbinfo.get('LiveStreamId'):
window('emby_%s.livestreamid' % playurl, value=pbinfo["LiveStreamId"])
return playurl
def getPlayUrl(self):
#TEMP !
if settings('experimentalPlayback') == "true":
return self.getPlayUrlNew()
2016-03-31 17:41:06 +00:00
playurl = None
2016-06-16 21:24:07 +00:00
if (self.item.get('Type') in ("Recording", "TvChannel") and self.item.get('MediaSources')
and self.item['MediaSources'][0]['Protocol'] == "Http"):
2016-03-31 17:41:06 +00:00
# Play LiveTV or recordings
log.info("File protocol is http (livetv).")
playurl = "%s/emby/Videos/%s/stream.ts?audioCodec=copy&videoCodec=copy" % (self.server, self.item['Id'])
window('emby_%s.playmethod' % playurl, value="DirectPlay")
2016-03-31 17:41:06 +00:00
2016-03-31 17:46:51 +00:00
elif self.item.get('MediaSources') and self.item['MediaSources'][0]['Protocol'] == "Http":
2016-03-31 17:41:06 +00:00
# Only play as http, used for channels, or online hosting of content
log.info("File protocol is http.")
2016-03-31 17:41:06 +00:00
playurl = self.httpPlay()
window('emby_%s.playmethod' % playurl, value="DirectStream")
elif self.isDirectPlay():
log.info("File is direct playing.")
2016-03-31 17:41:06 +00:00
playurl = self.directPlay()
playurl = playurl.encode('utf-8')
# Set playmethod property
window('emby_%s.playmethod' % playurl, value="DirectPlay")
elif self.isDirectStream():
log.info("File is direct streaming.")
2016-03-31 17:41:06 +00:00
playurl = self.directStream()
# Set playmethod property
window('emby_%s.playmethod' % playurl, value="DirectStream")
elif self.isTranscoding():
log.info("File is transcoding.")
2016-03-31 17:41:06 +00:00
playurl = self.transcoding()
# Set playmethod property
window('emby_%s.playmethod' % playurl, value="Transcode")
return playurl
def httpPlay(self):
# Audio, Video, Photo
2016-03-31 17:46:51 +00:00
itemid = self.item['Id']
mediatype = self.item['MediaType']
2016-03-31 17:41:06 +00:00
if mediatype == "Audio":
2016-03-31 17:49:55 +00:00
playurl = "%s/emby/Audio/%s/stream" % (self.server, itemid)
2016-03-31 17:41:06 +00:00
else:
2016-03-31 17:49:55 +00:00
playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, itemid)
2016-03-31 17:41:06 +00:00
return playurl
def isDirectPlay(self):
# Requirement: Filesystem, Accessible path
if settings('playFromStream') == "true":
# User forcing to play via HTTP
log.info("Can't direct play, play from HTTP enabled.")
2016-03-31 17:41:06 +00:00
return False
2016-03-31 17:46:51 +00:00
videotrack = self.item['MediaSources'][0]['Name']
2016-03-31 17:41:06 +00:00
transcodeH265 = settings('transcodeH265')
videoprofiles = [x['Profile'] for x in self.item['MediaSources'][0]['MediaStreams'] if 'Profile' in x]
2016-06-16 21:24:07 +00:00
transcodeHi10P = settings('transcodeHi10P')
if transcodeHi10P == "true" and "H264" in videotrack and "High 10" in videoprofiles:
return False
2016-03-31 17:41:06 +00:00
if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack):
# Avoid H265/HEVC depending on the resolution
resolution = int(videotrack.split("P", 1)[0])
res = {
'1': 480,
'2': 720,
'3': 1080
}
log.info("Resolution is: %sP, transcode for resolution: %sP+"
% (resolution, res[transcodeH265]))
2016-03-31 17:41:06 +00:00
if res[transcodeH265] <= resolution:
return False
2016-03-31 17:46:51 +00:00
canDirectPlay = self.item['MediaSources'][0]['SupportsDirectPlay']
2016-03-31 17:41:06 +00:00
# Make sure direct play is supported by the server
if not canDirectPlay:
log.info("Can't direct play, server doesn't allow/support it.")
2016-03-31 17:41:06 +00:00
return False
2016-03-31 17:46:51 +00:00
location = self.item['LocationType']
2016-03-31 17:41:06 +00:00
if location == "FileSystem":
# Verify the path
if not self.fileExists():
log.info("Unable to direct play.")
log.info(self.directPlay())
xbmcgui.Dialog().ok(
heading=lang(29999),
line1=lang(33011),
line2=(self.directPlay()))
sys.exit()
2016-03-31 17:41:06 +00:00
return True
def directPlay(self):
try:
2016-03-31 17:46:51 +00:00
playurl = self.item['MediaSources'][0]['Path']
2016-03-31 17:41:06 +00:00
except (IndexError, KeyError):
2016-03-31 17:46:51 +00:00
playurl = self.item['Path']
2016-03-31 17:41:06 +00:00
2016-03-31 17:46:51 +00:00
if self.item.get('VideoType'):
2016-03-31 17:41:06 +00:00
# Specific format modification
2016-03-31 17:49:55 +00:00
if self.item['VideoType'] == "Dvd":
2016-03-31 17:41:06 +00:00
playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl
2016-03-31 17:49:55 +00:00
elif self.item['VideoType'] == "BluRay":
2016-03-31 17:41:06 +00:00
playurl = "%s/BDMV/index.bdmv" % playurl
# Assign network protocol
if playurl.startswith('\\\\'):
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
def fileExists(self):
if 'Path' not in self.item:
# File has no path defined in server
return False
# Convert path to direct play
path = self.directPlay()
log.info("Verifying path: %s" % path)
2016-03-31 17:41:06 +00:00
if xbmcvfs.exists(path):
log.info("Path exists.")
2016-03-31 17:41:06 +00:00
return True
elif ":" not in path:
log.info("Can't verify path, assumed linux. Still try to direct play.")
2016-03-31 17:41:06 +00:00
return True
else:
log.info("Failed to find file.")
2016-03-31 17:41:06 +00:00
return False
def isDirectStream(self):
2016-03-31 17:46:51 +00:00
videotrack = self.item['MediaSources'][0]['Name']
2016-06-18 18:56:56 +00:00
transcodeH265 = settings('transcodeH265')
videoprofiles = [x['Profile'] for x in self.item['MediaSources'][0]['MediaStreams'] if 'Profile' in x]
2016-06-18 18:56:56 +00:00
transcodeHi10P = settings('transcodeHi10P')
if transcodeHi10P == "true" and "H264" in videotrack and "High 10" in videoprofiles:
return False
2016-03-31 17:41:06 +00:00
if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack):
# Avoid H265/HEVC depending on the resolution
resolution = int(videotrack.split("P", 1)[0])
res = {
'1': 480,
'2': 720,
'3': 1080
}
log.info("Resolution is: %sP, transcode for resolution: %sP+"
% (resolution, res[transcodeH265]))
2016-03-31 17:41:06 +00:00
if res[transcodeH265] <= resolution:
return False
# Requirement: BitRate, supported encoding
2016-03-31 17:46:51 +00:00
canDirectStream = self.item['MediaSources'][0]['SupportsDirectStream']
2016-03-31 17:41:06 +00:00
# Make sure the server supports it
if not canDirectStream:
return False
# Verify the bitrate
if not self.isNetworkSufficient():
log.info("The network speed is insufficient to direct stream file.")
2016-03-31 17:41:06 +00:00
return False
return True
def directStream(self):
2016-03-31 17:46:51 +00:00
if 'Path' in self.item and self.item['Path'].endswith('.strm'):
2016-03-31 17:41:06 +00:00
# Allow strm loading when direct streaming
playurl = self.directPlay()
2016-03-31 17:52:36 +00:00
elif self.item['Type'] == "Audio":
playurl = "%s/emby/Audio/%s/stream.mp3" % (self.server, self.item['Id'])
2016-03-31 17:41:06 +00:00
else:
2016-06-28 04:55:14 +00:00
playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, self.item['Id'])
2016-03-31 17:41:06 +00:00
return playurl
def isNetworkSufficient(self):
settings = self.getBitrate()*1000
try:
sourceBitrate = int(self.item['MediaSources'][0]['Bitrate'])
except (KeyError, TypeError):
log.info("Bitrate value is missing.")
2016-03-31 17:41:06 +00:00
else:
log.info("The add-on settings bitrate is: %s, the video bitrate required is: %s"
% (settings, sourceBitrate))
2016-03-31 17:41:06 +00:00
if settings < sourceBitrate:
return False
return True
def isTranscoding(self):
# Make sure the server supports it
2016-03-31 17:52:36 +00:00
if not self.item['MediaSources'][0]['SupportsTranscoding']:
2016-03-31 17:41:06 +00:00
return False
return True
def transcoding(self):
2016-03-31 17:46:51 +00:00
if 'Path' in self.item and self.item['Path'].endswith('.strm'):
2016-03-31 17:41:06 +00:00
# Allow strm loading when transcoding
playurl = self.directPlay()
else:
2016-03-31 17:46:51 +00:00
itemid = self.item['Id']
deviceId = self.clientInfo.get_device_id()
2016-03-31 17:41:06 +00:00
playurl = (
"%s/emby/Videos/%s/master.m3u8?MediaSourceId=%s"
% (self.server, itemid, itemid)
)
playurl = (
"%s&VideoCodec=h264&AudioCodec=ac3&MaxAudioChannels=6&deviceId=%s&VideoBitrate=%s"
% (playurl, deviceId, self.getBitrate()*1000))
return playurl
def getBitrate(self):
# get the addon video quality
bitrate = {
'0': 664,
'1': 996,
'2': 1320,
'3': 2000,
'4': 3200,
'5': 4700,
'6': 6200,
'7': 7700,
'8': 9200,
'9': 10700,
'10': 12200,
'11': 13700,
'12': 15200,
'13': 16700,
'14': 18200,
'15': 20000,
'16': 25000,
'17': 30000,
'18': 35000,
2016-03-31 17:41:06 +00:00
'16': 40000,
'17': 100000,
'18': 1000000
}
# max bit rate supported by server (max signed 32bit integer)
2016-06-18 18:56:56 +00:00
return bitrate.get(settings('videoBitrate'), 2147483)
2016-03-31 17:41:06 +00:00
def audioSubsPref(self, url, listitem):
dialog = xbmcgui.Dialog()
# For transcoding only
# Present the list of audio to select from
audioStreamsList = {}
audioStreams = []
audioStreamsChannelsList = {}
subtitleStreamsList = {}
subtitleStreams = ['No subtitles']
downloadableStreams = []
selectAudioIndex = ""
selectSubsIndex = ""
playurlprefs = "%s" % url
try:
2016-03-31 17:46:51 +00:00
mediasources = self.item['MediaSources'][0]
2016-03-31 17:41:06 +00:00
mediastreams = mediasources['MediaStreams']
except (TypeError, KeyError, IndexError):
return
for stream in mediastreams:
# Since Emby returns all possible tracks together, have to sort them.
index = stream['Index']
2016-03-31 17:52:36 +00:00
if 'Audio' in stream['Type']:
2016-03-31 17:41:06 +00:00
codec = stream['Codec']
channelLayout = stream.get('ChannelLayout', "")
try:
track = "%s - %s - %s %s" % (index, stream['Language'], codec, channelLayout)
except:
track = "%s - %s %s" % (index, codec, channelLayout)
audioStreamsChannelsList[index] = stream['Channels']
audioStreamsList[track] = index
audioStreams.append(track)
2016-03-31 17:52:36 +00:00
elif 'Subtitle' in stream['Type']:
2016-03-31 17:41:06 +00:00
try:
track = "%s - %s" % (index, stream['Language'])
except:
track = "%s - %s" % (index, stream['Codec'])
default = stream['IsDefault']
forced = stream['IsForced']
downloadable = stream['IsTextSubtitleStream']
if default:
track = "%s - Default" % track
if forced:
track = "%s - Forced" % track
if downloadable:
downloadableStreams.append(index)
subtitleStreamsList[track] = index
subtitleStreams.append(track)
if len(audioStreams) > 1:
resp = dialog.select(lang(33013), audioStreams)
if resp > -1:
# User selected audio
selected = audioStreams[resp]
selectAudioIndex = audioStreamsList[selected]
playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
else: # User backed out of selection
playurlprefs += "&AudioStreamIndex=%s" % mediasources['DefaultAudioStreamIndex']
else: # There's only one audiotrack.
selectAudioIndex = audioStreamsList[audioStreams[0]]
playurlprefs += "&AudioStreamIndex=%s" % selectAudioIndex
if len(subtitleStreams) > 1:
resp = dialog.select(lang(33014), subtitleStreams)
if resp == 0:
# User selected no subtitles
pass
elif resp > -1:
# User selected subtitles
selected = subtitleStreams[resp]
selectSubsIndex = subtitleStreamsList[selected]
# Load subtitles in the listitem if downloadable
if selectSubsIndex in downloadableStreams:
2016-03-31 17:46:51 +00:00
itemid = self.item['Id']
2016-03-31 17:41:06 +00:00
url = [("%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
% (self.server, itemid, itemid, selectSubsIndex))]
2016-07-29 09:12:30 +00:00
log.info("Set up subtitles: %s %s" % (selectSubsIndex, url))
2016-03-31 17:41:06 +00:00
listitem.setSubtitles(url)
else:
# Burn subtitles
playurlprefs += "&SubtitleStreamIndex=%s" % selectSubsIndex
else: # User backed out of selection
playurlprefs += "&SubtitleStreamIndex=%s" % mediasources.get('DefaultSubtitleStreamIndex', "")
# Get number of channels for selected audio track
audioChannels = audioStreamsChannelsList.get(selectAudioIndex, 0)
if audioChannels > 2:
playurlprefs += "&AudioBitrate=384000"
else:
playurlprefs += "&AudioBitrate=192000"
return playurlprefs
def getPlaybackInfo(self):
#Gets the playback Info for the current item
url = "{server}/emby/Items/%s/PlaybackInfo?format=json" %self.item['Id']
body = {
"UserId": self.userid,
"DeviceProfile": self.getDeviceProfile(),
"StartTimeTicks": 0, #TODO
"AudioStreamIndex": None, #TODO
"SubtitleStreamIndex": None, #TODO
"MediaSourceId": None,
"LiveStreamId": None
}
pbinfo = self.doUtils(url, postBody=body, action_type="POST")
xbmc.log("getPlaybackInfo: %s" %pbinfo)#debug
mediaSource = self.getOptimalMediaSource(pbinfo["MediaSources"])
if mediaSource and mediaSource["RequiresOpening"]:
mediaSource = self.getLiveStream(pbinfo["PlaySessionId"], mediaSource)
return mediaSource
def getOptimalMediaSource(self, mediasources):
'''
Select the best possible mediasource for playback
We select the best stream based on a score
TODO: Incorporate user preferences for best stream selection
'''
bestSource = {}
lastScore = 0
for mediasource in mediasources:
score = 0
#transform filepath to kodi compliant
if mediasource["Protocol"] == "File":
mediasource["Path"] = self.transformFilePath(mediasource["Path"])
#The bitrate and video dimension is an important quality argument so also base our score on that
score += mediasource.get("Bitrate",0)
for stream in mediasource["MediaStreams"]:
if stream["Type"] == "Video" and stream.get("Width"):
#some streams report high bitrate but have no video width so stream with video width has highest score
#this is especially true for videos in channels, like trailers
score += stream["Width"] * 10000
#directplay has the highest score
if mediasource["SupportsDirectPlay"] and self.supportsDirectPlay(mediasource):
score += 100000000
#directstream also scores well, the transcode option has no points as its a last resort
if mediasource["SupportsDirectStream"]:
score += 5000000
#TODO: If our user has any specific preferences we can alter the score
# For now we just select the highest quality as our prefered score
if score >= lastScore:
lastScore = score
bestSource = mediasource
xbmc.log("getOptimalMediaSource: %s" %bestSource)#debug
return bestSource
def getLiveStream(self, playSessionId, mediaSource):
url = "{server}/emby/LiveStreams/Open?format=json"
body = {
"UserId": self.userid,
"DeviceProfile": self.getDeviceProfile(),
"ItemId": self.item["Id"],
"PlaySessionId": playSessionId,
"OpenToken": mediaSource["OpenToken"],
"StartTimeTicks": 0, #TODO
"AudioStreamIndex": None, #TODO
"SubtitleStreamIndex": None #TODO
}
streaminfo = self.doUtils(url, postBody=body, action_type="POST")
xbmc.log("getLiveStream: %s" %streaminfo)#debug
streaminfo["MediaSource"]["SupportsDirectPlay"] = self.supportsDirectPlay(streaminfo["MediaSource"])
return streaminfo["MediaSource"]
def transformFilePath(self, playurl):
#Transform filepath to Kodi compatible path
if self.item.get('VideoType'):
# Specific format modification
if self.item['VideoType'] == "Dvd":
playurl = "%s/VIDEO_TS/VIDEO_TS.IFO" % playurl
elif self.item['VideoType'] == "BluRay":
playurl = "%s/BDMV/index.bdmv" % playurl
# Assign network protocol
if playurl.startswith('\\\\'):
playurl = playurl.replace("\\\\", "smb://")
playurl = playurl.replace("\\", "/")
return playurl
def supportsDirectPlay(self, mediasource):
#Figure out if the path can be directly played as the bool returned from the server response is not 100% reliable
return xbmcvfs.exists(mediasource["Path"])
def getDeviceProfile(self):
return {
"Name": "Kodi",
"MaxStreamingBitrate": self.getBitrate()*1000,
"MusicStreamingTranscodingBitrate": 1280000,
"TimelineOffsetSeconds": 5,
"Identification": {
"ModelName": "Kodi",
"Headers": [
{
"Name": "User-Agent",
"Value": "Kodi",
"Match": 2
}
]
},
"TranscodingProfiles": [
{
"Container": "mp3",
"AudioCodec": "mp3",
"Type": 0
},
{
"Container": "ts",
"AudioCodec": "aac",
"VideoCodec": "h264",
"Type": 1
},
{
"Container": "jpeg",
"Type": 2
}
],
"DirectPlayProfiles": [
{
"Container": "",
"Type": 0
},
{
"Container": "",
"Type": 1
},
{
"Container": "",
"Type": 2
}
],
"ResponseProfiles": [],
"ContainerProfiles": [],
"CodecProfiles": [],
"SubtitleProfiles": [
{
"Format": "srt",
"Method": 2
},
{
"Format": "sub",
"Method": 2
},
{
"Format": "srt",
"Method": 1
},
{
"Format": "ass",
"Method": 1,
"DidlMode": ""
},
{
"Format": "ssa",
"Method": 1,
"DidlMode": ""
},
{
"Format": "smi",
"Method": 1,
"DidlMode": ""
},
{
"Format": "dvdsub",
"Method": 1,
"DidlMode": ""
},
{
"Format": "pgs",
"Method": 1,
"DidlMode": ""
},
{
"Format": "pgssub",
"Method": 1,
"DidlMode": ""
},
{
"Format": "sub",
"Method": 1,
"DidlMode": ""
}
]
}