Refactor audio poster handling and Vite config

Audio poster image is now imported as a module and referenced via AUDIO_POSTER_URL in both chapters-editor and video-editor players. Removed custom file copy plugin from Vite config, updated asset handling to use Vite's asset import and output options, and added type declarations for image modules.
This commit is contained in:
Yiannis Christodoulou 2025-10-19 14:23:17 +03:00
parent 95cb6904ce
commit 67ba1cd467
10 changed files with 105 additions and 30 deletions

View File

@ -0,0 +1,6 @@
// Import the audio poster image as a module
// Vite will handle this and provide the correct URL
import audioPosterJpg from '../../public/audio-poster.jpg';
export const AUDIO_POSTER_URL = audioPosterJpg;

View File

@ -1,5 +1,6 @@
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState, useRef } from 'react';
import { formatTime } from '@/lib/timeUtils'; import { formatTime } from '@/lib/timeUtils';
import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl';
import '../styles/IOSVideoPlayer.css'; import '../styles/IOSVideoPlayer.css';
interface IOSVideoPlayerProps { interface IOSVideoPlayerProps {
@ -45,7 +46,7 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
// Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None" // Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None"
const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || ''; const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || '';
const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== ''; const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? '/audio-poster.jpg' : undefined)); setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined));
}, [videoRef]); }, [videoRef]);
// Function to jump 15 seconds backward // Function to jump 15 seconds backward

View File

@ -1,5 +1,6 @@
import React, { useRef, useEffect, useState } from 'react'; import React, { useRef, useEffect, useState } from 'react';
import { formatTime, formatDetailedTime } from '@/lib/timeUtils'; import { formatTime, formatDetailedTime } from '@/lib/timeUtils';
import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl';
import logger from '../lib/logger'; import logger from '../lib/logger';
import '../styles/VideoPlayer.css'; import '../styles/VideoPlayer.css';
@ -44,7 +45,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
// Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None" // Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None"
const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || ''; const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || '';
const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== ''; const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
const posterImage = isValidPoster ? mediaPosterUrl : (isAudioFile ? '/audio-poster.jpg' : undefined); const posterImage = isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined);
// Detect iOS device and Safari browser // Detect iOS device and Safari browser
useEffect(() => { useEffect(() => {

View File

@ -0,0 +1,32 @@
/// <reference types="vite/client" />
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.svg' {
const src: string;
export default src;
}
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.webp' {
const src: string;
export default src;
}

View File

@ -2,33 +2,12 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import path, { dirname } from 'path'; import path, { dirname } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { copyFileSync, mkdirSync } from 'fs';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
// Plugin to copy audio-poster.jpg to build output
const copyAudioPoster = () => {
return {
name: 'copy-audio-poster',
closeBundle() {
const outDir = path.resolve(__dirname, '../../../static/video_editor');
const sourceFile = path.resolve(__dirname, 'client/public/audio-poster.jpg');
const destFile = path.resolve(outDir, 'audio-poster.jpg');
try {
mkdirSync(outDir, { recursive: true });
copyFileSync(sourceFile, destFile);
console.log('✓ Copied audio-poster.jpg to build output');
} catch (error) {
console.error('Error copying audio-poster.jpg:', error);
}
},
};
};
export default defineConfig({ export default defineConfig({
plugins: [react(), copyAudioPoster()], plugins: [react()],
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, 'client', 'src'), '@': path.resolve(__dirname, 'client', 'src'),
@ -53,8 +32,14 @@ export default defineConfig({
output: { output: {
assetFileNames: (assetInfo) => { assetFileNames: (assetInfo) => {
if (assetInfo.name === 'style.css') return 'chapters-editor.css'; if (assetInfo.name === 'style.css') return 'chapters-editor.css';
// Keep original names for image assets
if (assetInfo.name && /\.(png|jpe?g|svg|gif|webp)$/i.test(assetInfo.name)) {
return assetInfo.name; return assetInfo.name;
}
return assetInfo.name || 'asset-[hash][extname]';
}, },
// Inline small assets, emit larger ones
inlineDynamicImports: true,
globals: { globals: {
react: 'React', react: 'React',
'react-dom': 'ReactDOM', 'react-dom': 'ReactDOM',
@ -65,5 +50,7 @@ export default defineConfig({
outDir: '../../../static/video_editor', outDir: '../../../static/video_editor',
emptyOutDir: true, emptyOutDir: true,
external: ['react', 'react-dom'], external: ['react', 'react-dom'],
// Inline assets smaller than 100KB, emit larger ones
assetsInlineLimit: 102400,
}, },
}); });

View File

@ -0,0 +1,6 @@
// Import the audio poster image as a module
// Vite will handle this and provide the correct URL
import audioPosterJpg from '../../public/audio-poster.jpg';
export const AUDIO_POSTER_URL = audioPosterJpg;

View File

@ -1,5 +1,6 @@
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState, useRef } from 'react';
import { formatTime } from '@/lib/timeUtils'; import { formatTime } from '@/lib/timeUtils';
import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl';
import '../styles/IOSVideoPlayer.css'; import '../styles/IOSVideoPlayer.css';
interface IOSVideoPlayerProps { interface IOSVideoPlayerProps {
@ -45,7 +46,7 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
// Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None" // Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None"
const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || ''; const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || '';
const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== ''; const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? '/audio-poster.jpg' : undefined)); setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined));
}, [videoRef]); }, [videoRef]);
// Function to jump 15 seconds backward // Function to jump 15 seconds backward

