# Copyright 2014 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# This file defines performance test scenarios related to playing video and
# audio files using HTML5 APIs (as opposed to older tech like Flash and
# Silverlight). These scenarios simply exercise the Chromium code in particular
# ways. The metrics that are produced are calculated in a separate step.

import abc

from telemetry.page import page as page_module
from telemetry.page import traffic_setting as traffic_setting_module
from telemetry import story

# A complete list of page tags to check. This prevents misspellings and provides
# documentation of scenarios for code readers. These tags can be used to filter
# the list of pages to run using flags like --story-tag-filter=X.
_PAGE_TAGS_LIST = [
  # Audio codecs.
  'pcm',
  'mp3',
  'aac',
  'vorbis',
  'opus',
  # Video codecs.
  'av1',
  'h264',
  'vp8',
  'vp9',
  # Test types.
  'audio_video',
  'audio_only',
  'video_only',
  # Other filter tags.
  'is_50fps',
  'is_4k',
  'is_2min',
  # Play action.
  'seek',
  'beginning_to_end',
  'background',
  # Add javascript load.
  'busyjs',
  # Constrained network settings.
  'cns',
  # VideoStack API.
  'src',
  'mse',
]


# A list of traffic setting names to append to page names when used.
# Traffic settings is a way to constrain the network to match real-world
# scenarios.
_TRAFFIC_SETTING_NAMES = {
  traffic_setting_module.GPRS: 'GPRS',
  traffic_setting_module.REGULAR_2G: 'Regular-2G',
  traffic_setting_module.GOOD_2G: 'Good-2G',
  traffic_setting_module.REGULAR_3G: 'Regular-3G',
  traffic_setting_module.GOOD_3G: 'Good-3G',
  traffic_setting_module.REGULAR_4G: 'Regular-4G',
  traffic_setting_module.DSL: 'DSL',
  traffic_setting_module.WIFI: 'WiFi',
}

_URL_BASE = 'file://media_cases/'

#
# The following section contains base classes for pages.
#


class _MediaPage(page_module.Page):
  def __init__(
    self,
    url,
    page_set,
    tags,
    extra_browser_args=None,
    traffic_setting=traffic_setting_module.NONE,
    measure_memory=True,
  ):
    name = url.split('/')[-1]
    if traffic_setting != traffic_setting_module.NONE:
      name += '_' + _TRAFFIC_SETTING_NAMES[traffic_setting]
      tags.append('cns')
    if tags:
      for t in tags:
        assert t in _PAGE_TAGS_LIST
    assert not ('src' in tags and 'mse' in tags)
    super(_MediaPage, self).__init__(
      url=url,
      page_set=page_set,
      tags=tags,
      name=name,
      extra_browser_args=extra_browser_args,
      traffic_setting=traffic_setting,
    )
    self._measure_memory = measure_memory

  def GetExtraTracingMetrics(self):
    metrics = super(_MediaPage, self).GetExtraTracingMetrics()
    if self.ShouldMeasureMemory():
      metrics.append('memoryMetric')
    return metrics

  def ShouldMeasureMemory(self):
    """Returns whether the page should do a memory dump.

    Also controls whether the pages enables the memory metric.
    Subclasses should override to disable memory measurement.
    Memory dumps are cpu-intensive, so it is reasonable keep it off in
    some cases to avoid skewing cpu usage measurements.
    """
    return self._measure_memory


class _BeginningToEndPlayPage(_MediaPage):
  """A normal play page simply plays the given media until the end."""

  def __init__(
    self,
    url,
    page_set,
    tags,
    extra_browser_args=None,
    traffic_setting=traffic_setting_module.NONE,
  ):
    if 'is_2min' in tags:
      self._ended_timeout = 140
      measure_memory = False
    else:
      self._ended_timeout = 60
      measure_memory = True
    tags.append('beginning_to_end')
    tags.append('src')
    super(_BeginningToEndPlayPage, self).__init__(
      url,
      page_set,
      tags,
      extra_browser_args,
      traffic_setting=traffic_setting,
      measure_memory=measure_memory,
    )

  def RunPageInteractions(self, action_runner):
    # Play the media until it has finished or it times out.
    action_runner.PlayMedia(
      playing_event_timeout_in_seconds=60,
      ended_event_timeout_in_seconds=self._ended_timeout,
    )
    # Generate memory dump for memoryMetric.
    if self.ShouldMeasureMemory():
      action_runner.MeasureMemory()


