review: allow manual start selection

* return start video
* use ubuntu instead of alpine because ffmpeg in alpine lacks drawtext support
* only cache if inputs are exactly the same, preventing accidents (prev scheme too weak)
* fix a bug where end range is wrong if start_range doesn't start at 0
	conceptually, this was a more serious bug as time_offset was wrong, but luckily the bug applied
	equally to both racers so there was no net effect
condor-scripts
Mike Lang 3 years ago
parent d37cca52c9
commit 345cb53f4e

@ -1,7 +1,6 @@
FROM alpine:3.7 FROM ubuntu:18.04
# dependencies needed for compiling c extensions # dependencies needed for compiling c extensions
# also busybox-extras for telnet for easier use of backdoor RUN chmod 1777 /tmp && apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --yes python-pip python-dev file gcc make ffmpeg
RUN apk --update add py2-pip gcc python-dev musl-dev file make busybox-extras
# Install gevent so that we don't need to re-install it when common changes # Install gevent so that we don't need to re-install it when common changes
RUN pip install gevent RUN pip install gevent
@ -11,7 +10,6 @@ COPY common /tmp/common
RUN pip install /tmp/common && rm -r /tmp/common RUN pip install /tmp/common && rm -r /tmp/common
# Install actual application # Install actual application
RUN apk add ffmpeg
COPY restreamer /tmp/restreamer COPY restreamer /tmp/restreamer
RUN pip install /tmp/restreamer && rm -r /tmp/restreamer RUN pip install /tmp/restreamer && rm -r /tmp/restreamer

@ -365,20 +365,32 @@ def review_race(match_id, race_number):
race start time to look for the start signal. Default 0,5. race start time to look for the start signal. Default 0,5.
finish_range: As start_range, but how long to make the final review video before/after finish_range: As start_range, but how long to make the final review video before/after
the nominal duration. Default -5,10. the nominal duration. Default -5,10.
racer1_start, racer2_start: Explicit start times, as float.
""" """
if app.condor_db is None: if app.condor_db is None:
return "Reviews are disabled", 501 return "Reviews are disabled", 501
start_range = map(float, request.args.get('start_range', '0,5').split(',')) start_range = map(float, request.args.get('start_range', '0,5').split(','))
finish_range = map(float, request.args.get('finish_range', '-5,10').split(',')) finish_range = map(float, request.args.get('finish_range', '-5,10').split(','))
racer1_start = float(request.args['racer1_start']) if 'racer1_start' in request.args else None
racer2_start = float(request.args['racer2_start']) if 'racer2_start' in request.args else None
try: try:
review_path = review(match_id, race_number, app.static_folder, app.condor_db, start_range, finish_range) review_path = review(
match_id, race_number, app.static_folder, app.condor_db, start_range, finish_range,
racer1_start, racer2_start,
)
except RaceNotFound as e: except RaceNotFound as e:
return str(e), 404 return str(e), 404
except NoSegments: except NoSegments:
logging.warning("Failed review due to no segments", exc_info=True) logging.warning("Failed review due to no segments", exc_info=True)
return "Video content is missing - cannot review automatically", 400 return "Video content is missing - cannot review automatically", 400
except CantFindStart as e: except CantFindStart as e:
return str(e), 400 return (
"{}\n"
"Please check start video and adjust start_range or set racer{}_start: {}\n"
"Note timestamps in that video are only valid for the current start_range.\n"
).format(
e, e.racer_number, os.path.join(app.static_url_path, os.path.relpath(e.path, app.static_folder))
), 400
relative_path = os.path.relpath(review_path, app.static_folder) relative_path = os.path.relpath(review_path, app.static_folder)
review_url = os.path.join(app.static_url_path, relative_path) review_url = os.path.join(app.static_url_path, relative_path)

