VideoPlayer.swift (10443B)
1 // 2 // VideoPlayer.swift 3 // damus 4 // 5 // Created by William Casarin on 2023-05-25. 6 // 7 8 import Foundation 9 // 10 // VideoPlayer.swift 11 // VideoPlayer 12 // 13 // Created by Gesen on 2019/7/7. 14 // Copyright © 2019 Gesen. All rights reserved. 15 // 16 17 import AVFoundation 18 import GSPlayer 19 import SwiftUI 20 21 public enum VideoState { 22 /// From the first load to get the first frame of the video 23 case loading 24 25 /// Playing now 26 case playing(totalDuration: Double) 27 28 /// Pause, will be called repeatedly when the buffer progress changes 29 case paused(playProgress: Double, bufferProgress: Double) 30 31 /// An error occurred and cannot continue playing 32 case error(NSError) 33 } 34 35 enum VideoHandler { 36 case onBufferChanged((Double) -> Void) 37 case onPlayToEndTime(() -> Void) 38 case onReplay(() -> Void) 39 case onStateChanged((VideoState) -> Void) 40 } 41 42 @MainActor 43 public class VideoPlayerModel: ObservableObject { 44 @Published var autoReplay: Bool = true 45 @Published var muted: Bool = true 46 @Published var play: Bool = true 47 @Published var size: CGSize? = nil 48 @Published var has_audio: Bool? = nil 49 @Published var contentMode: UIView.ContentMode = .scaleAspectFill 50 51 fileprivate var time: CMTime? 52 53 var handlers: [VideoHandler] = [] 54 55 init() { 56 } 57 58 func stop() { 59 self.play = false 60 } 61 62 func start() { 63 self.play = true 64 } 65 66 func mute() { 67 self.muted = true 68 } 69 70 func unmute() { 71 self.muted = false 72 } 73 74 /// Whether the video will be automatically replayed until the end of the video playback. 75 func autoReplay(_ value: Bool) -> Self { 76 autoReplay = value 77 return self 78 } 79 80 /// Whether the video is muted, only for this instance. 81 func mute(_ value: Bool) -> Self { 82 muted = value 83 return self 84 } 85 86 /// A string defining how the video is displayed within an AVPlayerLayer bounds rect. 87 /// scaleAspectFill -> resizeAspectFill, scaleAspectFit -> resizeAspect, other -> resize 88 func contentMode(_ value: UIView.ContentMode) -> Self { 89 contentMode = value 90 return self 91 } 92 93 /// Trigger a callback when the buffer progress changes, 94 /// the value is between 0 and 1. 95 func onBufferChanged(_ handler: @escaping (Double) -> Void) -> Self { 96 self.handlers.append(.onBufferChanged(handler)) 97 return self 98 } 99 100 /// Playing to the end. 101 func onPlayToEndTime(_ handler: @escaping () -> Void) -> Self { 102 self.handlers.append(.onPlayToEndTime(handler)) 103 return self 104 } 105 106 /// Replay after playing to the end. 107 func onReplay(_ handler: @escaping () -> Void) -> Self { 108 self.handlers.append(.onReplay(handler)) 109 return self 110 } 111 112 /// Playback status changes, such as from play to pause. 113 func onStateChanged(_ handler: @escaping (VideoState) -> Void) -> Self { 114 self.handlers.append(.onStateChanged(handler)) 115 return self 116 } 117 } 118 119 @available(iOS 13, *) 120 public struct VideoPlayer { 121 private(set) var url: URL 122 123 @ObservedObject var model: VideoPlayerModel 124 125 /// Init video player instance. 126 /// - Parameters: 127 /// - url: http/https URL 128 /// - play: play/pause 129 /// - time: current time 130 public init(url: URL, model: VideoPlayerModel) { 131 self.url = url 132 self._model = ObservedObject(wrappedValue: model) 133 } 134 } 135 136 @available(iOS 13, *) 137 public extension VideoPlayer { 138 139 /// Set the preload size, the default value is 1024 * 1024, unit is byte. 140 static var preloadByteCount: Int { 141 get { VideoPreloadManager.shared.preloadByteCount } 142 set { VideoPreloadManager.shared.preloadByteCount = newValue } 143 } 144 145 /// Set the video urls to be preload queue. 146 /// Preloading will automatically cache a short segment of the beginning of the video 147 /// and decide whether to start or pause the preload based on the buffering of the currently playing video. 148 /// - Parameter urls: URL array 149 static func preload(urls: [URL]) { 150 VideoPreloadManager.shared.set(waiting: urls) 151 } 152 153 /// Set custom http header, such as token. 154 static func customHTTPHeaderFields(transform: @escaping (URL) -> [String: String]?) { 155 VideoLoadManager.shared.customHTTPHeaderFields = transform 156 } 157 158 /// Get the total size of the video cache. 159 static func calculateCachedSize() -> UInt { 160 return VideoCacheManager.calculateCachedSize() 161 } 162 163 /// Clean up all caches. 164 static func cleanAllCache() { 165 try? VideoCacheManager.cleanAllCache() 166 } 167 } 168 169 func get_video_size(player: AVPlayer) async -> CGSize? { 170 let res = Task.detached(priority: .background) { 171 return player.currentImage?.size 172 } 173 return await res.value 174 } 175 176 func video_has_audio(player: AVPlayer) async -> Bool { 177 let tracks = try? await player.currentItem?.asset.load(.tracks) 178 return tracks?.filter({ t in t.mediaType == .audio }).first != nil 179 } 180 181 @available(iOS 13, *) 182 extension VideoPlayer: UIViewRepresentable { 183 184 public func makeUIView(context: Context) -> VideoPlayerView { 185 let uiView = VideoPlayerView() 186 187 uiView.playToEndTime = { 188 if self.model.autoReplay == false { 189 self.model.play = false 190 } 191 DispatchQueue.main.async { 192 for handler in model.handlers { 193 if case .onPlayToEndTime(let cb) = handler { 194 cb() 195 } 196 } 197 } 198 } 199 200 uiView.contentMode = self.model.contentMode 201 202 uiView.replay = { 203 DispatchQueue.main.async { 204 for handler in model.handlers { 205 if case .onReplay(let cb) = handler { 206 cb() 207 } 208 } 209 } 210 } 211 212 uiView.stateDidChanged = { [unowned uiView] _ in 213 let state: VideoState = uiView.convertState() 214 215 if case .playing = state { 216 context.coordinator.startObserver(uiView: uiView) 217 218 if let player = uiView.player { 219 Task { 220 let has_audio = await video_has_audio(player: player) 221 let size = await get_video_size(player: player) 222 Task { @MainActor in 223 if let size { 224 self.model.size = size 225 } 226 self.model.has_audio = has_audio 227 } 228 } 229 } 230 231 } else { 232 context.coordinator.stopObserver(uiView: uiView) 233 } 234 235 DispatchQueue.main.async { 236 for handler in model.handlers { 237 if case .onStateChanged(let cb) = handler { 238 cb(state) 239 } 240 } 241 } 242 } 243 244 return uiView 245 } 246 247 public func makeCoordinator() -> Coordinator { 248 Coordinator(self) 249 } 250 251 public func updateUIView(_ uiView: VideoPlayerView, context: Context) { 252 if context.coordinator.observingURL != url { 253 context.coordinator.clean() 254 context.coordinator.observingURL = url 255 } 256 257 if model.play { 258 uiView.play(for: url) 259 } else { 260 uiView.pause(reason: .userInteraction) 261 } 262 263 uiView.isMuted = model.muted 264 uiView.isAutoReplay = model.autoReplay 265 266 if let observerTime = context.coordinator.observerTime, let modelTime = model.time, 267 modelTime != observerTime && modelTime.isValid && modelTime.isNumeric { 268 uiView.seek(to: modelTime, completion: { _ in }) 269 } 270 } 271 272 public static func dismantleUIView(_ uiView: VideoPlayerView, coordinator: VideoPlayer.Coordinator) { 273 uiView.pause(reason: .hidden) 274 } 275 276 public class Coordinator: NSObject { 277 var videoPlayer: VideoPlayer 278 var observingURL: URL? 279 var observer: Any? 280 var observerTime: CMTime? 281 var observerBuffer: Double? 282 283 init(_ videoPlayer: VideoPlayer) { 284 self.videoPlayer = videoPlayer 285 } 286 287 @MainActor 288 func startObserver(uiView: VideoPlayerView) { 289 guard observer == nil else { return } 290 291 observer = uiView.addPeriodicTimeObserver(forInterval: .init(seconds: 0.25, preferredTimescale: 60)) { [weak self, unowned uiView] time in 292 guard let `self` = self else { return } 293 294 Task { @MainActor in 295 self.videoPlayer.model.time = time 296 } 297 self.observerTime = time 298 299 self.updateBuffer(uiView: uiView) 300 } 301 } 302 303 func stopObserver(uiView: VideoPlayerView) { 304 guard let observer = observer else { return } 305 306 uiView.removeTimeObserver(observer) 307 308 self.observer = nil 309 } 310 311 func clean() { 312 self.observingURL = nil 313 self.observer = nil 314 self.observerTime = nil 315 self.observerBuffer = nil 316 } 317 318 @MainActor 319 func updateBuffer(uiView: VideoPlayerView) { 320 let bufferProgress = uiView.bufferProgress 321 guard bufferProgress != observerBuffer else { return } 322 323 for handler in videoPlayer.model.handlers { 324 if case .onBufferChanged(let cb) = handler { 325 DispatchQueue.main.async { 326 cb(bufferProgress) 327 } 328 } 329 } 330 331 observerBuffer = bufferProgress 332 } 333 } 334 } 335 336 private extension VideoPlayerView { 337 338 func convertState() -> VideoState { 339 switch state { 340 case .none, .loading: 341 return .loading 342 case .playing: 343 return .playing(totalDuration: totalDuration) 344 case .paused(let p, let b): 345 return .paused(playProgress: p, bufferProgress: b) 346 case .error(let error): 347 return .error(error) 348 } 349 } 350 }