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