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 | captioned?: boolean; | 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 | // Global namespace modules | 96 | // Global namespace modules |
| 89 | interface Window { | 97 | interface Window { |
| 90 | ai: any; | 98 | ai: any; |
| @@ -2077,9 +2077,12 @@ export function getMediaIndex(mes) { | |||
| 2077 | export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROLL_BEHAVIOR.ADJUST) { | 2077 | export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROLL_BEHAVIOR.ADJUST) { |
| 2078 | ensureMessageMediaIsArray(mes); | 2078 | ensureMessageMediaIsArray(mes); |
| 2079 | 2079 | ||
| 2080 | const fileWrapper = messageElement.find('.mes_file_wrapper'); | ||
| 2081 | const mediaWrapper = messageElement.find('.mes_media_wrapper'); | ||
| 2082 | |||
| 2080 | const hasMedia = Array.isArray(mes?.extra?.media) && mes.extra.media.length > 0; | 2083 | const hasMedia = Array.isArray(mes?.extra?.media) && mes.extra.media.length > 0; |
| 2081 | const hasFiles = Array.isArray(mes?.extra?.files) && mes.extra.files.length > 0; | 2084 | const hasFiles = Array.isArray(mes?.extra?.files) && mes.extra.files.length > 0; |
| 2082 | const mediaDisplay = getMediaDisplay(mes); | 2085 | const mediaDisplay = hasMedia ? getMediaDisplay(mes) : null; |
| 2083 | const hideMessageText = hasMedia && mes?.extra?.inline_image === false; | 2086 | const hideMessageText = hasMedia && mes?.extra?.inline_image === false; |
| 2084 | 2087 | ||
| 2085 | const mediaBlocks = []; | 2088 | const mediaBlocks = []; |
| @@ -2235,6 +2238,52 @@ export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROL | |||
| 2235 | return appendImageAttachment(attachment, index); | 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 | // Add media gallery to message | 2287 | // Add media gallery to message |
| 2239 | if (hasMedia && mediaDisplay === MEDIA_DISPLAY.GALLERY) { | 2288 | if (hasMedia && mediaDisplay === MEDIA_DISPLAY.GALLERY) { |
| 2240 | const mediaIndex = getMediaIndex(mes); | 2289 | const mediaIndex = getMediaIndex(mes); |
| @@ -2258,7 +2307,7 @@ export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROL | |||
| 2258 | } | 2307 | } |
| 2259 | 2308 | ||
| 2260 | // Remove existing file containers | 2309 | // Remove existing file containers |
| 2261 | messageElement.find('.mes_file_wrapper').empty(); | 2310 | fileWrapper.empty(); |
| 2262 | 2311 | ||
| 2263 | // Add files to message | 2312 | // Add files to message |
| 2264 | if (hasFiles) { | 2313 | if (hasFiles) { |
| @@ -2268,13 +2317,22 @@ export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROL | |||
| 2268 | template.attr('data-index', index); | 2317 | template.attr('data-index', index); |
| 2269 | template.find('.mes_file_name').text(file.name).attr('title', file.name); | 2318 | template.find('.mes_file_name').text(file.name).attr('title', file.name); |
| 2270 | template.find('.mes_file_size').text(humanFileSize(file.size)).attr('title', file.size); | 2319 | template.find('.mes_file_size').text(humanFileSize(file.size)).attr('title', file.size); |
| 2271 | messageElement.find('.mes_file_wrapper').append(template); | 2320 | fileWrapper.append(template); |
| 2321 | } | ||
| 2272 | } | 2322 | } |
| 2323 | |||
| 2324 | // Early return if no media | ||
| 2325 | if (!hasMedia) { | ||
| 2326 | mediaWrapper.empty(); | ||
| 2327 | doAdjustScroll(); | ||
| 2328 | return; | ||
| 2273 | } | 2329 | } |
| 2274 | 2330 | ||
| 2275 | // TODO: Consider making this awaitable | 2331 | // TODO: Consider making this awaitable |
| 2276 | Promise.race([Promise.all(mediaPromises), delay(debounce_timeout.short)]).then(() => { | 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 | doAdjustScroll(); | 2336 | doAdjustScroll(); |
| 2279 | }); | 2337 | }); |
| 2280 | } | 2338 | } |