diff --git a/README.md b/README.md index 0f65729..b568aa7 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Note: Once installed in a [python environment](https://docs.python.org/3/library 7. Upload batch content 8. Download database content -*_Important_*: the following workflow has been implemented while generating HEVC test content. Although it hasn't been tested with AVC content, it is expected to work exactly the same. For audio content, a [separate set of instructions](https://github.com/cta-wave/Test-Content-Generation/blob/master/Instructions/audio.md) is available. It is suggested to use this worflow for all content future generation. +*_Important_*: the following workflow has been implemented while generating HEVC test content. Although it hasn't been tested thoroughly with other codecs, it is expected to work exactly the same. For audio content, a [separate set of instructions](https://github.com/cta-wave/Test-Content-Generation/blob/master/Instructions/audio.md) is available. It is suggested to use this worflow for all content future generation. ### 1. Download mezzanine content @@ -67,7 +67,6 @@ Batch files used to produce reference content is stored in the [./profiles](prof ### 3. Batch encode/package content - #### 3.1 Video content typical usage of `tcgen encode`: @@ -81,7 +80,7 @@ For detail on each available options use : `tcgen encode --help` -The encoding and packaging is performed using [GPAC](http://gpac.io), leveraging [libavcodec](https://ffmpeg.org/libavcodec.html) with [x264](http://www.videolan.org/developers/x264.html) and [x265](https://www.x265.org/) to generate the CMAF content along with a DASH manifest. The intent is to keep the size of the post-processing (e.g. manifest manipulation) as small as possible. +The encoding and packaging is performed using [GPAC](http://gpac.io), leveraging [libavcodec](https://ffmpeg.org/libavcodec.html) with [x264](http://www.videolan.org/developers/x264.html), [x265](https://www.x265.org/) and [VVenC](https://github.com/fraunhoferhhi/vvenc) to generate the CMAF content along with a DASH manifest. The intent is to keep the size of the post-processing (e.g. manifest manipulation) as small as possible. diff --git a/profiles/vvc.mezzanine_v4.csv b/profiles/vvc.mezzanine_v4.csv new file mode 100644 index 0000000..b5ac5cb --- /dev/null +++ b/profiles/vvc.mezzanine_v4.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,mezzanine_prefix_25HZ,mezzanine_prefix_30HZ +1,L1_1920x1080,False,True,hvc1,2.0,multiple,duration,1920x1080,1,8000,30.0,cmf2,vvc1,False,1/1,croatia_,tos_ +#1_enc,L1_1920x1080,False,True,hvc1,2.0,multiple,duration,1920x1080,1,8000,30.0,cmf2,vvc1,True,1/1,croatia_,tos_ +#2,L1_1920x1080,True,False,hev1,2.0,single,pframes,1920x1080,1,8000,30.0,cmfc,vvc1,False,1/1,croatia_,tos_ +#3,L1_1920x1080,True,True,hvc1,2.0,multiple,duration,1920x1080,1,8000,30.0,cmf2,vvc1,False,1/1,croatia_,tos_ +#4,L1_1920x1080,False,False,hvc1,2.0,multiple,duration,1920x1080,1,8000,30.0,cmf2,vvc1,False,1/1,croatia_,tos_ +#5,L1_1920x1080,False,True,hev1,2.0,multiple,duration,1920x1080,1,8000,30.0,cmf2,vvc1,False,1/1,croatia_,tos_ +#6,L1_1920x1080,False,True,hvc1,2.0,multiple,pframes,1920x1080,1,8000,30.0,cmf2,vvc1,False,1/1,croatia_,tos_ +#7,L1_1920x1080,False,True,hvc1,2.0,multiple,duration,1920x1080,1,8000,30.0,cmfc,vvc1,False,1/1,croatia_,tos_ +#10,L1_1920x1080,False,True,hvc1,2.0,multiple,duration,1920x1080,1,8000,60.0,cmf2,vvc1,False,1/1,croatia_,tos_ +#11,L1_1920x1080,False,True,hvc1,2.0,multiple,duration,1920x1080,0.5,5000,60.0,cmf2,vvc1,False,1/1,croatia_,tos_ +#12,J1_1280x720,False,True,hvc1,2.0,multiple,duration,1280x720,0.5,3100,60.0,cmf2,vvc1,False,1/1,croatia_,tos_ diff --git a/src/tcgen/models.py b/src/tcgen/models.py index 26cbcac..70def31 100644 --- a/src/tcgen/models.py +++ b/src/tcgen/models.py @@ -16,6 +16,7 @@ "cud1": ("video", "h265", None), "clg1": ("video", "h265", None), "chd1": ("video", "h265", None), + "vvc1": ("video", "h266", None), "caac": ("audio", "aac", "caac"), "dts1": ("audio", "copy", None), "dts2": ("audio", "copy", None) @@ -239,6 +240,7 @@ class CmafBrand(str, Enum): CUD1: str = "cud1" CLG1: str = "clg1" CHD1: str = "chd1" + VVCCHD: str = "vvc1" def __str__(self): return self.value @@ -253,6 +255,10 @@ def from_string(cls, s): return cls.CLG1 elif s == "chd1" : return cls.CHD1 + elif s == "vvc1" : + return cls.VVCCHD + else: + raise Exception(f'unknown CMAF media profile: {s}') def locate_source_content(tc:'TestContent', fps_family:FPS_FAMILY): m = tc.get_mezzanine(fps_family) @@ -556,7 +562,7 @@ def from_matrix_column(col) -> 'TestContent': # 13 - Duration of stream duration = float(col[12].rstrip('s')) if bool(col[12]) else -1 - # 14 - AVC/HEVC profile and level + # 14 - codec profile and level codec = col[13] # 15 - CMAF media profile diff --git a/src/tcgen/run_encode.py b/src/tcgen/run_encode.py index d978cac..a8a6847 100644 --- a/src/tcgen/run_encode.py +++ b/src/tcgen/run_encode.py @@ -13,6 +13,7 @@ class VideoCodecOptions(Enum): AVC = "h264" HEVC = "h265" + VVC = "h266" class AudioCodecOptions(Enum): AAC = "aac" @@ -105,6 +106,16 @@ class HEVCCHD1: m_resolution_h = "2160" m_frame_rate = 60 +# VVC: ISO/IEC 23000-19 +class VVCCHD: + m_profile = "main10" + m_level = "41" + m_color_primary = "9" # GF_COLOR_PRIM_BT2020 + m_color_trc = "16" # GF_COLOR_TRC_SMPTE2084 + m_colorspace = "9" # GF_COLOR_MX_BT2020_NCL + m_resolution_w = "1920" + m_resolution_h = "1080" + m_frame_rate = 60 # DASHing class DASH: @@ -245,9 +256,10 @@ def __init__(self, representation_config): self.m_media_type = value elif name == "codec": if value != VideoCodecOptions.AVC.value and value != VideoCodecOptions.HEVC.value \ + and value != VideoCodecOptions.VVC.value \ and value != AudioCodecOptions.AAC.value and value != AudioCodecOptions.COPY.value: - print("Supported codecs are AVC denoted by \"h264\" and HEVC denoted by \"h265\" for video, and " - "AAC denoted by \"aac\" or the special value \"copy\" disables the audio transcoding.") + print("Supported codecs are AVC denoted by \"h264\", HEVC (\"h265\") and VVC (\"h266\") for video, and " + "AAC denoted by \"aac\" or the special value \"copy\" disables the audio transcoding. Found \"" + value + "\".") sys.exit(1) self.m_codec = value elif name == "vse": @@ -255,7 +267,7 @@ def __init__(self, representation_config): value != VisualSampleEntry.AVC1p3.value and \ value != VisualSampleEntry.HEV1.value and value != VisualSampleEntry.HVC1.value: print("Supported video sample entries for AVC are \"avc1\", \"avc3\", \"avc1+3\" and" - " for HEVC \"hev1\" and \"hvc1\".") + " for HEVC \"hev1\" and \"hvc1\".") # TODO: add 'vvc1' and 'vvi1' sys.exit(1) else: self.m_video_sample_entry = value @@ -358,6 +370,22 @@ def __init__(self, representation_config): if self.m_resolution_w is None and self.m_resolution_h is None: self.m_resolution_w = HEVCCLG1.m_resolution_w self.m_resolution_h = HEVCCLG1.m_resolution_h + elif value == "vvc1": + if self.m_profile is None: + self.m_profile = VVCCHD.m_profile + if self.m_level is None: + self.m_level = VVCCHD.m_level + if self.m_frame_rate is None: + self.m_frame_rate = VVCCHD.m_frame_rate + if self.m_color_primary is None: + self.m_color_primary = VVCCHD.m_color_primary + if self.m_color_trc is None: + self.m_color_trc = VVCCHD.m_color_trc + if self.m_colorspace is None: + self.m_colorspace = VVCCHD.m_colorspace + if self.m_resolution_w is None and self.m_resolution_h is None: + self.m_resolution_w = VVCCHD.m_resolution_w + self.m_resolution_h = VVCCHD.m_resolution_h else: print("Unknown CMAF profile: " + name) @@ -431,6 +459,7 @@ def format_command(self, i): if self.m_media_type in ("v", "video"): is_avc = self.m_codec == VideoCodecOptions.AVC.value is_hevc = self.m_codec == VideoCodecOptions.HEVC.value + is_vvc = self.m_codec == VideoCodecOptions.VVC.value # Resize command += "ffsws:osize=" + self.m_resolution_w + "x" + self.m_resolution_h @@ -447,6 +476,8 @@ def format_command(self, i): command += ":c=libx264" elif is_hevc: command += ":c=libx265" + elif is_vvc: + command += ":c=vvc" command += ":b=" + self.m_bitrate + "k" command += ":bf=" + str(self.m_num_b_frames) command += ":fintra=" + self.m_segment_duration @@ -489,19 +520,26 @@ def format_command(self, i): if bool(self.m_max_cll_fall): command += f":max-cll={self.m_max_cll_fall}" - if self.m_pic_timing == "True": - if is_avc: - command += ":nal-hrd=vbr" - elif is_hevc: - command += ":hrd=1" + elif is_vvc: + command += "::vvenc-params=\"" + command += "refreshsec=" + self.m_segment_duration + command += ":refreshtype=idr" - # common x264 / x265 options - command += ":vbv-bufsize=" + str(int(self.m_bitrate) * 3) + \ - ":vbv-maxrate=" + str(int(int(self.m_bitrate) * 3 / 2)) + if is_avc or is_hevc: + if self.m_pic_timing == "True": + if is_avc: + command += ":nal-hrd=vbr" + elif is_hevc: + command += ":hrd=1" + + # common x264 / x265 options + command += ":vbv-bufsize=" + str(int(self.m_bitrate) * 3) + \ + ":vbv-maxrate=" + str(int(int(self.m_bitrate) * 3 / 2)) + + # FIXME: VVenC parsing is broken + if self.m_aspect_ratio_x and self.m_aspect_ratio_y: + 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 encoder specific parameters bsrw = None diff --git a/src/tcgen/tcgen.py b/src/tcgen/tcgen.py index 90c9f5a..d1b65ee 100755 --- a/src/tcgen/tcgen.py +++ b/src/tcgen/tcgen.py @@ -11,6 +11,9 @@ import asyncio import xml.etree.ElementTree as ET import shutil +import logging +import sys +import traceback from tcgen.models import TestContent, FPS_FAMILY, locate_source_content, Mezzanine from tcgen.database import Database, most_recent_batch @@ -36,10 +39,10 @@ def cli(ctx): @click.option('-b', '--batch-dir', default=datetime.today().strftime('%Y-%m-%d'), help='batch directory name. default value uses the current date, eg. 2024-12-31') @click.option('--encode/--no-encode', default=True, help="encode content") @click.option('--format-mpd/--no-format-mpd', default=True, help="patch mpd content to match CTA WAVE requirements") -@click.option('-t', '--test-id', help='process only vector with id "-', default=None) +@click.option('-t', '--test-id', help='process only vector with id', default=None) @click.option('-f', '--fps-family', default='ALL', help='process only one of 14.985_29.97_59.94 - 12.5_25_50 - 15_30_60') @click.option('--drm-config', default=(Path(__file__) / '../../../DRM.xml').resolve(), help='path to DRM.xml config file') -@click.option('--dry_run/--no-dry-run', default=False, help="dry run, usefull for debugging") +@click.option('--dry_run/--no-dry-run', default=False, help="dry run, useful for debugging") def encode(ctx, mezzanine, config, vectors_dir, batch_dir, encode, format_mpd, test_id, fps_family, drm_config, dry_run): """ Encode content from MEZZANINE directory into test vectors using content options specified in CONFIG. @@ -73,6 +76,7 @@ def encode(ctx, mezzanine, config, vectors_dir, batch_dir, encode, format_mpd, t patch_mpd(output_mpd, m, tc) except BaseException as e: + traceback.print_exc() print(e) @@ -119,6 +123,7 @@ def export(ctx, mezzanine, config, vectors_dir, database, zip): m = locate_source_content(tv, fps) db.add_entry(tv, m, batch_dir.name) except BaseException as e: + traceback.print_exc() logging.warning(f'{test_entry_key} : {e}') if database is not None: