Skip to content

Commit d10556e

Browse files
authored
Merge pull request jellyfin-archive#982 from jkim2492/unstable
Change subtitle renderer from Roku embedded to a custom task
2 parents a809f8a + 45dc9e5 commit d10556e

File tree

8 files changed

+258
-21
lines changed

8 files changed

+258
-21
lines changed

components/JFVideo.brs

+43-15
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,44 @@ sub init()
2828
m.getNextEpisodeTask = createObject("roSGNode", "GetNextEpisodeTask")
2929
m.getNextEpisodeTask.observeField("nextEpisodeData", "onNextEpisodeDataLoaded")
3030

31+
m.top.observeField("state", "onState")
32+
m.top.observeField("content", "onContentChange")
33+
34+
'Captions
35+
m.captionGroup = m.top.findNode("captionGroup")
36+
m.captionGroup.createchildren(9, "LayoutGroup")
37+
m.captionTask = createObject("roSGNode", "captionTask")
38+
m.captionTask.observeField("currentCaption", "updateCaption")
39+
m.captionTask.observeField("useThis", "checkCaptionMode")
40+
m.top.observeField("currentSubtitleTrack", "loadCaption")
41+
m.top.observeField("globalCaptionMode", "toggleCaption")
42+
if get_user_setting("playback.subs.custom") = "false"
43+
m.top.suppressCaptions = false
44+
else
45+
m.top.suppressCaptions = true
46+
toggleCaption()
47+
end if
48+
end sub
49+
50+
sub loadCaption()
51+
if m.top.suppressCaptions
52+
m.captionTask.url = m.top.currentSubtitleTrack
53+
end if
54+
end sub
55+
56+
sub toggleCaption()
57+
m.captionTask.playerState = m.top.state + m.top.globalCaptionMode
58+
if LCase(m.top.globalCaptionMode) = "on"
59+
m.captionTask.playerState = m.top.state + m.top.globalCaptionMode + "w"
60+
m.captionGroup.visible = true
61+
else
62+
m.captionGroup.visible = false
63+
end if
64+
end sub
65+
66+
sub updateCaption ()
67+
m.captionGroup.removeChildrenIndex(m.captionGroup.getChildCount(), 0)
68+
m.captionGroup.appendChildren(m.captionTask.currentCaption)
3169
end sub
3270

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

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

39-
' If video content type is not episode, remove position observer
40-
if m.top.content.contenttype <> 4
41-
m.top.unobserveField("position")
42-
end if
4377
end sub
4478

4579
sub onNextEpisodeDataLoaded()
4680
m.checkedForNextEpisode = true
4781

4882
m.top.observeField("position", "onPositionChanged")
49-
50-
if m.getNextEpisodeTask.nextEpisodeData.Items.count() <> 2
51-
m.top.unobserveField("position")
52-
end if
5383
end sub
5484

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

83114
' Checks if we need to display the Next Episode button
84115
sub checkTimeToDisplayNextEpisode()
85-
nextEpisodeCountdown = Int(m.top.runTime - m.top.position)
86-
if nextEpisodeCountdown < 0
87-
hideNextEpisodeButton()
88-
return
89-
end if
116+
if m.top.content.contenttype <> 4 then return
90117

91-
if int(m.top.position) >= (m.top.runTime - Val(m.nextupbuttonseconds))
118+
if int(m.top.position) >= (m.top.runTime - 30)
92119
showNextEpisodeButton()
93120
updateCount()
94121
return
@@ -102,6 +129,7 @@ end sub
102129

103130
' When Video Player state changes
104131
sub onPositionChanged()
132+
m.captionTask.currentPos = Int(m.top.position * 1000)
105133
' Check if dialog is open
106134
m.dialog = m.top.getScene().findNode("dialogBackground")
107135
if not isValid(m.dialog)
@@ -112,6 +140,7 @@ end sub
112140
'
113141
' When Video Player state changes
114142
sub onState(msg)
143+
m.captionTask.playerState = m.top.state + m.top.globalCaptionMode
115144
' When buffering, start timer to monitor buffering process
116145
if m.top.state = "buffering" and m.bufferCheckTimer <> invalid
117146

