Skip to content

Commit

Permalink
🚀 [Story video] Use the inlined video response instead of issuing an …
Browse files Browse the repository at this point in the history
…XHR request, for the 1st video of the 1st web story page (#37499)

* For the first video of the first web story page, use the inlined video response instead of issuing an XHR request.

* Refactor to prevent unnecessary XHR-related function calls

* Fix promise

* Syntax fix

* Allow the XHR request to be sent if the inline video is unexpectedly absent

* Lint

* Remove unnecessary variable from shouldUseInlineVideoResponse and update comment

* Wrap JSON.parse() in a try/catch

* Pull out a new requestCachedVideoSources() method from the fetchCachedSources() method

* Clean up preexisting lengthy if-statement

* Simplify JSON parsing logic in requestCachedVideoSources

* Add a check for the 'sources' key

* Lint fix

* Add initial, untested version of tests for inline video response logic

* Add a describe grouping for the set of inline video response tests

* Update logic for determining whether the inline response should be used

* Get tests passing

* Lint fixes
  • Loading branch information
coreymasanto committed Feb 14, 2022
1 parent 795f3d5 commit c8ea23b
Show file tree
Hide file tree
Showing 2 changed files with 191 additions and 25 deletions.
127 changes: 127 additions & 0 deletions extensions/amp-video/0.1/test/test-video-cache.js
Expand Up @@ -494,6 +494,88 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => {
});
});

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
const storyEl = createStoryForInlineVideoTesting();
env.win.document.body.appendChild(storyEl);
setUpInlinedVideoResponse();

const xhrSpy = env.sandbox.spy(xhrService, 'fetch');

// Fetch the sources for the first video in the story
const videoEl = storyEl.querySelectorAll('amp-video')[0];
await fetchCachedSources(videoEl, env.ampdoc);

expect(xhrSpy).to.have.not.been.called;
const inlinedSources = videoEl.querySelectorAll(
'source[src="inlined_video_response.mp4"]'
);
expect(inlinedSources).to.have.lengthOf(1);
});

it('should send an XHR request for any video that is not the very first one within the story', async () => {
// Set up an inlined source response for the first video in the story
const storyEl = createStoryForInlineVideoTesting();
env.win.document.body.appendChild(storyEl);
setUpInlinedVideoResponse();

const xhrSpy = env.sandbox.spy(xhrService, 'fetch');

// Fetch the sources for video #2: the 2nd video on the first story page
const videoEl2 = storyEl.querySelectorAll('amp-video')[1];
await fetchCachedSources(videoEl2, env.ampdoc);

// Fetch the sources for video #3: the 1st video on the 2nd story page
const videoEl3 = storyEl.querySelectorAll('amp-video')[2];
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'
);
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'
);
});

it('should send XHR request if inline config not provided', async () => {
// Create story without setting an inlined source response
const storyEl = createStoryForInlineVideoTesting();
env.win.document.body.appendChild(storyEl);

const xhrSpy = env.sandbox.spy(xhrService, 'fetch');

// Fetch the sources for the first video in the story
const videoEl = storyEl.querySelectorAll('amp-video')[0];
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'
);
});

it('should send XHR request if inlined video response fails to parse', async () => {
// Set up an improperly configured response for the first video in the story
const storyEl = createStoryForInlineVideoTesting();
env.win.document.body.appendChild(storyEl);
const scriptEl = createElementWithAttributes(env.win.document, 'script', {
'id': 'amp-google-video-cache-response',
'type': 'application/json',
});
scriptEl.textContent = '{"faulty": [{}]}';

const xhrSpy = env.sandbox.spy(xhrService, 'fetch');

// Fetch the sources for the first video in the story
const videoEl = storyEl.querySelectorAll('amp-video')[0];
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'
);
});
});

