|
|
|
@ -295,8 +295,9 @@ def ffmpeg_cut_segment(segment, cut_start=None, cut_end=None):
|
|
|
|
|
def ffmpeg_cut_stdin(output_file, cut_start, duration, encode_args):
|
|
|
|
|
"""Return a Popen object which is ffmpeg cutting from stdin.
|
|
|
|
|
This is used when doing a full cut.
|
|
|
|
|
Note the explicit output file object instead of using a pipe,
|
|
|
|
|
because most video formats require a seekable file.
|
|
|
|
|
If output_file is not subprocess.PIPE,
|
|
|
|
|
uses explicit output file object instead of using a pipe,
|
|
|
|
|
because some video formats require a seekable file.
|
|
|
|
|
"""
|
|
|
|
|
args = [
|
|
|
|
|
'ffmpeg',
|
|
|
|
@ -304,16 +305,20 @@ def ffmpeg_cut_stdin(output_file, cut_start, duration, encode_args):
|
|
|
|
|
'-i', '-',
|
|
|
|
|
'-ss', cut_start,
|
|
|
|
|
'-t', duration,
|
|
|
|
|
] + list(encode_args) + [
|
|
|
|
|
# We want ffmpeg to write to our tempfile, which is its stdout.
|
|
|
|
|
# However, it assumes that '-' means the output is not seekable.
|
|
|
|
|
# We trick it into understanding that its stdout is seekable by
|
|
|
|
|
# telling it to write to the fd via its /proc/self filename.
|
|
|
|
|
'/proc/self/fd/1',
|
|
|
|
|
# But of course, that file "already exists", so we need to give it
|
|
|
|
|
# permission to "overwrite" it.
|
|
|
|
|
'-y',
|
|
|
|
|
]
|
|
|
|
|
] + list(encode_args)
|
|
|
|
|
if output_file is subprocess.PIPE:
|
|
|
|
|
args.append('-') # output to stdout
|
|
|
|
|
else:
|
|
|
|
|
args += [
|
|
|
|
|
# We want ffmpeg to write to our tempfile, which is its stdout.
|
|
|
|
|
# However, it assumes that '-' means the output is not seekable.
|
|
|
|
|
# We trick it into understanding that its stdout is seekable by
|
|
|
|
|
# telling it to write to the fd via its /proc/self filename.
|
|
|
|
|
'/proc/self/fd/1',
|
|
|
|
|
# But of course, that file "already exists", so we need to give it
|
|
|
|
|
# permission to "overwrite" it.
|
|
|
|
|
'-y',
|
|
|
|
|
]
|
|
|
|
|
args = map(str, args)
|
|
|
|
|
logging.info("Running full cut with args: {}".format(" ".join(args)))
|
|
|
|
|
return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=output_file)
|
|
|
|
@ -401,41 +406,67 @@ def fast_cut_segments(segments, start, end):
|
|
|
|
|
yield chunk
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@timed('cut', type='full', normalize=lambda _, segments, start, end, encode_args: (end - start).total_seconds())
|
|
|
|
|
def full_cut_segments(segments, start, end, encode_args):
|
|
|
|
|
def feed_input(segments, pipe):
|
|
|
|
|
"""Write each segment's data into the given pipe in order.
|
|
|
|
|
This is used to provide input to ffmpeg in a full cut."""
|
|
|
|
|
for segment in segments:
|
|
|
|
|
with open(segment.path) as f:
|
|
|
|
|
try:
|
|
|
|
|
shutil.copyfileobj(f, pipe)
|
|
|
|
|
except OSError as e:
|
|
|
|
|
# ignore EPIPE, as this just means the end cut meant we didn't need all it
|
|
|
|
|
if e.errno != errno.EPIPE:
|
|
|
|
|
raise
|
|
|
|
|
pipe.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@timed('cut',
|
|
|
|
|
type=lambda _, segments, start, end, encode_args, stream=False: "full-streamed" if stream else "full-buffered",
|
|
|
|
|
normalize=lambda _, segments, start, end, *a, **k: (end - start).total_seconds()),
|
|
|
|
|
)
|
|
|
|
|
def full_cut_segments(segments, start, end, encode_args, stream=False):
|
|
|
|
|
"""If stream=true, assume encode_args gives a streamable format,
|
|
|
|
|
and begin returning output immediately instead of waiting for ffmpeg to finish
|
|
|
|
|
and buffering to disk."""
|
|
|
|
|
# how far into the first segment to begin
|
|
|
|
|
cut_start = max(0, (start - segments[0].start).total_seconds())
|
|
|
|
|
# duration
|
|
|
|
|
duration = (end - start).total_seconds()
|
|
|
|
|
|
|
|
|
|
ffmpeg = None
|
|
|
|
|
input_feeder = None
|
|
|
|
|
try:
|
|
|
|
|
# Most ffmpeg output formats require a seekable file.
|
|
|
|
|
# For the same reason, it's not safe to begin uploading until ffmpeg
|
|
|
|
|
# has finished. We create a temporary file for this.
|
|
|
|
|
tempfile = TemporaryFile()
|
|
|
|
|
|
|
|
|
|
if stream:
|
|
|
|
|
# When streaming, we can just use a pipe
|
|
|
|
|
tempfile = subprocess.PIPE
|
|
|
|
|
else:
|
|
|
|
|
# Some ffmpeg output formats require a seekable file.
|
|
|
|
|
# For the same reason, it's not safe to begin uploading until ffmpeg
|
|
|
|
|
# has finished. We create a temporary file for this.
|
|
|
|
|
tempfile = TemporaryFile()
|
|
|
|
|
|
|
|
|
|
ffmpeg = ffmpeg_cut_stdin(tempfile, cut_start, duration, encode_args)
|
|
|
|
|
input_feeder = gevent.spawn(feed_input, segments, ffmpeg.stdin)
|
|
|
|
|
|
|
|
|
|
# stream the input
|
|
|
|
|
for segment in segments:
|
|
|
|
|
with open(segment.path) as f:
|
|
|
|
|
try:
|
|
|
|
|
shutil.copyfileobj(f, ffmpeg.stdin)
|
|
|
|
|
except OSError as e:
|
|
|
|
|
# ignore EPIPE, as this just means the end cut meant we didn't need all input
|
|
|
|
|
if e.errno != errno.EPIPE:
|
|
|
|
|
raise
|
|
|
|
|
ffmpeg.stdin.close()
|
|
|
|
|
# When streaming, we can return data as it is available
|
|
|
|
|
if stream:
|
|
|
|
|
for chunk in read_chunks(ffmpeg.stdout):
|
|
|
|
|
yield chunk
|
|
|
|
|
|
|
|
|
|
# check if any errors occurred in input writing, or if ffmpeg exited non-success.
|
|
|
|
|
if ffmpeg.wait() != 0:
|
|
|
|
|
raise Exception("Error while streaming cut: ffmpeg exited {}".format(ffmpeg.returncode))
|
|
|
|
|
input_feeder.get() # re-raise any errors from feed_input()
|
|
|
|
|
|
|
|
|
|
# Now actually yield the resulting file
|
|
|
|
|
for chunk in read_chunks(tempfile):
|
|
|
|
|
yield chunk
|
|
|
|
|
# When not streaming, we can only return the data once ffmpeg has exited
|
|
|
|
|
if not stream:
|
|
|
|
|
for chunk in read_chunks(tempfile):
|
|
|
|
|
yield chunk
|
|
|
|
|
finally:
|
|
|
|
|
# if something goes wrong, try to clean up ignoring errors
|
|
|
|
|
if input_feeder is not None:
|
|
|
|
|
input_feeder.kill()
|
|
|
|
|
if ffmpeg is not None and ffmpeg.poll() is None:
|
|
|
|
|
for action in (ffmpeg.kill, ffmpeg.stdin.close, ffmpeg.stdout.close):
|
|
|
|
|
try:
|
|
|
|
|