Post

Replies

Boosts

Views

Activity

Can I use AVPlayerViewController with AVPlayerLooper?
I want to use AVPlayerViewController to display the video but it should be in auto-play mode. Previously I was using AVPlayer for that and listening to the .AVPlayerItemDidPlayToEndTime notification but I wonder if there is a better way? eg. using AVPlayerLooper for instance so I don't have to use that .AVPlayerItemDidPlayToEndTime anymore I wrote something like this but it is not working - I have a black screen with video controls - probably because AVPlayerViewController does not have any playable content... struct VideoPlayerQueuedView: UIViewControllerRepresentable { let videoUrl: URL func makeUIViewController(context: Context) -> AVPlayerViewController { let queuePlayer = AVQueuePlayer() let playerViewController = AVPlayerViewController() // Create an AVPlayerItem from the videoUrl let playerItem = AVPlayerItem(url: videoUrl) // Create an AVPlayerLooper with the queuePlayer and the playerItem as the template item let playerLooper = AVPlayerLooper(player: queuePlayer, templateItem: playerItem) // Set the player property of AVPlayerViewController playerViewController.player = queuePlayer return playerViewController } func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { // Update the video player if needed } }
0
0
723
Jun ’23
SwiftUI - observing AVPlayer playback state
I am learning SwiftUI, I want to observe an AVPlayer status so I know when the videos is paused or not. My current approach is more less like this: I have VideosView that holds a list of a videos (in ZStack cards). VideoViews has a VideosViewModel. in VideosView i am calling in onAppear VideosViewModel.getItems... struct ItemModel: Identifiable, Codable, Hashable, Equatable { var id: String var author: String // video owner var url: URL? // url to the video var player: AVPlayer? // AVPlayer created based on self.url... mutating func setPlayer(_ avPlayer: AVPlayer) { self.player = avPlayer } } // vm class FeedViewModel: ObservableObject { @Published private(set) var items: [ItemModel] = [] func getItems() async { do { // fetch data from the API let data = try await dataService.fetchFeeds() // download and attach videos downloadFeedVideos(data) } catch { // .... } } private func downloadFeedVideos(_ feeds: [ItemModel]) { for index in feeds.indices { var item = feeds[index] if let videoURL = item.url { self.downloadQueue.queueDownloadIfFileNotExists( videoURL, DownloadOperation( session: URLSession.shared, downloadTaskURL: videoURL, completionHandler: { [weak self] (localURL, response, error) in guard let tempUrl = localURL else { return } let saveResult = self?.fileManagerService.saveInTemp(tempUrl, fileName: videoURL.lastPathComponent) switch saveResult { case .success(let savedURL): DispatchQueue.main.async { // maybe this is a wrong place to have it? item.setPlayer(AVPlayer(url: savedURL)) self?.items.append(item) if self?.items.count ?? 0 > 1 { // once first video is downloaded, use all device cores to fetch next videos // all newest iOS devices has 6 cores self?.downloadQueue.setMaxConcurrentOperationCount(.max) } } case .none: break case .failure(_): EventTracking().track("Video download fail", [ "id": item.id, "ulr": videoURL.absoluteString.decodeURL() ]) } }), { fileCacheURL in // file already downloaded DispatchQueue.main.async { item.setPlayer(AVPlayer(url: fileCacheURL)) self.items.append(item) } }) } } } } I found this article with some pseudo-code of how to track video playback state but I'm not sure how to implement it in my code.... https://developer.apple.com/documentation/avfoundation/media_playback/observing_playback_state
1
0
997
May ’23
Why AVPlayer returns video duration (seconds) as Double?
I need to get video duration in seconds. If I use code below guard let playerItem = self.player?.currentItem else { return 0 } print("playerItem.duration", playerItem.duration, playerItem.duration.seconds, playerItem.duration.toString) it returns me a double value 37.568 CMTime(value: 37568, timescale: 1000, flags: __C.CMTimeFlags(rawValue: 1), epoch: 0) 37.568 00:00:37 I wonder why it's a 37.568 instead of 37.000? if video is 00:00:37? Does it means, it is a 37,5s long?
2
0
735
May ’23
How to get enum associated type?
I have an API endpoint that returns array of various items, so I have created an enum that can hold those items and therefore I can create an array of them - more less like below let arr: [CollectableEnum] = [ .feed(.init(id: "1")), .clinic(.init(id: "1")), .feed(.init(id: "2")), ] Now I need to iterate through them and display properly in SwiftUI I wonder, how to get an value from that enum, so I can get an ItemModel rather than CollectableEnum that holds that ItemModel enum code: enum CollectableEnum: Identifiable, Hashable { case feed(ItemModel) case clinic(ClinicModel) var id: String { switch self { case .feed(let feed): return feed.id case .clinic(let clinic): return clinic.id } } // what return type should I put here? var associatedValue { switch self { case .feed(let feed): return feed case .clinic(let clinic): return clinic } } public func hash(into hasher: inout Hasher) { return hasher.combine(self) } }
1
0
716
May ’23
Help me converting completionHandler func to async/await
So I have a class like below class DownloadOperation: Operation { private var task : URLSessionDownloadTask! enum OperationState : Int { case ready case executing case finished } // default state is ready (when the operation is created) private var state : OperationState = .ready { willSet { self.willChangeValue(forKey: "isExecuting") self.willChangeValue(forKey: "isFinished") } didSet { self.didChangeValue(forKey: "isExecuting") self.didChangeValue(forKey: "isFinished") } } override var isReady: Bool { return state == .ready } override var isExecuting: Bool { return state == .executing } override var isFinished: Bool { return state == .finished } init( session: URLSession, downloadTaskURL: URL, item: ItemModel, completionHandler: ((URL?, URLResponse?, ItemModel, Error?) -> Void)? ) { super.init() // use weak self to prevent retain cycle task = session.downloadTask( with: downloadTaskURL, completionHandler: { [weak self] (localURL, response, error) in /* if there is a custom completionHandler defined, pass the result gotten in downloadTask's completionHandler to the custom completionHandler */ if let completionHandler = completionHandler { // localURL is the temporary URL the downloaded file is located completionHandler(localURL, response, item, error) } /* set the operation state to finished once the download task is completed or have error */ self?.state = .finished }) } override func start() { /* if the operation or queue got cancelled even before the operation has started, set the operation state to finished and return */ if(self.isCancelled) { state = .finished return } // set the state to executing state = .executing // start the downloading self.task.resume() } override func cancel() { super.cancel() // cancel the downloading self.task.cancel() } } I would like to call it in async func, but I'm having difficulties with converting it to asyn func func getItemsAsync() async { requestStatus = .pending do { let feedsData = try await dataService.fetchAllFeedsAsync() for index in feedsData.indices { var item = feedsData[index] // make sure Item has a URL guard let videoURL = item.url else { return } let operation = DownloadOperation( session: URLSession.shared, downloadTaskURL: videoURL, item: item, completionHandler: { [weak self] (localURL, response, item, error) in guard let tempUrl = localURL else { return } let saveResult = self?.fileManagerService.saveInTemp(tempUrl, fileName: videoURL.lastPathComponent) switch saveResult { case .success(let savedURL): let newItem: ItemModel = .init( id: item.id, player: AVPlayer(url: savedURL) ) await MainActor.run(body: { self?.items.append(newItem) if items.count ?? 0 > 1 { // once first video is downloaded, use all device cores to fetch next videos // all newest iOS devices has 6 cores downloadQueue.setMaxConcurrentOperationCount(.max) } }) case .none: break case .failure(_): EventTracking().track("Video download fail", [ "id": item.id, "ulr": videoURL.absoluteString.decodeURL() ]) } }) let fileCaheURL = downloadQueue.queueDownloadIfFileNotExists2(videoURL, operation) if let fileCaheURL = fileCaheURL { // ... do some other magic } } } catch let error { requestStatus = .error errorMessage = error.localizedDescription } }
1
0
565
May ’23
Help me understand .onAppear/.task aka app authentication state
I am trying to make simple app auth. In ContentView I have two tabs, each tab will have their own .task{} in which I will make an HTTP request to obtain some data. The problem that I am facing is when you open the app, switch tabs and tap "toggle" at the top, you will see in the console that both .task{} and .onAppear{} has been called event tho these views wren't visible at all. Why? And how to make API calls only when app states is logged As a workaround I could add extra check in each .task to check the app state but it doesn't seems right... import SwiftUI @main struct myApp: App { @StateObject private var auth: Auth = Auth() var body: some Scene { WindowGroup { if auth.isLogged { ContentView() .environmentObject(auth) } else { VStack { Button("toggle auth") { auth.isLogged.toggle() } } } } } } @MainActor class Auth: ObservableObject { @Published var isLogged: Bool = false } struct ContentView: View { // Annimatin Properties @State private var expanded: Bool = false @Namespace private var animation @EnvironmentObject var auth: Auth var body: some View { Button("toggle") { self.auth.isLogged.toggle() } TabView { SampleTab("Listen Now", "play.circle.fill") SampleTab("Browse", "square.grid.2x2.fill") } } @ViewBuilder func CustomBottomSheet() -> some View { // Animating sheet backgrund (To look like it's expanding from the Bottom) VStack { Text("Hello") } .frame(height: 70) .overlay(alignment: .bottom, content: { Rectangle() .fill(.gray.opacity(0.3)) .frame(height: 1) }) .offset(y: -49) } @ViewBuilder func SampleTab(_ title: String, _ icon: String) -> some View { // iOS Bug, it can be avoided by wrapping the view inside ScrollView ScrollView(.vertical, showsIndicators: false, content: { Text(title) .padding(.top, 25) }) .tabItem { Image(systemName: icon) Text(title) } .onAppear { print("======= onappear \(title)") } .task { // I am gonna make an HTPP request here... print("======= task \(title)") } } }
1
0
758
Apr ’23
Where to store/cache downloaded videos - TikTok clone
I wonder how TikTok/Instagram caches the video so the playing is so smooth. I was thinking about downloading them on a sequence and allow user to skip n video only when n+1 is downloaded. But where should I store/download it? Should I use NACache for that or just save them in app temp with FileManager? Also some pseudo-code of how to do that with SwiftUI might be very helpful. 🙏🏼
1
0
774
Apr ’23
Swiftui - ZStack + DragGesture - how to remove first item
So I have created simple code like below. I have 2 problems: first is when you try do drag first video it is not being removed, instead last one (from the background) is. I have a Task in .onAppear which download all videos at the same time. How to make is sync, so next video will be downloaded only when first one is completed? import SwiftUI import AVKit struct Video: Identifiable { var id: Int var cacheName: String var reply: Bool } struct PlayerView: View { @Binding var data: [Video] @State private var offset = CGSize.zero @State private var video: Video? var body: some View { ZStack { ForEach(self.data.indices.reversed(), id: \.self) { index in let i = self.data[index] if index < 3 { if let player = CacheManager.shared.get(name: i.cacheName) { Player(player: player) .frame(width: 300, height: 300) .overlay { Text("\(index) \(i.id)").foregroundColor(.blue) } .scaleEffect(1 - 0.05 * CGFloat(index), anchor: .top) .offset( x: 0, y: ({ if (index > 0) { return CGFloat(index) * -5.5 } //not a first card, or swiping up if offset.height < 0 { return 0 } // swiping down return offset.height * 1.1 }()) ) .onTapGesture(perform: { print("tapped", index) if index == 0 { self.video = i } }) .gesture( DragGesture() .onChanged { gesture in offset = gesture.translation } .onEnded { value in if abs(offset.height) > 100 && index == 0 { withAnimation { print("iiii", i) // <<<<<< this function removes invalid element from the array, WHY? self.data.removeAll { $0.cacheName == i.cacheName } offset = .zero } } else { offset = .zero } }) .onAppear { if index == 0 { player.play() } else { player.pause() } } .onDisappear { player.pause() } } else { Text("not found") } } else { EmptyView() } } } .fullScreenCover(item: $video, content: { video in if let player = CacheManager.shared.get(name: video.cacheName) { ZStack { Player(player: player) .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height) .ignoresSafeArea() VStack { Button(action: { player.seek(to: .zero, completionHandler: {_ in player.play() }) }, label: { Text("PLAY").foregroundColor(.black) }) Button(action: { self.video = nil }, label: { Text("CLOSE").foregroundColor(.black) }) } } } else { Text("none") } }) } } struct Player: UIViewControllerRepresentable { var player: AVPlayer func makeUIViewController(context: Context) -> AVPlayerViewController { let view = AVPlayerViewController() view.player = player view.showsPlaybackControls = false view.videoGravity = .resizeAspectFill return view } func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { } } let urls = [ "https://dnr07ippqik25.cloudfront.net/01GXX7WFGJVZ11RCC6Q7XMYH02.mp4", "https://dnr07ippqik25.cloudfront.net/01GXX7D1MJRNG10BT16EV3RF4Z.mp4", "https://dnr07ippqik25.cloudfront.net/01GXX7FXXHPP3WGX7ZY5SCXCC5.mp4", "https://dnr07ippqik25.cloudfront.net/01GXX7F5R5MTEF6AWTRA806SAZ.mp4" ] struct ContentView: View { @State var data: [Video] = [] private let cm = CacheManager.shared var body: some View { PlayerView(data: $data) .onAppear { urls.indices.forEach { index in Task { do { let url = URL(string: urls[index])! let folder = try! FileManager.default .url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) let destination = folder.appendingPathComponent(url.lastPathComponent) if cm.get(name: urls[index]) != nil { data.append(.init(id: index, cacheName: urls[index], reply: false)) } else { try await downloadFiles(url: url, destination: destination) cm.add(player: AVPlayer(url: destination), name: urls[index]) data.append(.init(id: index, cacheName: urls[index], reply: false)) } } catch { print(error) } } } } } } func downloadFiles(url: URL, destination: URL) async throws { if !FileManager.default.fileExists(atPath: destination.path) { let (source, _) = try await URLSession.shared.download(from: url) try FileManager.default.moveItem(at: source, to: destination) } else { print("file already downloaded") } } class CacheManager { static let shared = CacheManager() private init() {} var avPlayerCache: NSCache<NSString, AVPlayer> = { let cache = NSCache<NSString, AVPlayer>() cache.countLimit = 50 cache.totalCostLimit = 1024 * 1024 * 100 // 100MB return cache }() func add(player: AVPlayer, name: String) { avPlayerCache.setObject( player, forKey: name as NSString ) } func remove(name: String) { avPlayerCache.removeObject(forKey: name as NSString) } func get(name: String) -> AVPlayer? { return avPlayerCache.object(forKey: name as NSString) } }
0
0
741
Apr ’23