File size: 9,121 Bytes
350f182 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | 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)
|