blob: 18cc9024d04490503fa85e87f9c6f09e6a4918d4 [file] [log] [blame]
# Copyright 2021 The Cobalt Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
""" Endurance test of playbacks."""
import argparse
import logging
import random
import time
import _env # pylint: disable=unused-import
from cobalt.media_integration_tests.test_app import Features, MediaSessionPlaybackState
from cobalt.media_integration_tests.test_case import TestCase
from cobalt.media_integration_tests.test_util import PlaybackUrls
# The variables below are all in seconds.
STATUS_CHECK_INTERVAL = 0.5
SINGLE_PLAYBACK_WATCH_TIME_MAXIMUM = 2.0 * 60 * 60
PLAYER_INITIALIZATION_WAITING_TIMEOUT = 5.0 * 60
MEDIA_TIME_UPDATE_WAITING_TIMEOUT = 10.0
WRITTEN_INPUT_WAITING_TIMEOUT = 30.0
PLAYBACK_END_WAITING_TIMEOUT = 30.0
# Needs to interact with the app regularly to keep it active. Otherwise, the
# playback may be paused.
IDLE_TIME_MAXIMUM = 60 * 60
class EnduranceTest(TestCase):
"""
Test case for playback endurance test.
"""
def __init__(self, *args, **kwargs):
super(EnduranceTest, self).__init__(*args, **kwargs)
parser = argparse.ArgumentParser()
parser.add_argument(
'--startup_url', default=PlaybackUrls.PLAYLIST, type=str)
# Stop the test after |max_running_time| (in hours). If |max_running_time|
# is 0 or negative, the test will not stop until it gets an error.
parser.add_argument('--max_running_time', default=20, type=float)
# Insert random actions if |random_action_interval| is greater than 0.
parser.add_argument('--random_action_interval', default=0.0, type=float)
args, _ = parser.parse_known_args()
self.startup_url = args.startup_url
# Convert |max_running_time| to seconds.
self.max_running_time = args.max_running_time * 60 * 60
self.needs_random_action = args.random_action_interval > 0
self.random_action_interval = args.random_action_interval
def ResetTimestamps(self):
self.last_media_time = -1
self.last_media_time_update_time = -1
self.last_written_audio_timestamp = -1
self.last_written_audio_update_time = -1
self.last_written_video_timestamp = -1
self.last_written_video_update_time = -1
self.audio_eos_written_time = -1
self.video_eos_written_time = -1
self.playback_end_time = -1
def OnPlayerStateChanged(self, player_state_handler, player_state):
current_running_time = time.time() - self.start_time
element_state = player_state.video_element_state
pipeline_state = player_state.pipeline_state
media_session_state = player_state.media_session_state
if not player_state_handler.IsPlayerPlaying():
return
# Reset timestamps after player identifier is changed.
if self.player_identifier != pipeline_state.identifier:
self.player_identifier = pipeline_state.identifier
self.playback_start_time = current_running_time
self.ResetTimestamps()
# Reset timestamps after resume. It could happen after pause, fastforward,
# rewind and playback switch.
if media_session_state.playback_state != MediaSessionPlaybackState.PLAYING:
self.playback_is_playing = False
elif not self.playback_is_playing:
self.playback_is_playing = True
self.ResetTimestamps()
# Update media time.
if self.last_media_time != element_state.current_time:
self.last_media_time = element_state.current_time
self.last_media_time_update_time = current_running_time
# Update written audio timestamp.
if (self.last_written_audio_timestamp !=
pipeline_state.last_written_audio_timestamp):
self.last_written_audio_timestamp = (
pipeline_state.last_written_audio_timestamp)
self.last_written_audio_update_time = current_running_time
# Update written video timestamp.
if (self.last_written_video_timestamp !=
pipeline_state.last_written_video_timestamp):
self.last_written_video_timestamp = (
pipeline_state.last_written_video_timestamp)
self.last_written_video_update_time = current_running_time
# Update audio eos timestamp.
if (self.audio_eos_written_time == -1 and
pipeline_state.is_audio_eos_written):
self.audio_eos_written_time = current_running_time
# Update video eos timestamp.
if (self.video_eos_written_time == -1 and
pipeline_state.is_video_eos_written):
self.video_eos_written_time = current_running_time
# Update playback end time.
if (self.audio_eos_written_time != -1 and
self.video_eos_written_time != -1 and self.playback_end_time == -1 and
element_state.current_time >= element_state.duration):
self.playback_end_time = current_running_time
def GenerateErrorString(self, err_msg):
return (
'%s (running time: %f, player identifier: "%s", is playing: %r, '
'playback start time: %f, last media time: %f (updated at %f), '
'last written audio timestamp: %d (updated at %f), '
'last written video timestamp: %d (updated at %f), '
'audio eos written at %f, video eos written at %f, '
'playback ended at %f).' %
(err_msg, time.time() - self.start_time, self.player_identifier,
self.playback_is_playing, self.playback_start_time,
self.last_media_time, self.last_media_time_update_time,
self.last_written_audio_timestamp, self.last_written_audio_update_time,
self.last_written_video_timestamp, self.last_written_video_update_time,
self.audio_eos_written_time, self.video_eos_written_time,
self.playback_end_time))
def SendRandomAction(self, app):
def SuspendAndResume(app):
app.Suspend()
app.WaitUntilReachState(
lambda _app: not _app.ApplicationState().is_visible)
# Wait for 1 second before resume.
time.sleep(1)
app.Resume()
actions = {
'PlayPause': lambda _app: _app.PlayOrPause(),
'Fastforward': lambda _app: _app.Fastforward(),
'Rewind': lambda _app: _app.Rewind(),
'PlayPrevious': lambda _app: _app.PlayPrevious(),
'PlayNext': lambda _app: _app.PlayNext(),
}
if TestCase.IsFeatureSupported(Features.SUSPEND_AND_RESUME):
actions['SuspendAndResume'] = SuspendAndResume
action_name = random.choice(list(actions.keys()))
logging.info('Send random action (%s).', action_name)
actions[action_name](app)
def test_playback_endurance(self):
self.start_time = time.time()
self.player_identifier = ''
self.playback_is_playing = False
self.playback_start_time = -1
self.last_state_check_time = 0
self.last_action_time = 0
self.ResetTimestamps()
app = self.CreateCobaltApp(self.startup_url)
app.AddPlayerStateChangeHandler(self.OnPlayerStateChanged)
with app:
while True:
# Put the sleep at the top of the loop.
time.sleep(STATUS_CHECK_INTERVAL)
current_running_time = time.time() - self.start_time
# Stop the test after reaching max running time.
if (self.max_running_time > 0 and
current_running_time > self.max_running_time):
break
# Skip if there's no running player.
if not app.player_state_handler.IsPlayerPlaying():
# TODO: identify network problem
self.assertTrue(
current_running_time - self.last_state_check_time <
PLAYER_INITIALIZATION_WAITING_TIMEOUT,
self.GenerateErrorString(
'Timed out waiting for player initialization (waited: %f).' %
(current_running_time - self.last_state_check_time)))
continue
self.last_state_check_time = current_running_time
# Skip to next playback if it has been played for long time.
if (self.playback_start_time != -1 and
current_running_time - self.playback_start_time >
SINGLE_PLAYBACK_WATCH_TIME_MAXIMUM):
app.PlayNext()
# Set start time to -1 here to avoid send same command in next loop.
self.playback_start_time = -1
continue
if self.playback_is_playing:
# Check media time.
if self.last_media_time_update_time > 0:
# TODO: identify network problem
self.assertTrue(
current_running_time - self.last_media_time_update_time <
MEDIA_TIME_UPDATE_WAITING_TIMEOUT,
self.GenerateErrorString(
'Timed out waiting for media time update (waited: %f).' %
(current_running_time - self.last_media_time_update_time)))
# Check written audio timestamp.
if (self.last_written_audio_update_time > 0 and
self.audio_eos_written_time == -1):
self.assertTrue(
current_running_time - self.last_written_audio_update_time <
WRITTEN_INPUT_WAITING_TIMEOUT,
self.GenerateErrorString(
'Timed out waiting for new audio input (waited: %f).' %
(current_running_time -
self.last_written_audio_update_time)))
# Check written video timestamp.
if (self.last_written_video_update_time > 0 and
self.video_eos_written_time == -1):
self.assertTrue(
current_running_time - self.last_written_video_update_time <
WRITTEN_INPUT_WAITING_TIMEOUT,
self.GenerateErrorString(
'Timed out waiting for new video input (waited: %f).' %
(current_running_time -
self.last_written_video_update_time)))
# Check if the playback ends properly.
if (self.audio_eos_written_time > 0 and
self.video_eos_written_time > 0 and self.playback_end_time > 0):
self.assertTrue(
current_running_time - self.playback_end_time <
PLAYBACK_END_WAITING_TIMEOUT,
self.GenerateErrorString(
'Timed out waiting for playback to end (waited: %f).' %
(current_running_time - self.playback_end_time,)))
# Send random actions.
if (self.needs_random_action and
current_running_time - self.last_action_time >
self.random_action_interval):
self.SendRandomAction(app)
self.last_action_time = current_running_time
# Interact with the app to keep it active.
if current_running_time - self.last_action_time > IDLE_TIME_MAXIMUM:
# Play the previous playback again.
app.PlayPrevious()
self.last_action_time = current_running_time
app.RemovePlayerStateChangeHandler(self.OnPlayerStateChanged)