diff --git a/aux.py b/aux.py index 6870cd0..4329926 100644 --- a/aux.py +++ b/aux.py @@ -1,9 +1,32 @@ #! /bin/env python3 # encoding: utf-8 +import queue +import subprocess +import sys +import threading +import time + +import over # FIXME + +# -------------------------------------------------- + +def capture_output(stream, fifo): + while True: + chunk = stream.read1(100) + + if chunk: + fifo.put(chunk) + else: + break + + stream.close() + class Command: def __init__(self, sequence): self.__dict__["sequence"] = list(sequence) + self.__dict__["thread"] = None + self.__dict__["fifo"] = None def __setattr__(self, name, value): found = False @@ -29,3 +52,76 @@ class Command: out.append(str(item)) return out + + def run(self, async=False, stderr=False): + """ + Executes the command in the current environment. + + async return immediatelly, use poll_output to get output + stderr capture stderr instead of stdout + """ + + self.__dict__["process"] = subprocess.Popen(self.dump(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1) + self.__dict__["fifo"] = queue.Queue() + self.__dict__["thread"] = threading.Thread( + target=capture_output, + args=(self.process.stderr if stderr else self.process.stdout, self.fifo) + ) + self.thread.daemon = True # thread dies with the program + self.thread.start() + + if not async: + buffer = [] + probably_dead = False + + while True: + probably_dead = self.process.poll() is not None + + chunk = self.poll_output() + + if chunk: + buffer.append(chunk) + else: + time.sleep(0.01) + + if not chunk and self.process.poll() is not None and probably_dead: + break + + return b"".join(buffer) + + else: + return None + + def poll_output(self): + """ + Returns the output of a currently running process and clears the buffer. + + Returns None if no process is running. + + Blocking - always returns at least one char. + """ + + if self.fifo.empty(): + return None + + else: + buffer = [] + + while not self.fifo.empty(): + buffer.append(self.fifo.get_nowait()) + + return b"".join(buffer) + + @property + def running(self): + return self.process.poll() is None + +# -------------------------------------------------- + +def parse_fps(raw): + if "/" in raw: + num, den = (int(x) for x in raw.split("/")) + + return float(num) / float(den) + + return float(raw) diff --git a/over-video.py b/over-video.py index 510f01a..2c90250 100755 --- a/over-video.py +++ b/over-video.py @@ -6,8 +6,9 @@ import os import over import re import subprocess +import time -from aux import Command +from aux import Command, parse_fps # -------------------------------------------------- @@ -38,7 +39,11 @@ if __name__ == "__main__": main.add_help("Description", ["Over-Video is a simple video converter."]) main.parse() + _print("using profile §b%s§/: video_quality=%.1f, audio_quality=%.1f, normalize=%s" %(main.cfg.profile, main.cfg.video_quality, main.cfg.audio_quality, "%.1f dB" %(main.cfg.normalize_mean) if main.cfg.normalize else "None")) + for tgt in main.targets: + print() + if not os.path.exists(tgt) or os.path.isdir(tgt): _print("target §y%s§/ §ris not a readable file§/, skipping" %(tgt), prefix.fail) continue @@ -47,7 +52,72 @@ if __name__ == "__main__": identify_cmd = Command(command.identify) identify_cmd.INFILE = tgt - identify_raw = subprocess.check_output(identify_cmd.dump()).decode("utf-8") - identify_dict = json.loads(identify_raw) + identify_raw = identify_cmd.run() + identify_dict = json.loads(identify_raw.decode("utf-8")) - print(identify_dict) + info = over.core.types.ndict() + info.duration = float(identify_dict["format"]["duration"]) + + video_streams = [s for s in identify_dict["streams"] if s["codec_type"] == "video"] + audio_streams = [s for s in identify_dict["streams"] if s["codec_type"] == "audio"] + + amount_vs = len(video_streams) + amount_as = len(audio_streams) + + if amount_vs != 1: + _print("detected §r%d§/ video streams" %(amount_vs), prefix.fail) + main.exit(1) + + if amount_as != 1: + _print("detected §r%d§/ audio streams" %(amount_as), prefix.fail) + main.exit(1) + + video = video_streams[0] + audio = audio_streams[0] + + info.video_codec = video["codec_name"] + info.video_size_x = video["width"] + info.video_size_y = video["height"] + info.video_fps = over.core.textui.Unit(parse_fps(video["r_frame_rate"]), "Hz") + info.video_bitrate = over.core.textui.Unit(audio["bit_rate"], "b/s") + + info.audio_codec = audio["codec_name"] + info.audio_channels = audio["channels"] + info.audio_samplerate = over.core.textui.Unit(audio["sample_rate"], "Hz") + info.audio_language = audio["tags"]["language"] + info.audio_bitrate = over.core.textui.Unit(audio["bit_rate"], "b/s") + + _print("§mvideo§/: size=§b%d§/x§b%d§/ px, framerate=%s, codec=%s, bitrate=%s" %(info.video_size_x, info.video_size_y, info.video_fps, info.video_codec, info.video_bitrate)) + _print("§caudio§/: channels=§b%d§/, samplerate=%s, codec=%s, bitrate=%s, language=%s" %(info.audio_channels, info.audio_samplerate, info.audio_codec, info.audio_bitrate, info.audio_language)) + + if (main.cfg.armed or main.cfg.dump_commands) and main.cfg.normalize: + _print("running normalization pre-pass") + + norm_pre_cmd = Command(command.normalize_prepass) + norm_pre_cmd.INFILE = tgt + norm_pre_cmd.run(async=True, stderr=True) + + pb = over.core.textui.ProgressBar(50, int(info.video_fps.value * info.duration), "frames") + + while True: + probably_dead = not norm_pre_cmd.running + + time.sleep(.25) + + o = norm_pre_cmd.poll_output() + + if o: + if b"frame=" in o: + frame_id = re.findall(b"frame= *(\d+) ", o)[0] + pb.update(int(frame_id)) + + elif b"mean_volume: " in o: + info.mean_volume = float(re.findall(b"mean_volume: (-\d+\.\d+) dB", o)[0]) + info.volume_correction = info.mean_volume - main.cfg.normalize_mean + else: + print(o) + + elif not norm_pre_cmd.running and probably_dead: + break + + _print("detected volume %.2f dB, correction %.2f dB" %(info.mean_volume, info.volume_correction))