fixed transitions
This commit is contained in:
parent
85d628fc54
commit
87250b8bfe
@ -19,6 +19,9 @@ const { logger } = require('../utils/logger');
|
||||
|
||||
// Cache for project history-mode status (cleared per request cycle)
|
||||
const projectHistoryModeCache = new Map();
|
||||
const projectRegenInProgress = new Set();
|
||||
const singleReverseGenerationInProgress = new Set();
|
||||
const reverseGenerationPromiseByStorageKey = new Map();
|
||||
|
||||
// Create base service from factory
|
||||
const BaseService = createEntityService(Tour_pagesDBApi, {
|
||||
@ -276,11 +279,10 @@ class TourPagesService extends BaseService {
|
||||
const storageKey = element.transitionVideoUrl;
|
||||
|
||||
try {
|
||||
// Get or generate reversed video URL
|
||||
const reversedUrl = await TourPagesService.getOrGenerateReversedVariant(
|
||||
storageKey,
|
||||
currentUser,
|
||||
);
|
||||
// Fast path for request cycle: only reuse existing reversed variants.
|
||||
// Missing variants are generated asynchronously to keep save requests fast.
|
||||
const reversedUrl =
|
||||
await TourPagesService.getExistingReversedVariant(storageKey);
|
||||
|
||||
if (reversedUrl && reversedUrl !== element.reverseVideoUrl) {
|
||||
element.reverseVideoUrl = reversedUrl;
|
||||
@ -292,43 +294,39 @@ class TourPagesService extends BaseService {
|
||||
isForward,
|
||||
storageKey,
|
||||
},
|
||||
'Added reversed video URL to element',
|
||||
'Added existing reversed video URL to element',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (projectId) {
|
||||
TourPagesService.enqueueSingleReverseGeneration({
|
||||
projectId,
|
||||
pageId: data.id,
|
||||
storageKey,
|
||||
currentUser,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err, storageKey },
|
||||
'Failed to get/generate reversed variant',
|
||||
);
|
||||
// Continue without reversed video - button will be disabled in frontend
|
||||
logger.error({ err, storageKey }, 'Failed to process reversed variant');
|
||||
// Continue without reversed video. Background generation can still populate later.
|
||||
}
|
||||
}
|
||||
|
||||
// If this page has a history-mode back button, trigger regeneration of
|
||||
// other pages' forward elements that are missing reversed videos.
|
||||
// This runs every save but only processes elements without reverseVideoUrl.
|
||||
if (
|
||||
thisPageHasHistoryMode &&
|
||||
projectId &&
|
||||
!options._skipHistoryModeCheck
|
||||
) {
|
||||
if (thisPageHasHistoryMode && projectId && !options._skipHistoryModeCheck) {
|
||||
logger.info(
|
||||
{ projectId },
|
||||
'History mode back button detected - regenerating forward elements',
|
||||
'History mode back button detected - scheduling forward regeneration',
|
||||
);
|
||||
|
||||
try {
|
||||
await TourPagesService.regenerateProjectReversedVideos(
|
||||
projectId,
|
||||
currentUser,
|
||||
data.id,
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
{ err, projectId },
|
||||
'Failed to regenerate project reversed videos',
|
||||
);
|
||||
}
|
||||
TourPagesService.enqueueProjectReversedVideosRegeneration(
|
||||
projectId,
|
||||
currentUser,
|
||||
data.id,
|
||||
);
|
||||
}
|
||||
|
||||
if (wasModified) {
|
||||
@ -343,13 +341,13 @@ class TourPagesService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get existing reversed variant URL or generate one
|
||||
* Get existing reversed variant URL if already generated.
|
||||
* Does not trigger generation.
|
||||
*
|
||||
* @param {string} storageKey - Original video storage key
|
||||
* @param {Object} currentUser - Current user for permissions
|
||||
* @returns {Promise<string|null>} Reversed video storage key or null
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
static async getOrGenerateReversedVariant(storageKey, currentUser) {
|
||||
// Find the asset by storage key
|
||||
static async getExistingReversedVariant(storageKey) {
|
||||
const asset = await AssetsDBApi.findBy({ storage_key: storageKey });
|
||||
|
||||
if (!asset) {
|
||||
@ -357,17 +355,203 @@ class TourPagesService extends BaseService {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if reversed variant already exists
|
||||
const variants = asset.asset_variants_asset || [];
|
||||
const reversedVariant = variants.find((v) => v.variant_type === 'reversed');
|
||||
|
||||
if (reversedVariant) {
|
||||
logger.debug({ assetId: asset.id }, 'Using existing reversed variant');
|
||||
return reversedVariant.storage_key;
|
||||
return reversedVariant?.storage_key || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue one reversed video generation task without blocking request cycle.
|
||||
* Updates all matching project elements after generation succeeds.
|
||||
*
|
||||
* @param {Object} task
|
||||
* @param {string} task.projectId
|
||||
* @param {string} task.storageKey
|
||||
* @param {Object} task.currentUser
|
||||
* @param {string} [task.pageId]
|
||||
*/
|
||||
static enqueueSingleReverseGeneration({ projectId, storageKey, currentUser, pageId }) {
|
||||
if (!projectId || !storageKey) return;
|
||||
|
||||
const taskKey = `${projectId}:${storageKey}`;
|
||||
if (singleReverseGenerationInProgress.has(taskKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate reversed video
|
||||
return TourPagesService.generateReversedVariant(asset, currentUser);
|
||||
singleReverseGenerationInProgress.add(taskKey);
|
||||
|
||||
setImmediate(async () => {
|
||||
const log = logger.child({
|
||||
projectId,
|
||||
storageKey,
|
||||
pageId,
|
||||
operation: 'singleReverseGeneration',
|
||||
});
|
||||
|
||||
try {
|
||||
log.info('Starting background reversed variant generation');
|
||||
const reversedUrl = await TourPagesService.getOrGenerateReversedVariant(
|
||||
storageKey,
|
||||
currentUser,
|
||||
);
|
||||
|
||||
if (!reversedUrl) {
|
||||
log.warn('Background reversed variant generation finished without result');
|
||||
return;
|
||||
}
|
||||
|
||||
await TourPagesService.applyReversedUrlToProjectElements(
|
||||
projectId,
|
||||
storageKey,
|
||||
reversedUrl,
|
||||
currentUser,
|
||||
);
|
||||
} catch (err) {
|
||||
log.error({ err }, 'Background reversed generation failed');
|
||||
} finally {
|
||||
singleReverseGenerationInProgress.delete(taskKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue project-wide reverse regeneration (history mode) without blocking save request.
|
||||
*
|
||||
* @param {string} projectId
|
||||
* @param {Object} currentUser
|
||||
* @param {string} [excludePageId]
|
||||
*/
|
||||
static enqueueProjectReversedVideosRegeneration(
|
||||
projectId,
|
||||
currentUser,
|
||||
excludePageId,
|
||||
) {
|
||||
if (!projectId) return;
|
||||
if (projectRegenInProgress.has(projectId)) return;
|
||||
|
||||
projectRegenInProgress.add(projectId);
|
||||
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await TourPagesService.regenerateProjectReversedVideos(
|
||||
projectId,
|
||||
currentUser,
|
||||
excludePageId,
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error({ err, projectId }, 'Background project regeneration failed');
|
||||
} finally {
|
||||
projectRegenInProgress.delete(projectId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Write generated reversed URL into all matching navigation elements in dev pages.
|
||||
*
|
||||
* @param {string} projectId
|
||||
* @param {string} storageKey
|
||||
* @param {string} reversedUrl
|
||||
* @param {Object} currentUser
|
||||
*/
|
||||
static async applyReversedUrlToProjectElements(
|
||||
projectId,
|
||||
storageKey,
|
||||
reversedUrl,
|
||||
currentUser,
|
||||
) {
|
||||
if (!projectId || !storageKey || !reversedUrl) return;
|
||||
|
||||
const log = logger.child({
|
||||
projectId,
|
||||
storageKey,
|
||||
operation: 'applyReversedUrlToProjectElements',
|
||||
});
|
||||
|
||||
const { rows: pages } = await Tour_pagesDBApi.findAll({
|
||||
projectId,
|
||||
environment: 'dev',
|
||||
});
|
||||
|
||||
let pagesUpdated = 0;
|
||||
|
||||
for (const page of pages) {
|
||||
const uiSchema =
|
||||
typeof page.ui_schema_json === 'string'
|
||||
? JSON.parse(page.ui_schema_json || '{}')
|
||||
: page.ui_schema_json || {};
|
||||
|
||||
if (!uiSchema.elements || !Array.isArray(uiSchema.elements)) continue;
|
||||
|
||||
let pageModified = false;
|
||||
|
||||
for (const element of uiSchema.elements) {
|
||||
const shouldAttachReverse =
|
||||
TourPagesService.isBackElement(element) ||
|
||||
TourPagesService.isForwardElementWithTarget(element);
|
||||
|
||||
if (!shouldAttachReverse) continue;
|
||||
if (element.transitionVideoUrl !== storageKey) continue;
|
||||
if (element.reverseVideoUrl) continue;
|
||||
|
||||
element.reverseVideoUrl = reversedUrl;
|
||||
pageModified = true;
|
||||
}
|
||||
|
||||
if (!pageModified) continue;
|
||||
|
||||
await Tour_pagesDBApi.partialUpdate(
|
||||
page.id,
|
||||
{ ui_schema_json: JSON.stringify(uiSchema) },
|
||||
{ currentUser },
|
||||
);
|
||||
pagesUpdated++;
|
||||
}
|
||||
|
||||
if (pagesUpdated > 0) {
|
||||
log.info({ pagesUpdated }, 'Applied reversed URL to project elements');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get existing reversed variant URL or generate one
|
||||
* @param {string} storageKey - Original video storage key
|
||||
* @param {Object} currentUser - Current user for permissions
|
||||
* @returns {Promise<string|null>} Reversed video storage key or null
|
||||
*/
|
||||
static async getOrGenerateReversedVariant(storageKey, currentUser) {
|
||||
const existingVariant =
|
||||
await TourPagesService.getExistingReversedVariant(storageKey);
|
||||
|
||||
if (existingVariant) {
|
||||
return existingVariant;
|
||||
}
|
||||
|
||||
if (reverseGenerationPromiseByStorageKey.has(storageKey)) {
|
||||
return reverseGenerationPromiseByStorageKey.get(storageKey);
|
||||
}
|
||||
|
||||
const generationPromise = (async () => {
|
||||
// Find the asset by storage key
|
||||
const asset = await AssetsDBApi.findBy({ storage_key: storageKey });
|
||||
|
||||
if (!asset) {
|
||||
logger.warn({ storageKey }, 'Asset not found for transition');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate reversed video
|
||||
return TourPagesService.generateReversedVariant(asset, currentUser);
|
||||
})();
|
||||
|
||||
reverseGenerationPromiseByStorageKey.set(storageKey, generationPromise);
|
||||
|
||||
try {
|
||||
return await generationPromise;
|
||||
} finally {
|
||||
reverseGenerationPromiseByStorageKey.delete(storageKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -18,7 +18,6 @@ import axios from 'axios';
|
||||
import { logger } from '../lib/logger';
|
||||
import {
|
||||
markPresignedUrlFailed,
|
||||
isRelativeStoragePath,
|
||||
resolveAssetPlaybackUrl,
|
||||
isPresignedUrl,
|
||||
buildProxyUrl,
|
||||
@ -153,7 +152,6 @@ export function useTransitionPlayback(
|
||||
const activeSourceUrlRef = useRef<string | null>(null);
|
||||
const lastLoadedBlobUrlRef = useRef<string | null>(null);
|
||||
const lastLoadedSourceUrlRef = useRef<string | null>(null);
|
||||
const didTryFallbackRef = useRef(false);
|
||||
const didTryDecodeRetryRef = useRef(false);
|
||||
const currentPlayableUrlRef = useRef<string | null>(null);
|
||||
const startWatchdogTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
@ -175,9 +173,8 @@ export function useTransitionPlayback(
|
||||
// Otherwise, use the original videoUrl
|
||||
const sourceUrl = useMemo(() => {
|
||||
if (!transition) return '';
|
||||
// Use reversed video if this is back navigation with a separate reversed video
|
||||
if (transition.isBack && transition.reverseVideoUrl) {
|
||||
return transition.reverseVideoUrl;
|
||||
if (transition.isBack) {
|
||||
return transition.reverseVideoUrl || '';
|
||||
}
|
||||
return transition.videoUrl;
|
||||
}, [transition]);
|
||||
@ -185,8 +182,8 @@ export function useTransitionPlayback(
|
||||
// Storage key for cache lookup - use reversed video key for back navigation
|
||||
const storageKey = useMemo(() => {
|
||||
if (!transition) return undefined;
|
||||
if (transition.isBack && transition.reverseVideoUrl) {
|
||||
return transition.reverseVideoUrl;
|
||||
if (transition.isBack) {
|
||||
return transition.reverseVideoUrl || undefined;
|
||||
}
|
||||
return transition.storageKey;
|
||||
}, [transition]);
|
||||
@ -304,7 +301,16 @@ export function useTransitionPlayback(
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
const currentTransition = transitionRef.current;
|
||||
if (!currentTransition || !video || !sourceUrl) {
|
||||
if (!currentTransition || !video) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sourceUrl) {
|
||||
logger.info('No playable transition source, skipping playback', {
|
||||
isBack: currentTransition.isBack,
|
||||
targetPageId: currentTransition.targetPageId,
|
||||
});
|
||||
void finishPlayback('missing-source');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -321,7 +327,6 @@ export function useTransitionPlayback(
|
||||
activeSourceUrlRef.current = sourceKey;
|
||||
didFinishRef.current = false;
|
||||
didStartPlaybackRef.current = false;
|
||||
didTryFallbackRef.current = false;
|
||||
didTryDecodeRetryRef.current = false;
|
||||
currentPlayableUrlRef.current = null;
|
||||
setPhase('preparing');
|
||||
@ -616,35 +621,6 @@ export function useTransitionPlayback(
|
||||
return;
|
||||
}
|
||||
|
||||
const currentUrl = currentPlayableUrlRef.current;
|
||||
if (
|
||||
currentUrl &&
|
||||
isPresignedUrl(currentUrl) &&
|
||||
!didTryFallbackRef.current
|
||||
) {
|
||||
logger.info('Presigned URL failed, trying proxy fallback', {
|
||||
url: currentUrl.slice(0, 80),
|
||||
});
|
||||
|
||||
const originalVideoUrl = currentTransition.videoUrl;
|
||||
if (originalVideoUrl && isRelativeStoragePath(originalVideoUrl)) {
|
||||
markPresignedUrlFailed(originalVideoUrl);
|
||||
}
|
||||
|
||||
const videoStorageKey = currentTransition.videoUrl;
|
||||
if (videoStorageKey && isRelativeStoragePath(videoStorageKey)) {
|
||||
const fallbackUrl = buildProxyUrl(videoStorageKey);
|
||||
didTryFallbackRef.current = true;
|
||||
video.pause();
|
||||
video.src = fallbackUrl;
|
||||
currentPlayableUrlRef.current = fallbackUrl;
|
||||
video.currentTime = 0;
|
||||
video.load();
|
||||
attemptPlay();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handleError('video-error');
|
||||
};
|
||||
|
||||
|
||||
@ -50,11 +50,31 @@ export const findIncomingNavigationElement = (
|
||||
);
|
||||
const elements = Array.isArray(uiSchema.elements) ? uiSchema.elements : [];
|
||||
|
||||
// Find navigation element pointing to target page
|
||||
const found = elements.find(
|
||||
const candidateElements = elements.filter(
|
||||
(el) =>
|
||||
isNavigationType(String(el.type || '')) &&
|
||||
el.targetPageSlug === targetPageSlug,
|
||||
el.targetPageSlug === targetPageSlug &&
|
||||
!isBackNavigation(el as unknown as NavigableElement),
|
||||
);
|
||||
|
||||
const scoreCandidate = (candidate: Record<string, unknown>): number => {
|
||||
const hasTransition = Boolean(candidate.transitionVideoUrl);
|
||||
const hasReverse = Boolean(candidate.reverseVideoUrl);
|
||||
|
||||
if (hasTransition && hasReverse) return 3;
|
||||
if (hasTransition) return 2;
|
||||
if (hasReverse) return 1;
|
||||
return 0;
|
||||
};
|
||||
|
||||
const found = candidateElements.reduce<Record<string, unknown> | null>(
|
||||
(best, candidate) => {
|
||||
if (!best) return candidate;
|
||||
const bestScore = scoreCandidate(best);
|
||||
const candidateScore = scoreCandidate(candidate);
|
||||
return candidateScore > bestScore ? candidate : best;
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
if (!found) return null;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user