diff --git a/components/data/ChannelData.brs b/components/data/ChannelData.brs
new file mode 100644
index 00000000..c8690780
--- /dev/null
+++ b/components/data/ChannelData.brs
@@ -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
diff --git a/components/data/ChannelData.xml b/components/data/ChannelData.xml
new file mode 100644
index 00000000..4f291be3
--- /dev/null
+++ b/components/data/ChannelData.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/home/LoadItemsTask.xml b/components/home/LoadItemsTask.xml
index 64028889..0614d0a2 100644
--- a/components/home/LoadItemsTask.xml
+++ b/components/home/LoadItemsTask.xml
@@ -11,5 +11,6 @@
+
\ No newline at end of file
diff --git a/components/livetv/Channels.brs b/components/livetv/Channels.brs
new file mode 100644
index 00000000..6205166d
--- /dev/null
+++ b/components/livetv/Channels.brs
@@ -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
diff --git a/components/livetv/Channels.xml b/components/livetv/Channels.xml
new file mode 100644
index 00000000..b255a2da
--- /dev/null
+++ b/components/livetv/Channels.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source/Main.brs b/source/Main.brs
index c685c378..789d79d1 100644
--- a/source/Main.brs
+++ b/source/Main.brs
@@ -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,22 @@ 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
+ video = CreateVideoPlayerGroup(video_id)
+ 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
+ end if
else if isNodeEvent(msg, "search_value")
query = msg.getRoSGNode().search_value
group.findNode("SearchBox").visible = false
@@ -241,6 +266,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)
@@ -340,7 +367,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")
diff --git a/source/ShowScenes.brs b/source/ShowScenes.brs
index 37b00377..1d3dc30f 100644
--- a/source/ShowScenes.brs
+++ b/source/ShowScenes.brs
@@ -352,6 +352,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")
@@ -443,3 +494,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
diff --git a/source/VideoPlayer.brs b/source/VideoPlayer.brs
index de1fd542..c21f9217 100644
--- a/source/VideoPlayer.brs
+++ b/source/VideoPlayer.brs
@@ -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,37 @@ function VideoContent(video) as object
end if
video.content.PlayStart = int(position/10000000)
- video.PlaySessionId = ItemGetSession(video.id, position)
+ playbackInfo = ItemPostPlaybackInfo(video.id, position)
+ 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"]
@@ -229,6 +254,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
diff --git a/source/api/Items.brs b/source/api/Items.brs
index 8a606e9a..6286a393 100644
--- a/source/api/Items.brs
+++ b/source/api/Items.brs
@@ -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
diff --git a/source/utils/TranscodeSubtitles.brs b/source/utils/TranscodeSubtitles.brs
index 50422f0f..fdb1c9d1 100644
--- a/source/utils/TranscodeSubtitles.brs
+++ b/source/utils/TranscodeSubtitles.brs
@@ -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