88import io # string jail
99import shutil # delete stuff fast
1010import atexit # clean up my mess
11+ import subprocess # command line wizard
1112
1213# stderr: go to sleep
1314class SuppressStderr :
@@ -349,6 +350,10 @@ def play_ascii_video_stream_streaming(folder, audio, frame_queue, total_frames,
349350 stop_flag = threading .Event ()
350351 pause_flag = threading .Event ()
351352 pause_flag .clear ()
353+ playback_state = {
354+ 'volume' : 1.0 ,
355+ 'is_muted' : False ,
356+ }
352357 rewind_forward = pyqueue .Queue ()
353358
354359 def keyboard_listener ():
@@ -366,6 +371,15 @@ def keyboard_listener():
366371 pause_flag .set ()
367372 elif key in ('q' , 'Q' ):
368373 stop_flag .set ()
374+ elif key in ('m' , 'M' ):
375+ playback_state ['is_muted' ] = not playback_state ['is_muted' ]
376+ elif key in ('-' , '_' ):
377+ # Decrease volume, ensure it doesn't go below 0
378+ playback_state ['volume' ] = max (0.0 , round (playback_state ['volume' ] - 0.1 , 1 ))
379+ elif key in ('+' , '=' ):
380+ # Increase volume, ensure it doesn't go above 1.0, and unmute
381+ playback_state ['volume' ] = min (1.0 , round (playback_state ['volume' ] + 0.1 , 1 ))
382+ playback_state ['is_muted' ] = False
369383 elif key in ('a' , 'A' ):
370384 rewind_forward .put (- 5 * speed ) # rewind 5s
371385 elif key in ('d' , 'D' ):
@@ -395,6 +409,7 @@ def keyboard_listener():
395409
396410 def play_audio_from (pos ):
397411 pygame .mixer .music .load (audio )
412+ pygame .mixer .music .set_volume (0.0 if playback_state ['is_muted' ] else playback_state ['volume' ])
398413 pygame .mixer .music .play (start = pos )
399414
400415 def format_time (t ):
@@ -469,6 +484,11 @@ def format_time(t):
469484 sleep_for = tgt - now
470485 if sleep_for > 0 :
471486 time .sleep (sleep_for )
487+
488+ # Apply volume changes from keyboard continuously
489+ current_vol = 0.0 if playback_state ['is_muted' ] else playback_state ['volume' ]
490+ if pygame .mixer .music .get_volume () != current_vol :
491+ pygame .mixer .music .set_volume (current_vol )
472492 # Check terminal size
473493 try :
474494 term_size = shutil .get_terminal_size ()
@@ -487,13 +507,23 @@ def format_time(t):
487507 print ('Buffering...' .center (actual_frame_wide ), end = '\n ' ) # Use actual_frame_wide for centering
488508 # --- Seek bar with play/pause and time ---
489509 play_emoji = '⏸️' if not pause_flag .is_set () else '▶️'
510+
511+ # Volume UI
512+ vol_bar_width = 10
513+ vol_icon = '🔇' if playback_state ['is_muted' ] or playback_state ['volume' ] == 0 else '🔊'
514+ if playback_state ['is_muted' ]:
515+ vol_bar = '-' * vol_bar_width
516+ else :
517+ vol_level = int (playback_state ['volume' ] * vol_bar_width )
518+ vol_bar = '█' * vol_level + '-' * (vol_bar_width - vol_level )
519+ vol_str = f" { vol_icon } [{ vol_bar } ]"
520+
490521 time_str = f"{ format_time (i / speed )} / { format_time (total_time )} "
491- # Assume emoji is 2 cells wide. Total fixed width is emoji(2) + " [] "(4) + time_str
492- fixed_len = 2 + 4 + len (time_str )
522+ fixed_len = 2 + 4 + len (time_str ) + len (vol_str )
493523 bar_width = max (1 , actual_frame_wide - fixed_len ) # Use actual_frame_wide here
494524 bar_pos = int ((i / (total_frames - 1 )) * bar_width ) if total_frames > 1 else 0
495525 bar = '█' * bar_pos + '-' * (bar_width - bar_pos )
496- print (f"{ play_emoji } [{ bar } ] { time_str } " )
526+ print (f"{ play_emoji } [{ bar } ] { time_str } { vol_str } " )
497527 # --- End seek bar ---
498528 i += 1
499529 if frames_buffer :
@@ -533,7 +563,9 @@ def box_line(text, color=text_color):
533563 f"{ box_color } ╔{ '═' * (box_width - 2 )} ╗{ reset } " ,
534564 box_line (" Turns videos into ugly terminal art. With sound. " ),
535565 box_line (" Made by a lazy coder. " + Fore .MAGENTA + "@github/SajagIN" + text_color + " " ),
536- box_line (f" { Fore .GREEN } Space{ reset } { text_color } = pause { Fore .GREEN } Q{ reset } { text_color } = quit { Fore .GREEN } A/D{ reset } { text_color } = rewind/forward 5s " ),
566+ box_line (f" { Fore .GREEN } Space{ reset } { text_color } = pause/play { Fore .GREEN } Q{ reset } { text_color } = quit" ),
567+ box_line (f" { Fore .GREEN } A/D{ reset } { text_color } = seek 5s { Fore .GREEN } ←/→{ reset } { text_color } = seek 1s" ),
568+ box_line (f" { Fore .GREEN } M{ reset } { text_color } = mute { Fore .GREEN } +/-{ reset } { text_color } = volume" ),
537569 f"{ box_color } ╚{ '═' * (box_width - 2 )} ╝{ reset } " ,
538570 ""
539571 ]
@@ -566,6 +598,35 @@ def box_line(text, color=text_color):
566598 atexit .unregister_all = getattr (atexit , 'unregister_all' , lambda : None ) # For repeated runs in interactive mode
567599 atexit .unregister_all ()
568600 atexit .register (lambda : cleanup_temp_folder (temp ))
601+
602+ # --- Downscale to 144p if needed ---
603+ # Ensure temp exists
604+ if not os .path .exists (temp ):
605+ os .makedirs (temp )
606+ # Check video resolution
607+ try :
608+ clip = VideoFileClip (vid )
609+ w , h = clip .size
610+ clip .close ()
611+ if h > 144 :
612+ # Downscale to 144p and use as new source
613+ downscaled_path = os .path .join (temp , 'downscaled_144p.mp4' )
614+ # Use ffmpeg from env or system
615+ ffmpeg_bin = os .environ .get ('FFMPEG_BINARY' , 'ffmpeg' )
616+ # -y to overwrite, -vf scale=-2:144 to keep aspect
617+ cmd = [ffmpeg_bin , '-y' , '-i' , vid , '-vf' , 'scale=-2:144' , '-c:v' , 'libx264' , '-preset' , 'ultrafast' , '-crf' , '28' , '-c:a' , 'copy' , downscaled_path ]
618+ try :
619+ subprocess .run (cmd , check = True , stdout = subprocess .DEVNULL , stderr = subprocess .DEVNULL )
620+ vid = downscaled_path
621+ except Exception as e :
622+ print (Fore .RED + f"Downscaling failed: { e } " + reset )
623+ sys .exit (1 )
624+ # else: use original vid
625+ except Exception as e :
626+ print (Fore .RED + f"Failed to check video resolution: { e } " + reset )
627+ sys .exit (1 )
628+ # --- End downscale logic ---
629+
569630 try :
570631 frames , audio , frame_queue , total_frames , video_duration = get_stuff_from_video_stream (vid , temp , speed = fps , buffer_size = fps )
571632 print (Fore .GREEN + Style .BRIGHT + 'Streaming ASCII video...' + reset )
0 commit comments