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

Change subtitle renderer from Roku embedded to a custom task #982

Merged
merged 18 commits into from
Feb 21, 2023
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
58 changes: 43 additions & 15 deletions components/JFVideo.brs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,44 @@ sub init()
m.getNextEpisodeTask = createObject("roSGNode", "GetNextEpisodeTask")
m.getNextEpisodeTask.observeField("nextEpisodeData", "onNextEpisodeDataLoaded")

m.top.observeField("state", "onState")
m.top.observeField("content", "onContentChange")

'Captions
m.captionGroup = m.top.findNode("captionGroup")
m.captionGroup.createchildren(9, "LayoutGroup")
m.captionTask = createObject("roSGNode", "captionTask")
m.captionTask.observeField("currentCaption", "updateCaption")
m.captionTask.observeField("useThis", "checkCaptionMode")
m.top.observeField("currentSubtitleTrack", "loadCaption")
m.top.observeField("globalCaptionMode", "toggleCaption")
if get_user_setting("playback.subs.custom") = "false"
m.top.suppressCaptions = false
else
m.top.suppressCaptions = true
toggleCaption()
end if
end sub

sub loadCaption()
if m.top.suppressCaptions
m.captionTask.url = m.top.currentSubtitleTrack
end if
end sub

sub toggleCaption()
m.captionTask.playerState = m.top.state + m.top.globalCaptionMode
if LCase(m.top.globalCaptionMode) = "on"
m.captionTask.playerState = m.top.state + m.top.globalCaptionMode + "w"
m.captionGroup.visible = true
else
m.captionGroup.visible = false
end if
end sub

sub updateCaption ()
m.captionGroup.removeChildrenIndex(m.captionGroup.getChildCount(), 0)
m.captionGroup.appendChildren(m.captionTask.currentCaption)
end sub

' Event handler for when video content field changes
Expand All @@ -36,25 +74,18 @@ sub onContentChange()

m.top.observeField("position", "onPositionChanged")

' If video content type is not episode, remove position observer
if m.top.content.contenttype <> 4
m.top.unobserveField("position")
end if
end sub

sub onNextEpisodeDataLoaded()
m.checkedForNextEpisode = true

m.top.observeField("position", "onPositionChanged")

if m.getNextEpisodeTask.nextEpisodeData.Items.count() <> 2
m.top.unobserveField("position")
end if
end sub

'
' Runs Next Episode button animation and sets focus to button
sub showNextEpisodeButton()
if m.top.content.contenttype <> 4 then return
if not m.nextEpisodeButton.visible
m.showNextEpisodeButtonAnimation.control = "start"
m.nextEpisodeButton.setFocus(true)
Expand Down Expand Up @@ -82,13 +113,9 @@ end sub

' Checks if we need to display the Next Episode button
sub checkTimeToDisplayNextEpisode()
nextEpisodeCountdown = Int(m.top.runTime - m.top.position)
if nextEpisodeCountdown < 0
hideNextEpisodeButton()
return
end if
if m.top.content.contenttype <> 4 then return

if int(m.top.position) >= (m.top.runTime - Val(m.nextupbuttonseconds))
if int(m.top.position) >= (m.top.runTime - 30)
showNextEpisodeButton()
updateCount()
return
Expand All @@ -102,6 +129,7 @@ end sub

' When Video Player state changes
sub onPositionChanged()
m.captionTask.currentPos = Int(m.top.position * 1000)
' Check if dialog is open
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep this comment, just move 1 line down so it's below your captaintask code.

m.dialog = m.top.getScene().findNode("dialogBackground")
if not isValid(m.dialog)
Expand All @@ -112,6 +140,7 @@ end sub
'
' When Video Player state changes
sub onState(msg)
m.captionTask.playerState = m.top.state + m.top.globalCaptionMode
' When buffering, start timer to monitor buffering process
if m.top.state = "buffering" and m.bufferCheckTimer <> invalid

Expand All @@ -134,7 +163,6 @@ sub onState(msg)
m.top.control = "stop"
m.top.backPressed = true
else if m.top.state = "playing"

' Check if next episde is available
if isValid(m.top.showID)
if m.top.showID <> "" and not m.checkedForNextEpisode and m.top.content.contenttype = 4
Expand Down
14 changes: 11 additions & 3 deletions components/JFVideo.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<component name="JFVideo" extends="Video">
<interface>
<field id="backPressed" type="boolean" alwaysNotify="true" />
Expand All @@ -7,7 +7,6 @@
<field id="PlaySessionId" type="string" />
<field id="Subtitles" type="array" />
<field id="SelectedSubtitle" type="integer" />
<field id="captionMode" type="string" />
<field id="container" type="string" />
<field id="directPlaySupported" type="boolean" />
<field id="systemOverlay" type="boolean" value="false" />
Expand All @@ -23,16 +22,25 @@
<field id="mediaSourceId" type="string" />
<field id="audioIndex" type="integer" />
<field id="runTime" type="integer" />

</interface>
<script type="text/brightscript" uri="JFVideo.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />

