updated video controls and optimized encoder

main
oscar 2 months ago
parent ffa0a7c62c
commit 4b1b7d08e3

1
.gitignore vendored

@ -182,3 +182,4 @@ cython_debug/
/failed
/static/thumbnails
/.temp
.last_checked

@ -0,0 +1,69 @@
import os, shutil, config
import ffmpeg
from tqdm import tqdm
def is_av1(filepath):
try:
probe = ffmpeg.probe(filepath)
for stream in probe['streams']:
if stream['codec_type'] == 'video' and 'codec_name' in stream:
if stream['codec_name'] == 'av1':
return True
except ffmpeg.Error as e:
print(f"Error probing {filepath}: {e}")
return "Fucked"
return False
def save_last_checked(filepath):
with open(".last_checked", "w") as f:
f.write(filepath)
def get_last_checked():
if os.path.exists(".last_checked"):
with open(".last_checked", "r") as f:
return f.read().strip()
return None
def init_list(videos):
last_checked = get_last_checked()
if last_checked:
for video in videos:
if os.path.basename(video['filepath']) == last_checked:
return videos[videos.index(video) + 1:]
return videos
def reencode_videos_av1():
conn, cursor = config.get_local_db_connection()
cursor.execute("SELECT filepath, id, codec FROM videos WHERE status != 'missing' AND filepath IS NOT NULL ORDER BY size ASC;")
videos = cursor.fetchall()
os.makedirs("fucked", exist_ok=True)
videos = init_list(videos)
with tqdm(videos, desc="Checking videos", unit="file") as pbar:
for video in videos:
pbar.update(1)
if pbar.n % 100 == 0:
save_last_checked(os.path.basename(video['filepath']))
if video['codec'] == 'av1':
continue
input_path = video['filepath']
isav1 = is_av1(input_path)
if isav1 == "Fucked":
print(f"🚫 Error probing {input_path}")
shutil.move(input_path, "fucked/" + os.path.basename(input_path))
continue
if isav1 == False:
continue
cursor.execute("UPDATE videos SET codec = %s WHERE id = %s", ('av1', video['id']))
conn.commit()
if __name__ == "__main__":
reencode_videos_av1()

