From f67e71a3dd857e7295e06e5fe2594e2e8f951764 Mon Sep 17 00:00:00 2001 From: Romulo Quidute Filho <116586593+rquidute@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:11:50 -0300 Subject: [PATCH] Fix Push AV Stream Verification player reading stale field names (#103) * Fix Push AV Stream Verification player reading stale field names The Push AV Server's /streams API returns each stream's uploaded files under valid_uploads/error_uploads (lists of {file_path, reasons?}) since the server became session-oriented. The CLI's push_av_stream_verification.html was never updated and still looked for files/valid_files/invalid_files, which no longer exist in the response. As a result, allFiles was always empty, no .mpd/.m4s entry point was ever found, and the video player stayed blank even when the DUT successfully uploaded CMAF content to the server. Add getStreamFilePaths() to read valid_uploads/error_uploads first, falling back to the legacy files/valid_files/invalid_files shape for compatibility with older server responses. Also surface per-file non-conforming reasons in the Non-Conforming Files section using the reasons field now provided by error_uploads entries. Add regression tests asserting the rendered template references the current field names ahead of the legacy fallback. * Apply defensive null-checks to Push AV upload parsing per code review Use optional chaining (u?.file_path, u?.reasons) and filter(Boolean) when mapping valid_uploads/error_uploads entries to file paths and reasons, so a malformed or null entry in the server response can't throw a TypeError and block the verification page from rendering. Update the corresponding test assertion to check for the file_path field name generically instead of the literal 'u.file_path' loop variable expression, which no longer appears verbatim once optional chaining is used. --- .../camera/test_camera_http_server.py | 46 +++++++++++++++++++ .../camera/push_av_stream_verification.html | 42 ++++++++++++++--- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/tests/test_run/camera/test_camera_http_server.py b/tests/test_run/camera/test_camera_http_server.py index acaab32..e0d79d0 100644 --- a/tests/test_run/camera/test_camera_http_server.py +++ b/tests/test_run/camera/test_camera_http_server.py @@ -293,6 +293,52 @@ def test_do_post_unknown_path_sends_404(self): assert handler._error_code == 404 +# --------------------------------------------------------------------------- +# VideoStreamingHandler.serve_player - Push AV Stream Verification template +# --------------------------------------------------------------------------- + + +@pytest.mark.unit +class TestServePlayerPushAVTemplate: + """Regression tests for issue #1051: the Push AV Server returns each stream's + uploaded files under valid_uploads/error_uploads (list of {file_path, reasons?}), + not the legacy files/valid_files/invalid_files shape. The rendered template's + JS must read the current field names or the video player stays blank even + when the DUT has successfully uploaded content.""" + + def _render(self): + handler = _make_handler( + path="/", + server_attrs={ + "prompt_options": {"PASS": 1, "FAIL": 2}, + "prompt_text": "Verify the video stream", + "is_push_av_verification": True, + "push_av_server_url": "https://192.168.0.53:1234", + }, + ) + handler.serve_player() + return handler.wfile.getvalue().decode("utf-8") + + def test_renders_without_template_error(self): + html_content = self._render() + assert "Template error" not in html_content + assert "" in html_content or "" in html_content + + def test_reads_valid_and_error_uploads_fields(self): + html_content = self._render() + assert "stream.valid_uploads" in html_content + assert "stream.error_uploads" in html_content + assert "file_path" in html_content + + def test_no_longer_relies_solely_on_legacy_file_fields(self): + """The old field names may still appear as a fallback, but the current + server field names must be checked first.""" + html_content = self._render() + valid_uploads_idx = html_content.index("stream.valid_uploads") + valid_files_idx = html_content.index("stream.valid_files") + assert valid_uploads_idx < valid_files_idx + + # --------------------------------------------------------------------------- # VideoStreamingHandler.handle_response # --------------------------------------------------------------------------- diff --git a/th_cli/test_run/camera/push_av_stream_verification.html b/th_cli/test_run/camera/push_av_stream_verification.html index 486f719..e2260c2 100644 --- a/th_cli/test_run/camera/push_av_stream_verification.html +++ b/th_cli/test_run/camera/push_av_stream_verification.html @@ -526,6 +526,19 @@ nonConformingSection.style.display = 'none'; }} + // Push AV Server returns each stream's uploaded files as valid_uploads/error_uploads, + // each entry being an object with a file_path (and reasons, for error_uploads). + // Older server responses used plain filename arrays under files/valid_files/invalid_files - + // keep supporting those too so this page degrades gracefully against either shape. + function getStreamFilePaths(stream) {{ + if (stream.valid_uploads || stream.error_uploads) {{ + const validPaths = (stream.valid_uploads || []).map(u => u?.file_path).filter(Boolean); + const invalidPaths = (stream.error_uploads || []).map(u => u?.file_path).filter(Boolean); + return [...validPaths, ...invalidPaths]; + }} + return stream.files || [...(stream.valid_files || []), ...(stream.invalid_files || [])]; + }} + function selectStream(index) {{ if (!streamsData || !streamsData[index]) {{ console.error('Stream data not found for index:', index); @@ -535,8 +548,7 @@ const stream = streamsData[index]; selectedStreamId = stream.id || index; - // Combine valid_files and invalid_files (or use files if present) - const allFiles = stream.files || [...(stream.valid_files || []), ...(stream.invalid_files || [])]; + const allFiles = getStreamFilePaths(stream); // Update selected styling document.querySelectorAll('.stream-item').forEach(item => {{ @@ -582,9 +594,14 @@ const streamNum = parseInt(stream.id) + 1; streamContentsTitle.textContent = `Stream ${{streamNum}} Contents`; - // Combine valid and invalid files (or use files if present) - const validFiles = stream.valid_files || []; - const invalidFiles = stream.invalid_files || []; + const hasUploadsShape = !!(stream.valid_uploads || stream.error_uploads); + const validFiles = hasUploadsShape + ? (stream.valid_uploads || []).map(u => u?.file_path).filter(Boolean) + : (stream.valid_files || []); + const invalidUploads = hasUploadsShape ? (stream.error_uploads || []) : []; + const invalidFiles = hasUploadsShape + ? invalidUploads.map(u => u?.file_path).filter(Boolean) + : (stream.invalid_files || []); const allFiles = stream.files || [...validFiles, ...invalidFiles]; // Display files @@ -611,13 +628,26 @@ // Display conformance status if (invalidFiles.length > 0) {{ - nonConformingContent.innerHTML = `
Found ${{invalidFiles.length}} non-conforming file(s)
`; + const reasons = invalidUploads + .flatMap(u => u?.reasons || []) + .filter(Boolean); + const reasonsHtml = reasons.length > 0 + ? `
${{reasons.map(r => html_escape(r)).join('
')}}
` + : ''; + nonConformingContent.innerHTML = + `
Found ${{invalidFiles.length}} non-conforming file(s)${{reasonsHtml}}
`; }} else {{ nonConformingContent.innerHTML = '
All files conform to Matter Spec.
'; }} nonConformingSection.style.display = 'block'; }} + function html_escape(str) {{ + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + }} + let currentDashPlayer = null; // Track current player instance function playStream(streamUrl) {{