Skip to content

Commit

Permalink
🚀 Sort video sources by tuples of (codec, bitrate) and append codec i…
Browse files Browse the repository at this point in the history
…nformation to the video's type attribute (#36224)

* Sort the video sources first by descending codec priority, and then by descending bitrate value

* Update sorting test and fix the returned negative/positive sorting values in the comparator function

* Replace the .foreach with a for loop because values returned within the .foreach were being ignored, and fixing it would result in less readable code than a simple for loop

* Remove unnecessary code

* Add codec information recieved from video cache to type attribute on video sources; split added test into 2 (validation of bitrate sort and validation of codec sort); update test video json to include codec information

* Rewrite sort using indexof() instead of a for loop, for increased clarity

* Slight sort() return code & comment update

* Remove unnecessary 'type' key when creating video elements

* Add a third test that ensures that sources are sorted by both codec and bitrate

* Add a 4th source to the 3rd video cache test

* Fix comments within the sort() and forEach(), and use constants to describe the sorting behavior
  • Loading branch information
coreymasanto committed Oct 7, 2021
1 parent 1032c4d commit 4d1be4e
Show file tree
Hide file tree
Showing 2 changed files with 246 additions and 19 deletions.
215 changes: 198 additions & 17 deletions extensions/amp-video/0.1/test/test-video-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,12 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => {
json: () =>
Promise.resolve({
sources: [
{'url': 'video1.mp4', 'bitrate_kbps': 700, type: 'video/mp4'},
{
'url': 'video1.mp4',
'codec': 'h264',
'bitrate_kbps': 700,
type: 'video/mp4',
},
],
}),
});
Expand All @@ -136,17 +141,81 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => {
const addedSource = videoEl.querySelector('source');
expect(addedSource.getAttribute('src')).to.equal('video1.mp4');
expect(addedSource.getAttribute('data-bitrate')).to.equal('700');
expect(addedSource.getAttribute('type')).to.equal('video/mp4');
expect(addedSource.getAttribute('type')).to.equal(
'video/mp4; codecs=h264'
);
});

it('should add the sources sorted by bitrate', async () => {
it('should add the sources sorted by codec priority', 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'},
{
'url': 'video1.mp4',
'codec': 'vp09.02.30.11',
'bitrate_kbps': 700,
type: 'video/mp4',
},
{
'url': 'video2.mp4',
'codec': 'unknown',
'bitrate_kbps': 2000,
type: 'video/mp4',
},
{
'url': 'video3.mp4',
'codec': 'h264',
'bitrate_kbps': 1500,
type: 'video/mp4',
},
],
}),
});

const videoEl = createVideo([{src: 'video.mp4'}]);

await fetchCachedSources(videoEl, env.ampdoc);

const addedSources = videoEl.querySelectorAll('source');
const codecRegex = /(?<=codecs=)[A-Za-z0-9.]+/;
const srcCodec0 = addedSources[0]
.getAttribute('type')
.match(codecRegex)[0];
const srcCodec1 = addedSources[1]
.getAttribute('type')
.match(codecRegex)[0];
const srcCodec2 = addedSources[2]
.getAttribute('type')
.match(codecRegex)[0];
expect(srcCodec0).to.equal('vp09.02.30.11');
expect(srcCodec1).to.equal('h264');
expect(srcCodec2).to.equal('unknown');
});

it('should add the sources sorted by bitrate, for any subset of sources whose codecs have equivalent priority', async () => {
env.sandbox.stub(xhrService, 'fetch').resolves({
json: () =>
Promise.resolve({
sources: [
{
'url': 'video1.mp4',
'codec': 'vp09.02.30.11',
'bitrate_kbps': 700,
type: 'video/mp4',
},
{
'url': 'video2.mp4',
'codec': 'vp09.00.15.08',
'bitrate_kbps': 2000,
type: 'video/mp4',
},
{
'url': 'video3.mp4',
'codec': 'vp09.00.25.00',
'bitrate_kbps': 1500,
type: 'video/mp4',
},
],
}),
});
Expand All @@ -161,37 +230,134 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => {
expect(addedSources[2].getAttribute('data-bitrate')).to.equal('700');
});

it('should add the sources sorted first by codec priority, and then by bitrate', async () => {
env.sandbox.stub(xhrService, 'fetch').resolves({
json: () =>
Promise.resolve({
sources: [
{
'url': 'video1.mp4',
'codec': 'h264',
'bitrate_kbps': 2000,
type: 'video/mp4',
},
{
'url': 'video2.mp4',
'codec': 'vp09.02.30.11',
'bitrate_kbps': 1000,
type: 'video/mp4',
},
{
'url': 'video3.mp4',
'codec': 'vp09.00.15.08',
'bitrate_kbps': 2000,
type: 'video/mp4',
},
{
'url': 'video4.mp4',
'codec': 'h264',
'bitrate_kbps': 3000,
type: 'video/mp4',
},
],
}),
});

const videoEl = createVideo([{src: 'video.mp4'}]);

await fetchCachedSources(videoEl, env.ampdoc);

const addedSources = videoEl.querySelectorAll('source');
const codecRegex = /(?<=codecs=)[A-Za-z0-9.]+/;
const srcCodec0 = addedSources[0]
.getAttribute('type')
.match(codecRegex)[0];
const srcCodec1 = addedSources[1]
.getAttribute('type')
.match(codecRegex)[0];
const srcCodec2 = addedSources[2]
.getAttribute('type')
.match(codecRegex)[0];
const srcCodec3 = addedSources[3]
.getAttribute('type')
.match(codecRegex)[0];

expect(addedSources[0].getAttribute('data-bitrate')).to.equal('2000');
expect(srcCodec0).to.equal('vp09.00.15.08');

expect(addedSources[1].getAttribute('data-bitrate')).to.equal('1000');
expect(srcCodec1).to.equal('vp09.02.30.11');

expect(addedSources[2].getAttribute('data-bitrate')).to.equal('3000');
expect(srcCodec2).to.equal('h264');

expect(addedSources[3].getAttribute('data-bitrate')).to.equal('2000');
expect(srcCodec3).to.equal('h264');
});

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'},
{
'url': 'video1.mp4',
'codec': 'h264',
'bitrate_kbps': 700,
type: 'video/mp4',
},
{
'url': 'video2.mp4',
'codec': 'h264',
'bitrate_kbps': 2000,
type: 'video/mp4',
},
{
'url': 'video3.mp4',
'codec': 'h264',
'bitrate_kbps': 1500,
type: 'video/mp4',
},
],
}),
});

