Last active
June 17, 2024 15:43
-
-
Save ZiTAL/3e4c3ee3b875a708e7d8e8992f7b942c to your computer and use it in GitHub Desktop.
php / python: automatize upscaling videos using ffmpeg and realesrgan
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
new ia( | |
[ | |
'extensions' => ['mp4'], | |
'suffix' => '-ia', | |
'from' => '/home/zital/Videos/to_encode/', | |
'to' => '/home/zital/Videos/encoded/', | |
'ffmpeg_bin' => '/usr/bin/ffmpeg', | |
'ffmpeg_argv' => '-b:v 4340k -an -c:v h264_nvenc -pix_fmt yuv420p', | |
'realesrgan_bin' => '/usr/bin/realesrgan-ncnn-vulkan', | |
'realesrgan_scale' => 2 | |
]); | |
class ia | |
{ | |
private $params; | |
private $ffmpeg; | |
private $realesrgan; | |
public function __construct($params) | |
{ | |
$this->params = $params; | |
$this->realesrgan = new RealeSrgan($params['realesrgan_bin'], $params['realesrgan_scale']); | |
$videos_to_encode = $this->getVideosToEncode(); | |
foreach($videos_to_encode as $vte) | |
{ | |
$this->ffmpeg = new Ffmpeg( | |
[ | |
'bin' => $this->params['ffmpeg_bin'], | |
'suffix' => $this->params['suffix'], | |
'argv' => $this->params['ffmpeg_argv'] | |
], $vte); | |
$this->video2img($vte); | |
$this->img2ia($vte); | |
$this->ia2video($vte); | |
$this->video2audio($vte); | |
$this->VideoAudioMerge($vte); | |
$this->removeTmpFiles($vte); | |
} | |
} | |
private function getVideosToEncode() | |
{ | |
$extensions = $this->params['extensions']; | |
$from = $this->params['from']; | |
$files = scandir($from); | |
$files = array_filter($files, function($file) use ($from, $extensions) | |
{ | |
$r = "{$from}/{$file}"; | |
$ext = strtolower(pathinfo($r, PATHINFO_EXTENSION)); | |
if(in_array($ext, $extensions)) | |
{ | |
if(!$this->isVideoAlreadyEncoded($r)) | |
return true; | |
} | |
return false; | |
}); | |
$files = array_map(function($file) use ($from) | |
{ | |
return "{$from}{$file}"; | |
}, $files); | |
return array_values($files); | |
} | |
private function isVideoAlreadyEncoded($video) | |
{ | |
$info = pathinfo($video); | |
$file = "{$this->params['to']}{$info['filename']}{$this->params['suffix']}.{$info['extension']}"; | |
if(is_file($file)) | |
return true; | |
return false; | |
} | |
private function video2img($video) | |
{ | |
$hash = Hash::get($video); | |
$dir = "{$this->params['to']}{$hash}"; | |
if(!is_dir($dir)) | |
mkdir($dir); | |
$this->ffmpeg->video2img($dir); | |
} | |
private function img2ia($video) | |
{ | |
$hash = Hash::get($video); | |
$dir = "{$this->params['to']}{$hash}"; | |
$imgs = $this->getImagesToEncode($dir); | |
foreach($imgs as $img) | |
{ | |
$info = pathinfo($img); | |
$to = "{$info['dirname']}/{$info['filename']}{$this->params['suffix']}.{$info['extension']}"; | |
if(!is_file($to)) | |
$this->realesrgan->main($img, $to); | |
} | |
} | |
private function getImagesToEncode($dir) | |
{ | |
$files = scandir($dir); | |
$files = array_filter($files, function($file) | |
{ | |
if(preg_match("/^frame_[0-9]+\.png$/", $file)) | |
return true; | |
return false; | |
}); | |
$files = array_map(function($file) use ($dir) | |
{ | |
return "{$dir}/{$file}"; | |
}, $files); | |
return array_values($files); | |
} | |
private function ia2video($video) | |
{ | |
$hash = Hash::get($video); | |
$dir = "{$this->params['to']}{$hash}"; | |
$this->ffmpeg->ia2video($dir); | |
} | |
private function video2audio($video) | |
{ | |
$hash = Hash::get($video); | |
$dir = "{$this->params['to']}{$hash}"; | |
$this->ffmpeg->video2audio($dir); | |
} | |
private function VideoAudioMerge($video) | |
{ | |
$this->ffmpeg->VideoAudioMerge($this->params['to']); | |
} | |
private function removeTmpFiles($video) | |
{ | |
$hash = Hash::get($video); | |
$dir = "{$this->params['to']}{$hash}"; | |
$command = "rm -rf {$dir}"; | |
echo "Remove temporary files:\n{$command}\n"; | |
shell_exec($command); | |
} | |
} | |
class Ffmpeg | |
{ | |
private $bin; | |
private $suffix; | |
private $argv; | |
private $video; | |
private $info; | |
public function __construct($params, $video) | |
{ | |
$this->bin = $params['bin']; | |
$this->suffix = $params['suffix']; | |
$this->argv = $params['argv']; | |
$this->video = $video; | |
$command = "{$this->bin} -i {$video} 2>&1"; | |
echo "Ffmpeg video info:\n{$command}\n"; | |
$this->info = shell_exec($command); | |
} | |
public function getFrameRate() | |
{ | |
preg_match("/\s+([0-9]+) fps/mi", $this->info, $m); | |
if($m) | |
return (int)$m[1]; | |
return false; | |
} | |
public function video2img($dir) | |
{ | |
$command = "{$this->bin} -i {$this->video} {$dir}/frame_%09d.png -y"; | |
echo "Video to Images:\n{$command}\n"; | |
shell_exec($command); | |
} | |
public function ia2video($dir) | |
{ | |
$frame_rate = $this->getFrameRate(); | |
$command = "{$this->bin} -i {$dir}/frame_%9d{$this->suffix}.png {$this->argv} -r {$frame_rate} {$dir}/input.mp4 -y"; | |
echo "IA to Video:\n{$command}\n"; | |
shell_exec($command); | |
} | |
public function video2audio($dir) | |
{ | |
$command = "{$this->bin} -i {$this->video} -vn -acodec copy {$dir}/input.m4a -y"; | |
echo "Video to Audio:\n{$command}\n"; | |
shell_exec($command); | |
} | |
public function VideoAudioMerge($to) | |
{ | |
$info = pathinfo($this->video); | |
$output = "{$to}/{$info['filename']}{$this->suffix}.{$info['extension']}"; | |
$hash = Hash::get($this->video); | |
$dir = "{$to}{$hash}"; | |
$command = "{$this->bin} -i {$dir}/input.mp4 -i {$dir}/input.m4a -codec copy {$output} -y"; | |
echo "Merge Video and Audio:\n{$command}\n"; | |
shell_exec($command); | |
} | |
} | |
class RealeSrgan | |
{ | |
private $bin; | |
private $scale; | |
public function __construct($bin, $scale) | |
{ | |
$this->bin = $bin; | |
$this->scale = $scale; | |
} | |
public function main($img, $to) | |
{ | |
$command = "{$this->bin} -i {$img} -s {$this->scale} -o {$to} 2>&1"; | |
echo "Real-Esrgan:\n{$command}\n"; | |
$output = shell_exec($command); | |
echo "{$output}\n"; | |
if(preg_match("/find_blob_index_by_name\soutput\s+failed/", $output)) | |
{ | |
sleep(5); | |
$this->main($img, $to); | |
} | |
} | |
} | |
class Hash | |
{ | |
private static $cache = []; | |
public static function get($file) | |
{ | |
if(isset(self::$cache[$file])) | |
return self::$cache[$file]; | |
else | |
{ | |
self::set($file); | |
return self::get($file); | |
} | |
} | |
private static function set($value) | |
{ | |
$hash = md5_file($value); | |
self::$cache[$value] = $hash; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# -*- coding: utf-8 -*- | |
import os | |
import re | |
import hashlib | |
import subprocess | |
from glob import glob | |
from time import sleep | |
class Hash: | |
_cache = {} | |
@staticmethod | |
def get(file_path): | |
if file_path in Hash._cache: | |
return Hash._cache[file_path] | |
else: | |
Hash.set(file_path) | |
return Hash._cache[file_path] | |
@staticmethod | |
def set(file_path): | |
hash_md5 = hashlib.md5() | |
with open(file_path, 'rb') as f: | |
for chunk in iter(lambda: f.read(4096), b""): | |
hash_md5.update(chunk) | |
Hash._cache[file_path] = hash_md5.hexdigest() | |
class Ffmpeg: | |
def __init__(self, params, video): | |
self.bin = params['bin'] | |
self.suffix = params['suffix'] | |
self.argv = params['argv'] | |
self.video = video | |
self.info = self._get_video_info() | |
def _get_video_info(self): | |
command = f"{self.bin} -i {self.video} 2>&1" | |
print(f"Ffmpeg video info:\n{command}\n") | |
return subprocess.getoutput(command) | |
def get_frame_rate(self): | |
match = re.search(r"\s+([0-9]+) fps", self.info) | |
if match: | |
return int(match.group(1)) | |
return False | |
def video2img(self, dir_path): | |
command = f"{self.bin} -i {self.video} {dir_path}/frame_%09d.png -y" | |
print(f"Video to Images:\n{command}\n") | |
subprocess.run(command, shell=True) | |
def ia2video(self, dir_path): | |
frame_rate = self.get_frame_rate() | |
command = f"{self.bin} -i {dir_path}/frame_%09d{self.suffix}.png {self.argv} -r {frame_rate} {dir_path}/input.mp4 -y" | |
print(f"IA to Video:\n{command}\n") | |
subprocess.run(command, shell=True) | |
def video2audio(self, dir_path): | |
command = f"{self.bin} -i {self.video} -vn -acodec copy {dir_path}/input.m4a -y" | |
print(f"Video to Audio:\n{command}\n") | |
subprocess.run(command, shell=True) | |
def video_audio_merge(self, to_dir): | |
info = os.path.splitext(self.video) | |
output = f"{to_dir}/{os.path.basename(info[0])}{self.suffix}{info[1]}" | |
hash_value = Hash.get(self.video) | |
dir_path = f"{to_dir}{hash_value}" | |
command = f"{self.bin} -i {dir_path}/input.mp4 -i {dir_path}/input.m4a -codec copy {output} -y" | |
print(f"Merge Video and Audio:\n{command}\n") | |
subprocess.run(command, shell=True) | |
class RealeSrgan: | |
def __init__(self, bin_path, scale): | |
self.bin = bin_path | |
self.scale = scale | |
def main(self, img, to): | |
try: | |
command = f"{self.bin} -i {img} -s {self.scale} -o {to} 2>&1" | |
print(f"Real-Esrgan:\n{command}\n") | |
output = subprocess.getoutput(command) | |
print(f"{output}\n") | |
if re.search(r"find_blob_index_by_name\soutput\s+failed", output): | |
sleep(5) | |
self.main(img, to) | |
except: | |
sleep(5) | |
self.main(img, to) | |
class IA: | |
def __init__(self, params): | |
self.params = params | |
self.realesrgan = RealeSrgan(params['realesrgan_bin'], params['realesrgan_scale']) | |
self.ffmpeg = None | |
videos_to_encode = self.get_videos_to_encode() | |
for vte in videos_to_encode: | |
self.ffmpeg = Ffmpeg( | |
{ | |
'bin': self.params['ffmpeg_bin'], | |
'suffix': self.params['suffix'], | |
'argv': self.params['ffmpeg_argv'] | |
}, vte) | |
self.video2img(vte) | |
self.img2ia(vte) | |
self.ia2video(vte) | |
self.video2audio(vte) | |
self.video_audio_merge() | |
self.remove_tmp_files(vte) | |
def get_videos_to_encode(self): | |
extensions = self.params['extensions'] | |
from_dir = self.params['from'] | |
files = [f for f in os.listdir(from_dir) if os.path.isfile(os.path.join(from_dir, f))] | |
filtered_files = [os.path.join(from_dir, f) for f in files if any(f.lower().endswith(ext) for ext in extensions) and not self.is_video_already_encoded(os.path.join(from_dir, f))] | |
filtered_files.sort() | |
return filtered_files | |
def is_video_already_encoded(self, video): | |
info = os.path.splitext(video) | |
file_path = f"{self.params['to']}{os.path.basename(info[0])}{self.params['suffix']}{info[1]}" | |
return os.path.isfile(file_path) | |
def video2img(self, video): | |
hash_value = Hash.get(video) | |
dir_path = os.path.join(self.params['to'], hash_value) | |
os.makedirs(dir_path, exist_ok=True) | |
self.ffmpeg.video2img(dir_path) | |
def img2ia(self, video): | |
hash_value = Hash.get(video) | |
dir_path = os.path.join(self.params['to'], hash_value) | |
images = self.get_images_to_encode(dir_path) | |
for img in images: | |
to = f"{os.path.splitext(img)[0]}{self.params['suffix']}{os.path.splitext(img)[1]}" | |
if not os.path.isfile(to): | |
self.realesrgan.main(img, to) | |
def get_images_to_encode(self, dir_path): | |
files = [f for f in os.listdir(dir_path) if re.match(r"^frame_\d+\.png$", f)] | |
files.sort() | |
return [os.path.join(dir_path, f) for f in files] | |
def ia2video(self, video): | |
hash_value = Hash.get(video) | |
dir_path = os.path.join(self.params['to'], hash_value) | |
self.ffmpeg.ia2video(dir_path) | |
def video2audio(self, video): | |
hash_value = Hash.get(video) | |
dir_path = os.path.join(self.params['to'], hash_value) | |
self.ffmpeg.video2audio(dir_path) | |
def video_audio_merge(self): | |
self.ffmpeg.video_audio_merge(self.params['to']) | |
def remove_tmp_files(self, video): | |
hash_value = Hash.get(video) | |
dir_path = os.path.join(self.params['to'], hash_value) | |
print(f"Remove temporary files:\nrm -rf {dir_path}\n") | |
subprocess.run(f"rm -rf {dir_path}", shell=True) | |
if __name__ == "__main__": | |
IA( | |
{ | |
'extensions': ['mp4'], | |
'suffix': '-ia', | |
'from': '/home/zital/Videos/to_encode/', | |
'to': '/home/zital/Videos/encoded/', | |
'ffmpeg_bin': '/usr/bin/ffmpeg', | |
'ffmpeg_argv': '-b:v 4340k -an -c:v h264_nvenc -pix_fmt yuv420p', | |
'realesrgan_bin': '/usr/bin/realesrgan-ncnn-vulkan', | |
'realesrgan_scale': 2 | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example:
https://mastodon.eus/@zital/112514534796184159