From 4d95d36a1e0013c2088ae1b0b31a83a731a1897d Mon Sep 17 00:00:00 2001 From: William Casarin Date: Fri, 26 May 2023 10:00:29 -0700 Subject: [PATCH] Add GSPlayer + VideoPlayer --- damus.xcodeproj/project.pbxproj | 29 ++ .../xcshareddata/swiftpm/Package.resolved | 9 + damus/Views/Video/VideoPlayer.swift | 326 ++++++++++++++++++ 3 files changed, 364 insertions(+) create mode 100644 damus/Views/Video/VideoPlayer.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 412dfedf..350acd82 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -207,6 +207,8 @@ 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 */; }; 4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128929E9D10C0006FA5A /* SignalView.swift */; }; @@ -651,6 +653,7 @@ 4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = ""; }; 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingEventView.swift; sourceTree = ""; }; 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = ""; }; + 4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = ""; }; 4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = ""; }; 4CDA128929E9D10C0006FA5A /* SignalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalView.swift; sourceTree = ""; }; @@ -771,6 +774,7 @@ buildActionMask = 2147483647; files = ( 4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */, + 4CCF9AB22A1FE80C00E03CFB /* GSPlayer in Frameworks */, 4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -949,6 +953,14 @@ path = Settings; sourceTree = ""; }; + 4C1A9A2829DDF53B00516EAC /* Video */ = { + isa = PBXGroup; + children = ( + 4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */, + ); + path = Video; + sourceTree = ""; + }; 4C30AC7029A5676F00E2BD5A /* Notifications */ = { isa = PBXGroup; children = ( @@ -975,6 +987,7 @@ 4C7D09692A0AEA0400943473 /* CodeScanner */, 4C7D095A2A098C5C00943473 /* Wallet */, 4C8D1A6D29F31E4100ACDF75 /* Buttons */, + 4C1A9A2829DDF53B00516EAC /* Video */, 4C1A9A1B29DDCF8B00516EAC /* Settings */, 4CFF8F6129CC9A80008DB934 /* Images */, 4CCEB7AC29B53D180078AA28 /* Search */, @@ -1501,6 +1514,7 @@ packageProductDependencies = ( 4C649880286E0EE300EAE2B3 /* secp256k1 */, 4C06670328FC7EC500038D2A /* Kingfisher */, + 4CCF9AB12A1FE80C00E03CFB /* GSPlayer */, ); productName = damus; productReference = 4CE6DEE327F7A08100C66700 /* damus.app */; @@ -1605,6 +1619,7 @@ packageReferences = ( 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */, 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */, + 4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */, ); productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */; projectDirPath = ""; @@ -1669,6 +1684,7 @@ 4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */, 4C216F34286F5ACD00040376 /* DMView.swift in Sources */, 4C3EA64428FF558100C48A62 /* sha256.c in Sources */, + 4CCF9AAF2A1FDBDB00E03CFB /* VideoPlayer.swift in Sources */, 4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */, 4C363AA828297703006E126D /* InsertSort.swift in Sources */, 4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */, @@ -2441,6 +2457,14 @@ revision = 40b4b38b3b1c83f7088c76189a742870e0ca06a9; }; }; + 4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/wxxsw/GSPlayer"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.2.26; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -2454,6 +2478,11 @@ package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */; productName = secp256k1; }; + 4CCF9AB12A1FE80C00E03CFB /* GSPlayer */ = { + isa = XCSwiftPackageProductDependency; + package = 4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */; + productName = GSPlayer; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f4b491d9..e9bbe6de 100644 --- a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "gsplayer", + "kind" : "remoteSourceControl", + "location" : "https://github.com/wxxsw/GSPlayer.git", + "state" : { + "revision" : "aa6dad7943d52f5207f7fcc2ad3e4274583443b8", + "version" : "0.2.26" + } + }, { "identity" : "kingfisher", "kind" : "remoteSourceControl", diff --git a/damus/Views/Video/VideoPlayer.swift b/damus/Views/Video/VideoPlayer.swift new file mode 100644 index 00000000..c2dfabfc --- /dev/null +++ b/damus/Views/Video/VideoPlayer.swift @@ -0,0 +1,326 @@ +// +// 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) +} + +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 contentMode: UIView.ContentMode = .scaleAspectFill + var time: CMTime = 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() + } +} + +@available(iOS 13, *) +public extension VideoPlayer { + + +} + +@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) + } else { + context.coordinator.stopObserver(uiView: uiView) + } + + if model.size == nil, let size = uiView.player?.currentImage?.size { + model.size = size + } + + 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) + } + + print("intrinsic video size \(uiView.intrinsicContentSize)") + uiView.isMuted = model.muted + uiView.isAutoReplay = model.autoReplay + + if let observerTime = context.coordinator.observerTime, model.time != observerTime { + uiView.seek(to: model.time, toleranceBefore: model.time, toleranceAfter: model.time, 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 + } + + 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 } + + 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 + } + + 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) + } + } +}