damus

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

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 }