1"""Extracts media info metrics from media info data points."""
2
3import collections
4import enum
5
6
7# Direction and type numbers map to constants in the DataPoint group in
8# callstats.proto
9class Direction(enum.Enum):
10    """
11    Possible directions for media entries of a data point.
12    """
13    SENDER = 0
14    RECEIVER = 1
15    BANDWIDTH_ESTIMATION = 2
16
17
18class MediaType(enum.Enum):
19    """
20    Possible media types for media entries of a data point.
21    """
22    AUDIO = 1
23    VIDEO = 2
24    DATA = 3
25
26
27TimestampedValues = collections.namedtuple('TimestampedValues',
28                                           ['TimestampEpochSeconds',
29                                            'ValueList'])
30
31
32class MediaInfoMetricsExtractor(object):
33    """
34    Extracts media metrics from a list of raw media info data points.
35
36    Media info datapoints are expected to be dictionaries in the format returned
37    by cfm_facade.get_media_info_data_points.
38    """
39
40    def __init__(self, data_points):
41        """
42        Initializes with a set of data points.
43
44        @param data_points Data points as a list of dictionaries. Dictionaries
45            should be in the format returned by
46            cfm_facade.get_media_info_data_points.  I.e., as returned when
47            querying the Browser Window for datapoints when the ExportMediaInfo
48            mod is active.
49        """
50        self._data_points = data_points
51
52    def get_top_level_metric(self, name):
53        """
54        Gets top level metrics.
55
56        Gets metrics that are global for one datapoint. I.e., metrics that
57        are not in the media list, such as CPU usage.
58
59        @param name Name of the metric. Names map to the names in the DataPoint
60            group in callstats.proto.
61
62        @returns A list with TimestampedValues. The ValueList is guaranteed to
63            not contain any None values.
64
65        @raises KeyError If any datapoint is missing a metric with the
66            specified name.  The KeyError will contain 2 args. The first is the
67            key, the second the entire data_point dictionary.
68        """
69        metrics = []
70        for data_point in self._data_points:
71            value = _get_value(data_point, name)
72            if value is not None:
73                timestamp = data_point['timestamp']
74                metrics.append(TimestampedValues(timestamp, [value]))
75        return metrics
76
77    def get_media_metric(self,
78                         name,
79                         direction=None,
80                         media_type=None,
81                         post_process_func=lambda x: x):
82        """
83        Gets media metrics.
84
85        Gets metrics that are in the media part of the datapoint. A DataPoint
86        often contains many media entries, why there are multiple values for a
87        specific name.
88
89        @param name Name of the metric. Names map to the names in the DataPoint
90            group in callstats.proto.
91        @param direction: Only include metrics with this direction in the media
92            stream. See the Direction constants in this module. If None, all
93            directions are included.
94        @param media_type: Only include metrics with this media type in the
95            media stream.  See the MediaType constants in this module. If None,
96            all media types are included.
97        @param post_process_func: Function that takes a list of values and
98            optionally transforms it, returning the updated list or a scalar
99            value.  Default is to return the unmodified list. This method is
100            called for the list of values in the same datapoint. Example usage
101            is to sum the received audio bytes for all streams for one logged
102            line.
103        @raises KeyError If any data point matching the direction and
104            media_type filter is missing metrics with the specified name. The
105            KeyError will contain 2 args. The first is the key, the second the
106            media dictionary that is missing the key.
107
108        @returns A list with TimestampedValues. The ValueList is guaranteed to
109            not contain any None values.
110        """
111        metrics = []
112        for data_point in self._data_points:
113            timestamp = data_point['timestamp']
114            values = [
115                    _get_value(media, name)
116                    for media in data_point['media']
117                    if _media_matches(media, direction, media_type)
118            ]
119            # Filter None values and only add the metric if there is at least
120            # one value left.
121            values = [x for x in values if x is not None]
122            if values:
123                values = post_process_func(values)
124                values = values if isinstance(values, list) else [values]
125                metrics.append(TimestampedValues(timestamp, values))
126        return metrics
127
128
129def _get_value(dictionary, key):
130    """
131    Returns the value in the dictionary for the specified key.
132
133    The only difference of using this method over dictionary[key] is that the
134    KeyError raised here contains both the key and the dictionary.
135
136    @param dictionary The dictionary to lookup the key in.
137    @param key The key to lookup.
138
139    @raises KeyError If the key does not exist. The KeyError will contain 2
140            args. The first is the key, the second the entire dictionary.
141    """
142    try:
143        return dictionary[key]
144    except KeyError:
145        raise KeyError(key, dictionary)
146
147def _media_matches(media, direction, media_type):
148    direction_match = (True if direction is None
149                       else media['direction'] == direction.value)
150    type_match = (True if media_type is None
151                  else media['mediatype'] == media_type.value)
152    return direction_match and type_match
153
154