diff --git a/CHANGELOG.md b/CHANGELOG.md
index 146fcf03..f681e52f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,9 +11,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Error dialog for video load fail
- SponsorBlock sections and category info
- Loading spinner
+- Picture in picture support
+- [Deep linking support](https://developer.roku.com/en-ca/docs/developer-program/discovery/implementing-deep-linking.md)
+ - Both Launch arguments and Input arguments are supported, using the "contentId" key as the Youtube video id
+- Error and Exit dialogs
### Fixed
- Bug where playing and exiting a video too quickly would cause the video to play in the background.
+- Bug where logging in causes issues if a video is already playing
+- Bug where casting from web app while playing a video from the search
+- Spinner for the video
## [0.4.0] - 2022-11-24
### Added
diff --git a/src/components/Dialog/DialogUtils.bs b/src/components/Dialog/DialogUtils.bs
index 570adb3d..4ab38d38 100644
--- a/src/components/Dialog/DialogUtils.bs
+++ b/src/components/Dialog/DialogUtils.bs
@@ -1,7 +1,15 @@
-function ErrorMessage(message as string, title as string)
+function ShowDialog(message as dynamic, title as string) as object
+ return ShowDialogButtons(message, title, ["Ok"])
+end function
+
+function ShowDialogButtons(message as dynamic, title as string, buttons as object) as object
dialog = CreateObject("roSGNode", "Dialog")
- dialog.message = [message]
+ if GetInterface(message, "ifArray") = invalid
+ message = message.Tokenize("\n")
+ end if
+ dialog.message = message
dialog.title = title
- dialog.buttons = ["Ok"]
+ dialog.buttons = buttons
m.top.getScene().dialog = dialog
+ return dialog
end function
diff --git a/src/components/HomeGridScreen/HomeGridScreen.bs b/src/components/HomeGridScreen/HomeGridScreen.bs
index a20a98c2..71475553 100644
--- a/src/components/HomeGridScreen/HomeGridScreen.bs
+++ b/src/components/HomeGridScreen/HomeGridScreen.bs
@@ -123,6 +123,11 @@ function OnkeyEvent(key as string, press as boolean) as boolean
if press = false
return false
end if
+ if key = "options"
+ if ToggleVideoPictureInPicture(m.top)
+ return true
+ end if
+ end if
if key = "left" or key = "back"
m.top.escape = key
return true
diff --git a/src/components/MainScene.bs b/src/components/MainScene.bs
index 719850f3..1af7db28 100644
--- a/src/components/MainScene.bs
+++ b/src/components/MainScene.bs
@@ -100,3 +100,17 @@ function GetIndexOfChild(parent as object, childId as string) as integer
end for
return -1
end function
+
+function LaunchArgumentsReceived()
+ ? "LaunchArgumentsReceived" m.top.launchArgs
+ if m.top.launchArgs <> invalid and m.top.launchArgs.contentId <> invalid
+ PlayVideo(m.top.launchArgs.contentId, m.homeGridScreen)
+ end if
+end function
+
+function InputArgumentsReceived()
+ ? "InputArgumentsReceived" m.top.inputArgs
+ if m.top.inputArgs <> invalid and m.top.inputArgs.contentId <> invalid
+ PlayVideo(m.top.inputArgs.contentId)
+ end if
+end function
diff --git a/src/components/MainScene.xml b/src/components/MainScene.xml
index 5e141219..4e05405d 100644
--- a/src/components/MainScene.xml
+++ b/src/components/MainScene.xml
@@ -1,6 +1,11 @@
+
+
+
+
+
diff --git a/src/components/NavBar/NavBar.bs b/src/components/NavBar/NavBar.bs
index 444997e1..08bf3562 100644
--- a/src/components/NavBar/NavBar.bs
+++ b/src/components/NavBar/NavBar.bs
@@ -1,3 +1,5 @@
+import "pkg:/components/VideoPlayer/Video.bs"
+
function Init()
m.buttonGroup = m.top.findNode("NavBarButtonGroup")
m.top.observeField("focusIndex", "OnFocusIndexChange")
@@ -7,6 +9,11 @@ function onKeyEvent(key as string, press as boolean) as boolean
if not press
return false
end if
+ if key = "options"
+ if ToggleVideoPictureInPicture(m.top)
+ return true
+ end if
+ end if
if key = "down"
i = m.top.focusIndex
target = i + 1
@@ -31,11 +38,23 @@ function onKeyEvent(key as string, press as boolean) as boolean
m.top.focusIndex = -1
return true
end if
+ else if key = "back"
+ dialog = ShowDialogButtons("Do you want to exit Playlet?", "Exit", ["Exit", "Cancel"])
+ dialog.observeField("buttonSelected", "OnExitDialog", ["buttonSelected"])
+ return true
end if
return false
end function
+function OnExitDialog(event as object)
+ buttonIndex = event.GetData()
+ if buttonIndex = 0 ' Exit
+ ? "EXITING APP"
+ m.top.GetScene().exitChannel = true
+ end if
+end function
+
function OnFocusIndexChange()
focusIndex = m.top.focusIndex
childCount = m.buttonGroup.getChildCount()
diff --git a/src/components/SearchScreen/SearchScreen.bs b/src/components/SearchScreen/SearchScreen.bs
index b7d8b31f..c82f01bc 100644
--- a/src/components/SearchScreen/SearchScreen.bs
+++ b/src/components/SearchScreen/SearchScreen.bs
@@ -148,6 +148,11 @@ function OnkeyEvent(key as string, press as boolean) as boolean
if press = false
return false
end if
+ if key = "options"
+ if ToggleVideoPictureInPicture(m.top)
+ return true
+ end if
+ end if
if key = "back"
ScrollUp()
m.top.escape = key
diff --git a/src/components/VideoPlayer/Video.bs b/src/components/VideoPlayer/Video.bs
index 43552865..55692cda 100644
--- a/src/components/VideoPlayer/Video.bs
+++ b/src/components/VideoPlayer/Video.bs
@@ -10,7 +10,9 @@ function PlayVideo(videoId as string, sender = invalid as object)
end if
videoContainer.videoPlayer = videoContainer.createChild("VideoPlayer")
videoContainer.videoPlayer.videoId = videoId
- videoContainer.videoPlayer.SetFocus(true)
+ if videoContainer.fullscreen
+ videoContainer.videoPlayer.SetFocus(true)
+ end if
end function
function CloseVideo(setFocus = true as boolean)
@@ -27,7 +29,30 @@ function CloseVideo(setFocus = true as boolean)
videoContainer.sender.SetFocus(true)
end if
end if
- m.global.loadingIndicator.visible = false
+end function
+
+function ToggleVideoPictureInPicture(sender = invalid as object) as boolean
+ videoContainer = GetVideoContainer()
+ if videoContainer.videoPlayer = invalid
+ return false
+ end if
+ videoContainer.fullscreen = not videoContainer.fullscreen
+ if sender <> invalid
+ videoContainer.sender = sender
+ end if
+ if videoContainer.fullscreen
+ videoContainer.videoPlayer.SetFocus(true)
+ else
+ if videoContainer.sender <> invalid
+ if videoContainer.sender.hasField("focus")
+ videoContainer.sender.focus = true
+ else
+ videoContainer.sender.SetFocus(true)
+ end if
+ end if
+ videoContainer.sender = invalid
+ end if
+ return true
end function
function GetVideoContainer() as object
diff --git a/src/components/VideoPlayer/VideoContainer.xml b/src/components/VideoPlayer/VideoContainer.xml
index 9c2f8787..f6b05c46 100644
--- a/src/components/VideoPlayer/VideoContainer.xml
+++ b/src/components/VideoPlayer/VideoContainer.xml
@@ -4,5 +4,6 @@
+
\ No newline at end of file
diff --git a/src/components/VideoPlayer/VideoPlayer.bs b/src/components/VideoPlayer/VideoPlayer.bs
index c698901e..c7c06203 100644
--- a/src/components/VideoPlayer/VideoPlayer.bs
+++ b/src/components/VideoPlayer/VideoPlayer.bs
@@ -5,11 +5,15 @@ import "pkg:/components/VideoPlayer/Video.bs"
function Init()
SetupSponsorBlock()
m.videoDetailsTask = m.top.findNode("VideoDetailsTask")
- m.busySpinner = FindChildNodeOfType(m.top, "BusySpinner")
- m.busySpinnerLabel = FindChildNodeOfType(m.busySpinner, "Label")
- ' TODO: show spinner while fetching video details
+ ' busySpinner is our spinner, shown when loading the video details (like links, title, etc)
+ m.busySpinner = m.top.findNode("BusySpinner")
+ ' Once the Content is ready, the rokuBusySpinner shows up, which is loading the video feed
+ m.rokuBusySpinner = FindChildNodeOfType(m.top, "BusySpinner")
+ ' The label that shows the buffering percentage
+ m.rokuBusySpinnerLabel = FindChildNodeOfType(m.rokuBusySpinner, "Label")
SetupBusySpinner()
- m.global.loadingIndicator.visible = true
+ m.busySpinner.visible = true
+ m.busySpinner.control = "start"
m.videoPlayingSuccess = false
m.ignoreNextFinishedState = false
@@ -21,6 +25,10 @@ function Init()
m.top.observeField("control", "OnControlChange")
m.top.observeField("videoId", "StartVideoDetailsTask")
+ m.videoContainer = GetVideoContainer()
+ m.videoContainer.observeFieldScoped("fullscreen", "OnFullScreenChange")
+ OnFullScreenChange()
+
m.top.ObserveField("state", "OnVideoPlayerStateChange")
end function
@@ -28,6 +36,14 @@ function OnkeyEvent(key as string, press as boolean) as boolean
if press = false
return false
end if
+ ' Unfortunately, a Video node cannot capture the "options" key (because, Roku...)
+ ' https://community.roku.com/t5/Roku-Developer-Program/Bug-in-10-0-1-Options-key-is-not-being-consumed-by-onKeyEvent-when-Video-node-is-in-focus/m-p/709200/highlight/true#M49312
+ ' Because of that, the button "down" is used to shrink the video for picture in picture mode
+ if key = "down"
+ if ToggleVideoPictureInPicture()
+ return true
+ end if
+ end if
if key = "back"
CloseVideo()
return true
@@ -56,8 +72,9 @@ function OnVideoDetailsTaskResults() as void
return
end if
if metadata = invalid
- m.global.loadingIndicator.visible = false
- ErrorMessage(`Failed to load video information for ${videoId}`, "Video load fail")
+ m.busySpinner.visible = false
+ m.busySpinner.control = "stop"
+ ShowDialog(`Failed to load video information for ${videoId}`, "Video load fail")
CloseVideo()
end if
@@ -84,7 +101,8 @@ function OnVideoDetailsTaskResults() as void
contentNode.title = metadata.title
contentNode.secondaryTitle = metadata.author
SetCaptions(metadata, m.top, contentNode)
- m.global.loadingIndicator.visible = false
+ m.busySpinner.visible = false
+ m.busySpinner.control = "stop"
m.top.content = contentNode
m.top.control = "play"
end function
@@ -107,31 +125,36 @@ function FindChildNodeOfType(node as object, nodeType as string) as object
end function
function SetupBusySpinner()
+ m.rokuBusySpinner.poster.width = 150
+ m.rokuBusySpinner.poster.height = 150
+ m.rokuBusySpinner.poster.uri = "pkg:/images/spinner.png"
+ m.rokuBusySpinner.observeField("translation", "OnRokuSpinnerMoved")
+
+ m.rokuBusySpinnerLabel.width = 150
+ m.rokuBusySpinnerLabel.height = 150
+ m.rokuBusySpinnerLabel.translation = [0, 0]
+ m.rokuBusySpinnerLabel.observeField("translation", "OnRokuSpinnerLabelMoved")
+
m.busySpinner.poster.width = 150
m.busySpinner.poster.height = 150
m.busySpinner.poster.uri = "pkg:/images/spinner.png"
- m.busySpinner.observeField("translation", "OnSpinnerMoved")
- m.busySpinnerLabel.width = 150
- m.busySpinnerLabel.height = 150
- m.busySpinnerLabel.translation = [0, 0]
- m.busySpinnerLabel.observeField("translation", "OnSpinnerLabelMoved")
end function
-function OnSpinnerMoved()
- currentTranslation = m.busySpinner.translation
+function OnRokuSpinnerMoved()
+ currentTranslation = m.rokuBusySpinner.translation
parentRect = m.top.boundingRect()
- centerx = (parentRect.width - m.busySpinner.poster.width) / 2
- centery = (parentRect.height - m.busySpinner.poster.height) / 2
+ centerx = (parentRect.width - m.rokuBusySpinner.poster.width) / 2
+ centery = (parentRect.height - m.rokuBusySpinner.poster.height) / 2
if currentTranslation[0] <> centerx or currentTranslation[1] <> centery
- m.busySpinner.translation = [centerx, centery]
+ m.rokuBusySpinner.translation = [centerx, centery]
end if
end function
-function OnSpinnerLabelMoved()
- currentTranslation = m.busySpinnerLabel.translation
+function OnRokuSpinnerLabelMoved()
+ currentTranslation = m.rokuBusySpinnerLabel.translation
if currentTranslation[0] <> 0 or currentTranslation[1] <> 0
- m.busySpinnerLabel.translation = [0, 0]
+ m.rokuBusySpinnerLabel.translation = [0, 0]
end if
end function
@@ -174,7 +197,46 @@ function OnVideoPlayerStateChange() as void
return
end if
- if state = "error" or state = "finished"
+ if state = "finished"
+ CloseVideo()
+ end if
+
+ if state = "error"
+ errorInfo = m.top.errorInfo
+ messageLines = [`VideoId: ${m.top.videoId}`]
+ for each info in errorInfo
+ messageLines.push(`${info}: ${errorInfo[info]}`)
+ end for
+ ShowDialog(messageLines, "Error playing video")
CloseVideo()
end if
end function
+
+function OnFullScreenChange()
+ margin = 20
+ width = 1280
+ height = 720
+ if m.videoContainer.fullscreen
+ m.top.translation = [0, 0]
+ m.top.width = width
+ m.top.height = height
+ else
+ small_width = width / 3
+ small_height = height / 3
+ x = width - small_width - margin
+ y = height - small_height - margin
+
+ m.top.translation = [x, y]
+ m.top.width = small_width
+ m.top.height = small_height
+ end if
+
+ m.top.enableUI = m.videoContainer.fullscreen
+ PositionSpinner()
+end function
+
+function PositionSpinner()
+ centerx = (m.top.width - m.busySpinner.poster.width) / 2
+ centery = (m.top.height - m.busySpinner.poster.height) / 2
+ m.busySpinner.translation = [centerx, centery]
+end function
diff --git a/src/components/VideoPlayer/VideoPlayer.xml b/src/components/VideoPlayer/VideoPlayer.xml
index e8c9b828..22fea447 100644
--- a/src/components/VideoPlayer/VideoPlayer.xml
+++ b/src/components/VideoPlayer/VideoPlayer.xml
@@ -28,5 +28,8 @@
repeat="true"
duration="0.25"
/>
+
\ No newline at end of file
diff --git a/src/manifest b/src/manifest
index 150a9d81..a7bf1cae 100644
--- a/src/manifest
+++ b/src/manifest
@@ -21,4 +21,6 @@ splash_screen_sd=pkg:/images/splash-screen_sd.jpg
splash_color=#242424
splash_min_time=0
+supports_input_launch=1
+
bs_const=DEBUG=true;WEB_SERVER_BASIC_AUTH=false;DASH_THUMBNAILS=false
\ No newline at end of file
diff --git a/src/source/main.bs b/src/source/main.bs
index 24268b52..47b99c09 100644
--- a/src/source/main.bs
+++ b/src/source/main.bs
@@ -1,11 +1,16 @@
-function main() as void
+function main(args as object) as void
screen = CreateObject("roSGScreen")
m.port = CreateObject("roMessagePort")
screen.setMessagePort(m.port)
m.global = screen.getGlobalNode()
- screen.CreateScene("MainScene")
+ scene = screen.CreateScene("MainScene")
screen.show()
+ scene.ObserveField("exitChannel", m.port)
+ scene.launchArgs = args
+
+ input = CreateObject("roInput")
+ input.setMessagePort(m.port)
' The following comment is to enable the SceneGraph inspector
' on the VSCode BrightScript plugin.
@@ -20,6 +25,14 @@ function main() as void
if msg.isScreenClosed()
return
end if
+ else if msgType = "roSGNodeEvent"
+ field = msg.getField()
+ data = msg.getData()
+ if field = "exitChannel" and data = true
+ END
+ end if
+ else if msgType = "roInputEvent"
+ scene.inputArgs = msg.getInfo()
end if
end while
end function