Compare commits

..

No commits in common. "567bb18e91faa515984ee01bce3cfaeacc3d5ff9" and "4e5b5a3e5b024ccfffb73e98de3b2684ffa601b7" have entirely different histories.

29 changed files with 197 additions and 359 deletions

4
.gitignore vendored
View File

@ -31,7 +31,3 @@ 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

View File

@ -1,3 +1 @@
/templates/cms/*
/templates/*.html
*.scss

View File

@ -1 +1 @@
VERSION = "6.7.1.beta-9"
VERSION = "6.7.1.beta-8"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 KiB

View File

@ -1,6 +0,0 @@
// 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,6 +1,5 @@
import { useEffect, useState, useRef } from 'react';
import { formatTime } from '@/lib/timeUtils';
import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl';
import '../styles/IOSVideoPlayer.css';
interface IOSVideoPlayerProps {
@ -12,7 +11,6 @@ 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);
@ -28,25 +26,15 @@ 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) {
url = source.src;
setVideoUrl(source.src);
}
} else {
// Fallback to sample video if needed
url = '/videos/sample-video.mp4';
setVideoUrl('/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
@ -139,7 +127,6 @@ 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>

View File

@ -3947,11 +3947,11 @@ const TimelineControls = ({
<button
onClick={() => setShowSaveChaptersModal(true)}
className="save-chapters-button"
data-tooltip={clipSegments.length === 0
data-tooltip={clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length === 0
? "Clear all chapters"
: "Save chapters"}
>
{clipSegments.length === 0
{clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length === 0
? 'Clear Chapters'
: 'Save Chapters'}
</button>
@ -3974,7 +3974,7 @@ const TimelineControls = ({
className="modal-button modal-button-primary"
onClick={handleSaveChaptersConfirm}
>
{clipSegments.length === 0
{clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length === 0
? 'Clear Chapters'
: 'Save Chapters'}
</button>
@ -3982,9 +3982,14 @@ const TimelineControls = ({
}
>
<p className="modal-message">
{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.`}
{(() => {
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.`;
}
})()}
</p>
</Modal>

View File

@ -1,6 +1,5 @@
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';
@ -39,14 +38,6 @@ 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 = () => {
@ -363,7 +354,6 @@ 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 */}

View File

@ -147,15 +147,8 @@ const useVideoChapters = () => {
initialSegments.push(segment);
}
} else {
// 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];
// Start with empty state - no default segment
initialSegments = [];
}
// Initialize history state with the segments
@ -274,24 +267,17 @@ 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, creating default segment');
const defaultSegment: Segment = {
id: 1,
chapterTitle: '',
startTime: 0,
endTime: video.duration,
};
logger.debug('Safari: Successfully initialized metadata with empty state');
setDuration(video.duration);
setTrimEnd(video.duration);
setClipSegments([defaultSegment]);
setClipSegments([]);
const initialState: EditorState = {
trimStart: 0,
trimEnd: video.duration,
splitPoints: [],
clipSegments: [defaultSegment],
clipSegments: [],
};
setHistory([initialState]);

View File

@ -6,7 +6,6 @@ if (typeof window !== 'undefined') {
window.MEDIA_DATA = {
videoUrl: '',
mediaId: '',
posterUrl: ''
};
window.lastSeekedPosition = 0;
}
@ -16,7 +15,6 @@ declare global {
MEDIA_DATA: {
videoUrl: string;
mediaId: string;
posterUrl?: string;
};
seekToFunction?: (time: number) => void;
lastSeekedPosition: number;

View File

@ -1,32 +0,0 @@
/// <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,14 +32,8 @@ export default defineConfig({
output: {
assetFileNames: (assetInfo) => {
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 || 'asset-[hash][extname]';
return assetInfo.name;
},
// Inline small assets, emit larger ones
inlineDynamicImports: true,
globals: {
react: 'React',
'react-dom': 'ReactDOM',
@ -50,7 +44,5 @@ export default defineConfig({
outDir: '../../../static/video_editor',
emptyOutDir: true,
external: ['react', 'react-dom'],
// Inline assets smaller than 100KB, emit larger ones
assetsInlineLimit: 102400,
},
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 KiB

View File

@ -236,46 +236,6 @@ 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} />

View File

@ -1,6 +0,0 @@
// 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,6 +1,5 @@
import { useEffect, useState, useRef } from 'react';
import { formatTime } from '@/lib/timeUtils';
import { AUDIO_POSTER_URL } from '@/assets/audioPosterUrl';
import '../styles/IOSVideoPlayer.css';
interface IOSVideoPlayerProps {
@ -12,7 +11,6 @@ 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);
@ -28,25 +26,15 @@ 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) {
url = source.src;
setVideoUrl(source.src);
}
} else {
// Fallback to sample video if needed
url = '/videos/sample-video.mp3';
setVideoUrl('/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
@ -139,7 +127,6 @@ 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>

View File

@ -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 } from '../services/videoApi';
import { trimVideo, autoSaveVideo, fetchAutoSavedSegments } from '../services/videoApi';
import logger from '../lib/logger';
import '../styles/TimelineControls.css';
import '../styles/TwoRowTooltip.css';
@ -1078,10 +1078,44 @@ 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, don't load anything - the useVideoTrimmer hook already creates the initial full-length segment
// If no saved segments, use default segments
if (!savedData) {
logger.debug('No saved segments found in MEDIA_DATA, skipping load (initial segment already created by useVideoTrimmer)');
return;
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('Loading saved segments:', savedData);
@ -1098,6 +1132,7 @@ const TimelineControls = ({
endTime: parseTimeString(seg.endTime),
thumbnail: '',
}));
console.log('convertedSegments', convertedSegments);
// Dispatch event to update segments
const updateEvent = new CustomEvent('update-segments', {

View File

@ -1,6 +1,5 @@
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';
@ -37,15 +36,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const [tooltipTime, setTooltipTime] = useState(0);
const sampleVideoUrl =
(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);
(typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp4';
// Detect iOS device
useEffect(() => {
@ -353,7 +344,6 @@ 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>

View File

@ -5,8 +5,7 @@ import "./index.css";
if (typeof window !== "undefined") {
window.MEDIA_DATA = {
videoUrl: "",
mediaId: "",
posterUrl: ""
mediaId: ""
};
window.lastSeekedPosition = 0;
}
@ -16,7 +15,6 @@ declare global {
MEDIA_DATA: {
videoUrl: string;
mediaId: string;
posterUrl?: string;
};
seekToFunction?: (time: number) => void;
lastSeekedPosition: number;

View File

@ -1,32 +0,0 @@
/// <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,17 +32,11 @@ export default defineConfig({
},
rollupOptions: {
output: {
// Ensure CSS file has a predictable name and keep image assets
// Ensure CSS file has a predictable name
assetFileNames: (assetInfo) => {
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 || 'asset-[hash][extname]';
return assetInfo.name;
},
// Inline small assets, emit larger ones
inlineDynamicImports: true,
// Add this to ensure the final bundle exposes React correctly
globals: {
'react': 'React',
@ -53,8 +47,6 @@ export default defineConfig({
// Output to Django's static directory
outDir: '../../../static/video_editor',
emptyOutDir: true,
external: ['react', 'react-dom'],
// Inline assets smaller than 100KB, emit larger ones
assetsInlineLimit: 102400,
external: ['react', 'react-dom']
},
});

Binary file not shown.

Before

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

Binary file not shown.

Before

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

View File

@ -13,12 +13,13 @@
<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 %}

View File

@ -13,7 +13,6 @@
<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 }}"