Preserve media playback state when running appendMediaToMessage (#4771) * Preserve media playback state when running appendMediaToMessage * Better selector specificity * Fix local variable name * Move typedef to global.d.ts * Check for readyState on save/restore * Only check for currentSrc on restoration callback
Signed| @@ -85,6 +85,14 @@ declare global { | ||
| 85 | 85 | captioned?: boolean; |
| 86 | 86 | } |
| 87 | 87 | |
| 88 | + /** Media playback state */ | |
| 89 | + interface MediaState { | |
| 90 | + /** Current playback time */ | |
| 91 | + currentTime: number; | |
| 92 | + /** Whether the media is paused */ | |
| 93 | + paused: boolean; | |
| 94 | + } | |
| 95 | + | |
| 88 | 96 | // Global namespace modules |
| 89 | 97 | interface Window { |
| 90 | 98 | ai: any; |
| @@ -2077,9 +2077,12 @@ export function getMediaIndex(mes) { | ||
| 2077 | 2077 | export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROLL_BEHAVIOR.ADJUST) { |
| 2078 | 2078 | ensureMessageMediaIsArray(mes); |
| 2079 | 2079 | |
| 2080 | + const fileWrapper = messageElement.find('.mes_file_wrapper'); | |
| 2081 | + const mediaWrapper = messageElement.find('.mes_media_wrapper'); | |
| 2082 | + | |
| 2080 | 2083 | const hasMedia = Array.isArray(mes?.extra?.media) && mes.extra.media.length > 0; |
| 2081 | 2084 | const hasFiles = Array.isArray(mes?.extra?.files) && mes.extra.files.length > 0; |
| 2082 | 2085 | const mediaDisplay = hasMedia ? getMediaDisplay(mes) : null; |
| 2083 | 2086 | const hideMessageText = hasMedia && mes?.extra?.inline_image === false; |
| 2084 | 2087 | |
| 2085 | 2088 | const mediaBlocks = []; |
| @@ -2235,6 +2238,52 @@ export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROL | ||
| 2235 | 2238 | return appendImageAttachment(attachment, index); |
| 2236 | 2239 | } |
| 2237 | 2240 | |
| 2241 | + /** | |
| 2242 | + * Saves the current playback times of media elements in the message. | |
| 2243 | + * @returns {Map<string, MediaState>} Media playback times by source URL | |
| 2244 | + */ | |
| 2245 | + function saveMediaStates() { | |
| 2246 | + const states = new Map(); | |
| 2247 | + const media = mediaWrapper.find('video, audio'); | |
| 2248 | + media.each((_, element) => { | |
| 2249 | + if (element instanceof HTMLMediaElement) { | |
| 2250 | + if (!element.currentSrc || element.readyState === HTMLMediaElement.HAVE_NOTHING) { | |
| 2251 | + return; | |
| 2252 | + } | |
| 2253 | + const state = { currentTime: element.currentTime, paused: element.paused }; | |
| 2254 | + states.set(element.currentSrc, state); | |
| 2255 | + } | |
| 2256 | + }); | |
| 2257 | + return states; | |
| 2258 | + } | |
| 2259 | + | |
| 2260 | + /** | |
| 2261 | + * Restores the playback times of media elements in the message. | |
| 2262 | + * @param {Map<string, MediaState>} states Media playback times by source URL | |
| 2263 | + */ | |
| 2264 | + function restoreMediaStates(states) { | |
| 2265 | + const media = mediaWrapper.find('video, audio'); | |
| 2266 | + media.each((_, element) => { | |
| 2267 | + if (element instanceof HTMLMediaElement) { | |
| 2268 | + const restoreState = () => { | |
| 2269 | + if (!states.has(element.currentSrc)) { | |
| 2270 | + return; | |
| 2271 | + } | |
| 2272 | + const state = states.get(element.currentSrc); | |
| 2273 | + element.currentTime = state.currentTime; | |
| 2274 | + if (!state.paused) { | |
| 2275 | + element.play(); | |
| 2276 | + } | |
| 2277 | + }; | |
| 2278 | + if (element.readyState < HTMLMediaElement.HAVE_METADATA) { | |
| 2279 | + element.addEventListener('loadedmetadata', () => restoreState(), { once: true }); | |
| 2280 | + } else { | |
| 2281 | + restoreState(); | |
| 2282 | + } | |
| 2283 | + } | |
| 2284 | + }); | |
| 2285 | + } | |
| 2286 | + | |
| 2238 | 2287 | // Add media gallery to message |
| 2239 | 2288 | if (hasMedia && mediaDisplay === MEDIA_DISPLAY.GALLERY) { |
| 2240 | 2289 | const mediaIndex = getMediaIndex(mes); |
| @@ -2258,7 +2307,7 @@ export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROL | ||
| 2258 | 2307 | } |
| 2259 | 2308 | |
| 2260 | 2309 | // Remove existing file containers |
| 2261 | 2310 | messageElement.find('.mes_file_wrapper')fileWrapper.empty(); |
| 2262 | 2311 | |
| 2263 | 2312 | // Add files to message |
| 2264 | 2313 | if (hasFiles) { |
| @@ -2268,13 +2317,22 @@ export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROL | ||
| 2268 | 2317 | template.attr('data-index', index); |
| 2269 | 2318 | template.find('.mes_file_name').text(file.name).attr('title', file.name); |
| 2270 | 2319 | template.find('.mes_file_size').text(humanFileSize(file.size)).attr('title', file.size); |
| 2271 | 2320 | messageElement.find('.mes_file_wrapper')fileWrapper.append(template); |
| 2272 | 2321 | } |
| 2273 | 2322 | } |
| 2274 | 2323 | |
| 2324 | + // Early return if no media | |
| 2325 | + if (!hasMedia) { | |
| 2326 | + mediaWrapper.empty(); | |
| 2327 | + doAdjustScroll(); | |
| 2328 | + return; | |
| 2329 | + } | |
| 2330 | + | |
| 2275 | 2331 | // TODO: Consider making this awaitable |
| 2276 | 2332 | Promise.race([Promise.all(mediaPromises), delay(debounce_timeout.short)]).then(() => { |
| 2277 | - messageElement.find('.mes_media_wrapper').empty().append(mediaBlocks); | |
| 2333 | + const states = saveMediaStates(); | |
| 2334 | + mediaWrapper.empty().append(mediaBlocks); | |
| 2335 | + restoreMediaStates(states); | |
| 2278 | 2336 | doAdjustScroll(); |
| 2279 | 2337 | }); |
| 2280 | 2338 | } |