@ -23,7 +23,7 @@ def get_video_info(filepath):
)
if video_stream:
# Convert from bits/sec to kbps
bitrate_kbps = int(format_info['bit_rate']) // 1000
bitrate_kbps = int(format_info['bit_rate']) // 1000
width = video_stream['width']
height = video_stream['height']
return bitrate_kbps, (width, height)
@ -31,15 +31,6 @@ def get_video_info(filepath):
print(f"Error getting video info for {filepath}: {e}")
return None, (None, None)
def get_files(folder):
all_files = []
for root, _, filenames in os.walk(folder):
for filename in filenames:
if filename.lower().endswith(('.mp4', '.mkv', '.avi', '.mov')):
if not "encoded" in root:
all_files.append(os.path.join(root, filename))
return sorted(all_files, key=os.path.getsize)
def get_target_bitrate(width, height):
resolutions = {
(854, 480): 1000,
@ -95,11 +86,14 @@ def encode_video(filepath, output_path, target_bitrate):
g=keyframe_interval
)
.run(
overwrite_output=True,
overwrite_output=True, quiet=True
)
)
print(f" Finished encoding {os.path.basename(filepath)} to AV1 at {target_bitrate} kbps "
f"(maxrate={max_bitrate} kbps).")
return True
except ffmpeg.Error as e:
print(f" Error encoding {os.path.basename(filepath)} to AV1: {e}")
@ -116,10 +110,6 @@ def check_and_replace_if_smaller(original_path, temp_output_path):
size_diff_perc = (1 - processed_size / original_size) * 100
size_diff_mb = size_original_mb - size_processed_mb
if size_original_mb < 25:
shutil.move(temp_output_path, original_path)
return True
if processed_size >= original_size or size_diff_mb < 1:
os.remove(temp_output_path)
return False
@ -143,45 +133,54 @@ def reencode_videos_av1():
cursor.execute("SELECT * FROM videos WHERE codec IS NULL AND status != 'missing' AND filepath IS NOT NULL ORDER BY size ASC;")
videos = cursor.fetchall()
for video in tqdm(videos, desc="Processing videos", unit="file"):
input_path = video['filepath']
if not os.path.exists(input_path):
print(f"🚫 File not found: {input_path}")
continue
file_size_in_mb = os.path.getsize(input_path) / (1024 * 1024)
print(f"\nProcessing {os.path.basename(input_path)} ({file_size_in_mb:.2f} MB)...")
# 2) Get current bitrate & resolution
current_bitrate, (width, height) = get_video_info(input_path)
if not current_bitrate:
print("Video's bitrate is not available. Skipping")
continue
target_bitrate = get_target_bitrate(width, height)
# If current bitrate <= target, it's not worth it to re-encode
if current_bitrate <= target_bitrate:
target_bitrate = current_bitrate
if is_av1(input_path):
print("Video is already encoded in AV1. Skipping")
update_codec_db(video['id'], 'av1')
continue
# 3) Re-encode
output_path = os.path.join('.temp', os.path.basename(input_path))
os.makedirs(os.path.dirname(output_path), exist_ok=True)
encoded = encode_video(input_path, output_path, target_bitrate)
if not encoded:
print("Encoding failed. Skipping.")
continue
# 4) Compare file sizes and replace if smaller
check_and_replace_if_smaller(input_path, output_path)
update_codec_db(video['id'], 'av1')
# for video in tqdm(videos, desc="Processing videos", unit="file"):
with tqdm(videos, desc="Processing videos", unit="file") as pbar:
for video in videos:
pbar.update(1)
input_path = video['filepath']
if not os.path.exists(input_path):
print(f"🚫 File not found: {input_path}")
continue
file_size_in_mb = os.path.getsize(input_path) / (1024 * 1024)
print(f"\nProcessing {os.path.basename(input_path)} ({file_size_in_mb:.2f} MB)...")
if file_size_in_mb < 1:
print("Video is too small. Skipping.")
os.remove(input_path)
continue
# 2) Get current bitrate & resolution
current_bitrate, (width, height) = get_video_info(input_path)
if not current_bitrate:
print("Video's bitrate is not available. Skipping")
continue
target_bitrate = get_target_bitrate(width, height)
# If current bitrate <= target, it's not worth it to re-encode
if current_bitrate <= target_bitrate:
target_bitrate = current_bitrate
if is_av1(input_path):
print("Video is already encoded in AV1. Skipping")
update_codec_db(video['id'], 'av1')
continue
# 3) Re-encode
output_path = os.path.join('.temp', os.path.basename(input_path))
os.makedirs(os.path.dirname(output_path), exist_ok=True)
encoded = encode_video(input_path, output_path, target_bitrate)
if not encoded:
print("Encoding failed. Skipping.")
continue
# 4) Compare file sizes and replace if smaller
if check_and_replace_if_smaller(input_path, output_path):
update_codec_db(video['id'], 'av1')
if __name__ == "__main__":
reencode_videos_av1()

