Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Dash.js rendered subtitles #352

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions docs/tutorials/Subtitles.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ You provide subtitles to BigscreenPlayer by setting `media.captions` in the `.in

```js
// 1️⃣ Add an array of caption blocks to your playback data.
playbackData.media.captions = [/* caption blocks... */];
playbackData.media.captions = [
/* caption blocks... */
]

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

1. `media.captions` MUST be an array containing at least one object.
Expand All @@ -34,7 +36,7 @@ const captions = [
{ url: "https://some.cdn/subtitles.xml" },
{ url: "https://other.cdn/subtitles.xml" },
/* ... */
];
]
```

Subtitles delivered as a whole do not require any additional metadata in the manifest to work.
Expand All @@ -49,13 +51,15 @@ const captions = [
{
url: "https://some.cdn/subtitles/$segment$.m4s",
segmentLength: 3.84,
cdn: "default",
},
{
url: "https://other.cdn/subtitles/$segment$.m4s",
segmentLength: 3.84,
cdn: "default",
},
/* ... */
];
]
```

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:
Expand All @@ -73,12 +77,24 @@ You can style the subtitles by setting `media.subtitleCustomisation` in the `.in

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

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

### Low Latency Streams

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.

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.

An override has been added to allow subtitles to be rendered directly by Dash.js instead of the current side-chain.

Subtitles can be enabled and disabled in the usual way using the `setSubtitlesEnabled()` function. However, they are signalled and delivered by the chosen MPD.

Using Dash.js subtitles can be enabled using `window.bigscreenPlayer.overrides.embeddedSubtitles = true`.

##  Design

### Why not include subtitles in the manifest?
Expand Down
34 changes: 33 additions & 1 deletion src/playbackstrategy/msestrategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ function MSEStrategy(mediaSources, windowType, mediaKind, playbackElement, isUHD
STREAM_INITIALIZED: "streamInitialized",
FRAGMENT_CONTENT_LENGTH_MISMATCH: "fragmentContentLengthMismatch",
QUOTA_EXCEEDED: "quotaExceeded",
TEXT_TRACKS_ADDED: "allTextTracksAdded",
}

function onLoadedMetaData() {
Expand Down Expand Up @@ -512,10 +513,16 @@ function MSEStrategy(mediaSources, windowType, mediaKind, playbackElement, isUHD

function setUpMediaPlayer(playbackTime) {
const dashSettings = getDashSettings(playerSettings)
const embeddedSubs = window.bigscreenPlayer?.overrides?.embeddedSubtitles ?? false

mediaPlayer = MediaPlayer().create()
mediaPlayer.updateSettings(dashSettings)
mediaPlayer.initialize(mediaElement, null, true)

if (embeddedSubs) {
mediaPlayer.attachTTMLRenderingDiv(document.querySelector("#bsp_subtitles"))
}

modifySource(playbackTime)
}

Expand All @@ -525,7 +532,6 @@ function MSEStrategy(mediaSources, windowType, mediaKind, playbackElement, isUHD
windowType,
initialSeekableRangeStartSeconds: mediaSources.time().windowStartTime / 1000,
})

mediaPlayer.attachSource(`${source}${anchor}`)
}

Expand Down Expand Up @@ -562,9 +568,24 @@ function MSEStrategy(mediaSources, windowType, mediaKind, playbackElement, isUHD
mediaPlayer.on(DashJSEvents.GAP_JUMP, onGapJump)
mediaPlayer.on(DashJSEvents.GAP_JUMP_TO_END, onGapJump)
mediaPlayer.on(DashJSEvents.QUOTA_EXCEEDED, onQuotaExceeded)
mediaPlayer.on(DashJSEvents.TEXT_TRACKS_ADDED, disableTextTracks)
mediaPlayer.on(DashJSEvents.MANIFEST_LOADING_FINISHED, manifestLoadingFinished)
}

function disableTextTracks() {
eirikbjornr marked this conversation as resolved.
Show resolved Hide resolved
const textTracks = mediaElement.textTracks
for (let index = 0; index < textTracks.length; index++) {
textTracks[index].mode = "disabled"
}
}

function enableTextTracks() {
const textTracks = mediaElement.textTracks
for (let index = 0; index < textTracks.length; index++) {
textTracks[index].mode = "showing"
}
}

function manifestLoadingFinished(event) {
manifestLoadCount++
manifestRequestTime = event.request.requestEndDate.getTime() - event.request.requestStartDate.getTime()
Expand All @@ -587,6 +608,10 @@ function MSEStrategy(mediaSources, windowType, mediaKind, playbackElement, isUHD
}
}

function customiseSubtitles(options) {
return mediaPlayer && mediaPlayer.updateSettings({ streaming: { text: { imsc: { options } } } })
}

function getDuration() {
return mediaPlayer && mediaPlayer.isReady() ? mediaPlayer.duration() : 0
}
Expand Down Expand Up @@ -711,6 +736,12 @@ function MSEStrategy(mediaSources, windowType, mediaKind, playbackElement, isUHD
getSeekableRange,
getCurrentTime,
getDuration,
setSubtitles: (state) => {
if (state) {
enableTextTracks()
}
mediaPlayer.enableText(state)
},
getPlayerElement: () => mediaElement,
tearDown: () => {
cleanUpMediaPlayer()
Expand Down Expand Up @@ -750,6 +781,7 @@ function MSEStrategy(mediaSources, windowType, mediaKind, playbackElement, isUHD
startAutoResumeTimeout()
}
},
customiseSubtitles,
play: () => mediaPlayer.play(),
setCurrentTime: (time) => {
publishedSeekEvent = false
Expand Down
10 changes: 10 additions & 0 deletions src/playercomponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ function PlayerComponent(
}
}

function setSubtitles(state) {
return playbackStrategy && playbackStrategy.setSubtitles(state)
}

function customiseSubtitles(styleOpts) {
return playbackStrategy && playbackStrategy.customiseSubtitles(styleOpts)
}

function getDuration() {
return playbackStrategy && playbackStrategy.getDuration()
}
Expand Down Expand Up @@ -394,6 +402,8 @@ function PlayerComponent(
return {
play,
pause,
setSubtitles,
customiseSubtitles,
transitions,
isEnded,
setPlaybackRate,
Expand Down
90 changes: 90 additions & 0 deletions src/subtitles/embeddedsubtitles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import DOMHelpers from "../domhelpers"
import Utils from "../utils/playbackutils"

function EmbeddedSubtitles(mediaPlayer, autoStart, parentElement, mediaSources, defaultStyleOpts) {
let currentSubtitlesElement

let imscRenderOpts = transformStyleOptions(defaultStyleOpts)

if (autoStart) {
start()
mediaPlayer.addEventCallback(this, onMediaPlayerReady)
}

function onMediaPlayerReady() {
mediaPlayer.removeEventCallback(this, onMediaPlayerReady)
}

function removeCurrentSubtitlesElement() {
if (currentSubtitlesElement) {
DOMHelpers.safeRemoveElement(currentSubtitlesElement)
currentSubtitlesElement = undefined
}
}

function addCurrentSubtitlesElement() {
removeCurrentSubtitlesElement()
currentSubtitlesElement = document.createElement("div")
currentSubtitlesElement.id = "bsp_subtitles"
currentSubtitlesElement.style.position = "absolute"
parentElement.appendChild(currentSubtitlesElement)
}

function start() {
mediaPlayer.setSubtitles(true)
customise(imscRenderOpts)
if (!currentSubtitlesElement) {
addCurrentSubtitlesElement()
}
}

function stop() {
mediaPlayer.setSubtitles(false)
}

function tearDown() {
stop()
}

function customise(styleOpts) {
const customStyleOptions = transformStyleOptions(styleOpts)
imscRenderOpts = Utils.merge(imscRenderOpts, customStyleOptions)
mediaPlayer.customiseSubtitles(imscRenderOpts)
}

// Opts: { backgroundColour: string (css colour, hex), fontFamily: string , size: number, lineHeight: number }
function transformStyleOptions(opts) {
if (opts === undefined) return

const customStyles = {}

if (opts.backgroundColour) {
customStyles.spanBackgroundColorAdjust = { transparent: opts.backgroundColour }
}

if (opts.fontFamily) {
customStyles.fontFamily = opts.fontFamily
}

if (opts.size > 0) {
customStyles.sizeAdjust = opts.size
}

if (opts.lineHeight) {
customStyles.lineHeightAdjust = opts.lineHeight
}

return customStyles
}

addCurrentSubtitlesElement()

return {
start,
stop,
customise,
tearDown,
}
}

export default EmbeddedSubtitles
Loading