const videoEl = createVideo([{src: 'video.mp4'}]);
videoEl.setAttribute('src', 'video1.mp4');
videoEl.setAttribute('type', 'video/mp4');
videoEl.setAttribute('type', 'video/mp4; codecs=h264');

await fetchCachedSources(videoEl, env.ampdoc);

const lastSource = videoEl.querySelector('source:last-of-type');
expect(lastSource.getAttribute('src')).to.equal('video1.mp4');
expect(lastSource.getAttribute('type')).to.equal('video/mp4');
expect(lastSource.getAttribute('type')).to.equal(
'video/mp4; codecs=h264'
);
});

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'},
{
'url': 'video1.mp4',
'codec': 'h264',
'bitrate_kbps': 700,
type: 'video/mp4',
},
{
'url': 'video2.mp4',
'codec': 'h264',
'bitrate_kbps': 2000,
type: 'video/mp4',
},
{
'url': 'video3.mp4',
'codec': 'h264',
'bitrate_kbps': 1500,
type: 'video/mp4',
},
],
}),
});
Expand All @@ -218,7 +384,12 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => {
json: () =>
Promise.resolve({
sources: [
{'url': 'video.mp4', 'bitrate_kbps': 700, 'type': 'video/mp4'},
{
'url': 'video.mp4',
'codec': 'h264',
'bitrate_kbps': 700,
'type': 'video/mp4',
},
],
}),
});
Expand All @@ -235,7 +406,12 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => {
json: () =>
Promise.resolve({
sources: [
{'url': 'video.mp4', 'bitrate_kbps': 700, 'type': 'video/mp4'},
{
'url': 'video.mp4',
'codec': 'h264',
'bitrate_kbps': 700,
'type': 'video/mp4',
},
],
}),
});
Expand All @@ -254,7 +430,12 @@ describes.realWin('amp-video cached-sources', {amp: true}, (env) => {
json: () =>
Promise.resolve({
sources: [
{'url': 'video.mp4', 'bitrate_kbps': 700, 'type': 'video/mp4'},
{
'url': 'video.mp4',
'codec': 'h264',
'bitrate_kbps': 700,
'type': 'video/mp4',
},
],
}),
});
Expand Down
50 changes: 48 additions & 2 deletions extensions/amp-video/0.1/video-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {matches} from '#core/dom/query';
import {toArray} from '#core/types/array';
import {user} from '../../../src/log';

/** @const {!Array<string>} */
const CODECS_IN_ASCENDING_PRIORITY = ['h264', 'vp09'];

/**
* Add the caching sources to the video if opted in.
* The request is sent to the AMP cache url with /mbv path prefix,
Expand Down Expand Up @@ -85,17 +88,60 @@ function selectVideoSource(videoEl) {
*/
function applySourcesToVideo(videoEl, sources, maxBitrate) {
sources
.sort((a, b) => a['bitrate_kbps'] - b['bitrate_kbps'])
.sort((a, b) => {
// This comparator sorts the video sources from least to most preferred.

const A_GOES_FIRST = -1;
const B_GOES_FIRST = 1;

// 'codec' values can contain metadata after the '.' that we must strip
// for sorting purposes. For example, "vp09.00.30.08" contains level,
// profile, and color depth values that are ignored in this sort.
const aCodec = a['codec']?.split('.')[0];
const bCodec = b['codec']?.split('.')[0];

// Codec priority is the primary sorting factor of this comparator.
// The greater the codec priority, the more the source is preferred.
const aCodecPriority = CODECS_IN_ASCENDING_PRIORITY.indexOf(aCodec);
const bCodecPriority = CODECS_IN_ASCENDING_PRIORITY.indexOf(bCodec);
if (aCodecPriority > bCodecPriority) {
return B_GOES_FIRST;
}
if (aCodecPriority < bCodecPriority) {
return A_GOES_FIRST;
}

// Bitrate is the tiebreaking sorting factor of this comparator.
// The greater the bitrate, the more the source is preferred.
const aBitrate = a['bitrate_kbps'];
const bBitrate = b['bitrate_kbps'];
if (aBitrate > bBitrate) {
return B_GOES_FIRST;
}
if (aBitrate < bBitrate) {
return A_GOES_FIRST;
}

return 0;
})
.forEach((source) => {
// This callback inserts each source as the first child within the video.
// So, although the sources were just sorted in ascending preference,
// they are ultimately arranged within the video element in descending
// preference.

if (source['bitrate_kbps'] > maxBitrate) {
return;
}

let type = source['type'];
type += source['codec'] ? '; codecs=' + source['codec'] : '';
const sourceEl = createElementWithAttributes(
videoEl.ownerDocument,
'source',
{
'src': source['url'],
'type': source['type'],
type,
'data-bitrate': source['bitrate_kbps'],
'i-amphtml-video-cached-source': '',
}
Expand Down

0 comments on commit 4d1be4e

Please sign in to comment.