-
Notifications
You must be signed in to change notification settings - Fork 0
/
qsvAv1
executable file
·370 lines (337 loc) · 15.7 KB
/
qsvAv1
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
#!/bin/bash
# Debug output
#set -ex
###########################
# Assumes jellyfin-ffmpeg is installed and symlinked in /usr/bin.
# Most functions may work with vanilla ffmpeg, but may require additional Intel drivers.
# Removing Dolby Vision function requires jellyfin-ffmpeg 7.0.2-4 and newer.
# Must have jq and bc installed.
# User running this script must be in render group. Requires user logout if added to group.
##########################
# Variable initialization. Work-in-progress initialization and cleanup.
INPUTFILE=""
CHAPTERSEPISODE=0
ANIME=0
RESCALE=0
FILTERS=""
CHAPTERFILES=""
FINALAUDIO=""
SELECTEDAUDIO=""
SELECTEDSUBS=""
AUDIOCOMMAND=""
SUBCOMMAND=""
# Reads input paramamters
while getopts i:c:d:a:r:f:s:e:o: option; do
case $option in
i) INPUTFILE=${OPTARG};;
c) CHAPTERSEPISODE=${OPTARG};;
d) DV=${OPTARG};;
a) ANIME=${OPTARG};;
r) RESCALE=${OPTARG};;
f) FOSS=${OPTARG};;
s) SEASONNUM=${OPTARG};;
e) EPISODESTART=${OPTARG};;
o) CONVERTAUDIO=${OPTARG};;
\?) echo "Invalid option"
exit;;
esac
done
mkdir WIP
# Checks for required input file name
if [ -z "$INPUTFILE" ]; then
echo "No input file was provided."
exit 1
fi
# Get ffprobe info to be used by other functions.
AUDIOFFPROBE=$(ffprobe -loglevel error -i "$INPUTFILE" -select_streams a -show_entries stream -v quiet -of json)
SUBFFPROBE=$(ffprobe -loglevel error -i "$INPUTFILE" -select_streams s -show_entries stream -v quiet -of json)
EMPTYAUDIOJSON=$(jq '[.streams[] | select(.tags.language == ("eng") or .tags.language == ("jpn"))]' <<< "$AUDIOFFPROBE" | jq -e '. | length == 0')
# Checks for subtitle track or untagged subtitle tracks.
EMPTYSUBJSON=$(jq -e '.streams | length == 0' <<< "$SUBFFPROBE")
UNTAGGEDSUBJSON=$(jq '[.streams[] | select(.tags.language == ("eng") or .tags.language == ("jpn"))]' <<< "$SUBFFPROBE" | jq -e '. | length == 0')
ISHIGH10=$(ffprobe -loglevel error -i "$INPUTFILE" -select_streams v -show_entries stream -v quiet -of json | jq '.streams[] | select(.profile == ("High 10")) | length != 0')
# Uses Intel QSV to encode the original AV1. Maps the audio tracks for the selected languages, and converts to them to OPUS.
# Includes attachments if present. If splitting out episodes from a MKV that came off the disc as a single file, it will loop through for each episode.
# If using jellyfin-ffmpeg7.0.2-4 and newer, DV 7.6 and 8 will automatically be stripped.
av1Encode () {
echo -e "$CHAPTERFILES" | while read file; do
inputFile="$file"
local audioCommand=$(audioMapping)
local subCommand=$(subMapping)
ext=$(echo $inputFile | awk -F '.' '{print $NF}')
if [ $CHAPTERSEPISODE -eq 0 ]; then
outputFile=$(echo $inputFile | awk -F '/' '{print $NF}' | sed "s/.$ext/-all.mkv/g")
else
outputFile=$(echo $inputFile | awk -F '/' '{print $NF}')
fi
decodeAccel=""
fpsMode=""
if [[ "$ISHIGH10" == "true" ]]; then
fpsMode="-fps_mode passthrough"
else
decodeAccel="-hwaccel qsv"
fi
ffmpeg \
-y \
-init_hw_device vaapi=va:,driver=iHD,kernel_driver=i915 \
-hide_banner \
-stats \
-fflags +genpts+igndts \
-loglevel error \
-init_hw_device qsv=qs@va \
-filter_hw_device qs \
$decodeAccel \
-hwaccel_output_format qsv \
-extra_hw_frames 40 \
-i "$inputFile" \
-map_chapters 0 \
-map 0:v:0 \
-c:v av1_qsv \
-q:v 22 \
-look_ahead_depth 40 \
-preset veryslow \
-vf vpp_qsv="$FILTERS" \
$audioCommand \
$subCommand \
-map 0:t? \
$fpsMode \
-disposition:s 0 \
-low_delay_brc 1 \
-extbrc 1 \
-adaptive_i 1 \
-adaptive_b 1 \
-b_strategy 1 \
-bf 39 \
-max_muxing_queue_size 2048 \
-max_interleave_delta 0 \
-avoid_negative_ts disabled \
"$outputFile" < /dev/null
done
}
#-hwaccel qsv \
# Selects English and Japanese audio tracks for processing. If lossless tracks are present for a language, lossy tracks for that language are ignored.
audioMapping () {
if [ $ANIME -eq 1 ]; then
# checks if audio.json is empty due to untagged streams
if [ "${EMPTYAUDIOJSON}" = 'true' ]; then
SELECTEDAUDIO=$(jq '[.streams[]]' <<< "$AUDIOFFPROBE")
fi
# if lossless tracks are present, ignore lossy tracks
if [ "${EMPTYAUDIOJSON}" = 'true' ]; then
FINALAUDIO=$(jq -r '.[] | .index' <<< "$SELECTEDAUDIO")
else
lossless=$(jq -r '.[] | select(.tags.language == ("eng"))' <<< "$SELECTEDAUDIO" | egrep -i "dts-hd ma|truehd" | head -1)
if [ ! -z "$lossless" ]; then
codec=$(echo $lossless | awk -F ': ' '{print $2}' | sed 's/,//g' | sed 's/\"//g')
if [ ! -z "`echo $codec | grep -i dts`" ]; then
FINALAUDIO=$(jq -r '.[] | select((.profile == ("DTS-HD MA") or .profile == ("DTS-HD MA + DTS:X")) and .tags.language == ("eng")) | .index' <<< "$SELECTEDAUDIO")
elif [ ! -z "`echo $codec | grep -i truehd`" ]; then
FINALAUDIO=$(jq -r '.[] | select(.codec_name == ("truehd") and .tags.language == ("eng")) | .index' <<< "$SELECTEDAUDIO")
fi
else
FINALAUDIO=$(jq -r '.[] | select(.tags.language == ("eng")) | .index' <<< "$SELECTEDAUDIO")
fi
lossless=$(jq -r '.[] | select(.tags.language == ("jpn"))' <<< "$SELECTEDAUDIO" | egrep -i "dts-hd ma|truehd" | head -1)
if [ ! -z "$lossless" ]; then
codec=$(echo $lossless | awk -F ': ' '{print $2}' | sed 's/,//g' | sed 's/\"//g')
if [ ! -z "`echo $codec | grep -i dts`" ]; then
FINALAUDIO="${FINALAUDIO} $(jq -r '.[] | select((.profile == ("DTS-HD MA") or .profile == ("DTS-HD MA + DTS:X")) and .tags.language == ("jpn")) | .index' <<< "$SELECTEDAUDIO")"
elif [ ! -z "`echo $codec | grep -i truehd`" ]; then
FINALAUDIO="${FINALAUDIO} $(jq -r '.[] | select(.codec_name == ("truehd") and .tags.language == ("jpn")) | .index' <<< "$SELECTEDAUDIO")"
fi
else
FINALAUDIO="${FINALAUDIO} $(jq -r '.[] | select(.tags.language == ("jpn")) | .index' <<< "$SELECTEDAUDIO")"
fi
fi
else
# checks if audio.json is empty due to untagged streams
if [ "${EMPTYAUDIOJSON}" = 'true' ]; then
SELECTEDAUDIO=$(jq '[.streams[]]' <<< "$AUDIOFFPROBE")
FINALAUDIO=$(jq -r '.[] | .index' <<< "$SELECTEDAUDIO")
else
lossless=$(egrep -i "dts-hd ma|truehd" <<< "$SELECTEDAUDIO" | head -1)
if [ ! -z "$lossless" ]; then
codec=$(echo $lossless | awk -F ': ' '{print $2}' | sed 's/,//g' | sed 's/\"//g')
if [ ! -z "`echo $codec | grep -i dts`" ]; then
FINALAUDIO=$(jq -r '.[] | select(.profile == ("DTS-HD MA") or .profile == ("DTS-HD MA + DTS:X")) | .index' <<< "$SELECTEDAUDIO")
elif [ ! -z "`echo $codec | grep -i truehd`" ]; then
FINALAUDIO=$(jq -r '.[] | select(.codec_name == ("truehd")) | .index' <<< "$SELECTEDAUDIO")
fi
else
FINALAUDIO=$(jq -r '.[] | select(.tags.language == ("eng")) | .index' <<< "$SELECTEDAUDIO")
fi
fi
fi
skipConversion=$(jq -r '.[] | select(.codec_name == ("aac") or .codec_name == ("mp3") or .codec_name == ("flac") or .codec_name ==("opus") or .codec_name ==("vorbis")) | .codec_name' <<< "$SELECTEDAUDIO")
if [[ ! -z "$skipConversion" ]]; then
#echo "Audio codec already FOSS or widely compatible."
AUDIOCOMMAND="-c:a copy"
firstTrack=$(jq -r '.streams[0] | .index' <<< "$AUDIOFFPROBE" | bc)
for rawIndex in $FINALAUDIO; do
index=$(awk "BEGIN {print $rawIndex - $firstTrack}" | bc)
AUDIOCOMMAND="$AUDIOCOMMAND -map 0:a:${index}"
done
else
AUDIOCOMMAND="-c:a libopus -vbr on -compression_level 10 -application audio"
firstTrack=$(jq -r '.streams[0] | .index' <<< "$AUDIOFFPROBE" | bc)
for rawIndex in $FINALAUDIO; do
index=$(awk "BEGIN {print $rawIndex - $firstTrack}" | bc)
channels=$(jq --arg l $index '.streams[$l|fromjson].channels' <<< "$AUDIOFFPROBE")
if [ $channels -gt 2 -a $channels -lt 6 ]; then
AUDIOCOMMAND="$AUDIOCOMMAND -mapping_family 1 -ac ${channels} -map 0:a:${index}"
else
AUDIOCOMMAND="$AUDIOCOMMAND -map 0:a:${index}"
fi
done
fi
echo "$AUDIOCOMMAND"
}
# Selects and maps English and Japanese subtitle tracks.
subMapping () {
firstTrack=$(jq -r '.streams[0] | .index' <<< "$SUBFFPROBE" | bc)
# Checks for no subtitle track. Common for older fansubs with burned in subtitles.
if [ "${EMPTYSUBJSON}" = 'true' ]; then
#echo "No subtitle track"
SUBCOMMAND="-sn"
else
# If audio language tags are not present, it is safe to assume subtitle language tags aren't either
if [ "${EMPTYAUDIOJSON}" = 'true' ]; then
SUBCOMMAND="-map 0:s -c:s copy"
else
SUBCOMMAND="-c:s copy"
while read rawIndex; do
index=$(awk "BEGIN {print $rawIndex - $firstTrack}" | bc)
SUBCOMMAND="$SUBCOMMAND -map 0:s:$index"
done <<< "$(jq '.[].index' <<< "$SELECTEDSUBS")"
fi
fi
echo "$SUBCOMMAND"
}
# Extracts chapter data from input file.
splitEpisodes () {
chapters=$1
# Probes videos for chapter information and prints to text file
chapterProbe=$(ffprobe -loglevel error -i "$INPUTFILE" -print_format json -show_chapters -loglevel error)
# Parses json file for the start time of each chapter. Also counts number of chapters
rawChapters=$(echo $chapterProbe | jq .chapters[].start_time | wc -l)
# ffmpeg chapters start at zero. Subtracting 1 to align with last chapter.
totalChapters=$(echo $(($rawChapters - 1)) | bc)
if [ $chapters -ne 0 ]; then
i=0
STARTSTOP=""
# Builds a colon separated list of chapter start:end
while [ $i -le $totalChapters ]; do
first=$i
last=$(echo $(($i + $CHAPTERSEPISODE - 1)) | bc)
if [ $last -gt $totalChapters ]; then
last=${totalChapters}
fi
STARTSTOP=$(echo "$STARTSTOP $first:$last")
startTime=$(jq .chapters[$first].start_time <<< "$chapterProbe" | sed 's/\"//g')
endTime=$(jq .chapters[$last].end_time <<< "$chapterProbe" | sed 's/\"//g')
ext=$(echo $INPUTFILE | awk -F '.' '{print $NF}')
outputFile=$(echo $INPUTFILE | sed "s/.$ext//g")
chapterSelect="-ss $startTime -to $endTime"
segmentFile=$(echo "WIP/${first}-${last}-${outputFile}.mkv")
ffmpeg -y -fflags +genpts+igndts -i "$INPUTFILE" -map 0 -codec copy $chapterSelect -max_muxing_queue_size 2048 -max_interleave_delta 0 -avoid_negative_ts disabled "$segmentFile" < /dev/null
CHAPTERFILES="${CHAPTERFILES}\n${segmentFile}"
i=$(echo $(($i + $CHAPTERSEPISODE)) | bc)
done
else
CHAPTERFILES=$(echo "$INPUTFILE")
fi
}
# Generates video filters for vpp_qsv
generateFilters () {
# ffprobe to determine if interlaced. If yes, set yadif deinterlace filter.
rawParams=$(ffprobe -loglevel error -i "$INPUTFILE" -select_streams v:0 -show_entries stream=width,height,field_order,pix_fmt,sample_aspect_ratio)
interlaced=$(echo $rawParams | grep -o 'field_order=[a-z]\+' | awk -F '=' '{print $2}')
inputBitDepth=$(echo $rawParams | grep -o 'pix_fmt=[a-z,0-9]\+' | awk -F '=' '{print $2}')
sar=$(echo $rawParams | grep -o 'sample_aspect_ratio=[0-9]*:[0-9]\+' | awk -F '=' '{print $2}')
width=$(echo $rawParams | grep -o 'width=[0-9]\+' | awk -F '=' '{print $2}')
height=$(echo $rawParams | grep -o 'height=[0-9]\+' | awk -F '=' '{print $2}')
deinterlaced=""
# Checks for various interface types. Uses QSV "advanced" de-interlace option if interlaced.
case $interlaced in
tt) deinterlaced="deinterlace=2";;
bb) deinterlaced="deinterlace=2";;
tb) deinterlaced="deinterlace=2";;
bt) deinterlaced="deinterlace=2";;
*) deinterlaced="deinterlace=0";;
esac
# ffmpeg to detect black bars for cropping.
rawCrop=$(ffmpeg -ss 100 -to 150 -i "$INPUTFILE" -vf cropdetect=mode=black,metadata=mode=print -f null - 2>&1 | awk '/crop=/ {a=$NF} END{print a}' | awk -F '=' '{print $2}')
w=$(echo $rawCrop | awk -F ":" '{print $1}')
h=$(echo $rawCrop | awk -F ":" '{print $2}')
x=$(echo $rawCrop | awk -F ":" '{print $3}')
y=$(echo $rawCrop | awk -F ":" '{print $4}')
inputFormat=""
if [[ "$inputBitDepth" == *"p10le"* ]] || [[ "$inputBitDepth" == *"p12le"* ]]; then
inputFormat="p010le"
else
inputFormat="nv12"
fi
# RESCALE=1 forces 16:9 ratio. Some poorly mastered DVDs will have a SAR and DAR that don't result
# in the ratio equaling 16:9. RESCALE=2 forces 4:3 ratio. Else dynamically determine DAR and set SAR to 1:1.
if [ $RESCALE -eq 1 ]; then
h_float=$(awk "BEGIN {print $w / 1.777777778}" | bc)
h_scale=$(printf "%.0f" $h_float)
FILTERS="${deinterlaced}:cw=$w:ch=$h:cx=$x:cy=$y:async_depth=4,hwdownload,format=${inputFormat},setsar=1:1,format=${inputFormat},hwupload,scale_qsv=w=$w:h=$h_scale:format=p010le"
elif [ $RESCALE -eq 2 ]; then
w_float=$(awk "BEGIN {print $w / 1.125}" | bc)
w_scale=$(printf "%.0f" $w_float)
FILTERS="${deinterlaced}:cw=$w:ch=$h:cx=$x:cy=$y:async_depth=4,hwdownload,format=${inputFormat},setsar=1:1,format=${inputFormat},hwupload,scale_qsv=w=$w_scale:h=$h:format=p010le"
else
if [[ "$sar" != "1:1" ]]; then
sarRatio1=$(echo $sar | awk -F ':' '{print $1}')
sarRatio2=$(echo $sar | awk -F ':' '{print $2}')
sarRatio=$(awk "BEGIN {print $sarRatio1 / $sarRatio2}")
darRatio=$(awk "BEGIN {print $width / $height}")
ratio=$(awk "BEGIN {print $darRatio * $sarRatio}")
h_float=$(awk "BEGIN {print $w / $ratio}" | bc)
h_scale=$(printf "%.0f" $h_float)
FILTERS="${deinterlaced}:cw=$w:ch=$h:cx=$x:cy=$y:async_depth=4,hwdownload,format=${inputFormat},setsar=1:1,format=${inputFormat},hwupload,scale_qsv=w=$w:h=$h_scale:format=p010le"
else
FILTERS="${deinterlaced}:cw=$w:ch=$h:cx=$x:cy=$y:async_depth=4:format=p010le"
fi
fi
}
# Creates lanuage English selection parameters for audio and subtitles. If ANIME option is enabled, also selects Japanese tracks.
if [ $ANIME -eq 1 ]; then
echo "Selecting English and Japanese audio/subtitle tracks."
SELECTEDAUDIO=$(jq -r '[.streams[] | select(.tags.language == ("eng") or .tags.language == ("jpn"))]' <<< "$AUDIOFFPROBE")
if [ "${UNTAGGEDSUBJSON}" = 'true' ]; then
if [ "${EMPTYSUBJSON}" = 'true' ]; then
echo "No subtitle track"
else
SELECTEDSUBS=$(jq -r '[.streams[]]' <<< "$SUBFFPROBE")
fi
else
SELECTEDSUBS=$(jq -r '[.streams[] | select(.tags.language == ("eng"))]' <<< "$SUBFFPROBE")
fi
else
echo "Selecting English audio/subtitle tracks."
SELECTEDAUDIO=$(jq -r '[.streams[] | select(.tags.language == ("eng"))]' <<< "$AUDIOFFPROBE")
if [ "${UNTAGGEDSUBJSON}" = 'true' ]; then
if [ "${EMPTYSUBJSON}" = 'true' ]; then
echo "No subtitle track"
else
SELECTEDSUBS=$(jq -r '[.streams[]]' <<< "$SUBFFPROBE")
fi
else
SELECTEDSUBS=$(jq -r '[.streams[] | select(.tags.language == ("eng"))]' <<< "$SUBFFPROBE")
fi
fi
# If the -c option is used to provide the number of chapters per episode, in instances where a rip has all episodes in one MKV, this will call
# the function. Otherwise the entire file will be processed.
echo "Generating chapter start/stop times."
splitEpisodes $CHAPTERSEPISODE
# Analyzes video, generates crop, deinterlace, video quality, rescaling parameters for insertion in vpp_qsv filter.
echo "Generating video filter parameters."
generateFilters
# If the -f 1 option is used, all audio tracks will be converted to OPUS.
# If a lossless Dolby/DTS codec exists for English and/or Japanese, the lossy tracks
# will be discarded in favor of converting only the lossless track.
av1Encode
# WIP cleanup
rm -rf WIP/