?
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 |
--------------------------------------------------------------------------------