class _SeekPage(_MediaPage):
  """A seek page seeks twice in the video and measures the seek time."""

  def __init__(
    self,
    url,
    page_set,
    tags,
    extra_browser_args=None,
    action_timeout_in_seconds=60,
    traffic_setting=traffic_setting_module.NONE,
  ):
    tags.append('seek')
    tags.append('src')
    self._action_timeout = action_timeout_in_seconds
    super(_SeekPage, self).__init__(
      url, page_set, tags, extra_browser_args, traffic_setting=traffic_setting
    )

  def RunPageInteractions(self, action_runner):
    timeout = self._action_timeout
    # Start the media playback.
    action_runner.PlayMedia(playing_event_timeout_in_seconds=timeout)
    # Wait for 1 second so that we know the play-head is at ~1s.
    action_runner.Wait(1)
    # Seek to before the play-head location.
    action_runner.SeekMedia(
      seconds=0.5, timeout_in_seconds=timeout, label='seek_warm'
    )
    # Seek to after the play-head location.
    action_runner.SeekMedia(
      seconds=9, timeout_in_seconds=timeout, label='seek_cold'
    )
    # Generate memory dump for memoryMetric.
    if self.ShouldMeasureMemory():
      action_runner.MeasureMemory()


class _BackgroundPlaybackPage(_MediaPage):
  """A Background playback page plays the given media in a background tab.

  The motivation for this test case is crbug.com/678663.
  """

  def __init__(
    self,
    url,
    page_set,
    tags,
    extra_browser_args=None,
    background_time=10,
    traffic_setting=traffic_setting_module.NONE,
  ):
    self._background_time = background_time
    tags.append('background')
    tags.append('src')
    # --disable-background-media-suspend is required since for Android,
    # background playback gets suspended. This flag makes Android work the same
    # way as desktop and not turn off video playback in the background.
    extra_browser_args = extra_browser_args or []
    extra_browser_args.append('--disable-background-media-suspend')
    super(_BackgroundPlaybackPage, self).__init__(
      url, page_set, tags, extra_browser_args
    )

  def RunPageInteractions(self, action_runner):
    # Steps:
    # 1. Play a video
    # 2. Open new tab overtop to obscure the video
    # 3. Close the tab to go back to the tab that is playing the video.
    action_runner.PlayMedia(playing_event_timeout_in_seconds=60)
    action_runner.Wait(0.5)
    new_tab = action_runner.tab.browser.tabs.New()
    new_tab.Activate()
    action_runner.Wait(self._background_time)
    new_tab.Close()
    action_runner.Wait(0.5)
    # Generate memory dump for memoryMetric.
    if self.ShouldMeasureMemory():
      action_runner.MeasureMemory()


class _MSEPage(_MediaPage):
  def __init__(
    self, url, page_set, tags, extra_browser_args=None, number_of_runs=10
  ):
    assert number_of_runs >= 1
    self._number_of_runs = number_of_runs
    tags.append('mse')
    super(_MSEPage, self).__init__(
      url, page_set, tags, extra_browser_args, measure_memory=False
    )

  def RunPageInteractions(self, action_runner):
    # The page automatically runs the test at load time.
    self._CheckTestResult(action_runner)
    # Now run it a few more times to get more reliable data.
    # Note that each run takes ~.5s, so running a bunch of times to get reliable
    # data is reasonable.
    for _ in range(self._number_of_runs - 1):
      url = action_runner.tab.url
      action_runner.tab.ClearCache(force=True)
      action_runner.tab.Navigate(url)
      self._CheckTestResult(action_runner)

  def _CheckTestResult(self, action_runner):
    action_runner.WaitForJavaScriptCondition('window.__testDone == true')
    test_failed = action_runner.EvaluateJavaScript('window.__testFailed')
    if test_failed:
      raise RuntimeError(action_runner.EvaluateJavaScript('window.__testError'))


