Skip to content

Commit 2732ada

Browse files
sivakondri-CTmemnochproxy
authored andcommitted
lf_interop_video_streaming.py : Integrate iot test results and reports into video streaming report
VERIFIED CLI 1 : python3 -u lf_interop_video_streaming.py --mgr 192.168.242.2 \ --duration 1m \ --url https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd \ --test_name videostreaming \ --webgui_incremental 1 \ --media_source dash \ --media_quality 4k \ --postcleanup \ --precleanup \ --iot_test \ --iot_testname "IotVideoStreaming" VERIFIED CLI 2 : python3 -u lf_interop_video_streaming.py --mgr 192.168.242.2 \ --duration 1m \ --url https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd \ --test_name videostreaming \ --webgui_incremental 1 \ --media_source dash \ --media_quality 4k \ --postcleanup \ --precleanup Signed-off-by: sivakondri-CT <kondru.sankar@candelatech.com>
1 parent 670802d commit 2732ada

1 file changed

Lines changed: 194 additions & 21 deletions

File tree

py-scripts/lf_interop_video_streaming.py

Lines changed: 194 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@
5959
python3 lf_interop_video_streaming.py --mgr 192.168.214.219 --url "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8" --media_source hls
6060
--media_quality 1080P --duration 1m --device_list 1.10,1.12 --debug --test_name video_streaming_test --expected_passfail_value 5
6161
62+
Example-11: Command Line Interface to run the Test along with IOT without device list
63+
python3 -u lf_interop_video_streaming.py --mgr 192.168.242.2 --duration 1m --url https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd --test_name videostreaming
64+
--webgui_incremental 1 --media_source dash --media_quality 4k --postcleanup --precleanup --iot_test --iot_testname "IotVideoStreaming"
65+
66+
Example-12: Command Line Interface to run the Test along with IOT with device list
67+
python3 -u lf_interop_video_streaming.py --mgr 192.168.242.2 --duration 1m --url https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd --test_name videostreaming
68+
--webgui_incremental 1 --media_source dash --media_quality 4k --postcleanup --precleanup --iot_test --iot_testname "IotVideoStreaming" --iot_device_list "switch.smart_plug_1_socket_1"
6269
6370
6471
SCRIPT CLASSIFICATION: Test
@@ -97,6 +104,7 @@
97104
from lf_graph import lf_bar_graph_horizontal
98105
from lf_graph import lf_line_graph
99106
import threading
107+
from collections import OrderedDict
100108

101109

102110
if sys.version_info[0] != 3:
@@ -107,6 +115,7 @@
107115
base = importlib.import_module('py-scripts.lf_base_interop_profile')
108116
lf_csv = importlib.import_module("py-scripts.lf_csv")
109117
realm = importlib.import_module("py-json.realm")
118+
LFCliBase = realm.LFCliBase
110119
Realm = realm.Realm
111120
base_RealDevice = base.RealDevice
112121
lf_report = importlib.import_module("py-scripts.lf_report")
@@ -1205,7 +1214,7 @@ def handle_passfail_criteria(self, data: dict):
12051214
'Average Rx Rate (Mbps)': avg_rx_rate_list
12061215
}
12071216

