chromium/content/test/gpu/gpu_tests/webcodecs_integration_test.py

# 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 sys
import json
import itertools
from typing import Any, List, Set
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())
    for test in tests:
      yield test

  @classmethod
  def GenerateFrameTests(cls) -> ct.TestGenerator:
    for source_type in frame_sources:
      yield ('WebCodecs_DrawImage_' + source_type, 'draw-image.html', [{
          'source_type':
          source_type
      }])
      yield ('WebCodecs_TexImage2d_' + source_type, 'tex-image-2d.html', [{
          'source_type':
          source_type
      }])
      yield ('WebCodecs_copyTo_' + source_type, 'copyTo.html', [{
          'source_type':
          source_type
      }])
      yield ('WebCodecs_convertToRGB_' + source_type, 'convert-to-rgb.html', [{
          'source_type':
          source_type
      }])
    for source_type in hbd_frame_sources:
      yield ('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 in high_res_codecs:
      for acc in accelerations:
        for bitrate_mode in ['constant', 'variable']:
          for bitrate in [1500000, 2000000, 3000000]:
            args = (codec, acc, bitrate_mode, bitrate)
            yield ('WebCodecs_EncodingRateControl_%s_%s_%s_%s' % args,
                   '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'
    codec = 'avc1.42001E'
    acc = 'prefer-hardware'
    args = (source_type, codec, acc)
    yield ('WebCodecs_PerFrameQpEncoding_%s_%s_%s' % args,
           'frame-qp-encoding.html', [{
               'source_type': source_type,
               'codec': codec,
               'acceleration': acc
           }])

    for source_type in frame_sources:
      for codec in video_codecs:
        for acc in accelerations:
          args = (source_type, codec, acc)
          yield ('WebCodecs_EncodeDecode_%s_%s_%s' % args, 'encode-decode.html',
                 [{
                     'source_type': source_type,
                     'codec': codec,
                     'acceleration': acc
                 }])

    for source_type in frame_sources:
      # Also verify we can deal with the encoder's frame delay that we can
      # encounter for the AVC High profile (avc1.64).
      for codec in video_codecs + ['avc1.64001E']:
        for acc in accelerations:
          args = (source_type, codec, acc)
          yield ('WebCodecs_Encode_%s_%s_%s' % args, 'encode.html', [{
              'source_type':
              source_type,
              'codec':
              codec,
              'acceleration':
              acc
          }])

    for codec in video_codecs:
      for acc in accelerations:
        for bitrate_mode in ['constant', 'variable']:
          for latency_mode in ['realtime', 'quality']:
            source_type = 'offscreen'
            content_hint = 'motion'
            args = (source_type, codec, acc, bitrate_mode, latency_mode)
            yield ('WebCodecs_EncodingModes_%s_%s_%s_%s_%s' % args,
                   '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 in video_codecs:
      for content_hint in ['detail', 'text', 'motion']:
        source_type = 'offscreen'
        acc = 'prefer-hardware'
        bitrate_mode = 'constant'
        latency_mode = 'realtime'
        yield ('WebCodecs_ContentHint_%s_%s' % (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 in video_codecs:
      for acc in accelerations:
        for layers in [2, 3]:
          args = (codec, acc, layers)
          yield ('WebCodecs_SVC_%s_%s_layers_%d' % args, 'svc.html', [{
              'codec':
              codec,
              'acceleration':
              acc,
              'layers':
              layers
          }])

    for codec in video_codecs:
      for acc in accelerations:
        args = (codec, acc)
        yield ('WebCodecs_EncodeColorSpace_%s_%s' % args,
               'encode-color-space.html', [{
                   'codec': codec,
                   'acceleration': acc
               }])

    for codec in video_codecs:
      for source_type in frame_sources:
        args = (codec, source_type)
        yield ('WebCodecs_FrameSizeChange_%s_%s' % args,
               'frame-size-change.html', [{
                   'codec': codec,
                   'source_type': source_type
               }])
# pylint: enable=too-many-branches

  def RunActualGpuTest(self, test_path: str, args: ct.TestArgs) -> None:
    url = self.UrlOfStaticFilePath(html_path + '/' + test_path)
    tab = self.tab
    arg_obj = args[0]
    os_name = self.platform.GetOSName()
    arg_obj['validate_camera_frames'] = self.CameraCanShowFourColors(os_name)
    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')

  @classmethod
  def SetUpProcess(cls) -> None:
    super(WebCodecsIntegrationTest, cls).SetUpProcess()
    args = [
        '--use-fake-device-for-media-stream',
        '--use-fake-ui-for-media-stream',
        '--enable-blink-features=SharedArrayBuffer',
        cba.ENABLE_PLATFORM_HEVC_ENCODER_SUPPORT,
        cba.ENABLE_EXPERIMENTAL_WEB_PLATFORM_FEATURES,
    ] + cba.ENABLE_WEBGPU_FOR_TESTING

    # If we don't call CustomizeBrowserArgs cls.platform is None
    cls.CustomizeBrowserArgs(args)

    if cls.CameraCanShowFourColors(cls.platform.GetOSName()):
      args.append('--use-file-for-fake-video-capture=' + four_colors_img_path)
      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__])