From 11b7c796f8520595ec84bf70e26acaff9c04b9f6 Mon Sep 17 00:00:00 2001 From: ThibaultBee <37510686+ThibaultBee@users.noreply.github.com> Date: Fri, 9 Feb 2024 16:15:38 +0100 Subject: [PATCH] feat(*): make resolution clearer --- .../ExampleUIkit/Settings.bundle/Root.plist | 12 +- .../ExampleUIkit/SettingsManager.swift | 15 ++- .../ApiVideoLiveStream.swift | 42 +++--- .../models/Configuration.swift | 62 ++++++--- .../models/Resolution.swift | 125 ++++++++++++------ 5 files changed, 163 insertions(+), 93 deletions(-) diff --git a/Examples/ExampleUIKit/ExampleUIkit/Settings.bundle/Root.plist b/Examples/ExampleUIKit/ExampleUIkit/Settings.bundle/Root.plist index 437d731..230a07e 100644 --- a/Examples/ExampleUIKit/ExampleUIkit/Settings.bundle/Root.plist +++ b/Examples/ExampleUIKit/ExampleUIkit/Settings.bundle/Root.plist @@ -48,24 +48,22 @@ Key Resolution DefaultValue - 858x480 + 854x480 Values - 352x240 + 426x240 480x360 - 858x480 + 854x480 1280x720 1920x1080 - 3840x2160 Titles - 352x240 + 426x240 480x360 - 858x480 + 854x480 1280x720 1920x1080 - 3840x2160 diff --git a/Examples/ExampleUIKit/ExampleUIkit/SettingsManager.swift b/Examples/ExampleUIKit/ExampleUIkit/SettingsManager.swift index 0c8a38c..d607d77 100644 --- a/Examples/ExampleUIKit/ExampleUIkit/SettingsManager.swift +++ b/Examples/ExampleUIKit/ExampleUIkit/SettingsManager.swift @@ -20,10 +20,10 @@ enum SettingsManager { private static var resolution: Resolution { do { - return try UserDefaults.standard.string(forKey: "Resolution")?.toResolution() ?? Resolution.RESOLUTION_720 + return try UserDefaults.standard.string(forKey: "Resolution")?.toResolution() ?? Resolution + .RESOLUTION_16_9_720P } catch { - print("Can't get resolution from user defaults") - return Resolution.RESOLUTION_720 + fatalError("Can't get resolution from user defaults") } } @@ -57,13 +57,18 @@ extension String { guard let height = Int(resolutionArray[1]) else { throw ParameterError.Invalid("Height is invalid") } - return try Resolution.getResolution(width: width, height: height) + let resolution = Resolution(rawValue: CGSize(width: width, height: height)) + if let resolution { + return resolution + } else { + throw ParameterError.Invalid("Resolution is invalid for \(width)x\(height)") + } } } extension Resolution { func toString() -> String { - "\(size.width)x\(size.height)" + "\(rawValue.width)x\(rawValue.height)" } } diff --git a/Sources/ApiVideoLiveStream/ApiVideoLiveStream.swift b/Sources/ApiVideoLiveStream/ApiVideoLiveStream.swift index 05403cb..884100b 100644 --- a/Sources/ApiVideoLiveStream/ApiVideoLiveStream.swift +++ b/Sources/ApiVideoLiveStream/ApiVideoLiveStream.swift @@ -23,7 +23,6 @@ public class ApiVideoLiveStream { /// The delegate of the ApiVideoLiveStream public weak var delegate: ApiVideoLiveStreamDelegate? - // swiftlint:disable force_cast /// Getter and Setter for an AudioConfig public var audioConfig: AudioConfig { get { @@ -34,13 +33,12 @@ public class ApiVideoLiveStream { } } - // swiftlint:disable force_cast force_try /// Getter and Setter for a VideoConfig public var videoConfig: VideoConfig { get { - try! VideoConfig( + VideoConfig( bitrate: Int(self.rtmpStream.videoSettings.bitRate), - resolution: Resolution.getResolution( + resolution: CGSize( width: Int(self.rtmpStream.videoSettings.videoSize.width), height: Int(self.rtmpStream.videoSettings.videoSize.height) ), @@ -53,7 +51,6 @@ public class ApiVideoLiveStream { } } - // swiftlint:disable force_cast /// Getter and Setter for the Bitrate number for the video public var videoBitrate: Int { get { @@ -100,7 +97,6 @@ public class ApiVideoLiveStream { } #if os(iOS) - // swiftlint:disable implicit_return /// Zoom on the video capture public var zoomRatio: CGFloat { get { @@ -322,10 +318,10 @@ public class ApiVideoLiveStream { self.rtmpStream.frameRate = videoConfig.fps self.rtmpStream.sessionPreset = AVCaptureSession.Preset.high - let width = self.rtmpStream.videoOrientation.isLandscape ? videoConfig.resolution.size.width : videoConfig - .resolution.size.height - let height = self.rtmpStream.videoOrientation.isLandscape ? videoConfig.resolution.size.height : videoConfig - .resolution.size.width + let width = self.rtmpStream.videoOrientation.isLandscape ? videoConfig.resolution.width : videoConfig + .resolution.height + let height = self.rtmpStream.videoOrientation.isLandscape ? videoConfig.resolution.height : videoConfig + .resolution.width self.rtmpStream.videoSettings = VideoCodecSettings( videoSize: CGSize(width: width, height: height), @@ -444,22 +440,16 @@ public class ApiVideoLiveStream { self.rtmpStream.lockQueue.async { self.rtmpStream.videoOrientation = orientation - do { - let resolution = try Resolution.getResolution( - width: Int(self.rtmpStream.videoSettings.videoSize.width), - height: Int(self.rtmpStream.videoSettings.videoSize.height) - ) - self.rtmpStream.videoSettings.videoSize = CGSize( - width: - self.rtmpStream.videoOrientation.isLandscape ? - resolution.size.width : resolution.size.height, - height: - self.rtmpStream.videoOrientation.isLandscape ? - resolution.size.height : resolution.size.width - ) - } catch { - print("Failed to set resolution to orientation \(orientation)") - } + + let videoSize = self.rtmpStream.videoSettings.videoSize + self.rtmpStream.videoSettings.videoSize = CGSize( + width: + self.rtmpStream.videoOrientation.isLandscape ? + videoSize.width : videoSize.height, + height: + self.rtmpStream.videoOrientation.isLandscape ? + videoSize.height : videoSize.width + ) } } #endif diff --git a/Sources/ApiVideoLiveStream/models/Configuration.swift b/Sources/ApiVideoLiveStream/models/Configuration.swift index e78526e..03300bf 100644 --- a/Sources/ApiVideoLiveStream/models/Configuration.swift +++ b/Sources/ApiVideoLiveStream/models/Configuration.swift @@ -1,3 +1,4 @@ +import CoreGraphics import Foundation import Network @@ -14,51 +15,80 @@ public struct AudioConfig { public struct VideoConfig { public let bitrate: Int - public let resolution: Resolution + public let resolution: CGSize public let fps: Float64 public let gopDuration: TimeInterval - /// Creates a video configuration object with default video bitrate + /// Creates a video configuration object with explicit video bitrate and CGSize resolution /// - Parameters: + /// - bitrate: The video bitrate in bits per second /// - resolution: The video resolution /// - fps: The video framerate /// - gopDuration: The time interval between two key frames public init( - resolution: Resolution = Resolution.RESOLUTION_720, + bitrate: Int, + resolution: CGSize = CGSize(width: 1_280, height: 720), fps: Float64 = 30, gopDuration: TimeInterval = 1.0 ) { - self.bitrate = VideoConfig.getDefaultBitrate(resolution: resolution) + self.bitrate = bitrate self.resolution = resolution self.fps = fps self.gopDuration = gopDuration } - /// Creates a video configuration object with explicit video bitrate + /// Creates a video configuration object with default video bitrate and CGSize resolution /// - Parameters: - /// - bitrate: The video bitrate in bits per second /// - resolution: The video resolution /// - fps: The video framerate /// - gopDuration: The time interval between two key frames public init( - bitrate: Int, - resolution: Resolution = Resolution.RESOLUTION_720, + resolution: CGSize = CGSize(width: 1_280, height: 720), fps: Float64 = 30, gopDuration: TimeInterval = 1.0 ) { - self.bitrate = bitrate + self.bitrate = VideoConfig.getDefaultBitrate(resolution) self.resolution = resolution self.fps = fps self.gopDuration = gopDuration } - static func getDefaultBitrate(resolution: Resolution) -> Int { - switch resolution { - case Resolution.RESOLUTION_240: return 800_000 - case Resolution.RESOLUTION_360: return 1_000_000 - case Resolution.RESOLUTION_480: return 1_300_000 - case Resolution.RESOLUTION_720: return 2_000_000 - case Resolution.RESOLUTION_1080: return 3_500_000 + /// Creates a video configuration object with default video bitrate + /// - Parameters: + /// - resolution: The video resolution + /// - fps: The video framerate + /// - gopDuration: The time interval between two key frames + public init( + resolution: Resolution, + fps: Float64 = 30, + gopDuration: TimeInterval = 1.0 + ) { + self.init(resolution: resolution.rawValue, fps: fps, gopDuration: gopDuration) + } + + /// Creates a video configuration object with explicit video bitrate + /// - Parameters: + /// - bitrate: The video bitrate in bits per second + /// - resolution: The video resolution + /// - fps: The video framerate + /// - gopDuration: The time interval between two key frames + public init( + bitrate: Int, + resolution: Resolution, + fps: Float64 = 30, + gopDuration: TimeInterval = 1.0 + ) { + self.init(bitrate: bitrate, resolution: resolution.rawValue, fps: fps, gopDuration: gopDuration) + } + + static func getDefaultBitrate(_ size: CGSize) -> Int { + let numOfPixels = size.width * size.height + switch numOfPixels { + case 0 ... 102_240: return 800_000 // for 4/3 and 16/9 240p + case 102_241 ... 230_400: return 1_000_000 // for 16/9 360p + case 230_401 ... 409_920: return 1_300_000 // for 4/3 and 16/9 480p + case 409_921 ... 921_600: return 2_000_000 // for 4/3 600p, 4/3 768p and 16/9 720p + default: return 3_000_000 // for 16/9 1080p } } } diff --git a/Sources/ApiVideoLiveStream/models/Resolution.swift b/Sources/ApiVideoLiveStream/models/Resolution.swift index efc8cb3..a1f3c60 100644 --- a/Sources/ApiVideoLiveStream/models/Resolution.swift +++ b/Sources/ApiVideoLiveStream/models/Resolution.swift @@ -1,60 +1,107 @@ import Foundation +/// Resolution of the video public enum Resolution { - case RESOLUTION_240 - case RESOLUTION_360 - case RESOLUTION_480 - case RESOLUTION_720 - case RESOLUTION_1080 + // 16:9 + /// 426x240 + case RESOLUTION_16_9_240P + /// 640x360: nHD + case RESOLUTION_16_9_360P + /// 854x480: FWVGA + case RESOLUTION_16_9_480P + /// 1280x720: WXGA + case RESOLUTION_16_9_720P + /// 1920x1080: FHD + case RESOLUTION_16_9_1080P - public var size: CGSize { - switch self { - case .RESOLUTION_240: - return CGSize(width: 352, height: 240) - - case .RESOLUTION_360: - return CGSize(width: 480, height: 360) + // 4:3 + /// 320x240: QVGA + case RESOLUTION_4_3_240P + /// 640x480: VGA + case RESOLUTION_4_3_480P + /// 800x600: SVGA + case RESOLUTION_4_3_600P + /// 1024x768: XGA + case RESOLUTION_4_3_768P + /// 1440x1080 + case RESOLUTION_4_3_1080P +} - case .RESOLUTION_480: - return CGSize(width: 858, height: 480) +// MARK: RawRepresentable - case .RESOLUTION_720: - return CGSize(width: 1_280, height: 720) - - case .RESOLUTION_1080: - return CGSize(width: 1_920, height: 1_080) - } - } +extension Resolution: RawRepresentable { + public typealias RawValue = CGSize - public static func getResolution(width: Int, height: Int) throws -> Resolution { - let widerWidth = max(width, height) - let widerHeight = min(width, height) + public init?(rawValue: RawValue) { + let widerWidth = max(rawValue.width, rawValue.height) + let widerHeight = min(rawValue.width, rawValue.height) switch (widerWidth, widerHeight) { - case (352, 240): - return .RESOLUTION_240 + case (426, 240): + self = .RESOLUTION_16_9_240P - case (480, 360): - return .RESOLUTION_360 + case (640, 360): + self = .RESOLUTION_16_9_360P - case (858, 480): - return .RESOLUTION_480 + case (854, 480): + self = .RESOLUTION_16_9_480P case (1_280, 720): - return .RESOLUTION_720 + self = .RESOLUTION_16_9_720P case (1_920, 1_080): - return .RESOLUTION_1080 + self = .RESOLUTION_16_9_1080P + + case (320, 240): + self = .RESOLUTION_4_3_240P + + case (640, 480): + self = .RESOLUTION_4_3_480P + + case (800, 600): + self = .RESOLUTION_4_3_600P - default: - throw ConfigurationError.invalidParameter("Resolution \(width)x\(height) is not supported") + case (1_024, 768): + self = .RESOLUTION_4_3_768P + + case (1_440, 1_080): + self = .RESOLUTION_4_3_1080P + + default: return nil } } -} -public extension CGSize { - var resolution: Resolution { - get throws { - try Resolution.getResolution(width: Int(width), height: Int(height)) + public var rawValue: RawValue { + switch self { + case .RESOLUTION_16_9_240P: + return CGSize(width: 426, height: 240) + + case .RESOLUTION_16_9_360P: + return CGSize(width: 640, height: 360) + + case .RESOLUTION_16_9_480P: + return CGSize(width: 854, height: 480) + + case .RESOLUTION_16_9_720P: + return CGSize(width: 1_280, height: 720) + + case .RESOLUTION_16_9_1080P: + return CGSize(width: 1_920, height: 1_080) + + case .RESOLUTION_4_3_240P: + return CGSize(width: 320, height: 240) + + case .RESOLUTION_4_3_480P: + return CGSize(width: 640, height: 480) + + case .RESOLUTION_4_3_600P: + return CGSize(width: 800, height: 600) + + case .RESOLUTION_4_3_768P: + return CGSize(width: 1_024, height: 768) + + case .RESOLUTION_4_3_1080P: + return CGSize(width: 1_440, height: 1_080) } } + }