def chooseMedia(self, item, forceUpdate=False): # If we've already evaluated this item, use our previous choice. if not forceUpdate and item.mediaChoice is not None and item.mediaChoice.media is not None and not item.mediaChoice.media.isIndirect( ): return item.mediaChoice # See if we're missing media/stream details for this item. if item.isLibraryItem() and item.isVideoItem() and len( item.media) > 0 and not item.media[0].hasStreams(): # TODO(schuyler): Fetch the details util.WARN_LOG("Can't make media choice, missing details") # Take a first pass through the media items to create an array of candidates # that we'll evaluate more completely. If we find a forced item, we use it. # If we find an indirect, we only keep a single candidate. indirect = False candidates = [] maxResolution = item.settings.getMaxResolution(item.getQualityType()) for mediaIndex in range(len(item.media)): media = item.media[mediaIndex] media.mediaIndex = mediaIndex if media.isSelected(): candidates = [] candidates.append(media) break if media.isIndirect(): # Only add indirect media if the resolution fits. We cannot # exit early as the user may have selected media. indirect = True if media.getVideoResolution() <= maxResolution: candidates.append(media) elif media.isAccessible(): # Only consider testing available media candidates.append(media) # Only use the first indirect media item if indirect and candidates: candidates = candidates[0] # Make sure we have at least one valid item, regardless of availability if len(candidates) == 0: candidates.append(item.media[0]) # Now that we have an array of candidates, evaluate them completely. choices = [] for media in candidates: choice = None if media is not None: if item.isVideoItem(): choice = self.evaluateMediaVideo(item, media) elif item.isMusicItem(): choice = self.evaluateMediaMusic(item, media) else: choice = mediachoice.MediaChoice(media) choices.append(choice) item.mediaChoice = self.sortChoices(choices)[-1] util.LOG("MDE: MediaChoice: {0}".format(item.mediaChoice)) return item.mediaChoice
def init(self): self.isSupported = self.response.server.supportsFeature("streamingBrain") self.item = self.response.items[0] if self.item and self.item.media: self.original.transcodeDecision = mediachoice.MediaChoice(self.item.media[0]) # Decision codes and text self.decisionsCodes = {} self.decisionsTexts = {} for key in ["directPlayDecision", "generalDecision", "mdeDecision", "transcodeDecision", "termination"]: self.decisionsCodes[key] = self.response.container.get(key + "Code", "-1").asInt() self.decisionsTexts[key] = self.response.container.get(key + "Text")
def evaluateMediaMusic(self, item, media): # Resolve indirects before doing anything else. if media.isIndirect(): util.LOG("Resolve indirect media for {0}".format(item)) media = media.resolveIndirect() choice = mediachoice.MediaChoice(media) if media is None: return choice # Verify the server supports audio transcoding, otherwise force direct play if not item.getServer().supportsAudioTranscoding: util.LOG( "MDE: force direct play because the server does not support audio transcoding" ) choice.isDirectPlayable = True return choice # See if this part has a server decision to transcode and obey it if choice.part and choice.part.get( "decision", serverdecision.ServerDecision.DECISION_DIRECT_PLAY ) != serverdecision.ServerDecision.DECISION_DIRECT_PLAY: util.WARN_LOG("MDE: Server has decided this cannot direct play") return choice # Verify the codec and container are compatible codec = media.audioCodec container = media.get('container') canPlayCodec = item.settings.supportsAudioStream( codec, media.audioChannels.asInt()) canPlayContainer = ( codec == container) or True # (container in ("mp4", "mka", "mkv")) choice.isDirectPlayable = (canPlayCodec and canPlayContainer) if choice.isDirectPlayable: # Inspect the audio stream attributes if the codec/container can direct # play. For now we only need to verify the sample rate. if choice.audioStream is not None and choice.audioStream.samplingRate.asInt( ) >= 192000: util.LOG("MDE: sampling rate is not compatible") choice.isDirectPlayable = False else: util.LOG("MDE: container or codec is incompatible") return choice
def init(self): self.isSupported = self.response.server.supportsFeature( "streamingBrain") for item in self.response.items: util.TEST(item) if item and item.media: self.item = item self.original.transcodeDecision = mediachoice.MediaChoice( self.item.media[0]) # Decision codes and text self.decisionsCodes = {} self.decisionsTexts = {} for key in [ "directPlayDecision", "generalDecision", "mdeDecision", "transcodeDecision", "termination" ]: self.decisionsCodes[key] = self.response.container.get( key + "Code", "-1").asInt() self.decisionsTexts[key] = self.response.container.get(key + "Text") util.DEBUG_LOG("Decision codes: {0}".format(self.decisionsCodes))
def evaluateMediaVideo(self, item, media, partIndex=0): # Resolve indirects before doing anything else. if media.isIndirect(): util.LOG("Resolve indirect media for {0}".format(item)) media = media.resolveIndirect() choice = mediachoice.MediaChoice(media, partIndex) server = item.getServer() if not media: return choice choice.isSelected = media.isSelected() choice.protocol = media.protocol("http") maxResolution = item.settings.getMaxResolution( item.getQualityType(), self.isSupported4k(media, choice.videoStream)) maxBitrate = item.settings.getMaxBitrate(item.getQualityType()) choice.resolution = media.getVideoResolution() if choice.resolution > maxResolution or media.bitrate.asInt( ) > maxBitrate: choice.forceTranscode = True if choice.subtitleStream: choice.subtitleDecision = self.evaluateSubtitles( choice.subtitleStream) choice.hasBurnedInSubtitles = ( choice.subtitleDecision != choice.SUBTITLES_SOFT_DP and choice.subtitleDecision != choice.SUBTITLES_SOFT_ANY) else: choice.hasBurnedInSubtitles = False # For evaluation purposes, we only care about the first part part = media.parts[partIndex] if not part: return choice # Although PMS has already told us which streams are selected, we can't # necessarily tell the video player which streams we want. So we need to # iterate over the streams and see if there are any red flags that would # prevent direct play. If there are multiple video streams, we're hosed. # For audio streams, we have a fighting chance if the selected stream can # be selected by language, but we need to be careful about guessing which # audio stream the Roku will pick for a given language. numVideoStreams = 0 problematicAudioStream = False if part.get('hasChapterVideoStream').asBool(): numVideoStreams = 1 for stream in part.streams: streamType = stream.streamType.asInt() if streamType == stream.TYPE_VIDEO: numVideoStreams = numVideoStreams + 1 if stream.codec == "h264" or ( stream.codec == "hevc" and item.settings.getPreference("allow_hevc", False) ) or (stream.codec == "vp9" and item.settings.getGlobal("vp9Support")): choice.sorts.videoDS = 1 # Special cases to force direct play forceDirectPlay = False if choice.protocol == "hls": util.LOG("MDE: Assuming HLS is direct playable") forceDirectPlay = True elif not server.supportsVideoTranscoding: # See if we can use another server to transcode, otherwise force direct play transcodeServer = item.getTranscodeServer(True, "video") if not transcodeServer or not transcodeServer.supportsVideoTranscoding: util.LOG( "MDE: force direct play because the server does not support video transcoding" ) forceDirectPlay = True # See if we found any red flags based on the streams. Otherwise, go ahead # with our codec checks. if forceDirectPlay: # Consider the choice DP, but continue to allow the # choice to have the sorts set properly. choice.isDirectPlayable = True elif choice.hasBurnedInSubtitles: util.LOG("MDE: Need to burn in subtitles") elif choice.protocol != "http": util.LOG("MDE: " + choice.protocol + " not supported") elif numVideoStreams > 1: util.LOG("MDE: Multiple video streams, won't try to direct play") elif problematicAudioStream: util.LOG( "MDE: Problematic AAC stream with more than 2 channels prevents direct play" ) elif self.canDirectPlay(item, choice): choice.isDirectPlayable = True elif item.isMediaSynthesized: util.LOG("MDE: assuming synthesized media can direct play") choice.isDirectPlayable = True # Check for a server decision. This is authority as it's the only playback type # the server will allow. This will also support forcing direct play, overriding # only our local MDE checks based on the user pref, and only if the server # agrees. decision = part.get("decision") if decision: if decision != serverdecision.ServerDecision.DECISION_DIRECT_PLAY: util.LOG("MDE: Server has decided this cannot direct play") choice.isDirectPlayable = False else: util.LOG("MDE: Server has allowed direct play") choice.isDirectPlayable = True # Setup sorts if choice.videoStream is not None: choice.sorts.bitrate = choice.videoStream.bitrate.asInt() elif choice.media is not None: choice.sorts.bitrate = choice.media.bitrate.asInt() else: choice.sorts.bitrate = 0 if choice.audioStream is not None: choice.sorts.audioChannels = choice.audioStream.channels.asInt() elif choice.media is not None: choice.sorts.audioChannels = choice.media.audioChannels.asInt() else: choice.sorts.audioChannels = 0 choice.sorts.videoDS = not ( choice.sorts.videoDS is None or choice.forceTranscode is True) and choice.sorts.videoDS or 0 choice.sorts.resolution = choice.resolution # Server properties probably don't need to be associated with each choice choice.sorts.canTranscode = server.supportsVideoTranscoding and 1 or 0 choice.sorts.canRemuxOnly = server.supportsVideoRemuxOnly and 1 or 0 choice.sorts.directPlay = (choice.isDirectPlayable is True and choice.forceTranscode is not True) and 1 or 0 choice.sorts.proxyType = choice.media.proxyType and choice.media.proxyType or self.proxyTypes.NORMAL return choice