• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2020 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4import collections
5import enum
6import json
7import os
8import logging
9import time
10
11from autotest_lib.client.common_lib import error
12from autotest_lib.client.common_lib.cros import chrome
13from autotest_lib.client.common_lib.cros import power_load_util
14from autotest_lib.client.cros.input_playback import keyboard
15from autotest_lib.client.cros.power import power_dashboard
16from autotest_lib.client.cros.power import power_status
17from autotest_lib.client.cros.power import power_test
18
19
20class power_MeetClient(power_test.power_Test):
21    """class for power_MeetClient test.
22
23    This test should be call from power_MeetCall server test only.
24    """
25    version = 1
26
27    video_url = 'http://meet.google.com'
28    doc_url = 'http://doc.new'
29
30    def initialize(self,
31                   seconds_period=5.,
32                   pdash_note='',
33                   force_discharge=False):
34        """initialize method."""
35        super(power_MeetClient, self).initialize(
36                seconds_period=seconds_period,
37                pdash_note=pdash_note,
38                force_discharge=force_discharge)
39
40    def run_once(self,
41                 meet_code,
42                 duration=180,
43                 layout='Tiled',
44                 username=None,
45                 password=None):
46        """run_once method.
47
48        @param meet_code: Meet code generated in power_MeetCall.
49        @param duration: duration in seconds.
50        @param layout: string of meet layout to use.
51        @param username: Google account to use.
52        @param password: password for Google account.
53        """
54        if not username and not password:
55            username = power_load_util.get_meet_username()
56            password = power_load_util.get_meet_password()
57        if not username or not password:
58            raise error.TestFail('Need to supply both username and password.')
59        extra_browser_args = self.get_extra_browser_args_for_camera_test()
60        with keyboard.Keyboard() as keys,\
61             chrome.Chrome(init_network_controller=True,
62                           gaia_login=True,
63                           username=username,
64                           password=password,
65                           extra_browser_args=extra_browser_args,
66                           autotest_ext=True) as cr:
67
68            # Move existing window to left half and open video page
69            tab = cr.browser.tabs[0]
70            tab.Activate()
71
72            # Run in full-screen.
73            fullscreen = tab.EvaluateJavaScript('document.webkitIsFullScreen')
74            if not fullscreen:
75                keys.press_key('f4')
76
77            url = self.video_url + '/' + meet_code
78            logging.info('Navigating left window to %s', url)
79            tab.Navigate(url)
80
81            # Workaround when camera isn't init for some unknown reason.
82            time.sleep(10)
83            tab.EvaluateJavaScript('location.reload()')
84
85            tab.WaitForDocumentReadyStateToBeComplete()
86            logging.info(meet_code)
87            self.keyvals['meet_code'] = meet_code
88
89            def wait_until(cond, error_msg):
90                """Helper for javascript polling wait."""
91                for _ in range(60):
92                    time.sleep(1)
93                    if tab.EvaluateJavaScript(cond):
94                        return
95                raise error.TestFail(error_msg)
96
97            wait_until('window.hasOwnProperty("hrTelemetryApi")',
98                       'Meet API does not existed.')
99            wait_until('hrTelemetryApi.isInMeeting()',
100                       'Can not join meeting.')
101            wait_until('hrTelemetryApi.getParticipantCount() > 1',
102                       'Meeting has no other participant.')
103
104            # Make sure camera and mic are on.
105            tab.EvaluateJavaScript('hrTelemetryApi.setCameraMuted(false)')
106            tab.EvaluateJavaScript('hrTelemetryApi.setMicMuted(false)')
107
108            if layout == 'Tiled':
109                tab.EvaluateJavaScript('hrTelemetryApi.setTiledLayout()')
110            elif layout == 'Auto':
111                tab.EvaluateJavaScript('hrTelemetryApi.setAutoLayout()')
112            elif layout == 'Sidebar':
113                tab.EvaluateJavaScript('hrTelemetryApi.setSidebarLayout()')
114            elif layout == 'Spotlight':
115                tab.EvaluateJavaScript('hrTelemetryApi.setSpotlightLayout()')
116            else:
117                raise error.TestError('Unknown layout %s' % layout)
118
119            self.keyvals['layout'] = layout
120
121            self.start_measurements()
122            time.sleep(duration)
123            end_time = self._start_time + duration
124
125            # Collect stat
126            if not tab.EvaluateJavaScript('window.hasOwnProperty("realtime")'):
127                logging.info('Account %s is not in allowlist for MediaInfoAPI',
128                             username)
129                return
130
131            meet_data = tab.EvaluateJavaScript(
132                'realtime.media.getMediaInfoDataPoints()')
133
134            power_dashboard.get_dashboard_factory().registerDataType(
135                MeetStatLogger, MeetStatDashboard)
136
137            self._meas_logs.append(
138                    MeetStatLogger(self._start_time, end_time, meet_data))
139
140
141class MeetStatLogger(power_status.MeasurementLogger):
142    """Class for logging meet data point to power dashboard.
143
144    Format of meet_data http://google3/logs/proto/buzz/callstats.proto
145    """
146
147    def __init__(self, start_ts, end_ts, meet_data):
148        # Do not call parent constructor to avoid making a new thread.
149        self.times = [start_ts]
150
151        # Meet epoch timestamp uses millisec unit.
152        self.meet_data = [data_point for data_point in meet_data
153            if start_ts * 1000 <= data_point['timestamp'] <= end_ts * 1000]
154
155    def calc(self, mtype=None):
156        return {}
157
158    def save_results(self, resultsdir, fname_prefix=None):
159        # Save raw dict from meet to file. Ignore fname_prefix.
160        with open(os.path.join(resultsdir, 'meet_powerlog.json'), 'w') as f:
161            json.dump(self.meet_data , f, indent=4, separators=(',', ': '),
162                      ensure_ascii=False)
163
164
165class MeetStatDashboard(power_dashboard.MeasurementLoggerDashboard):
166    """Dashboard class for MeetStatLogger class."""
167
168    # Direction and type numbers map to constants in the proto
169    class Direction(enum.IntEnum):
170        """Possible directions for media entries of a data point."""
171        SENDER = 0
172        RECEIVER = 1
173
174    class MediaType(enum.IntEnum):
175        """Possible media types for media entries of a data point."""
176        VIDEO = 2
177
178    # Important metrics to collect.
179    MEET_KEYS = [
180        'encodeUsagePercent',
181        'fps',
182        'height',
183        'width',
184    ]
185
186    def _get_ssrc_dict(self, meet_data):
187        """ Extract http://what/ssrc for all video stream and map to string.
188
189        The format of the string would be sender_# / receiver_# where # denotes
190        index for the video counting from 0.
191
192        Returns:
193            dict from ssrc to video stream string.
194        """
195        ret = {}
196        count = [0, 0]
197
198        # We only care about video streams.
199        for media in meet_data[-1]['media']:
200            if media['mediatype'] != self.MediaType.VIDEO:
201                continue
202            if (media['direction'] != self.Direction.SENDER and
203                media['direction'] != self.Direction.RECEIVER):
204                continue
205            name = [media['directionStr'], str(count[media['direction']])]
206            if media['direction'] == self.Direction.SENDER:
207                name.append(media['sendercodecname'])
208            else:
209                name.append(media['receiverCodecName'])
210            count[media['direction']] += 1
211            ret[media['ssrc']] = '_'.join(name)
212
213        return ret
214
215    def _get_meet_unit(self, key):
216        """Return unit from name of the key."""
217        if key.endswith('fps'):
218            return 'fps'
219        if key.endswith('Percent'):
220            return 'percent'
221        if key.endswith('width') or key.endswith('height') :
222            return 'point'
223        raise error.TestError('Unexpected key: %s' % key)
224
225    def _get_meet_type(self, key):
226        """Return type from name of the key."""
227        if key.endswith('fps'):
228            return 'meet_fps'
229        if key.endswith('Percent'):
230            return 'meet_encoder_load'
231        if key.endswith('width'):
232            return 'meet_width'
233        if key.endswith('height'):
234            return 'meet_height'
235        raise error.TestError('Unexpected key: %s' % key)
236
237    def _convert(self):
238        """Convert meet raw dict to data to power dict."""
239
240        meet_data = self._logger.meet_data
241        ssrc_dict = self._get_ssrc_dict(meet_data)
242
243        # Dict from timestamp to dict of meet_key to value
244        parse_dict = collections.defaultdict(
245                     lambda: collections.defaultdict(int))
246
247        key_set = set()
248        testname='power_MeetCall'
249
250        for data_point in meet_data:
251            timestamp = data_point['timestamp']
252            for media in data_point['media']:
253                ssrc = media.get('ssrc', 0)
254                if ssrc not in ssrc_dict:
255                    continue
256                name = ssrc_dict[media['ssrc']]
257                for meet_key in self.MEET_KEYS:
258                    if meet_key not in media:
259                        continue
260                    key = '%s_%s' % (name, meet_key)
261                    key_set.add(key)
262                    parse_dict[timestamp][key] = media[meet_key]
263
264        timestamps = sorted(parse_dict.keys())
265        sample_count = len(timestamps)
266
267        powerlog_data = collections.defaultdict(list)
268        for ts in sorted(parse_dict.keys()):
269            for key in key_set:
270                powerlog_data[key].append(parse_dict[ts][key])
271
272        powerlog_dict =  {
273            'sample_count': sample_count,
274            'sample_duration': 1,
275            'average': {k: 1.0 * sum(v) / sample_count
276                        for k, v in powerlog_data.iteritems()},
277            'data': powerlog_data,
278            'unit': {k: self._get_meet_unit(k) for k in key_set},
279            'type': {k: self._get_meet_type(k) for k in key_set},
280            'checkpoint': [[testname]] * sample_count,
281        }
282
283        return powerlog_dict
284