You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
278 lines
9.9 KiB
Python
278 lines
9.9 KiB
Python
import os, math, tempfile, click
|
|
|
|
|
|
from ffx.ffx_controller import FfxController
|
|
|
|
from ffx.process import executeProcess
|
|
|
|
from ffx.media_descriptor import MediaDescriptor
|
|
from ffx.track_type import TrackType
|
|
|
|
from ffx.helper import dictCache
|
|
|
|
|
|
SHORT_SUBTITLE_SEQUENCE = [{'start': 1, 'end': 2, 'text': 'yolo'},
|
|
{'start': 3, 'end': 4, 'text': 'zolo'},
|
|
{'start': 5, 'end': 6, 'text': 'golo'}]
|
|
|
|
def getTimeString(hours: float = 0.0,
|
|
minutes: float = 0.0,
|
|
seconds: float = 0.0,
|
|
millis: float = 0.0,
|
|
format: str = ''):
|
|
|
|
duration = (hours * 3600.0
|
|
+ minutes * 60.0
|
|
+ seconds
|
|
+ millis / 1000.0)
|
|
|
|
hours = math.floor(duration / 3600.0)
|
|
remaining = duration - 3600.0 * hours
|
|
|
|
minutes = math.floor(remaining / 60.0)
|
|
remaining = remaining - 60.0 * minutes
|
|
|
|
seconds = math.floor(remaining)
|
|
remaining = remaining - seconds
|
|
|
|
millis = math.floor(remaining * 1000)
|
|
|
|
if format == 'ass':
|
|
return f"{hours:01d}:{minutes:02d}:{seconds:02d}.{millis:02d}"
|
|
|
|
# srt & vtt
|
|
return f"{hours:02d}:{minutes:02d}:{seconds:02d}.{millis:03d}"
|
|
|
|
|
|
def createAssFile(entries: dict, directory = None):
|
|
|
|
# [Script Info]
|
|
# ; Script generated by FFmpeg/Lavc61.3.100
|
|
# ScriptType: v4.00+
|
|
# PlayResX: 384
|
|
# PlayResY: 288
|
|
# ScaledBorderAndShadow: yes
|
|
# YCbCr Matrix: None
|
|
#
|
|
# [V4+ Styles]
|
|
# Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
|
# Style: Default,Arial,16,&Hffffff,&Hffffff,&H0,&H0,0,0,0,0,100,100,0,0,1,1,0,2,10,10,10,1
|
|
#
|
|
# [Events]
|
|
# Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|
# Dialogue: 0,0:00:01.00,0:00:02.00,Default,,0,0,0,,yolo
|
|
# Dialogue: 0,0:00:03.00,0:00:04.00,Default,,0,0,0,,zolo
|
|
# Dialogue: 0,0:00:05.00,0:00:06.00,Default,,0,0,0,,golo
|
|
tmpFileName = tempfile.mktemp(suffix=".ass", dir = directory)
|
|
|
|
with open(tmpFileName, 'w') as tmpFile:
|
|
|
|
tmpFile.write("[Script Info]\n")
|
|
tmpFile.write("; Script generated by Ffx\n")
|
|
tmpFile.write("ScriptType: v4.00+\n")
|
|
tmpFile.write("PlayResX: 384\n")
|
|
tmpFile.write("PlayResY: 288\n")
|
|
tmpFile.write("ScaledBorderAndShadow: yes\n")
|
|
tmpFile.write("YCbCr Matrix: None\n")
|
|
tmpFile.write("\n")
|
|
tmpFile.write("[V4+ Styles]\n")
|
|
tmpFile.write("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n")
|
|
tmpFile.write("Style: Default,Arial,16,&Hffffff,&Hffffff,&H0,&H0,0,0,0,0,100,100,0,0,1,1,0,2,10,10,10,1\n")
|
|
tmpFile.write("\n")
|
|
tmpFile.write("[Events]\n")
|
|
tmpFile.write("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n")
|
|
|
|
for entryIndex in range(len(entries)):
|
|
tmpFile.write(f"Dialogue: 0,{getTimeString(seconds=entries[entryIndex]['start'], format='ass')},{getTimeString(seconds=entries[entryIndex]['end'], format='ass')},Default,,0,0,0,,{entries[entryIndex]['text']}\n")
|
|
|
|
return tmpFileName
|
|
|
|
def createSrtFile(entries: dict, directory = None):
|
|
# 1
|
|
# 00:00:00,000 --> 00:00:02,500
|
|
# Welcome to the Example Subtitle File!
|
|
#
|
|
# 2
|
|
# 00:00:03,000 --> 00:00:06,000
|
|
# This is a demonstration of SRT subtitles.
|
|
#
|
|
# 3
|
|
# 00:00:07,000 --> 00:00:10,500
|
|
# You can use SRT files to add subtitles to your videos.
|
|
|
|
tmpFileName = tempfile.mktemp(suffix=".srt", dir = directory)
|
|
|
|
with open(tmpFileName, 'w') as tmpFile:
|
|
|
|
for entryIndex in range(len(entries)):
|
|
|
|
tmpFile.write(f"{entryIndex}\n")
|
|
tmpFile.write(f"{getTimeString(seconds=entries[entryIndex]['start'])} --> {getTimeString(seconds=entries[entryIndex]['end'])}\n")
|
|
tmpFile.write(f"{entries[entryIndex]['text']}\n\n")
|
|
|
|
return tmpFileName
|
|
|
|
|
|
def createVttFile(entries: dict, directory = None):
|
|
# WEBVTT
|
|
#
|
|
# 01:20:33.050 --> 01:20:35.050
|
|
# Yolo
|
|
|
|
tmpFileName = tempfile.mktemp(suffix=".vtt", dir = directory)
|
|
|
|
with open(tmpFileName, 'w') as tmpFile:
|
|
|
|
tmpFile.write("WEBVTT\n")
|
|
|
|
for entryIndex in range(len(entries)):
|
|
|
|
tmpFile.write("\n")
|
|
tmpFile.write(f"{getTimeString(seconds=entries[entryIndex]['start'])} --> {getTimeString(seconds=entries[entryIndex]['end'])}\n")
|
|
tmpFile.write(f"{entries[entryIndex]['text']}\n")
|
|
|
|
|
|
return tmpFileName
|
|
|
|
|
|
def createMediaTestFile(mediaDescriptor: MediaDescriptor,
|
|
directory: str = '',
|
|
baseName: str = 'media',
|
|
format: str = '',
|
|
extension: str = 'mkv',
|
|
sizeX: int = 1280,
|
|
sizeY: int = 720,
|
|
rate: int = 25,
|
|
length: int = 10,
|
|
logger = None):
|
|
|
|
# subtitleFilePath = createVttFile(SHORT_SUBTITLE_SEQUENCE)
|
|
|
|
# commandTokens = FfxController.COMMAND_TOKENS
|
|
commandTokens = ['ffmpeg', '-y']
|
|
|
|
generatorCache = []
|
|
generatorTokens = []
|
|
mappingTokens = []
|
|
importTokens = []
|
|
metadataTokens = []
|
|
|
|
|
|
for mediaTagKey, mediaTagValue in mediaDescriptor.getTags().items():
|
|
metadataTokens += ['-metadata:g', f"{mediaTagKey}={mediaTagValue}"]
|
|
|
|
subIndexCounter = {}
|
|
|
|
for trackDescriptor in mediaDescriptor.getAllTrackDescriptors():
|
|
|
|
trackType = trackDescriptor.getType()
|
|
|
|
if trackType == TrackType.VIDEO:
|
|
|
|
cacheIndex, generatorCache = dictCache({'type': TrackType.VIDEO}, generatorCache)
|
|
# click.echo(f"createMediaTestFile() cache index={cacheIndex} size={len(generatorCache)}")
|
|
|
|
if cacheIndex == -1:
|
|
generatorTokens += ['-f',
|
|
'lavfi',
|
|
'-i',
|
|
f"color=size={sizeX}x{sizeY}:rate={rate}:color=black"]
|
|
|
|
sourceIndex = len(generatorCache) - 1 if cacheIndex == -1 else cacheIndex
|
|
mappingTokens += ['-map', f"{sourceIndex}:v:0"]
|
|
|
|
if not trackType in subIndexCounter.keys():
|
|
subIndexCounter[trackType] = 0
|
|
for mediaTagKey, mediaTagValue in trackDescriptor.getTags().items():
|
|
metadataTokens += [f"-metadata:s:{trackType.indicator()}:{subIndexCounter[trackType]}",
|
|
f"{mediaTagKey}={mediaTagValue}"]
|
|
subIndexCounter[trackType] += 1
|
|
|
|
if trackType == TrackType.AUDIO:
|
|
|
|
audioLayout = 'stereo'
|
|
|
|
cacheIndex, generatorCache = dictCache({'type': TrackType.AUDIO, 'layout': audioLayout}, generatorCache)
|
|
# click.echo(f"createMediaTestFile() cache index={cacheIndex} size={len(generatorCache)}")
|
|
|
|
# click.echo(f"generartorCache index={cacheIndex} len={len(generatorCache)}")
|
|
if cacheIndex == -1:
|
|
generatorTokens += ['-f',
|
|
'lavfi',
|
|
'-i',
|
|
f"anullsrc=channel_layout={audioLayout}:sample_rate=44100"]
|
|
|
|
sourceIndex = len(generatorCache) - 1 if cacheIndex == -1 else cacheIndex
|
|
mappingTokens += ['-map', f"{sourceIndex}:a:0"]
|
|
|
|
if not trackType in subIndexCounter.keys():
|
|
subIndexCounter[trackType] = 0
|
|
for mediaTagKey, mediaTagValue in trackDescriptor.getTags().items():
|
|
metadataTokens += [f"-metadata:s:{trackType.indicator()}:{subIndexCounter[trackType]}",
|
|
f"{mediaTagKey}={mediaTagValue}"]
|
|
subIndexCounter[trackType] += 1
|
|
|
|
if trackType == TrackType.SUBTITLE:
|
|
|
|
cacheIndex, generatorCache = dictCache({'type': TrackType.SUBTITLE}, generatorCache)
|
|
# click.echo(f"createMediaTestFile() cache index={cacheIndex} size={len(generatorCache)}")
|
|
|
|
if cacheIndex == -1:
|
|
importTokens = ['-i', createVttFile(SHORT_SUBTITLE_SEQUENCE, directory=directory if directory else None)]
|
|
|
|
sourceIndex = len(generatorCache) - 1 if cacheIndex == -1 else cacheIndex
|
|
mappingTokens += ['-map', f"{sourceIndex}:s:0"]
|
|
|
|
if not trackType in subIndexCounter.keys():
|
|
subIndexCounter[trackType] = 0
|
|
for mediaTagKey, mediaTagValue in trackDescriptor.getTags().items():
|
|
metadataTokens += [f"-metadata:s:{trackType.indicator()}:{subIndexCounter[trackType]}",
|
|
f"{mediaTagKey}={mediaTagValue}"]
|
|
subIndexCounter[trackType] += 1
|
|
|
|
ffxContext = {'logger': logger}
|
|
fc = FfxController(ffxContext, mediaDescriptor)
|
|
|
|
commandTokens += (generatorTokens
|
|
+ importTokens
|
|
+ mappingTokens
|
|
+ metadataTokens
|
|
+ fc.generateDispositionTokens())
|
|
|
|
|
|
commandTokens += ['-t', str(length)]
|
|
|
|
if format:
|
|
commandTokens += ['-f', format]
|
|
|
|
fileName = f"{baseName}.{extension}"
|
|
|
|
if directory:
|
|
outputPath = os.path.join(directory, fileName)
|
|
else:
|
|
outputPath = fileName
|
|
|
|
commandTokens += [outputPath]
|
|
|
|
|
|
if not logger is None:
|
|
logger.debug(f"createMediaTestFile(): Command sequence: {commandTokens}")
|
|
|
|
out, err, rc = executeProcess(commandTokens)
|
|
|
|
if not logger is None:
|
|
if out:
|
|
logger.debug(f"createMediaTestFile(): Process output: {out}")
|
|
if rc:
|
|
logger.debug(f"createMediaTestFile(): Process returned ERROR {rc} ({err})")
|
|
|
|
|
|
return outputPath
|
|
|
|
|
|
def createEmptyDirectory():
|
|
return tempfile.mkdtemp()
|
|
|
|
def createEmptyFile(suffix=None):
|
|
return tempfile.mkstemp(suffix=suffix)
|