damus

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

commit 9cf53a9e93286ef881e57e5c29f2364f91e2ea7e
parent 3569da568776dd2e015ca4130c23813c8889d71d
Author: Bryan Montz <bryanmontz@me.com>
Date:   Wed,  6 Sep 2023 11:49:06 -0500

video: remove VideoPlayer and switch to VideoController for cache

Closes: https://github.com/damus-io/damus/pull/1539
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 8++++----
Mdamus/Components/ImageCarousel.swift | 2+-
Adamus/Util/AVPlayer+Additions.swift | 35+++++++++++++++++++++++++++++++++++
Mdamus/Util/EventCache.swift | 26--------------------------
Mdamus/Views/Video/DamusVideoPlayerViewModel.swift | 12++++++++++++
Mdamus/Views/Video/VideoController.swift | 4++++
Ddamus/Views/Video/VideoPlayer.swift | 350-------------------------------------------------------------------------------
7 files changed, 56 insertions(+), 381 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -317,7 +317,6 @@ 4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; }; 4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */; }; 4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; }; - 4CCF9AAF2A1FDBDB00E03CFB /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */; }; 4CCF9AB22A1FE80C00E03CFB /* GSPlayer in Frameworks */ = {isa = PBXBuildFile; productRef = 4CCF9AB12A1FE80C00E03CFB /* GSPlayer */; }; 4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; }; 4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; }; @@ -394,6 +393,7 @@ 50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; }; 50A60D142A28BEEE00186190 /* RelayLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A60D132A28BEEE00186190 /* RelayLog.swift */; }; 50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; }; + 50C3E08A2AA8E3F7006A4BC0 /* AVPlayer+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C3E0892AA8E3F7006A4BC0 /* AVPlayer+Additions.swift */; }; 50DA11262A16A23F00236234 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50DA11252A16A23F00236234 /* Launch.storyboard */; }; 5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */; }; 5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; }; @@ -990,7 +990,6 @@ 4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; }; 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingEventView.swift; sourceTree = "<group>"; }; 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; }; - 4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; }; 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; }; 4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; }; 4CDA128929E9D10C0006FA5A /* SignalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalView.swift; sourceTree = "<group>"; }; @@ -1072,6 +1071,7 @@ 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; }; 50A60D132A28BEEE00186190 /* RelayLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayLog.swift; sourceTree = "<group>"; }; 50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; }; + 50C3E0892AA8E3F7006A4BC0 /* AVPlayer+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVPlayer+Additions.swift"; sourceTree = "<group>"; }; 50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = "<group>"; }; 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLogoGradient.swift; sourceTree = "<group>"; }; 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; }; @@ -1366,7 +1366,6 @@ 4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */, 50A16FFC2AA7525700DFEC1F /* DamusVideoPlayerViewModel.swift */, 50A16FFE2AA76A0900DFEC1F /* VideoController.swift */, - 4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */, 50A16FFA2AA6C06600DFEC1F /* AVPlayerView.swift */, ); path = Video; @@ -1791,6 +1790,7 @@ 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */, D2277EE92A089BD5006C3807 /* Router.swift */, 4C2B10272A7B0F5C008AA43E /* Log.swift */, + 50C3E0892AA8E3F7006A4BC0 /* AVPlayer+Additions.swift */, ); path = Util; sourceTree = "<group>"; @@ -2507,7 +2507,6 @@ 4C216F34286F5ACD00040376 /* DMView.swift in Sources */, 4C32B9572A9AD44700DC3548 /* Root.swift in Sources */, 4C3EA64428FF558100C48A62 /* sha256.c in Sources */, - 4CCF9AAF2A1FDBDB00E03CFB /* VideoPlayer.swift in Sources */, 504323A72A34915F006AE6DC /* RelayModel.swift in Sources */, 4CA9276A2A290FC00098A105 /* ContextButton.swift in Sources */, 4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */, @@ -2554,6 +2553,7 @@ 4C7D09742A0AEF9000943473 /* AlbyGradient.swift in Sources */, 4C687C272A6039500092C550 /* TestData.swift in Sources */, 3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */, + 50C3E08A2AA8E3F7006A4BC0 /* AVPlayer+Additions.swift in Sources */, 4C198DF229F88C6B004C165C /* BlurHashDecode.swift in Sources */, F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */, 4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */, diff --git a/damus/Components/ImageCarousel.swift b/damus/Components/ImageCarousel.swift @@ -105,7 +105,7 @@ struct ImageCarousel: View { } } .onAppear { - if self.image_fill == nil, let size = state.events.lookup_media_size(url: url) { + if self.image_fill == nil, let size = state.video.size_for_url(url) { let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight) self.image_fill = fill } diff --git a/damus/Util/AVPlayer+Additions.swift b/damus/Util/AVPlayer+Additions.swift @@ -0,0 +1,35 @@ +// +// AVPlayer+Additions.swift +// damus +// +// Created by Bryan Montz on 9/6/23. +// + +import AVFoundation +import Foundation +import UIKit + +extension AVPlayer { +#if !os(macOS) + var currentImage: UIImage? { + guard + let playerItem = currentItem, + let cgImage = try? AVAssetImageGenerator(asset: playerItem.asset).copyCGImage(at: currentTime(), actualTime: nil) + else { return nil } + + return UIImage(cgImage: cgImage) + } +#else + var currentImage: NSImage? { + guard + let playerItem = currentItem, + let cgImage = try? AVAssetImageGenerator(asset: playerItem.asset).copyCGImage(at: currentTime(), actualTime: nil) + else { + return nil + } + let width: CGFloat = CGFloat(cgImage.width) + let height: CGFloat = CGFloat(cgImage.height) + return NSImage(cgImage: cgImage, size: NSMakeSize(width, height)) + } +#endif +} diff --git a/damus/Util/EventCache.swift b/damus/Util/EventCache.swift @@ -141,7 +141,6 @@ class EventCache { private var replies = ReplyMap() private var cancellable: AnyCancellable? private var image_metadata: [String: ImageMetadataState] = [:] // lowercased URL key - private var video_meta: [URL: VideoPlayerModel] = [:] private var event_data: [NoteId: EventData] = [:] //private var thread_latest: [String: Int64] @@ -204,30 +203,6 @@ class EventCache { return image_metadata[url.absoluteString.lowercased()] } - @MainActor - func lookup_media_size(url: URL) -> CGSize? { - if let img_meta = lookup_img_metadata(url: url) { - return img_meta.meta.dim?.size - } - - return get_video_player_model(url: url).size - } - - func store_video_player_model(url: URL, meta: VideoPlayerModel) { - video_meta[url] = meta - } - - @MainActor - func get_video_player_model(url: URL) -> VideoPlayerModel { - if let model = video_meta[url] { - return model - } - - let model = VideoPlayerModel() - video_meta[url] = model - return model - } - func parent_events(event: NostrEvent, keypair: Keypair) -> [NostrEvent] { var parents: [NostrEvent] = [] @@ -289,7 +264,6 @@ class EventCache { private func prune() { events = [:] - video_meta = [:] event_data = [:] replies.replies = [:] } diff --git a/damus/Views/Video/DamusVideoPlayerViewModel.swift b/damus/Views/Video/DamusVideoPlayerViewModel.swift @@ -10,6 +10,18 @@ import Combine import Foundation import SwiftUI +func get_video_size(player: AVPlayer) async -> CGSize? { + let res = Task.detached(priority: .background) { + return player.currentImage?.size + } + return await res.value +} + +func video_has_audio(player: AVPlayer) async -> Bool { + let tracks = try? await player.currentItem?.asset.load(.tracks) + return tracks?.filter({ t in t.mediaType == .audio }).first != nil +} + @MainActor final class DamusVideoPlayerViewModel: ObservableObject { diff --git a/damus/Views/Video/VideoController.swift b/damus/Views/Video/VideoController.swift @@ -37,4 +37,8 @@ final class VideoController: ObservableObject { func metadata(for url: URL) -> VideoMetadata? { metadatas[url] } + + func size_for_url(_ url: URL) -> CGSize? { + metadatas[url]?.size + } } diff --git a/damus/Views/Video/VideoPlayer.swift b/damus/Views/Video/VideoPlayer.swift @@ -1,350 +0,0 @@ -// -// VideoPlayer.swift -// damus -// -// Created by William Casarin on 2023-05-25. -// - -import Foundation -// -// VideoPlayer.swift -// VideoPlayer -// -// Created by Gesen on 2019/7/7. -// Copyright © 2019 Gesen. All rights reserved. -// - -import AVFoundation -import GSPlayer -import SwiftUI - -public enum VideoState { - /// From the first load to get the first frame of the video - case loading - - /// Playing now - case playing(totalDuration: Double) - - /// Pause, will be called repeatedly when the buffer progress changes - case paused(playProgress: Double, bufferProgress: Double) - - /// An error occurred and cannot continue playing - case error(NSError) -} - -enum VideoHandler { - case onBufferChanged((Double) -> Void) - case onPlayToEndTime(() -> Void) - case onReplay(() -> Void) - case onStateChanged((VideoState) -> Void) -} - -@MainActor -public class VideoPlayerModel: ObservableObject { - @Published var autoReplay: Bool = true - @Published var muted: Bool = true - @Published var play: Bool = true - @Published var size: CGSize? = nil - @Published var has_audio: Bool? = nil - @Published var contentMode: UIView.ContentMode = .scaleAspectFill - - fileprivate var time: CMTime? - - var handlers: [VideoHandler] = [] - - init() { - } - - func stop() { - self.play = false - } - - func start() { - self.play = true - } - - func mute() { - self.muted = true - } - - func unmute() { - self.muted = false - } - - /// Whether the video will be automatically replayed until the end of the video playback. - func autoReplay(_ value: Bool) -> Self { - autoReplay = value - return self - } - - /// Whether the video is muted, only for this instance. - func mute(_ value: Bool) -> Self { - muted = value - return self - } - - /// A string defining how the video is displayed within an AVPlayerLayer bounds rect. - /// scaleAspectFill -> resizeAspectFill, scaleAspectFit -> resizeAspect, other -> resize - func contentMode(_ value: UIView.ContentMode) -> Self { - contentMode = value - return self - } - - /// Trigger a callback when the buffer progress changes, - /// the value is between 0 and 1. - func onBufferChanged(_ handler: @escaping (Double) -> Void) -> Self { - self.handlers.append(.onBufferChanged(handler)) - return self - } - - /// Playing to the end. - func onPlayToEndTime(_ handler: @escaping () -> Void) -> Self { - self.handlers.append(.onPlayToEndTime(handler)) - return self - } - - /// Replay after playing to the end. - func onReplay(_ handler: @escaping () -> Void) -> Self { - self.handlers.append(.onReplay(handler)) - return self - } - - /// Playback status changes, such as from play to pause. - func onStateChanged(_ handler: @escaping (VideoState) -> Void) -> Self { - self.handlers.append(.onStateChanged(handler)) - return self - } -} - -@available(iOS 13, *) -public struct VideoPlayer { - private(set) var url: URL - - @ObservedObject var model: VideoPlayerModel - - /// Init video player instance. - /// - Parameters: - /// - url: http/https URL - /// - play: play/pause - /// - time: current time - public init(url: URL, model: VideoPlayerModel) { - self.url = url - self._model = ObservedObject(wrappedValue: model) - } -} - -@available(iOS 13, *) -public extension VideoPlayer { - - /// Set the preload size, the default value is 1024 * 1024, unit is byte. - static var preloadByteCount: Int { - get { VideoPreloadManager.shared.preloadByteCount } - set { VideoPreloadManager.shared.preloadByteCount = newValue } - } - - /// Set the video urls to be preload queue. - /// Preloading will automatically cache a short segment of the beginning of the video - /// and decide whether to start or pause the preload based on the buffering of the currently playing video. - /// - Parameter urls: URL array - static func preload(urls: [URL]) { - VideoPreloadManager.shared.set(waiting: urls) - } - - /// Set custom http header, such as token. - static func customHTTPHeaderFields(transform: @escaping (URL) -> [String: String]?) { - VideoLoadManager.shared.customHTTPHeaderFields = transform - } - - /// Get the total size of the video cache. - static func calculateCachedSize() -> UInt { - return VideoCacheManager.calculateCachedSize() - } - - /// Clean up all caches. - static func cleanAllCache() { - try? VideoCacheManager.cleanAllCache() - } -} - -func get_video_size(player: AVPlayer) async -> CGSize? { - let res = Task.detached(priority: .background) { - return player.currentImage?.size - } - return await res.value -} - -func video_has_audio(player: AVPlayer) async -> Bool { - let tracks = try? await player.currentItem?.asset.load(.tracks) - return tracks?.filter({ t in t.mediaType == .audio }).first != nil -} - -@available(iOS 13, *) -extension VideoPlayer: UIViewRepresentable { - - public func makeUIView(context: Context) -> VideoPlayerView { - let uiView = VideoPlayerView() - - uiView.playToEndTime = { - if self.model.autoReplay == false { - self.model.play = false - } - DispatchQueue.main.async { - for handler in model.handlers { - if case .onPlayToEndTime(let cb) = handler { - cb() - } - } - } - } - - uiView.contentMode = self.model.contentMode - - uiView.replay = { - DispatchQueue.main.async { - for handler in model.handlers { - if case .onReplay(let cb) = handler { - cb() - } - } - } - } - - uiView.stateDidChanged = { [unowned uiView] _ in - let state: VideoState = uiView.convertState() - - if case .playing = state { - context.coordinator.startObserver(uiView: uiView) - - if let player = uiView.player { - Task { - let has_audio = await video_has_audio(player: player) - let size = await get_video_size(player: player) - Task { @MainActor in - if let size { - self.model.size = size - } - self.model.has_audio = has_audio - } - } - } - - } else { - context.coordinator.stopObserver(uiView: uiView) - } - - DispatchQueue.main.async { - for handler in model.handlers { - if case .onStateChanged(let cb) = handler { - cb(state) - } - } - } - } - - return uiView - } - - public func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - public func updateUIView(_ uiView: VideoPlayerView, context: Context) { - if context.coordinator.observingURL != url { - context.coordinator.clean() - context.coordinator.observingURL = url - } - - if model.play { - uiView.play(for: url) - } else { - uiView.pause(reason: .userInteraction) - } - - uiView.isMuted = model.muted - uiView.isAutoReplay = model.autoReplay - - if let observerTime = context.coordinator.observerTime, let modelTime = model.time, - modelTime != observerTime && modelTime.isValid && modelTime.isNumeric { - uiView.seek(to: modelTime, completion: { _ in }) - } - } - - public static func dismantleUIView(_ uiView: VideoPlayerView, coordinator: VideoPlayer.Coordinator) { - uiView.pause(reason: .hidden) - } - - public class Coordinator: NSObject { - var videoPlayer: VideoPlayer - var observingURL: URL? - var observer: Any? - var observerTime: CMTime? - var observerBuffer: Double? - - init(_ videoPlayer: VideoPlayer) { - self.videoPlayer = videoPlayer - } - - @MainActor - func startObserver(uiView: VideoPlayerView) { - guard observer == nil else { return } - - observer = uiView.addPeriodicTimeObserver(forInterval: .init(seconds: 0.25, preferredTimescale: 60)) { [weak self, unowned uiView] time in - guard let `self` = self else { return } - - Task { @MainActor in - self.videoPlayer.model.time = time - } - self.observerTime = time - - self.updateBuffer(uiView: uiView) - } - } - - func stopObserver(uiView: VideoPlayerView) { - guard let observer = observer else { return } - - uiView.removeTimeObserver(observer) - - self.observer = nil - } - - func clean() { - self.observingURL = nil - self.observer = nil - self.observerTime = nil - self.observerBuffer = nil - } - - @MainActor - func updateBuffer(uiView: VideoPlayerView) { - let bufferProgress = uiView.bufferProgress - guard bufferProgress != observerBuffer else { return } - - for handler in videoPlayer.model.handlers { - if case .onBufferChanged(let cb) = handler { - DispatchQueue.main.async { - cb(bufferProgress) - } - } - } - - observerBuffer = bufferProgress - } - } -} - -private extension VideoPlayerView { - - func convertState() -> VideoState { - switch state { - case .none, .loading: - return .loading - case .playing: - return .playing(totalDuration: totalDuration) - case .paused(let p, let b): - return .paused(playProgress: p, bufferProgress: b) - case .error(let error): - return .error(error) - } - } -}