From 72124d2b9361d665cdaa77c7369909b8ba158ce9 Mon Sep 17 00:00:00 2001 From: Betafer Date: Fri, 21 Aug 2026 00:48:11 +0200 Subject: [PATCH] feat(sender): add safe per-receiver codec selection --- .../TBDisplaySenderContentView.swift | 26 ++++ .../TBDisplaySenderLocalization.swift | 22 +++ .../TBDisplaySenderManager.swift | 55 ++++++- .../TBDisplaySenderService.swift | 138 +++++++++++++++--- .../TBSenderAutomationParsingTests.swift | 92 ++++++++++++ TargetBridge-Shared/Languages/de.json | 8 + TargetBridge-Shared/Languages/en.json | 8 + TargetBridge-Shared/Languages/fr.json | 8 + TargetBridge-Shared/Languages/it.json | 8 + TargetBridge-Shared/Languages/zh.json | 8 + 10 files changed, 352 insertions(+), 21 deletions(-) diff --git a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderContentView.swift b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderContentView.swift index de97d34..8ec0b31 100644 --- a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderContentView.swift +++ b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderContentView.swift @@ -615,6 +615,32 @@ private struct TBDisplaySenderSessionSettingsSheet: View { .disabled(session.isConnected || session.isStreaming) } + settingRow( + TBDisplaySenderL10n.videoCompression(service.language), + details: TBDisplaySenderL10n.codecPreferenceHint( + session.codecPreference, + language: service.language + ) + ) { + Picker( + TBDisplaySenderL10n.videoCompression(service.language), + selection: Binding( + get: { session.codecPreference }, + set: { service.setCodecPreference($0, for: session) } + ) + ) { + ForEach(TBDisplayCodecPreference.allCases) { preference in + Text(TBDisplaySenderL10n.codecPreferenceTitle( + preference, + language: service.language + )) + .tag(preference) + } + } + .pickerStyle(.menu) + .disabled(session.isConnected || session.isStreaming) + } + if session.captureSource == .extendedDesktop { settingRow(renderMatchingTitle, details: renderMatchingDetails) { Toggle("", isOn: $session.matchRenderToStream) diff --git a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderLocalization.swift b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderLocalization.swift index 2f5fe16..d32d210 100644 --- a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderLocalization.swift +++ b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderLocalization.swift @@ -488,6 +488,28 @@ enum TBDisplaySenderL10n { ]) } + static func videoCompression(_ language: TBDisplaySenderLanguage) -> String { + text("sender.codec.label", language) + } + + static func codecPreferenceTitle( + _ preference: TBDisplayCodecPreference, + language: TBDisplaySenderLanguage + ) -> String { + text("sender.codec.\(preference.rawValue)", language) + } + + static func codecPreferenceHint( + _ preference: TBDisplayCodecPreference, + language: TBDisplaySenderLanguage + ) -> String { + text("sender.codec.hint.\(preference.rawValue)", language) + } + + static func codecUnavailable(_ language: TBDisplaySenderLanguage) -> String { + text("sender.codec.unavailable", language) + } + static func missingScreenRecordingPermission(language: TBDisplaySenderLanguage) -> String { text("sender.error.screen_recording_permission", language) } diff --git a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderManager.swift b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderManager.swift index 0d4e2fc..98137ed 100644 --- a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderManager.swift +++ b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderManager.swift @@ -222,7 +222,9 @@ final class TBDisplaySenderService: ObservableObject { receiverAddress: session.receiverIP.trimmingCharacters(in: .whitespacesAndNewlines), receiverProfileAvailable: session.receiverSupportsHEVCDecodeHint != nil, receiverSupportsHEVC: session.receiverSupportsHEVCDecodeHint, - requiresHEVC: session.capturePreset.codecName == "HEVC", + requiresHEVC: session.codecPreference == .hevc || ( + session.codecPreference == .automatic && session.capturePreset.codecName == "HEVC" + ), cableRate: session.cableTestResult, requiresSenderInputMonitoring: role == .senderMaster, senderInputMonitoringGranted: localInputMonitoringTrusted, @@ -317,6 +319,7 @@ final class TBDisplaySenderService: ObservableObject { private static let persistedSessionsKey = "fd.tbdisplaysender.sessions.v1" private static let receiverDisplayProfilesKey = "fd.tbdisplaysender.receiverDisplayProfiles.v1" + private static let receiverCodecPreferencesKey = "fd.tbdisplaysender.receiverCodecPreferences.v1" /// Earlier builds persisted `false` when the audio addon had not finished /// loading. Repair that ambiguous state exactly once without making future /// deliberate choices reversible. @@ -342,6 +345,7 @@ final class TBDisplaySenderService: ObservableObject { var inputControlRole: String? var inputBindings: [TBInputBinding]? var matchRenderToStream: Bool? + var codecPreference: String? } private var lastPersistedData: Data? @@ -376,7 +380,8 @@ final class TBDisplaySenderService: ObservableObject { volume: session.volume, inputControlRole: session.inputControlRole.rawValue, inputBindings: session.inputBindings, - matchRenderToStream: session.matchRenderToStream + matchRenderToStream: session.matchRenderToStream, + codecPreference: session.codecPreference.rawValue ) } guard let data = try? JSONEncoder().encode(configs) else { return } @@ -464,6 +469,9 @@ final class TBDisplaySenderService: ObservableObject { session.brightness = config.brightness session.volume = config.volume ?? 0.5 session.matchRenderToStream = config.matchRenderToStream ?? false + session.codecPreference = config.codecPreference + .flatMap(TBDisplayCodecPreference.init(rawValue:)) + ?? .automatic } func refreshLocalInterfaces() { @@ -476,6 +484,7 @@ final class TBDisplaySenderService: ObservableObject { func applyDiscoveredReceiver(_ receiver: TBDiscoveredReceiver, to session: TBDisplaySenderSession) { session.receiverIP = receiver.ip(for: session.transportKind) session.receiverSupportsHEVCDecodeHint = receiver.supportsHEVCDecode + restoreCodecPreference(for: receiver, to: session, defaultToAutomatic: true) if session.localInterfaceIP.isEmpty { session.localInterfaceIP = suggestedInterfaceForNewSession(transportKind: session.transportKind)?.ip ?? availableInterfaces(for: session.transportKind).first?.ip @@ -505,6 +514,48 @@ final class TBDisplaySenderService: ObservableObject { session.selectedReceiverID = receiver.id session.receiverIP = receiver.ip(for: session.transportKind) session.receiverSupportsHEVCDecodeHint = receiver.supportsHEVCDecode + restoreCodecPreference(for: receiver, to: session, defaultToAutomatic: false) + } + } + + func setCodecPreference(_ preference: TBDisplayCodecPreference, for session: TBDisplaySenderSession) { + guard !session.isConnected, !session.isStreaming else { return } + session.codecPreference = preference + guard let key = receiverCodecPreferenceKey(for: session) else { return } + var preferences = persistedCodecPreferences + preferences[key] = preference.rawValue + UserDefaults.standard.set(preferences, forKey: Self.receiverCodecPreferencesKey) + } + + private var persistedCodecPreferences: [String: String] { + UserDefaults.standard.dictionary(forKey: Self.receiverCodecPreferencesKey) as? [String: String] ?? [:] + } + + private func receiverCodecPreferenceKey(for receiver: TBDiscoveredReceiver) -> String { + "service:\(receiver.serviceName)" + } + + private func receiverCodecPreferenceKey(for session: TBDisplaySenderSession) -> String? { + let selectedID = session.selectedReceiverID.trimmingCharacters(in: .whitespacesAndNewlines) + if let serviceName = selectedID.split(separator: "|", maxSplits: 1).first, + !serviceName.isEmpty { + return "service:\(serviceName)" + } + + let receiverIP = session.receiverIP.trimmingCharacters(in: .whitespacesAndNewlines) + return receiverIP.isEmpty ? nil : "ip:\(receiverIP)" + } + + private func restoreCodecPreference( + for receiver: TBDiscoveredReceiver, + to session: TBDisplaySenderSession, + defaultToAutomatic: Bool + ) { + let rawValue = persistedCodecPreferences[receiverCodecPreferenceKey(for: receiver)] + if let rawValue, let preference = TBDisplayCodecPreference(rawValue: rawValue) { + session.codecPreference = preference + } else if defaultToAutomatic { + session.codecPreference = .automatic } } diff --git a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderService.swift b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderService.swift index 4479346..7fce362 100644 --- a/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderService.swift +++ b/TargetBridge-Sender/TBDisplaySender/TBDisplaySenderService.swift @@ -319,6 +319,19 @@ struct TBFrameRatePacer { } } +enum TBDisplayCodecPreference: String, CaseIterable, Identifiable, Codable { + case automatic + case h264 + case hevc + + var id: String { rawValue } +} + +struct TBDisplayCodecDecision: Equatable { + let codecType: CMVideoCodecType + let usedFallback: Bool +} + enum TBDisplayCaptureSource: String, CaseIterable, Identifiable { case desktopMirror case extendedDesktop @@ -1178,6 +1191,17 @@ final class TBDisplaySenderSession: NSObject, ObservableObject, Identifiable, @u } } } + @Published var codecPreference: TBDisplayCodecPreference = .automatic { + didSet { + if !isStreaming { + streamResolutionText = TBDisplaySenderL10n.streamSummary( + preset: capturePreset, + source: captureSource, + language: language + ) + } + } + } @Published var captureSource: TBDisplayCaptureSource = .desktopMirror { didSet { if !isStreaming { @@ -1268,7 +1292,7 @@ final class TBDisplaySenderSession: NSObject, ObservableObject, Identifiable, @u /// While a binding trigger key is held (matched), swallow its key-up so the /// raw trigger key never reaches the slave. private var suppressedTriggerKeyCode: UInt16? - private static var cachedSupportsHEVCHardwareEncode: Bool? + private static var cachedHardwareEncoderSupport: [String: Bool] = [:] private var receivedInputEventCount: UInt64 = 0 var onRemoteSwitchRequest: ((Int) -> Void)? var onRemoteDeactivateInputRequest: (() -> Void)? @@ -1334,9 +1358,14 @@ final class TBDisplaySenderSession: NSObject, ObservableObject, Identifiable, @u statusText = state.text(language) } - private static func probeHEVCHardwareEncoderSupport() -> Bool { - if let cachedSupportsHEVCHardwareEncode { - return cachedSupportsHEVCHardwareEncode + private static func probeHardwareEncoderSupport( + codecType: CMVideoCodecType, + width: Int, + height: Int + ) -> Bool { + let cacheKey = "\(codecType)-\(width)x\(height)" + if let cached = cachedHardwareEncoderSupport[cacheKey] { + return cached } let encoderSpecification: CFDictionary = [ @@ -1347,9 +1376,9 @@ final class TBDisplaySenderSession: NSObject, ObservableObject, Identifiable, @u var session: VTCompressionSession? let status = VTCompressionSessionCreate( allocator: kCFAllocatorDefault, - width: 1920, - height: 1080, - codecType: kCMVideoCodecType_HEVC, + width: Int32(width), + height: Int32(height), + codecType: codecType, encoderSpecification: encoderSpecification, imageBufferAttributes: nil, compressedDataAllocator: nil, @@ -1362,21 +1391,74 @@ final class TBDisplaySenderSession: NSObject, ObservableObject, Identifiable, @u } let supported = status == noErr - cachedSupportsHEVCHardwareEncode = supported + cachedHardwareEncoderSupport[cacheKey] = supported return supported } - private func resolvedCodecType(for preset: TBDisplayCapturePreset, profile: TBMonitorDisplayProfile?) -> CMVideoCodecType { - switch preset { - case .standard1440p, .smooth1440p60, .smooth1800p60: - let receiverSupportsHEVC = profile?.supportsHEVCDecode ?? receiverSupportsHEVCDecodeHint ?? false - if receiverSupportsHEVC, Self.probeHEVCHardwareEncoderSupport() { - return kCMVideoCodecType_HEVC - } - return kCMVideoCodecType_H264 - case .crisp2160p60, .retina4k60, .native5k, .native5k60Experimental: - return preset.codecType + static func chooseCodec( + preference: TBDisplayCodecPreference, + preset: TBDisplayCapturePreset, + receiverSupportsHEVC: Bool, + senderSupportsH264: Bool, + senderSupportsHEVC: Bool + ) -> TBDisplayCodecDecision? { + let h264Available = senderSupportsH264 + let hevcAvailable = receiverSupportsHEVC && senderSupportsHEVC + let preferredCodec: CMVideoCodecType + + switch preference { + case .automatic: + preferredCodec = preset.codecType == kCMVideoCodecType_HEVC || hevcAvailable + ? kCMVideoCodecType_HEVC + : kCMVideoCodecType_H264 + case .h264: + preferredCodec = kCMVideoCodecType_H264 + case .hevc: + preferredCodec = kCMVideoCodecType_HEVC + } + + if preferredCodec == kCMVideoCodecType_HEVC, hevcAvailable { + return TBDisplayCodecDecision(codecType: kCMVideoCodecType_HEVC, usedFallback: false) } + if preferredCodec == kCMVideoCodecType_H264, h264Available { + return TBDisplayCodecDecision(codecType: kCMVideoCodecType_H264, usedFallback: false) + } + if preferredCodec == kCMVideoCodecType_HEVC, h264Available { + return TBDisplayCodecDecision(codecType: kCMVideoCodecType_H264, usedFallback: true) + } + if preferredCodec == kCMVideoCodecType_H264, hevcAvailable { + return TBDisplayCodecDecision(codecType: kCMVideoCodecType_HEVC, usedFallback: true) + } + + return nil + } + + private func resolvedCodecDecision( + for preset: TBDisplayCapturePreset, + profile: TBMonitorDisplayProfile? + ) -> TBDisplayCodecDecision? { + let receiverSupportsHEVC = profile?.supportsHEVCDecode + ?? receiverSupportsHEVCDecodeHint + ?? false + return Self.chooseCodec( + preference: codecPreference, + preset: preset, + receiverSupportsHEVC: receiverSupportsHEVC, + senderSupportsH264: Self.probeHardwareEncoderSupport( + codecType: kCMVideoCodecType_H264, + width: preset.width, + height: preset.height + ), + senderSupportsHEVC: Self.probeHardwareEncoderSupport( + codecType: kCMVideoCodecType_HEVC, + width: preset.width, + height: preset.height + ) + ) + } + + private func resolvedCodecType(for preset: TBDisplayCapturePreset, profile: TBMonitorDisplayProfile?) -> CMVideoCodecType { + resolvedCodecDecision(for: preset, profile: profile)?.codecType ?? preset.codecType } private func codecName(for codecType: CMVideoCodecType) -> String { @@ -2543,10 +2625,28 @@ final class TBDisplaySenderSession: NSObject, ObservableObject, Identifiable, @u let preset = capturePreset let usesRawNV12 = rawNV12Enabled(for: profile) - let codecType = resolvedCodecType(for: preset, profile: profile) + let codecDecision: TBDisplayCodecDecision + if usesRawNV12 { + codecDecision = TBDisplayCodecDecision(codecType: preset.codecType, usedFallback: false) + } else { + guard let resolvedDecision = resolvedCodecDecision(for: preset, profile: profile) else { + setStatus(.captureError(TBDisplaySenderL10n.codecUnavailable(language))) + TBLog.connection.error( + "capture: no compatible hardware codec preset=\(preset.rawValue, privacy: .public) preference=\(self.codecPreference.rawValue, privacy: .public)" + ) + return false + } + codecDecision = resolvedDecision + } + let codecType = codecDecision.codecType let codecName = usesRawNV12 ? "NV12 RAW" : codecName(for: codecType) activeCodecType = usesRawNV12 ? nil : codecType activeCodecName = codecName + if codecDecision.usedFallback { + TBLog.connection.notice( + "capture: codec fallback preference=\(self.codecPreference.rawValue, privacy: .public) selected=\(codecName, privacy: .public)" + ) + } guard let connection else { return false } // The encode/send pipeline runs entirely on its own serial queue, diff --git a/TargetBridge-Sender/TBDisplaySenderTests/TBSenderAutomationParsingTests.swift b/TargetBridge-Sender/TBDisplaySenderTests/TBSenderAutomationParsingTests.swift index 36d8c9c..4892e6a 100644 --- a/TargetBridge-Sender/TBDisplaySenderTests/TBSenderAutomationParsingTests.swift +++ b/TargetBridge-Sender/TBDisplaySenderTests/TBSenderAutomationParsingTests.swift @@ -70,6 +70,98 @@ final class TBSenderAutomationParsingTests: XCTestCase { XCTAssertEqual(TBDisplayCapturePreset.native5k.captureRequestFrameRate, 96) } + func testAutomaticCodecUsesH264WhenHEVCIsUnavailable() { + let decision = TBDisplaySenderSession.chooseCodec( + preference: .automatic, + preset: .standard1440p, + receiverSupportsHEVC: false, + senderSupportsH264: true, + senderSupportsHEVC: true + ) + XCTAssertEqual(decision?.codecType, kCMVideoCodecType_H264) + XCTAssertEqual(decision?.usedFallback, false) + } + + func testAutomaticCodecUsesHEVCWhenBothMacsSupportIt() { + for preset in [TBDisplayCapturePreset.standard1440p, .retina4k60] { + let decision = TBDisplaySenderSession.chooseCodec( + preference: .automatic, + preset: preset, + receiverSupportsHEVC: true, + senderSupportsH264: true, + senderSupportsHEVC: true + ) + XCTAssertEqual(decision?.codecType, kCMVideoCodecType_HEVC) + XCTAssertEqual(decision?.usedFallback, false) + } + } + + func testAutomaticHighResolutionFallsBackToH264WithoutHEVC() { + let decision = TBDisplaySenderSession.chooseCodec( + preference: .automatic, + preset: .retina4k60, + receiverSupportsHEVC: false, + senderSupportsH264: true, + senderSupportsHEVC: true + ) + XCTAssertEqual(decision?.codecType, kCMVideoCodecType_H264) + XCTAssertEqual(decision?.usedFallback, true) + } + + func testManualCodecChoicesAreHonouredWhenAvailable() { + let h264 = TBDisplaySenderSession.chooseCodec( + preference: .h264, + preset: .retina4k60, + receiverSupportsHEVC: true, + senderSupportsH264: true, + senderSupportsHEVC: true + ) + XCTAssertEqual(h264?.codecType, kCMVideoCodecType_H264) + XCTAssertEqual(h264?.usedFallback, false) + + let hevc = TBDisplaySenderSession.chooseCodec( + preference: .hevc, + preset: .standard1440p, + receiverSupportsHEVC: true, + senderSupportsH264: true, + senderSupportsHEVC: true + ) + XCTAssertEqual(hevc?.codecType, kCMVideoCodecType_HEVC) + XCTAssertEqual(hevc?.usedFallback, false) + } + + func testManualCodecFallsBackOnlyToACompatibleAlternative() { + let hevcToH264 = TBDisplaySenderSession.chooseCodec( + preference: .hevc, + preset: .standard1440p, + receiverSupportsHEVC: false, + senderSupportsH264: true, + senderSupportsHEVC: true + ) + XCTAssertEqual(hevcToH264?.codecType, kCMVideoCodecType_H264) + XCTAssertEqual(hevcToH264?.usedFallback, true) + + let h264ToHEVC = TBDisplaySenderSession.chooseCodec( + preference: .h264, + preset: .native5k, + receiverSupportsHEVC: true, + senderSupportsH264: false, + senderSupportsHEVC: true + ) + XCTAssertEqual(h264ToHEVC?.codecType, kCMVideoCodecType_HEVC) + XCTAssertEqual(h264ToHEVC?.usedFallback, true) + } + + func testCodecSelectionFailsCleanlyWhenNoHardwarePathExists() { + XCTAssertNil(TBDisplaySenderSession.chooseCodec( + preference: .automatic, + preset: .native5k, + receiverSupportsHEVC: false, + senderSupportsH264: false, + senderSupportsHEVC: false + )) + } + func testFrameRatePacerSamples75HzInputAt60Hz() { var pacer = TBFrameRatePacer(maximumFrameRate: 60) let emitted = (0..<750).filter { frame in diff --git a/TargetBridge-Shared/Languages/de.json b/TargetBridge-Shared/Languages/de.json index 0075cd5..ed4335e 100644 --- a/TargetBridge-Shared/Languages/de.json +++ b/TargetBridge-Shared/Languages/de.json @@ -35,6 +35,14 @@ "sender.option.manual_entry": "Manuelle Eingabe", "sender.option.not_detected": "Nicht erkannt", "sender.group.stream_profile": "Stream-Profil", + "sender.codec.label": "Videokomprimierung", + "sender.codec.automatic": "Automatisch (empfohlen)", + "sender.codec.h264": "H.264", + "sender.codec.hevc": "HEVC", + "sender.codec.hint.automatic": "Wählt den besten kompatiblen Codec für diesen Mac und den Anzeige-Mac.", + "sender.codec.hint.h264": "Bevorzugt H.264 und wechselt sicher, wenn es für die gewählte Auflösung nicht verfügbar ist.", + "sender.codec.hint.hevc": "Bevorzugt HEVC und wechselt sicher, wenn einer der Macs es nicht verwenden kann.", + "sender.codec.unavailable": "Für diese Auflösung ist kein kompatibler Hardware-Videocodec verfügbar.", "sender.group.display_profiles": "Anzeigeprofile", "sender.display_profiles.hint": "Wähle eine fertige Konfiguration. TargetBridge merkt sich die letzte Auswahl für diesen Empfänger.", "sender.display_profiles.work_5k": "Arbeit 5K", diff --git a/TargetBridge-Shared/Languages/en.json b/TargetBridge-Shared/Languages/en.json index e38314e..781e04c 100644 --- a/TargetBridge-Shared/Languages/en.json +++ b/TargetBridge-Shared/Languages/en.json @@ -35,6 +35,14 @@ "sender.option.manual_entry": "Manual entry", "sender.option.not_detected": "Not detected", "sender.group.stream_profile": "Stream profile", + "sender.codec.label": "Video compression", + "sender.codec.automatic": "Automatic (recommended)", + "sender.codec.h264": "H.264", + "sender.codec.hevc": "HEVC", + "sender.codec.hint.automatic": "Chooses the best compatible codec for this Mac and display Mac.", + "sender.codec.hint.h264": "Prefers H.264 and switches safely if it is unavailable at the selected resolution.", + "sender.codec.hint.hevc": "Prefers HEVC and switches safely if either Mac cannot use it.", + "sender.codec.unavailable": "No compatible hardware video codec is available for this resolution.", "sender.group.display_profiles": "Display profiles", "sender.display_profiles.hint": "Apply a ready-to-use setup. TargetBridge remembers the last choice for this receiver.", "sender.display_profiles.work_5k": "Work 5K", diff --git a/TargetBridge-Shared/Languages/fr.json b/TargetBridge-Shared/Languages/fr.json index e18d886..cededbd 100644 --- a/TargetBridge-Shared/Languages/fr.json +++ b/TargetBridge-Shared/Languages/fr.json @@ -35,6 +35,14 @@ "sender.option.manual_entry": "Saisie manuelle", "sender.option.not_detected": "Non détecté", "sender.group.stream_profile": "Profil de flux", + "sender.codec.label": "Compression vidéo", + "sender.codec.automatic": "Automatique (recommandé)", + "sender.codec.h264": "H.264", + "sender.codec.hevc": "HEVC", + "sender.codec.hint.automatic": "Choisit le meilleur codec compatible avec ce Mac et le Mac utilisé comme écran.", + "sender.codec.hint.h264": "Préfère H.264 et change de manière sûre s’il n’est pas disponible à la résolution choisie.", + "sender.codec.hint.hevc": "Préfère HEVC et change de manière sûre si l’un des deux Mac ne peut pas l’utiliser.", + "sender.codec.unavailable": "Aucun codec vidéo matériel compatible n’est disponible pour cette résolution.", "sender.group.display_profiles": "Profils d’affichage", "sender.display_profiles.hint": "Appliquez une configuration prête à l’emploi. TargetBridge mémorise le dernier choix pour ce receiver.", "sender.display_profiles.work_5k": "Travail 5K", diff --git a/TargetBridge-Shared/Languages/it.json b/TargetBridge-Shared/Languages/it.json index 37bc70f..72a08d6 100644 --- a/TargetBridge-Shared/Languages/it.json +++ b/TargetBridge-Shared/Languages/it.json @@ -35,6 +35,14 @@ "sender.option.manual_entry": "Inserimento manuale", "sender.option.not_detected": "Non rilevato", "sender.group.stream_profile": "Profilo stream", + "sender.codec.label": "Compressione video", + "sender.codec.automatic": "Automatica (consigliata)", + "sender.codec.h264": "H.264", + "sender.codec.hevc": "HEVC", + "sender.codec.hint.automatic": "Sceglie il formato migliore compatibile con questo Mac e con il Mac usato come schermo.", + "sender.codec.hint.h264": "Preferisce H.264 e cambia in modo sicuro se non è disponibile alla risoluzione scelta.", + "sender.codec.hint.hevc": "Preferisce HEVC e cambia in modo sicuro se uno dei due Mac non può usarlo.", + "sender.codec.unavailable": "Nessun formato video hardware compatibile è disponibile per questa risoluzione.", "sender.group.display_profiles": "Profili display", "sender.display_profiles.hint": "Applica una configurazione pronta. TargetBridge ricorda l'ultima scelta per questo receiver.", "sender.display_profiles.work_5k": "Lavoro 5K", diff --git a/TargetBridge-Shared/Languages/zh.json b/TargetBridge-Shared/Languages/zh.json index 6cc9928..1663266 100644 --- a/TargetBridge-Shared/Languages/zh.json +++ b/TargetBridge-Shared/Languages/zh.json @@ -35,6 +35,14 @@ "sender.option.manual_entry": "手动输入", "sender.option.not_detected": "未检测到", "sender.group.stream_profile": "码流配置", + "sender.codec.label": "视频压缩", + "sender.codec.automatic": "自动(推荐)", + "sender.codec.h264": "H.264", + "sender.codec.hevc": "HEVC", + "sender.codec.hint.automatic": "为此 Mac 和用作显示器的 Mac 选择最佳兼容编码。", + "sender.codec.hint.h264": "优先使用 H.264;若所选分辨率不可用,则安全切换。", + "sender.codec.hint.hevc": "优先使用 HEVC;若任一台 Mac 无法使用,则安全切换。", + "sender.codec.unavailable": "此分辨率没有可用的兼容硬件视频编码。", "sender.group.display_profiles": "显示配置", "sender.display_profiles.hint": "一键应用预设。TargetBridge 会记住此 receiver 的上次选择。", "sender.display_profiles.work_5k": "5K 工作",