From 8de1db1e7a916ad0fde9f599dc7bec0f4b0e07e4 Mon Sep 17 00:00:00 2001 From: lithium03 Date: Wed, 20 Nov 2019 17:48:43 +0900 Subject: [PATCH] v1.4.2(54) Add network cache option. Fix ffplayer bug in position when MPEG-TS wrap around occured. --- .../RemoteCloud.xcodeproj/project.pbxproj | 18 ++ RemoteCloud/RemoteCloud/FileCache.swift | 254 ++++++++++++++++++ RemoteCloud/RemoteCloud/RemoteStorage.swift | 1 + .../Storages/GoogleDriveStorage.swift | 10 + .../cache.xcdatamodel/contents | 16 ++ .../project.pbxproj | 4 +- .../ccViewer/TableViewControllerItems.swift | 1 - .../ccViewer/TableViewControllerSetting.swift | 157 ++++++++++- ccViewer/ccViewer/ViewControllerImage.swift | 2 +- .../ccViewer/ja.lproj/Localizable.strings | 4 + ffplayer/ffplayer/player_base.cpp | 41 +-- ffplayer/ffplayer/player_base.hpp | 1 - 12 files changed, 483 insertions(+), 26 deletions(-) create mode 100644 RemoteCloud/RemoteCloud/FileCache.swift create mode 100644 RemoteCloud/RemoteCloud/cache.xcdatamodeld/cache.xcdatamodel/contents diff --git a/RemoteCloud/RemoteCloud.xcodeproj/project.pbxproj b/RemoteCloud/RemoteCloud.xcodeproj/project.pbxproj index 4cfde2c..ebc6116 100644 --- a/RemoteCloud/RemoteCloud.xcodeproj/project.pbxproj +++ b/RemoteCloud/RemoteCloud.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ 3F3ABABD2273119D002A8D16 /* pCloudStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F3ABABC2273119D002A8D16 /* pCloudStorage.swift */; }; 3F92E7052336BFBA00D1FF9B /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3F92E7042336BFBA00D1FF9B /* Media.xcassets */; }; 3F987DEC2277608A0020A796 /* Cryptomator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F987DEB2277608A0020A796 /* Cryptomator.swift */; }; + 3FA46CE223849615007ACC82 /* FileCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA46CE123849615007ACC82 /* FileCache.swift */; }; + 3FA46CE5238496B7007ACC82 /* cache.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 3FA46CE3238496B7007ACC82 /* cache.xcdatamodeld */; }; 3FE23B1F2340B96200916DA6 /* UploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FE23B1E2340B96200916DA6 /* UploadManager.swift */; }; /* End PBXBuildFile section */ @@ -66,6 +68,8 @@ 3F3ABABC2273119D002A8D16 /* pCloudStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = pCloudStorage.swift; sourceTree = ""; }; 3F92E7042336BFBA00D1FF9B /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = ""; }; 3F987DEB2277608A0020A796 /* Cryptomator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cryptomator.swift; sourceTree = ""; }; + 3FA46CE123849615007ACC82 /* FileCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCache.swift; sourceTree = ""; }; + 3FA46CE4238496B7007ACC82 /* cache.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = cache.xcdatamodel; sourceTree = ""; }; 3FE23B1E2340B96200916DA6 /* UploadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadManager.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -131,6 +135,8 @@ 3F3ABABA2272C707002A8D16 /* Secret.swift */, 3F92E7042336BFBA00D1FF9B /* Media.xcassets */, 3FE23B1E2340B96200916DA6 /* UploadManager.swift */, + 3FA46CE123849615007ACC82 /* FileCache.swift */, + 3FA46CE3238496B7007ACC82 /* cache.xcdatamodeld */, ); path = RemoteCloud; sourceTree = ""; @@ -275,6 +281,7 @@ 3CA2C95E2235569800481DB5 /* cloud.xcdatamodeld in Sources */, 3F3ABABB2272C707002A8D16 /* Secret.swift in Sources */, 3CF14614223B29360025081A /* CryptRclone.swift in Sources */, + 3FA46CE223849615007ACC82 /* FileCache.swift in Sources */, 3CA2C951223495E700481DB5 /* RemoteStorage.swift in Sources */, 3C14C452225CE19E0044E4A7 /* CueSheet.swift in Sources */, 3C0654A122384BED003BD932 /* CryptCarotDAV.swift in Sources */, @@ -287,6 +294,7 @@ 3FE23B1F2340B96200916DA6 /* UploadManager.swift in Sources */, 3C14C454225DBB1A0044E4A7 /* LocalStorage.swift in Sources */, 3CA2C952223495E700481DB5 /* GoogleDriveStorage.swift in Sources */, + 3FA46CE5238496B7007ACC82 /* cache.xcdatamodeld in Sources */, 3C400922224190920028A45A /* OneDriveStorage.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -569,6 +577,16 @@ sourceTree = ""; versionGroupType = wrapper.xcdatamodel; }; + 3FA46CE3238496B7007ACC82 /* cache.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + 3FA46CE4238496B7007ACC82 /* cache.xcdatamodel */, + ); + currentVersion = 3FA46CE4238496B7007ACC82 /* cache.xcdatamodel */; + path = cache.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; /* End XCVersionGroup section */ }; rootObject = 3CA2C9252234952800481DB5 /* Project object */; diff --git a/RemoteCloud/RemoteCloud/FileCache.swift b/RemoteCloud/RemoteCloud/FileCache.swift new file mode 100644 index 0000000..08dd0ab --- /dev/null +++ b/RemoteCloud/RemoteCloud/FileCache.swift @@ -0,0 +1,254 @@ +// +// FileCache.swift +// RemoteCloud +// +// Created by rei8 on 2019/11/20. +// Copyright © 2019 lithium03. All rights reserved. +// + +import Foundation +import CoreData + +public class FileCache { + public var cacheMaxSize: Int { + get { + return UserDefaults.standard.integer(forKey: "networkCacheSize") + } + set { + UserDefaults.standard.set(newValue, forKey: "networkCacheSize") + } + } + + public func getCache(storage: String, id: String, offset: Int64, size: Int64) -> URL? { + var ret: URL? = nil + guard cacheMaxSize > 0 else { + return nil + } + if Thread.isMainThread { + guard let orgItem = CloudFactory.shared.data.getData(storage: storage, fileId: id) else { + return nil + } + + let viewContext = self.persistentContainer.viewContext + + let fetchrequest = NSFetchRequest(entityName: "FileCacheItem") + fetchrequest.predicate = NSPredicate(format: "storage == %@ && id == %@ && chunkOffset == %lld", storage, id, offset) + do{ + guard let item = try viewContext.fetch(fetchrequest).first as? FileCacheItem else { + return nil + } + if orgItem.mdate != item.mdate || orgItem.size != item.orgSize { + if let target = item.filename?.uuidString { + let base = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("NetCache", isDirectory: true) + let target_path = base.appendingPathComponent(String(target.prefix(2)), isDirectory: true).appendingPathComponent(String(target.prefix(4).suffix(2)), isDirectory: true).appendingPathComponent(target) + try FileManager.default.removeItem(at: target_path) + } + viewContext.delete(item) + try viewContext.save() + return nil + } + if let target = item.filename?.uuidString, (size == item.chunkSize || size < 0) { + let base = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("NetCache", isDirectory: true) + let target_path = base.appendingPathComponent(String(target.prefix(2)), isDirectory: true).appendingPathComponent(String(target.prefix(4).suffix(2)), isDirectory: true).appendingPathComponent(target) + if FileManager.default.fileExists(atPath: target_path.path) { + ret = target_path + item.rdate = Date() + try? viewContext.save() + } + else { + viewContext.delete(item) + try viewContext.save() + } + } + } + catch{ + return nil + } + return ret + } + else { + DispatchQueue.main.sync { + ret = getCache(storage: storage, id: id, offset: offset, size: size) + } + return ret + } + } + + public func saveCache(storage: String, id: String, offset: Int64, data: Data) { + guard cacheMaxSize > 0 else { + increseFreeSpace() + return + } + do { + let size = Int64(data.count) + let base = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("NetCache", isDirectory: true) + let newId = UUID() + let target = newId.uuidString + let target_path = base.appendingPathComponent(String(target.prefix(2)), isDirectory: true).appendingPathComponent(String(target.prefix(4).suffix(2)), isDirectory: true) + try FileManager.default.createDirectory(at: target_path, withIntermediateDirectories: true, attributes: nil) + try data.write(to: target_path.appendingPathComponent(target)) + + if getCacheSize() > cacheMaxSize { + increseFreeSpace() + } + + DispatchQueue.main.async { + guard let orgItem = CloudFactory.shared.data.getData(storage: storage, fileId: id) else { + try? FileManager.default.removeItem(at: target_path.appendingPathComponent(target)) + return + } + + let viewContext = self.persistentContainer.viewContext + + let fetchrequest = NSFetchRequest(entityName: "FileCacheItem") + fetchrequest.predicate = NSPredicate(format: "storage == %@ && id == %@ && chunkOffset == %lld && chunkSize == %lld", storage, id, offset, size) + do { + if let items = try viewContext.fetch(fetchrequest) as? [FileCacheItem], items.count > 0 { + try FileManager.default.removeItem(at: target_path.appendingPathComponent(target)) + return + } + } + catch { + print(error) + } + + let newitem = FileCacheItem(context: viewContext) + newitem.filename = newId + newitem.storage = storage + newitem.id = id + newitem.chunkSize = size + newitem.chunkOffset = offset + newitem.rdate = Date() + newitem.mdate = orgItem.mdate + newitem.orgSize = orgItem.size + + try? viewContext.save() + } + } + catch { + print(error) + } + } + + public func increseFreeSpace() { + guard let attributes = try? FileManager.default.attributesOfFileSystem(forPath: NSTemporaryDirectory()) else { + return + } + let freesize = (attributes[.systemFreeSize] as? NSNumber)?.int64Value ?? 0 + var incSize = getCacheSize() - cacheMaxSize + if freesize < 512*1024*1024 { + let incSize2 = 512*1024*1024 - Int(freesize) + if incSize < 0 { + incSize = incSize2 + } + else { + incSize += incSize2 + } + } + var delSize = 0 + guard let base = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("NetCache", isDirectory: true) else { + return + } + DispatchQueue.main.async { + let viewContext = self.persistentContainer.viewContext + + let fetchrequest = NSFetchRequest(entityName: "FileCacheItem") + fetchrequest.predicate = NSPredicate(value: true) + fetchrequest.sortDescriptors = [NSSortDescriptor(key: "rdate", ascending: true)] + do { + if let items = try viewContext.fetch(fetchrequest) as? [FileCacheItem], items.count > 0 { + for item in items { + if delSize > incSize { + break + } + guard let target = item.filename?.uuidString else { + continue + } + let target_path = base.appendingPathComponent(String(target.prefix(2)), isDirectory: true).appendingPathComponent(String(target.prefix(4).suffix(2)), isDirectory: true).appendingPathComponent(target) + try FileManager.default.removeItem(at: target_path) + delSize += Int(item.chunkSize) + viewContext.delete(item) + } + try viewContext.save() + } + } + catch { + print(error) + } + } + } + + public func getCacheSize() -> Int { + guard let base = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("NetCache", isDirectory: true) else { + return 0 + } + + let resourceKeys = Set([.fileAllocatedSizeKey]) + let directoryEnumerator = FileManager.default.enumerator(at: base, includingPropertiesForKeys: Array(resourceKeys), options: .skipsHiddenFiles)! + + var allocSize = 0 + for case let fileURL as URL in directoryEnumerator { + guard let resourceValues = try? fileURL.resourceValues(forKeys: resourceKeys), + let size = resourceValues.fileAllocatedSize + else { + continue + } + allocSize += size + } + return allocSize + } + + // MARK: - Core Data stack + public lazy var persistentContainer: NSPersistentContainer = { + /* + The persistent container for the application. This implementation + creates and returns a container, having loaded the store for the + application to it. This property is optional since there are legitimate + error conditions that could cause the creation of the store to fail. + */ + let modelURL = Bundle(for: CloudFactory.self).url(forResource: "cache", withExtension: "momd")! + let mom = NSManagedObjectModel(contentsOf: modelURL)! + + let container = NSPersistentContainer(name: "cache", managedObjectModel: mom) + let location = container.persistentStoreDescriptions.first!.url! + let description = NSPersistentStoreDescription(url: location) + description.shouldInferMappingModelAutomatically = true + description.shouldMigrateStoreAutomatically = true + description.setOption(FileProtectionType.completeUnlessOpen as NSObject, forKey: NSPersistentStoreFileProtectionKey) + container.persistentStoreDescriptions = [description] + + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + + /* + Typical reasons for an error here include: + * The parent directory does not exist, cannot be created, or disallows writing. + * The persistent store is not accessible, due to permissions or data protection when the device is locked. + * The device is out of space. + * The store could not be migrated to the current model version. + Check the error message to determine what the actual problem was. + */ + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + return container + }() + + // MARK: - Core Data Saving support + + public func saveContext () { + let context = persistentContainer.viewContext + if context.hasChanges { + do { + try context.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + let nserror = error as NSError + fatalError("Unresolved error \(nserror), \(nserror.userInfo)") + } + } + } +} diff --git a/RemoteCloud/RemoteCloud/RemoteStorage.swift b/RemoteCloud/RemoteCloud/RemoteStorage.swift index 4046779..3fcecdf 100644 --- a/RemoteCloud/RemoteCloud/RemoteStorage.swift +++ b/RemoteCloud/RemoteCloud/RemoteStorage.swift @@ -233,6 +233,7 @@ public class CloudFactory { } public let data = dataItems() + public let cache = FileCache() public func getIcon(service: CloudStorages) -> UIImage? { switch service { diff --git a/RemoteCloud/RemoteCloud/Storages/GoogleDriveStorage.swift b/RemoteCloud/RemoteCloud/Storages/GoogleDriveStorage.swift index 9531135..4214fb6 100644 --- a/RemoteCloud/RemoteCloud/Storages/GoogleDriveStorage.swift +++ b/RemoteCloud/RemoteCloud/Storages/GoogleDriveStorage.swift @@ -686,6 +686,13 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess } override func readFile(fileId: String, start: Int64? = nil, length: Int64? = nil, callCount: Int = 0, onFinish: ((Data?) -> Void)?) { + if let cache = CloudFactory.shared.cache.getCache(storage: storageName!, id: fileId, offset: start ?? 0, size: length ?? -1) { + if let data = try? Data(contentsOf: cache) { + os_log("%{public}@", log: log, type: .debug, "hit cache(google:\(storageName ?? "") \(fileId) \(start ?? -1) \(length ?? -1) \((start ?? 0) + (length ?? 0))") + onFinish?(data) + return + } + } if lastCall.timeIntervalSinceNow > -callWait || callSemaphore.wait(wallTimeout: .now()+Double.random(in: 0.. 0 { cancelTime = Date(timeIntervalSinceNow: 0.5) @@ -748,6 +755,9 @@ public class GoogleDriveStorage: NetworkStorage, URLSessionTaskDelegate, URLSess return } } + if let d = data { + CloudFactory.shared.cache.saveCache(storage: self.storageName!, id: fileId, offset: start ?? 0, data: d) + } onFinish?(data) } task.resume() diff --git a/RemoteCloud/RemoteCloud/cache.xcdatamodeld/cache.xcdatamodel/contents b/RemoteCloud/RemoteCloud/cache.xcdatamodeld/cache.xcdatamodel/contents new file mode 100644 index 0000000..7121432 --- /dev/null +++ b/RemoteCloud/RemoteCloud/cache.xcdatamodeld/cache.xcdatamodel/contents @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ccViewer/CryptCloudViewer.xcodeproj/project.pbxproj b/ccViewer/CryptCloudViewer.xcodeproj/project.pbxproj index 8b5395a..47a8a6b 100644 --- a/ccViewer/CryptCloudViewer.xcodeproj/project.pbxproj +++ b/ccViewer/CryptCloudViewer.xcodeproj/project.pbxproj @@ -813,7 +813,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = ccViewer/ccViewer.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; DEVELOPMENT_TEAM = 7A9X38B4YU; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; @@ -848,7 +848,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = ccViewer/ccViewer.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; DEVELOPMENT_TEAM = 7A9X38B4YU; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; diff --git a/ccViewer/ccViewer/TableViewControllerItems.swift b/ccViewer/ccViewer/TableViewControllerItems.swift index a80c83e..7797e9b 100644 --- a/ccViewer/ccViewer/TableViewControllerItems.swift +++ b/ccViewer/ccViewer/TableViewControllerItems.swift @@ -1696,7 +1696,6 @@ class CustomPresentationController: UIPresentationController { } overlayView.frame = containerView.bounds - //overlayView.gestureRecognizers = [UITapGestureRecognizer(target: self, action: #selector(CustomPresentationController.overlayViewDidTouch(_:)))] overlayView.backgroundColor = .black overlayView.alpha = 0.0 containerView.insertSubview(overlayView, at: 0) diff --git a/ccViewer/ccViewer/TableViewControllerSetting.swift b/ccViewer/ccViewer/TableViewControllerSetting.swift index 37d9634..94c5411 100644 --- a/ccViewer/ccViewer/TableViewControllerSetting.swift +++ b/ccViewer/ccViewer/TableViewControllerSetting.swift @@ -7,6 +7,7 @@ // import UIKit +import RemoteCloud class TableViewControllerSetting: UITableViewController, UITextFieldDelegate { @@ -77,7 +78,8 @@ class TableViewControllerSetting: UITableViewController, UITextFieldDelegate { NSLocalizedString("Partial Play", comment: ""), //6 NSLocalizedString("Player control", comment: ""), //7 NSLocalizedString("Cast converter", comment: ""), //8 - NSLocalizedString("Help", comment: "") //9 + NSLocalizedString("Network cache", comment: ""), //9 + NSLocalizedString("Help", comment: "") //10 ] let settings = [["Password"], [NSLocalizedString("Run one more", comment: "")], @@ -99,6 +101,8 @@ class TableViewControllerSetting: UITableViewController, UITextFieldDelegate { NSLocalizedString("Keep open when done", comment: "")], [NSLocalizedString("Ignore overlay subtiles", comment: ""), NSLocalizedString("Auto select streams", comment: "")], + [NSLocalizedString("Current cache size", comment: ""), + NSLocalizedString("Cache limit", comment: "")], [NSLocalizedString("View online help", comment: ""), NSLocalizedString("View privacy policy", comment: ""), NSLocalizedString("Version", comment: "")] @@ -275,6 +279,30 @@ class TableViewControllerSetting: UITableViewController, UITextFieldDelegate { } cell.accessoryView = aSwitch case 9: + switch indexPath.row { + case 0: + let formatter2 = ByteCountFormatter() + formatter2.allowedUnits = [.useAll] + formatter2.countStyle = .file + let s2 = formatter2.string(fromByteCount: Int64(CloudFactory.shared.cache.getCacheSize())) + cell.detailTextLabel?.text = s2 + case 1: + let limit = CloudFactory.shared.cache.cacheMaxSize + if limit > 0 { + let formatter2 = ByteCountFormatter() + formatter2.allowedUnits = [.useAll] + formatter2.countStyle = .file + let s2 = formatter2.string(fromByteCount: Int64(limit)) + cell.detailTextLabel?.text = s2 + } + else { + cell.detailTextLabel?.text = NSLocalizedString("Not use", comment: "") + } + cell.accessoryType = .disclosureIndicator + default: + break + } + case 10: switch indexPath.row { case 0...1: cell.accessoryType = .detailButton @@ -339,6 +367,35 @@ class TableViewControllerSetting: UITableViewController, UITextFieldDelegate { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { self.tableView.deselectRow(at: indexPath, animated: true) self.view.endEditing(true) + + switch indexPath.section { + case 9: + switch indexPath.row { + case 1: + let contentVC = SizePickerViewController() + contentVC.modalPresentationStyle = .popover + contentVC.preferredContentSize = CGSize(width: 300, height: 200) + var rect = tableView.rectForRow(at: indexPath) + rect = CGRect(x: view.frame.width - 30, y: rect.minY, width: 30, height: rect.height) + contentVC.popoverPresentationController?.sourceRect = rect + contentVC.popoverPresentationController?.sourceView = view + contentVC.popoverPresentationController?.permittedArrowDirections = .any + contentVC.popoverPresentationController?.delegate = self + contentVC.initalValue = CloudFactory.shared.cache.cacheMaxSize + contentVC.onSelect = { size in + CloudFactory.shared.cache.cacheMaxSize = size + CloudFactory.shared.cache.increseFreeSpace() + DispatchQueue.main.asyncAfter(deadline: .now()+2) { + self.tableView.reloadData() + } + } + present(contentVC, animated: true, completion: nil) + default: + break + } + default: + break + } } override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { @@ -352,7 +409,7 @@ class TableViewControllerSetting: UITableViewController, UITextFieldDelegate { default: break } - case 9: + case 10: switch indexPath.row { case 0: let url = URL(string: NSLocalizedString("Online help URL", comment: ""))! @@ -391,5 +448,101 @@ class TableViewControllerSetting: UITableViewController, UITextFieldDelegate { // Pass the selected object to the new view controller. } */ +} + +extension TableViewControllerSetting: UIPopoverPresentationControllerDelegate { + + // for iPhone + func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { + return .none + } +} + +class SizePickerViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource { + + let sizeKey = [ + 0: NSLocalizedString("Not use", comment: ""), + 1*1000*1000: "1 MB", + 5*1000*1000: "5 MB", + 10*1000*1000: "10 MB", + 50*1000*1000: "50 MB", + 100*1000*1000: "100 MB", + 200*1000*1000: "200 MB", + 500*1000*1000: "500 MB", + 1000*1000*1000: "1 GB", + 2*1000*1000*1000: "2 GB", + 3*1000*1000*1000: "3 GB", + 5*1000*1000*1000: "5 GB", + 10*1000*1000*1000: "10 GB", + 15*1000*1000*1000: "15 GB", + 20*1000*1000*1000: "20 GB", + 25*1000*1000*1000: "25 GB", + 30*1000*1000*1000: "30 GB", + 40*1000*1000*1000: "40 GB", + 50*1000*1000*1000: "50 GB", + ] + + var initalValue = 0 + var onSelect: ((Int)->Void)? + + override func viewDidLoad() { + super.viewDidLoad() + + if #available(iOS 13.0, *) { + view.backgroundColor = .systemBackground + } else { + view.backgroundColor = .white + } + let pickerView = UIPickerView() + pickerView.delegate = self + pickerView.dataSource = self + view.addSubview(pickerView) + + pickerView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor).isActive = true + pickerView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor).isActive = true + pickerView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = true + pickerView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor).isActive = true + + let val = sizeKey.keys.sorted() + if let idx = val.firstIndex(where: { $0 >= initalValue }) { + pickerView.selectRow(idx, inComponent: 0, animated: false) + } + } + + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 1 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + return sizeKey.count + } + + func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView { + + let pickerLabel: UILabel + if let v = view as? UILabel { + pickerLabel = v + } + else { + pickerLabel = UILabel() + } + pickerLabel.textAlignment = .center + if #available(iOS 13.0, *) { + pickerLabel.font = .monospacedSystemFont(ofSize: 24, weight: .regular) + } + else { + pickerLabel.font = .monospacedDigitSystemFont(ofSize: 24, weight: .regular) + } + let key = sizeKey.keys.sorted()[row] + pickerLabel.text = sizeKey[key] + return pickerLabel + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + let val = sizeKey.keys.sorted()[row] + onSelect?(val) + } } + + diff --git a/ccViewer/ccViewer/ViewControllerImage.swift b/ccViewer/ccViewer/ViewControllerImage.swift index 5012c97..4b34c88 100644 --- a/ccViewer/ccViewer/ViewControllerImage.swift +++ b/ccViewer/ccViewer/ViewControllerImage.swift @@ -306,7 +306,7 @@ class ViewControllerImage: UIViewController, UIScrollViewDelegate, UIDocumentInt guard let attributes = try? FileManager.default.attributesOfFileSystem(forPath: NSTemporaryDirectory()) else { return } - let freesize = (attributes[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value ?? 0 + let freesize = (attributes[.systemFreeSize] as? NSNumber)?.int64Value ?? 0 if items[itemIdx].size >= freesize { let alart = UIAlertController(title: "No storage", message: "item is too big", preferredStyle: .alert) diff --git a/ccViewer/ccViewer/ja.lproj/Localizable.strings b/ccViewer/ccViewer/ja.lproj/Localizable.strings index 6a0ab59..e7ec545 100644 --- a/ccViewer/ccViewer/ja.lproj/Localizable.strings +++ b/ccViewer/ccViewer/ja.lproj/Localizable.strings @@ -83,3 +83,7 @@ "Ignore overlay subtiles" = "焼き込み字幕を入れない"; "Auto select streams" = "自動で映像と焼き込み字幕を選択する"; +"Network cache" = "通信キャッシュ設定"; +"Not use" = "使用しない"; +"Current cache size" = "使用キャッシュ量"; +"Cache limit" = "最大キャッシュサイズ"; diff --git a/ffplayer/ffplayer/player_base.cpp b/ffplayer/ffplayer/player_base.cpp index 734248d..5d44ed8 100644 --- a/ffplayer/ffplayer/player_base.cpp +++ b/ffplayer/ffplayer/player_base.cpp @@ -339,7 +339,7 @@ int decode_thread(struct stream_param *stream) // if no chapter, skip to start if(!player->pFormatCtx->nb_chapters) { av_log(NULL, AV_LOG_INFO, "go back to start\n"); - player->seek_pos = player->start_time_org; + player->seek_pos = (player->pFormatCtx->start_time == AV_NOPTS_VALUE)? 0 : player->pFormatCtx->start_time; player->seek_req_type = Player::seek_type_pos; continue; } @@ -358,7 +358,7 @@ int decode_thread(struct stream_param *stream) i--; if(i < 0) { av_log(NULL, AV_LOG_INFO, "go back to start\n"); - player->seek_pos = player->start_time_org; + player->seek_pos = (player->pFormatCtx->start_time == AV_NOPTS_VALUE)? 0 : player->pFormatCtx->start_time; player->seek_req_type = Player::seek_type_pos; } else { @@ -452,16 +452,10 @@ int decode_thread(struct stream_param *stream) continue; } error = false; - if (player->start_time_org == AV_NOPTS_VALUE) { - if(player->pFormatCtx->start_time != AV_NOPTS_VALUE) - player->start_time_org = player->pFormatCtx->start_time; - else - player->start_time_org = 0; - } if (isnan(player->master_clock_offset)) { player->master_clock_start = av_gettime(); AVRational timebase = { 1, AV_TIME_BASE }; - player->master_clock_offset = packet.pts * av_q2d(player->pFormatCtx->streams[packet.stream_index]->time_base) - player->start_time_org * av_q2d(timebase); + player->master_clock_offset = packet.pts * av_q2d(player->pFormatCtx->streams[packet.stream_index]->time_base) - ((player->pFormatCtx->start_time == AV_NOPTS_VALUE)? 0 : player->pFormatCtx->start_time) * av_q2d(timebase); } if (packet.stream_index == player->video.videoStream) { @@ -913,7 +907,7 @@ int video_thread(Player *is) int64_t pts_t; if ((pts_t = frame.best_effort_timestamp) != AV_NOPTS_VALUE) { pts = pts_t * av_q2d(is->video.video_st->time_base); - pts -= av_q2d(AV_TIME_BASE_Q) * is->start_time_org; + pts -= av_q2d(AV_TIME_BASE_Q) * ((is->pFormatCtx->start_time == AV_NOPTS_VALUE)? 0 : is->pFormatCtx->start_time); //av_log(NULL, AV_LOG_INFO, "video clock %f\n", pts); if (isnan(is->video.video_clock_start)) { @@ -1161,7 +1155,11 @@ void Player::video_display(VideoPicture *vp) if (vp->allocated) { subtitle_display(vp); struct stream_param *stream = (struct stream_param *)param; - stream->draw_pict(stream->stream, vp->bmp.data[0], vp->width, vp->height, vp->bmp.linesize[0], get_master_clock()); + double clock = get_master_clock(); + while (clock > 0x1FFFFFFFF / 90000.0) { + clock -= 0x1FFFFFFFF / 90000.0; + } + stream->draw_pict(stream->stream, vp->bmp.data[0], vp->width, vp->height, vp->bmp.linesize[0], clock); } } @@ -1242,7 +1240,11 @@ double Player::load_sound(float *buffer, int num_packet) memset(buffer, 0, sizeof(float)*num_packet*ch); audio_last_call = av_gettime(); audio_clock_base = audio.pts_base - (double)(audio.write_idx - audio.read_idx) / audio.sample_rate; - return audio_clock_base; + double clock = audio_clock_base; + while (clock > 0x1FFFFFFFF / 90000.0) { + clock -= 0x1FFFFFFFF / 90000.0; + } + return clock; } int offset = audio.read_idx % audio.buf_length; @@ -1265,6 +1267,10 @@ double Player::load_sound(float *buffer, int num_packet) audio_last_call = av_gettime(); audio_clock_base = audio.pts_base - (double)(audio.write_idx - audio.read_idx) / audio.sample_rate; } + double clock = audio_clock_base; + while (clock > 0x1FFFFFFFF / 90000.0) { + clock -= 0x1FFFFFFFF / 90000.0; + } struct stream_param *stream = (struct stream_param *)param; if (!isnan(stream->play_duration)) { @@ -1272,12 +1278,12 @@ double Player::load_sound(float *buffer, int num_packet) if (!isnan(stream->start_skip)) { s = stream->start_skip; } - if (audio_clock_base - s > stream->play_duration) { + if (clock - s > stream->play_duration) { Quit(); } } - return audio_clock_base; + return clock; } static inline @@ -1398,7 +1404,7 @@ void audio_thread(Player *is) double pts = NAN; if (pts_t != AV_NOPTS_VALUE) { pts = av_q2d(is->audio.audio_st->time_base)*pts_t; - pts -= av_q2d(AV_TIME_BASE_Q) * is->start_time_org; + pts -= av_q2d(AV_TIME_BASE_Q) * ((is->pFormatCtx->start_time == AV_NOPTS_VALUE)? 0 : is->pFormatCtx->start_time); } //av_log(NULL, AV_LOG_INFO, "audio pts %f\n", pts); @@ -1527,10 +1533,7 @@ void Player::clear_soundbufer() void Player::seek(int64_t pos) { - if(start_time_org == AV_NOPTS_VALUE) - return; - - seek_pos = pos + start_time_org; + seek_pos = pos + ((pFormatCtx->start_time == AV_NOPTS_VALUE)? 0 : pFormatCtx->start_time); seek_req_type = seek_type_pos; } diff --git a/ffplayer/ffplayer/player_base.hpp b/ffplayer/ffplayer/player_base.hpp index 3a4589d..a4e274f 100644 --- a/ffplayer/ffplayer/player_base.hpp +++ b/ffplayer/ffplayer/player_base.hpp @@ -161,7 +161,6 @@ class Player { } VideoInfo; VideoInfo video; - int64_t start_time_org = AV_NOPTS_VALUE; enum seek_type { seek_type_none, seek_type_pos,