feat(dashcam): mobile layout fix + optional hw-encode conversion on import
- yaris.css: on mobile (<576px) hide file/size/date/upload cols, show filename inside thumb cell, icon-only buttons, dash-actions-td nowrap - local-videos.js: filename in thumb cell for mobile; Converting badge with spinner; auto-refresh every 10s while any file is converting - app_video.py: CONVERT_SETTING, is_convert_enabled(), queue_convert(), _convert_bg() — h264_v4l2m2m + aac faststart, deletes original on success - app_dash.py: call queue_convert() after each successful copy on import - app_settings.py: expose video_convert_enabled in get/save settings - settings.html: toggle for 'Convert imported videos for web playback' Co-authored-by: Copilot <[email protected]>
This commit is contained in:
+70
-1
@@ -12,10 +12,76 @@ from modules import app_dash, app_sql
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
THUMBNAIL_DIR = os.path.join(BASE_DIR, "dash", "thumbnails")
|
||||
RETENTION_SETTING = "local_video_retention_days"
|
||||
CONVERT_SETTING = "video_convert_enabled"
|
||||
_thumbnail_lock = Semaphore()
|
||||
# Tracks which relative paths are currently being generated (set of strings)
|
||||
_generating = set()
|
||||
_generating_lock = Semaphore()
|
||||
_converting = set()
|
||||
_converting_lock = Semaphore()
|
||||
|
||||
|
||||
def is_convert_enabled():
|
||||
return app_sql.get_setting(CONVERT_SETTING, "0") == "1"
|
||||
|
||||
|
||||
def queue_convert(relative_path):
|
||||
"""Spawn a background greenlet to re-encode relative_path to web-ready MP4.
|
||||
No-op if conversion is disabled, already running, or file is already MP4."""
|
||||
if not is_convert_enabled():
|
||||
return
|
||||
if relative_path.lower().endswith(".mp4"):
|
||||
return
|
||||
with _converting_lock:
|
||||
if relative_path in _converting:
|
||||
return
|
||||
_converting.add(relative_path)
|
||||
gevent.spawn(_convert_bg, relative_path)
|
||||
|
||||
|
||||
def _convert_bg(relative_path):
|
||||
try:
|
||||
source_path = _local_path(relative_path)
|
||||
mp4_relative = os.path.splitext(relative_path)[0] + ".mp4"
|
||||
mp4_path = _local_path(mp4_relative)
|
||||
|
||||
if not app_dash.begin_local_video_use(relative_path):
|
||||
return
|
||||
|
||||
try:
|
||||
tmp_path = mp4_path + ".converting.mp4"
|
||||
cmd = [
|
||||
"ffmpeg", "-nostdin", "-loglevel", "error", "-y",
|
||||
"-i", source_path,
|
||||
"-c:v", "h264_v4l2m2m",
|
||||
"-c:a", "aac",
|
||||
"-movflags", "faststart",
|
||||
"-f", "mp4",
|
||||
tmp_path,
|
||||
]
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
while proc.poll() is None:
|
||||
sleep(1)
|
||||
if proc.returncode == 0 and os.path.isfile(tmp_path) and os.path.getsize(tmp_path) > 0:
|
||||
os.replace(tmp_path, mp4_path)
|
||||
# Delete original now that MP4 is ready
|
||||
try:
|
||||
os.remove(source_path)
|
||||
except OSError:
|
||||
pass
|
||||
# Also remove old thumbnail so it gets regenerated from MP4
|
||||
_source, thumb = _thumbnail_path(relative_path)
|
||||
if os.path.isfile(thumb):
|
||||
try:
|
||||
os.remove(thumb)
|
||||
except OSError:
|
||||
pass
|
||||
elif os.path.exists(tmp_path):
|
||||
os.remove(tmp_path)
|
||||
finally:
|
||||
app_dash.end_local_video_use(relative_path)
|
||||
finally:
|
||||
with _converting_lock:
|
||||
_converting.discard(relative_path)
|
||||
|
||||
|
||||
def _local_path(relative_path):
|
||||
@@ -47,6 +113,8 @@ def list_local_videos():
|
||||
except OSError as e:
|
||||
errors.append(f"{relative_path}: {e}")
|
||||
continue
|
||||
with _converting_lock:
|
||||
converting = relative_path in _converting
|
||||
files.append(
|
||||
{
|
||||
"path": relative_path,
|
||||
@@ -57,6 +125,7 @@ def list_local_videos():
|
||||
stat.st_size,
|
||||
stat.st_mtime,
|
||||
) in uploaded_stats,
|
||||
"converting": converting,
|
||||
}
|
||||
)
|
||||
total_size += stat.st_size
|
||||
|
||||
Reference in New Issue
Block a user