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

b10f6803719c16dc064d555531402956787c84a9

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
2 files changed, +70 -4Showing whitespace changes
public/global.d.ts+8 -0
@@ -85,6 +85,14 @@ declare global {
85 captioned?: boolean;85 captioned?: boolean;
86 }86 }
8787
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 modules96 // Global namespace modules
89 interface Window {97 interface Window {
90 ai: any;98 ai: any;
public/script.js+62 -4
@@ -2077,9 +2077,12 @@ export function getMediaIndex(mes) {
2077export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROLL_BEHAVIOR.ADJUST) {2077export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROLL_BEHAVIOR.ADJUST) {
2078 ensureMessageMediaIsArray(mes);2078 ensureMessageMediaIsArray(mes);
20792079
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;
20842087
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 }
22372240
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 message2287 // 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 }
22592308
2260 // Remove existing file containers2309 // Remove existing file containers
2261 messageElement.find('.mes_file_wrapper').empty();2310 fileWrapper.empty();
22622311
2263 // Add files to message2312 // 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 }
22742330
2275 // TODO: Consider making this awaitable2331 // 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}