<children>
<Group id="captionGroup" translation="[960,1020]"></Group>

<timer id="playbackTimer" repeat="true" duration="30" />
<timer id="bufferCheckTimer" repeat="true" />
<JFButton id="nextEpisode" opacity="0" textColor="#f0f0f0" focusedTextColor="#202020" focusFootprintBitmapUri="pkg:/images/option-menu-bg.9.png" focusBitmapUri="pkg:/images/white.9.png" translation="[1500, 900]" />
<JFButton id="nextEpisode"
opacity="0"
textColor="#f0f0f0"
focusedTextColor="#202020"
focusFootprintBitmapUri="pkg:/images/option-menu-bg.9.png"
focusBitmapUri="pkg:/images/white.9.png"
translation="[1500, 900]" />

<!--animation for the play next episode button-->
<Animation id="showNextEpisodeButton" duration="1.0" repeat="false" easeFunction="inQuad">
Expand Down
147 changes: 147 additions & 0 deletions components/captionTask.brs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
sub init()
m.top.observeField("url", "fetchCaption")
m.top.currentCaption = []
m.top.currentPos = 0

m.captionTimer = m.top.findNode("captionTimer")
m.captionTimer.ObserveField("fire", "updateCaption")

m.captionList = []
m.reader = createObject("roUrlTransfer")
m.font = CreateObject("roSGNode", "Font")
m.tags = CreateObject("roRegex", "{\\an\d*}|&lt;.*?&gt;|<.*?>", "s")

' Caption Style
m.fontSizeDict = { "Default": 60, "Large": 60, "Extra Large": 70, "Medium": 50, "Small": 40 }
m.percentageDict = { "Default": 1.0, "100%": 1.0, "75%": 0.75, "50%": 0.5, "25%": 0.25, "Off": 0 }
m.textColorDict = { "Default": &HFFFFFFFF, "White": &HFFFFFFFF, "Black": &H000000FF, "Red": &HFF0000FF, "Green": &H008000FF, "Blue": &H0000FFFF, "Yellow": &HFFFF00FF, "Magenta": &HFF00FFFF, "Cyan": &H00FFFFFF }
m.bgColorDict = { "Default": &H000000FF, "White": &HFFFFFFFF, "Black": &H000000FF, "Red": &HFF0000FF, "Green": &H008000FF, "Blue": &H0000FFFF, "Yellow": &HFFFF00FF, "Magenta": &HFF00FFFF, "Cyan": &H00FFFFFF }

m.settings = CreateObject("roDeviceInfo")
m.fontSize = m.fontSizeDict[m.settings.GetCaptionsOption("Text/Size")]
m.textColor = m.textColorDict[m.settings.GetCaptionsOption("Text/Color")]
m.textOpac = m.percentageDict[m.settings.GetCaptionsOption("Text/Opacity")]
m.bgColor = m.bgColorDict[m.settings.GetCaptionsOption("Background/Color")]
m.bgOpac = m.percentageDict[m.settings.GetCaptionsOption("Background/Opacity")]
setFont()
end sub

sub setFont()
fs = CreateObject("roFileSystem")
fontlist = fs.Find("tmp:/", "font")
if fontlist.count() > 0
m.font.uri = "tmp:/" + fontlist[0]
m.font.size = m.fontSize
else
reg = CreateObject("roFontRegistry")
m.font = reg.GetDefaultFont(m.fontSize, false, false)
end if
end sub

sub fetchCaption()
m.captionTimer.control = "stop"
re = CreateObject("roRegex", "(http.*?\.vtt)", "s")
url = re.match(m.top.url)[0]
if url <> invalid
m.reader.setUrl(url)
text = m.reader.GetToString()
m.captionList = parseVTT(text)
m.captionTimer.control = "start"
else
m.captionTimer.control = "stop"
end if
end sub

function newlabel(txt)
label = CreateObject("roSGNode", "Label")
label.text = txt
label.font = m.font
label.color = m.textColor
label.opacity = m.textOpac
return label
end function

function newLayoutGroup(labels)
newlg = CreateObject("roSGNode", "LayoutGroup")
newlg.appendchildren(labels)
newlg.horizalignment = "center"
newlg.vertalignment = "bottom"
return newlg
end function

function newRect(lg)
rectLG = CreateObject("roSGNode", "LayoutGroup")
rectxy = lg.BoundingRect()
rect = CreateObject("roSGNode", "Rectangle")
rect.color = m.bgColor
rect.opacity = m.bgOpac
rect.width = rectxy.width + 50
rect.height = rectxy.height
if lg.getchildCount() = 0
rect.width = 0
rect.height = 0
end if
rectLG.translation = [0, -rect.height / 2]
rectLG.horizalignment = "center"
rectLG.vertalignment = "center"
rectLG.appendchild(rect)
return rectLG
end function


