diff --git a/examples/jwplayer.amp.html b/examples/jwplayer.amp.html new file mode 100644 index 000000000000..fcaab564f442 --- /dev/null +++ b/examples/jwplayer.amp.html @@ -0,0 +1,33 @@ + + + + + JWPlayer AMP Example + + + + + + + + +

JWPlayer AMP Examples

+ +

Responsive

+ + + + +

Non-responsive, with a playlist

+ + + + + + diff --git a/extensions/amp-jwplayer/0.1/amp-jwplayer.js b/extensions/amp-jwplayer/0.1/amp-jwplayer.js new file mode 100644 index 000000000000..9a25d163c6b4 --- /dev/null +++ b/extensions/amp-jwplayer/0.1/amp-jwplayer.js @@ -0,0 +1,125 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {getLengthNumeral, isLayoutSizeDefined} from '../../../src/layout'; +import {loadPromise} from '../../../src/event-helper'; +import {setStyles} from '../../../src/style'; +import {user} from '../../../src/log'; + +class AmpJWPlayer extends AMP.BaseElement { + + /** @override */ + preconnectCallback(onLayout) { + // Host that serves player configuration and content redirects + this.preconnect.url('https://content.jwplatform.com', onLayout); + // CDN which hosts jwplayer assets + this.preconnect.url('https://ssl.p.jwpcdn.com', onLayout); + } + + /** @override */ + isLayoutSupported(layout) { + return isLayoutSizeDefined(layout); + } + + /** @override */ + buildCallback() { + const width = this.element.getAttribute('width'); + const height = this.element.getAttribute('height'); + + /** @private @const {number} */ + this.width_ = getLengthNumeral(width); + + /** @private @const {number} */ + this.height_ = getLengthNumeral(height); + + /** @private @const {string} */ + this.contentid_ = user.assert( + (this.element.getAttribute('data-playlist-id') || + this.element.getAttribute('data-media-id')), + 'Either the data-media-id or the data-playlist-id ' + + 'attributes must be specified for %s', + this.element); + + /** @private @const {string} */ + this.playerid_ = user.assert( + this.element.getAttribute('data-player-id'), + 'The data-player-id attribute is required for %s', + this.element); + + if (!this.getPlaceholder()) { + this.buildImagePlaceholder_(); + } + } + + + /** @override */ + layoutCallback() { + const iframe = document.createElement('iframe'); + const src = 'https://content.jwplatform.com/players/' + + encodeURIComponent(this.contentid_) + '-' + + encodeURIComponent(this.playerid_) + '.html'; + + iframe.setAttribute('frameborder', '0'); + iframe.setAttribute('allowfullscreen', 'true'); + iframe.src = src; + this.applyFillContent(iframe); + iframe.width = this.width_; + iframe.height = this.height_; + this.element.appendChild(iframe); + /** @private {?Element} */ + this.iframe_ = iframe; + return loadPromise(iframe); + } + + /** @override */ + pauseCallback() { + if (this.iframe_ && this.iframe_.contentWindow) { + // The /players page can respond to "play" and "pause" commands from the + // iframe's parent + this.iframe_.contentWindow./*OK*/postMessage('pause', + 'https://content.jwplatform.com'); + } + } + + /** @private */ + buildImagePlaceholder_() { + const imgPlaceholder = new Image(); + + setStyles(imgPlaceholder, { + 'object-fit': 'cover', + }); + + imgPlaceholder.src = 'https://content.jwplatform.com/thumbs/' + + encodeURIComponent(this.contentid_) + '-720.jpg'; + imgPlaceholder.setAttribute('placeholder', ''); + imgPlaceholder.width = this.width_; + imgPlaceholder.height = this.height_; + + this.applyFillContent(imgPlaceholder); + + // Not every media item has a thumbnail image. If no image is found, + // don't add the placeholder to the DOM. + loadPromise(imgPlaceholder).then(() => { + this.element.appendChild(imgPlaceholder); + }).catch(() => { + // If the thumbnail image isn't available, we can safely ignore this + // error, and no image placeholder will be inserted. + }); + } + +}; + +AMP.registerElement('amp-jwplayer', AmpJWPlayer); diff --git a/extensions/amp-jwplayer/0.1/test/test-amp-jwplayer.js b/extensions/amp-jwplayer/0.1/test/test-amp-jwplayer.js new file mode 100644 index 000000000000..84798c456106 --- /dev/null +++ b/extensions/amp-jwplayer/0.1/test/test-amp-jwplayer.js @@ -0,0 +1,101 @@ +/** + * Copyright 2016 The AMP HTML Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS-IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {createIframePromise} from '../../../../testing/iframe'; +require('../amp-jwplayer'); +import {adopt} from '../../../../src/runtime'; + +adopt(window); + +describe('amp-jwplayer', () => { + + function getjwplayer(attributes) { + return createIframePromise().then(iframe => { + const jw = iframe.doc.createElement('amp-jwplayer'); + for (const key in attributes) { + jw.setAttribute(key, attributes[key]); + } + jw.setAttribute('width', '320'); + jw.setAttribute('height', '180'); + jw.setAttribute('layout', 'responsive'); + iframe.doc.body.appendChild(jw); + jw.implementation_.layoutCallback(); + return jw; + }); + } + + it('renders', () => { + return getjwplayer({ + 'data-media-id': 'Wferorsv', + 'data-player-id': 'sDZEo0ea', + }).then(jw => { + const iframe = jw.querySelector('iframe'); + expect(iframe).to.not.be.null; + expect(iframe.tagName).to.equal('IFRAME'); + expect(iframe.src).to.equal( + 'https://content.jwplatform.com/players/Wferorsv-sDZEo0ea.html'); + expect(iframe.getAttribute('width')).to.equal('320'); + expect(iframe.getAttribute('height')).to.equal('180'); + }); + }); + + it('renders with a playlist', () => { + return getjwplayer({ + 'data-playlist-id': '482jsTAr', + 'data-player-id': 'sDZEo0ea', + }).then(jw => { + const iframe = jw.querySelector('iframe'); + expect(iframe).to.not.be.null; + expect(iframe.tagName).to.equal('IFRAME'); + expect(iframe.src).to.equal( + 'https://content.jwplatform.com/players/482jsTAr-sDZEo0ea.html'); + }); + }); + + // These tests fail if the corresponding errors occur in buildCallback instead of + // layoutCallback. Commenting them out for now. + /* + it('fails if no media is specified', () => { + return getjwplayer({ + 'data-player-id': 'sDZEo0ea', + }).should.eventually.be.rejectedWith( + /Either the data-media-id or the data-playlist-id attributes must be/ + ); + }); + + it('fails if no player is specified', () => { + return getjwplayer({ + 'data-media-id': 'Wferorsv', + }).should.eventually.be.rejectedWith( + /The data-player-id attribute is required for/ + ); + }); + */ + + it('renders with a bad playlist', () => { + return getjwplayer({ + 'data-playlist-id': 'zzz', + 'data-player-id': 'sDZEo0ea', + }).then(jw => { + const iframe = jw.querySelector('iframe'); + expect(iframe).to.not.be.null; + expect(iframe.tagName).to.equal('IFRAME'); + expect(iframe.src).to.equal( + 'https://content.jwplatform.com/players/zzz-sDZEo0ea.html'); + }); + }); + +}); diff --git a/extensions/amp-jwplayer/amp-jwplayer.md b/extensions/amp-jwplayer/amp-jwplayer.md new file mode 100644 index 000000000000..befba787706f --- /dev/null +++ b/extensions/amp-jwplayer/amp-jwplayer.md @@ -0,0 +1,115 @@ + + +### `amp-jwplayer` + + + + + + + + + + + + + + + + + + +
DescriptionAn amp-jwplayer component displays a cloud-hosted JW Player.
AvailabilityStable
Required Script<script async custom-element="amp-jwplayer" src="https://cdn.ampproject.org/v0/amp-jwplayer-0.1.js"></script>
Examplesjwplayer.amp.html
+ +The following lists validation errors specific to the `amp-jwplayer` tag +(see also `amp-jwplayer` in the [AMP validator specification](https://github.com/ampproject/amphtml/blob/master/validator/validator.protoascii): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Validation ErrorDescription
TAG_REQUIRED_BY_MISSINGError thrown when required amp-jwplayer extension .js script tag is missing or incorrect.
MANDATORY_ATTR_MISSINGError thrown when data-account attribute is missing.
IMPLIED_LAYOUT_INVALIDError thrown when implied layout is set to CONTAINER; this layout type isn't supported.
MANDATORY_ONEOF_ATTR_MISSINGError thrown when either the data-media-id or data-playlist-id attributes are missing.
IMPLIED_LAYOUT_INVALIDError thrown when implied layout is set to CONTAINER; this layout type isn't supported.
SPECIFIED_LAYOUT_INVALIDError thrown when specified layout is set to CONTAINER; this layout type isn't supported.
INVALID_PROPERTY_VALUE_IN_ATTR_VALUEError thrown when invalid value is given for attributes height or width. For example, height=auto triggers this error for all supported layout types, with the exception of NODISPLAY.
+ +#### Example + +The `width` and `height` attributes determine the aspect ratio of the player embedded in responsive layouts. + +Example: + +```html + + +``` + +Non-responsive layout is also supported. + +Example: + +```html + + +``` + +#### Attributes + +**data-player-id** + +JW Platform player id. This is an 8-digit alphanumeric sequence that can be found in the [Players](https://dashboard.jwplayer.com/#/players) section in your JW Player Dashboard. (**Required**) + +**data-media-id** + +The JW Platform media id. This is an 8-digit alphanumeric sequence that can be found in the [Content](https://dashboard.jwplayer.com/#/content) section in your JW Player Dashboard. (**Required if `data-playlist-id` is not defined.**) + +**data-playlist-id** + +The JW Platform playlist id. This is an 8-digit alphanumeric sequence that can be found in the [Playlists](https://dashboard.jwplayer.com/#/content/playlists) section in your JW Player Dashboard. If both `data-playlist-id` and `data-media-id` are specified, `data-playlist-id` takes precedence. (**Required if `data-media-id` is not defined.**) \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 590535696e61..3b8a9b006648 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -85,6 +85,7 @@ function buildExtensions(options) { buildExtension('amp-iframe', '0.1', false, options); buildExtension('amp-image-lightbox', '0.1', true, options); buildExtension('amp-instagram', '0.1', false, options); + buildExtension('amp-jwplayer', '0.1', false, options); buildExtension('amp-lightbox', '0.1', false, options); buildExtension('amp-list', '0.1', false, options); buildExtension('amp-mustache', '0.1', false, options); @@ -340,6 +341,7 @@ function buildExamples(watch) { buildExample('font.amp.html'); buildExample('facebook.amp.html'); buildExample('instagram.amp.html'); + buildExample('jwplayer.amp.html'); buildExample('pinterest.amp.html'); buildExample('reach-player.amp.html'); buildExample('released.amp.html');