Skip to content

Instantly share code, notes, and snippets.

@ZiTAL
Last active June 17, 2024 15:43
Show Gist options
  • Save ZiTAL/3e4c3ee3b875a708e7d8e8992f7b942c to your computer and use it in GitHub Desktop.
Save ZiTAL/3e4c3ee3b875a708e7d8e8992f7b942c to your computer and use it in GitHub Desktop.
php / python: automatize upscaling videos using ffmpeg and realesrgan
<?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;
}
}
#!/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
})
@ZiTAL
Copy link
Author

ZiTAL commented Jun 14, 2024

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment