Merge pull request #223 from neilsb/initial-live-tv
Initial Live TV Support
This commit is contained in:
commit
93c64f7422
|
@ -2,8 +2,13 @@ sub init()
|
|||
m.title = m.top.findNode("title")
|
||||
m.staticTitle = m.top.findNode("staticTitle")
|
||||
m.poster = m.top.findNode("poster")
|
||||
|
||||
m.backdrop = m.top.findNode("backdrop")
|
||||
m.backdrop.color = "#404040FF"
|
||||
|
||||
' Randmomise the background colors
|
||||
posterBackgrounds = [ "#5ccea9", "#d2b019", "#dd452b", "#338abb", "#6b689d" ]
|
||||
m.backdrop.color = posterBackgrounds[rnd(posterBackgrounds.count()) - 1]
|
||||
|
||||
updateSize()
|
||||
end sub
|
||||
|
||||
|
|
15
components/data/ChannelData.brs
Normal file
15
components/data/ChannelData.brs
Normal file
|
@ -0,0 +1,15 @@
|
|||
sub setFields()
|
||||
json = m.top.json
|
||||
|
||||
m.top.id = json.id
|
||||
m.top.title = json.name
|
||||
m.top.live = true
|
||||
end sub
|
||||
|
||||
sub setPoster()
|
||||
if m.top.image <> invalid
|
||||
m.top.posterURL = m.top.image.url
|
||||
else
|
||||
m.top.posterURL = ""
|
||||
end if
|
||||
end sub
|
11
components/data/ChannelData.xml
Normal file
11
components/data/ChannelData.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<component name="ChannelData" extends="ContentNode">
|
||||
<interface>
|
||||
<field id="id" type="string" />
|
||||
<field id="title" type="string" />
|
||||
<field id="image" type="node" onChange="setPoster" />
|
||||
<field id="posterURL" type="string" />
|
||||
<field id="json" type="associativearray" onChange="setFields" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="ChannelData.brs" />
|
||||
</component>
|
|
@ -16,6 +16,12 @@ sub itemContentChanged()
|
|||
m.itemText.maxWidth = imageWidth
|
||||
itemTextExtra.width = imageWidth
|
||||
|
||||
' Randmomise the background colors
|
||||
m.backdrop = m.top.findNode("backdrop")
|
||||
posterBackgrounds = [ "#5ccea9", "#d2b019", "#dd452b", "#338abb", "#6b689d" ]
|
||||
m.backdrop.color = posterBackgrounds[rnd(posterBackgrounds.count()) - 1]
|
||||
m.backdrop.width = imageWidth
|
||||
|
||||
' Whether to use WidePoster or Thumbnail in this row
|
||||
usePoster = m.top.GetParent().content.usePoster
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<component name="HomeItem" extends="Group">
|
||||
<children>
|
||||
<Rectangle id="backdrop" width="464" height="261" translation="[8,5]" />
|
||||
<Poster id="itemPoster" width="464" height="261" translation="[8,5]" />
|
||||
<ScrollingLabel id="itemText" horizAlign="center" vertAlign="center" font="font:SmallBoldSystemFont" height="64" maxWidth="456" translation="[8,267]" repeatCount="0" />
|
||||
<Label id="itemTextExtra" horizAlign="left" vertAlign="center" font="font:SmallBoldSystemFont" height="32" width="456" translation="[8,300]" visible="false" color="#777777FF" />
|
||||
|
|
|
@ -11,5 +11,6 @@
|
|||
<script type="text/brightscript" uri="pkg:/source/api/Items.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
|
||||
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
|
||||
</component>
|
14
components/livetv/Channels.brs
Normal file
14
components/livetv/Channels.brs
Normal file
|
@ -0,0 +1,14 @@
|
|||
sub init()
|
||||
|
||||
end sub
|
||||
|
||||
function onKeyEvent(key as string, press as boolean) as boolean
|
||||
if not press then return false
|
||||
|
||||
if key = "down"
|
||||
m.top.lastFocus = m.top.focusedChild
|
||||
m.top.findNode("paginator").setFocus(true)
|
||||
end if
|
||||
|
||||
return false
|
||||
end function
|
14
components/livetv/Channels.xml
Normal file
14
components/livetv/Channels.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<component name="Channels" extends="JFGroup">
|
||||
<children>
|
||||
<ItemGrid id="picker" visible="true" itemsPerRow="6" />
|
||||
<OptionsSlider id="options" />
|
||||
<Rectangle translation="[0,981]" width="1920" height="100" color="#101010" />
|
||||
</children>
|
||||
<interface>
|
||||
<field id="channelSelected" alias="picker.itemSelected" />
|
||||
<field id="objects" alias="picker.objects" />
|
||||
<field id="pageNumber" type="integer" />
|
||||
</interface>
|
||||
<script type="text/brightscript" uri="Channels.brs" />
|
||||
</component>
|
|
@ -213,5 +213,21 @@
|
|||
<source>Server</source>
|
||||
<translation>Server</translation>
|
||||
</message>
|
||||
|
||||
<message>
|
||||
<source>Loading Channel Data</source>
|
||||
<translation>Loading Channel Data</translation>
|
||||
</message>
|
||||
|
||||
<message>
|
||||
<source>Error loading Channel Data</source>
|
||||
<translation>Error loading Channel Data</translation>
|
||||
</message>
|
||||
|
||||
<message>
|
||||
<source>Unable to load Channel Data from the server</source>
|
||||
<translation>Unable to load Channel Data from the server</translation>
|
||||
</message>
|
||||
|
||||
</context>
|
||||
</TS>
|
||||
|
|
|
@ -213,5 +213,21 @@
|
|||
<source>Server</source>
|
||||
<translation>Server</translation>
|
||||
</message>
|
||||
|
||||
<message>
|
||||
<source>Loading Channel Data</source>
|
||||
<translation>Loading Channel Data</translation>
|
||||
</message>
|
||||
|
||||
<message>
|
||||
<source>Error loading Channel Data</source>
|
||||
<translation>Error loading Channel Data</translation>
|
||||
</message>
|
||||
|
||||
<message>
|
||||
<source>Unable to load Channel Data from the server</source>
|
||||
<translation>Unable to load Channel Data from the server</translation>
|
||||
</message>
|
||||
|
||||
</context>
|
||||
</TS>
|
||||
|
|
|
@ -213,5 +213,21 @@
|
|||
<source>Server</source>
|
||||
<translation>Server</translation>
|
||||
</message>
|
||||
|
||||
<message>
|
||||
<source>Loading Channel Data</source>
|
||||
<translation>Loading Channel Data</translation>
|
||||
</message>
|
||||
|
||||
<message>
|
||||
<source>Error loading Channel Data</source>
|
||||
<translation>Error loading Channel Data</translation>
|
||||
</message>
|
||||
|
||||
<message>
|
||||
<source>Unable to load Channel Data from the server</source>
|
||||
<translation>Unable to load Channel Data from the server</translation>
|
||||
</message>
|
||||
|
||||
</context>
|
||||
</TS>
|
||||
|
|
|
@ -103,6 +103,15 @@ sub Main()
|
|||
group = CreateCollectionsList(selectedItem.Id)
|
||||
group.overhangTitle = selectedItem.name
|
||||
m.scene.appendChild(group)
|
||||
else if (selectedItem.type = "CollectionFolder" OR selectedItem.type = "UserView") AND selectedItem.collectionType = "livetv"
|
||||
group.lastFocus = group.focusedChild
|
||||
group.setFocus(false)
|
||||
group.visible = false
|
||||
|
||||
m.overhang.title = selectedItem.name
|
||||
group = CreateChannelList(selectedItem.Id)
|
||||
group.overhangTitle = selectedItem.name
|
||||
m.scene.appendChild(group)
|
||||
else if selectedItem.type = "Episode" then
|
||||
' play episode
|
||||
' todo: create an episode page to link here
|
||||
|
@ -222,6 +231,37 @@ sub Main()
|
|||
ReportPlayback(group, "start")
|
||||
m.overhang.visible = false
|
||||
end if
|
||||
else if isNodeEvent(msg, "channelSelected")
|
||||
' If you select a Channel from ANYWHERE, follow this flow
|
||||
node = getMsgPicker(msg, "picker")
|
||||
video_id = node.id
|
||||
|
||||
' Show Channel Loading spinner
|
||||
dialog = createObject("roSGNode", "ProgressDialog")
|
||||
dialog.title = tr("Loading Channel Data")
|
||||
m.scene.dialog = dialog
|
||||
|
||||
video = CreateVideoPlayerGroup(video_id)
|
||||
dialog.close = true
|
||||
|
||||
if video <> invalid then
|
||||
group.lastFocus = group.focusedChild
|
||||
group.setFocus(false)
|
||||
group.visible = false
|
||||
group = video
|
||||
m.scene.appendChild(group)
|
||||
group.setFocus(true)
|
||||
group.control = "play"
|
||||
ReportPlayback(group, "start")
|
||||
m.overhang.visible = false
|
||||
else
|
||||
dialog = createObject("roSGNode", "Dialog")
|
||||
dialog.title = tr("Error loading Channel Data")
|
||||
dialog.message = tr("Unable to load Channel Data from the server")
|
||||
dialog.buttons = [tr("OK")]
|
||||
m.scene.dialog = dialog
|
||||
end if
|
||||
|
||||
else if isNodeEvent(msg, "search_value")
|
||||
query = msg.getRoSGNode().search_value
|
||||
group.findNode("SearchBox").visible = false
|
||||
|
@ -239,6 +279,8 @@ sub Main()
|
|||
CollectionLister(group, m.page_size)
|
||||
else if collectionType = "TVShows"
|
||||
SeriesLister(group, m.page_size)
|
||||
else if collectionType = "Channels"
|
||||
ChannelLister(group, m.page_size)
|
||||
end if
|
||||
' TODO - abstract away the "picker" node
|
||||
group.findNode("picker").setFocus(true)
|
||||
|
@ -338,7 +380,7 @@ sub Main()
|
|||
end if
|
||||
else if isNodeEvent(msg, "position")
|
||||
video = msg.getRoSGNode()
|
||||
if video.position >= video.duration then
|
||||
if video.position >= video.duration and not video.content.live then
|
||||
stopPlayback()
|
||||
end if
|
||||
else if isNodeEvent(msg, "fire")
|
||||
|
@ -452,6 +494,12 @@ function LoginFlow(startOver = false as boolean)
|
|||
end if
|
||||
|
||||
wipe_groups()
|
||||
|
||||
'Send Device Profile information to server
|
||||
body = getDeviceCapabilities()
|
||||
req = APIRequest("/Sessions/Capabilities/Full")
|
||||
req.SetRequest("POST")
|
||||
postJson(req, FormatJson(body))
|
||||
return true
|
||||
end function
|
||||
|
||||
|
|
|
@ -344,6 +344,57 @@ function CreateCollectionsList(libraryId)
|
|||
return group
|
||||
end function
|
||||
|
||||
|
||||
function CreateChannelList(libraryId)
|
||||
group = CreateObject("roSGNode", "Channels")
|
||||
group.id = libraryId
|
||||
|
||||
group.observeField("channelSelected", m.port)
|
||||
|
||||
|
||||
sidepanel = group.findNode("options")
|
||||
channel_options = [
|
||||
{"title": "Sort Field",
|
||||
"base_title": "Sort Field",
|
||||
"key": "channel_sort_field",
|
||||
"default": "Name",
|
||||
"values": [
|
||||
{display: tr("Name"), value: "SortName"}
|
||||
]},
|
||||
{"title": "Sort Order",
|
||||
"base_title": "Sort Order",
|
||||
"key": "channel_sort_order",
|
||||
"default": "Ascending",
|
||||
"values": [
|
||||
{display: tr("Descending"), value: "Descending"},
|
||||
{display: tr("Ascending"), value: "Ascending"}
|
||||
]}
|
||||
]
|
||||
new_options = []
|
||||
for each opt in channel_options
|
||||
o = CreateObject("roSGNode", "OptionsData")
|
||||
o.title = tr(opt.title)
|
||||
o.choices = opt.values
|
||||
o.base_title = tr(opt.base_title)
|
||||
o.config_key = opt.key
|
||||
o.value = get_user_setting(opt.key, opt.default)
|
||||
new_options.append([o])
|
||||
end for
|
||||
|
||||
sidepanel.options = new_options
|
||||
sidepanel.observeField("closeSidePanel", m.port)
|
||||
|
||||
p = CreatePaginator()
|
||||
group.appendChild(p)
|
||||
|
||||
group.pageNumber = 1
|
||||
p.currentPage = group.pageNumber
|
||||
|
||||
ChannelLister(group, m.page_size)
|
||||
|
||||
return group
|
||||
end function
|
||||
|
||||
function CreateSearchPage()
|
||||
' Search + Results Page
|
||||
group = CreateObject("roSGNode", "SearchResults")
|
||||
|
@ -419,3 +470,15 @@ function CollectionLister(group, page_size)
|
|||
p = group.findNode("paginator")
|
||||
p.maxPages = div_ceiling(group.objects.TotalRecordCount, page_size)
|
||||
end function
|
||||
|
||||
function ChannelLister(group, page_size)
|
||||
sort_order = get_user_setting("channel_sort_order", "Ascending")
|
||||
sort_field = get_user_setting("channel_sort_field", "SortName")
|
||||
group.objects = Channels({"limit": page_size,
|
||||
"StartIndex": page_size * (group.pageNumber - 1),
|
||||
"SortBy": sort_field,
|
||||
"SortOrder": sort_order,
|
||||
})
|
||||
p = group.findNode("paginator")
|
||||
p.maxPages = div_ceiling(group.objects.TotalRecordCount, page_size)
|
||||
end function
|
||||
|
|
|
@ -21,8 +21,6 @@ function VideoContent(video) as object
|
|||
|
||||
meta = ItemMetaData(video.id)
|
||||
video.content.title = meta.Name
|
||||
container = getContainerType(meta)
|
||||
video.container = container
|
||||
|
||||
' If there is a last playback positon, ask user if they want to resume
|
||||
position = meta.json.UserData.PlaybackPositionTicks
|
||||
|
@ -39,10 +37,42 @@ function VideoContent(video) as object
|
|||
end if
|
||||
video.content.PlayStart = int(position/10000000)
|
||||
|
||||
video.PlaySessionId = ItemGetSession(video.id, position)
|
||||
playbackInfo = ItemPostPlaybackInfo(video.id, position)
|
||||
|
||||
if playbackInfo = invalid then
|
||||
return invalid
|
||||
end if
|
||||
|
||||
video.PlaySessionId = playbackInfo.PlaySessionId
|
||||
|
||||
if meta.live then
|
||||
video.content.live = true
|
||||
video.content.StreamFormat = "hls"
|
||||
|
||||
'Original MediaSource seems to be a placeholder and real stream data is avaiable
|
||||
'after POSTing to PlaybackInfo
|
||||
json = meta.json
|
||||
json.AddReplace("MediaSources", playbackInfo.MediaSources)
|
||||
json.AddReplace("MediaStreams", playbackInfo.MediaSources[0].MediaStreams)
|
||||
meta.json = json
|
||||
end if
|
||||
|
||||
container = getContainerType(meta)
|
||||
video.container = container
|
||||
|
||||
transcodeParams = getTranscodeParameters(meta)
|
||||
transcodeParams.append({"PlaySessionId": video.PlaySessionId})
|
||||
|
||||
if meta.live then
|
||||
_livestream_params = {
|
||||
"MediaSourceId": playbackInfo.MediaSources[0].Id,
|
||||
"LiveStreamId": playbackInfo.MediaSources[0].LiveStreamId,
|
||||
"MinSegments": 2 'This is a guess about initial buffer size, segments are 3s each
|
||||
}
|
||||
params.append(_livestream_params)
|
||||
transcodeParams.append(_livestream_params)
|
||||
end if
|
||||
|
||||
subtitles = sortSubtitles(meta.id,meta.json.MediaStreams)
|
||||
video.Subtitles = subtitles["all"]
|
||||
video.content.SubtitleTracks = subtitles["text"]
|
||||
|
@ -177,12 +207,26 @@ end function
|
|||
|
||||
function directPlaySupported(meta as object) as boolean
|
||||
devinfo = CreateObject("roDeviceInfo")
|
||||
return devinfo.CanDecodeVideo({ Codec: meta.json.MediaStreams[0].codec }).result
|
||||
if meta.json.MediaSources[0] <> invalid and meta.json.MediaSources[0].SupportsDirectPlay = false then
|
||||
return false
|
||||
end if
|
||||
streamInfo = { Codec: meta.json.MediaStreams[0].codec }
|
||||
if meta.json.MediaStreams[0].Profile <> invalid and meta.json.MediaStreams[0].Profile.len() > 0 then
|
||||
streamInfo.Profile = meta.json.MediaStreams[0].Profile
|
||||
end if
|
||||
if meta.json.MediaSources[0].container <> invalid and meta.json.MediaSources[0].container.len() > 0 then
|
||||
streamInfo.Container = meta.json.MediaSources[0].container
|
||||
end if
|
||||
return devinfo.CanDecodeVideo(streamInfo).result
|
||||
end function
|
||||
|
||||
function decodeAudioSupported(meta as object) as boolean
|
||||
devinfo = CreateObject("roDeviceInfo")
|
||||
return devinfo.CanDecodeAudio({ Codec: meta.json.MediaStreams[1].codec, ChCnt: meta.json.MediaStreams[1].channels }).result
|
||||
streamInfo = { Codec: meta.json.MediaStreams[1].codec, ChCnt: meta.json.MediaStreams[1].channels }
|
||||
if meta.json.MediaStreams[1].Bitrate <> invalid then
|
||||
streamInfo.BitRate = meta.json.MediaStreams[1].Bitrate
|
||||
end if
|
||||
return devinfo.CanDecodeAudio(streamInfo).result
|
||||
end function
|
||||
|
||||
function getContainerType(meta as object) as string
|
||||
|
@ -226,6 +270,12 @@ function ReportPlayback(video, state = "update" as string)
|
|||
"PositionTicks": str(int(video.position)) + "0000000",
|
||||
"IsPaused": (video.state = "paused"),
|
||||
}
|
||||
if video.content.live then
|
||||
params.append({
|
||||
"MediaSourceId": video.transcodeParams.MediaSourceId,
|
||||
"LiveStreamId": video.transcodeParams.LiveStreamId
|
||||
})
|
||||
end if
|
||||
PlaystateUpdate(video.id, state, params)
|
||||
end function
|
||||
|
||||
|
|
|
@ -22,17 +22,32 @@ function UserItemsResume(params = {} as object)
|
|||
return data
|
||||
end function
|
||||
|
||||
function ItemGetSession(id as string, StartTimeTicks = 0 as longinteger)
|
||||
function ItemGetPlaybackInfo(id as string, StartTimeTicks = 0 as longinteger)
|
||||
params = {
|
||||
UserId: get_setting("active_user"),
|
||||
StartTimeTicks: StartTimeTicks,
|
||||
IsPlayback: "true",
|
||||
AutoOpenLiveStream: "true",
|
||||
MaxStreamingBitrate: "140000000"
|
||||
"UserId": get_setting("active_user"),
|
||||
"StartTimeTicks": StartTimeTicks,
|
||||
"IsPlayback": true,
|
||||
"AutoOpenLiveStream": true,
|
||||
"MaxStreamingBitrate": "140000000"
|
||||
}
|
||||
resp = APIRequest(Substitute("Items/{0}/PlaybackInfo", id), params)
|
||||
data = getJson(resp)
|
||||
return data.PlaySessionId
|
||||
return getJson(resp)
|
||||
end function
|
||||
|
||||
function ItemPostPlaybackInfo(id as string, StartTimeTicks = 0 as longinteger)
|
||||
body = {
|
||||
"DeviceProfile": getDeviceProfile()
|
||||
}
|
||||
params = {
|
||||
"UserId": get_setting("active_user"),
|
||||
"StartTimeTicks": StartTimeTicks,
|
||||
"IsPlayback": true,
|
||||
"AutoOpenLiveStream": true,
|
||||
"MaxStreamingBitrate": "140000000"
|
||||
}
|
||||
req = APIRequest(Substitute("Items/{0}/PlaybackInfo", id), params)
|
||||
req.SetRequest("POST")
|
||||
return postJson(req, FormatJson(body))
|
||||
end function
|
||||
|
||||
' Search across all libraries
|
||||
|
@ -162,6 +177,11 @@ function ItemMetaData(id as string)
|
|||
tmp.image = PosterImage(data.id)
|
||||
tmp.json = data
|
||||
return tmp
|
||||
else if data.type = "TvChannel"
|
||||
tmp = CreateObject("roSGNode", "ChannelData")
|
||||
tmp.image = PosterImage(data.id)
|
||||
tmp.json = data
|
||||
return tmp
|
||||
else
|
||||
print "Items.brs::ItemMetaData processed unhandled type: " data.type
|
||||
' Return json if we don't know what it is
|
||||
|
@ -228,3 +248,30 @@ function TVNext(id as string)
|
|||
end for
|
||||
return data
|
||||
end function
|
||||
|
||||
function Channels(params = {})
|
||||
if params["limit"] = invalid
|
||||
params["limit"] = 30
|
||||
end if
|
||||
if params["page"] = invalid
|
||||
params["page"] = 1
|
||||
end if
|
||||
params["recursive"] = true
|
||||
|
||||
resp = APIRequest("LiveTv/Channels", params)
|
||||
|
||||
data = getJson(resp)
|
||||
results = []
|
||||
for each item in data.Items
|
||||
imgParams = { "maxWidth": 712, "maxheight": 400 }
|
||||
tmp = CreateObject("roSGNode", "ChannelData")
|
||||
tmp.image = PosterImage(item.id, imgParams)
|
||||
if tmp.image <> invalid
|
||||
tmp.image.posterDisplayMode = "scaleToFit"
|
||||
end if
|
||||
tmp.json = item
|
||||
results.push(tmp)
|
||||
end for
|
||||
data.Items = results
|
||||
return data
|
||||
end function
|
||||
|
|
|
@ -81,7 +81,7 @@ function postJson(req, data="" as string)
|
|||
req.setMessagePort(CreateObject("roMessagePort"))
|
||||
req.AsyncPostFromString(data)
|
||||
|
||||
resp = wait(5000, req.GetMessagePort())
|
||||
resp = wait(30000, req.GetMessagePort())
|
||||
if type(resp) <> "roUrlEvent"
|
||||
return invalid
|
||||
end if
|
||||
|
|
|
@ -136,7 +136,7 @@ sub rebuildURL(captions as boolean)
|
|||
if video.isTranscoded then
|
||||
deleteTranscode(video.PlaySessionId)
|
||||
end if
|
||||
video.PlaySessionId = ItemGetSession(video.id, int(video.position) + playBackBuffer)
|
||||
video.PlaySessionId = ItemGetPlaybackInfo(video.id, int(video.position) + playBackBuffer).PlaySessionId
|
||||
tmpParams.PlaySessionId = video.PlaySessionId
|
||||
video.transcodeParams = tmpParams
|
||||
|
||||
|
|
185
source/utils/deviceCapabilities.brs
Normal file
185
source/utils/deviceCapabilities.brs
Normal file
|
@ -0,0 +1,185 @@
|
|||
'Device Capabilities for Roku.
|
||||
'This may need tweaking or be dynamically created if devices vary
|
||||
'significantly
|
||||
|
||||
function getDeviceCapabilities() as object
|
||||
|
||||
return {
|
||||
"PlayableMediaTypes": [
|
||||
"Audio",
|
||||
"Video"
|
||||
],
|
||||
"SupportedCommands": [],
|
||||
"SupportsPersistentIdentifier": false,
|
||||
"SupportsMediaControl": false,
|
||||
"DeviceProfile": getDeviceProfile()
|
||||
}
|
||||
end function
|
||||
|
||||
|
||||
function getDeviceProfile() as object
|
||||
|
||||
'Check if 5.1 Audio Output connected
|
||||
maxAudioChannels = 2
|
||||
di = CreateObject("roDeviceInfo")
|
||||
if di.GetAudioOutputChannel() = "5.1 surround" then
|
||||
maxAudioChannels = 6
|
||||
end if
|
||||
|
||||
'Check for Supported Codecs
|
||||
deviceSpecificCodecs = ""
|
||||
if di.CanDecodeVideo({Codec: "hevc"}).Result = true
|
||||
deviceSpecificCodecs = ",h265"
|
||||
end if
|
||||
|
||||
if di.CanDecodeVideo({Codec: "vp9"}).Result = true
|
||||
deviceSpecificCodecs = deviceSpecificCodecs + ",vp9"
|
||||
end if
|
||||
|
||||
|
||||
|
||||
return {
|
||||
"MaxStreamingBitrate": 120000000,
|
||||
"MaxStaticBitrate": 100000000,
|
||||
"MusicStreamingTranscodingBitrate": 192000,
|
||||
"DirectPlayProfiles": [
|
||||
{
|
||||
"Container": "mp4,m4v,mov",
|
||||
"Type": "Video",
|
||||
"VideoCodec": "h264" + deviceSpecificCodecs,
|
||||
"AudioCodec": "aac,opus,flac,vorbis"
|
||||
},
|
||||
{
|
||||
"Container": "mkv,webm",
|
||||
"Type": "Video",
|
||||
"VideoCodec": "h264,vp8" + deviceSpecificCodecs,
|
||||
"AudioCodec": "aac,opus,flac,vorbis"
|
||||
},
|
||||
{
|
||||
"Container": "mp3",
|
||||
"Type": "Audio",
|
||||
"AudioCodec": "mp3"
|
||||
},
|
||||
{
|
||||
"Container": "aac",
|
||||
"Type": "Audio"
|
||||
},
|
||||
{
|
||||
"Container": "m4a",
|
||||
"AudioCodec": "aac",
|
||||
"Type": "Audio"
|
||||
},
|
||||
{
|
||||
"Container": "flac",
|
||||
"Type": "Audio"
|
||||
}
|
||||
],
|
||||
"TranscodingProfiles": [
|
||||
{
|
||||
"Container": "aac",
|
||||
"Type": "Audio",
|
||||
"AudioCodec": "aac",
|
||||
"Context": "Streaming",
|
||||
"Protocol": "http",
|
||||
"MaxAudioChannels": maxAudioChannels
|
||||
},
|
||||
{
|
||||
"Container": "mp3",
|
||||
"Type": "Audio",
|
||||
"AudioCodec": "mp3",
|
||||
"Context": "Streaming",
|
||||
"Protocol": "http",
|
||||
"MaxAudioChannels": 2
|
||||
},
|
||||
{
|
||||
"Container": "mp3",
|
||||
"Type": "Audio",
|
||||
"AudioCodec": "mp3",
|
||||
"Context": "Static",
|
||||
"Protocol": "http",
|
||||
"MaxAudioChannels": 2
|
||||
},
|
||||
{
|
||||
"Container": "aac",
|
||||
"Type": "Audio",
|
||||
"AudioCodec": "aac",
|
||||
"Context": "Static",
|
||||
"Protocol": "http",
|
||||
"MaxAudioChannels": maxAudioChannels
|
||||
},
|
||||
{
|
||||
"Container": "ts",
|
||||
"Type": "Video",
|
||||
"AudioCodec": "aac",
|
||||
"VideoCodec": "h264",
|
||||
"Context": "Streaming",
|
||||
"Protocol": "hls",
|
||||
"MaxAudioChannels": maxAudioChannels,
|
||||
"MinSegments": "1",
|
||||
"BreakOnNonKeyFrames": true
|
||||
},
|
||||
{
|
||||
"Container": "mp4",
|
||||
"Type": "Video",
|
||||
"AudioCodec": "aac,opus,flac,vorbis",
|
||||
"VideoCodec": "h264",
|
||||
"Context": "Static",
|
||||
"Protocol": "http"
|
||||
}
|
||||
],
|
||||
"ContainerProfiles": [],
|
||||
"CodecProfiles": [
|
||||
{
|
||||
"Type": "VideoAudio",
|
||||
"Codec": "aac",
|
||||
"Conditions": [
|
||||
{
|
||||
"Condition": "Equals",
|
||||
"Property": "IsSecondaryAudio",
|
||||
"Value": "false",
|
||||
"IsRequired": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Type": "Video",
|
||||
"Codec": "h264",
|
||||
"Conditions": [
|
||||
{
|
||||
"Condition": "EqualsAny",
|
||||
"Property": "VideoProfile",
|
||||
"Value": "high|main|baseline|constrained baseline",
|
||||
"IsRequired": false
|
||||
},
|
||||
{
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "VideoLevel",
|
||||
"Value": "51",
|
||||
"IsRequired": false
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SubtitleProfiles": [
|
||||
{
|
||||
"Format": "vtt",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "ass",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "ssa",
|
||||
"Method": "External"
|
||||
}
|
||||
],
|
||||
"ResponseProfiles": [
|
||||
{
|
||||
"Type": "Video",
|
||||
"Container": "m4v",
|
||||
"MimeType": "video/mp4"
|
||||
}
|
||||
]
|
||||
}
|
||||
end function
|
Loading…
Reference in New Issue
Block a user