-
-
Notifications
You must be signed in to change notification settings - Fork 59
Convert more functions to async
/await
#91
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
Draft
Borewit
wants to merge
1
commit into
hvianna:dev
Choose a base branch
from
Borewit:more-async-await
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -841,7 +841,6 @@ let audioElement = [], | |
supportsFileSystemAPI, // browser supports File System API (may be disabled via config.yaml) | ||
useFileSystemAPI, // load music from local device when in web server mode | ||
userPresets, | ||
waitingMetadata = 0, | ||
wasMuted, // mute status before switching to microphone input | ||
webServer; // web server available? (boolean) | ||
|
||
|
@@ -1140,14 +1139,22 @@ const toggleDisplay = ( el, status ) => { | |
el.style.display = status ? '' : 'none'; | ||
} | ||
|
||
// promise-compatible `onloadeddata` event handler for media elements | ||
const waitForLoadedData = async audioEl => new Promise( ( resolve, reject ) => { | ||
/** | ||
* Wait for a media element to load data | ||
* @param {HTMLMediaElement} audioEl | ||
* @returns {Promise<void>} | ||
*/ | ||
const waitForLoadedData = audioEl => new Promise((resolve, reject) => { | ||
const cleanup = () => { | ||
audioEl.onloadeddata = null; | ||
audioEl.onerror = null; | ||
}; | ||
audioEl.onerror = () => { | ||
audioEl.onerror = audioEl.onloadeddata = null; | ||
reject(); | ||
} | ||
cleanup(); | ||
reject(new Error("Failed to load media element")); | ||
}; | ||
audioEl.onloadeddata = () => { | ||
audioEl.onerror = audioEl.onloadeddata = null; | ||
cleanup(); | ||
debugLog( 'onLoadedData', { mediaEl: audioEl.id.slice(-1) } ); | ||
resolve(); | ||
}; | ||
|
@@ -1219,52 +1226,49 @@ function addMetadata( metadata, target ) { | |
* @param {object} { album, artist, codec, duration, title } | ||
* @returns {Promise} resolves to 1 when song added, or 0 if queue is full | ||
*/ | ||
function addSongToPlayQueue( fileObject, content ) { | ||
async function addSongToPlayQueue( fileObject, content ) { | ||
|
||
return new Promise( resolve => { | ||
if ( queueLength() >= MAX_QUEUED_SONGS ) { | ||
resolve(0); | ||
return; | ||
} | ||
if ( queueLength() >= MAX_QUEUED_SONGS ) { | ||
return 0; | ||
} | ||
|
||
const { fileName, baseName, extension } = parsePath( fileExplorer.decodeChars( fileObject.file ) ), | ||
uri = normalizeSlashes( fileObject.file ), | ||
newEl = document.createElement('li'), // create new list element | ||
trackData = newEl.dataset; | ||
const { fileName, baseName, extension } = parsePath( fileExplorer.decodeChars( fileObject.file ) ), | ||
uri = normalizeSlashes( fileObject.file ), | ||
newEl = document.createElement('li'), // create new list element | ||
trackData = newEl.dataset; | ||
|
||
Object.assign( trackData, DATASET_TEMPLATE ); // initialize element's dataset attributes | ||
Object.assign( trackData, DATASET_TEMPLATE ); // initialize element's dataset attributes | ||
|
||
if ( ! content ) | ||
content = parseTrackName( baseName ); | ||
if ( ! content ) | ||
content = parseTrackName( baseName ); | ||
|
||
trackData.album = content.album || ''; | ||
trackData.artist = content.artist || ''; | ||
trackData.title = content.title || fileName || uri.slice( uri.lastIndexOf('//') + 2 ); | ||
trackData.duration = content.duration || ''; | ||
trackData.codec = content.codec || extension.toUpperCase(); | ||
trackData.album = content.album || ''; | ||
trackData.artist = content.artist || ''; | ||
trackData.title = content.title || fileName || uri.slice( uri.lastIndexOf('//') + 2 ); | ||
trackData.duration = content.duration || ''; | ||
trackData.codec = content.codec || extension.toUpperCase(); | ||
// trackData.subs = + !! fileObject.subs; // show 'subs' badge in the playqueue (TO-DO: resolve CSS conflict) | ||
|
||
trackData.file = uri; // for web server access | ||
newEl.handle = fileObject.handle; // for File System API access | ||
newEl.dirHandle = fileObject.dirHandle; | ||
newEl.subs = fileObject.subs; // only defined when coming from the file explorer (not playlists) | ||
trackData.file = uri; // for web server access | ||
newEl.handle = fileObject.handle; // for File System API access | ||
newEl.dirHandle = fileObject.dirHandle; | ||
newEl.subs = fileObject.subs; // only defined when coming from the file explorer (not playlists) | ||
|
||
playlist.appendChild( newEl ); | ||
playlist.appendChild( newEl ); | ||
|
||
if ( FILE_EXT_AUDIO.includes( extension ) || ! extension ) { | ||
// disable retrieving metadata of video files for now - https://github.com/Borewit/music-metadata-browser/issues/950 | ||
trackData.retrieve = 1; // flag this item as needing metadata | ||
retrieveMetadata(); | ||
} | ||
if ( FILE_EXT_AUDIO.includes( extension ) || ! extension ) { | ||
// disable retrieving metadata of video files for now - https://github.com/Borewit/music-metadata-browser/issues/950 | ||
trackData.retrieve = 1; // flag this item as needing metadata | ||
await retrieveMetadata(); | ||
} | ||
|
||
if ( queueLength() == 1 && ! isPlaying() ) | ||
loadSong(0).then( () => resolve(1) ); | ||
else { | ||
if ( playlistPos > queueLength() - 3 ) | ||
loadSong( NEXT_TRACK ); | ||
resolve(1); | ||
} | ||
}); | ||
if ( queueLength() === 1 && ! isPlaying() ) { | ||
await loadSong( 0 ); | ||
} else { | ||
if ( playlistPos > queueLength() - 3 ) | ||
await loadSong( NEXT_TRACK ); | ||
} | ||
return 1; | ||
} | ||
|
||
/** | ||
|
@@ -2017,7 +2021,7 @@ function keyboardControls( event ) { | |
} | ||
|
||
/** | ||
* Sets (or removes) the `src` attribute of a audio element and | ||
* Sets (or removes) the `src` attribute of an audio element and | ||
* releases any data blob (File System API) previously in use by it | ||
* | ||
* @param {object} audio element | ||
|
@@ -2040,22 +2044,28 @@ function loadAudioSource( audioEl, newSource ) { | |
/** | ||
* Load a file blob into an audio element | ||
* | ||
* @param {object} audio element | ||
* @param {object} file blob | ||
* @param {boolean} `true` to start playing | ||
* @returns {Promise} resolves to a string containing the URL created for the blob | ||
* @param {Blob} fileBlob The audio file blob | ||
* @param {HTMLAudioElement} audioEl The audio element | ||
* @param {boolean} playIt When `true` will start playing | ||
* @returns {Promise<string>} Resolves to blob URL | ||
*/ | ||
async function loadFileBlob( fileBlob, audioEl, playIt ) { | ||
const url = URL.createObjectURL( fileBlob ); | ||
loadAudioSource( audioEl, url ); | ||
async function loadFileBlob(fileBlob, audioEl, playIt) { | ||
const url = URL.createObjectURL(fileBlob); | ||
loadAudioSource(audioEl, url); | ||
|
||
try { | ||
await waitForLoadedData( audioEl ); | ||
if ( playIt ) | ||
audioEl.play(); | ||
await waitForLoadedData(audioEl); | ||
if (playIt) { | ||
try { | ||
await audioEl.play(); | ||
} catch (err) { | ||
consoleLog("Playback failed:", err); | ||
} | ||
} | ||
return url; | ||
} catch (err) { | ||
throw new Error("Failed to load audio from Blob"); | ||
} | ||
catch ( e ) {} | ||
|
||
return url; | ||
} | ||
|
||
/** | ||
|
@@ -3295,60 +3305,54 @@ async function retrieveBackgrounds() { | |
} | ||
|
||
/** | ||
* Retrieve metadata for files in the play queue | ||
* Retrieve metadata for the first MAX_METADATA_REQUESTS files in the play queue, | ||
* which have no metadata assigned yet | ||
*/ | ||
async function retrieveMetadata() { | ||
// leave when we already have enough concurrent requests pending | ||
if ( waitingMetadata >= MAX_METADATA_REQUESTS ) | ||
return; | ||
|
||
// find the first play queue item for which we haven't retrieved the metadata yet | ||
const queueItem = Array.from( playlist.children ).find( el => el.dataset.retrieve ); | ||
|
||
if ( queueItem ) { | ||
// find the first MAX_METADATA_REQUESTS items for which we haven't retrieved the metadata yet | ||
const retrievalQueue = Array.from(playlist.children).filter(el => el.dataset.retrieve).slice(0, MAX_METADATA_REQUESTS); | ||
|
||
// Execute in parallel | ||
return Promise.all(retrievalQueue.map(async queueItem => { | ||
let uri = queueItem.dataset.file, | ||
revoke = false; | ||
|
||
waitingMetadata++; | ||
delete queueItem.dataset.retrieve; | ||
|
||
queryMetadata: { | ||
if ( queueItem.handle ) { | ||
try { | ||
if ( await queueItem.handle.requestPermission() != 'granted' ) | ||
break queryMetadata; | ||
|
||
uri = URL.createObjectURL( await queueItem.handle.getFile() ); | ||
revoke = true; | ||
} | ||
catch( e ) { | ||
break queryMetadata; | ||
} | ||
if ( queueItem.handle ) { | ||
try { | ||
if ( await queueItem.handle.requestPermission() !== 'granted' ) | ||
return; | ||
|
||
uri = URL.createObjectURL( await queueItem.handle.getFile() ); | ||
revoke = true; | ||
} | ||
catch( e ) { | ||
consoleLog(`Error converting queued file="${queueItem.handle.file}" to URI`, e); | ||
return; | ||
} | ||
} | ||
|
||
try { | ||
const metadata = await mm.fetchFromUrl( uri, { skipPostHeaders: true } ); | ||
if ( metadata ) { | ||
addMetadata( metadata, queueItem ); // add metadata to play queue item | ||
try { | ||
const metadata = await mm.fetchFromUrl( uri, { skipPostHeaders: true } ); | ||
Comment on lines
+3324
to
+3339
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Edit: just saw your previous comment on this.. |
||
addMetadata( metadata, queueItem ); // add metadata to play queue item | ||
syncMetadataToAudioElements( queueItem ); | ||
if ( ! ( metadata.common.picture && metadata.common.picture.length ) ) { | ||
getFolderCover( queueItem ).then( cover => { | ||
queueItem.dataset.cover = cover; | ||
syncMetadataToAudioElements( queueItem ); | ||
if ( ! ( metadata.common.picture && metadata.common.picture.length ) ) { | ||
getFolderCover( queueItem ).then( cover => { | ||
queueItem.dataset.cover = cover; | ||
syncMetadataToAudioElements( queueItem ); | ||
}); | ||
} | ||
} | ||
}); | ||
} | ||
catch( e ) {} | ||
|
||
if ( revoke ) | ||
URL.revokeObjectURL( uri ); | ||
} | ||
catch( e ) { | ||
consoleLog(`Failed to fetch or add metadata for queued file="${queueItem.handle.file}"`, e); | ||
} | ||
|
||
waitingMetadata--; | ||
retrieveMetadata(); // call again to continue processing the queue | ||
} | ||
if ( revoke ) | ||
URL.revokeObjectURL( uri ); | ||
})); | ||
} | ||
|
||
/** | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
We don't really need to wait for
retrieveMetadata()
, as it doesn't return any value and can run in parallel. I could be mistaken, but I think this would slow down the process of adding multiple files to the play queue.Uh oh!
There was an error while loading. Please reload this page.
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 see no limit with this recursion, so this could potentially spin up a large number of tasks, equal to the total queued items. This can easily drain memory and become a bottleneck, rather then a speed boost. I strongly advice against that. If you do:
It has been proved it can become an issue, and therefor I even documented strategies against it: https://github.com/Borewit/music-metadata?tab=readme-ov-file#how-can-i-traverse-a-long-list-of-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.
The limit control is done in
retrieveMetadata()
itself. A recursive call is made at the end, after a request resolves.https://github.com/hvianna/audioMotion.js/blob/a6b6e3e45fd8f0dfa69a485309e53d9969757eb0/src/index.js#L3299C1-L3302C10
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 see.
In that case better to take
Math.min(MAX_METADATA_REQUESTS, queue.length)
from the queue, and run those. No need to introduce an arbitrary counter for that.Sorry, I have not analyzed what you do with the remainder of the queue, and how you trigger new queue processing requests.
If the goal is to process the entire queue with respecting
MAX_METADATA_REQUESTS
in parallel, I suggest using something like p-limit, to easily control the number of parallel async tasks.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 don't think it's so simple, as
queue
here is the entire play queue, not just the files waiting to be parsed for metadata.Uh oh!
There was an error while loading. Please reload this page.
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.
MAX_METADATA_REQUESTS
seems to serve a dual role: it limits not only the number of parallel metadata requests, but also the number of tracks in the queue for which metadata will be retrieved. This means only a few tracks will ever get metadata assigned, while the rest remain untouched. Is it really beneficial to prioritize just the first few, while potentially ignoring the rest?It’s true that metadata retrieval is an expensive operation. The cost breaks down into:
But more important than parallelism or CPU-bound limits is something that hasn't been discussed yet:
Metadata extraction, even when well-structured, runs on the main thread. If we attempt to process the entire queue at once (even asynchronously), we risk blocking the UI and degrading application responsiveness. The most important goal in my onion, is not get the metadata fast, it’s to do so without compromising user experience.
Correct me if I am wrong, the rationale behind
MAX_METADATA_REQUESTS
is to balance responsiveness with progressive enhancement. But currently, it limits the total number of tracks processed, not just concurrency, which results in metadata being retrieved only for a small portion of the playlist.Taking a step back, and doing one step at a time:
I’ve updated the code to restore controlled parallel processing, ensuring that we still respect performance limits while improving structure through
async
/await
. I also removedwaitingMetadata
, and all promises are now properly tracked.The main goal of this PR remains improving maintainability and clarity, without sacrificing UX.
Maybe introduce a better way of processing the metadata in a different PR. Maybe run the metadata extraction in a worker thread?
Looking forward to your thoughts, @hvianna.
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.
😮 If it's only retrieving the metadata for the first four entries in the queue, something is off. Its intended purpose is solely to limit the number of concurrent requests, and it should parse all tracks in the queue eventually. You can check how it's working the dev branch.
Would appreciate your help on that, once we have this current version released!
Uh oh!
There was an error while loading. Please reload this page.
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.
Well on the dev branch that is what
retrieveMetadata
does.But
retrieveMetadata
is called byaddSongToPlayQueue
andaddSongToPlayQueue
is also spinned in parallel.So on the dev branch the number of parallel tasks spinned up, equal the number of tracks you added, by the total number of tracks remaining without metadata on the queue. Then a lot of things are trying to do the same thing, and somewhere in that mess
waitingMetadata
limits the number of parallel tasks.Here Knuth's optimization principle comes into play:
My advice: