From ae813be027fb53aae93e686af399c07af2f1c23c Mon Sep 17 00:00:00 2001 From: bp2008 Date: Mon, 15 Mar 2021 15:45:37 -0600 Subject: [PATCH 1/3] Fixed many issues with subtitle handling Most of the subtitle handling in hlsjs-playback was inherited from the HTML5Video playback, which is fundamentally incompatible with the way hls.js wants to do things. This commit uses hls.js Subtitle Tracks Control API and related events to solve every issue I could find. This commit also adds better handling of `forced`,`default`,`autoselect` attributes than hls.js provides natively. --- src/hls.js | 129 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 109 insertions(+), 20 deletions(-) diff --git a/src/hls.js b/src/hls.js index 94c0cf4..e1b2e72 100644 --- a/src/hls.js +++ b/src/hls.js @@ -5,7 +5,7 @@ import { Events, HTML5Video, Log, Playback, PlayerError, Utils } from '@clappr/core' import HLSJS from 'hls.js' -const { now, assign, listContainsIgnoreCase } = Utils +const { now, assign, listContainsIgnoreCase, isNumber } = Utils const AUTO = -1 @@ -159,10 +159,10 @@ export default class HlsjsPlayback extends HTML5Video { _setup() { this._ccIsSetup = false - this._ccTracksUpdated = false this._hls && this._hls.destroy() this._hls = new HLSJS(assign({}, this.options.playback.hlsjsConfig)) this._hls.once(HLSJS.Events.MEDIA_ATTACHED, () => this._hls.loadSource(this.options.src)) + this._hls.on(HLSJS.Events.MANIFEST_LOADED, (evt, data) => this._onManifestParsed(evt, data)) this._hls.on(HLSJS.Events.LEVEL_LOADED, (evt, data) => this._updatePlaybackType(evt, data)) this._hls.on(HLSJS.Events.LEVEL_UPDATED, (evt, data) => this._onLevelUpdated(evt, data)) this._hls.on(HLSJS.Events.LEVEL_SWITCHING, (evt,data) => this._onLevelSwitch(evt, data)) @@ -170,8 +170,7 @@ export default class HlsjsPlayback extends HTML5Video { this._hls.on(HLSJS.Events.FRAG_LOADED, (evt, data) => this._onFragmentLoaded(evt, data)) this._hls.on(HLSJS.Events.FRAG_PARSING_METADATA, (evt, data) => this._onFragmentParsingMetadata(evt, data)) this._hls.on(HLSJS.Events.ERROR, (evt, data) => this._onHLSJSError(evt, data)) - this._hls.on(HLSJS.Events.SUBTITLE_TRACK_LOADED, (evt, data) => this._onSubtitleLoaded(evt, data)) - this._hls.on(HLSJS.Events.SUBTITLE_TRACKS_UPDATED, () => this._ccTracksUpdated = true) + this._hls.on(HLSJS.Events.SUBTITLE_TRACK_SWITCH, (evt, data) => this._onSubtitleTrackSwitch(evt, data)) this._hls.attachMedia(this.el) } @@ -450,11 +449,6 @@ export default class HlsjsPlayback extends HTML5Video { _updatePlaybackType(evt, data) { this._playbackType = data.details.live ? Playback.LIVE : Playback.VOD this._onLevelUpdated(evt, data) - - // Live stream subtitle tracks detection hack (may not immediately available) - if (this._ccTracksUpdated && this._playbackType === Playback.LIVE && this.hasClosedCaptionsTracks) - this._onSubtitleLoaded() - } _fillLevels() { @@ -599,17 +593,6 @@ export default class HlsjsPlayback extends HTML5Video { this.trigger(Events.PLAYBACK_FRAGMENT_LOADED, data) } - _onSubtitleLoaded() { - // This event may be triggered multiple times - // Setup CC only once (disable CC by default) - if (!this._ccIsSetup) { - this.trigger(Events.PLAYBACK_SUBTITLE_AVAILABLE) - const trackId = this._playbackType === Playback.LIVE ? -1 : this.closedCaptionsTrackId - this.closedCaptionsTrackId = trackId - this._ccIsSetup = true - } - } - _onLevelSwitch(evt, data) { if (!this.levels.length) this._fillLevels() @@ -646,6 +629,112 @@ export default class HlsjsPlayback extends HTML5Video { isSeekEnabled() { return (this._playbackType === Playback.VOD || this.dvrEnabled) } + + _onManifestParsed(evt, data) { + if (this.hasClosedCaptionsTracks) + this._onSubtitleLoaded() + } + + _onSubtitleLoaded() { + // This event may be triggered multiple times + // Setup CC only once (disable CC by default) + if (!this._ccIsSetup) { + let trackId = this._playbackType === Playback.LIVE ? -1 : this._hls.subtitleTrackController.subtitleTrack + // At the time of this writing, our auto-selection logic is better than what hls.js includes. + let preferredTrack = this.getPreferredTextTrack() + if (preferredTrack) + trackId = preferredTrack.id + this.closedCaptionsTrackId = trackId + this._ccIsSetup = true + this.trigger(Events.PLAYBACK_SUBTITLE_AVAILABLE) + } + } + + getPreferredTextTrack() + { + let track = null + let allTracks = this.closedCaptionsTracks + let forcedTracks = allTracks.filter(t => t.attributes.forced) + if (forcedTracks.length > 0) { + track = this.getTextTrackForLanguage(forcedTracks.filter(t => t.attributes.default)) + if (!track) track = this.getTextTrackForLanguage(forcedTracks.filter(t => t.attributes.autoselect)) + if (!track) track = this.getTextTrackForLanguage(forcedTracks) + } + if (!track) + track = this.getTextTrackForLanguage(allTracks.filter(t => t.attributes.forced)) + return track + } + + getTextTrackForLanguage(tracks) + { + let track = null + if (tracks.length > 0) { + let lang = this.getPreferredLanguageCode().toUpperCase() + track = tracks.find(t => t.track.language.toUpperCase() === lang) + if (!track) track = tracks.find(t => t.track.language.toUpperCase().indexOf(lang) > -1) + if (!track) track = tracks.find(t => lang.indexOf(t.track.language.toUpperCase()) > -1) + if (!track) track = tracks[0] + } + return track + } + + getPreferredLanguageCode() { + try { + return navigator.userLanguage || (navigator.languages && navigator.languages.length && navigator.languages[0]) || navigator.language || navigator.browserLanguage || navigator.systemLanguage || 'en' + } catch (ex) { + return 'en' + } + } + + get closedCaptionsTracks() { + let textTracks = super.closedCaptionsTracks + // Here we extend what the base class provided. + for (let i = 0; i < textTracks.length; i++) { + let hlsCaptionsTrack = this.getHlsCaptionsTrack(textTracks[i].id) + if (hlsCaptionsTrack) + textTracks[i].attributes = { autoselect: hlsCaptionsTrack.autoselect, default: hlsCaptionsTrack.default, forced: hlsCaptionsTrack.forced } + } + return textTracks + } + + getHlsCaptionsTrack(trackId) { + for (let i = 0; i < this._hls.subtitleTrackController.subtitleTracks.length; i++) { + if (this._hls.subtitleTrackController.subtitleTracks[i].id === trackId) + return this._hls.subtitleTrackController.subtitleTracks[i] + } + return null + } + + get closedCaptionsTrackId() { + return this._ccTrackId + } + + set closedCaptionsTrackId(trackId) { + // This property overrides the one in html5_video playback (core). + if (!isNumber(trackId)) + return + // Instruct hls.js to switch the active subtitle track. + // This should trigger _onSubtitleTrackSwitch which is where we set _ccTrackId and raise our own event. + this._hls.subtitleDisplay = trackId !== -1 + this._hls.subtitleTrack = trackId + } + + _onSubtitleTrackSwitch(evt, data) { + // This is called the active subtitle track has been switched. + let trackId = data.id + if (!isNumber(trackId)) + return + + if (this.closedCaptionsTracks.length === 0) + return // Text tracks have not yet been loaded into the DOM by hls.js + + if (this._ccTrackId !== trackId) { + this._ccTrackId = trackId + this.trigger(Events.PLAYBACK_SUBTITLE_CHANGED, { + id: trackId + }) + } + } } HlsjsPlayback.canPlay = function(resource, mimeType) { From 32c34c75173ad8098edec1026b9fc8790a494a98 Mon Sep 17 00:00:00 2001 From: bp2008 Date: Mon, 15 Mar 2021 16:24:00 -0600 Subject: [PATCH 2/3] fixed minor styling inconsistencies --- src/hls.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/hls.js b/src/hls.js index e1b2e72..541aa4a 100644 --- a/src/hls.js +++ b/src/hls.js @@ -650,8 +650,7 @@ export default class HlsjsPlayback extends HTML5Video { } } - getPreferredTextTrack() - { + getPreferredTextTrack() { let track = null let allTracks = this.closedCaptionsTracks let forcedTracks = allTracks.filter(t => t.attributes.forced) @@ -665,8 +664,7 @@ export default class HlsjsPlayback extends HTML5Video { return track } - getTextTrackForLanguage(tracks) - { + getTextTrackForLanguage(tracks) { let track = null if (tracks.length > 0) { let lang = this.getPreferredLanguageCode().toUpperCase() From 27d2128fd13bc8f7443423558929b29b50266260 Mon Sep 17 00:00:00 2001 From: bp2008 Date: Mon, 15 Mar 2021 16:25:19 -0600 Subject: [PATCH 3/3] Removed unused function arguments from `_onManifestParsed` Silly Hound --- src/hls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hls.js b/src/hls.js index 541aa4a..7fbe9cb 100644 --- a/src/hls.js +++ b/src/hls.js @@ -630,7 +630,7 @@ export default class HlsjsPlayback extends HTML5Video { return (this._playbackType === Playback.VOD || this.dvrEnabled) } - _onManifestParsed(evt, data) { + _onManifestParsed() { if (this.hasClosedCaptionsTracks) this._onSubtitleLoaded() }