SkipAV
Audio/Video functionality for Skip apps.
See what API is included here.
About
The SkipAV framework provides a small subset of the AVKit and AVFoundation frameworks
as well as a SwiftUI.VideoPlayer component for
Android based on the androidx.media3 package's ExoPlayer.
This framework works in both transpiled Skip Lite and compiled Skip Fuse mode.
Setup
To include this framework in your project, add the following
dependency to your Package.swift file:
let package = Package( name: "my-package", products: [ .library(name: "MyProduct", targets: ["MyTarget"]), ], dependencies: [ .package(url: "https://source.skip.dev/skip-av.git", from: "1.0.0"), ], targets: [ .target(name: "MyTarget", dependencies: [ .product(name: "SkipAV", package: "skip-av") ]) ] )
Dependencies
SkipAV depends on the skip transpiler plugin and the SkipUI package.
SkipAV is part of the core SkipStack and is not intended to be imported directly. The transpiler includes import skip.av.* in generated Kotlin for any Swift source that imports the AVKit or AVFoundation frameworks.
Example
import SwiftUI import AVKit struct PlayerView: View { @State var player = AVPlayer(playerItem: AVPlayerItem(url: URL(string: "https://skip.dev/assets/introduction.mov")!)) @State var isPlaying: Bool = false var body: some View { VStack { Button { isPlaying ? player.pause() : player.play() isPlaying = !isPlaying player.seek(to: .zero) } label: { Image(systemName: isPlaying ? "stop" : "play") .padding() } VideoPlayer(player: player) } } }
Event Handling
Some AVPlayerItem notifications, such as .didPlayToEndTimeNotification
and .timeJumpedNotification, can be handled through the NotificationCenter.
For example:
VideoPlayer(player: player) .onReceive(NotificationCenter.default.publisher(for: AVPlayerItem.didPlayToEndTimeNotification)) { event in logger.info("didPlayToEndTimeNotification: \(event)") } .onReceive(NotificationCenter.default.publisher(for: AVPlayerItem.timeJumpedNotification)) { event in logger.info("timeJumpedNotification: \(event)") }
AVAudioRecorder
This framework also supports the 'AVFoundation.AVAudioRecorder' and AVFoundation.AVAudioPlayer' APIs via Android's MediaRecorder and MediaPlayer. These APIs can be used for audio recording and playback.
import SwiftUI import AVFoundation struct AudioPlayground: View { @State var isRecording: Bool = false @State var errorMessage: String? = nil @State var audioRecorder: AVAudioRecorder? @State var audioPlayer: AVAudioPlayer? var body: some View { #if SKIP let context = androidx.compose.ui.platform.LocalContext.current #endif return VStack { Button(isRecording ? "Stop Recording" : "Start Recording") { self.isRecording ? self.stopRecording() : self.startRecording() } Button("Play Recording") { try? self.playRecording() } if let errorMessage { Text(errorMessage) .foregroundColor(.red) } } .padding() #if SKIP .onAppear { requestAudioRecordingPermission(context: context) } #endif } var captureURL: URL { get { #if SKIP let context = ProcessInfo.processInfo.androidContext let file = java.io.File(context.filesDir, "recording.m4a") return URL(fileURLWithPath: file.absolutePath) #else return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask) .first!.appendingPathComponent("recording.m4a") #endif } } func startRecording() { do { #if !SKIP setupAudioSession() #endif self.audioRecorder = try AVAudioRecorder(url: captureURL, settings: [AVFormatIDKey: Int(kAudioFormatMPEG4AAC), AVSampleRateKey: 12000, AVNumberOfChannelsKey: 1, AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue]) } catch { print(error.localizedDescription) } audioRecorder?.record() isRecording = true } func stopRecording() { isRecording = false audioRecorder?.stop() } func playRecording() throws { do { guard FileManager.default.fileExists(atPath: captureURL.path) else { errorMessage = "Recording file does not exist." return } audioPlayer = try AVAudioPlayer(contentsOf: captureURL) audioPlayer?.play() errorMessage = "" } catch { logger.error("Could not play audio: \(error.localizedDescription)") errorMessage = "Could not play audio: \(error.localizedDescription)" } } #if SKIP func requestAudioRecordingPermission(context: android.content.Context) { guard let activity = context as? android.app.Activity else { return } // You must also list these permissions in your Manifest.xml let permissions = listOf(android.Manifest.permission.RECORD_AUDIO, android.Manifest.permission.READ_EXTERNAL_STORAGE, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) androidx.core.app.ActivityCompat.requestPermissions(activity, permissions.toTypedArray(), 1) } #else func setupAudioSession() { let session = AVAudioSession.sharedInstance() do { try session.setCategory(.playAndRecord, mode: .default) try session.setActive(true) } catch { errorMessage = "Failed to setup audio session: \(error.localizedDescription)" } } #endif }
API Support
The following table summarizes SkipAV's API support on Android. Anything not listed here is likely not supported. Note that in your iOS-only code - i.e. code within #if !SKIP blocks - you can use any Swift API you want. Additionally:
Support levels:
- ✅ – Full
- 🟢 – High
- 🟡 – Medium
- 🟠 – Low
| Support | API |
|---|---|
| 🟢 |
AVAudioPlayer
|
| 🟢 |
AVAudioRecorder
|
| 🟡 |
AVPlayer
|
| 🟠 |
AVPlayerItem
|
| 🟠 |
AVAsset
|
| 🟢 |
CMTime / CMTimeRange
|
| 🟡 |
VideoPlayer
|
Contributing
We welcome contributions to SkipAV. The Skip product documentation includes helpful instructions and tips on local Skip library development.
License
This software is licensed under the Mozilla Public License 2.0.