From f53349fc93e97dd1344025119b8566501950213b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Tue, 18 Jul 2023 11:09:17 +0200 Subject: [PATCH] feat: Add originalLanguage to the Track structure (#5409) Shaka in most of places normalizes tracks' language code to be compliant with ISO 639-1 when possible. However, it does not do that all the time (i.e. normalization is missing in MSS parser) and there is no way to get value that has been explicitly set in a manifest. Moreover, documentation is misleading, as it claims that value is taken directly from a manifest. Normalization should take place, specifically to easify PeriodCombiner algorithm and also to not break existing applications. However, original value can be desired for some implementations. This PR introduces new field to get original language value from the manifest. --- externs/shaka/manifest.js | 4 ++ externs/shaka/offline.js | 3 ++ externs/shaka/player.js | 13 ++++-- lib/dash/dash_parser.js | 9 +++- lib/hls/hls_parser.js | 53 ++++++++++++------------ lib/mss/mss_parser.js | 4 +- lib/offline/indexeddb/v1_storage_cell.js | 1 + lib/offline/indexeddb/v2_storage_cell.js | 1 + lib/offline/manifest_converter.js | 1 + lib/offline/storage.js | 1 + lib/player.js | 7 +++- lib/util/periods.js | 2 + lib/util/stream_utils.js | 5 +++ test/dash/dash_parser_manifest_unit.js | 3 +- test/hls/hls_parser_unit.js | 22 ++++++++++ test/media/adaptation_set_unit.js | 1 + test/offline/manifest_convert_unit.js | 5 +++ test/offline/storage_integration.js | 2 + test/player_unit.js | 15 +++++++ test/test/util/manifest_generator.js | 6 ++- test/test/util/offline_utils.js | 1 + test/test/util/streaming_engine_util.js | 3 ++ test/util/periods_unit.js | 18 +++++--- 23 files changed, 139 insertions(+), 41 deletions(-) diff --git a/externs/shaka/manifest.js b/externs/shaka/manifest.js index 67ebeb9965..7fb1c03693 100644 --- a/externs/shaka/manifest.js +++ b/externs/shaka/manifest.js @@ -344,6 +344,7 @@ shaka.extern.FetchCryptoKeysFunction; * drmInfos: !Array., * keyIds: !Set., * language: string, + * originalLanguage: ?string, * label: ?string, * type: string, * primary: boolean, @@ -429,6 +430,9 @@ shaka.extern.FetchCryptoKeysFunction; * The Stream's language, specified as a language code.
* Audio stream's language must be identical to the language of the containing * Variant. + * @property {?string} originalLanguage + * Optional.
+ * The original language, if any, that appeared in the manifest. * @property {?string} label * The Stream's label, unique text that should describe the audio/text track. * @property {string} type diff --git a/externs/shaka/offline.js b/externs/shaka/offline.js index 43ede57a32..4296dea087 100644 --- a/externs/shaka/offline.js +++ b/externs/shaka/offline.js @@ -123,6 +123,7 @@ shaka.extern.ManifestDB; * hdr: (string|undefined), * kind: (string|undefined), * language: string, + * originalLanguage: (?string|undefined), * label: ?string, * width: ?number, * height: ?number, @@ -162,6 +163,8 @@ shaka.extern.ManifestDB; * The kind of text stream; undefined for audio/video. * @property {string} language * The language of the stream; '' for video. + * @property {(?string|undefined)} originalLanguage + * The original language, if any, that appeared in the manifest. * @property {?string} label * The label of the stream; '' for video. * @property {?number} width diff --git a/externs/shaka/player.js b/externs/shaka/player.js index ba1a42c555..e19af7c06d 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -238,7 +238,8 @@ shaka.extern.BufferedInfo; * originalVideoId: ?string, * originalAudioId: ?string, * originalTextId: ?string, - * originalImageId: ?string + * originalImageId: ?string, + * originalLanguage: ?string * }} * * @description @@ -260,8 +261,10 @@ shaka.extern.BufferedInfo; * The bandwidth required to play the track, in bits/sec. * * @property {string} language - * The language of the track, or 'und' if not given. This is the - * exact value provided in the manifest; it may need to be normalized. + * The language of the track, or 'und' if not given. This value + * is normalized as follows - language part is always lowercase and translated + * to ISO-639-1 when possible, locale part is always uppercase, + * i.e. 'en-US'. * @property {?string} label * The track label, which is unique text that should describe the track. * @property {?string} kind @@ -340,6 +343,10 @@ shaka.extern.BufferedInfo; * @property {?string} originalImageId * (image tracks only) The original ID of the image track, if any, as it * appeared in the original manifest. + * @property {?string} originalLanguage + * The original language of the track, if any, as it appeared in the original + * manifest. This is the exact value provided in the manifest; for normalized + * value use language property. * @exportDoc */ shaka.extern.Track; diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 8454130134..3a2780b085 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -1108,8 +1108,8 @@ shaka.dash.DashParser = class { this.config_.dash.ignoreDrmInfo, this.config_.dash.keySystemsByURI); - const language = - shaka.util.LanguageUtils.normalize(elem.getAttribute('lang') || 'und'); + const language = shaka.util.LanguageUtils.normalize( + context.adaptationSet.language || 'und'); // This attribute is currently non-standard, but it is supported by Kaltura. let label = elem.getAttribute('label'); @@ -1380,6 +1380,7 @@ shaka.dash.DashParser = class { drmInfos: contentProtection.drmInfos, keyIds, language, + originalLanguage: context.adaptationSet.language, label, type: context.adaptationSet.contentType, primary: isPrimary, @@ -1569,6 +1570,7 @@ shaka.dash.DashParser = class { pixelAspectRatio: pixelAspectRatio, emsgSchemeIdUris: emsgSchemeIdUris, id: elem.getAttribute('id'), + language: elem.getAttribute('lang'), numChannels: numChannels, audioSamplingRate: audioSamplingRate, availabilityTimeOffset: availabilityTimeOffset, @@ -1979,6 +1981,7 @@ shaka.dash.DashParser.RequestSegmentCallback; * pixelAspectRatio: (string|undefined), * emsgSchemeIdUris: !Array., * id: ?string, + * language: ?string, * numChannels: ?number, * audioSamplingRate: ?number, * availabilityTimeOffset: number @@ -2014,6 +2017,8 @@ shaka.dash.DashParser.RequestSegmentCallback; * emsg registered schemeIdUris. * @property {?string} id * The ID of the element. + * @property {?string} language + * The original language of the element. * @property {?number} numChannels * The number of audio channels, or null if unknown. * @property {?number} audioSamplingRate diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 167a755c4c..9b5bc923e8 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -666,7 +666,7 @@ shaka.hls.HlsParser = class { const type = basicInfo.type; const mimeType = basicInfo.mimeType; const codecs = basicInfo.codecs; - const language = basicInfo.language || 'und'; + const languageValue = basicInfo.language; const height = basicInfo.height; const width = basicInfo.width; const channelsCount = basicInfo.channelCount; @@ -685,7 +685,7 @@ shaka.hls.HlsParser = class { // Make the stream info, with those values. const streamInfo = await this.convertParsedPlaylistIntoStreamInfo_( - playlist, uri, uri, codecs, type, language, primary, name, + playlist, uri, uri, codecs, type, languageValue, primary, name, channelsCount, closedCaptions, characteristics, forced, sampleRate, spatialAudio, mimeType); this.uriToStreamInfosMap_.set(uri, streamInfo); @@ -698,7 +698,7 @@ shaka.hls.HlsParser = class { // Wrap the stream from that stream info with a variant. variants.push({ id: 0, - language: language, + language: this.getLanguage_(languageValue), disabledUntilTime: 0, primary: true, audio: type == 'audio' ? streamInfo.stream : null, @@ -1609,16 +1609,15 @@ shaka.hls.HlsParser = class { } /** - * Get the language value. + * Get the normalized language value. * - * @param {!shaka.hls.Tag} tag + * @param {?string} languageValue * @return {string} * @private */ - getLanguage_(tag) { + getLanguage_(languageValue) { const LanguageUtils = shaka.util.LanguageUtils; - const languageValue = tag.getAttributeValue('LANGUAGE') || 'und'; - return LanguageUtils.normalize(languageValue); + return LanguageUtils.normalize(languageValue || 'und'); } /** @@ -1751,7 +1750,8 @@ shaka.hls.HlsParser = class { for (const tag of closedCaptionsTags) { goog.asserts.assert(tag.name == 'EXT-X-MEDIA', 'Should only be called on media tags!'); - const language = this.getLanguage_(tag); + const languageValue = tag.getAttributeValue('LANGUAGE'); + const language = this.getLanguage_(languageValue); // The GROUP-ID value is a quoted-string that specifies the group to which // the Rendition belongs. @@ -1797,7 +1797,7 @@ shaka.hls.HlsParser = class { return this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri); } - const language = this.getLanguage_(tag); + const language = tag.getAttributeValue('LANGUAGE'); const name = tag.getAttributeValue('NAME'); // NOTE: According to the HLS spec, "DEFAULT=YES" requires "AUTOSELECT=YES". @@ -1860,7 +1860,7 @@ shaka.hls.HlsParser = class { return this.uriToStreamInfosMap_.get(verbatimImagePlaylistUri); } - const language = this.getLanguage_(tag); + const language = tag.getAttributeValue('LANGUAGE'); const name = tag.getAttributeValue('NAME'); const characteristics = tag.getAttributeValue('CHARACTERISTICS'); @@ -1930,7 +1930,7 @@ shaka.hls.HlsParser = class { const closedCaptions = this.getClosedCaptions_(tag, type); const codecs = shaka.util.ManifestParserUtils.guessCodecs(type, allCodecs); const streamInfo = this.createStreamInfo_(verbatimMediaPlaylistUri, - codecs, type, /* language= */ 'und', /* primary= */ false, + codecs, type, /* language= */ null, /* primary= */ false, /* name= */ null, /* channelcount= */ null, closedCaptions, /* characteristics= */ null, /* forced= */ false, /* sampleRate= */ null, /* spatialAudio= */ false); @@ -1949,7 +1949,7 @@ shaka.hls.HlsParser = class { * @param {string} verbatimMediaPlaylistUri * @param {string} codecs * @param {string} type - * @param {string} language + * @param {?string} languageValue * @param {boolean} primary * @param {?string} name * @param {?number} channelsCount @@ -1961,7 +1961,7 @@ shaka.hls.HlsParser = class { * @return {!shaka.hls.HlsParser.StreamInfo} * @private */ - createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language, + createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, languageValue, primary, name, channelsCount, closedCaptions, characteristics, forced, sampleRate, spatialAudio) { // TODO: Refactor, too many parameters @@ -1971,9 +1971,9 @@ shaka.hls.HlsParser = class { // This stream is lazy-loaded inside the createSegmentIndex function. // So we start out with a stream object that does not contain the actual // segment index, then download when createSegmentIndex is called. - const stream = this.makeStreamObject_(codecs, type, language, primary, name, - channelsCount, closedCaptions, characteristics, forced, sampleRate, - spatialAudio); + const stream = this.makeStreamObject_(codecs, type, languageValue, primary, + name, channelsCount, closedCaptions, characteristics, forced, + sampleRate, spatialAudio); const streamInfo = { stream, type, @@ -2013,7 +2013,7 @@ shaka.hls.HlsParser = class { const wasLive = this.isLive_(); const realStreamInfo = await this.convertParsedPlaylistIntoStreamInfo_( playlist, verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, codecs, - type, language, primary, name, channelsCount, closedCaptions, + type, languageValue, primary, name, channelsCount, closedCaptions, characteristics, forced, sampleRate, spatialAudio); if (abortSignal.aborted) { return; @@ -2250,7 +2250,7 @@ shaka.hls.HlsParser = class { * @param {string} absoluteMediaPlaylistUri * @param {string} codecs * @param {string} type - * @param {string} language + * @param {?string} languageValue * @param {boolean} primary * @param {?string} name * @param {?number} channelsCount @@ -2264,7 +2264,7 @@ shaka.hls.HlsParser = class { * @private */ async convertParsedPlaylistIntoStreamInfo_(playlist, verbatimMediaPlaylistUri, - absoluteMediaPlaylistUri, codecs, type, language, primary, name, + absoluteMediaPlaylistUri, codecs, type, languageValue, primary, name, channelsCount, closedCaptions, characteristics, forced, sampleRate, spatialAudio, mimeType = undefined) { if (playlist.type != shaka.hls.PlaylistType.MEDIA) { @@ -2335,9 +2335,9 @@ shaka.hls.HlsParser = class { const {nextMediaSequence, nextPart} = this.getNextMediaSequenceAndPart_(mediaSequenceNumber, segments); - const stream = this.makeStreamObject_(codecs, type, language, primary, name, - channelsCount, closedCaptions, characteristics, forced, sampleRate, - spatialAudio); + const stream = this.makeStreamObject_(codecs, type, languageValue, primary, + name, channelsCount, closedCaptions, characteristics, forced, + sampleRate, spatialAudio); stream.segmentIndex = segmentIndex; stream.encrypted = encrypted; stream.drmInfos = drmInfos; @@ -2414,7 +2414,7 @@ shaka.hls.HlsParser = class { * manually on the object after creation. * @param {string} codecs * @param {string} type - * @param {string} language + * @param {?string} languageValue * @param {boolean} primary * @param {?string} name * @param {?number} channelsCount @@ -2426,7 +2426,7 @@ shaka.hls.HlsParser = class { * @return {!shaka.extern.Stream} * @private */ - makeStreamObject_(codecs, type, language, primary, name, channelsCount, + makeStreamObject_(codecs, type, languageValue, primary, name, channelsCount, closedCaptions, characteristics, forced, sampleRate, spatialAudio) { // Fill out a "best-guess" mimeType, for now. It will be replaced once the // stream is lazy-loaded. @@ -2445,7 +2445,8 @@ shaka.hls.HlsParser = class { encrypted: false, drmInfos: [], keyIds: new Set(), - language, + language: this.getLanguage_(languageValue), + originalLanguage: languageValue, label: name, // For historical reasons, since before "originalId". type, primary, diff --git a/lib/mss/mss_parser.js b/lib/mss/mss_parser.js index 6b99a056b2..b643dbd773 100644 --- a/lib/mss/mss_parser.js +++ b/lib/mss/mss_parser.js @@ -17,6 +17,7 @@ goog.require('shaka.media.SegmentReference'); goog.require('shaka.mss.ContentProtection'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.util.Error'); +goog.require('shaka.util.LanguageUtils'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.Mp4Generator'); goog.require('shaka.util.OperationManager'); @@ -513,7 +514,8 @@ shaka.mss.MssParser = class { encrypted: drmInfos.length > 0, drmInfos: drmInfos, keyIds: new Set(), - language: lang || 'und', + language: shaka.util.LanguageUtils.normalize(lang || 'und'), + originalLanguage: lang, label: '', type: '', primary: false, diff --git a/lib/offline/indexeddb/v1_storage_cell.js b/lib/offline/indexeddb/v1_storage_cell.js index 3b17603617..f8893f5d40 100644 --- a/lib/offline/indexeddb/v1_storage_cell.js +++ b/lib/offline/indexeddb/v1_storage_cell.js @@ -159,6 +159,7 @@ shaka.offline.indexeddb.V1StorageCell = class hdr: undefined, kind: old.kind, language: old.language, + originalLanguage: old.language || null, label: old.label, width: old.width, height: old.height, diff --git a/lib/offline/indexeddb/v2_storage_cell.js b/lib/offline/indexeddb/v2_storage_cell.js index 33d9645911..47b201a649 100644 --- a/lib/offline/indexeddb/v2_storage_cell.js +++ b/lib/offline/indexeddb/v2_storage_cell.js @@ -108,6 +108,7 @@ shaka.offline.indexeddb.V2StorageCell = class hdr: undefined, kind: old.kind, language: old.language, + originalLanguage: old.language || null, label: old.label, width: old.width, height: old.height, diff --git a/lib/offline/manifest_converter.js b/lib/offline/manifest_converter.js index 3e56dab90e..dbce6e8e07 100644 --- a/lib/offline/manifest_converter.js +++ b/lib/offline/manifest_converter.js @@ -194,6 +194,7 @@ shaka.offline.ManifestConverter = class { drmInfos: [], keyIds: streamDB.keyIds, language: streamDB.language, + originalLanguage: streamDB.originalLanguage || null, label: streamDB.label, type: streamDB.type, primary: streamDB.primary, diff --git a/lib/offline/storage.js b/lib/offline/storage.js index 10c4346be5..4e489257db 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -1308,6 +1308,7 @@ shaka.offline.Storage = class { hdr: stream.hdr, kind: stream.kind, language: stream.language, + originalLanguage: stream.originalLanguage, label: stream.label, width: stream.width || null, height: stream.height || null, diff --git a/lib/player.js b/lib/player.js index d7ecc9f9e9..919c1fbbf3 100644 --- a/lib/player.js +++ b/lib/player.js @@ -2382,6 +2382,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { drmInfos: [], // Filled in by DrmEngine config. keyIds: new Set(), language: 'und', + originalLanguage: null, label: null, type: ContentType.VIDEO, primary: false, @@ -4873,6 +4874,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { drmInfos: [], keyIds: new Set(), language: language, + originalLanguage: language, label: label || null, type: ContentType.TEXT, primary: false, @@ -5024,6 +5026,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { drmInfos: [], keyIds: new Set(), language: 'und', + originalLanguage: null, label: null, type: ContentType.IMAGE, primary: false, @@ -5433,6 +5436,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // Add an empty segmentIndex, for the benefit of the period combiner // in our builtin DASH parser. const segmentIndex = new shaka.media.MetaSegmentIndex(); + const language = video.closedCaptions.get(id); const textStream = { id: this.nextExternalStreamId_++, // A globally unique ID. originalId: id, // The CC ID string, like 'CC1', 'CC3', etc. @@ -5444,7 +5448,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { encrypted: false, drmInfos: [], keyIds: new Set(), - language: video.closedCaptions.get(id), + language, + originalLanguage: language, label: null, type: ContentType.TEXT, primary: false, diff --git a/lib/util/periods.js b/lib/util/periods.js index 3a3e6939a0..ef976297b6 100644 --- a/lib/util/periods.js +++ b/lib/util/periods.js @@ -1500,6 +1500,7 @@ shaka.util.PeriodCombiner = class { mimeType: '', codecs: '', language: '', + originalLanguage: null, label: null, width: null, height: null, @@ -1538,6 +1539,7 @@ shaka.util.PeriodCombiner = class { drmInfos: [], keyIds: new Set(), language: '', + originalLanguage: null, label: null, type, primary: false, diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index 2aa0338863..9640c727e8 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -1149,6 +1149,7 @@ shaka.util.StreamUtils = class { originalTextId: null, originalImageId: null, accessibilityPurpose: null, + originalLanguage: null, }; if (video) { @@ -1172,6 +1173,7 @@ shaka.util.StreamUtils = class { track.label = audio.label; track.audioRoles = audio.roles; track.accessibilityPurpose = audio.accessibilityPurpose; + track.originalLanguage = audio.originalLanguage; } return track; @@ -1222,6 +1224,7 @@ shaka.util.StreamUtils = class { originalTextId: stream.originalId, originalImageId: null, accessibilityPurpose: stream.accessibilityPurpose, + originalLanguage: stream.originalLanguage, }; return track; @@ -1297,6 +1300,7 @@ shaka.util.StreamUtils = class { originalTextId: null, originalImageId: stream.originalId, accessibilityPurpose: null, + originalLanguage: null, }; return track; @@ -1418,6 +1422,7 @@ shaka.util.StreamUtils = class { originalTextId: null, originalImageId: null, accessibilityPurpose: null, + originalLanguage: html5Track.language, }; return track; diff --git a/test/dash/dash_parser_manifest_unit.js b/test/dash/dash_parser_manifest_unit.js index 9d635cf7d3..6fd0cfb2e8 100644 --- a/test/dash/dash_parser_manifest_unit.js +++ b/test/dash/dash_parser_manifest_unit.js @@ -140,7 +140,7 @@ describe('DashParser Manifest', () => { ' ', ' ', ' ', + ' lang="spa" label="spanish">', ' ', ' ', ' ', @@ -194,6 +194,7 @@ describe('DashParser Manifest', () => { }); manifest.addPartialTextStream((stream) => { stream.language = 'es'; + stream.originalLanguage = 'spa'; stream.label = 'spanish'; stream.primary = true; stream.mimeType = 'text/vtt'; diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index 20bb088400..2e95381ca6 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -191,6 +191,7 @@ describe('HlsParser', () => { }); variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.language = 'en'; + stream.originalLanguage = 'eng'; stream.channelsCount = 16; stream.audioSamplingRate = 48000; stream.spatialAudio = true; @@ -199,11 +200,13 @@ describe('HlsParser', () => { }); manifest.addPartialTextStream((stream) => { stream.language = 'en'; + stream.originalLanguage = 'eng'; stream.kind = TextStreamKind.SUBTITLE; stream.mime('text/vtt', ''); }); manifest.addPartialTextStream((stream) => { stream.language = 'es'; + stream.originalLanguage = 'es'; stream.kind = TextStreamKind.SUBTITLE; stream.mime('text/vtt', ''); }); @@ -269,6 +272,7 @@ describe('HlsParser', () => { }); variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.language = 'en'; + stream.originalLanguage = 'eng'; stream.channelsCount = 16; stream.spatialAudio = true; stream.mime('audio/mp4', 'mp4a'); @@ -276,11 +280,13 @@ describe('HlsParser', () => { }); manifest.addPartialTextStream((stream) => { stream.language = 'en'; + stream.originalLanguage = 'eng'; stream.kind = TextStreamKind.SUBTITLE; stream.mime('text/vtt', ''); }); manifest.addPartialTextStream((stream) => { stream.language = 'es'; + stream.originalLanguage = 'es'; stream.kind = TextStreamKind.SUBTITLE; stream.mime('text/vtt', ''); }); @@ -934,6 +940,7 @@ describe('HlsParser', () => { }); variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.language = 'en'; + stream.originalLanguage = 'eng'; }); }); manifest.addPartialVariant((variant) => { @@ -944,6 +951,7 @@ describe('HlsParser', () => { }); variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.language = 'fr'; + stream.originalLanguage = 'fr'; }); }); manifest.sequenceMode = sequenceMode; @@ -981,6 +989,7 @@ describe('HlsParser', () => { variant.addPartialStream(ContentType.VIDEO); variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.language = 'en'; + stream.originalLanguage = 'en'; }); }); manifest.addPartialVariant((variant) => { @@ -988,6 +997,7 @@ describe('HlsParser', () => { variant.addPartialStream(ContentType.VIDEO); variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.language = 'fr'; + stream.originalLanguage = 'fr'; }); }); manifest.sequenceMode = sequenceMode; @@ -1141,6 +1151,7 @@ describe('HlsParser', () => { variant.addPartialStream(ContentType.VIDEO); variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.language = 'en'; + stream.originalLanguage = 'en'; }); }); manifest.addPartialVariant((variant) => { @@ -1148,6 +1159,7 @@ describe('HlsParser', () => { variant.addPartialStream(ContentType.VIDEO); variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.language = 'en'; + stream.originalLanguage = 'en'; stream.roles = [ 'public.accessibility.describes-video', 'public.accessibility.describes-music-and-sound', @@ -1365,6 +1377,7 @@ describe('HlsParser', () => { }); manifest.addPartialTextStream((stream) => { stream.language = 'en'; + stream.originalLanguage = 'eng'; stream.forced = true; stream.kind = TextStreamKind.SUBTITLE; stream.mime('text/vtt', ''); @@ -1433,11 +1446,13 @@ describe('HlsParser', () => { }); manifest.addPartialTextStream((stream) => { stream.language = 'en'; + stream.originalLanguage = 'eng'; stream.kind = TextStreamKind.SUBTITLE; stream.mime('text/vtt', ''); }); manifest.addPartialTextStream((stream) => { stream.language = 'es'; + stream.originalLanguage = 'es'; stream.kind = TextStreamKind.SUBTITLE; stream.mime('text/vtt', ''); }); @@ -1566,6 +1581,7 @@ describe('HlsParser', () => { }); manifest.addPartialTextStream((stream) => { stream.language = 'en'; + stream.originalLanguage = 'eng'; stream.forced = true; stream.kind = TextStreamKind.SUBTITLE; stream.mime('text/vtt', ''); @@ -2321,6 +2337,7 @@ describe('HlsParser', () => { }); manifest.addPartialTextStream((stream) => { stream.language = 'en'; + stream.originalLanguage = 'eng'; stream.mime('application/mp4', 'stpp.ttml.im1t'); }); manifest.sequenceMode = sequenceMode; @@ -2971,6 +2988,7 @@ describe('HlsParser', () => { }); variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.language = 'en'; + stream.originalLanguage = 'eng'; }); }); manifest.addPartialVariant((variant) => { @@ -3917,6 +3935,7 @@ describe('HlsParser', () => { }); variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.language = 'en'; + stream.originalLanguage = 'en'; }); }); manifest.addPartialVariant((variant) => { @@ -3926,6 +3945,7 @@ describe('HlsParser', () => { }); variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.language = 'fr'; + stream.originalLanguage = 'fr'; }); }); manifest.addPartialVariant((variant) => { @@ -3935,6 +3955,7 @@ describe('HlsParser', () => { }); variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.language = 'en'; + stream.originalLanguage = 'en'; }); }); manifest.addPartialVariant((variant) => { @@ -3944,6 +3965,7 @@ describe('HlsParser', () => { }); variant.addPartialStream(ContentType.AUDIO, (stream) => { stream.language = 'fr'; + stream.originalLanguage = 'fr'; }); }); manifest.sequenceMode = sequenceMode; diff --git a/test/media/adaptation_set_unit.js b/test/media/adaptation_set_unit.js index 81f811d2fa..af2960ec0e 100644 --- a/test/media/adaptation_set_unit.js +++ b/test/media/adaptation_set_unit.js @@ -211,6 +211,7 @@ describe('AdaptationSet', () => { keyIds: new Set(), label: null, language: '', + originalLanguage: null, mimeType: mimeType, originalId: String(id), primary: false, diff --git a/test/offline/manifest_convert_unit.js b/test/offline/manifest_convert_unit.js index 17a1770c4a..dd25a53466 100644 --- a/test/offline/manifest_convert_unit.js +++ b/test/offline/manifest_convert_unit.js @@ -303,6 +303,7 @@ describe('ManifestConverter', () => { mimeType: '', codecs: '', language: '', + originalLanguage: null, label: null, width: null, height: null, @@ -362,6 +363,7 @@ describe('ManifestConverter', () => { hdr: undefined, kind: undefined, language: '', + originalLanguage: null, label: null, width: 250, height: 100, @@ -412,6 +414,7 @@ describe('ManifestConverter', () => { hdr: undefined, kind: undefined, language: 'en', + originalLanguage: 'en', label: null, width: null, height: null, @@ -461,6 +464,7 @@ describe('ManifestConverter', () => { hdr: undefined, kind: undefined, language: 'en', + originalLanguage: 'en', label: null, width: null, height: null, @@ -522,6 +526,7 @@ describe('ManifestConverter', () => { encrypted: streamDb.encrypted, keyIds: streamDb.keyIds, language: streamDb.language, + originalLanguage: streamDb.originalLanguage, label: streamDb.label, type: streamDb.type, primary: streamDb.primary, diff --git a/test/offline/storage_integration.js b/test/offline/storage_integration.js index db865d2cfe..f6de41348d 100644 --- a/test/offline/storage_integration.js +++ b/test/offline/storage_integration.js @@ -1374,6 +1374,7 @@ filterDescribe('Storage', storageSupport, () => { type: 'variant', bandwidth: bandwidth, language: language, + originalLanguage: language, label: null, kind: null, width: height * (16 / 9), @@ -1419,6 +1420,7 @@ filterDescribe('Storage', storageSupport, () => { type: 'text', bandwidth: 1000, language: language, + originalLanguage: language, label: null, kind: null, width: null, diff --git a/test/player_unit.js b/test/player_unit.js index 54c892aff9..45b942a4e0 100644 --- a/test/player_unit.js +++ b/test/player_unit.js @@ -1542,6 +1542,7 @@ describe('Player', () => { manifest.addTextStream(50, (stream) => { stream.originalId = 'text-es'; stream.language = 'es'; + stream.originalLanguage = 'es'; stream.label = 'Spanish'; stream.bandwidth = 10; stream.mimeType = 'text/vtt'; @@ -1550,6 +1551,7 @@ describe('Player', () => { manifest.addTextStream(51, (stream) => { stream.originalId = 'text-en'; stream.language = 'en'; + stream.originalLanguage = 'en'; stream.label = 'English'; stream.bandwidth = 10; stream.mimeType = 'application/ttml+xml'; @@ -1559,6 +1561,7 @@ describe('Player', () => { manifest.addTextStream(52, (stream) => { stream.originalId = 'text-commentary'; stream.language = 'en'; + stream.originalLanguage = 'en'; stream.label = 'English'; stream.bandwidth = 10; stream.mimeType = 'application/ttml+xml'; @@ -1584,6 +1587,7 @@ describe('Player', () => { type: 'variant', bandwidth: 1300, language: 'en', + originalLanguage: 'en', label: null, kind: null, width: 100, @@ -1621,6 +1625,7 @@ describe('Player', () => { type: 'variant', bandwidth: 2300, language: 'en', + originalLanguage: 'en', label: null, kind: null, width: 200, @@ -1658,6 +1663,7 @@ describe('Player', () => { type: 'variant', bandwidth: 1100, language: 'en', + originalLanguage: 'en', label: null, kind: null, width: 100, @@ -1695,6 +1701,7 @@ describe('Player', () => { type: 'variant', bandwidth: 2100, language: 'en', + originalLanguage: 'en', label: null, kind: null, width: 200, @@ -1732,6 +1739,7 @@ describe('Player', () => { type: 'variant', bandwidth: 1100, language: 'en', + originalLanguage: 'en', label: null, kind: null, width: 100, @@ -1769,6 +1777,7 @@ describe('Player', () => { type: 'variant', bandwidth: 2100, language: 'en', + originalLanguage: 'en', label: null, kind: null, width: 200, @@ -1807,6 +1816,7 @@ describe('Player', () => { bandwidth: 1100, language: 'es', label: 'es-label', + originalLanguage: 'es', kind: null, width: 100, height: 200, @@ -1844,6 +1854,7 @@ describe('Player', () => { bandwidth: 2100, language: 'es', label: 'es-label', + originalLanguage: 'es', kind: null, width: 200, height: 400, @@ -1882,6 +1893,7 @@ describe('Player', () => { active: true, type: ContentType.TEXT, language: 'es', + originalLanguage: 'es', label: 'Spanish', kind: 'caption', mimeType: 'text/vtt', @@ -1919,6 +1931,7 @@ describe('Player', () => { active: false, type: ContentType.TEXT, language: 'en', + originalLanguage: 'en', label: 'English', kind: 'caption', mimeType: 'application/ttml+xml', @@ -1956,6 +1969,7 @@ describe('Player', () => { active: false, type: ContentType.TEXT, language: 'en', + originalLanguage: 'en', label: 'English', kind: 'caption', mimeType: 'application/ttml+xml', @@ -1996,6 +2010,7 @@ describe('Player', () => { active: false, type: ContentType.IMAGE, language: '', + originalLanguage: null, label: null, kind: null, mimeType: 'image/jpeg', diff --git a/test/test/util/manifest_generator.js b/test/test/util/manifest_generator.js index ec750ef018..04238cdfcd 100644 --- a/test/test/util/manifest_generator.js +++ b/test/test/util/manifest_generator.js @@ -455,7 +455,7 @@ shaka.test.ManifestGenerator.Stream = class { * @param {boolean} isPartial * @param {?number} id * @param {shaka.util.ManifestParserUtils.ContentType} type - * @param {string=} lang + * @param {?string=} lang * @param {string=} label */ constructor(manifest, isPartial, id, type, lang, label) { @@ -526,7 +526,9 @@ shaka.test.ManifestGenerator.Stream = class { /** @type {!Set.} */ this.keyIds = new Set(); /** @type {string} */ - this.language = lang || 'und'; + this.language = shaka.util.LanguageUtils.normalize(lang || 'und'); + /** @type {?string} */ + this.originalLanguage = lang || null; /** @type {?string} */ this.label = label || null; /** @type {boolean} */ diff --git a/test/test/util/offline_utils.js b/test/test/util/offline_utils.js index 7ca655e848..8106864607 100644 --- a/test/test/util/offline_utils.js +++ b/test/test/util/offline_utils.js @@ -42,6 +42,7 @@ shaka.test.OfflineUtils = class { pixelAspectRatio: undefined, kind: undefined, language: '', + originalLanguage: null, label: null, width: null, height: null, diff --git a/test/test/util/streaming_engine_util.js b/test/test/util/streaming_engine_util.js index 9620c90523..6e9310a8b3 100644 --- a/test/test/util/streaming_engine_util.js +++ b/test/test/util/streaming_engine_util.js @@ -402,6 +402,7 @@ shaka.test.StreamingEngineUtil = class { type: ContentType.AUDIO, label: '', language: 'und', + originalLanguage: null, drmInfos: [], encrypted: false, keyIds: new Set(), @@ -441,6 +442,7 @@ shaka.test.StreamingEngineUtil = class { type: ContentType.VIDEO, label: '', language: 'und', + originalLanguage: null, drmInfos: [], encrypted: false, keyIds: new Set(), @@ -477,6 +479,7 @@ shaka.test.StreamingEngineUtil = class { type: ManifestParserUtils.ContentType.TEXT, label: '', language: 'und', + originalLanguage: null, drmInfos: [], encrypted: false, keyIds: new Set(), diff --git a/test/util/periods_unit.js b/test/util/periods_unit.js index 4fe27da73a..e3ab2e5c4c 100644 --- a/test/util/periods_unit.js +++ b/test/util/periods_unit.js @@ -347,6 +347,7 @@ describe('PeriodCombiner', () => { const spanish = variants.find( (v) => v.video.height == 1080 && v.language == 'es'); expect(spanish.audio.originalId).toBe('es*,en,es'); + expect(spanish.audio.originalLanguage).toBe('es'); expect(spanish.video.originalId).toBe('1080,480,1080'); // The French track is primary in the last period and has English 480p in @@ -354,6 +355,7 @@ describe('PeriodCombiner', () => { const french = variants.find( (v) => v.video.height == 1080 && v.language == 'fr'); expect(french.audio.originalId).toBe('fr,en,fr*'); + expect(french.audio.originalLanguage).toBe('fr'); expect(french.video.originalId).toBe('1080,480,1080'); // Because there's no English in the first or last periods, the English @@ -361,6 +363,7 @@ describe('PeriodCombiner', () => { const english = variants.find( (v) => v.video.height == 1080 && v.language == 'en'); expect(english.audio.originalId).toBe('es*,en,fr*'); + expect(english.audio.originalLanguage).toBe('en'); expect(english.video.originalId).toBe('1080,480,1080'); }); @@ -406,7 +409,9 @@ describe('PeriodCombiner', () => { const english = variants.find( (v) => v.video.height == 1080 && v.language == 'en'); expect(spanish.audio.originalId).toBe('es,en'); + expect(spanish.audio.originalLanguage).toBe('es'); expect(english.audio.originalId).toBe('es,en'); + expect(english.audio.originalLanguage).toBe('en'); }); it('Multiple representations of the same resolution', async () => { @@ -680,7 +685,7 @@ describe('PeriodCombiner', () => { ], textStreams: [ makeTextStream('en'), - makeTextStream('es'), + makeTextStream('spa'), ], imageStreams: [], }, @@ -703,7 +708,9 @@ describe('PeriodCombiner', () => { const spanish = textStreams.find((s) => s.language == 'es'); const english = textStreams.find((s) => s.language == 'en'); expect(spanish.originalId).toBe(',,es'); + expect(spanish.originalLanguage).toBe('spa'); expect(english.originalId).toBe('en,,en'); + expect(english.originalLanguage).toBe('en'); }); it('handles image track gaps', async () => { @@ -1547,7 +1554,8 @@ describe('PeriodCombiner', () => { language); streamGenerator.primary = primary; streamGenerator.channelsCount = channels; - streamGenerator.originalId = primary ? language + '*' : language; + streamGenerator.originalId = primary ? + streamGenerator.language + '*' : streamGenerator.language; if (channels != 2) { streamGenerator.originalId += `-${channels}c`; } @@ -1568,7 +1576,8 @@ describe('PeriodCombiner', () => { /* type= */ shaka.util.ManifestParserUtils.ContentType.TEXT, language); streamGenerator.primary = primary; - streamGenerator.originalId = primary ? language + '*' : language; + streamGenerator.originalId = primary ? + streamGenerator.language + '*' : streamGenerator.language; return streamGenerator.build_(); } @@ -1583,8 +1592,7 @@ describe('PeriodCombiner', () => { /* manifest= */ null, /* isPartial= */ false, /* id= */ nextId++, - /* type= */ shaka.util.ManifestParserUtils.ContentType.IMAGE, - /* lang= */ 'und'); + /* type= */ shaka.util.ManifestParserUtils.ContentType.IMAGE); streamGenerator.size(width, height); streamGenerator.originalId = height.toString(); streamGenerator.mime('image/jpeg');