Skip to content

Commit

Permalink
Merge pull request #33 from yanj-github/main
Browse files Browse the repository at this point in the history
Release v1.0.2
  • Loading branch information
yanj-github committed Oct 22, 2021
2 parents 33add98 + 9311218 commit b33439b
Show file tree
Hide file tree
Showing 15 changed files with 560 additions and 314 deletions.
16 changes: 5 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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
2 changes: 2 additions & 0 deletions config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
261 changes: 209 additions & 52 deletions configuration_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -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
8 changes: 8 additions & 0 deletions global_configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
2 changes: 1 addition & 1 deletion observation_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@

MAJOR = 1
MINOR = 0
PATCH = 1
PATCH = 2
VERSION = f"{MAJOR}.{MINOR}.{PATCH}"

logger = logging.getLogger(__name__)
Expand Down
Loading

0 comments on commit b33439b

Please sign in to comment.