Let PMS StreamingBrain decide on whether we need to force-transcode
This commit is contained in:
parent
69049c62f1
commit
e498736a96
11 changed files with 723 additions and 419 deletions
|
@ -668,6 +668,16 @@ msgctxt "#33003"
|
|||
msgid "Server is online"
|
||||
msgstr ""
|
||||
|
||||
# Plex notification when we need to transcode
|
||||
msgctxt "#33004"
|
||||
msgid "PMS enforced transcoding"
|
||||
msgstr ""
|
||||
|
||||
# Plex notification when we need to use direct streaming (instead of transcoding)
|
||||
msgctxt "#33005"
|
||||
msgid "PMS enforced direct streaming"
|
||||
msgstr ""
|
||||
|
||||
# Error notification
|
||||
msgctxt "#33009"
|
||||
msgid "Invalid username or password"
|
||||
|
|
|
@ -19,7 +19,7 @@ from . import playlist_func as PL
|
|||
from . import playqueue as PQ
|
||||
from . import json_rpc as js
|
||||
from . import transfer
|
||||
from .playutils import PlayUtils
|
||||
from .playback_decision import set_playurl
|
||||
from . import variables as v
|
||||
from . import app
|
||||
|
||||
|
@ -460,17 +460,15 @@ def _conclude_playback(playqueue, pos):
|
|||
api = API(item.xml)
|
||||
api.part = item.part or 0
|
||||
listitem = api.listitem(listitem=transfer.PKCListItem)
|
||||
playutils = PlayUtils(api, item)
|
||||
playurl = playutils.getPlayUrl()
|
||||
set_playurl(api, item)
|
||||
else:
|
||||
listitem = transfer.PKCListItem()
|
||||
api = None
|
||||
playurl = item.file
|
||||
if not playurl:
|
||||
if not item.file:
|
||||
LOG.info('Did not get a playurl, aborting playback silently')
|
||||
_ensure_resolve(abort=True)
|
||||
_ensure_resolve()
|
||||
return
|
||||
listitem.setPath(playurl.encode('utf-8'))
|
||||
listitem.setPath(item.file.encode('utf-8'))
|
||||
if item.playmethod == 'DirectStream':
|
||||
listitem.setSubtitles(api.cache_external_subs())
|
||||
elif item.playmethod == 'Transcode':
|
||||
|
|
458
resources/lib/playback_decision.py
Normal file
458
resources/lib/playback_decision.py
Normal file
|
@ -0,0 +1,458 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
from requests import exceptions
|
||||
|
||||
from .downloadutils import DownloadUtils as DU
|
||||
from .plex_api import API
|
||||
from . import plex_functions as PF, utils, app, variables as v
|
||||
|
||||
|
||||
LOG = getLogger('PLEX.playback_decision')
|
||||
|
||||
# largest signed 32bit integer: 2147483
|
||||
MAX_SIGNED_INT = int(2**31 - 1)
|
||||
# PMS answer codes
|
||||
DIRECT_PLAY_OK = 1000
|
||||
CONVERSION_OK = 1001 # PMS can either direct stream or transcode
|
||||
|
||||
|
||||
def set_playurl(api, item):
|
||||
if api.mediastream_number() is None:
|
||||
# E.g. user could choose between several media streams and cancelled
|
||||
return
|
||||
item.playmethod = int(utils.settings('playType'))
|
||||
LOG.info('User chose playback method %s in PKC settings',
|
||||
v.EXPLICIT_PLAYBACK_METHOD[item.playmethod])
|
||||
_initial_best_playback_method(api, item)
|
||||
LOG.info('PKC decided on playback method %s',
|
||||
v.EXPLICIT_PLAYBACK_METHOD[item.playmethod])
|
||||
if item.playmethod == v.PLAYBACK_METHOD_DIRECT_PATH:
|
||||
# No need to ask the PMS whether we can play - we circumvent
|
||||
# the PMS entirely
|
||||
LOG.info('The playurl for %s is: %s',
|
||||
v.EXPLICIT_PLAYBACK_METHOD[item.playmethod], item.file)
|
||||
return
|
||||
LOG.info('Lets ask the PMS next')
|
||||
try:
|
||||
_pms_playback_decision(api, item)
|
||||
except (exceptions.RequestException, AttributeError, IndexError, SystemExit) as err:
|
||||
LOG.warn('Could not find suitable settings for playback, aborting')
|
||||
LOG.warn('Error received: %s', err)
|
||||
item.playmethod = None
|
||||
item.file = None
|
||||
else:
|
||||
item.file = api.transcode_video_path(item.playmethod,
|
||||
quality=item.quality)
|
||||
LOG.info('The playurl for %s is: %s',
|
||||
v.EXPLICIT_PLAYBACK_METHOD[item.playmethod], item.file)
|
||||
|
||||
|
||||
def _initial_best_playback_method(api, item):
|
||||
"""
|
||||
Sets the highest available playback method without talking to the PMS
|
||||
Also sets self.path for a direct path, if available and accessible
|
||||
"""
|
||||
if api.should_stream():
|
||||
# True for e.g. plex.tv watch later
|
||||
LOG.info('Plex item optimized for DirectPlay')
|
||||
item.playmethod = v.PLAYBACK_METHOD_DIRECT_PLAY
|
||||
return
|
||||
item.file = api.file_path()
|
||||
item.file = api.validate_playurl(item.file, api.plex_type, force_check=True)
|
||||
# Check whether we have a strm file that we need to throw at Kodi 1:1
|
||||
if item.file is not None and item.file.endswith('.strm'):
|
||||
# Use direct path in any case, regardless of user setting
|
||||
LOG.debug('.strm file detected')
|
||||
item.playmethod = v.PLAYBACK_METHOD_DIRECT_PATH
|
||||
elif _must_transcode(api, item):
|
||||
item.playmethod = v.PLAYBACK_METHOD_TRANSCODE
|
||||
elif item.playmethod in (v.PLAYBACK_METHOD_DIRECT_PLAY,
|
||||
v.PLAYBACK_METHOD_DIRECT_STREAM):
|
||||
pass
|
||||
elif item.file is None:
|
||||
# E.g. direct path was not possible to access
|
||||
item.playmethod = v.PLAYBACK_METHOD_DIRECT_PLAY
|
||||
else:
|
||||
item.playmethod = v.PLAYBACK_METHOD_DIRECT_PATH
|
||||
|
||||
|
||||
def _pms_playback_decision(api, item):
|
||||
"""
|
||||
We CANNOT distinguish direct playing from direct streaming from the PMS'
|
||||
answer
|
||||
"""
|
||||
ask_for_user_quality_settings = False
|
||||
if item.playmethod <= 2:
|
||||
LOG.info('Asking PMS with maximal quality settings')
|
||||
item.quality = _max_quality()
|
||||
decision_api = _ask_pms(api, item)
|
||||
if decision_api.decision_code() > CONVERSION_OK:
|
||||
ask_for_user_quality_settings = True
|
||||
else:
|
||||
ask_for_user_quality_settings = True
|
||||
if ask_for_user_quality_settings:
|
||||
item.quality = _transcode_quality()
|
||||
LOG.info('Asking PMS with user quality settings')
|
||||
decision_api = _ask_pms(api, item)
|
||||
|
||||
# Process the PMS answer
|
||||
if decision_api.decision_code() > CONVERSION_OK:
|
||||
LOG.error('Neither DirectPlay, DirectStream nor transcoding possible')
|
||||
error = '%s\n%s' % (decision_api.general_play_decision_text(),
|
||||
decision_api.transcode_decision_text())
|
||||
utils.messageDialog(heading=utils.lang(29999),
|
||||
msg=error)
|
||||
raise AttributeError('Neither DirectPlay, DirectStream nor transcoding possible')
|
||||
if (item.playmethod == v.PLAYBACK_METHOD_DIRECT_PLAY and
|
||||
decision_api.decision_code() == DIRECT_PLAY_OK):
|
||||
# All good
|
||||
return
|
||||
LOG.info('PMS video stream decision: %s, PMS audio stream decision: %s, '
|
||||
'PMS subtitle stream decision: %s',
|
||||
decision_api.video_decision(),
|
||||
decision_api.audio_decision(),
|
||||
decision_api.subtitle_decision())
|
||||
# Only look at the video stream since that'll be most CPU-intensive for
|
||||
# the PMS
|
||||
video_direct_streaming = decision_api.video_decision() == 'copy'
|
||||
if video_direct_streaming:
|
||||
if item.playmethod < v.PLAYBACK_METHOD_DIRECT_STREAM:
|
||||
LOG.warn('The PMS forces us to direct stream')
|
||||
# "PMS enforced direct streaming"
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
utils.lang(33005),
|
||||
icon='{plex}')
|
||||
item.playmethod = v.PLAYBACK_METHOD_DIRECT_STREAM
|
||||
else:
|
||||
if item.playmethod < v.PLAYBACK_METHOD_TRANSCODE:
|
||||
LOG.warn('The PMS forces us to transcode')
|
||||
# "PMS enforced transcoding"
|
||||
utils.dialog('notification',
|
||||
utils.lang(29999),
|
||||
utils.lang(33004),
|
||||
icon='{plex}')
|
||||
item.playmethod = v.PLAYBACK_METHOD_TRANSCODE
|
||||
|
||||
|
||||
def _ask_pms(api, item):
|
||||
xml = PF.playback_decision(path=api.path_and_plex_id(),
|
||||
media=api.mediastream,
|
||||
part=api.part,
|
||||
playmethod=item.playmethod,
|
||||
video=api.plex_type in v.PLEX_VIDEOTYPES,
|
||||
args=item.quality)
|
||||
decision_api = API(xml)
|
||||
LOG.info('PMS general decision %s: %s',
|
||||
decision_api.general_play_decision_code(),
|
||||
decision_api.general_play_decision_text())
|
||||
LOG.info('PMS Direct Play decision %s: %s',
|
||||
decision_api.direct_play_decision_code(),
|
||||
decision_api.direct_play_decision_text())
|
||||
LOG.info('PMS MDE decision %s: %s',
|
||||
decision_api.mde_play_decision_code(),
|
||||
decision_api.mde_play_decision_text())
|
||||
LOG.info('PMS transcoding decision %s: %s',
|
||||
decision_api.transcode_decision_code(),
|
||||
decision_api.transcode_decision_text())
|
||||
return decision_api
|
||||
|
||||
|
||||
def _must_transcode(api, item):
|
||||
"""
|
||||
Returns True if we need to transcode because
|
||||
- codec is in h265
|
||||
- 10bit video codec
|
||||
- HEVC codec
|
||||
- playqueue_item force_transcode is set to True
|
||||
- state variable FORCE_TRANSCODE set to True
|
||||
(excepting trailers etc.)
|
||||
- video bitrate above specified settings bitrate
|
||||
if the corresponding file settings are set to 'true'
|
||||
"""
|
||||
if api.plex_type in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SONG):
|
||||
LOG.info('Plex clip or music track, not transcoding')
|
||||
return False
|
||||
if item.playmethod == v.PLAYBACK_METHOD_TRANSCODE:
|
||||
return True
|
||||
videoCodec = api.video_codec()
|
||||
LOG.debug("videoCodec received from the PMS: %s", videoCodec)
|
||||
if item.force_transcode is True:
|
||||
LOG.info('User chose to force-transcode')
|
||||
return True
|
||||
codec = videoCodec['videocodec']
|
||||
if codec is None:
|
||||
# e.g. trailers. Avoids TypeError with "'h265' in codec"
|
||||
LOG.info('No codec from PMS, not transcoding.')
|
||||
return False
|
||||
if ((utils.settings('transcodeHi10P') == 'true' and
|
||||
videoCodec['bitDepth'] == '10') and
|
||||
('h264' in codec)):
|
||||
LOG.info('Option to transcode 10bit h264 video content enabled.')
|
||||
return True
|
||||
try:
|
||||
bitrate = int(videoCodec['bitrate'])
|
||||
except (TypeError, ValueError):
|
||||
LOG.info('No video bitrate from PMS, not transcoding.')
|
||||
return False
|
||||
if bitrate > _get_max_bitrate():
|
||||
LOG.info('Video bitrate of %s is higher than the maximal video'
|
||||
'bitrate of %s that the user chose. Transcoding',
|
||||
bitrate, _get_max_bitrate())
|
||||
return True
|
||||
try:
|
||||
resolution = int(videoCodec['resolution'])
|
||||
except (TypeError, ValueError):
|
||||
if videoCodec['resolution'] == '4k':
|
||||
resolution = 2160
|
||||
else:
|
||||
LOG.info('No video resolution from PMS, not transcoding.')
|
||||
return False
|
||||
if 'h265' in codec or 'hevc' in codec:
|
||||
if resolution >= _getH265():
|
||||
LOG.info('Option to transcode h265/HEVC enabled. Resolution '
|
||||
'of the media: %s, transcoding limit resolution: %s',
|
||||
resolution, _getH265())
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _transcode_quality():
|
||||
return {
|
||||
'maxVideoBitrate': get_bitrate(),
|
||||
'videoResolution': get_resolution(),
|
||||
'videoQuality': 100,
|
||||
'mediaBufferSize': int(float(utils.settings('kodi_video_cache')) / 1024.0),
|
||||
}
|
||||
|
||||
|
||||
def _max_quality():
|
||||
return {
|
||||
'maxVideoBitrate': MAX_SIGNED_INT,
|
||||
'videoResolution': '3840x2160', # 4K
|
||||
'videoQuality': 100,
|
||||
'mediaBufferSize': int(float(utils.settings('kodi_video_cache')) / 1024.0),
|
||||
}
|
||||
|
||||
|
||||
def get_bitrate():
|
||||
"""
|
||||
Get the desired transcoding bitrate from the settings
|
||||
"""
|
||||
videoQuality = utils.settings('transcoderVideoQualities')
|
||||
bitrate = {
|
||||
'0': 320,
|
||||
'1': 720,
|
||||
'2': 1500,
|
||||
'3': 2000,
|
||||
'4': 3000,
|
||||
'5': 4000,
|
||||
'6': 8000,
|
||||
'7': 10000,
|
||||
'8': 12000,
|
||||
'9': 20000,
|
||||
'10': 40000,
|
||||
}
|
||||
# max bit rate supported by server (max signed 32bit integer)
|
||||
return bitrate.get(videoQuality, MAX_SIGNED_INT)
|
||||
|
||||
|
||||
def get_resolution():
|
||||
"""
|
||||
Get the desired transcoding resolutions from the settings
|
||||
"""
|
||||
chosen = utils.settings('transcoderVideoQualities')
|
||||
res = {
|
||||
'0': '420x420',
|
||||
'1': '576x320',
|
||||
'2': '720x480',
|
||||
'3': '1024x768',
|
||||
'4': '1280x720',
|
||||
'5': '1280x720',
|
||||
'6': '1920x1080',
|
||||
'7': '1920x1080',
|
||||
'8': '1920x1080',
|
||||
'9': '1920x1080',
|
||||
'10': '1920x1080',
|
||||
}
|
||||
return res[chosen]
|
||||
|
||||
|
||||
def _get_max_bitrate():
|
||||
max_bitrate = utils.settings('maxVideoQualities')
|
||||
bitrate = {
|
||||
'0': 320,
|
||||
'1': 720,
|
||||
'2': 1500,
|
||||
'3': 2000,
|
||||
'4': 3000,
|
||||
'5': 4000,
|
||||
'6': 8000,
|
||||
'7': 10000,
|
||||
'8': 12000,
|
||||
'9': 20000,
|
||||
'10': 40000,
|
||||
'11': MAX_SIGNED_INT # deactivated
|
||||
}
|
||||
# max bit rate supported by server (max signed 32bit integer)
|
||||
return bitrate.get(max_bitrate, MAX_SIGNED_INT)
|
||||
|
||||
|
||||
def _getH265():
|
||||
"""
|
||||
Returns the user settings for transcoding h265: boundary resolutions
|
||||
of 480, 720 or 1080 as an int
|
||||
|
||||
OR 2147483 (MAX_SIGNED_INT, int) if user chose not to transcode
|
||||
"""
|
||||
H265 = {
|
||||
'0': MAX_SIGNED_INT,
|
||||
'1': 480,
|
||||
'2': 720,
|
||||
'3': 1080
|
||||
}
|
||||
return H265[utils.settings('transcodeH265')]
|
||||
|
||||
|
||||
def audio_subtitle_prefs(api, listitem):
|
||||
"""
|
||||
For transcoding only
|
||||
|
||||
Called at the very beginning of play; used to change audio and subtitle
|
||||
stream by a PUT request to the PMS
|
||||
"""
|
||||
# Set media and part where we're at
|
||||
if (api.mediastream is None and
|
||||
api.mediastream_number() is None):
|
||||
return
|
||||
try:
|
||||
mediastreams = api.plex_media_streams()
|
||||
except (TypeError, IndexError):
|
||||
LOG.error('Could not get media %s, part %s',
|
||||
api.mediastream, api.part)
|
||||
return
|
||||
part_id = mediastreams.attrib['id']
|
||||
audio_streams_list = []
|
||||
audio_streams = []
|
||||
subtitle_streams_list = []
|
||||
# No subtitles as an option
|
||||
subtitle_streams = [utils.lang(39706)]
|
||||
downloadable_streams = []
|
||||
download_subs = []
|
||||
# selectAudioIndex = ""
|
||||
select_subs_index = ""
|
||||
audio_numb = 0
|
||||
# Remember 'no subtitles'
|
||||
sub_num = 1
|
||||
default_sub = None
|
||||
|
||||
for stream in mediastreams:
|
||||
# Since Plex returns all possible tracks together, have to sort
|
||||
# them.
|
||||
index = stream.attrib.get('id')
|
||||
typus = stream.attrib.get('streamType')
|
||||
# Audio
|
||||
if typus == "2":
|
||||
codec = stream.attrib.get('codec')
|
||||
channellayout = stream.attrib.get('audioChannelLayout', "")
|
||||
try:
|
||||
track = "%s %s - %s %s" % (audio_numb + 1,
|
||||
stream.attrib['language'],
|
||||
codec,
|
||||
channellayout)
|
||||
except KeyError:
|
||||
track = "%s %s - %s %s" % (audio_numb + 1,
|
||||
utils.lang(39707), # unknown
|
||||
codec,
|
||||
channellayout)
|
||||
audio_streams_list.append(index)
|
||||
audio_streams.append(utils.try_encode(track))
|
||||
audio_numb += 1
|
||||
|
||||
# Subtitles
|
||||
elif typus == "3":
|
||||
try:
|
||||
track = "%s %s" % (sub_num + 1, stream.attrib['language'])
|
||||
except KeyError:
|
||||
track = "%s %s (%s)" % (sub_num + 1,
|
||||
utils.lang(39707), # unknown
|
||||
stream.attrib.get('codec'))
|
||||
default = stream.attrib.get('default')
|
||||
forced = stream.attrib.get('forced')
|
||||
downloadable = stream.attrib.get('key')
|
||||
|
||||
if default:
|
||||
track = "%s - %s" % (track, utils.lang(39708)) # Default
|
||||
if forced:
|
||||
track = "%s - %s" % (track, utils.lang(39709)) # Forced
|
||||
if downloadable:
|
||||
# We do know the language - temporarily download
|
||||
if 'language' in stream.attrib:
|
||||
path = api.download_external_subtitles(
|
||||
'{server}%s' % stream.attrib['key'],
|
||||
"subtitle.%s.%s" % (stream.attrib['languageCode'],
|
||||
stream.attrib['codec']))
|
||||
# We don't know the language - no need to download
|
||||
else:
|
||||
path = api.attach_plex_token_to_url(
|
||||
"%s%s" % (app.CONN.server,
|
||||
stream.attrib['key']))
|
||||
downloadable_streams.append(index)
|
||||
download_subs.append(utils.try_encode(path))
|
||||
else:
|
||||
track = "%s (%s)" % (track, utils.lang(39710)) # burn-in
|
||||
if stream.attrib.get('selected') == '1' and downloadable:
|
||||
# Only show subs without asking user if they can be
|
||||
# turned off
|
||||
default_sub = index
|
||||
|
||||
subtitle_streams_list.append(index)
|
||||
subtitle_streams.append(utils.try_encode(track))
|
||||
sub_num += 1
|
||||
|
||||
if audio_numb > 1:
|
||||
resp = utils.dialog('select', utils.lang(33013), audio_streams)
|
||||
if resp > -1:
|
||||
# User selected some audio track
|
||||
args = {
|
||||
'audioStreamID': audio_streams_list[resp],
|
||||
'allParts': 1
|
||||
}
|
||||
DU().downloadUrl('{server}/library/parts/%s' % part_id,
|
||||
action_type='PUT',
|
||||
parameters=args)
|
||||
|
||||
if sub_num == 1:
|
||||
# No subtitles
|
||||
return
|
||||
|
||||
select_subs_index = None
|
||||
if (utils.settings('pickPlexSubtitles') == 'true' and
|
||||
default_sub is not None):
|
||||
LOG.info('Using default Plex subtitle: %s', default_sub)
|
||||
select_subs_index = default_sub
|
||||
else:
|
||||
resp = utils.dialog('select', utils.lang(33014), subtitle_streams)
|
||||
if resp > 0:
|
||||
select_subs_index = subtitle_streams_list[resp - 1]
|
||||
else:
|
||||
# User selected no subtitles or backed out of dialog
|
||||
select_subs_index = ''
|
||||
|
||||
LOG.debug('Adding external subtitles: %s', download_subs)
|
||||
# Enable Kodi to switch autonomously to downloadable subtitles
|
||||
if download_subs:
|
||||
listitem.setSubtitles(download_subs)
|
||||
# Don't additionally burn in subtitles
|
||||
if select_subs_index in downloadable_streams:
|
||||
select_subs_index = ''
|
||||
# Now prep the PMS for our choice
|
||||
args = {
|
||||
'subtitleStreamID': select_subs_index,
|
||||
'allParts': 1
|
||||
}
|
||||
DU().downloadUrl('{server}/library/parts/%s' % part_id,
|
||||
action_type='PUT',
|
||||
parameters=args)
|
|
@ -177,6 +177,8 @@ class PlaylistItem(object):
|
|||
self.playmethod = None
|
||||
self.playcount = None
|
||||
self.offset = None
|
||||
# Transcoding quality, if needed
|
||||
self.quality = None
|
||||
# If Plex video consists of several parts; part number
|
||||
self.part = 0
|
||||
self.force_transcode = False
|
||||
|
|
|
@ -1,371 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
from logging import getLogger
|
||||
|
||||
from .downloadutils import DownloadUtils as DU
|
||||
from . import utils, app
|
||||
from . import variables as v
|
||||
|
||||
###############################################################################
|
||||
LOG = getLogger('PLEX.playutils')
|
||||
###############################################################################
|
||||
|
||||
|
||||
class PlayUtils():
|
||||
|
||||
def __init__(self, api, playqueue_item):
|
||||
"""
|
||||
init with api (PlexAPI wrapper of the PMS xml element) and
|
||||
playqueue_item (PlaylistItem())
|
||||
"""
|
||||
self.api = api
|
||||
self.item = playqueue_item
|
||||
|
||||
def getPlayUrl(self):
|
||||
"""
|
||||
Returns the playurl [unicode] for the part or returns None.
|
||||
(movie might consist of several files)
|
||||
"""
|
||||
if self.api.mediastream_number() is None:
|
||||
return
|
||||
playurl = self.direct_path()
|
||||
if playurl is not None:
|
||||
LOG.info("File is direct playing.")
|
||||
self.item.playmethod = 'DirectPath'
|
||||
elif self.isDirectStream():
|
||||
LOG.info("File is direct streaming.")
|
||||
playurl = self.api.transcode_video_path('DirectStream')
|
||||
self.item.playmethod = 'DirectStream'
|
||||
else:
|
||||
LOG.info("File is transcoding.")
|
||||
playurl = self.api.transcode_video_path(
|
||||
'Transcode',
|
||||
quality={
|
||||
'maxVideoBitrate': self.get_bitrate(),
|
||||
'videoResolution': self.get_resolution(),
|
||||
'videoQuality': '100',
|
||||
'mediaBufferSize': int(
|
||||
utils.settings('kodi_video_cache')) / 1024,
|
||||
})
|
||||
self.item.playmethod = 'Transcode'
|
||||
LOG.info("The playurl is: %s", playurl)
|
||||
self.item.file = playurl
|
||||
return playurl
|
||||
|
||||
def direct_path(self):
|
||||
"""
|
||||
Returns the path if we can use direct paths and the path is accessible
|
||||
Returns None otherwise
|
||||
"""
|
||||
# True for e.g. plex.tv watch later
|
||||
if self.api.should_stream() is True:
|
||||
LOG.info("Plex item optimized for direct streaming")
|
||||
return
|
||||
# Check whether we have a strm file that we need to throw at Kodi 1:1
|
||||
path = self.api.file_path()
|
||||
if path is not None and path.endswith('.strm'):
|
||||
LOG.info('.strm file detected')
|
||||
return self.api.validate_playurl(path,
|
||||
self.api.plex_type,
|
||||
force_check=True)
|
||||
# set to either 'Direct Play=1' or 'Transcode=2'
|
||||
# and NOT to 'Try Direct Path=0'
|
||||
if utils.settings('playType') != "0":
|
||||
# User forcing to play via HTTP
|
||||
LOG.info("User chose to not use direct paths")
|
||||
return
|
||||
if self.mustTranscode():
|
||||
return
|
||||
return self.api.validate_playurl(path,
|
||||
self.api.plex_type,
|
||||
force_check=True)
|
||||
|
||||
def mustTranscode(self):
|
||||
"""
|
||||
Returns True if we need to transcode because
|
||||
- codec is in h265
|
||||
- 10bit video codec
|
||||
- HEVC codec
|
||||
- playqueue_item force_transcode is set to True
|
||||
- state variable FORCE_TRANSCODE set to True
|
||||
(excepting trailers etc.)
|
||||
- video bitrate above specified settings bitrate
|
||||
if the corresponding file settings are set to 'true'
|
||||
"""
|
||||
if self.api.plex_type in (v.PLEX_TYPE_CLIP, v.PLEX_TYPE_SONG):
|
||||
LOG.info('Plex clip or music track, not transcoding')
|
||||
return False
|
||||
videoCodec = self.api.video_codec()
|
||||
LOG.info("videoCodec: %s", videoCodec)
|
||||
if self.item.force_transcode is True:
|
||||
LOG.info('User chose to force-transcode')
|
||||
return True
|
||||
codec = videoCodec['videocodec']
|
||||
if codec is None:
|
||||
# e.g. trailers. Avoids TypeError with "'h265' in codec"
|
||||
LOG.info('No codec from PMS, not transcoding.')
|
||||
return False
|
||||
if ((utils.settings('transcodeHi10P') == 'true' and
|
||||
videoCodec['bitDepth'] == '10') and
|
||||
('h264' in codec)):
|
||||
LOG.info('Option to transcode 10bit h264 video content enabled.')
|
||||
return True
|
||||
try:
|
||||
bitrate = int(videoCodec['bitrate'])
|
||||
except (TypeError, ValueError):
|
||||
LOG.info('No video bitrate from PMS, not transcoding.')
|
||||
return False
|
||||
if bitrate > self.get_max_bitrate():
|
||||
LOG.info('Video bitrate of %s is higher than the maximal video'
|
||||
'bitrate of %s that the user chose. Transcoding',
|
||||
bitrate, self.get_max_bitrate())
|
||||
return True
|
||||
try:
|
||||
resolution = int(videoCodec['resolution'])
|
||||
except (TypeError, ValueError):
|
||||
if videoCodec['resolution'] == '4k':
|
||||
resolution = 2160
|
||||
else:
|
||||
LOG.info('No video resolution from PMS, not transcoding.')
|
||||
return False
|
||||
if 'h265' in codec or 'hevc' in codec:
|
||||
if resolution >= self.getH265():
|
||||
LOG.info('Option to transcode h265/HEVC enabled. Resolution '
|
||||
'of the media: %s, transcoding limit resolution: %s',
|
||||
resolution, self.getH265())
|
||||
return True
|
||||
return False
|
||||
|
||||
def isDirectStream(self):
|
||||
# Never transcode Music
|
||||
if self.api.plex_type == 'track':
|
||||
return True
|
||||
if utils.settings('playType') == '3':
|
||||
# User forcing to play via HTTP
|
||||
LOG.info("User chose to transcode")
|
||||
return False
|
||||
if self.mustTranscode():
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_max_bitrate():
|
||||
# get the addon video quality
|
||||
videoQuality = utils.settings('maxVideoQualities')
|
||||
bitrate = {
|
||||
'0': 320,
|
||||
'1': 720,
|
||||
'2': 1500,
|
||||
'3': 2000,
|
||||
'4': 3000,
|
||||
'5': 4000,
|
||||
'6': 8000,
|
||||
'7': 10000,
|
||||
'8': 12000,
|
||||
'9': 20000,
|
||||
'10': 40000,
|
||||
'11': 99999999 # deactivated
|
||||
}
|
||||
# max bit rate supported by server (max signed 32bit integer)
|
||||
return bitrate.get(videoQuality, 2147483)
|
||||
|
||||
@staticmethod
|
||||
def getH265():
|
||||
"""
|
||||
Returns the user settings for transcoding h265: boundary resolutions
|
||||
of 480, 720 or 1080 as an int
|
||||
|
||||
OR 9999999 (int) if user chose not to transcode
|
||||
"""
|
||||
H265 = {
|
||||
'0': 99999999,
|
||||
'1': 480,
|
||||
'2': 720,
|
||||
'3': 1080
|
||||
}
|
||||
return H265[utils.settings('transcodeH265')]
|
||||
|
||||
@staticmethod
|
||||
def get_bitrate():
|
||||
"""
|
||||
Get the desired transcoding bitrate from the settings
|
||||
"""
|
||||
videoQuality = utils.settings('transcoderVideoQualities')
|
||||
bitrate = {
|
||||
'0': 320,
|
||||
'1': 720,
|
||||
'2': 1500,
|
||||
'3': 2000,
|
||||
'4': 3000,
|
||||
'5': 4000,
|
||||
'6': 8000,
|
||||
'7': 10000,
|
||||
'8': 12000,
|
||||
'9': 20000,
|
||||
'10': 40000,
|
||||
}
|
||||
# max bit rate supported by server (max signed 32bit integer)
|
||||
return bitrate.get(videoQuality, 2147483)
|
||||
|
||||
@staticmethod
|
||||
def get_resolution():
|
||||
"""
|
||||
Get the desired transcoding resolutions from the settings
|
||||
"""
|
||||
chosen = utils.settings('transcoderVideoQualities')
|
||||
res = {
|
||||
'0': '420x420',
|
||||
'1': '576x320',
|
||||
'2': '720x480',
|
||||
'3': '1024x768',
|
||||
'4': '1280x720',
|
||||
'5': '1280x720',
|
||||
'6': '1920x1080',
|
||||
'7': '1920x1080',
|
||||
'8': '1920x1080',
|
||||
'9': '1920x1080',
|
||||
'10': '1920x1080',
|
||||
}
|
||||
return res[chosen]
|
||||
|
||||
def audio_subtitle_prefs(self, listitem):
|
||||
"""
|
||||
For transcoding only
|
||||
|
||||
Called at the very beginning of play; used to change audio and subtitle
|
||||
stream by a PUT request to the PMS
|
||||
"""
|
||||
# Set media and part where we're at
|
||||
if (self.api.mediastream is None and
|
||||
self.api.mediastream_number() is None):
|
||||
return
|
||||
try:
|
||||
mediastreams = self.api.plex_media_streams()
|
||||
except (TypeError, IndexError):
|
||||
LOG.error('Could not get media %s, part %s',
|
||||
self.api.mediastream, self.api.part)
|
||||
return
|
||||
part_id = mediastreams.attrib['id']
|
||||
audio_streams_list = []
|
||||
audio_streams = []
|
||||
subtitle_streams_list = []
|
||||
# No subtitles as an option
|
||||
subtitle_streams = [utils.lang(39706)]
|
||||
downloadable_streams = []
|
||||
download_subs = []
|
||||
# selectAudioIndex = ""
|
||||
select_subs_index = ""
|
||||
audio_numb = 0
|
||||
# Remember 'no subtitles'
|
||||
sub_num = 1
|
||||
default_sub = None
|
||||
|
||||
for stream in mediastreams:
|
||||
# Since Plex returns all possible tracks together, have to sort
|
||||
# them.
|
||||
index = stream.attrib.get('id')
|
||||
typus = stream.attrib.get('streamType')
|
||||
# Audio
|
||||
if typus == "2":
|
||||
codec = stream.attrib.get('codec')
|
||||
channellayout = stream.attrib.get('audioChannelLayout', "")
|
||||
try:
|
||||
track = "%s %s - %s %s" % (audio_numb + 1,
|
||||
stream.attrib['language'],
|
||||
codec,
|
||||
channellayout)
|
||||
except KeyError:
|
||||
track = "%s %s - %s %s" % (audio_numb + 1,
|
||||
utils.lang(39707), # unknown
|
||||
codec,
|
||||
channellayout)
|
||||
audio_streams_list.append(index)
|
||||
audio_streams.append(utils.try_encode(track))
|
||||
audio_numb += 1
|
||||
|
||||
# Subtitles
|
||||
elif typus == "3":
|
||||
try:
|
||||
track = "%s %s" % (sub_num + 1, stream.attrib['language'])
|
||||
except KeyError:
|
||||
track = "%s %s (%s)" % (sub_num + 1,
|
||||
utils.lang(39707), # unknown
|
||||
stream.attrib.get('codec'))
|
||||
default = stream.attrib.get('default')
|
||||
forced = stream.attrib.get('forced')
|
||||
downloadable = stream.attrib.get('key')
|
||||
|
||||
if default:
|
||||
track = "%s - %s" % (track, utils.lang(39708)) # Default
|
||||
if forced:
|
||||
track = "%s - %s" % (track, utils.lang(39709)) # Forced
|
||||
if downloadable:
|
||||
# We do know the language - temporarily download
|
||||
if 'language' in stream.attrib:
|
||||
path = self.api.download_external_subtitles(
|
||||
'{server}%s' % stream.attrib['key'],
|
||||
"subtitle.%s.%s" % (stream.attrib['languageCode'],
|
||||
stream.attrib['codec']))
|
||||
# We don't know the language - no need to download
|
||||
else:
|
||||
path = self.api.attach_plex_token_to_url(
|
||||
"%s%s" % (app.CONN.server,
|
||||
stream.attrib['key']))
|
||||
downloadable_streams.append(index)
|
||||
download_subs.append(utils.try_encode(path))
|
||||
else:
|
||||
track = "%s (%s)" % (track, utils.lang(39710)) # burn-in
|
||||
if stream.attrib.get('selected') == '1' and downloadable:
|
||||
# Only show subs without asking user if they can be
|
||||
# turned off
|
||||
default_sub = index
|
||||
|
||||
subtitle_streams_list.append(index)
|
||||
subtitle_streams.append(utils.try_encode(track))
|
||||
sub_num += 1
|
||||
|
||||
if audio_numb > 1:
|
||||
resp = utils.dialog('select', utils.lang(33013), audio_streams)
|
||||
if resp > -1:
|
||||
# User selected some audio track
|
||||
args = {
|
||||
'audioStreamID': audio_streams_list[resp],
|
||||
'allParts': 1
|
||||
}
|
||||
DU().downloadUrl('{server}/library/parts/%s' % part_id,
|
||||
action_type='PUT',
|
||||
parameters=args)
|
||||
|
||||
if sub_num == 1:
|
||||
# No subtitles
|
||||
return
|
||||
|
||||
select_subs_index = None
|
||||
if (utils.settings('pickPlexSubtitles') == 'true' and
|
||||
default_sub is not None):
|
||||
LOG.info('Using default Plex subtitle: %s', default_sub)
|
||||
select_subs_index = default_sub
|
||||
else:
|
||||
resp = utils.dialog('select', utils.lang(33014), subtitle_streams)
|
||||
if resp > 0:
|
||||
select_subs_index = subtitle_streams_list[resp - 1]
|
||||
else:
|
||||
# User selected no subtitles or backed out of dialog
|
||||
select_subs_index = ''
|
||||
|
||||
LOG.debug('Adding external subtitles: %s', download_subs)
|
||||
# Enable Kodi to switch autonomously to downloadable subtitles
|
||||
if download_subs:
|
||||
listitem.setSubtitles(download_subs)
|
||||
# Don't additionally burn in subtitles
|
||||
if select_subs_index in downloadable_streams:
|
||||
select_subs_index = ''
|
||||
# Now prep the PMS for our choice
|
||||
args = {
|
||||
'subtitleStreamID': select_subs_index,
|
||||
'allParts': 1
|
||||
}
|
||||
DU().downloadUrl('{server}/library/parts/%s' % part_id,
|
||||
action_type='PUT',
|
||||
parameters=args)
|
|
@ -10,11 +10,12 @@ from .artwork import Artwork
|
|||
from .file import File
|
||||
from .media import Media
|
||||
from .user import User
|
||||
from .playback import Playback
|
||||
|
||||
from ..plex_db import PlexDB
|
||||
|
||||
|
||||
class API(Base, Artwork, File, Media, User):
|
||||
class API(Base, Artwork, File, Media, User, Playback):
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ from logging import getLogger
|
|||
from ..utils import cast
|
||||
from ..downloadutils import DownloadUtils as DU
|
||||
from .. import utils, variables as v, app, path_ops, clientinfo
|
||||
from .. import plex_functions as PF
|
||||
|
||||
LOG = getLogger('PLEX.api')
|
||||
|
||||
|
@ -209,7 +210,9 @@ class Media(object):
|
|||
Transcode Video support; returns the URL to get a media started
|
||||
|
||||
Input:
|
||||
action 'DirectStream' or 'Transcode'
|
||||
action 'DirectPlay'
|
||||
'DirectStream'
|
||||
'Transcode'
|
||||
|
||||
quality: {
|
||||
'videoResolution': e.g. '1024x768',
|
||||
|
@ -224,47 +227,23 @@ class Media(object):
|
|||
"""
|
||||
if self.mediastream is None and self.mediastream_number() is None:
|
||||
return
|
||||
quality = {} if quality is None else quality
|
||||
xargs = clientinfo.getXArgsDeviceInfo()
|
||||
# For DirectPath, path/key of PART is needed
|
||||
# trailers are 'clip' with PMS xmls
|
||||
if action == "DirectStream":
|
||||
headers = clientinfo.getXArgsDeviceInfo()
|
||||
if action == v.PLAYBACK_METHOD_DIRECT_PLAY:
|
||||
path = self.xml[self.mediastream][self.part].get('key')
|
||||
url = app.CONN.server + path
|
||||
# e.g. Trailers already feature an '?'!
|
||||
return utils.extend_url(url, xargs)
|
||||
|
||||
# For Transcoding
|
||||
headers = {
|
||||
'X-Plex-Platform': 'Android',
|
||||
'X-Plex-Platform-Version': '7.0',
|
||||
'X-Plex-Product': 'Plex for Android',
|
||||
'X-Plex-Version': '5.8.0.475'
|
||||
}
|
||||
return utils.extend_url(app.CONN.server + path, headers)
|
||||
# Direct Streaming and Transcoding
|
||||
arguments = PF.transcoding_arguments(path=self.path_and_plex_id(),
|
||||
media=self.mediastream,
|
||||
part=self.part,
|
||||
playmethod=action,
|
||||
args=quality)
|
||||
headers.update(arguments)
|
||||
# Path/key to VIDEO item of xml PMS response is needed, not part
|
||||
path = self.xml.get('key')
|
||||
transcode_path = app.CONN.server + \
|
||||
'/video/:/transcode/universal/start.m3u8'
|
||||
args = {
|
||||
'audioBoost': utils.settings('audioBoost'),
|
||||
'autoAdjustQuality': 0,
|
||||
'directPlay': 0,
|
||||
'directStream': 1,
|
||||
'protocol': 'hls', # seen in the wild: 'dash', 'http', 'hls'
|
||||
'session': v.PKC_MACHINE_IDENTIFIER, # TODO: create new unique id
|
||||
'fastSeek': 1,
|
||||
'path': path,
|
||||
'mediaIndex': self.mediastream,
|
||||
'partIndex': self.part,
|
||||
'hasMDE': 1,
|
||||
'location': 'lan',
|
||||
'subtitleSize': utils.settings('subtitleSize')
|
||||
}
|
||||
LOG.debug("Setting transcode quality to: %s", quality)
|
||||
xargs.update(headers)
|
||||
xargs.update(args)
|
||||
xargs.update(quality)
|
||||
return utils.extend_url(transcode_path, xargs)
|
||||
return utils.extend_url(transcode_path, headers)
|
||||
|
||||
def cache_external_subs(self):
|
||||
"""
|
||||
|
|
107
resources/lib/plex_api/playback.py
Normal file
107
resources/lib/plex_api/playback.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, division, unicode_literals
|
||||
|
||||
from ..utils import cast
|
||||
|
||||
|
||||
class Playback(object):
|
||||
def decision_code(self):
|
||||
"""
|
||||
Returns the general_play_decision_code or mde_play_decision_code if
|
||||
not available. Returns None if something went wrong
|
||||
"""
|
||||
return self.general_play_decision_code() or self.mde_play_decision_code()
|
||||
|
||||
def general_play_decision_code(self):
|
||||
"""
|
||||
Returns the 'generalDecisionCode' as an int or None
|
||||
Generally, the 1xxx codes constitute a a success decision, 2xxx a
|
||||
general playback error, 3xxx a direct play error, and 4xxx a transcode
|
||||
error.
|
||||
|
||||
General decisions can include:
|
||||
|
||||
1000: Direct play OK.
|
||||
1001: Direct play not available; Conversion OK.
|
||||
2000: Neither direct play nor conversion is available.
|
||||
2001: Not enough bandwidth for any playback of this item.
|
||||
2002: Number of allowed streams has been reached. Stop a playback or ask
|
||||
admin for more permissions.
|
||||
2003: File is unplayable.
|
||||
2004: Streaming Session doesn’t exist or timed out.
|
||||
2005: Client stopped playback.
|
||||
2006: Admin Terminated Playback.
|
||||
"""
|
||||
return cast(int, self.xml.get('generalDecisionCode'))
|
||||
|
||||
def general_play_decision_text(self):
|
||||
"""
|
||||
Returns the text associated with the general_play_decision_code() as
|
||||
text in unicode or None
|
||||
"""
|
||||
return self.xml.get('generalDecisionText')
|
||||
|
||||
def mde_play_decision_code(self):
|
||||
return cast(int, self.xml.get('mdeDecisionCode'))
|
||||
|
||||
def mde_play_decision_text(self):
|
||||
"""
|
||||
Returns the text associated with the mde_play_decision_code() as
|
||||
text in unicode or None
|
||||
"""
|
||||
return self.xml.get('mdeDecisionText')
|
||||
|
||||
def direct_play_decision_code(self):
|
||||
return cast(int, self.xml.get('directPlayDecisionCode'))
|
||||
|
||||
def direct_play_decision_text(self):
|
||||
"""
|
||||
Returns the text associated with the mde_play_decision_code() as
|
||||
text in unicode or None
|
||||
"""
|
||||
return self.xml.get('directPlayDecisionText')
|
||||
|
||||
def transcode_decision_code(self):
|
||||
return cast(int, self.xml.get('directPlayDecisionCode'))
|
||||
|
||||
def transcode_decision_text(self):
|
||||
"""
|
||||
Returns the text associated with the mde_play_decision_code() as
|
||||
text in unicode or None
|
||||
"""
|
||||
return self.xml.get('directPlayDecisionText')
|
||||
|
||||
def video_decision(self):
|
||||
"""
|
||||
Returns "copy" if PMS streaming brain decided to DirectStream, so copy
|
||||
an existing video stream into a new container. Returns "transcode" if
|
||||
the video stream will be transcoded.
|
||||
|
||||
Raises IndexError if something went wrong. Might also return None
|
||||
"""
|
||||
for stream in self.xml[0][0][0]:
|
||||
if stream.get('streamType') == '1':
|
||||
return stream.get('decision')
|
||||
|
||||
def audio_decision(self):
|
||||
"""
|
||||
Returns "copy" if PMS streaming brain decided to DirectStream, so copy
|
||||
an existing audio stream into a new container. Returns "transcode" if
|
||||
the audio stream will be transcoded.
|
||||
|
||||
Raises IndexError if something went wrong. Might also return None
|
||||
"""
|
||||
for stream in self.xml[0][0][0]:
|
||||
if stream.get('streamType') == '2':
|
||||
return stream.get('decision')
|
||||
|
||||
def subtitle_decision(self):
|
||||
"""
|
||||
Returns the PMS' decision on the subtitle stream.
|
||||
|
||||
Raises IndexError if something went wrong. Might also return None
|
||||
"""
|
||||
for stream in self.xml[0][0][0]:
|
||||
if stream.get('streamType') == '3':
|
||||
return stream.get('decision')
|
|
@ -1040,3 +1040,74 @@ def show_episodes(plex_id):
|
|||
'skipRefresh': 1,
|
||||
}
|
||||
return DownloadChunks(utils.extend_url(url, arguments))
|
||||
|
||||
|
||||
def transcoding_arguments(path, media, part, playmethod, args=None):
|
||||
if playmethod == v.PLAYBACK_METHOD_DIRECT_PLAY:
|
||||
direct_play = 1
|
||||
direct_stream = 1
|
||||
elif playmethod == v.PLAYBACK_METHOD_DIRECT_STREAM:
|
||||
direct_play = 0
|
||||
direct_stream = 1
|
||||
elif playmethod == v.PLAYBACK_METHOD_TRANSCODE:
|
||||
direct_play = 0
|
||||
direct_stream = 0
|
||||
arguments = {
|
||||
# e.g. '/library/metadata/831399'
|
||||
'path': path,
|
||||
# 1 if you want to directPlay, 0 if you want to transcode
|
||||
'directPlay': direct_play,
|
||||
# 1 if you want to play a stream copy of data into a new container. This
|
||||
# is unlikely to come up but it’s possible if you are playing something
|
||||
# with a lot of tracks, a direct stream can result in lower bandwidth
|
||||
# when a direct play would be over the limit.
|
||||
# Assume Kodi can always handle any stream thrown at it!
|
||||
'directStream': direct_stream,
|
||||
# Same for audio - assume Kodi can play any audio stream passed in!
|
||||
'directStreamAudio': direct_stream,
|
||||
# This tells the server that you definitively know that the client can
|
||||
# direct play (when you have directPlay=1) the content in spite of what
|
||||
# client profiles may say about what the client can play. When this is
|
||||
# set and directPlay=1, the server just checks bandwidth restrictions
|
||||
# and gives you a reservation if bandwidth restrictions are met
|
||||
'hasMDE': direct_play,
|
||||
# where # is an integer, 0 indexed. If you specify directPlay, this is
|
||||
# required. -1 indicates let the server choose.
|
||||
'mediaIndex': media,
|
||||
# Similar to mediaIndex but indicates which part you want to direct
|
||||
# play. Really only comes into play with multi-part files which are
|
||||
# uncommon. -1 here means concatenate the parts together but that
|
||||
# requires the transcoder.
|
||||
'partIndex': part,
|
||||
# all the rest
|
||||
'audioBoost': utils.settings('audioBoost'),
|
||||
'autoAdjustQuality': 1,
|
||||
'protocol': 'hls', # seen in the wild: 'http', 'dash', 'http', 'hls'
|
||||
'session': v.PKC_MACHINE_IDENTIFIER, # TODO: create new unique id
|
||||
'fastSeek': 1,
|
||||
# none, embedded, sidecar
|
||||
# Essentially indicating what you want to do with subtitles and state
|
||||
# you aren’t want it to burn them into the video (requires transcoding)
|
||||
'subtitles': 'none',
|
||||
# 'subtitleSize': utils.settings('subtitleSize')
|
||||
'copyts': 1
|
||||
}
|
||||
if args:
|
||||
arguments.update(args)
|
||||
return arguments
|
||||
|
||||
|
||||
def playback_decision(path, media, part, playmethod, video=True, args=None):
|
||||
"""
|
||||
Let's the PMS decide how we should playback this file
|
||||
"""
|
||||
arguments = transcoding_arguments(path, media, part, playmethod, args=args)
|
||||
if video:
|
||||
url = '{server}/video/:/transcode/universal/decision'
|
||||
else:
|
||||
url = '{server}/music/:/transcode/universal/decision'
|
||||
LOG.debug('Asking the PMS if we can play this video with settings: %s',
|
||||
arguments)
|
||||
return DU().downloadUrl(utils.extend_url(url, arguments),
|
||||
headerOptions=v.STREAMING_HEADERS,
|
||||
reraise=True)
|
||||
|
|
|
@ -51,7 +51,6 @@ class Service(object):
|
|||
return
|
||||
# Initial logging
|
||||
LOG.info("======== START %s ========", v.ADDON_NAME)
|
||||
LOG.info("Platform: %s", v.PLATFORM)
|
||||
LOG.info("KODI Version: %s", v.KODILONGVERSION)
|
||||
LOG.info("%s Version: %s", v.ADDON_NAME, v.ADDON_VERSION)
|
||||
LOG.info("PKC Direct Paths: %s",
|
||||
|
|
|
@ -70,9 +70,6 @@ else:
|
|||
DEVICE = "Unknown"
|
||||
|
||||
MODEL = platform.release() or 'Unknown'
|
||||
# Plex' own platform for e.g. Plex Media Player
|
||||
PLATFORM = 'Konvergo'
|
||||
PLATFORM_VERSION = '2.26.0.947-1e21fa2b'
|
||||
|
||||
DEVICENAME = try_decode(_ADDON.getSetting('deviceName'))
|
||||
if not DEVICENAME:
|
||||
|
@ -138,6 +135,7 @@ EXTERNAL_SUBTITLE_TEMP_PATH = try_decode(xbmc.translatePath(
|
|||
# Multiply Plex time by this factor to receive Kodi time
|
||||
PLEX_TO_KODI_TIMEFACTOR = 1.0 / 1000.0
|
||||
|
||||
|
||||
# Playlist stuff
|
||||
PLAYLIST_PATH = os.path.join(KODI_PROFILE, 'playlists')
|
||||
PLAYLIST_PATH_MIXED = os.path.join(PLAYLIST_PATH, 'mixed')
|
||||
|
@ -447,6 +445,58 @@ CONTENT_FROM_PLEX_TYPE = {
|
|||
None: CONTENT_TYPE_FILE
|
||||
}
|
||||
|
||||
|
||||
# Plex profile for transcoding and direct streaming
|
||||
# Uses the empty Generic.xml at Plex Media Server/Resources/Profiles for any
|
||||
# Playback decisions
|
||||
PLATFORM = 'Generic'
|
||||
# Version seems to be irrelevant for the generic platform
|
||||
PLATFORM_VERSION = '1.0.0'
|
||||
# Overrides (replace=true) any existing entries in generic.xml
|
||||
STREAMING_HEADERS = {
|
||||
'X-Plex-Client-Profile-Extra':
|
||||
# Would allow to DirectStream anything, but seems to be rather faulty
|
||||
# 'add-transcode-target('
|
||||
# 'type=videoProfile&'
|
||||
# 'context=streaming&'
|
||||
# 'protocol=hls&'
|
||||
# 'container=mpegts&'
|
||||
# 'videoCodec=h264,*&'
|
||||
# 'audioCodec=aac,*&'
|
||||
# 'subtitleCodec=ass,pgs,vobsub,srt,*&'
|
||||
# 'replace=true)'
|
||||
('add-transcode-target('
|
||||
'type=videoProfile&'
|
||||
'context=streaming&'
|
||||
'protocol=hls&'
|
||||
'container=mpegts&'
|
||||
'videoCodec=h264,hevc,mpeg4,mpeg2video&'
|
||||
'audioCodec=aac,flac,vorbis,opus,ac3,eac3,mp3,mp2,pcm&'
|
||||
'subtitleCodec=ass,pgs,vobsub&'
|
||||
'replace=true)'
|
||||
'+add-direct-play-profile('
|
||||
'type=videoProfile&'
|
||||
'container=*&'
|
||||
'videoCodec=*&'
|
||||
'audioCodec=*&'
|
||||
'subtitleCodec=*&'
|
||||
'replace=true)')
|
||||
}
|
||||
|
||||
PLAYBACK_METHOD_DIRECT_PATH = 0
|
||||
PLAYBACK_METHOD_DIRECT_PLAY = 1
|
||||
PLAYBACK_METHOD_DIRECT_STREAM = 2
|
||||
PLAYBACK_METHOD_TRANSCODE = 3
|
||||
|
||||
EXPLICIT_PLAYBACK_METHOD = {
|
||||
PLAYBACK_METHOD_DIRECT_PATH: 'DirectPath',
|
||||
PLAYBACK_METHOD_DIRECT_PLAY: 'DirectPlay',
|
||||
PLAYBACK_METHOD_DIRECT_STREAM: 'DirectStream',
|
||||
PLAYBACK_METHOD_TRANSCODE: 'Transcode',
|
||||
None: None
|
||||
}
|
||||
|
||||
|
||||
KODI_TO_PLEX_ARTWORK = {
|
||||
'poster': 'thumb',
|
||||
'banner': 'banner',
|
||||
|
|
Loading…
Reference in a new issue