From d29ac7c8f056ad015db73088f90e68cd307b8859 Mon Sep 17 00:00:00 2001 From: Dmitri Date: Fri, 5 Jun 2026 16:19:50 +0200 Subject: [PATCH] improved background audio functionaliry --- backend/src/db/api/tour_pages.js | 16 ++ ...605000001-add-background-audio-settings.js | 47 ++++ backend/src/db/models/tour_pages.js | 24 ++ .../Constructor/BackgroundSettingsEditor.tsx | 96 +++++++- .../Constructor/CanvasBackground.tsx | 34 ++- .../Constructor/ElementEditorPanel.tsx | 6 + frontend/src/components/Constructor/types.ts | 73 ++---- .../src/components/RuntimePresentation.tsx | 35 ++- .../elements/AudioPlayerElement.tsx | 30 ++- .../elements/VideoPlayerElement.tsx | 31 ++- frontend/src/context/ConstructorContext.tsx | 6 + .../src/hooks/audio/useAudioEventManager.ts | 115 +++++++++ frontend/src/hooks/useAudioEffects.ts | 28 ++- .../src/hooks/useBackgroundAudioPlayback.ts | 218 ++++++++++++++++++ .../src/hooks/useConstructorPageActions.ts | 15 ++ frontend/src/hooks/usePageBackground.ts | 30 +++ frontend/src/lib/backgroundAudioController.ts | 80 +++++++ frontend/src/pages/constructor.tsx | 30 +++ frontend/src/types/constructor.ts | 39 ++++ frontend/src/types/entities.ts | 5 + frontend/src/types/runtime.ts | 5 + 21 files changed, 879 insertions(+), 84 deletions(-) create mode 100644 backend/src/db/migrations/20260605000001-add-background-audio-settings.js create mode 100644 frontend/src/hooks/audio/useAudioEventManager.ts create mode 100644 frontend/src/hooks/useBackgroundAudioPlayback.ts create mode 100644 frontend/src/lib/backgroundAudioController.ts diff --git a/backend/src/db/api/tour_pages.js b/backend/src/db/api/tour_pages.js index 5b89bc9..c38e37b 100644 --- a/backend/src/db/api/tour_pages.js +++ b/backend/src/db/api/tour_pages.js @@ -73,6 +73,22 @@ class Tour_pagesDBApi extends GenericDBApi { background_image_url: data.background_image_url || null, background_video_url: data.background_video_url || null, background_audio_url: data.background_audio_url || null, + background_audio_autoplay: + data.background_audio_autoplay !== undefined + ? data.background_audio_autoplay + : true, + background_audio_loop: + data.background_audio_loop !== undefined + ? data.background_audio_loop + : true, + background_audio_start_time: + data.background_audio_start_time !== undefined + ? data.background_audio_start_time + : null, + background_audio_end_time: + data.background_audio_end_time !== undefined + ? data.background_audio_end_time + : null, background_loop: data.background_loop || false, background_video_autoplay: data.background_video_autoplay !== undefined diff --git a/backend/src/db/migrations/20260605000001-add-background-audio-settings.js b/backend/src/db/migrations/20260605000001-add-background-audio-settings.js new file mode 100644 index 0000000..33680f8 --- /dev/null +++ b/backend/src/db/migrations/20260605000001-add-background-audio-settings.js @@ -0,0 +1,47 @@ +'use strict'; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn('tour_pages', 'background_audio_autoplay', { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: true, + }); + await queryInterface.addColumn('tour_pages', 'background_audio_loop', { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: true, + }); + await queryInterface.addColumn( + 'tour_pages', + 'background_audio_start_time', + { + type: Sequelize.DECIMAL(10, 1), + allowNull: true, + defaultValue: null, + }, + ); + await queryInterface.addColumn('tour_pages', 'background_audio_end_time', { + type: Sequelize.DECIMAL(10, 1), + allowNull: true, + defaultValue: null, + }); + }, + + async down(queryInterface, _Sequelize) { + await queryInterface.removeColumn( + 'tour_pages', + 'background_audio_autoplay', + ); + await queryInterface.removeColumn('tour_pages', 'background_audio_loop'); + await queryInterface.removeColumn( + 'tour_pages', + 'background_audio_start_time', + ); + await queryInterface.removeColumn( + 'tour_pages', + 'background_audio_end_time', + ); + }, +}; diff --git a/backend/src/db/models/tour_pages.js b/backend/src/db/models/tour_pages.js index 7040788..78e4c48 100644 --- a/backend/src/db/models/tour_pages.js +++ b/backend/src/db/models/tour_pages.js @@ -66,6 +66,30 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.TEXT, }, + background_audio_autoplay: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true, + }, + + background_audio_loop: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true, + }, + + background_audio_start_time: { + type: DataTypes.DECIMAL(10, 1), + allowNull: true, + defaultValue: null, + }, + + background_audio_end_time: { + type: DataTypes.DECIMAL(10, 1), + allowNull: true, + defaultValue: null, + }, + background_loop: { type: DataTypes.BOOLEAN, diff --git a/frontend/src/components/Constructor/BackgroundSettingsEditor.tsx b/frontend/src/components/Constructor/BackgroundSettingsEditor.tsx index a45a563..f962440 100644 --- a/frontend/src/components/Constructor/BackgroundSettingsEditor.tsx +++ b/frontend/src/components/Constructor/BackgroundSettingsEditor.tsx @@ -7,7 +7,11 @@ */ import React from 'react'; -import type { AssetOption, VideoPlaybackSettings } from './types'; +import type { + AssetOption, + VideoPlaybackSettings, + AudioPlaybackSettings, +} from './types'; import { addFallbackAssetOption } from '../../lib/constructorHelpers'; interface BackgroundSettingsEditorProps { @@ -24,12 +28,18 @@ interface BackgroundSettingsEditorProps { videoStartTime?: number | null; videoEndTime?: number | null; onVideoSettingsChange?: (settings: VideoPlaybackSettings) => void; + // Audio-specific playback settings (only used when type='audio') + audioAutoplay?: boolean; + audioLoop?: boolean; + audioStartTime?: number | null; + audioEndTime?: number | null; + onAudioSettingsChange?: (settings: AudioPlaybackSettings) => void; } const LABELS: Record = { image: 'Background image', video: 'Background video', - audio: 'Background audio (loop)', + audio: 'Background audio', }; const BackgroundSettingsEditor: React.FC = ({ @@ -45,6 +55,11 @@ const BackgroundSettingsEditor: React.FC = ({ videoStartTime, videoEndTime, onVideoSettingsChange, + audioAutoplay, + audioLoop, + audioStartTime, + audioEndTime, + onAudioSettingsChange, }) => { const label = LABELS[type] || 'Background'; const selectOptions = addFallbackAssetOption( @@ -56,6 +71,9 @@ const BackgroundSettingsEditor: React.FC = ({ // Show video playback settings when type is video and a video is selected const showVideoSettings = type === 'video' && value && onVideoSettingsChange; + // Show audio playback settings when type is audio and an audio is selected + const showAudioSettings = type === 'audio' && value && onAudioSettingsChange; + return (
)} + + {/* Audio Playback Settings */} + {showAudioSettings && ( +
+

+ Playback Settings +

+ + + + + +
+
+ + + onAudioSettingsChange({ + startTime: e.target.value + ? parseFloat(e.target.value) + : null, + }) + } + /> +
+
+ + + onAudioSettingsChange({ + endTime: e.target.value ? parseFloat(e.target.value) : null, + }) + } + /> +
+
+
+ )} ); }; diff --git a/frontend/src/components/Constructor/CanvasBackground.tsx b/frontend/src/components/Constructor/CanvasBackground.tsx index e94f4b1..9836471 100644 --- a/frontend/src/components/Constructor/CanvasBackground.tsx +++ b/frontend/src/components/Constructor/CanvasBackground.tsx @@ -15,6 +15,7 @@ import React, { } from 'react'; import NextImage from 'next/image'; import { useBackgroundVideoPlayback } from '../../hooks/useBackgroundVideoPlayback'; +import { useBackgroundAudioPlayback } from '../../hooks/useBackgroundAudioPlayback'; import PreviousBackgroundOverlay from '../PreviousBackgroundOverlay'; import { baseURLApi } from '../../config'; @@ -62,6 +63,15 @@ interface CanvasBackgroundProps { videoStoragePath?: string; /** Pause video playback (e.g., during navigation to show frozen frame) */ pauseVideo?: boolean; + // Audio playback settings + audioAutoplay?: boolean; + audioLoop?: boolean; + audioStartTime?: number | null; + audioEndTime?: number | null; + /** Original storage path for audio - used for play-once tracking (not the resolved blob URL) */ + audioStoragePath?: string; + /** Pause audio playback (e.g., during ducking) */ + pauseAudio?: boolean; } const CanvasBackground: React.FC = ({ @@ -81,6 +91,12 @@ const CanvasBackground: React.FC = ({ videoEndTime = null, videoStoragePath, pauseVideo = false, + audioAutoplay = true, + audioLoop = true, + audioStartTime = null, + audioEndTime = null, + audioStoragePath, + pauseAudio = false, }) => { // During page switching with video paused, keep showing the previous video URL. // This prevents black flash when the video element would remount with a new URL. @@ -104,6 +120,17 @@ const CanvasBackground: React.FC = ({ paused: pauseVideo, }); + // Use background audio playback hook for custom start/end time handling and ducking + const { audioRef } = useBackgroundAudioPlayback({ + audioUrl: backgroundAudioUrl, + audioStoragePath: audioStoragePath || backgroundAudioUrl, + autoplay: audioAutoplay, + loop: audioLoop, + startTime: audioStartTime, + endTime: audioEndTime, + paused: pauseAudio, + }); + // Block autoplay if: video already played this session OR externally paused const effectiveAutoplay = videoAutoplay && !shouldBlockAutoplay && !pauseVideo; @@ -465,13 +492,14 @@ const CanvasBackground: React.FC = ({ /> )} - {/* Background audio */} + {/* Background audio - controlled by useBackgroundAudioPlayback hook for + custom start/end time handling and ducking (pauses when element audio plays) */} {backgroundAudioUrl && (