Ffmpeg for use in iOS application coded in Swift - swift

I have been browsing the web, even this website... bu cannot find a good option to implement ffmpeg functionality in an iOS application made in swift.
Options looked at and reasons why they are not solutions:
SwiftFFmpeg - I am not sure it can run on iOS, plus I don't see an option to run my desired Ffmpeg command.
MobileFFmpeg - Not maintained anymore, and a new project by the same founders created a "better altenrative" called ffmpeg-kit.
ffmpeg-kit - looks amazing, but their API only allows for interaction in the Objective-C language.
Any solutions anyone can give me?

First...
Make sure you understand what "packages" are available. You can build it yourself, but to be honest, unless you have a very specific reason, I'd just use the pre-build packages. See "8. Packages" of the ffmpeg-kit README.MD to see what's on offer
Second
Go to the Releases and find the version you're interested (I used FFmpegKit Native v4.5.1), scroll down and expand the "Assets" list for the release you're interested in.
For this experiment I used ffmpeg-kit-full-4.5.1-macos-xcframework.zip. If you're doing this in iOS, the workflow is basically the same, you just need to take into account that your file access is sandboxed (and yes, I've done this, and yes, it takes a long time to transcode video, compared to a desktop)
Third
Create a new Xcode project. Again, for this experiment, I created a "MacOS App" using "Storyboards" as the UI (you could try using SwiftUI, but that's another layer of complexity this example didn't need right now)
Unzip the *xcframework.zip file you download from the last step.
In Xcode, select the "project" node, select the MacOS target (🤞 there's only one target).
Select "General", drag the *.xcframework folders from Finder to the "Frameworks, Libraries and Embedded Content" section of your project
Forth
For this experiment, I opened the ViewController class (which was automatically created by Xcode) and simply added...
func syncCommand() {
guard let session = FFmpegKit.execute("-i file1.mp4 -c:v file2.mp4") else {
print("!! Failed to create session")
return
}
let returnCode = session.getReturnCode()
if ReturnCode.isSuccess(returnCode) {
} else if ReturnCode.isCancel(returnCode) {
} else {
print("Command failed with state \(FFmpegKitConfig.sessionState(toString: session.getState()) ?? "Unknown") and rc \(returnCode?.description ?? "Unknown").\(session.getFailStackTrace() ?? "Unknown")")
}
}
func asyncCommand() {
FFmpegKit.executeAsync("-i file1.mp4 -c:v file2.mp4") { session in
guard let session = session else {
print("!! Invalid session")
return
}
guard let returnCode = session.getReturnCode() else {
print("!! Invalid return code")
return
}
print("FFmpeg process exited with state \(FFmpegKitConfig.sessionState(toString: session.getState()) ?? "Unknown") and rc \(returnCode).\(session.getFailStackTrace() ?? "Unknown")")
} withLogCallback: { logs in
guard let logs = logs else { return }
// CALLED WHEN SESSION PRINTS LOGS
} withStatisticsCallback: { stats in
guard let stats = stats else { return }
// CALLED WHEN SESSION GENERATES STATISTICS
}
}
The code above is basically the "2. Execute synchronous FFmpeg commands." and "4. Execute asynchronous FFmpeg commands by providing session specific execute/log/session callbacks." examples from the ffmpeg-kit/apple documentation
!! Important !! - don't forget to add import ffmpegkit to the start of the file!
At this point, this should now compile (you'll get a couple of warnings about logs and stats not been used, you can ignore those).
After thoughts...
You should realise by now that the code I've provided won't actually run, for two reasons.
I've not actually called either func from anywhere (I tested it by placing it in the viewDidLoad func of the ViewController class)
The input file, used in the execute command, doesn't exist. You will need to provide an actual reference to an actual file, preferably with an absolute path. This, how ever, may require you to change the "App Sandbox" settings under the targets "Signing and Capabilities"
Xcodes auto code suggestions aren't bad and I mostly filled out the above using it, and the Obj-c code as a starting point.
Also, beware, SO is not a "tutorial" site, Xcode is a complex beast and you may need to spend some time exploring other resources to overcome issues you encounter

Related

Process to change data model

How to change the .xcdatamodeld file i.e. the data model?
Since the program has already been run and the Persistent Store Coordinator (PSC) contains a url to .sqlite, .sqlite-shm and .sqlite-wal files on disk I think the process is as follows but am unsure. Any input would be appreciated.
Run code below to delete the url from PSC.
Delete sqlite files from disk.
Change .xcdatamodeld file.
CodeGen is set to manual so create new managed object subclasses.
Make appropriate changes to code.
Run program which I assume will enter a url into the PSC and create the 3 sqlite files on disk but now based on the new .xcdatamodeld file.
func deletePersistentStore() {
guard let persistentStoreURL = container.persistentStoreCoordinator.persistentStores.first?.url
else {
print("URL Missing")
return
}
do {
try container.persistentStoreCoordinator.destroyPersistentStore(
at: persistentStoreURL,
ofType: "SQLite",
options: nil)
} catch {
print("Persistent Store Not Deleted: \(error) - \(error.localizedDescription)")
}
print("\(container.persistentStoreCoordinator.persistentStores.count)")
// prints 0
print("\(String(describing: container.persistentStoreCoordinator.persistentStores.first?.url) )")
// prints nil
}
It sounds like you’re still developing this app and that it’s not released yet. If that’s true, you could do something like this. But it would be easier to delete the app from your phone (or simulator), change the model, and then install a new copy of the app.
That would let you skip steps 1 and 2.
If your app is already released, you should look into Core Data model migration. It’s a process that lets you update the data model without deleting existing data. In most cases it’s nearly automatic, but it depends on how much your model is changing.

Cleanest & safest way to include prepopulated .sqlite file in Swift app

The file needs to be read and write later. On Android I used Room & RoomAssetHelper.
I understand the basic logic that I need to include it in the project assets and then copy it to somewhere accessible for the app at first start. But I want to avoid writing these things manually and risking making an error (I am not too confident with reading files & DBs).
All of the answers that I find are from people giving quick & dirty advice on how to manually code the logic for this. I would like to do it on a clean & professional level.
Is there a library that would do most of the "risky" work for me?
(Meaning import & copy the .sqlite file, so I can start using it in my code)
I found GRDB.swift, but I cannot figure out if it supports prepopulated files.
Please stop looking for a magical library that will do all of this for you automatically.
What you need to do yourself without any SQLite library?
Add your prepopulated database.sqlite as an asset to your project.
When the app launches, check if the database.sqlite file is present at the expected location (inside your app's documents directory for example). You can check this using FileManager APIs.
If the file exists at the expected path, you are fine, no need to copy any file.
If the database.sqlite file does not exist at the expected path, you need to copy your database.sqlite file at the path using FileManager APIs.
CAUTION :
Be aware that in this step, you may encounter an error while copying the file. This should not happen for most cases. In rare caes that it does happen, you should adjust your app accordingly - indicate to user somehow that initialization failed, free some space on your phone, restart app etc.
Steps 2-4 need to be checked on every app launch - put this logic somewhere close your app startup process. If all of above instructions are followed and you succeed either via step 3 OR 4, you now have the database.sqlite file where you want it to be.
Where the SQLite library comes in?
At this point, you can use any library that suits your purpose and you feel comfortable with.
As you mentioned GRDB.swift, it allows you to specify a custom path for your database file. Copy-pasting the current version minimal setup code here for reference.
import GRDB
// 1. Open a database connection
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
// 2. Define the database schema
try dbQueue.write { db in
try db.create(table: "player") { t in
t.autoIncrementedPrimaryKey("id")
t.column("name", .text).notNull()
t.column("score", .integer).notNull()
}
}
// 3. Define a record type
struct Player: Codable, FetchableRecord, PersistableRecord {
var id: Int64
var name: String
var score: Int
}
// 4. Access the database
try dbQueue.write { db in
try Player(id: 1, name: "Arthur", score: 100).insert(db)
try Player(id: 2, name: "Barbara", score: 1000).insert(db)
}
let players: [Player] = try dbQueue.read { db in
try Player.fetchAll(db)
}

UserDefaults/NSUserDefaults removeObject(forKey:) mysteriously failing in app and between apps

I am working on an "uninstaller" for an macOS app we've had for several years now. The purpose for the uninstaller is to allow us to put a given system into a nascent state as if the original app had never been installed so that we can more reliably test the install process.
The original app has an extensive array of preferences stored in UserDefaults. In the original app there is a resetToDefaults() method which works just fine resetting all the defaults however for the uninstaller we'd wanted to remove the values completely. It looks to be straight-forward and this is what I came up with...
func flushPreferences() {
let defaults = getDefaultPreferences()
for preferenceName in defaults.keys.sorted() {
UserDefaults.standard.removeObject(forKey: preferenceName)
}
UserDefaults.standard.synchronize()
}
... which does not work at all.
I read in the documentation
Removing a default has no effect on the value returned by the objectForKey: method if the same key exists in a domain that precedes the standard application domain in the search list.
and I don't really understand what "domain" relates to and thought it might be app so tried the code as a test in the original app and that does nothing either.
Someone else suggested this, which also does nothing
let appDomain = Bundle.main.bundleIdentifier!
UserDefaults.standard.removePersistentDomain(forName: appDomain)
I also found some test code which works absolutely fine... which looks to be nearly identical to what I'm doing. I even tried using it with hard-coding one of our pref keys and that fails as well.
func testRemoveObject() {
let myKey:String = "myKey"
UserDefaults.standard.set(true, forKey: myKey)
let beforeVal = UserDefaults.standard.value(forKey: myKey)
print("before: \(beforeVal ?? "nil")")
UserDefaults.standard.removeObject(forKey: myKey) // Note: This is the only line needed, others are debugging
let afterVal = UserDefaults.standard.value(forKey: myKey)
print("after: \(afterVal ?? "nil")")
}
What am I missing? It looks like this one (based on what I've been able to find on the web) can be somewhat mysterious but I'm thinking it must be something obvious that I'm not seeing.
Well, thanks to red_menace's suggestion I found one article that led to another that suggested that the following command will reset the user preferences cache:
killall -u #USER cfprefsd
which seemed to work (yay) but upon further investigation it appears that simply closing the app is what updates the actual preference in the .plist and that changing it in the app will not show up until you exit.
This makes sense as it explains why you can create a preference on the fly save it, confirm it saved, delete it and confirm it deleted but cannot delete a previously saved preference — as similar to the persistent prefs perhaps the new preference is not added to the cache until the application exits.
This could also explain the various odd behaviors that other posters were finding (only worked every other time, had to do it asynchronously, etc.). As for NSUserDefaults.synchronize() has been depreciated and developer.apple.com indicates that it is unneeded and does nothing.
So one problem solved...
As it turns out my initial instinct was accurate as well and you cannot access prefs from one app in another using the removeObject(forKey: preferenceName)
// Will not work cross-application, though will work locally (inter-app)
UserDefaults.standard.removeObject(forKey: preferenceName)
To get it to work cross applications you have to use CFPreferencesSetAppValue(_ key:, value:, applicationID:) which is part of the "Preferences Utilities" section of the Core Library which requires that you know the appDomain of the initial app. So, the final solution is:
In the source app:
let appDomain = Bundle.main.bundleIdentifier! // Note, needed by uninstaller
will give you the domain for the stored preference in the source app.
And in the app doing the changing — the final working code:
func flushPreferences() {
let defaults = getDefaultPreferences()
let sourceAppDomain = "{THE_BUNDLE_ID_FROM_SOURCE_APP}"
for preferenceName in defaults.keys {
print("Preference name: \(preferenceName)")
CFPreferencesSetAppValue(preferenceName as CFString,
nil,
sourceAppDomain as CFString)
}
}
Hope this helps someone save some time at some point - thanks to everyone who contributed. This one was a BEAR!

How to drag and drop an external item for Xcode-ui-testing

In my application I allow the user to drag and drop items from Finder (or any other source of a file based URL) into my application. What I want to do is to add a mechanism that will allow me to test this in the Xcode UI testing.
I can see how to use XCUIElement.press(forDuration:thenDragTo:) to test the drag and drop of a source and destination within the application, but I have been unable to find a way to test when the source of the drag is outside of the application.
In a somewhat related test, I test the copy and paste portion of the application by setting the string I want to paste into NSPasteboard.general, then using XCUIElement.typeKey("v", modifierFlags: .command) to paste it into the desired element. That is a little less than ideal as it depends on Command-v actually being implemented as the paste command, but that is unlikely to change so it is acceptable for my needs. (In fact I've written an XCUIElement.paste(_ s: String) extension that makes it easy for me to add this in a test.)
I believe that drag and drop is also using an NSPasteboard for its communications, so with a little investigation into the underlying mechanism, I should be able to set my object into the correct pasteboard just like I do for the cut and paste. I'm reasonably certain I can figure that part out. But I haven't figured out how to perform the actual drop.
My goal would be to create an XCUIElement.drop(_ url) that would setup the proper "public.file-url" object into the correct pasteboard, and then simulate/perform the drop into the element.
Any ideas?
I should note that I have already tried the following two items:
First, I did use the Xcode record feature to attempt to record the drag and drop operation and see what events it would give me. Unfortunately, it records absolutely nothing.
Second, I do have a menu based alternative where the user selects a file via the file selector. So if I could simulate the file selection, that would be a suitable testing alternative for my purposes. Unfortunately, I didn't make any progress along that path either. When I used Xcode to record the events, it recorded the menu selection, nothing that was actually done in the dialog.
Based on your comments I would recommend you to read this article documentation piece
https://developer.apple.com/documentation/xctest/xcuiapplication
Notice the init(bundleIdentifier: String) and init(url: URL) methods. These allow you to interact with apps apart from the target application.
Then you can use XCUIElement.press(forDuration:thenDragTo:)
import XCTest
import XCTApps
import ScreenObject
let notes = XCTApps.notes.app
let photos = XCTApps.photos.app
class Tests: XCTestCase {
func testDragAndDrop() {
photos.launch()
notes.launch()
photos.images.lastMatch.press(forDuration: 1, thenDragTo: notes.textViews["Note Body Text View"])
}
}
P.S. In this example I use XCTApps because I don't want to remember or google bundle identifiers :D
https://github.com/rzakhar/XCTApps
Ok, so I haven't yet figured out the answer to my question (how to test a drag and drop), but I have come up with an acceptable workaround for my test.
Specifically, as I thought more about the pasteboard I realized that if I'm allowing the user to drag and drop a file into my application, then I should also be allowing them to cut and paste a file into the application.
Once I had that realization, then it was a reasonably simple process to test the necessary feature of my application by pasting a URL instead of dragging and dropping the URL. This has the added advantage that I can add the necessary test file to my testing package, keeping everything nicely self contained.
To this end I've added the following function to my XCUIElement extension:
extension XCUIElement {
func paste(url: URL) {
precondition(url.isFileURL, "This must be a file URL to match the pasteboard type.")
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(url.absoluteString, forType: .fileURL)
click()
typeKey("v", modifierFlags: .command)
}
}
Then in my test code I add the following to trigger the event:
let mainWindow = app.windows[/*...my main window name goes here...*/]
let testBundle = Bundle(for: type(of: self))
let fileURL = testBundle.url(forResource: "Resources/simple", withExtension: "json")
mainWindow.paste(url: fileURL!)
Granted, this doesn't actually test the drag and drop, but it does test the same portion of my code, since in my AppDelegate I have my onPaste action method calling the same underlying method as my performDrop method.
I will wait a couple of days to see if anyone comes up with an answer to the actual question (since I would still find that useful), but if no one does, I'll accept my own answer.

Key data not storing to iCloud with NSUbiquitousKeyValueStore.defaultStore

So I have a simple goal, just to get this working. So theoretically if you ruined this.
Triggered save players
waited a bit
Deleted the app
Rebuilt (downloaded)
Triggered restore then the word "MEDO" would print to the console
But instead it is null, making me pretty sure that it is not saving to iC cloud some reason. I followed this tutorial. Perhaps it is too outdated to work?
func c_restoreCharecters()
{
let icloud = NSUbiquitousKeyValueStore.defaultStore()
println(icloud.objectForKey("username"))
}
func c_savePlayers()
{
let icloud = NSUbiquitousKeyValueStore.defaultStore()
icloud.setObject("MEDO", forKey: "username")
println(icloud.synchronize())
}
Obviously I am going to use this concept for something completely different in the future, but I have to get the basics down first!
Some other stuff:
iCloud set up in my settings:
My Entitlements file: