Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
0b69c71
Initial commit
Tophness Oct 9, 2025
d7f0da3
adjust run command
Tophness Oct 9, 2025
7e7007e
forgot to include server
Tophness Oct 9, 2025
9399181
Update README.md
Tophness Oct 9, 2025
4033106
Update README.md
Tophness Oct 9, 2025
e3ad47e
add undo/redo, delete clips, project media window, recent projects. f…
Tophness Oct 9, 2025
a21356f
add ability to drag tracks onto ghost tracks
Tophness Oct 9, 2025
354c0d1
fix bug dragging ghost tracks from project media
Tophness Oct 9, 2025
cad9618
add dynamic scroll and dynamic timescale
Tophness Oct 10, 2025
eacb498
fix ai joiner to start polling for outputs during manual generate, st…
Tophness Oct 10, 2025
14fbfaf
fix FPS conversions
Tophness Oct 10, 2025
e776c35
fix missing import
Tophness Oct 10, 2025
77ca09d
add ability to generate into new track, move statuses to main editor
Tophness Oct 10, 2025
3438e8f
add image and audio media types
Tophness Oct 10, 2025
a5b51df
add ability to resize clips
Tophness Oct 10, 2025
25bcc33
add playhead snapping for all functions
Tophness Oct 10, 2025
dc68e86
added feature to unlink and relink audio tracks
Tophness Oct 10, 2025
bea0f48
add ability to add media directly to timeline
Tophness Oct 10, 2025
afc567a
add ability to drag and drop from OS into timeline or project media
Tophness Oct 10, 2025
1756734
fix ai joiner not inserting on new track with new stream structure
Tophness Oct 10, 2025
a223c1e
fix ai joiner not inserting on new track with new stream structure (2)
Tophness Oct 10, 2025
8472a8a
add Create mode for AI plusin
Tophness Oct 10, 2025
deef010
add snapping between clips for resize
Tophness Oct 10, 2025
da34630
fixed minimize closing all windows
Tophness Oct 10, 2025
8b51fc4
fixed export with images and blank canvas
Tophness Oct 10, 2025
836a172
fix multi-track preview rendering
Tophness Oct 10, 2025
109a2aa
fix multi-track preview rendering (2)
Tophness Oct 10, 2025
de9ba22
speed up seeking
Tophness Oct 10, 2025
153c858
add select clips, delete key and left right arrow keys
Tophness Oct 10, 2025
69bb822
finally moved from server/client to a native plugin
Tophness Oct 11, 2025
dced8dd
Merge branch 'main' into video_editor
Tophness Oct 11, 2025
4f9c03b
update screenshots
Tophness Oct 11, 2025
1622655
update install instructions
Tophness Oct 11, 2025
4aded7a
Update README.md
Tophness Oct 11, 2025
8c9286c
Update README.md
Tophness Oct 11, 2025
f7d62b5
Update README.md
Tophness Oct 11, 2025
0953b56
Add create to new track option back
Tophness Oct 11, 2025
5562803
Update README.md
Tophness Oct 11, 2025
19a5cd8
allow region selection resizing, remove all region context menu optio…
Tophness Oct 11, 2025
e45796c
add preview video scaling
Tophness Oct 12, 2025
e6fd0aa
fix scroll centering, overlapping track labels
Tophness Oct 12, 2025
46d2c6c
fix 1st frame being rendered blank when adding video to timeline
Tophness Oct 12, 2025
0bd6a8d
add zoom to 0 on timescale
Tophness Oct 12, 2025
0ca8ba1
fix track labels overlaying 0s
Tophness Oct 12, 2025
eef79e4
fix add track button not working
Tophness Oct 12, 2025
088940b
snap timehead to single frames
Tophness Oct 12, 2025
cc553e2
automatically set resolution from project resolution or clip resoluti…
Tophness Oct 12, 2025
80ecba2
add join to start frame with ai and join to end frame with ai modes
Tophness Oct 12, 2025
bbe9df1
make sure advanced options tabs dont need scroll
Tophness Oct 12, 2025
f2324d4
allow sliders to be text editable
Tophness Oct 12, 2025
2a867c2
save selected regions to project
Tophness Oct 12, 2025
4d79760
set minheight
Tophness Oct 12, 2025
a8fd923
add entire ffmpeg library of containers and formats to export options
Tophness Oct 12, 2025
cdcde06
use ms instead of sec globally
Tophness Oct 12, 2025
75d29f9
fix timescale shifts
Tophness Oct 12, 2025
fde093f
add buttons to snap to nearest clip collision
Tophness Oct 12, 2025
78f2a95
hold shift for universal frame by frame snapping on all operations
Tophness Oct 12, 2025
19b6ae3
improve timescale
Tophness Oct 12, 2025
4452582
clamp timescale conversions
Tophness Oct 12, 2025
baad78f
added audio for playback, major overhaul of encoder and playback syst…
Tophness Oct 14, 2025
4473bc5
Merge branch 'main' into video_editor
Tophness Oct 14, 2025
47a64b5
remove failed attempt to make onnxruntime import plugin-based
Tophness Oct 14, 2025
f151887
Update README.md
Tophness Oct 15, 2025
7029b66
Update README.md
Tophness Oct 15, 2025
ad19ab4
use icons for multimedia controls
Tophness Oct 17, 2025
08a7e00
Update README.md
Tophness Oct 17, 2025
b5c7a74
fix configuration
Tophness Oct 17, 2025
73eac71
fix queue index problems in plugin
Tophness Oct 17, 2025
f9f4bd1
fix queue width
Tophness Oct 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 45 additions & 242 deletions README.md

