diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/gst_resources_generator.py b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/gst_resources_generator.py index 78ee8af43b..0c412ddae7 100755 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/gst_resources_generator.py +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/gst_resources_generator.py @@ -22,6 +22,8 @@ import json import logging from typing import Dict, List +from checkbox_support.scripts.image_checker import has_desktop_environment +from checkbox_support.snap_utils.system import on_ubuntucore logging.basicConfig(level=logging.INFO) @@ -61,6 +63,9 @@ def register_arguments() -> argparse.Namespace: class GstResources: + + VIDEO_GOLDEN_SAMPLES = "video_golden_samples" + def __init__(self, args: argparse.Namespace) -> None: self._args = args try: @@ -81,6 +86,7 @@ def __init__(self, args: argparse.Namespace) -> None: raise SystemExit("{}".format(e)) self._current_scenario_name = "" self._resource_items = [] + self._has_desktop_environment = has_desktop_environment() def _v4l2_video_decoder_md5_checksum_comparison_helper( self, @@ -135,6 +141,36 @@ def gst_v4l2_video_decoder_md5_checksum_comparison( ] ) + def gst_v4l2_audio_video_synchronization( + self, scenario_data: Dict + ) -> None: + video_sink = "" + if on_ubuntucore(): + video_sink = scenario_data["video_sinks"]["on_core"] + elif self._has_desktop_environment: + video_sink = scenario_data["video_sinks"]["on_desktop"] + else: + video_sink = scenario_data["video_sinks"]["on_server"] + + for item in scenario_data["cases"]: + for sample_file in item["golden_sample_files"]: + self._resource_items.append( + { + "scenario": self._current_scenario_name, + "video_sink": video_sink, + "decoder_plugin": item["decoder_plugin"], + "golden_sample_file_name": sample_file["file_name"], + "golden_sample_file": os.path.join( + self._args.video_codec_testing_data_path, + self.VIDEO_GOLDEN_SAMPLES, + sample_file["file_name"], + ), + "capssetter_pipeline": sample_file[ + "capssetter_pipeline" + ], + } + ) + def main(self): for scenario in self._scenarios: self._current_scenario_name = scenario diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/gst_v4l2_audio_video_synchronization.py b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/gst_v4l2_audio_video_synchronization.py new file mode 100755 index 0000000000..8db9158e2a --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/gst_v4l2_audio_video_synchronization.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +# This file is part of Checkbox. +# +# Copyright 2024 Canonical Ltd. +# Written by: +# Patrick Chang +# +# Checkbox is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, +# as published by the Free Software Foundation. +# +# Checkbox is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Checkbox. If not, see . + +import argparse +import logging +import os +import shlex +import subprocess +from typing import Any + +logging.basicConfig(level=logging.INFO) + + +def register_arguments(): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=( + "Script helps playing a golden sample video on specific display" + ' by the specific "video sink", e.g. waylandsink. The golden ' + "is special video for verifying AV Synchronization." + ), + ) + + parser.add_argument( + "-gp", + "--golden_sample_path", + required=True, + type=str, + help="Path of Golden Sample file", + ) + + parser.add_argument( + "-dp", + "--decoder_plugin", + required=True, + type=str, + help="Decoder plugin be used in gstreamer pipeline e.g. v4l2h264dec", + ) + + parser.add_argument( + "-vs", + "--video_sink", + default="waylandsink", + type=str, + help=( + "Specific value of video-sink for gstreamer that a video can be" + " displayed on. (Default: waylandsink)" + ), + ) + + parser.add_argument( + "-cp", + "--capssetter_pipeline", + default="", + type=str, + help=("Specific value for caps setting. (Default: " ")"), + ) + + args = parser.parse_args() + return args + + +def build_gst_command( + gst_bin: str, + golden_sample_path: str, + decoder: str, + video_sink: str, + capssetter_pipeline: str, +) -> str: + """ + Builds a GStreamer command to process the golden sample. + + :param gst_bin: + The binary name of gstreamer. Default is "gst-launch-1.0" + You can assign the snap name to GST_LAUNCH_BIN env variable if you + want to using snap. + :param golden_sample: + The path to the golden sample file. + :param decoder: + The decoder to use for the video, e.g., "v4l2vp8dec", "v4l2vp9dec". + :param video_sink: + The specific sink for video displaying on, e.g. "waylandsink" + :param capssetter_pipeline: + The specific value for caps setting + + :returns: + The GStreamer command to execute. + """ + + # Why we need capssetter_pipeline? + # Because some golden samples need a special colorimetry and configuration + # to get it streaming smoothly. + if capssetter_pipeline: + decoder = "{} ! {}".format(capssetter_pipeline, decoder) + + cmd = ( + "{} -v filesrc location={} ! qtdemux name=demux demux.video_0 !" + " queue ! parsebin ! {} ! v4l2convert " + "output-io-mode=dmabuf-import capture-io-mode=dmabuf ! {} " + "demux.audio_0 ! queue ! decodebin ! audioconvert ! audioresample !" + " autoaudiosink" + ).format(gst_bin, golden_sample_path, decoder, video_sink) + + return cmd + + +def execute_command(cmd: str, timeout: int = 30) -> None: + """ + Executes the GStreamer command to play video. + + :param cmd: + The GStreamer command to execute. + """ + try: + logging.info("Starting command: '{}'".format(cmd)) + ret = subprocess.run( + shlex.split(cmd), + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + timeout=timeout, + ) + logging.info(ret.stdout) + except subprocess.TimeoutExpired: + # Ignore the timeout exception because some golden samples's length is + # too long + pass + except Exception as e: + logging.error(e.stderr) + raise SystemExit(e.returncode) + + +def play_video_for_av_synchronization_test(args: Any) -> None: + """ + This function performs the following steps: + + 1. Checks if the golden sample file exists. + 2. Builds a GStreamer command to process the golden sample using the + specified decoder and video sink. + 3. Executes the GStreamer command. + + :param args: + An object containing the following attributes: + - `golden_sample_path` (str): The path to the golden sample file. + - `decoder_plugin` (str): The video decoder to use, e.g., + "v4l2vp8dec", "v4l2vp9dec". + - `video_sink` (str): The specific sink for video displaying on, + e.g. "waylandsink + + :raises SystemExit: + If the golden sample file or the golden MD5 checksum file does not + exist, or if the extracted MD5 checksum does not match the golden MD5 + checksum. + """ + # Check the golden sample exists + if not os.path.exists(args.golden_sample_path): + raise SystemExit( + "Golden Sample '{}' doesn't exist".format(args.golden_sample_path) + ) + gst_launch_bin = os.getenv("GST_LAUNCH_BIN", "gst-launch-1.0") + cmd = build_gst_command( + gst_bin=gst_launch_bin, + golden_sample_path=args.golden_sample_path, + decoder=args.decoder_plugin, + video_sink=args.video_sink, + capssetter_pipeline=args.capssetter_pipeline, + ) + # The video will be displayed on the real display + execute_command(cmd) + + +def main() -> None: + args = register_arguments() + play_video_for_av_synchronization_test(args) + + +if __name__ == "__main__": + main() diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/video-codec-test-confs/genio-1200.json b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/video-codec-test-confs/genio-1200.json index ee5d5f4b0c..97d7a31b1a 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/video-codec-test-confs/genio-1200.json +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/video-codec-test-confs/genio-1200.json @@ -24,6 +24,36 @@ "color_spaces": ["YUV"], "source_format": "webm" } - ] + ], + "gst_v4l2_audio_video_synchronization": { + "video_sinks": { + "on_desktop": "waylandsink", + "on_server": "kmssink connector-id=32 driver-name=mediatek", + "on_core": "kmssink connector-id=32 driver-name=mediatek" + }, + "cases": [ + { + "decoder_plugin": "v4l2h264dec", + "golden_sample_files": [ + { + "file_name": "480p.mp4", + "capssetter_pipeline": "" + }, + { + "file_name": "720p.mp4", + "capssetter_pipeline": "" + }, + { + "file_name": "1080p.mp4", + "capssetter_pipeline": "" + }, + { + "file_name": "4K.mp4", + "capssetter_pipeline": "capssetter replace=true caps='video/x-h264, stream-format=(string)byte-stream, alignment=(string)au, level=(string)5.2, profile=(string)main, width=(int)3840, height=(int)2160, framerate=(fraction)24/1, pixel-aspect-ratio=(fraction)1/1, colorimetry=(string)bt2020, chroma-format=(string)4:2:0, bit-depth-luma=(uint)8, bit-depth-chroma=(uint)8, parsed=(boolean)true'" + } + ] + } + ] + } } diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/video-codec-test-confs/genio-350.json b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/video-codec-test-confs/genio-350.json new file mode 100644 index 0000000000..d9979a9f63 --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/video-codec-test-confs/genio-350.json @@ -0,0 +1,28 @@ +{ + "gst_v4l2_audio_video_synchronization": { + "video_sinks": { + "on_server": "kmssink connector-id=32 driver-name=mediatek", + "on_core": "kmssink connector-id=32 driver-name=mediatek" + }, + "cases": [ + { + "decoder_plugin": "v4l2h264dec", + "golden_sample_files": [ + { + "file_name": "480p.mp4", + "capssetter_pipeline": "" + }, + { + "file_name": "720p.mp4", + "capssetter_pipeline": "" + }, + { + "file_name": "1080p.mp4", + "capssetter_pipeline": "" + } + ] + } + ] + } +} + diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/video-codec-test-confs/genio-510.json b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/video-codec-test-confs/genio-510.json new file mode 100644 index 0000000000..b64c271c59 --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/video-codec-test-confs/genio-510.json @@ -0,0 +1,33 @@ +{ + "gst_v4l2_audio_video_synchronization": { + "video_sinks": { + "on_desktop": "waylandsink", + "on_server": "kmssink connector-id=32 driver-name=mediatek", + "on_core": "kmssink connector-id=32 driver-name=mediatek" + }, + "cases": [ + { + "decoder_plugin": "v4l2h264dec", + "golden_sample_files": [ + { + "file_name": "480p.mp4", + "capssetter_pipeline": "" + }, + { + "file_name": "720p.mp4", + "capssetter_pipeline": "" + }, + { + "file_name": "1080p.mp4", + "capssetter_pipeline": "" + }, + { + "file_name": "4K.mp4", + "capssetter_pipeline": "capssetter replace=true caps='video/x-h264, stream-format=(string)byte-stream, alignment=(string)au, level=(string)5.2, profile=(string)main, width=(int)3840, height=(int)2160, framerate=(fraction)24/1, pixel-aspect-ratio=(fraction)1/1, colorimetry=(string)bt2020, chroma-format=(string)4:2:0, bit-depth-luma=(uint)8, bit-depth-chroma=(uint)8, parsed=(boolean)true'" + } + ] + } + ] + } +} + diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/video-codec-test-confs/genio-700.json b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/video-codec-test-confs/genio-700.json new file mode 100644 index 0000000000..b64c271c59 --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/video-codec-test-confs/genio-700.json @@ -0,0 +1,33 @@ +{ + "gst_v4l2_audio_video_synchronization": { + "video_sinks": { + "on_desktop": "waylandsink", + "on_server": "kmssink connector-id=32 driver-name=mediatek", + "on_core": "kmssink connector-id=32 driver-name=mediatek" + }, + "cases": [ + { + "decoder_plugin": "v4l2h264dec", + "golden_sample_files": [ + { + "file_name": "480p.mp4", + "capssetter_pipeline": "" + }, + { + "file_name": "720p.mp4", + "capssetter_pipeline": "" + }, + { + "file_name": "1080p.mp4", + "capssetter_pipeline": "" + }, + { + "file_name": "4K.mp4", + "capssetter_pipeline": "capssetter replace=true caps='video/x-h264, stream-format=(string)byte-stream, alignment=(string)au, level=(string)5.2, profile=(string)main, width=(int)3840, height=(int)2160, framerate=(fraction)24/1, pixel-aspect-ratio=(fraction)1/1, colorimetry=(string)bt2020, chroma-format=(string)4:2:0, bit-depth-luma=(uint)8, bit-depth-chroma=(uint)8, parsed=(boolean)true'" + } + ] + } + ] + } +} + diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/video-codec/jobs.pxu b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/video-codec/jobs.pxu index b1ad933470..05d4253744 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/video-codec/jobs.pxu +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/video-codec/jobs.pxu @@ -27,3 +27,30 @@ flags: also-after-suspend environ: GST_LAUNCH_BIN command: gst_v4l2_video_decoder_md5_checksum_comparison.py -dp {{decoder_plugin}} -cs {{color_space}} -gp {{golden_sample_file}} -gmp {{golden_md5_checkum_file}} + +unit: template +template-engine: jinja2 +template-resource: video_codec_resource +template-filter: video_codec_resource.scenario == "gst_v4l2_audio_video_synchronization" +template-unit: job +template-id: ce-oem-video-codec/gst_v4l2_audio_video_synchronization +_template-summary: To check if the relative timing of audio and video is synchronized under a specific decoder +id: ce-oem-video-codec/{{ scenario }}-{{ decoder_plugin }}-{{ golden_sample_file_name }}-{{ video_sink }} +_summary: AV-Sync test of decoder {{ decoder_plugin }} with {{ golden_sample_file_name }} file +_purpose: To check if the relative timing of audio and video of {{ golden_sample_file_name }} file is synchronized under a specific {{ decoder_plugin }} decoder +plugin: user-interact-verify +category_id: video-codec +estimated_duration: 30s +imports: from com.canonical.plainbox import manifest +flags: also-after-suspend +environ: GST_LAUNCH_BIN +_steps: + 1. Plug a headset/earphone to the device + 2. Adjust the audio sink to be headset via Gnome Settings or command + 3. Press enter to perform test +_verification: + Ensure there's no delay between the audio output and the visual display + 1. Do you hear the beep sound and see the video be displayed? + 2. Is the timing of audio and video synchronized? +command: + gst_v4l2_audio_video_synchronization.py -dp {{decoder_plugin}} -gp {{golden_sample_file}} -vs {{video_sink}} -cp "{{capssetter_pipeline}}" diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/video-codec/test-plan.pxu b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/video-codec/test-plan.pxu index fc43ebe1bc..7b346f7824 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/video-codec/test-plan.pxu +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/video-codec/test-plan.pxu @@ -11,7 +11,10 @@ id: ce-oem-video-codec-manual unit: test plan _name: Video Codec manual tests _description: Manual tests for Video Codec in before suspend and post suspend stage +bootstrap_include: + video_codec_resource include: + ce-oem-video-codec/gst_v4l2_audio_video_synchronization id: ce-oem-video-codec-automated unit: test plan