From 93112183eda5285690efacfc2d354e173444feac Mon Sep 17 00:00:00 2001 From: Yan Jiang Date: Thu, 21 Oct 2021 20:06:45 +0100 Subject: [PATCH] Release v1.0.2 --- README.md | 16 +- config.ini | 2 + configuration_parser.py | 261 +++++++++++++---- global_configurations.py | 8 + observation_framework.py | 2 +- observation_result_handler.py | 37 ++- observations/duration_matches_cmaf_track.py | 96 ++++--- .../earliest_sample_same_presentation_time.py | 8 +- observations/every_sample_rendered.py | 272 ++++++++++-------- observations/observation.py | 36 ++- observations/sample_matches_current_time.py | 50 ++-- observations/start_up_delay.py | 56 ++-- ...k_over_wave_baseline_splice_constraints.py | 24 +- test_code/random_access_to_time.py | 2 +- test_code/sequential_track_playback.py | 4 +- 15 files changed, 560 insertions(+), 314 deletions(-) diff --git a/README.md b/README.md index eeffb50..6c97cc4 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,7 @@ For example: Add the new test name, python module, and class name to the *"of_testname_map.json"* file. -# Release Notes for Release v1.0.1 +# Release Notes for Release v1.0.2 ## Implemented: * Installation and usage instructions (in this README). @@ -251,19 +251,13 @@ Add the new test name, python module, and class name to the *"of_testname_map.js * 8.5 switching-set-playback.html * 8.6 regular-playback-of-chunked-content.html * 8.7 regular-playback-of-chunked-content-non-aligned-append.html + * 8.8 playback-over-wave-baseline-splice-constraints.html * 8.9 out-of-order-loading.html * 8.10 overlapping-fragments.html * 8.11 fullscreen-playback-of-switching-sets.html * 8.12 playback-of-encrypted-content.html + * 8.13 restricted-splicing-of-encrypted-content-https.html + * 8.14 sequential-playback-of-encrypted-and-non-encrypted-baseline-content-https.html * 9.2 regular-playback-of-a-cmaf-presentation.html * 9.3 random-access-of-a-wave-presentation.html - -**NOTE:** Due to lack of test content/configuration support, the following functionality has been implemented -but only with hard-coded parameters. These will need amending when support is available: -* 8.8 playback-over-wave-baseline-splice-constraints.html -* 8.13 restricted-splicing-of-encrypted-content-https.html -* 8.14 sequential-playback-of-encrypted-and-non-encrypted-baseline-content-https.html -* 9.4 splicing-of-wave-program-with-baseline-constraints.html - -## TODO -* Final splicing test changes when the test content and configurations are available. + * 9.4 Splicing of WAVE Program with Baseline Constraints diff --git a/config.ini b/config.ini index bc155cf..3de82ea 100644 --- a/config.ini +++ b/config.ini @@ -62,3 +62,5 @@ end_frame_num_tolerance = 0 mid_frame_num_tolerance = 10 splice_start_frame_num_tolerance = 0 splice_end_frame_num_tolerance = 0 +duration_tolerance_ms = 10 +ct_frame_tolerance = 2 diff --git a/configuration_parser.py b/configuration_parser.py index 7614f92..101cb80 100644 --- a/configuration_parser.py +++ b/configuration_parser.py @@ -27,12 +27,11 @@ """ from exceptions import ConfigError import logging -from typing import Dict +from typing import Dict, List import requests import isodate import json -import sys from global_configurations import GlobalConfigurations @@ -46,10 +45,12 @@ class ConfigurationParser: """test runner server url to get configuration files from""" test_config_json: Dict[str, Dict[str, Dict[str, str]]] """loaded test_config.json file in string""" - tests_json: Dict[str, Dict[str, Dict[str, str]]] + tests_json: Dict[str, Dict[str, List[Dict[str, Dict[str, str]]]]] """loaded tests_json file in string""" - test_config: str - """configuration part extracted from test.json file""" + video_config: List[Dict[str, Dict[str, str]]] + """video configuration part extracted from test.json file""" + audio_config: List[Dict[str, Dict[str, str]]] + """audio configuration part extracted from test.json file""" def __init__(self, global_configurations: GlobalConfigurations): self.server_url = global_configurations.get_test_runner_url() @@ -63,6 +64,15 @@ def __init__(self, global_configurations: GlobalConfigurations): self.test_config_json = self._get_json_from_tr("test-config.json") self.tests_json = self._get_json_from_tr("tests.json") + def parse_config( + self, content_type:str + ) -> List[Dict[str, Dict[str, str]]]: + if content_type == "audio": + test_config = self.audio_config + else: + test_config = self.video_config + return test_config + def parse_tests_json(self, test_id: str): """Parse tests json configuration data save content configuration data to parse separately @@ -71,7 +81,8 @@ def parse_tests_json(self, test_id: str): test_path = self.tests_json["tests"][test_id]["path"] test_code = self.tests_json["tests"][test_id]["code"] - self.test_config = self.tests_json["tests"][test_id]["config"] + self.video_config = self.tests_json["tests"][test_id]["switchingSets"]["video"] + self.audio_config = self.tests_json["tests"][test_id]["switchingSets"]["audio"] return test_path, test_code except KeyError as e: @@ -80,62 +91,120 @@ def parse_tests_json(self, test_id: str): f"Detected test id({test_id}) is not defined in \"tests.json\". " ) - - def parse_tests_json_content_config(self, parameters: list, test_path: str) -> dict: - """parse content related config parameters for current test""" + def parse_fragment_duration( + self, + test_path: str, + content_type: str, + parameter: str, + test_config: List[Dict[str, str]] + ) -> dict: + """parse fragment duration + fragment_duration: single track playback + fragment_duration_list: switching set + fragment_duration_multi_mpd: multi-mpd switching sets + """ parameters_dict = {} - - for parameter in parameters: - if parameter == "period_duration": - # TODO: hardcode this for now until the configuration is available on TR - # assume the period_duration 2 or 3 if not raise exception here - parameters_dict[parameter] = [20000, 20000, 20000] - # parameters_dict[parameter] = [20000, 20000] - elif parameter == "fragment_duration": - # fragment_duration_list is used for test when only one representation is used - # for example: sequential playback - for representation in self.test_config["representations"].values(): - if representation["type"] == "video": - try: - if representation["fragment_duration"] == None: - raise TypeError - # the multiplication happens so that we get the fragment duration in ms - parameters_dict[parameter] = representation["fragment_duration"] * 1000 - # we are interested just about the first video representation's fragment duration - break - except (TypeError, KeyError) as e: - raise ConfigError( - f"Failed to get a parameter:{e} for the test '{test_path}'" - ) - elif parameter == "fragment_duration_list": - # fragment_duration_list is used for tests when more then one representation is used - # for example: switching set - # this list is needed to identify the switching points, and to calculate durations - parameters_dict[parameter] = {} - counter = 1 - for representation in self.test_config["representations"].values(): - if representation["type"] == "video": + if parameter == "fragment_duration": + # fragment_duration is used for test when only one representation is used + # for example: sequential playback + for representation in test_config[0]["representations"].values(): + if representation["type"] == content_type: + try: + if representation["fragment_duration"] == None: + raise TypeError + # the multiplication happens so that we get the fragment duration in ms + # we are interested just about the first video representation's fragment duration + parameters_dict[parameter] = representation["fragment_duration"] * 1000 + break + except (TypeError, KeyError) as e: + raise ConfigError( + f"Failed to get a parameter:{e} for the test '{test_path}'" + ) + elif parameter == "fragment_duration_list": + # fragment_duration_list is used for tests when more then one representation is used + # for example: switching set + # this list is needed to identify the switching points, and to calculate durations + parameters_dict[parameter] = {} + rep_index = 1 + for representation in test_config[0]["representations"].values(): + if representation["type"] == content_type: + try: + if representation["fragment_duration"] == None: + raise TypeError + # the multiplication happens so that we get the fragment duration in ms + parameters_dict[parameter][rep_index] = representation["fragment_duration"] * 1000 + rep_index += 1 + except (TypeError, KeyError) as e: + raise ConfigError( + f"Failed to get a parameter:{e} for the test '{test_path}'" + ) + elif parameter == "fragment_duration_multi_mpd": + # fragment_duration_multi_mpd is used for tests when more then mpd is used + # for splicing set. This is an 2D array, this list is needed to identify + # the switching points and splicing point, and to calculate durations + parameters_dict[parameter] = {} + content_index = 1 + rep_index = 1 + for config in test_config: + for representation in config["representations"].values(): + if representation["type"] == content_type: try: if representation["fragment_duration"] == None: raise TypeError # the multiplication happens so that we get the fragment duration in ms - parameters_dict[parameter][counter] = representation["fragment_duration"] * 1000 - counter += 1 + parameters_dict[parameter][(content_index, rep_index)] = representation["fragment_duration"] * 1000 + rep_index += 1 except (TypeError, KeyError) as e: raise ConfigError( f"Failed to get a parameter:{e} for the test '{test_path}'" ) + content_index += 1 + rep_index = 1 + + return parameters_dict + + def parse_cmaf_track_duration( + self, test_path: str, test_config: Dict[str, Dict[str, str]] + ): + """parse cmaf track duration to ms""" + parameters_dict = {} + try: + config_value = isodate.parse_duration(test_config["cmaf_track_duration"]) + ms = config_value.microseconds / 1000 + s_to_ms = config_value.seconds * 1000 + value = ms + s_to_ms + parameters_dict["cmaf_track_duration"] = value + except KeyError as e: + raise ConfigError( + f"Failed to get a parameter:{e} for the test '{test_path}'" + ) + return parameters_dict + + def parse_tests_json_content_config( + self, + parameters: list, + test_path: str, + content_type: str + ) -> dict: + """parse content related config parameters for current test""" + parameters_dict = {} + + # parse video/audio configuration + # TODO: audio parsing, audio observation not implemented + test_config = self.parse_config(content_type) + + # parse parameter one by one + for parameter in parameters: + if parameter == "cmaf_track_duration": + # cmaf_track_duration is only required in single mpd + parameters_dict.update(self.parse_cmaf_track_duration(test_path, + test_config[0])) else: - try: - config_value = isodate.parse_duration(self.test_config[parameter]) - ms = config_value.microseconds / 1000 - s_to_ms = config_value.seconds * 1000 - value = ms + s_to_ms - parameters_dict[parameter] = value - except KeyError as e: - raise ConfigError( - f"Failed to get a parameter:{e} for the test '{test_path}'" - ) + # parse fragment duration handled differently for single mpd and mutiple + parameters_dict.update(self.parse_fragment_duration(test_path, + content_type, + parameter, + test_config)) return parameters_dict @@ -191,3 +260,91 @@ def _get_json_from_local( return config_data except requests.exceptions.RequestException as e: raise ConfigError(e) + + +class PlayoutParser: + """Playout Utility Parsing class""" + + @staticmethod + def get_switching_playout(playout: List[List[int]]) -> List[int]: + """for switching set to extract track ID list + switching set ID column 0 and the fragment ID column 2 + are ignored for swithing set tests + """ + switching_playout = [i[1] for i in playout] + return switching_playout + + @staticmethod + def get_playout_sequence(switching_playout: List[int]): + """for switching set return playout sequence + playout_sequence: a list of track number to identify different track changes + """ + playout_sequence = [switching_playout[0]] + for i in range(1, len(switching_playout)): + # when track change + if switching_playout[i] != switching_playout[i - 1]: + playout_sequence.append(switching_playout[i]) + return playout_sequence + + @staticmethod + def get_splicing_period_list( + playouts: List[List[int]], fragment_duration_multi_mpd: dict + ) -> List[float]: + """ return period duration list of splicing + e.g: [main duration, ad duration, main duration] + """ + period_list = [] + current_period = 0 + switching_set = playouts[0][0] + for playout in playouts: + if playout[0] != switching_set: + period_list.append(current_period) + current_period = 0 + switching_set = playout[0] + current_period += fragment_duration_multi_mpd[(playout[0], playout[1])] + period_list.append(current_period) + return period_list + + @staticmethod + def get_change_type_list(playouts: List[List[int]]) -> List[str]: + """ save ecah change type in a list + """ + change_type_list = [] + switching_set = playouts[0][0] + track_num = playouts[0][1] + for playout in playouts: + if playout[0] != switching_set: + change_type_list.append("splicing") + elif playout[1] != track_num: + change_type_list.append("switching") + switching_set = playout[0] + track_num = playout[1] + return change_type_list + + @staticmethod + def get_ending_playout_list(playouts: List[List[int]]) -> List[List[int]]: + """ when content change save each previous ending playout + """ + ending_playout_list = [] + switching_set = playouts[0][0] + track_num = playouts[0][1] + for i, playout in enumerate(playouts): + if (playout[0] != switching_set or playout[1] != track_num): + ending_playout_list.append(playouts[i-1]) + switching_set = playout[0] + track_num = playout[1] + return ending_playout_list + + @staticmethod + def get_starting_playout_list(playouts: List[List[int]]) -> List[List[int]]: + """ when content change save each current starting playout + """ + starting_playout_list = [] + switching_set = playouts[0][0] + track_num = playouts[0][1] + for playout in playouts: + if (playout[0] != switching_set or playout[1] != track_num): + starting_playout_list.append(playout) + switching_set = playout[0] + track_num = playout[1] + return starting_playout_list \ No newline at end of file diff --git a/global_configurations.py b/global_configurations.py index 063c0b4..342f9c4 100644 --- a/global_configurations.py +++ b/global_configurations.py @@ -150,6 +150,8 @@ def get_tolerances(self) -> Dict[str, int]: "mid_frame_num_tolerance": 0, "splice_start_frame_num_tolerance": 0, "splice_end_frame_num_tolerance": 0, + "duration_tolerance_ms": 10, + "ct_frame_tolerance": 2 } try: tolerances["start_frame_num_tolerance"] = int( @@ -167,6 +169,12 @@ def get_tolerances(self) -> Dict[str, int]: tolerances["splice_end_frame_num_tolerance"] = int( self.config["TOLERANCES"]["splice_end_frame_num_tolerance"] ) + tolerances["duration_tolerance_ms"] = int( + self.config["TOLERANCES"]["duration_tolerance_ms"] + ) + tolerances["ct_frame_tolerance"] = int( + self.config["TOLERANCES"]["ct_frame_tolerance"] + ) except KeyError: pass return tolerances diff --git a/observation_framework.py b/observation_framework.py index 7de80a5..9f72965 100644 --- a/observation_framework.py +++ b/observation_framework.py @@ -49,7 +49,7 @@ MAJOR = 1 MINOR = 0 -PATCH = 1 +PATCH = 2 VERSION = f"{MAJOR}.{MINOR}.{PATCH}" logger = logging.getLogger(__name__) diff --git a/observation_result_handler.py b/observation_result_handler.py index 6b18103..deacf79 100644 --- a/observation_result_handler.py +++ b/observation_result_handler.py @@ -32,6 +32,7 @@ import os import json +from datetime import datetime from json.decoder import JSONDecodeError from typing import List from global_configurations import GlobalConfigurations @@ -48,12 +49,15 @@ class ObservationResultHandler: result_url: str """test runner server url to download and post result""" global_configurations: GlobalConfigurations + observation_config: List[dict] + """list of OF configuration dictionary""" def __init__(self, global_configurations: GlobalConfigurations): self.result_url = ( global_configurations.get_test_runner_url() + "/_wave/api/results/" ) self.global_configurations = global_configurations + self.observation_config = global_configurations.get_tolerances() def _download_result(self, url: str, filename: str) -> None: """Get session result from the Test Runner @@ -111,19 +115,31 @@ def _update_subtest(self, subtests_data: list, observation_results: list) -> lis return subtests_data def _save_result_to_file( - self, filename: str, observation_results: List[dict] + self, + filename: str, + observation_results: List[dict], + observation_time: str ) -> None: """save observation result to a result file""" try: with open(filename, "w") as f: - json.dump(observation_results, f, indent=4) + data = {} + data.update({"meta": {}}) + data["meta"].update({"datetime_observation": observation_time}) + data["meta"].update({"observation_config": self.observation_config}) + data.update({"results": observation_results}) + json.dump(data, f, indent=4) except IOError as e: raise ObsFrameError( f"Error: Unable to write the result file {filename}." ) from e def _update_result_file( - self, filename: str, test_path: str, observation_results: List[dict] + self, + filename: str, + test_path: str, + observation_results: List[dict], + observation_time: str ) -> None: """Update result json file to add observation result to subtest section""" try: @@ -131,6 +147,12 @@ def _update_result_file( with open(filename) as json_file: data = json.load(json_file) + # add meta when it is not defined + if not "meta" in data: + data.update({"meta": {}}) + data["meta"].update({"datetime_observation": observation_time}) + data["meta"].update({"observation_config": self.observation_config}) + for result_data in data["results"]: if ("/" + test_path) == result_data["test"]: result_data["subtests"] = self._update_subtest( @@ -203,6 +225,7 @@ def post_result( filename = result_file_path + "/" + session_token + "/" + api_name + ".json" self._create_results_dir(filename) + observation_time_str = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") # if conf.ini mode is set to DEBUG then just save results to a file # (only used for development) @@ -213,9 +236,13 @@ def post_result( + test_path.replace("/", "-").replace(".html", "") + "_debug.json" ) - self._save_result_to_file(debug_result_filename, observation_results) + self._save_result_to_file( + debug_result_filename, observation_results, observation_time_str + ) else: url = self.result_url + session_token + "/" + api_name + "/json" self._download_result(url, filename) - self._update_result_file(filename, test_path, observation_results) + self._update_result_file( + filename, test_path, observation_results, observation_time_str + ) self._import_result(url, filename) diff --git a/observations/duration_matches_cmaf_track.py b/observations/duration_matches_cmaf_track.py index 1c5a210..a75c1be 100644 --- a/observations/duration_matches_cmaf_track.py +++ b/observations/duration_matches_cmaf_track.py @@ -28,14 +28,11 @@ from .observation import Observation from typing import List, Dict -from dpctf_qr_decoder import MezzanineDecodedQr +from dpctf_qr_decoder import MezzanineDecodedQr, TestStatusDecodedQr from global_configurations import GlobalConfigurations logger = logging.getLogger(__name__) -DURATION_TOLERANCE_MS = 10 - - class DurationMatchesCMAFTrack(Observation): """DurationMatchesCMAFTrack class The playback duration of the playback matches the duration of the CMAF Track, i.e. TR [k, S] = TR [k, 1] + td[k]. @@ -50,59 +47,65 @@ def _get_starting_missing_frame( self, expected_first_frame_num: int, first_qr_code: MezzanineDecodedQr ) -> int: """return missing starting frame numbers that take account in duration check - when missing_frames exceed the start_frame_num_tolerance - duration check only take account start_frame_num_tolerance - Args: expected_first_frame_num: expected frame number of the first frame first_qr_code: first MezzanineDecodedQr from MezzanineDecodedQr lists - Returns: int: missing frame numbers on starting that take account in duration check """ - start_frame_num_tolerance = self.tolerances["start_frame_num_tolerance"] missing_frames = first_qr_code.frame_number - expected_first_frame_num - - if start_frame_num_tolerance < abs(missing_frames): - missing_frames = start_frame_num_tolerance - return missing_frames def _get_ending_missing_frame( self, expected_last_frame_num: int, last_qr_code: MezzanineDecodedQr ) -> int: """Return missing ending frame numbers that take account in duration check - when missing_frames exceed the end_frame_num_tolerance - duration check only take account end_frame_num_tolerance - Args: expected_last_frame_num: expected frame number of the last frame last_qr_code: last MezzanineDecodedQr from MezzanineDecodedQr lists - Returns: - int: missing frame numbers om ending that take account in duration check + int: missing frame numbers on ending that take account in duration check """ - end_frame_num_tolerance = self.tolerances["end_frame_num_tolerance"] missing_frames = expected_last_frame_num - last_qr_code.frame_number + return missing_frames - if end_frame_num_tolerance < abs(missing_frames): - missing_frames = end_frame_num_tolerance + @staticmethod + def get_frame_change_after_play( + mezzanine_qr_codes: List[MezzanineDecodedQr], + test_status_qr_codes: List[TestStatusDecodedQr], + camera_frame_duration_ms: dict, + ) -> int: + first_play_qr_index = 0 + event_found, play_ct = Observation._get_play_event( + test_status_qr_codes, camera_frame_duration_ms + ) - return missing_frames + if event_found: + for i, mezzanine_qr_code in enumerate(mezzanine_qr_codes): + frame_ct = ( + mezzanine_qr_code.first_camera_frame_num * camera_frame_duration_ms + ) + print("play_ct", play_ct, "frame_ct", frame_ct) + if frame_ct >= play_ct: + first_play_qr_index = i + break + print(first_play_qr_index) + return first_play_qr_index def make_observation( self, _unused1, mezzanine_qr_codes: List[MezzanineDecodedQr], - _unused2, + test_status_qr_codes: List[TestStatusDecodedQr], parameters_dict: dict, - _unused3, + _unused2, ) -> Dict[str, str]: """Implements the logic: (QRn.last_camera_frame_num - QRa.first_camera_frame_num) * camera_frame_duration_ms == expected_track_duration +/- tolerance """ logger.info(f"Making observation {self.result['name']}...") + duration_tolerance_ms = self.tolerances["duration_tolerance_ms"] if len(mezzanine_qr_codes) < 2: self.result["status"] = "FAIL" @@ -113,45 +116,50 @@ def make_observation( return self.result camera_frame_duration_ms = parameters_dict["camera_frame_duration_ms"] - first_frame_duration = 1000 / mezzanine_qr_codes[0].frame_rate - last_frame_duration = 1000 / mezzanine_qr_codes[-1].frame_rate - playback_duration = ( - mezzanine_qr_codes[-1].first_camera_frame_num - - mezzanine_qr_codes[1].first_camera_frame_num - ) * camera_frame_duration_ms + last_frame_duration + first_frame_duration - expected_track_duration = parameters_dict["expected_track_duration"] - + first_play_qr_index = self.get_frame_change_after_play( + mezzanine_qr_codes, test_status_qr_codes, camera_frame_duration_ms + ) starting_missing_frame = self._get_starting_missing_frame( parameters_dict["first_frame_num"], mezzanine_qr_codes[0] ) ending_missing_frame = self._get_ending_missing_frame( parameters_dict["last_frame_num"], mezzanine_qr_codes[-1] ) + first_frame_duration = 1000 / mezzanine_qr_codes[0].frame_rate + last_frame_duration = 1000 / mezzanine_qr_codes[-1].frame_rate + + # playback duration get measured from the frame change after play() + # till the last detected frame + playback_duration = ( + mezzanine_qr_codes[-1].first_camera_frame_num + - mezzanine_qr_codes[first_play_qr_index].first_camera_frame_num + ) * camera_frame_duration_ms + last_frame_duration # adjust expected track duration based on the missing frames - expected_track_duration = ( - expected_track_duration - - starting_missing_frame * first_frame_duration - + ending_missing_frame * last_frame_duration + start_frames_to_take_out = starting_missing_frame + first_play_qr_index + expected_duration = ( + parameters_dict["expected_track_duration"] + - start_frames_to_take_out * first_frame_duration + - ending_missing_frame * last_frame_duration ) - - if abs(expected_track_duration - playback_duration) > DURATION_TOLERANCE_MS: + + if abs(expected_duration - playback_duration) > duration_tolerance_ms: self.result["status"] = "FAIL" self.result["message"] = ( f"Playback duration {round(playback_duration, 2)}ms does not match expected duration " - f"{round(expected_track_duration, 2)}ms +/- tolerance of {DURATION_TOLERANCE_MS}ms." + f"{round(expected_duration, 2)}ms +/- tolerance of {duration_tolerance_ms}ms." ) else: self.result["status"] = "PASS" self.result["message"] = ( - f"Playback duration is {round(playback_duration, 2)}ms, CMAF track duration is " - f"{expected_track_duration}ms." + f"Playback duration is {round(playback_duration, 2)}ms, expected track duration is " + f"{round(expected_duration, 2)}ms." ) self.result["message"] += ( - f" Allowed tolerance is {DURATION_TOLERANCE_MS}ms." - f" Starting missing frame tolerance is {starting_missing_frame}." - f" Ending missing frame tolerance is {ending_missing_frame}." + f" Allowed tolerance is {duration_tolerance_ms}ms." + f" Starting missing frame number is {starting_missing_frame}." + f" Ending missing frame number is {ending_missing_frame}." ) logger.debug(f"[{self.result['status']}] {self.result['message']}") diff --git a/observations/earliest_sample_same_presentation_time.py b/observations/earliest_sample_same_presentation_time.py index e0e2524..5c78b77 100644 --- a/observations/earliest_sample_same_presentation_time.py +++ b/observations/earliest_sample_same_presentation_time.py @@ -28,6 +28,7 @@ from .sample_matches_current_time import SampleMatchesCurrentTime from typing import List, Dict from dpctf_qr_decoder import MezzanineDecodedQr, TestStatusDecodedQr +from global_configurations import GlobalConfigurations logger = logging.getLogger(__name__) @@ -39,9 +40,9 @@ class EarliestSampleSamePresentationTime(SampleMatchesCurrentTime): presentation time. """ - def __init__(self, _): + def __init__(self, global_configurations: GlobalConfigurations): super().__init__( - None, + global_configurations, "[OF] Video only: The presentation starts with the earliest video sample and the audio sample " "that corresponds to the same presentation time.", ) @@ -67,6 +68,7 @@ def make_observation( camera_frame_rate = parameters_dict["camera_frame_rate"] camera_frame_duration_ms = parameters_dict["camera_frame_duration_ms"] + ct_frame_tolerance = self.tolerances["ct_frame_tolerance"] allowed_tolerance = parameters_dict["tolerance"] self.result["message"] += f"Allowed tolerance is {allowed_tolerance}." @@ -84,6 +86,7 @@ def make_observation( camera_frame_duration_ms, camera_frame_rate, mezzanine_qr_codes, + ct_frame_tolerance ) diff_found, time_diff = self._find_diff_within_tolerance( mezzanine_qr_codes, @@ -91,6 +94,7 @@ def make_observation( first_possible, last_possible, allowed_tolerance, + ct_frame_tolerance ) if not diff_found: self.result["status"] = "FAIL" diff --git a/observations/every_sample_rendered.py b/observations/every_sample_rendered.py index f77c203..328138b 100644 --- a/observations/every_sample_rendered.py +++ b/observations/every_sample_rendered.py @@ -32,6 +32,7 @@ from global_configurations import GlobalConfigurations from exceptions import ObsFrameTerminate from test_code.test import TestType +from configuration_parser import PlayoutParser logger = logging.getLogger(__name__) @@ -206,6 +207,53 @@ def _check_every_frame( return check + def get_content_change_position( + self, + mezzanine_qr_codes: List[MezzanineDecodedQr], + ) -> List[int]: + """ loop through the detected mezzanine list to save + content change positions + """ + current_content_id = mezzanine_qr_codes[0].content_id + current_frame_rate = mezzanine_qr_codes[0].frame_rate + change_starting_index_list = [0] + for i in range(1, len(mezzanine_qr_codes)): + if ( + mezzanine_qr_codes[i].content_id != current_content_id + or mezzanine_qr_codes[i].frame_rate != current_frame_rate + ): + # the content did change save the starting index + change_starting_index_list.append(i) + current_content_id = mezzanine_qr_codes[i].content_id + current_frame_rate = mezzanine_qr_codes[i].frame_rate + return change_starting_index_list + + def check_every_frame_by_block( + self, + mezzanine_qr_codes: List[MezzanineDecodedQr], + change_index_list: List[int], + playout_sequence: List[int] + ) -> bool: + """check mid frames for block by block + each block is different track + """ + mid_frame_result = True + for i, starting_index in enumerate(change_index_list): + if starting_index == change_index_list[-1]: + # check mid frames for last block + mid_frame_result = mid_frame_result and self._check_every_frame( + mezzanine_qr_codes[change_index_list[-1]:], + playout_sequence[-1] + ) + else: + last_index = change_index_list[i + 1] - 1 + mid_frame_result = mid_frame_result and self._check_every_frame( + mezzanine_qr_codes[starting_index:last_index], + playout_sequence[i] + ) + + return mid_frame_result + def observe_switching_mid_frame( self, mezzanine_qr_codes: List[MezzanineDecodedQr], @@ -216,37 +264,25 @@ def observe_switching_mid_frame( playback more than one representations Parse playout parameter to a list of switching point in media timeline - switching_times: a list of switching point in media timeline + switching_positions: a list of switching position in media timeline check every switching points starting frame and ending frame check that the samples shall be rendered in increasing order within the same representations for QRb to QRn: QR[i-1].mezzanine_frame_num + 1 == QR[i].mezzanine_frame_num """ - switching_times = [] - switching_point = 0 - switching_times.append(switching_point) - playout_sequence = [playout[0]] + switching_positions = [] + switching_position = 0 + switching_positions.append(switching_position) for i in range(1, len(playout)): - switching_point += fragment_duration_list[playout[i]] + switching_position += fragment_duration_list[playout[i]] + # when track change if playout[i] != playout[i - 1]: - switching_times.append(switching_point) - playout_sequence.append(playout[i]) + switching_positions.append(switching_position) - current_content_id = mezzanine_qr_codes[0].content_id - current_frame_rate = mezzanine_qr_codes[0].frame_rate - change_switching_index_list = [0] - for i in range(1, len(mezzanine_qr_codes)): - if ( - mezzanine_qr_codes[i].content_id != current_content_id - or mezzanine_qr_codes[i].frame_rate != current_frame_rate - ): - # the content did change - change_switching_index_list.append(i) - current_content_id = mezzanine_qr_codes[i].content_id - current_frame_rate = mezzanine_qr_codes[i].frame_rate + change_switching_index_list = self.get_content_change_position(mezzanine_qr_codes) - # check configuration and actual switching matches and validate configuration - configured_switching_num = len(switching_times) + # check configuration and actual switching matches + configured_switching_num = len(switching_positions) actual_switching_num = len(change_switching_index_list) if actual_switching_num != configured_switching_num: self.result["message"] += ( @@ -256,37 +292,18 @@ def observe_switching_mid_frame( ) return False - if configured_switching_num == 1: - self.result["message"] += ( - f" Switching test configuration is not correct on Test Runner. " - f"The switching test should switch at least once. " - ) - return False - # check mid frames block by block - - mid_frame_result = True - for i, starting_index in enumerate(change_switching_index_list): - if starting_index == change_switching_index_list[-1]: - # check mid frames for last block - check_frame_result = self._check_every_frame( - mezzanine_qr_codes[change_switching_index_list[-1]:], - playout_sequence[-1] - ) - mid_frame_result = mid_frame_result and check_frame_result - else: - last_index = change_switching_index_list[i + 1] - 1 - check_frame_result = self._check_every_frame( - mezzanine_qr_codes[starting_index:last_index], - playout_sequence[i] - ) - mid_frame_result = mid_frame_result and check_frame_result + playout_sequence = PlayoutParser.get_playout_sequence(playout) + mid_frame_result = self.check_every_frame_by_block( + mezzanine_qr_codes, change_switching_index_list, playout_sequence + ) + for i, starting_index in enumerate(change_switching_index_list): if i > 0: # check previous ending frame and new starting frame numbers # the expected frame number position in the content being switched from is the expected relative time # of the switch (derived from the test config) * that content's frames per second previous_ending_frame_num = round( - switching_times[i] + switching_positions[i] / 1000 * mezzanine_qr_codes[starting_index - 1].frame_rate ) @@ -307,7 +324,7 @@ def observe_switching_mid_frame( # compare expected with the actual frame number detected at this switch point current_starting_frame_num = ( round( - switching_times[i] + switching_positions[i] / 1000 * mezzanine_qr_codes[starting_index].frame_rate ) @@ -327,75 +344,64 @@ def observe_switching_mid_frame( return mid_frame_result def observe_splicing_mid_frame( - self, mezzanine_qr_codes: List[MezzanineDecodedQr], period_duration: List[float] + self, mezzanine_qr_codes: List[MezzanineDecodedQr], + playouts: List[List[int]], + fragment_duration_multi_mpd: dict ) -> bool: - """splicing set tests has 2 or 3 playback periods - 3 perids: main content - ad insertion - main content - 2 perids: main content - ad insertion + """playout[i]: Provides the triple (Switching Set, CMAF track number, Fragment number) + for every playout position i=1,…,N that is be played out. on each splicing point: check previous ending and new starting frames are correct for each periods check that the samples shall be rendered in increasing order within the same period for QRb to QRn: QR[i-1].mezzanine_frame_num + 1 == QR[i].mezzanine_frame_num """ - current_content_id = mezzanine_qr_codes[0].content_id - current_frame_rate = mezzanine_qr_codes[0].frame_rate - period_starting_index_list = [0] - for i in range(1, len(mezzanine_qr_codes)): - if ( - mezzanine_qr_codes[i].content_id != current_content_id - or mezzanine_qr_codes[i].frame_rate != current_frame_rate - ): - # the content did change - period_starting_index_list.append(i) - current_content_id = mezzanine_qr_codes[i].content_id - current_frame_rate = mezzanine_qr_codes[i].frame_rate - - configured_splicing_num = len(period_duration) - actual_splicing_num = len(period_starting_index_list) - if actual_splicing_num != configured_splicing_num: - self.result["message"] += ( - f" Number of splices does not match. " - f"Test is configured to splice {configured_splicing_num} times. " - f"Actual number of splices is {actual_splicing_num}. " - ) - return False - - if configured_splicing_num != 3 and configured_splicing_num != 2: - self.result["message"] += ( - f" Number of splices is incorrect. " - f"The splicing test should splice 2 or 3 times. " - f"but the configured number of splices is {configured_splicing_num}. " - ) - return False - splice_start_frame_num_tolerance = self.tolerances[ "splice_start_frame_num_tolerance" ] splice_end_frame_num_tolerance = self.tolerances[ "splice_end_frame_num_tolerance" ] - # check mid frames block by block + + change_starting_index_list = self.get_content_change_position(mezzanine_qr_codes) + change_type_list = PlayoutParser.get_change_type_list(playouts) + ending_playout_list = PlayoutParser.get_ending_playout_list(playouts) + starting_playout_list = PlayoutParser.get_starting_playout_list(playouts) + + # check if the configured content change and actual content change matches + # if not report error + actual_change_num = len(change_starting_index_list) + configured_change_num = len(change_type_list) + 1 + if actual_change_num != configured_change_num: + self.result["message"] += ( + f" Number of changes does not match the 'playout' configuration. " + f"Test is configured to change {configured_change_num} times. " + f"Actual number of change is {actual_change_num}. " + ) + return False + # check mid frames block by block based on the starting index of content change mid_frame_result = True - for i, starting_index in enumerate(period_starting_index_list): - if starting_index == period_starting_index_list[-1]: + for i, starting_index in enumerate(change_starting_index_list): + if starting_index == change_starting_index_list[-1]: # check mid frames for last block - mid_frame_result = mid_frame_result and self._check_every_frame( - mezzanine_qr_codes[period_starting_index_list[-1]:] + check_frame_result = mid_frame_result and self._check_every_frame( + mezzanine_qr_codes[change_starting_index_list[-1]:] ) + mid_frame_result = mid_frame_result and check_frame_result else: - last_index = period_starting_index_list[i + 1] - 1 - mid_frame_result = mid_frame_result and self._check_every_frame( + last_index = change_starting_index_list[i + 1] - 1 + check_frame_result = mid_frame_result and self._check_every_frame( mezzanine_qr_codes[starting_index:last_index] ) + mid_frame_result = mid_frame_result and check_frame_result if i > 0: # check previous ending frame and new starting frame numbers - # the expected frame number position in the content being switched from is the expected relative time - # of the switch (derived from the test config) * that content's frames per second + ending_playout = ending_playout_list[i -1] + ending_fragment_duration = fragment_duration_multi_mpd[(ending_playout[0], ending_playout[1])] + ending_fragment_num = ending_playout[2] previous_ending_frame_num = round( - period_duration[i] - / 1000 + ending_fragment_num * ending_fragment_duration / 1000 * mezzanine_qr_codes[starting_index - 1].frame_rate ) @@ -405,41 +411,58 @@ def observe_splicing_mid_frame( - previous_ending_frame_num ) - if diff_ending_frame > splice_end_frame_num_tolerance: - mid_frame_result = False - self.result["message"] += ( - f" Ending with incorrect frame when splicing at period number {i}. " - f"Ending frame found is {mezzanine_qr_codes[starting_index -1].frame_number }, " - f"expected to end with {previous_ending_frame_num}. " - f"Splice end frame tolerance is {splice_end_frame_num_tolerance}." - ) - - if i == 2: - # if splice back to the main content start from where it left off - current_starting_frame_num = ( - round( - period_duration[0] - / 1000 - * mezzanine_qr_codes[starting_index].frame_rate + if change_type_list[i-1] == "splicing": + if diff_ending_frame > splice_end_frame_num_tolerance: + mid_frame_result = False + self.result["message"] += ( + f" Ending with incorrect frame when splicing at period number {i}. " + f"Ending frame found is {mezzanine_qr_codes[starting_index -1].frame_number }, " + f"expected to end with {previous_ending_frame_num}. " + f"Splice end frame tolerance is {splice_end_frame_num_tolerance}." ) - + 1 - ) else: - current_starting_frame_num = 1 + if diff_ending_frame > 0: + mid_frame_result = False + self.result["message"] += ( + f" Ending with incorrect frame when switching at number {i}. " + f"Ending frame found is {mezzanine_qr_codes[starting_index -1].frame_number }, " + f"expected to end with {previous_ending_frame_num}. " + ) + + starting_playout = starting_playout_list[i -1] + starting_fragment_duration = fragment_duration_multi_mpd[(starting_playout[0], starting_playout[1])] + starting_fragment_num = starting_playout[2] - 1 + current_starting_frame_num = ( + round( + starting_fragment_num * starting_fragment_duration / 1000 + * mezzanine_qr_codes[starting_index].frame_rate + ) + + 1 + ) # compare expected with the actual frame number detected at this splice point diff_starting_frame = abs( mezzanine_qr_codes[starting_index].frame_number - current_starting_frame_num ) - if diff_starting_frame > splice_start_frame_num_tolerance: - mid_frame_result = False - self.result["message"] += ( - f" Starting from incorrect frame when splicing at period number {i + 1}. " - f"Starting frame found is {mezzanine_qr_codes[starting_index].frame_number }, " - f"expected to start from {current_starting_frame_num}. " - f"Splice start frame tolerance is {splice_start_frame_num_tolerance}." - ) + + if change_type_list[i-1] == "splicing": + if diff_starting_frame > splice_start_frame_num_tolerance: + mid_frame_result = False + self.result["message"] += ( + f" Starting from incorrect frame when splicing at period number {i + 1}. " + f"Starting frame found is {mezzanine_qr_codes[starting_index].frame_number }, " + f"expected to start from {current_starting_frame_num}. " + f"Splice start frame tolerance is {splice_start_frame_num_tolerance}." + ) + else: + if diff_starting_frame > 0: + mid_frame_result = False + self.result["message"] += ( + f" Starting from incorrect frame when switching at number {i + 1}. " + f"Starting frame found is {mezzanine_qr_codes[starting_index].frame_number }, " + f"expected to start from {current_starting_frame_num}. " + ) return mid_frame_result @@ -488,14 +511,17 @@ def make_observation( ) if test_type == TestType.SWITCHING: + switching_playout = PlayoutParser.get_switching_playout(parameters_dict["playout"]) mid_frame_result = self.observe_switching_mid_frame( mezzanine_qr_codes, - parameters_dict["playout"], + switching_playout, parameters_dict["fragment_duration_list"], ) elif test_type == TestType.SPLICING: mid_frame_result = self.observe_splicing_mid_frame( - mezzanine_qr_codes, parameters_dict["period_duration"] + mezzanine_qr_codes, + parameters_dict["playout"], + parameters_dict["fragment_duration_multi_mpd"], ) else: # check that the samples shall be rendered in increasing order: diff --git a/observations/observation.py b/observations/observation.py index 41a239d..2bad9b0 100644 --- a/observations/observation.py +++ b/observations/observation.py @@ -24,8 +24,9 @@ """ import logging -from typing import Dict +from typing import List, Dict, Tuple from global_configurations import GlobalConfigurations +from dpctf_qr_decoder import TestStatusDecodedQr logger = logging.getLogger(__name__) @@ -62,3 +63,36 @@ def __init__(self, name: str, global_configurations: GlobalConfigurations = None else: self.tolerances = global_configurations.get_tolerances() self.missing_frame_threshold = global_configurations.get_missing_frame_threshold() + + @staticmethod + def _get_play_event( + test_status_qr_codes: List[TestStatusDecodedQr], + camera_frame_duration_ms: float, + ) -> (Tuple[bool, float]): + """loop through event qr code to find 1st playing play event + + Args: + test_status_qr_codes (List[TestStatusDecodedQr]): Test Status QR codes list containing + currentTime as reported by MSE. + camera_frame_duration_ms (float): duration of a camera frame on msecs. + + Returns: + (bool, float): True if the 1st play event is found, play_current_time from the 1st test runner play event. + """ + for i in range(0, len(test_status_qr_codes)): + current_status = test_status_qr_codes[i] + + # check for the 1st play action from TR events + if current_status.last_action == "play": + play_event_camera_frame_num = current_status.camera_frame_num + + if i + 1 < len(test_status_qr_codes): + next_status = test_status_qr_codes[i + 1] + previous_qr_generation_delay = next_status.delay + play_current_time = ( + play_event_camera_frame_num * camera_frame_duration_ms + ) - previous_qr_generation_delay + return True, play_current_time + break + + return False, 0 diff --git a/observations/sample_matches_current_time.py b/observations/sample_matches_current_time.py index 2132c69..fc539fb 100644 --- a/observations/sample_matches_current_time.py +++ b/observations/sample_matches_current_time.py @@ -32,6 +32,8 @@ from typing import List, Dict, Tuple from dpctf_qr_decoder import MezzanineDecodedQr, TestStatusDecodedQr from test_code.test import TestType +from configuration_parser import PlayoutParser +from global_configurations import GlobalConfigurations logger = logging.getLogger(__name__) @@ -47,13 +49,13 @@ class SampleMatchesCurrentTime(Observation): The presented sample matches the one reported by the currentTime value within the tolerance of the sample duration. """ - def __init__(self, _, name: str = None): + def __init__(self, global_configurations: GlobalConfigurations, name: str = None): if name is None: name = ( "[OF] The presented sample matches the one reported by the currentTime value within the " "tolerance of the sample duration." ) - super().__init__(name) + super().__init__(name, global_configurations) @staticmethod def _get_target_camera_frame_num( @@ -62,7 +64,8 @@ def _get_target_camera_frame_num( camera_frame_duration_ms: float, camera_frame_rate: float, mezzanine_qr_codes: List[MezzanineDecodedQr], - ) -> (float, float): + ct_frame_tolerance: int + ) -> (Tuple[float, float]): """Calculate expected target camera frame numbers of the current time event by compensating for the delay in the QR code generation and applying tolerances. sample_tolerance_in_recording = 1000/mezzanine_frame_rate/(1000/camera_frame_rate) @@ -74,6 +77,7 @@ def _get_target_camera_frame_num( camera_frame_duration_ms (float): duration of a camera frame on msecs. camera_frame_rate: recording frame rate mezzanine_qr_codes (List[MezzanineDecodedQr]): Ordered list of unique mezzanine QR codes found. + ct_frame_tolerance(int): OF tolerance of frame number configured in config.ini Returns: First and last possible camera frame numbers which we expect may match the status event QR currentTime. @@ -86,7 +90,7 @@ def _get_target_camera_frame_num( mezzanine_frame_rate = mezzanine_qr_codes[i - 1].frame_rate break - sample_tolerance_in_recording = camera_frame_rate / mezzanine_frame_rate + sample_tolerance_in_recording = ct_frame_tolerance * camera_frame_rate / mezzanine_frame_rate first_possible = ( target_camera_frame_num - CAMERA_FRAME_ADJUSTMENT @@ -107,7 +111,8 @@ def _find_diff_within_tolerance( first_possible: float, last_possible: float, allowed_tolerance: float, - ) -> (bool, float): + ct_frame_tolerance: int + ) -> (Tuple[bool, float]): """Applies the logic: for first_possible_camera_frame_num_of_target to last_possible_camera_frame_num_of_target foreach mezzanine_qr_code on camera_frame @@ -120,6 +125,7 @@ def _find_diff_within_tolerance( first_possible (float): First point (as fractional camera frame number) that could contain currentTime. last_possible (float): Last point (as fractional camera frame number) that could contain currentTime. allowed_tolerance (float): Test-specific tolerance as specified in test-config.json. + ct_frame_tolerance(int): OF tolerance of frame number configured in config.ini. Returns: (bool, float): True if time difference passed, Actual time difference detected. @@ -139,7 +145,7 @@ def _find_diff_within_tolerance( if new_time_diff < time_diff: time_diff = new_time_diff - if time_diff <= allowed_tolerance + 1000 / code.frame_rate: + if time_diff <= allowed_tolerance + ct_frame_tolerance * 1000 / code.frame_rate: diff_found = True break @@ -169,9 +175,9 @@ def make_observation( time_diff_file: str, ) -> Dict[str, str]: """Implements the logic: - sample_tolerance_in_recording = 1000/mezzanine_frame_rate/(1000/camera_frame_rate) - = camera_frame_rate/mezzanine_frame_rate - sample_tolerance = 1000/mezzanine_frame_rate + sample_tolerance_in_recording = ct_frame_tolerance * 1000/mezzanine_frame_rate/(1000/camera_frame_rate) + = ct_frame_tolerance * camera_frame_rate/mezzanine_frame_rate + sample_tolerance = ct_frame_tolerance * 1000/mezzanine_frame_rate target_camera_frame_num_of_ct_event = ct_event.first_seen_camera_frame_num - (ct_event.d / camera_frame_duration_ms) @@ -195,6 +201,7 @@ def make_observation( camera_frame_rate = parameters_dict["camera_frame_rate"] camera_frame_duration_ms = parameters_dict["camera_frame_duration_ms"] + ct_frame_tolerance = self.tolerances["ct_frame_tolerance"] allowed_tolerance = parameters_dict["tolerance"] failure_report_count = 0 self.result["message"] += f" Allowed tolerance is {allowed_tolerance}." @@ -204,29 +211,28 @@ def make_observation( # media time for period 3 starts from where it was left but need to add the ad insertion duration # so the actual media time is += period_duration[1] if test_type == TestType.SPLICING: + period_list = PlayoutParser.get_splicing_period_list( + parameters_dict["playout"], parameters_dict["fragment_duration_multi_mpd"] + ) + change_type_list = PlayoutParser.get_change_type_list(parameters_dict["playout"]) + period_index = 0 + change_count = 0 current_content_id = mezzanine_qr_codes[0].content_id current_frame_rate = mezzanine_qr_codes[0].frame_rate for i in range(1, len(mezzanine_qr_codes)): if (mezzanine_qr_codes[i].content_id != current_content_id or mezzanine_qr_codes[i].frame_rate != current_frame_rate): # the content did change - period_index += 1 + change_count += 1 current_content_id = mezzanine_qr_codes[i].content_id current_frame_rate = mezzanine_qr_codes[i].frame_rate + if change_type_list[change_count-1] == "splicing": + period_index += 1 + if period_index > 0: - mezzanine_qr_codes[i].media_time += parameters_dict[ - "period_duration" - ][period_index - 1] - - if period_index > len(parameters_dict["period_duration"]): - self.result["status"] = "FAIL" - self.result[ - "message" - ] += f" The number of periods does not match with the configuration. " - logger.debug(f"[{self.result['status']}]: {self.result['message']}") - return self.result + mezzanine_qr_codes[i].media_time += period_list[period_index - 1] time_differences = [] @@ -243,6 +249,7 @@ def make_observation( camera_frame_duration_ms, camera_frame_rate, mezzanine_qr_codes, + ct_frame_tolerance ) diff_found, time_diff = self._find_diff_within_tolerance( mezzanine_qr_codes, @@ -250,6 +257,7 @@ def make_observation( first_possible, last_possible, allowed_tolerance, + ct_frame_tolerance ) # The multiplication happens so that we get the results in ms time_differences.append((current_status.current_time * 1000, time_diff)) diff --git a/observations/start_up_delay.py b/observations/start_up_delay.py index f78270c..24710af 100644 --- a/observations/start_up_delay.py +++ b/observations/start_up_delay.py @@ -44,39 +44,6 @@ def __init__(self, _): "[OF] The start-up delay should be sufficiently low, i.e., TR [k, 1] - Ti < TSMax." ) - @staticmethod - def _get_play_event( - test_status_qr_codes: List[TestStatusDecodedQr], - camera_frame_duration_ms: float, - ) -> (bool, float): - """loop through event qr code to find 1st playing play event - - Args: - test_status_qr_codes (List[TestStatusDecodedQr]): Test Status QR codes list containing - currentTime as reported by MSE. - camera_frame_duration_ms (float): duration of a camera frame on msecs. - - Returns: - (bool, float): True if the 1st play event is found, play_current_time from the 1st test runner play event. - """ - for i in range(0, len(test_status_qr_codes)): - current_status = test_status_qr_codes[i] - - # check for the 1st play action from TR events - if current_status.last_action == "play": - play_event_camera_frame_num = current_status.camera_frame_num - - if i + 1 < len(test_status_qr_codes): - next_status = test_status_qr_codes[i + 1] - previous_qr_generation_delay = next_status.delay - play_current_time = ( - play_event_camera_frame_num * camera_frame_duration_ms - ) - previous_qr_generation_delay - return True, play_current_time - break - - return False, 0 - def make_observation( self, _unused, @@ -111,22 +78,33 @@ def make_observation( max_permitted_startup_delay_ms = parameters_dict["ts_max"] camera_frame_duration_ms = parameters_dict["camera_frame_duration_ms"] - first_frame_current_time = ( - mezzanine_qr_codes[1].first_camera_frame_num * camera_frame_duration_ms - - float(1000 / mezzanine_qr_codes[0].frame_rate) - ) - event_found, play_current_time = self._get_play_event( + event_found, play_ct = Observation._get_play_event( test_status_qr_codes, camera_frame_duration_ms ) + + frame_change_found = False + for mezzanine_qr_code in mezzanine_qr_codes: + frame_ct = ( + mezzanine_qr_code.first_camera_frame_num * camera_frame_duration_ms + ) + if frame_ct > play_ct: + frame_change_found = True + break + if not event_found: self.result["status"] = "FAIL" self.result["message"] = ( f"A test status QR code with first 'play' last_action " f"followed by a further test status QR code was not found." ) + elif not frame_change_found: + self.result["status"] = "FAIL" + self.result["message"] = ( + f"No frame change detected after 'play'." + ) else: - start_up_delay = first_frame_current_time - play_current_time + start_up_delay = frame_ct - play_ct self.result["message"] = ( f"Maximum permitted startup delay is {max_permitted_startup_delay_ms}ms." f"The presentation start up delay is {round(start_up_delay, 4)}ms" diff --git a/test_code/playback_over_wave_baseline_splice_constraints.py b/test_code/playback_over_wave_baseline_splice_constraints.py index d52ce99..8381ebe 100644 --- a/test_code/playback_over_wave_baseline_splice_constraints.py +++ b/test_code/playback_over_wave_baseline_splice_constraints.py @@ -48,29 +48,29 @@ def _init_parameters(self) -> None: """initialise the test_config_parameters required for the test""" self.parameters = [ "ts_max", - "tolerance" + "tolerance", + "playout" ] self.content_parameters = [ - "cmaf_track_duration", - "period_duration" + "fragment_duration_multi_mpd" ] def _get_last_frame_num(self, frame_rate: float) -> int: """return last frame number - this is calculated based on last period duration + this is calculated based on last track duration """ - last_frame_num = round( - self.parameters_dict["period_duration"][-1] - / 1000 - * frame_rate - ) + last_playout = self.parameters_dict["playout"][-1] + fragment_duration = self.parameters_dict["fragment_duration_multi_mpd"][(last_playout[0], last_playout[1])] + last_track_duration = fragment_duration * last_playout[2] + last_frame_num = round(last_track_duration / 1000 * frame_rate) return last_frame_num def _get_expected_track_duration(self) -> float: """return expected CMAF duration - for splicing test this is sum of all period duration + for splicing test this is sum of all fragment duration from the playout """ cmaf_track_duration = 0 - for duration in self.parameters_dict["period_duration"]: - cmaf_track_duration += duration + for playout in self.parameters_dict["playout"]: + fragment_duration = self.parameters_dict["fragment_duration_multi_mpd"][(playout[0], playout[1])] + cmaf_track_duration += fragment_duration return cmaf_track_duration diff --git a/test_code/random_access_to_time.py b/test_code/random_access_to_time.py index 0351561..3de4460 100644 --- a/test_code/random_access_to_time.py +++ b/test_code/random_access_to_time.py @@ -50,7 +50,7 @@ def _get_first_frame_num(self, frame_rate: float) -> int: """return first frame number""" random_access_time = self.parameters_dict["random_access_time"] first_frame_num = round(random_access_time * frame_rate) - return first_frame_num + return first_frame_num + 1 def _get_expected_track_duration(self) -> float: """return expected cmaf track duration""" diff --git a/test_code/sequential_track_playback.py b/test_code/sequential_track_playback.py index 0490ff3..cd4ce6a 100644 --- a/test_code/sequential_track_playback.py +++ b/test_code/sequential_track_playback.py @@ -28,7 +28,7 @@ import logging from .test import TestType -from typing import List +from typing import List, Optional from configuration_parser import ConfigurationParser from global_configurations import GlobalConfigurations from dpctf_qr_decoder import MezzanineDecodedQr, TestStatusDecodedQr @@ -122,7 +122,7 @@ def _load_parameters_dict( ) self.parameters_dict.update( configuration_parser.parse_tests_json_content_config( - self.content_parameters, test_path + self.content_parameters, test_path, "video" ) )