Large diffs are not rendered by default.

210 changes: 210 additions & 0 deletions encoding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import ffmpeg
import subprocess
import re
from PyQt6.QtCore import QObject, pyqtSignal, QThread

class _ExportRunner(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal(bool, str)

def __init__(self, ffmpeg_cmd, total_duration_ms, parent=None):
super().__init__(parent)
self.ffmpeg_cmd = ffmpeg_cmd
self.total_duration_ms = total_duration_ms
self.process = None

def run(self):
try:
startupinfo = None
if hasattr(subprocess, 'STARTUPINFO'):
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW

self.process = subprocess.Popen(
self.ffmpeg_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
encoding="utf-8",
errors='ignore',
startupinfo=startupinfo
)

full_output = []
time_pattern = re.compile(r"time=(\d{2}):(\d{2}):(\d{2})\.(\d{2})")

for line in iter(self.process.stdout.readline, ""):
full_output.append(line)
match = time_pattern.search(line)
if match:
h, m, s, cs = [int(g) for g in match.groups()]
processed_ms = (h * 3600 + m * 60 + s) * 1000 + cs * 10
if self.total_duration_ms > 0:
percentage = int((processed_ms / self.total_duration_ms) * 100)
self.progress.emit(min(100, percentage))

self.process.stdout.close()
return_code = self.process.wait()

if return_code == 0:
self.progress.emit(100)
self.finished.emit(True, "Export completed successfully!")
else:
print("--- FFmpeg Export FAILED ---")
print("Command: " + " ".join(self.ffmpeg_cmd))
print("".join(full_output))
self.finished.emit(False, f"Export failed with code {return_code}. Check console.")

except FileNotFoundError:
self.finished.emit(False, "Export failed: ffmpeg.exe not found in your system's PATH.")
except Exception as e:
self.finished.emit(False, f"An exception occurred during export: {e}")

def get_process(self):
return self.process

class Encoder(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal(bool, str)

def __init__(self, parent=None):
super().__init__(parent)
self.worker_thread = None
self.worker = None
self._is_running = False

def start_export(self, timeline, project_settings, export_settings):
if self._is_running:
self.finished.emit(False, "An export is already in progress.")
return

self._is_running = True

try:
total_dur_ms = timeline.get_total_duration()
total_dur_sec = total_dur_ms / 1000.0
w, h, fps = project_settings['width'], project_settings['height'], project_settings['fps']
sample_rate, channel_layout = '44100', 'stereo'

# --- VIDEO GRAPH CONSTRUCTION (Definitive Solution) ---

all_video_clips = sorted(
[c for c in timeline.clips if c.track_type == 'video'],
key=lambda c: c.track_index
)

# 1. Start with a base black canvas that defines the project's duration.
# This is our master clock and bottom layer.
final_video = ffmpeg.input(f'color=c=black:s={w}x{h}:r={fps}:d={total_dur_sec}', f='lavfi')

# 2. Process each clip individually and overlay it.
for clip in all_video_clips:
# A new, separate input for every single clip guarantees no conflicts.
if clip.media_type == 'image':
clip_input = ffmpeg.input(clip.source_path, loop=1, framerate=fps)
else:
clip_input = ffmpeg.input(clip.source_path)

# a) Calculate the time shift to align the clip's content with the timeline.
# This ensures the correct frame of the source is shown at the start of the clip.
timeline_start_sec = clip.timeline_start_ms / 1000.0
clip_start_sec = clip.clip_start_ms / 1000.0
time_shift_sec = timeline_start_sec - clip_start_sec

# b) Prepare the layer: apply the time shift, then scale and pad.
timed_layer = (
clip_input.video
.setpts(f'PTS+{time_shift_sec}/TB')
.filter('scale', w, h, force_original_aspect_ratio='decrease')
.filter('pad', w, h, '(ow-iw)/2', '(oh-ih)/2', 'black')
)

# c) Define the visibility window for the overlay on the master timeline.
timeline_end_sec = (clip.timeline_start_ms + clip.duration_ms) / 1000.0
enable_expression = f'between(t,{timeline_start_sec:.6f},{timeline_end_sec:.6f})'

# d) Overlay the prepared layer onto the composition, enabling it only during its time window.
# eof_action='pass' handles finite streams gracefully.
final_video = ffmpeg.overlay(final_video, timed_layer, enable=enable_expression, eof_action='pass')

# 3. Set final output format and framerate.
final_video = final_video.filter('format', pix_fmts='yuv420p').filter('fps', fps=fps)


# --- AUDIO GRAPH CONSTRUCTION (UNCHANGED and CORRECT) ---
track_audio_streams = []
for i in range(1, timeline.num_audio_tracks + 1):
track_clips = sorted([c for c in timeline.clips if c.track_type == 'audio' and c.track_index == i], key=lambda c: c.timeline_start_ms)
if not track_clips:
continue

track_segments = []
last_end_ms = 0
for clip in track_clips:
gap_ms = clip.timeline_start_ms - last_end_ms
if gap_ms > 10:
track_segments.append(ffmpeg.input(f'anullsrc=r={sample_rate}:cl={channel_layout}:d={gap_ms/1000.0}', f='lavfi'))

clip_start_sec = clip.clip_start_ms / 1000.0
clip_duration_sec = clip.duration_ms / 1000.0
audio_source_node = ffmpeg.input(clip.source_path)
a_seg = audio_source_node.audio.filter('atrim', start=clip_start_sec, duration=clip_duration_sec).filter('asetpts', 'PTS-STARTPTS')
track_segments.append(a_seg)
last_end_ms = clip.timeline_start_ms + clip.duration_ms

if track_segments:
track_audio_streams.append(ffmpeg.concat(*track_segments, v=0, a=1))

# --- FINAL OUTPUT ASSEMBLY ---
output_args = {}
stream_args = []
has_audio = bool(track_audio_streams) and export_settings.get('acodec')

if export_settings.get('vcodec'):
stream_args.append(final_video)
output_args['vcodec'] = export_settings['vcodec']
if export_settings.get('v_bitrate'): output_args['b:v'] = export_settings['v_bitrate']

if has_audio:
final_audio = ffmpeg.filter(track_audio_streams, 'amix', inputs=len(track_audio_streams), duration='longest')
stream_args.append(final_audio)
output_args['acodec'] = export_settings['acodec']
if export_settings.get('a_bitrate'): output_args['b:a'] = export_settings['a_bitrate']

if not has_audio:
output_args['an'] = None

if not stream_args:
raise ValueError("No streams to output. Check export settings.")

ffmpeg_cmd = ffmpeg.output(*stream_args, export_settings['output_path'], **output_args).overwrite_output().compile()

except Exception as e:
self.finished.emit(False, f"Error building FFmpeg command: {e}")
self._is_running = False
return

self.worker_thread = QThread()
self.worker = _ExportRunner(ffmpeg_cmd, total_dur_ms)
self.worker.moveToThread(self.worker_thread)

self.worker.progress.connect(self.progress.emit)
self.worker.finished.connect(self._on_export_runner_finished)

self.worker_thread.started.connect(self.worker.run)
self.worker_thread.start()

def _on_export_runner_finished(self, success, message):
self._is_running = False
self.finished.emit(success, message)

if self.worker_thread:
self.worker_thread.quit()
self.worker_thread.wait()
self.worker_thread = None
self.worker = None

def cancel_export(self):
if self.worker and self.worker.get_process() and self.worker.get_process().poll() is None:
self.worker.get_process().terminate()
print("Export cancelled by user.")
8 changes: 8 additions & 0 deletions icons/pause.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions icons/play.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions icons/previous_frame.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions icons/snap_to_start.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions icons/stop.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading