2020-01-11 12:57:09 +00:00
|
|
|
#!/usr/bin/env python
|
2020-01-13 17:22:23 +00:00
|
|
|
"""
|
|
|
|
Todo:
|
|
|
|
Option for KeyFrame Separation
|
|
|
|
Fix error if no audio stream
|
2020-01-13 17:52:03 +00:00
|
|
|
Windows PySceneDetect fail
|
2020-01-13 17:22:23 +00:00
|
|
|
"""
|
2020-01-13 17:56:33 +00:00
|
|
|
import sys
|
2020-01-08 00:20:18 +00:00
|
|
|
import os
|
2020-01-11 23:56:30 +00:00
|
|
|
import shutil
|
2020-01-09 19:27:27 +00:00
|
|
|
from os.path import join
|
2020-01-09 19:35:43 +00:00
|
|
|
from psutil import virtual_memory
|
2020-01-09 14:54:01 +00:00
|
|
|
import argparse
|
2020-01-09 15:50:26 +00:00
|
|
|
import time
|
2020-01-11 01:18:01 +00:00
|
|
|
from shutil import rmtree
|
2020-01-09 19:09:49 +00:00
|
|
|
from math import ceil
|
2020-01-08 20:44:44 +00:00
|
|
|
from multiprocessing import Pool
|
2020-01-08 18:56:47 +00:00
|
|
|
try:
|
|
|
|
import scenedetect
|
2020-01-11 07:26:20 +00:00
|
|
|
except ImportError:
|
2020-01-08 18:56:47 +00:00
|
|
|
print('ERROR: No PyScenedetect installed, try: sudo pip install scenedetect')
|
2020-01-08 00:20:18 +00:00
|
|
|
|
2020-01-12 02:54:16 +00:00
|
|
|
|
2020-01-13 17:22:23 +00:00
|
|
|
FFMPEG = 'ffmpeg -hide_banner'
|
2020-01-10 23:19:27 +00:00
|
|
|
|
|
|
|
|
2020-01-11 01:19:27 +00:00
|
|
|
class ProgressBar:
|
|
|
|
"""
|
|
|
|
Progress Bar for tracking encoding progress
|
|
|
|
"""
|
|
|
|
|
2020-01-11 08:17:52 +00:00
|
|
|
def __init__(self, tasks):
|
2020-01-13 03:33:20 +00:00
|
|
|
self.bar_iteration: int = 0
|
2020-01-11 08:17:52 +00:00
|
|
|
self.tasks = tasks
|
2020-01-11 07:27:58 +00:00
|
|
|
|
2020-01-13 15:53:19 +00:00
|
|
|
# Print empty bar on initialization
|
2020-01-11 07:27:58 +00:00
|
|
|
self.print()
|
2020-01-11 01:19:27 +00:00
|
|
|
|
|
|
|
def print(self):
|
2020-01-11 23:57:20 +00:00
|
|
|
terminal_size, _ = shutil.get_terminal_size((80, 20))
|
2020-01-11 09:22:49 +00:00
|
|
|
bar_length = terminal_size - (2 * len(str(self.tasks))) - 13
|
2020-01-11 02:48:58 +00:00
|
|
|
|
2020-01-13 03:33:20 +00:00
|
|
|
if self.bar_iteration == 0:
|
2020-01-11 01:19:27 +00:00
|
|
|
percent = 0
|
2020-01-11 08:17:52 +00:00
|
|
|
fill_size = 0
|
2020-01-11 01:19:27 +00:00
|
|
|
else:
|
2020-01-13 03:33:20 +00:00
|
|
|
percent = round(100 * (self.bar_iteration / self.tasks), 1)
|
|
|
|
fill_size = int(bar_length * self.bar_iteration // self.tasks)
|
2020-01-11 01:19:27 +00:00
|
|
|
|
2020-01-13 03:33:20 +00:00
|
|
|
end = f'{percent}% {self.bar_iteration}/{self.tasks}'
|
2020-01-11 08:26:36 +00:00
|
|
|
in_bar = ('█' * fill_size) + '-' * (bar_length - fill_size)
|
2020-01-11 02:48:58 +00:00
|
|
|
|
2020-01-11 08:17:52 +00:00
|
|
|
print(f'\r|{in_bar}| {end} ', end='')
|
2020-01-11 01:19:27 +00:00
|
|
|
|
|
|
|
def tick(self):
|
2020-01-13 03:33:20 +00:00
|
|
|
self.bar_iteration += 1
|
2020-01-11 01:19:27 +00:00
|
|
|
self.print()
|
|
|
|
|
|
|
|
|
2020-01-12 15:55:22 +00:00
|
|
|
class Av1an:
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.here = os.getcwd()
|
2020-01-13 03:33:20 +00:00
|
|
|
self.workers = 0
|
|
|
|
self.encoder = 'aomenc'
|
|
|
|
self.args = None
|
2020-01-13 17:52:03 +00:00
|
|
|
self.audio = ''
|
2020-01-13 04:46:14 +00:00
|
|
|
self.threshold = 20
|
2020-01-13 18:06:27 +00:00
|
|
|
self.logging = None
|
2020-01-12 15:55:22 +00:00
|
|
|
|
|
|
|
def arg_parsing(self):
|
|
|
|
"""
|
|
|
|
Command line parser
|
|
|
|
Have default params
|
|
|
|
"""
|
|
|
|
default_encode_aomenc = '--cpu-used=6 --end-usage=q --cq-level=40'
|
|
|
|
default_audio = '-c:a copy'
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
parser.add_argument('--encoding_params', '-e', type=str, default=default_encode_aomenc,
|
|
|
|
help='encoding settings')
|
|
|
|
parser.add_argument('--file_path', '-i', type=str, default='bruh.mp4', help='Input File', required=True)
|
|
|
|
parser.add_argument('--encoder', '-enc', type=str, default='aomenc', help='Choosing encoder')
|
2020-01-13 03:33:20 +00:00
|
|
|
parser.add_argument('--workers', '-t', type=int, default=0, help='Number of workers')
|
2020-01-13 04:46:14 +00:00
|
|
|
parser.add_argument('--audio_params', '-a', type=str, default=default_audio, help='FFmpeg audio settings')
|
2020-01-13 04:48:24 +00:00
|
|
|
parser.add_argument('--threshold', '-tr', type=int, default=self.threshold, help='PySceneDetect Threshold')
|
2020-01-13 17:22:23 +00:00
|
|
|
parser.add_argument('--logging', '-log', type=str, default=self.logging, help='Enable logging')
|
2020-01-13 15:53:19 +00:00
|
|
|
|
2020-01-13 16:59:01 +00:00
|
|
|
self.args = parser.parse_args()
|
2020-01-13 15:53:19 +00:00
|
|
|
|
2020-01-13 16:59:09 +00:00
|
|
|
if self.logging != self.args.logging:
|
2020-01-13 17:56:33 +00:00
|
|
|
if sys.platform == 'linux':
|
|
|
|
self.logging = f'&>> {self.args.logging}.log'
|
2020-01-13 18:06:27 +00:00
|
|
|
os.system(f'echo " Av1an Logging "> {self.args.logging}.log')
|
|
|
|
else:
|
|
|
|
self.logging = '> NUL'
|
2020-01-13 18:01:40 +00:00
|
|
|
else:
|
2020-01-13 18:06:27 +00:00
|
|
|
if sys.platform == 'linux':
|
|
|
|
self.logging = '&> /dev/null'
|
|
|
|
else:
|
|
|
|
self.logging = '> NUL'
|
|
|
|
|
2020-01-13 17:56:33 +00:00
|
|
|
|
2020-01-12 15:55:22 +00:00
|
|
|
|
|
|
|
def determine_resources(self):
|
|
|
|
"""
|
2020-01-13 03:33:20 +00:00
|
|
|
Returns number of workers that machine can handle with selected encoder
|
2020-01-12 15:55:22 +00:00
|
|
|
:return: int
|
|
|
|
"""
|
2020-01-13 03:33:20 +00:00
|
|
|
self.encoder = self.args.encoder.strip()
|
2020-01-12 15:55:22 +00:00
|
|
|
cpu = os.cpu_count()
|
2020-01-13 03:33:20 +00:00
|
|
|
ram = round(virtual_memory().total / 2 ** 30)
|
|
|
|
|
|
|
|
if self.args.workers != 0:
|
|
|
|
self.workers = self.args.workers
|
|
|
|
|
|
|
|
elif self.encoder == 'aomenc':
|
|
|
|
self.workers = ceil(min(cpu, ram/1.5))
|
|
|
|
|
|
|
|
elif self.encoder == 'rav1e':
|
|
|
|
self.workers = ceil(min(cpu, ram/1.2)) // 3
|
|
|
|
else:
|
|
|
|
print('Error: no valid encoder')
|
|
|
|
exit()
|
2020-01-12 15:55:22 +00:00
|
|
|
|
|
|
|
def setup(self, input_file):
|
|
|
|
|
|
|
|
if not os.path.exists(input_file):
|
|
|
|
print("File don't exist")
|
|
|
|
exit()
|
|
|
|
|
|
|
|
# Make temporal directories, and remove them if already presented
|
|
|
|
if os.path.isdir(join(os.getcwd(), ".temp")):
|
|
|
|
rmtree(join(self.here, ".temp"))
|
|
|
|
|
|
|
|
os.makedirs(join(self.here, '.temp', 'split'))
|
|
|
|
os.makedirs(join(self.here, '.temp', 'encode'))
|
|
|
|
|
|
|
|
def extract_audio(self, input_vid, audio_params):
|
|
|
|
"""
|
|
|
|
Extracting audio from video file
|
|
|
|
Encoding audio if needed
|
|
|
|
"""
|
2020-01-13 05:26:16 +00:00
|
|
|
ffprobe = 'ffprobe -hide_banner -loglevel error -show_streams -select_streams a'
|
|
|
|
check = fr'{ffprobe} -i {join(self.here,input_vid)} &> {join(self.here,".temp","audio_check.txt")}'
|
2020-01-13 16:59:01 +00:00
|
|
|
|
2020-01-13 05:26:16 +00:00
|
|
|
os.system(check)
|
2020-01-13 17:22:23 +00:00
|
|
|
cmd = f'{FFMPEG} -i {join(self.here,input_vid)} -vn {audio_params} {join(os.getcwd(),".temp","audio.mkv")} {self.logging}'
|
|
|
|
os.system(cmd)
|
2020-01-11 06:10:21 +00:00
|
|
|
|
2020-01-12 15:55:22 +00:00
|
|
|
def split_video(self, input_vid):
|
|
|
|
"""
|
|
|
|
PySceneDetect used split video by scenes and pass it to encoder
|
|
|
|
Optimal threshold settings 15-50
|
|
|
|
"""
|
2020-01-13 16:59:09 +00:00
|
|
|
cmd2 = f'scenedetect -i {input_vid} --output .temp/split detect-content --threshold {self.threshold} list-scenes split-video -c {self.logging}'
|
|
|
|
os.system(cmd2)
|
2020-01-12 15:55:22 +00:00
|
|
|
print(f'\rVideo {input_vid} splitted')
|
|
|
|
|
|
|
|
def get_video_queue(self, source_path):
|
|
|
|
"""
|
|
|
|
Returns sorted list of all videos that need to be encoded. Big first
|
|
|
|
"""
|
|
|
|
videos = []
|
|
|
|
for root, dirs, files in os.walk(source_path):
|
|
|
|
for file in files:
|
|
|
|
f = os.path.getsize(os.path.join(root, file))
|
|
|
|
videos.append([file, f])
|
|
|
|
|
|
|
|
videos = sorted(videos, key=lambda x: -x[1])
|
|
|
|
print(f'Splited videos: {len(videos)}')
|
|
|
|
return videos
|
|
|
|
|
|
|
|
def encode(self, commands):
|
|
|
|
"""
|
|
|
|
Passing encoding params to ffmpeg for encoding
|
|
|
|
TODO:
|
|
|
|
Replace ffmpeg with aomenc because ffmpeg libaom doen't work with parameters properly
|
|
|
|
"""
|
|
|
|
for i in commands[:-1]:
|
2020-01-13 15:53:29 +00:00
|
|
|
cmd = rf'{FFMPEG} -an {i} {self.logging}'
|
2020-01-13 04:18:39 +00:00
|
|
|
os.system(cmd)
|
2020-01-12 15:55:22 +00:00
|
|
|
|
|
|
|
def concatenate_video(self, input_video):
|
|
|
|
"""
|
|
|
|
Using FFMPEG to concatenate all encoded videos to 1 file.
|
|
|
|
Reading all files in A-Z order and saving it to concat.txt
|
|
|
|
"""
|
|
|
|
with open(f'{join(self.here, ".temp", "concat.txt")}', 'w') as f:
|
|
|
|
|
|
|
|
for root, firs, files in os.walk(join(self.here, '.temp', 'encode')):
|
|
|
|
for file in sorted(files):
|
|
|
|
f.write(f"file '{join(root, file)}'\n")
|
|
|
|
|
|
|
|
concat = join(self.here, ".temp", "concat.txt")
|
|
|
|
|
2020-01-13 17:52:03 +00:00
|
|
|
is_audio_here = os.path.getsize(join(self.here,".temp", "audio_check.txt"))
|
|
|
|
if is_audio_here:
|
|
|
|
self.audio = f'-i {join(self.here, ".temp", "audio.mkv")} -c copy'
|
|
|
|
|
2020-01-12 15:55:22 +00:00
|
|
|
output = f'{input_video.split(".")[0]}_av1.mkv'
|
|
|
|
|
2020-01-13 17:52:03 +00:00
|
|
|
cmd = f'{FFMPEG} -f concat -safe 0 -i {concat} {self.audio} -y {output} {self.logging}'
|
2020-01-13 17:22:23 +00:00
|
|
|
os.system(cmd)
|
2020-01-08 22:43:20 +00:00
|
|
|
|
2020-01-12 15:55:22 +00:00
|
|
|
def compose_encoding_queue(self, encoding_params, files, encoder):
|
|
|
|
"""
|
|
|
|
Composing encoding commands
|
|
|
|
Examples:
|
|
|
|
1_pass Aomenc:
|
|
|
|
ffmpeg -i input_file -pix_fmt yuv420p -f yuv4mpegpipe - |
|
|
|
|
aomenc -q --passes=1 --cpu-used=8 --end-usage=q --cq-level=63 --aq-mode=0 -o output_file
|
|
|
|
|
|
|
|
2_pass Aomenc:
|
|
|
|
ffmpeg -i input_file -pix_fmt yuv420p -f yuv4mpegpipe - |
|
|
|
|
aomenc -q --passes=2 --pass=1 --cpu-used=8 --end-usage=q --cq-level=63 --aq-mode=0 --log_file -o /dev/null -
|
|
|
|
|
|
|
|
ffmpeg -i input_file -pix_fmt yuv420p -f yuv4mpegpipe - |
|
|
|
|
aomenc -q --passes=2 --pass=2 --cpu-used=8 --end-usage=q --cq-level=63 --aq-mode=0 --log_file -o output_file -
|
|
|
|
|
|
|
|
rav1e:
|
|
|
|
ffmpeg -i bruh.mp4 -pix_fmt yuv420p -f yuv4mpegpipe - |
|
|
|
|
rav1e - --speed=5 --tile-rows 2 --tile-cols 2 --output output.ivf
|
|
|
|
"""
|
|
|
|
file_paths = [(f'{join(os.getcwd(), ".temp", "split", file_name)}',
|
|
|
|
f'{join(os.getcwd(), ".temp", "encode", file_name)}',
|
|
|
|
file_name) for file_name in files]
|
|
|
|
|
2020-01-13 17:22:23 +00:00
|
|
|
ffmpeg_pipe = f'-loglevel error -pix_fmt yuv420p -f yuv4mpegpipe - |'
|
2020-01-12 15:55:22 +00:00
|
|
|
if encoder == 'aomenc':
|
|
|
|
single_pass = 'aomenc -q --passes=1 '
|
|
|
|
two_pass_1_aom = '--passes=2 --pass=1'
|
|
|
|
two_pass_2_aom = '--passes=2 --pass=2'
|
|
|
|
|
|
|
|
pass_1_commands = [
|
|
|
|
(f'-i {file[0]} {ffmpeg_pipe}' +
|
2020-01-13 17:22:23 +00:00
|
|
|
f' {single_pass} {encoding_params} -o {file[1]} - {self.logging}', file[2])
|
2020-01-12 15:55:22 +00:00
|
|
|
for file in file_paths]
|
|
|
|
|
|
|
|
pass_2_commands = [
|
|
|
|
(f'-i {file[0]} {ffmpeg_pipe}' +
|
2020-01-13 17:22:23 +00:00
|
|
|
f' aomenc -q {two_pass_1_aom} {encoding_params} --fpf={file[0]}.log -o /dev/null - {self.logging}',
|
2020-01-12 15:55:22 +00:00
|
|
|
f'-i {file[0]} {ffmpeg_pipe}' +
|
2020-01-13 17:22:23 +00:00
|
|
|
f' aomenc -q {two_pass_2_aom} {encoding_params} --fpf={file[0]}.log -o {file[1]} - {self.logging}'
|
2020-01-12 15:55:22 +00:00
|
|
|
, file[2])
|
|
|
|
for file in file_paths]
|
|
|
|
|
|
|
|
return pass_2_commands
|
|
|
|
|
|
|
|
if encoder == 'rav1e':
|
2020-01-13 17:22:23 +00:00
|
|
|
pass_1_commands = [(f'-i {file[0]} {ffmpeg_pipe} ' +
|
|
|
|
f' rav1e - {encoding_params} --output {file[1]}.ivf', f'{file[2]}.ivf {self.logging}')
|
2020-01-12 15:55:22 +00:00
|
|
|
for file in file_paths]
|
|
|
|
return pass_1_commands
|
|
|
|
|
2020-01-13 16:59:01 +00:00
|
|
|
def main(self):
|
2020-01-13 03:33:20 +00:00
|
|
|
|
2020-01-13 16:59:01 +00:00
|
|
|
# Parse initial arguments
|
|
|
|
self.arg_parsing()
|
2020-01-12 15:55:22 +00:00
|
|
|
|
|
|
|
# Check validity of request and create temp folders/files
|
2020-01-13 03:33:20 +00:00
|
|
|
self.setup(self.args.file_path)
|
2020-01-12 15:55:22 +00:00
|
|
|
|
|
|
|
# Extracting audio
|
2020-01-13 03:33:20 +00:00
|
|
|
self.extract_audio(self.args.file_path, self.args.audio_params)
|
2020-01-12 15:55:22 +00:00
|
|
|
|
|
|
|
# Splitting video and sorting big-first
|
2020-01-13 03:33:20 +00:00
|
|
|
self.split_video(self.args.file_path)
|
2020-01-12 15:55:22 +00:00
|
|
|
vid_queue = self.get_video_queue('.temp/split')
|
|
|
|
files = [i[0] for i in vid_queue[:-1]]
|
|
|
|
|
2020-01-13 03:33:20 +00:00
|
|
|
# Determine resources
|
|
|
|
self.determine_resources()
|
|
|
|
|
2020-01-12 15:55:22 +00:00
|
|
|
# Make encode queue
|
2020-01-13 16:59:01 +00:00
|
|
|
commands = self.compose_encoding_queue(self.args.encoding_params, files, self.args.encoder)
|
2020-01-12 15:55:22 +00:00
|
|
|
|
|
|
|
# Creating threading pool to encode bunch of files at the same time
|
2020-01-13 16:59:01 +00:00
|
|
|
print(f'Starting encoding with {self.workers} workers. \nParameters:{self.args.encoding_params}\nEncoding..')
|
2020-01-12 15:55:22 +00:00
|
|
|
|
2020-01-13 03:33:20 +00:00
|
|
|
# Progress bar
|
2020-01-12 15:55:22 +00:00
|
|
|
bar = ProgressBar(len(vid_queue))
|
2020-01-13 03:33:20 +00:00
|
|
|
pool = Pool(self.workers)
|
2020-01-12 15:55:22 +00:00
|
|
|
for i, _ in enumerate(pool.imap_unordered(self.encode, commands), 1):
|
|
|
|
bar.tick()
|
2020-01-11 01:19:27 +00:00
|
|
|
|
2020-01-11 06:40:20 +00:00
|
|
|
bar.tick()
|
|
|
|
|
2020-01-13 16:59:01 +00:00
|
|
|
self.concatenate_video(self.args.file_path)
|
2020-01-08 20:44:44 +00:00
|
|
|
|
2020-01-08 23:36:39 +00:00
|
|
|
|
2020-01-08 20:44:44 +00:00
|
|
|
if __name__ == '__main__':
|
2020-01-09 15:33:47 +00:00
|
|
|
|
|
|
|
# Main thread
|
2020-01-12 15:55:22 +00:00
|
|
|
|
2020-01-09 15:49:28 +00:00
|
|
|
start = time.time()
|
2020-01-11 06:10:21 +00:00
|
|
|
|
2020-01-12 15:55:22 +00:00
|
|
|
av1an = Av1an()
|
2020-01-13 16:59:01 +00:00
|
|
|
av1an.main()
|
2020-01-11 06:10:21 +00:00
|
|
|
|
2020-01-11 09:22:49 +00:00
|
|
|
print(f'\n Completed in {round(time.time()-start, 1)} seconds')
|
2020-01-09 16:25:27 +00:00
|
|
|
|
|
|
|
# Delete temp folders
|
2020-01-11 12:49:01 +00:00
|
|
|
rmtree(join(os.getcwd(), ".temp"))
|
2020-01-11 07:26:47 +00:00
|
|
|
|
|
|
|
# To prevent console from hanging
|
2020-01-13 17:59:30 +00:00
|
|
|
# os.popen('stty sane', 'r')
|