Merge pull request #982 from jkim2492/unstable
Change subtitle renderer from Roku embedded to a custom task
This commit is contained in:
commit
d10556e925
|
@ -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
|
||||
|
|
|
@ -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
147
components/captionTask.brs
Normal 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*}|<.*?>|<.*?>", "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
components/captionTask.xml
Normal file
15
components/captionTask.xml
Normal 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>
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user