@ -155,6 +155,29 @@
}
</style>
<style>
.osd {
position: absolute;
left: 50%;
top: 45%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, .6);
color: #fff;
padding: 6px 10px;
border-radius: 8px;
font-weight: 600;
pointer-events: none;
opacity: 0;
transition: opacity .15s;
}
.player {
position: relative;
}
/* ensure OSD positions over the video */
</style>
<script>
function openFolder(filePath) {
@ -214,6 +237,19 @@
} catch (err) {
alert('Could not toggle favorite. ' + (err?.message || ''));
}
// If a number key (09) is pressed (without modifiers), jump to that %.
if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
// e.key is '0'..'9' for main row; it also works for numpad if you check code:
if (/^[0-9]$/.test(e.key)) {
e.preventDefault();
const digit = Number(e.key); // 0..9
const pct = digit * 10; // 0->0%, 1->10%, ..., 9->90%
jumpToPercent(pct);
return; // stop further handling
}
}
});
</script>
@ -242,6 +278,9 @@
</div>
</div>
<div id="osd" class="osd"></div>
<div class="moments">
<h3>Moments</h3>
<div id="momentsList"></div>
@ -286,6 +325,7 @@
const muteBtn = document.getElementById('muteBtn');
const addBtn = document.getElementById('addMoment');
const list = document.getElementById('momentsList');
const osd = document.getElementById('osd');
const videoId = "{{ video.video_id }}";
function fmt(t) {
@ -297,23 +337,37 @@
String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
}
function showOSD(text) {
if (!osd) return;
osd.textContent = text;
osd.style.opacity = '1';
clearTimeout(showOSD._t);
showOSD._t = setTimeout(() => { osd.style.opacity = '0'; }, 600);
}
function jumpToPercent(percent) {
if (!isFinite(video.duration) || video.duration <= 0) return;
const t = (percent / 100) * video.duration;
video.currentTime = Math.min(Math.max(t, 0), video.duration);
showOSD(`${percent}%`);
}
// --- playback controls ---
playPause.addEventListener('click', () => {
if (video.paused) { video.play(); }
else { video.pause(); }
if (video.paused) { video.play(); } else { video.pause(); }
});
video.addEventListener('play', () => playPause.textContent = '⏸');
video.addEventListener('pause', () => playPause.textContent = '▶️');
video.addEventListener('loadedmetadata', () => {
seek.max = video.duration;
seek.max = video.duration || 0;
dur.textContent = fmt(video.duration);
});
video.addEventListener('timeupdate', () => {
cur.textContent = fmt(video.currentTime);
seek.value = video.currentTime;
});
seek.addEventListener('input', () => video.currentTime = seek.value);
seek.addEventListener('input', () => { video.currentTime = Number(seek.value); });
muteBtn.addEventListener('click', () => {
video.muted = !video.muted;
@ -322,14 +376,30 @@
// --- keyboard shortcuts ---
document.addEventListener('keydown', e => {
if (e.target.tagName === "INPUT") return; // dont steal focus
// don't hijack when typing in inputs/textareas
const tag = (e.target.tagName || '').toUpperCase();
if (tag === 'INPUT' || tag === 'TEXTAREA' || e.isComposing) return;
// Number keys 09 (top row) → jump to 090%
if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey && /^[0-9]$/.test(e.key)) {
e.preventDefault();
jumpToPercent(Number(e.key) * 10);
return;
}
// Numpad 09 → same behavior
if (/^Numpad[0-9]$/.test(e.code)) {
e.preventDefault();
jumpToPercent(Number(e.code.replace('Numpad', '')) * 10);
return;
}
switch (e.code) {
case "Space": e.preventDefault(); video.paused ? video.play() : video.pause(); break;
case "ArrowRight": video.currentTime = Math.min(video.duration, video.currentTime + 5); break;
case "ArrowRight": video.currentTime = Math.min(video.duration || Infinity, video.currentTime + 5); break;
case "ArrowLeft": video.currentTime = Math.max(0, video.currentTime - 5); break;
case "ArrowUp": video.volume = Math.min(1, video.volume + 0.1); break;
case "ArrowDown": video.volume = Math.max(0, video.volume - 0.1); break;
case "KeyM": video.muted = !video.muted; muteBtn.textContent = video.muted ? '🔇' : '🔊'; break;
case "ArrowUp": video.volume = Math.min(1, (video.volume || 0) + 0.1); showOSD(`${Math.round(video.volume * 100)}%`); break;
case "ArrowDown": video.volume = Math.max(0, (video.volume || 0) - 0.1); showOSD(`${Math.round(video.volume * 100)}%`); break;
case "KeyM": video.muted = !video.muted; muteBtn.textContent = video.muted ? '🔇' : '🔊'; showOSD(video.muted ? 'Muted' : 'Unmuted'); break;
}
});
@ -348,13 +418,17 @@
pill.onclick = () => { video.currentTime = m.timestamp; video.play(); };
list.appendChild(pill);
});
} catch (e) { list.textContent = 'Could not load moments.'; }
} catch {
list.textContent = 'Could not load moments.';
}
}
async function addMoment() {
const ts = Math.floor(video.currentTime || 0);
try {
const res = await fetch(`/api/moments/${encodeURIComponent(videoId)}`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ timestamp: ts })
});
const data = await res.json();
@ -365,12 +439,14 @@
pill.onclick = () => { video.currentTime = ts; video.play(); };
if (list.textContent === 'No moments yet.') list.textContent = '';
list.appendChild(pill);
} catch (e) { alert('Could not add moment'); }
} catch {
alert('Could not add moment');
}
}
addBtn.addEventListener('click', addMoment);
loadMoments();
})();
})();
</script>
</body>

Loading…
Cancel
Save