Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Picture in picture #20

Merged
merged 10 commits into from
Dec 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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