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:
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)
- }
- }
-}