-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathtransformer.py
279 lines (242 loc) · 11.3 KB
/
transformer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
from MediaParser import Parser
from util import Log
from task import Task,TaskTypes
# Transformer is the class that has the pretty difficult task of turning what the file *is* into what the file *should be*. This is fairly easy for video, but audio has complications, i.e. what if the source has surround but no stereo? What if stereo and surround, do you want to ovewrite the stereo sound? Note that language detection isn't done right now.
"""
Options is the dictionary that determines *how* the parser is transformed.
Qualities describe a file target.
video:
- ignore: true - Just copy, we don't care about it.
- codec: h264/hevc - the codec to use
- allowhvec: true/false - Don't bother transcoding hevc.
- 10bit - Use 10 bit video encoding (helps color banding, only reccomended with x265)
- res: keep/720p/1080p/480p - the resolution to scale down to, if needed. if the video is around this, it won't be scaled to exact dimens.
- deinterlace true/false/force - Deinterlace if ffproc thinks that it's interlaced, or force it to.
- quality: 20 - the crf quality setting to use
- force: true/false - Force the video to be transcoded, even if it's already in the right codec.
- encodepreset: veryslow/slow/fast/ultrafast - ffmpeg speed settings. Slower = equivalent quality, smaller file sizes.
audio:
surround:
- keep: true/false - Keep the surround channel
stereo:
- keep: true/false - Keep the stereo channel that's already there
- create: yes - Create from surround
- ffproc_filtering - Use ffproc's filtergraph to make better stereo (less "background" noise, nightmode style filter to normalize volume for better listening with headphones or stereo speakers)
- bitrate 128k - don't go above this bitrate
- force_libfdk true/false - If this is false, the worker will change the libfdk_aac codec to aac if it does not have libfdk_aac installed. Will result in low-quality audio.
lang:
- ignore: true/false - Ignore language tags, just take the best audio track available (this may result in weird behaviour if you have descriptive audio)
- allowed: array - Allowed languages. Ignore all other audio tracks.
output: (currently unused!)
- filetype matroska/mp4 - What output file format to use
- quickstart true/false - Run a postprocessing step to enable mp4 quickstart.
"""
"""
Returns an array of transcode targets of the form:
- { type:video, index, codec, quality, scaling, deinterlacing}
- { type:audio, index, codec, bitrate, downconvert, customdownconvert}
These transcode targets can then be used to create an ffmpeg command line.
"""
defaultoptions = { "video":{"deinterlace": "yes", "allowhevc": True, "ignore":False, "codec": "h264","force":False, "encodepreset": "veryslow", "quality": "20", "res":"1080p"}, "audio":{"surround":{"keep":True},"stereo":{"keep": True,"create": True,"ffproc_filtering":True,"bitrate":"128k","force_libfdk":True}}, "format":{"filetype":"mp4"} }
TAG = "MediaTransformer"
def media_transform(parser, options):
#keep track if we do anything other than copying
tcodeVideo=False
tcodeAudio=False
# Start with the video.
# We're going to check the parser video_stream and compare it to our target.
cstream = parser.video_stream
voptions = options["video"]
codec = "copy"
if cstream["codec"] != voptions["codec"] or voptions["force"] == True:
if voptions["allowhevc"] == True and cstream["codec"] == "hevc":
Log.i(TAG, "Skipping transcode for HVEC as per override.")
else:
tcodeVideo = True
Log.i(TAG, "Transcoding video track")
codec = voptions["codec"]
else:
Log.i(TAG, "Copying video track")
deinterlace = False
if (parser.is_interlaced and voptions["deinterlace"] == "yes") or voptions["deinterlace"] == "forced":
Log.i(TAG, "Deinterlacing video track (will cause transcode!)")
tcodeVideo=True
codec = voptions["codec"]
deinterlace = True
scaleopts = False
if voptions["res"] != "keep":
dres = 0
if voptions["res"] == "1080p":
dres = 1080
elif voptions["res"] == "720p":
dres = 720
elif voptions["res"] == "480p":
dres = 480
if(cstream["height"] < dres):
scaleopts = False
elif(abs(cstream["height"] - dres) < 30):
scaleopts = False
else:
Log.i(TAG, "Scaling video (will cause transcode!)")
codec = voptions["codec"]
scaleopts = dres
bit10 = False
if "10bit" in voptions:
bit10 = voptions["10bit"]
video_build = {"type":"video", "index":cstream["index"], "codec": codec, "quality": voptions["quality"], "deinterlacing": deinterlace, "scaleopts": scaleopts, "10bit": bit10}
if options["video"]["ignore"] == True:
Log.w(TAG, "Ignoring incorrect video codec")
video_build = {"type":"video", "index":cstream["index"], "codec": "copy", "quality": "10", "deinterlacing": False, "scaleopts": False}
aoptions = options["audio"]
audio_building=[]
surround_exists = False
stereo_exists = False
#Now the hard part. Figuring out the mess of audio streams
#Find the master track. This is the highest bitrate, highest number of channels stream, which is also in the right language.
audio_master = {"channels": 0, 'language':'und'}
audio_stereo = None
ignore_language = False
valid_laguages = ["eng"]
if "lang" in aoptions:
if "ignore" in aoptions["lang"] and aoptions["lang"]["ignore"] == True:
ignore_language = True
if "allowed" in aoptions["lang"]:
valid_languages = aoptions["lang"]["allowed"]
#this feels naieve. Take a closer look at this!
for track in parser.audio_streams:
if ignore_language or ( track["language"] in valid_languages or track["language"] == "und" or track["language"] == None):
if track["channels"] > audio_master["channels"]:
audio_master = track
if track["channels"] < 6:
audio_stereo = track
stereo_exists = True
if audio_master["channels"] > 2:
surround_exists = True
#Add our audio channels.
#Use the existing surround track
if surround_exists and aoptions["surround"]["keep"] == True:
audio_building.append({"type":"audio","index":audio_master["index"], "codec": "copy","ffprocdown":False,"downconvert":False})
Log.i(TAG, "Copying surround audio")
#Use our existing stereo track.
if stereo_exists and aoptions["stereo"]["keep"] == True:
if "aac" == audio_stereo["codec"]:
Log.i(TAG, "Copying stereo audio")
audio_building.append({"type":"audio","index":audio_stereo["index"], "codec": "copy","ffprocdown":False,"downconvert":False})
else:
tcodeAudio = True
Log.i(TAG, "Transcoding existing stereo audio")
audio_building.append({"type":"audio","index":audio_stereo["index"], "codec": "aac", "bitrate": aoptions["stereo"]["bitrate"],"downconvert":False, "forcefdk":aoptions["stereo"]["force_libfdk"],"ffprocdown":False})
#Create from surround.
if surround_exists and (not stereo_exists or aoptions["stereo"]["keep"] == False) and aoptions["stereo"]["create"] == True:
Log.i(TAG, "Downmixing surround to stereo")
tcodeAudio = True
audio_building.append({"type":"audio","index":audio_master["index"], "codec": "aac", "bitrate": aoptions["stereo"]["bitrate"],"downconvert":True, "forcefdk":aoptions["stereo"]["force_libfdk"],"ffprocdown":aoptions["stereo"]["ffproc_filtering"]})
#Are we doing any transcoding?
tcode = tcodeVideo or tcodeAudio
remux = False
if not tcode and parser.file_format.find(options["format"]["filetype"]) == -1:
remux = True
audio_building.append(video_build)
return {"video": tcodeVideo, "audio": tcodeAudio, "remux": remux, "tcodeData":audio_building}
# Returns a Task object that has been mostly populated - infile and outfile still needed.
def ffmpeg_tasks_create(parser, options):
streams = media_transform(parser,options)
if streams["video"]==False and streams["audio"]==False and streams["remux"]==False:
return None
ffmpeg=[]
astreamindex = 0
for stream in streams["tcodeData"][::-1]:
#Map the stream into the output. Order will be video, stereo, surround based on media_transform function, and iterating the list backwards.
#Note that if we're using custom downconverting, then we can't map the regular channel - we need to build a filtergraph.
if stream["type"] == "audio" and stream["ffprocdown"] == True:
ffmpeg.append("-filter_complex")
ffmpeg.append("[0:"+str(stream["index"])+"]pan=stereo| FL < FL + 0.7*FC + 0.3*BL + 0.3*SL | FR < FR + 0.7*FC + 0.3*BR + 0.3*SR, dynaudnorm[a]")
ffmpeg.append("-map")
ffmpeg.append("[a]")
else:
ffmpeg.append("-map")
ffmpeg.append("0:"+str(stream["index"]))
if stream["type"] == "video":
ffmpeg.append("-c:v")
codec_to_ffmpeg = stream["codec"]
if codec_to_ffmpeg == "copy":
ffmpeg.append("copy")
elif codec_to_ffmpeg == "h264":
#Behold, the insane list of arguments needed to tune h.264 for decent compression even
ffmpeg.append("libx264")
ffmpeg.append("-crf")
ffmpeg.append(options["video"]["quality"])
ffmpeg.append("-level:v")
ffmpeg.append("4.1")
ffmpeg.append("-preset")
ffmpeg.append(options["video"]["encodepreset"])
ffmpeg.append("-bf")
ffmpeg.append("16")
ffmpeg.append("-b_strategy")
ffmpeg.append("2")
ffmpeg.append("-subq")
ffmpeg.append("10")
ffmpeg.append("-refs")
ffmpeg.append("4")
elif codec_to_ffmpeg == "hevc":
ffmpeg.append("libx265")
ffmpeg.append("-crf")
ffmpeg.append(options["video"]["quality"])
ffmpeg.append("-preset")
ffmpeg.append(options["video"]["encodepreset"])
# fix for #16. Allows Apple devices to play hevc result files.
ffmpeg.append("-tag:v")
ffmpeg.append("hvc1")
else:
Log.e(TAG, "Unknown codec selected, you're on your own!")
ffmpeg.append(stream["codec"])
if stream['10bit']:
ffmpeg.append("-pix_fmt")
ffmpeg.append("yuv420p10le")
if stream["scaleopts"] != False and stream["deinterlacing"] == True:
#Scaling and deinterlacing
ffmpeg.append("-vf")
ffmpeg.append("yadif=0:-1:0,scale=-1:"+str(stream["scaleopts"]))
ffmpeg.append("-sws_flags")
ffmpeg.append("lanczos")
elif stream["scaleopts"] == False and stream["deinterlacing"] == True:
#Just deinterlacing
ffmpeg.append("-vf")
ffmpeg.append("yadif=0:-1:0")
elif stream["scaleopts"] != False and stream["deinterlacing"] == False:
#Just scaling
ffmpeg.append("-vf")
ffmpeg.append("scale=-1:"+str(stream["scaleopts"]))
ffmpeg.append("-sws_flags")
ffmpeg.append("lanczos")
else:
#Nothing
pass
elif stream["type"] == "audio":
ffmpeg.append("-c:a:"+str(astreamindex))
codec_to_ffmpeg = stream["codec"]
if codec_to_ffmpeg == "copy":
ffmpeg.append("copy")
elif codec_to_ffmpeg == "aac":
ffmpeg.append("libfdk_aac")
ffmpeg.append("-b:a:"+str(astreamindex))
ffmpeg.append(stream["bitrate"])
else:
Log.e(TAG, "Unknown codec selected, you're on your own!")
ffmpeg.append(stream["codec"])
if stream["ffprocdown"] == False and stream["downconvert"] == True:
#Use stock downconverting, not our own pan & dynaudnorm filter.
ffmpeg.append("-ac:a:"+str(astreamindex))
ffmpeg.append("2")
astreamindex=astreamindex+1
if options["format"]["filetype"] == "mp4":
ffmpeg.append("-movflags")
ffmpeg.append("+faststart")
ffmpeg.append("-map_metadata")
ffmpeg.append("-1")
task_type = TaskTypes.REMUX
if streams["video"] == True:
task_type = TaskTypes.VIDEO
elif streams["audio"] == True:
task_type = TaskTypes.AUDIO
return Task(tasktype=task_type, command="ffmpeg", arguments=ffmpeg)