Merge pull request #982 from jkim2492/unstable

Change subtitle renderer from Roku embedded to a custom task
This commit is contained in:
1hitsong 2023-02-21 12:53:23 -05:00 committed by GitHub
commit d10556e925
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 258 additions and 21 deletions

View File

@ -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
@ -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)
@ -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
@ -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
m.dialog = m.top.getScene().findNode("dialogBackground")
if not isValid(m.dialog)
@ -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
@ -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

View File

@ -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" />
@ -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" />
@ -23,6 +22,7 @@
<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" />
@ -30,9 +30,17 @@
<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">

147
components/captionTask.brs Normal file
View File

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

View File

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

View File

@ -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>
@ -1119,4 +1129,3 @@
</message>
</context>
</TS>

View File

@ -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.",
@ -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",

View File

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

View File

@ -76,7 +76,6 @@ end function
function setupSubtitle(video, subtitles, subtitle_idx = -1) as integer
if subtitle_idx = -1
' If we are not using text-based subtitles, turn them off
video.globalCaptionMode = "Off"
return -1
end if