Skip to content

Commit

Permalink
Handle decode errors in amp-video (#28005)
Browse files Browse the repository at this point in the history
Decode errors do not fallback to the next available source. This code manually handles that fallback.

Fixes #27982
  • Loading branch information
cramforce committed Apr 24, 2020
1 parent b2b850d commit ade5213
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 1 deletion.
53 changes: 52 additions & 1 deletion extensions/amp-video/0.1/amp-video.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {Services} from '../../../src/services';
import {VideoEvents} from '../../../src/video-interface';
import {VisibilityState} from '../../../src/visibility-state';
import {
childElement,
childElementByTag,
childElementsByTag,
fullscreenEnter,
Expand All @@ -28,7 +29,7 @@ import {
removeElement,
} from '../../../src/dom';
import {descendsFromStory} from '../../../src/utils/story';
import {dev, devAssert} from '../../../src/log';
import {dev, devAssert, user} from '../../../src/log';
import {getMode} from '../../../src/mode';
import {htmlFor} from '../../../src/static-template';
import {installVideoManagerForDoc} from '../../../src/service/video-manager-impl';
Expand Down Expand Up @@ -364,6 +365,55 @@ class AmpVideo extends AMP.BaseElement {
return promise;
}

/**
* Gracefully handle media errors if possible.
* @param {!Event} event
*/
handleMediaError_(event) {
if (
!this.video_.error ||
this.video_.error.code != MediaError.MEDIA_ERR_DECODE
) {
return;
}
// HTMLMediaElements automatically fallback to the next source if a load fails
// but they don't try the next source upon a decode error.
// This code does this fallback manually.
user().error(
TAG,
`Decode error in ${this.video_.currentSrc}`,
this.element
);
// No fallback available for bare src.
if (this.video_.src) {
return;
}
// Find the source element that caused the decode error.
let sourceCount = 0;
const currentSource = childElement(this.video_, (source) => {
if (source.tagName != 'SOURCE') {
return false;
}
sourceCount++;
return source.src == this.video_.currentSrc;
});
if (sourceCount == 0) {
return;
}
dev().assertElement(
currentSource,
`Can't find source element for currentSrc ${this.video_.currentSrc}`
);
removeElement(currentSource);
// Resets the loading and will catch the new source if any.
event.stopImmediatePropagation();
this.video_.load();
// Unfortunately we don't know exactly what operation caused the decode to
// fail. But to help, we need to retry. Since play is most common, we're
// doing that.
this.play(false);
}

/**
* @private
* Propagate sources that are cached by the CDN.
Expand Down Expand Up @@ -498,6 +548,7 @@ class AmpVideo extends AMP.BaseElement {
*/
installEventHandlers_() {
const video = dev().assertElement(this.video_);
video.addEventListener('error', (e) => this.handleMediaError_(e));

const forwardEventsUnlisten = this.forwardEvents(
[
Expand Down
58 changes: 58 additions & 0 deletions extensions/amp-video/0.1/test/test-amp-video.js
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,64 @@ describes.realWin(
expect(catchSpy.called).to.be.true;
});

it('decode error retries the next source', async () => {
const s0 = doc.createElement('source');
s0.setAttribute('src', './0.mp4');
const s1 = doc.createElement('source');
s1.setAttribute('src', 'https://example.com/1.mp4');
const video = await getVideo(
{
width: 160,
height: 90,
},
[s0, s1]
);
const ele = video.implementation_.video_;
ele.play = env.sandbox.stub();
ele.load = env.sandbox.stub();
Object.defineProperty(ele, 'error', {
value: {
code: MediaError.MEDIA_ERR_DECODE,
},
});
const secondErrorHandler = env.sandbox.stub();
ele.addEventListener('error', secondErrorHandler);
expect(ele.childElementCount).to.equal(2);
ele.dispatchEvent(new ErrorEvent('error'));
expect(ele.childElementCount).to.equal(1);
expect(ele.load).to.have.been.called;
expect(ele.play).to.have.been.called;
expect(secondErrorHandler).to.not.have.been.called;
});

it('non-decode error has no side effect', async () => {
const s0 = doc.createElement('source');
s0.setAttribute('src', 'https://example.com/0.mp4');
const s1 = doc.createElement('source');
s1.setAttribute('src', 'https://example.com/1.mp4');
const video = await getVideo(
{
width: 160,
height: 90,
},
[s0, s1]
);
const ele = video.implementation_.video_;
ele.play = env.sandbox.stub();
ele.load = env.sandbox.stub();
Object.defineProperty(ele, 'error', {
value: {
code: MediaError.MEDIA_ERR_ABORTED,
},
});
const secondErrorHandler = env.sandbox.stub();
ele.addEventListener('error', secondErrorHandler);
expect(ele.childElementCount).to.equal(2);
ele.dispatchEvent(new ErrorEvent('error'));
expect(ele.childElementCount).to.equal(2);
expect(secondErrorHandler).to.have.been.called;
});

it('should propagate ARIA attributes', async () => {
const v = await getVideo({
src: 'video.mp4',
Expand Down

0 comments on commit ade5213

Please sign in to comment.