Skip to content

Commit c44dc84

Browse files
ryanmccartneytom-cowardShiningTrapezeirikbjornr
authored
Add support for Dash.js rendered subtitles (#383)
Co-authored-by: Tom Coward <34864926+tom-coward@users.noreply.github.com> Co-authored-by: Peter Clay Holden Morris-Hind <peter.morris-hind@bbc.co.uk> Co-authored-by: Eirik Jo Björnerstedt <eirik.bjornerstedt@bbc.co.uk>
1 parent b0e8551 commit c44dc84

13 files changed

Lines changed: 595 additions & 48 deletions

docs/tutorials/Subtitles.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ You provide subtitles to BigscreenPlayer by setting `media.captions` in the `.in
1010

1111
```js
1212
// 1️⃣ Add an array of caption blocks to your playback data.
13-
playbackData.media.captions = [/* caption blocks... */];
13+
playbackData.media.captions = [
14+
/* caption blocks... */
15+
]
1416

1517
// 2️⃣ Pass playback data that contains captions to the player.
16-
player.init(document.querySelector("video"), playbackData, /* other opts */);
18+
player.init(document.querySelector("video"), playbackData /* other opts */)
1719
```
1820

1921
1. `media.captions` MUST be an array containing at least one object.
@@ -34,7 +36,7 @@ const captions = [
3436
{ url: "https://some.cdn/subtitles.xml" },
3537
{ url: "https://other.cdn/subtitles.xml" },
3638
/* ... */
37-
];
39+
]
3840
```
3941

4042
Subtitles delivered as a whole do not require any additional metadata in the manifest to work.
@@ -49,13 +51,15 @@ const captions = [
4951
{
5052
url: "https://some.cdn/subtitles/$segment$.m4s",
5153
segmentLength: 3.84,
54+
cdn: "default",
5255
},
5356
{
5457
url: "https://other.cdn/subtitles/$segment$.m4s",
5558
segmentLength: 3.84,
59+
cdn: "default",
5660
},
5761
/* ... */
58-
];
62+
]
5963
```
6064

6165
The segment number is calculated from the presentation timeline. You MUST ensure your subtitle segments are enumerated to match your media segments and you account for offsets such as:
@@ -73,12 +77,24 @@ You can style the subtitles by setting `media.subtitleCustomisation` in the `.in
7377

7478
```js
7579
// 1️⃣ Create an object mapping out styles for your subtitles.
76-
playbackData.media.subtitleCustomisation = { lineHeight: 1.5, size: 1 };
80+
playbackData.media.subtitleCustomisation = { lineHeight: 1.5, size: 1 }
7781

7882
// 2️⃣ Pass playback data that contains subtitle customisation (and captions) to the player.
79-
player.init(document.querySelector("video"), playbackData, /* other opts */);
83+
player.init(document.querySelector("video"), playbackData /* other opts */)
8084
```
8185

86+
### Low Latency Streams
87+
88+
When using Dash.js with a low-latency MPD segments are delivered using Chunked Transfer Encoding (CTE) - the default side chain doesn't allow for delivery in this case.
89+
90+
Whilst it is possible to collect chunks as they are delivered, wait until a full segment worth of subtitles have been delivered and pass these to the render function this breaks the low-latency workflow.
91+
92+
An override has been added to allow subtitles to be rendered directly by Dash.js instead of the current side-chain.
93+
94+
Subtitles can be enabled and disabled in the usual way using the `setSubtitlesEnabled()` function. However, they are signalled and delivered by the chosen MPD.
95+
96+
Using Dash.js subtitles can be enabled using `window.bigscreenPlayer.overrides.embeddedSubtitles = true`.
97+
8298
##  Design
8399

84100
### Why not include subtitles in the manifest?

package-lock.json

