This commit renames this class to better represent what it does. This reduces some of the term overloading between this class and other video controller classes/structs. (Such as AVPlayerController) Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
148 lines
4.5 KiB
Swift
148 lines
4.5 KiB
Swift
//
|
|
// DamusVideoPlayerViewModel.swift
|
|
// damus
|
|
//
|
|
// Created by Bryan Montz on 9/5/23.
|
|
//
|
|
|
|
import AVFoundation
|
|
import AVKit
|
|
import Combine
|
|
import Foundation
|
|
import SwiftUI
|
|
|
|
func video_has_audio(player: AVPlayer) async -> Bool {
|
|
do {
|
|
let hasAudibleTracks = ((try await player.currentItem?.asset.loadMediaSelectionGroup(for: .audible)) != nil)
|
|
let tracks = try? await player.currentItem?.asset.load(.tracks)
|
|
let hasAudioTrack = tracks?.filter({ t in t.mediaType == .audio }).first != nil // Deal with odd cases of audio only MOV
|
|
return hasAudibleTracks || hasAudioTrack
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
final class DamusVideoPlayerViewModel: ObservableObject {
|
|
|
|
private let url: URL
|
|
private let player_item: AVPlayerItem
|
|
let player: AVPlayer
|
|
fileprivate let coordinator: DamusVideoCoordinator
|
|
let player_view_controller = AVPlayerViewController()
|
|
let id = UUID()
|
|
|
|
@Published var has_audio = false
|
|
@Published var is_live = false
|
|
@Binding var video_size: CGSize?
|
|
@Published var is_muted = true
|
|
@Published var is_loading = true
|
|
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
private var videoSizeObserver: NSKeyValueObservation?
|
|
private var videoDurationObserver: NSKeyValueObservation?
|
|
|
|
private var is_scrolled_into_view = false {
|
|
didSet {
|
|
if is_scrolled_into_view && !oldValue {
|
|
// we have just scrolled from out of view into view
|
|
coordinator.focused_model_id = id
|
|
} else if !is_scrolled_into_view && oldValue {
|
|
// we have just scrolled from in view to out of view
|
|
if coordinator.focused_model_id == id {
|
|
coordinator.focused_model_id = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
init(url: URL, video_size: Binding<CGSize?>, coordinator: DamusVideoCoordinator, mute: Bool? = nil) {
|
|
self.url = url
|
|
player_item = AVPlayerItem(url: url)
|
|
player = AVPlayer(playerItem: player_item)
|
|
self.coordinator = coordinator
|
|
_video_size = video_size
|
|
|
|
Task {
|
|
await load()
|
|
}
|
|
|
|
is_muted = mute ?? coordinator.should_mute_video(url: url)
|
|
player.isMuted = is_muted
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(did_play_to_end),
|
|
name: Notification.Name.AVPlayerItemDidPlayToEndTime,
|
|
object: player_item
|
|
)
|
|
|
|
coordinator.$focused_model_id
|
|
.sink { [weak self] model_id in
|
|
model_id == self?.id ? self?.player.play() : self?.player.pause()
|
|
}
|
|
.store(in: &cancellables)
|
|
|
|
observeVideoSize()
|
|
observeDuration()
|
|
}
|
|
|
|
private func observeVideoSize() {
|
|
videoSizeObserver = player.currentItem?.observe(\.presentationSize, options: [.new], changeHandler: { [weak self] (playerItem, change) in
|
|
guard let self else { return }
|
|
if let newSize = change.newValue, newSize != .zero {
|
|
DispatchQueue.main.async {
|
|
self.video_size = newSize // Update the bound value
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
private func observeDuration() {
|
|
videoDurationObserver = player.currentItem?.observe(\.duration, options: [.new], changeHandler: { [weak self] (playerItem, change) in
|
|
guard let self else { return }
|
|
if let newDuration = change.newValue, newDuration != .zero {
|
|
DispatchQueue.main.async {
|
|
self.is_live = newDuration == .indefinite
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
private func load() async {
|
|
if let meta = coordinator.metadata(for: url) {
|
|
has_audio = meta.has_audio
|
|
video_size = meta.size
|
|
} else {
|
|
has_audio = await video_has_audio(player: player)
|
|
}
|
|
|
|
is_loading = false
|
|
}
|
|
|
|
func did_tap_mute_button() {
|
|
is_muted.toggle()
|
|
player.isMuted = is_muted
|
|
coordinator.toggle_should_mute_video(url: url)
|
|
}
|
|
|
|
func set_view_is_visible(_ is_visible: Bool) {
|
|
is_scrolled_into_view = is_visible
|
|
}
|
|
|
|
func view_did_disappear() {
|
|
set_view_is_visible(false)
|
|
}
|
|
|
|
@objc private func did_play_to_end() {
|
|
player.seek(to: CMTime.zero)
|
|
player.play()
|
|
}
|
|
|
|
deinit {
|
|
videoSizeObserver?.invalidate()
|
|
videoDurationObserver?.invalidate()
|
|
}
|
|
}
|