diff --git a/extensions/amp-youtube/0.1/amp-youtube.js b/extensions/amp-youtube/0.1/amp-youtube.js index 44bdb651d438..577742af1442 100644 --- a/extensions/amp-youtube/0.1/amp-youtube.js +++ b/extensions/amp-youtube/0.1/amp-youtube.js @@ -69,6 +69,9 @@ class AmpYoutube extends AMP.BaseElement { /** @private {?string} */ this.liveChannelid_ = null; + /** @private {?string} */ + this.channelid_ = null; + /** @private {?boolean} */ this.muted_ = false; @@ -137,6 +140,7 @@ class AmpYoutube extends AMP.BaseElement { buildCallback() { this.videoid_ = this.getVideoId_(); this.liveChannelid_ = this.getLiveChannelId_(); + this.channelid_ = this.getChannelId_(); this.assertDatasourceExists_(); const deferred = new Deferred(); @@ -156,7 +160,11 @@ class AmpYoutube extends AMP.BaseElement { const baseUrl = `https://www.youtube${urlSuffix}.com/embed/`; const descriptor = this.videoid_ ? `${encodeURIComponent(this.videoid_ || '')}?` - : `live_stream?channel=${encodeURIComponent(this.liveChannelid_ || '')}&`; + : this.liveChannelid_ + ? `live_stream?channel=${encodeURIComponent(this.liveChannelid_ || '')}&` + : // Channel embeds use the channel's uploads playlist. The uploads + // playlist id is the channel id prefixed with "UU". + `?listType=playlist&list=UU${encodeURIComponent(this.channelid_ || '')}&`; return `${baseUrl}${descriptor}enablejsapi=1&=1`; } @@ -331,6 +339,14 @@ class AmpYoutube extends AMP.BaseElement { return this.element.getAttribute('data-live-channelid'); } + /** + * @return {?string} + * @private + */ + getChannelId_() { + return this.element.getAttribute('data-channelid'); + } + /** * @return {?string} * @private @@ -353,11 +369,13 @@ class AmpYoutube extends AMP.BaseElement { assertDatasourceExists_() { const datasourceExists = !(this.videoid_ && this.liveChannelid_) && - (this.videoid_ || this.liveChannelid_); + !(this.videoid_ && this.channelid_) && + !(this.liveChannelid_ && this.channelid_) && + (this.videoid_ || this.liveChannelid_ || this.channelid_); userAssert( datasourceExists, - 'Exactly one of data-videoid or ' + - 'data-live-channelid should be present for %s', + 'Exactly one of data-videoid, data-live-channelid or ' + + 'data-channelid should be present for %s', this.element ); } diff --git a/extensions/amp-youtube/0.1/test/test-amp-youtube.js b/extensions/amp-youtube/0.1/test/test-amp-youtube.js index 8cb1570af503..7a18758a32d2 100644 --- a/extensions/amp-youtube/0.1/test/test-amp-youtube.js +++ b/extensions/amp-youtube/0.1/test/test-amp-youtube.js @@ -9,8 +9,10 @@ import {VideoEvents_Enum} from '../../../../src/video-interface'; const EXAMPLE_VIDEOID = 'mGENRKrdoGY'; const EXAMPLE_LIVE_CHANNELID = 'UCB8Kb4pxYzsDsHxzBfnid4Q'; +const EXAMPLE_CHANNELID = 'UCB8Kb4pxYzsDsHxzBfnid4Q'; const EXAMPLE_VIDEOID_URL = `https://www.youtube.com/embed/${EXAMPLE_VIDEOID}?enablejsapi=1&=1&playsinline=1`; const EXAMPLE_LIVE_CHANNELID_URL = `https://www.youtube.com/embed/live_stream?channel=${EXAMPLE_LIVE_CHANNELID}&enablejsapi=1&=1&playsinline=1`; +const EXAMPLE_CHANNELID_URL = `https://www.youtube.com/embed/?listType=playlist&list=UU${EXAMPLE_CHANNELID}&enablejsapi=1&=1&playsinline=1`; const EXAMPLE_NO_COOKIE_VIDEOID_URL = `https://www.youtube-nocookie.com/embed/${EXAMPLE_VIDEOID}?enablejsapi=1&=1&playsinline=1`; describes.realWin( @@ -256,6 +258,14 @@ describes.realWin( expect(iframe.src).to.equal(EXAMPLE_LIVE_CHANNELID_URL); }); + it('renders for channel ids', async () => { + const yt = await getYt({'data-channelid': EXAMPLE_CHANNELID}); + const iframe = yt.querySelector('iframe'); + expect(iframe).to.not.be.null; + expect(iframe.tagName).to.equal('IFRAME'); + expect(iframe.src).to.equal(EXAMPLE_CHANNELID_URL); + }); + it('uses privacy-enhanced mode', async () => { const yt = await getYt({ 'data-videoid': EXAMPLE_VIDEOID, @@ -267,14 +277,60 @@ describes.realWin( expect(iframe.src).to.equal(EXAMPLE_NO_COOKIE_VIDEOID_URL); }); - it('requires data-videoid or data-live-channelid', () => { + it('requires exactly one datasource (empty)', () => { return allowConsoleError(() => { return getYt({}).should.eventually.be.rejectedWith( - /Exactly one of data-videoid or data-live-channelid should/ + /Exactly one of data-videoid, data-live-channelid or data-channelid should/ ); }); }); + it('rejects data-videoid and data-channelid together', () => { + return allowConsoleError(() => { + return getYt({ + 'data-videoid': EXAMPLE_VIDEOID, + 'data-channelid': EXAMPLE_CHANNELID, + }).should.eventually.be.rejectedWith( + /Exactly one of data-videoid, data-live-channelid or data-channelid should/ + ); + }); + }); + + it('rejects data-live-channelid and data-channelid together', () => { + return allowConsoleError(() => { + return getYt({ + 'data-live-channelid': EXAMPLE_LIVE_CHANNELID, + 'data-channelid': EXAMPLE_CHANNELID, + }).should.eventually.be.rejectedWith( + /Exactly one of data-videoid, data-live-channelid or data-channelid should/ + ); + }); + }); + + it('rejects all three datasources together', () => { + return allowConsoleError(() => { + return getYt({ + 'data-videoid': EXAMPLE_VIDEOID, + 'data-live-channelid': EXAMPLE_LIVE_CHANNELID, + 'data-channelid': EXAMPLE_CHANNELID, + }).should.eventually.be.rejectedWith( + /Exactly one of data-videoid, data-live-channelid or data-channelid should/ + ); + }); + }); + + it('uses no-cookie mode for channel embeds', async () => { + const yt = await getYt({ + 'data-channelid': EXAMPLE_CHANNELID, + 'credentials': 'omit', + }); + const iframe = yt.querySelector('iframe'); + expect(iframe).to.not.be.null; + expect(iframe.src).to.contain('youtube-nocookie.com'); + expect(iframe.src).to.contain('listType=playlist'); + expect(iframe.src).to.contain('list=UU' + EXAMPLE_CHANNELID); + }); + it('adds an img placeholder in prerender mode if source is videoid', async () => { const yt = await getYt( {'data-videoid': EXAMPLE_VIDEOID}, diff --git a/extensions/amp-youtube/0.1/test/validator-amp-youtube.out b/extensions/amp-youtube/0.1/test/validator-amp-youtube.out index 62d66446c09e..008e84477b7f 100644 --- a/extensions/amp-youtube/0.1/test/validator-amp-youtube.out +++ b/extensions/amp-youtube/0.1/test/validator-amp-youtube.out @@ -34,7 +34,7 @@ FAIL | | >> ^~~~~~~~~ -amp-youtube/0.1/test/validator-amp-youtube.html:34:2 The tag 'amp-youtube' is missing a mandatory attribute - pick one of ['data-live-channelid', 'data-videoid']. (see https://amp.dev/documentation/components/amp-youtube) +amp-youtube/0.1/test/validator-amp-youtube.html:34:2 The tag 'amp-youtube' is missing a mandatory attribute - pick one of ['data-live-channelid', 'data-videoid', 'data-channelid']. (see https://amp.dev/documentation/components/amp-youtube) | | >> ^~~~~~~~~ @@ -49,7 +49,7 @@ amp-youtube/0.1/test/validator-amp-youtube.html:39:2 The attribute 'data-videoid | | > ^~~~~~~~~ -amp-youtube/0.1/test/validator-amp-youtube.html:43:2 Mutually exclusive attributes encountered in tag 'amp-youtube' - pick one of ['data-live-channelid', 'data-videoid']. (see https://amp.dev/documentation/components/amp-youtube) +amp-youtube/0.1/test/validator-amp-youtube.html:43:2 Mutually exclusive attributes encountered in tag 'amp-youtube' - pick one of ['data-live-channelid', 'data-videoid', 'data-channelid']. (see https://amp.dev/documentation/components/amp-youtube) | data-live-channelid="UCB8Kb4pxYzsDsHxzBfnid4Q" | data-videoid="dQw4w9WgXcQ"> | diff --git a/extensions/amp-youtube/1.0/test/validator-amp-youtube.out b/extensions/amp-youtube/1.0/test/validator-amp-youtube.out index 206af6806f00..6bdf112f854f 100644 --- a/extensions/amp-youtube/1.0/test/validator-amp-youtube.out +++ b/extensions/amp-youtube/1.0/test/validator-amp-youtube.out @@ -42,7 +42,7 @@ FAIL | | >> ^~~~~~~~~ -amp-youtube/1.0/test/validator-amp-youtube.html:42:4 The tag 'amp-youtube' is missing a mandatory attribute - pick one of ['data-live-channelid', 'data-videoid']. (see https://amp.dev/documentation/components/amp-youtube) +amp-youtube/1.0/test/validator-amp-youtube.html:42:4 The tag 'amp-youtube' is missing a mandatory attribute - pick one of ['data-live-channelid', 'data-videoid', 'data-channelid']. (see https://amp.dev/documentation/components/amp-youtube) | | | @@ -62,7 +62,7 @@ amp-youtube/1.0/test/validator-amp-youtube.html:48:4 The attribute 'data-videoid | | > ^~~~~~~~~ -amp-youtube/1.0/test/validator-amp-youtube.html:56:4 Mutually exclusive attributes encountered in tag 'amp-youtube' - pick one of ['data-live-channelid', 'data-videoid']. (see https://amp.dev/documentation/components/amp-youtube) +amp-youtube/1.0/test/validator-amp-youtube.html:56:4 Mutually exclusive attributes encountered in tag 'amp-youtube' - pick one of ['data-live-channelid', 'data-videoid', 'data-channelid']. (see https://amp.dev/documentation/components/amp-youtube) | width="480" | height="270" | data-live-channelid="UCB8Kb4pxYzsDsHxzBfnid4Q" diff --git a/extensions/amp-youtube/amp-youtube.md b/extensions/amp-youtube/amp-youtube.md index 36dadaf3f4c6..f3c414ff5375 100644 --- a/extensions/amp-youtube/amp-youtube.md +++ b/extensions/amp-youtube/amp-youtube.md @@ -223,6 +223,10 @@ For example, in this URL: `https://www.youtube.com/watch?v=Z1q71gFeRqM`, `Z1q71g The Youtube channel id that provides a stable livestream url. For example, in this URL: `https://www.youtube.com/embed/live_stream?channel=UCB8Kb4pxYzsDsHxzBfnid4Q`, `UCB8Kb4pxYzsDsHxzBfnid4Q` is the channel id. You can provide a `data-live-channelid` instead of a `data-videoid` attribute to embed a stable url for a live stream instead of a video. Channels do not come with default placeholders. You can provide a placeholder for the video per example 2 above. +### data-channelid + +The YouTube channel id whose uploads playlist should be embedded. For example, in this URL: `https://www.youtube.com/embed?listType=playlist&list=UUUB8Kb4pxYzsDsHxzBfnid4Q`, `UB8Kb4pxYzsDsHxzBfnid4Q` is the channel id (the uploads playlist id is the channel id prefixed with `UU`). You can provide a `data-channelid` instead of a `data-videoid` attribute to embed a channel's uploads instead of a single video. Channels do not come with default placeholders. You can provide a placeholder for the video per example 2 above. + ### data-param-\* All `data-param-*` attributes (with the exception of `data-param-autoplay` and `data-param-loop`) will be added as query parameter to the YouTube iframe src. This may be used to pass custom values through to YouTube plugins, such as whether to show controls. diff --git a/extensions/amp-youtube/validator-amp-youtube.protoascii b/extensions/amp-youtube/validator-amp-youtube.protoascii index 411cb89b679a..0eb52cedac30 100644 --- a/extensions/amp-youtube/validator-amp-youtube.protoascii +++ b/extensions/amp-youtube/validator-amp-youtube.protoascii @@ -41,12 +41,17 @@ tags: { # } attrs: { name: "data-live-channelid" - mandatory_oneof: "['data-live-channelid', 'data-videoid']" + mandatory_oneof: "['data-live-channelid', 'data-videoid', 'data-channelid']" value_regex: "[^=/?:]+" } attrs: { name: "data-videoid" - mandatory_oneof: "['data-live-channelid', 'data-videoid']" + mandatory_oneof: "['data-live-channelid', 'data-videoid', 'data-channelid']" + value_regex: "[^=/?:]+" + } + attrs: { + name: "data-channelid" + mandatory_oneof: "['data-live-channelid', 'data-videoid', 'data-channelid']" value_regex: "[^=/?:]+" } attrs: { diff --git a/src/bento/components/bento-youtube/1.0/base-element.js b/src/bento/components/bento-youtube/1.0/base-element.js index e3b2af7bacd2..e4ff5bf16b81 100644 --- a/src/bento/components/bento-youtube/1.0/base-element.js +++ b/src/bento/components/bento-youtube/1.0/base-element.js @@ -19,6 +19,7 @@ BaseElement['props'] = { 'controls': {attr: 'controls', type: 'boolean'}, 'videoid': {attr: 'data-videoid'}, 'liveChannelid': {attr: 'data-live-channelid'}, + 'channelid': {attr: 'data-channelid'}, 'dock': {attr: 'dock', media: true}, 'credentials': {attr: 'credentials'}, // TODO(wg-components): Current behavior defaults to loading="auto". diff --git a/src/bento/components/bento-youtube/1.0/component.js b/src/bento/components/bento-youtube/1.0/component.js index 46d71467a392..88814eb3ed52 100644 --- a/src/bento/components/bento-youtube/1.0/component.js +++ b/src/bento/components/bento-youtube/1.0/component.js @@ -69,6 +69,7 @@ function createDefaultInfo() { function BentoYoutubeWithRef( { autoplay, + channelid, credentials, liveChannelid, loop, @@ -80,15 +81,19 @@ function BentoYoutubeWithRef( ref ) { const datasourceExists = - !(videoid && liveChannelid) && (videoid || liveChannelid); + !(videoid && liveChannelid) && + !(videoid && channelid) && + !(liveChannelid && channelid) && + (videoid || liveChannelid || channelid); if (!datasourceExists) { throw new Error( - 'Exactly one of data-videoid or data-live-channelid should be present for ' + 'Exactly one of data-videoid, data-live-channelid or ' + + 'data-channelid should be present for ' ); } - let src = getEmbedUrl(credentials, videoid, liveChannelid); + let src = getEmbedUrl(credentials, videoid, liveChannelid, channelid); if (!('playsinline' in params)) { params['playsinline'] = '1'; } @@ -205,7 +210,7 @@ function BentoYoutubeWithRef( * @return {string} * @private */ -function getEmbedUrl(credentials, videoid, liveChannelid) { +function getEmbedUrl(credentials, videoid, liveChannelid, channelid) { let urlSuffix = ''; if (credentials === 'omit') { urlSuffix = '-nocookie'; @@ -214,10 +219,16 @@ function getEmbedUrl(credentials, videoid, liveChannelid) { let descriptor = ''; if (videoid) { descriptor = `${encodeURIComponent(videoid)}?`; - } else { + } else if (liveChannelid) { descriptor = `live_stream?channel=${encodeURIComponent( liveChannelid || '' )}&`; + } else { + // Channel embeds use the channel's uploads playlist. The uploads + // playlist id is the channel id prefixed with "UU". + descriptor = `?listType=playlist&list=UU${encodeURIComponent( + channelid || '' + )}&`; } return `${baseUrl}${descriptor}enablejsapi=1&=1`; } diff --git a/test/fixtures/errors.html b/test/fixtures/errors.html index 63e29225ac45..8070924e039e 100644 --- a/test/fixtures/errors.html +++ b/test/fixtures/errors.html @@ -24,7 +24,7 @@