@ -3,6 +3,7 @@ import datetime
import logging import logging
import os import os
import subprocess import subprocess
from hashlib import sha256
from urlparse import urlparse from urlparse import urlparse
from uuid import uuid4 from uuid import uuid4
@ -21,14 +22,16 @@ class RaceNotFound(Exception):
class CantFindStart(Exception): class CantFindStart(Exception):
def __init__(self, racer, found): def __init__(self, racer, racer_number, found, path):
self.racer = racer self.racer = racer
self.racer_number = racer_number
self.found = found self.found = found
self.path = path
def __str__(self): def __str__(self):
if self.found > 0: if self.found > 0:
return "Found multiple ({}) possible start points for {}".format(self.found, self.racer) return "Found multiple ({}) possible start points for racer {} ({})".format(self.found, self.racer_number, self.racer)
else: else:
return "Failed to find start point for {}".format(self.racer) return "Failed to find start point for racer {} ({})".format(self.racer_number, self.racer)
def ts(dt): def ts(dt):
@ -77,7 +80,10 @@ def conn_from_url(url):
) )
def review(match_id, race_number, base_dir, db_url, start_range=(0, 5), finish_range=(-5, 10)): def review(
match_id, race_number, base_dir, db_url, start_range=(0, 5), finish_range=(-5, 10),
racer1_start=None, racer2_start=None,
):
logger = logging.getLogger("review").getChild("{}-{}".format(match_id, race_number)) logger = logging.getLogger("review").getChild("{}-{}".format(match_id, race_number))
conn = conn_from_url(db_url) conn = conn_from_url(db_url)
@ -111,11 +117,15 @@ def review(match_id, race_number, base_dir, db_url, start_range=(0, 5), finish_r
(racer1, racer2, start, duration), = data (racer1, racer2, start, duration), = data
end = start + datetime.timedelta(seconds=duration/100.) end = start + datetime.timedelta(seconds=duration/100.)
# cache hash encapsulates all input args
cache_hash = sha256(str((match_id, race_number, start_range, finish_range, racer1_start, racer2_start)))
cache_str = cache_hash.digest().encode('base64')[:12]
output_name = "{}-{}-{}-{}".format(match_id, racer1, racer2, race_number) output_name = "{}-{}-{}-{}".format(match_id, racer1, racer2, race_number)
output_dir = os.path.join(base_dir, "reviews", output_name) output_dir = os.path.join(base_dir, "reviews", output_name)
if not os.path.exists(output_dir): if not os.path.exists(output_dir):
os.makedirs(output_dir) os.makedirs(output_dir)
result_name = "review_{}_{}.mp4".format(*finish_range) result_name = "review_{}.mp4".format(cache_str)
result_path = os.path.join(output_dir, result_name) result_path = os.path.join(output_dir, result_name)
if os.path.exists(result_path): if os.path.exists(result_path):
logger.info("Result already exists for {}, reusing".format(result_path)) logger.info("Result already exists for {}, reusing".format(result_path))
@ -123,39 +133,42 @@ def review(match_id, race_number, base_dir, db_url, start_range=(0, 5), finish_r
finish_paths = [] finish_paths = []
for racer_number, racer in enumerate((racer1, racer2)): for racer_index, (racer, time_offset) in enumerate(((racer1, racer1_start), (racer2, racer2_start))):
nonce = str(uuid4()) nonce = str(uuid4())
start_path = os.path.join(output_dir, "start-{}-{}.mp4".format(racer_number, nonce)) racer_number = racer_index + 1
logger.info("Cutting start for racer {} ({})".format(racer_number, racer)) if time_offset is None:
start_start, start_end = add_range(start, start_range) start_path = os.path.join(output_dir, "start-{}-{}.mp4".format(racer_number, nonce))
cut_to_file(logger, start_path, base_dir, racer, start_start, start_end) logger.info("Cutting start for racer {} ({})".format(racer_number, racer))
start_start, start_end = add_range(start, start_range)
logger.info("Running blackdetect") cut_to_file(logger, start_path, base_dir, racer, start_start, start_end)
args = [
'ffmpeg', '-hide_banner', logger.info("Running blackdetect")
'-i', start_path, args = [
'-vf', 'blackdetect=d=0.1', 'ffmpeg', '-hide_banner',
'-f', 'null', '/dev/null' '-i', start_path,
] '-vf', 'blackdetect=d=0.1',
proc = subprocess.Popen(args, stderr=subprocess.PIPE) '-f', 'null', '/dev/null'
out, err = proc.communicate() ]
if proc.wait() != 0: proc = subprocess.Popen(args, stderr=subprocess.PIPE)
raise Exception("ffmpeg exited {}\n{}".format(proc.wait(), err)) out, err = proc.communicate()
lines = [ if proc.wait() != 0:
line for line in err.strip().split('\n') raise Exception("ffmpeg exited {}\n{}".format(proc.wait(), err))
if line.startswith('[blackdetect @ ') lines = [
] line for line in err.strip().split('\n')
if len(lines) == 1: if line.startswith('[blackdetect @ ')
line, = lines ]
black_end = line.split(' ')[4] if len(lines) == 1:
assert black_end.startswith('black_end:') line, = lines
time_offset = float(black_end.split(':')[1]) black_end = line.split(' ')[4]
else: assert black_end.startswith('black_end:')
found = len(lines) time_offset = float(black_end.split(':')[1])
logger.warning("Unable to detect start (expected 1 black interval, but found {})".format(found)) else:
raise CantFindStart(racer, found) found = len(lines)
time_offset = datetime.timedelta(seconds=time_offset) logger.warning("Unable to detect start (expected 1 black interval, but found {}), re-cutting with timestamps".format(found))
cut_to_file(logger, start_path, base_dir, racer, start_start, start_end, frame_counter=True)
raise CantFindStart(racer, racer_number, found, start_path)
time_offset = datetime.timedelta(seconds=time_offset - start_range[0])
# start each racer's finish video at TIME_OFFSET later, so they are the same # start each racer's finish video at TIME_OFFSET later, so they are the same
# time since their actual start. # time since their actual start.

Loading…
Cancel
Save