| # Copyright 2021 The Chromium Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| import os | |
| import posixpath | |
| import sys | |
| import json | |
| import itertools | |
| from typing import Any | |
| import unittest | |
| import gpu_path_util | |
| from gpu_tests import common_browser_args as cba | |
| from gpu_tests import common_typing as ct | |
| from gpu_tests import gpu_integration_test | |
| from gpu_tests.util import host_information | |
| html_path = os.path.join( | |
| gpu_path_util.CHROMIUM_SRC_DIR, 'content', 'test', 'data', 'gpu', 'webcodecs' | |
| ) | |
| data_path = os.path.join( | |
| gpu_path_util.CHROMIUM_SRC_DIR, 'media', 'test', 'data' | |
| ) | |
| four_colors_img_path = os.path.join(data_path, 'four-colors.y4m') | |
| frame_sources = [ | |
| 'camera', | |
| 'capture', | |
| 'offscreen', | |
| 'arraybuffer', | |
| 'hw_decoder', | |
| 'sw_decoder', | |
| ] | |
| hbd_frame_sources = ['hbd_arraybuffer'] | |
| video_codecs = [ | |
| 'avc1.42001E', | |
| 'hvc1.1.6.L123.00', | |
| 'vp8', | |
| 'vp09.00.10.08', | |
| 'av01.0.04M.08', | |
| ] | |
| accelerations = ['prefer-hardware', 'prefer-software'] | |
| class WebCodecsIntegrationTest(gpu_integration_test.GpuIntegrationTest): | |
| @classmethod | |
| def Name(cls) -> str: | |
| return 'webcodecs' | |
| @classmethod | |
| def _SuiteSupportsParallelTests(cls) -> bool: | |
| return True | |
| def _GetSerialGlobs(self) -> set[str]: | |
| serial_globs = set() | |
| if host_information.IsWindows() and host_information.IsNvidiaGpu(): | |
| serial_globs |= { | |
| # crbug.com/1473480. Windows + NVIDIA has a maximum parallel encode | |
| # limit of 2, so serialize hardware encoding tests on Windows. | |
| 'WebCodecs_*prefer-hardware*', | |
| } | |
| return serial_globs | |
| def _GetSerialTests(self) -> set[str]: | |
| serial_tests = set() | |
| if host_information.IsWindows() and host_information.IsArmCpu(): | |
| serial_tests |= { | |
| # crbug.com/323824490. Seems to flakily lose the D3D11 device when | |
| # run in parallel. | |
| 'WebCodecs_FrameSizeChange_vp09.00.10.08_hw_decoder', | |
| } | |
| return serial_tests | |
| # pylint: disable=too-many-branches | |
| @classmethod | |
| def GenerateGpuTests(cls, options: ct.ParsedCmdArgs) -> ct.TestGenerator: | |
| tests = itertools.chain( | |
| cls.GenerateFrameTests(), | |
| cls.GenerateVideoTests(), | |
| cls.GenerateAudioTests(), | |
| cls.BitrateTests(), | |
| cls.GenerateD3D12EncodingTests(), | |
| ) | |
| yield from tests | |
| @classmethod | |
| def GenerateFrameTests(cls) -> ct.TestGenerator: | |
| for source_type in frame_sources: | |
| yield ( | |
| f'WebCodecs_DrawImage_{source_type}', | |
| 'draw-image.html', | |
| [ | |
| { | |
| 'source_type': source_type, | |
| } | |
| ], | |
| ) | |
| yield ( | |
| f'WebCodecs_TexImage2d_{source_type}', | |
| 'tex-image-2d.html', | |
| [ | |
| { | |
| 'source_type': source_type, | |
| } | |
| ], | |
| ) | |
| yield ( | |
| f'WebCodecs_copyTo_{source_type}', | |
| 'copyTo.html', | |
| [ | |
| { | |
| 'source_type': source_type, | |
| } | |
| ], | |
| ) | |
| yield ( | |
| f'WebCodecs_convertToRGB_{source_type}', | |
| 'convert-to-rgb.html', | |
| [ | |
| { | |
| 'source_type': source_type, | |
| } | |
| ], | |
| ) | |
| for source_type in ['offscreen', 'hw_decoder', 'sw_decoder']: | |
| for codec in video_codecs: | |
| for acc in accelerations: | |
| yield ( | |
| f'WebCodecs_cropEncode_{source_type}_{codec}_{acc}', | |
| 'crop-encode.html', | |
| [ | |
| { | |
| 'source_type': source_type, | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| } | |
| ], | |
| ) | |
| for source_type in hbd_frame_sources: | |
| yield ( | |
| f'WebCodecs_DrawImage_{source_type}', | |
| 'draw-image.html', | |
| [ | |
| { | |
| 'source_type': source_type, | |
| } | |
| ], | |
| ) | |
| @classmethod | |
| def GenerateAudioTests(cls) -> ct.TestGenerator: | |
| yield ( | |
| 'WebCodecs_AudioEncoding_AAC_LC', | |
| 'audio-encode-decode.html', | |
| [ | |
| { | |
| 'codec': 'mp4a.67', | |
| 'sample_rate': 48000, | |
| 'channels': 2, | |
| 'aac_format': 'aac', | |
| } | |
| ], | |
| ) | |
| yield ( | |
| 'WebCodecs_AudioEncoding_AAC_LC_ADTS', | |
| 'audio-encode-decode.html', | |
| [ | |
| { | |
| 'codec': 'mp4a.67', | |
| 'sample_rate': 48000, | |
| 'channels': 2, | |
| 'aac_format': 'adts', | |
| } | |
| ], | |
| ) | |
| @classmethod | |
| def BitrateTests(cls) -> ct.TestGenerator: | |
| high_res_codecs = [ | |
| 'avc1.420034', | |
| 'hvc1.1.6.L123.00', | |
| 'vp8', | |
| 'vp09.00.10.08', | |
| 'av01.0.04M.08', | |
| ] | |
| for codec, acc, bitrate_mode, bitrate in itertools.product( | |
| high_res_codecs, | |
| accelerations, | |
| ['constant', 'variable'], | |
| [1500000, 2000000, 3000000], | |
| ): | |
| yield ( | |
| f'WebCodecs_EncodingRateControl_{codec}_{acc}_{bitrate_mode}_{bitrate}', | |
| 'encoding-rate-control.html', | |
| [ | |
| { | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| 'bitrate_mode': bitrate_mode, | |
| 'bitrate': bitrate, | |
| } | |
| ], | |
| ) | |
| @classmethod | |
| def GenerateVideoTests(cls) -> ct.TestGenerator: | |
| yield ( | |
| 'WebCodecs_WebRTCPeerConnection_Window', | |
| 'webrtc-peer-connection.html', | |
| [ | |
| { | |
| 'use_worker': False, | |
| } | |
| ], | |
| ) | |
| yield ( | |
| 'WebCodecs_WebRTCPeerConnection_Worker', | |
| 'webrtc-peer-connection.html', | |
| [ | |
| { | |
| 'use_worker': True, | |
| } | |
| ], | |
| ) | |
| yield ( | |
| 'WebCodecs_Terminate_Worker', | |
| 'terminate-worker.html', | |
| [ | |
| { | |
| 'source_type': 'offscreen', | |
| } | |
| ], | |
| ) | |
| source_type = 'offscreen' | |
| acc = 'prefer-hardware' | |
| for codec in ['avc1.42001E', 'hvc1.1.6.L93.B0']: | |
| yield ( | |
| f'WebCodecs_PerFrameQpEncoding_{source_type}_{codec}_{acc}', | |
| 'frame-qp-encoding.html', | |
| [ | |
| { | |
| 'source_type': source_type, | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| } | |
| ], | |
| ) | |
| codec = 'av01.0.04M.08' | |
| acc = 'prefer-software' | |
| for layers in range(4): | |
| yield ( | |
| f'WebCodecs_ManualSVC_{codec}_{acc}_layers_{layers}', | |
| 'manual-svc.html', | |
| [ | |
| { | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| 'layers': layers, | |
| } | |
| ], | |
| ) | |
| for source_type, codec, acc in itertools.product( | |
| frame_sources, video_codecs, accelerations | |
| ): | |
| yield ( | |
| f'WebCodecs_EncodeDecode_{source_type}_{codec}_{acc}', | |
| 'encode-decode.html', | |
| [ | |
| { | |
| 'source_type': source_type, | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| } | |
| ], | |
| ) | |
| # Also verify we can deal with the encoder's frame delay that we can | |
| # encounter for the AVC High profile (avc1.64). | |
| for source_type, codec, acc in itertools.product( | |
| frame_sources, video_codecs + ['avc1.64001E'], accelerations | |
| ): | |
| yield ( | |
| f'WebCodecs_Encode_{source_type}_{codec}_{acc}', | |
| 'encode.html', | |
| [ | |
| { | |
| 'source_type': source_type, | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| } | |
| ], | |
| ) | |
| resolutions = ['1920x1080', '3840x2160', '7680x3840'] | |
| framerates = [30, 60, 120, 240] | |
| # Use at least level 6.2 (H.264/H.265/VP9), or level 6.3 (AV1) mimetypes to | |
| # test 8k 120fps support. | |
| codecs = [ | |
| 'avc1.64003E', | |
| 'hvc1.1.6.L186.B0', | |
| 'vp09.00.62.08', | |
| 'av01.1.19M.08', | |
| ] | |
| for resolution, framerate, codec in itertools.product( | |
| resolutions, framerates, codecs | |
| ): | |
| acc = 'prefer-hardware' | |
| latency_mode = 'quality' | |
| yield ( | |
| f'WebCodecs_EncodingFramerateResolutions_' | |
| f'{resolution}_{framerate}_{codec}_{acc}_{latency_mode}', | |
| 'encoding-framerate-resolutions.html', | |
| [ | |
| { | |
| 'resolution': resolution, | |
| 'framerate': framerate, | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| 'latency_mode': latency_mode, | |
| } | |
| ], | |
| ) | |
| for codec, acc, bitrate_mode, latency_mode in itertools.product( | |
| video_codecs, | |
| accelerations, | |
| ['constant', 'variable'], | |
| ['realtime', 'quality'], | |
| ): | |
| source_type = 'offscreen' | |
| content_hint = 'motion' | |
| yield ( | |
| f'WebCodecs_EncodingModes_' | |
| f'{source_type}_{codec}_{acc}_{bitrate_mode}_{latency_mode}', | |
| 'encoding-modes.html', | |
| [ | |
| { | |
| 'source_type': source_type, | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| 'bitrate_mode': bitrate_mode, | |
| 'latency_mode': latency_mode, | |
| 'content_hint': content_hint, | |
| } | |
| ], | |
| ) | |
| for codec, content_hint in itertools.product( | |
| video_codecs, ['detail', 'text', 'motion'] | |
| ): | |
| source_type = 'offscreen' | |
| acc = 'prefer-hardware' | |
| bitrate_mode = 'constant' | |
| latency_mode = 'realtime' | |
| yield ( | |
| f'WebCodecs_ContentHint_{codec}_{content_hint}', | |
| 'encoding-modes.html', | |
| [ | |
| { | |
| 'source_type': source_type, | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| 'bitrate_mode': bitrate_mode, | |
| 'latency_mode': latency_mode, | |
| 'content_hint': content_hint, | |
| } | |
| ], | |
| ) | |
| for codec, acc, layers in itertools.product( | |
| video_codecs, accelerations, [2, 3] | |
| ): | |
| yield ( | |
| f'WebCodecs_SVC_{codec}_{acc}_layers_{layers}', | |
| 'svc.html', | |
| [ | |
| { | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| 'layers': layers, | |
| } | |
| ], | |
| ) | |
| for codec, acc in itertools.product(video_codecs, accelerations): | |
| yield ( | |
| f'WebCodecs_EncodeColorSpace_{codec}_{acc}', | |
| 'encode-color-space.html', | |
| [ | |
| { | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| } | |
| ], | |
| ) | |
| yield ( | |
| f'WebCodecs_MixedSourceEncoding_{codec}_{acc}', | |
| 'mixed-source-encoding.html', | |
| [ | |
| { | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| } | |
| ], | |
| ) | |
| for codec, source_type in itertools.product(video_codecs, frame_sources): | |
| yield ( | |
| f'WebCodecs_FrameSizeChange_{codec}_{source_type}', | |
| 'frame-size-change.html', | |
| [ | |
| { | |
| 'codec': codec, | |
| 'source_type': source_type, | |
| } | |
| ], | |
| ) | |
| @classmethod | |
| def GenerateD3D12EncodingTests(cls) -> ct.TestGenerator: | |
| source_type = 'offscreen' | |
| acc = 'prefer-hardware' | |
| for source_type, codec in itertools.product(frame_sources, video_codecs): | |
| yield ( | |
| f'WebCodecs_D3D12_EncodeDecode_{source_type}_{codec}', | |
| 'encode-decode.html', | |
| [ | |
| { | |
| 'source_type': source_type, | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| 'runs_on_d3d12_encoder': True, | |
| } | |
| ], | |
| ) | |
| for source_type, codec in itertools.product(frame_sources, video_codecs): | |
| yield ( | |
| f'WebCodecs_D3D12_Encode_{source_type}_{codec}', | |
| 'encode.html', | |
| [ | |
| { | |
| 'source_type': source_type, | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| 'runs_on_d3d12_encoder': True, | |
| } | |
| ], | |
| ) | |
| for codec, bitrate_mode, latency_mode in itertools.product( | |
| video_codecs, ['constant', 'variable'], ['realtime', 'quality'] | |
| ): | |
| source_type = 'offscreen' | |
| content_hint = 'motion' | |
| yield ( | |
| f'WebCodecs_D3D12_EncodingModes_' | |
| f'{source_type}_{codec}_{bitrate_mode}_{latency_mode}', | |
| 'encoding-modes.html', | |
| [ | |
| { | |
| 'source_type': source_type, | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| 'bitrate_mode': bitrate_mode, | |
| 'latency_mode': latency_mode, | |
| 'content_hint': content_hint, | |
| 'runs_on_d3d12_encoder': True, | |
| } | |
| ], | |
| ) | |
| for codec, content_hint in itertools.product( | |
| video_codecs, ['detail', 'text', 'motion'] | |
| ): | |
| source_type = 'offscreen' | |
| bitrate_mode = 'constant' | |
| latency_mode = 'realtime' | |
| yield ( | |
| f'WebCodecs_D3D12_ContentHint_{codec}_{content_hint}', | |
| 'encoding-modes.html', | |
| [ | |
| { | |
| 'source_type': source_type, | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| 'bitrate_mode': bitrate_mode, | |
| 'latency_mode': latency_mode, | |
| 'content_hint': content_hint, | |
| 'runs_on_d3d12_encoder': True, | |
| } | |
| ], | |
| ) | |
| for codec, layers in itertools.product(video_codecs, [2, 3]): | |
| yield ( | |
| f'WebCodecs_D3D12_SVC_{codec}_layers_{layers}', | |
| 'svc.html', | |
| [ | |
| { | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| 'layers': layers, | |
| 'runs_on_d3d12_encoder': True, | |
| } | |
| ], | |
| ) | |
| for codec in video_codecs: | |
| yield ( | |
| f'WebCodecs_D3D12_EncodeColorSpace_{codec}', | |
| 'encode-color-space.html', | |
| [ | |
| { | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| 'runs_on_d3d12_encoder': True, | |
| } | |
| ], | |
| ) | |
| yield ( | |
| f'WebCodecs_D3D12_MixedSourceEncoding_{codec}', | |
| 'mixed-source-encoding.html', | |
| [ | |
| { | |
| 'codec': codec, | |
| 'acceleration': acc, | |
| 'runs_on_d3d12_encoder': True, | |
| } | |
| ], | |
| ) | |
| for codec, source_type in itertools.product(video_codecs, frame_sources): | |
| yield ( | |
| f'WebCodecs_D3D12_FrameSizeChange_{codec}_{source_type}', | |
| 'frame-size-change.html', | |
| [ | |
| { | |
| 'codec': codec, | |
| 'source_type': source_type, | |
| 'runs_on_d3d12_encoder': True, | |
| } | |
| ], | |
| ) | |
| # pylint: enable=too-many-branches | |
| def RunActualGpuTest(self, test_path: str, args: ct.TestArgs) -> None: | |
| url = self.UrlOfStaticFilePath(posixpath.join(html_path, test_path)) | |
| arg_obj = args[0] | |
| os_name = self.platform.GetOSName() | |
| enable_d3d12_encoder = arg_obj.get('runs_on_d3d12_encoder', False) | |
| if enable_d3d12_encoder and not host_information.IsWindows(): | |
| self.skipTest('Skipping D3D12 based test.') | |
| return | |
| args = self.GetBrowserArguments(enable_d3d12_encoder) | |
| self.RestartBrowserIfNecessaryWithArgs(args) | |
| arg_obj['validate_camera_frames'] = self.CameraCanShowFourColors(os_name) | |
| tab = self.tab | |
| tab.Navigate(url) | |
| tab.action_runner.WaitForJavaScriptCondition( | |
| 'document.readyState == "complete"' | |
| ) | |
| tab.EvaluateJavaScript('TEST.run(' + json.dumps(arg_obj) + ')') | |
| tab.action_runner.WaitForJavaScriptCondition('TEST.finished', timeout=60) | |
| if tab.EvaluateJavaScript('TEST.skipped'): | |
| self.skipTest('Skipping test:' + tab.EvaluateJavaScript('TEST.summary()')) | |
| if not tab.EvaluateJavaScript('TEST.success'): | |
| self.fail('Test failure:' + tab.EvaluateJavaScript('TEST.summary()')) | |
| @staticmethod | |
| def CameraCanShowFourColors(os_name: str) -> bool: | |
| return os_name not in ('android', 'chromeos', 'fuchsia') | |
| @classmethod | |
| def GetBrowserArguments(cls, enable_d3d12_encoder: bool) -> list[str]: | |
| args = [ | |
| '--use-fake-device-for-media-stream', | |
| '--use-fake-ui-for-media-stream', | |
| '--enable-blink-features=SharedArrayBuffer', | |
| cba.ENABLE_EXPERIMENTAL_WEB_PLATFORM_FEATURES, | |
| ] | |
| if enable_d3d12_encoder: | |
| args.append('--enable-features=D3D12VideoEncodeAccelerator') | |
| args.append('--disable-features=MediaFoundationVideoEncodeAccelerator') | |
| if cls.CameraCanShowFourColors(cls.platform.GetOSName()): | |
| args.append('--use-file-for-fake-video-capture=' + four_colors_img_path) | |
| return args | |
| @classmethod | |
| def SetUpProcess(cls) -> None: | |
| super(WebCodecsIntegrationTest, cls).SetUpProcess() | |
| args = [] | |
| # If we don't call CustomizeBrowserArgs cls.platform is None | |
| cls.CustomizeBrowserArgs(args) | |
| args = cls.GetBrowserArguments(False) | |
| cls.CustomizeBrowserArgs(args) | |
| cls.StartBrowser() | |
| cls.SetStaticServerDirs([html_path, data_path]) | |
| @classmethod | |
| def ExpectationsFiles(cls) -> list[str]: | |
| return [ | |
| os.path.join( | |
| os.path.dirname(os.path.abspath(__file__)), | |
| 'test_expectations', | |
| 'webcodecs_expectations.txt', | |
| ) | |
| ] | |
| def load_tests( | |
| loader: unittest.TestLoader, tests: Any, pattern: Any | |
| ) -> unittest.TestSuite: | |
| del loader, tests, pattern # Unused. | |
| return gpu_integration_test.LoadAllTestsInModule(sys.modules[__name__]) |