diff --git a/externs/shaka/manifest.js b/externs/shaka/manifest.js index 2270421916..f88a6cfc48 100644 --- a/externs/shaka/manifest.js +++ b/externs/shaka/manifest.js @@ -326,6 +326,7 @@ shaka.extern.FetchCryptoKeysFunction; * trickModeVideo: ?shaka.extern.Stream, * emsgSchemeIdUris: ?Array., * roles: !Array., + * accessibilityPurpose: ?shaka.dash.DashParser.AccessibilityPurpose, * forced: boolean, * channelsCount: ?number, * audioSamplingRate: ?number, @@ -424,6 +425,8 @@ shaka.extern.FetchCryptoKeysFunction; * @property {!Array.} roles * The roles of the stream as they appear on the manifest, * e.g. 'main', 'caption', or 'commentary'. + * @property {?shaka.dash.DashParser.AccessibilityPurpose} accessibilityPurpose + * The DASH accessibility descriptor, if one was provided for this stream. * @property {boolean} forced * Defaults to false.
* Whether the stream set was forced diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 39947751df..145477d054 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -225,6 +225,7 @@ shaka.extern.BufferedInfo; * primary: boolean, * roles: !Array., * audioRoles: Array., + * accessibilityPurpose: ?shaka.dash.DashParser.AccessibilityPurpose, * forced: boolean, * videoId: ?number, * audioId: ?number, @@ -301,6 +302,9 @@ shaka.extern.BufferedInfo; * The roles of the audio in the track, e.g. 'main' or * 'commentary'. Will be null for text tracks or variant tracks * without audio. + * @property {?shaka.dash.DashParser.AccessibilityPurpose} accessibilityPurpose + * The DASH accessibility descriptor, if one was provided for this track. + * For text tracks, this describes the text; otherwise, this is for the audio. * @property {boolean} forced * True indicates that this in the forced text language for the content. * This flag is based on signals from the manifest. diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index fc2b005472..0f02c16fd5 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -933,6 +933,8 @@ shaka.dash.DashParser = class { const accessibilities = XmlUtils.findChildren(elem, 'Accessibility'); const LanguageUtils = shaka.util.LanguageUtils; const closedCaptions = new Map(); + /** @type {?shaka.dash.DashParser.AccessibilityPurpose} */ + let accessibilityPurpose; for (const prop of accessibilities) { const schemeId = prop.getAttribute('schemeIdUri'); const value = prop.getAttribute('value'); @@ -1015,6 +1017,15 @@ shaka.dash.DashParser = class { kind = ManifestParserUtils.TextStreamKind.CLOSED_CAPTION; } } + } else if (schemeId == 'urn:tva:metadata:cs:AudioPurposeCS:2007') { + // See DASH DVB Document A168 Rev.6 Table 5. + if (value == '1') { + accessibilityPurpose = + shaka.dash.DashParser.AccessibilityPurpose.VISUALLY_IMPAIRED; + } else if (value == '2') { + accessibilityPurpose = + shaka.dash.DashParser.AccessibilityPurpose.HARD_OF_HEARING; + } } } @@ -1057,7 +1068,7 @@ shaka.dash.DashParser = class { const streams = representations.map((representation) => { const parsedRepresentation = this.parseRepresentation_(context, contentProtection, kind, language, label, main, roleValues, - closedCaptions, representation); + closedCaptions, representation, accessibilityPurpose); if (parsedRepresentation) { parsedRepresentation.hdr = parsedRepresentation.hdr || videoRange; } @@ -1133,12 +1144,14 @@ shaka.dash.DashParser = class { * @param {!Array.} roles * @param {Map.} closedCaptions * @param {!Element} node + * @param {?shaka.dash.DashParser.AccessibilityPurpose} accessibilityPurpose + * * @return {?shaka.extern.Stream} The Stream, or null when there is a * non-critical parsing error. * @private */ parseRepresentation_(context, contentProtection, kind, language, label, - isPrimary, roles, closedCaptions, node) { + isPrimary, roles, closedCaptions, node, accessibilityPurpose) { const XmlUtils = shaka.util.XmlUtils; const ContentType = shaka.util.ManifestParserUtils.ContentType; @@ -1320,6 +1333,7 @@ shaka.dash.DashParser = class { hdr, tilesLayout, matchedStreams: [], + accessibilityPurpose, }; } @@ -2072,6 +2086,16 @@ shaka.dash.DashParser.AdaptationInfo; shaka.dash.DashParser.GenerateSegmentIndexFunction; +/** + * @enum {string} + * @export + */ +shaka.dash.DashParser.AccessibilityPurpose = { + VISUALLY_IMPAIRED: 'visually impaired', + HARD_OF_HEARING: 'hard of hearing', +}; + + /** * @typedef {{ * generateSegmentIndex: shaka.dash.DashParser.GenerateSegmentIndexFunction diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 0314e5a975..f086346c73 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -2321,6 +2321,7 @@ shaka.hls.HlsParser = class { closedCaptions, hdr: undefined, tilesLayout: undefined, + accessibilityPurpose: null, }; } diff --git a/lib/mss/mss_parser.js b/lib/mss/mss_parser.js index 87ea5444a3..356cd8f91f 100644 --- a/lib/mss/mss_parser.js +++ b/lib/mss/mss_parser.js @@ -532,6 +532,7 @@ shaka.mss.MssParser = class { timescale: context.timescale, codecPrivateData: null, }, + accessibilityPurpose: null, }; // This is specifically for text tracks. diff --git a/lib/offline/manifest_converter.js b/lib/offline/manifest_converter.js index cd6502e39e..b85122c67c 100644 --- a/lib/offline/manifest_converter.js +++ b/lib/offline/manifest_converter.js @@ -205,6 +205,7 @@ shaka.offline.ManifestConverter = class { spatialAudio: streamDB.spatialAudio, closedCaptions: streamDB.closedCaptions, tilesLayout: streamDB.tilesLayout, + accessibilityPurpose: null, }; return stream; diff --git a/lib/player.js b/lib/player.js index 37b11bff88..d704cd5eb9 100644 --- a/lib/player.js +++ b/lib/player.js @@ -2384,6 +2384,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { audioSamplingRate: null, spatialAudio: false, closedCaptions: null, + accessibilityPurpose: null, }, bandwidth: 100, allowedByApplication: true, @@ -4860,6 +4861,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { audioSamplingRate: null, spatialAudio: false, closedCaptions: null, + accessibilityPurpose: null, }; const fullMimeType = shaka.util.MimeUtils.getFullType( @@ -5011,6 +5013,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { spatialAudio: false, closedCaptions: null, tilesLayout: '1x1', + accessibilityPurpose: null, }; this.manifest_.imageStreams.push(stream); @@ -5430,6 +5433,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { audioSamplingRate: null, spatialAudio: false, closedCaptions: null, + accessibilityPurpose: null, }; manifest.textStreams.push(textStream); closedCaptionsSet.add(id); diff --git a/lib/util/periods.js b/lib/util/periods.js index b80b1b6cb5..67ef5f95e6 100644 --- a/lib/util/periods.js +++ b/lib/util/periods.js @@ -1529,6 +1529,7 @@ shaka.util.PeriodCombiner = class { audioSamplingRate: null, spatialAudio: false, closedCaptions: null, + accessibilityPurpose: null, }; } diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index 22d24cfd51..1a3aafe7d4 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -1109,6 +1109,7 @@ shaka.util.StreamUtils = class { originalAudioId: null, originalTextId: null, originalImageId: null, + accessibilityPurpose: null, }; if (video) { @@ -1130,6 +1131,7 @@ shaka.util.StreamUtils = class { track.spatialAudio = audio.spatialAudio; track.label = audio.label; track.audioRoles = audio.roles; + track.accessibilityPurpose = audio.accessibilityPurpose; } return track; @@ -1179,6 +1181,7 @@ shaka.util.StreamUtils = class { originalAudioId: null, originalTextId: stream.originalId, originalImageId: null, + accessibilityPurpose: stream.accessibilityPurpose, }; return track; @@ -1253,6 +1256,7 @@ shaka.util.StreamUtils = class { originalAudioId: null, originalTextId: null, originalImageId: stream.originalId, + accessibilityPurpose: null, }; return track; @@ -1373,6 +1377,7 @@ shaka.util.StreamUtils = class { originalAudioId: null, originalTextId: null, originalImageId: null, + accessibilityPurpose: null, }; return track; diff --git a/test/dash/dash_parser_manifest_unit.js b/test/dash/dash_parser_manifest_unit.js index b261e2bdc2..ef7f78c392 100644 --- a/test/dash/dash_parser_manifest_unit.js +++ b/test/dash/dash_parser_manifest_unit.js @@ -1864,6 +1864,49 @@ describe('DashParser Manifest', () => { expect(stream).toBeUndefined(); }); + it('reads accessibility purpose elements', async () => { + const manifestText = [ + '', + ' ', + ' ', + ' ', + ' ', + ' t-en.vtt', + ' ', + ' ', + ' ', + ' ', + ' v-sd.mp4', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' v-sd.mp4', + ' ', + ' ', + ' ', + ' ', + '', + ].join('\n'); + + fakeNetEngine.setResponseText('dummy://foo', manifestText); + /** @type {shaka.extern.Manifest} */ + const manifest = await parser.start('dummy://foo', playerInterface); + const textStream = manifest.textStreams[0]; + expect(textStream.accessibilityPurpose) + .toBe(shaka.dash.DashParser.AccessibilityPurpose.HARD_OF_HEARING); + const variant = manifest.variants[0]; + expect(variant.video.accessibilityPurpose).toBeUndefined(); + expect(variant.audio.accessibilityPurpose) + .toBe(shaka.dash.DashParser.AccessibilityPurpose.VISUALLY_IMPAIRED); + }); + it('converts Accessibility element to "kind"', async () => { const manifestText = [ '', diff --git a/test/media/adaptation_set_unit.js b/test/media/adaptation_set_unit.js index f6256da31c..81f811d2fa 100644 --- a/test/media/adaptation_set_unit.js +++ b/test/media/adaptation_set_unit.js @@ -218,6 +218,7 @@ describe('AdaptationSet', () => { forced: false, trickModeVideo: null, type: '', + accessibilityPurpose: null, }; } }); diff --git a/test/offline/manifest_convert_unit.js b/test/offline/manifest_convert_unit.js index aad5f1cdc5..17a1770c4a 100644 --- a/test/offline/manifest_convert_unit.js +++ b/test/offline/manifest_convert_unit.js @@ -389,6 +389,7 @@ describe('ManifestConverter', () => { spatialAudio: false, closedCaptions: null, tilesLayout: undefined, + accessibilityPurpose: null, }; } @@ -438,6 +439,7 @@ describe('ManifestConverter', () => { spatialAudio: false, closedCaptions: null, tilesLayout: undefined, + accessibilityPurpose: null, }; } @@ -486,6 +488,7 @@ describe('ManifestConverter', () => { spatialAudio: false, closedCaptions: null, tilesLayout: undefined, + accessibilityPurpose: null, }; } @@ -531,6 +534,7 @@ describe('ManifestConverter', () => { spatialAudio: streamDb.spatialAudio, closedCaptions: streamDb.closedCaptions, tilesLayout: streamDb.tilesLayout, + accessibilityPurpose: null, }; expect(stream).toEqual(expectedStream); diff --git a/test/offline/storage_integration.js b/test/offline/storage_integration.js index 1f3ab5f182..db865d2cfe 100644 --- a/test/offline/storage_integration.js +++ b/test/offline/storage_integration.js @@ -1403,6 +1403,7 @@ filterDescribe('Storage', storageSupport, () => { originalAudioId: audioId.toString(), originalTextId: null, originalImageId: null, + accessibilityPurpose: null, }; } @@ -1447,6 +1448,7 @@ filterDescribe('Storage', storageSupport, () => { originalAudioId: null, originalTextId: id.toString(), originalImageId: null, + accessibilityPurpose: null, }; } diff --git a/test/player_unit.js b/test/player_unit.js index 4e2fdf515c..c7f7dea61b 100644 --- a/test/player_unit.js +++ b/test/player_unit.js @@ -1609,6 +1609,7 @@ describe('Player', () => { originalVideoId: 'video-1kbps', originalTextId: null, originalImageId: null, + accessibilityPurpose: undefined, }, { id: 101, @@ -1645,6 +1646,7 @@ describe('Player', () => { originalVideoId: 'video-2kbps', originalTextId: null, originalImageId: null, + accessibilityPurpose: undefined, }, { id: 102, @@ -1681,6 +1683,7 @@ describe('Player', () => { originalVideoId: 'video-1kbps', originalTextId: null, originalImageId: null, + accessibilityPurpose: undefined, }, { id: 103, @@ -1717,6 +1720,7 @@ describe('Player', () => { originalVideoId: 'video-2kbps', originalTextId: null, originalImageId: null, + accessibilityPurpose: undefined, }, { id: 104, @@ -1753,6 +1757,7 @@ describe('Player', () => { originalVideoId: 'video-1kbps', originalTextId: null, originalImageId: null, + accessibilityPurpose: undefined, }, { id: 105, @@ -1789,6 +1794,7 @@ describe('Player', () => { originalVideoId: 'video-2kbps', originalTextId: null, originalImageId: null, + accessibilityPurpose: undefined, }, { id: 106, @@ -1825,6 +1831,7 @@ describe('Player', () => { originalVideoId: 'video-1kbps', originalTextId: null, originalImageId: null, + accessibilityPurpose: undefined, }, { id: 107, @@ -1861,6 +1868,7 @@ describe('Player', () => { originalVideoId: 'video-2kbps', originalTextId: null, originalImageId: null, + accessibilityPurpose: undefined, }, ]; @@ -1900,6 +1908,7 @@ describe('Player', () => { originalVideoId: null, originalTextId: 'text-es', originalImageId: null, + accessibilityPurpose: undefined, }, { id: 51, @@ -1936,6 +1945,7 @@ describe('Player', () => { originalVideoId: null, originalTextId: 'text-en', originalImageId: null, + accessibilityPurpose: undefined, }, { id: 52, @@ -1972,6 +1982,7 @@ describe('Player', () => { originalVideoId: null, originalTextId: 'text-commentary', originalImageId: null, + accessibilityPurpose: undefined, }, ]; @@ -2011,6 +2022,7 @@ describe('Player', () => { originalVideoId: null, originalTextId: null, originalImageId: 'thumbnail', + accessibilityPurpose: null, }, ]; diff --git a/test/test/util/manifest_generator.js b/test/test/util/manifest_generator.js index 959c294119..a65557d810 100644 --- a/test/test/util/manifest_generator.js +++ b/test/test/util/manifest_generator.js @@ -548,6 +548,8 @@ shaka.test.ManifestGenerator.Stream = class { this.hdr = undefined; /** @type {(string|undefined)} */ this.tilesLayout = undefined; + /** @type {?shaka.dash.DashParser.AccessibilityPurpose} */ + this.accessibilityPurpose; } /** @type {shaka.extern.Stream} */ diff --git a/test/test/util/streaming_engine_util.js b/test/test/util/streaming_engine_util.js index ba49e67b25..3ff4f8f756 100644 --- a/test/test/util/streaming_engine_util.js +++ b/test/test/util/streaming_engine_util.js @@ -412,6 +412,7 @@ shaka.test.StreamingEngineUtil = class { roles: [], forced: false, spatialAudio: false, + accessibilityPurpose: null, }; } @@ -449,6 +450,7 @@ shaka.test.StreamingEngineUtil = class { roles: [], forced: false, spatialAudio: false, + accessibilityPurpose: null, }; } @@ -484,6 +486,7 @@ shaka.test.StreamingEngineUtil = class { roles: [], forced: false, spatialAudio: false, + accessibilityPurpose: null, }; } };