DamusVideoPlayerViewModel.swift (4598B)
1 // 2 // DamusVideoPlayerViewModel.swift 3 // damus 4 // 5 // Created by Bryan Montz on 9/5/23. 6 // 7 8 import AVFoundation 9 import AVKit 10 import Combine 11 import Foundation 12 import SwiftUI 13 14 func video_has_audio(player: AVPlayer) async -> Bool { 15 do { 16 let hasAudibleTracks = ((try await player.currentItem?.asset.loadMediaSelectionGroup(for: .audible)) != nil) 17 let tracks = try? await player.currentItem?.asset.load(.tracks) 18 let hasAudioTrack = tracks?.filter({ t in t.mediaType == .audio }).first != nil // Deal with odd cases of audio only MOV 19 return hasAudibleTracks || hasAudioTrack 20 } catch { 21 return false 22 } 23 } 24 25 @MainActor 26 final class DamusVideoPlayerViewModel: ObservableObject { 27 28 private let url: URL 29 private let player_item: AVPlayerItem 30 let player: AVPlayer 31 fileprivate let controller: VideoController 32 let player_view_controller = AVPlayerViewController() 33 let id = UUID() 34 35 @Published var has_audio = false 36 @Published var is_live = false 37 @Binding var video_size: CGSize? 38 @Published var is_muted = true 39 @Published var is_loading = true 40 41 private var cancellables = Set<AnyCancellable>() 42 43 private var videoSizeObserver: NSKeyValueObservation? 44 private var videoDurationObserver: NSKeyValueObservation? 45 46 private var is_scrolled_into_view = false { 47 didSet { 48 if is_scrolled_into_view && !oldValue { 49 // we have just scrolled from out of view into view 50 controller.focused_model_id = id 51 } else if !is_scrolled_into_view && oldValue { 52 // we have just scrolled from in view to out of view 53 if controller.focused_model_id == id { 54 controller.focused_model_id = nil 55 } 56 } 57 } 58 } 59 60 init(url: URL, video_size: Binding<CGSize?>, controller: VideoController, mute: Bool? = nil) { 61 self.url = url 62 player_item = AVPlayerItem(url: url) 63 player = AVPlayer(playerItem: player_item) 64 self.controller = controller 65 _video_size = video_size 66 67 Task { 68 await load() 69 } 70 71 is_muted = mute ?? controller.should_mute_video(url: url) 72 player.isMuted = is_muted 73 74 NotificationCenter.default.addObserver( 75 self, 76 selector: #selector(did_play_to_end), 77 name: Notification.Name.AVPlayerItemDidPlayToEndTime, 78 object: player_item 79 ) 80 81 controller.$focused_model_id 82 .sink { [weak self] model_id in 83 model_id == self?.id ? self?.player.play() : self?.player.pause() 84 } 85 .store(in: &cancellables) 86 87 observeVideoSize() 88 observeDuration() 89 } 90 91 private func observeVideoSize() { 92 videoSizeObserver = player.currentItem?.observe(\.presentationSize, options: [.new], changeHandler: { [weak self] (playerItem, change) in 93 guard let self else { return } 94 if let newSize = change.newValue, newSize != .zero { 95 DispatchQueue.main.async { 96 self.video_size = newSize // Update the bound value 97 } 98 } 99 }) 100 } 101 102 private func observeDuration() { 103 videoDurationObserver = player.currentItem?.observe(\.duration, options: [.new], changeHandler: { [weak self] (playerItem, change) in 104 guard let self else { return } 105 if let newDuration = change.newValue, newDuration != .zero { 106 DispatchQueue.main.async { 107 self.is_live = newDuration == .indefinite 108 } 109 } 110 }) 111 } 112 113 private func load() async { 114 if let meta = controller.metadata(for: url) { 115 has_audio = meta.has_audio 116 video_size = meta.size 117 } else { 118 has_audio = await video_has_audio(player: player) 119 } 120 121 is_loading = false 122 } 123 124 func did_tap_mute_button() { 125 is_muted.toggle() 126 player.isMuted = is_muted 127 controller.toggle_should_mute_video(url: url) 128 } 129 130 func set_view_is_visible(_ is_visible: Bool) { 131 is_scrolled_into_view = is_visible 132 } 133 134 func view_did_disappear() { 135 set_view_is_visible(false) 136 } 137 138 @objc private func did_play_to_end() { 139 player.seek(to: CMTime.zero) 140 player.play() 141 } 142 143 deinit { 144 videoSizeObserver?.invalidate() 145 videoDurationObserver?.invalidate() 146 } 147 }