Lines changed: 11 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"@babel/plugin-transform-runtime": "^7.23.9",
3131
"@babel/preset-env": "^7.23.8",
3232
"@babel/preset-typescript": "^7.23.3",
33-
"@rollup/plugin-alias": "^5.1.0",
33+
"@rollup/plugin-alias": "^5.1.1",
3434
"@rollup/plugin-babel": "^6.0.4",
3535
"@rollup/plugin-commonjs": "^25.0.7",
3636
"@rollup/plugin-inject": "^5.0.5",
@@ -64,7 +64,7 @@
6464
"typescript-eslint": "^7.2.0"
6565
},
6666
"dependencies": {
67-
"dashjs": "github:bbc/dash.js#smp-v4.7.3-6",
67+
"dashjs": "github:bbc/dash.js#smp-v4.7.3-7",
6868
"smp-imsc": "github:bbc/imscJS#v1.0.3"
6969
},
7070
"repository": {

rollup.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import PackageJSON from "./package.json" assert { type: "json" }
22

3+
import alias from "@rollup/plugin-alias"
34
import replace from "@rollup/plugin-replace"
45
import typescript from "@rollup/plugin-typescript"
56
import { dts } from "rollup-plugin-dts"
@@ -10,6 +11,9 @@ export default [
1011
external: [/^dashjs/, "smp-imsc", "tslib"],
1112
output: [{ dir: "dist/esm", format: "es" }],
1213
plugins: [
14+
alias({
15+
entries: [{ find: "imsc", replacement: "smp-imsc" }],
16+
}),
1317
replace({
1418
preventAssignment: true,
1519
__VERSION__: () => PackageJSON.version,

rollup.dev.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import PackageJSON from "./package.json" assert { type: "json" }
22

3+
import alias from "@rollup/plugin-alias"
34
import babel from "@rollup/plugin-babel"
45
import commonjs from "@rollup/plugin-commonjs"
56
import resolve from "@rollup/plugin-node-resolve"
@@ -20,6 +21,9 @@ export default {
2021
format: "es",
2122
},
2223
plugins: [
24+
alias({
25+
entries: [{ find: "imsc", replacement: "smp-imsc" }],
26+
}),
2327
replace({
2428
preventAssignment: true,
2529
__VERSION__: () => PackageJSON.version,

src/bigscreenplayer.js

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,6 @@ function BigscreenPlayer() {
114114
!initialPresentationTime &&
115115
initialPresentationTime !== 0
116116

117-
readyHelper = ReadyHelper(
118-
initialPresentationTime,
119-
mediaSources.time().manifestType,
120-
PlayerComponent.getLiveSupport(),
121-
_callbacks.playerReady
122-
)
123-
124117
playerComponent = PlayerComponent(
125118
playbackElement,
126119
{ media, enableAudioDescribed, initialPlaybackTime: initialPresentationTime },
@@ -130,13 +123,21 @@ function BigscreenPlayer() {
130123
callAudioDescribedCallbacks
131124
)
132125

133-
subtitles = Subtitles(
134-
playerComponent,
135-
enableSubtitles,
136-
playbackElement,
137-
media.subtitleCustomisation,
138-
mediaSources,
139-
callSubtitlesCallbacks
126+
readyHelper = ReadyHelper(
127+
initialPresentationTime,
128+
mediaSources.time().manifestType,
129+
PlayerComponent.getLiveSupport(),
130+
() => {
131+
_callbacks.playerReady()
132+
subtitles = Subtitles(
133+
playerComponent,
134+
enableSubtitles,
135+
playbackElement,
136+
media.subtitleCustomisation,
137+
mediaSources,
138+
callSubtitlesCallbacks
139+
)
140+
}
140141
)
141142
}
142143

src/playbackstrategy/msestrategy.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ function MSEStrategy(
2828

2929
let mediaPlayer
3030
let mediaElement
31+
let subtitleElement
32+
let subtitlesEnabled = false
3133
const manifestType = mediaSources.time().manifestType
3234

3335
const playerSettings = Utils.merge(
@@ -37,12 +39,15 @@ function MSEStrategy(
3739
},
3840
streaming: {
3941
blacklistExpiryTime: mediaSources.failoverResetTime(),
42+
lastMediaSettingsCachingInfo: { enabled: false },
4043
buffer: {
4144
bufferToKeep: 4,
4245
bufferTimeAtTopQuality: 12,
4346
bufferTimeAtTopQualityLongForm: 15,
4447
},
45-
lastMediaSettingsCachingInfo: { enabled: false },
48+
text: {
49+
defaultEnabled: false,
50+
},
4651
},
4752
},
4853
customPlayerSettings
@@ -108,6 +113,7 @@ function MSEStrategy(
108113
STREAM_INITIALIZED: "streamInitialized",
109114
FRAGMENT_CONTENT_LENGTH_MISMATCH: "fragmentContentLengthMismatch",
110115
QUOTA_EXCEEDED: "quotaExceeded",
116+
TEXT_TRACKS_ADDED: "allTextTracksAdded",
111117
CURRENT_TRACK_CHANGED: "currentTrackChanged",
112118
}
113119

@@ -555,6 +561,13 @@ function MSEStrategy(
555561
}
556562
}
557563

564+
function setUpSubtitleElement(playbackElement) {
565+
subtitleElement = document.createElement("div")
566+
subtitleElement.id = "bsp_subtitles"
567+
subtitleElement.style.position = "absolute"
568+
playbackElement.appendChild(subtitleElement, playbackElement.firstChild)
569+
}
570+
558571
function setUpMediaElement(playbackElement) {
559572
mediaElement = mediaKind === MediaKinds.AUDIO ? document.createElement("audio") : document.createElement("video")
560573

@@ -578,6 +591,7 @@ function MSEStrategy(
578591

579592
function setUpMediaPlayer(presentationTimeInSeconds) {
580593
const dashSettings = getDashSettings(playerSettings)
594+
const embeddedSubs = window.bigscreenPlayer?.overrides?.embeddedSubtitles ?? false
581595
const protectionData = mediaSources.currentProtectionData()
582596

583597
mediaPlayer = MediaPlayer().create()
@@ -589,6 +603,11 @@ function MSEStrategy(
589603

590604
mediaPlayer.initialize(mediaElement, null)
591605

606+
if (embeddedSubs) {
607+
setUpSubtitleElement(playbackElement)
608+
mediaPlayer.attachTTMLRenderingDiv(subtitleElement)
609+
}
610+
592611
modifySource(presentationTimeInSeconds)
593612
}
594613

@@ -670,10 +689,15 @@ function MSEStrategy(
670689
mediaPlayer.on(DashJSEvents.GAP_JUMP, onGapJump)
671690
mediaPlayer.on(DashJSEvents.GAP_JUMP_TO_END, onGapJump)
672691
mediaPlayer.on(DashJSEvents.QUOTA_EXCEEDED, onQuotaExceeded)
692+
mediaPlayer.on(DashJSEvents.TEXT_TRACKS_ADDED, handleTextTracks)
673693
mediaPlayer.on(DashJSEvents.MANIFEST_LOADING_FINISHED, manifestLoadingFinished)
674694
mediaPlayer.on(DashJSEvents.CURRENT_TRACK_CHANGED, onCurrentTrackChanged)
675695
}
676696

697+
function handleTextTracks() {
698+
mediaPlayer.enableText(subtitlesEnabled)
699+
}
700+
677701
function manifestLoadingFinished(event) {
678702
manifestLoadCount++
679703
manifestRequestTime = event.request.requestEndDate.getTime() - event.request.requestStartDate.getTime()
@@ -700,6 +724,10 @@ function MSEStrategy(
700724
: { start: 0, end: getDuration() }
701725
}
702726

727+
function customiseSubtitles(options) {
728+
return mediaPlayer && mediaPlayer.updateSettings({ streaming: { text: { imsc: { options } } } })
729+
}
730+
703731
function getDuration() {
704732
const duration = mediaPlayer && mediaPlayer.isReady() && mediaPlayer.duration()
705733

@@ -777,6 +805,11 @@ function MSEStrategy(
777805
)
778806
}
779807

808+
function isSubtitlesAvailable() {
809+
const textTracks = mediaPlayer.getTracksFor("text")
810+
return (textTracks && textTracks.length > 0) ?? false
811+
}
812+
780813
function isTrackAudioDescribed(track) {
781814
return (
782815
track.roles.includes("alternate") &&
@@ -855,9 +888,13 @@ function MSEStrategy(
855888
mediaElement.removeEventListener("ratechange", onRateChange)
856889

857890
DOMHelpers.safeRemoveElement(mediaElement)
858-
859891
mediaElement = undefined
860892
}
893+
894+
if (subtitleElement) {
895+
DOMHelpers.safeRemoveElement(subtitleElement)
896+
subtitleElement = undefined
897+
}
861898
}
862899

863900
function getSafelySeekableRange() {
@@ -952,9 +989,17 @@ function MSEStrategy(
952989
getCurrentTime,
953990
isAudioDescribedAvailable,
954991
isAudioDescribedEnabled,
992+
isSubtitlesAvailable,
955993
setAudioDescribedOn,
956994
setAudioDescribedOff,
957995
getDuration,
996+
setSubtitles: (state) => {
997+
subtitlesEnabled = state ?? false
998+
999+
if (mediaPlayer) {
1000+
mediaPlayer.enableText(subtitlesEnabled)
1001+
}
1002+
},
9581003
getPlayerElement: () => mediaElement,
9591004
tearDown,
9601005
reset: () => {
@@ -964,6 +1009,7 @@ function MSEStrategy(
9641009
},
9651010
isEnded: () => isEnded,
9661011
isPaused,
1012+
customiseSubtitles,
9671013
pause,
9681014
play: () => mediaPlayer.play(),
9691015
setCurrentTime,

0 commit comments

Comments
 (0)