Skip to content

Commit 44c4fde

Browse files
authored
Merge pull request #497 from 1hitsong/multipleCaptions
Add support for displaying multiple captions, in different locations, at the same time
2 parents 453f412 + 712cc70 commit 44c4fde

File tree

6 files changed

+199
-95
lines changed

6 files changed

+199
-95
lines changed

bslint.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"rules": {
3-
"unused-variable": "error"
4-
}
5-
}
2+
"rules": {
3+
"unused-variable": "warn"
4+
}
5+
}

components/captionTask.bs

Lines changed: 185 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import "pkg:/source/api/baserequest.bs"
2+
import "pkg:/source/enums/CaptionMoveDirection.bs"
23
import "pkg:/source/enums/MediaPlaybackState.bs"
4+
import "pkg:/source/enums/String.bs"
35
import "pkg:/source/utils/config.bs"
46

57
sub init()
@@ -69,58 +71,173 @@ end function
6971
function newLayoutGroup(labels)
7072
newlg = CreateObject("roSGNode", "LayoutGroup")
7173
newlg.appendchildren(labels)
72-
newlg.horizalignment = "center"
73-
newlg.vertalignment = "bottom"
74+
newlg.layoutDirection = "vert"
75+
newlg.horizalignment = "left"
76+
newlg.vertalignment = "top"
77+
newlg.itemSpacings = "[5]"
7478
return newlg
7579
end function
7680

77-
function newRect(lg)
78-
rectLG = CreateObject("roSGNode", "LayoutGroup")
81+
function newRect(lg, id as string)
7982
rectxy = lg.BoundingRect()
83+
8084
rect = CreateObject("roSGNode", "Rectangle")
85+
rect.addreplace("id", id)
86+
rect.appendchild(lg)
87+
lg.translation = [5, 5]
88+
8189
rect.color = m.bgColor
8290
rect.opacity = m.bgOpac
83-
rect.width = rectxy.width + 50
84-
rect.height = rectxy.height
91+
rect.width = rectxy.width + 10
92+
rect.height = rectxy.height + 10
93+
8594
if lg.getchildCount() = 0
8695
rect.width = 0
8796
rect.height = 0
8897
end if
89-
rectLG.translation = [0, -rect.height / 2]
90-
rectLG.horizalignment = "center"
91-
rectLG.vertalignment = "center"
92-
rectLG.appendchild(rect)
93-
return rectLG
98+
99+
return rect
94100
end function
95101

96102

97103
sub updateCaption()
98-
m.top.currentCaption = []
99-
if LCase(m.top.playerState) = "playingon"
100-
m.top.currentPos = m.top.currentPos + 100
101-
texts = []
102-
captionStyles = {}
103-
for each entry in m.captionList
104-
if entry["start"] <= m.top.currentPos and m.top.currentPos < entry["end"]
105-
captionStyles = entry["styles"]
106-
t = m.tags.replaceAll(entry["text"], "")
107-
texts.push(t)
108-
end if
109-
end for
110-
labels = []
111-
for each text in texts
112-
labels.push(newlabel (text))
113-
end for
114-
lines = newLayoutGroup(labels)
115-
rect = newRect(lines)
116-
m.top.currentCaption = [rect, lines]
117-
118-
m.top.captionStyles = captionStyles
119-
else if LCase(m.top.playerState.right(1)) = "w"
104+
if LCase(m.top.playerState.right(1)) = "w"
120105
m.top.playerState = m.top.playerState.left(len (m.top.playerState) - 1)
106+
return
121107
end if
108+
109+
m.top.currentCaption = []
110+
111+
if not isStringEqual(m.top.playerState, "playingon") then return
112+
113+
m.top.currentPos = m.top.currentPos + 100
114+
captions = []
115+
116+
for each entry in m.captionList
117+
if entry["start"] <= m.top.currentPos and m.top.currentPos < entry["end"]
118+
119+
labels = []
120+
for each text in entry["text"]
121+
labels.push(newlabel(text))
122+
end for
123+
124+
lines = newLayoutGroup(labels)
125+
rect = newRect(lines, entry.LookupCI("id"))
126+
127+
finalStyles = prepareStyles(entry["styles"], rect, labels.count())
128+
rect.translation = finalStyles.translation
129+
130+
adjustForCaptionOverlaps(rect, captions)
131+
132+
captions.push(rect)
133+
end if
134+
end for
135+
136+
m.top.currentCaption = captions
122137
end sub
123138

