Skip to content

Commit

Permalink
Picture in picture (#20)
Browse files Browse the repository at this point in the history
* Refactor and fix player

* Rename functions, fix video loading indicator

* Hide loading indicator on exit

* Picture in picture, exit dialog, fix spinner

* Set launch and input args

* Deep linking support

* Update changelog

* Support dialog multiline

* Comments and changelog
  • Loading branch information
iBicha authored Dec 2, 2022
1 parent 68a8d61 commit 6ec392a
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 28 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 11 additions & 3 deletions src/components/Dialog/DialogUtils.bs
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions src/components/HomeGridScreen/HomeGridScreen.bs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/components/MainScene.bs
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions src/components/MainScene.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>

<component name="MainScene" extends="Scene">
<interface>
<field id="exitChannel" type="bool" alwaysNotify="true" />
<field id="launchArgs" type="assocarray" onChange="LaunchArgumentsReceived" />
<field id="inputArgs" type="assocarray" onChange="InputArgumentsReceived" />
</interface>
<children>
<SearchScreen id="SearchScreen" visible="false" />
<HomeGridScreen id="HomeGridScreen" />
Expand Down
19 changes: 19 additions & 0 deletions src/components/NavBar/NavBar.bs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import "pkg:/components/VideoPlayer/Video.bs"

function Init()
m.buttonGroup = m.top.findNode("NavBarButtonGroup")
m.top.observeField("focusIndex", "OnFocusIndexChange")
Expand All @@ -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
Expand All @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions src/components/SearchScreen/SearchScreen.bs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 27 additions & 2 deletions src/components/VideoPlayer/Video.bs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/components/VideoPlayer/VideoContainer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
<interface>
<field id="videoPlayer" type="node" />
<field id="sender" type="node" />
<field id="fullscreen" type="boolean" value="true" />
</interface>
</component>
104 changes: 83 additions & 21 deletions src/components/VideoPlayer/VideoPlayer.bs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,13 +25,25 @@ 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

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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions src/components/VideoPlayer/VideoPlayer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,8 @@
repeat="true"
duration="0.25"
/>
<BusySpinner
id="busySpinner"
spinInterval="1" />
</children>
</component>
2 changes: 2 additions & 0 deletions src/manifest
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit 6ec392a

Please sign in to comment.