def _GetDesktopOnlyMediaPages(page_set):
  return [
    _BeginningToEndPlayPage(
      url=_URL_BASE + 'video.html?src=tulip0.av1.mp4',
      page_set=page_set,
      tags=['av1', 'video_only'],
    ),
    _SeekPage(
      url=_URL_BASE + 'video.html?src=tulip0.av1.mp4&seek',
      page_set=page_set,
      tags=['av1', 'video_only', 'seek'],
    ),
    _MSEPage(
      url=_URL_BASE + 'mse.html?media=tulip0.av1.mp4',
      page_set=page_set,
      tags=['av1', 'video_only'],
    ),
  ]


def _GetCrossPlatformMediaPages(page_set):
  return [
    # 1080p 50fps crowd test cases. High non-60fps frame rate is good for
    # finding rendering and efficiency regressions.
    _BeginningToEndPlayPage(
      url=_URL_BASE + 'video.html?src=crowd1080.webm',
      page_set=page_set,
      tags=['is_50fps', 'vp8', 'vorbis', 'audio_video'],
    ),
    _BeginningToEndPlayPage(
      url=_URL_BASE + 'video.html?src=crowd1080.mp4',
      page_set=page_set,
      tags=['is_50fps', 'h264', 'aac', 'audio_video'],
    ),
    _BeginningToEndPlayPage(
      url=_URL_BASE + 'video.html?src=crowd1080_vp9.webm',
      page_set=page_set,
      tags=['is_50fps', 'vp9', 'video_only'],
    ),
    # Audio only test cases. MP3 and OGG are important to test since they are
    # unstructured containers and thus are prone to efficiency regressions.
    _BeginningToEndPlayPage(
      url=_URL_BASE + 'video.html?src=tulip2.ogg&type=audio',
      page_set=page_set,
      tags=['vorbis', 'audio_only'],
    ),
    _BeginningToEndPlayPage(
      url=_URL_BASE + 'video.html?src=tulip2.mp3&type=audio',
      page_set=page_set,
      tags=['mp3', 'audio_only'],
    ),
    _BeginningToEndPlayPage(
      url=_URL_BASE + 'video.html?src=tulip2.m4a&type=audio',
      page_set=page_set,
      tags=['aac', 'audio_only'],
    ),
    # Baseline + busyjs test.
    _BeginningToEndPlayPage(
      url=_URL_BASE + 'video.html?src=tulip2.mp4',
      page_set=page_set,
      tags=['h264', 'aac', 'audio_video'],
    ),
    _BeginningToEndPlayPage(
      url=_URL_BASE + 'video.html?src=tulip2.mp4&busyjs',
      page_set=page_set,
      tags=['h264', 'aac', 'audio_video', 'busyjs'],
    ),
    # Baseline + WiFi test.
    _BeginningToEndPlayPage(
      url=_URL_BASE + 'video.html?src=tulip2.vp9.webm',
      page_set=page_set,
      tags=['vp9', 'opus', 'audio_video'],
    ),
    _BeginningToEndPlayPage(
      url=_URL_BASE + 'video.html?src=tulip2.vp9.webm',
      page_set=page_set,
      tags=['vp9', 'opus', 'audio_video'],
      traffic_setting=traffic_setting_module.WIFI,
    ),
    # Longer 2-minute tests.
    _BeginningToEndPlayPage(
      url=_URL_BASE + 'video.html?src=foodmarket_720p30fps.mp4',
      page_set=page_set,
      tags=['is_2min', 'h264', 'aac', 'audio_video'],
    ),
    _BeginningToEndPlayPage(
      url=_URL_BASE + 'video.html?src=boat_1080p60fps_vp9.webm',
      page_set=page_set,
      tags=['is_2min', 'vp9', 'opus', 'audio_video'],
    ),
    # Seek tests in MP3 and OGG are important since they don't have seek
    # indices and thus show off efficiency regressions easily.
    _SeekPage(
      url=_URL_BASE + 'video.html?src=tulip2.ogg&type=audio&seek',
      page_set=page_set,
      tags=['vorbis', 'audio_only'],
    ),
    _SeekPage(
      url=_URL_BASE + 'video.html?src=tulip2.mp3&type=audio&seek',
      page_set=page_set,
      tags=['mp3', 'audio_only'],
    ),
    # High resolution seek test cases which will exaggerate any decoding
    # efficiency or buffering regressions.
    _SeekPage(
      url=_URL_BASE + 'video.html?src=garden2_10s.webm&seek',
      page_set=page_set,
      tags=['is_4k', 'vp8', 'vorbis', 'audio_video'],
    ),
    _SeekPage(
      url=_URL_BASE + 'video.html?src=garden2_10s.mp4&seek',
      page_set=page_set,
      tags=['is_4k', 'h264', 'aac', 'audio_video'],
    ),
    _SeekPage(
      url=(_URL_BASE + 'video.html?src=smpte_3840x2160_60fps_vp9.webm&seek'),
      page_set=page_set,
      tags=['is_4k', 'vp9', 'video_only'],
      action_timeout_in_seconds=120,
    ),
    # Basic test that ensures background playback works properly.
    _BackgroundPlaybackPage(
      url=_URL_BASE + 'video.html?src=tulip2.vp9.webm&background',
      page_set=page_set,
      tags=['vp9', 'opus', 'audio_video'],
    ),
    # Basic MSE test pages for common configurations. Note: By default the
    # test will only append the first 128k of the specified files, so when
    # adding tests ensure that is enough to trigger a timeupdate event. If not
    # you'll need to specify an additional appendSize parameter.
    _MSEPage(
      url=_URL_BASE + 'mse.html?media=aac_audio.mp4,h264_video.mp4',
      page_set=page_set,
      tags=['h264', 'aac', 'audio_video'],
    ),
    _MSEPage(
      url=_URL_BASE + 'mse.html?media=tulip2.vp9.webm',
      page_set=page_set,
      tags=['vp9', 'opus', 'audio_video'],
    ),
    _MSEPage(
      url=_URL_BASE + 'mse.html?media=aac_audio.mp4',
      page_set=page_set,
      tags=['aac', 'audio_only'],
    ),
    _MSEPage(
      url=_URL_BASE + 'mse.html?media=h264_video.mp4',
      page_set=page_set,
      tags=['h264', 'video_only'],
    ),
  ]


