| | 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
|
| |
|
| |
|
| | 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 = 128000
|
| | if audio_stream and 'bit_rate' in audio_stream:
|
| | audio_bitrate = int(audio_stream['bit_rate'])
|
| | elif 'bit_rate' in probe['format']:
|
| |
|
| | 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
|
| |
|
| |
|
| |
|
| | target_size_bits = target_size_mb * 8388608
|
| |
|
| |
|
| | audio_bitrate = info['audio_bitrate']
|
| |
|
| |
|
| | total_bitrate = target_size_bits / duration
|
| |
|
| |
|
| | video_bitrate = total_bitrate - audio_bitrate
|
| |
|
| |
|
| | 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)
|
| |
|
| |
|
| | 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}
|
| |
|
| |
|
| | 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))
|
| |
|
| |
|
| | 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
|
| | }
|
| |
|
| |
|
| |
|
| | 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
|
| |
|
| |
|
| | 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
|
| | }
|
| | 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:
|
| |
|
| | 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:
|
| |
|
| | 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"
|
| | )
|
| |
|
| |
|
| | insert_record(uploaded_file.name, target_size, final_size_mb, duration_sec, resolution, fps if fps != "Original" else info.get('r_frame_rate', 0))
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | 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)
|
| |
|