-
Notifications
You must be signed in to change notification settings - Fork 3.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create task queue to synchronize function calls to media pool elements #13259
Conversation
The I will continue investigating the cause there. Additionally, this seems to make the overall navigation slower, as these operations are all blocking on one another. I can't think of a good way to feature detect whether the browser can handle allowing these to be completely async as it was before, but I will also at least try using |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! Do you think we could improve this queue, later, to bless several elements in parallel? From what I understand they're blessed one by one here
blessPromise.catch(reason => { | ||
dev().expectedError('AMP-STORY', 'Blessing media element failed:', | ||
reason, mediaEl); | ||
}).then(() => dispatch(mediaEl, 'bless', false)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth adding a comment to mention this line works as a finally
and is triggered on both success or failure of the promise
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
refactored this
/** | ||
* @const {!ElementTaskDef} | ||
*/ | ||
const NOOP_ELEMENT_TASK = () => Promise.resolve(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this method store the expected taskName
and log an error message saying this task does not exist?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
refactored this
|
||
task(mediaEl) | ||
.catch(reason => dev().error('AMP-STORY', reason)) | ||
.then(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool pattern, but unless it's widely used, I think it's worth adding a // Finally
comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
dev().expectedError('AMP-STORY', 'Blessing media element failed:', | ||
reason); | ||
}); | ||
* @param {!HTMLMediaElement} poolMediaEl The media element to bless. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: s/poolMediaEl/mediaEl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done (moved to media-tasks.js)
const queue = mediaEl[ELEMENT_TASK_QUEUE_PROPERTY_NAME]; | ||
queue.push(taskName); | ||
|
||
if (queue.length === 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: might be worth implementing a isQueueRunning()
or shouldExecuteQueue
or something helper, that would do just that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
made a constant
This reverts commit 4e7af606f3fa03ca98fc6a4b3cd17e99947f1d10.
…solution, split into separate files
51787f9
to
83a0e1d
Compare
The queue is per-element, so there's nothing preventing multiple elements from being blessed in parallel. But, for a given element, only a single task is executed at a time. |
The performance seems a bit better after the promise changes to
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really great work, just a few nits/questions
* to replace the specified element. | ||
*/ | ||
loadInternal_(domMediaEl) { | ||
if (!isConnectedNode(domMediaEl)) { | ||
// Don't handle nodes that aren't even in the document. | ||
return null; | ||
return Promise.resolve(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we reject the promise instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this is really a rejection; we've done all the loading we want to do for that element, it just happens that we don't want to do any :)
*/ | ||
register(domMediaEl) { | ||
const mediaType = this.getMediaType_(domMediaEl); | ||
if (this.isAllocatedMediaElement_(mediaType, domMediaEl)) { | ||
// This media element originated from the media pool. | ||
return; | ||
return Promise.resolve(); | ||
} | ||
|
||
const id = domMediaEl.id || this.createDomMediaElementId_(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like I'm missing something here, but should the result from this.createDomMediaElementId_()
be assigned to domMediaEl.id
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Below, we set domMediaEl.id = id
, which handles this.
} | ||
|
||
|
||
/** | ||
* Preloads the content of the specified media element in the DOM. | ||
* @param {!HTMLMediaElement} domMediaEl The media element, found in the DOM, | ||
* whose content should be loaded. | ||
* @return {!Promise<!HTMLMediaElement>} A promise that is resolved when the | ||
* specified media element has been successfully loaded. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/has been successfully loaded/has successfully started preloading?
This comment makes me think all the media content is loaded and ready to play until the end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, copy/paste error. Changed.
return this.enqueueMediaElementTask_(poolMediaEl, new PauseTask()) | ||
.then(() => { | ||
if (opt_rewindToBeginning) { | ||
this.enqueueMediaElementTask_(poolMediaEl, new RewindTask()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe: this.rewindToBeginning(domMediaEl)
Also, do we actually need to wait for the PauseTask to be completed before we rewind the video? Looks like it was working synchronously before
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure, this was just to be very cautious. We can experiment later, since none of this is well-documented :)
Didn't use rewindToBeginning
to prevent getting the matching element from the pool twice and wasting cycles.
@@ -0,0 +1,459 @@ | |||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think eventually we'll want to have all these in a media-tasks
folder.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in separate files*
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
mutatePromise: () => {}, | ||
}; | ||
sandbox.stub(vsyncApi, 'mutatePromise') | ||
.callsFake(callback => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit, does this work?
sandbox.stub(vsyncApi, 'mutatePromise').resolves(callback => {callback()});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep!
ampproject#13259) * Return play promise from playFn to ensure media playback is not interrupted. * Refactor playFn to always return a promise. Improve documentation. * Add debug messages * Remove muted property change, since this is in a separate PR * Fix merge * Implement a task queue for media element functions * Only dispatch event for blessing media, to avoid conflicts with existing event names. * Add debugging * Add element to debug logging * Move debug message * Attempt fix bless promises * Simplify debug logs * Add logging and fix event setup * Add queue contents debugging * Capture event * Logging changes * logging updates * More logging * More logging * Short circuit bless before enqueue * Logging * More logging * Make bless promise * Inline bless play * More logging * Add bless play logging * Add vsync * Revert "Add vsync" This reverts commit 4e7af606f3fa03ca98fc6a4b3cd17e99947f1d10. * Reset media source when deallocated. * Serialize dom el for testing * Update video source as a task * Migrate tasks to classes; schedule microtasks where possible. * Use vsync, make all methods promise, block page methods on promise resolution, split into separate files * Add missing added files * Add unit tests for media tasks * Fix wrong class name * Sort imports * Remove logging * Address review comments * Sort imports in utils * Log queue contents * Remove vsync, add logging * Fix getDefaultSources and isQueueEmpty. * Fix initializing to default source. Don't play for bless when already playing. * Remove allocated elements from unallocated array. * Fix types and address review comments. * Update MediaPool tests
ampproject#13259) * Return play promise from playFn to ensure media playback is not interrupted. * Refactor playFn to always return a promise. Improve documentation. * Add debug messages * Remove muted property change, since this is in a separate PR * Fix merge * Implement a task queue for media element functions * Only dispatch event for blessing media, to avoid conflicts with existing event names. * Add debugging * Add element to debug logging * Move debug message * Attempt fix bless promises * Simplify debug logs * Add logging and fix event setup * Add queue contents debugging * Capture event * Logging changes * logging updates * More logging * More logging * Short circuit bless before enqueue * Logging * More logging * Make bless promise * Inline bless play * More logging * Add bless play logging * Add vsync * Revert "Add vsync" This reverts commit 4e7af606f3fa03ca98fc6a4b3cd17e99947f1d10. * Reset media source when deallocated. * Serialize dom el for testing * Update video source as a task * Migrate tasks to classes; schedule microtasks where possible. * Use vsync, make all methods promise, block page methods on promise resolution, split into separate files * Add missing added files * Add unit tests for media tasks * Fix wrong class name * Sort imports * Remove logging * Address review comments * Sort imports in utils * Log queue contents * Remove vsync, add logging * Fix getDefaultSources and isQueueEmpty. * Fix initializing to default source. Don't play for bless when already playing. * Remove allocated elements from unallocated array. * Fix types and address review comments. * Update MediaPool tests
ampproject#13259) * Return play promise from playFn to ensure media playback is not interrupted. * Refactor playFn to always return a promise. Improve documentation. * Add debug messages * Remove muted property change, since this is in a separate PR * Fix merge * Implement a task queue for media element functions * Only dispatch event for blessing media, to avoid conflicts with existing event names. * Add debugging * Add element to debug logging * Move debug message * Attempt fix bless promises * Simplify debug logs * Add logging and fix event setup * Add queue contents debugging * Capture event * Logging changes * logging updates * More logging * More logging * Short circuit bless before enqueue * Logging * More logging * Make bless promise * Inline bless play * More logging * Add bless play logging * Add vsync * Revert "Add vsync" This reverts commit 4e7af606f3fa03ca98fc6a4b3cd17e99947f1d10. * Reset media source when deallocated. * Serialize dom el for testing * Update video source as a task * Migrate tasks to classes; schedule microtasks where possible. * Use vsync, make all methods promise, block page methods on promise resolution, split into separate files * Add missing added files * Add unit tests for media tasks * Fix wrong class name * Sort imports * Remove logging * Address review comments * Sort imports in utils * Log queue contents * Remove vsync, add logging * Fix getDefaultSources and isQueueEmpty. * Fix initializing to default source. Don't play for bless when already playing. * Remove allocated elements from unallocated array. * Fix types and address review comments. * Update MediaPool tests
Fixes #13232. Also includes changes from #13245.
This PR creates a task queue on each media element in the
MediaPool
, implemented as an array of strings. The array stores the next functions to be invoked for that element (e.g. `['bless', 'load', 'play', 'unmute']), and ensures that they are each only called after the previous one has finished executing.This is necessary due to the async nature of the
amp-story
code, as well as the async nature of the media element'splay()
function. In some cases,play()
is invoked but then other code is executed before any callbacks onplay()
's promise chain are invoked. In our case, the "other code" that gets executed is sometimes also media-related code, callingload()
orplay()
again, both of which can cause the media in question to cease playback.See #13232 for more details on the issue.