diff --git a/apps/ios/VMC.xcodeproj/project.pbxproj b/apps/ios/VMC.xcodeproj/project.pbxproj index b489b27..3f77350 100644 --- a/apps/ios/VMC.xcodeproj/project.pbxproj +++ b/apps/ios/VMC.xcodeproj/project.pbxproj @@ -345,7 +345,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 56; + CURRENT_PROJECT_VERSION = 57; DEVELOPMENT_TEAM = X25NYX3K49; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -360,7 +360,7 @@ "$(inherited)", "@executable_path/Frameworks" ); - MARKETING_VERSION = 0.5.43; + MARKETING_VERSION = 0.5.44; OTHER_LDFLAGS = ("$(inherited)", "-ObjC"); PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -378,7 +378,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 56; + CURRENT_PROJECT_VERSION = 57; DEVELOPMENT_TEAM = X25NYX3K49; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -393,7 +393,7 @@ "$(inherited)", "@executable_path/Frameworks" ); - MARKETING_VERSION = 0.5.43; + MARKETING_VERSION = 0.5.44; OTHER_LDFLAGS = ("$(inherited)", "-ObjC"); PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/apps/ios/VMC/Playback/CencMp4Decryptor.swift b/apps/ios/VMC/Playback/CencMp4Decryptor.swift index c488144..e6898b7 100644 --- a/apps/ios/VMC/Playback/CencMp4Decryptor.swift +++ b/apps/ios/VMC/Playback/CencMp4Decryptor.swift @@ -16,6 +16,27 @@ enum CencMp4Decryptor { var fallback: TrackCrypto? } + /// Diagnose-hook (alleen aanroepen onder de crypto-lock van de proxy). + static var diagTag = "" + static var diagSink: ((String) -> Void)? + private static var diagCounts: [String: Int] = [:] + + private static func diag(_ message: @autoclosure () -> String) { + guard let sink = diagSink else { return } + let n = diagCounts[diagTag, default: 0] + guard n < 8 else { return } + diagCounts[diagTag] = n + 1 + sink("decrypt[\(diagTag)] \(message())") + } + + static func resetDiag() { + diagCounts.removeAll() + } + + private static func hex(_ d: Data) -> String { + d.map { String(format: "%02x", $0) }.joined() + } + @discardableResult static func process(data: inout Data, keys: ClearKeyKeyMap, state: inout SessionState) -> Bool { guard !data.isEmpty, !keys.isEmpty else { return false } @@ -253,6 +274,7 @@ enum CencMp4Decryptor { } if let crypto { + diag("init track=\(trackId ?? 0) fmt=\(originalFormat ?? "-") scheme=\(crypto.scheme) pat=\(crypto.cryptByteBlock):\(crypto.skipByteBlock) iv=\(crypto.perSampleIvSize) civ=\(crypto.constantIv?.count ?? 0) kid=\(hex(crypto.kid)) exact=\(keys.hasExactKey(forKid: crypto.kid)) keys=\(keys.uniqueKeyCount)") if let trackId { state.byTrackId[trackId] = crypto } @@ -263,6 +285,8 @@ enum CencMp4Decryptor { state.fallback = crypto } _ = keys.key(forKid: crypto.kid) + } else { + diag("init track=\(trackId ?? 0) fmt=\(originalFormat ?? "-") clear (geen tenc)") } } } @@ -313,6 +337,8 @@ enum CencMp4Decryptor { var sampleSizes: [Int] = [] var sencSamples: [SencSample] = [] var perSampleIvSize = 8 + var tfhdFlagsSeen: UInt32 = 0 + var trunFlagsSeen: UInt32 = 0 if let known = state.byTrackId.values.first, state.byTrackId.count == 1 { perSampleIvSize = known.perSampleIvSize } @@ -332,14 +358,19 @@ enum CencMp4Decryptor { baseDataOffset = Int64(readU64(data, o)); o += 8 dataOffsetPresent = true } - if flags & 0x000002 != 0 { + if flags & 0x000002 != 0 { // sample-description-index guard o + 4 <= tfhd.payloadEnd else { break } o += 4 } - if flags & 0x000008 != 0 { + if flags & 0x000008 != 0 { // default-sample-duration + guard o + 4 <= tfhd.payloadEnd else { break } + o += 4 + } + if flags & 0x000010 != 0 { // default-sample-size guard o + 4 <= tfhd.payloadEnd else { break } defaultSampleSize = readU32(data, o); o += 4 } + tfhdFlagsSeen = flags _ = versionFlags } @@ -349,6 +380,7 @@ enum CencMp4Decryptor { let versionFlags = readU32(data, trun.payloadStart) let version = Int(versionFlags >> 24) let flags = versionFlags & 0x00FF_FFFF + trunFlagsSeen = flags var o = trun.payloadStart + 4 let sampleCount = Int(readU32(data, o)); o += 4 guard sampleCount >= 0, sampleCount < 100_000 else { continue } @@ -425,6 +457,7 @@ enum CencMp4Decryptor { } guard !sampleSizes.isEmpty, sencSamples.count == sampleSizes.count || sencSamples.isEmpty == false else { + diag("frag track=\(trackId) skip: samples=\(sampleSizes.count) senc=\(sencSamples.count) tfhd=0x\(String(tfhdFlagsSeen, radix: 16)) trun=0x\(String(trunFlagsSeen, radix: 16))") // Advance cursor even if we cannot decrypt. for sz in sampleSizes { mdatCursor += sz } continue @@ -436,6 +469,7 @@ enum CencMp4Decryptor { return keys.key(forKid: crypto.kid) }() guard let key else { + diag("frag track=\(trackId) skip: geen key (kid=\(crypto.map { hex($0.kid) } ?? "-") keys=\(keys.uniqueKeyCount))") for sz in sampleSizes { mdatCursor += sz } continue } @@ -468,6 +502,8 @@ enum CencMp4Decryptor { } mdatCursor += size } + let decryptedCount = didDecrypt ? min(sencSamples.count, sampleSizes.count) : 0 + diag("frag track=\(trackId) crypto=\(crypto != nil) scheme=\(scheme) pat=\(cryptBlocks):\(skipBlocks) samples=\(sampleSizes.count) senc=\(sencSamples.count) iv=\(sencSamples.first?.iv.count ?? 0) subs=\(sencSamples.first?.subsamples?.count ?? -1) size0=\(sampleSizes.first ?? 0) dec=\(decryptedCount) tfhd=0x\(String(tfhdFlagsSeen, radix: 16)) trun=0x\(String(trunFlagsSeen, radix: 16)) mdat=\(mdat.payloadLength)") // AVPlayer/VLC mogen geen tweede decrypt-poging doen op clear samples. if didDecrypt { for senc in findBoxesInRange(data, type: "senc", from: traf.payloadStart, to: traf.payloadEnd) { diff --git a/apps/ios/VMC/Playback/ClearKeyDashProxy.swift b/apps/ios/VMC/Playback/ClearKeyDashProxy.swift index fa3e9b3..790894c 100644 --- a/apps/ios/VMC/Playback/ClearKeyDashProxy.swift +++ b/apps/ios/VMC/Playback/ClearKeyDashProxy.swift @@ -171,6 +171,7 @@ final class ClearKeyDashProxy { self.cryptoVideo = CencMp4Decryptor.SessionState() self.cryptoAudio = CencMp4Decryptor.SessionState() self.cryptoShared = CencMp4Decryptor.SessionState() + CencMp4Decryptor.resetDiag() self.cryptoLock.unlock() self.generation += 1 } @@ -623,6 +624,10 @@ final class ClearKeyDashProxy { } cryptoLock.lock() defer { cryptoLock.unlock() } + if CencMp4Decryptor.diagSink == nil { + CencMp4Decryptor.diagSink = { proxyLog.info("\($0, privacy: .public)") } + } + CencMp4Decryptor.diagTag = lane.rawValue switch lane { case .video: _ = CencMp4Decryptor.process(data: &data, keys: keys, state: &cryptoVideo) diff --git a/apps/ios/VMC/Playback/ClearKeyMaterial.swift b/apps/ios/VMC/Playback/ClearKeyMaterial.swift index b5ce7a9..a0cfccd 100644 --- a/apps/ios/VMC/Playback/ClearKeyMaterial.swift +++ b/apps/ios/VMC/Playback/ClearKeyMaterial.swift @@ -33,6 +33,13 @@ struct ClearKeyKeyMap { return keysByKid.values.first } + func hasExactKey(forKid kid: Data) -> Bool { + guard kid.count == 16 else { return false } + if keysByKid[kid.map { String(format: "%02x", $0) }.joined()] != nil { return true } + let swapped = Self.swapUuidEndianness(kid) + return keysByKid[swapped.map { String(format: "%02x", $0) }.joined()] != nil + } + func key(forKid kid: Data) -> Data? { if kid.count == 16 { let hex = kid.map { String(format: "%02x", $0) }.joined() diff --git a/apps/ios/VMC/UI/SettingsScreen.swift b/apps/ios/VMC/UI/SettingsScreen.swift index 28ff228..a4c15f7 100644 --- a/apps/ios/VMC/UI/SettingsScreen.swift +++ b/apps/ios/VMC/UI/SettingsScreen.swift @@ -96,7 +96,7 @@ struct SettingsScreen: View { Section("Info") { LabeledContent("App", value: "VMC iOS") - LabeledContent("Versie", value: "0.5.43") + LabeledContent("Versie", value: "0.5.44") LabeledContent("Live TV", value: app.iptvEnabled ? "Aan" : "Uit") LabeledContent("Events", value: app.eventsEnabled ? "Aan" : "Uit") }