Compare commits

...

5 Commits

3 changed files with 250 additions and 97 deletions

7
empty.idx Normal file
View File

@@ -0,0 +1,7 @@
# VobSub index file, v7 (do not modify this line!)
size: 720x480
palette: 000000, 0000ff, 00ff00, ff0000, ffff00, ff00ff, 00ffff, ffffff, 808000, 8080ff, 800080, 80ff80, 008080, ff8080, 555555, aaaaaa
langidx: 0
id: fr, index: 0
timestamp: 00:00:00:000, filepos: 000000000

0
empty.sub Normal file
View File

View File

@@ -10,6 +10,7 @@ from os import mkdir, set_inheritable
from io import BytesIO, TextIOWrapper from io import BytesIO, TextIOWrapper
import json import json
from enum import Enum, IntEnum, unique, auto from enum import Enum, IntEnum, unique, auto
import shutil
@unique @unique
class SupportedFormat(IntEnum): class SupportedFormat(IntEnum):
@@ -25,8 +26,7 @@ class SupportedFormat(IntEnum):
elif self is SupportedFormat.Matroska: elif self is SupportedFormat.Matroska:
return 'matroska,webm' return 'matroska,webm'
else: else:
return "Unsupported format" return 'Unsupported format'
def getFormat(inputFile): def getFormat(inputFile):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -75,6 +75,26 @@ def ffmpegConvert(inputFile, inputFormat, outputFile, outputFormat):
if line.startswith('out_time='): if line.startswith('out_time='):
print(line, end='') print(line, end='')
def getFramesInStream(inputFile, begin, end, streamKind, subStreamId=0):
logger = logging.getLogger(__name__)
infd = inputFile.fileno()
set_inheritable(infd, True)
with Popen(['ffprobe', '-loglevel', 'quiet', '-read_intervals', ('%s%%%s' %(begin, end)), '-show_entries', 'frame', '-select_streams', '%s:%d' % (streamKind, subStreamId), '-of', 'json', '/proc/self/fd/%d' % infd], stdout=PIPE, close_fds=False) as ffprobe:
out, _ = ffprobe.communicate()
frames = json.load(BytesIO(out))
res = []
if 'frames' in frames:
frames = frames['frames']
for frame in frames:
ts = timedelta(seconds=float(frame['pts_time']))
if begin <= ts and ts <= end:
res.append(frame)
return res
else:
logger.error('Impossible to retrieve frames inside file around [%s,%s]' % (begin, end))
return None
def getNearestIFrame(inputFile, timestamp, before=True, delta=timedelta(seconds=2)): def getNearestIFrame(inputFile, timestamp, before=True, delta=timedelta(seconds=2)):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -89,48 +109,43 @@ def getNearestIFrame(inputFile, timestamp, before=True, delta=timedelta(seconds=
logger.debug('Looking for iframe in [%s, %s]' % (tbegin, tend)) logger.debug('Looking for iframe in [%s, %s]' % (tbegin, tend))
with Popen(['ffprobe', '-loglevel', 'quiet', '-read_intervals', ('%s%%%s' %(tbegin, tend)), '-show_entries', 'frame', '-select_streams', 'v', '-of', 'json', '/proc/self/fd/%d' % infd], stdout=PIPE, close_fds=False) as ffprobe: frames = getFramesInStream(inputFile=inputFile, begin=tbegin, end=tend, streamKind='v')
out, _ = ffprobe.communicate() if frames == None:
frames = json.load(BytesIO(out)) return None
if 'frames' in frames:
frames = frames['frames'] iframes = []
iframes = [] for frame in frames:
for frame in frames: if frame['pict_type'] == 'I':
if frame['pict_type'] == 'I': iframes.append(frame)
iframes.append(frame)
found = False
found = False res = None
res = None for frame in iframes:
for frame in iframes: if before and timedelta(seconds=float(frame['pts_time'])) <= timestamp:
if before and timedelta(seconds=float(frame['pts_time'])) <= timestamp: found = True
found = True iframe = frame
iframe = frame if not before and timedelta(seconds=float(frame['pts_time'])) >= timestamp:
if not before and timedelta(seconds=float(frame['pts_time'])) >= timestamp: found = True
found = True iframe = frame
iframe = frame break
break
if found: if found:
logger.debug("Found: %s" % res) logger.debug("Found: %s" % res)
its = timedelta(seconds=float(iframe['pts_time'])) its = timedelta(seconds=float(iframe['pts_time']))
nbFrames = 0 nbFrames = 0
for frame in frames: for frame in frames:
ts = timedelta(seconds=float(frame['pts_time'])) ts = timedelta(seconds=float(frame['pts_time']))
if before: if before:
if its <= ts and ts <= timestamp: if its <= ts and ts <= timestamp:
nbFrames = nbFrames+1 nbFrames = nbFrames+1
else:
if timestamp <= ts and ts <= its:
nbFrames = nbFrames+1
else: else:
logger.error("Impossible to find I-frame around: %s" % timestamp) if timestamp <= ts and ts <= its:
nbFrames = nbFrames+1
else:
logger.error("Impossible to find I-frame around: %s" % timestamp)
return(nbFrames-1, iframe) return(nbFrames-1, iframe)
else:
logger.error('Impossible to retrieve video frames inside file around [%s,%s]' % (tbegin, tend))
return None
def extractMKVPart(inputFile, outputFile, begin, end): def extractMKVPart(inputFile, outputFile, begin, end):
inputFile.seek(0,0) inputFile.seek(0,0)
@@ -147,19 +162,146 @@ def extractPictures(inputFile, begin, nbFrames, prefix, width=640, height=480):
inputFile.seek(0,0) inputFile.seek(0,0)
infd = inputFile.fileno() infd = inputFile.fileno()
set_inheritable(infd, True) set_inheritable(infd, True)
with Popen(['ffmpeg', '-loglevel', 'quiet', '-ss', '%s'%begin, '-i', '/proc/self/fd/%d' % infd, '-s', '%dx%d'%(width, height), '-vframes', '%d'%nbFrames, '-c:v', 'ppm', '-f', 'image2', '%s-%%03d.ppm' % prefix], stdout=PIPE, close_fds=False) as ffmpeg: with Popen(['ffmpeg', '-y', '-loglevel', 'quiet', '-ss', '%s'%begin, '-i', '/proc/self/fd/%d' % infd, '-s', '%dx%d'%(width, height), '-vframes', '%d'%nbFrames, '-c:v', 'ppm', '-f', 'image2', '%s-%%03d.ppm' % prefix], stdout=PIPE, close_fds=False) as ffmpeg:
for line in TextIOWrapper(ffmpeg.stdout, encoding="utf-8"): for line in TextIOWrapper(ffmpeg.stdout, encoding="utf-8"):
print(line, end='') print(line, end='')
def extractSound(inputFile, begin, outputFile, channel=0, nbPackets=10, sampleRate=48000, nbChannels=2): def extractSound(inputFile, begin, outputFile, subChannel=0, nbPackets=0, sampleRate=48000, nbChannels=2):
inputFile.seek(0,0) inputFile.seek(0,0)
outputFile.seek(0,0)
infd = inputFile.fileno() infd = inputFile.fileno()
outfd = outputFile.fileno()
set_inheritable(infd, True) set_inheritable(infd, True)
with Popen(['ffmpeg', '-loglevel', 'quiet', '-ss', '%s'%begin, '-i', '/proc/self/fd/%d' % infd, '-frames:a:%d' % channel, '%d' % nbPackets, set_inheritable(outfd, True)
'-c:a', 'pcm_s32le', '-sample_rate', '%d' % 48000, '-channels', '%d' % 2, '-f', 's32le', '%s.pcm' % outputFile], stdout=PIPE, close_fds=False) as ffmpeg: with Popen(['ffmpeg', '-y', '-loglevel', 'quiet', '-ss', '%s'%begin, '-i', '/proc/self/fd/%d' % infd, '-frames:a:%d' % subChannel, '%d' % nbPackets,
'-c:a', 'pcm_s32le', '-sample_rate', '%d' % sampleRate, '-channels', '%d' % nbChannels, '-f', 's32le', '/proc/self/fd/%d' % outfd], stdout=PIPE, close_fds=False) as ffmpeg:
for line in TextIOWrapper(ffmpeg.stdout, encoding="utf-8"): for line in TextIOWrapper(ffmpeg.stdout, encoding="utf-8"):
print(line, end='') print(line, end='')
def extractAllStreams(inputFile, begin, end, streams, filesPrefix, nbFrames, width, height):
logger = logging.getLogger(__name__)
# encoderParams = [ 'ffmpeg', '-y', '-loglevel', 'quiet' ]
encoderParams = [ 'ffmpeg', '-y' ]
inputParams = []
codecsParams = []
if begin < end:
videoID=0
audioID=0
subTitleID=0
audioFiles = {}
for stream in streams:
if stream['codec_type'] == 'video':
print("Extracting video stream: %s" % stream)
frameRate = stream['r_frame_rate']
pattern = re.compile('^(?P<numerator>[0-9]+)/(?P<denominator>[0-9]+)$')
m = pattern.match(frameRate)
print(m)
if m != None:
frameRate = float(m['numerator']) / float(m['denominator'])
print(frameRate)
sar = stream['sample_aspect_ratio']
dar = stream['display_aspect_ratio']
pixelFormat = stream['pix_fmt']
colorRange = stream['color_range']
colorSpace =stream['color_space']
colorTransfer = stream['color_transfer']
colorPrimaries = stream['color_primaries']
codec = stream['codec_name']
extractPictures(inputFile=inputFile, begin=begin, nbFrames=nbFrames, prefix="%s-%d" % (filesPrefix, videoID), width=width, height=height)
inputParams.extend(['-framerate', '%f'%frameRate, '-i', '%s-%d-%%03d.ppm' % (filesPrefix, videoID)])
codecsParams.extend(['-c:v:%d' % videoID, codec, '-pix_fmt', pixelFormat, '-colorspace:v:%d' % videoID, colorSpace, '-color_primaries:v:%d' % videoID, colorPrimaries,
'-color_trc:v:%d' % videoID, colorTransfer, '-color_range:v:%d' % videoID, colorRange])
videoID=videoID+1
elif stream['codec_type'] == 'audio':
print("Extracting audio stream: %s" % stream)
sampleRate = int(stream['sample_rate'])
nbChannels = int(stream['channels'])
bitRate = int(stream['bit_rate'])
codec = stream['codec_name']
if 'tags' in stream:
if 'language' in stream['tags']:
codecsParams.extend(['-metadata:s:a:%d' % audioID, 'language=%s' % stream['tags']['language']])
packets = getFramesInStream(inputFile=inputFile, begin=begin, end=end, streamKind='a', subStreamId=audioID)
nbPackets = len(packets)
print("Found %d packets to be extracted from audio track." % nbPackets)
audioFiles[audioID] = open('%s-%d.pcm' % (filesPrefix,audioID), 'w')
# TODO: test if successfully openened
extractSound(inputFile=inputFile, begin=begin, nbPackets=nbPackets, outputFile=audioFiles[audioID], sampleRate=sampleRate, nbChannels=nbChannels)
inputParams.extend(['-f', 's32le', '-ar', '%d'%sampleRate, '-ac', '%d'%nbChannels, '-i', '/proc/self/fd/%d' % audioFiles[audioID].fileno()])
codecsParams.extend(['-c:a:%d' % audioID, codec, '-b:a:%d' % audioID, '%d' % bitRate])
audioID=audioID+1
elif stream['codec_type'] == 'subtitle':
print("Extracting a subtitle stream: %s" % stream)
codec = stream['codec_name']
inputParams.extend(['-i', './empty.idx'])
if 'tags' in stream:
if 'language' in stream['tags']:
codecsParams.extend(['-metadata:s:s:%d' % subTitleID, 'language=%s' % stream['tags']['language']])
codecsParams.extend(['-c:s:%d' % subTitleID, 'copy'])
subTitleID=subTitleID+1
else:
logger.info("Unknown stream type: %s" % stream['codec_type'])
# Example:
# ffmpeg -framerate 25.85 -i image-%02d.ppm -f s32le -ar 48000 -ac 2 -i ./audio-1.pcm -c:a eac3 -b:a 128k -c:v libx264 -crf 25.85 -vf "scale=1920:1080,format=yuv420p" -colorspace:v "bt709" -color_primaries:v "bt709" -color_trc:v "bt709" -color_range:v "tv" -top 1 -flags:v +ilme+ildct -bsf:v h264_mp4toannexb,dump_extra=keyframe -metadata MAJOR_BRAND=isom -metadata MINOR_VERSION=512 -movflags +faststart cut-1.mkv
# Create a new MKV movie with all streams that have been extracted.
encoderParams.extend(inputParams)
for index in range(0,videoID+audioID+subTitleID):
encoderParams.extend(['-map', '%d' % index])
encoderParams.extend(codecsParams)
output = open('%s.mkv' % filesPrefix,'w')
outfd = output.fileno()
set_inheritable(outfd, True)
encoderParams.extend(['-top', '1', '-bsf:v', 'h264_mp4toannexb,dump_extra=keyframe', '-f', 'matroska', '/proc/self/fd/%d' % outfd])
print(encoderParams)
with Popen(encoderParams, stdout=PIPE, close_fds=False) as ffmpeg:
for line in TextIOWrapper(ffmpeg.stdout, encoding="utf-8"):
print(line, end='')
return output
else:
# Nothing to be done. We are already at a i-frame boundary.
return None
# Merge a list of mkv files passed as input, and produce a new MKV as output
def mergeMKVs(inputs, outputName):
fds = []
out = open(outputName, 'w')
# TODO: Check success or failure
outfd = out.fileno()
fds.append(outfd)
set_inheritable(outfd, True)
mergeParams = ['mkvmerge']
first = True
for mkv in inputs:
if mkv !=None:
fd = mkv.fileno()
fds.append(fd)
set_inheritable(fd, True)
if first:
mergeParams.append('/proc/self/fd/%d' % fd)
first = False
else:
mergeParams.append('+/proc/self/fd/%d' % fd)
mergeParams.extend(['-o', '/proc/self/fd/%d' % outfd])
# We merge all files.
with Popen(mergeParams, stdout=PIPE, close_fds=False) as mkvmerge:
for line in TextIOWrapper(mkvmerge.stdout, encoding="utf-8"):
print(line, end='')
for fd in fds:
set_inheritable(fd, False)
return out
def parseTimeInterval(interval): def parseTimeInterval(interval):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -335,72 +477,76 @@ def main():
# Pour chaque portion # Pour chaque portion
partnum = 0 partnum = 0
mkvparts = []
checks = []
pos = timedelta()
for ts1, ts2 in parts: for ts1, ts2 in parts:
# Trouver l'estampille de la trame 'I' la plus proche (mais postérieure) au début de la portion.
# Trouver l'estampille de la trame 'I' la plus proche (mais antérieure) à la fin de la portion.
# On a alors
# debut ----- trame --------- trame --------- fin.
# 'B/P' 'B/P'* 'I' 'I' 'B/P'* 'B/P'
# Si la trame de début est déjà 'I', il n'y a rien à faire (idem pour la fin).
# Sinon on extrait les trames 'B' ou 'P' depuis le début jusqu'à la trame 'I' non incluse
partnum = partnum + 1 partnum = partnum + 1
preFrame = getNearestIFrame(mkv, ts1, before=False) headFrames = getNearestIFrame(mkv, ts1, before=False)
if preFrame == None: if headFrames == None:
exit(-1) exit(-1)
postFrame = getNearestIFrame(mkv, ts2, before=True) tailFrames = getNearestIFrame(mkv, ts2, before=True)
if postFrame == None: if tailFrames == None:
exit(-1) exit(-1)
nbPreFrame, preIFrame = preFrame nbHeadFrames, headIFrame = headFrames
nbPostFrame, postIFrame = postFrame nbTailFrames, tailIFrame = tailFrames
print("Found pre I-frame and %d frames between: %s" % (nbPreFrame, preIFrame)) print("Found head I-frame and %d frames between: %s" % (nbHeadFrames, headIFrame))
print("Found I-frame and %d frames between: %s" % (nbPostFrame, postIFrame)) print("Found I-frame and %d frames between: %s" % (nbTailFrames, tailIFrame))
preIFrameTS = timedelta(seconds=float(preIFrame['pts_time'])) headIFrameTS = timedelta(seconds=float(headIFrame['pts_time']))
postIFrameTS = timedelta(seconds=float(postIFrame['pts_time'])) tailIFrameTS = timedelta(seconds=float(tailIFrame['pts_time']))
if ts1 < preIFrameTS: checks.append(pos+headIFrameTS-ts1)
for stream in streams:
if stream['codec_type'] == 'video':
extractPictures(inputFile=mkv, begin=ts1, nbFrames=nbPreFrame, prefix="pre-part-%d" % partnum, width=width, height=height)
elif stream['codec_type'] == 'audio':
print("Extracting audio stream: %s" % stream)
sampleRate = stream['sample_rate']
nbChannel = stream['channels']
extractSound(inputFile=mkv, begin=ts1, nbPackets=nbPreFrame, outputFile="pre-part-%d" % partnum, sampleRate=sampleRate, nbChannels=nbChannels)
else:
pass
else:
# Nothing to do
pass
if postIFrameTS < ts2: subparts = []
for stream in streams:
if stream['codec_type'] == 'video': if nbHeadFrames > 0:
extractPictures(inputFile=mkv, begin=postIFrameTS, nbFrames=nbPostFrame, prefix="post-part-%d" % partnum, width=width, height=height) head = extractAllStreams(inputFile=mkv, begin=ts1, end=headIFrameTS, nbFrames=nbHeadFrames, filesPrefix='part-%d-head' % (partnum), streams=streams, width=width, height=height)
elif stream['codec_type'] == 'audio': subparts.append(head)
print("Extracting audio stream: %s" % stream)
sampleRate = stream['sample_rate'] if nbTailFrames > 0:
nbChannel = stream['channels'] tail = extractAllStreams(inputFile=mkv, begin=tailIFrameTS, end=ts2, nbFrames=nbTailFrames, filesPrefix='part-%d-tail' % (partnum), streams=streams, width=width, height=height)
# TODO: how many packets should be dumped ...
# TODO: take into account multiple sound tracks ...
extractSound(inputFile=mkv, begin=postIFrameTS, nbPackets=nbPostFrame, outputFile="post-part-%d" % partnum, sampleRate=sampleRate, nbChannels=nbChannels)
else:
pass
else:
# Nothing to do !
pass
# Creating MKV file that corresponds to current part between I-frames # Creating MKV file that corresponds to current part between I-frames
with open('part-%d.mkv' % partnum, 'w') as partmkv: internal = open('part-%d-internal.mkv' % partnum, 'w')
extractMKVPart(inputFile=mkv, outputFile=partmkv, begin=preIFrameTS, end=postIFrameTS) # TODO: test if failure
extractMKVPart(inputFile=mkv, outputFile=internal, begin=headIFrameTS, end=tailIFrameTS)
subparts.append(internal)
# Trouver l'estampille de la trame 'I' la plus proche (mais postérieure) au début de la portion. if nbTailFrames > 0:
# Trouver l'estampille de la trame 'I' la plus proche (mais antérieure) à la fin de la portion. subparts.append(tail)
# On a alors
# debut ----- trame --------- trame --------- fin. part = mergeMKVs(inputs=subparts, outputName="part-%d.mkv" % partnum)
# 'B/P' 'B/P'* 'I' 'I' 'B/P'* 'B/P' mkvparts.append(part)
# Si la trame de début est déjà 'I', il n'y a rien à faire (idem pour la fin).
# Sinon on extrait les trames 'B' ou 'P' depuis le début jusqu'à la trame 'I' non incluse pos = pos+tailIFrameTS-ts1
# Fabriquer une courte vidéo au format MKV reprenant les mêmes codecs que la vidéo originale avec les fichiers extraits précedemment.
# We need to check the end also
checks.append(pos)
nbParts = len(mkvparts)
if nbParts > 1:
mergeMKVs(inputs=mkvparts, outputName=args.outputFile)
elif nbParts == 1:
print("A single part")
else:
print("Nothing produced !")
for c in checks:
logger.info("Please check cut smoothness at: %s" % c)
# Appeler mkvmerge
if __name__ == "__main__": if __name__ == "__main__":
main() main()