├── TDS Video ├── ScreenRecord.swift ├── TDS.cer ├── .DS_Store ├── Assets.xcassets │ ├── Contents.json │ ├── .DS_Store │ ├── Cursor.imageset │ │ ├── mac-osx-arrow-cursor.png │ │ └── Contents.json │ ├── ICON.appiconset │ │ ├── AppIcon~ios-marketing.png │ │ └── Contents.json │ ├── test1.imageset │ │ ├── AppIcon~ios-marketing.png │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── AppIcon~ios-marketing 1.png │ │ ├── AppIcon~ios-marketing 2.png │ │ ├── AppIcon~ios-marketing.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── TDS Video-Bridging-Header.h ├── AVPlayerViewController+ButtonInteractionSwizzling.h ├── Transcoding │ ├── Extensions │ │ ├── Data+Extensions.swift │ │ ├── VTDecompressionSession+Extensions.swift │ │ └── VTCompressionSession+Extensions.swift │ ├── NALUs │ │ ├── H264NALU.swift │ │ └── HEVCNALU.swift │ ├── VideoDecoderAnnexBAdaptor.swift │ ├── VideoEncoderAnnexBAdaptor.swift │ ├── VideoTranscoderError.swift │ ├── VideoEncoder.swift │ └── VideoDecoder.swift ├── TDS Video.entitlements ├── AppIntent.swift ├── TDSAuth.swift ├── CPTemplateApplicationScene.h ├── VIews │ ├── WebServerPage.swift │ ├── ScreenMirroringView.swift │ ├── WebViewForDRM.swift │ ├── Help.swift │ ├── ScreenMirroingSettings.swift │ ├── webview.swift │ ├── WebViewButtons.swift │ ├── MainView.swift │ ├── paymentscreen.swift │ └── CameraRoll.swift ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── UserD.swift ├── CPTemplate.h ├── AVPlayerViewController+ButtonInteractionSwizzling.m ├── CPTemplateApplicationScene+Swizzle.m ├── TDSVideoMainScreen.swift ├── SceneDelegate.swift ├── AppDelegate.swift ├── TDSLocationAPI.swift ├── MDapiClass.swift ├── AVPlayerViewController.m ├── ViewController.swift ├── HTTPServer.swift └── TDSVideoAPI.swift ├── .DS_Store ├── TDS Video 2 └── .DS_Store ├── TDS Video.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── thomas.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── swiftpm │ │ └── Package.resolved ├── xcuserdata │ └── thomas.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── xcshareddata │ └── xcschemes │ ├── TDS Video.xcscheme │ ├── CarplayintentUI.xcscheme │ └── Carplayintent.xcscheme ├── ScreenRec ├── ScreenRec.entitlements ├── Info.plist └── SampleHandler.swift ├── CarplayintentUI ├── CarplayintentUI.entitlements ├── Info.plist ├── IntentViewController.swift └── Base.lproj │ └── MainInterface.storyboard ├── CustomWebViews.swift ├── UploadVideo ├── UploadVideo.entitlements ├── Info.plist ├── Base.lproj │ └── MainInterface.storyboard └── ShareViewController.swift ├── CarPlayViewContollerProtocal.swift ├── Carplayintent ├── IntentHandler.swift └── Info.plist ├── ScreenRecSetupUI ├── Info.plist └── BroadcastSetupViewController.swift ├── CarPlayAccess.swift ├── entitlements.plist ├── CarPlayMapView.swift ├── README.md └── CustomVideoPlayerViewController.swift /TDS Video/ScreenRecord.swift: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdye12/TDS-Carplay/HEAD/.DS_Store -------------------------------------------------------------------------------- /TDS Video/TDS.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdye12/TDS-Carplay/HEAD/TDS Video/TDS.cer -------------------------------------------------------------------------------- /TDS Video/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdye12/TDS-Carplay/HEAD/TDS Video/.DS_Store -------------------------------------------------------------------------------- /TDS Video 2/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdye12/TDS-Carplay/HEAD/TDS Video 2/.DS_Store -------------------------------------------------------------------------------- /TDS Video/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TDS Video/Assets.xcassets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdye12/TDS-Carplay/HEAD/TDS Video/Assets.xcassets/.DS_Store -------------------------------------------------------------------------------- /TDS Video/Assets.xcassets/Cursor.imageset/mac-osx-arrow-cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdye12/TDS-Carplay/HEAD/TDS Video/Assets.xcassets/Cursor.imageset/mac-osx-arrow-cursor.png -------------------------------------------------------------------------------- /TDS Video/Assets.xcassets/ICON.appiconset/AppIcon~ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdye12/TDS-Carplay/HEAD/TDS Video/Assets.xcassets/ICON.appiconset/AppIcon~ios-marketing.png -------------------------------------------------------------------------------- /TDS Video/Assets.xcassets/test1.imageset/AppIcon~ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdye12/TDS-Carplay/HEAD/TDS Video/Assets.xcassets/test1.imageset/AppIcon~ios-marketing.png -------------------------------------------------------------------------------- /TDS Video/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdye12/TDS-Carplay/HEAD/TDS Video/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing 1.png -------------------------------------------------------------------------------- /TDS Video/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdye12/TDS-Carplay/HEAD/TDS Video/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing 2.png -------------------------------------------------------------------------------- /TDS Video/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdye12/TDS-Carplay/HEAD/TDS Video/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png -------------------------------------------------------------------------------- /TDS Video.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ScreenRec/ScreenRec.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /TDS Video/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TDS Video.xcodeproj/project.xcworkspace/xcuserdata/thomas.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasdye12/TDS-Carplay/HEAD/TDS Video.xcodeproj/project.xcworkspace/xcuserdata/thomas.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /CarplayintentUI/CarplayintentUI.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.carplay-communication 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CustomWebViews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomWebViews.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 21/03/2025. 6 | // 7 | 8 | 9 | struct ZoomSettings: Codable { 10 | var widthMultiplier: CGFloat 11 | var heightMultiplier: CGFloat 12 | var originX: CGFloat 13 | var originY: CGFloat 14 | } 15 | -------------------------------------------------------------------------------- /TDS Video/Assets.xcassets/ICON.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon~ios-marketing.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /UploadVideo/UploadVideo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.net.thomasdye.TDS-docs 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /CarPlayViewContollerProtocal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CarPlayViewContollerProtocal.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 06/03/2025. 6 | // 7 | 8 | 9 | import UIKit 10 | 11 | open class CarPlayViewControllerProtocol: UIViewController { 12 | open func loadViewIncar(_ window: UIWindow?) -> CarPlayViewControllerProtocol { 13 | fatalError("Subclasses must override loadViewInCar(view:)") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /TDS Video/Assets.xcassets/Cursor.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mac-osx-arrow-cursor.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /TDS Video/Assets.xcassets/test1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon~ios-marketing.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /TDS Video/TDS Video-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | 6 | #import "CPTemplateApplicationScene.h" 7 | #import "CPTemplate.h" 8 | //#import "UIPhysicalButtonInteraction.h" 9 | #import "AVPlayerViewController+Swizzling.h" 10 | #import "AVPlayerViewController+ButtonInteractionSwizzling.h" 11 | //#import "AVSystemVolumeController.h" 12 | 13 | //#import "TDS_Video-Swift.h" 14 | -------------------------------------------------------------------------------- /TDS Video.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "2897f0d5b72d1079322a785732be4412eeae39f3e897eaa4a6a60e761f57be72", 3 | "pins" : [ 4 | { 5 | "identity" : "swifter", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/httpswift/swifter.git", 8 | "state" : { 9 | "revision" : "9483a5d459b45c3ffd059f7b55f9638e268632fd", 10 | "version" : "1.5.0" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /TDS Video/AVPlayerViewController+ButtonInteractionSwizzling.h: -------------------------------------------------------------------------------- 1 | // 2 | // AVPlayerViewController+ButtonInteractionSwizzling.h 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 06/08/2024. 6 | // 7 | 8 | #ifndef AVPlayerViewController_ButtonInteractionSwizzling_h 9 | #define AVPlayerViewController_ButtonInteractionSwizzling_h 10 | #import 11 | 12 | @interface AVPlayerViewController (ButtonInteractionSwizzling) 13 | 14 | @end 15 | 16 | #endif /* AVPlayerViewController_ButtonInteractionSwizzling_h */ 17 | -------------------------------------------------------------------------------- /Carplayintent/IntentHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntentHandler.swift 3 | // Carplayintent 4 | // 5 | // Created by Thomas Dye on 02/08/2024. 6 | // 7 | 8 | import Intents 9 | 10 | class IntentHandler: INExtension { 11 | 12 | override func handler(for intent: INIntent) -> Any { 13 | // This is the default implementation. If you want different objects to handle different intents, 14 | // you can override this and return the handler you want for that particular intent. 15 | 16 | return self 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /ScreenRec/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.broadcast-services-upload 9 | NSExtensionPrincipalClass 10 | $(PRODUCT_MODULE_NAME).SampleHandler 11 | RPBroadcastProcessMode 12 | RPBroadcastProcessModeSampleBuffer 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /TDS Video/Transcoding/Extensions/Data+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Data { 4 | func split(separator: Data) -> [Data] { 5 | var chunks: [Data] = [] 6 | var position = startIndex 7 | while let range = self[position...].range(of: separator) { 8 | if range.lowerBound > position { 9 | chunks.append(self[position.. 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionAttributes 8 | 9 | IntentsSupported 10 | 11 | INSendMessageIntent 12 | 13 | 14 | NSExtensionMainStoryboard 15 | MainInterface 16 | NSExtensionPointIdentifier 17 | com.apple.intents-ui-service 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /UploadVideo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionAttributes 8 | 9 | NSExtensionActivationRule 10 | 11 | NSExtensionActivationSupportsWebURLWithMaxCount 12 | 1 13 | 14 | 15 | NSExtensionMainStoryboard 16 | MainInterface 17 | NSExtensionPointIdentifier 18 | com.apple.share-services 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /ScreenRecSetupUI/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionAttributes 8 | 9 | NSExtensionActivationRule 10 | 11 | NSExtensionActivationSupportsReplayKitStreaming 12 | 13 | 14 | 15 | NSExtensionPointIdentifier 16 | com.apple.broadcast-services-setupui 17 | NSExtensionPrincipalClass 18 | $(PRODUCT_MODULE_NAME).BroadcastSetupViewController 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Carplayintent/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionAttributes 8 | 9 | IntentsRestrictedWhileLocked 10 | 11 | IntentsSupported 12 | 13 | INSetMessageAttributeIntent 14 | INSearchForMessagesIntent 15 | INSendMessageIntent 16 | 17 | 18 | NSExtensionPointIdentifier 19 | com.apple.intents-service 20 | NSExtensionPrincipalClass 21 | $(PRODUCT_MODULE_NAME).IntentHandler 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /CarPlayAccess.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CarPlayAccess.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 16/04/2025. 6 | // 7 | 8 | 9 | import UIKit 10 | 11 | class TDSCarplayAccess { 12 | static var shared = TDSCarplayAccess() 13 | 14 | private let settingsKey = "ShowTDSCarPlaySettings" 15 | 16 | var ShowTDSCarPlaySettings: Bool = true 17 | 18 | var DisableIsStationary: Bool { 19 | get { 20 | UserDefaults.standard.bool(forKey: settingsKey) 21 | } 22 | set { 23 | UserDefaults.standard.set(newValue, forKey: settingsKey) 24 | } 25 | } 26 | 27 | 28 | private init() { 29 | // Optionally initialize from UserDefaults here if needed 30 | 31 | } 32 | 33 | 34 | 35 | } 36 | 37 | struct CarplaySettingsResponse: Decodable { 38 | let showCarplaySettings: Bool? 39 | } 40 | -------------------------------------------------------------------------------- /TDS Video/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon~ios-marketing.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | }, 9 | { 10 | "appearances" : [ 11 | { 12 | "appearance" : "luminosity", 13 | "value" : "dark" 14 | } 15 | ], 16 | "filename" : "AppIcon~ios-marketing 1.png", 17 | "idiom" : "universal", 18 | "platform" : "ios", 19 | "size" : "1024x1024" 20 | }, 21 | { 22 | "appearances" : [ 23 | { 24 | "appearance" : "luminosity", 25 | "value" : "tinted" 26 | } 27 | ], 28 | "filename" : "AppIcon~ios-marketing 2.png", 29 | "idiom" : "universal", 30 | "platform" : "ios", 31 | "size" : "1024x1024" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | application-identifier 6 | K3S7UENN8E.* 7 | com.apple.developer.carplay-calling 8 | 9 | com.apple.developer.carplay-communication 10 | 11 | com.apple.developer.carplay-maps 12 | 13 | com.apple.developer.carplay-messaging 14 | 15 | com.apple.developer.team-identifier 16 | K3S7UENN8E 17 | com.apple.security.application-groups 18 | 19 | group.net.thomasdye.Auth_Creds 20 | 21 | get-task-allow 22 | 23 | keychain-access-groups 24 | 25 | K3S7UENN8E.* 26 | com.apple.token 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /TDS Video/TDS Video.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.associated-domains 8 | 9 | webcredentials:auth.thomasdye.net 10 | 11 | com.apple.developer.carplay-calling 12 | 13 | com.apple.developer.carplay-communication 14 | 15 | com.apple.developer.kernel.increased-memory-limit 16 | 17 | com.apple.developer.networking.networkextension 18 | 19 | com.apple.security.application-groups 20 | 21 | group.net.thomasdye.Auth_Creds 22 | group.net.thomasdye.TDS-docs 23 | 24 | keychain-access-groups 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /TDS Video/Transcoding/NALUs/H264NALU.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct H264NALU { 4 | static let startCode = Data([0x00, 0x00, 0x00, 0x01]) 5 | 6 | let data: Data 7 | 8 | var annexB: Data { 9 | H264NALU.startCode + data 10 | } 11 | 12 | var avcc: Data { 13 | let bigEndianLength = CFSwapInt32HostToBig(UInt32(data.count)) 14 | return withUnsafeBytes(of: bigEndianLength) { Data($0) } + data 15 | } 16 | 17 | var isPFrame: Bool { 18 | guard let firstByte = data.first else { return false } 19 | return (firstByte & 0x1f) == 1 20 | } 21 | 22 | var isIFrame: Bool { 23 | guard let firstByte = data.first else { return false } 24 | return (firstByte & 0x1f) == 5 25 | } 26 | 27 | var isSPS: Bool { 28 | guard let firstByte = data.first else { return false } 29 | return (firstByte & 0x1f) == 7 30 | } 31 | 32 | var isPPS: Bool { 33 | guard let firstByte = data.first else { return false } 34 | return (firstByte & 0x1f) == 8 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /TDS Video/AppIntent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppIntent.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 25/03/2025. 6 | // 7 | 8 | 9 | import AppIntents 10 | 11 | 12 | struct SaveToSharedDefaultsIntent: AppIntent { 13 | static var title: LocalizedStringResource = "Send URL to screen" 14 | 15 | @Parameter(title: "URL as String") 16 | var textToSave: String 17 | var isDiscoverable:Bool = true 18 | static var openAppWhenRun: Bool = false 19 | 20 | func perform() async throws -> some IntentResult { 21 | // Replace with your actual App Group ID 22 | let sharedDefaults = UserDefaults(suiteName: "group.net.thomasdye.TDS-docs") 23 | sharedDefaults?.set(textToSave, forKey: "TDVideo-SharedURL") 24 | 25 | // Post cross-process notification 26 | let notificationName = "group.net.thomasdye.TDS-docs.TDVideo-SharedURL" 27 | CFNotificationCenterPostNotification( 28 | CFNotificationCenterGetDarwinNotifyCenter(), 29 | CFNotificationName(notificationName as CFString), 30 | nil, 31 | nil, 32 | true 33 | ) 34 | 35 | return .result() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /TDS Video/Transcoding/NALUs/HEVCNALU.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct HEVCNALU { 4 | static let startCode = Data([0x00, 0x00, 0x00, 0x01]) 5 | 6 | let data: Data 7 | 8 | var annexB: Data { 9 | HEVCNALU.startCode + data 10 | } 11 | 12 | var avcc: Data { 13 | let bigEndianLength = CFSwapInt32HostToBig(UInt32(data.count)) 14 | return withUnsafeBytes(of: bigEndianLength) { Data($0) } + data 15 | } 16 | 17 | var isPFrame: Bool { 18 | guard let firstByte = data.first else { return false } 19 | return ((firstByte & 0x7e) >> 1) == 1 20 | } 21 | 22 | var isIFrame: Bool { 23 | guard let firstByte = data.first else { return false } 24 | return ((firstByte & 0x7e) >> 1) == 20 25 | } 26 | 27 | var isVPS: Bool { 28 | guard let firstByte = data.first else { return false } 29 | return ((firstByte & 0x7e) >> 1) == 32 30 | } 31 | 32 | var isSPS: Bool { 33 | guard let firstByte = data.first else { return false } 34 | return ((firstByte & 0x7e) >> 1) == 33 35 | } 36 | 37 | var isPPS: Bool { 38 | guard let firstByte = data.first else { return false } 39 | return ((firstByte & 0x7e) >> 1) == 34 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /TDS Video/TDSAuth.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TDSAuth.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 21/03/2025. 6 | // 7 | 8 | //var auth = Auth(Config: Auth_Config(APNS:true, 9 | // BundleID: "net.thomasdye.TDS-Video",AccountType: "TDS", 10 | // TokenSuiteName: "group.net.thomasdye.Auth_Creds", RequireFaceID: false, 11 | // LoginViewconfig: LoginViewConfigops( 12 | // Title: "TDS CarPlay Video", 13 | // Username: false, Password: false, 14 | // Canlogin: false, signinwithApple: false,SignUP: false, 15 | // Nologin_message: "Server error",LoginButtonColour: UIColor(red: 0.2, green: 0.7, blue: 1, alpha: 1), 16 | // LoginImage_name: "TDSCCTV 2", 17 | // TDS_Login_explain_text:true 18 | // ), 19 | // 20 | // UNAuthorizationOptions: [.alert,.badge,.carPlay,.sound] 21 | // 22 | // )) 23 | -------------------------------------------------------------------------------- /ScreenRecSetupUI/BroadcastSetupViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BroadcastSetupViewController.swift 3 | // ScreenRecSetupUI 4 | // 5 | // Created by Thomas Dye on 11/01/2025. 6 | // 7 | 8 | import ReplayKit 9 | 10 | class BroadcastSetupViewController: UIViewController { 11 | 12 | // Call this method when the user has finished interacting with the view controller and a broadcast stream can start 13 | func userDidFinishSetup() { 14 | // URL of the resource where broadcast can be viewed that will be returned to the application 15 | let broadcastURL = URL(string:"http://apple.com/broadcast/streamID") 16 | 17 | // Dictionary with setup information that will be provided to broadcast extension when broadcast is started 18 | let setupInfo: [String : NSCoding & NSObjectProtocol] = ["broadcastName": "example" as NSCoding & NSObjectProtocol] 19 | 20 | // Tell ReplayKit that the extension is finished setting up and can begin broadcasting 21 | self.extensionContext?.completeRequest(withBroadcast: broadcastURL!, setupInfo: setupInfo) 22 | } 23 | 24 | func userDidCancelSetup() { 25 | let error = NSError(domain: "YouAppDomain", code: -1, userInfo: nil) 26 | // Tell ReplayKit that the extension was cancelled by the user 27 | self.extensionContext?.cancelRequest(withError: error) 28 | } 29 | override func viewDidLoad() { 30 | userDidFinishSetup() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /TDS Video/CPTemplateApplicationScene.h: -------------------------------------------------------------------------------- 1 | // 2 | // CPTemplateApplicationScene.h 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 05/08/2024. 6 | // 7 | 8 | 9 | 10 | #ifndef CPTemplateApplicationScene_h 11 | #define CPTemplateApplicationScene_h 12 | 13 | 14 | @import CarPlay; 15 | #import 16 | #import 17 | 18 | @interface CPTemplateApplicationScene (Swizzle) 19 | 20 | // Public method to check if the car window should be created 21 | - (BOOL)_shouldCreateCarWindow; 22 | 23 | @end 24 | 25 | @interface CPInterfaceController (Swizzle) 26 | 27 | // Public method to check if the car window should be created 28 | @property (nonnull, nonatomic, strong, readonly) CPWindow *windowProvider; 29 | // carWindow 30 | 31 | - (void)_pushTemplate:(CPTemplate *_Nullable)template 32 | presentationStyle:(NSInteger)presentationStyle 33 | animated:(BOOL)animated 34 | completion:(void (^)(BOOL success, NSError *error))completion; 35 | 36 | - (void)_pushMapTemplate:(CPTemplate *_Nullable)template 37 | presentationStyle:(NSInteger)presentationStyle 38 | animated:(BOOL)animated 39 | completion:(void (^)(BOOL success, NSError *error))completion; 40 | - (void)_pushListTemplate:(CPTemplate *_Nullable)template 41 | presentationStyle:(NSInteger)presentationStyle 42 | animated:(BOOL)animated 43 | completion:(void (^)(BOOL success, NSError *error))completion; 44 | @end 45 | 46 | 47 | #endif /* CPTemplateApplicationScene_h */ 48 | -------------------------------------------------------------------------------- /CarplayintentUI/IntentViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntentViewController.swift 3 | // CarplayintentUI 4 | // 5 | // Created by Thomas Dye on 02/08/2024. 6 | // 7 | 8 | import IntentsUI 9 | 10 | // As an example, this extension's Info.plist has been configured to handle interactions for INSendMessageIntent. 11 | // You will want to replace this or add other intents as appropriate. 12 | // The intents whose interactions you wish to handle must be declared in the extension's Info.plist. 13 | 14 | // You can test this example integration by saying things to Siri like: 15 | // "Send a message using " 16 | 17 | class IntentViewController: UIViewController, INUIHostedViewControlling { 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | // Do any additional setup after loading the view. 22 | } 23 | 24 | // MARK: - INUIHostedViewControlling 25 | 26 | // Prepare your view controller for the interaction to handle. 27 | func configureView(for parameters: Set, of interaction: INInteraction, interactiveBehavior: INUIInteractiveBehavior, context: INUIHostedViewContext, completion: @escaping (Bool, Set, CGSize) -> Void) { 28 | // Do configuration here, including preparing views and calculating a desired size for presentation. 29 | completion(true, parameters, self.desiredSize) 30 | } 31 | 32 | var desiredSize: CGSize { 33 | return self.extensionContext!.hostedViewMaximumAllowedSize 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /TDS Video/Transcoding/Extensions/VTDecompressionSession+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import VideoToolbox 3 | 4 | extension VTDecompressionSession { 5 | static func create( 6 | formatDescription: CMVideoFormatDescription, 7 | decoderSpecification: CFDictionary, 8 | imageBufferAttributes: CFDictionary? 9 | ) throws -> VTDecompressionSession { 10 | var session: VTDecompressionSession? 11 | let status = VTDecompressionSessionCreate( 12 | allocator: nil, 13 | formatDescription: formatDescription, 14 | decoderSpecification: decoderSpecification, 15 | imageBufferAttributes: imageBufferAttributes, 16 | outputCallback: nil, 17 | decompressionSessionOut: &session 18 | ) 19 | if let error = VideoTranscoderError(status: status) { 20 | throw error 21 | } 22 | guard let session else { throw VideoTranscoderError.unknown(status) } 23 | return session 24 | } 25 | 26 | func decodeFrame( 27 | _ sampleBuffer: CMSampleBuffer, 28 | flags decodeFlags: VTDecodeFrameFlags = [], 29 | outputHandler: @escaping VTDecompressionOutputHandler 30 | ) throws { 31 | var infoFlagsOut = VTDecodeInfoFlags() 32 | let status = VTDecompressionSessionDecodeFrame( 33 | self, 34 | sampleBuffer: sampleBuffer, 35 | flags: decodeFlags, 36 | infoFlagsOut: &infoFlagsOut, 37 | outputHandler: outputHandler 38 | ) 39 | if let error = VideoTranscoderError(status: status) { throw error } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /TDS Video/VIews/WebServerPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebServerPage.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 16/04/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WebServerPage: View { 11 | @State private var ipAddresses: [String] = [] 12 | 13 | var body: some View { 14 | VStack(alignment: .leading, spacing: 16) { 15 | Text("Live View Available") 16 | .font(.title) 17 | .bold() 18 | 19 | Text("While screen recording is active, you can view this device from another device on the same network by opening the following address in a web browser:") 20 | Text("This is a work in progress, so please be patient, some features may not work yet. I have added this at request to support cars like Tesla where CarPlay is not available.") 21 | .font(.caption) 22 | Text("if you like this, please let me know and I will work on improving the functionality") 23 | .font(.caption) 24 | Text("No sound is handled!") 25 | Divider() 26 | Text("IP address to connect to the screen") 27 | ForEach(ipAddresses, id: \.self) { ip in 28 | Text("http://\(ip):8080") 29 | .font(.system(.body, design: .monospaced)) 30 | .foregroundColor(.blue) 31 | 32 | } 33 | 34 | Spacer() 35 | } 36 | .padding() 37 | .onAppear { 38 | self.ipAddresses = HTTPServer.shared.getAllIPAddresses().map { 39 | $0.components(separatedBy: ": ").last ?? $0 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /CarPlayMapView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CarPlayMapView.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 06/03/2025. 6 | // 7 | 8 | class CarPlayMapView: UIViewController, CPMapTemplateDelegate { 9 | 10 | var mapView: MKMapView? 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | print("loading the carplay mapview") 16 | 17 | let region = MKCoordinateRegion( 18 | center: CLLocationCoordinate2D(latitude: 0, longitude: 0), 19 | span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5) 20 | ) 21 | 22 | self.mapView = MKMapView(frame: view.bounds) 23 | self.mapView!.setRegion(region, animated: true) 24 | self.mapView!.showsUserLocation = true 25 | self.mapView!.setUserTrackingMode(.follow, animated: true) 26 | self.mapView!.overrideUserInterfaceStyle = .light 27 | 28 | self.mapView!.translatesAutoresizingMaskIntoConstraints = false 29 | self.view.addSubview(self.mapView!) 30 | 31 | self.mapView!.topAnchor.constraint(equalTo: view.topAnchor).isActive = true 32 | self.mapView!.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true 33 | self.mapView!.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true 34 | self.mapView!.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true 35 | } 36 | 37 | func mapTemplateDidBeginPanGesture(_ mapTemplate: CPMapTemplate) { 38 | print("🚙🚙🚙🚙🚙 Panning") 39 | } 40 | 41 | func mapTemplate(_ mapTemplate: CPMapTemplate, panWith direction: CPMapTemplate.PanDirection) { 42 | print("🚙🚙🚙🚙🚙 Panning: \(direction)") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /UploadVideo/Base.lproj/MainInterface.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /TDS Video/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /TDS Video/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /TDS Video.xcodeproj/xcuserdata/thomas.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Carplayintent.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | CarplayintentUI.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | ScreenRec.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 5 21 | 22 | ScreenRecSetupUI.xcscheme_^#shared#^_ 23 | 24 | orderHint 25 | 3 26 | 27 | TDS McdonaldsApi.xcscheme_^#shared#^_ 28 | 29 | orderHint 30 | 0 31 | 32 | TDS Video 2.xcscheme_^#shared#^_ 33 | 34 | orderHint 35 | 6 36 | 37 | TDS Video.xcscheme_^#shared#^_ 38 | 39 | orderHint 40 | 2 41 | 42 | UploadVideo.xcscheme_^#shared#^_ 43 | 44 | orderHint 45 | 4 46 | 47 | 48 | SuppressBuildableAutocreation 49 | 50 | E9B3711E2C5D7B830013612E 51 | 52 | primary 53 | 54 | 55 | E9B371A42C5D86080013612E 56 | 57 | primary 58 | 59 | 60 | E9B371B02C5D86080013612E 61 | 62 | primary 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /TDS Video/VIews/ScreenMirroringView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenMirroringView.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 16/04/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | 11 | struct ScreenMirroringView: UIViewControllerRepresentable { 12 | 13 | var CarPlayVideoImageView: UIImageView = UIImageView() 14 | 15 | func makeUIViewController(context: Context) -> UIViewController { 16 | 17 | let rootViewController = UIViewController() 18 | // Remove all existing constraints affecting CarPlayVideoImageView 19 | NSLayoutConstraint.deactivate(self.CarPlayVideoImageView.constraints) 20 | self.CarPlayVideoImageView.removeFromSuperview() 21 | rootViewController.view.addSubview(self.CarPlayVideoImageView) 22 | self.CarPlayVideoImageView.translatesAutoresizingMaskIntoConstraints = false 23 | NSLayoutConstraint.activate([ 24 | self.CarPlayVideoImageView.topAnchor.constraint(equalTo: rootViewController.view.topAnchor, constant: 0), 25 | self.CarPlayVideoImageView.bottomAnchor.constraint(equalTo: rootViewController.view.bottomAnchor, constant: 0), 26 | self.CarPlayVideoImageView.leadingAnchor.constraint(equalTo: rootViewController.view.leadingAnchor, constant: 0), 27 | self.CarPlayVideoImageView.trailingAnchor.constraint(equalTo: rootViewController.view.trailingAnchor, constant: 0) 28 | ]) 29 | 30 | rootViewController.view.setNeedsLayout() 31 | rootViewController.view.layoutIfNeeded() 32 | 33 | ScreenCaptureManager.shared.addImageView(imageView: CarPlayVideoImageView,orientation: .up) 34 | return rootViewController 35 | } 36 | 37 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 38 | } 39 | -------------------------------------------------------------------------------- /TDS Video/Transcoding/Extensions/VTCompressionSession+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import VideoToolbox 3 | 4 | extension VTCompressionSession { 5 | static func create( 6 | size: CGSize, 7 | codecType: CMVideoCodecType, 8 | encoderSpecification: CFDictionary 9 | ) throws -> VTCompressionSession { 10 | var session: VTCompressionSession? 11 | let status = VTCompressionSessionCreate( 12 | allocator: nil, 13 | width: Int32(size.width), 14 | height: Int32(size.height), 15 | codecType: codecType, 16 | encoderSpecification: encoderSpecification, 17 | imageBufferAttributes: nil, 18 | compressedDataAllocator: nil, 19 | outputCallback: nil, 20 | refcon: nil, 21 | compressionSessionOut: &session 22 | ) 23 | if let error = VideoTranscoderError(status: status) { 24 | throw error 25 | } 26 | guard let session else { throw VideoTranscoderError.unknown(status) } 27 | return session 28 | } 29 | 30 | func encodeFrame( 31 | _ pixelBuffer: CVPixelBuffer, 32 | presentationTimeStamp: CMTime, 33 | duration: CMTime, 34 | outputHandler: @escaping VTCompressionOutputHandler 35 | ) throws { 36 | var infoFlagsOut = VTEncodeInfoFlags() 37 | let status = VTCompressionSessionEncodeFrame( 38 | self, 39 | imageBuffer: pixelBuffer, 40 | presentationTimeStamp: presentationTimeStamp, 41 | duration: duration, 42 | frameProperties: nil, 43 | infoFlagsOut: &infoFlagsOut, 44 | outputHandler: outputHandler 45 | ) 46 | if let error = VideoTranscoderError(status: status) { 47 | throw error 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /TDS Video/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Editor 10 | CFBundleURLSchemes 11 | 12 | TDSVideo 13 | 14 | 15 | 16 | CFBundleTypeRole 17 | Editor 18 | CFBundleURLSchemes 19 | 20 | TDSVIDEOPAYMENT 21 | 22 | 23 | 24 | NSUserActivityTypes 25 | 26 | INSendMessageIntent 27 | 28 | UIApplicationSceneManifest 29 | 30 | UIApplicationSupportsMultipleScenes 31 | 32 | UISceneConfigurations 33 | 34 | CPTemplateApplicationSceneSessionRoleApplication 35 | 36 | 37 | UISceneClassName 38 | CPTemplateApplicationScene 39 | UISceneConfigurationName 40 | CarPlaySceneDelegate 41 | UISceneDelegateClassName 42 | $(PRODUCT_MODULE_NAME).CarPlaySceneDelegate 43 | 44 | 45 | UIWindowSceneSessionRoleApplication 46 | 47 | 48 | UISceneConfigurationName 49 | Default Configuration 50 | UISceneDelegateClassName 51 | $(PRODUCT_MODULE_NAME).SceneDelegate 52 | UISceneStoryboardFile 53 | Main 54 | 55 | 56 | 57 | 58 | UIFileSharingEnabled 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /TDS Video/UserD.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserD.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 25/03/2025. 6 | // 7 | 8 | 9 | 10 | extension UserDefaults { 11 | @objc dynamic var TDVideoSharedURL: String { 12 | return string(forKey: "TDVideo-SharedURL")! 13 | } 14 | } 15 | 16 | 17 | 18 | import UIKit 19 | 20 | class TDSVideoURlFromOutSideOFAppListener { 21 | static let shared = TDSVideoURlFromOutSideOFAppListener() 22 | 23 | private let notificationName = "group.net.thomasdye.TDS-docs.TDVideo-SharedURL" 24 | private let sharedDefaults = UserDefaults(suiteName: "group.net.thomasdye.TDS-docs") 25 | 26 | var onUpdate: ((String) -> Void)? 27 | 28 | private init() { 29 | CFNotificationCenterAddObserver( 30 | CFNotificationCenterGetDarwinNotifyCenter(), 31 | UnsafeRawPointer(Unmanaged.passUnretained(self).toOpaque()), 32 | { (_, observer, _, _, _) in 33 | guard let observer = observer else { return } 34 | let instance = Unmanaged.fromOpaque(observer).takeUnretainedValue() 35 | instance.defaultsChanged() 36 | }, 37 | notificationName as CFString, 38 | nil, 39 | .deliverImmediately 40 | ) 41 | } 42 | 43 | deinit { 44 | CFNotificationCenterRemoveObserver( 45 | CFNotificationCenterGetDarwinNotifyCenter(), 46 | UnsafeRawPointer(Unmanaged.passUnretained(self).toOpaque()), 47 | CFNotificationName(notificationName as CFString), 48 | nil 49 | ) 50 | } 51 | 52 | private func defaultsChanged() { 53 | let newValue = sharedDefaults?.string(forKey: "TDVideo-SharedURL") ?? "" 54 | DispatchQueue.main.async { 55 | self.onUpdate?(newValue) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /CarplayintentUI/Base.lproj/MainInterface.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /TDS Video/VIews/WebViewForDRM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebViewForDRM.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 13/04/2025. 6 | // 7 | 8 | import SwiftUI 9 | import WebKit 10 | 11 | struct WebView2: UIViewControllerRepresentable { 12 | // var url: URL 13 | 14 | func makeUIViewController(context: Context) -> CustomSafariController { 15 | let webViewController = CustomSafariController.shared 16 | // CustomWebViewController.shared.IsIncar = false 17 | return webViewController 18 | } 19 | 20 | func updateUIViewController(_ uiViewController: CustomSafariController, context: Context) { 21 | // uiViewController.loadURL(url) 22 | } 23 | 24 | 25 | } 26 | 27 | struct WebViewContainer2: View { 28 | @State private var ShowCarButtons: Bool = false 29 | @State private var showURLInput: Bool = false 30 | @State private var userInputURL: String = "" 31 | 32 | var body: some View { 33 | VStack { 34 | WebView2() 35 | 36 | Spacer() 37 | } 38 | .toolbar(content: { 39 | Button("Google", action: { 40 | CustomSafariController.shared.loadURL(URL(string: "https://google.com")!) 41 | }) 42 | 43 | Button("Send to Car", action: { 44 | TDSVideoShared.shared.CarPlayComp?(.init(type: .web, URL: nil)) 45 | }) 46 | Menu("More Options") { 47 | Button("Control Button", action: { 48 | ShowCarButtons = true 49 | }) 50 | Button("Reload", action: { 51 | showURLInput = true 52 | }) 53 | } 54 | 55 | }) 56 | .sheet(isPresented: $showURLInput) { 57 | URLInputSheet(showURLInput: $showURLInput, userInputURL: $userInputURL) 58 | } 59 | .sheet(isPresented: $ShowCarButtons) { 60 | WebViewButtons() 61 | } 62 | .ignoresSafeArea(.all) 63 | } 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /TDS Video/CPTemplate.h: -------------------------------------------------------------------------------- 1 | /* Generated by RuntimeBrowser 2 | Image: /System/Library/Frameworks/CarPlay.framework/CarPlay 3 | */ 4 | 5 | 6 | 7 | 8 | 9 | 10 | @interface CPTemplate (Swizzling) 11 | 12 | @property (readonly, copy) NSString *debugDescription; 13 | @property (nonatomic, retain) NSOperationQueue *deferredOperationQueue; 14 | @property (readonly, copy) NSString *description; 15 | @property (readonly) unsigned long long hash; 16 | @property (nonatomic, readonly) NSUUID *identifier; 17 | @property (nonatomic, retain) NSMutableArray *internalLeadingBarButtons; 18 | @property (nonatomic, retain) NSMutableArray *internalTrailingBarButtons; 19 | @property (nonatomic, retain) NSArray *leadingNavigationBarButtons; 20 | @property (readonly) Class superclass; 21 | //@property (nonatomic) *templateDelegate; 22 | //@property (nonatomic, retain) *templateProvider; 23 | @property (nonatomic, retain) NSArray *trailingNavigationBarButtons; 24 | @property (nonatomic, retain) id userInfo; 25 | 26 | + (bool)supportsSecureCoding; 27 | 28 | 29 | - (bool)barButton:(id)arg1 setImage:(id)arg2; 30 | - (bool)barButton:(id)arg1 setTitle:(id)arg2; 31 | - (bool)control:(id)arg1 setEnabled:(bool)arg2; 32 | - (void)dealloc; 33 | - (id)deferredOperationQueue; 34 | - (id)description; 35 | - (void)encodeWithCoder:(id)arg1; 36 | - (void)handleActionForControlIdentifier:(id)arg1; 37 | - (id)identifier; 38 | - (id)init; 39 | - (id)initWithCoder:(id)arg1; 40 | - (id)internalLeadingBarButtons; 41 | - (id)internalTrailingBarButtons; 42 | - (id)leadingNavigationBarButtons; 43 | - (void)setDeferredOperationQueue:(id)arg1; 44 | - (void)setInternalLeadingBarButtons:(id)arg1; 45 | - (void)setInternalTrailingBarButtons:(id)arg1; 46 | - (void)setLeadingNavigationBarButtons:(id)arg1; 47 | - (void)setTemplateDelegate:(id)arg1; 48 | - (void)setTemplateProvider:(id)arg1; 49 | - (void)setTrailingNavigationBarButtons:(id)arg1; 50 | - (void)setUserInfo:(id)arg1; 51 | - (id)templateDelegate; 52 | - (void)templateDidAppear:(id)arg1 animated:(bool)arg2; 53 | - (void)templateDidDisappear:(id)arg1 animated:(bool)arg2; 54 | - (id)templateProvider; 55 | - (void)templateWillAppear:(id)arg1 animated:(bool)arg2; 56 | - (void)templateWillDisappear:(id)arg1 animated:(bool)arg2; 57 | - (id)trailingNavigationBarButtons; 58 | - (id)userInfo; 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /TDS Video/AVPlayerViewController+ButtonInteractionSwizzling.m: -------------------------------------------------------------------------------- 1 | // 2 | // AVPlayerViewController+ButtonInteractionSwizzling.m 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 06/08/2024. 6 | // 7 | 8 | #import "AVPlayerViewController+ButtonInteractionSwizzling.h" 9 | #import 10 | 11 | @implementation AVPlayerViewController (ButtonInteractionSwizzling) 12 | 13 | // Example custom method that you may need to implement based on your investigation 14 | - (void)my_custom_registerWithArbiterSkippingEvaluationAndObservation { 15 | // Custom implementation to bypass the assertion 16 | @try { 17 | [self my_custom_registerWithArbiterSkippingEvaluationAndObservation]; // Call the original method 18 | } @catch (NSException *exception) { 19 | NSLog(@"Caught exception: %@", exception); 20 | // Handle the exception or perform necessary cleanup 21 | } 22 | } 23 | 24 | // Implement the swizzling method 25 | + (void)swizzleMethod:(Class)class originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector { 26 | Method originalMethod = class_getInstanceMethod(class, originalSelector); 27 | Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); 28 | 29 | BOOL didAddMethod = class_addMethod(class, 30 | originalSelector, 31 | method_getImplementation(swizzledMethod), 32 | method_getTypeEncoding(swizzledMethod)); 33 | 34 | if (didAddMethod) { 35 | class_replaceMethod(class, 36 | swizzledSelector, 37 | method_getImplementation(originalMethod), 38 | method_getTypeEncoding(originalMethod)); 39 | } else { 40 | method_exchangeImplementations(originalMethod, swizzledMethod); 41 | } 42 | } 43 | 44 | // Perform the swizzling in +load to ensure it happens early 45 | + (void)load { 46 | static dispatch_once_t onceToken; 47 | dispatch_once(&onceToken, ^{ 48 | Class class = NSClassFromString(@"_UIPhysicalButtonInteraction"); 49 | if (class) { 50 | SEL originalSelector = NSSelectorFromString(@"_registerWithArbiterSkippingEvaluationAndObservation"); 51 | SEL swizzledSelector = @selector(my_custom_registerWithArbiterSkippingEvaluationAndObservation); 52 | [self swizzleMethod:class originalSelector:originalSelector swizzledSelector:swizzledSelector]; 53 | } 54 | }); 55 | } 56 | 57 | @end 58 | 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TDS-CarPlay 2 | 3 | ![Downloads](https://api.thomasdye.net/app/ThomasRandom/TDSVideo/AmountOfDownloads) 4 | 5 | # This app is not meant to be used while driving 6 | 7 | ## What is this 8 | 9 | TDS CarPlay is an app that allows you to view videos and show your screen on your carpay screen, allowing for watching movies and other things while in your car 10 | 11 | 12 | ## Download 13 | 14 | Test flight Beta link download link - [TDS CarPlay](https://testflight.apple.com/join/6drWGVde) 15 | The beta is now full - i will leave the link here in case people leave it, 16 | To stay up to date with info and when I come out with a new beta, subscribe to the news letter [news letter](https://news.thomasdye.net/) 17 | 4th beta is now here - [TDS CarPlay BETA 4](https://testflight.apple.com/join/kYbkecxa) 18 | 19 | ## Links 20 | 21 | Youtube Video tutorial - https://youtu.be/gI3Tj2KP290 22 | 23 | 24 | 25 | 26 | Any issues please reach out. 27 | 28 | 29 | ## Wheres the code? 30 | 31 | I am pleased to say the code has now been added for you all to use, see bellow about use. 32 | 33 | 34 | 35 | 36 | 37 | ## Install 38 | 39 | To install this 40 | 41 | 42 | 1. Download the code 43 | 44 | 45 | 2. Change the bundle IDS 46 | 47 | 48 | 3. Build and run the code 49 | 50 | 51 | 4. Enjoy the ability to watch videos in your car 52 | 53 | 54 | ## Feature request / Bugs 55 | 56 | Please make issues here and tell me about what you'd like in the app or your problem and I will work on them as soon as I can. 57 | 58 | 59 | ## Support 60 | 61 | If you are liking the app it would really help if you'd buy me a coffee and support the feature development of the app and other things do. 62 | Buy me a coffee - https://buymeacoffee.com/Thomadye 63 | 64 | I would like to say a massive thank you to everyone who has downloaded my app, its way more popular then i ever thought it would be! 65 | 66 | If you are interested in updates please follow on one of the two bellow - I post updates there about what i am doing building and stuff all tech related. 67 | 68 | https://www.youtube.com/channel/UCn8DxUf388I1B7Lxzqv5KRw 69 | http://instagram.com/thomas_dye 70 | 71 | 72 | 73 | 74 | 75 | ## Use of My App and Code 76 | 77 | I’m happy for anyone to use my app and code for **personal, non-commercial** use. 78 | 79 | **You are not permitted to sell, redistribute, or use this app or its code for commercial purposes** (i.e. to make money) without my explicit permission. 80 | 81 | If you would like to use this project in a commercial setting, please [contact me](mailto:apple@thomasdye.net) to discuss licensing terms. 82 | 83 | -------------------------------------------------------------------------------- /TDS Video/VIews/Help.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Help.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 19/03/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct Help: View { 11 | var body: some View { 12 | ScrollView { 13 | VStack(alignment: .leading, spacing: 16) { 14 | Group { 15 | Text("📺 Screen Recording Setup") 16 | .font(.headline) 17 | 18 | Text(""" 19 | 1. Open the app. 20 | 2. Start screen recording using the app extension: 21 | - Open Control Centre. 22 | - Long-press the screen recording button. 23 | - Select **TDS Video** from the dropdown. 24 | 3. This screen recording will begin streaming. 25 | 4. Open the client app to view the stream. 26 | """) 27 | 28 | Text("❗️ If the TDS Video extension doesn't appear, the system may not have updated the list of available apps. Try waiting a few moments or restarting your device.") 29 | .font(.caption) 30 | .foregroundColor(.secondary) 31 | } 32 | 33 | Divider() 34 | 35 | Group { 36 | Text("🌐 Web Page Streaming") 37 | .font(.headline) 38 | 39 | Text(""" 40 | 1. Open the Web View screen. 41 | 2. Press **Reload** to enter a URL (e.g., `home` for Google). 42 | 3. Tap **Car Load** to stream the web content to the car display. 43 | 4. Leave the web view, then press **Load Web View** to restore the view. 44 | 5. Use the **WebView Buttons** screen to adjust or control the page. 45 | """) 46 | 47 | Text("Tip: For left-handed cars, a toggle is available in settings to adjust the screen layout.") 48 | } 49 | 50 | Divider() 51 | 52 | Group { 53 | Text("⚠️ Dealing with Lag") 54 | .font(.headline) 55 | 56 | Text(""" 57 | Occasional lag can occur. If you experience issues: 58 | - Close both apps completely. 59 | - Reopen and retry the screen recording process. 60 | """) 61 | } 62 | 63 | Divider() 64 | 65 | Group { 66 | Text("ℹ️ Other Info") 67 | .font(.headline) 68 | 69 | Text(""" 70 | **Why no AirPlay?** 71 | Audio routing to CarPlay through AirPlay is not currently possible due to system limitations. Implementing this would cause audio to be rerouted through AirPlay, not the car. 72 | """) 73 | } 74 | 75 | Spacer() 76 | } 77 | .padding() 78 | } 79 | .navigationTitle("Help") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /TDS Video/VIews/ScreenMirroingSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScreenMirroingSettings.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 20/03/2025. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ScreenMirroingSettings: View { 11 | 12 | @ObservedObject var videoAPI = ScreenCaptureManager.shared 13 | @AppStorage("CarIsRightHanded") private var carIsRightHanded: Bool = false 14 | 15 | var body: some View { 16 | Form { 17 | Section(header: Label("Orientation", systemImage: "rectangle.rotate")) { 18 | Picker("Screen Mirroring Orientation", selection: $videoAPI.selectedOrientation) { 19 | ForEach(ScreenOrientation.allCases, id: \.self) { orientation in 20 | Text(orientation.humanReadable()) 21 | .tag(orientation) 22 | } 23 | } 24 | .pickerStyle(.menu) 25 | } 26 | 27 | Section(header: Label("Aspect Ratio", systemImage: "aspectratio")) { 28 | Picker("Aspect Ratio", selection: $videoAPI.selectedAspectRatio) { 29 | ForEach(AspectRatio.allCases, id: \.self) { ratio in 30 | Text(ratio.humanReadableName()) 31 | .tag(ratio) 32 | } 33 | } 34 | .pickerStyle(.menu) 35 | 36 | Text("Choose the display aspect ratio best suited to your vehicle screen.") 37 | .font(.caption) 38 | .foregroundColor(.secondary) 39 | } 40 | 41 | Section(header: Label("Screen Offset", systemImage: "arrow.left.and.right")) { 42 | Picker("Offset Side", selection: Binding( 43 | get: { OffsetOption.from(offset: videoAPI.Screenoffset) }, 44 | set: { 45 | videoAPI.Screenoffset = $0.offset 46 | ScreenCaptureManager.shared.CarPlaysideofcarchange?($0.offset) 47 | } 48 | )) { 49 | ForEach(OffsetOption.allCases) { option in 50 | Text(option.displayName) 51 | .tag(option) 52 | } 53 | } 54 | .pickerStyle(.menu) 55 | 56 | Text("Current Offset: \(videoAPI.Screenoffset.rawValue)") 57 | .font(.caption2) 58 | .foregroundColor(.secondary) 59 | } 60 | 61 | // Section(header: Label("Car Configuration", systemImage: "car")) { 62 | // Toggle("Right-Hand Drive Car", isOn: $carIsRightHanded) 63 | // 64 | // Text("Toggle this if your car is right-hand drive. This adjusts screen layout and controls accordingly.") 65 | // .font(.caption2) 66 | // .foregroundColor(.secondary) 67 | // } 68 | } 69 | .navigationTitle("Mirroring Settings") 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /TDS Video/CPTemplateApplicationScene+Swizzle.m: -------------------------------------------------------------------------------- 1 | // 2 | // CPTemplateApplicationScene+Swizzle.m 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 05/08/2024. 6 | // 7 | 8 | #import "CPTemplateApplicationScene.h" 9 | #import 10 | 11 | @implementation CPTemplateApplicationScene (Swizzle) 12 | 13 | + (void)load { 14 | static dispatch_once_t onceToken; 15 | dispatch_once(&onceToken, ^{ 16 | Class class = [self class]; 17 | 18 | SEL originalSelector = NSSelectorFromString(@"_shouldCreateCarWindow"); 19 | SEL swizzledSelector = @selector(xyz_shouldCreateCarWindow); 20 | 21 | Method originalMethod = class_getInstanceMethod(class, originalSelector); 22 | Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); 23 | 24 | BOOL didAddMethod = class_addMethod(class, 25 | originalSelector, 26 | method_getImplementation(swizzledMethod), 27 | method_getTypeEncoding(swizzledMethod)); 28 | 29 | if (didAddMethod) { 30 | class_replaceMethod(class, 31 | swizzledSelector, 32 | method_getImplementation(originalMethod), 33 | method_getTypeEncoding(originalMethod)); 34 | } else { 35 | method_exchangeImplementations(originalMethod, swizzledMethod); 36 | } 37 | 38 | }); 39 | } 40 | 41 | - (BOOL)xyz_shouldCreateCarWindow { 42 | // Custom logic or call the original implementation if needed 43 | return YES; 44 | } 45 | 46 | - (BOOL)publicShouldCreateCarWindow { 47 | return [self xyz_shouldCreateCarWindow]; 48 | } 49 | 50 | @end 51 | 52 | 53 | #import 54 | 55 | 56 | @implementation CPInterfaceController (Bypass) 57 | 58 | + (void)load { 59 | Method original = class_getInstanceMethod(self, @selector(clientPushedIllegalTemplateOfClass:)); 60 | Method swizzled = class_getInstanceMethod(self, @selector(bypass_clientPushedIllegalTemplateOfClass:)); 61 | 62 | method_exchangeImplementations(original, swizzled); 63 | 64 | } 65 | 66 | - (void)bypass_clientPushedIllegalTemplateOfClass:(Class)cls { 67 | NSLog(@"Bypassing illegal template restriction for class:"); 68 | // Do nothing - just bypass the check 69 | } 70 | 71 | 72 | @end 73 | 74 | 75 | @implementation CPWindow (Bypass) 76 | 77 | - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { 78 | UIView *view = [super hitTest:point withEvent:event]; 79 | if (!view) { 80 | NSLog(@"Touch ignored, forwarding to first responder..."); 81 | return self.rootViewController.view; 82 | } 83 | return view; 84 | } 85 | 86 | @end 87 | 88 | @implementation CPWindow (Fix) 89 | 90 | - (BOOL)canBecomeFirstResponder { 91 | return YES; 92 | } 93 | 94 | - (BOOL)canResignFirstResponder { 95 | return NO; 96 | } 97 | 98 | - (void)didMoveToSuperview { 99 | [super didMoveToSuperview]; 100 | [self becomeFirstResponder]; // Ensure it handles input 101 | } 102 | 103 | @end 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /TDS Video/TDSVideoMainScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TDSVideoMainScreen.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 16/04/2025. 6 | // 7 | // This file is show I can show apple something else to get it approved 8 | 9 | import SwiftUI 10 | 11 | struct TDSVideoMainScreen: View { 12 | @State private var ipAddresses: [String] = [] 13 | @State private var showingCodeAlert = false 14 | @State private var connectionCode = "" 15 | @State private var showRebootAlert = false 16 | 17 | var body: some View { 18 | NavigationView { 19 | ScrollView { 20 | VStack(alignment: .leading, spacing: 20) { 21 | Text("Welcome to TDS Video!") 22 | .font(.largeTitle) 23 | .bold() 24 | 25 | Text("How to Share Your Screen") 26 | .font(.title2) 27 | .bold() 28 | 29 | Text(""" 30 | This app allows you to share your screen locally with other devices on the same network. 31 | 32 | 1. Open Control Center on your device. 33 | 2. Tap 'Screen Recording'. 34 | 3. In the screen recording menu, choose **My App** from the list of extensions. 35 | 4. Start recording. 36 | 37 | Anyone on the same Wi-Fi can view your screen by entering one of the local IP addresses listed below in their browser. 38 | """) 39 | 40 | Text("Available IP Addresses:") 41 | .font(.headline) 42 | 43 | ForEach(ipAddresses, id: \.self) { ip in 44 | Text("http://\(ip):8080") 45 | .font(.system(.body, design: .monospaced)) 46 | .foregroundColor(.blue) 47 | } 48 | 49 | Spacer() 50 | } 51 | .padding() 52 | } 53 | .navigationBarTitle("TDS Video", displayMode: .inline) 54 | .toolbar { 55 | ToolbarItem(placement: .navigationBarLeading) { 56 | Button("Enter Code") { 57 | showingCodeAlert = true 58 | } 59 | } 60 | } 61 | .alert("Enter Connection Code", isPresented: $showingCodeAlert, actions: { 62 | TextField("Connection Code", text: $connectionCode) 63 | Button("OK") { 64 | if connectionCode.lowercased() == "carplay" { 65 | // Trigger reboot instruction 66 | TDSCarplayAccess.shared.ShowTDSCarPlaySettings = true 67 | showRebootAlert = true 68 | } 69 | connectionCode = "" 70 | } 71 | Button("Cancel", role: .cancel) { 72 | connectionCode = "" 73 | } 74 | }, message: { 75 | Text("Enter the connection code to proceed.") 76 | }) 77 | .alert("Reboot Required", isPresented: $showRebootAlert) { 78 | Button("OK", role: .cancel) {} 79 | } message: { 80 | Text("CarPlay mode is now enabled. Please close and reopen the app for changes to take effect.") 81 | } 82 | .onAppear { 83 | self.ipAddresses = HTTPServer.shared.getAllIPAddresses().map { 84 | $0.components(separatedBy: ": ").last ?? $0 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /UploadVideo/ShareViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShareViewController.swift 3 | // UploadVideo 4 | // 5 | // Created by Thomas Dye on 05/08/2024. 6 | // 7 | 8 | import UIKit 9 | import Social 10 | import MobileCoreServices 11 | import AVFoundation 12 | 13 | class ShareViewController: SLComposeServiceViewController { 14 | 15 | override func isContentValid() -> Bool { 16 | return true 17 | } 18 | 19 | override func didSelectPost() { 20 | guard let item = extensionContext?.inputItems.first as? NSExtensionItem, 21 | let attachments = item.attachments else { 22 | return 23 | } 24 | 25 | for attachment in attachments { 26 | print(attachment) 27 | if attachment.hasItemConformingToTypeIdentifier(UTType.url.identifier) { 28 | handleURL(attachment: attachment) 29 | } else if attachment.hasItemConformingToTypeIdentifier(UTType.movie.identifier) { 30 | handleVideo(attachment: attachment) 31 | } 32 | } 33 | } 34 | 35 | private func handleURL(attachment: NSItemProvider) { 36 | attachment.loadItem(forTypeIdentifier: UTType.url.identifier as String, options: nil) { (item, error) in 37 | if let url = item as? URL { 38 | self.saveURL(url: url) 39 | } 40 | } 41 | } 42 | 43 | private func handleVideo(attachment: NSItemProvider) { 44 | attachment.loadItem(forTypeIdentifier: UTType.movie.identifier as String, options: nil) { (item, error) in 45 | if let url = item as? URL { 46 | self.saveVideo(url: url) 47 | } 48 | } 49 | } 50 | 51 | private func saveURL(url: URL) { 52 | let sharedDefaults = UserDefaults(suiteName: "group.net.thomasdye.TDS-docs") 53 | sharedDefaults?.set(url.absoluteString, forKey: "TDVideo-SharedURL") 54 | 55 | // Post cross-process notification 56 | let notificationName = "group.net.thomasdye.TDS-docs.TDVideo-SharedURL" 57 | CFNotificationCenterPostNotification( 58 | CFNotificationCenterGetDarwinNotifyCenter(), 59 | CFNotificationName(notificationName as CFString), 60 | nil, 61 | nil, 62 | true 63 | ) 64 | 65 | // Complete request to close extension 66 | self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) 67 | } 68 | 69 | 70 | 71 | 72 | private func saveVideo(url: URL) { 73 | saveURL(url: url) 74 | } 75 | private func openAppWithURL(url: URL) { 76 | let encodedURL = url.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" 77 | let scheme = "TDSVideo://shared-url?url=\(encodedURL)" 78 | 79 | if let appURL = URL(string: scheme) { 80 | DispatchQueue.main.async { 81 | self.extensionContext?.open(appURL, completionHandler: { success in 82 | if success { 83 | print("Opened main app successfully") 84 | } else { 85 | print("Failed to open main app") 86 | } 87 | }) 88 | } 89 | } else { 90 | print("Invalid URL: \(scheme)") 91 | } 92 | } 93 | 94 | 95 | override func configurationItems() -> [Any]! { 96 | return [] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /TDS Video/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // TDS McdonaldsApi 4 | // 5 | // Created by Thomas Dye on 02/08/2024. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | 20 | guard let _ = (scene as? UIWindowScene) else { return } 21 | } 22 | 23 | func sceneDidDisconnect(_ scene: UIScene) { 24 | // Called as the scene is being released by the system. 25 | // This occurs shortly after the scene enters the background, or when its session is discarded. 26 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 27 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 28 | } 29 | 30 | 31 | 32 | func sceneDidEnterBackground(_ scene: UIScene) { 33 | // Scene entered the background 34 | print("The scene has entered the background.") 35 | // Save data or release resources here 36 | ScreenCaptureManager.shared.InBackground() 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Scene will enter the foreground 41 | print("The scene will enter the foreground.") 42 | } 43 | 44 | func sceneDidBecomeActive(_ scene: UIScene) { 45 | // Scene became active 46 | print("The scene is active.") 47 | ScreenCaptureManager.shared.InForeground() 48 | let userDefaults = UserDefaults(suiteName: "group.net.thomasdye.TDS-docs") 49 | 50 | // Observe changes to "TDSSharedURL" 51 | // userDefaults?.addObserver(self, forKeyPath: "TDSSharedURL", options: .new, context: nil) 52 | 53 | if let sharedURLString = userDefaults?.string(forKey: "TDVideo-SharedURL"), 54 | let sharedURL = URL(string: sharedURLString) { 55 | CustomWebViewController.shared.initView() 56 | CustomWebViewController.shared.loadURL(sharedURL) 57 | TDSVideoShared.shared.CarPlayComp?(.init(type: .web, URL: sharedURL)) 58 | 59 | 60 | 61 | // Handle immediately if already saved 62 | userDefaults?.removeObject(forKey: "TDVideo-SharedURL") 63 | } 64 | } 65 | 66 | func sceneWillResignActive(_ scene: UIScene) { 67 | // Scene will resign active 68 | print("The scene is about to become inactive.") 69 | } 70 | func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { 71 | guard let urlContext = URLContexts.first else { return } 72 | let url = urlContext.url 73 | 74 | print("App opened with URL: \(url.absoluteString)") 75 | 76 | if url.scheme?.lowercased() == "tdsvideo" { // Ensure case matches 77 | if let components = URLComponents(url: url, resolvingAgainstBaseURL: true), 78 | let queryItems = components.queryItems, 79 | let sharedURLString = queryItems.first(where: { $0.name == "url" })?.value, 80 | let sharedURL = URL(string: sharedURLString.removingPercentEncoding ?? "") { 81 | 82 | print("Received shared URL in app: \(sharedURL)") 83 | // Handle the shared URL (e.g., update UI or store for later use) 84 | } 85 | } 86 | 87 | if url.scheme?.lowercased() == "tdsvideopayment" { 88 | TDSVideoAPI.shared.HidePaymentScreen() 89 | 90 | } 91 | 92 | } 93 | 94 | 95 | 96 | } 97 | 98 | -------------------------------------------------------------------------------- /TDS Video/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TDS McdonaldsApi 4 | // 5 | // Created by Thomas Dye on 02/08/2024. 6 | // 7 | 8 | import UIKit 9 | import CarPlay 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var observer: NSKeyValueObservation? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | 18 | 19 | return true 20 | } 21 | 22 | // MARK: UISceneSession Lifecycle 23 | 24 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 25 | // Called when a new scene session is being created. 26 | // Use this method to select a configuration to create the new scene with. 27 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 28 | } 29 | 30 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 31 | // Called when the user discards a scene session. 32 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 33 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 34 | } 35 | 36 | 37 | // func application(_ application: UIApplication, didConnectCarInterfaceController interfaceController: CPInterfaceController, to window: CPWindow) { 38 | // print(window) 39 | // } 40 | 41 | func application(_ application: UIApplication, didConnectCarInterfaceController interfaceController: CPInterfaceController, to window: CPWindow) { 42 | print("Scene Connected") 43 | } 44 | 45 | func application(_ application: UIApplication, didDisconnectCarInterfaceController interfaceController: CPInterfaceController, from window: CPWindow) { 46 | print("Scene Disconenced") 47 | } 48 | 49 | func applicationDidEnterBackground(_ application: UIApplication) { 50 | // Application entered the background 51 | print("The app has entered the background.") 52 | // Perform any background tasks or cleanup here 53 | } 54 | 55 | func applicationWillEnterForeground(_ application: UIApplication) { 56 | // Application will enter the foreground 57 | print("The app will enter the foreground.") 58 | } 59 | func applicationWillTerminate(_ application: UIApplication) { 60 | // Code to execute just before the app terminates 61 | print("App will terminate") 62 | ScreenCaptureManager.shared.Stop() 63 | } 64 | 65 | @objc private func handleSharedURL(_ notification: Notification) { 66 | if let urlString = notification.userInfo?["url"] as? String, 67 | let url = URL(string: urlString) { 68 | print("Received shared URL: \(url)") 69 | // Handle the URL in the app 70 | } 71 | } 72 | 73 | @objc private func handleSharedVideo(_ notification: Notification) { 74 | if let videoURLString = notification.userInfo?["videoURL"] as? String, 75 | let videoURL = URL(string: videoURLString) { 76 | print("Received shared video URL: \(videoURL)") 77 | // Handle the video in the app 78 | } 79 | } 80 | 81 | // Handle changes 82 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 83 | print(keyPath) 84 | if keyPath == "TDVideo-SharedURL" { 85 | let userDefaults = UserDefaults(suiteName: "group.net.thomasdye.TDS-docs") 86 | 87 | if let sharedURLString = userDefaults?.string(forKey: "TDVideo-SharedURL"), 88 | let sharedURL = URL(string: sharedURLString) { 89 | 90 | print("Live shared URL received: \(sharedURL)") 91 | 92 | // Clear after retrieving 93 | userDefaults?.removeObject(forKey: "TDSSharedURL") 94 | } 95 | } 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /TDS Video.xcodeproj/xcshareddata/xcschemes/TDS Video.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 36 | 42 | 43 | 44 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 69 | 75 | 76 | 77 | 78 | 84 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /TDS Video/TDSLocationAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TDSLocationAPI.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 17/03/2025. 6 | // 7 | 8 | import Foundation 9 | import CoreLocation 10 | import SwiftUI 11 | class TDSLocationAPI: NSObject, CLLocationManagerDelegate,ObservableObject { 12 | static let shared = TDSLocationAPI() 13 | 14 | private let locationManager = CLLocationManager() 15 | private var currentLocation: CLLocation? 16 | private var lastSentLocation: CLLocation? 17 | private let stationaryDistanceThreshold: CLLocationDistance = 50 // 10 m 18 | 19 | // NEW: track whether updates are currently active 20 | @Published var isUpdatingLocation = false 21 | 22 | // NEW: track whether user is currently considered “stationary” 23 | @Published var isStationary = true 24 | 25 | // private var locationContinuation: CheckedContinuation? 26 | // 27 | var latitude: Double? { currentLocation?.coordinate.latitude } 28 | var longitude: Double? { currentLocation?.coordinate.longitude } 29 | // 30 | // var Access:Bool = true 31 | // private override init() { 32 | // super.init() 33 | // locationManager.delegate = self 34 | // locationManager.desiredAccuracy = kCLLocationAccuracyBest 35 | // locationManager.startUpdatingLocation() 36 | // if TDSCarplayAccess.shared.DisableIsStationary == true { 37 | // isStationary = true 38 | // } 39 | // } 40 | // 41 | // func requestLocationPermission() { 42 | // locationManager.requestWhenInUseAuthorization() 43 | // } 44 | // 45 | // 46 | // func CombinedValue() -> Bool { 47 | // if TDSCarplayAccess.shared.DisableIsStationary == true { 48 | // return true 49 | // } 50 | // if isUpdatingLocation == false { 51 | // return false 52 | // } 53 | // if isStationary == false { 54 | // return false 55 | // } 56 | // return true 57 | // } 58 | // @MainActor 59 | // func startUpdatingLocation() { 60 | // guard CLLocationManager.locationServicesEnabled() else { return } 61 | // lastSentLocation = nil 62 | // 63 | // isStationary = false 64 | // isUpdatingLocation = true 65 | // locationManager.startUpdatingLocation() 66 | // if TDSCarplayAccess.shared.DisableIsStationary == true { 67 | // isStationary = true 68 | // } 69 | // } 70 | // @MainActor 71 | // func stopUpdatingLocation() { 72 | // locationManager.stopUpdatingLocation() 73 | // isUpdatingLocation = false 74 | // } 75 | // 76 | // /// Returns a tuple of (updating, stationary) 77 | // func currentMovementStatus() -> (updating: Bool, stationary: Bool) { 78 | // return (isUpdatingLocation, isStationary) 79 | // } 80 | // 81 | // // MARK: - CLLocationManagerDelegate 82 | // @MainActor 83 | // func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 84 | // guard let newLoc = locations.last else { return } 85 | // 86 | // currentLocation = newLoc 87 | // 88 | // if let last = lastSentLocation { 89 | // let distanceMoved = newLoc.distance(from: last) 90 | // if distanceMoved < stationaryDistanceThreshold { 91 | // // still within threshold → stationary 92 | // isStationary = true 93 | // // keep updating until movement detected 94 | // return 95 | // } else { 96 | // // moved beyond threshold → not stationary 97 | // if TDSCarplayAccess.shared.DisableIsStationary == true { 98 | // isStationary = false 99 | // } 100 | // } 101 | // } 102 | // 103 | // // first fix or moved enough to count as “non‑stationary” 104 | // lastSentLocation = newLoc 105 | // locationContinuation?.resume(returning: newLoc) 106 | // locationContinuation = nil 107 | // stopUpdatingLocation() 108 | // } 109 | // 110 | // @MainActor 111 | // func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { 112 | // print("Failed to get location: \(error.localizedDescription)") 113 | // locationContinuation?.resume(returning: nil) 114 | // locationContinuation = nil 115 | // stopUpdatingLocation() 116 | // } 117 | } 118 | 119 | -------------------------------------------------------------------------------- /TDS Video/Transcoding/VideoDecoderAnnexBAdaptor.swift: -------------------------------------------------------------------------------- 1 | import CoreMedia 2 | import Foundation 3 | import OSLog 4 | 5 | // MARK: - VideoDecoderAnnexBAdaptor 6 | 7 | public final class VideoDecoderAnnexBAdaptor { 8 | // MARK: Lifecycle 9 | 10 | public init( 11 | videoDecoder: VideoDecoder, 12 | codec: Codec 13 | ) { 14 | self.videoDecoder = videoDecoder 15 | self.codec = codec 16 | } 17 | 18 | // MARK: Public 19 | 20 | public func decode(_ data: Data) { 21 | switch codec { 22 | case .h264: 23 | decodeH264(data) 24 | case .hevc: 25 | decodeHEVC(data) 26 | } 27 | } 28 | 29 | // MARK: Internal 30 | 31 | static let logger = Logger(subsystem: "Transcoding", category: "VideoDecoderAnnexBAdaptor") 32 | 33 | let videoDecoder: VideoDecoder 34 | let codec: Codec 35 | var formatDescription: CMVideoFormatDescription? 36 | 37 | var vps: Data? 38 | var sps: Data? 39 | var pps: Data? 40 | 41 | func decodeH264(_ data: Data) { 42 | for nalu in data.split(separator: H264NALU.startCode).map({ H264NALU(data: Data($0)) }) { 43 | if nalu.isSPS { 44 | sps = nalu.data 45 | } else if nalu.isPPS { 46 | pps = nalu.data 47 | } else if nalu.isPFrame || nalu.isIFrame { 48 | if nalu.isIFrame, let sps, let pps { 49 | do { 50 | let formatDescription = try CMVideoFormatDescription(h264ParameterSets: [sps, pps]) 51 | videoDecoder.setFormatDescription(formatDescription) 52 | self.formatDescription = formatDescription 53 | } catch { 54 | Self.logger.error("Failed to create format description with error: \(error, privacy: .public)") 55 | } 56 | } 57 | decodeAVCCFrame(nalu.avcc) 58 | } 59 | } 60 | } 61 | 62 | func decodeHEVC(_ data: Data) { 63 | for nalu in data.split(separator: HEVCNALU.startCode).map({ HEVCNALU(data: Data($0)) }) { 64 | if nalu.isVPS { 65 | vps = nalu.data 66 | } else if nalu.isSPS { 67 | sps = nalu.data 68 | } else if nalu.isPPS { 69 | pps = nalu.data 70 | } else if nalu.isPFrame || nalu.isIFrame { 71 | if nalu.isIFrame, let vps, let sps, let pps { 72 | do { 73 | let formatDescription = try CMVideoFormatDescription(hevcParameterSets: [vps, sps, pps]) 74 | videoDecoder.setFormatDescription(formatDescription) 75 | self.formatDescription = formatDescription 76 | } catch { 77 | Self.logger.error("Failed to create format description with error: \(error, privacy: .public)") 78 | } 79 | } 80 | decodeAVCCFrame(nalu.avcc) 81 | } 82 | } 83 | } 84 | 85 | func decodeAVCCFrame(_ data: Data) { 86 | guard let formatDescription else { 87 | Self.logger.warning("No format description; need sync frame") 88 | return 89 | } 90 | var data = data 91 | data.withUnsafeMutableBytes { pointer in 92 | do { 93 | let dataBuffer = try CMBlockBuffer(buffer: pointer, allocator: kCFAllocatorNull) 94 | let sampleBuffer = try CMSampleBuffer( 95 | dataBuffer: dataBuffer, 96 | formatDescription: formatDescription, 97 | numSamples: 1, 98 | sampleTimings: [], 99 | sampleSizes: [] 100 | ) 101 | videoDecoder.decode(sampleBuffer) 102 | } catch { 103 | Self.logger.error("Failed to create sample buffer with error: \(error, privacy: .public)") 104 | } 105 | } 106 | } 107 | } 108 | 109 | // MARK: VideoDecoderAnnexBAdaptor.Codec 110 | 111 | public extension VideoDecoderAnnexBAdaptor { 112 | enum Codec { 113 | case h264 114 | case hevc 115 | } 116 | } 117 | 118 | // MARK: VideoDecoderAnnexBAdaptor.Error 119 | 120 | public extension VideoDecoderAnnexBAdaptor { 121 | enum Error: Swift.Error { 122 | case unsupportedCodec 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /TDS Video/VIews/webview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // webview.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 05/08/2024. 6 | // 7 | 8 | import SwiftUI 9 | import WebKit 10 | 11 | struct WebView: UIViewControllerRepresentable { 12 | func makeUIViewController(context: Context) -> CustomWebViewController { 13 | let webViewController = CustomWebViewController.shared 14 | CustomWebViewController.shared.IsIncar = false 15 | return webViewController 16 | } 17 | 18 | func updateUIViewController(_ uiViewController: CustomWebViewController, context: Context) {} 19 | } 20 | 21 | struct WebViewContainer: View { 22 | @State private var showCarButtons = false 23 | @State private var showURLInput = false 24 | @State private var userInputURL: String = "" 25 | 26 | var body: some View { 27 | ZStack { 28 | WebView() 29 | .ignoresSafeArea() 30 | 31 | // You could place overlay loading UI or status feedback here 32 | } 33 | .navigationTitle("Web Browser") 34 | .navigationBarTitleDisplayMode(.inline) 35 | .toolbar { 36 | ToolbarItemGroup(placement: .bottomBar) { 37 | Button { 38 | CustomWebViewController.shared.loadURL(URL(string: "https://google.com")!) 39 | } label: { 40 | Label("Google", systemImage: "globe") 41 | } 42 | 43 | Button { 44 | TDSVideoShared.shared.CarPlayComp?(.init(type: .web, URL: nil)) 45 | } label: { 46 | Label("To Car", systemImage: "car.fill") 47 | } 48 | 49 | Menu { 50 | Button("Control Page Buttons") { 51 | showCarButtons = true 52 | } 53 | Button("Enter URL") { 54 | showURLInput = true 55 | } 56 | } label: { 57 | Label("More", systemImage: "ellipsis.circle") 58 | } 59 | } 60 | } 61 | .sheet(isPresented: $showURLInput) { 62 | URLInputSheet(showURLInput: $showURLInput, userInputURL: $userInputURL) 63 | } 64 | .sheet(isPresented: $showCarButtons) { 65 | WebViewButtons() 66 | } 67 | } 68 | } 69 | 70 | struct URLInputSheet: View { 71 | @Binding var showURLInput: Bool 72 | @Binding var userInputURL: String 73 | @FocusState private var urlFieldFocused: Bool 74 | 75 | var body: some View { 76 | NavigationStack { 77 | Form { 78 | Section(header: Text("Custom URL")) { 79 | TextField("Enter full URL (https://...)", text: $userInputURL) 80 | .textInputAutocapitalization(.never) 81 | .keyboardType(.URL) 82 | .textFieldStyle(.roundedBorder) 83 | .focused($urlFieldFocused) 84 | 85 | Button("Load URL") { 86 | if let url = URL(string: userInputURL.trimmingCharacters(in: .whitespacesAndNewlines)) { 87 | CustomWebViewController.shared.loadURL(url) 88 | showURLInput = false 89 | } else { 90 | print("Invalid URL") 91 | } 92 | } 93 | .disabled(userInputURL.isEmpty) 94 | } 95 | 96 | Section(header: Text("Shared URL")) { 97 | if let shared = loadSharedURL() { 98 | Button("Load Shared URL: \(shared.absoluteString)") { 99 | CustomWebViewController.shared.loadURL(shared) 100 | showURLInput = false 101 | } 102 | } else { 103 | Text("No shared URL available") 104 | .foregroundColor(.secondary) 105 | } 106 | } 107 | 108 | Section { 109 | Button("Cancel", role: .cancel) { 110 | showURLInput = false 111 | } 112 | } 113 | } 114 | .navigationTitle("Enter Web URL") 115 | .onAppear { 116 | urlFieldFocused = true 117 | } 118 | } 119 | } 120 | 121 | func loadSharedURL() -> URL? { 122 | TDSVideoShared.shared.loadSharedURL() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /TDS Video.xcodeproj/xcshareddata/xcschemes/CarplayintentUI.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 46 | 47 | 50 | 56 | 57 | 58 | 61 | 67 | 68 | 69 | 70 | 71 | 83 | 85 | 91 | 92 | 93 | 94 | 102 | 104 | 110 | 111 | 112 | 113 | 115 | 116 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /TDS Video.xcodeproj/xcshareddata/xcschemes/Carplayintent.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 11 | 17 | 23 | 24 | 25 | 31 | 37 | 38 | 39 | 40 | 41 | 47 | 48 | 51 | 57 | 58 | 59 | 62 | 68 | 69 | 70 | 71 | 72 | 84 | 86 | 92 | 93 | 94 | 95 | 103 | 105 | 111 | 112 | 113 | 114 | 116 | 117 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /TDS Video/Transcoding/VideoEncoderAnnexBAdaptor.swift: -------------------------------------------------------------------------------- 1 | import CoreMedia 2 | import Foundation 3 | import OSLog 4 | 5 | public final class VideoEncoderAnnexBAdaptor { 6 | // MARK: Lifecycle 7 | 8 | public init(videoEncoder: VideoEncoder) { 9 | self.videoEncoder = videoEncoder 10 | conversionTask = Task { [weak self] in 11 | for await sampleBuffer in videoEncoder.encodedSampleBuffers { 12 | guard let self else { return } 13 | let sampleAttachments = CMSampleBufferGetSampleAttachmentsArray( 14 | sampleBuffer, 15 | createIfNecessary: false 16 | ) as? [[CFString: Any]] 17 | let notSync = sampleAttachments?.first?[kCMSampleAttachmentKey_NotSync] as? Bool ?? false 18 | 19 | var elementaryStream = Data() 20 | 21 | if !notSync { 22 | guard let formatDesciption = sampleBuffer.formatDescription else { 23 | Self.logger.error("Encoded sample buffer missing format description") 24 | continue 25 | } 26 | switch formatDesciption.mediaSubType { 27 | case .h264: 28 | guard formatDesciption.parameterSets.count > 1 else { 29 | Self.logger.error("Encoded sample buffer missing parameter set") 30 | continue 31 | } 32 | elementaryStream += H264NALU(data: formatDesciption.parameterSets[0]).annexB 33 | elementaryStream += H264NALU(data: formatDesciption.parameterSets[1]).annexB 34 | 35 | case .hevc: 36 | guard formatDesciption.parameterSets.count > 2 else { 37 | Self.logger.error("Encoded sample buffer missing parameter set") 38 | continue 39 | } 40 | elementaryStream += HEVCNALU(data: formatDesciption.parameterSets[0]).annexB 41 | elementaryStream += HEVCNALU(data: formatDesciption.parameterSets[1]).annexB 42 | elementaryStream += HEVCNALU(data: formatDesciption.parameterSets[2]).annexB 43 | default: 44 | Self.logger.error("Encoded sample buffer has unsupported media sub type") 45 | continue 46 | } 47 | } 48 | guard let dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else { 49 | print("CMSampleBufferGetDataBuffer returned nil") 50 | continue 51 | } 52 | var length: Int = 0 53 | var dataPointer: UnsafeMutablePointer? 54 | let status = CMBlockBufferGetDataPointer( 55 | dataBuffer, 56 | atOffset: 0, 57 | lengthAtOffsetOut: nil, 58 | totalLengthOut: &length, 59 | dataPointerOut: &dataPointer 60 | ) 61 | guard status == noErr, let dataPointer else { 62 | print("CMBlockBufferGetDataPointer failed with status: \(status)") 63 | continue 64 | } 65 | 66 | var offset = 0 67 | while offset < length { 68 | var naluLength: UInt32 = 0 69 | memcpy(&naluLength, dataPointer.advanced(by: offset), 4) 70 | offset += 4 71 | 72 | switch sampleBuffer.formatDescription?.mediaSubType { 73 | case .some(.h264): 74 | elementaryStream += H264NALU(data: Data( 75 | bytes: dataPointer.advanced(by: offset), 76 | count: Int(naluLength.bigEndian) 77 | )).annexB 78 | case .some(.hevc): 79 | elementaryStream += HEVCNALU(data: Data( 80 | bytes: dataPointer.advanced(by: offset), 81 | count: Int(naluLength.bigEndian) 82 | )).annexB 83 | default: 84 | break 85 | } 86 | 87 | offset += Int(naluLength.bigEndian) 88 | } 89 | 90 | for continuation in continuations.values { 91 | continuation.yield(elementaryStream) 92 | } 93 | } 94 | } 95 | } 96 | 97 | // MARK: Public 98 | 99 | public var annexBData: AsyncStream { 100 | .init { continuation in 101 | let id = UUID() 102 | continuations[id] = continuation 103 | continuation.onTermination = { [weak self] _ in 104 | self?.continuations[id] = nil 105 | } 106 | } 107 | } 108 | 109 | // MARK: Internal 110 | 111 | static let logger = Logger(subsystem: "Transcoding", category: "VideoEncoderAnnexBAdaptor") 112 | 113 | let videoEncoder: VideoEncoder 114 | var conversionTask: Task? 115 | 116 | var continuations: [UUID: AsyncStream.Continuation] = [:] 117 | } 118 | -------------------------------------------------------------------------------- /TDS Video/MDapiClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MDapiClass.swift 3 | // TDS McdonaldsApi 4 | // 5 | // Created by Thomas Dye on 02/08/2024. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import ObjectiveC.runtime 11 | import AVKit 12 | 13 | struct CarplayComClass { 14 | var type:typeview 15 | var URL:URL? 16 | var AVplayer:AVPlayer? 17 | var reloadWeb:Bool? 18 | // var 19 | 20 | 21 | 22 | enum typeview: String, Codable { 23 | case video = "video" 24 | case web = "web" 25 | case IOSAPP = "IOSAPP" 26 | case rawVideo = "rawVideo" 27 | 28 | } 29 | } 30 | 31 | 32 | @objc class TDSVideoShared: NSObject { 33 | @objc static let shared = TDSVideoShared() 34 | var CarPlayComp: ((CarplayComClass) -> Void)? 35 | 36 | // var URL:URL? 37 | var VideoPlayer:AVPlayer? 38 | var VideoPlayerForFile:AVPlayer? 39 | var VideoPlayerView:AVPlayerViewController? 40 | var window:UIWindow? 41 | var playerLayer: AVPlayerLayer! 42 | 43 | @objc func setPlayer(_ player: AVPlayerViewController) { 44 | // self.VideoPlayer = player 45 | self.VideoPlayerView = player 46 | 47 | player.player?.play() 48 | DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: { 49 | player.player?.pause() 50 | }) 51 | 52 | guard let playerView = player.view else { 53 | return 54 | } 55 | // print(player.player?.currentItem?.asset.url) 56 | 57 | // Find the AVPlayerLayer 58 | if let sublayers = playerView.layer.sublayers { 59 | for layer in sublayers { 60 | if let playerLayer = layer as? AVPlayerLayer { 61 | self.playerLayer = playerLayer 62 | break 63 | } 64 | } 65 | } 66 | 67 | // player.view.removeFromSuperview() 68 | let newPlayerView = UIView(frame: self.window!.frame) 69 | newPlayerView.backgroundColor = .black 70 | 71 | // Add the new UIView to your view hierarchy 72 | self.window!.addSubview(newPlayerView) 73 | 74 | // Set the frame of the AVPlayerLayer and add it to the new UIView 75 | playerLayer.frame = newPlayerView.bounds 76 | newPlayerView.layer.addSublayer(playerLayer) 77 | 78 | // // Play the video 79 | // player.play() 80 | 81 | 82 | 83 | // self.CarPlayComp?(CarplayComClass(type: .rawVideo, URL: nil, AVplayer: player)) 84 | } 85 | 86 | func loadSharedURL() -> URL? { 87 | let userDefaults = UserDefaults(suiteName: "group.net.thomasdye.TDS-docs") 88 | if let urlString = userDefaults?.string(forKey: "TDSSharedURL") { 89 | return URL(string: urlString) 90 | } 91 | return nil 92 | } 93 | 94 | // TDSSharedVideo 95 | 96 | func loadSharedURLVideo() -> String? { 97 | let userDefaults = UserDefaults(suiteName: "group.net.thomasdye.TDS-docs") 98 | if let urlString = userDefaults?.string(forKey: "TDSSharedVideo") { 99 | return urlString 100 | } 101 | return nil 102 | } 103 | 104 | @objc func SHowCustomONscreenPlayer(_ player:AVPlayer){ 105 | print("here") 106 | } 107 | 108 | 109 | 110 | 111 | 112 | } 113 | 114 | 115 | 116 | 117 | 118 | class ErrorHandling:NSObject { 119 | static let Shared = ErrorHandling() 120 | 121 | var MainVC:UIViewController? 122 | 123 | func showError(_ error:Error) { 124 | // Create an instance of UIAlertController 125 | let alert = UIAlertController(title: "There was an issue", message:"\(error)", preferredStyle: .alert) 126 | 127 | // Optionally you can add more actions 128 | let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (action) in 129 | print("Cancel button tapped") 130 | } 131 | alert.addAction(cancelAction) 132 | 133 | DispatchQueue.main.async { 134 | self.MainVC?.present(alert, animated: true, completion: nil) 135 | } 136 | } 137 | 138 | func showError(_ error:String) { 139 | // Create an instance of UIAlertController 140 | let alert = UIAlertController(title: "There was an issue", message:"\(error)", preferredStyle: .alert) 141 | 142 | 143 | // Optionally you can add more actions 144 | let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (action) in 145 | print("Cancel button tapped") 146 | } 147 | alert.addAction(cancelAction) 148 | DispatchQueue.main.async { 149 | self.MainVC?.present(alert, animated: true, completion: nil) 150 | } 151 | // Present the alert 152 | 153 | } 154 | } 155 | 156 | 157 | 158 | 159 | class ManagmentURLS { 160 | static let shared = ManagmentURLS() 161 | func fetchOrders() -> URL { 162 | return URL(string: "https://api.thomasdye.net/app/ThomasRandom/mcdonalds/backend/V1/Api/getOrders")! 163 | } 164 | // updload token mcdonalds/backend/V1/Api/ "uploadAccessToken" 165 | 166 | func UploadToken(_ token:String) -> URL { 167 | return URL(string: "https://api.thomasdye.net/app/ThomasRandom/mcdonalds/backend/V1/Api/\(token)")! 168 | } 169 | } 170 | 171 | 172 | -------------------------------------------------------------------------------- /TDS Video/Transcoding/VideoTranscoderError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import VideoToolbox 3 | 4 | public enum VideoTranscoderError: Error { 5 | case propertyNotSupported 6 | case propertyReadOnly 7 | case parameter 8 | case invalidSession 9 | case allocationFailed 10 | case pixelTransferNotSupported 11 | case couldNotFindVideoDecoder 12 | case couldNotCreateInstance 13 | case couldNotFindVideoEncoder 14 | case videoDecoderBadData 15 | case videoDecoderUnsupportedDataFormat 16 | case videoDecoderMalfunction 17 | case videoEncoderMalfunction 18 | case videoDecoderNotAvailableNow 19 | case imageRotationNotSupported 20 | case pixelRotationNotSupported 21 | case videoEncoderNotAvailableNow 22 | case formatDescriptionChangeNotSupported 23 | case insufficientSourceColorData 24 | case couldNotCreateColorCorrectionData 25 | case colorSyncTransformConvertFailed 26 | case videoDecoderAuthorization 27 | case videoEncoderAuthorization 28 | case colorCorrectionPixelTransferFailed 29 | case multiPassStorageIdentifierMismatch 30 | case multiPassStorageInvalid 31 | case frameSiloInvalidTimeStamp 32 | case frameSiloInvalidTimeRange 33 | case couldNotFindTemporalFilter 34 | case pixelTransferNotPermitted 35 | case colorCorrectionImageRotationFailed 36 | case videoDecoderRemoved 37 | case sessionMalfunction 38 | case videoDecoderNeedsRosetta 39 | case videoEncoderNeedsRosetta 40 | case videoDecoderReferenceMissing 41 | case videoDecoderCallbackMessaging 42 | case videoDecoderUnknown 43 | case extensionDisabled 44 | case videoEncoderMVHEVCVideoLayerIDsMismatch 45 | case couldNotOutputTaggedBufferGroup 46 | case unknown(OSStatus) 47 | 48 | // MARK: Lifecycle 49 | 50 | init?(status: OSStatus) { 51 | guard status != noErr else { return nil } 52 | switch status { 53 | case kVTPropertyNotSupportedErr: 54 | self = .propertyNotSupported 55 | case kVTPropertyReadOnlyErr: 56 | self = .propertyReadOnly 57 | case kVTParameterErr: 58 | self = .parameter 59 | case kVTInvalidSessionErr: 60 | self = .invalidSession 61 | case kVTAllocationFailedErr: 62 | self = .allocationFailed 63 | case kVTPixelTransferNotSupportedErr: 64 | self = .pixelTransferNotPermitted 65 | case kVTCouldNotFindVideoDecoderErr: 66 | self = .couldNotFindVideoDecoder 67 | case kVTCouldNotCreateInstanceErr: 68 | self = .couldNotCreateInstance 69 | case kVTCouldNotFindVideoEncoderErr: 70 | self = .couldNotFindVideoEncoder 71 | case kVTVideoDecoderBadDataErr: 72 | self = .videoDecoderBadData 73 | case kVTVideoDecoderUnsupportedDataFormatErr: 74 | self = .videoDecoderUnsupportedDataFormat 75 | case kVTVideoDecoderMalfunctionErr: 76 | self = .videoDecoderMalfunction 77 | case kVTVideoEncoderMalfunctionErr: 78 | self = .videoEncoderMalfunction 79 | case kVTVideoDecoderNotAvailableNowErr: 80 | self = .videoDecoderNotAvailableNow 81 | case kVTImageRotationNotSupportedErr: 82 | self = .imageRotationNotSupported 83 | case kVTPixelRotationNotSupportedErr: 84 | self = .pixelRotationNotSupported 85 | case kVTVideoEncoderNotAvailableNowErr: 86 | self = .videoEncoderNotAvailableNow 87 | case kVTFormatDescriptionChangeNotSupportedErr: 88 | self = .formatDescriptionChangeNotSupported 89 | case kVTInsufficientSourceColorDataErr: 90 | self = .insufficientSourceColorData 91 | case kVTCouldNotCreateColorCorrectionDataErr: 92 | self = .couldNotCreateColorCorrectionData 93 | case kVTColorSyncTransformConvertFailedErr: 94 | self = .colorSyncTransformConvertFailed 95 | case kVTVideoDecoderAuthorizationErr: 96 | self = .videoDecoderAuthorization 97 | case kVTVideoEncoderAuthorizationErr: 98 | self = .videoEncoderAuthorization 99 | case kVTColorCorrectionPixelTransferFailedErr: 100 | self = .colorCorrectionPixelTransferFailed 101 | case kVTMultiPassStorageIdentifierMismatchErr: 102 | self = .multiPassStorageIdentifierMismatch 103 | case kVTMultiPassStorageInvalidErr: 104 | self = .multiPassStorageInvalid 105 | case kVTFrameSiloInvalidTimeStampErr: 106 | self = .frameSiloInvalidTimeStamp 107 | case kVTFrameSiloInvalidTimeRangeErr: 108 | self = .frameSiloInvalidTimeRange 109 | case kVTCouldNotFindTemporalFilterErr: 110 | self = .couldNotFindTemporalFilter 111 | case kVTPixelTransferNotPermittedErr: 112 | self = .pixelTransferNotPermitted 113 | case kVTColorCorrectionImageRotationFailedErr: 114 | self = .colorCorrectionImageRotationFailed 115 | case kVTVideoDecoderRemovedErr: 116 | self = .videoDecoderRemoved 117 | case kVTSessionMalfunctionErr: 118 | self = .sessionMalfunction 119 | case kVTVideoDecoderNeedsRosettaErr: 120 | self = .videoDecoderNeedsRosetta 121 | case kVTVideoEncoderNeedsRosettaErr: 122 | self = .videoEncoderNeedsRosetta 123 | case kVTVideoDecoderReferenceMissingErr: 124 | self = .videoDecoderReferenceMissing 125 | case kVTVideoDecoderCallbackMessagingErr: 126 | self = .videoDecoderCallbackMessaging 127 | case kVTVideoDecoderUnknownErr: 128 | self = .videoDecoderUnknown 129 | case kVTExtensionDisabledErr: 130 | self = .extensionDisabled 131 | case kVTVideoEncoderMVHEVCVideoLayerIDsMismatchErr: 132 | self = .videoEncoderMVHEVCVideoLayerIDsMismatch 133 | case kVTCouldNotOutputTaggedBufferGroupErr: 134 | self = .couldNotOutputTaggedBufferGroup 135 | default: 136 | self = .unknown(status) 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /TDS Video/AVPlayerViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // AVPlayerViewController.m 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 06/08/2024. 6 | // 7 | 8 | #import "AVPlayerViewController+Swizzling.h" 9 | #import 10 | 11 | #import "AVPlayerViewController+Swizzling.h" 12 | #import 13 | //#import "TDS Video-Bridging-Header.h" 14 | 15 | //@implementation AVPlayer (Swizzling) 16 | // 17 | //+ (void)load { 18 | // static dispatch_once_t onceToken; 19 | // dispatch_once(&onceToken, ^{ 20 | // Class class = [self class]; 21 | // 22 | // // Swizzle the methods 23 | // [self swizzleMethod:class originalSelector:@selector(play) swizzledSelector:@selector(my_custom_play)]; 24 | // [self swizzleMethod:class originalSelector:@selector(pause) swizzledSelector:@selector(my_custom_pause)]; 25 | // }); 26 | //} 27 | // 28 | //+ (void)swizzleMethod:(Class)class originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector { 29 | // Method originalMethod = class_getInstanceMethod(class, originalSelector); 30 | // Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); 31 | // 32 | // BOOL didAddMethod = class_addMethod(class, 33 | // originalSelector, 34 | // method_getImplementation(swizzledMethod), 35 | // method_getTypeEncoding(swizzledMethod)); 36 | // 37 | // if (didAddMethod) { 38 | // class_replaceMethod(class, 39 | // swizzledSelector, 40 | // method_getImplementation(originalMethod), 41 | // method_getTypeEncoding(originalMethod)); 42 | // } else { 43 | // method_exchangeImplementations(originalMethod, swizzledMethod); 44 | // } 45 | //} 46 | // 47 | //// Custom play method 48 | //- (void)my_custom_play { 49 | // // Your custom code here 50 | // NSLog(@"AVPlayer play method swizzled!"); 51 | // 52 | // // Call the original play method (now swizzled) 53 | // [self my_custom_play]; 54 | //} 55 | // 56 | //// Custom pause method 57 | //- (void)my_custom_pause { 58 | // // Your custom code here 59 | // NSLog(@"AVPlayer pause method swizzled!"); 60 | // 61 | // // Call the original pause method (now swizzled) 62 | // [self my_custom_pause]; 63 | //} 64 | // 65 | //@end 66 | 67 | @implementation AVPlayerViewController (Swizzling) 68 | 69 | // Implement your custom methods 70 | - (void)my_custom_viewDidLoad { 71 | 72 | // self._volumeController = self 73 | // Perform any custom logic with the AVPlayer 74 | // NSLog(@"AVPlayer extracted: %@", player); 75 | NSLog(@"AVPlayerViewController viewDidLoad swizzled!"); 76 | 77 | // Call the original viewDidLoad method 78 | // [self my_custom_viewDidLoad]; 79 | } 80 | 81 | - (void)my_custom_viewDidAppear:(BOOL)animated { 82 | // Your custom code here 83 | NSLog(@"AVPlayerViewController viewDidAppear swizzled!"); 84 | 85 | 86 | 87 | // Call the original viewDidAppear method 88 | [self my_custom_viewDidAppear:animated]; 89 | 90 | // AVPlayerViewController *player = self; 91 | // [[TDSVideoShared shared] setPlayer:player]; 92 | 93 | // Run the player setup after a delay of 5 seconds 94 | // dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC); 95 | // dispatch_after(delay, dispatch_get_main_queue(), ^{ 96 | // AVPlayerViewController *player = self; 97 | // [[TDSVideoShared shared] setPlayer:player]; 98 | // }); 99 | } 100 | 101 | 102 | // Implement your custom init methods 103 | - (instancetype)my_custom_init { 104 | [super init]; 105 | if (self) { 106 | NSLog(@"AVPlayerViewController init swizzled!"); 107 | // Any custom initialization code here 108 | } 109 | return self; 110 | } 111 | 112 | - (void)my_custom_didMoveToParentViewController:(UIViewController *)parent { 113 | // Your custom code here 114 | NSLog(@"AVPlayerViewController didMoveToParentViewController swizzled!"); 115 | 116 | 117 | // Call the Swift method 118 | // self.player needs to be saved to TDSvideo.shared.AV 119 | // Call the Swift method 120 | 121 | 122 | // Call the original didMoveToParentViewController method 123 | 124 | } 125 | 126 | // Implement the swizzling method 127 | + (void)swizzleMethod:(Class)class originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector { 128 | Method originalMethod = class_getInstanceMethod(class, originalSelector); 129 | Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); 130 | 131 | BOOL didAddMethod = class_addMethod(class, 132 | originalSelector, 133 | method_getImplementation(swizzledMethod), 134 | method_getTypeEncoding(swizzledMethod)); 135 | 136 | if (didAddMethod) { 137 | class_replaceMethod(class, 138 | swizzledSelector, 139 | method_getImplementation(originalMethod), 140 | method_getTypeEncoding(originalMethod)); 141 | } else { 142 | method_exchangeImplementations(originalMethod, swizzledMethod); 143 | } 144 | } 145 | 146 | // Perform the swizzling in +load to ensure it happens early 147 | + (void)load { 148 | static dispatch_once_t onceToken; 149 | dispatch_once(&onceToken, ^{ 150 | Class class = [self class]; 151 | 152 | // // Swizzle the methods 153 | // [self swizzleMethod:class originalSelector:@selector(viewDidLoad) swizzledSelector:@selector(my_custom_viewDidLoad)]; 154 | [self swizzleMethod:class originalSelector:@selector(viewDidAppear:) swizzledSelector:@selector(my_custom_viewDidAppear:)]; 155 | // [self swizzleMethod:class originalSelector:@selector(init) swizzledSelector:@selector(my_custom_init)]; 156 | // [self swizzleMethod:class originalSelector:@selector(didMoveToParentViewController:) swizzledSelector:@selector(my_custom_didMoveToParentViewController:)]; 157 | }); 158 | } 159 | 160 | @end 161 | -------------------------------------------------------------------------------- /ScreenRec/SampleHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SampleHandler.swift 3 | // ScreenRec 4 | // 5 | // Created by Thomas Dye on 11/01/2025. 6 | // 7 | 8 | import ReplayKit 9 | import Network 10 | import VideoToolbox 11 | //import Transcoding 12 | 13 | import ReplayKit 14 | import Network 15 | import VideoToolbox 16 | import AVFoundation 17 | 18 | class SampleHandler: RPBroadcastSampleHandler { 19 | 20 | // MARK: - Network Connection to Main App 21 | public var connection: NWConnection? 22 | private let host = NWEndpoint.Host("127.0.0.1") 23 | private let port = NWEndpoint.Port(integerLiteral: 12345) 24 | 25 | 26 | let videoEncoderAnnexBAdaptor = VideoEncoderAnnexBAdaptor( 27 | videoEncoder: VideoEncoder(config: .ultraLowLatency) 28 | ) 29 | 30 | // MARK: - Broadcast Lifecycle 31 | override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) { 32 | print("broadcast started") 33 | // Optionally, create/Configure compression session if needed: 34 | // createCompressionSession(width: 720, height: 1280) 35 | 36 | // Establish the connection and start listening for Annex B data 37 | connect() 38 | } 39 | 40 | override func broadcastFinished() { 41 | print("broadcast finished") 42 | 43 | 44 | 45 | // Cancel the network connection 46 | connection?.cancel() 47 | } 48 | 49 | /// Sets up the NWConnection and handles automatic reconnect 50 | private func connect() { 51 | HTTPServer.shared.Start() 52 | videoEncoderAnnexBAdaptor.videoEncoder.invalidate() 53 | // Create a new NWConnection 54 | connection = NWConnection(host: host, port: port, using: .tcp) 55 | 56 | // Monitor connection state 57 | connection?.stateUpdateHandler = { [weak self] newState in 58 | guard let self = self else { return } 59 | switch newState { 60 | case .ready: 61 | print("Connection is ready") 62 | self.receiveMessage() 63 | case .failed(let error): 64 | print("Connection failed with error: \(error.localizedDescription)") 65 | self.handleConnectionError() 66 | case .waiting(let error): 67 | // .waiting often indicates network issues or transition; you can decide to retry here too. 68 | print("Connection is waiting with error: \(error.localizedDescription)") 69 | self.handleConnectionError() 70 | case .cancelled: 71 | print("Connection cancelled") 72 | default: 73 | break 74 | } 75 | } 76 | 77 | // Start the connection 78 | connection?.start(queue: .global(qos: .background)) 79 | 80 | // Create a Task that sends data from the annexBData AsyncSequence 81 | Task { 82 | for await data in videoEncoderAnnexBAdaptor.annexBData { 83 | print("Sending \(data.count) bytes") 84 | connection?.send(content: data, completion: .contentProcessed({ error in 85 | if let error = error { 86 | print("Send error:", error) 87 | } 88 | })) 89 | } 90 | } 91 | } 92 | 93 | private func receiveMessage() { 94 | guard let connection = connection else { return } 95 | 96 | connection.receive(minimumIncompleteLength: 2, maximumLength: 2048) { data, context, isComplete, error in 97 | if let data = data, !data.isEmpty { 98 | let message = String(data: data, encoding: .utf8) ?? "Unknown" 99 | 100 | print("Received message: \(message)") 101 | if message == "RECONNECT" { 102 | self.videoEncoderAnnexBAdaptor.videoEncoder.invalidate() 103 | } 104 | if message == "STOP" { 105 | let error = NSError( 106 | domain: "com.example.broadcast", 107 | code: 1001, 108 | userInfo: [NSLocalizedDescriptionKey: "STOPPED FROM APP"] 109 | ) 110 | self.finishBroadcastWithError(error) 111 | } 112 | 113 | 114 | } else if let error = error { 115 | print("Error receiving message: \(error)") 116 | } 117 | 118 | // Call receiveMessage() again to keep receiving data 119 | if isComplete == false { 120 | self.receiveMessage() 121 | } 122 | } 123 | } 124 | /// Handle connection errors by cancelling and attempting to reconnect 125 | private func handleConnectionError() { 126 | connection?.cancel() 127 | connection = nil 128 | 129 | // Simple retry after 2 seconds 130 | DispatchQueue.global().asyncAfter(deadline: .now() + 5) { [weak self] in 131 | print("Attempting to reconnect…") 132 | self?.connect() 133 | } 134 | } 135 | 136 | /// Called for each incoming video/audio sample from the entire device screen 137 | override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, 138 | with sampleBufferType: RPSampleBufferType) { 139 | switch sampleBufferType { 140 | case .video: 141 | // Encode the video frame to H.264 142 | videoEncoderAnnexBAdaptor.videoEncoder.encode(sampleBuffer) 143 | if HTTPServer.shared.isRunning { 144 | guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } 145 | let ciImage = CIImage(cvPixelBuffer: imageBuffer) 146 | let context = CIContext() 147 | 148 | if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) { 149 | HTTPServer.shared.send(image: cgImage) 150 | } 151 | } 152 | 153 | 154 | case .audioApp, .audioMic: 155 | // Handle audio encoding here if desired 156 | break 157 | @unknown default: 158 | break 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /TDS Video/Transcoding/VideoEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import OSLog 3 | import VideoToolbox 4 | #if canImport(UIKit) 5 | import UIKit 6 | #endif 7 | 8 | // MARK: - VideoEncoder 9 | 10 | public final class VideoEncoder { 11 | // MARK: Lifecycle 12 | 13 | public init(config: Config) { 14 | self.config = config 15 | 16 | #if canImport(UIKit) 17 | willEnterForegroundTask = Task { [weak self] in 18 | for await _ in await NotificationCenter.default.notifications( 19 | named: UIApplication.willEnterForegroundNotification 20 | ) { 21 | self?.invalidate() 22 | } 23 | } 24 | #endif 25 | } 26 | 27 | // MARK: Public 28 | 29 | public var config: Config { 30 | didSet { 31 | encodingQueue.sync { 32 | sessionInvalidated = true 33 | } 34 | } 35 | } 36 | 37 | public var encodedSampleBuffers: AsyncStream { 38 | .init { continuation in 39 | let id = UUID() 40 | continuations[id] = continuation 41 | continuation.onTermination = { [weak self] _ in 42 | self?.continuations[id] = nil 43 | } 44 | } 45 | } 46 | 47 | public func invalidate() { 48 | encodingQueue.sync { 49 | sessionInvalidated = true 50 | } 51 | } 52 | 53 | public func encode(_ sampleBuffer: CMSampleBuffer) { 54 | guard let imageBuffer = sampleBuffer.imageBuffer else { 55 | Self.logger.error("Invalid sample buffer passed to video encoder; missing imageBuffer") 56 | return 57 | } 58 | encode( 59 | imageBuffer, 60 | presentationTimeStamp: sampleBuffer.presentationTimeStamp, 61 | duration: sampleBuffer.duration 62 | ) 63 | } 64 | 65 | public func encode( 66 | _ pixelBuffer: CVPixelBuffer, 67 | presentationTimeStamp: CMTime = CMClockGetTime(.hostTimeClock), 68 | duration: CMTime = .invalid 69 | ) { 70 | encodingQueue.sync { 71 | let pixelBufferWidth = CGFloat(CVPixelBufferGetWidth(pixelBuffer)) 72 | let pixelBufferHeight = CGFloat(CVPixelBufferGetHeight(pixelBuffer)) 73 | if pixelBufferWidth != outputSize?.width || pixelBufferHeight != outputSize?.height { 74 | outputSize = CGSize(width: pixelBufferWidth, height: pixelBufferHeight) 75 | } 76 | 77 | if compressionSession == nil || sessionInvalidated { 78 | compressionSession = createCompressionSession() 79 | } 80 | 81 | guard let compressionSession else { 82 | Self.logger.error("No compression session") 83 | return 84 | } 85 | 86 | guard CVPixelBufferLockBaseAddress( 87 | pixelBuffer, 88 | CVPixelBufferLockFlags(rawValue: 0) 89 | ) == kCVReturnSuccess else { 90 | return 91 | } 92 | 93 | defer { 94 | CVPixelBufferUnlockBaseAddress( 95 | pixelBuffer, 96 | CVPixelBufferLockFlags(rawValue: 0) 97 | ) 98 | } 99 | 100 | do { 101 | try compressionSession.encodeFrame( 102 | pixelBuffer, 103 | presentationTimeStamp: presentationTimeStamp, 104 | duration: duration 105 | ) { [weak self] status, _, sampleBuffer in 106 | guard let self else { return } 107 | outputQueue.sync { 108 | do { 109 | if let error = VideoTranscoderError(status: status) { throw error } 110 | guard let sampleBuffer else { return } 111 | for continuation in self.continuations.values { 112 | continuation.yield(sampleBuffer) 113 | } 114 | } catch { 115 | Self.logger.error("Error in decode frame output handler: \(error, privacy: .public)") 116 | } 117 | } 118 | } 119 | } catch { 120 | Self.logger.error("Failed to encode frame with error: \(error, privacy: .public)") 121 | } 122 | } 123 | } 124 | 125 | // MARK: Internal 126 | 127 | static let logger = Logger(subsystem: "Transcoding", category: "VideoEncoder") 128 | 129 | var continuations: [UUID: AsyncStream.Continuation] = [:] 130 | 131 | var willEnterForegroundTask: Task? 132 | 133 | lazy var encodingQueue = DispatchQueue( 134 | label: String(describing: Self.self), 135 | qos: .userInitiated 136 | ) 137 | 138 | lazy var outputQueue = DispatchQueue( 139 | label: "\(String(describing: Self.self)).output", 140 | qos: .userInitiated 141 | ) 142 | 143 | var sessionInvalidated = false { 144 | didSet { 145 | dispatchPrecondition(condition: .onQueue(encodingQueue)) 146 | } 147 | } 148 | 149 | var compressionSession: VTCompressionSession? { 150 | didSet { 151 | dispatchPrecondition(condition: .onQueue(encodingQueue)) 152 | if let oldValue { VTCompressionSessionInvalidate(oldValue) } 153 | sessionInvalidated = false 154 | } 155 | } 156 | 157 | var outputSize: CGSize? { 158 | didSet { 159 | dispatchPrecondition(condition: .onQueue(encodingQueue)) 160 | sessionInvalidated = true 161 | } 162 | } 163 | 164 | func createCompressionSession() -> VTCompressionSession? { 165 | dispatchPrecondition(condition: .onQueue(encodingQueue)) 166 | do { 167 | let session = try VTCompressionSession.create( 168 | size: outputSize ?? CGSize(width: 1920, height: 1080), 169 | codecType: config.codecType, 170 | encoderSpecification: config.encoderSpecification 171 | ) 172 | config.apply(to: session) 173 | VTCompressionSessionPrepareToEncodeFrames(session) 174 | return session 175 | } catch { 176 | Self.logger.error("Failed to create compression session with error: \(error, privacy: .public)") 177 | return nil 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /TDS Video/Transcoding/VideoDecoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import OSLog 3 | import VideoToolbox 4 | #if canImport(UIKit) 5 | import UIKit 6 | #endif 7 | 8 | // MARK: - VideoDecoder 9 | 10 | public final class VideoDecoder { 11 | // MARK: Lifecycle 12 | 13 | public init(config: Config) { 14 | self.config = config 15 | 16 | #if canImport(UIKit) 17 | willEnterForegroundTask = Task { [weak self] in 18 | for await _ in await NotificationCenter.default.notifications( 19 | named: UIApplication.willEnterForegroundNotification 20 | ) { 21 | self?.invalidate() 22 | } 23 | } 24 | #endif 25 | } 26 | var ErrorFrameHandler: ((Error) -> Void)? 27 | // MARK: Public 28 | 29 | public var config: Config { 30 | didSet { 31 | decodingQueue.sync { 32 | sessionInvalidated = true 33 | } 34 | } 35 | } 36 | 37 | public var decodedSampleBuffers: AsyncStream { 38 | .init { continuation in 39 | let id = UUID() 40 | continuations[id] = continuation 41 | continuation.onTermination = { [weak self] _ in 42 | self?.continuations[id] = nil 43 | } 44 | } 45 | } 46 | 47 | public func invalidate() { 48 | decodingQueue.sync { 49 | sessionInvalidated = true 50 | } 51 | } 52 | 53 | public func setFormatDescription(_ formatDescription: CMFormatDescription) { 54 | decodingQueue.sync { 55 | self.formatDescription = formatDescription 56 | } 57 | } 58 | 59 | public func decode(_ sampleBuffer: CMSampleBuffer) { 60 | decodingQueue.sync { 61 | if decompressionSession == nil || sessionInvalidated { 62 | decompressionSession = createDecompressionSession() 63 | } 64 | 65 | guard let decompressionSession else { 66 | Self.logger.error("No decompression session") 67 | return 68 | } 69 | 70 | do { 71 | try decompressionSession.decodeFrame( 72 | sampleBuffer, 73 | flags: [._1xRealTimePlayback] 74 | ) { [weak self] status, _, imageBuffer, presentationTimeStamp, presentationDuration in 75 | guard let self else { return } 76 | outputQueue.sync { 77 | do { 78 | if let error = VideoTranscoderError(status: status) { throw error } 79 | guard let imageBuffer else { return } 80 | let formatDescription = try CMVideoFormatDescription(imageBuffer: imageBuffer) 81 | let sampleTiming = CMSampleTimingInfo( 82 | duration: presentationDuration, 83 | presentationTimeStamp: presentationTimeStamp, 84 | decodeTimeStamp: sampleBuffer.decodeTimeStamp 85 | ) 86 | let sampleBuffer = try CMSampleBuffer( 87 | imageBuffer: imageBuffer, 88 | formatDescription: formatDescription, 89 | sampleTiming: sampleTiming 90 | ) 91 | for continuation in self.continuations.values { 92 | continuation.yield(sampleBuffer) 93 | } 94 | } catch { 95 | self.ErrorFrameHandler?(error) 96 | Self.logger.error("Error in decode frame output handler: \(error, privacy: .public)") 97 | } 98 | } 99 | } 100 | } catch { 101 | Self.logger.error("Failed to decode frame with error: \(error, privacy: .public)") 102 | } 103 | } 104 | } 105 | 106 | // MARK: Internal 107 | 108 | static let logger = Logger(subsystem: "Transcoding", category: "VideoDecoder") 109 | 110 | var continuations: [UUID: AsyncStream.Continuation] = [:] 111 | 112 | var willEnterForegroundTask: Task? 113 | 114 | lazy var decodingQueue = DispatchQueue( 115 | label: String(describing: Self.self), 116 | qos: .userInitiated 117 | ) 118 | 119 | lazy var outputQueue = DispatchQueue( 120 | label: "\(String(describing: Self.self)).output", 121 | qos: .userInitiated 122 | ) 123 | 124 | var sessionInvalidated = false { 125 | didSet { 126 | dispatchPrecondition(condition: .onQueue(decodingQueue)) 127 | } 128 | } 129 | 130 | var formatDescription: CMFormatDescription? { 131 | didSet { 132 | dispatchPrecondition(condition: .onQueue(decodingQueue)) 133 | if let decompressionSession, 134 | let formatDescription, 135 | VTDecompressionSessionCanAcceptFormatDescription( 136 | decompressionSession, 137 | formatDescription: formatDescription 138 | ) { 139 | return 140 | } 141 | sessionInvalidated = true 142 | } 143 | } 144 | 145 | var decompressionSession: VTDecompressionSession? { 146 | didSet { 147 | dispatchPrecondition(condition: .onQueue(decodingQueue)) 148 | if let oldValue { VTDecompressionSessionInvalidate(oldValue) } 149 | sessionInvalidated = false 150 | } 151 | } 152 | 153 | func createDecompressionSession() -> VTDecompressionSession? { 154 | dispatchPrecondition(condition: .onQueue(decodingQueue)) 155 | do { 156 | guard let formatDescription else { 157 | Self.logger.error("Missing format description when creating decompression session") 158 | return nil 159 | } 160 | let session = try VTDecompressionSession.create( 161 | formatDescription: formatDescription, 162 | decoderSpecification: config.decoderSpecification, 163 | imageBufferAttributes: config.imageBufferAttributes 164 | ) 165 | config.apply(to: session) 166 | return session 167 | } catch { 168 | Self.logger.error("Failed to create decompression session with error: \(error, privacy: .public)") 169 | return nil 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /TDS Video/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // TDS McdonaldsApi 4 | // 5 | // Created by Thomas Dye on 02/08/2024. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | 11 | import Network 12 | import ReplayKit 13 | import AVFoundation 14 | 15 | 16 | 17 | 18 | class ViewController: UIViewController { 19 | 20 | override func viewDidLoad() { 21 | self.view.backgroundColor = .black 22 | 23 | Task { 24 | 25 | if TDSCarplayAccess.shared.ShowTDSCarPlaySettings == false { 26 | 27 | 28 | let hostingController = UIHostingController(rootView: TDSVideoMainScreen()) 29 | self.addChild(hostingController) 30 | self.view.addSubview(hostingController.view) 31 | hostingController.view.translatesAutoresizingMaskIntoConstraints = false 32 | NSLayoutConstraint.activate([ 33 | hostingController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), 34 | hostingController.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), 35 | hostingController.view.topAnchor.constraint(equalTo: self.view.topAnchor), 36 | hostingController.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor) 37 | ]) 38 | hostingController.didMove(toParent: self) 39 | 40 | return 41 | } 42 | 43 | ScreenCaptureManager.shared.start() 44 | // auth.APNSObject.RequestAPNS() 45 | 46 | print(UserDefaults.standard.bool(forKey: "CarIsRightHanded")) 47 | let tempDirectory = FileManager.default.temporaryDirectory 48 | TDSVideoAPI.shared.deleteOldFiles(from: tempDirectory, olderThan: 4) 49 | 50 | // TDSLocationAPI.shared.requestLocationPermission() 51 | DispatchQueue.global(qos: .background).async { 52 | // TDSLocationAPI.shared.startUpdatingLocation() 53 | } 54 | 55 | 56 | 57 | 58 | let hostingController = UIHostingController(rootView: MainView()) 59 | self.addChild(hostingController) 60 | self.view.addSubview(hostingController.view) 61 | hostingController.view.translatesAutoresizingMaskIntoConstraints = false 62 | NSLayoutConstraint.activate([ 63 | hostingController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), 64 | hostingController.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), 65 | hostingController.view.topAnchor.constraint(equalTo: self.view.topAnchor), 66 | hostingController.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor) 67 | ]) 68 | hostingController.didMove(toParent: self) 69 | await TDSVideoAPI.shared.DeviceBooted(VC: self) 70 | } 71 | 72 | // auth.Request_AccountCreate(viewController: self, comp: {res in 73 | // 74 | // 75 | // }) 76 | 77 | } 78 | 79 | 80 | 81 | 82 | // private func configureCaptureSession() { 83 | // // 1. Set session preset (e.g., high resolution) 84 | // captureSession.sessionPreset = .high 85 | // 86 | // // 2. Select default video device 87 | // guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, 88 | // for: .video, 89 | // position: .back) else { 90 | // print("Unable to access back camera!") 91 | // return 92 | // } 93 | // 94 | // // 3. Create input 95 | // do { 96 | // let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice) 97 | // if captureSession.canAddInput(videoDeviceInput) { 98 | // captureSession.addInput(videoDeviceInput) 99 | // } 100 | // } catch { 101 | // print("Error creating video device input: \(error.localizedDescription)") 102 | // return 103 | // } 104 | // 105 | // // 4. Configure output 106 | // videoOutput.videoSettings = [ 107 | // kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA 108 | // ] 109 | // videoOutput.alwaysDiscardsLateVideoFrames = true 110 | // 111 | // // 5. Set queue & delegate 112 | // let videoQueue = DispatchQueue(label: "camera.video.queue") 113 | // videoOutput.setSampleBufferDelegate(self, queue: videoQueue) 114 | // 115 | // // 6. Add output 116 | // if captureSession.canAddOutput(videoOutput) { 117 | // captureSession.addOutput(videoOutput) 118 | // } 119 | // 120 | // // (Optional) Adjust orientation if needed 121 | // guard let connection = videoOutput.connection(with: .video), 122 | // connection.isVideoOrientationSupported else { 123 | // return 124 | // } 125 | // connection.videoOrientation = .portrait 126 | // } 127 | 128 | } 129 | 130 | 131 | //extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate { 132 | // func captureOutput(_ output: AVCaptureOutput, 133 | // didOutput sampleBuffer: CMSampleBuffer, 134 | // from connection: AVCaptureConnection) { 135 | // self.captureOutput(sampleBuffer: sampleBuffer) 136 | // } 137 | //} 138 | func saveImageToDocumentsDirectory(image: UIImage) -> URL? { 139 | // Get the document directory URL 140 | guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { 141 | print("Error: Could not access document directory.") 142 | return nil 143 | } 144 | 145 | // Format the current date to create a unique file name 146 | let dateFormatter = DateFormatter() 147 | dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss" 148 | let fileName = dateFormatter.string(from: Date()) + ".png" 149 | 150 | // Create the file URL 151 | let fileURL = documentsDirectory.appendingPathComponent(fileName) 152 | 153 | // Convert the UIImage to PNG data 154 | guard let imageData = image.pngData() else { 155 | print("Error: Could not convert image to PNG data.") 156 | return nil 157 | } 158 | 159 | // Write the data to the file 160 | do { 161 | try imageData.write(to: fileURL) 162 | print("Image saved successfully to \(fileURL)") 163 | return fileURL 164 | } catch { 165 | print("Error saving image: \(error)") 166 | return nil 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /TDS Video/VIews/WebViewButtons.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebViewButtons.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 06/08/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct WebViewButtons: View { 11 | @State var buttonColour: Color = .purple 12 | @State var centerButtonColour: Color = .purple 13 | let Size: CGFloat = 300 14 | @Environment(\.colorScheme) var colorScheme 15 | 16 | var body: some View { 17 | ScrollView { 18 | VStack(spacing: 24) { 19 | Text("Cursor Navigation") 20 | .font(.headline) 21 | 22 | cursorControl 23 | 24 | Divider() 25 | 26 | Text("Scroll Content") 27 | .font(.headline) 28 | 29 | scrollControls 30 | 31 | Divider() 32 | 33 | Text("Resize Web Content") 34 | .font(.headline) 35 | 36 | resizeControls 37 | 38 | Divider() 39 | 40 | Text("Move Viewport") 41 | .font(.headline) 42 | 43 | viewportControls 44 | 45 | Divider() 46 | 47 | saveControls 48 | 49 | Divider() 50 | 51 | extraControls 52 | } 53 | .padding() 54 | } 55 | } 56 | 57 | // MARK: - Controls 58 | 59 | private var cursorControl: some View { 60 | VStack(spacing: 12) { 61 | HStack { 62 | Spacer() 63 | controlButton("chevron.up") { 64 | CustomWebViewController.shared.moveCursorUp(by: 10) 65 | } 66 | .simultaneousGesture(longPressGesture { 67 | CustomWebViewController.shared.moveCursorUp(by: 50) 68 | }) 69 | Spacer() 70 | } 71 | 72 | HStack { 73 | controlButton("chevron.left") { 74 | CustomWebViewController.shared.moveCursorLeft(by: 10) 75 | } 76 | .simultaneousGesture(longPressGesture { 77 | CustomWebViewController.shared.moveCursorLeft(by: 50) 78 | }) 79 | 80 | selectButton 81 | 82 | controlButton("chevron.right") { 83 | CustomWebViewController.shared.moveCursorRight(by: 10) 84 | } 85 | .simultaneousGesture(longPressGesture { 86 | CustomWebViewController.shared.moveCursorRight(by: 50) 87 | }) 88 | } 89 | 90 | HStack { 91 | Spacer() 92 | controlButton("chevron.down") { 93 | CustomWebViewController.shared.moveCursorDown(by: 10) 94 | } 95 | .simultaneousGesture(longPressGesture { 96 | CustomWebViewController.shared.moveCursorDown(by: 50) 97 | }) 98 | Spacer() 99 | } 100 | } 101 | } 102 | 103 | private var scrollControls: some View { 104 | HStack { 105 | controlButton("arrow.up.circle") { 106 | CustomWebViewController.shared.scrollBy(x: 0, y: -100) 107 | } 108 | controlButton("arrow.down.circle") { 109 | CustomWebViewController.shared.scrollBy(x: 0, y: 100) 110 | } 111 | } 112 | } 113 | 114 | private var resizeControls: some View { 115 | HStack { 116 | controlButton("plus.magnifyingglass") { 117 | CustomWebViewController.shared.resizeContent(by: 1.1) 118 | } 119 | controlButton("minus.magnifyingglass") { 120 | CustomWebViewController.shared.resizeContent(by: 0.9) 121 | } 122 | } 123 | } 124 | 125 | private var viewportControls: some View { 126 | VStack { 127 | HStack { 128 | controlButton("chevron.left") { 129 | CustomWebViewController.shared.moveHorizontally(by: -10) 130 | } 131 | controlButton("chevron.right") { 132 | CustomWebViewController.shared.moveHorizontally(by: 10) 133 | } 134 | } 135 | HStack { 136 | controlButton("chevron.up") { 137 | CustomWebViewController.shared.moveVertically(by: -10) 138 | } 139 | controlButton("chevron.down") { 140 | CustomWebViewController.shared.moveVertically(by: 10) 141 | } 142 | } 143 | } 144 | } 145 | 146 | private var saveControls: some View { 147 | Button("💾 Save current settings for domain") { 148 | CustomWebViewController.shared.saveViewSettings() 149 | } 150 | .padding(.vertical, 6) 151 | .frame(maxWidth: .infinity) 152 | .background(Color(.secondarySystemBackground)) 153 | .cornerRadius(8) 154 | } 155 | 156 | private var extraControls: some View { 157 | HStack { 158 | controlButton("magnifyingglass") { 159 | CustomWebViewController.shared.resetZoom() 160 | } 161 | 162 | controlButton("arrow.counterclockwise.circle") { 163 | CustomWebViewController.shared.reloadPage() 164 | } 165 | 166 | Button { 167 | CustomWebViewController.shared.toggleCursor() 168 | } label: { 169 | Image("Cursor") 170 | .resizable() 171 | .aspectRatio(contentMode: .fit) 172 | .frame(height: Size / 7) 173 | .foregroundColor(buttonColour) 174 | } 175 | .buttonStyle(.plain) 176 | } 177 | } 178 | 179 | // MARK: - Components 180 | 181 | private var selectButton: some View { 182 | Button(action: { 183 | CustomWebViewController.shared.select() 184 | }) { 185 | ZStack { 186 | Circle() 187 | .fill(centerButtonColour) 188 | .frame(width: Size / 3, height: Size / 3) 189 | .shadow(color: .gray, radius: 10) 190 | Text("Select") 191 | .foregroundColor(buttonColour) 192 | .bold() 193 | } 194 | } 195 | } 196 | 197 | private func controlButton(_ systemImage: String, action: @escaping () -> Void) -> some View { 198 | Button(action: action) { 199 | Image(systemName: systemImage) 200 | .resizable() 201 | .aspectRatio(contentMode: .fit) 202 | .frame(height: Size / 7) 203 | .foregroundColor(buttonColour) 204 | } 205 | .buttonStyle(.plain) 206 | } 207 | 208 | private func longPressGesture(action: @escaping () -> Void) -> some Gesture { 209 | LongPressGesture(minimumDuration: 0.5) 210 | .onEnded { _ in action() } 211 | } 212 | } 213 | 214 | #Preview { 215 | WebViewButtons() 216 | } 217 | -------------------------------------------------------------------------------- /CustomVideoPlayerViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomVideoPlayerViewController.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 05/08/2024. 6 | // 7 | 8 | import UIKit 9 | import AVFoundation 10 | import MediaPlayer 11 | 12 | 13 | class CustomVideoPlayerViewController: UIViewController { 14 | // var player: AVPlayer? 15 | var playerLayer: AVPlayerLayer? 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | view.backgroundColor = .black 21 | 22 | setupRemoteCommandCenter() 23 | } 24 | 25 | func setupPlayer(url: URL) { 26 | TDSVideoShared.shared.VideoPlayerForFile = AVPlayer(url: url) 27 | playerLayer = AVPlayerLayer(player: TDSVideoShared.shared.VideoPlayerForFile ) 28 | guard let playerLayer = playerLayer else { return } 29 | 30 | playerLayer.frame = view.bounds 31 | playerLayer.videoGravity = .resizeAspect 32 | view.layer.insertSublayer(playerLayer, at: 0) 33 | 34 | // Set up Now Playing Info 35 | setupNowPlayingInfo() 36 | } 37 | 38 | func setupPlayer(player: AVPlayer) { 39 | TDSVideoShared.shared.VideoPlayerForFile = player 40 | playerLayer = AVPlayerLayer(player: TDSVideoShared.shared.VideoPlayerForFile ) 41 | guard let playerLayer = playerLayer else { return } 42 | 43 | playerLayer.frame = view.bounds 44 | playerLayer.videoGravity = .resize 45 | view.layer.insertSublayer(playerLayer, at: 0) 46 | 47 | // Set up Now Playing Info 48 | setupNowPlayingInfo() 49 | } 50 | 51 | func setupPlayerlayer(playerLayer: AVPlayerLayer) { 52 | 53 | self.playerLayer = playerLayer 54 | 55 | playerLayer.frame = view.bounds 56 | playerLayer.videoGravity = .resize 57 | view.layer.insertSublayer(playerLayer, at: 0) 58 | 59 | // Set up Now Playing Info 60 | setupNowPlayingInfo() 61 | // setupRemoteTransportControls() 62 | } 63 | 64 | override func viewDidAppear(_ animated: Bool) { 65 | super.viewDidAppear(animated) 66 | TDSVideoShared.shared.VideoPlayerForFile?.play() 67 | updateNowPlayingInfo() 68 | } 69 | 70 | override func viewWillDisappear(_ animated: Bool) { 71 | super.viewWillDisappear(animated) 72 | TDSVideoShared.shared.VideoPlayerForFile?.pause() 73 | } 74 | 75 | func setupRemoteCommandCenter() { 76 | let commandCenter = MPRemoteCommandCenter.shared() 77 | 78 | commandCenter.playCommand.addTarget { [unowned self] event in 79 | if TDSVideoShared.shared.VideoPlayerForFile?.rate == 0.0 { 80 | TDSVideoShared.shared.VideoPlayerForFile?.play() 81 | self.updateNowPlayingInfo() 82 | return .success 83 | } 84 | return .commandFailed 85 | } 86 | 87 | commandCenter.pauseCommand.addTarget { [unowned self] event in 88 | if TDSVideoShared.shared.VideoPlayerForFile?.rate != 0.0 { 89 | TDSVideoShared.shared.VideoPlayerForFile?.pause() 90 | self.updateNowPlayingInfo() 91 | return .success 92 | } 93 | return .commandFailed 94 | } 95 | 96 | commandCenter.togglePlayPauseCommand.addTarget { [unowned self] event in 97 | if TDSVideoShared.shared.VideoPlayerForFile?.rate == 0.0 { 98 | TDSVideoShared.shared.VideoPlayerForFile?.play() 99 | } else { 100 | TDSVideoShared.shared.VideoPlayerForFile?.pause() 101 | } 102 | self.updateNowPlayingInfo() 103 | return .success 104 | } 105 | 106 | commandCenter.skipForwardCommand.addTarget { [unowned self] event in 107 | self.skipForward() 108 | self.updateNowPlayingInfo() 109 | return .success 110 | } 111 | 112 | commandCenter.skipBackwardCommand.addTarget { [unowned self] event in 113 | self.skipBackward() 114 | self.updateNowPlayingInfo() 115 | return .success 116 | } 117 | 118 | commandCenter.skipForwardCommand.preferredIntervals = [15] // Skip forward 15 seconds 119 | commandCenter.skipBackwardCommand.preferredIntervals = [15] // Skip backward 15 seconds 120 | 121 | commandCenter.changePlaybackPositionCommand.isEnabled = true 122 | commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in 123 | guard 124 | let self = self, 125 | let player = TDSVideoShared.shared.VideoPlayerForFile, 126 | let positionEvent = event as? MPChangePlaybackPositionCommandEvent 127 | else { return .commandFailed } 128 | 129 | let newTime = CMTimeMakeWithSeconds(positionEvent.positionTime, preferredTimescale: 1) 130 | player.seek(to: newTime) { _ in 131 | self.updateNowPlayingInfo() 132 | } 133 | return .success 134 | } 135 | } 136 | 137 | func setupNowPlayingInfo() { 138 | guard let currentItem = TDSVideoShared.shared.VideoPlayerForFile?.currentItem else { return } 139 | 140 | var nowPlayingInfo = [String: Any]() 141 | 142 | // Set title & artist (shows up in Control Center) 143 | nowPlayingInfo[MPMediaItemPropertyTitle] = "TDS Video In Car Player" 144 | nowPlayingInfo[MPMediaItemPropertyArtist] = "" 145 | 146 | // Duration 147 | let durationInSeconds = CMTimeGetSeconds(currentItem.asset.duration) 148 | nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = durationInSeconds 149 | 150 | // Current playback time & rate 151 | let currentTimeInSeconds = CMTimeGetSeconds( TDSVideoShared.shared.VideoPlayerForFile?.currentTime() ?? .zero) 152 | nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTimeInSeconds 153 | // Rate of 1.0 = normal speed, 0.0 = paused 154 | nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = TDSVideoShared.shared.VideoPlayerForFile?.rate ?? 0.0 155 | 156 | MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo 157 | } 158 | 159 | 160 | 161 | 162 | func updateNowPlayingInfo() { 163 | let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default() 164 | var nowPlayingInfo = nowPlayingInfoCenter.nowPlayingInfo ?? [String: Any]() 165 | 166 | if let player = TDSVideoShared.shared.VideoPlayerForFile, let currentItem = player.currentItem { 167 | nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = CMTimeGetSeconds(player.currentTime()) 168 | nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate 169 | } 170 | 171 | nowPlayingInfoCenter.nowPlayingInfo = nowPlayingInfo 172 | } 173 | 174 | func skipForward() { 175 | guard let player = TDSVideoShared.shared.VideoPlayerForFile , let currentItem = player.currentItem else { return } 176 | let currentTime = CMTimeGetSeconds(player.currentTime()) 177 | let newTime = currentTime + 15.0 178 | if newTime < CMTimeGetSeconds(currentItem.duration) { 179 | let time = CMTimeMakeWithSeconds(newTime, preferredTimescale: currentItem.asset.duration.timescale) 180 | player.seek(to: time) 181 | } 182 | } 183 | 184 | func skipBackward() { 185 | guard let player = TDSVideoShared.shared.VideoPlayerForFile else { return } 186 | let currentTime = CMTimeGetSeconds(player.currentTime()) 187 | let newTime = max(currentTime - 15.0, 0) 188 | let time = CMTimeMakeWithSeconds(newTime, preferredTimescale: player.currentItem?.asset.duration.timescale ?? 1) 189 | player.seek(to: time) 190 | } 191 | 192 | 193 | 194 | } 195 | -------------------------------------------------------------------------------- /TDS Video/VIews/MainView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainView.swift 3 | // TDS McdonaldsApi 4 | // 5 | // Created by Thomas Dye on 02/08/2024. 6 | // 7 | 8 | import SwiftUI 9 | import ReplayKit 10 | 11 | struct MainView: View { 12 | @State private var accessToken: String = "" 13 | // @State private var authStatus = CLLocationManager.authorizationStatus() 14 | @ObservedObject var videoAPI = TDSVideoAPI.shared 15 | @State private var showingCodeAlert = false 16 | @State private var connectionCode = "" 17 | @State private var showRebootAlert = false 18 | @State var isStationary = false 19 | @StateObject private var locationAPI = TDSLocationAPI.shared 20 | var body: some View { 21 | NavigationStack { 22 | List { 23 | // location access 24 | // Section(header: Text("Safety")) { 25 | // // 1) Permission flow 26 | // switch authStatus { 27 | // case .notDetermined: 28 | // Button("Allow Location Access") { 29 | // locationAPI.requestLocationPermission() 30 | // } 31 | // 32 | // case .restricted, .denied: 33 | // Text("Location access denied. Please enable in Settings.") 34 | // .foregroundColor(.red) 35 | // 36 | // case .authorizedWhenInUse, .authorizedAlways: 37 | // // 2) Permission ok, so we can check stationary 38 | // Button(action: { 39 | // locationAPI.startUpdatingLocation() 40 | // }) { 41 | // HStack { 42 | // Image(systemName: locationAPI.isStationary ? "checkmark.circle" : "location") 43 | // Text(locationAPI.isStationary ? "Stationary ✓" : "Check Stationary Status") 44 | // } 45 | // } 46 | // .disabled(locationAPI.isStationary) // once stationary, you can’t press again 47 | // 48 | // // 3) Status text 49 | // Text(locationAPI.isStationary 50 | // ? "You’re stationary. Safety Mode enabled." 51 | // : "Remain still to enable Safety Mode.") 52 | // .font(.subheadline) 53 | // .foregroundColor(.secondary) 54 | // 55 | // @unknown default: 56 | // Text("Unknown authorization status.") 57 | // } 58 | // } 59 | 60 | 61 | Section(header: Text("Getting Started")) { 62 | NavigationLink(destination: Help()) { 63 | Label("Help", systemImage: "questionmark.circle") 64 | } 65 | 66 | Button(action: openYouTubeHelp) { 67 | Label("Watch Help Video", systemImage: "play.rectangle.fill") 68 | .foregroundColor(.blue) 69 | .bold() 70 | } 71 | } 72 | 73 | Section(header: Text("Screen Mirroring & Web")) { 74 | NavigationLink(destination: ScreenMirroingSettings()) { 75 | Label("Screen Mirroring Settings", systemImage: "rectangle.on.rectangle") 76 | } 77 | NavigationLink(destination: ScreenMirroringView()) { 78 | Label("View Screen Mirroring", systemImage: "rectangle.on.rectangle") 79 | } 80 | 81 | NavigationLink(destination: WebViewContainer()) { 82 | Label("Open Web Browser", systemImage: "safari") 83 | } 84 | NavigationLink(destination: WebServerPage()) { 85 | Label("HTTP server", systemImage: "safari") 86 | } 87 | 88 | // NavigationLink(destination: WebViewContainer2()) { 89 | // Label("DRM Web Content", systemImage: "lock.shield") 90 | // } 91 | 92 | Button(action: { 93 | TDSVideoShared.shared.CarPlayComp?(.init(type: .web, URL: nil)) 94 | }) { 95 | Label("Load Web in Car", systemImage: "car.fill") 96 | } 97 | 98 | NavigationLink(destination: WebViewButtons()) { 99 | Label("Web Control Buttons", systemImage: "cursorarrow.rays") 100 | } 101 | 102 | NavigationLink(destination: SingleVideoPicker()) { 103 | Label("Stream Video Files", systemImage: "film.stack") 104 | } 105 | } 106 | // .disabled(!locationAPI.isStationary) 107 | 108 | Section(footer: 109 | VStack(alignment: .leading, spacing: 4) { 110 | Text("I hope you are enjoying this app! Consider supporting future development.") 111 | .font(.caption) 112 | 113 | Button(action: openCoffeeDonation) { 114 | Label("Buy me a coffee", systemImage: "cup.and.saucer.fill") 115 | .foregroundColor(.orange) 116 | } 117 | 118 | Button(action: openGitHubRepo) { 119 | Label("GitHub: Feature & Bug Reports", systemImage: "chevron.left.forwardslash.chevron.right") 120 | .foregroundColor(.purple) 121 | } 122 | 123 | Text("© 2025 Thomas Dye. All rights reserved.") 124 | .font(.caption2) 125 | .foregroundColor(.secondary) 126 | .padding(.top, 6) 127 | } 128 | .padding(.vertical, 4) 129 | ) { 130 | EmptyView() 131 | } 132 | } 133 | .navigationTitle("TDS CarPlay Tools") 134 | // keep authStatus up to date when app comes back to foreground 135 | // .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in 136 | // authStatus = CLLocationManager.authorizationStatus() 137 | // } 138 | .toolbar { 139 | ToolbarItem(placement: .navigationBarLeading) { 140 | Button("Enter Code") { 141 | showingCodeAlert = true 142 | } 143 | } 144 | } 145 | .alert("Enter Connection Code", isPresented: $showingCodeAlert, actions: { 146 | TextField("Connection Code", text: $connectionCode) 147 | Button("OK") { 148 | if connectionCode.lowercased() == "carplay" { 149 | // Trigger reboot instruction 150 | TDSCarplayAccess.shared.DisableIsStationary = true 151 | showRebootAlert = true 152 | } 153 | connectionCode = "" 154 | } 155 | Button("Cancel", role: .cancel) { 156 | connectionCode = "" 157 | } 158 | }, message: { 159 | Text("Enter the connection code to proceed.") 160 | }) 161 | .alert("Reboot Required", isPresented: $showRebootAlert) { 162 | Button("OK", role: .cancel) {} 163 | } message: { 164 | Text("CarPlay mode is now enabled. Please close and reopen the app for changes to take effect.") 165 | } 166 | } 167 | } 168 | 169 | // MARK: - Actions 170 | 171 | func openYouTubeHelp() { 172 | openURL("https://youtu.be/gI3Tj2KP290") 173 | } 174 | 175 | func openCoffeeDonation() { 176 | openURL("https://buymeacoffee.com/Thomadye") 177 | } 178 | 179 | func openGitHubRepo() { 180 | openURL("https://github.com/thomasdye12/TDS-Carplay") 181 | } 182 | 183 | func openURL(_ urlString: String) { 184 | if let url = URL(string: urlString) { 185 | UIApplication.shared.open(url) 186 | } 187 | } 188 | 189 | 190 | } 191 | 192 | 193 | #Preview { 194 | MainView() 195 | } 196 | -------------------------------------------------------------------------------- /TDS Video/HTTPServer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPServer.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 16/04/2025. 6 | // 7 | 8 | import Swifter 9 | import UIKit 10 | 11 | 12 | 13 | //
14 | // 15 | // 16 | // 17 | //
18 | class HTTPServer { 19 | 20 | static var shared = HTTPServer() 21 | var isRunning = false 22 | 23 | let server = HttpServer() 24 | 25 | init () { 26 | 27 | } 28 | 29 | let html = """ 30 | 31 | 32 | 60 | 61 | 62 | 63 | 64 | 135 | 136 | 137 | 138 | """ 139 | 140 | 141 | 142 | func Start(){ 143 | server["/"] = { _ in 144 | return HttpResponse.ok(.html(self.html)) 145 | } 146 | 147 | 148 | server["/frame"] = { request in 149 | guard let image = self.currentFrame else { 150 | return HttpResponse.notFound 151 | } 152 | 153 | let uiImage = UIImage(cgImage: image) 154 | guard let imageData = uiImage.jpegData(compressionQuality: 0.3) else { 155 | return HttpResponse.internalServerError 156 | } 157 | 158 | return HttpResponse.raw(200, "OK", ["Content-Type": "image/jpeg"]) { writer in 159 | try writer.write(imageData) 160 | } 161 | } 162 | 163 | 164 | server["/mjpeg"] = { [weak self] request in 165 | return HttpResponse.raw(200, "OK", [ 166 | "Content-Type": "multipart/x-mixed-replace; boundary=frame", 167 | "Cache-Control": "no-cache", 168 | "Connection": "close" 169 | ]) { writer in 170 | guard let self = self else { return } 171 | 172 | while self.isRunning { 173 | if let image = self.currentFrame { 174 | let uiImage = UIImage(cgImage: image) 175 | if let jpegData = uiImage.jpegData(compressionQuality: 0.7) { 176 | let part = """ 177 | --frame\r 178 | Content-Type: image/jpeg\r 179 | Content-Length: \(jpegData.count)\r 180 | \r 181 | """.data(using: .utf8)! 182 | 183 | try writer.write(part) 184 | try writer.write(jpegData) 185 | try writer.write("\r\n".data(using: .utf8)!) 186 | } 187 | } 188 | 189 | Thread.sleep(forTimeInterval: 0.033) // ~30 FPS (adjust as needed) 190 | } 191 | } 192 | } 193 | 194 | 195 | do { 196 | isRunning = true 197 | try server.start(8080, forceIPv4: true) 198 | print("Server running at :8080") 199 | print(getAllIPAddresses()) 200 | } catch { 201 | isRunning = false 202 | print("Server failed to start: \(error)") 203 | } 204 | 205 | } 206 | 207 | func Stop(){ 208 | server.stop() 209 | isRunning = false 210 | } 211 | 212 | func getAllIPAddresses() -> [String] { 213 | var addresses: [String] = [] 214 | 215 | var ifaddr: UnsafeMutablePointer? = nil 216 | if getifaddrs(&ifaddr) == 0 { 217 | var ptr = ifaddr 218 | while ptr != nil { 219 | defer { ptr = ptr?.pointee.ifa_next } 220 | 221 | guard let interface = ptr?.pointee else { continue } 222 | let addrFamily = interface.ifa_addr.pointee.sa_family 223 | 224 | if addrFamily == UInt8(AF_INET) { 225 | let name = String(cString: interface.ifa_name) 226 | if name != "lo0" { 227 | var addr = interface.ifa_addr.pointee 228 | var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST)) 229 | 230 | if getnameinfo(&addr, 231 | socklen_t(interface.ifa_addr.pointee.sa_len), 232 | &hostname, 233 | socklen_t(hostname.count), 234 | nil, 235 | socklen_t(0), 236 | NI_NUMERICHOST) == 0 { 237 | let ip = String(cString: hostname) 238 | addresses.append("\(name): \(ip)") 239 | } 240 | } 241 | } 242 | } 243 | freeifaddrs(ifaddr) 244 | } 245 | 246 | return addresses 247 | } 248 | 249 | 250 | 251 | var currentFrame: CGImage? 252 | 253 | func send(image: CGImage) { 254 | currentFrame = image 255 | } 256 | 257 | 258 | } 259 | -------------------------------------------------------------------------------- /TDS Video/VIews/paymentscreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // paymentscreen.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 18/03/2025. 6 | // 7 | import SwiftUI 8 | import UIKit 9 | //struct SupportScreen: View { 10 | // var AppOpenAmount:Int 11 | // var body: some View { 12 | // VStack(spacing: 20) { 13 | // Image("test1") 14 | // .resizable() 15 | // .aspectRatio(contentMode: .fit) 16 | // .cornerRadius(10) 17 | // .padding() 18 | // .frame(width: 150) 19 | // 20 | // Text("Support the App") 21 | // .font(.largeTitle) 22 | // .bold() 23 | // .padding(.top) 24 | // 25 | // Text("I hope you're enjoying the app! We've noticed you've opened it \(AppOpenAmount) times—awesome! If you find it useful, consider making a contribution to help support its development. Your support keeps the app running smoothly and helps us bring new updates!") 26 | // .font(.body) 27 | // .multilineTextAlignment(.center) 28 | // .padding(.horizontal) 29 | // 30 | // Button(action: { 31 | // if let url = URL(string: "https://www.buymeacoffee.com/Thomadye") { 32 | // UIApplication.shared.open(url) 33 | // print("Buy Me a Coffee link pressed") 34 | // TDSVideoAPI.shared.BuyMeACoffeePressedFromPayment() 35 | // } 36 | // }) { 37 | // HStack { 38 | // Image(systemName: "cup.and.saucer.fill") 39 | // .font(.title) 40 | // Text("Buy Me a Coffee") 41 | // .font(.headline) 42 | // } 43 | // .foregroundColor(.white) 44 | // .padding() 45 | // .frame(maxWidth: .infinity) 46 | // .background(Color.orange) 47 | // .cornerRadius(10) 48 | // .padding(.horizontal) 49 | // } 50 | // Button(action: { 51 | // TDSVideoAPI.shared.sendEmail() 52 | // }) { 53 | // HStack { 54 | // Image(systemName: "envelope.fill") 55 | // .font(.title) 56 | // Text("Got a bug or issue? Send an email!") 57 | // .font(.headline) 58 | // } 59 | // .foregroundColor(.white) 60 | // .padding() 61 | // .frame(maxWidth: .infinity) 62 | // .background(Color.blue) 63 | // .cornerRadius(10) 64 | // .padding(.horizontal) 65 | // } 66 | // Button(action: { 67 | // TDSVideoAPI.shared.HidebyuymeACoffeePressed() 68 | // }) { 69 | // HStack { 70 | //// Image(systemName: "envelope.fill") 71 | //// .font(.title) 72 | // Text("Already Donated / Hide ") 73 | // .font(.headline) 74 | // } 75 | // .foregroundColor(.white) 76 | // .padding() 77 | // .frame(maxWidth: .infinity) 78 | // .background(Color.gray) 79 | // .cornerRadius(10) 80 | // .padding(.horizontal) 81 | // } 82 | // Text("Created by Thomas Dye, Copyright © 2025 Thomas Dye. All rights reserved.") 83 | // .font(.caption2) 84 | // .foregroundColor(.secondary) 85 | // 86 | // 87 | // } 88 | // .padding() 89 | // } 90 | //} 91 | 92 | import SwiftUI 93 | 94 | struct SupportScreen: View { 95 | var AppOpenAmount: Int 96 | 97 | var body: some View { 98 | VStack(spacing: 20) { 99 | Image("test1") 100 | .resizable() 101 | .aspectRatio(contentMode: .fit) 102 | .cornerRadius(10) 103 | // .padding(.top) 104 | .frame(width: 100) 105 | 106 | Text("Supporting the App") 107 | .font(.largeTitle) 108 | .bold() 109 | 110 | // Display app usage and downloads 111 | Text("Thank you for downloading my App. I Am sure you have heard about it from somewhere. To be able to keep up with demand and provide the best possible experience for you. I have had to make the app a paid experience. I am Sorry for that as I am sure you are not happy about it, however I will make sure to keep the app up to date and you can always contact me with an Issue.") 112 | .font(.body) 113 | .multilineTextAlignment(.center) 114 | // .padding(.horizontal) 115 | 116 | Button(action: { 117 | // TODO: Replace with your actual YouTube channel URL 118 | if let url = URL(string: "https://payments.thomasdye.net/CP/b82b9c80-b318-47fb-9b2e-b4857cffe42a/?deviceID=\(UIDevice.current.identifierForVendor?.uuidString ?? "SOMEIDNOTKNOW")") { 119 | UIApplication.shared.open(url) 120 | print("ONE TIME - Payment link link pressed") 121 | } 122 | }) { 123 | HStack { 124 | Image(systemName: "link") 125 | .font(.title) 126 | Text("Pay To use £10") 127 | .font(.headline) 128 | } 129 | .foregroundColor(.white) 130 | .padding() 131 | .frame(maxWidth: .infinity) 132 | .background(Color.red) 133 | .cornerRadius(10) 134 | .padding(.horizontal) 135 | } 136 | 137 | Text("To see what the app can do you can watch my video on youtube where I show you about it.") 138 | .font(.caption) 139 | .multilineTextAlignment(.center) 140 | .padding(.horizontal) 141 | Button(action: { 142 | // TODO: Replace with your actual YouTube channel URL 143 | if let url = URL(string: "https://youtu.be/gI3Tj2KP290") { 144 | UIApplication.shared.open(url) 145 | print("YouTube Subscribe link pressed") 146 | // TDSVideoAPI.shared.HidebyuymeACoffeePressed() 147 | } 148 | }) { 149 | HStack { 150 | Image(systemName: "play.rectangle.fill") 151 | .font(.title) 152 | Text("Watch on YouTube") 153 | .font(.headline) 154 | } 155 | .foregroundColor(.white) 156 | .padding() 157 | .frame(maxWidth: .infinity) 158 | .background(Color.red) 159 | .cornerRadius(10) 160 | .padding(.horizontal) 161 | } 162 | 163 | // Report bugs 164 | Button(action: { 165 | TDSVideoAPI.shared.sendEmail() 166 | }) { 167 | HStack { 168 | Image(systemName: "envelope.fill") 169 | .font(.title) 170 | Text("Got a bug or issue? Send an email!") 171 | .font(.headline) 172 | } 173 | .foregroundColor(.white) 174 | .padding() 175 | .frame(maxWidth: .infinity) 176 | .background(Color.blue) 177 | .cornerRadius(10) 178 | .padding(.horizontal) 179 | } 180 | Spacer() 181 | 182 | // // Hide view 183 | // Button(action: { 184 | // TDSVideoAPI.shared.HidebyuymeACoffeePressed() 185 | // }) { 186 | // Text("Already Donated / Hide") 187 | // .font(.caption2) 188 | // .foregroundColor(.white) 189 | // .padding(2) 190 | // .frame(maxWidth: .infinity) 191 | // .background(Color.gray) 192 | // .cornerRadius(10) 193 | // .padding(.horizontal) 194 | // } 195 | 196 | // Footer 197 | Text("Created by Thomas Dye. © 2025 Thomas Dye. All rights reserved.") 198 | .font(.caption2) 199 | .foregroundColor(.secondary) 200 | } 201 | .padding() 202 | } 203 | } 204 | 205 | #Preview { 206 | SupportScreen(AppOpenAmount: 50) 207 | } 208 | -------------------------------------------------------------------------------- /TDS Video/VIews/CameraRoll.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraRoll.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 19/03/2025. 6 | // 7 | 8 | import SwiftUI 9 | import AVKit 10 | import UniformTypeIdentifiers 11 | import PhotosUI 12 | 13 | struct SingleVideoPicker: View { 14 | @State private var isVideoPickerPresented = false 15 | @State private var selectedVideoURL: URL? 16 | @State private var isPlaying = false 17 | @State private var savedVideos: [URL] = [] 18 | @State private var isPhotosPickerPresented = false 19 | private let videoFolder = FileManager.default.temporaryDirectory.appendingPathComponent("SavedVideos", isDirectory: true) 20 | 21 | var body: some View { 22 | VStack(spacing: 20) { 23 | Text("Pick a file you would like, wait while it is made available, then press the Send to car button.") 24 | .multilineTextAlignment(.center) 25 | 26 | if let videoURL = selectedVideoURL { 27 | let player = TDSVideoShared.shared.VideoPlayerForFile 28 | 29 | HStack(spacing: 30) { 30 | Button(action: { skip(by: -10) }) { 31 | Label("Back 10s", systemImage: "gobackward.10") 32 | } 33 | 34 | Button(action: { 35 | if isPlaying { 36 | player?.pause() 37 | } else { 38 | player?.play() 39 | } 40 | isPlaying.toggle() 41 | }) { 42 | Image(systemName: isPlaying ? "pause.fill" : "play.fill") 43 | .font(.title) 44 | } 45 | 46 | Button(action: { skip(by: 10) }) { 47 | Label("Forward 10s", systemImage: "goforward.10") 48 | } 49 | } 50 | 51 | Button("Send to car") { 52 | var data = CarplayComClass(type: .video) 53 | data.URL = videoURL 54 | TDSVideoShared.shared.CarPlayComp?(data) 55 | } 56 | .padding(.top) 57 | } 58 | 59 | Divider() 60 | 61 | Text("Saved Videos") 62 | .font(.headline) 63 | 64 | List { 65 | ForEach(savedVideos, id: \.self) { url in 66 | HStack { 67 | Text(url.lastPathComponent) 68 | .lineLimit(1) 69 | Spacer() 70 | // Button("Play") { 71 | // selectedVideoURL = url 72 | // TDSVideoShared.shared.VideoPlayer = AVPlayer(url: url) 73 | // isPlaying = false 74 | // } 75 | // .padding(.horizontal) 76 | 77 | Button(role: .destructive) { 78 | deleteVideo(url) 79 | } label: { 80 | Image(systemName: "trash") 81 | } 82 | } 83 | } 84 | } 85 | .frame(height: 200) 86 | 87 | Button("Pick a New File") { 88 | isVideoPickerPresented = true 89 | } 90 | .fileImporter( 91 | isPresented: $isVideoPickerPresented, 92 | allowedContentTypes: [.movie], 93 | allowsMultipleSelection: false 94 | ) { result in 95 | do { 96 | guard let fileURL = try result.get().first else { return } 97 | 98 | guard fileURL.startAccessingSecurityScopedResource() else { 99 | print("Unable to access security-scoped resource.") 100 | return 101 | } 102 | defer { fileURL.stopAccessingSecurityScopedResource() } 103 | 104 | try createVideoFolderIfNeeded() 105 | 106 | let destinationURL = videoFolder.appendingPathComponent(fileURL.lastPathComponent) 107 | 108 | // Handle duplicates 109 | var uniqueURL = destinationURL 110 | var count = 1 111 | while FileManager.default.fileExists(atPath: uniqueURL.path) { 112 | uniqueURL = videoFolder.appendingPathComponent("\(fileURL.deletingPathExtension().lastPathComponent)-\(count).mov") 113 | count += 1 114 | } 115 | 116 | try FileManager.default.copyItem(at: fileURL, to: uniqueURL) 117 | 118 | savedVideos.append(uniqueURL) 119 | selectedVideoURL = uniqueURL 120 | TDSVideoShared.shared.VideoPlayer = AVPlayer(url: uniqueURL) 121 | isPlaying = false 122 | 123 | } catch { 124 | print("Error importing file: \(error.localizedDescription)") 125 | } 126 | } 127 | Button("Pick from Photos") { 128 | isPhotosPickerPresented = true 129 | } 130 | .sheet(isPresented: $isPhotosPickerPresented) { 131 | PhotoVideoPicker { url in 132 | if let url = url { 133 | handleNewVideo(url) 134 | } 135 | } 136 | } 137 | } 138 | .padding() 139 | .onAppear(perform: loadSavedVideos) 140 | } 141 | 142 | private func skip(by seconds: Double) { 143 | guard let player = TDSVideoShared.shared.VideoPlayerForFile else { return } 144 | let currentTime = player.currentTime() 145 | let newTime = CMTimeGetSeconds(currentTime) + seconds 146 | let time = CMTimeMakeWithSeconds(newTime, preferredTimescale: currentTime.timescale) 147 | player.seek(to: time) 148 | } 149 | 150 | private func createVideoFolderIfNeeded() throws { 151 | if !FileManager.default.fileExists(atPath: videoFolder.path) { 152 | try FileManager.default.createDirectory(at: videoFolder, withIntermediateDirectories: true) 153 | } 154 | } 155 | 156 | private func loadSavedVideos() { 157 | do { 158 | try createVideoFolderIfNeeded() 159 | let urls = try FileManager.default.contentsOfDirectory(at: videoFolder, includingPropertiesForKeys: nil) 160 | savedVideos = urls.filter { $0.pathExtension.lowercased() == "mov" } 161 | } catch { 162 | print("Error loading saved videos: \(error.localizedDescription)") 163 | } 164 | } 165 | 166 | private func deleteVideo(_ url: URL) { 167 | do { 168 | try FileManager.default.removeItem(at: url) 169 | savedVideos.removeAll { $0 == url } 170 | if selectedVideoURL == url { 171 | selectedVideoURL = nil 172 | isPlaying = false 173 | } 174 | } catch { 175 | print("Failed to delete video: \(error.localizedDescription)") 176 | } 177 | } 178 | 179 | private func handleNewVideo(_ fileURL: URL) { 180 | do { 181 | try createVideoFolderIfNeeded() 182 | 183 | let destinationURL = videoFolder.appendingPathComponent(fileURL.lastPathComponent) 184 | var uniqueURL = destinationURL 185 | var count = 1 186 | while FileManager.default.fileExists(atPath: uniqueURL.path) { 187 | uniqueURL = videoFolder.appendingPathComponent("\(fileURL.deletingPathExtension().lastPathComponent)-\(count).mov") 188 | count += 1 189 | } 190 | 191 | try FileManager.default.copyItem(at: fileURL, to: uniqueURL) 192 | 193 | savedVideos.append(uniqueURL) 194 | selectedVideoURL = uniqueURL 195 | TDSVideoShared.shared.VideoPlayer = AVPlayer(url: uniqueURL) 196 | isPlaying = false 197 | 198 | } catch { 199 | print("Error importing file: \(error.localizedDescription)") 200 | } 201 | } 202 | 203 | 204 | } 205 | 206 | 207 | struct PhotoVideoPicker: UIViewControllerRepresentable { 208 | var onVideoPicked: (URL?) -> Void 209 | 210 | func makeUIViewController(context: Context) -> PHPickerViewController { 211 | var config = PHPickerConfiguration() 212 | config.selectionLimit = 1 213 | config.filter = .videos 214 | 215 | let picker = PHPickerViewController(configuration: config) 216 | picker.delegate = context.coordinator 217 | return picker 218 | } 219 | 220 | func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {} 221 | 222 | func makeCoordinator() -> Coordinator { 223 | Coordinator(onVideoPicked: onVideoPicked) 224 | } 225 | 226 | class Coordinator: NSObject, PHPickerViewControllerDelegate { 227 | var onVideoPicked: (URL?) -> Void 228 | 229 | init(onVideoPicked: @escaping (URL?) -> Void) { 230 | self.onVideoPicked = onVideoPicked 231 | } 232 | 233 | func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { 234 | picker.dismiss(animated: true) 235 | 236 | guard let provider = results.first?.itemProvider, provider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) else { 237 | onVideoPicked(nil) 238 | return 239 | } 240 | 241 | provider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in 242 | guard let url = url else { 243 | print("Error loading video: \(error?.localizedDescription ?? "Unknown error")") 244 | self.onVideoPicked(nil) 245 | return 246 | } 247 | 248 | // Move the file to a temp URL to keep it 249 | let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".mov") 250 | do { 251 | try FileManager.default.copyItem(at: url, to: tempURL) 252 | DispatchQueue.main.async { 253 | self.onVideoPicked(tempURL) 254 | } 255 | } catch { 256 | print("Copy failed: \(error)") 257 | DispatchQueue.main.async { 258 | self.onVideoPicked(nil) 259 | } 260 | } 261 | } 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /TDS Video/TDSVideoAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TDSVideoAPI.swift 3 | // TDS Video 4 | // 5 | // Created by Thomas Dye on 17/03/2025. 6 | // 7 | 8 | import Foundation 9 | import Security 10 | import CommonCrypto 11 | import SwiftUI 12 | import CoreMotion 13 | 14 | struct TDSDeviceInfo: Codable { 15 | let uuid: String 16 | let openCount: Int 17 | let latitude: Double? 18 | let longitude: Double? 19 | let hasSeenPayment:Bool 20 | } 21 | 22 | struct TDSDeviceURL: Codable { 23 | let url:String 24 | } 25 | 26 | 27 | class TDSVideoAPI:NSObject,ObservableObject { 28 | static let shared = TDSVideoAPI() 29 | private let serverURL = URL(string: "https://api.thomasdye.net/app/ThomasRandom/TDSVideo/ApppTrackingV2")! 30 | private let pinnedPublicKeyHash = "dLd2Fq91ht5iLfGjD6gNvTt5p6otE41l9Bss5hicNoQ=" // Replace with actual hash 31 | // dLd2Fq91ht5iLfGjD6gNvTt5p6otE41l9Bss5hicNoQ= 32 | let motionActivityManager = CMMotionActivityManager() 33 | var showPayment:Bool = false 34 | var paymentscreen:UIViewController? 35 | 36 | private override init() { 37 | } 38 | 39 | private let callCountKey = "TDSVideoAPICallCount" 40 | 41 | private var callCount: Int { 42 | get { 43 | return UserDefaults.standard.integer(forKey: callCountKey) 44 | } 45 | set { 46 | UserDefaults.standard.set(newValue, forKey: callCountKey) 47 | } 48 | } 49 | 50 | // // Stored property for the selected orientation 51 | // @Published var selectedOrientation: ScreenOrientation { 52 | // get { 53 | // // Retrieve the stored value from UserDefaults 54 | // if let storedValue = UserDefaults.standard.value(forKey: orientationKey) as? Int, 55 | // let orientation = ScreenOrientation(rawValue: storedValue) { 56 | // return orientation 57 | // } else { 58 | // return .left // Default value if not set 59 | // } 60 | // } 61 | // set { 62 | // // Store the new value in UserDefaults 63 | // 64 | // } 65 | // } 66 | 67 | func DeviceBooted(VC:UIViewController) async { 68 | let uuid = BackgroundAuthID(DeviceUUID: UUID().uuidString) 69 | self.showPayment = false 70 | // Increment call count 71 | callCount += 1 72 | 73 | DispatchQueue.main.asyncAfter(deadline: .now() + 30) { 74 | self.sendDeviceUUID(UUID: uuid,callCount:self.callCount) 75 | } 76 | if callCount > 4 { 77 | self.showPayment = true 78 | } 79 | 80 | 81 | // let HasSeenPaymentScreenBefore = UserDefaults.standard.string(forKey: "buymeACoffeePressedV2") 82 | // let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown" 83 | // if HasSeenPaymentScreenBefore == buildNumber{ 84 | // self.showPayment = false 85 | // } 86 | let PAYMENTTDSVIDEO = UserDefaults.standard.bool(forKey: "PAYMENTTDSVIDEO") 87 | if PAYMENTTDSVIDEO == true { 88 | self.showPayment = false 89 | } 90 | if showPayment { 91 | DispatchQueue.main.async { 92 | self.paymentscreen = UIHostingController(rootView: SupportScreen(AppOpenAmount: self.callCount)) 93 | self.paymentscreen?.modalPresentationStyle = .fullScreen 94 | if let paymentscreen = self.paymentscreen { 95 | VC.present(paymentscreen, animated: true) 96 | } 97 | } 98 | 99 | } 100 | 101 | } 102 | 103 | func sendDeviceUUID(UUID:String,callCount:Int) { 104 | 105 | 106 | var request = URLRequest(url: serverURL) 107 | request.httpMethod = "POST" 108 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 109 | // request.setValue("Bearer \(auth.GetToken())", forHTTPHeaderField: "Authorization") 110 | 111 | // Fetch user's location 112 | let latitude = TDSLocationAPI.shared.latitude 113 | let longitude = TDSLocationAPI.shared.longitude 114 | 115 | // Create the request body using the struct 116 | let deviceInfo = TDSDeviceInfo(uuid: UUID, openCount: callCount, latitude: latitude, longitude: longitude, hasSeenPayment: UserDefaults.standard.bool(forKey: "buymeACoffeePressed")) 117 | 118 | // Convert struct to JSON 119 | guard let jsonData = try? JSONEncoder().encode(deviceInfo) else { 120 | print("Failed to encode JSON") 121 | return 122 | } 123 | 124 | 125 | request.httpBody = jsonData 126 | 127 | let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) 128 | let task = session.dataTask(with: request) { data, response, error in 129 | if let error = error { 130 | print("Request failed: \(error.localizedDescription)") 131 | return 132 | } 133 | 134 | if let httpResponse = response as? HTTPURLResponse { 135 | print("Response status code: \(httpResponse.statusCode)") 136 | } 137 | } 138 | 139 | task.resume() 140 | 141 | } 142 | 143 | 144 | 145 | 146 | // 147 | 148 | func BuyMeACoffeePressedFromPayment() { 149 | DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: { 150 | self.BuyMeACoffeePressedFromPayment() 151 | }) 152 | 153 | } 154 | func HidebyuymeACoffeePressed() { 155 | UserDefaults.standard.set(true, forKey: "buymeACoffeePressed") 156 | let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "unknown" 157 | UserDefaults.standard.set(buildNumber, forKey: "buymeACoffeePressedV2") 158 | self.paymentscreen?.dismiss(animated: true) 159 | } 160 | func sendEmail() { 161 | let subject = "TDS Video Support Request device UUID: \(UIDevice.current.identifierForVendor?.uuidString ?? "")" 162 | let body = "" 163 | let email = "apple@thomasdye.net" // Replace with your email 164 | let encodedSubject = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" 165 | let encodedBody = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" 166 | 167 | if let emailURL = URL(string: "mailto:\(email)?subject=\(encodedSubject)&body=\(encodedBody)") { 168 | UIApplication.shared.open(emailURL) 169 | print("Email button pressed") 170 | } 171 | } 172 | 173 | 174 | func deleteOldFiles(from directory: URL, olderThan days: Int) { 175 | let fileManager = FileManager.default 176 | let calendar = Calendar.current 177 | let expirationDate = calendar.date(byAdding: .day, value: -days, to: Date()) 178 | 179 | do { 180 | let fileURLs = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles]) 181 | 182 | for fileURL in fileURLs { 183 | let resourceValues = try fileURL.resourceValues(forKeys: [.contentModificationDateKey]) 184 | 185 | if let modificationDate = resourceValues.contentModificationDate, 186 | let expirationDate = expirationDate, 187 | modificationDate < expirationDate { 188 | try fileManager.removeItem(at: fileURL) 189 | print("Deleted old file: \(fileURL.lastPathComponent)") 190 | } 191 | } 192 | } catch { 193 | print("Error while deleting old files: \(error.localizedDescription)") 194 | } 195 | } 196 | 197 | 198 | func HidePaymentScreen() { 199 | UserDefaults.standard.set(true, forKey: "PAYMENTTDSVIDEO") 200 | self.paymentscreen?.dismiss(animated: true) 201 | self.showPayment = false 202 | 203 | } 204 | 205 | 206 | } 207 | 208 | extension TDSVideoAPI: URLSessionDelegate { 209 | 210 | } 211 | 212 | extension Data { 213 | func sha256() -> Data { 214 | var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) 215 | self.withUnsafeBytes { 216 | _ = CC_SHA256($0.baseAddress, CC_LONG(self.count), &hash) 217 | } 218 | return Data(hash) 219 | } 220 | } 221 | public enum ScreenOrientation: Int, @unchecked Sendable, CaseIterable { 222 | case up = 0 223 | case down = 1 224 | case left = 2 225 | case right = 3 226 | case upMirrored = 4 227 | case downMirrored = 5 228 | case leftMirrored = 6 229 | case rightMirrored = 7 230 | 231 | func humanReadable() -> String { 232 | switch self { 233 | case .up: 234 | return "Up" 235 | case .down: 236 | return "Down" 237 | case .left: 238 | return "Left" 239 | case .right: 240 | return "Right" 241 | case .upMirrored: 242 | return "Up Mirrored" 243 | case .downMirrored: 244 | return "Down Mirrored" 245 | case .leftMirrored: 246 | return "Left Mirrored" 247 | case .rightMirrored: 248 | return "Right Mirrored" 249 | } 250 | } 251 | } 252 | 253 | public enum AspectRatio : Int, @unchecked Sendable, CaseIterable { 254 | 255 | case scaleToFill = 0 256 | 257 | case scaleAspectFit = 1 258 | 259 | case scaleAspectFill = 2 260 | 261 | case redraw = 3 262 | 263 | case center = 4 264 | 265 | case top = 5 266 | 267 | case bottom = 6 268 | 269 | case left = 7 270 | 271 | case right = 8 272 | 273 | case topLeft = 9 274 | 275 | case topRight = 10 276 | 277 | case bottomLeft = 11 278 | 279 | case bottomRight = 12 280 | 281 | func humanReadableName() -> String { 282 | switch self { 283 | case .scaleToFill: return "Scale To Fill" 284 | case .scaleAspectFit: return "Scale Aspect Fit" 285 | case .scaleAspectFill: return "Scale Aspect Fill" 286 | case .redraw: return "Redraw" 287 | case .center: return "Center" 288 | case .top: return "Top" 289 | case .bottom: return "Bottom" 290 | case .left: return "Left" 291 | case .right: return "Right" 292 | case .topLeft: return "Top Left" 293 | case .topRight: return "Top Right" 294 | case .bottomLeft: return "Bottom Left" 295 | case .bottomRight: return "Bottom Right" 296 | } 297 | } 298 | 299 | } 300 | 301 | 302 | func BackgroundAuthID(DeviceUUID:String?) -> String { 303 | if let id = UserDefaults.standard.string(forKey: "BackgroundAuthID") { 304 | return id 305 | } 306 | let newID = (DeviceUUID ?? UUID().uuidString ) 307 | UserDefaults.standard.set(newID, forKey: "BackgroundAuthID") 308 | return newID 309 | 310 | } 311 | --------------------------------------------------------------------------------