1+ from datetime import datetime
2+ import json
3+ import os
4+ from pprint import pprint
5+ import shlex
6+ import signal
7+ import subprocess
8+ import sys
9+
10+ class Session ():
11+ class Settings :
12+ class RF :
13+ SD = 21
14+ HD = 22
15+ FHD = 23
16+ UHD = 26
17+
18+ class ENCOPTS :
19+ SD = "ctu=32:qg-size=16"
20+ HD = "ctu=32:qg-size=32"
21+ FHD = "ctu=64:qg-size=64"
22+ UHD = "ctu=64:qg-size=64"
23+
24+ def __init__ (self , file , args ):
25+ signal .signal (signal .SIGINT , self .signal_handler )
26+
27+ self .args = args
28+
29+ # Get source file metadata
30+ cmd = "ffprobe -v quiet -print_format json -show_streams " + file
31+ metadata = subprocess .check_output (shlex .split (cmd )).decode ("utf-8" )
32+ metadata = json .loads (metadata )["streams" ][0 ]
33+
34+ # Populate metadata-based attributes
35+ self .path = {"source" : file }
36+ self .source = {"height" : int (metadata ["height" ]), "width" : int (metadata ["width" ]), "duration" : float (metadata ["duration" ]), "filename" : os .path .splitext (os .path .relpath (self .path ["source" ], "source" ))[0 ], "filesize" : os .path .getsize (self .path ["source" ]), "bitrate" : int (metadata ["bit_rate" ]), "frames" : int (metadata ["nb_frames" ]), "codec" : metadata ["codec_name" ]}
37+ height = self .source ["height" ]
38+ if height < 720 :
39+ resolution = "SD"
40+ elif 720 <= height < 1080 :
41+ resolution = "HD"
42+ elif 1080 <= height < 2160 :
43+ resolution = "FHD"
44+ elif 2160 <= height :
45+ resolution = "UHD"
46+ self .source ["resolution" ] = resolution
47+
48+ # Create empty attributes for dynamic session options
49+ self .encoder_quality = None
50+ self .encoder_preset = None
51+ self .preset_name = None
52+ self .encoder_options = None
53+
54+ # Construct session options and parameters
55+ self .map_options ()
56+ self .file_decorator = "_RF" + str (self .encoder_quality )
57+ self .file_decorator += "_{preset}" .format (preset = self .encoder_preset .capitalize ())
58+ if self .args .baseline :
59+ self .file_decorator += "_Baseline"
60+ elif self .args .best :
61+ self .file_decorator += "_Best"
62+ if self .args .small :
63+ self .file_decorator += "_Small"
64+ self .path ["output" ] = "hevc/" + self .source ["filename" ] + self .file_decorator + ".mp4"
65+ self .path ["log" ] = "performance/" + self .source ["filename" ] + self .file_decorator + ".log"
66+
67+ # Verify no attributes are None
68+ self .validate ()
69+
70+ # Build HandBrakeCLI command
71+ self .command = "HandBrakeCLI --encoder-preset {encoder_preset} --preset-import-file presets.json --preset {preset_name} --quality {quality} --encopts {encopts} --input {source_path} --output {output_path}" .format (encoder_preset = self .encoder_preset , preset_name = self .preset_name , quality = str (self .encoder_quality ), encopts = self .encoder_options , source_path = self .path ["source" ], output_path = self .path ["output" ])
72+
73+ def signal_handler (self , sig , frame ):
74+ """ Delete output file if ctrl+c is caught, since file will be corrupt
75+ """
76+ if hasattr (self , "job" ):
77+ self .job .terminate ()
78+ self .cleanup ()
79+ sys .exit ("\n \n {date}: Caught ctrl+c, aborting.\n \n " .format (date = datetime .now ()))
80+
81+ def cleanup (self ):
82+ """ Always deletes output file, deletes log if --delete is passed from command-line
83+ """
84+ if os .path .exists (self .path ["output" ]):
85+ os .remove (self .path ["output" ])
86+ if self .args .delete :
87+ if os .path .exists (self .path ["log" ]):
88+ os .remove (self .path ["log" ])
89+
90+ def log (self , elapsed_time , fps , compression_ratio ):
91+ """ Summarizes transcode session for screen and log
92+ """
93+ with open (self .path ["log" ], "w" ) as logfile :
94+ summary = "{elapsed_time}\n {fps} fps\n {compression_ratio}% reduction" .format (elapsed_time = elapsed_time , fps = fps , compression_ratio = compression_ratio )
95+ logfile .write (summary + "\n \n " + session .args + "\n \n " )
96+ pprint (vars (self ), logfile )
97+ print (summary )
98+
99+ def map_options (self ):
100+ """ Start with settings based on source resolution and then override defaults based on command-line arguments
101+ """
102+ self .encoder_quality = getattr (self .Settings .RF , self .source ["resolution" ])
103+ self .encoder_options = getattr (self .Settings .ENCOPTS , self .source ["resolution" ])
104+ if self .args .best :
105+ self .preset_name = "Best"
106+ elif self .args .baseline :
107+ self .preset_name = "Baseline"
108+ else :
109+ self .preset_name = "Default"
110+ if self .args .preset :
111+ self .encoder_preset = self .args .preset .lower ()
112+ else :
113+ self .encoder_preset = "slow"
114+ if self .args .quality :
115+ self .encoder_quality = self .args .quality
116+ if self .args .small :
117+ self .encoder_options += ":tu-intra-depth=3:tu-inter-depth=3"
118+
119+ def start (self ):
120+ """ Starts HandBrakeCLI session and creates job attribute
121+ """
122+ self .job = subprocess .Popen (shlex .split (self .command , posix = False )) # Posix=False to escape double-quotes in arguments
123+
124+ def summarize (self ):
125+ """ Summarize transcode session before starting
126+ """
127+ print ("{date}: Starting transcode session for {source}:" .format (date = str (datetime .now ()), source = self .path ["source" ]))
128+ pprint (vars (self ))
129+ print ()
130+
131+ def validate (self ):
132+ """ Verifies that no session attributes are null
133+ """
134+ if any (value is None for attribute , value in self .__dict__ .items ()):
135+ sys .exit ("FATAL: Session.validate(): found null attribute for " + self .path ["source" ])
0 commit comments