Can i use AVFoundation to play sound from a local ...
# ios
j
Can i use AVFoundation to play sound from a local file on iOS in a kotlin multiplatform project?
f
Hi, if you put your code inside the iOSMain code, it will work.
j
Nice. Are there any examples of this? This needs to be called from kotlin, right? I cannot use swift directly?
f
You need to write it in Kotlin, but it's from the OBJ-C naming not the swift naming.
in swift
Bundle
become
NSBundle
in Obj-C/Kotlin
For exemple, a native dialog from kotlin iOSMain:
Copy code
val dialog =
    UIAlertController.alertControllerWithTitle(title, message, UIAlertControllerStyleAlert)
val action =
    UIAlertAction.actionWithTitle(
        "OK",
        UIAlertActionStyleDefault,
    ) { _ -> dialog.dismissViewControllerAnimated(true, null) }
dialog.addAction(action)

UIApplication.sharedApplication
    .keyWindow
    ?.rootViewController
    ?.presentViewController(dialog, animated = true, completion = null)
👀 1
You can use the iOS Api from Kotlin iOS, but the particular syntax needs to be mastered 😄
j
Right thanks
I need to play a local .mp3 file from a file://... uri
f
Do it first in swift and convert your code in Kotlin, it's technically possible and it's a good exercise.
🙌 1
j
Okay, will do @François Thanks for the help. Can post the code here underway because I'm writing open source code
I’m struggling with translating this swift code to kotlin in `iosMain`:
Copy code
guard let url = Bundle.main.url(forResource: name, withExtension: "mp3") else {
            print("Sound file not found")
            return
        }
f
Copy code
NSBundle.mainBundle().pathForResource("name", "mp3")?.let {
        println("my file")
    } ?: run {
        println("Sound file not found")
    }
