356 lines
12 KiB
Swift
356 lines
12 KiB
Swift
// Copyright 2013 The Flutter Authors
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import AVFoundation
|
|
import Flutter
|
|
import XCTest
|
|
|
|
@testable import camera_avfoundation
|
|
|
|
private let testResolutionPreset = PlatformResolutionPreset.medium
|
|
private let testFramesPerSecond: Int64 = 15
|
|
private let testVideoBitrate: Int64 = 200000
|
|
private let testAudioBitrate: Int64 = 32000
|
|
|
|
private final class TestMediaSettingsAVWrapper: FLTCamMediaSettingsAVWrapper {
|
|
let lockExpectation: XCTestExpectation
|
|
let unlockExpectation: XCTestExpectation
|
|
let minFrameDurationExpectation: XCTestExpectation
|
|
let maxFrameDurationExpectation: XCTestExpectation
|
|
let beginConfigurationExpectation: XCTestExpectation
|
|
let commitConfigurationExpectation: XCTestExpectation
|
|
let audioSettingsExpectation: XCTestExpectation
|
|
let videoSettingsExpectation: XCTestExpectation
|
|
|
|
init(test: XCTestCase, expectAudio: Bool) {
|
|
lockExpectation = test.expectation(description: "lockExpectation")
|
|
unlockExpectation = test.expectation(description: "unlockExpectation")
|
|
minFrameDurationExpectation = test.expectation(description: "minFrameDurationExpectation")
|
|
maxFrameDurationExpectation = test.expectation(description: "maxFrameDurationExpectation")
|
|
beginConfigurationExpectation = test.expectation(description: "beginConfigurationExpectation")
|
|
commitConfigurationExpectation = test.expectation(description: "commitConfigurationExpectation")
|
|
audioSettingsExpectation = test.expectation(description: "audioSettingsExpectation")
|
|
audioSettingsExpectation.isInverted = !expectAudio
|
|
videoSettingsExpectation = test.expectation(description: "videoSettingsExpectation")
|
|
}
|
|
|
|
override func lockDevice(_ captureDevice: CaptureDevice) throws {
|
|
lockExpectation.fulfill()
|
|
}
|
|
|
|
override func unlockDevice(_ captureDevice: CaptureDevice) {
|
|
unlockExpectation.fulfill()
|
|
}
|
|
|
|
override func beginConfiguration(for videoCaptureSession: CaptureSession) {
|
|
beginConfigurationExpectation.fulfill()
|
|
}
|
|
|
|
override func commitConfiguration(for videoCaptureSession: CaptureSession) {
|
|
commitConfigurationExpectation.fulfill()
|
|
}
|
|
|
|
override func setMinFrameDuration(_ duration: CMTime, on captureDevice: CaptureDevice) {
|
|
// Camera allows to set frame rate with 1/10 precision.
|
|
let expectedDuration = CMTimeMake(value: 10, timescale: Int32(testFramesPerSecond * 10))
|
|
if duration == expectedDuration {
|
|
minFrameDurationExpectation.fulfill()
|
|
}
|
|
}
|
|
|
|
override func setMaxFrameDuration(_ duration: CMTime, on captureDevice: CaptureDevice) {
|
|
// Camera allows to set frame rate with 1/10 precision.
|
|
let expectedDuration = CMTimeMake(value: 10, timescale: Int32(testFramesPerSecond * 10))
|
|
if duration == expectedDuration {
|
|
maxFrameDurationExpectation.fulfill()
|
|
}
|
|
}
|
|
|
|
override func assetWriterAudioInput(withOutputSettings outputSettings: [String: Any]?)
|
|
-> AssetWriterInput
|
|
{
|
|
if let bitrate = outputSettings?[AVEncoderBitRateKey] as? Int, bitrate == Int(testAudioBitrate)
|
|
{
|
|
audioSettingsExpectation.fulfill()
|
|
}
|
|
return MockAssetWriterInput()
|
|
}
|
|
|
|
override func assetWriterVideoInput(withOutputSettings outputSettings: [String: Any]?)
|
|
-> AssetWriterInput
|
|
{
|
|
if let compressionProperties = outputSettings?[AVVideoCompressionPropertiesKey]
|
|
as? [String: Any],
|
|
let bitrate = compressionProperties[AVVideoAverageBitRateKey] as? Int,
|
|
let frameRate = compressionProperties[AVVideoExpectedSourceFrameRateKey] as? Double,
|
|
bitrate == testVideoBitrate, frameRate == Double(testFramesPerSecond)
|
|
{
|
|
videoSettingsExpectation.fulfill()
|
|
}
|
|
|
|
// AVAssetWriterInput needs these three keys, otherwise it throws.
|
|
var outputSettingsWithRequiredKeys = outputSettings ?? [:]
|
|
outputSettingsWithRequiredKeys[AVVideoCodecKey] = AVVideoCodecType.h264
|
|
outputSettingsWithRequiredKeys[AVVideoWidthKey] = 1280
|
|
outputSettingsWithRequiredKeys[AVVideoHeightKey] = 720
|
|
|
|
return MockAssetWriterInput()
|
|
}
|
|
|
|
override func addInput(_ writerInput: AssetWriterInput, to writer: AssetWriter) {
|
|
// No-op.
|
|
}
|
|
|
|
override func recommendedVideoSettingsForAssetWriter(
|
|
withFileType fileType: AVFileType, for output: CaptureVideoDataOutput
|
|
) -> [String: Any]? {
|
|
return [:]
|
|
}
|
|
}
|
|
|
|
final class CameraSettingsTests: XCTestCase {
|
|
func testSettings_shouldPassConfigurationToCameraDeviceAndWriter() {
|
|
let enableAudio: Bool = true
|
|
let settings = PlatformMediaSettings(
|
|
resolutionPreset: testResolutionPreset,
|
|
framesPerSecond: testFramesPerSecond,
|
|
videoBitrate: testVideoBitrate,
|
|
audioBitrate: testAudioBitrate,
|
|
enableAudio: enableAudio
|
|
)
|
|
let injectedWrapper = TestMediaSettingsAVWrapper(test: self, expectAudio: enableAudio)
|
|
|
|
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
|
configuration.mediaSettingsWrapper = injectedWrapper
|
|
configuration.mediaSettings = settings
|
|
let camera = CameraTestUtils.createTestCamera(configuration)
|
|
|
|
// Expect FPS configuration is passed to camera device.
|
|
wait(
|
|
for: [
|
|
injectedWrapper.lockExpectation,
|
|
injectedWrapper.beginConfigurationExpectation,
|
|
injectedWrapper.minFrameDurationExpectation,
|
|
injectedWrapper.maxFrameDurationExpectation,
|
|
injectedWrapper.commitConfigurationExpectation,
|
|
injectedWrapper.unlockExpectation,
|
|
], timeout: 1, enforceOrder: true)
|
|
|
|
camera.startVideoRecording(
|
|
completion: { error in
|
|
// No-op.
|
|
}, messengerForStreaming: nil)
|
|
|
|
wait(
|
|
for: [
|
|
injectedWrapper.audioSettingsExpectation,
|
|
injectedWrapper.videoSettingsExpectation,
|
|
], timeout: 1)
|
|
}
|
|
|
|
func testSettings_ShouldBeSupportedByMethodCall() {
|
|
let mockDevice = MockCaptureDevice()
|
|
let mockSession = MockCaptureSession()
|
|
mockSession.canSetSessionPresetStub = { _ in true }
|
|
let camera = CameraPlugin(
|
|
registry: MockFlutterTextureRegistry(),
|
|
messenger: MockFlutterBinaryMessenger(),
|
|
globalAPI: MockGlobalEventApi(),
|
|
deviceDiscoverer: MockCameraDeviceDiscoverer(),
|
|
permissionManager: MockCameraPermissionManager(),
|
|
deviceFactory: { _ in mockDevice },
|
|
captureSessionFactory: { mockSession },
|
|
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
|
|
captureSessionQueue: DispatchQueue(label: "io.flutter.camera.captureSessionQueue")
|
|
)
|
|
|
|
let expectation = self.expectation(description: "Result finished")
|
|
let mediaSettings = PlatformMediaSettings(
|
|
resolutionPreset: testResolutionPreset,
|
|
framesPerSecond: testFramesPerSecond,
|
|
videoBitrate: testVideoBitrate,
|
|
audioBitrate: testAudioBitrate,
|
|
enableAudio: false
|
|
)
|
|
var resultValue: Int64?
|
|
camera.createCameraOnSessionQueue(
|
|
withName: "acamera",
|
|
settings: mediaSettings
|
|
) { result in
|
|
resultValue = self.assertSuccess(result)
|
|
expectation.fulfill()
|
|
}
|
|
|
|
waitForExpectations(timeout: 30, handler: nil)
|
|
XCTAssertNotNil(resultValue)
|
|
}
|
|
|
|
func testSettings_ShouldSelectFormatWhichSupports60FPS() {
|
|
let settings = PlatformMediaSettings(
|
|
resolutionPreset: testResolutionPreset,
|
|
framesPerSecond: 60,
|
|
videoBitrate: testVideoBitrate,
|
|
audioBitrate: testAudioBitrate,
|
|
enableAudio: false
|
|
)
|
|
|
|
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
|
configuration.mediaSettings = settings
|
|
let camera = CameraTestUtils.createTestCamera(configuration)
|
|
|
|
let range = camera.captureDevice.flutterActiveFormat.flutterVideoSupportedFrameRateRanges[0]
|
|
XCTAssertLessThanOrEqual(range.minFrameRate, 60)
|
|
XCTAssertGreaterThanOrEqual(range.maxFrameRate, 60)
|
|
}
|
|
|
|
func test_setUpCaptureSessionForAudioIfNeeded_skipsAudioSession_whenAudioDisabled() {
|
|
let settings = PlatformMediaSettings(
|
|
resolutionPreset: testResolutionPreset,
|
|
framesPerSecond: testFramesPerSecond,
|
|
videoBitrate: testVideoBitrate,
|
|
audioBitrate: testAudioBitrate,
|
|
enableAudio: false
|
|
)
|
|
|
|
let wrapper = TestMediaSettingsAVWrapper(test: self, expectAudio: false)
|
|
let mockAudioSession = MockCaptureSession()
|
|
|
|
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
|
configuration.mediaSettingsWrapper = wrapper
|
|
configuration.mediaSettings = settings
|
|
configuration.audioCaptureSession = mockAudioSession
|
|
let camera = CameraTestUtils.createTestCamera(configuration)
|
|
|
|
wait(
|
|
for: [
|
|
wrapper.lockExpectation,
|
|
wrapper.beginConfigurationExpectation,
|
|
wrapper.minFrameDurationExpectation,
|
|
wrapper.maxFrameDurationExpectation,
|
|
wrapper.commitConfigurationExpectation,
|
|
wrapper.unlockExpectation,
|
|
],
|
|
timeout: 1,
|
|
enforceOrder: true
|
|
)
|
|
|
|
camera.startVideoRecording(completion: { _ in }, messengerForStreaming: nil)
|
|
|
|
wait(
|
|
for: [
|
|
wrapper.audioSettingsExpectation,
|
|
wrapper.videoSettingsExpectation,
|
|
],
|
|
timeout: 1
|
|
)
|
|
|
|
XCTAssertEqual(
|
|
mockAudioSession.addedAudioOutputCount, 0,
|
|
"Audio session should not receive AVCaptureAudioDataOutput when enableAudio is false"
|
|
)
|
|
}
|
|
|
|
func test_setUpCaptureSessionForAudioIfNeeded_addsAudioSession_whenAudioEnabled() {
|
|
let settings = PlatformMediaSettings(
|
|
resolutionPreset: testResolutionPreset,
|
|
framesPerSecond: testFramesPerSecond,
|
|
videoBitrate: testVideoBitrate,
|
|
audioBitrate: testAudioBitrate,
|
|
enableAudio: true
|
|
)
|
|
|
|
let wrapper = TestMediaSettingsAVWrapper(test: self, expectAudio: true)
|
|
let mockAudioSession = MockCaptureSession()
|
|
|
|
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
|
configuration.mediaSettingsWrapper = wrapper
|
|
configuration.mediaSettings = settings
|
|
configuration.audioCaptureSession = mockAudioSession
|
|
let camera = CameraTestUtils.createTestCamera(configuration)
|
|
|
|
wait(
|
|
for: [
|
|
wrapper.lockExpectation,
|
|
wrapper.beginConfigurationExpectation,
|
|
wrapper.minFrameDurationExpectation,
|
|
wrapper.maxFrameDurationExpectation,
|
|
wrapper.commitConfigurationExpectation,
|
|
wrapper.unlockExpectation,
|
|
],
|
|
timeout: 1,
|
|
enforceOrder: true
|
|
)
|
|
|
|
camera.startVideoRecording(completion: { _ in }, messengerForStreaming: nil)
|
|
|
|
wait(
|
|
for: [
|
|
wrapper.audioSettingsExpectation,
|
|
wrapper.videoSettingsExpectation,
|
|
],
|
|
timeout: 1
|
|
)
|
|
|
|
XCTAssertGreaterThan(
|
|
mockAudioSession.addedAudioOutputCount, 0,
|
|
"Audio session should receive AVCaptureAudioDataOutput when enableAudio is true"
|
|
)
|
|
}
|
|
|
|
func testResolutionPresetWithMax_mustIgnoreLossyFormatsAndSquares() {
|
|
let videoSessionMock = MockCaptureSession()
|
|
videoSessionMock.canSetSessionPresetStub = { _ in true }
|
|
|
|
let lossyFormat = MockCaptureDeviceFormat(
|
|
codecType: 1_651_798_066, // 'btp2'
|
|
width: 4224,
|
|
height: 3024
|
|
)
|
|
let squareFormat = MockCaptureDeviceFormat(
|
|
codecType: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
|
|
width: 4032,
|
|
height: 4032
|
|
)
|
|
let safe4KFormat = MockCaptureDeviceFormat(
|
|
codecType: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
|
|
width: 3840,
|
|
height: 2160
|
|
)
|
|
|
|
let captureDeviceMock = MockCaptureDevice()
|
|
captureDeviceMock.flutterFormats = [lossyFormat, squareFormat, safe4KFormat]
|
|
|
|
var currentFormat: CaptureDeviceFormat = safe4KFormat
|
|
captureDeviceMock.activeFormatStub = { currentFormat }
|
|
captureDeviceMock.setActiveFormatStub = { newFormat in currentFormat = newFormat }
|
|
|
|
let configuration = CameraTestUtils.createTestCameraConfiguration()
|
|
configuration.videoCaptureDeviceFactory = { _ in captureDeviceMock }
|
|
configuration.videoCaptureSession = videoSessionMock
|
|
|
|
configuration.videoDimensionsConverter = { format in
|
|
return CMVideoFormatDescriptionGetDimensions(format.formatDescription)
|
|
}
|
|
|
|
configuration.mediaSettings = CameraTestUtils.createDefaultMediaSettings(
|
|
resolutionPreset: PlatformResolutionPreset.max
|
|
)
|
|
|
|
let _ = CameraTestUtils.createTestCamera(configuration)
|
|
|
|
let selectedFormat = captureDeviceMock.flutterActiveFormat
|
|
let selectedDimensions = CMVideoFormatDescriptionGetDimensions(selectedFormat.formatDescription)
|
|
|
|
XCTAssertEqual(
|
|
selectedDimensions.width,
|
|
3840,
|
|
"Camera should have ignored the lossy and square formats, safely falling back to 4K."
|
|
)
|
|
XCTAssertEqual(
|
|
selectedDimensions.height,
|
|
2160,
|
|
"Camera should have ignored the lossy and square formats, safely falling back to 4K."
|
|
)
|
|
}
|
|
}
|