mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-05 15:08:53 -05:00
Compare commits
15 Commits
4e5b5a3e5b
...
567bb18e91
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
567bb18e91 | ||
|
|
22e3d9d708 | ||
|
|
76341b11fa | ||
|
|
7343817393 | ||
|
|
ce0ae1a8da | ||
|
|
67ba1cd467 | ||
|
|
95cb6904ce | ||
|
|
0a68060700 | ||
|
|
6dc6b3e983 | ||
|
|
e0f13e9635 | ||
|
|
0fac57ee9b | ||
|
|
1de914ec02 | ||
|
|
653f7c0418 | ||
|
|
54e2f1d7e4 | ||
|
|
bfcb774183 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -31,3 +31,7 @@ static/video_editor/videos/sample-video-37s.mp4
|
||||
static/video_editor/videos/sample-video-10m.mp4
|
||||
static/video_editor/videos/sample-video-10s.mp4
|
||||
frontend-tools/video-js/public/videos/sample-video-white.mp4
|
||||
frontend-tools/video-editor/client/public/videos/sample-video.mp3
|
||||
frontend-tools/chapters-editor/client/public/videos/sample-video.mp3
|
||||
static/chapters_editor/videos/sample-video.mp3
|
||||
static/video_editor/videos/sample-video.mp3
|
||||
|
||||
@ -1 +1,3 @@
|
||||
/templates/cms/*
|
||||
/templates/cms/*
|
||||
/templates/*.html
|
||||
*.scss
|
||||
@ -1 +1 @@
|
||||
VERSION = "6.7.1.beta-8"
|
||||
VERSION = "6.7.1.beta-9"
|
||||
|
||||
BIN
frontend-tools/chapters-editor/client/public/audio-poster.jpg
Normal file
BIN
frontend-tools/chapters-editor/client/public/audio-poster.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 695 KiB |
@ -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;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { formatTime } from '@/lib/timeUtils';
|
||||
import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl';
|
||||
import '../styles/IOSVideoPlayer.css';
|
||||
|
||||
interface IOSVideoPlayerProps {
|
||||
@ -11,6 +12,7 @@ interface IOSVideoPlayerProps {
|
||||
const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => {
|
||||
const [videoUrl, setVideoUrl] = useState<string>('');
|
||||
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
|
||||
const [posterImage, setPosterImage] = useState<string | undefined>(undefined);
|
||||
|
||||
// Refs for hold-to-continue functionality
|
||||
const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
@ -26,15 +28,25 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
|
||||
|
||||
// Get the video source URL from the main player
|
||||
useEffect(() => {
|
||||
let url = '';
|
||||
if (videoRef.current && videoRef.current.querySelector('source')) {
|
||||
const source = videoRef.current.querySelector('source') as HTMLSourceElement;
|
||||
if (source && source.src) {
|
||||
setVideoUrl(source.src);
|
||||
url = source.src;
|
||||
}
|
||||
} else {
|
||||
// Fallback to sample video if needed
|
||||
setVideoUrl('/videos/sample-video.mp4');
|
||||
url = '/videos/sample-video.mp4';
|
||||
}
|
||||
setVideoUrl(url);
|
||||
|
||||
// Check if the media is an audio file and set poster image
|
||||
const isAudioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
|
||||
|
||||
// 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 isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
|
||||
setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined));
|
||||
}, [videoRef]);
|
||||
|
||||
// Function to jump 15 seconds backward
|
||||
@ -127,6 +139,7 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
|
||||
x-webkit-airplay="allow"
|
||||
preload="auto"
|
||||
crossOrigin="anonymous"
|
||||
poster={posterImage}
|
||||
>
|
||||
<source src={videoUrl} type="video/mp4" />
|
||||
<p>Your browser doesn't support HTML5 video.</p>
|
||||
|
||||
@ -3947,11 +3947,11 @@ const TimelineControls = ({
|
||||
<button
|
||||
onClick={() => setShowSaveChaptersModal(true)}
|
||||
className="save-chapters-button"
|
||||
data-tooltip={clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length === 0
|
||||
data-tooltip={clipSegments.length === 0
|
||||
? "Clear all chapters"
|
||||
: "Save chapters"}
|
||||
>
|
||||
{clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length === 0
|
||||
{clipSegments.length === 0
|
||||
? 'Clear Chapters'
|
||||
: 'Save Chapters'}
|
||||
</button>
|
||||
@ -3974,7 +3974,7 @@ const TimelineControls = ({
|
||||
className="modal-button modal-button-primary"
|
||||
onClick={handleSaveChaptersConfirm}
|
||||
>
|
||||
{clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length === 0
|
||||
{clipSegments.length === 0
|
||||
? 'Clear Chapters'
|
||||
: 'Save Chapters'}
|
||||
</button>
|
||||
@ -3982,14 +3982,9 @@ const TimelineControls = ({
|
||||
}
|
||||
>
|
||||
<p className="modal-message">
|
||||
{(() => {
|
||||
const chaptersWithTitles = clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length;
|
||||
if (chaptersWithTitles === 0) {
|
||||
return "Are you sure you want to clear all chapters? This will remove all existing chapters from the database.";
|
||||
} else {
|
||||
return `Are you sure you want to save the chapters? This will save ${chaptersWithTitles} chapters to the database.`;
|
||||
}
|
||||
})()}
|
||||
{clipSegments.length === 0
|
||||
? "Are you sure you want to clear all chapters? This will remove all existing chapters from the database."
|
||||
: `Are you sure you want to save the chapters? This will save ${clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length} chapters to the database.`}
|
||||
</p>
|
||||
</Modal>
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { formatTime, formatDetailedTime } from '@/lib/timeUtils';
|
||||
import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl';
|
||||
import logger from '../lib/logger';
|
||||
import '../styles/VideoPlayer.css';
|
||||
|
||||
@ -38,6 +39,14 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
const sampleVideoUrl =
|
||||
(typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp4';
|
||||
|
||||
// Check if the media is an audio file
|
||||
const isAudioFile = sampleVideoUrl.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
|
||||
|
||||
// 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 isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
|
||||
const posterImage = isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined);
|
||||
|
||||
// Detect iOS device and Safari browser
|
||||
useEffect(() => {
|
||||
const checkIOS = () => {
|
||||
@ -354,6 +363,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
x-webkit-airplay="allow"
|
||||
controls={false}
|
||||
muted={isMuted}
|
||||
poster={posterImage}
|
||||
>
|
||||
<source src={sampleVideoUrl} type="video/mp4" />
|
||||
{/* Safari fallback for audio files */}
|
||||
|
||||
@ -147,8 +147,15 @@ const useVideoChapters = () => {
|
||||
initialSegments.push(segment);
|
||||
}
|
||||
} else {
|
||||
// Start with empty state - no default segment
|
||||
initialSegments = [];
|
||||
// Create a default segment that spans the entire video on first load
|
||||
const initialSegment: Segment = {
|
||||
id: 1,
|
||||
chapterTitle: '',
|
||||
startTime: 0,
|
||||
endTime: video.duration,
|
||||
};
|
||||
|
||||
initialSegments = [initialSegment];
|
||||
}
|
||||
|
||||
// Initialize history state with the segments
|
||||
@ -267,17 +274,24 @@ const useVideoChapters = () => {
|
||||
|
||||
// Check if we now have duration and initialize if needed
|
||||
if (video.duration > 0 && clipSegments.length === 0) {
|
||||
logger.debug('Safari: Successfully initialized metadata with empty state');
|
||||
logger.debug('Safari: Successfully initialized metadata, creating default segment');
|
||||
|
||||
const defaultSegment: Segment = {
|
||||
id: 1,
|
||||
chapterTitle: '',
|
||||
startTime: 0,
|
||||
endTime: video.duration,
|
||||
};
|
||||
|
||||
setDuration(video.duration);
|
||||
setTrimEnd(video.duration);
|
||||
setClipSegments([]);
|
||||
setClipSegments([defaultSegment]);
|
||||
|
||||
const initialState: EditorState = {
|
||||
trimStart: 0,
|
||||
trimEnd: video.duration,
|
||||
splitPoints: [],
|
||||
clipSegments: [],
|
||||
clipSegments: [defaultSegment],
|
||||
};
|
||||
|
||||
setHistory([initialState]);
|
||||
|
||||
@ -6,6 +6,7 @@ if (typeof window !== 'undefined') {
|
||||
window.MEDIA_DATA = {
|
||||
videoUrl: '',
|
||||
mediaId: '',
|
||||
posterUrl: ''
|
||||
};
|
||||
window.lastSeekedPosition = 0;
|
||||
}
|
||||
@ -15,6 +16,7 @@ declare global {
|
||||
MEDIA_DATA: {
|
||||
videoUrl: string;
|
||||
mediaId: string;
|
||||
posterUrl?: string;
|
||||
};
|
||||
seekToFunction?: (time: number) => void;
|
||||
lastSeekedPosition: number;
|
||||
|
||||
32
frontend-tools/chapters-editor/client/src/vite-env.d.ts
vendored
Normal file
32
frontend-tools/chapters-editor/client/src/vite-env.d.ts
vendored
Normal 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;
|
||||
}
|
||||
|
||||
@ -32,8 +32,14 @@ export default defineConfig({
|
||||
output: {
|
||||
assetFileNames: (assetInfo) => {
|
||||
if (assetInfo.name === 'style.css') return 'chapters-editor.css';
|
||||
return assetInfo.name;
|
||||
// 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 || 'asset-[hash][extname]';
|
||||
},
|
||||
// Inline small assets, emit larger ones
|
||||
inlineDynamicImports: true,
|
||||
globals: {
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
@ -44,5 +50,7 @@ export default defineConfig({
|
||||
outDir: '../../../static/video_editor',
|
||||
emptyOutDir: true,
|
||||
external: ['react', 'react-dom'],
|
||||
// Inline assets smaller than 100KB, emit larger ones
|
||||
assetsInlineLimit: 102400,
|
||||
},
|
||||
});
|
||||
|
||||
BIN
frontend-tools/video-editor/client/public/audio-poster.jpg
Normal file
BIN
frontend-tools/video-editor/client/public/audio-poster.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 695 KiB |
@ -236,6 +236,46 @@ const App = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Don't handle keyboard shortcuts if user is typing in an input field
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.code) {
|
||||
case 'Space':
|
||||
event.preventDefault(); // Prevent default spacebar behavior (scrolling, button activation)
|
||||
handlePlay();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
event.preventDefault();
|
||||
if (videoRef.current) {
|
||||
const newTime = Math.max(currentTime - 10, 0);
|
||||
handleMobileSafeSeek(newTime);
|
||||
logger.debug('Jumped backward 10 seconds to:', formatDetailedTime(newTime));
|
||||
}
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
event.preventDefault();
|
||||
if (videoRef.current) {
|
||||
const newTime = Math.min(currentTime + 10, duration);
|
||||
handleMobileSafeSeek(newTime);
|
||||
logger.debug('Jumped forward 10 seconds to:', formatDetailedTime(newTime));
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [handlePlay, handleMobileSafeSeek, currentTime, duration, videoRef]);
|
||||
|
||||
return (
|
||||
<div className="bg-background min-h-screen">
|
||||
<MobilePlayPrompt videoRef={videoRef} onPlay={handlePlay} />
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { formatTime } from '@/lib/timeUtils';
|
||||
import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl';
|
||||
import '../styles/IOSVideoPlayer.css';
|
||||
|
||||
interface IOSVideoPlayerProps {
|
||||
@ -11,6 +12,7 @@ interface IOSVideoPlayerProps {
|
||||
const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => {
|
||||
const [videoUrl, setVideoUrl] = useState<string>('');
|
||||
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
|
||||
const [posterImage, setPosterImage] = useState<string | undefined>(undefined);
|
||||
|
||||
// Refs for hold-to-continue functionality
|
||||
const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
@ -26,15 +28,25 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
|
||||
|
||||
// Get the video source URL from the main player
|
||||
useEffect(() => {
|
||||
let url = '';
|
||||
if (videoRef.current && videoRef.current.querySelector('source')) {
|
||||
const source = videoRef.current.querySelector('source') as HTMLSourceElement;
|
||||
if (source && source.src) {
|
||||
setVideoUrl(source.src);
|
||||
url = source.src;
|
||||
}
|
||||
} else {
|
||||
// Fallback to sample video if needed
|
||||
setVideoUrl('/videos/sample-video.mp4');
|
||||
url = '/videos/sample-video.mp3';
|
||||
}
|
||||
setVideoUrl(url);
|
||||
|
||||
// Check if the media is an audio file and set poster image
|
||||
const isAudioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
|
||||
|
||||
// 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 isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
|
||||
setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined));
|
||||
}, [videoRef]);
|
||||
|
||||
// Function to jump 15 seconds backward
|
||||
@ -127,6 +139,7 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
|
||||
x-webkit-airplay="allow"
|
||||
preload="auto"
|
||||
crossOrigin="anonymous"
|
||||
poster={posterImage}
|
||||
>
|
||||
<source src={videoUrl} type="video/mp4" />
|
||||
<p>Your browser doesn't support HTML5 video.</p>
|
||||
|
||||
@ -3,7 +3,7 @@ import { formatTime, formatDetailedTime } from '../lib/timeUtils';
|
||||
import { generateThumbnail, generateSolidColor } from '../lib/videoUtils';
|
||||
import { Segment } from './ClipSegments';
|
||||
import Modal from './Modal';
|
||||
import { trimVideo, autoSaveVideo, fetchAutoSavedSegments } from '../services/videoApi';
|
||||
import { trimVideo, autoSaveVideo } from '../services/videoApi';
|
||||
import logger from '../lib/logger';
|
||||
import '../styles/TimelineControls.css';
|
||||
import '../styles/TwoRowTooltip.css';
|
||||
@ -1078,44 +1078,10 @@ const TimelineControls = ({
|
||||
// Get savedSegments directly from window.MEDIA_DATA
|
||||
let savedData = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.savedSegments) || null;
|
||||
|
||||
// If no saved segments, use default segments
|
||||
// If no saved segments, don't load anything - the useVideoTrimmer hook already creates the initial full-length segment
|
||||
if (!savedData) {
|
||||
logger.debug('No saved segments found in MEDIA_DATA, using default segments');
|
||||
savedData = {
|
||||
segments: [
|
||||
{
|
||||
startTime: '00:00:01.130',
|
||||
endTime: '00:00:05.442',
|
||||
name: 'segment',
|
||||
},
|
||||
{
|
||||
startTime: '00:00:06.152',
|
||||
endTime: '00:00:10.518',
|
||||
name: 'segment',
|
||||
},
|
||||
{
|
||||
startTime: '00:00:11.518',
|
||||
endTime: '00:00:15.121',
|
||||
name: 'segment',
|
||||
},
|
||||
{
|
||||
startTime: '00:00:16.757',
|
||||
endTime: '00:00:20.769',
|
||||
name: 'segment',
|
||||
},
|
||||
{
|
||||
startTime: '00:00:21.158',
|
||||
endTime: '00:00:25.870',
|
||||
name: 'segment',
|
||||
},
|
||||
{
|
||||
startTime: '00:00:26.430',
|
||||
endTime: '00:00:29.798',
|
||||
name: 'segment',
|
||||
},
|
||||
],
|
||||
updated_at: '2025-06-24 14:59:14',
|
||||
};
|
||||
logger.debug('No saved segments found in MEDIA_DATA, skipping load (initial segment already created by useVideoTrimmer)');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('Loading saved segments:', savedData);
|
||||
@ -1132,7 +1098,6 @@ const TimelineControls = ({
|
||||
endTime: parseTimeString(seg.endTime),
|
||||
thumbnail: '',
|
||||
}));
|
||||
console.log('convertedSegments', convertedSegments);
|
||||
|
||||
// Dispatch event to update segments
|
||||
const updateEvent = new CustomEvent('update-segments', {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { formatTime, formatDetailedTime } from '@/lib/timeUtils';
|
||||
import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl';
|
||||
import logger from '../lib/logger';
|
||||
import '../styles/VideoPlayer.css';
|
||||
|
||||
@ -36,7 +37,15 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
const [tooltipTime, setTooltipTime] = useState(0);
|
||||
|
||||
const sampleVideoUrl =
|
||||
(typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp4';
|
||||
(typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp3';
|
||||
|
||||
// Check if the media is an audio file
|
||||
const isAudioFile = sampleVideoUrl.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
|
||||
|
||||
// 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 isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
|
||||
const posterImage = isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined);
|
||||
|
||||
// Detect iOS device
|
||||
useEffect(() => {
|
||||
@ -344,6 +353,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
x-webkit-airplay="allow"
|
||||
controls={false}
|
||||
muted={isMuted}
|
||||
poster={posterImage}
|
||||
>
|
||||
<source src={sampleVideoUrl} type="video/mp4" />
|
||||
<p>Your browser doesn't support HTML5 video.</p>
|
||||
|
||||
@ -5,7 +5,8 @@ import "./index.css";
|
||||
if (typeof window !== "undefined") {
|
||||
window.MEDIA_DATA = {
|
||||
videoUrl: "",
|
||||
mediaId: ""
|
||||
mediaId: "",
|
||||
posterUrl: ""
|
||||
};
|
||||
window.lastSeekedPosition = 0;
|
||||
}
|
||||
@ -15,6 +16,7 @@ declare global {
|
||||
MEDIA_DATA: {
|
||||
videoUrl: string;
|
||||
mediaId: string;
|
||||
posterUrl?: string;
|
||||
};
|
||||
seekToFunction?: (time: number) => void;
|
||||
lastSeekedPosition: number;
|
||||
|
||||
32
frontend-tools/video-editor/client/src/vite-env.d.ts
vendored
Normal file
32
frontend-tools/video-editor/client/src/vite-env.d.ts
vendored
Normal 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;
|
||||
}
|
||||
|
||||
@ -32,11 +32,17 @@ export default defineConfig({
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// Ensure CSS file has a predictable name
|
||||
// Ensure CSS file has a predictable name and keep image assets
|
||||
assetFileNames: (assetInfo) => {
|
||||
if (assetInfo.name === 'style.css') return 'video-editor.css';
|
||||
return assetInfo.name;
|
||||
// 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 || 'asset-[hash][extname]';
|
||||
},
|
||||
// Inline small assets, emit larger ones
|
||||
inlineDynamicImports: true,
|
||||
// Add this to ensure the final bundle exposes React correctly
|
||||
globals: {
|
||||
'react': 'React',
|
||||
@ -47,6 +53,8 @@ export default defineConfig({
|
||||
// Output to Django's static directory
|
||||
outDir: '../../../static/video_editor',
|
||||
emptyOutDir: true,
|
||||
external: ['react', 'react-dom']
|
||||
external: ['react', 'react-dom'],
|
||||
// Inline assets smaller than 100KB, emit larger ones
|
||||
assetsInlineLimit: 102400,
|
||||
},
|
||||
});
|
||||
BIN
static/chapters_editor/audio-poster.jpg
Normal file
BIN
static/chapters_editor/audio-poster.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 695 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
static/video_editor/audio-poster.jpg
Normal file
BIN
static/video_editor/audio-poster.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 695 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -13,13 +13,12 @@
|
||||
<script>
|
||||
window.MEDIA_DATA = {
|
||||
videoUrl: "{{ media_file_path }}",
|
||||
posterUrl: "{{ media_object.poster_url }}",
|
||||
mediaId: "{{ media_object.friendly_token }}",
|
||||
redirectURL: "{{ media_object.get_absolute_url }}",
|
||||
redirectUserMediaURL: "{{ media_object.user.get_absolute_url }}",
|
||||
chapters: {{ chapters|safe }},
|
||||
};
|
||||
|
||||
console.log(window.MEDIA_DATA.chapters)
|
||||
</script>
|
||||
{%endblock topimports %}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
<script>
|
||||
window.MEDIA_DATA = {
|
||||
videoUrl: "{{ media_file_path }}",
|
||||
posterUrl: "{{ media_object.poster_url }}",
|
||||
mediaId: "{{ media_object.friendly_token }}",
|
||||
redirectURL: "{{ media_object.get_absolute_url }}",
|
||||
redirectUserMediaURL: "{{ media_object.user.get_absolute_url }}"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user