139+
sub adjustForCaptionOverlaps(newCaption, existingCaptions, moveDirection = CaptionMoveDirection.UP)
140+
newCaptionBounds = newCaption.sceneBoundingRect()
141+
142+
for i = existingCaptions.count() - 1 to 0 step -1
143+
comparisonCaption = existingCaptions[i]
144+
145+
' Don't compare the caption to itself
146+
if isStringEqual(newCaption.lookupCI("id"), comparisonCaption.lookupCI("id")) then continue for
147+
148+
comparisonCaptionBounds = comparisonCaption.sceneBoundingRect()
149+
150+
overlapping = isOverlapping(newCaption, comparisonCaption)
151+
152+
if overlapping
153+
' If we're in the top half of the screen, push the other caption down
154+
' otherwise push the other caption up
155+
if newCaptionBounds.y < 540
156+
moveDirection = CaptionMoveDirection.DOWN
157+
end if
158+
159+
if moveDirection = CaptionMoveDirection.DOWN
160+
calculatedVerticalPosition = newCaptionBounds.y + newCaptionBounds.height + 5
161+
else
162+
calculatedVerticalPosition = newCaptionBounds.y - comparisonCaptionBounds.height - 5
163+
end if
164+
165+
if calculatedVerticalPosition < 0 then calculatedVerticalPosition = 0
166+
167+
comparisonCaption.translation = [comparisonCaption.translation[0], calculatedVerticalPosition]
168+
adjustForCaptionOverlaps(comparisonCaption, existingCaptions, moveDirection)
169+
end if
170+
end for
171+
end sub
172+
173+
function isOverlapping(node1, node2) as boolean
174+
node1Bounds = node1.sceneBoundingRect()
175+
node2Bounds = node2.sceneBoundingRect()
176+
return node1Bounds.x < (node2Bounds.x + node2Bounds.width) and node2Bounds.x < (node1Bounds.x + node1Bounds.width) and node1Bounds.y < (node2Bounds.y + node2Bounds.height) and node2Bounds.y < (node1Bounds.y + node1Bounds.height)
177+
end function
178+
179+
function prepareStyles(styles as object, rect, lineCount = 1 as integer) as object
180+
captionStyles = styles
181+
finalStyles = { translation: [960, 1020] }
182+
183+
verticalPosition = 1020
184+
horizontalPositon = 960 - (rect.BoundingRect().width / 2)
185+
186+
if not isValidAndNotEmpty(captionStyles)
187+
return finalStyles
188+
end if
189+
190+
captionLine = chainLookupReturn(captionStyles, "line", string.EMPTY)
191+
192+
' Line
193+
if not isStringEqual(captionLine, string.EMPTY)
194+
captionLineHeight = (rect.BoundingRect().height / lineCount)
195+
verticalPosition = processLineCue(captionLine, captionLineHeight)
196+
end if
197+
198+
finalStyles.translation = [horizontalPositon, verticalPosition]
199+
200+
return finalStyles
201+
end function
202+
203+
function processLineCue(captionLine as string, captionLineHeight as float) as integer
204+
' A percentage
205+
if captionLine.Instr("%") <> -1
206+
calculatedPosition = 1080 * (val(captionLine) / 100)
207+
208+
if calculatedPosition + captionLineHeight > 1080
209+
calculatedPosition = 1080 - captionLineHeight
210+
end if
211+
212+
return calculatedPosition
213+
end if
214+
215+
' Note: Non-numeric values are treated as 0
216+
lineValue = val(captionLine)
217+
218+
if lineValue = 0 then return 0
219+
220+
' A positive line value. Count from the top
221+
if lineValue >= 0
222+
calculatedPosition = lineValue * captionLineHeight
223+
224+
if calculatedPosition + captionLineHeight > 1080
225+
calculatedPosition = 1080 - captionLineHeight
226+
end if
227+
228+
return calculatedPosition
229+
end if
230+
231+
' A negative line value. Count from the bottom
232+
calculatedPosition = 1080 - Abs(lineValue * captionLineHeight)
233+
234+
if calculatedPosition < 0
235+
calculatedPosition = 0
236+
end if
237+
238+
return calculatedPosition
239+
end function
240+
124241
function isTime(text)
125242
return text.right(1) = chr(31)
126243
end function
@@ -158,20 +275,52 @@ function parseVTT(lines)
158275
curEnd = -1
159276
entries = []
160277