@@ -134,7 +163,6 @@ sub onState(msg)
134163
m.top.control = "stop"
135164
m.top.backPressed = true
136165
else if m.top.state = "playing"
137-
138166
' Check if next episde is available
139167
if isValid(m.top.showID)
140168
if m.top.showID <> "" and not m.checkedForNextEpisode and m.top.content.contenttype = 4

components/JFVideo.xml

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8" ?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<component name="JFVideo" extends="Video">
33
<interface>
44
<field id="backPressed" type="boolean" alwaysNotify="true" />
@@ -7,7 +7,6 @@
77
<field id="PlaySessionId" type="string" />
88
<field id="Subtitles" type="array" />
99
<field id="SelectedSubtitle" type="integer" />
10-
<field id="captionMode" type="string" />
1110
<field id="container" type="string" />
1211
<field id="directPlaySupported" type="boolean" />
1312
<field id="systemOverlay" type="boolean" value="false" />
@@ -23,16 +22,25 @@
2322
<field id="mediaSourceId" type="string" />
2423
<field id="audioIndex" type="integer" />
2524
<field id="runTime" type="integer" />
25+
2626
</interface>
2727
<script type="text/brightscript" uri="JFVideo.brs" />
2828
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
2929
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
3030
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
3131

3232
<children>
33+
<Group id="captionGroup" translation="[960,1020]"></Group>
34+
3335
<timer id="playbackTimer" repeat="true" duration="30" />
3436
<timer id="bufferCheckTimer" repeat="true" />
35-
<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]" />
37+
<JFButton id="nextEpisode"
38+
opacity="0"
39+
textColor="#f0f0f0"
40+
focusedTextColor="#202020"
41+
focusFootprintBitmapUri="pkg:/images/option-menu-bg.9.png"
42+
focusBitmapUri="pkg:/images/white.9.png"
43+
translation="[1500, 900]" />
3644

3745
<!--animation for the play next episode button-->
3846
<Animation id="showNextEpisodeButton" duration="1.0" repeat="false" easeFunction="inQuad">

