From b32612064a66428d8114545abbbb93670a790f30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=CC=81lvaro=20Velad=20Galva=CC=81n?= Date: Wed, 25 Oct 2023 12:04:19 +0200 Subject: [PATCH 1/4] feat(HLS): Add support for REQ-VIDEO-LAYOUT Also add preferredVideoLayout config --- demo/config.js | 13 ++ externs/shaka/manifest.js | 4 + externs/shaka/offline.js | 3 + externs/shaka/player.js | 10 ++ lib/dash/dash_parser.js | 1 + lib/hls/hls_parser.js | 21 ++- lib/media/adaptation_set_criteria.js | 37 +++- lib/mss/mss_parser.js | 1 + 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 | 18 +- lib/util/player_configuration.js | 1 + lib/util/stream_utils.js | 5 + test/hls/hls_parser_unit.js | 66 +++++++ test/media/adaptation_set_criteria_unit.js | 192 ++++++++++++++++++--- test/offline/manifest_convert_unit.js | 4 + test/offline/storage_integration.js | 2 + test/player_unit.js | 12 ++ test/test/util/manifest_generator.js | 2 + 21 files changed, 366 insertions(+), 30 deletions(-) diff --git a/demo/config.js b/demo/config.js index 1aa0248b01..25b0bb20e6 100644 --- a/demo/config.js +++ b/demo/config.js @@ -438,6 +438,19 @@ shakaDemo.Config = class { this.addSelectInput_('Preferred HDR Level', 'preferredVideoHdrLevel', hdrLevels, hdrLevelNames); + const videoLayouts = { + '': '', + 'CH-STEREO': 'CH-STEREO', + 'CH-MONO': 'CH-MONO', + }; + const videoLayoutsNames = { + 'CH-STEREO': 'Stereoscopic', + 'CH-MONO': 'Monoscopic', + '': 'No Preference', + }; + this.addSelectInput_('Preferred video layout', 'preferredVideoLayout', + videoLayouts, videoLayoutsNames); + this.addBoolInput_('Start At Segment Boundary', 'streaming.startAtSegmentBoundary') .addBoolInput_('Ignore Text Stream Failures', diff --git a/externs/shaka/manifest.js b/externs/shaka/manifest.js index 6987e76f61..d004d1f239 100644 --- a/externs/shaka/manifest.js +++ b/externs/shaka/manifest.js @@ -364,6 +364,7 @@ shaka.extern.FetchCryptoKeysFunction; * frameRate: (number|undefined), * pixelAspectRatio: (string|undefined), * hdr: (string|undefined), + * videoLayout: (string|undefined), * bandwidth: (number|undefined), * width: (number|undefined), * height: (number|undefined), @@ -434,6 +435,9 @@ shaka.extern.FetchCryptoKeysFunction; * @property {(string|undefined)} hdr * Video streams only.
* The Stream's HDR info + * @property {(string|undefined)} videoLayout + * Video streams only.
+ * The Stream's video layout info. * @property {(number|undefined)} bandwidth * Audio and video streams only.
* The stream's required bandwidth in bits per second. diff --git a/externs/shaka/offline.js b/externs/shaka/offline.js index 1f47b746d7..e937141c4c 100644 --- a/externs/shaka/offline.js +++ b/externs/shaka/offline.js @@ -122,6 +122,7 @@ shaka.extern.ManifestDB; * frameRate: (number|undefined), * pixelAspectRatio: (string|undefined), * hdr: (string|undefined), + * videoLayout: (string|undefined), * kind: (string|undefined), * language: string, * originalLanguage: (?string|undefined), @@ -164,6 +165,8 @@ shaka.extern.ManifestDB; * The Stream's pixel aspect ratio * @property {(string|undefined)} hdr * The Stream's HDR info + * @property {(string|undefined)} videoLayout + * The Stream's video layout info. * @property {(string|undefined)} kind * The kind of text stream; undefined for audio/video. * @property {string} language diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 957ef53ab8..d3c8e0eeef 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -216,6 +216,7 @@ shaka.extern.BufferedInfo; * frameRate: ?number, * pixelAspectRatio: ?string, * hdr: ?string, + * videoLayout: ?string, * mimeType: ?string, * audioMimeType: ?string, * videoMimeType: ?string, @@ -280,6 +281,8 @@ shaka.extern.BufferedInfo; * The video pixel aspect ratio provided in the manifest, if present. * @property {?string} hdr * The video HDR provided in the manifest, if present. + * @property {?string} videoLayout + * The video layout provided in the manifest, if present. * @property {?string} mimeType * The MIME type of the content provided in the manifest. * @property {?string} audioMimeType @@ -1537,6 +1540,7 @@ shaka.extern.OfflineConfiguration; * preferredAudioCodecs: !Array., * preferredAudioChannelCount: number, * preferredVideoHdrLevel: string, + * preferredVideoLayout: string, * preferredDecodingAttributes: !Array., * preferForcedSubs: boolean, * restrictions: shaka.extern.Restrictions, @@ -1597,6 +1601,12 @@ shaka.extern.OfflineConfiguration; * Defaults to 'AUTO'. * Note that one some platforms, such as Chrome, attempting to play PQ content * may cause problems. + * @property {string} preferredVideoLayout + * The preferred video layout of the video. + * Can be 'CH-STEREO', 'CH-MONO', or '' for no preference. + * If the content is predominantly stereoscopic you should use 'CH-STEREO'. + * If the content is predominantly monoscopic you should use 'CH-MONO'. + * Defaults to ''. * @property {!Array.} preferredDecodingAttributes * The list of preferred attributes of decodingInfo, in the order of their * priorities. diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 91df111f51..93ce23d44f 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -1470,6 +1470,7 @@ shaka.dash.DashParser = class { spatialAudio, closedCaptions, hdr, + videoLayout: undefined, tilesLayout, matchedStreams: [], accessibilityPurpose, diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 3cbd6f1f0e..3957487ce7 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -720,7 +720,8 @@ shaka.hls.HlsParser = class { if (type == 'video') { this.addVideoAttributes_(streamInfo.stream, width, height, - /* frameRate= */ null, /* videoRange= */ null); + /* frameRate= */ null, /* videoRange= */ null, + /* videoLayout= */ null); } // Wrap the stream from that stream info with a variant. @@ -1219,6 +1220,12 @@ shaka.hls.HlsParser = class { const [width, height] = resolution ? resolution.split('x') : [null, null]; const videoRange = tag.getAttributeValue('VIDEO-RANGE'); + let videoLayout = tag.getAttributeValue('REQ-VIDEO-LAYOUT'); + if (videoLayout == 'CH-STEREO,CH-MONO') { + videoLayout = 'CH-STEREO'; + } else if (videoLayout == 'CH-MONO,CH-STEREO') { + videoLayout = 'CH-MONO'; + } const streamInfos = this.createStreamInfosForVariantTag_(tag, resolution, frameRate); @@ -1234,6 +1241,7 @@ shaka.hls.HlsParser = class { height, frameRate, videoRange, + videoLayout, drmInfos, keyIds); }); @@ -1510,6 +1518,7 @@ shaka.hls.HlsParser = class { * @param {?string} height * @param {?string} frameRate * @param {?string} videoRange + * @param {?string} videoLayout * @param {!Array.} drmInfos * @param {!Set.} keyIds * @return {!Array.} @@ -1517,13 +1526,13 @@ shaka.hls.HlsParser = class { */ createVariants_( audioInfos, videoInfos, bandwidth, width, height, frameRate, videoRange, - drmInfos, keyIds) { + videoLayout, drmInfos, keyIds) { const ContentType = shaka.util.ManifestParserUtils.ContentType; const DrmEngine = shaka.media.DrmEngine; for (const info of videoInfos) { this.addVideoAttributes_( - info.stream, width, height, frameRate, videoRange); + info.stream, width, height, frameRate, videoRange, videoLayout); } // In case of audio-only or video-only content or the audio/video is @@ -2376,6 +2385,7 @@ shaka.hls.HlsParser = class { spatialAudio, closedCaptions, hdr: undefined, + videoLayout: undefined, tilesLayout: undefined, accessibilityPurpose: null, external: false, @@ -3464,14 +3474,17 @@ shaka.hls.HlsParser = class { * @param {?string} height * @param {?string} frameRate * @param {?string} videoRange + * @param {?string} videoLayout * @private */ - addVideoAttributes_(stream, width, height, frameRate, videoRange) { + addVideoAttributes_(stream, width, height, frameRate, videoRange, + videoLayout) { if (stream) { stream.width = Number(width) || undefined; stream.height = Number(height) || undefined; stream.frameRate = Number(frameRate) || undefined; stream.hdr = videoRange || undefined; + stream.videoLayout = videoLayout || undefined; } } diff --git a/lib/media/adaptation_set_criteria.js b/lib/media/adaptation_set_criteria.js index 483e5ea866..47c07f33ae 100644 --- a/lib/media/adaptation_set_criteria.js +++ b/lib/media/adaptation_set_criteria.js @@ -61,13 +61,14 @@ shaka.media.ExampleBasedCriteria = class { const role = ''; const label = ''; const hdrLevel = ''; + const videoLayout = ''; const channelCount = example.audio && example.audio.channelsCount ? example.audio.channelsCount : 0; /** @private {!shaka.media.AdaptationSetCriteria} */ this.fallback_ = new shaka.media.PreferenceBasedCriteria( - example.language, role, channelCount, hdrLevel, label, + example.language, role, channelCount, hdrLevel, videoLayout, label, codecSwitchingStrategy, enableAudioGroups); } @@ -106,11 +107,12 @@ shaka.media.PreferenceBasedCriteria = class { * @param {string} role * @param {number} channelCount * @param {string} hdrLevel + * @param {string} videoLayout * @param {string=} label * @param {shaka.config.CodecSwitchingStrategy=} codecSwitchingStrategy * @param {boolean=} enableAudioGroups */ - constructor(language, role, channelCount, hdrLevel, label = '', + constructor(language, role, channelCount, hdrLevel, videoLayout, label = '', codecSwitchingStrategy = shaka.config.CodecSwitchingStrategy.RELOAD, enableAudioGroups = false) { /** @private {string} */ @@ -122,6 +124,8 @@ shaka.media.PreferenceBasedCriteria = class { /** @private {string} */ this.hdrLevel_ = hdrLevel; /** @private {string} */ + this.videoLayout_ = videoLayout; + /** @private {string} */ this.label_ = label; /** @private {shaka.config.CodecSwitchingStrategy} */ this.codecSwitchingStrategy_ = codecSwitchingStrategy; @@ -156,6 +160,17 @@ shaka.media.PreferenceBasedCriteria = class { shaka.log.warning('No exact match for variant role could be found.'); } + if (this.videoLayout_) { + const byVideoLayout = Class.filterVariantsByVideoLayout_( + current, this.videoLayout_); + if (byVideoLayout.length) { + current = byVideoLayout; + } else { + shaka.log.warning( + 'No exact match for the video layout could be found.'); + } + } + if (this.hdrLevel_) { const byHdrLevel = Class.filterVariantsByHDRLevel_( current, this.hdrLevel_); @@ -289,4 +304,22 @@ shaka.media.PreferenceBasedCriteria = class { return true; }); } + + + /** + * Filters variants according to the given video layout config. + * + * @param {!Array.} variants + * @param {string} videoLayout + * @private + */ + static filterVariantsByVideoLayout_(variants, videoLayout) { + return variants.filter((variant) => { + if (variant.video && variant.video.videoLayout && + variant.video.videoLayout != videoLayout) { + return false; + } + return true; + }); + } }; diff --git a/lib/mss/mss_parser.js b/lib/mss/mss_parser.js index 1dba33acd0..c52c1a690f 100644 --- a/lib/mss/mss_parser.js +++ b/lib/mss/mss_parser.js @@ -537,6 +537,7 @@ shaka.mss.MssParser = class { spatialAudio: false, closedCaptions: null, hdr: undefined, + videoLayout: undefined, tilesLayout: undefined, matchedStreams: [], mssPrivateData: { diff --git a/lib/offline/indexeddb/v1_storage_cell.js b/lib/offline/indexeddb/v1_storage_cell.js index e809812c7e..15f63ba808 100644 --- a/lib/offline/indexeddb/v1_storage_cell.js +++ b/lib/offline/indexeddb/v1_storage_cell.js @@ -158,6 +158,7 @@ shaka.offline.indexeddb.V1StorageCell = class frameRate: old.frameRate, pixelAspectRatio: undefined, hdr: undefined, + videoLayout: undefined, kind: old.kind, language: old.language, originalLanguage: old.language || null, diff --git a/lib/offline/indexeddb/v2_storage_cell.js b/lib/offline/indexeddb/v2_storage_cell.js index 7e0b4c78c7..0883ccfc1e 100644 --- a/lib/offline/indexeddb/v2_storage_cell.js +++ b/lib/offline/indexeddb/v2_storage_cell.js @@ -107,6 +107,7 @@ shaka.offline.indexeddb.V2StorageCell = class frameRate: old.frameRate, pixelAspectRatio: old.pixelAspectRatio, hdr: undefined, + videoLayout: undefined, kind: old.kind, language: old.language, originalLanguage: old.language || null, diff --git a/lib/offline/manifest_converter.js b/lib/offline/manifest_converter.js index 92b8d091d9..6eb9e3497b 100644 --- a/lib/offline/manifest_converter.js +++ b/lib/offline/manifest_converter.js @@ -190,6 +190,7 @@ shaka.offline.ManifestConverter = class { frameRate: streamDB.frameRate, pixelAspectRatio: streamDB.pixelAspectRatio, hdr: streamDB.hdr, + videoLayout: streamDB.videoLayout, kind: streamDB.kind, encrypted: streamDB.encrypted, drmInfos: [], diff --git a/lib/offline/storage.js b/lib/offline/storage.js index 6740363608..126be2d33d 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -1309,6 +1309,7 @@ shaka.offline.Storage = class { frameRate: stream.frameRate, pixelAspectRatio: stream.pixelAspectRatio, hdr: stream.hdr, + videoLayout: stream.videoLayout, kind: stream.kind, language: stream.language, originalLanguage: stream.originalLanguage, diff --git a/lib/player.js b/lib/player.js index f50776d56a..d61f307327 100644 --- a/lib/player.js +++ b/lib/player.js @@ -619,6 +619,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.config_.preferredVariantRole, this.config_.preferredAudioChannelCount, this.config_.preferredVideoHdrLevel, + this.config_.preferredVideoLayout, this.config_.preferredAudioLabel, this.config_.mediaSource.codecSwitchingStrategy, this.config_.manifest.dash.enableAudioGroups); @@ -2152,6 +2153,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.config_.preferredVariantRole, this.config_.preferredAudioChannelCount, this.config_.preferredVideoHdrLevel, + this.config_.preferredVideoLayout, this.config_.preferredAudioLabel, this.config_.mediaSource.codecSwitchingStrategy, this.config_.manifest.dash.enableAudioGroups); @@ -4311,8 +4313,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget { selectAudioLanguage(language, role, channelsCount = 0, safeMargin = 0) { if (this.manifest_ && this.playhead_) { this.currentAdaptationSetCriteria_ = - new shaka.media.PreferenceBasedCriteria(language, role || '', - channelsCount, /* hdrLevel= */ '', /* label= */ '', + new shaka.media.PreferenceBasedCriteria( + language, + role || '', + channelsCount, + /* hdrLevel= */ '', + /* videoLayout= */ '', + /* label= */ '', this.config_.mediaSource.codecSwitchingStrategy, this.config_.manifest.dash.enableAudioGroups); @@ -4431,7 +4438,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // label have the same language. this.currentAdaptationSetCriteria_ = new shaka.media.PreferenceBasedCriteria( - firstVariantWithLabel.language, '', 0, '', label, + firstVariantWithLabel.language, + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ '', + label, this.config_.mediaSource.codecSwitchingStrategy, this.config_.manifest.dash.enableAudioGroups); diff --git a/lib/util/player_configuration.js b/lib/util/player_configuration.js index 41698c14a7..1eced1737e 100644 --- a/lib/util/player_configuration.js +++ b/lib/util/player_configuration.js @@ -345,6 +345,7 @@ shaka.util.PlayerConfiguration = class { preferredTextRole: '', preferredAudioChannelCount: 2, preferredVideoHdrLevel: 'AUTO', + preferredVideoLayout: '', preferredVideoCodecs: [], preferredAudioCodecs: [], preferForcedSubs: false, diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index 8e1c31d07b..7381193001 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -1091,6 +1091,7 @@ shaka.util.StreamUtils = class { frameRate: null, pixelAspectRatio: null, hdr: null, + videoLayout: null, mimeType: mimeType, audioMimeType: audioMimeType, videoMimeType: videoMimeType, @@ -1126,6 +1127,7 @@ shaka.util.StreamUtils = class { track.pixelAspectRatio = video.pixelAspectRatio || null; track.videoBandwidth = video.bandwidth || null; track.hdr = video.hdr || null; + track.videoLayout = video.videoLayout || null; } if (audio) { @@ -1166,6 +1168,7 @@ shaka.util.StreamUtils = class { frameRate: null, pixelAspectRatio: null, hdr: null, + videoLayout: null, mimeType: stream.mimeType, audioMimeType: null, videoMimeType: null, @@ -1242,6 +1245,7 @@ shaka.util.StreamUtils = class { frameRate: null, pixelAspectRatio: null, hdr: null, + videoLayout: null, mimeType: stream.mimeType, audioMimeType: null, videoMimeType: null, @@ -1369,6 +1373,7 @@ shaka.util.StreamUtils = class { frameRate: null, pixelAspectRatio: null, hdr: null, + videoLayout: null, mimeType: null, audioMimeType: null, videoMimeType: null, diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index b4a6813060..1f3d8b925f 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -1488,6 +1488,72 @@ describe('HlsParser', () => { expect(actual).toEqual(manifest); }); + it('parses manifest with video layout metadata', async () => { + const master = [ + '#EXTM3U\n', + '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",', + 'URI="audio"\n', + '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="eng",', + 'FORCED=YES,URI="text"\n', + '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",', + 'REQ-VIDEO-LAYOUT=CH-STEREO,RESOLUTION=960x540,FRAME-RATE=60,', + 'AUDIO="aud1",SUBTITLES="sub1"\n', + 'video\n', + ].join(''); + + const media = [ + '#EXTM3U\n', + '#EXT-X-PLAYLIST-TYPE:VOD\n', + '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', + '#EXTINF:5,\n', + '#EXT-X-BYTERANGE:121090@616\n', + 'main.mp4', + ].join(''); + + const textMedia = [ + '#EXTM3U\n', + '#EXT-X-PLAYLIST-TYPE:VOD\n', + '#EXTINF:5,\n', + '#EXT-X-BYTERANGE:121090@616\n', + 'main.vtt', + ].join(''); + + const manifest = shaka.test.ManifestGenerator.generate((manifest) => { + manifest.anyTimeline(); + manifest.addPartialVariant((variant) => { + variant.addPartialStream(ContentType.VIDEO, (stream) => { + stream.mime('video/mp4', 'avc1'); + stream.videoLayout = 'CH-STEREO'; + }); + variant.addPartialStream(ContentType.AUDIO, (stream) => { + stream.mime('audio/mp4', 'mp4a'); + }); + }); + manifest.addPartialTextStream((stream) => { + stream.language = 'en'; + stream.originalLanguage = 'eng'; + stream.forced = true; + stream.kind = TextStreamKind.SUBTITLE; + stream.mime('text/vtt', ''); + }); + manifest.sequenceMode = sequenceMode; + manifest.type = shaka.media.ManifestParser.HLS; + }); + + fakeNetEngine + .setResponseText('test:/master', master) + .setResponseText('test:/audio', media) + .setResponseText('test:/video', media) + .setResponseText('test:/text', textMedia) + .setResponseText('test:/main.vtt', vttText) + .setResponseValue('test:/init.mp4', initSegmentData) + .setResponseValue('test:/main.mp4', segmentData); + + const actual = await parser.start('test:/master', playerInterface); + await loadAllStreamsFor(actual); + expect(actual).toEqual(manifest); + }); + it('parses manifest with SUBTITLES', async () => { const master = [ '#EXTM3U\n', diff --git a/test/media/adaptation_set_criteria_unit.js b/test/media/adaptation_set_criteria_unit.js index 8be0df3cf8..a8a69fccad 100644 --- a/test/media/adaptation_set_criteria_unit.js +++ b/test/media/adaptation_set_criteria_unit.js @@ -19,7 +19,12 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('en', '', 0, ''); + const builder = new shaka.media.PreferenceBasedCriteria( + /* language= */ 'en', + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ ''); const set = builder.create(manifest.variants); checkSet(set, [ @@ -40,7 +45,12 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('en', '', 0, ''); + const builder = new shaka.media.PreferenceBasedCriteria( + /* language= */ 'en', + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ ''); const set = builder.create(manifest.variants); checkSet(set, [ @@ -86,8 +96,14 @@ describe('AdaptationSetCriteria', () => { return true; }; - const builder = new shaka.media.PreferenceBasedCriteria('en', '', 0, '', - undefined, shaka.config.CodecSwitchingStrategy.SMOOTH); + const builder = new shaka.media.PreferenceBasedCriteria( + /* language= */ 'en', + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ '', + /* label= */ '', + shaka.config.CodecSwitchingStrategy.SMOOTH); const set = builder.create(manifest.variants); expect(Array.from(set.values()).length).toBe(3); @@ -126,8 +142,14 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('en', '', 0, '', - undefined, shaka.config.CodecSwitchingStrategy.RELOAD); + const builder = new shaka.media.PreferenceBasedCriteria( + /* language= */ 'en', + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ '', + /* label= */ '', + shaka.config.CodecSwitchingStrategy.RELOAD); const set = builder.create(manifest.variants); expect(Array.from(set.values()).length).toBe(1); @@ -156,7 +178,11 @@ describe('AdaptationSetCriteria', () => { }); const builder = new shaka.media.PreferenceBasedCriteria( - 'en', 'main', 0, ''); + /* language= */ 'en', + /* role= */ 'main', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ ''); const set = builder.create(manifest.variants); checkSet(set, [ @@ -205,7 +231,12 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('en', '', 0, ''); + const builder = new shaka.media.PreferenceBasedCriteria( + /* language= */ 'en', + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ ''); const set = builder.create(manifest.variants); // Which role is chosen is an implementation detail. @@ -263,7 +294,12 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('zh', '', 0, ''); + const builder = new shaka.media.PreferenceBasedCriteria( + /* language= */ 'zh', + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ ''); const set = builder.create(manifest.variants); // Which role is chosen is an implementation detail. @@ -299,7 +335,12 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('zh', '', 0, ''); + const builder = new shaka.media.PreferenceBasedCriteria( + /* language= */ 'zh', + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ ''); const set = builder.create(manifest.variants); // Which language is chosen is an implementation detail. @@ -356,7 +397,11 @@ describe('AdaptationSetCriteria', () => { }); const builder = new shaka.media.PreferenceBasedCriteria( - 'zh', '', 0, ''); + /* language= */ 'zh', + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ ''); const set = builder.create(manifest.variants); // Which role is chosen is an implementation detail. Each role is @@ -414,7 +459,11 @@ describe('AdaptationSetCriteria', () => { }); const builder = new shaka.media.PreferenceBasedCriteria( - 'zh', '', 0, ''); + /* language= */ 'zh', + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ ''); const set = builder.create(manifest.variants); checkSet(set, [ @@ -442,7 +491,45 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('', '', 0, 'PQ'); + const builder = new shaka.media.PreferenceBasedCriteria( + /* language= */ '', + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ 'PQ', + /* videoLayout= */ ''); + const set = builder.create(manifest.variants); + + checkSet(set, [ + manifest.variants[0], + manifest.variants[2], + ]); + }); + + it('chooses variants with preferred video layout (CH-STEREO)', () => { + const manifest = shaka.test.ManifestGenerator.generate((manifest) => { + manifest.addVariant(1, (variant) => { + variant.addVideo(10, (stream) => { + stream.videoLayout = 'CH-STEREO'; + }); + }); + manifest.addVariant(2, (variant) => { + variant.addVideo(20, (stream) => { + stream.videoLayout = 'CH-MONO'; + }); + }); + manifest.addVariant(3, (variant) => { + variant.addVideo(30, (stream) => { + stream.videoLayout = ''; + }); + }); + }); + + const builder = new shaka.media.PreferenceBasedCriteria( + /* language= */ '', + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ 'CH-STEREO'); const set = builder.create(manifest.variants); checkSet(set, [ @@ -451,6 +538,38 @@ describe('AdaptationSetCriteria', () => { ]); }); + it('chooses variants with preferred video layout (CH-MONO)', () => { + const manifest = shaka.test.ManifestGenerator.generate((manifest) => { + manifest.addVariant(1, (variant) => { + variant.addVideo(10, (stream) => { + stream.videoLayout = 'CH-STEREO'; + }); + }); + manifest.addVariant(2, (variant) => { + variant.addVideo(20, (stream) => { + stream.videoLayout = 'CH-MONO'; + }); + }); + manifest.addVariant(3, (variant) => { + variant.addVideo(30, (stream) => { + stream.videoLayout = 'CH-STEREO'; + }); + }); + }); + + const builder = new shaka.media.PreferenceBasedCriteria( + /* language= */ '', + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ 'CH-MONO'); + const set = builder.create(manifest.variants); + + checkSet(set, [ + manifest.variants[1], + ]); + }); + it('chooses variants with preferred audio channels count', () => { const manifest = shaka.test.ManifestGenerator.generate((manifest) => { manifest.addVariant(1, (variant) => { @@ -470,7 +589,12 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('', '', 2, ''); + const builder = new shaka.media.PreferenceBasedCriteria( + /* language= */ '', + /* role= */ '', + /* channelCount= */ 2, + /* hdrLevel= */ '', + /* videoLayout= */ ''); const set = builder.create(manifest.variants); checkSet(set, [ @@ -499,8 +623,12 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = - new shaka.media.PreferenceBasedCriteria('', '', 6, ''); + const builder = new shaka.media.PreferenceBasedCriteria( + /* language= */ '', + /* role= */ '', + /* channelCount= */ 6, + /* hdrLevel= */ '', + /* videoLayout= */ ''); const set = builder.create(manifest.variants); checkSet(set, [ @@ -529,7 +657,12 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('', '', 2, ''); + const builder = new shaka.media.PreferenceBasedCriteria( + /* language= */ '', + /* role= */ '', + /* channelCount= */ 2, + /* hdrLevel= */ '', + /* videoLayout= */ ''); const set = builder.create(manifest.variants); checkSet(set, [ @@ -558,7 +691,12 @@ describe('AdaptationSetCriteria', () => { }); const builder = new shaka.media.PreferenceBasedCriteria( - '', '', 0, '', 'preferredLabel'); + /* language= */ '', + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ '', + /* label= */ 'preferredLabel'); const set = builder.create(manifest.variants); checkSet(set, [ @@ -600,7 +738,12 @@ describe('AdaptationSetCriteria', () => { }); const builder = new shaka.media.PreferenceBasedCriteria( - 'zh', '', 0, '', 'preferredLabel'); + /* language= */ 'zh', + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ '', + /* label= */ 'preferredLabel'); const set = builder.create(manifest.variants); checkSet(set, [ @@ -634,8 +777,15 @@ describe('AdaptationSetCriteria', () => { }); }); - const builder = new shaka.media.PreferenceBasedCriteria('en', '', 0, '', - '', shaka.config.CodecSwitchingStrategy.RELOAD, true); + const builder = new shaka.media.PreferenceBasedCriteria( + /* language= */ 'en', + /* role= */ '', + /* channelCount= */ 0, + /* hdrLevel= */ '', + /* videoLayout= */ '', + /* label= */ '', + shaka.config.CodecSwitchingStrategy.RELOAD, + /* enableAudioGroups= */ true); const set = builder.create(manifest.variants); checkSet(set, [ diff --git a/test/offline/manifest_convert_unit.js b/test/offline/manifest_convert_unit.js index aed542218a..f6a47b1926 100644 --- a/test/offline/manifest_convert_unit.js +++ b/test/offline/manifest_convert_unit.js @@ -364,6 +364,7 @@ describe('ManifestConverter', () => { frameRate: 22, pixelAspectRatio: '59:54', hdr: undefined, + videoLayout: undefined, kind: undefined, language: '', originalLanguage: null, @@ -417,6 +418,7 @@ describe('ManifestConverter', () => { frameRate: undefined, pixelAspectRatio: undefined, hdr: undefined, + videoLayout: undefined, kind: undefined, language: 'en', originalLanguage: 'en', @@ -469,6 +471,7 @@ describe('ManifestConverter', () => { frameRate: undefined, pixelAspectRatio: undefined, hdr: undefined, + videoLayout: undefined, kind: undefined, language: 'en', originalLanguage: 'en', @@ -528,6 +531,7 @@ describe('ManifestConverter', () => { frameRate: streamDb.frameRate, pixelAspectRatio: streamDb.pixelAspectRatio, hdr: streamDb.hdr, + videoLayout: streamDb.videoLayout, width: streamDb.width || undefined, height: streamDb.height || undefined, kind: streamDb.kind, diff --git a/test/offline/storage_integration.js b/test/offline/storage_integration.js index 2215b12c6a..efa9c90c8d 100644 --- a/test/offline/storage_integration.js +++ b/test/offline/storage_integration.js @@ -1382,6 +1382,7 @@ filterDescribe('Storage', storageSupport, () => { frameRate: 30, pixelAspectRatio: '59:54', hdr: null, + videoLayout: null, mimeType: 'video/mp4,audio/mp4', audioMimeType: 'audio/mp4', videoMimeType: 'video/mp4', @@ -1428,6 +1429,7 @@ filterDescribe('Storage', storageSupport, () => { frameRate: null, pixelAspectRatio: null, hdr: null, + videoLayout: null, mimeType: 'text/vtt', audioMimeType: null, videoMimeType: null, diff --git a/test/player_unit.js b/test/player_unit.js index 8279f4b94a..2ce865822d 100644 --- a/test/player_unit.js +++ b/test/player_unit.js @@ -1503,6 +1503,7 @@ describe('Player', () => { frameRate: 1000000 / 42000, pixelAspectRatio: '59:54', hdr: null, + videoLayout: null, mimeType: 'video/mp4', audioMimeType: 'audio/mp4', videoMimeType: 'video/mp4', @@ -1541,6 +1542,7 @@ describe('Player', () => { frameRate: 24, pixelAspectRatio: '59:54', hdr: null, + videoLayout: null, mimeType: 'video/mp4', audioMimeType: 'audio/mp4', videoMimeType: 'video/mp4', @@ -1579,6 +1581,7 @@ describe('Player', () => { frameRate: 1000000 / 42000, pixelAspectRatio: '59:54', hdr: null, + videoLayout: null, mimeType: 'video/mp4', audioMimeType: 'audio/mp4', videoMimeType: 'video/mp4', @@ -1617,6 +1620,7 @@ describe('Player', () => { frameRate: 24, pixelAspectRatio: '59:54', hdr: null, + videoLayout: null, mimeType: 'video/mp4', audioMimeType: 'audio/mp4', videoMimeType: 'video/mp4', @@ -1655,6 +1659,7 @@ describe('Player', () => { frameRate: 1000000 / 42000, pixelAspectRatio: '59:54', hdr: null, + videoLayout: null, mimeType: 'video/mp4', audioMimeType: 'audio/mp4', videoMimeType: 'video/mp4', @@ -1693,6 +1698,7 @@ describe('Player', () => { frameRate: 24, pixelAspectRatio: '59:54', hdr: null, + videoLayout: null, mimeType: 'video/mp4', audioMimeType: 'audio/mp4', videoMimeType: 'video/mp4', @@ -1731,6 +1737,7 @@ describe('Player', () => { frameRate: 1000000 / 42000, pixelAspectRatio: '59:54', hdr: null, + videoLayout: null, mimeType: 'video/mp4', audioMimeType: 'audio/mp4', videoMimeType: 'video/mp4', @@ -1769,6 +1776,7 @@ describe('Player', () => { frameRate: 24, pixelAspectRatio: '59:54', hdr: null, + videoLayout: null, mimeType: 'video/mp4', audioMimeType: 'audio/mp4', videoMimeType: 'video/mp4', @@ -1826,6 +1834,7 @@ describe('Player', () => { frameRate: null, pixelAspectRatio: null, hdr: null, + videoLayout: null, videoId: null, audioId: null, originalAudioId: null, @@ -1864,6 +1873,7 @@ describe('Player', () => { frameRate: null, pixelAspectRatio: null, hdr: null, + videoLayout: null, videoId: null, audioId: null, originalAudioId: null, @@ -1902,6 +1912,7 @@ describe('Player', () => { frameRate: null, pixelAspectRatio: null, hdr: null, + videoLayout: null, videoId: null, audioId: null, originalAudioId: null, @@ -1943,6 +1954,7 @@ describe('Player', () => { frameRate: null, pixelAspectRatio: null, hdr: null, + videoLayout: null, videoId: null, audioId: null, originalAudioId: null, diff --git a/test/test/util/manifest_generator.js b/test/test/util/manifest_generator.js index 1a6167cc8e..810c87b22f 100644 --- a/test/test/util/manifest_generator.js +++ b/test/test/util/manifest_generator.js @@ -556,6 +556,8 @@ shaka.test.ManifestGenerator.Stream = class { /** @type {(string|undefined)} */ this.hdr = undefined; /** @type {(string|undefined)} */ + this.videoLayout = undefined; + /** @type {(string|undefined)} */ this.tilesLayout = undefined; /** @type {?shaka.media.ManifestParser.AccessibilityPurpose} */ this.accessibilityPurpose; From 106808642beacd654b5a509999a6d5388f27fa3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=CC=81lvaro=20Velad=20Galva=CC=81n?= Date: Wed, 25 Oct 2023 12:20:04 +0200 Subject: [PATCH 2/4] Add 3D label to UI --- ui/resolution_selection.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/resolution_selection.js b/ui/resolution_selection.js index ce0355325a..488fb1652e 100644 --- a/ui/resolution_selection.js +++ b/ui/resolution_selection.js @@ -97,7 +97,8 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { otherIdx = tracks.findIndex((t) => { return t.height == track.height && t.frameRate == track.frameRate && - t.hdr == track.hdr; + t.hdr == track.hdr && + t.videoLayout == track.videoLayout; }); } return otherIdx == idx; @@ -150,6 +151,9 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { if (track.hdr == 'PQ' || track.hdr == 'HLG') { text += ' (HDR)'; } + if (track.videoLayout == 'CH-STEREO') { + text += ' (3D)'; + } span.textContent = text; } button.appendChild(span); From b0d349ee54d6a74788d2709e88d7d1bc31a11c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=CC=81lvaro=20Velad=20Galva=CC=81n?= Date: Thu, 26 Oct 2023 08:50:14 +0200 Subject: [PATCH 3/4] Update default value for videoLayout --- lib/hls/hls_parser.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 3957487ce7..6b95581ad8 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -1220,7 +1220,12 @@ shaka.hls.HlsParser = class { const [width, height] = resolution ? resolution.split('x') : [null, null]; const videoRange = tag.getAttributeValue('VIDEO-RANGE'); - let videoLayout = tag.getAttributeValue('REQ-VIDEO-LAYOUT'); + + // According to the HLS spec: + // By default a video variant is monoscopic, so an attribute + // consisting entirely of REQ-VIDEO-LAYOUT="CH-MONO" is unnecessary + // and SHOULD NOT be present. + let videoLayout = tag.getAttributeValue('REQ-VIDEO-LAYOUT') || 'CH-MONO'; if (videoLayout == 'CH-STEREO,CH-MONO') { videoLayout = 'CH-STEREO'; } else if (videoLayout == 'CH-MONO,CH-STEREO') { From 7721ee5e620020ccb5b4c1a68e9eecb4b33e419c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?A=CC=81lvaro=20Velad=20Galva=CC=81n?= Date: Thu, 26 Oct 2023 09:01:43 +0200 Subject: [PATCH 4/4] Update implementation --- lib/hls/hls_parser.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 6b95581ad8..30415b13cb 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -1221,16 +1221,20 @@ shaka.hls.HlsParser = class { const videoRange = tag.getAttributeValue('VIDEO-RANGE'); + let videoLayout = tag.getAttributeValue('REQ-VIDEO-LAYOUT'); + if (videoLayout && videoLayout.includes(',')) { + // If multiple video layout strings are present, pick the first valid + // one. + const layoutStrings = videoLayout.split(',').filter((layoutString) => { + return layoutString == 'CH-STEREO' || layoutString == 'CH-MONO'; + }); + videoLayout = layoutStrings[0]; + } // According to the HLS spec: // By default a video variant is monoscopic, so an attribute // consisting entirely of REQ-VIDEO-LAYOUT="CH-MONO" is unnecessary // and SHOULD NOT be present. - let videoLayout = tag.getAttributeValue('REQ-VIDEO-LAYOUT') || 'CH-MONO'; - if (videoLayout == 'CH-STEREO,CH-MONO') { - videoLayout = 'CH-STEREO'; - } else if (videoLayout == 'CH-MONO,CH-STEREO') { - videoLayout = 'CH-MONO'; - } + videoLayout = videoLayout || 'CH-MONO'; const streamInfos = this.createStreamInfosForVariantTag_(tag, resolution, frameRate);