1# Copyright 2023 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Verifies advertised FPS from device as webcam."""
15
16import logging
17import time
18
19import AVFoundation
20import Cocoa
21from libdispatch import dispatch_queue_create
22import objc
23
24
25objc.loadBundle('AVFoundation',
26                bundle_path=objc.pathForFramework('AVFoundation.framework'),
27                module_globals=globals())
28
29_TOTAL_NUM_FRAMES = 0
30_DEVICE_NAME = 'android' # TODO b/277159494
31_FPS_TEST_DURATION = 10  # seconds
32
33
34class SampleBufferDelegate(Cocoa.NSObject):
35  """Notified upon every frame. Used to help calculate FPS.
36  """
37
38  def captureOutput_didOutputSampleBuffer_fromConnection_(self,
39                                                          output,
40                                                          sample_buffer,
41                                                          connection):
42    global _TOTAL_NUM_FRAMES
43
44    if sample_buffer:
45      _TOTAL_NUM_FRAMES += 1
46
47
48def initialize_device():
49  """Initializes android webcam device.
50
51  Returns:
52      Returns the device if an android device is found, None otherwise
53  """
54  devices = AVFoundation.AVCaptureDevice.devices()
55
56  res_device = None
57  for device in devices:
58    if (device.hasMediaType_(AVFoundation.AVMediaTypeVideo) and
59        (_DEVICE_NAME in device.localizedName().lower())):
60      res_device = device
61      logging.info('Using webcam %s', res_device.localizedName())
62      break
63
64  return res_device
65
66
67def initialize_formats_and_resolutions(device):
68  """Initializes list of device advertised formats, resolutions and FPS.
69
70  Args:
71      device: the device from which the info will be retrieved
72
73  Returns:
74      Returns a list of the formats, resolutions, and frame rates
75      [ (format, [resolution, [frame rates] ] )]
76  """
77  supported_formats = device.formats()
78  formats_and_resolutions = []
79
80  for format_index, frmt in enumerate(supported_formats):
81    formats_and_resolutions.append((frmt, []))
82    frame_rate_ranges = frmt.videoSupportedFrameRateRanges()
83
84    for frame_rate_range in frame_rate_ranges:
85      min_frame_rate = frame_rate_range.minFrameRate()
86      max_frame_rate = frame_rate_range.maxFrameRate()
87      default_frame_rate = (
88          device.activeVideoMinFrameDuration().timescale /
89          device.activeVideoMinFrameDuration().value)
90
91      formats_and_resolutions[format_index][1].append(min_frame_rate + 1)
92      formats_and_resolutions[format_index][1].append(max_frame_rate)
93      formats_and_resolutions[format_index][1].append(default_frame_rate)
94
95  return formats_and_resolutions
96
97
98def setup_for_test_fps(device, supported_formats_and_resolutions):
99  """Configures device with format, resolution, and FPS to be tested.
100
101  Args:
102      device: device under test
103      supported_formats_and_resolutions: list containing supported device
104          configurations
105
106  Returns:
107      Returns a list of tuples, where the first element is the frame
108      rate that was being tested and the second element is the actual fps
109  """
110  res = []
111
112  delegate = SampleBufferDelegate.alloc().init()
113  session = AVFoundation.AVCaptureSession.alloc().init()
114
115  input_for_session = (
116      AVFoundation.AVCaptureDeviceInput.deviceInputWithDevice_error_(device,
117                                                                     None))
118  session.addInput_(input_for_session[0])
119
120  video_output = AVFoundation.AVCaptureVideoDataOutput.alloc().init()
121  queue = dispatch_queue_create(b'1', None)
122  video_output.setSampleBufferDelegate_queue_(delegate, queue)
123  session.addOutput_(video_output)
124
125  session.startRunning()
126
127  for elem in supported_formats_and_resolutions:
128    frmt = elem[0]
129    frame_rates = elem[1]
130
131    for frame_rate in frame_rates:
132      global _TOTAL_NUM_FRAMES
133      _TOTAL_NUM_FRAMES = 0  # reset total num frames for every test
134      device.lockForConfiguration_(None)
135      device.setActiveFormat_(frmt)
136      device.setActiveVideoMinFrameDuration_(
137          AVFoundation.CMTimeMake(1, frame_rate))
138      device.setActiveVideoMaxFrameDuration_(
139          AVFoundation.CMTimeMake(1, frame_rate))
140      device.unlockForConfiguration()
141
142      res.append((frame_rate, test_fps()))
143
144  session.removeInput_(input_for_session)
145  session.removeOutput_(video_output)
146  session.stopRunning()
147  input_for_session[0].release()
148  video_output.release()
149  return res
150
151
152def test_fps():
153  """Tests and returns test estimated fps.
154
155  Returns:
156      Test estimated fps
157  """
158  start_time = time.time()
159  time.sleep(_FPS_TEST_DURATION)
160  end_time = time.time()
161  fps = _TOTAL_NUM_FRAMES / (end_time - start_time)
162  return fps
163
164
165def main():
166  device = initialize_device()
167
168  if not device:
169    logging.error('Supported device not found!')
170    return []
171
172  supported_formats_and_resolutions = initialize_formats_and_resolutions(device)
173
174  if not supported_formats_and_resolutions:
175    logging.error('Error retrieving formats and resolutions')
176    return []
177
178  res = setup_for_test_fps(device, supported_formats_and_resolutions)
179
180  return res
181
182
183if __name__ == '__main__':
184  main()
185