278+
currentLine = []
279+
entry = {}
280+
281+
isNewLine = false
282+
161283
for i = 0 to lines.count() - 1
162284
if isTime(lines[i])
285+
currentLine = []
286+
163287
curStart = toMs(lines[i])
164288
lineData = extractStyles(lines[i + 1])
165289
curEnd = toMs(lineData.LookupCI("endTimestamp"))
166290
curstyles = chainLookup(lineData, "styles")
291+
entry = { "start": curStart, "end": curEnd, "styles": curstyles, "id": `caption${i}` }
292+
isNewLine = true
293+
167294
i += 1
168-
else if curStart <> -1
295+
continue for
296+
end if
297+
298+
if isNewLine
169299
trimmed = lines[i].trim()
170-
if trimmed <> chr(0)
171-
entry = { "start": curStart, "end": curEnd, "text": trimmed, "styles": curstyles }
300+
finalText = m.tags.replaceAll(trimmed, "")
301+
302+
' We reached a blank line
303+
if isStringEqual(finalText, string.EMPTY)
304+
entry.AddReplace("text", currentLine)
172305
entries.push(entry)
306+
isNewLine = false
307+
308+
continue for
173309
end if
310+
311+
' We're inside a text block
312+
currentLine.push(finalText)
313+
314+
' We reached the end of the file and it doesn't end with a blank line
315+
if i = (lines.count() - 1)
316+
entry.AddReplace("text", currentLine)
317+
entries.push(entry)
318+
319+
exit for
320+
end if
321+
174322
end if
175323
end for
324+
176325
return entries
177326
end function

components/video/VideoPlayerView.bs

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,6 @@ sub onAllowCaptionsChange()
385385
m.captionGroup = m.top.findNode("captionGroup")
386386
m.captionGroup.createchildren(9, "LayoutGroup")
387387
m.captionTask = createObject("roSGNode", "captionTask")
388-
m.captionTask.observeField("captionStyles", "updateCaptionStyles")
389388
m.captionTask.observeField("currentCaption", "updateCaption")
390389
m.captionTask.observeField("useThis", "checkCaptionMode")
391390
m.top.observeField("subtitleTrack", "loadCaption")
@@ -417,59 +416,6 @@ sub toggleCaption()
417416
end if
418417
end sub
419418

420-
sub updateCaptionStyles()
421-
captionStyles = m.captionTask.captionStyles
422-
if not isValidAndNotEmpty(captionStyles)
423-
m.captionGroup.translation = [960, 1020]
424-
end if
425-
426-
captionLine = chainLookupReturn(captionStyles, "line", string.EMPTY)
427-
428-
' Line
429-
if not isStringEqual(captionLine, string.EMPTY)
430-
' A percentage
431-
if captionLine.Instr("%") <> -1
432-
calculatedPosition = 1080 * (val(captionLine) / 100)
433-
calculatedPosition += m.captionGroup.BoundingRect().height
434-
435-
if calculatedPosition > 1020
436-
calculatedPosition = 1020
437-
end if
438-
439-
m.captionGroup.translation = [960, calculatedPosition]
440-
return
441-
end if
442-
443-
' Note: Non-numeric values are treated as 0
444-
lineValue = val(captionLine)
445-
446-
' A positive line value. Count from the top
447-
if lineValue >= 0
448-
calculatedPosition = (lineValue + 1) * m.captionGroup.BoundingRect().height
449-
450-
if calculatedPosition > 1020
451-
calculatedPosition = 1020
452-
end if
453-
454-
m.captionGroup.translation = [960, calculatedPosition]
455-
return
456-
end if
457-
458-
' A negative line value. Count from the bottom
459-
calculatedPosition = 1080 - Abs((lineValue + 1) * m.captionGroup.BoundingRect().height)
460-
461-
if calculatedPosition < 0
462-
calculatedPosition = 0
463-
end if
464-
465-
m.captionGroup.translation = [960, calculatedPosition]
466-
return
467-
end if
468-
469-
' No styles passed, reset to default position
470-
m.captionGroup.translation = [960, 1020]
471-
end sub
472-
473419
' Removes old subtitle lines and adds new subtitle lines
474420
sub updateCaption()
475421
m.captionGroup.removeChildrenIndex(m.captionGroup.getChildCount(), 0)

components/video/VideoPlayerView.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
</interface>
3131

3232
<children>
33-
<Group id="captionGroup" translation="[960,1020]" />
33+
<Group id="captionGroup" translation="[0,0]" />
3434
<timer id="playbackTimer" repeat="true" duration="30" />
3535
<timer id="bufferCheckTimer" repeat="true" />
3636
<OSD id="osd" visible="false" inactiveTimeout="5" />
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
enum CaptionMoveDirection
2+
NOTSET
3+
UP
4+
DOWN
5+
end enum

source/static/whatsNew/3.0.9.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,9 @@
5050
{
5151
"description": "Burn subtitles if video is transcoding",
5252
"author": "1hitsong"
53+
},
54+
{
55+
"description": "Add custom subtitle support for multiple captions, in different locations, at the same time",
56+
"author": "1hitsong"
5357
}
5458
]

0 commit comments

Comments
 (0)