class _MediaCasesStorySet(story.StorySet):
  """Abstract Media Cases Story Set to be overriden."""

  def __init__(self, measure_memory=False):
    super(_MediaCasesStorySet, self).__init__(
      cloud_storage_bucket=story.PARTNER_BUCKET
    )
    self.measure_memory = measure_memory
    for page in self._BuildPages():
      self.AddStory(page)

  @abc.abstractmethod
  def _BuildPages(self):
    """Subclasses should implement this to return an iterator of pages."""


class MediaCasesDesktopStorySet(_MediaCasesStorySet):
  """
  Description: Video Stack Perf pages that report time_to_play, seek time and
  many other media-specific and generic metrics.
  """

  def _BuildPages(self):
    return iter(
      _GetCrossPlatformMediaPages(self) + _GetDesktopOnlyMediaPages(self)
    )


class MediaCasesMobileStorySet(_MediaCasesStorySet):
  """
  Description: Video Stack Perf pages that report time_to_play, seek time and
  many other media-specific and generic metrics.

  The mobile story set removes stories that are too difficult for mobile
  devices.
  """

  def _BuildPages(self):
    for page in _GetCrossPlatformMediaPages(self):
      if 'is_4k' in page.tags or 'is_50fps' in page.tags:
        continue
      yield page
