Fixed play button appearing for background videos in iPhones.
This commit is contained in:
parent
0d6a8b17cf
commit
166dafc217
@ -176,7 +176,11 @@ const CanvasBackground: React.FC<CanvasBackgroundProps> = ({
|
|||||||
isFadingIn={isFadingIn}
|
isFadingIn={isFadingIn}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Background video - z-1 keeps it below backdrop blur layer (z-5) */}
|
{/* Background video - z-1 keeps it below backdrop blur layer (z-5)
|
||||||
|
Note: muted attribute is always true for iOS autoplay compatibility.
|
||||||
|
Actual muted state is controlled via useBackgroundVideoPlayback hook
|
||||||
|
which sets video.muted property via JavaScript (useEffect).
|
||||||
|
webkit-playsinline is legacy attribute for older iOS versions. */}
|
||||||
{backgroundVideoUrl && (
|
{backgroundVideoUrl && (
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
@ -187,6 +191,7 @@ const CanvasBackground: React.FC<CanvasBackgroundProps> = ({
|
|||||||
loop={useNativeLoop}
|
loop={useNativeLoop}
|
||||||
muted={videoMuted}
|
muted={videoMuted}
|
||||||
playsInline
|
playsInline
|
||||||
|
webkit-playsinline=''
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,8 @@ import {
|
|||||||
mdiFullscreen,
|
mdiFullscreen,
|
||||||
mdiFullscreenExit,
|
mdiFullscreenExit,
|
||||||
mdiDelete,
|
mdiDelete,
|
||||||
|
mdiVolumeHigh,
|
||||||
|
mdiVolumeOff,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useOfflineMode } from '../../hooks/useOfflineMode';
|
import { useOfflineMode } from '../../hooks/useOfflineMode';
|
||||||
@ -38,6 +40,12 @@ interface RuntimeControlsProps {
|
|||||||
canvasWidth?: number;
|
canvasWidth?: number;
|
||||||
/** Canvas height in pixels (for positioning relative to canvas) */
|
/** Canvas height in pixels (for positioning relative to canvas) */
|
||||||
canvasHeight?: number;
|
canvasHeight?: number;
|
||||||
|
/** Whether to show the sound toggle button (video has sound enabled) */
|
||||||
|
showSoundButton?: boolean;
|
||||||
|
/** Current muted state of the video */
|
||||||
|
isMuted?: boolean;
|
||||||
|
/** Callback to toggle sound on/off */
|
||||||
|
onSoundToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -109,7 +117,9 @@ function ControlIcon({
|
|||||||
style={{
|
style={{
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
animation: spinning ? 'runtime-controls-spin 1s linear infinite' : undefined,
|
animation: spinning
|
||||||
|
? 'runtime-controls-spin 1s linear infinite'
|
||||||
|
: undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<path fill={fill} d={path} />
|
<path fill={fill} d={path} />
|
||||||
@ -420,6 +430,9 @@ export default function RuntimeControls({
|
|||||||
toggleFullscreen,
|
toggleFullscreen,
|
||||||
canvasWidth = 0,
|
canvasWidth = 0,
|
||||||
canvasHeight = 0,
|
canvasHeight = 0,
|
||||||
|
showSoundButton = false,
|
||||||
|
isMuted = true,
|
||||||
|
onSoundToggle,
|
||||||
}: RuntimeControlsProps) {
|
}: RuntimeControlsProps) {
|
||||||
// Counter-scale to resist pinch-zoom
|
// Counter-scale to resist pinch-zoom
|
||||||
const counterScale = useCounterZoom();
|
const counterScale = useCounterZoom();
|
||||||
@ -429,10 +442,12 @@ export default function RuntimeControls({
|
|||||||
// Canvas is centered with: left: 50%, top: 50%, transform: translate(-50%, -50%)
|
// Canvas is centered with: left: 50%, top: 50%, transform: translate(-50%, -50%)
|
||||||
// So we offset from viewport edge by (viewport - canvas) / 2 + padding
|
// So we offset from viewport edge by (viewport - canvas) / 2 + padding
|
||||||
const padding = 16;
|
const padding = 16;
|
||||||
const rightOffset = canvasWidth > 0 && viewport.width > 0
|
const rightOffset =
|
||||||
|
canvasWidth > 0 && viewport.width > 0
|
||||||
? (viewport.width - canvasWidth) / 2 + padding
|
? (viewport.width - canvasWidth) / 2 + padding
|
||||||
: padding;
|
: padding;
|
||||||
const topOffset = canvasHeight > 0 && viewport.height > 0
|
const topOffset =
|
||||||
|
canvasHeight > 0 && viewport.height > 0
|
||||||
? (viewport.height - canvasHeight) / 2 + padding
|
? (viewport.height - canvasHeight) / 2 + padding
|
||||||
: padding;
|
: padding;
|
||||||
|
|
||||||
@ -463,6 +478,14 @@ export default function RuntimeControls({
|
|||||||
onClick={toggleFullscreen}
|
onClick={toggleFullscreen}
|
||||||
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
||||||
/>
|
/>
|
||||||
|
{showSoundButton && onSoundToggle && (
|
||||||
|
<ControlButton
|
||||||
|
icon={isMuted ? mdiVolumeOff : mdiVolumeHigh}
|
||||||
|
color='info'
|
||||||
|
onClick={onSoundToggle}
|
||||||
|
title={isMuted ? 'Unmute sound' : 'Mute sound'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import { useCanvasScale } from '../hooks/useCanvasScale';
|
|||||||
import { CANVAS_CONFIG } from '../config/canvas.config';
|
import { CANVAS_CONFIG } from '../config/canvas.config';
|
||||||
import LayoutGuest from '../layouts/Guest';
|
import LayoutGuest from '../layouts/Guest';
|
||||||
import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator';
|
import { usePreloadOrchestrator } from '../hooks/usePreloadOrchestrator';
|
||||||
|
import { useVideoSoundControl } from '../hooks/useVideoSoundControl';
|
||||||
import { usePageDataLoader } from '../hooks/usePageDataLoader';
|
import { usePageDataLoader } from '../hooks/usePageDataLoader';
|
||||||
import { useProjectAssets } from '../hooks/useProjectAssets';
|
import { useProjectAssets } from '../hooks/useProjectAssets';
|
||||||
import { usePageNavigation } from '../hooks/usePageNavigation';
|
import { usePageNavigation } from '../hooks/usePageNavigation';
|
||||||
@ -92,8 +93,14 @@ export default function RuntimePresentation({
|
|||||||
|
|
||||||
// Canvas scale for responsive UI elements and letterbox mode
|
// Canvas scale for responsive UI elements and letterbox mode
|
||||||
// Uses page's design dimensions (saved at constructor save time) for presentation isolation
|
// Uses page's design dimensions (saved at constructor save time) for presentation isolation
|
||||||
const { cssVars, letterboxStyles, isPortrait, showRotatePrompt, canvasWidth, canvasHeight } =
|
const {
|
||||||
useCanvasScale({
|
cssVars,
|
||||||
|
letterboxStyles,
|
||||||
|
isPortrait,
|
||||||
|
showRotatePrompt,
|
||||||
|
canvasWidth,
|
||||||
|
canvasHeight,
|
||||||
|
} = useCanvasScale({
|
||||||
designWidth: currentPage?.design_width ?? undefined,
|
designWidth: currentPage?.design_width ?? undefined,
|
||||||
designHeight: currentPage?.design_height ?? undefined,
|
designHeight: currentPage?.design_height ?? undefined,
|
||||||
});
|
});
|
||||||
@ -534,7 +541,9 @@ export default function RuntimePresentation({
|
|||||||
// Background video playback settings from selected page
|
// Background video playback settings from selected page
|
||||||
const videoAutoplay = selectedPage?.background_video_autoplay ?? true;
|
const videoAutoplay = selectedPage?.background_video_autoplay ?? true;
|
||||||
const videoLoop = selectedPage?.background_video_loop ?? true;
|
const videoLoop = selectedPage?.background_video_loop ?? true;
|
||||||
const videoMuted = selectedPage?.background_video_muted ?? true;
|
// Note: pageVideoMuted is the page setting, but we use soundControl.isMuted for actual muted state
|
||||||
|
// This allows iOS to autoplay (starts muted) while giving user control via sound button
|
||||||
|
const pageVideoMuted = selectedPage?.background_video_muted ?? true;
|
||||||
const videoStartTime =
|
const videoStartTime =
|
||||||
selectedPage?.background_video_start_time != null
|
selectedPage?.background_video_start_time != null
|
||||||
? parseFloat(String(selectedPage.background_video_start_time))
|
? parseFloat(String(selectedPage.background_video_start_time))
|
||||||
@ -544,6 +553,14 @@ export default function RuntimePresentation({
|
|||||||
? parseFloat(String(selectedPage.background_video_end_time))
|
? parseFloat(String(selectedPage.background_video_end_time))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// Sound control hook for iOS autoplay compatibility
|
||||||
|
// Videos start muted (for iOS autoplay), user can unmute via sound button
|
||||||
|
const soundControl = useVideoSoundControl({
|
||||||
|
pageHasSound: pageVideoMuted === false, // Show button when page allows sound
|
||||||
|
hasBackgroundVideo: Boolean(backgroundVideoUrl),
|
||||||
|
videoUrl: backgroundVideoUrl, // Track video changes for page navigation reset
|
||||||
|
});
|
||||||
|
|
||||||
// Note: useBackgroundVideoPlayback is handled internally by CanvasBackground component
|
// Note: useBackgroundVideoPlayback is handled internally by CanvasBackground component
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@ -589,7 +606,10 @@ export default function RuntimePresentation({
|
|||||||
{/* Dark theme for browser UI and background */}
|
{/* Dark theme for browser UI and background */}
|
||||||
<meta name='theme-color' content='#000000' />
|
<meta name='theme-color' content='#000000' />
|
||||||
<meta name='color-scheme' content='dark' />
|
<meta name='color-scheme' content='dark' />
|
||||||
<meta name='apple-mobile-web-app-status-bar-style' content='black-translucent' />
|
<meta
|
||||||
|
name='apple-mobile-web-app-status-bar-style'
|
||||||
|
content='black-translucent'
|
||||||
|
/>
|
||||||
<style>{`
|
<style>{`
|
||||||
html, body { background-color: #000000 !important; }
|
html, body { background-color: #000000 !important; }
|
||||||
`}</style>
|
`}</style>
|
||||||
@ -681,7 +701,7 @@ export default function RuntimePresentation({
|
|||||||
}}
|
}}
|
||||||
videoAutoplay={videoAutoplay}
|
videoAutoplay={videoAutoplay}
|
||||||
videoLoop={videoLoop}
|
videoLoop={videoLoop}
|
||||||
videoMuted={videoMuted}
|
videoMuted={soundControl.isMuted}
|
||||||
videoStartTime={videoStartTime}
|
videoStartTime={videoStartTime}
|
||||||
videoEndTime={videoEndTime}
|
videoEndTime={videoEndTime}
|
||||||
videoStoragePath={selectedPage?.background_video_url}
|
videoStoragePath={selectedPage?.background_video_url}
|
||||||
@ -786,7 +806,7 @@ export default function RuntimePresentation({
|
|||||||
</div>
|
</div>
|
||||||
{/* End inner canvas container */}
|
{/* End inner canvas container */}
|
||||||
|
|
||||||
{/* Controls: Offline toggle and Fullscreen button */}
|
{/* Controls: Offline toggle, Fullscreen, and Sound buttons */}
|
||||||
{/* Positioned outside canvas to avoid scaling with canvas transform */}
|
{/* Positioned outside canvas to avoid scaling with canvas transform */}
|
||||||
<RuntimeControls
|
<RuntimeControls
|
||||||
projectId={project?.id || null}
|
projectId={project?.id || null}
|
||||||
@ -797,6 +817,9 @@ export default function RuntimePresentation({
|
|||||||
toggleFullscreen={toggleFullscreen}
|
toggleFullscreen={toggleFullscreen}
|
||||||
canvasWidth={canvasWidth}
|
canvasWidth={canvasWidth}
|
||||||
canvasHeight={canvasHeight}
|
canvasHeight={canvasHeight}
|
||||||
|
showSoundButton={soundControl.showSoundButton}
|
||||||
|
isMuted={soundControl.isMuted}
|
||||||
|
onSoundToggle={soundControl.toggleSound}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Toast notifications for offline download status */}
|
{/* Toast notifications for offline download status */}
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
@import "tailwind/_base.css";
|
@import 'tailwind/_base.css';
|
||||||
@import "tailwind/_components.css";
|
@import 'tailwind/_components.css';
|
||||||
@import "tailwind/_utilities.css";
|
@import 'tailwind/_utilities.css';
|
||||||
@import 'intro.js/introjs.css';
|
@import 'intro.js/introjs.css';
|
||||||
@import "_checkbox-radio-switch.css";
|
@import '_checkbox-radio-switch.css';
|
||||||
@import "_progress.css";
|
@import '_progress.css';
|
||||||
@import "_scrollbars.css";
|
@import '_scrollbars.css';
|
||||||
@import "_table.css";
|
@import '_table.css';
|
||||||
@import "_helper.css";
|
@import '_helper.css';
|
||||||
@import '_calendar.css';
|
@import '_calendar.css';
|
||||||
@import '_select-dropdown.css';
|
@import '_select-dropdown.css';
|
||||||
@import "_theme.css";
|
@import '_theme.css';
|
||||||
@import '_rich-text.css';
|
@import '_rich-text.css';
|
||||||
|
|
||||||
/* Page transition timing - single source of truth */
|
/* Page transition timing - single source of truth */
|
||||||
@ -37,7 +37,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.introjs-tooltip {
|
.introjs-tooltip {
|
||||||
@apply min-w-[400px] max-w-[480px] p-2 !important;
|
@apply min-w-[400px] max-w-[480px] p-2 !important;
|
||||||
}
|
}
|
||||||
@ -55,7 +54,7 @@
|
|||||||
.introjs-bullets ul li a.active {
|
.introjs-bullets ul li a.active {
|
||||||
@apply bg-blue-600 !important;
|
@apply bg-blue-600 !important;
|
||||||
}
|
}
|
||||||
.introjs-prevbutton{
|
.introjs-prevbutton {
|
||||||
@apply bg-transparent border border-blue-600 text-blue-600 !important;
|
@apply bg-transparent border border-blue-600 text-blue-600 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,8 +119,14 @@
|
|||||||
animation-name: page-crossfade-in;
|
animation-name: page-crossfade-in;
|
||||||
-webkit-animation-duration: var(--crossfade-duration, 700ms);
|
-webkit-animation-duration: var(--crossfade-duration, 700ms);
|
||||||
animation-duration: var(--crossfade-duration, 700ms);
|
animation-duration: var(--crossfade-duration, 700ms);
|
||||||
-webkit-animation-timing-function: var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
-webkit-animation-timing-function: var(
|
||||||
animation-timing-function: var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
--crossfade-easing,
|
||||||
|
cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
);
|
||||||
|
animation-timing-function: var(
|
||||||
|
--crossfade-easing,
|
||||||
|
cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
);
|
||||||
-webkit-animation-fill-mode: forwards;
|
-webkit-animation-fill-mode: forwards;
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
-webkit-animation-play-state: running;
|
-webkit-animation-play-state: running;
|
||||||
@ -143,8 +148,14 @@
|
|||||||
animation-name: page-crossfade-out;
|
animation-name: page-crossfade-out;
|
||||||
-webkit-animation-duration: var(--crossfade-duration, 700ms);
|
-webkit-animation-duration: var(--crossfade-duration, 700ms);
|
||||||
animation-duration: var(--crossfade-duration, 700ms);
|
animation-duration: var(--crossfade-duration, 700ms);
|
||||||
-webkit-animation-timing-function: var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
-webkit-animation-timing-function: var(
|
||||||
animation-timing-function: var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
--crossfade-easing,
|
||||||
|
cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
);
|
||||||
|
animation-timing-function: var(
|
||||||
|
--crossfade-easing,
|
||||||
|
cubic-bezier(0.4, 0, 0.2, 1)
|
||||||
|
);
|
||||||
-webkit-animation-fill-mode: forwards;
|
-webkit-animation-fill-mode: forwards;
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
-webkit-animation-play-state: running;
|
-webkit-animation-play-state: running;
|
||||||
@ -183,8 +194,10 @@
|
|||||||
/* Use this for better Safari stability - transitions don't have the "snap" issue
|
/* Use this for better Safari stability - transitions don't have the "snap" issue
|
||||||
when state changes because they interpolate between current and target values */
|
when state changes because they interpolate between current and target values */
|
||||||
.crossfade-transition {
|
.crossfade-transition {
|
||||||
transition: opacity var(--crossfade-duration, 700ms) var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
transition: opacity var(--crossfade-duration, 700ms)
|
||||||
-webkit-transition: opacity var(--crossfade-duration, 700ms) var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
||||||
|
-webkit-transition: opacity var(--crossfade-duration, 700ms)
|
||||||
|
var(--crossfade-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
||||||
-webkit-transform: translate3d(0, 0, 0);
|
-webkit-transform: translate3d(0, 0, 0);
|
||||||
transform: translate3d(0, 0, 0);
|
transform: translate3d(0, 0, 0);
|
||||||
-webkit-backface-visibility: hidden;
|
-webkit-backface-visibility: hidden;
|
||||||
@ -355,3 +368,26 @@
|
|||||||
font-family: 'Instrument Sans Variable', sans-serif;
|
font-family: 'Instrument Sans Variable', sans-serif;
|
||||||
font-stretch: 100%;
|
font-stretch: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* iOS Safari: Hide native video play button overlay
|
||||||
|
These selectors target WebKit-specific pseudo-elements that appear
|
||||||
|
when autoplay is blocked or when iOS detects an unmuted video */
|
||||||
|
video::-webkit-media-controls-start-playback-button {
|
||||||
|
display: none !important;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
video::-webkit-media-controls-overlay-play-button {
|
||||||
|
display: none !important;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent controls from appearing on autoplay fail */
|
||||||
|
video::-webkit-media-controls-panel {
|
||||||
|
display: none !important;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
video::-webkit-media-controls {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|||||||
100
frontend/src/hooks/useVideoSoundControl.ts
Normal file
100
frontend/src/hooks/useVideoSoundControl.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* useVideoSoundControl Hook
|
||||||
|
*
|
||||||
|
* Manages video sound state with iOS autoplay compatibility.
|
||||||
|
* Videos start muted to ensure autoplay works on iOS WebKit browsers,
|
||||||
|
* then can be unmuted by user interaction.
|
||||||
|
*
|
||||||
|
* iOS WebKit autoplay policy:
|
||||||
|
* - Videos CAN autoplay if they have `playsinline` AND `muted` attributes
|
||||||
|
* - Unmuted videos require user interaction → native play button appears
|
||||||
|
* - By forcing muted start + custom sound button, we avoid native controls
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
|
import { logger } from '../lib/logger';
|
||||||
|
|
||||||
|
export interface UseVideoSoundControlOptions {
|
||||||
|
/** Whether page settings allow sound (background_video_muted === false) */
|
||||||
|
pageHasSound: boolean;
|
||||||
|
/** Whether page has a background video */
|
||||||
|
hasBackgroundVideo: boolean;
|
||||||
|
/** Current video URL - used to detect page changes (optional) */
|
||||||
|
videoUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseVideoSoundControlResult {
|
||||||
|
/** Current muted state (always starts true for iOS compatibility) */
|
||||||
|
isMuted: boolean;
|
||||||
|
/** Whether to show sound toggle button */
|
||||||
|
showSoundButton: boolean;
|
||||||
|
/** Toggle muted state */
|
||||||
|
toggleSound: () => void;
|
||||||
|
/** Force muted state (for page changes) */
|
||||||
|
setMuted: (muted: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing video sound state with iOS autoplay compatibility.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { isMuted, showSoundButton, toggleSound } = useVideoSoundControl({
|
||||||
|
* pageHasSound: selectedPage?.background_video_muted === false,
|
||||||
|
* hasBackgroundVideo: Boolean(backgroundVideoUrl),
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // Pass to RuntimeControls
|
||||||
|
* <RuntimeControls
|
||||||
|
* showSoundButton={showSoundButton}
|
||||||
|
* isMuted={isMuted}
|
||||||
|
* onSoundToggle={toggleSound}
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* // Pass to CanvasBackground (always muted for autoplay)
|
||||||
|
* <CanvasBackground videoMuted={isMuted} />
|
||||||
|
*/
|
||||||
|
export function useVideoSoundControl({
|
||||||
|
pageHasSound,
|
||||||
|
hasBackgroundVideo,
|
||||||
|
videoUrl,
|
||||||
|
}: UseVideoSoundControlOptions): UseVideoSoundControlResult {
|
||||||
|
// Always start muted for iOS autoplay compatibility
|
||||||
|
const [isMuted, setIsMuted] = useState(true);
|
||||||
|
|
||||||
|
// Track previous video URL to detect page changes
|
||||||
|
const prevVideoUrl = useRef(videoUrl);
|
||||||
|
|
||||||
|
// Reset to muted when video changes (page navigation)
|
||||||
|
// This ensures iOS autoplay works on every page - new videos always start muted
|
||||||
|
useEffect(() => {
|
||||||
|
// Only reset if there's a new video (URL changed and we have a video)
|
||||||
|
if (prevVideoUrl.current !== videoUrl && videoUrl) {
|
||||||
|
setIsMuted(true);
|
||||||
|
logger.debug('[useVideoSoundControl] Video changed, resetting to muted', {
|
||||||
|
from: prevVideoUrl.current?.slice(-20),
|
||||||
|
to: videoUrl?.slice(-20),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
prevVideoUrl.current = videoUrl;
|
||||||
|
}, [videoUrl]);
|
||||||
|
|
||||||
|
const toggleSound = useCallback(() => {
|
||||||
|
setIsMuted((prev) => {
|
||||||
|
logger.debug('[useVideoSoundControl] Toggle sound:', {
|
||||||
|
from: prev,
|
||||||
|
to: !prev,
|
||||||
|
});
|
||||||
|
return !prev;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isMuted,
|
||||||
|
// Show button only if page allows sound AND has a background video
|
||||||
|
showSoundButton: pageHasSound && hasBackgroundVideo,
|
||||||
|
toggleSound,
|
||||||
|
setMuted: setIsMuted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useVideoSoundControl;
|
||||||
@ -88,6 +88,7 @@ import {
|
|||||||
type NavigationElementType,
|
type NavigationElementType,
|
||||||
} from '../context/ConstructorContext';
|
} from '../context/ConstructorContext';
|
||||||
import { useCanvasScale } from '../hooks/useCanvasScale';
|
import { useCanvasScale } from '../hooks/useCanvasScale';
|
||||||
|
import { useVideoSoundControl } from '../hooks/useVideoSoundControl';
|
||||||
import { CANVAS_CONFIG } from '../config/canvas.config';
|
import { CANVAS_CONFIG } from '../config/canvas.config';
|
||||||
|
|
||||||
// Constructor helpers (extracted utilities)
|
// Constructor helpers (extracted utilities)
|
||||||
@ -195,6 +196,14 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
backgroundVideoEndTime,
|
backgroundVideoEndTime,
|
||||||
} = usePageBackground();
|
} = usePageBackground();
|
||||||
|
|
||||||
|
// Sound control hook for iOS autoplay compatibility
|
||||||
|
// Videos start muted (for iOS autoplay), can be controlled via page settings
|
||||||
|
const soundControl = useVideoSoundControl({
|
||||||
|
pageHasSound: backgroundVideoMuted === false, // Show button when page allows sound
|
||||||
|
hasBackgroundVideo: Boolean(backgroundVideoUrl),
|
||||||
|
videoUrl: backgroundVideoUrl, // Track video changes for page navigation reset
|
||||||
|
});
|
||||||
|
|
||||||
const [selectedMenuItem, setSelectedMenuItem] =
|
const [selectedMenuItem, setSelectedMenuItem] =
|
||||||
useState<EditorMenuItem>('none');
|
useState<EditorMenuItem>('none');
|
||||||
// Transition preview state managed by useTransitionPreview hook (below)
|
// Transition preview state managed by useTransitionPreview hook (below)
|
||||||
@ -1580,7 +1589,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
|
|||||||
}}
|
}}
|
||||||
videoAutoplay={backgroundVideoAutoplay}
|
videoAutoplay={backgroundVideoAutoplay}
|
||||||
videoLoop={backgroundVideoLoop}
|
videoLoop={backgroundVideoLoop}
|
||||||
videoMuted={backgroundVideoMuted}
|
videoMuted={soundControl.isMuted}
|
||||||
videoStartTime={backgroundVideoStartTime}
|
videoStartTime={backgroundVideoStartTime}
|
||||||
videoEndTime={backgroundVideoEndTime}
|
videoEndTime={backgroundVideoEndTime}
|
||||||
videoStoragePath={
|
videoStoragePath={
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user