damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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 }