function createVideo(children) {
const videoEl = createElementWithAttributes(env.win.document, 'amp-video', {
'cache': 'google',
Expand All @@ -510,4 +592,49 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => {
env.win.document.body.appendChild(videoEl);
return videoEl;
}

function createStoryForInlineVideoTesting() {
const storyEl = env.win.document.createElement('amp-story');
const storyPageEl1 = env.win.document.createElement('amp-story-page');
const storyPageEl2 = env.win.document.createElement('amp-story-page');
storyEl.appendChild(storyPageEl1);
storyEl.appendChild(storyPageEl2);

// Place two videos on the first page. video #1 is nested more deeply than
// video #2, but it should still be considered the first video on the page.
const gridLayerEl = env.win.document.createElement('amp-story-grid-layer');
const videoEl1 = createVideo([{src: 'video1.mp4'}]);
gridLayerEl.appendChild(videoEl1);
storyPageEl1.appendChild(gridLayerEl);

// Place video #2 on the first page.
const videoEl2 = createVideo([{src: 'video2.mp4'}]);
storyPageEl1.appendChild(videoEl2);

// Place video #3 on the second page.
const videoEl3 = createVideo([{src: 'video3.mp4'}]);
storyPageEl2.appendChild(videoEl3);

return storyEl;
}

function setUpInlinedVideoResponse() {
const scriptEl = createElementWithAttributes(env.win.document, 'script', {
'id': 'amp-google-video-cache-response',
'type': 'application/json',
});
scriptEl.textContent = `
{
"sources": [
{
"url": "inlined_video_response.mp4",
"codec": "h264",
"type": "video/mp4",
"bitrate_kbps": 400
}
],
"has_audio": false
}`;
env.win.document.head.appendChild(scriptEl);
}
});
89 changes: 64 additions & 25 deletions extensions/amp-video/0.1/video-cache.js
Expand Up @@ -35,37 +35,20 @@ export function fetchCachedSources(
if (Services.platformFor(win).isBot()) {
return Promise.resolve();
}
if (
!(
videoEl.getAttribute('src') ||
videoEl.querySelector('source[src]')?.getAttribute('src')
)
) {

const videoSrc = videoEl.getAttribute('src');
const sourceSrc = videoEl.querySelector('source[src]')?.getAttribute('src');
if (!videoSrc && !sourceSrc) {
user().error('AMP-VIDEO', 'Video cache not properly configured');
return Promise.resolve();
}

Services.performanceFor(ampdoc.win).addEnabledExperiment('video-cache');

const {canonicalUrl, sourceUrl} = Services.documentInfoForDoc(win.document);
maybeReplaceSrcWithSourceElement(videoEl, win);
const videoUrl = resolveRelativeUrl(selectVideoSource(videoEl), sourceUrl);
return getCacheUrlService(videoEl, ampdoc)
.then((service) => service.createCacheUrl(videoUrl))
.then((cacheUrl) => {
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,
});
return Services.xhrFor(win).fetch(requestUrl, {prerenderSafe: true});
})
.then((response) => response.json())
.then((jsonResponse) => {
applySourcesToVideo(videoEl, jsonResponse['sources'], maxBitrate);
applyAudioInfoToVideo(videoEl, jsonResponse['has_audio']);
return requestCachedVideoSources(videoEl, ampdoc)
.then((response) => {
applySourcesToVideo(videoEl, response['sources'], maxBitrate);
applyAudioInfoToVideo(videoEl, response['has_audio']);
})
.catch(() => {
// If cache fails, video should still load properly.
Expand Down Expand Up @@ -215,3 +198,59 @@ function getCacheUrlService(videoEl, ampdoc) {
.installExtensionForDoc(ampdoc, 'amp-cache-url')
.then(() => Services.cacheUrlServicePromiseForDoc(videoEl));
}

/**
* Fetch the sources for the given video element.
* @param {!Element} videoEl
* @param {!AmpDoc} ampdoc
* @return {!Promise<!Object>} JSON representing AMP's cached video sources.
*/
function requestCachedVideoSources(videoEl, ampdoc) {
const {win} = ampdoc;
if (shouldUseInlineVideoResponse(videoEl, win)) {
const inlineResponseEl = win.document.getElementById(
'amp-google-video-cache-response'
);
try {
const inlineResponseJson = JSON.parse(inlineResponseEl.textContent);
if (inlineResponseJson['sources']) {
return Promise.resolve(inlineResponseJson);
}
} catch (err) {
// If parsing the response fails, an XHR request will be made below.
}
}

const {canonicalUrl, sourceUrl} = Services.documentInfoForDoc(win.document);
maybeReplaceSrcWithSourceElement(videoEl, win);
const videoUrl = resolveRelativeUrl(selectVideoSource(videoEl), sourceUrl);
return getCacheUrlService(videoEl, ampdoc)
.then((service) => service.createCacheUrl(videoUrl))
.then((cacheUrl) => {
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,
});
return Services.xhrFor(win)
.fetch(requestUrl, {prerenderSafe: true})
.then((xhrResponse) => xhrResponse.json());
});
}

/**
* Returns `true` if the video's inline response should be used instead of
* issuing an XHR request.
* @param {!Element} videoEl
* @param {!Window} win
* @return {boolean}
*/
function shouldUseInlineVideoResponse(videoEl, win) {
// Google video cache inlines the first video of the first web story page.
const firstVid = win.document.querySelector(
'amp-story-page:first-of-type amp-video'
);
return videoEl === firstVid;
}

0 comments on commit c8ea23b

Please sign in to comment.