added media playback
All checks were successful
PyInstaller Build / build (push) Successful in 2m23s
All checks were successful
PyInstaller Build / build (push) Successful in 2m23s
This commit is contained in:
183
main.py
183
main.py
@@ -3,6 +3,7 @@ import threading
|
||||
import shutil
|
||||
import subprocess
|
||||
import wx
|
||||
import wx.media
|
||||
|
||||
AUDIO_FORMATS = ["mp3", "wav", "aac", "flac", "ogg", "m4a"]
|
||||
VIDEO_FORMATS = ["mp4", "mkv", "mov", "webm"]
|
||||
@@ -30,8 +31,27 @@ class ConverterFrame(wx.Frame):
|
||||
self.log_ctrl = wx.TextCtrl(panel, style=wx.TE_MULTILINE | wx.TE_READONLY)
|
||||
self.status = wx.StaticText(panel, label="Ready")
|
||||
|
||||
self.media = wx.media.MediaCtrl(panel, style=wx.SIMPLE_BORDER)
|
||||
self.play_btn = wx.Button(panel, label="Play/Pause")
|
||||
self.stop_btn = wx.Button(panel, label="Stop")
|
||||
self.position_slider = wx.Slider(panel, minValue=0, maxValue=1000, style=wx.SL_HORIZONTAL)
|
||||
self.time_label = wx.StaticText(panel, label="00:00 / 00:00")
|
||||
self.hotkeys_label = wx.StaticText(
|
||||
panel,
|
||||
label="Hotkeys: Space Play/Pause, Ctrl+S Stop, Ctrl+Enter Convert, Alt+Left/Right Seek",
|
||||
)
|
||||
self.media_length_ms = 0
|
||||
self.dragging_slider = False
|
||||
self.pending_play = False
|
||||
self.media_loaded = False
|
||||
self.loaded_media_path = ""
|
||||
self.player_timer = wx.Timer(self)
|
||||
|
||||
self.audio_bitrate.SetHint("e.g. 192k")
|
||||
self.video_bitrate.SetHint("e.g. 2000k")
|
||||
self.media.SetMinSize((200, 36))
|
||||
if hasattr(self.media, "SetVolume"):
|
||||
self.media.SetVolume(1.0)
|
||||
|
||||
form = wx.FlexGridSizer(cols=3, hgap=8, vgap=8)
|
||||
form.Add(wx.StaticText(panel, label="Input"), 0, wx.ALIGN_CENTER_VERTICAL)
|
||||
@@ -67,6 +87,7 @@ class ConverterFrame(wx.Frame):
|
||||
main = wx.BoxSizer(wx.VERTICAL)
|
||||
main.Add(form, 0, wx.EXPAND | wx.ALL, 12)
|
||||
main.Add(self.start_btn, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 12)
|
||||
main.Add(self.build_player(panel), 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 12)
|
||||
main.Add(self.log_ctrl, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 12)
|
||||
main.Add(self.status, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 12)
|
||||
|
||||
@@ -77,8 +98,19 @@ class ConverterFrame(wx.Frame):
|
||||
self.input_picker.Bind(wx.EVT_FILEPICKER_CHANGED, self.on_input_change)
|
||||
self.start_btn.Bind(wx.EVT_BUTTON, self.on_start)
|
||||
|
||||
self.play_btn.Bind(wx.EVT_BUTTON, self.on_play_pause)
|
||||
self.stop_btn.Bind(wx.EVT_BUTTON, self.on_stop)
|
||||
self.position_slider.Bind(wx.EVT_SLIDER, self.on_seek)
|
||||
self.position_slider.Bind(wx.EVT_SCROLL_THUMBRELEASE, self.on_seek_release)
|
||||
self.Bind(wx.media.EVT_MEDIA_LOADED, self.on_media_loaded, self.media)
|
||||
self.Bind(wx.media.EVT_MEDIA_FINISHED, self.on_media_finished, self.media)
|
||||
self.Bind(wx.EVT_TIMER, self.on_player_timer, self.player_timer)
|
||||
|
||||
self.setup_hotkeys()
|
||||
|
||||
self.update_format_choices()
|
||||
self.Center()
|
||||
self.player_timer.Start(250)
|
||||
|
||||
def on_mode_change(self, _event):
|
||||
self.update_format_choices()
|
||||
@@ -89,6 +121,7 @@ class ConverterFrame(wx.Frame):
|
||||
|
||||
def on_input_change(self, _event):
|
||||
self.suggest_output_path()
|
||||
self.load_media_from_input()
|
||||
|
||||
def update_format_choices(self):
|
||||
mode = self.mode_choice.GetStringSelection()
|
||||
@@ -108,6 +141,156 @@ class ConverterFrame(wx.Frame):
|
||||
suggested = base + "." + out_ext
|
||||
self.output_picker.SetPath(suggested)
|
||||
|
||||
def build_player(self, panel):
|
||||
player = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
controls = wx.BoxSizer(wx.HORIZONTAL)
|
||||
controls.Add(self.play_btn, 0, wx.RIGHT, 8)
|
||||
controls.Add(self.stop_btn, 0, wx.RIGHT, 8)
|
||||
controls.Add(self.time_label, 0, wx.ALIGN_CENTER_VERTICAL)
|
||||
|
||||
player.Add(wx.StaticText(panel, label="Audio Player"), 0, wx.BOTTOM, 4)
|
||||
player.Add(self.media, 0, wx.EXPAND | wx.BOTTOM, 6)
|
||||
player.Add(self.position_slider, 0, wx.EXPAND | wx.BOTTOM, 6)
|
||||
player.Add(controls, 0, wx.EXPAND | wx.BOTTOM, 4)
|
||||
player.Add(self.hotkeys_label, 0)
|
||||
|
||||
return player
|
||||
|
||||
def setup_hotkeys(self):
|
||||
self.ID_PLAYPAUSE = wx.NewIdRef()
|
||||
self.ID_STOP = wx.NewIdRef()
|
||||
self.ID_CONVERT = wx.NewIdRef()
|
||||
self.ID_SEEK_BACK = wx.NewIdRef()
|
||||
self.ID_SEEK_FWD = wx.NewIdRef()
|
||||
|
||||
entries = [
|
||||
wx.AcceleratorEntry(wx.ACCEL_NORMAL, wx.WXK_SPACE, self.ID_PLAYPAUSE),
|
||||
wx.AcceleratorEntry(wx.ACCEL_CTRL, ord("S"), self.ID_STOP),
|
||||
wx.AcceleratorEntry(wx.ACCEL_CTRL, wx.WXK_RETURN, self.ID_CONVERT),
|
||||
wx.AcceleratorEntry(wx.ACCEL_ALT, wx.WXK_LEFT, self.ID_SEEK_BACK),
|
||||
wx.AcceleratorEntry(wx.ACCEL_ALT, wx.WXK_RIGHT, self.ID_SEEK_FWD),
|
||||
]
|
||||
|
||||
self.SetAcceleratorTable(wx.AcceleratorTable(entries))
|
||||
self.Bind(wx.EVT_MENU, self.on_play_pause, id=self.ID_PLAYPAUSE)
|
||||
self.Bind(wx.EVT_MENU, self.on_stop, id=self.ID_STOP)
|
||||
self.Bind(wx.EVT_MENU, self.on_start, id=self.ID_CONVERT)
|
||||
self.Bind(wx.EVT_MENU, self.on_seek_back, id=self.ID_SEEK_BACK)
|
||||
self.Bind(wx.EVT_MENU, self.on_seek_forward, id=self.ID_SEEK_FWD)
|
||||
|
||||
def load_media_from_input(self):
|
||||
path = self.input_picker.GetPath()
|
||||
if not path:
|
||||
return
|
||||
self.load_media(path)
|
||||
|
||||
def load_media(self, path):
|
||||
if not os.path.isfile(path):
|
||||
return
|
||||
self.media_length_ms = 0
|
||||
self.media_loaded = False
|
||||
self.loaded_media_path = path
|
||||
self.position_slider.SetRange(0, 1000)
|
||||
self.position_slider.SetValue(0)
|
||||
self.update_time_label(0)
|
||||
if not self.media.Load(path):
|
||||
self.pending_play = False
|
||||
self.append_log("Unable to load media: " + path)
|
||||
else:
|
||||
self.media_loaded = True
|
||||
|
||||
def on_media_loaded(self, _event):
|
||||
self.media_loaded = True
|
||||
length = self.media.Length()
|
||||
if length and length > 0:
|
||||
self.media_length_ms = length
|
||||
self.position_slider.SetRange(0, length)
|
||||
self.update_time_label(0)
|
||||
if self.pending_play:
|
||||
self.pending_play = False
|
||||
self.media.Play()
|
||||
|
||||
def on_media_finished(self, _event):
|
||||
self.update_time_label(self.media_length_ms)
|
||||
if self.media_length_ms > 0:
|
||||
self.position_slider.SetValue(self.media_length_ms)
|
||||
|
||||
def on_player_timer(self, _event):
|
||||
if self.dragging_slider or self.media_length_ms <= 0:
|
||||
return
|
||||
if self.media.GetState() == wx.media.MEDIASTATE_PLAYING:
|
||||
position = self.media.Tell()
|
||||
if position >= 0:
|
||||
self.position_slider.SetValue(position)
|
||||
self.update_time_label(position)
|
||||
|
||||
def on_play_pause(self, _event):
|
||||
path = self.input_picker.GetPath()
|
||||
if not path:
|
||||
return
|
||||
|
||||
state = self.media.GetState()
|
||||
if state == wx.media.MEDIASTATE_PLAYING:
|
||||
self.pending_play = False
|
||||
self.media.Pause()
|
||||
return
|
||||
|
||||
if (not self.media_loaded) or (self.loaded_media_path != path):
|
||||
self.load_media(path)
|
||||
|
||||
if self.media.Play():
|
||||
self.pending_play = False
|
||||
else:
|
||||
self.pending_play = True
|
||||
|
||||
def on_stop(self, _event):
|
||||
self.pending_play = False
|
||||
self.media.Stop()
|
||||
self.position_slider.SetValue(0)
|
||||
self.update_time_label(0)
|
||||
|
||||
def on_seek(self, _event):
|
||||
self.dragging_slider = True
|
||||
if self.media_length_ms <= 0:
|
||||
return
|
||||
self.update_time_label(self.position_slider.GetValue())
|
||||
|
||||
def on_seek_release(self, _event):
|
||||
self.dragging_slider = False
|
||||
if self.media_length_ms <= 0:
|
||||
return
|
||||
self.media.Seek(self.position_slider.GetValue())
|
||||
|
||||
def on_seek_back(self, _event):
|
||||
self.seek_relative(-5000)
|
||||
|
||||
def on_seek_forward(self, _event):
|
||||
self.seek_relative(5000)
|
||||
|
||||
def seek_relative(self, delta_ms):
|
||||
if self.media_length_ms <= 0:
|
||||
return
|
||||
position = self.media.Tell()
|
||||
if position < 0:
|
||||
position = 0
|
||||
new_pos = max(0, min(self.media_length_ms, position + delta_ms))
|
||||
self.media.Seek(new_pos)
|
||||
self.position_slider.SetValue(new_pos)
|
||||
self.update_time_label(new_pos)
|
||||
|
||||
def update_time_label(self, position_ms):
|
||||
total = self.media_length_ms
|
||||
self.time_label.SetLabel(
|
||||
f"{self.format_time(position_ms)} / {self.format_time(total)}"
|
||||
)
|
||||
|
||||
def format_time(self, millis):
|
||||
seconds = max(0, int(millis / 1000))
|
||||
minutes = seconds // 60
|
||||
seconds = seconds % 60
|
||||
return f"{minutes:02d}:{seconds:02d}"
|
||||
|
||||
def on_start(self, _event):
|
||||
if not shutil.which("ffmpeg"):
|
||||
wx.MessageBox("ffmpeg not found on PATH.", "Error", wx.ICON_ERROR)
|
||||
|
||||
Reference in New Issue
Block a user