diff --git a/extensions/amp-video/0.1/test-e2e/test-amp-video-flexible-bitrate.js b/extensions/amp-video/0.1/test-e2e/test-amp-video-flexible-bitrate.js index 1c2544bde1ce..3ed570153c87 100644 --- a/extensions/amp-video/0.1/test-e2e/test-amp-video-flexible-bitrate.js +++ b/extensions/amp-video/0.1/test-e2e/test-amp-video-flexible-bitrate.js @@ -213,7 +213,7 @@ describes.endtoend( ); await expect( - 'https://amp-dev.cdn.ampproject.org/mbv/s/amp.dev/static/samples/video/tokyo.mp4?amp_video_host_url=https%3A%2F%2Famp.dev%2F&__amp_source_origin=http%3A%2F%2Flocalhost%3A8000' + 'https://amp-dev.cdn.ampproject.org/mbv/s/amp.dev/static/samples/video/tokyo.mp4?amp_video_host_url=https%3A%2F%2Famp.dev%2F&_video_require_acao_header=1&__amp_source_origin=http%3A%2F%2Flocalhost%3A8000' ).to.have.been.sent; }); }); diff --git a/extensions/amp-video/0.1/test/test-video-cache.js b/extensions/amp-video/0.1/test/test-video-cache.js index 8181f5b308db..d78cf12330e5 100644 --- a/extensions/amp-video/0.1/test/test-video-cache.js +++ b/extensions/amp-video/0.1/test/test-video-cache.js @@ -1,4 +1,5 @@ import {createElementWithAttributes} from '#core/dom'; +import * as Preact from '#core/dom/jsx'; import {Services} from '#service'; import {installPerformanceService} from '#service/performance-impl'; @@ -41,7 +42,7 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => { await fetchCachedSources(videoEl, env.ampdoc); expect(xhrSpy).to.have.been.calledWith( - 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video1.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com' + 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video1.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com&_video_require_acao_header=1' ); }); @@ -52,7 +53,7 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => { await fetchCachedSources(videoEl, env.ampdoc); expect(xhrSpy).to.have.been.calledWith( - 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video1.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com' + 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video1.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com&_video_require_acao_header=1' ); }); @@ -66,7 +67,7 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => { await fetchCachedSources(videoEl, env.ampdoc); expect(xhrSpy).to.have.been.calledWith( - 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video2.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com' + 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video2.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com&_video_require_acao_header=1' ); }); @@ -81,7 +82,7 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => { await fetchCachedSources(videoEl, env.ampdoc); expect(xhrSpy).to.have.been.calledWith( - 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video1.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com' + 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video1.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com&_video_require_acao_header=1' ); }); }); @@ -94,41 +95,40 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => { await fetchCachedSources(videoEl, env.ampdoc); expect(xhrSpy).to.have.been.calledWith( - 'https://website-com.cdn.ampproject.org/mbv/s/website.com/video.html?amp_video_host_url=https%3A%2F%2Fcanonical.com' + 'https://website-com.cdn.ampproject.org/mbv/s/website.com/video.html?amp_video_host_url=https%3A%2F%2Fcanonical.com&_video_require_acao_header=1' ); }); - it('should send the request to the correct address if the video has a relative url', async () => { - const videoEl = createVideo([{'src': 'video.html'}]); + it('should always add the ACAO header', async () => { + const videoEl = createVideo([{'src': 'https://website.com/video.html'}]); const xhrSpy = env.sandbox.spy(xhrService, 'fetch'); await fetchCachedSources(videoEl, env.ampdoc); expect(xhrSpy).to.have.been.calledWith( - 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video.html?amp_video_host_url=https%3A%2F%2Fcanonical.com' + 'https://website-com.cdn.ampproject.org/mbv/s/website.com/video.html?amp_video_host_url=https%3A%2F%2Fcanonical.com&_video_require_acao_header=1' ); }); - it('should send the request to the correct address if the video has a .gif extension', async () => { - const videoEl = createVideo([{'src': 'https://website.com/video.gif'}]); + it('should send the request to the correct address if the video has a relative url', async () => { + const videoEl = createVideo([{'src': 'video.html'}]); const xhrSpy = env.sandbox.spy(xhrService, 'fetch'); await fetchCachedSources(videoEl, env.ampdoc); expect(xhrSpy).to.have.been.calledWith( - 'https://website-com.cdn.ampproject.org/mbv/s/website.com/video.gif?amp_video_host_url=https%3A%2F%2Fcanonical.com' + 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video.html?amp_video_host_url=https%3A%2F%2Fcanonical.com&_video_require_acao_header=1' ); }); - it('should add the ACAO queryparam if the video is crossorigin', async () => { - const videoEl = createVideo([{'src': 'video.html'}]); - videoEl.setAttribute('crossorigin', ''); + it('should send the request to the correct address if the video has a .gif extension', async () => { + const videoEl = createVideo([{'src': 'https://website.com/video.gif'}]); const xhrSpy = env.sandbox.spy(xhrService, 'fetch'); await fetchCachedSources(videoEl, env.ampdoc); expect(xhrSpy).to.have.been.calledWith( - 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video.html?amp_video_host_url=https%3A%2F%2Fcanonical.com&_video_require_acao_header=1' + 'https://website-com.cdn.ampproject.org/mbv/s/website.com/video.gif?amp_video_host_url=https%3A%2F%2Fcanonical.com&_video_require_acao_header=1' ); }); }); @@ -396,6 +396,14 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => { expect(videoEl.querySelector('source[data-bitrate]')).to.not.be.null; }); + it('should set the crossorigin attribute to the video', async () => { + const videoEl = createVideo([{'src': 'video.html'}]); + + await fetchCachedSources(videoEl, env.ampdoc); + + expect(videoEl.hasAttribute('crossorigin')).to.be.true; + }); + it('should set an attribute on cached video sources', async () => { env.sandbox.stub(xhrService, 'fetch').resolves({ json: () => @@ -494,6 +502,65 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => { }); }); + describe('captions field', async () => { + it('should append track element if the cache responds with captions', async () => { + env.sandbox.stub(xhrService, 'fetch').resolves({ + json: () => + Promise.resolve({ + 'captions': { + 'src': 'captions_src_response.vtt', + 'srclang': 'en-us', + }, + 'sources': [ + {'url': 'video.mp4', 'bitrate_kbps': 700, 'type': 'video/mp4'}, + ], + }), + }); + const videoEl = createVideo([{src: 'video.mp4'}]); + await fetchCachedSources(videoEl, env.ampdoc); + + const trackEl = videoEl.querySelector('track'); + expect(trackEl).to.exist; + }); + it('should not append track element if video already has a track child', async () => { + env.sandbox.stub(xhrService, 'fetch').resolves({ + json: () => + Promise.resolve({ + 'captions': { + 'src': 'captions_src_response.vtt', + 'srclang': 'en-us', + }, + 'sources': [ + {'url': 'video.mp4', 'bitrate_kbps': 700, 'type': 'video/mp4'}, + ], + }), + }); + const videoEl = createVideo([{src: 'video.mp4'}]); + videoEl.appendChild(); + await fetchCachedSources(videoEl, env.ampdoc); + + const trackEl = videoEl.querySelector( + 'track[src="captions_src_response.vtt"]' + ); + expect(trackEl).to.not.exist; + }); + it('should not append track element if captions does not exist', async () => { + env.sandbox.stub(xhrService, 'fetch').resolves({ + json: () => + Promise.resolve({ + 'sources': [ + {'url': 'video.mp4', 'bitrate_kbps': 700, 'type': 'video/mp4'}, + ], + }), + }); + const videoEl = createVideo([{src: 'video.mp4'}]); + await fetchCachedSources(videoEl, env.ampdoc); + + const trackEl = videoEl.querySelector('track'); + expect(trackEl).to.not.exist; + }); + }); + describe('web stories: inlined video', async () => { it('should use the inlined source for the first video in the story instead of sending an XHR request', async () => { // Set up an inlined source response for the first video in the story @@ -531,10 +598,10 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => { await fetchCachedSources(videoEl3, env.ampdoc); expect(xhrSpy).to.have.been.calledWith( - 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video2.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com' + 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video2.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com&_video_require_acao_header=1' ); expect(xhrSpy).to.have.been.calledWith( - 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video3.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com' + 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video3.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com&_video_require_acao_header=1' ); }); @@ -550,7 +617,7 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => { await fetchCachedSources(videoEl, env.ampdoc); expect(xhrSpy).to.have.been.calledWith( - 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video1.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com' + 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video1.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com&_video_require_acao_header=1' ); }); @@ -571,7 +638,7 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => { await fetchCachedSources(videoEl, env.ampdoc); expect(xhrSpy).to.have.been.calledWith( - 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video1.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com' + 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video1.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com&_video_require_acao_header=1' ); }); }); diff --git a/extensions/amp-video/0.1/video-cache.js b/extensions/amp-video/0.1/video-cache.js index 7bceefd338df..d66dc7b8cc51 100644 --- a/extensions/amp-video/0.1/video-cache.js +++ b/extensions/amp-video/0.1/video-cache.js @@ -36,6 +36,11 @@ export function fetchCachedSources( return Promise.resolve(); } + // Always set crossorigin attribute so captions can be set. + if (!videoEl.hasAttribute('crossorigin')) { + videoEl.setAttribute('crossorigin', ''); + } + const videoSrc = videoEl.getAttribute('src'); const sourceSrc = videoEl.querySelector('source[src]')?.getAttribute('src'); if (!videoSrc && !sourceSrc) { @@ -49,6 +54,7 @@ export function fetchCachedSources( .then((response) => { applySourcesToVideo(videoEl, response['sources'], maxBitrate); applyAudioInfoToVideo(videoEl, response['has_audio']); + applyCaptionsTrackToVideo(videoEl, response['captions']); }) .catch(() => { // If cache fails, video should still load properly. @@ -156,6 +162,29 @@ function applyAudioInfoToVideo(videoEl, hasAudio) { } } +/** + * Appends captions track to video if captions url is defined and video + * element doesn't have a track child specified in the document. + * @param {!Element} videoEl + * @param {!Object} captionsResponse + */ +function applyCaptionsTrackToVideo(videoEl, captionsResponse) { + if ( + !captionsResponse || + !captionsResponse['src'] || + !captionsResponse['srclang'] || + videoEl.querySelector('track') + ) { + return; + } + const trackEl = createElementWithAttributes(videoEl.ownerDocument, 'track', { + 'src': captionsResponse['src'], + 'srclang': captionsResponse['srclang'], + 'kind': 'captions', + }); + videoEl.appendChild(trackEl); +} + /** * If present, moves the src attribute to a source element to enable playing * from multiple sources: the cached ones and the fallback initial src. @@ -230,9 +259,7 @@ function requestCachedVideoSources(videoEl, ampdoc) { const requestUrl = addParamsToUrl(cacheUrl.replace(/\/[ic]\//, '/mbv/'), { 'amp_video_host_url': /* document url that contains the video */ canonicalUrl, - 'amp_video_require_acao_header': videoEl.hasAttribute('crossorigin') - ? 1 - : null, + 'amp_video_require_acao_header': 1, }); return Services.xhrFor(win) .fetch(requestUrl, {prerenderSafe: true})