253 lines
10 KiB
Swift
253 lines
10 KiB
Swift
//
|
|
// Swiftfin is subject to the terms of the Mozilla Public
|
|
// License, v2.0. If a copy of the MPL was not distributed with this
|
|
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
|
//
|
|
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
|
//
|
|
|
|
// lol can someone buy me a coffee this took forever :|
|
|
|
|
import Foundation
|
|
import JellyfinAPI
|
|
|
|
enum CPUModel {
|
|
case A4
|
|
case A5
|
|
case A5X
|
|
case A6
|
|
case A6X
|
|
case A7
|
|
case A7X
|
|
case A8
|
|
case A8X
|
|
case A9
|
|
case A9X
|
|
case A10
|
|
case A10X
|
|
case A11
|
|
case A12
|
|
case A12X
|
|
case A12Z
|
|
case A13
|
|
case A14
|
|
case A99
|
|
}
|
|
|
|
class DeviceProfileBuilder {
|
|
public var bitrate: Int = 0
|
|
|
|
public func setMaxBitrate(bitrate: Int) {
|
|
self.bitrate = bitrate
|
|
}
|
|
|
|
public func buildProfile() -> DeviceProfile {
|
|
let maxStreamingBitrate = bitrate
|
|
let maxStaticBitrate = bitrate
|
|
let musicStreamingTranscodingBitrate = bitrate
|
|
|
|
// Build direct play profiles
|
|
var directPlayProfiles: [DirectPlayProfile] = []
|
|
directPlayProfiles =
|
|
[DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "aac,mp3,wav", videoCodec: "h264,mpeg4,vp9", type: .video)]
|
|
|
|
// Device supports Dolby Digital (AC3, EAC3)
|
|
if supportsFeature(minimumSupported: .A8X) {
|
|
if supportsFeature(minimumSupported: .A9) {
|
|
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus",
|
|
videoCodec: "hevc,h264,hev1,mpeg4,vp9",
|
|
type: .video)] // HEVC/H.264 with Dolby Digital
|
|
} else {
|
|
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "ac3,eac3,aac,mp3,wav,opus",
|
|
videoCodec: "h264,mpeg4,vp9", type: .video)] // H.264 with Dolby Digital
|
|
}
|
|
}
|
|
|
|
// Device supports Dolby Vision?
|
|
if supportsFeature(minimumSupported: .A10X) {
|
|
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus",
|
|
videoCodec: "dvhe,dvh1,h264,hevc,hev1,mpeg4,vp9",
|
|
type: .video)] // H.264/HEVC with Dolby Digital - No Atmos - Vision
|
|
}
|
|
|
|
// Device supports Dolby Atmos?
|
|
if supportsFeature(minimumSupported: .A12) {
|
|
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv,webm",
|
|
audioCodec: "aac,mp3,wav,ac3,eac3,flac,truehd,dts,dca,opus",
|
|
videoCodec: "h264,hevc,dvhe,dvh1,h264,hevc,hev1,mpeg4,vp9",
|
|
type: .video)] // H.264/HEVC with Dolby Digital & Atmos - Vision
|
|
}
|
|
|
|
// Build transcoding profiles
|
|
var transcodingProfiles: [TranscodingProfile] = []
|
|
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264,mpeg4", audioCodec: "aac,mp3,wav")]
|
|
|
|
// Device supports Dolby Digital (AC3, EAC3)
|
|
if supportsFeature(minimumSupported: .A8X) {
|
|
if supportsFeature(minimumSupported: .A9) {
|
|
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264,hevc,mpeg4",
|
|
audioCodec: "aac,mp3,wav,eac3,ac3,flac,opus", _protocol: "hls",
|
|
context: .streaming, maxAudioChannels: "6", minSegments: 2,
|
|
breakOnNonKeyFrames: true)]
|
|
} else {
|
|
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264,mpeg4",
|
|
audioCodec: "aac,mp3,wav,eac3,ac3,opus", _protocol: "hls",
|
|
context: .streaming, maxAudioChannels: "6", minSegments: 2,
|
|
breakOnNonKeyFrames: true)]
|
|
}
|
|
}
|
|
|
|
// Device supports FLAC?
|
|
if supportsFeature(minimumSupported: .A10X) {
|
|
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "hevc,h264,mpeg4",
|
|
audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", _protocol: "hls",
|
|
context: .streaming, maxAudioChannels: "6", minSegments: 2,
|
|
breakOnNonKeyFrames: true)]
|
|
}
|
|
|
|
var codecProfiles: [CodecProfile] = []
|
|
|
|
let h264CodecConditions: [ProfileCondition] = [
|
|
ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false),
|
|
ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "high|main|baseline|constrained baseline",
|
|
isRequired: false),
|
|
ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "80", isRequired: false),
|
|
ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false),
|
|
]
|
|
let hevcCodecConditions: [ProfileCondition] = [
|
|
ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false),
|
|
ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "high|main|main 10", isRequired: false),
|
|
ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "175", isRequired: false),
|
|
ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false),
|
|
]
|
|
|
|
codecProfiles.append(CodecProfile(type: .video, applyConditions: h264CodecConditions, codec: "h264"))
|
|
|
|
if supportsFeature(minimumSupported: .A9) {
|
|
codecProfiles.append(CodecProfile(type: .video, applyConditions: hevcCodecConditions, codec: "hevc"))
|
|
}
|
|
|
|
var subtitleProfiles: [SubtitleProfile] = []
|
|
|
|
subtitleProfiles.append(SubtitleProfile(format: "ass", method: .embed))
|
|
subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .embed))
|
|
subtitleProfiles.append(SubtitleProfile(format: "subrip", method: .embed))
|
|
subtitleProfiles.append(SubtitleProfile(format: "sub", method: .embed))
|
|
subtitleProfiles.append(SubtitleProfile(format: "pgssub", method: .embed))
|
|
|
|
// These need to be filtered. Most subrips are embedded. I hate subtitles.
|
|
subtitleProfiles.append(SubtitleProfile(format: "subrip", method: .external))
|
|
subtitleProfiles.append(SubtitleProfile(format: "sub", method: .external))
|
|
subtitleProfiles.append(SubtitleProfile(format: "ass", method: .external))
|
|
subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .external))
|
|
subtitleProfiles.append(SubtitleProfile(format: "vtt", method: .external))
|
|
subtitleProfiles.append(SubtitleProfile(format: "ass", method: .external))
|
|
subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .external))
|
|
|
|
let responseProfiles: [ResponseProfile] = [ResponseProfile(container: "m4v", type: .video, mimeType: "video/mp4")]
|
|
|
|
let profile = DeviceProfile(maxStreamingBitrate: maxStreamingBitrate, maxStaticBitrate: maxStaticBitrate,
|
|
musicStreamingTranscodingBitrate: musicStreamingTranscodingBitrate,
|
|
directPlayProfiles: directPlayProfiles, transcodingProfiles: transcodingProfiles, containerProfiles: [],
|
|
codecProfiles: codecProfiles, responseProfiles: responseProfiles, subtitleProfiles: subtitleProfiles)
|
|
|
|
return profile
|
|
}
|
|
|
|
private func supportsFeature(minimumSupported: CPUModel) -> Bool {
|
|
let intValues: [CPUModel: Int] = [
|
|
.A4: 1,
|
|
.A5: 2,
|
|
.A5X: 3,
|
|
.A6: 4,
|
|
.A6X: 5,
|
|
.A7: 6,
|
|
.A7X: 7,
|
|
.A8: 8,
|
|
.A8X: 9,
|
|
.A9: 10,
|
|
.A9X: 11,
|
|
.A10: 12,
|
|
.A10X: 13,
|
|
.A11: 14,
|
|
.A12: 15,
|
|
.A12X: 16,
|
|
.A12Z: 16,
|
|
.A13: 17,
|
|
.A14: 18,
|
|
.A99: 99,
|
|
]
|
|
return intValues[CPUinfo()] ?? 0 >= intValues[minimumSupported] ?? 0
|
|
}
|
|
|
|
/**********************************************
|
|
* CPUInfo():
|
|
* Returns a hardcoded value of the current
|
|
* devices CPU name.
|
|
***********************************************/
|
|
private func CPUinfo() -> CPUModel {
|
|
|
|
#if targetEnvironment(simulator)
|
|
let identifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]!
|
|
#else
|
|
|
|
var systemInfo = utsname()
|
|
uname(&systemInfo)
|
|
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
|
let identifier = machineMirror.children.reduce("") { identifier, element in
|
|
guard let value = element.value as? Int8, value != 0 else { return identifier }
|
|
return identifier + String(UnicodeScalar(UInt8(value)))
|
|
}
|
|
#endif
|
|
|
|
switch identifier {
|
|
case "iPod5,1": return .A5
|
|
case "iPod7,1": return .A8
|
|
case "iPod9,1": return .A10
|
|
case "iPhone3,1", "iPhone3,2", "iPhone3,3": return .A4
|
|
case "iPhone4,1": return .A5
|
|
case "iPhone5,1", "iPhone5,2": return .A6
|
|
case "iPhone5,3", "iPhone5,4": return .A6
|
|
case "iPhone6,1", "iPhone6,2": return .A7
|
|
case "iPhone7,2": return .A8
|
|
case "iPhone7,1": return .A8
|
|
case "iPhone8,1": return .A9
|
|
case "iPhone8,2", "iPhone8,4": return .A9
|
|
case "iPhone9,1", "iPhone9,3": return .A10
|
|
case "iPhone9,2", "iPhone9,4": return .A10
|
|
case "iPhone10,1", "iPhone10,4": return .A11
|
|
case "iPhone10,2", "iPhone10,5": return .A11
|
|
case "iPhone10,3", "iPhone10,6": return .A11
|
|
case "iPhone11,2", "iPhone11,6", "iPhone11,8": return .A12
|
|
case "iPhone12,1", "iPhone12,3", "iPhone12,5", "iPhone12,8": return .A13
|
|
case "iPhone13,1", "iPhone13,2", "iPhone13,3", "iPhone13,4": return .A14
|
|
case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return .A5
|
|
case "iPad3,1", "iPad3,2", "iPad3,3": return .A5X
|
|
case "iPad3,4", "iPad3,5", "iPad3,6": return .A6X
|
|
case "iPad4,1", "iPad4,2", "iPad4,3": return .A7
|
|
case "iPad5,3", "iPad5,4": return .A8X
|
|
case "iPad6,11", "iPad6,12": return .A9
|
|
case "iPad2,5", "iPad2,6", "iPad2,7": return .A5
|
|
case "iPad4,4", "iPad4,5", "iPad4,6": return .A7
|
|
case "iPad4,7", "iPad4,8", "iPad4,9": return .A7
|
|
case "iPad5,1", "iPad5,2": return .A8
|
|
case "iPad11,1", "iPad11,2": return .A12
|
|
case "iPad6,3", "iPad6,4": return .A9X
|
|
case "iPad6,7", "iPad6,8": return .A9X
|
|
case "iPad7,1", "iPad7,2": return .A10X
|
|
case "iPad7,3", "iPad7,4": return .A10X
|
|
case "iPad7,5", "iPad7,6", "iPad7,11", "iPad7,12": return .A10
|
|
case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return .A12X
|
|
case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return .A12X
|
|
case "iPad8,9", "iPad8,10", "iPad8,11", "iPad8,12": return .A12Z
|
|
case "iPad11,3", "iPad11,4", "iPad11,6", "iPad11,7": return .A12
|
|
case "iPad13,1", "iPad13,2": return .A14
|
|
case "AppleTV5,3": return .A8
|
|
case "AppleTV6,2": return .A10X
|
|
case "AppleTV11,1": return .A12
|
|
case "AudioAccessory1,1": return .A8
|
|
default: return .A99
|
|
}
|
|
}
|
|
}
|