324 lines
9.8 KiB
Python
324 lines
9.8 KiB
Python
'''
|
|
Tools for using ffmpeg
|
|
'''
|
|
import numpy
|
|
import sys
|
|
import os
|
|
import subprocess
|
|
|
|
import core
|
|
from toolkit.common import checkOutput, openPipe
|
|
|
|
|
|
def findFfmpeg():
|
|
if getattr(sys, 'frozen', False):
|
|
# The application is frozen
|
|
if sys.platform == "win32":
|
|
return os.path.join(core.Core.wd, 'ffmpeg.exe')
|
|
else:
|
|
return os.path.join(core.Core.wd, 'ffmpeg')
|
|
|
|
else:
|
|
if sys.platform == "win32":
|
|
return "ffmpeg"
|
|
else:
|
|
try:
|
|
with open(os.devnull, "w") as f:
|
|
checkOutput(
|
|
['ffmpeg', '-version'], stderr=f
|
|
)
|
|
return "ffmpeg"
|
|
except subprocess.CalledProcessError:
|
|
return "avconv"
|
|
|
|
|
|
def createFfmpegCommand(inputFile, outputFile, components, duration=-1):
|
|
'''
|
|
Constructs the major ffmpeg command used to export the video
|
|
'''
|
|
if duration == -1:
|
|
duration = getAudioDuration(inputFile)
|
|
safeDuration = "{0:.3f}".format(duration - 0.05) # used by filters
|
|
duration = "{0:.3f}".format(duration + 0.1) # used by input sources
|
|
Core = core.Core
|
|
|
|
# Test if user has libfdk_aac
|
|
encoders = checkOutput(
|
|
"%s -encoders -hide_banner" % Core.FFMPEG_BIN, shell=True
|
|
)
|
|
encoders = encoders.decode("utf-8")
|
|
|
|
acodec = Core.settings.value('outputAudioCodec')
|
|
|
|
options = Core.encoderOptions
|
|
containerName = Core.settings.value('outputContainer')
|
|
vcodec = Core.settings.value('outputVideoCodec')
|
|
vbitrate = str(Core.settings.value('outputVideoBitrate'))+'k'
|
|
acodec = Core.settings.value('outputAudioCodec')
|
|
abitrate = str(Core.settings.value('outputAudioBitrate'))+'k'
|
|
|
|
for cont in options['containers']:
|
|
if cont['name'] == containerName:
|
|
container = cont['container']
|
|
break
|
|
|
|
vencoders = options['video-codecs'][vcodec]
|
|
aencoders = options['audio-codecs'][acodec]
|
|
|
|
for encoder in vencoders:
|
|
if encoder in encoders:
|
|
vencoder = encoder
|
|
break
|
|
|
|
for encoder in aencoders:
|
|
if encoder in encoders:
|
|
aencoder = encoder
|
|
break
|
|
|
|
ffmpegCommand = [
|
|
Core.FFMPEG_BIN,
|
|
'-thread_queue_size', '512',
|
|
'-y', # overwrite the output file if it already exists.
|
|
|
|
# INPUT VIDEO
|
|
'-f', 'rawvideo',
|
|
'-vcodec', 'rawvideo',
|
|
'-s', '%sx%s' % (
|
|
Core.settings.value('outputWidth'),
|
|
Core.settings.value('outputHeight'),
|
|
),
|
|
'-pix_fmt', 'rgba',
|
|
'-r', Core.settings.value('outputFrameRate'),
|
|
'-t', duration,
|
|
'-i', '-', # the video input comes from a pipe
|
|
'-an', # the video input has no sound
|
|
|
|
# INPUT SOUND
|
|
'-t', duration,
|
|
'-i', inputFile
|
|
]
|
|
|
|
extraAudio = [
|
|
comp.audio for comp in components
|
|
if 'audio' in comp.properties()
|
|
]
|
|
segment = createAudioFilterCommand(extraAudio, safeDuration)
|
|
ffmpegCommand.extend(segment)
|
|
if segment:
|
|
# Only map audio from the filters, and video from the pipe
|
|
ffmpegCommand.extend([
|
|
'-map', '0:v',
|
|
'-map', '[a]',
|
|
])
|
|
|
|
ffmpegCommand.extend([
|
|
# OUTPUT
|
|
'-vcodec', vencoder,
|
|
'-acodec', aencoder,
|
|
'-b:v', vbitrate,
|
|
'-b:a', abitrate,
|
|
'-pix_fmt', Core.settings.value('outputVideoFormat'),
|
|
'-preset', Core.settings.value('outputPreset'),
|
|
'-f', container
|
|
])
|
|
|
|
if acodec == 'aac':
|
|
ffmpegCommand.append('-strict')
|
|
ffmpegCommand.append('-2')
|
|
|
|
ffmpegCommand.append(outputFile)
|
|
return ffmpegCommand
|
|
|
|
|
|
def createAudioFilterCommand(extraAudio, duration):
|
|
'''Add extra inputs and any needed filters to the main ffmpeg command.'''
|
|
# NOTE: Global filters are currently hard-coded here for debugging use
|
|
globalFilters = 0 # increase to add global filters
|
|
|
|
if not extraAudio and not globalFilters:
|
|
return []
|
|
|
|
ffmpegCommand = []
|
|
# Add -i options for extra input files
|
|
extraFilters = {}
|
|
for streamNo, params in enumerate(reversed(extraAudio)):
|
|
extraInputFile, params = params
|
|
ffmpegCommand.extend([
|
|
'-t', duration,
|
|
# Tell ffmpeg about shorter clips (seemingly not needed)
|
|
# streamDuration = getAudioDuration(extraInputFile)
|
|
# if streamDuration and streamDuration > float(safeDuration)
|
|
# else "{0:.3f}".format(streamDuration),
|
|
'-i', extraInputFile
|
|
])
|
|
# Construct dataset of extra filters we'll need to add later
|
|
for ffmpegFilter in params:
|
|
if streamNo + 2 not in extraFilters:
|
|
extraFilters[streamNo + 2] = []
|
|
extraFilters[streamNo + 2].append((
|
|
ffmpegFilter, params[ffmpegFilter]
|
|
))
|
|
|
|
# Start creating avfilters! Popen-style, so don't use semicolons;
|
|
extraFilterCommand = []
|
|
|
|
if globalFilters <= 0:
|
|
# Dictionary of last-used tmp labels for a given stream number
|
|
tmpInputs = {streamNo: -1 for streamNo in extraFilters}
|
|
else:
|
|
# Insert blank entries for global filters into extraFilters
|
|
# so the per-stream filters know what input to source later
|
|
for streamNo in range(len(extraAudio), 0, -1):
|
|
if streamNo + 1 not in extraFilters:
|
|
extraFilters[streamNo + 1] = []
|
|
# Also filter the primary audio track
|
|
extraFilters[1] = []
|
|
tmpInputs = {
|
|
streamNo: globalFilters - 1
|
|
for streamNo in extraFilters
|
|
}
|
|
|
|
# Add the global filters!
|
|
# NOTE: list length must = globalFilters, currently hardcoded
|
|
if tmpInputs:
|
|
extraFilterCommand.extend([
|
|
'[%s:a] ashowinfo [%stmp0]' % (
|
|
str(streamNo),
|
|
str(streamNo)
|
|
)
|
|
for streamNo in tmpInputs
|
|
])
|
|
|
|
# Now add the per-stream filters!
|
|
for streamNo, paramList in extraFilters.items():
|
|
for param in paramList:
|
|
source = '[%s:a]' % str(streamNo) \
|
|
if tmpInputs[streamNo] == -1 else \
|
|
'[%stmp%s]' % (
|
|
str(streamNo), str(tmpInputs[streamNo])
|
|
)
|
|
tmpInputs[streamNo] = tmpInputs[streamNo] + 1
|
|
extraFilterCommand.append(
|
|
'%s %s%s [%stmp%s]' % (
|
|
source, param[0], param[1], str(streamNo),
|
|
str(tmpInputs[streamNo])
|
|
)
|
|
)
|
|
|
|
# Join all the filters together and combine into 1 stream
|
|
extraFilterCommand = "; ".join(extraFilterCommand) + '; ' \
|
|
if tmpInputs else ''
|
|
ffmpegCommand.extend([
|
|
'-filter_complex',
|
|
extraFilterCommand +
|
|
'%s amix=inputs=%s:duration=first [a]'
|
|
% (
|
|
"".join([
|
|
'[%stmp%s]' % (str(i), tmpInputs[i])
|
|
if i in extraFilters else '[%s:a]' % str(i)
|
|
for i in range(1, len(extraAudio) + 2)
|
|
]),
|
|
str(len(extraAudio) + 1)
|
|
),
|
|
])
|
|
return ffmpegCommand
|
|
|
|
|
|
def testAudioStream(filename):
|
|
'''Test if an audio stream definitely exists'''
|
|
audioTestCommand = [
|
|
core.Core.FFMPEG_BIN,
|
|
'-i', filename,
|
|
'-vn', '-f', 'null', '-'
|
|
]
|
|
try:
|
|
checkOutput(audioTestCommand, stderr=subprocess.DEVNULL)
|
|
except subprocess.CalledProcessError:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
|
|
def getAudioDuration(filename):
|
|
'''Try to get duration of audio file as float, or False if not possible'''
|
|
command = [core.Core.FFMPEG_BIN, '-i', filename]
|
|
|
|
try:
|
|
fileInfo = checkOutput(command, stderr=subprocess.STDOUT)
|
|
except subprocess.CalledProcessError as ex:
|
|
fileInfo = ex.output
|
|
|
|
info = fileInfo.decode("utf-8").split('\n')
|
|
for line in info:
|
|
if 'Duration' in line:
|
|
d = line.split(',')[0]
|
|
d = d.split(' ')[3]
|
|
d = d.split(':')
|
|
duration = float(d[0])*3600 + float(d[1])*60 + float(d[2])
|
|
break
|
|
else:
|
|
# String not found in output
|
|
return False
|
|
return duration
|
|
|
|
|
|
def readAudioFile(filename, videoWorker):
|
|
'''
|
|
Creates the completeAudioArray given to components
|
|
and used to draw the classic visualizer.
|
|
'''
|
|
duration = getAudioDuration(filename)
|
|
if not duration:
|
|
print('Audio file doesn\'t exist or unreadable.')
|
|
return
|
|
|
|
command = [
|
|
core.Core.FFMPEG_BIN,
|
|
'-i', filename,
|
|
'-f', 's16le',
|
|
'-acodec', 'pcm_s16le',
|
|
'-ar', '44100', # ouput will have 44100 Hz
|
|
'-ac', '1', # mono (set to '2' for stereo)
|
|
'-']
|
|
in_pipe = openPipe(
|
|
command,
|
|
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, bufsize=10**8
|
|
)
|
|
|
|
completeAudioArray = numpy.empty(0, dtype="int16")
|
|
|
|
progress = 0
|
|
lastPercent = None
|
|
while True:
|
|
if core.Core.canceled:
|
|
return
|
|
# read 2 seconds of audio
|
|
progress += 4
|
|
raw_audio = in_pipe.stdout.read(88200*4)
|
|
if len(raw_audio) == 0:
|
|
break
|
|
audio_array = numpy.fromstring(raw_audio, dtype="int16")
|
|
completeAudioArray = numpy.append(completeAudioArray, audio_array)
|
|
|
|
percent = int(100*(progress/duration))
|
|
if percent >= 100:
|
|
percent = 100
|
|
|
|
if lastPercent != percent:
|
|
string = 'Loading audio file: '+str(percent)+'%'
|
|
videoWorker.progressBarSetText.emit(string)
|
|
videoWorker.progressBarUpdate.emit(percent)
|
|
|
|
lastPercent = percent
|
|
|
|
in_pipe.kill()
|
|
in_pipe.wait()
|
|
|
|
# add 0s the end
|
|
completeAudioArrayCopy = numpy.zeros(
|
|
len(completeAudioArray) + 44100, dtype="int16")
|
|
completeAudioArrayCopy[:len(completeAudioArray)] = completeAudioArray
|
|
completeAudioArray = completeAudioArrayCopy
|
|
|
|
return (completeAudioArray, duration)
|