jellyfin-kodi/resources/lib/playutils.py

708 lines
25 KiB
Python

# -*- coding: utf-8 -*-
#################################################################################################
import logging
import sys
import urllib
import xbmc
import xbmcgui
import xbmcvfs
import clientinfo
import downloadutils
import read_embyserver as embyserver
from utils import window, settings, language as lang
#################################################################################################
log = logging.getLogger("EMBY."+__name__)
#################################################################################################
class PlayUtils():
def __init__(self, item):
self.item = item
self.clientInfo = clientinfo.ClientInfo()
self.emby = embyserver.Read_EmbyServer()
self.userid = window('emby_currUser')
self.server = window('emby_server%s' % self.userid)
self.doUtils = downloadutils.DownloadUtils().downloadUrl
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
TODO: Close livestream if needed (RequiresClosing in livestream source)
'''
playurl = None
pbinfo = self.getPlaybackInfo()
if pbinfo:
xbmc.log("getPlayUrl pbinfo: %s" %(pbinfo))
if pbinfo["Protocol"] == "SupportsDirectPlay":
playmethod = "DirectPlay"
elif pbinfo["Protocol"] == "SupportsDirectStream":
playmethod = "DirectStream"
elif pbinfo.get('LiveStreamId'):
playmethod = "LiveStream"
else:
playmethod = "Transcode"
playurl = pbinfo["Path"]
xbmc.log("getPlayUrl playmethod: %s - playurl: %s" %(playmethod, playurl))
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):
# log filename, used by other addons eg subtitles which require the file name
try:
window('embyfilename', value=self.directPlay())
except:
log.info("Could not get file path for embyfilename window prop")
playurl = None
user_token = downloadutils.DownloadUtils().get_token()
if (self.item.get('Type') in ("Recording", "TvChannel") and self.item.get('MediaSources')
and self.item['MediaSources'][0]['Protocol'] == "Http"):
# 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'])
playurl += "&api_key=" + str(user_token)
window('emby_%s.playmethod' % playurl, value="DirectPlay")
elif self.item.get('MediaSources') and self.item['MediaSources'][0]['Protocol'] == "Http":
# Only play as http, used for channels, or online hosting of content
log.info("File protocol is http.")
playurl = self.httpPlay()
window('emby_%s.playmethod' % playurl, value="DirectStream")
elif self.isDirectPlay():
log.info("File is direct playing.")
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.")
playurl = self.directStream()
playurl = playurl.encode('utf-8')
# Set playmethod property
window('emby_%s.playmethod' % playurl, value="DirectStream")
elif self.isTranscoding():
log.info("File is transcoding.")
playurl = self.transcoding()
# Set playmethod property
window('emby_%s.playmethod' % playurl, value="Transcode")
return playurl
def httpPlay(self):
# Audio, Video, Photo
itemid = self.item['Id']
mediatype = self.item['MediaType']
if mediatype == "Audio":
playurl = "%s/emby/Audio/%s/stream?" % (self.server, itemid)
else:
playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, itemid)
user_token = downloadutils.DownloadUtils().get_token()
playurl += "&api_key=" + str(user_token)
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.")
return False
videotrack = self.item['MediaSources'][0]['Name']
transcodeH265 = settings('transcodeH265')
videoprofiles = [x['Profile'] for x in self.item['MediaSources'][0]['MediaStreams'] if 'Profile' in x]
transcodeHi10P = settings('transcodeHi10P')
if transcodeHi10P == "true" and "H264" in videotrack and "High 10" in videoprofiles:
return False
if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack):
# Avoid H265/HEVC depending on the resolution
try:
resolution = int(videotrack.split("P", 1)[0])
except ValueError: # 4k resolution
resolution = 3064
res = {
'1': 480,
'2': 720,
'3': 1080
}
log.info("Resolution is: %sP, transcode for resolution: %sP+"
% (resolution, res[transcodeH265]))
if res[transcodeH265] <= resolution:
return False
canDirectPlay = self.item['MediaSources'][0]['SupportsDirectPlay']
# Make sure direct play is supported by the server
if not canDirectPlay:
log.info("Can't direct play, server doesn't allow/support it.")
return False
# Verify screen resolution
if self.resolutionConflict():
log.info("Can't direct play, resolution limit is enabled")
return False
location = self.item['LocationType']
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()
return True
def directPlay(self):
try:
playurl = self.item['MediaSources'][0]['Path']
except (IndexError, KeyError):
playurl = self.item['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("\\", "/")
if "apple.com" in playurl:
USER_AGENT = "QuickTime/7.7.4"
playurl += "?|User-Agent=%s" % USER_AGENT
# Strm
if playurl.endswith('.strm'):
playurl = urllib.urlencode(playurl)
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)
if xbmcvfs.exists(path):
log.info("Path exists.")
return True
elif ":" not in path:
log.info("Can't verify path, assumed linux. Still try to direct play.")
return True
else:
log.info("Failed to find file.")
return False
def isDirectStream(self):
videotrack = self.item['MediaSources'][0]['Name']
transcodeH265 = settings('transcodeH265')
videoprofiles = [x['Profile'] for x in self.item['MediaSources'][0]['MediaStreams'] if 'Profile' in x]
transcodeHi10P = settings('transcodeHi10P')
if transcodeHi10P == "true" and "H264" in videotrack and "High 10" in videoprofiles:
return False
if transcodeH265 in ("1", "2", "3") and ("HEVC" in videotrack or "H265" in videotrack):
# Avoid H265/HEVC depending on the resolution
try:
resolution = int(videotrack.split("P", 1)[0])
except ValueError: # 4k resolution
resolution = 3064
res = {
'1': 480,
'2': 720,
'3': 1080
}
log.info("Resolution is: %sP, transcode for resolution: %sP+"
% (resolution, res[transcodeH265]))
if res[transcodeH265] <= resolution:
return False
# Requirement: BitRate, supported encoding
canDirectStream = self.item['MediaSources'][0]['SupportsDirectStream']
# 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.")
return False
# Verify screen resolution
if self.resolutionConflict():
log.info("Can't direct stream, resolution limit is enabled")
return False
return True
def directStream(self):
if 'Path' in self.item and self.item['Path'].endswith('.strm'):
# Allow strm loading when direct streaming
playurl = self.directPlay()
elif self.item['Type'] == "Audio":
playurl = "%s/emby/Audio/%s/stream.mp3?" % (self.server, self.item['Id'])
else:
playurl = "%s/emby/Videos/%s/stream?static=true" % (self.server, self.item['Id'])
user_token = downloadutils.DownloadUtils().get_token()
playurl += "&api_key=" + str(user_token)
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.")
else:
log.info("The add-on settings bitrate is: %s, the video bitrate required is: %s"
% (settings, sourceBitrate))
if settings < sourceBitrate:
return False
return True
def isTranscoding(self):
# Make sure the server supports it
if not self.item['MediaSources'][0]['SupportsTranscoding']:
return False
return True
def transcoding(self):
if 'Path' in self.item and self.item['Path'].endswith('.strm'):
# Allow strm loading when transcoding
playurl = self.directPlay()
else:
itemid = self.item['Id']
deviceId = self.clientInfo.get_device_id()
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))
# Limit to 8 bit if user selected transcode Hi10P
transcodeHi10P = settings('transcodeHi10P')
if transcodeHi10P == "true":
playurl = "%s&MaxVideoBitDepth=8" % playurl
# Adjust video resolution
if self.resolutionConflict():
screenRes = self.getScreenResolution()
playurl = "%s&maxWidth=%s&maxHeight=%s" % (playurl, screenRes['width'], screenRes['height'])
user_token = downloadutils.DownloadUtils().get_token()
playurl += "&api_key=" + str(user_token)
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,
'16': 40000,
'17': 100000,
'18': 1000000
}
# max bit rate supported by server (max signed 32bit integer)
return bitrate.get(settings('videoBitrate'), 2147483)
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:
mediasources = self.item['MediaSources'][0]
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']
if 'Audio' in stream['Type']:
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)
elif 'Subtitle' in stream['Type']:
try:
track = "%s - %s" % (index, stream['Language'])
except:
track = "%s - %s" % (index, stream['Codec'])
default = stream['IsDefault']
forced = stream['IsForced']
downloadable = stream['SupportsExternalStream']
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]
settings = self.emby.get_server_transcoding_settings()
# Load subtitles in the listitem if downloadable
if settings['EnableSubtitleExtraction'] and selectSubsIndex in downloadableStreams:
itemid = self.item['Id']
url = [("%s/Videos/%s/%s/Subtitles/%s/Stream.srt"
% (self.server, itemid, itemid, selectSubsIndex))]
log.info("Set up subtitles: %s %s" % (selectSubsIndex, url))
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)
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
Because we posted our deviceprofile to the server,
only streams will be returned that can actually be played by this client so no need to check bitrates etc.
'''
preferredStreamOrder = ["SupportsDirectPlay","SupportsDirectStream","SupportsTranscoding"]
bestSource = {}
for prefstream in preferredStreamOrder:
for source in mediasources:
if source[prefstream] == True:
if prefstream == "SupportsDirectPlay":
#always prefer direct play
alt_playurl = self.checkDirectPlayPath(source["Path"])
if alt_playurl:
bestSource = source
source["Path"] = alt_playurl
elif bestSource.get("BitRate",0) < source.get("Bitrate",0):
#prefer stream with highest bitrate for http sources
bestSource = source
elif not source.get("Bitrate") and source.get("RequiresOpening"):
#livestream
bestSource = source
xbmc.log("getOptimalMediaSource: %s" %bestSource)
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)
return streaminfo["MediaSource"]
def checkDirectPlayPath(self, playurl):
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("\\", "/")
if xbmcvfs.exists(playurl):
return playurl
else:
return None
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": ""
}
]
}
def resolutionConflict(self):
if settings('limitResolution') == "true":
screenRes = self.getScreenResolution()
videoRes = self.getVideoResolution()
if not videoRes:
return False
return videoRes['width'] > screenRes['width'] or videoRes['height'] > screenRes['height']
else:
return False
def getScreenResolution(self):
wind = xbmcgui.Window()
return {'width' : wind.getWidth(),
'height' : wind.getHeight()}
def getVideoResolution(self):
try:
return {'width' : self.item['MediaStreams'][0]['Width'],
'height' : self.item['MediaStreams'][0]['Height']}
except (KeyError, IndexError) as error:
log.debug(error)
log.debug(self.item)
return False