1208-
def generate_report(self, date, iterations_before_test_stopped_by_user, test_setup_info, realtime_dataset, report_path=''):
1217+
def generate_report(self, date, iterations_before_test_stopped_by_user, test_setup_info, realtime_dataset, report_path='', iot_summary=None):
12091218
logging.info("Creating Reports")
12101219
# Initialize the report object
12111220
if self.dowebgui and report_path == '':
@@ -1225,15 +1234,30 @@ def generate_report(self, date, iterations_before_test_stopped_by_user, test_set
12251234
keys = list(self.http_profile.created_cx.keys())
12261235

12271236
# Set report title, date, and build banner
1228-
report.set_title("Video Streaming Test")
1237+
report.set_title("Video Streaming Test Including IoT Devices" if iot_summary else "Video Streaming Test")
12291238
report.set_date(date)
12301239
report.build_banner()
1231-
report.set_obj_html("Objective", " The Candela Video streaming test is designed to measure the access point performance and stability by streaming the videos from the local browser"
1232-
"or from over the Internet in real clients like android which are connected to the access point,"
1233-
"this test allows the user to choose the options like video link, type of media source, media quality, number of playbacks."
1234-
"Along with the performance other measurements like No of Buffers, Wait-Time, per client Video Bitrate, Video Quality, and more. "
1235-
"The expected behavior is for the DUT to be able to handle several stations (within the limitations of the AP specs)"
1236-
"and make sure all capable clients can browse the video. ")
1240+
if iot_summary:
1241+
report.set_obj_html(
1242+
"Objective",
1243+
"The Candela Video Streaming Test Including IoT Devices is designed to evaluate an Access Point’s "
1244+
"performance and stability when handling both Real clients (Android, Windows, Linux, iOS) and IoT devices "
1245+
"(controlled via Home Assistant) simultaneously. "
1246+
"For Real clients, the test measures video streaming performance by playing user-defined media "
1247+
"(from a local browser or the Internet) with configurable options such as video link, media source type, "
1248+
"quality, and number of playbacks. Metrics such as buffering events, wait-time, per-client video bitrate, "
1249+
"and video quality are captured to validate the AP’s ability to sustain smooth playback across multiple clients. "
1250+
"For IoT clients, the test concurrently executes device-specific actions (e.g., camera streaming, switch toggling, "
1251+
"lock/unlock) and tracks success rate, latency, and failure rate. The goal is to ensure the AP can reliably support "
1252+
"video streaming for multiple real clients while maintaining responsive and consistent performance for IoT devices."
1253+
)
1254+
else:
1255+
report.set_obj_html("Objective", " The Candela Video streaming test is designed to measure the access point performance and stability by streaming the videos from the local browser"
1256+
"or from over the Internet in real clients like android which are connected to the access point,"
1257+
"this test allows the user to choose the options like video link, type of media source, media quality, number of playbacks."
1258+
"Along with the performance other measurements like No of Buffers, Wait-Time, per client Video Bitrate, Video Quality, and more. "
1259+
"The expected behavior is for the DUT to be able to handle several stations (within the limitations of the AP specs)"
1260+
"and make sure all capable clients can browse the video. ")
12371261
report.build_objective()
12381262
report.set_table_title("Input Parameters")
12391263
report.build_table_title()
@@ -1247,7 +1271,8 @@ def generate_report(self, date, iterations_before_test_stopped_by_user, test_set
12471271
# Create a string by joining the mapped pairs
12481272
gp_map = ", ".join(f"{group} -> {profile}" for group, profile in gp_pairs)
12491273
test_setup_info["Configuration"] = gp_map
1250-
1274+
if iot_summary:
1275+
test_setup_info = with_iot_params_in_table(test_setup_info, iot_summary)
12511276
report.test_setup_table(value="Test Setup Information", test_setup_data=test_setup_info)
12521277

12531278
device_type = []
@@ -1503,6 +1528,8 @@ def generate_report(self, date, iterations_before_test_stopped_by_user, test_set
15031528
dataframe3 = pd.DataFrame(dataframe2)
15041529
report.set_table_dataframe(dataframe3)
15051530
report.build_table()
1531+
if iot_summary:
1532+
self.build_iot_report_section(report, iot_summary)
15061533
report.build_footer()
15071534
report.write_html()
15081535
report.write_pdf()
@@ -1796,16 +1823,155 @@ def updating_webui_running_json(self):
17961823
with open(file_path, 'w') as file:
17971824
json.dump(data, file, indent=4)
17981825

1826+
def build_iot_report_section(self, report, iot_summary):
1827+
"""
1828+
Handles all IoT-related charts, tables, and increment-wise reports.
1829+
"""
1830+
outdir = report.path_date_time
1831+
os.makedirs(outdir, exist_ok=True)
1832+
1833+
def copy_into_report(raw_path, new_name):
1834+
"""Resolve and copy image into report dir."""
1835+
if not raw_path:
1836+
return None
1837+
1838+
abs_src = os.path.abspath(raw_path)
1839+
if not os.path.exists(abs_src):
1840+
# Search recursively under 'results' if absolute path missing
1841+
for root, _, files in os.walk(os.path.join(os.getcwd(), "results")):
1842+
if os.path.basename(raw_path) in files:
1843+
abs_src = os.path.join(root, os.path.basename(raw_path))
1844+
break
1845+
else:
1846+
return None
1847+
1848+
dst = os.path.join(outdir, new_name)
1849+
if os.path.abspath(abs_src) != os.path.abspath(dst):
1850+
shutil.copy2(abs_src, dst)
1851+
return new_name
1852+
1853+
# section header
1854+
report.set_custom_html('<div style="page-break-before: always;"></div>')
1855+
report.build_custom()
1856+
report.set_custom_html('<h2><u>IoT Results</u></h2>')
1857+
report.build_custom()
1858+
1859+
# Statistics
1860+
stats_png = copy_into_report(iot_summary.get("statistics_img"), "iot_statistics.png")
1861+
if stats_png:
1862+
report.build_chart_title("Test Statistics")
1863+
report.set_custom_html(f'<img src="{stats_png}" style="width:100%; height:auto;">')
1864+
report.build_custom()
1865+
1866+
# Request vs Latency
1867+
rvl_png = copy_into_report(iot_summary.get("req_vs_latency_img"), "iot_request_vs_latency.png")
1868+
if rvl_png:
1869+
report.build_chart_title("Request vs Average Latency")
1870+
report.set_custom_html(f'<img src="{rvl_png}" style="width:100%;">')
1871+
report.build_custom()
1872+
1873+
# Overall results table
1874+
ort = iot_summary.get("overall_result_table") or {}
1875+
if ort:
1876+
rows = [{
1877+
"Device": dev,
1878+
"Min Latency (ms)": stats.get("min_latency"),
1879+
"Avg Latency (ms)": stats.get("avg_latency"),
1880+
"Max Latency (ms)": stats.get("max_latency"),
1881+
"Total Iterations": stats.get("total_iterations"),
1882+
"Success Iters": stats.get("success_iterations"),
1883+
"Failed Iters": stats.get("failed_iterations"),
1884+
"No-Response Iters": stats.get("no_response_iterations"),
1885+
} for dev, stats in ort.items()]
1886+
1887+
df_overall = pd.DataFrame(rows).round(2)
1888+
1889+
report.set_custom_html('<div style="page-break-inside: avoid;">')
1890+
report.build_custom()
1891+
report.set_obj_html(_obj_title="Overall IoT Result Table", _obj=" ")
1892+
report.build_objective()
1893+
report.set_table_dataframe(df_overall)
1894+
report.build_table()
1895+
report.set_custom_html('</div>')
1896+
report.build_custom()
1897+
1898+
# Increment reports
1899+
inc = iot_summary.get("increment_reports") or {}
1900+
if inc:
1901+
report.set_custom_html('<h3>Reports by Increment Steps</h3>')
1902+
report.build_custom()
1903+
1904+
for step_name, rep in inc.items():
1905+
1906+
report.set_custom_html(f'<h4><u>{step_name.replace("_", " ")}</u></h4>')
1907+
report.build_custom()
1908+
1909+
# Latency graph
1910+
lat_png = copy_into_report(rep.get("latency_graph"), f"iot_{step_name}_latency.png")
1911+
if lat_png:
1912+
report.build_chart_title("Average Latency")
1913+
report.set_custom_html(f'<img src="{lat_png}" style="width:100%; height:auto;">')
1914+
report.build_custom()
1915+
1916+
# Success count graph
1917+
res_png = copy_into_report(rep.get("result_graph"), f"iot_{step_name}_results.png")
1918+
if res_png:
1919+
report.build_chart_title("Success Count")
1920+
report.set_custom_html(f'<img src="{res_png}" style="width:100%; height:auto;">')
1921+
report.build_custom()
1922+
1923+
# Tabular data for detailed iteration-level results
1924+
data_rows = rep.get("data") or []
1925+
if data_rows:
1926+
df = pd.DataFrame(data_rows).rename(
1927+
columns={"latency__ms": "Latency_ms", "latency_ms": "Latency_ms"}
1928+
)
1929+
if "Latency_ms" in df.columns:
1930+
df["Latency_ms"] = pd.to_numeric(df["Latency_ms"], errors="coerce").round(3)
1931+
if "Result" in df.columns:
1932+
df["Result"] = df["Result"].map(lambda x: "Success" if bool(x) else "Failure")
1933+
1934+
desired_cols = ["Iteration", "Device", "Current State", "Latency_ms", "Result"]
1935+
df = df[[c for c in desired_cols if c in df.columns]]
1936+
1937+
report.set_table_dataframe(df)
1938+
report.build_table()
1939+
1940+
report.set_custom_html('<hr>')
1941+
report.build_custom()
1942+
1943+
1944+
def with_iot_params_in_table(base: dict, iot_summary) -> dict:
1945+
"""
1946+
Append IoT params into the existing Throughput Input Parameters table.
1947+
Adds: IoT Test name, IoT Iterations, IoT Delay (s), IoT Increment.
1948+
Accepts dict or JSON string.
1949+
"""
1950+
try:
1951+
if not iot_summary:
1952+
return base
1953+
if isinstance(iot_summary, str):
1954+
try:
1955+
iot_summary = json.loads(iot_summary)
1956+
except Exception:
1957+
start = iot_summary.find("{")
1958+
end = iot_summary.rfind("}")
1959+
if start == -1 or end == -1 or end <= start:
1960+
return base
1961+
try:
1962+
iot_summary = json.loads(iot_summary[start:end + 1])
1963+
except Exception:
1964+
return base
17991965

1800-
def duration_to_seconds(duration):
1801-
duration = duration.strip().lower()
1802-
if duration.endswith("m"):
1803-
return int(duration[:-1]) * 60
1804-
if duration.endswith("h"):
1805-
return int(duration[:-1]) * 3600
1806-
if duration.endswith("s"):
1807-
return int(duration[:-1])
1808-
return int(duration) * 60
1966+
ti = (iot_summary.get("test_input_table") or {})
1967+
out = OrderedDict(base)
1968+
out["Iot Device List"] = ti.get("Device List", "")
1969+
out["IoT Iterations"] = ti.get("Iterations", "")
1970+
out["IoT Delay (s)"] = ti.get("Delay (seconds)", "")
1971+
out["IoT Increment"] = ti.get("Increment Pattern", "")
1972+
return out
1973+
except Exception:
1974+
return base
18091975

18101976

18111977
def trigger_iot(ip, port, iterations, delay, device_list, testname, increment):
@@ -2301,7 +2467,7 @@ def main():
23012467
thread = threading.Thread(target=trigger_iot, args=(iot_ip, iot_port, iot_iterations, iot_delay, iot_device_list, iot_testname, iot_increment))
23022468
thread.start()
23032469
else:
2304-
total_secs = duration_to_seconds(args.duration)
2470+
total_secs = int(LFCliBase.parse_time(args.duration).total_seconds())
23052471
iot_iterations = max(1, total_secs // args.iot_delay)
23062472
iot_thread = threading.Thread(
23072473
target=trigger_iot,
@@ -2453,12 +2619,19 @@ def main():
24532619
break
24542620
obj.stop()
24552621
date = str(datetime.now()).split(",")[0].replace(" ", "-").split(".")[0]
2622+
iot_summary = None
2623+
if args.iot_test and args.iot_testname:
2624+
base = os.path.join("results", args.iot_testname)
2625+
p = os.path.join(base, "iot_summary.json")
2626+
if os.path.exists(p):
2627+
with open(p) as f:
2628+
iot_summary = json.load(f)
24562629

24572630
# prev_inc_value = 0
24582631
if obj.resource_ids and obj.incremental:
2459-
obj.generate_report(date, list(set(iterations_before_test_stopped_by_user)), test_setup_info=test_setup_info, realtime_dataset=individual_df)
2632+
obj.generate_report(date, list(set(iterations_before_test_stopped_by_user)), test_setup_info=test_setup_info, realtime_dataset=individual_df, iot_summary=iot_summary)
24602633
elif obj.resource_ids:
2461-
obj.generate_report(date, list(set(iterations_before_test_stopped_by_user)), test_setup_info=test_setup_info, realtime_dataset=individual_df)
2634+
obj.generate_report(date, list(set(iterations_before_test_stopped_by_user)), test_setup_info=test_setup_info, realtime_dataset=individual_df, iot_summary=iot_summary)
24622635

24632636
# Perform post-cleanup operations
24642637
if args.postcleanup:

0 commit comments

Comments
 (0)