trim-vid / app.py
algorembrant's picture
Upload 9 files
350f182 verified
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)