Compare commits
9 commits
b62e9ebbf9
...
6baacd9b89
Author | SHA1 | Date | |
---|---|---|---|
6baacd9b89 | |||
d539490898 | |||
3fde5791a3 | |||
b29e2675d1 | |||
97616b341d | |||
2e9b8b148e | |||
95132370e7 | |||
8071d1a876 | |||
8c1d031def |
6 changed files with 311 additions and 247 deletions
|
@ -13,6 +13,7 @@ class Concat(dcc.doom_base.Wad):
|
||||||
parser = super().get_parser(prog_name)
|
parser = super().get_parser(prog_name)
|
||||||
parser.add_argument("start_map")
|
parser.add_argument("start_map")
|
||||||
parser.add_argument("end_map")
|
parser.add_argument("end_map")
|
||||||
|
parser.add_argument("-n", "--nooverlay", action="store_true")
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def take_action(self, parsed_args):
|
def take_action(self, parsed_args):
|
||||||
|
@ -34,10 +35,14 @@ class Concat(dcc.doom_base.Wad):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
chunk = av.open(v)
|
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()
|
ograph = av.filter.Graph()
|
||||||
sink = ograph.add("buffersink")
|
sink = ograph.add("buffersink")
|
||||||
asink = ograph.add("abuffersink")
|
asink = ograph.add("abuffersink")
|
||||||
|
|
||||||
|
if not parsed_args.nooverlay:
|
||||||
img = wand.image.Image(height=chunk.streams[0].height,width=chunk.streams[0].width)
|
img = wand.image.Image(height=chunk.streams[0].height,width=chunk.streams[0].width)
|
||||||
mapstring = v.name[-6:-4]
|
mapstring = v.name[-6:-4]
|
||||||
text = self._config["map_names"][f"map{mapstring}"]
|
text = self._config["map_names"][f"map{mapstring}"]
|
||||||
|
@ -50,53 +55,68 @@ class Concat(dcc.doom_base.Wad):
|
||||||
padfactor=8
|
padfactor=8
|
||||||
img.border("transparent", padfactor, 0)
|
img.border("transparent", padfactor, 0)
|
||||||
img.crop(width=img.width-img.width%padfactor, height=img.height)
|
img.crop(width=img.width-img.width%padfactor, height=img.height)
|
||||||
text_frame = av.video.frame.VideoFrame(img.width, img.height, format="rgba")
|
|
||||||
|
|
||||||
if len(output.streams.get()) == 0:
|
if len(output.streams.get()) == 0:
|
||||||
# TODO: less hardcoding.
|
# We can't use the input stream as a template here; it doesn't
|
||||||
output.add_stream("h264", rate=61440)
|
# have everything needed to do encoding and will fail
|
||||||
output.streams[0].extradata = copy.deepcopy(chunk.streams[0].extradata)
|
# mysteriously later.
|
||||||
output.streams[0].height=1440
|
vs = chunk.streams.video[0]
|
||||||
output.streams[0].width=2560
|
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].profile="High"
|
||||||
output.streams[0].qmax = chunk.streams[0].qmax
|
|
||||||
output.streams[0].qmin = chunk.streams[0].qmin
|
|
||||||
output.streams[0].codec_context.gop_size=30
|
output.streams[0].codec_context.gop_size=30
|
||||||
output.streams[0].codec_context.max_b_frames=2
|
output.streams[0].codec_context.max_b_frames=2
|
||||||
output.streams[0].codec_context.framerate = fractions.Fraction(60,1)
|
|
||||||
output.streams[0].codec_context.pix_fmt="yuv420p"
|
astr = chunk.streams.audio[0]
|
||||||
output.streams[0].codec_context.bit_rate = chunk.streams[0].codec_context.bit_rate
|
output.add_stream("aac", rate=astr.rate)
|
||||||
output.add_stream("aac", rate=48000)
|
output.streams[1].extradata = copy.deepcopy(astr.extradata)
|
||||||
output.streams[1].extradata = copy.deepcopy(output.streams[1].extradata)
|
output.streams[1].bit_rate=astr.bit_rate
|
||||||
output.streams[1].rate=48000
|
|
||||||
output.streams[1].bit_rate=chunk.streams[1].bit_rate
|
src = ograph.add_buffer(template=chunk.streams.video[0], time_base=chunk.streams.video[0].time_base)
|
||||||
src = ograph.add_buffer(template=chunk.streams[0], time_base=chunk.streams[0].time_base)
|
asrc = ograph.add_abuffer(template=chunk.streams.audio[0], time_base=chunk.streams.audio[0].time_base)
|
||||||
asrc = ograph.add_abuffer(template=chunk.streams[1], time_base=chunk.streams[1].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 = 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:240:60")
|
overlay_fo = ograph.add("fade", args="out:{}:{}".format(4*frame_rate, frame_rate))
|
||||||
overlay.link_to(overlay_fo, 0, 0)
|
overlay.link_to(overlay_fo, 0, 0)
|
||||||
composite = ograph.add("overlay", args="x=4:y=4")
|
composite = ograph.add("overlay", args="x=4:y=4")
|
||||||
src.link_to(composite, 0, 0)
|
src.link_to(composite, 0, 0)
|
||||||
overlay_fo.link_to(composite, 0, 1)
|
overlay_fo.link_to(composite, 0, 1)
|
||||||
ifade = ograph.add("fade", args="in:0:60")
|
|
||||||
iafade = ograph.add("afade", args="in:{}:48000".format(offset*48000/1000000))
|
|
||||||
ofade = ograph.add("fade", args="out:{}:60".format((chunk.duration*60/1000000)-60))
|
|
||||||
oafade = ograph.add("afade", args="out:{}:48000".format(((offset+chunk.duration)*48000/1000000)-48000))
|
|
||||||
composite.link_to(ifade, 0, 0)
|
composite.link_to(ifade, 0, 0)
|
||||||
|
else:
|
||||||
|
src.link_to(ifade, 0, 0)
|
||||||
|
|
||||||
asrc.link_to(iafade, 0, 0)
|
asrc.link_to(iafade, 0, 0)
|
||||||
ifade.link_to(ofade, 0, 0)
|
ifade.link_to(ofade, 0, 0)
|
||||||
iafade.link_to(oafade, 0, 0)
|
iafade.link_to(oafade, 0, 0)
|
||||||
ofade.link_to(sink, 0, 0)
|
ofade.link_to(sink, 0, 0)
|
||||||
oafade.link_to(asink, 0, 0)
|
oafade.link_to(asink, 0, 0)
|
||||||
|
|
||||||
ograph.configure()
|
ograph.configure()
|
||||||
|
|
||||||
for packet in chunk.demux():
|
for packet in chunk.demux():
|
||||||
if packet.dts is None:
|
if packet.dts is None:
|
||||||
continue
|
continue
|
||||||
packet.dts += (offset * packet.time_base.denominator) / (packet.time_base.numerator * 1000000)
|
packet.dts += (offset * packet.time_base.denominator) / (packet.time_base.numerator * 1000000)
|
||||||
packet.pts += (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_index == 0: # TODO: robustness
|
if packet.stream == chunk.streams.video[0]:
|
||||||
for ifr in packet.decode():
|
for ifr in packet.decode():
|
||||||
|
if not parsed_args.nooverlay:
|
||||||
text_frame = av.video.frame.VideoFrame(img.width, img.height, format="rgba")
|
text_frame = av.video.frame.VideoFrame(img.width, img.height, format="rgba")
|
||||||
text_frame.planes[0].update(img.make_blob(format="rgba"))
|
text_frame.planes[0].update(img.make_blob(format="rgba"))
|
||||||
text_frame.pts = ifr.pts
|
text_frame.pts = ifr.pts
|
||||||
|
|
|
@ -6,6 +6,7 @@ import os
|
||||||
import re
|
import re
|
||||||
import tomlkit
|
import tomlkit
|
||||||
|
|
||||||
|
|
||||||
class Wad(dcc.config.Base):
|
class Wad(dcc.config.Base):
|
||||||
def get_parser(self, prog_name):
|
def get_parser(self, prog_name):
|
||||||
parser = super().get_parser(prog_name)
|
parser = super().get_parser(prog_name)
|
||||||
|
@ -19,8 +20,6 @@ class Wad(dcc.config.Base):
|
||||||
if wcp.exists():
|
if wcp.exists():
|
||||||
self._wad_config = tomlkit.toml_file.TOMLFile(wcp).read()
|
self._wad_config = tomlkit.toml_file.TOMLFile(wcp).read()
|
||||||
self._config.update(self._wad_config.value)
|
self._config.update(self._wad_config.value)
|
||||||
#for k,v in self._wad_config.value.items():
|
|
||||||
#self._config.add(k,v)
|
|
||||||
|
|
||||||
def run(self, parsed_args):
|
def run(self, parsed_args):
|
||||||
self.wad_init(parsed_args)
|
self.wad_init(parsed_args)
|
||||||
|
@ -35,7 +34,7 @@ class WadMap(Wad):
|
||||||
def get_parser(self, prog_name):
|
def get_parser(self, prog_name):
|
||||||
parser = super().get_parser(prog_name)
|
parser = super().get_parser(prog_name)
|
||||||
parser.add_argument('map')
|
parser.add_argument('map')
|
||||||
parser.add_argument('-n','--name','--demo_name')
|
parser.add_argument('-n', '--name', '--demo_name')
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def run(self, parsed_args):
|
def run(self, parsed_args):
|
||||||
|
@ -73,42 +72,72 @@ class WadMap(Wad):
|
||||||
def complevel(self):
|
def complevel(self):
|
||||||
complevel = self.pwads.joinpath(self.wad).joinpath("complevel")
|
complevel = self.pwads.joinpath(self.wad).joinpath("complevel")
|
||||||
if not complevel.exists():
|
if not complevel.exists():
|
||||||
|
complevel = self._config.get("complevel")
|
||||||
|
if complevel is not None:
|
||||||
|
return complevel
|
||||||
raise Exception("No complevel set for wad {}.".format(self.wad))
|
raise Exception("No complevel set for wad {}.".format(self.wad))
|
||||||
|
|
||||||
with io.open(complevel) as f:
|
with io.open(complevel) as f:
|
||||||
return f.read().strip()
|
return f.read().strip()
|
||||||
|
|
||||||
def demo_in_path(self):
|
def demo_in_path(self):
|
||||||
candidates = [x for x in self.demos.joinpath(self.wad).glob(self._file_base("*.lmp"))]
|
candidates = [
|
||||||
|
x for x in self.demos.joinpath(self.wad)
|
||||||
|
.glob(self._file_base("*.lmp"))
|
||||||
|
]
|
||||||
if len(candidates) == 0:
|
if len(candidates) == 0:
|
||||||
raise Exception("no suitable demo candidates for WAD {} MAP {} name {}.".format(self.wad, self.map, self.name_string))
|
raise Exception(
|
||||||
|
"no suitable demo candidates for WAD {} MAP {} name {}."
|
||||||
|
.format(self.wad, self.map, self.name_string)
|
||||||
|
)
|
||||||
if len(candidates) == 1:
|
if len(candidates) == 1:
|
||||||
return candidates[0]
|
return candidates[0]
|
||||||
return sorted(filter(lambda s : re.search("-", str(s)), candidates))[-1]
|
return sorted(filter(lambda s: re.search("-", str(s)), candidates))[-1]
|
||||||
|
|
||||||
def dsda_text_path(self):
|
def dsda_text_path(self):
|
||||||
return self._ensure(self.demos.joinpath(self.wad)).joinpath(self._file_base(".txt"))
|
return (
|
||||||
|
self._ensure(self.demos.joinpath(self.wad))
|
||||||
|
.joinpath(self._file_base(".txt"))
|
||||||
|
)
|
||||||
|
|
||||||
def video_path(self):
|
def video_path(self):
|
||||||
return self._ensure(self.fabricate.joinpath(self.wad)).joinpath(self._file_base(".mp4"))
|
return (
|
||||||
|
self._ensure(self.fabricate.joinpath(self.wad))
|
||||||
|
.joinpath(self._file_base(".mp4"))
|
||||||
|
)
|
||||||
|
|
||||||
def demo_out_path(self):
|
def demo_out_path(self):
|
||||||
return self._ensure(self.demos.joinpath(self.wad)).joinpath(self._file_base(".lmp"))
|
return (
|
||||||
|
self._ensure(self.demos.joinpath(self.wad))
|
||||||
|
.joinpath(self._file_base(".lmp"))
|
||||||
|
)
|
||||||
|
|
||||||
def target_bucket(self):
|
def target_bucket(self):
|
||||||
return "doom/" + self._file_base(".lmp")
|
return "doom/" + self._file_base(".lmp")
|
||||||
|
|
||||||
def base_thumb_path(self):
|
def base_thumb_path(self):
|
||||||
return self._ensure(self.fabricate.joinpath(self.wad)).joinpath(self._file_base("_base.png"))
|
return (
|
||||||
|
self._ensure(self.fabricate.joinpath(self.wad))
|
||||||
|
.joinpath(self._file_base("_base.png"))
|
||||||
|
)
|
||||||
|
|
||||||
def m_doom_path(self):
|
def m_doom_path(self):
|
||||||
return self._ensure(self.fabricate.joinpath(self.wad)).joinpath("M_DOOM_scaled.png")
|
return (
|
||||||
|
self._ensure(self.fabricate.joinpath(self.wad))
|
||||||
|
.joinpath("M_DOOM_scaled.png")
|
||||||
|
)
|
||||||
|
|
||||||
def text_thumb_path(self):
|
def text_thumb_path(self):
|
||||||
return self._ensure(self.fabricate.joinpath(self.wad)).joinpath(self._file_base("_text.png"))
|
return (
|
||||||
|
self._ensure(self.fabricate.joinpath(self.wad))
|
||||||
|
.joinpath(self._file_base("_text.png"))
|
||||||
|
)
|
||||||
|
|
||||||
def thumb_path(self):
|
def thumb_path(self):
|
||||||
return self._ensure(self.fabricate.joinpath(self.wad)).joinpath(self._file_base("_thumb.png"))
|
return (
|
||||||
|
self._ensure(self.fabricate.joinpath(self.wad))
|
||||||
|
.joinpath(self._file_base("_thumb.png"))
|
||||||
|
)
|
||||||
|
|
||||||
def _file_base(self, ext):
|
def _file_base(self, ext):
|
||||||
return "{}_map{}{}{}".format(self.wad, self.map, self.name_string, ext)
|
return "{}_map{}{}{}".format(self.wad, self.map, self.name_string, ext)
|
||||||
|
|
17
dcc/fetch.py
17
dcc/fetch.py
|
@ -5,6 +5,7 @@ import pathlib
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
class Fetch(dcc.config.Base):
|
class Fetch(dcc.config.Base):
|
||||||
def get_parser(self, prog_name):
|
def get_parser(self, prog_name):
|
||||||
parser = super().get_parser(prog_name)
|
parser = super().get_parser(prog_name)
|
||||||
|
@ -16,9 +17,16 @@ class Fetch(dcc.config.Base):
|
||||||
if not parsed_args.id_or_name.isdigit():
|
if not parsed_args.id_or_name.isdigit():
|
||||||
idgames_id = self.search_idgames(parsed_args.id_or_name)
|
idgames_id = self.search_idgames(parsed_args.id_or_name)
|
||||||
|
|
||||||
with urllib.request.urlopen("https://www.doomworld.com/idgames/api/api.php?action=get&id={}&out=json".format(idgames_id)) as response:
|
with urllib.request.urlopen(
|
||||||
|
"https://www.doomworld.com/idgames/api/" +
|
||||||
|
"api.php?action=get&id={}&out=json".format(idgames_id)
|
||||||
|
) as response:
|
||||||
reply = json.loads(response.read())
|
reply = json.loads(response.read())
|
||||||
rpath = "/".join([dcc.config.MIRROR, reply["content"]["dir"], reply["content"]["filename"]])
|
rpath = "/".join([
|
||||||
|
dcc.config.MIRROR,
|
||||||
|
reply["content"]["dir"],
|
||||||
|
reply["content"]["filename"]
|
||||||
|
])
|
||||||
wad = reply["content"]["filename"][0:-4]
|
wad = reply["content"]["filename"][0:-4]
|
||||||
|
|
||||||
with urllib.request.urlopen(rpath) as response:
|
with urllib.request.urlopen(rpath) as response:
|
||||||
|
@ -27,7 +35,10 @@ class Fetch(dcc.config.Base):
|
||||||
|
|
||||||
# TODO: explicit error handling. Let users choose when >1 result.
|
# TODO: explicit error handling. Let users choose when >1 result.
|
||||||
def search_idgames(self, wad):
|
def search_idgames(self, wad):
|
||||||
with urllib.request.urlopen("https://www.doomworld.com/idgames/api/api.php?action=search&query={}&out=json".format(wad)) as response:
|
with urllib.request.urlopen(
|
||||||
|
"https://www.doomworld.com/idgames/api/" +
|
||||||
|
"api.php?action=search&query={}&out=json".format(wad)
|
||||||
|
) as response:
|
||||||
reply = json.loads(response.read())
|
reply = json.loads(response.read())
|
||||||
files = reply["content"]["file"]
|
files = reply["content"]["file"]
|
||||||
if type(files) is dict: # One result.
|
if type(files) is dict: # One result.
|
||||||
|
|
|
@ -2,6 +2,7 @@ import dcc.config
|
||||||
import dcc.doom_base
|
import dcc.doom_base
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
class Play(dcc.doom_base.WadMap):
|
class Play(dcc.doom_base.WadMap):
|
||||||
def take_action(self, parsed_args):
|
def take_action(self, parsed_args):
|
||||||
subprocess.run([self.dsda] + self.dsda_preamble())
|
subprocess.run([self.dsda] + self.dsda_preamble())
|
||||||
|
|
|
@ -2,7 +2,10 @@ import dcc.config
|
||||||
import dcc.doom_base
|
import dcc.doom_base
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
class Record(dcc.doom_base.WadMap):
|
class Record(dcc.doom_base.WadMap):
|
||||||
def take_action(self, parsed_args):
|
def take_action(self, parsed_args):
|
||||||
subprocess.run([self.dsda] + self.dsda_preamble() +
|
subprocess.run(
|
||||||
["-record", self.demo_out_path()])
|
[self.dsda] + self.dsda_preamble() +
|
||||||
|
["-record", self.demo_out_path()]
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue