From 7ec06b06ca450fa42c8388667caf61db43feee9f Mon Sep 17 00:00:00 2001 From: amtins Date: Sat, 17 Oct 2026 18:41:26 +0200 Subject: [PATCH] fix(media-groups): set text track kind to captions based on HLS characteristics Previously, all HLS SUBTITLES media group renditions were assigned the 'subtitles' text track kind by default. According to the W3C and WHATWG HTML standards, tracks that transcribe dialogue or describe music and sounds are called **captions**. They're intended to help people who are deaf or hard of hearing. The HLS RFC 8216 standard allows `CHARACTERISTICS` to contain several comma-separated values, such as `public.easy-to-read`. The kind is now determined by checking each value separately. Specifications references: - HLS RFC 8216 Section 4.3.4.1 (ref Characteristics): https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.4.1 ```text A SUBTITLES Rendition MAY include the following characteristics: "public.accessibility.transcribes-spoken-dialog", "public.accessibility.describes-music-and-sound", and "public.easy-to-read" (which indicates that the subtitles have been edited for ease of reading). ``` - HLS RFC 8216 Section 8.9: https://datatracker.ietf.org/doc/html/rfc8216#section-8.9 ```text CHARACTERISTICS Attribute Containing Multiple Characteristics Certain characteristics are valid in combination, as in: CHARACTERISTICS= "public.accessibility.transcribes-spoken-dialog,public.easy-to-read" ``` - WHATWG HTML / W3C HTML5 Track Kind 'captions': https://html.spec.whatwg.org/multipage/media.html#attr-track-kind-keyword-captions https://www.w3.org/TR/2011/WD-html5-20110113/video.html#the-track-element ```text captions Closed captions provide transcription or translation of the dialog, sound effects, relevant musical cues, and other relevant audio information, such as the cue's source (e.g., character, environment). They are suitable for when sound is unavailable or not clearly audible (e.g., because it is muted, drowned-out by ambient noise, or because the user is deaf). ``` - add a private function named `textTrackKind_` that will determine the correct kind - add test cases --- src/media-groups.js | 19 ++++- test/media-groups.test.js | 152 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 1 deletion(-) diff --git a/src/media-groups.js b/src/media-groups.js index b6861e9e3..0ba743407 100644 --- a/src/media-groups.js +++ b/src/media-groups.js @@ -22,6 +22,23 @@ const audioTrackKind_ = (properties) => { return kind; }; +/** + * Convert the properties of an HLS track into an textTrackKind. + * + * @private + */ +const textTrackKind_ = (properties) => { + if ( + properties.characteristics && + (properties.characteristics.indexOf('public.accessibility.transcribes-spoken-dialog') >= 0 || + properties.characteristics.indexOf('public.accessibility.describes-music-and-sound') >= 0) + ) { + return 'captions'; + } + + return 'subtitles'; +}; + /** * Pause provided segment loader and playlist loader if active * @@ -593,7 +610,7 @@ export const initialize = { if (typeof tracks[variantLabel] === 'undefined') { const track = tech.addRemoteTextTrack({ id: variantLabel, - kind: 'subtitles', + kind: textTrackKind_(properties), default: properties.default && properties.autoselect, language: properties.language, label: variantLabel diff --git a/test/media-groups.test.js b/test/media-groups.test.js index ce7c2be50..972c60689 100644 --- a/test/media-groups.test.js +++ b/test/media-groups.test.js @@ -1252,6 +1252,158 @@ QUnit.module('MediaGroups', function() { } ); + QUnit.test( + 'initialize subtitles sets text track kind to "subtitles" by default when no characteristics are present', + function(assert) { + const type = 'SUBTITLES'; + + this.main.mediaGroups[type].sub1 = { + en: { language: 'en', resolvedUri: 'sub1/en.m3u8' } + }; + + MediaGroups.initialize[type](type, this.settings); + + assert.equal( + this.mediaTypes[type].tracks.en.kind, + 'subtitles', + 'track kind is subtitles by default' + ); + } + ); + + QUnit.test( + 'initialize subtitles sets text track kind to "captions" when characteristics contains transcribes-spoken-dialog', + function(assert) { + const type = 'SUBTITLES'; + + this.main.mediaGroups[type].sub1 = { + en: { + language: 'en', + resolvedUri: 'sub1/en.m3u8', + characteristics: 'public.accessibility.transcribes-spoken-dialog' + } + }; + + MediaGroups.initialize[type](type, this.settings); + + assert.equal( + this.mediaTypes[type].tracks.en.kind, + 'captions', + 'track kind is captions when transcribing spoken dialog' + ); + } + ); + + QUnit.test( + 'initialize subtitles sets text track kind to "captions" when characteristics contains transcribes-spoken-dialog and easy-to-read', + function(assert) { + const type = 'SUBTITLES'; + + this.main.mediaGroups[type].sub1 = { + en: { + language: 'en', + resolvedUri: 'sub1/en.m3u8', + characteristics: 'public.accessibility.transcribes-spoken-dialog,public.easy-to-read' + } + }; + + MediaGroups.initialize[type](type, this.settings); + + assert.equal( + this.mediaTypes[type].tracks.en.kind, + 'captions', + 'track kind is captions when transcribing spoken dialog alongside other characteristics' + ); + } + ); + + QUnit.test( + 'initialize subtitles sets text track kind to "captions" when characteristics contains describes-music-and-sound', + function(assert) { + const type = 'SUBTITLES'; + + this.main.mediaGroups[type].sub1 = { + en: { + language: 'en', + resolvedUri: 'sub1/en.m3u8', + characteristics: 'public.accessibility.describes-music-and-sound' + } + }; + + MediaGroups.initialize[type](type, this.settings); + + assert.equal( + this.mediaTypes[type].tracks.en.kind, + 'captions', + 'track kind is captions when describing music and sound' + ); + } + ); + + QUnit.test( + 'initialize subtitles sets text track kind to "subtitles" when characteristics does not match caption criteria', + function(assert) { + const type = 'SUBTITLES'; + + this.main.mediaGroups[type].sub1 = { + en: { + language: 'en', + resolvedUri: 'sub1/en.m3u8', + characteristics: 'public.accessibility.describes-video' + } + }; + + MediaGroups.initialize[type](type, this.settings); + + assert.equal( + this.mediaTypes[type].tracks.en.kind, + 'subtitles', + 'track kind is subtitles for non-caption characteristics' + ); + } + ); + + QUnit.test( + 'initialize subtitles correctly sets track kind for mixed subtitle and caption renditions', + function(assert) { + const type = 'SUBTITLES'; + + this.main.mediaGroups[type].sub1 = { + 'en': { + language: 'en', + resolvedUri: 'sub1/en.m3u8' + }, + 'en-cc': { + language: 'en', + resolvedUri: 'sub1/en-cc.m3u8', + characteristics: 'public.accessibility.transcribes-spoken-dialog' + }, + 'fr': { + language: 'fr', + resolvedUri: 'sub1/fr.m3u8' + } + }; + + MediaGroups.initialize[type](type, this.settings); + + assert.equal( + this.mediaTypes[type].tracks.en.kind, + 'subtitles', + 'en track kind is subtitles' + ); + assert.equal( + this.mediaTypes[type].tracks['en-cc'].kind, + 'captions', + 'en-cc track kind is captions' + ); + assert.equal( + this.mediaTypes[type].tracks.fr.kind, + 'subtitles', + 'fr track kind is subtitles' + ); + } + ); + QUnit.test( 'initialize closed-captions correctly generates tracks and NO loaders', function(assert) {