components/captionTask.brs

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
sub init()
2+
m.top.observeField("url", "fetchCaption")
3+
m.top.currentCaption = []
4+
m.top.currentPos = 0
5+
6+
m.captionTimer = m.top.findNode("captionTimer")
7+
m.captionTimer.ObserveField("fire", "updateCaption")
8+
9+
m.captionList = []
10+
m.reader = createObject("roUrlTransfer")
11+
m.font = CreateObject("roSGNode", "Font")
12+
m.tags = CreateObject("roRegex", "{\\an\d*}|&lt;.*?&gt;|<.*?>", "s")
13+
14+
' Caption Style
15+
m.fontSizeDict = { "Default": 60, "Large": 60, "Extra Large": 70, "Medium": 50, "Small": 40 }
16+
m.percentageDict = { "Default": 1.0, "100%": 1.0, "75%": 0.75, "50%": 0.5, "25%": 0.25, "Off": 0 }
17+
m.textColorDict = { "Default": &HFFFFFFFF, "White": &HFFFFFFFF, "Black": &H000000FF, "Red": &HFF0000FF, "Green": &H008000FF, "Blue": &H0000FFFF, "Yellow": &HFFFF00FF, "Magenta": &HFF00FFFF, "Cyan": &H00FFFFFF }
18+
m.bgColorDict = { "Default": &H000000FF, "White": &HFFFFFFFF, "Black": &H000000FF, "Red": &HFF0000FF, "Green": &H008000FF, "Blue": &H0000FFFF, "Yellow": &HFFFF00FF, "Magenta": &HFF00FFFF, "Cyan": &H00FFFFFF }
19+
20+
m.settings = CreateObject("roDeviceInfo")
21+
m.fontSize = m.fontSizeDict[m.settings.GetCaptionsOption("Text/Size")]
22+
m.textColor = m.textColorDict[m.settings.GetCaptionsOption("Text/Color")]
23+
m.textOpac = m.percentageDict[m.settings.GetCaptionsOption("Text/Opacity")]
24+
m.bgColor = m.bgColorDict[m.settings.GetCaptionsOption("Background/Color")]
25+
m.bgOpac = m.percentageDict[m.settings.GetCaptionsOption("Background/Opacity")]
26+
setFont()
27+
end sub
28+
29+
sub setFont()
30+
fs = CreateObject("roFileSystem")
31+
fontlist = fs.Find("tmp:/", "font")
32+
if fontlist.count() > 0
33+
m.font.uri = "tmp:/" + fontlist[0]
34+
m.font.size = m.fontSize
35+
else
36+
reg = CreateObject("roFontRegistry")
37+
m.font = reg.GetDefaultFont(m.fontSize, false, false)
38+
end if
39+
end sub
40+
41+
sub fetchCaption()
42+
m.captionTimer.control = "stop"
43+
re = CreateObject("roRegex", "(http.*?\.vtt)", "s")
44+
url = re.match(m.top.url)[0]
45+
if url <> invalid
46+
m.reader.setUrl(url)
47+
text = m.reader.GetToString()
48+
m.captionList = parseVTT(text)
49+
m.captionTimer.control = "start"
50+
else
51+
m.captionTimer.control = "stop"
52+
end if
53+
end sub
54+
55+
function newlabel(txt)
56+
label = CreateObject("roSGNode", "Label")
57+
label.text = txt
58+
label.font = m.font
59+
label.color = m.textColor
60+
label.opacity = m.textOpac
61+
return label
62+
end function
63+
64+
function newLayoutGroup(labels)
65+
newlg = CreateObject("roSGNode", "LayoutGroup")
66+
newlg.appendchildren(labels)
67+
newlg.horizalignment = "center"
68+
newlg.vertalignment = "bottom"
69+
return newlg
70+
end function
71+
72+
function newRect(lg)
73+
rectLG = CreateObject("roSGNode", "LayoutGroup")
74+
rectxy = lg.BoundingRect()
75+
rect = CreateObject("roSGNode", "Rectangle")
76+
rect.color = m.bgColor
77+
rect.opacity = m.bgOpac
78+
rect.width = rectxy.width + 50
79+
rect.height = rectxy.height
80+
if lg.getchildCount() = 0
81+
rect.width = 0
82+
rect.height = 0
83+
end if
84+
rectLG.translation = [0, -rect.height / 2]
85+
rectLG.horizalignment = "center"
86+
rectLG.vertalignment = "center"
87+
rectLG.appendchild(rect)
88+
return rectLG
89+
end function
90+
91+
92+
sub updateCaption ()
93+
m.top.currentCaption = []
94+
if LCase(m.top.playerState) = "playingon"
95+
m.top.currentPos = m.top.currentPos + 100
96+
texts = []
97+
for each entry in m.captionList
98+
if entry["start"] <= m.top.currentPos and m.top.currentPos < entry["end"]
99+
t = m.tags.replaceAll(entry["text"], "")
100+
texts.push(t)
101+
end if
102+
end for
103+
labels = []
104+
for each text in texts
105+
labels.push(newlabel (text))
106+
end for
107+
lines = newLayoutGroup(labels)
108+
rect = newRect(lines)
109+
m.top.currentCaption = [rect, lines]
110+
else if LCase(m.top.playerState.right(1)) = "w"
111+
m.top.playerState = m.top.playerState.left(len (m.top.playerState) - 1)
112+
end if
113+
end sub
114+
115+
function isTime(text)
116+
return text.right(1) = chr(31)
117+
end function
118+
119+
function toMs(t)
120+
t = t.replace(".", ":")
121+
t = t.left(12)
122+
timestamp = t.tokenize(":")
123+
return 3600000 * timestamp[0].toint() + 60000 * timestamp[1].toint() + 1000 * timestamp[2].toint() + timestamp[3].toint()
124+
end function
125+
126+
function parseVTT(lines)
127+
lines = lines.replace(" --> ", chr(31) + chr(10))
128+
lines = lines.split(chr(10))
129+
curStart = -1
130+
curEnd = -1
131+
entries = []
132+
133+
for i = 0 to lines.count() - 1
134+
if isTime(lines[i])
135+
curStart = toMs (lines[i])
136+
curEnd = toMs (lines[i + 1])
137+
i += 1
138+
else if curStart <> -1
139+
trimmed = lines[i].trim()
140+
if trimmed <> chr(0)
141+
entry = { "start": curStart, "end": curEnd, "text": trimmed }
142+
entries.push(entry)
143+
end if
144+
end if
145+
end for
146+
return entries
147+
end function

