diff --git a/dcc/concat.py b/dcc/concat.py index 1f28199..764f977 100644 --- a/dcc/concat.py +++ b/dcc/concat.py @@ -8,7 +8,6 @@ import math import numpy as np import wand.image - class Concat(dcc.doom_base.Wad): def get_parser(self, prog_name): parser = super().get_parser(prog_name) @@ -21,191 +20,120 @@ class Concat(dcc.doom_base.Wad): logging.basicConfig() av.logging.set_level(av.logging.VERBOSE) av.logging.restore_default_callback() - videos = ( - self.fabricate.joinpath(parsed_args.wad) - .glob(f"{parsed_args.wad}_map*.mp4") - ) - fn_base = ( - f"{parsed_args.wad}_maps{parsed_args.start_map}" - + f"to{parsed_args.end_map}" - ) - output = av.open( - self.fabricate.joinpath(parsed_args.wad).joinpath( - f"{fn_base}.mp4"), "w" - ) - summary_file = open( - self.fabricate.joinpath(parsed_args.wad).joinpath( - f"{fn_base}.txt"), "w" - ) - - self._offset = 0 - summary = [] + videos = self.fabricate.joinpath(parsed_args.wad).glob(f"{parsed_args.wad}_map*.mp4") + output = av.open(self.fabricate.joinpath(parsed_args.wad).joinpath(f"{parsed_args.wad}_maps{parsed_args.start_map}to{parsed_args.end_map}.mp4"), "w") + offset = 0 # We'd like to use the concat filter here and connect everything into a # single filter graph... but it produces a "Resource temporarily - # unavailable" error when switching to inputs after the first. - # Presumably fixable, but it's easier to just make one graph per video - # and mux everything together at the end. + # unavailable" error when switching to inputs after the first. Presumably + # fixable, but it's easier to just make one graph per video and mux + # everything together at the end. for v in sorted(videos): # TODO: Support UDoom in literally any way. - if not ( - v.name >= f"{parsed_args.wad}_map{parsed_args.start_map}.mp4" - and v.name <= f"{parsed_args.wad}_map{parsed_args.end_map}.mp4" - ): + if not (v.name >= f"{parsed_args.wad}_map{parsed_args.start_map}.mp4" and + v.name <= f"{parsed_args.wad}_map{parsed_args.end_map}.mp4"): continue - start_time = self._offset / 1000000 - text = self._add_chunk(v, output, not parsed_args.nooverlay) - list.append( - summary, f"{text} {math.floor(start_time / 60):02}:" - + f"{math.floor(start_time % 60):02}" - ) + + chunk = av.open(v) + if not (len(chunk.streams.video) == 1 and len(chunk.streams.audio) == 1): + raise Exception(f"irregular chunk {v}: streams {chunk.streams} (expected 1 video & 1 audio)") + + ograph = av.filter.Graph() + sink = ograph.add("buffersink") + asink = ograph.add("abuffersink") + + if not parsed_args.nooverlay: + img = wand.image.Image(height=chunk.streams[0].height,width=chunk.streams[0].width) + mapstring = v.name[-6:-4] + text = self._config["map_names"][f"map{mapstring}"] + dcc.text.draw_text(img, f"MAP{mapstring}: {text}", font_size=120) + img.trim(reset_coords=True) + img.border("graya(25%, 25%)", 10, 10) + img.border(dcc.config.TEXT_STROKE_COLOR, 16, 16) + # for this to work... the image needs to have a width that's a multiple + # of 8. dude whyyyyyyy + padfactor=8 + img.border("transparent", padfactor, 0) + img.crop(width=img.width-img.width%padfactor, height=img.height) + + if len(output.streams.get()) == 0: + # We can't use the input stream as a template here; it doesn't + # have everything needed to do encoding and will fail + # mysteriously later. + vs = chunk.streams.video[0] + output.add_stream("h264", rate=int(vs.time_base.denominator/vs.time_base.numerator)) + output.streams[0].extradata = copy.deepcopy(vs.extradata) + output.streams[0].height=vs.height + output.streams[0].width=vs.width + output.streams[0].qmax = vs.qmax + output.streams[0].qmin = vs.qmin + output.streams[0].codec_context.bit_rate = vs.codec_context.bit_rate + output.streams[0].codec_context.framerate = vs.base_rate + output.streams[0].codec_context.pix_fmt = vs.codec_context.pix_fmt + # The following are only used for encoding and have no equivalent on the input stream. + output.streams[0].profile="High" + output.streams[0].codec_context.gop_size=30 + output.streams[0].codec_context.max_b_frames=2 + + astr = chunk.streams.audio[0] + output.add_stream("aac", rate=astr.rate) + output.streams[1].extradata = copy.deepcopy(astr.extradata) + output.streams[1].bit_rate=astr.bit_rate + + src = ograph.add_buffer(template=chunk.streams.video[0], time_base=chunk.streams.video[0].time_base) + asrc = ograph.add_abuffer(template=chunk.streams.audio[0], time_base=chunk.streams.audio[0].time_base) + # TODO: video fades are absolute relative to the input video; audio + # fades need to have their timestamps offset by the position in the + # final video. Clarify if this is really necessary. + frame_rate = chunk.streams.video[0].base_rate + sample_rate = chunk.streams.audio[0].rate + ifade = ograph.add("fade", args="in:0:{}".format(frame_rate)) + ofade = ograph.add("fade", args="out:{}:{}".format((chunk.duration*frame_rate/1000000)-frame_rate, frame_rate)) + iafade = ograph.add("afade", args="in:{}:{}".format(offset*sample_rate/1000000, sample_rate)) + oafade = ograph.add("afade", args="out:{}:{}".format(((offset+chunk.duration)*sample_rate/1000000)-sample_rate, sample_rate)) + if not parsed_args.nooverlay: + overlay = ograph.add_buffer(width=img.width, height=img.height, format="rgba", time_base=chunk.streams[0].time_base) + overlay_fo = ograph.add("fade", args="out:{}:{}".format(4*frame_rate, frame_rate)) + overlay.link_to(overlay_fo, 0, 0) + composite = ograph.add("overlay", args="x=4:y=4") + src.link_to(composite, 0, 0) + overlay_fo.link_to(composite, 0, 1) + composite.link_to(ifade, 0, 0) + else: + src.link_to(ifade, 0, 0) + + asrc.link_to(iafade, 0, 0) + ifade.link_to(ofade, 0, 0) + iafade.link_to(oafade, 0, 0) + ofade.link_to(sink, 0, 0) + oafade.link_to(asink, 0, 0) + ograph.configure() + + for packet in chunk.demux(): + if packet.dts is None: + continue + packet.dts += (offset * packet.time_base.denominator) / (packet.time_base.numerator * 1000000) + packet.pts += (offset * packet.time_base.denominator) / (packet.time_base.numerator * 1000000) + if packet.stream == chunk.streams.video[0]: + for ifr in packet.decode(): + if not parsed_args.nooverlay: + text_frame = av.video.frame.VideoFrame(img.width, img.height, format="rgba") + text_frame.planes[0].update(img.make_blob(format="rgba")) + text_frame.pts = ifr.pts + text_frame.dts = ifr.dts + text_frame.time_base = ifr.time_base + overlay.push(text_frame) + src.push(ifr) + ofr = sink.pull() + for p in output.streams[packet.stream_index].encode(ofr): + output.mux(p) + else: + for ifr in packet.decode(): + asrc.push(ifr) + ofr = asink.pull() + for p in output.streams[packet.stream_index].encode(ofr): + output.mux(p) + offset += chunk.duration + chunk.close() output.close() - for line in summary: - summary_file.write(f"{line}\n") - summary_file.close() - - def _add_chunk(self, v, output, overlay): - chunk = av.open(v) - if not (len(chunk.streams.video) == 1 - and len(chunk.streams.audio) == 1): - raise Exception( - f"irregular chunk {v}: streams {chunk.streams} " - + f"(expected 1 video & 1 audio)" - ) - - ograph = av.filter.Graph() - sink = ograph.add("buffersink") - asink = ograph.add("abuffersink") - - text = "" - if overlay: - img = wand.image.Image( - height=chunk.streams[0].height, - width=chunk.streams[0].width - ) - mapstring = v.name[-6:-4] - text = self._config["map_names"][f"map{mapstring}"] - dcc.text.draw_text(img, f"MAP{mapstring}: {text}", font_size=120) - img.trim(reset_coords=True) - img.border("graya(25%, 25%)", 10, 10) - img.border(dcc.config.TEXT_STROKE_COLOR, 16, 16) - # for this to work... the image needs to have a width that's a - # multiple of 8. dude whyyyyyyy - padfactor = 8 - img.border("transparent", padfactor, 0) - img.crop( - width=img.width - img.width % padfactor, - height=img.height - ) - - if len(output.streams.get()) == 0: - # We can't use the input stream as a template here; it doesn't - # have everything needed to do encoding and will fail - # mysteriously later. - vs = chunk.streams.video[0] - vr = int(vs.time_base.denominator/vs.time_base.numerator) - ovs = output.add_stream("h264", rate=vr) - ovs.extradata = copy.deepcopy(vs.extradata) - ovs.height = vs.height - ovs.width = vs.width - ovs.qmax = vs.qmax - ovs.qmin = vs.qmin - ovs.codec_context.bit_rate = vs.codec_context.bit_rate - ovs.codec_context.framerate = vs.base_rate - ovs.codec_context.pix_fmt = vs.codec_context.pix_fmt - # The following are only used for encoding and have no equivalent - # on the input stream. - ovs.profile = "High" - ovs.codec_context.gop_size = 30 - ovs.codec_context.max_b_frames = 2 - - astr = chunk.streams.audio[0] - oas = output.add_stream("aac", rate=astr.rate) - oas.extradata = copy.deepcopy(astr.extradata) - oas.bit_rate = astr.bit_rate - - src = ograph.add_buffer( - template=chunk.streams.video[0], - time_base=chunk.streams.video[0].time_base - ) - asrc = ograph.add_abuffer( - template=chunk.streams.audio[0], - time_base=chunk.streams.audio[0].time_base - ) - # TODO: video fades are absolute relative to the input video; audio - # fades need to have their timestamps offset by the position in the - # final video. Clarify if this is really necessary. - frame_rate = chunk.streams.video[0].base_rate - sample_rate = chunk.streams.audio[0].rate - ifade = ograph.add("fade", args="in:0:{}".format(frame_rate)) - ofade_start = (chunk.duration * frame_rate / 1000000) - frame_rate - ofade = ograph.add("fade", args=f"out:{ofade_start}:{frame_rate}") - iafade_start = self._offset * sample_rate / 1000000 - iafade = ograph.add("afade", args=f"in:{iafade_start}:{sample_rate}") - oafade_start = ( - (self._offset + chunk.duration) * sample_rate / 1000000 - - sample_rate - ) - oafade = ograph.add("afade", args=f"out:{oafade_start}:{sample_rate}") - - if overlay: - overlay = ograph.add_buffer( - width=img.width, height=img.height, - format="rgba", time_base=chunk.streams.video[0].time_base - ) - overlay_fo = ograph.add( - "fade", args=f"out:{4 * frame_rate}:{frame_rate}" - ) - overlay.link_to(overlay_fo, 0, 0) - composite = ograph.add("overlay", args="x=4:y=4") - src.link_to(composite, 0, 0) - overlay_fo.link_to(composite, 0, 1) - composite.link_to(ifade, 0, 0) - else: - src.link_to(ifade, 0, 0) - - asrc.link_to(iafade, 0, 0) - ifade.link_to(ofade, 0, 0) - iafade.link_to(oafade, 0, 0) - ofade.link_to(sink, 0, 0) - oafade.link_to(asink, 0, 0) - ograph.configure() - - for packet in chunk.demux(): - if packet.dts is None: - continue - pof = ( - (self._offset * packet.time_base.denominator) - / (packet.time_base.numerator * 1000000) - ) - packet.dts += pof - packet.pts += pof - if packet.stream == chunk.streams.video[0]: - for ifr in packet.decode(): - if overlay: - overlay.push(self._make_text_frame(img, ifr)) - src.push(ifr) - ofr = sink.pull() - for p in output.streams[packet.stream_index].encode(ofr): - output.mux(p) - else: - for ifr in packet.decode(): - asrc.push(ifr) - ofr = asink.pull() - for p in output.streams[packet.stream_index].encode(ofr): - output.mux(p) - self._offset += chunk.duration - chunk.close() - - return text - - def _make_text_frame(self, img, ifr): - # We need to give each frame its own memory it can own. - text_frame = av.video.frame.VideoFrame( - img.width, img.height, format="rgba" - ) - text_frame.planes[0].update(img.make_blob(format="rgba")) - text_frame.pts = ifr.pts - text_frame.dts = ifr.dts - text_frame.time_base = ifr.time_base - return text_frame diff --git a/dcc/config.py b/dcc/config.py index cac1f0a..29e9f61 100644 --- a/dcc/config.py +++ b/dcc/config.py @@ -6,62 +6,57 @@ import tomlkit from cliff.command import Command -THUMB_WIDTH = 1280 -THUMB_HEIGHT = 720 -FONT = "League-Spartan-Bold" -TEXT_FILL_COLOR = "white" -TEXT_STROKE_COLOR = "srgb(176,0,0)" +THUMB_WIDTH=1280 +THUMB_HEIGHT=720 +FONT="League-Spartan-Bold" +TEXT_FILL_COLOR="white" +TEXT_STROKE_COLOR="srgb(176,0,0)" -MIRROR = "https://youfailit.net/pub/idgames" # NYC +MIRROR="https://youfailit.net/pub/idgames" # NYC class Base(Command): - def get_parser(self, prog_name): - parser = super().get_parser(prog_name) - parser.add_argument( - '--doom', default=pathlib.Path.home().joinpath("doom")) - parser.add_argument('--config-name', default='config.toml') - return parser + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument('--doom', default=pathlib.Path.home().joinpath("doom")) + parser.add_argument('--config-name', default='config.toml') + return parser - def init_base(self, parsed_args): - self._doom = pathlib.Path(parsed_args.doom) - self._config_name = parsed_args.config_name - self._config = tomlkit.toml_file.TOMLFile( - self.doom.joinpath(self.config_name)).read() - self._dsda = self._config.get("dsda") - if self.dsda is None: - raise Exception( - "required key 'dsda' not set in config " - + f"{self.doom.joinpath(self.config_name)}.") - for d in ("iwads", "pwads", "demos", "fabricate"): - self._init_path(d) + def init_base(self, parsed_args): + self._doom = pathlib.Path(parsed_args.doom) + self._config_name = parsed_args.config_name + self._config = tomlkit.toml_file.TOMLFile(self.doom.joinpath(self.config_name)).read() + self._dsda = self._config.get("dsda") + if self.dsda is None: + raise Exception(f"required key 'dsda' not set in config {self.doom.joinpath(self.config_name)}.") + for d in ("iwads", "pwads", "demos", "fabricate"): + self._init_path(d) - def run(self, parsed_args): - self.init_base(parsed_args) - self.take_action(parsed_args) + def run(self, parsed_args): + self.init_base(parsed_args) + self.take_action(parsed_args) - def _init_path(self, what): - setattr( - self, f"_{what}", self.doom.joinpath(self._config.get(what, what))) - setattr( - type(self), what, property(lambda self: getattr(self, f"_{what}"))) + def _init_path(self, what): + setattr(self, f"_{what}", self.doom.joinpath(self._config.get(what, what))) + setattr(type(self), what, property(lambda self: getattr(self, f"_{what}"))) - @property - def doom(self): - return self._doom + @property + def doom(self): + return self._doom - @property - def config_name(self): - return self._config_name + @property + def config_name(self): + return self._config_name - @property - def dsda(self): - return self._doom.joinpath(self._dsda) + @property + def dsda(self): + return self._doom.joinpath(self._dsda) + + def iwad_path(self, wad): + iwad = self.iwads.joinpath(self._config.get("default_iwad")) + iwadpath = self.pwads.joinpath(wad).joinpath("iwad") + if iwadpath.exists(): + with io.open(iwadpath) as f: + iwad = self.iwads.joinpath(f.read().strip() + ".WAD") + return iwad - def iwad_path(self, wad): - iwad = self.iwads.joinpath(self._config.get("default_iwad")) - iwadpath = self.pwads.joinpath(wad).joinpath("iwad") - if iwadpath.exists(): - with io.open(iwadpath) as f: - iwad = self.iwads.joinpath(f.read().strip() + ".WAD") - return iwad diff --git a/dcc/dsda.py b/dcc/dsda.py index 9ad07f5..2672fb0 100644 --- a/dcc/dsda.py +++ b/dcc/dsda.py @@ -6,59 +6,51 @@ import shutil import subprocess import zipfile - class DSDA(dcc.doom_base.WadMap): - def get_parser(self, prog_name): - parser = super().get_parser(prog_name) - parser.add_argument("-s", "--single", action="store_true") - parser.add_argument("-a", "--abbreviation") - return parser + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument("-s", "--single", action="store_true") + parser.add_argument("-a", "--abbreviation") + return parser - def take_action(self, parsed_args): - dip = self.demo_in_path() - dtp = self.dsda_text_path() - if not dtp.exists(): - command = [self.dsda] - if shutil.which("xvfb-run") is not None: - command = ["xvfb-run"] + command - # TODO: negative tics should seek from the end, but this doesn't - # seem to work. - subprocess.run( - command + self.dsda_preamble() + [ - "-fastdemo", dip, "-nosound", "-skiptic", - "999999999", "-export_text_file" - ] - ) - editor = "nano" - if "EDITOR" in os.environ: - editor = os.environ["EDITOR"] - subprocess.run([editor, dtp]) - if parsed_args.abbreviation: - fh1 = parsed_args.abbreviation - if not parsed_args.single: - fh1 += self.map - else: - fh1 = self.wad[0:2] + self.map - if parsed_args.single: - fh1 = self.wad[0:min(len(self.wad), 4)] - fh2 = "" - with open(dtp, mode="r") as f: - for line in f: - if line[0:4] == "Time": - m = re.search("[^0-9]*([0-9]*):([0-9]*).[0-9]*", line) - if m is None: - continue - fh2 = m[1]+m[2] - if len(fh2) % 2 == 1: - fh2 = "0" + fh2 - break - if not fh2: - sys.exit(f"Failed to match any line in {dtp} against Time regex.") + def take_action(self, parsed_args): + dip = self.demo_in_path() + dtp = self.dsda_text_path() + if not dtp.exists(): + command = [self.dsda] + if shutil.which("xvfb-run") is not None: + command = ["xvfb-run"] + command + # TODO: negative tics should seek from the end, but this doesn't seem to work. + subprocess.run(command + self.dsda_preamble() + + ["-fastdemo", dip, "-nosound", "-skiptic", "999999999", "-export_text_file"]) + editor = "nano" + if "EDITOR" in os.environ: + editor = os.environ["EDITOR"] + subprocess.run([editor, dtp]) + if parsed_args.abbreviation: + fh1 = parsed_args.abbreviation + if not parsed_args.single: + fh1 += self.map + else: + fh1 = self.wad[0:2] + self.map + if parsed_args.single: + fh1 = self.wad[0:min(len(self.wad), 4)] + fh2 = "" + with open(dtp, mode="r") as f: + for line in f: + if line[0:4] == "Time": + m = re.search("[^0-9]*([0-9]*):([0-9]*).[0-9]*", line) + if m is None: + continue + fh2 = m[1]+m[2] + if len(fh2)%2==1: + fh2 = "0" + fh2 + break + if not fh2: + sys.exit("Failed to match any line in {} against Time regex.".format(dtp)) - # TODO: demo names other than uv-max. - fnf = fh1 + "-" + fh2 + ".zip" - with zipfile.ZipFile( - self.demos.joinpath(self.wad).joinpath(fnf), mode="w" - ) as zf: - zf.write(dip, arcname=dip.name) - zf.write(dtp, arcname=dtp.name) + # TODO: demo names other than uv-max. + fnf = fh1 + "-" + fh2 + ".zip" + with zipfile.ZipFile(self.demos.joinpath(self.wad).joinpath(fnf), mode="w") as zf: + zf.write(dip, arcname=dip.name) + zf.write(dtp, arcname=dtp.name) diff --git a/dcc/eureka.py b/dcc/eureka.py index ed049e3..4c86af0 100644 --- a/dcc/eureka.py +++ b/dcc/eureka.py @@ -2,27 +2,18 @@ import dcc.doom_base import dcc.config import subprocess - class Eureka(dcc.doom_base.WadMap): - def take_action(self, parsed_args): - iwad = self.iwad_path(parsed_args.wad) - pwadpath = self.pwads.joinpath(parsed_args.wad) - mw = list(pwadpath.glob( - '*{}*.wad'.format(parsed_args.wad), case_sensitive=False - )) - if len(mw) != 1: - raise Exception( - f"Unable to guess at main pwad for wad {parsed_args.wad}." - ) + def take_action(self, parsed_args): + iwad = self.iwad_path(parsed_args.wad) + pwadpath = self.pwads.joinpath(parsed_args.wad) + mw = list(pwadpath.glob('*{}*.wad'.format(parsed_args.wad), case_sensitive=False)) + if len(mw) != 1: + raise Exception("Unable to guess at main pwad for wad {}.".format(parsed_args.wad)) + complevel = self.complevel() + port = "vanilla" + if complevel == "9": + port = "boom" + if complevel == "11" or complevel == "21": + port = "mbf" - complevel = self.complevel() - port = "vanilla" - if complevel == "9": - port = "boom" - if complevel == "11" or complevel == "21": - port = "mbf" - - subprocess.run( - ["eureka"] + ["-iwad", iwad] + ["-w", parsed_args.map] - + ["-p", port] + [mw[0]] - ) + subprocess.run(["eureka"] + ["-iwad", iwad] + ["-w", parsed_args.map] + ["-p", port] + [mw[0]]) diff --git a/dcc/extract.py b/dcc/extract.py index 8d38289..43f49ae 100644 --- a/dcc/extract.py +++ b/dcc/extract.py @@ -4,43 +4,27 @@ import numpy as np import wand.color import wand.image - class Extract(dcc.config.base): - def get_parser(self, prog_name): - parser = super().get_parser(prog_name) - parser.add_argument('wad') - parser.add_argument('lump') - return parser + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument('wad') + parser.add_argument('lump') + return parser - def take_action(self, parsed_args): - wads = sorted( - self.pwads.joinpath(self.wad).glob('*.wad', case_sensitive=False), - reverse=True - ) + def take_action(self, parsed_args): + wads = sorted(self.pwads.joinpath(self.wad).glob('*.wad', case_sensitive=False), reverse=True) - for w in wads: - try: - # TODO: handle anything other than graphics. - wad = omg.WadIO(w) - gl = omg.Graphic(wad.read(parsed_args.lump)) - # With no arguments, convert() changes a paletted image to an - # RGB one. - with wand.image.Image.from_array( - np.array(gl.to_Image().convert()) - ) as img: - img.transparent_color(wand.color.Color("#ff00ff"), 0.0) - img.save( - filename=self.output.joinpath(parsed_args.wad) - .joinpath(parsed_args.lump + ".png") - ) - return - except Exception as e: - print( - f"Wad {w} likely has no lump {parsed_args.lump}" - + f"(exception {e})." - ) + for w in wads: + try: + # TODO: handle anything other than graphics. + wad = omg.WadIO(w) + gl = omg.Graphic(wad.read(parsed_args.lump)) + # With no arguments, convert() changes a paletted image to an RGB one. + with wand.image.Image.from_array(np.array(gl.to_Image().convert())) as img: + img.transparent_color(wand.color.Color("#ff00ff"), 0.0) + img.save(filename=self.output.joinpath(parsed_args.wad).joinpath(parsed_args.lump + ".png")) + return + except Exception as e: + print("Wad {} likely has no lump {} (exception {}).".format(w, parsed_args.lump, e)) - print( - "Lump {parsed_args.lump} not found in any wad in" - + f"{parsed_args.wad}" - ) + print("Lump {} not found in any wad in {}".format(parsed_args.lump, parsed_args.wad)) diff --git a/dcc/fabricate.py b/dcc/fabricate.py index 1af78e9..4c951ae 100644 --- a/dcc/fabricate.py +++ b/dcc/fabricate.py @@ -5,26 +5,23 @@ import shutil import subprocess import tempfile - class Fabricate(dcc.doom_base.WadMap): - def get_parser(self, prog_name): - parser = super().get_parser(prog_name) - parser.add_argument("--fg", action="store_true") - return parser + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument("--fg", action="store_true") + return parser - def take_action(self, parsed_args): - with tempfile.TemporaryDirectory() as td: - with contextlib.chdir(td): - command = [self.dsda] - if not parsed_args.fg and shutil.which("xvfb-run") is not None: - command = ["xvfb-run"] + command - options = [] - for k, v in self._config.get("fabricate_options", {}).items(): - list.append(options, f"{k}={v}") - if len(options) > 0: - options = ["-assign", ",".join(options)] - subprocess.run( - command + self.dsda_preamble() + options - + ["-timedemo", self.demo_in_path()] - + ["-viddump", self.video_path()] - ) + def take_action(self, parsed_args): + with tempfile.TemporaryDirectory() as td: + with contextlib.chdir(td): + command = [self.dsda] + if not parsed_args.fg and shutil.which("xvfb-run") is not None: + command = ["xvfb-run"] + command + options = [] + for k,v in self._config.get("fabricate_options", {}).items(): + list.append(options, f"{k}={v}") + if len(options) > 0: + options = ["-assign", ",".join(options)] + subprocess.run(command + self.dsda_preamble() + options + + ["-timedemo", self.demo_in_path()] + + ["-viddump", self.video_path()])