Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
163b8f7
For the first video of the first web story page, use the inlined vide…
coreymasanto Jan 26, 2022
0819baf
Fix merge conflict
coreymasanto Jan 26, 2022
cf79773
Refactor to prevent unnecessary XHR-related function calls
coreymasanto Jan 26, 2022
16ba4e8
Fix promise
coreymasanto Jan 26, 2022
ad8a9a0
Syntax fix
coreymasanto Jan 27, 2022
455d45e
Allow the XHR request to be sent if the inline video is unexpectedly …
coreymasanto Jan 27, 2022
4b3a02f
Lint
coreymasanto Jan 27, 2022
73c67c1
Remove unnecessary variable from shouldUseInlineVideoResponse and upd…
coreymasanto Jan 28, 2022
c92c2e7
Wrap JSON.parse() in a try/catch
coreymasanto Jan 28, 2022
6164988
Pull out a new requestCachedVideoSources() method from the fetchCache…
coreymasanto Jan 28, 2022
7c760e0
Clean up preexisting lengthy if-statement
coreymasanto Jan 28, 2022
b4093ea
Merge branch 'main' into inlinedVideo
coreymasanto Feb 8, 2022
5640b66
Simplify JSON parsing logic in requestCachedVideoSources
coreymasanto Feb 8, 2022
8787143
Add a check for the 'sources' key
coreymasanto Feb 8, 2022
d2465cc
Lint fix
coreymasanto Feb 8, 2022
bc73828
Add initial, untested version of tests for inline video response logic
coreymasanto Feb 11, 2022
697dd6b
Merge branch 'main' into inlinedVideo
coreymasanto Feb 11, 2022
b477b97
Add a describe grouping for the set of inline video response tests
coreymasanto Feb 11, 2022
2d108a6
Update logic for determining whether the inline response should be used
coreymasanto Feb 11, 2022
a5487d2
Get tests passing
coreymasanto Feb 11, 2022
c328d21
Lint fixes
coreymasanto Feb 11, 2022
e0e6de7
Merge branch 'main' into inlinedVideo
coreymasanto Feb 11, 2022
e0e8b85
Merge branch 'main' into inlinedVideo
coreymasanto Feb 11, 2022
d4ed4ec
Merge branch 'main' into inlinedVideo
coreymasanto Feb 14, 2022
db458ea
Merge branch 'main' into inlinedVideo
coreymasanto Feb 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions extensions/amp-video/0.1/test/test-video-cache.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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);
Comment thread
coreymasanto marked this conversation as resolved.
}
} 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;
}