sub updateCaption ()
m.top.currentCaption = []
if LCase(m.top.playerState) = "playingon"
m.top.currentPos = m.top.currentPos + 100
texts = []
for each entry in m.captionList
if entry["start"] <= m.top.currentPos and m.top.currentPos < entry["end"]
t = m.tags.replaceAll(entry["text"], "")
texts.push(t)
end if
end for
labels = []
for each text in texts
labels.push(newlabel (text))
end for
lines = newLayoutGroup(labels)
rect = newRect(lines)
m.top.currentCaption = [rect, lines]
else if LCase(m.top.playerState.right(1)) = "w"
m.top.playerState = m.top.playerState.left(len (m.top.playerState) - 1)
end if
end sub

function isTime(text)
return text.right(1) = chr(31)
end function

function toMs(t)
t = t.replace(".", ":")
t = t.left(12)
timestamp = t.tokenize(":")
return 3600000 * timestamp[0].toint() + 60000 * timestamp[1].toint() + 1000 * timestamp[2].toint() + timestamp[3].toint()
end function

function parseVTT(lines)
lines = lines.replace(" --> ", chr(31) + chr(10))
lines = lines.split(chr(10))
curStart = -1
curEnd = -1
entries = []

for i = 0 to lines.count() - 1
if isTime(lines[i])
curStart = toMs (lines[i])
curEnd = toMs (lines[i + 1])
i += 1
else if curStart <> -1
trimmed = lines[i].trim()
if trimmed <> chr(0)
entry = { "start": curStart, "end": curEnd, "text": trimmed }
entries.push(entry)
end if
end if
end for
return entries
end function
15 changes: 15 additions & 0 deletions components/captionTask.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<component name="captionTask" extends="Task">
<interface>
<field id="url" type="string" />
<field id="currentCaption" type="roArray" />
<field id="playerState" type="string" value="stopped" />
<field id="currentPos" type="int" />
</interface>
<script type="text/brightscript" uri="captionTask.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<children>
<timer id="captionTimer" repeat="true" duration="0.1" />
</children>
</component>
11 changes: 10 additions & 1 deletion locale/en_US/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,16 @@
<translation>Unable to find any albums or songs belonging to this artist</translation>
<extracomment>Popup message when we find no audio data for an artist</extracomment>
</message>
<message>
<source>Custom Subtitles</source>
<translation>Custom Subtitles</translation>
<extracomment>Name of a setting - custom subtitles that support CJK fonts</extracomment>
</message>
<message>
<source>Replace Roku's default subtitle functions with custom functions that support CJK fonts. Fallback fonts must be configured and enabled on the server for CJK rendering to work.</source>
<translation>Replace Roku's default subtitle functions with custom functions that support CJK fonts. Fallback fonts must be configured and enabled on the server for CJK rendering to work.</translation>
<extracomment>Description of a setting - custom subtitles that support CJK fonts</extracomment>
</message>
<message>
<source>Text Subtitles Only</source>
<translation>Text Subtitles Only</translation>
Expand Down Expand Up @@ -1119,4 +1129,3 @@
</message>
</context>
</TS>

22 changes: 21 additions & 1 deletion settings/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@
"type": "bool",
"default": "false"
},
{
"title": "Custom Subtitles",
"description": "Replace Roku's default subtitle functions with custom functions that support CJK fonts. Fallback fonts must be configured and enabled on the server for CJK rendering to work.",
"settingName": "playback.subs.custom",
"type": "bool",
"default": "false"
},
{
"title": "Next Episode Button Time",
"description": "Set how many seconds before the end of an episode the Next Episode button should appear. Set to 0 to disable.",
Expand Down Expand Up @@ -130,7 +137,20 @@
"default": "false"
},
{
"title": "Use Splashscreen as Screensaver",
"title": "Disable Community Rating for Episodes",
"description": "If enabled, the star and community rating for episodes of a TV show will be removed. This is to prevent spoilers of an upcoming good/bad episode.",
"settingName": "ui.tvshows.disableCommunityRating",
"type": "bool",
"default": "false"
}
]
},
{
"title": "Screensaver",
"description": "Options for Jellyfin's screensaver.",
"children": [
{
"title": "Use Splashscreen as Screensaver Background",
"description": "Use generated splashscreen image as Jellyfin's screensaver background. Jellyfin will need to be closed and reopened for change to take effect.",
"settingName": "ui.screensaver.splashBackground",
"type": "bool",
Expand Down
11 changes: 11 additions & 0 deletions source/Main.brs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ sub Main (args as dynamic) as void

m.scene.observeField("exit", m.port)

' Downloads and stores a fallback font to tmp:/
if parseJSON(APIRequest("/System/Configuration/encoding").GetToString())["EnableFallbackFont"] = true
re = CreateObject("roRegex", "Name.:.(.*?).,.Size", "s")
filename = APIRequest("FallbackFont/Fonts").GetToString()
filename = re.match(filename)
if filename.count() > 0
filename = filename[1]
APIRequest("FallbackFont/Fonts/" + filename).gettofile("tmp:/font")
end if
end if

' Only show the Whats New popup the first time a user runs a new client version.
if appInfo.GetVersion() <> get_setting("LastRunVersion")
' Ensure the user hasn't disabled Whats New popups
Expand Down
Loading