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 1b0d5ebfa1c4..6d71412291ae 100644 --- a/extensions/amp-video/0.1/test/test-video-cache.js +++ b/extensions/amp-video/0.1/test/test-video-cache.js @@ -78,6 +78,21 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => { 'https://example-com.cdn.ampproject.org/mbv/s/example.com/video2.mp4?amp_video_host_url=https%3A%2F%2Fcanonical.com' ); }); + + it('should select the video[src] and never the sources children', async () => { + const videoEl = createVideo([ + {src: 'video2.mp4'}, + {src: 'video3.mp4', type: 'video/mp4'}, + ]); + videoEl.setAttribute('src', 'video1.mp4'); + const xhrSpy = env.sandbox.spy(xhrService, 'fetch'); + + await fetchCachedSources(videoEl, env.win); + + 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' + ); + }); }); describe('url forming', () => { @@ -145,6 +160,56 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => { expect(addedSources[1].getAttribute('data-bitrate')).to.equal('1500'); expect(addedSources[2].getAttribute('data-bitrate')).to.equal('700'); }); + + it('should add video[src] as the last fallback source', async () => { + env.sandbox.stub(xhrService, 'fetch').resolves({ + json: () => + Promise.resolve({ + sources: [ + {'url': 'video1.mp4', 'bitrate_kbps': 700, type: 'video/mp4'}, + {'url': 'video2.mp4', 'bitrate_kbps': 2000, type: 'video/mp4'}, + {'url': 'video3.mp4', 'bitrate_kbps': 1500, type: 'video/mp4'}, + ], + }), + }); + + const videoEl = createVideo([{src: 'video.mp4'}]); + videoEl.setAttribute('src', 'video1.mp4'); + videoEl.setAttribute('type', 'video/mp4'); + + await fetchCachedSources(videoEl, env.win); + + const lastSource = videoEl.querySelector('source:last-of-type'); + expect(lastSource.getAttribute('src')).to.equal('video1.mp4'); + expect(lastSource.getAttribute('type')).to.equal('video/mp4'); + }); + + it('should clear the unused sources when video[src]', async () => { + env.sandbox.stub(xhrService, 'fetch').resolves({ + json: () => + Promise.resolve({ + sources: [ + {'url': 'video1.mp4', 'bitrate_kbps': 700, type: 'video/mp4'}, + {'url': 'video2.mp4', 'bitrate_kbps': 2000, type: 'video/mp4'}, + {'url': 'video3.mp4', 'bitrate_kbps': 1500, type: 'video/mp4'}, + ], + }), + }); + + const videoEl = createVideo([ + {src: 'video.mp4'}, + {src: 'video.mp4'}, + {src: 'video.mp4'}, + {src: 'video.mp4'}, + {src: 'video.mp4'}, + ]); + videoEl.setAttribute('src', 'video1.mp4'); + + await fetchCachedSources(videoEl, env.win); + + const addedSources = videoEl.querySelectorAll('source'); + expect(addedSources).to.have.lengthOf(4); // 3 from cache + 1 fallback. + }); }); describe('end to end', () => { diff --git a/extensions/amp-video/0.1/video-cache.js b/extensions/amp-video/0.1/video-cache.js index c8019a2b5604..56c30c7dbc63 100644 --- a/extensions/amp-video/0.1/video-cache.js +++ b/extensions/amp-video/0.1/video-cache.js @@ -16,7 +16,12 @@ import {Services} from '../../../src/services'; import {addParamsToUrl, resolveRelativeUrl} from '../../../src/url'; -import {createElementWithAttributes, matches} from '../../../src/dom'; +import { + createElementWithAttributes, + iterateCursor, + matches, + removeElement, +} from '../../../src/dom'; import {extensionScriptInNode} from '../../../src/service/extension-script'; import {toArray} from '../../../src/core/types/array'; import {user} from '../../../src/log'; @@ -33,13 +38,17 @@ import {user} from '../../../src/log'; export function fetchCachedSources(videoEl, win) { if ( !extensionScriptInNode(win, 'amp-cache-url', '0.1') || - !videoEl.querySelector('source[src]').getAttribute('src') + !( + videoEl.getAttribute('src') || + videoEl.querySelector('source[src]')?.getAttribute('src') + ) ) { user().error('AMP-VIDEO', 'Video cache not properly configured'); return Promise.resolve(); } const {canonicalUrl, sourceUrl} = Services.documentInfoForDoc(win.document); const servicePromise = Services.cacheUrlServicePromiseForDoc(videoEl); + maybeReplaceSrcWithSourceElement(videoEl, win); const videoUrl = resolveRelativeUrl(selectVideoSource(videoEl), sourceUrl); return servicePromise .then((service) => service.createCacheUrl(videoUrl)) @@ -95,3 +104,34 @@ function applySourcesToVideo(videoEl, sources) { videoEl.insertBefore(sourceEl, videoEl.firstChild); }); } + +/** + * If present, moves the src attribute to a source element to enable playing + * from multiple sources: the cached ones and the fallback initial src. + * @param {!Element} videoEl + * @param {!Window} win + */ +function maybeReplaceSrcWithSourceElement(videoEl, win) { + if (!videoEl.hasAttribute('src')) { + return; + } + const sourceEl = win.document.createElement('source'); + const srcAttr = videoEl.getAttribute('src'); + sourceEl.setAttribute('src', srcAttr); + + const typeAttr = videoEl.getAttribute('type'); + if (typeAttr) { + sourceEl.setAttribute('type', typeAttr); + } + + // Remove the src attr so the source children can play. + videoEl.removeAttribute('src'); + videoEl.removeAttribute('type'); + + // Remove all existing sources as they are never supposed to play for a video + // that has a src, cf https://html.spec.whatwg.org/#concept-media-load-algorithm + const sourceEls = videoEl.querySelectorAll('source'); + iterateCursor(sourceEls, (el) => removeElement(el)); + + videoEl.insertBefore(sourceEl, videoEl.firstChild); +}