diff --git a/README.md b/README.md
index d98c9605cf4a..24de5c6c0136 100644
--- a/README.md
+++ b/README.md
@@ -83,8 +83,10 @@ These components can:
* Replace HTML5 elements that are not directly permitted in the specification
such as [amp-img](builtins/amp-img.md) and [amp-video](builtins/amp-video.md).
* Implement embedded third-party content, such as
-[amp-youtube](extensions/amp-youtube/amp-youtube.md), [amp-ad](builtins/amp-ad.md),
-and [amp-twitter](extensions/amp-twitter/amp-twitter.md).
+[amp-ad](builtins/amp-ad.md),
+[amp-pinterest](extensions/amp-pinterest/amp-pinterest.md),
+[amp-twitter](extensions/amp-twitter/amp-twitter.md),
+and [amp-youtube](extensions/amp-youtube/amp-youtube.md).
* Provide for common patterns in web pages,
such as [amp-lightbox](extensions/amp-lightbox/amp-lightbox.md)
and [amp-carousel](extensions/amp-carousel/amp-carousel.md).
diff --git a/examples/brightcove.amp.html b/examples/brightcove.amp.html
new file mode 100644
index 000000000000..274cf8239d21
--- /dev/null
+++ b/examples/brightcove.amp.html
@@ -0,0 +1,24 @@
+
+
+
+
+ Brightcove Player Example
+
+
+
+
+
+
+
+
+
Brightcove Player
+
+
+
+
+
+
diff --git a/extensions/README.md b/extensions/README.md
index a906eb5e6d02..289fabfe8aab 100644
--- a/extensions/README.md
+++ b/extensions/README.md
@@ -20,6 +20,7 @@ Current list of extended components:
| --------------------------------------------- | ------------------------------------------------------------------------------------------- |
| [`amp-anim`](amp-anim/amp-anim.md) | Runtime-managed animated image, most typically a GIF. |
| [`amp-audio`](amp-audio/amp-audio.md) | Replacement for the HTML5 `audio` tag. |
+| [`amp-brightcove`](amp-brightcove/amp-brightcove.md) | Displays a Brightcove Video Cloud or Perform player. |
| [`amp-carousel`](amp-carousel/amp-carousel.md) | Generic carousel for displaying multiple similar pieces of content along a horizontal axis. |
| [`amp-fit-text`](amp-fit-text/amp-fit-text.md) | Expand or shrink font size to fit the content within the space given. |
| [`amp-font`](amp-font/amp-font.md) | Trigger and monitor the loading of custom fonts. |
diff --git a/extensions/amp-brightcove/0.1/amp-brightcove.js b/extensions/amp-brightcove/0.1/amp-brightcove.js
new file mode 100644
index 000000000000..6f158f2012c6
--- /dev/null
+++ b/extensions/amp-brightcove/0.1/amp-brightcove.js
@@ -0,0 +1,92 @@
+/**
+ * Copyright 2015 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 {isLayoutSizeDefined} from '../../../src/layout';
+import {loadPromise} from '../../../src/event-helper';
+
+
+class AmpBrightcove extends AMP.BaseElement {
+
+ /** @override */
+ createdCallback() {
+ this.preconnect.url('https://players.brightcove.net');
+ }
+
+ /** @override */
+ isLayoutSupported(layout) {
+ return isLayoutSizeDefined(layout);
+ }
+
+ /** @override */
+ layoutCallback() {
+ const width = this.element.getAttribute('width');
+ const height = this.element.getAttribute('height');
+ const account = AMP.assert(
+ this.element.getAttribute('data-account'),
+ 'The data-account attribute is required for %s',
+ this.element);
+ const playerid = (this.element.getAttribute('data-player-id') || 'default');
+ const embed = (this.element.getAttribute('data-embed') || 'default');
+ const iframe = document.createElement('iframe');
+ let src = 'https://players.brightcove.net/' + encodeURIComponent(account) + '/' + encodeURIComponent(playerid) + '_' + encodeURIComponent(embed) + '/index.html';
+ if (this.element.getAttribute('data-playlist-id')) {
+ src += '?playlistId=';
+ src += this.encodeId_(this.element.getAttribute('data-playlist-id'));
+ } else if (this.element.getAttribute('data-video-id')) {
+ src += '?videoId=';
+ src += this.encodeId_(this.element.getAttribute('data-video-id'));
+ }
+ iframe.setAttribute('frameborder', '0');
+ iframe.setAttribute('allowfullscreen', 'true');
+ iframe.src = src;
+ this.applyFillContent(iframe);
+ iframe.width = width;
+ iframe.height = height;
+ this.element.appendChild(iframe);
+ /** @private {?Element} */
+ this.iframe_ = iframe;
+ return loadPromise(iframe);
+ }
+
+ /** @private */
+ encodeId_(id) {
+ /* id is either a Brightcove-assigned id, or a customer-generated reference id.
+ reference ids are prefixed 'ref:' and the colon must be preserved unencoded */
+ if (id.substring(0,4) === 'ref:') {
+ return 'ref:' + encodeURIComponent(id.substring(4));
+ } else {
+ return encodeURIComponent(id);
+ }
+ }
+
+ /** @override */
+ documentInactiveCallback() {
+ /*
+ This stops playback with the postMessage API.
+ Add this script to the player in the player configuration in the Studio
+ or via the Player Management API:
+
+ http://players.brightcove.net/906043040001/plugins/postmessage_pause.js
+
+ It's not a 'real' video.js plugin, just a plain script running in
+ the iframe so needs no configuration options.
+ */
+ this.iframe_.contentWindow./*OK*/postMessage('pause', 'https://players.brightcove.net');
+ return false;
+ }
+};
+
+AMP.registerElement('amp-brightcove', AmpBrightcove);
diff --git a/extensions/amp-brightcove/0.1/test/test-amp-brightcove.js b/extensions/amp-brightcove/0.1/test/test-amp-brightcove.js
new file mode 100644
index 000000000000..663ade156d34
--- /dev/null
+++ b/extensions/amp-brightcove/0.1/test/test-amp-brightcove.js
@@ -0,0 +1,67 @@
+/**
+ * Copyright 2015 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-brightcove');
+import {adopt} from '../../../../src/runtime';
+
+adopt(window);
+
+describe('amp-brightcove', () => {
+
+ function getBrightcove(accountId, videoId, opt_responsive) {
+ return createIframePromise().then(iframe => {
+ const bc = iframe.doc.createElement('amp-brightcove');
+ bc.setAttribute('data-account', accountId);
+ bc.setAttribute('width', '111');
+ bc.setAttribute('height', '222');
+ if (videoId) {
+ bc.setAttribute('data-video-id', videoId);
+ }
+ if (opt_responsive) {
+ bc.setAttribute('layout', 'responsive');
+ }
+ iframe.doc.body.appendChild(bc);
+ bc.implementation_.layoutCallback();
+ return bc;
+ });
+ }
+
+ it('renders', () => {
+ return getBrightcove('906043040001','ref:ampdemo').then(bc => {
+ const iframe = bc.querySelector('iframe');
+ expect(iframe).to.not.be.null;
+ expect(iframe.tagName).to.equal('IFRAME');
+ expect(iframe.src).to.equal(
+ 'https://players.brightcove.net/906043040001/default_default/index.html?videoId=ref:ampdemo');
+ expect(iframe.getAttribute('width')).to.equal('111');
+ expect(iframe.getAttribute('height')).to.equal('222');
+ });
+ });
+
+ it('renders responsively', () => {
+ return getBrightcove('906043040001', 'ref:ampdemo', true).then(bc => {
+ const iframe = bc.querySelector('iframe');
+ expect(iframe).to.not.be.null;
+ expect(iframe.className).to.match(/-amp-fill-content/);
+ });
+ });
+
+ it('requires data-account', () => {
+ return getBrightcove('').should.eventually.be.rejectedWith(
+ /The data-account attribute is required for/);
+ });
+});
diff --git a/extensions/amp-brightcove/amp-brightcove.md b/extensions/amp-brightcove/amp-brightcove.md
new file mode 100644
index 000000000000..0539c2d014df
--- /dev/null
+++ b/extensions/amp-brightcove/amp-brightcove.md
@@ -0,0 +1,63 @@
+
+
+### `amp-brightcove`
+
+An `amp-brightcove` component displays a Brightcove [Video Cloud](https://www.brightcove.com/en/online-video-platform) or [Perform](https://www.brightcove.com/en/perform) player.
+
+Example:
+```html
+
+
+```
+
+The width and height will determine the aspect ratio of the player embed in responsive layouts.
+
+#### Attributes
+
+**data-account**
+
+The Brightcove Video Cloud or Perform account id
+
+**data-player**
+
+The Brightcove player id. This is a GUID or "default". The default value is "default".
+
+**data-embed**
+
+The Brightcove player id. This is a GUID or "default". The default value and most common value is "default".
+
+**data-video-id**
+
+The Video Cloud video id. Most Video Cloud players will need this.
+This is not used for Perform players.
+
+**data-playlist-id**
+
+The Video Cloud playlist id. For AMP HTML uses a video id will normally be used instead. If both a playlist and a video are specified, the playlist takes precedence.
+This is not used for Perform players.
+
+#### Player configuration
+
+This script should be added to the configuration of Brightcove Players used with this component. This allows the AMP document to pause the player. Only the script need be added, no plugin name or JSON are needed.
+
+* http://players.brightcove.net/906043040001/plugins/postmessage_pause.js
diff --git a/gulpfile.js b/gulpfile.js
index a1686c7fe8cb..99789a9ce4a5 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -67,6 +67,7 @@ function buildExtensions(options) {
// Each extension and version must be listed individually here.
buildExtension('amp-anim', '0.1', false, options);
buildExtension('amp-audio', '0.1', false, options);
+ buildExtension('amp-brightcove', '0.1', false, options);
buildExtension('amp-carousel', '0.1', true, options);
buildExtension('amp-fit-text', '0.1', true, options);
buildExtension('amp-font', '0.1', false, options);
@@ -84,7 +85,7 @@ function buildExtensions(options) {
*/
buildExtension('amp-slides', '0.1', false, options);
buildExtension('amp-twitter', '0.1', false, options);
- buildExtension('amp-vine', '0.1', false, options);
+ buildExtension('amp-vine', '0.1', false, options);
buildExtension('amp-youtube', '0.1', false, options);
}
diff --git a/spec/amp-html-components.md b/spec/amp-html-components.md
index d1be97c36bac..fa429882e2be 100644
--- a/spec/amp-html-components.md
+++ b/spec/amp-html-components.md
@@ -178,6 +178,7 @@ In these cases, services may set up endpoints that produce data that conforms to
- [amp-ad](../builtins/amp-ad.md)
- [amp-pixel](../builtins/amp-pixel.md)
- [amp-video](../builtins/amp-video.md)
+- [amp-brightcove](../extensions/amp-brightcove/amp-brightcove.md)
- [amp-carousel](../extensions/amp-carousel/amp-carousel.md)
- [amp-font](../extensions/amp-font/amp-font.md)
- [amp-lightbox](../extensions/amp-lightbox/amp-lightbox.md)
diff --git a/spec/amp-html-format.md b/spec/amp-html-format.md
index 8e57a389ee9c..e0dff020b291 100644
--- a/spec/amp-html-format.md
+++ b/spec/amp-html-format.md
@@ -241,13 +241,8 @@ The AMP runtime is loaded via the mandatory ``.
-
-The latter approach of triggering development mode with a `development` attribute causes the validator to emit a warning to the javascript developer console which will read `DEV_MODE_ENABLED development - please remove for prod`. This warning becomes an error in a production environment and causes validation to fail for performance reasons. The `development` attribute thus should not be used publicly, and is provided primarily for use with toolchains.
+Development mode may be triggered by appending `#development=1` to the URL of
+the page.
## Resources
diff --git a/src/layout.js b/src/layout.js
index 59846de69b4f..3ad43936b008 100644
--- a/src/layout.js
+++ b/src/layout.js
@@ -52,9 +52,12 @@ let Dimensions;
/**
- * Set or cached browser natural dimensions for elements. The tagname
- * initialized here will return true `hasNaturalDimensions`, even if yet to be
- * calculated. Exported for testing.
+ * The set of elements with natural dimensions, that is, elements
+ * which have a known dimension either based on their value specified here,
+ * or, if the value is null, a dimension specific to the browser.
+ * `hasNaturalDimensions` checks for membership in this set.
+ * `getNaturalDimensions` determines the dimensions for an element in the
+ * set and caches it.
* @type {!Object}
* @private Visible for testing only!
*/
@@ -72,6 +75,7 @@ export const naturalDimensions_ = {
*/
export const LOADING_ELEMENTS_ = {
'AMP-ANIM': true,
+ 'AMP-BRIGHTCOVE': true,
'AMP-IFRAME': true,
'AMP-IMG': true,
'AMP-INSTAGRAM': true,
@@ -202,11 +206,14 @@ export function hasNaturalDimensions(tagName) {
/**
* Determines the default dimensions for an element which could vary across
* different browser implementations, like