components/captionTask.xml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<component name="captionTask" extends="Task">
3+
<interface>
4+
<field id="url" type="string" />
5+
<field id="currentCaption" type="roArray" />
6+
<field id="playerState" type="string" value="stopped" />
7+
<field id="currentPos" type="int" />
8+
</interface>
9+
<script type="text/brightscript" uri="captionTask.brs" />
10+
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
11+
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
12+
<children>
13+
<timer id="captionTimer" repeat="true" duration="0.1" />
14+
</children>
15+
</component>

locale/en_US/translations.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,16 @@
869869
<translation>Unable to find any albums or songs belonging to this artist</translation>
870870
<extracomment>Popup message when we find no audio data for an artist</extracomment>
871871
</message>
872+
<message>
873+
<source>Custom Subtitles</source>
874+
<translation>Custom Subtitles</translation>
875+
<extracomment>Name of a setting - custom subtitles that support CJK fonts</extracomment>
876+
</message>
877+
<message>
878+
<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>
879+
<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>
880+
<extracomment>Description of a setting - custom subtitles that support CJK fonts</extracomment>
881+
</message>
872882
<message>
873883
<source>Text Subtitles Only</source>
874884
<translation>Text Subtitles Only</translation>
@@ -1119,4 +1129,3 @@
11191129
</message>
11201130
</context>
11211131
</TS>
1122-

settings/settings.json

+21-1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@
7777
"type": "bool",
7878
"default": "false"
7979
},
80+
{
81+
"title": "Custom Subtitles",
82+
"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.",
83+
"settingName": "playback.subs.custom",
84+
"type": "bool",
85+
"default": "false"
86+
},
8087
{
8188
"title": "Next Episode Button Time",
8289
"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 @@
130137
"default": "false"
131138
},
132139
{
133-
"title": "Use Splashscreen as Screensaver",
140+
"title": "Disable Community Rating for Episodes",
141+
"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.",
142+
"settingName": "ui.tvshows.disableCommunityRating",
143+
"type": "bool",
144+
"default": "false"
145+
}
146+
]
147+
},
148+
{
149+
"title": "Screensaver",
150+
"description": "Options for Jellyfin's screensaver.",
151+
"children": [
152+
{
153+
"title": "Use Splashscreen as Screensaver Background",
134154
"description": "Use generated splashscreen image as Jellyfin's screensaver background. Jellyfin will need to be closed and reopened for change to take effect.",
135155
"settingName": "ui.screensaver.splashBackground",
136156
"type": "bool",

source/Main.brs

+11
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ sub Main (args as dynamic) as void
5757

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

60+
' Downloads and stores a fallback font to tmp:/
61+
if parseJSON(APIRequest("/System/Configuration/encoding").GetToString())["EnableFallbackFont"] = true
62+
re = CreateObject("roRegex", "Name.:.(.*?).,.Size", "s")
63+
filename = APIRequest("FallbackFont/Fonts").GetToString()
64+
filename = re.match(filename)
65+
if filename.count() > 0
66+
filename = filename[1]
67+
APIRequest("FallbackFont/Fonts/" + filename).gettofile("tmp:/font")
68+
end if
69+
end if
70+
6071
' Only show the Whats New popup the first time a user runs a new client version.
6172
if appInfo.GetVersion() <> get_setting("LastRunVersion")
6273
' Ensure the user hasn't disabled Whats New popups

0 commit comments

Comments
 (0)