🙌 1
j
Thanks
Screenshot 2024-08-05 at 14.38.16.png
f
it's the tricky part of using reference
j
Copy code
error = null
works…
But seems unsafe
I mean “works” as in no red lines in IDEA
f
It's working but you won't the reason of the failure
Only OBJ-C devs remembers 😄
🙂 1
wait I will give you the correct answser
Copy code
fun <T> throwError(block: (errorPointer: CPointer<ObjCObjectVar<NSError?>>) -> T): T {
    memScoped {
        val errorPointer: CPointer<ObjCObjectVar<NSError?>> = this.alloc<ObjCObjectVar<NSError?>>().ptr
        val result: T = block(errorPointer)
        val error: NSError? = errorPointer.pointed.value
        if (error != null) {
            throw Exception(
                message = error.description,
                cause = Exception("${error.code}|${error.domain}|${error.underlyingErrors}"),
            )
        } else {
            return result
        }
    }
}
Adapt this snippet as your needs
There are some tricky stuff to do when using objc pointer
j
Woah! Thanks
How is it supposed to be consumed in: `val audioPlayer = AVAudioPlayer(contentsOfURL = url, error = ???)``
f
take a look at the link below the snippet
j
Aha
This is wild
🙂
Copy code
throwError { errorPointer: CPointer<ObjCObjectVar<NSError?>> ->  
                        val audioPlayer = AVAudioPlayer(contentsOfURL = url, error = errorPointer)
                    }
f
Copy code
val audioPlayer = throwError { errorPointer: CPointer<ObjCObjectVar<NSError?>> ->  
                        AVAudioPlayer(contentsOfURL = url, error = errorPointer)
                    }
j
Ouu nice
f
Remember
throwError
will throw an error, so don't forget to try/catch
👍 1
j
Yay, it plays from Kotlin
And then it stops after 5 sec
Now I need to probably use some coroutine to play the whole thing and then I need to play from a downloaded file (app requirement)
f
If you're looking for some iOS API, search for the Obj-C syntax, not the Swift Syntax. You saw the usage of
Bundle/NSBundle
, they don't have the same names.
🙌 1
j
Strange that it stops playing after ~5 sec when playing from kotlin
Maybe because its just randomly started inside a @Composable
f
I guess it's because the instance of
AVAudioPlayer
is quickly destroyed after using it, try to store it inside a
singleton
for running your player code and see what happened.
But Singleton is not the recommended solution 😄
j
Singleton works x)
Antipattern success
f
You need to keep the instance alive, choose the way you like and singleton is not the only one
j
What are the alternatives?
f
Depends on the pattern you're using. By default on compose, it's inside the viewmodel which is kept alive until the composable is destroyed.
f
Yeah, that's the one, it's the same as Android MVVM
j
Ok nice thanks
It works!
🙌 1
Oh no I was still using companion object inside of the ViewModel, sec
Okay it works with val in viewmodel
f
if you're already use Android ViewModel, it's the same thing on compose multiplatform.
m
might even be able to use it to play the audio!
j
I can’t figure out how to load a
.mp3
file that the app has downloaded and save to filesystem using the okio library into an
AVAudioPlayer
in an
iosMain
target using kotlin
Omg I was using a
.ogg
file
Still need to figure out this
f
The ogg file is unsupported by iOS (except using an external lib)
If you’re looking for the path of a file
Copy code
internal actual fun getDataStorePath(
    platform: IPlatform,
    fileName: String,
): Path {
    val documentDirectory: NSURL? =
        NSFileManager.defaultManager.URLForDirectory(
            directory = NSDocumentDirectory,
            inDomain = NSUserDomainMask,
            appropriateForURL = null,
            create = false,
            error = null,
        )
    return (requireNotNull(documentDirectory).path + "/$fileName").toPath()
}
Considering you saved your file inside the Document directory of your sandbox
The swift syntax is way different 😄
j
The file is saved to the iOS app cache
f
Change the constant NSDocumentDirectory to NSCacheDirectory (from memory)
👍 1
Okio KMP lib is good for manipulating file system, not for looking for file or path.
j
Right, that's how we use it
How do I use the
Path
from
getDataStorePath
in
AVAudioPlayer
?
f
Path is an Okio Class
So use it like an Okio Instance 😄
j
eeeh hmm ok
So I can’t use it like this?
Copy code
internal actual fun getDataStorePath(
    platform: IPlatform,
    fileName: String,
): Path {
    val documentDirectory: NSURL? =
        NSFileManager.defaultManager.URLForDirectory(
            directory = NSDocumentDirectory,
            inDomain = NSUserDomainMask,
            appropriateForURL = null,
            create = false,
            error = null,
        )
    return (requireNotNull(documentDirectory).path + "/$fileName").toPath()
}

val fileURL = getFileURLInCacheDirectory(fileName)

audioPlayer = throwError { errorPointer: CPointer<ObjCObjectVar<NSError?>> ->
                    AVAudioPlayer(contentsOfURL = fileURL, error = errorPointer)
                }
f
In your case, remove Path and use a String or convert your String to NSURL
1
j
And use the NSURL directly in
contentsOfURL
?
f
it’s what contentsOfURL need, no?
m
remember to always resolve the URL from the directory and not persist the full path. due to iOS sandboxing:
All third-party apps are “sandboxed”, so they are restricted from accessing files stored by other apps or from making changes to the device. Sandboxing is designed to prevent apps from gathering or modifying information stored by other apps. Each app has a unique home directory for its files, which is randomly assigned when the app is installed. If a third-party app needs to access information other than its own, it does so only by using services explicitly provided by iOS and iPadOS.
f
yes, it's because we need to use
NSFileManager.defaultManager.URLForDirectory
or
NSBundle.main.path...
m
j
@François Maybe I should use your
getDataStorePath
I'm struggling to play the file directly from an url into AvAudioPLayer
The app ID and thus the cache path is different in swift and kotlin....
Maybe I should use okio to fetch the file and then pass it to AVPlayer?
f
Please be more specific on your need, you can message me directly on Slack, if needed.
j
Thanks but I figured it out
m
might no longer be relevant, but here’s an example of persisting
bookmark
then translating back into a
url
during runtime