import streamlit as st import tempfile import tempfile import os import time import ffmpeg import subprocess import platform from db import init_db, insert_record, get_records # Initialize Database init_db() st.set_page_config(page_title="Video Compressor", layout="wide") def get_video_info(file_path): try: probe = ffmpeg.probe(file_path) video_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'video'), None) audio_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'audio'), None) duration = float(probe['format']['duration']) width = int(video_stream['width']) height = int(video_stream['height']) # Audio bitrate defaults to 128k if not found or 0 audio_bitrate = 128000 if audio_stream and 'bit_rate' in audio_stream: audio_bitrate = int(audio_stream['bit_rate']) elif 'bit_rate' in probe['format']: # fallback approximation audio_bitrate = 128000 return { 'duration': duration, 'width': width, 'height': height, 'audio_bitrate': audio_bitrate } except Exception as e: st.error(f"Error probing video: {e}") return None def compress_video_2pass(input_path, output_path, start_time, end_time, target_size_mb, resolution, fps, progress_bar): info = get_video_info(input_path) if not info: return False duration = end_time - start_time if duration <= 0: st.error("Invalid trim times (End time must be greater than Start time).") return False # Calculate bitrates # Target size in bits target_size_bits = target_size_mb * 8388608 # MB to bits (1024 * 1024 * 8) # Audio bitrate in bits per second audio_bitrate = info['audio_bitrate'] # Required total bitrate (b/s) total_bitrate = target_size_bits / duration # Required video bitrate (b/s) video_bitrate = total_bitrate - audio_bitrate # Ensure a minimum video bitrate to avoid errors if video_bitrate < 10000: st.warning("Target size is too small for this duration. Video quality will be extremely poor or fail. Setting a minimum bitrate.") video_bitrate = 10000 video_bitrate_k = int(video_bitrate / 1000) audio_bitrate_k = int(audio_bitrate / 1000) # Resolution scaling parameter scale_param = "" if resolution != "Original": height = int(resolution.replace("p", "")) scale_param = f"scale=-2:{height}" devnull = os.devnull input_kwargs = {'ss': start_time, 'to': end_time} # Setup Streams v_ext = ffmpeg.input(input_path, **input_kwargs) a_ext = v_ext.audio v = v_ext.video if scale_param: v = v.filter('scale', -2, int(resolution.replace('p', ''))) if fps and fps != "Original": v = v.filter('fps', fps=int(fps)) # Pass 1 progress_bar.progress(20, text="Pass 1/2: Analyzing Video...") passlog_base = os.path.join(tempfile.gettempdir(), f"ffmpeg2pass_{int(time.time())}") try: pass1_args = { 'c:v': 'libx264', 'b:v': f'{video_bitrate_k}k', 'pass': 1, 'passlogfile': passlog_base, 'f': 'mp4', 'y': None # overwrite } # In Windows, pass 1 log file must be handled properly, ffmpeg-python creates ffmpeg2pass-0.log in current dir # We construct the pass 1 output out1 = ffmpeg.output(v, devnull, **pass1_args) ffmpeg.run(out1, quiet=True, overwrite_output=True) except ffmpeg.Error as e: if e.stderr: st.error(f"Pass 1 Error: {e.stderr.decode('utf8')}") return False # Pass 2 progress_bar.progress(60, text="Pass 2/2: Compressing Video...") try: pass2_args = { 'c:v': 'libx264', 'b:v': f'{video_bitrate_k}k', 'pass': 2, 'passlogfile': passlog_base, 'c:a': 'aac', 'b:a': f'{audio_bitrate_k}k', 'y': None # overwrite } out2 = ffmpeg.output(v, a_ext, output_path, **pass2_args) ffmpeg.run(out2, quiet=True, overwrite_output=True) except ffmpeg.Error as e: if e.stderr: st.error(f"Pass 2 Error: {e.stderr.decode('utf8')}") return False finally: # Clean up pass log files for ext in ['-0.log', '-0.log.mbtree']: logfile = f"{passlog_base}{ext}" if os.path.exists(logfile): try: os.remove(logfile) except: pass progress_bar.progress(100, text="Done!") return True st.title("Video Compressor & Trimmer") st.markdown("Upload a video, trim it, set a target size, and this tool will use **2-pass encoding** to perfectly hit your target size (MB).") tab1, tab2 = st.tabs(["Compressor", "History"]) with tab1: uploaded_file = st.file_uploader("Upload Video", type=['mp4', 'mov', 'avi', 'mkv']) if uploaded_file is not None: # Save temp file with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as temp_input: temp_input.write(uploaded_file.read()) temp_input_path = temp_input.name info = get_video_info(temp_input_path) if info: st.success(f"Video Loaded: {info['duration']:.2f} seconds | {info['width']}x{info['height']}") col1, col2 = st.columns(2) with col1: start_time = st.number_input("Start Time (seconds)", min_value=0.0, max_value=info['duration']-0.1, value=0.0, step=1.0) with col2: end_time = st.number_input("End Time (seconds)", min_value=0.1, max_value=info['duration'], value=info['duration'], step=1.0) col3, col4, col5 = st.columns(3) with col3: resolution = st.selectbox("Resolution", ["Original", "1080p", "720p", "480p", "360p"]) with col4: fps = st.selectbox("FPS", ["Original", "60", "30", "24", "15"]) with col5: target_size = st.number_input("Target Size (MB)", min_value=0.1, value=10.0, step=1.0) if st.button("Compress & Export"): st.info("Ensure you have `ffmpeg` installed on your system.") progress_bar = st.progress(0, text="Initializing...") with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as temp_output: temp_output_path = temp_output.name start_perf = time.time() success = compress_video_2pass(temp_input_path, temp_output_path, start_time, end_time, target_size, resolution, fps, progress_bar) end_perf = time.time() if success: final_size_bytes = os.path.getsize(temp_output_path) final_size_mb = final_size_bytes / (1024 * 1024) duration_sec = end_time - start_time st.success(f"Compression Successful! Time taken: {end_perf - start_perf:.2f}s") st.metric("Final Size", f"{final_size_mb:.2f} MB") with open(temp_output_path, "rb") as file: btn = st.download_button( label="Download Compressed Video", data=file, file_name=f"compressed_{uploaded_file.name}", mime="video/mp4" ) # Log to DB insert_record(uploaded_file.name, target_size, final_size_mb, duration_sec, resolution, fps if fps != "Original" else info.get('r_frame_rate', 0)) # cleanup output temp file after offering download # os.remove(temp_output_path) -> actually we shouldn't remove immediately if download button needs it # Streamlit's download button reads from memory here since we opened it. # Cleanup input temp file if we change files or stop # (This is a simplistic approach, in production we'd use a more robust cleanup) with tab2: st.header("Compression History") records = get_records() if not records: st.write("No compression history found.") else: import pandas as pd df = pd.DataFrame(records, columns=['ID', 'Original File', 'Target Size (MB)', 'Final Size (MB)', 'Duration (s)', 'Resolution', 'FPS', 'Timestamp']) st.dataframe(df, hide_index=True)