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 -4Ignore whitespace
public/global.d.ts+8 -0
@@ -85,6 +85,14 @@ declare global {
8585 captioned?: boolean;
8686 }
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+
8896 // Global namespace modules
8997 interface Window {
9098 ai: any;
public/script.js+62 -4
@@ -2077,9 +2077,12 @@ export function getMediaIndex(mes) {
20772077export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROLL_BEHAVIOR.ADJUST) {
20782078 ensureMessageMediaIsArray(mes);
20792079
2080+ const fileWrapper = messageElement.find('.mes_file_wrapper');
2081+ const mediaWrapper = messageElement.find('.mes_media_wrapper');
2082+
20802083 const hasMedia = Array.isArray(mes?.extra?.media) && mes.extra.media.length > 0;
20812084 const hasFiles = Array.isArray(mes?.extra?.files) && mes.extra.files.length > 0;
20822085 const mediaDisplay = hasMedia ? getMediaDisplay(mes) : null;
20832086 const hideMessageText = hasMedia && mes?.extra?.inline_image === false;
20842087
20852088 const mediaBlocks = [];
@@ -2235,6 +2238,52 @@ export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROL
22352238 return appendImageAttachment(attachment, index);
22362239 }
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+
22382287 // Add media gallery to message
22392288 if (hasMedia && mediaDisplay === MEDIA_DISPLAY.GALLERY) {
22402289 const mediaIndex = getMediaIndex(mes);
@@ -2258,7 +2307,7 @@ export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROL
22582307 }
22592308
22602309 // Remove existing file containers
22612310 messageElement.find('.mes_file_wrapper')fileWrapper.empty();
22622311
22632312 // Add files to message
22642313 if (hasFiles) {
@@ -2268,13 +2317,22 @@ export function appendMediaToMessage(mes, messageElement, scrollBehavior = SCROL
22682317 template.attr('data-index', index);
22692318 template.find('.mes_file_name').text(file.name).attr('title', file.name);
22702319 template.find('.mes_file_size').text(humanFileSize(file.size)).attr('title', file.size);
22712320 messageElement.find('.mes_file_wrapper')fileWrapper.append(template);
22722321 }
22732322 }
22742323
2324+ // Early return if no media
2325+ if (!hasMedia) {
2326+ mediaWrapper.empty();
2327+ doAdjustScroll();
2328+ return;
2329+ }
2330+
22752331 // TODO: Consider making this awaitable
22762332 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);
22782336 doAdjustScroll();
22792337 });
22802338}