View File

@ -1,5 +1,6 @@
import React, { useRef, useEffect, useState } from 'react'; import React, { useRef, useEffect, useState } from 'react';
import { formatTime, formatDetailedTime } from '@/lib/timeUtils'; import { formatTime, formatDetailedTime } from '@/lib/timeUtils';
import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl';
import logger from '../lib/logger'; import logger from '../lib/logger';
import '../styles/VideoPlayer.css'; import '../styles/VideoPlayer.css';
@ -44,7 +45,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
// Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None" // Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None"
const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || ''; const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || '';
const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== ''; const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
const posterImage = isValidPoster ? mediaPosterUrl : (isAudioFile ? '/audio-poster.jpg' : undefined); const posterImage = isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined);
// Detect iOS device // Detect iOS device
useEffect(() => { useEffect(() => {

View File

@ -0,0 +1,32 @@
/// <reference types="vite/client" />
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.svg' {
const src: string;
export default src;
}
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.webp' {
const src: string;
export default src;
}

View File

@ -32,11 +32,17 @@ export default defineConfig({
}, },
rollupOptions: { rollupOptions: {
output: { output: {
// Ensure CSS file has a predictable name // Ensure CSS file has a predictable name and keep image assets
assetFileNames: (assetInfo) => { assetFileNames: (assetInfo) => {
if (assetInfo.name === 'style.css') return 'video-editor.css'; if (assetInfo.name === 'style.css') return 'video-editor.css';
// Keep original names for image assets
if (assetInfo.name && /\.(png|jpe?g|svg|gif|webp)$/i.test(assetInfo.name)) {
return assetInfo.name; return assetInfo.name;
}
return assetInfo.name || 'asset-[hash][extname]';
}, },
// Inline small assets, emit larger ones
inlineDynamicImports: true,
// Add this to ensure the final bundle exposes React correctly // Add this to ensure the final bundle exposes React correctly
globals: { globals: {
'react': 'React', 'react': 'React',
@ -47,6 +53,8 @@ export default defineConfig({
// Output to Django's static directory // Output to Django's static directory
outDir: '../../../static/video_editor', outDir: '../../../static/video_editor',
emptyOutDir: true, emptyOutDir: true,
external: ['react', 'react-dom'] external: ['react', 'react-dom'],
// Inline assets smaller than 100KB, emit larger ones
assetsInlineLimit: 102400,
}, },
}); });