JohnCoates/Aerial

View on GitHub
Aerial/Source/Models/Hardware/HardwareDetection.swift

Summary

Maintainability
C
1 day
Test Coverage
//
//  HardwareDetection.swift
//  Aerial
//
//  Created by Guillaume Louel on 03/06/2019.
//  Copyright © 2019 John Coates. All rights reserved.
//

// When available, macOS will use the fixed functions units in Intel CPUs (QuickSync) for hardware
// decoding of H.264 and H.265, independent of if there's a GPU present.
// This is an issue as H.265 decoding is only partially supported on some Intel CPUs, up to Kaby Lake
// generation where they support Main profile decoding, but not Main10 (which is used by Apple's videos).
//
// Mode info can be found here : https://github.com/JohnCoates/Aerial/blob/master/Documentation/HardwareDecoding.md

import Foundation

enum HEVCMain10Support: Int {
    case notsupported, unsure, partial, supported
}

final class HardwareDetection: NSObject {
    static let sharedInstance = HardwareDetection()

    // MARK: - Mac Model detection

    private func getMacModel() -> String {
        var size = 0
        sysctlbyname("hw.model", nil, &size, nil, 0)
        var machine = [CChar](repeating: 0, count: size)
        sysctlbyname("hw.model", &machine, &size, nil, 0)
        return String(cString: machine)
    }

    private func extractMacVersion(macModel: String, macSubmodel: String) -> Double {
        // Substring the thing
        let str = String(macModel.dropFirst(macSubmodel.count))
        let formatter = NumberFormatter()
        formatter.locale = Locale(identifier: "fr_FR")
        return formatter.number(from: str)?.doubleValue ?? 0.0
    }

    // Get best suggestion
    // swiftlint:disable:next cyclomatic_complexity
    func getSuggestedFormat() -> VideoFormat {
        switch isHEVCMain10HWDecodingAvailable() {
        case .supported:
            if #available(OSX 10.15, *) {
                return .v4KHDR
            } else {
                // That was a fun one to track...
                return .v4KHEVC
            }
        case .notsupported:
            return .v1080pH264
        case .partial:
            // This is tricky
            let macModel = getMacModel()

            if macModel.starts(with: "iMac") {
                // iMacs, as far as we know, partial 17+, full 18+
                let ver = extractMacVersion(macModel: macModel, macSubmodel: "iMac")
                if ver >= 17.0 {
                    return .v4KHEVC
                } else {
                    return .v1080pH264
                }
            } else if macModel.starts(with: "MacBookPro") {
                let ver = extractMacVersion(macModel: macModel, macSubmodel: "MacBookPro")
                // MacBookPro full 14+
                if ver >= 17.0 {
                    return .v1080pHEVC
                } else {
                    return .v1080pH264
                }
            } else if macModel.starts(with: "MacBookAir") {
                // Retina 8+, I *think* they handle main10
                return .v1080pH264
            } else if macModel.starts(with: "MacBook") {
                // MacBook 10+
                return .v1080pH264
            }

            return .v1080pH264
        case .unsure:
            // Eh
            return .v1080pH264
        }
    }

    // MARK: - HEVC Main10 detection

    func isHEVCMain10HWDecodingAvailable() -> HEVCMain10Support {
        let macModel = getMacModel()

        // Apple silicon supports everything!
        if isAppleSilicon() {
            return .supported
        }
            
        // This is a manually compiled list based on CPU generations of each mac model line
        if macModel.starts(with: "iMacPro") || macModel.starts(with: "ADP") {
            // iMacPro - always
            return .supported
        } else if macModel.starts(with: "iMac") {
            // iMacs, as far as we know, partial 17+, full 18+
            return getHEVCMain10Support(macModel: macModel, macSubmodel: "iMac", partial: 17.0, full: 18.0)
        } else if macModel.starts(with: "MacBookPro") {
            // MacBookPro full 14+
            return getHEVCMain10Support(macModel: macModel, macSubmodel: "MacBookPro", partial: 13.0, full: 14.0)
        } else if macModel.starts(with: "MacBookAir") {
            // Retina 8+, I *think* they handle main10
            return getHEVCMain10Support(macModel: macModel, macSubmodel: "MacBookAir", partial: 8.0, full: 8.0)
        } else if macModel.starts(with: "MacBook") {
            // MacBook 10+
            return getHEVCMain10Support(macModel: macModel, macSubmodel: "MacBook", partial: 9.0, full: 10.0)
        } else if macModel.starts(with: "Macmini") {
            // MacMini 8+
            return getHEVCMain10Support(macModel: macModel, macSubmodel: "Macmini", partial: 8.0, full: 8.0)
        } else if macModel.starts(with: "MacPro") {
            // Tentative, I *think* 7+ (2019 MacPro) should always support independant of GPU, akin to iMac Pro ?
            return getHEVCMain10Support(macModel: macModel, macSubmodel: "MacPro", partial: 7.0, full: 7.0)
        }

        // Older stuff (power/etc) should not even run, so list should be complete
        // Hackintosh/new SKUs will fail this test, this is indicative in any case so that's fine
        return .unsure
    }

    // Helper
    private func getHEVCMain10Support(macModel: String, macSubmodel: String, partial: Double, full: Double) -> HEVCMain10Support {
        let ver = extractMacVersion(macModel: macModel, macSubmodel: macSubmodel)

        if ver >= full {
            return .supported
        } else if ver >= partial {
            return .partial
        } else {
            return .notsupported
        }
    }
    
    func isAppleSilicon() -> Bool {
        if #available(macOS 12, *) {
            var systeminfo = utsname()
            uname(&systeminfo)
            let machine = withUnsafeBytes(of: &systeminfo.machine) {bufPtr->String in
                let data = Data(bufPtr)
                if let lastIndex = data.lastIndex(where: {$0 != 0}) {
                    return String(data: data[0...lastIndex], encoding: .isoLatin1)!
                } else {
                    return String(data: data, encoding: .isoLatin1)!
                }
            }
            
            debugLog(machine)
            return machine != "x86_64"
        } else {
            return false
        }
    }

}