mirror of
https://github.com/YusufB5/ASCILINE.git
synced 2026-06-23 22:48:06 +02:00
feat: yt-dlp support added and playback issues fixed
This commit is contained in:
parent
6047f8ed22
commit
0df68b97fe
3 changed files with 28 additions and 9 deletions
3
app.js
3
app.js
|
|
@ -206,6 +206,7 @@ function connectWebSocket() {
|
|||
if (seekPlayed) seekPlayed.style.transform = 'scaleX(0)';
|
||||
|
||||
audioOffset = 0;
|
||||
scrubMeta = null; // reset so new video gets fresh thumbnails
|
||||
// Lazy-load hover thumbnails: only fetch on first hover
|
||||
const qIdx = currentQueueIdx;
|
||||
if (seekWrap && !scrubMeta) {
|
||||
|
|
@ -558,7 +559,7 @@ function skip(delta) {
|
|||
function setupScrub(v) {
|
||||
scrubMeta = null;
|
||||
if (seekPreviewImg) seekPreviewImg.style.backgroundImage = '';
|
||||
fetch('/scrub?v=' + (v || 0)).then(r => r.json()).then(m => {
|
||||
fetch('/scrub?v=' + (v || 0) + '&t=' + Date.now()).then(r => r.json()).then(m => {
|
||||
if (!m || !m.available || !seekPreviewImg) return;
|
||||
scrubMeta = m;
|
||||
seekPreviewImg.style.width = m.cellW + 'px';
|
||||
|
|
|
|||
|
|
@ -42,17 +42,29 @@ def get_video_dimensions(path: str) -> tuple[int, int]:
|
|||
return w, h
|
||||
|
||||
|
||||
def calc_auto_rows(cols: int, vid_w: int, vid_h: int, pixel_mode: bool) -> int:
|
||||
def calc_auto_dimensions(cols: int, vid_w: int, vid_h: int, pixel_mode: bool) -> tuple[int, int]:
|
||||
"""
|
||||
Calculate rows from video aspect ratio.
|
||||
Calculate (cols, rows) from video aspect ratio.
|
||||
ASCII mode: characters are ~2x taller than wide, so divide by 2.
|
||||
Pixel mode: cells are square (CSS stretches), no correction needed.
|
||||
"""
|
||||
# Pixel mode uses GPU-accelerated fillRect → generous cap
|
||||
# ASCII mode uses CPU fillText per cell → tight cap to prevent stutter on vertical videos
|
||||
MAX_ROWS = 350 if pixel_mode else 100
|
||||
ratio = vid_w / max(vid_h, 1)
|
||||
|
||||
if pixel_mode:
|
||||
return max(1, round(cols / ratio))
|
||||
rows = max(1, round(cols / ratio))
|
||||
else:
|
||||
return max(1, round(cols / ratio / 2))
|
||||
rows = max(1, round(cols / ratio / 2))
|
||||
|
||||
if rows > MAX_ROWS:
|
||||
# Scale down BOTH cols and rows to preserve aspect ratio
|
||||
scale = MAX_ROWS / rows
|
||||
rows = MAX_ROWS
|
||||
cols = max(1, round(cols * scale))
|
||||
|
||||
return cols, rows
|
||||
|
||||
# Serve only whitelisted static files (security: prevents directory traversal)
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
|
@ -350,7 +362,8 @@ async def scrub_meta(v: int | None = None):
|
|||
if not built:
|
||||
return Response(content='{"available": false}', media_type="application/json")
|
||||
meta = dict(built["meta"])
|
||||
meta["sprite"] = f"/scrub_sprite?v={v if v is not None else 0}"
|
||||
vid_id = os.path.basename(video_path)
|
||||
meta["sprite"] = f"/scrub_sprite?v={v if v is not None else 0}&id={vid_id}"
|
||||
return Response(content=_json.dumps(meta), media_type="application/json")
|
||||
|
||||
|
||||
|
|
@ -461,7 +474,7 @@ async def websocket_endpoint(websocket: WebSocket):
|
|||
continue
|
||||
|
||||
if rows_cfg == 0:
|
||||
rows = calc_auto_rows(cols, vid_w, vid_h, pixel_mode)
|
||||
cols, rows = calc_auto_dimensions(cols, vid_w, vid_h, pixel_mode)
|
||||
print(f"[AUTO] {vid_w}x{vid_h} → grid {cols}x{rows}")
|
||||
else:
|
||||
rows = rows_cfg
|
||||
|
|
@ -860,6 +873,8 @@ if __name__ == "__main__":
|
|||
# ── High FPS Warning ──
|
||||
high_fps_videos = []
|
||||
for entry in queue:
|
||||
if ytdl.is_url(entry['video']):
|
||||
continue # skip remote URLs; yt-dlp normalizes to 30 FPS
|
||||
cap = cv2.VideoCapture(entry['video'])
|
||||
if cap.isOpened():
|
||||
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||
|
|
@ -893,9 +908,12 @@ if __name__ == "__main__":
|
|||
print(f" \033[32m▶\033[0m \033[1mResolution\033[0m: {res_str}")
|
||||
print(f" \033[32m▶\033[0m \033[1mDefault\033[0m : mode={args.mode} | pixel={'ON' if args.pixel else 'OFF'} | vol={args.vol}")
|
||||
print(f"\033[1;37m{'─'*55}\033[0m")
|
||||
for i, entry in enumerate(queue, 1):
|
||||
MAX_DISPLAY = 10
|
||||
for i, entry in enumerate(queue[:MAX_DISPLAY], 1):
|
||||
px = ' \033[35m[PIXEL]\033[0m' if entry.get('pixel') else ''
|
||||
print(f" {i:2}. \033[36m{entry['video']}\033[0m (mode={entry['mode']}{px} vol={entry['vol']})")
|
||||
if len(queue) > MAX_DISPLAY:
|
||||
print(f" \033[90m... and {len(queue) - MAX_DISPLAY} more\033[0m")
|
||||
print(f"\033[1;37m{'═'*55}\033[0m\n")
|
||||
print(f" \033[1;32m🚀 Server live →\033[0m \033[4;36mhttp://localhost:{args.port}\033[0m\n")
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ def test_normalize_rejects_audio_only(tmp_path):
|
|||
audio = tmp_path / "audio_only.mp4"
|
||||
r = subprocess.run(
|
||||
["ffmpeg", "-y", "-f", "lavfi", "-i", "sine=frequency=440:duration=1",
|
||||
"-c:a", "aac", "-loglevel", "error", str(audio)],
|
||||
"-c:a", "aac", "-strict", "-2", "-loglevel", "error", str(audio)],
|
||||
capture_output=True, text=True)
|
||||
assert r.returncode == 0, r.stderr
|
||||
with pytest.raises(RuntimeError, match="no video stream"):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue