diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47db7e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*_Log_*.txt +output/ +content_files/ +!**/*.csv +*venv/ +.vscode +**/*.numbers +**/*.DS_Store +.ignore/ +**/*.log \ No newline at end of file diff --git a/encode_dash.py b/encode_dash.py index 083c825..5b73b93 100755 --- a/encode_dash.py +++ b/encode_dash.py @@ -181,7 +181,7 @@ class HEVCCHD1: m_frame_rate = 60 # FIXME: The HDR metadata should not be hardcoded here. # these are specific to the source sequence used for CHD1 (SOL) - m_x265_opts = "hdr-opt=1:repeat-headers=1:colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:master-display=G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1):max-cll=992,361" + m_x265_opts = "" class HEVCCUD1: m_profile = "main" @@ -317,7 +317,7 @@ class Representation: m_vui_timing = None m_segment_duration = None m_num_b_frames = None - m_x265_opts = None + m_enc_opts = None def __init__(self, representation_config): config = representation_config.split(",") @@ -465,6 +465,8 @@ def __init__(self, representation_config): self.m_vui_timing = value elif name == "sd": self.m_segment_duration = value + elif name == "enc_opts": + self.m_enc_opts = value else: print("Unknown configuration option for representation: " + name + " , it will be ignored.") @@ -530,8 +532,9 @@ def form_command(self, index): command += "::x264-params=\"" elif self.m_codec == VideoCodecOptions.HEVC.value: command += "::x265-params=\"" - if self.m_x265_opts: - command += self.m_x265_opts + ":" + + if self.m_enc_opts: + command += self.m_enc_opts + ":" command += "level=" + self.m_level + ":" \ "no-open-gop=1" + ":" \ @@ -543,8 +546,8 @@ def form_command(self, index): "vbv-bufsize=" + str(int(self.m_bitrate) * 3) + ":" \ "vbv-maxrate=" + str(int(int(self.m_bitrate) * 3 / 2)) - #if self.m_aspect_ratio_x is not None and self.m_aspect_ratio_y is not None: - # command += ":sar=" + self.m_aspect_ratio_x + "\\:" + self.m_aspect_ratio_y + if self.m_aspect_ratio_x and self.m_aspect_ratio_y: + command += ":sar=" + self.m_aspect_ratio_x + "\\:" + self.m_aspect_ratio_y command += "\":" #closing codec-specific parameters @@ -655,6 +658,7 @@ def parse_args(args): title_notice = arg elif opt in ("-pf", "--profile"): wave_media_profile = arg + # print(representations) return [gpac_path, output_file, representations, dashing, outDir, copyright_notice, source_notice, title_notice, wave_media_profile, dry_run] diff --git a/profiles/avc.csv b/profiles/avc.csv deleted file mode 100644 index 3a0870f..0000000 --- a/profiles/avc.csv +++ /dev/null @@ -1,30 +0,0 @@ -Stream ID,mezzanine radius,pic timing,VUI timing,sample entry,CMAF frag dur,init constraints,frag_type,resolution,framerate,bitrate,duration,wave_profile,cmaf_profile -1,L1_1920x1080,TRUE,FALSE,avc1,2,regular,duration,1920x1080,0.5,5100,30,cmf2,cfhd -2,L1_1920x1080,FALSE,TRUE,avc3,5,single,pframes,1920x1080,0.5,5000,30,cmf2,cfhd -3,L1_1920x1080,TRUE,FALSE,avc1+3,2,regular,every_frame,1920x1080,0.5,4900,30,cmf2,cfhd -10,L1_1920x1080,FALSE,FALSE,avc1,2,regular,duration,1920x1080,0.5,4800,30,cmf2,cfhd -11,L1_1920x1080,TRUE,TRUE,avc1,2,regular,duration,1920x1080,0.5,4100,30,cmf2,cfhd -12,L1_1920x1080,TRUE,FALSE,avc3,2,regular,duration,1920x1080,0.5,4200,30,cmf2,cfhd -13,L1_1920x1080,TRUE,FALSE,avc1+3,2,regular,duration,1920x1080,0.5,4300,30,cmf2,cfhd -14,L1_1920x1080,TRUE,FALSE,avc1,5,regular,duration,1920x1080,0.5,4400,30,cmf2,cfhd -15,L1_1920x1080,TRUE,FALSE,avc1,2,single,duration,1920x1080,0.5,4500,30,cmf2,cfhd -16,L1_1920x1080,TRUE,FALSE,avc1,2,regular,pframes,1920x1080,0.5,4600,30,cmf2,cfhd -17,L1_1920x1080,TRUE,FALSE,avc1,2,regular,every_frame,1920x1080,0.5,4700,30,cmf2,cfhd -19,L1_1920x1080,TRUE,FALSE,avc1,2,regular,duration,1920x1080,0.5,5100,60,cmf2,cfhd -20,L2_1920x1080,TRUE,FALSE,avc1,2,regular,duration,1920x1080,1,6000,60,cmf2,chdf -21,K1_1600x900,TRUE,FALSE,avc1,2,regular,duration,1600x900,0.5,3000,60,cmf2,cfhd -22,J1_1280x720,TRUE,FALSE,avc1,2,regular,duration,1280x720,0.5,2000,60,cmf2,cfhd -23,J1_1280x720,TRUE,FALSE,avc1,2,regular,duration,1280x720,1,3000,60,cmf2,cfhd -24,I1_1024x576,TRUE,FALSE,avc1,2,regular,duration,1024x576,0.5,1200,60,cmf2,cfhd -25,I2_1024x576,TRUE,FALSE,avc1,2,regular,duration,1024x576,0.5,1500,60,cmf2,cfhd -26,H1_960x540,TRUE,FALSE,avc1,2,regular,duration,960x540,0.5,1100,60,cmf2,cfhd -27,G1_852x480,TRUE,FALSE,avc1,2,regular,duration,852x480,0.5,1000,60,cmf2,cfhd -28,F1_768x432,TRUE,FALSE,avc1,2,regular,duration,768x432,0.5,900,60,cmf2,cfhd -29,E1_720x404,TRUE,FALSE,avc1,2,regular,duration,720x404,0.5,800,60,cmf2,cfhd -30,D1_704x396,TRUE,FALSE,avc1,2,regular,duration,704x396,0.5,700,60,cmf2,cfhd -31,C1_640x360,TRUE,FALSE,avc1,2,regular,duration,640x360,0.5,600,60,cmf2,cfhd -32,B1_512x288,TRUE,FALSE,avc1,2,regular,duration,512x288,0.5,450,60,cmf2,cfhd -33,A1_480x270,TRUE,FALSE,avc1,2,regular,duration,480x270,0.5,400,60,cmf2,cfhd -34,A1_480x270,TRUE,FALSE,avc1,2,regular,duration,480x270,0.25,300,60,cmf2,cfhd -audio,A1_480x270,FALSE,FALSE,avc1,2,regular,duration,480x270,0.25,128,60,cmf2,caac -LD,LD1_1920x1080,TRUE,FALSE,avc1,2,regular,duration,1920x1080,0.5,5100,7200,cmf2,cfhd \ No newline at end of file diff --git a/profiles/caac.csv b/profiles/caac.csv new file mode 100644 index 0000000..95a56dc --- /dev/null +++ b/profiles/caac.csv @@ -0,0 +1,2 @@ +Stream ID;mezzanine radius;pic timing;VUI timing;sample entry;CMAF frag dur;init constraints;frag_type;resolution;framerate;bitrate;duration;wave_profile;cmaf_profile +audio;A1_480x270;FALSE;FALSE;avc1;2;regular;duration;480x270;0.25;128;60;cmf2;caac \ No newline at end of file diff --git a/profiles/cfhd.csv b/profiles/cfhd.csv new file mode 100644 index 0000000..6bb398c --- /dev/null +++ b/profiles/cfhd.csv @@ -0,0 +1,29 @@ +Stream ID;mezzanine radius;pic timing;VUI timing;sample entry;CMAF frag dur;init constraints;frag_type;resolution;framerate;bitrate;duration;cmaf_profile;wave_profile +1;L1_1920x1080;TRUE;FALSE;avc1;2;regular;duration;1920x1080;0.5;5100;30;cmf2;cfhd +2;L1_1920x1080;FALSE;TRUE;avc3;5;single;pframes;1920x1080;0.5;5000;30;cmf2;cfhd +3;L1_1920x1080;TRUE;FALSE;avc1+3;2;regular;every_frame;1920x1080;0.5;4900;30;cmf2;cfhd +10;L1_1920x1080;FALSE;FALSE;avc1;2;regular;duration;1920x1080;0.5;4800;30;cmf2;cfhd +11;L1_1920x1080;TRUE;TRUE;avc1;2;regular;duration;1920x1080;0.5;4100;30;cmf2;cfhd +12;L1_1920x1080;TRUE;FALSE;avc3;2;regular;duration;1920x1080;0.5;4200;30;cmf2;cfhd +13;L1_1920x1080;TRUE;FALSE;avc1+3;2;regular;duration;1920x1080;0.5;4300;30;cmf2;cfhd +14;L1_1920x1080;TRUE;FALSE;avc1;5;regular;duration;1920x1080;0.5;4400;30;cmf2;cfhd +15;L1_1920x1080;TRUE;FALSE;avc1;2;single;duration;1920x1080;0.5;4500;30;cmf2;cfhd +16;L1_1920x1080;TRUE;FALSE;avc1;2;regular;pframes;1920x1080;0.5;4600;30;cmf2;cfhd +17;L1_1920x1080;TRUE;FALSE;avc1;2;regular;every_frame;1920x1080;0.5;4700;30;cmf2;cfhd +19;L1_1920x1080;TRUE;FALSE;avc1;2;regular;duration;1920x1080;0.5;5100;60;cmf2;cfhd +20;L2_1920x1080;TRUE;FALSE;avc1;2;regular;duration;1920x1080;1;6000;60;cmf2;chdf +21;K1_1600x900;TRUE;FALSE;avc1;2;regular;duration;1600x900;0.5;3000;60;cmf2;cfhd +22;J1_1280x720;TRUE;FALSE;avc1;2;regular;duration;1280x720;0.5;2000;60;cmf2;cfhd +23;J1_1280x720;TRUE;FALSE;avc1;2;regular;duration;1280x720;1;3000;60;cmf2;cfhd +24;I1_1024x576;TRUE;FALSE;avc1;2;regular;duration;1024x576;0.5;1200;60;cmf2;cfhd +25;I2_1024x576;TRUE;FALSE;avc1;2;regular;duration;1024x576;0.5;1500;60;cmf2;cfhd +26;H1_960x540;TRUE;FALSE;avc1;2;regular;duration;960x540;0.5;1100;60;cmf2;cfhd +27;G1_852x480;TRUE;FALSE;avc1;2;regular;duration;852x480;0.5;1000;60;cmf2;cfhd +28;F1_768x432;TRUE;FALSE;avc1;2;regular;duration;768x432;0.5;900;60;cmf2;cfhd +29;E1_720x404;TRUE;FALSE;avc1;2;regular;duration;720x404;0.5;800;60;cmf2;cfhd +30;D1_704x396;TRUE;FALSE;avc1;2;regular;duration;704x396;0.5;700;60;cmf2;cfhd +31;C1_640x360;TRUE;FALSE;avc1;2;regular;duration;640x360;0.5;600;60;cmf2;cfhd +32;B1_512x288;TRUE;FALSE;avc1;2;regular;duration;512x288;0.5;450;60;cmf2;cfhd +33;A1_480x270;TRUE;FALSE;avc1;2;regular;duration;480x270;0.5;400;60;cmf2;cfhd +34;A1_480x270;TRUE;FALSE;avc1;2;regular;duration;480x270;0.25;300;60;cmf2;cfhd +LD;LD1_1920x1080;TRUE;FALSE;avc1;2;regular;duration;1920x1080;0.5;5100;7200;cmf2;cfhd \ No newline at end of file diff --git a/profiles/chd1.csv b/profiles/chd1.csv new file mode 100644 index 0000000..3a8048a --- /dev/null +++ b/profiles/chd1.csv @@ -0,0 +1,7 @@ +Stream ID;mezzanine radius;pic timing;VUI timing;sample entry;CMAF frag dur;init constraints;frag_type;resolution;framerate;bitrate;duration;cmaf_profile;wave_profile;cenc +61;O1_3840x2160;FALSE;TRUE;hvc1;2;regular;duration;3840x2160;1;15000;30;cmfc;chd1;TRUE +62;O1_3840x2160;TRUE;FALSE;hev1;2;regular;pframes;3840x2160;1;15000;30;cmf2;chd1;FALSE +70;O1_3840x2160;FALSE;TRUE;hvc1;2;regular;duration;3840x2160;1;15000;60;cmfc;chd1;FALSE +71;O1_3840x2160;FALSE;TRUE;hvc1;2;regular;duration;3840x2160;0.5;15000;60;cmfc;chd1;FALSE +72;M1_2560x1440;FALSE;TRUE;hvc1;2;regular;duration;2560x1440;0.5;10000;60;cmfc;chd1;FALSE +73;L1_1920x1080;FALSE;TRUE;hvc1;2;regular;duration;1920x1080;0.5;3500;60;cmfc;chd1;FALSE \ No newline at end of file diff --git a/profiles/chh1.csv b/profiles/chh1.csv new file mode 100644 index 0000000..8b02ed2 --- /dev/null +++ b/profiles/chh1.csv @@ -0,0 +1,12 @@ +Stream ID;mezzanine radius;pic timing;VUI timing;sample entry;CMAF frag dur;init constraints;frag_type;resolution;framerate;bitrate;duration;cmaf_profile;wave_profile;cenc;sar;Skip +1;L1_1920x1080;FALSE;TRUE;hvc1;2;regular;duration;1920x1080;1;3500;30;cmfc;chh1;TRUE;; +2;L1_1920x1080;TRUE;FALSE;hev1;2;regular;pframes;1920x1080;1;3600;30;cmf2;chh1;FALSE;; +3;L1_1920x1080;TRUE;TRUE;hvc1;2;regular;duration;1920x1080;1;3300;30;cmfc;chh1;FALSE;; +4;L1_1920x1080;FALSE;FALSE;hvc1;2;regular;duration;1920x1080;1;3200;30;cmfc;chh1;FALSE;; +5;L1_1920x1080;FALSE;TRUE;hev1;2;regular;duration;1920x1080;1;3100;30;cmfc;chh1;FALSE;; +6;L1_1920x1080;FALSE;TRUE;hvc1;2;regular;pframes;1920x1080;1;3800;30;cmfc;chh1;FALSE;; +7;L1_1920x1080;FALSE;TRUE;hvc1;2;regular;duration;1920x1080;1;3700;30;cmf2;chh1;FALSE;; +10;L1_1920x1080;FALSE;TRUE;hvc1;2;regular;duration;1920x1080;1;3400;60;cmfc;chh1;FALSE;; +11;L2_1920x1080;FALSE;TRUE;hvc1;2;regular;duration;1920x1080;0.5;3500;60;cmfc;chh1;FALSE;; +12;J1_1280x720;FALSE;TRUE;hvc1;2;regular;duration;1280x720;0.5;2000;60;cmfc;chh1;FALSE;; +non_square_par;L2_1920x1080;FALSE;TRUE;hvc1;2;regular;duration;1440x1080;1;2800;60;cmfc;chh1;FALSE;4/3; \ No newline at end of file diff --git a/profiles/chh1_splice_ad.csv b/profiles/chh1_splice_ad.csv new file mode 100644 index 0000000..3d849fc --- /dev/null +++ b/profiles/chh1_splice_ad.csv @@ -0,0 +1,4 @@ +Stream ID;mezzanine radius;pic timing;VUI timing;sample entry;CMAF frag dur;init constraints;frag_type;resolution;framerate;bitrate;duration;cmaf_profile;wave_profile;cenc +splice_ad;AD-B1_1920x1080;FALSE;TRUE;hvc1;2;regular;duration;1920x1080;1;3500;5.76;cmfc;chh1;TRUE +splice_ad;AD-B1_1920x1080;FALSE;TRUE;hvc1;2;regular;duration;1920x1080;1;3500;21.255;cmfc;chh1;TRUE +splice_ad;AD-B1_1920x1080;FALSE;TRUE;hvc1;2;regular;duration;1920x1080;1;3500;6.4;cmfc;chh1;TRUE \ No newline at end of file diff --git a/profiles/chh1_splice_main.csv b/profiles/chh1_splice_main.csv new file mode 100644 index 0000000..517a091 --- /dev/null +++ b/profiles/chh1_splice_main.csv @@ -0,0 +1,2 @@ +Stream ID;mezzanine radius;pic timing;VUI timing;sample entry;CMAF frag dur;init constraints;frag_type;resolution;framerate;bitrate;duration;cmaf_profile;wave_profile;cenc +splice_main;B1_1920x1080;FALSE;TRUE;hvc1;2;regular;duration;1920x1080;1;3500;10;cmfc;chh1;TRUE \ No newline at end of file diff --git a/profiles/hevc.csv b/profiles/hevc.csv deleted file mode 100644 index 3835aa2..0000000 --- a/profiles/hevc.csv +++ /dev/null @@ -1,12 +0,0 @@ -Stream ID,mezzanine radius,pic timing,VUI timing,sample entry,CMAF frag dur,init constraints,frag_type,resolution,framerate,bitrate,duration,cmaf_profile,wave_profile -1,L1_1920x1080,FALSE,TRUE,hvc1,2,regular,duration,1920x1080,1,3500,30,cmfc,chh1 -2,L1_1920x1080,TRUE,FALSE,hev1,2,regular,pframes,1920x1080,1,3600,30,cmfc,chh1 -3,L1_1920x1080,TRUE,TRUE,hvc1,2,regular,duration,1920x1080,1,3300,30,cmfc,chh1 -4,L1_1920x1080,FALSE,FALSE,hvc1,2,regular,duration,1920x1080,1,3200,30,cmfc,chh1 -5,L1_1920x1080,FALSE,TRUE,hev1,2,regular,duration,1920x1080,1,3100,30,cmfc,chh1 -6,L1_1920x1080,FALSE,TRUE,hvc1,2,regular,pframes,1920x1080,1,3800,30,cmfc,chh1 -7,L1_1920x1080,FALSE,TRUE,hvc1,2,regular,duration,1920x1080,1,3700,30,cmf2,chh1 -10,L1_1920x1080,FALSE,TRUE,hvc1,2,regular,duration,1920x1080,1,3400,60,cmfc,chh1 -11,L2_1920x1080,FALSE,TRUE,hvc1,2,regular,duration,1920x1080,0.5,3500,60,cmfc,chh1 -12,J1_1280x720,FALSE,TRUE,hvc1,2,regular,duration,1280x720,0.5,2000,60,cmfc,chh1 -non_square_par,L2_1920x1080,FALSE,TRUE,hvc1,2,regular,duration,1440x1080,1,2800,60,cmfc,chh1 \ No newline at end of file diff --git a/report.py b/report.py new file mode 100644 index 0000000..4f846b0 --- /dev/null +++ b/report.py @@ -0,0 +1,233 @@ +from pathlib import Path +import csv +import os +import subprocess +import pysftp + +root = Path('output') +# export AKAMAI_PRIVATE_KEY="/Users/ndvx/code/cta_wave/AccessKeys/Qualcomm/dashif_netstorage_key" + +FPS_FAMILIES = set() + +class TestVector: + + @staticmethod + def csv_headers(): + return ['stream_id', 'profile', *FPS_FAMILIES] + + def __init__(self, stream_id, profile) -> None: + self.stream_id = stream_id + self.profile = profile + self.fps_variants = {} + + @property + def sort_key(self): + k = 0 + if self.profile.startswith('chh1'): + k = 0 + elif self.profile.startswith('chd1'): + k = 1000 + id = self.stream_id + if self.stream_id.endswith('-cenc'): + k += 0.5 + id = self.stream_id.split('-')[0] + if id.startswith('t'): + k += int(id[1:]) + else: + k += len(id) + 100 + return k + + def csv_row(self): + row = { + 'stream_id': self.stream_id, + 'profile': self.profile.split('_')[0] + } + for ff in FPS_FAMILIES: + row[ff] = self.fps_variants.get(ff, '') + return row + + +def is_meta(fp): + return fp.name == '.DS_Store' + + +def _iter_dir(d): + for fp in d.iterdir(): + if is_meta(fp): + os.remove(fp) + continue + yield fp + +def csv_report(batch): + rows = [] + for cmaf_set in _iter_dir(root): + if not cmaf_set.is_dir(): + continue + + cmaf_set_rows = {} + + for fps_family_dir in _iter_dir(cmaf_set): + + fps_family = fps_family_dir.name + FPS_FAMILIES.add(fps_family) + """ + if fps_family == '25_50': + target = cmaf_set / '12.5_25_50' + fps_family_dir.rename(target) + continue + elif fps_family == '30_60': + target = cmaf_set / '15_30_60' + fps_family_dir.rename(target) + continue + else: + continue + """ + + for test_vector in _iter_dir(fps_family_dir): + + if is_meta(test_vector): + os.remove(test_vector) + + if test_vector.name not in cmaf_set_rows: + cmaf_set_rows[test_vector.name] = TestVector(test_vector.name, cmaf_set.name) + + tv = cmaf_set_rows[test_vector.name] + + try: + batch_dir = test_vector / batch + assert len([*_iter_dir(batch_dir)]) == 2, f'unexpected content: {batch_dir}' + + stream_mpd = batch_dir / 'stream.mpd' + assert stream_mpd.exists(), f'not found: {stream_mpd}' + + segments_dir = batch_dir / '1' + assert segments_dir.is_dir(), f'not a directory: {segments_dir}' + + cmaf_init = segments_dir / 'init.mp4' + assert cmaf_init.exists(), f'not found: {cmaf_init}' + + segments_count = len([*_iter_dir(segments_dir)]) - 1 + tv.fps_variants[fps_family] = segments_count + + except BaseException as e: + print(e) + tv.fps_variants[fps_family] = e + + + for tv in cmaf_set_rows.values(): + rows.append(tv) + + csv_out = root / f'report_{batch}.csv' + with open(csv_out, 'w', newline='') as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=TestVector.csv_headers()) + writer.writeheader() + for r in sorted(rows, key=lambda tv: tv.sort_key): + writer.writerow(r.csv_row()) + + +def _iter_batch(batch, skip_missing=True): + for cmaf_set in _iter_dir(root): + if not cmaf_set.is_dir(): + continue + for fps_family_dir in _iter_dir(cmaf_set): + for test_vector in _iter_dir(fps_family_dir): + res = test_vector / batch + if skip_missing and not res.exists(): + continue + yield test_vector / batch + + +def remove_batch(batch): + trashbin = Path('./trash') + for batch_item in _iter_batch(batch): + target = trashbin / batch_item.relative_to(root) + os.makedirs(target.parent) + batch_item.rename(target) + + +def merge_batch(target, *batches, dry_run=False): + for batch in batches: + for batch_item in _iter_batch(batch): + target_dir = batch_item.parent / target + if (not dry_run) and (not target_dir.exists()): + os.makedirs(target_dir) + for fp in _iter_dir(batch_item): + dst = target_dir/fp.name + if dry_run: + assert not dst.exists() + else: + fp.rename(dst) + + +def zip_batch(batch, dry_run=False, remove=False): + print(f"CWD={root}") + for batch_item in _iter_batch(batch): + + stream_id = batch_item.parent.name + # output_test_stream_folder = f"{o.wave_profile}_sets/{input.fps_family}/{o.stream_id}/{batch_folder}" + content_dir = batch_item.relative_to(root) + archive = content_dir / f'{stream_id}.zip' + zip_cmd = f"zip -r {archive} {content_dir}*" + if remove: + if archive.exists() and not dry_run: + os.remove(archive) + continue + print(zip_cmd) + if not dry_run: + result = subprocess.run(zip_cmd, shell=True, cwd=root) + print(result) + + +def upload_batch(batch, dry_run=False): + + host = "dashstorage.upload.akamai.com" + username = "sshacs" + cnopts = pysftp.CnOpts(knownhosts=host) + cnopts.hostkeys = None + remote_vectors_dir = "/129021/dash/WAVE/vectors/" + + try: + sftp = None if dry_run else pysftp.Connection(host=host, username=username, private_key=os.path.expanduser(os.getenv('AKAMAI_PRIVATE_KEY')), cnopts=cnopts) + + if sftp is not None: + print("Connection successfully established ... ") + sftp.cwd(remote_vectors_dir) + + for batch_item in _iter_batch(batch): + # eg. output/chh1_sets/14.985_29.97_59.94/splice_ad-cenc/2024-10-25 + print(f"\n##### {batch_item} ####################\n") + for r, _, files in batch_item.walk(top_down=True): + + remote_dir = remote_vectors_dir + str(r.relative_to(root)) + print(f"Create remote directory: {remote_dir}") + if not dry_run: + if not sftp.isfile(remote_dir): + sftp.makedirs(remote_dir, mode=644) + + for f in files: + local_file = r / f + remote_file = f"{remote_dir}/{f}" + print(f"Upload '{local_file}' to: '{remote_file}'") + if not dry_run: + sftp.put(local_file, remote_file, callback=lambda x,y: print(f"{x} transferred out of {y}")) + + if not dry_run: + sftp.close() + + except BaseException as e: + print(e) + + + + +if __name__ == "__main__": + + # remove_batch('2024-10-24') + + # merge_batch('2024-10-25', '2024-10-23') + # remove_batch('2024-10-23') + + # csv_report('2024-10-25') + + # zip_batch('2024-10-25') + upload_batch('2024-10-25') diff --git a/run-all.py b/run-all.py index 4118d56..f440170 100755 --- a/run-all.py +++ b/run-all.py @@ -23,6 +23,7 @@ ################################################################################ +CSV_DELIMITER = ";" GPAC_EXECUTABLE = "/usr/local/bin/gpac" @@ -45,18 +46,21 @@ # Mezzanine characteristics: class InputContent: - def __init__(self, content, root_folder, fps_family, fps:Fraction): - self.content = content - self.root_folder = root_folder - self.fps_family = fps_family # video: frame_rate_family ; audio: audio_codec - self.fps = fps + def __init__(self, *args, **row): + self.content = row['basename'] + self.root_folder = row['location'] + self.fps_family = row['fps_family'] + fps_num = int(row['fps_num']) + fps_den = row['fps_den'] + fps_den = 1 if fps_den == '' else int(fps_den) + self.fps = Fraction(fps_num, fps_den) + self.encoder_hdr_opts = row.get('encoder_hdr_opts', '') def get_annotations(self, input_basename): # Extract copyright annotation_filename = input.root_folder + input_basename + ".json" if not os.path.exists(annotation_filename): errtxt = "Annotation file \"" + annotation_filename + "\" not found. Skipping entry." - print(errtxt) raise FileNotFoundError(errtxt) with open(annotation_filename, 'r') as fo: data = json.load(fo) @@ -73,44 +77,60 @@ def create_new_db(): return database -def iter_csv(input_csv): - with open(input_csv) as csv_file: - csv_reader = csv.reader(csv_file, delimiter=',') - for row in csv_reader: - if row[0] == "Stream ID": - continue - yield row - - -def iter_test_streams(input_csv): - csv_line_index = 0 - for row in iter_csv(input_csv): - - if row[0] == "Stream ID": - continue - - # Decide which stream to process based on the media type and the WAVE media profile - wave_profile = row[13] - if PROFILES_TYPE[wave_profile] == "video": - if row[0][0] == 'a': - continue - if row[0][0].isdigit(): - stream_id = "{0}{1}".format("t", row[0]) - else: - stream_id = row[0] - else: - if row[0][0] != 'a': - continue - stream_id = row[0] - - # do CENC for 1st stream only - csv_line_index += 1 - cenc_stream = csv_line_index == 1 - - yield stream_id, wave_profile, row, cenc_stream - - -def process_mezzanine(input:InputContent, input_csv, local_output_folder, batch_folder, database={}, encode=True, cenc=True, zip=True, dry_run=False): +class OutputContent: + + @staticmethod + def _parse_csv_bool(value): + return True if value == 'TRUE' else False + + @staticmethod + def iter_test_streams(input_csv): + with open(input_csv) as csv_file: + csv_reader = csv.DictReader(csv_file, delimiter=CSV_DELIMITER) + for row in csv_reader: + stream = OutputContent(**row) + if not stream.skip: + yield stream + + def __init__(self, *args, **row): + # required + self.stream_id = row['Stream ID'] + if self.stream_id.isdigit(): + self.stream_id = f't{self.stream_id}' + self.mezzanine_radius = row['mezzanine radius'] + self.pic_timing = self._parse_csv_bool(row['pic timing']) + self.vui_timing = self._parse_csv_bool(row['VUI timing']) + self.sample_entry = row['sample entry'] + self.cmaf_frag_dur = row['CMAF frag dur'] + self.init_constraints = row['init constraints'] + self.frag_type = row['frag_type'] + self.resolution = row['resolution'] + self.framerate = row['framerate'] + self.bitrate = row['bitrate'] + self.duration = row['duration'] + self.cmaf_profile = row['cmaf_profile'] + self.wave_profile = row['wave_profile'] + self.cenc = self._parse_csv_bool(row['cenc']) + # optinal + self.sar = row.get('sar', '') + self.enc_opts = row.get('enc_opts', '') + self.skip = bool(row.get('skip', '')) + +def format_db_entry(o:OutputContent, source_notice, reps, seg_dur, mpdPath, zipPath): + return { + 'source': source_notice, + 'representations': reps, + 'segmentDuration': str(seg_dur), + 'fragmentType': o.frag_type, + 'hasSEI': o.pic_timing, + 'hasVUITiming': o.vui_timing, + 'visualSampleEntry': o.sample_entry, + 'mpdPath': mpdPath, + 'zipPath': zipPath + } + + +def process_mezzanine(input:InputContent, input_csv, local_output_folder, batch_folder, database={}, encode=True, cenc=True, zip=True, dry_run=False, quiet=False): if not (encode or cenc or zip): return @@ -120,97 +140,96 @@ def process_mezzanine(input:InputContent, input_csv, local_output_folder, batch_ source_notice = "" title_notice = "" - for (stream_id, wave_profile, row) in iter_test_streams(input_csv): - - cmaf_profile = "avchd" - if wave_profile == "cfhd": + for o in OutputContent.iter_test_streams(input_csv): + if o.wave_profile == "cfhd": codec = "h264" cmaf_profile = "avchd" - elif wave_profile == "chdf": + elif o.wave_profile == "chdf": codec = "h264" cmaf_profile = "avchdhf" - elif wave_profile == "chh1": + elif o.wave_profile == "chh1": codec = "h265" cmaf_profile = "chh1" - elif wave_profile == "chd1": + elif o.wave_profile == "chd1": codec = "h265" cmaf_profile = "chd1" - elif wave_profile == "caac": + elif o.wave_profile == "caac": codec = "aac" cmaf_profile = "caac" else: codec = "copy" - cmaf_profile = wave_profile + cmaf_profile = o.wave_profile - # test_stream.get_output_folder_base(input.fps_family) - output_folder_base = "{0}_sets/{1}".format(wave_profile, input.fps_family) - output_folder_complete = "{0}/{1}".format(local_output_folder, output_folder_base) - test_stream_folder_suffix = stream_id + "/" + batch_folder + output_folder_base = f"{o.wave_profile}_sets/{input.fps_family}" + output_folder_complete = f"{local_output_folder}/{output_folder_base}" + test_stream_folder_suffix = f"{o.stream_id}/{batch_folder}" - test_stream_folder = "{0}/{1}".format(output_folder_complete, test_stream_folder_suffix) - output_test_stream_folder = "{0}/{1}".format(output_folder_base, test_stream_folder_suffix) + test_stream_folder = f"{output_folder_complete}/{test_stream_folder_suffix}" + output_test_stream_folder = f"{output_folder_base}/{test_stream_folder_suffix}" server_test_stream_access_url = SERVER_ACCESS_URL + output_test_stream_folder - print("\n##### Processing test stream " + test_stream_folder + " #####\n") - # 0: Stream ID, 1: mezzanine radius, 2: pic timing SEI, 3: VUI timing, 4: sample entry, # 5: CMAF frag dur, 6: init constraints, 7: frag_type, 8: resolution, 9: framerate, # 10: bitrate, 11: duration - fps = min(FRAMERATES, key=lambda x:abs(x-float(row[9])*input.fps)) + fps = min(FRAMERATES, key=lambda x:abs(x-float(o.framerate)*input.fps)) input_basename = "" - if PROFILES_TYPE[wave_profile] == "video": - input_basename = "{0}_{1}@{2}_{3}".format(input.content, row[1], fps, row[11]) + if PROFILES_TYPE[o.wave_profile] == "video": + input_basename = f"{input.content}_{o.mezzanine_radius}@{fps}_{o.duration}" else: - input_basename = "{0}{1}".format(input.content, row[1]) + input_basename = f"{input.content}{o.mezzanine_radius}" input_filename = input_basename + ".mp4" copyright_notice, source_notice = None, None try: copyright_notice, source_notice = input.get_annotations(input_basename) except FileNotFoundError as e: + if not quiet: + print("\n##### Error while processing " + test_stream_folder + " #####\n") + print(e) continue - seg_dur = Fraction(row[5]) + print("\n##### Processing test stream " + test_stream_folder + " #####\n") + + seg_dur = Fraction(o.cmaf_frag_dur) if input.fps.denominator == 1001: - seg_dur = Fraction(row[5]) * Fraction(1001, 1000) - reps = [{"resolution": row[8], "framerate": fps, "bitrate": row[10], "input": input_filename}] + seg_dur = Fraction(o.cmaf_frag_dur) * Fraction(1001, 1000) + reps = [{"resolution": o.resolution, "framerate": fps, "bitrate": o.bitrate, "input": input_filename}] filename_v = input.root_folder + input_filename - reps_command = "id:{0},type:{1},codec:{2},vse:{3},cmaf:{4},fps:{5}/{6},res:{7},bitrate:{8},input:\"{9}\",pic_timing:{10},vui_timing:{11},sd:{12},bf:{13}"\ - .format(row[0], PROFILES_TYPE[wave_profile], codec, row[4], cmaf_profile, int(float(row[9])*input.fps.numerator), input.fps.denominator, row[8], row[10], - filename_v, row[2].capitalize(), row[3].capitalize(), str(seg_dur), row[7]) - #if row[1].find(row[8]) == -1: - # mezzanine_par = row[1].split('_')[1].split('x') - # encoding_par = row[8].split('x') - # reps_command += ",sar:" + str(int(mezzanine_par[0])*int(encoding_par[1])) + "/" + str(int(mezzanine_par[1])*int(encoding_par[0])) + reps_command = f"id:{o.stream_id},type:{PROFILES_TYPE[o.wave_profile]},codec:{codec},vse:{o.sample_entry},cmaf:{cmaf_profile}" + reps_command += f",fps:{int(float(o.framerate)*input.fps.numerator)}/{input.fps.denominator},res:{o.resolution},bitrate:{o.bitrate}" + reps_command += f",input:\"{filename_v}\",pic_timing:{o.pic_timing},vui_timing:{o.vui_timing},sd:{str(seg_dur)},bf:{o.frag_type}" + + if o.sar: + reps_command += f",sar:{o.sar}" + + if input.encoder_hdr_opts: + reps_command += f",enc_opts:{input.encoder_hdr_opts}" # Finalize one-AdaptationSet formatting reps_command = "--reps=" + reps_command - if PROFILES_TYPE[wave_profile] == "video": - title_notice = "{0}, {1}, {2}fps, {3}, Test Vector {4}".format(input.content, row[8], float(row[9])*input.fps.numerator/input.fps.denominator, wave_profile, row[0]) + if PROFILES_TYPE[o.wave_profile] == "video": + title_notice = "{0}, {1}, {2}fps, {3}, Test Vector {4}".format(input.content, o.bitrate, float(o.framerate)*input.fps.numerator/input.fps.denominator, o.wave_profile, o.stream_id) else: - title_notice = "{0}, Test Vector {1}".format(wave_profile, row[0]) + title_notice = "{0}, Test Vector {1}".format(o.wave_profile, o.stream_id) # Web exposed information - database[wave_profile.upper()][output_test_stream_folder] = { - 'source': source_notice, - 'representations': reps, - 'segmentDuration': str(seg_dur), - 'fragmentType': row[7], - 'hasSEI': row[2].lower() == 'true', - 'hasVUITiming': row[3].lower() == 'true', - 'visualSampleEntry': row[4], - 'mpdPath': '{0}stream.mpd'.format(server_test_stream_access_url), - 'zipPath': '{0}{1}.zip'.format(server_test_stream_access_url, stream_id) - } + database[o.wave_profile.upper()][output_test_stream_folder] = format_db_entry( + o, + source_notice, + reps, + str(seg_dur), + '{0}/stream.mpd'.format(server_test_stream_access_url), + '{0}/{1}.zip'.format(server_test_stream_access_url, o.stream_id) + ) if encode: # Encode, package, and manifest generation (DASH-only) encode_dash_cmd = f"./encode_dash.py --path={GPAC_EXECUTABLE} --out=stream.mpd --outdir={test_stream_folder}" - encode_dash_cmd += f" --dash=sd:{seg_dur},fd:{seg_dur},ft:{row[7]},fr:{input.fps},cmaf:{row[12]}" - encode_dash_cmd += f" --copyright=\'{copyright_notice}\' --source=\'{source_notice}\' --title=\'{title_notice}\' --profile={wave_profile} {reps_command}" + encode_dash_cmd += f" --dash=sd:{seg_dur},fd:{seg_dur},ft:{o.frag_type},fr:{input.fps},cmaf:{o.cmaf_profile}" + encode_dash_cmd += f" --copyright=\'{copyright_notice}\' --source=\'{source_notice}\' --title=\'{title_notice}\' --profile={o.wave_profile} {reps_command}" print("# Encoding:\n") if dry_run: encode_dash_cmd += " --dry-run" @@ -218,7 +237,7 @@ def process_mezzanine(input:InputContent, input_csv, local_output_folder, batch_ result = subprocess.run(encode_dash_cmd, shell=True) if zip: - zip_cmd = "zip -r " + output_test_stream_folder + stream_id + ".zip " + output_test_stream_folder + "*" + zip_cmd = "zip -r " + output_test_stream_folder + o.stream_id + ".zip " + output_test_stream_folder + "*" print("# Creating archive (cwd=" + local_output_folder + "):\n") print(zip_cmd + "\n") if not dry_run: @@ -226,36 +245,31 @@ def process_mezzanine(input:InputContent, input_csv, local_output_folder, batch_ # create an encrypted copy of the stream if requested - cenc_stream = bool(row[14]) - - if cenc and cenc_stream: + if cenc and o.cenc: - output_test_stream_folder_cenc = "{0}/{1}-cenc/{2}".format(output_folder_base, stream_id, batch_folder) + output_test_stream_folder_cenc = "{0}/{1}-cenc/{2}".format(output_folder_base, o.stream_id, batch_folder) cenc_cmd = GPAC_EXECUTABLE + " -strict-error -i " + output_test_stream_folder + "/stream.mpd:forward=mani cecrypt:cfile=" + sys.path[0] + "/DRM.xml" cenc_cmd += " @ -o " + output_test_stream_folder_cenc + "/stream.mpd:pssh=mv" - print("# Encrypting content:\n (cwd=" + local_output_folder + "):\n") + print("\n# Encrypting content (cwd=" + local_output_folder + "):\n") print(cenc_cmd) if not dry_run: result = subprocess.run(cenc_cmd, shell=True, cwd=local_output_folder) # Web exposed information server_test_stream_access_url_cenc = SERVER_ACCESS_URL + output_test_stream_folder_cenc - database["CENC"][output_test_stream_folder_cenc] = { - 'source': source_notice, - 'representations': reps, - 'segmentDuration': str(seg_dur), - 'fragmentType': row[7], - 'hasSEI': row[2].lower() == 'true', - 'hasVUITiming': row[3].lower() == 'true', - 'visualSampleEntry': row[4], - 'mpdPath': '{0}stream.mpd'.format(server_test_stream_access_url_cenc), - 'zipPath': '{0}{1}.zip'.format(server_test_stream_access_url_cenc, stream_id + "-cenc") - } + database["CENC"][output_test_stream_folder_cenc] = format_db_entry( + o, + source_notice, + reps, + str(seg_dur), + '{0}/stream.mpd'.format(server_test_stream_access_url_cenc), + '{0}/{1}.zip'.format(server_test_stream_access_url_cenc, o.stream_id + "-cenc") + ) ######################################################################################################################## # ZIP IT if zip: - zip_cmd = "zip -r " + output_test_stream_folder_cenc + stream_id + "-cenc.zip " + output_test_stream_folder_cenc + "*" + zip_cmd = "zip -r " + output_test_stream_folder_cenc + o.stream_id + "-cenc.zip " + output_test_stream_folder_cenc + "*" print("# Executing (cwd=" + local_output_folder + "):\n") print(zip_cmd) if not dry_run: @@ -307,29 +321,21 @@ def upload_sequence(output_folder_base, server_output_folder, dry_run): def get_mezzanine_list(mezzanine_cfg): res = [] with open(mezzanine_cfg, 'r') as fi: - csv_reader = csv.DictReader(fi, delimiter=',') + csv_reader = csv.DictReader(fi, delimiter=CSV_DELIMITER) for row in csv_reader: - skips = getattr(row, 'skip', '') - if skips != '': - continue - basename = row['basename'] - location = row['location'] - fps_family = row['fps_family'] - fps_num = int(row['fps_num']) - fps_den = row['fps_den'] - fps_den = 1 if fps_den == '' else int(fps_den) - res.append(InputContent(basename, location, fps_family, Fraction(fps_num, fps_den))) - return res + res.append(InputContent(**row)) + return res if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('MEZZANINE_CFG', help='a csv containing the list of mezzanine files to process') - parser.add_argument('OUTPUT_CFG', help='the wave profile being batch processed (chh1, cud1, chd1, ...). a corresponding csv configuration file is expected in the profiles directory') + parser.add_argument('OUTPUT_CFG', help='csv configuration for test content to be encoded') parser.add_argument('-d', '--dry-run', action='store_true', help='do not process commands, just print them out') parser.add_argument('-b', '--batch-dir', help='batch directory, defaults to YYYY-MM-DD') parser.add_argument('-z', '--zip', action='store_true', help='upload content to public server over sftp') parser.add_argument('-u', '--upload', action='store_true', help='upload content to public server over sftp') + parser.add_argument('-q', '--quiet', action='store_true', help='do not print mezzanine files not found') parser.add_argument('--no-encode', action='store_true', help='upload content to public server over sftp') parser.add_argument('--no-cenc', action='store_true', help='create a cenc encryption variant of the first stream of the list') args = parser.parse_args() @@ -339,7 +345,7 @@ def get_mezzanine_list(mezzanine_cfg): batch_folder = f"{datetime.today().strftime('%Y-%m-%d')}" for input in get_mezzanine_list(args.MEZZANINE_CFG): - output_folder_base = process_mezzanine(input, args.OUTPUT_CFG, local_output_folder, batch_folder, database, (not args.no_encode), (not args.no_cenc), args.zip, args.dry_run) + output_folder_base = process_mezzanine(input, args.OUTPUT_CFG, local_output_folder, batch_folder, database, (not args.no_encode), (not args.no_cenc), args.zip, args.dry_run, args.quiet) if not args.dry_run: with open('./database.json', 'w') as outfile: diff --git a/run-all.sh b/run-all.sh new file mode 100755 index 0000000..12c727e --- /dev/null +++ b/run-all.sh @@ -0,0 +1,5 @@ +batch_dir=2024-09-25 +python run-all.py -q -b=$batch_dir ./content_files/chh1_splice_main.csv ./profiles/chh1_splice_main.csv > batch.log +python run-all.py -q -b=$batch_dir ./content_files/chh1_splice_ad.csv ./profiles/chh1_splice_ad.csv >> batch.log +python run-all.py -b=$batch_dir ./content_files/chh1.csv ./profiles/chh1.csv >> batch.log +python run-all.py -b=$batch_dir ./content_files/chd1.csv ./profiles/chd1.csv >> batch.log diff --git a/splice/gen_hevc_chh1.sh b/splice/gen_hevc_chh1.sh deleted file mode 100755 index 18e1095..0000000 --- a/splice/gen_hevc_chh1.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash -set -eux - -export MEZZANINE_VERSION="4" -export BATCH="2023-09-01" - -export GPAC="/opt/bin/gpac -strict-error" - -export MPD=stream.mpd - -# see https://github.com/cta-wave/mezzanine/issues/40 -export SEGDUR=1.92 - -#available contents: -# releases/4/splice_ad_bbb_AD-A1_1280x720@25_5.76.mp4 -# releases/4/splice_ad_bbb_AD-A1_1280x720@29.97_21.255.mp4 -# releases/4/splice_ad_bbb_AD-A1_1280x720@30_6.4.mp4 -# releases/4/splice_ad_bbb_AD-B1_1920x1080@25_5.76.mp4 -# releases/4/splice_ad_bbb_AD-B1_1920x1080@29.97_21.255.mp4 -# releases/4/splice_ad_bbb_AD-B1_1920x1080@30_6.4.mp4 -# releases/4/splice_main_croatia_A1_1280x720@25_10.mp4 -# releases/4/splice_main_croatia_B1_1920x1080@25_10.mp4 -# releases/4/splice_main_tos_A1_1280x720@29.97_10.mp4 -# releases/4/splice_main_tos_A1_1280x720@30_10.mp4 -# releases/4/splice_main_tos_B1_1920x1080@29.97_10.mp4 -# releases/4/splice_main_tos_B1_1920x1080@30_10.mp4 - -#these command-lines are copied from the traces of the 'cfhd_sets' generation (run-all.py): - -export STREAM_ID=splice_main -export CONTENT_MAIN=content_files/releases/$MEZZANINE_VERSION/splice_main_croatia_A1_1280x720@25_10.mp4 -export COPYRIGHT='© Croatia (2019), credited to EBU, used and licensed under Creative Commons Attribution 4.0 International (CC BY 4.0) (https://creativecommons.org/licenses/by/4.0/) by the Consumer Technology Association (CTA)® / annotated, encoded and compressed from original.' -export SOURCE="splice_main_croatia_A1_1280x720@25_10 version $MEZZANINE_VERSION" -export TITLE='Croatia, 1280 x 720, 25fps, splice_main, Test Vector 1' -rm -rf output/cfhd_sets/12.5_25_50/$STREAM_ID/$BATCH/ -./encode_dash.py --path="$GPAC" --out="$MPD" --outdir=output/cfhd_sets/12.5_25_50/$STREAM_ID/$BATCH/ --dash=sd:$SEGDUR,fd:$SEGDUR,ft:duration,fr:25: --copyright="$COPYRIGHT" --source="$SOURCE" --title="$TITLE" --profile="cfhd" \ - --reps=id:1,type:video,codec:h264,vse:avc1,cmaf:avchdhf,fps:25/1,res:1280x720,bitrate:2000,input:$CONTENT_MAIN,pic_timing:True,vui_timing:False,sd:$SEGDUR,bf:2 - -pushd output -zip -r cfhd_sets/12.5_25_50/$STREAM_ID/$BATCH/$STREAM_ID.zip cfhd_sets/12.5_25_50/$STREAM_ID/$BATCH/* -popd - -#encrypt -$GPAC -i output/cfhd_sets/12.5_25_50/$STREAM_ID/$BATCH/$MPD:forward=mani cecrypt:cfile=DRM.xml @ -o output/cfhd_sets/12.5_25_50/$STREAM_ID-cenc/$BATCH/$MPD:pssh=mv - -pushd output -zip -r cfhd_sets/12.5_25_50/$STREAM_ID-cenc/$BATCH/$STREAM_ID-cenc.zip cfhd_sets/12.5_25_50/$STREAM_ID-cenc/$BATCH/* -popd - -export STREAM_ID=splice_ad -export CONTENT_AD=content_files/releases/$MEZZANINE_VERSION/splice_ad_bbb_AD-A1_1280x720@25_5.76.mp4 -export COPYRIGHT='© Croatia (2019), credited to EBU, used and licensed under Creative Commons Attribution 4.0 International (CC BY 4.0) (https://creativecommons.org/licenses/by/4.0/) by the Consumer Technology Association (CTA)® / annotated, encoded and compressed from original.' -export SOURCE="splice_ad_bbb_AD-A1_1280x720@25_5.76 version $MEZZANINE_VERSION" -export TITLE='Big Buck Bunny, 1280 x 720, 25fps, splice_ad, Test Vector 1' -rm -rf output/cfhd_sets/12.5_25_50/$STREAM_ID/$BATCH/ -./encode_dash.py --path="$GPAC" --out="$MPD" --outdir=output/cfhd_sets/12.5_25_50/$STREAM_ID/$BATCH/ --dash=sd:$SEGDUR,fd:$SEGDUR,ft:duration,fr:25: --copyright="$COPYRIGHT" --source="$SOURCE" --title="$TITLE" --profile="cfhd" \ - --reps=id:1,type:video,codec:h264,vse:avc1,cmaf:avchdhf,fps:25/1,res:1280x720,bitrate:2000,input:$CONTENT_AD,pic_timing:True,vui_timing:False,sd:$SEGDUR,bf:2 - -pushd output -zip -r cfhd_sets/12.5_25_50/$STREAM_ID/$BATCH/$STREAM_ID.zip cfhd_sets/12.5_25_50/$STREAM_ID/$BATCH/* -popd - -#encrypt -$GPAC -i output/cfhd_sets/12.5_25_50/$STREAM_ID/$BATCH/$MPD:forward=mani cecrypt:cfile=DRM.xml @ -o output/cfhd_sets/12.5_25_50/$STREAM_ID-cenc/$BATCH/$MPD:pssh=mv - -pushd output -zip -r cfhd_sets/12.5_25_50/$STREAM_ID-cenc/$BATCH/$STREAM_ID-cenc.zip cfhd_sets/12.5_25_50/$STREAM_ID-cenc/$BATCH/* -popd