Skip to content

Commit

Permalink
feature: Add audio player listener feature on iOS
Browse files Browse the repository at this point in the history
- update README.md and CHANGELOG.md
  • Loading branch information
John committed Sep 9, 2021
1 parent bf964b2 commit de50376
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 11 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## 0.1.2

* **Android** Added ability to receive notifications when a players `isPlaying` state changes.
* Added ability to receive notifications when a players `isPlaying` state changes.

## 0.1.1

Expand Down
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,21 @@ If you found your way here because `ocarina` was recommended by [`twilio_program
}
```

## Android Audio Player State Listeners
## Audio Player State Listeners

This feature was also developed in concert with the ongoing development of the audio system for [`twilio_programmable_video`](https://pub.dev/packages/twilio_programmable_video), though it was designed to be agnostic to what other plugins you are using. As such, you can certainly add custom listeners via this mechanism to receive the same notifications. The only requirement is that they have the following signature:

Android:
```kotlin
(url: String, isPlaying: Boolean) -> Unit
```

If you wish to use this feature with [`twilio_programmable_video`](https://pub.dev/packages/twilio_programmable_video), simply add the following to your `MainActivity.kt`.
iOS
```swift
(_ url: String, _ isPlaying: Bool) -> Void
```

The benefit of using this feature with the[`twilio_programmable_video`](https://pub.dev/packages/twilio_programmable_video) is that it will enable that plugin to update the [Audio Focus](https://developer.android.com/guide/topics/media-apps/audio-focus) and usage of Bluetooth Sco based upon whether there are active audio players, in addition to an active call.
If you wish to use this feature with [`twilio_programmable_video`](https://pub.dev/packages/twilio_programmable_video), simply add the following to your `MainActivity.kt`.

```kotlin
private lateinit var PACKAGE_ID: String
Expand All @@ -127,4 +131,30 @@ The benefit of using this feature with the[`twilio_programmable_video`](https://
super.cleanUpFlutterEngine(flutterEngine)
OcarinaPlugin.removeListener(PACKAGE_ID)
}
```
The benefit of using this feature (on Android) with the [`twilio_programmable_video`](https://pub.dev/packages/twilio_programmable_video) is that it will enable that plugin to update the [Audio Focus](https://developer.android.com/guide/topics/media-apps/audio-focus) and usage of Bluetooth Sco based upon whether there are active audio players, in addition to an active call.

Alternatively, you can use it with your own listener for your own purposes.

This feature has not been integrated with `twilio_programmable_video` on iOS, out of preference for usage of the `AVAudioEngineDevice` as a delegate for `ocarina`.

If you wish to use this feature on iOS, add the following to your `AppDelegate.swift`:

```swift
let bundleID = Bundle.main.bundleIdentifier

override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
SwiftOcarinaPlugin.addListener(bundleID!, ocarinaListener)
}

override func applicationWillTerminate(_ application: UIApplication) {
SwiftOcarinaPlugin.removeListener(bundleID!)
}

func ocarinaListener(_ id: String, _ isPlaying: Bool) {
// do things
}
```
99 changes: 92 additions & 7 deletions ios/Classes/SwiftOcarinaPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ protocol Player {
func seek(position: Int) -> Void
func volume(volume: Double) -> Void
func position() -> Int64?
func addListener() -> Void
func removeListener() -> Void
}

class LoopPlayer: Player {
let player: AVQueuePlayer
let playerLooper: AVPlayerLooper
var listener: NSKeyValueObservation?
let url: String

required init(url: String, volume: Double) {
let asset = AVAsset(url: URL(fileURLWithPath: url ))
self.url = url
let asset = AVAsset(url: URL(fileURLWithPath: url))

let playerItem = AVPlayerItem(asset: asset)

Expand Down Expand Up @@ -63,15 +68,39 @@ class LoopPlayer: Player {

return positionInMillis
}

func addListener() -> Void {
if listener == nil {
listener = player.observe(\AVQueuePlayer.rate, options: [.old, .new]) { [unowned self] _, change in
if let newValue = change.newValue,
let oldValue = change.oldValue,
newValue != oldValue {
SwiftOcarinaPlugin.notifyListeners(url: url, isPlaying: newValue > 0)
}
}
}
}

func removeListener() -> Void {
listener?.invalidate()
listener = nil
}
}

class SinglePlayer: Player {
var player: AVAudioPlayer
let player: AVPlayer
var listener: NSKeyValueObservation?
let url: String

required init(url: String, volume: Double) {
try! self.player = AVAudioPlayer(contentsOf: URL(fileURLWithPath: url ))
self.url = url
let asset = AVAsset(url: URL(fileURLWithPath: url))

let playerItem = AVPlayerItem(asset: asset)

player = AVPlayer(playerItem: playerItem)

self.player.volume = Float(volume)
self.player.prepareToPlay()
}

func play() {
Expand All @@ -89,21 +118,44 @@ class SinglePlayer: Player {

func stop() {
pause()
seek(position: 0)
}

func seek(position: Int) {
player.currentTime = Float64(position) / 1000
player.seek(to: CMTimeMakeWithSeconds(Float64(position / 1000), preferredTimescale: Int32(NSEC_PER_SEC)))
}

func volume(volume: Double) {
player.volume = Float(volume)
}

func position() -> Int64? {
let positionInMillis = Int64(player.currentTime * 1000)
guard let value = player.currentItem?.currentTime().value,
let timescale = player.currentItem?.currentTime().timescale else {
return nil
}

let positionInMillis = Int64((Float64(value) / Float64(timescale)) * 1000)

return positionInMillis
}

func addListener() -> Void {
if listener == nil {
listener = player.observe(\AVPlayer.rate, options: [.old, .new]) { [unowned self] _, change in
if let newValue = change.newValue,
let oldValue = change.oldValue,
newValue != oldValue {
SwiftOcarinaPlugin.notifyListeners(url: url, isPlaying: newValue > 0)
}
}
}
}

func removeListener() -> Void {
listener?.invalidate()
listener = nil
}
}

class PlayerDelegate {
Expand Down Expand Up @@ -166,7 +218,7 @@ class PlayerDelegate {
func position(_ id: Int) -> Int64 {
return positionDelegate(id)
}

init(load: @escaping LoadDelegate, dispose: @escaping DisposeDelegate, play: @escaping PlayDelegate, pause: @escaping PauseDelegate, resume: @escaping ResumeDelegate, stop: @escaping StopDelegate, volume: @escaping VolumeDelegate, seek: @escaping SeekDelegate, position: @escaping PositionDelegate) {
loadDelegate = load
disposeDelegate = dispose
Expand All @@ -190,8 +242,11 @@ public typealias VolumeDelegate = (_ id: Int, _ volume: Double) -> Void
public typealias SeekDelegate = (_ id: Int, _ positionInMillis: Int) -> Void
public typealias PositionDelegate = (_ id: Int) -> Int64

public typealias Listener = (_ url: String, _ isPlaying: Bool) -> Void

public class SwiftOcarinaPlugin: NSObject, FlutterPlugin {
static var players = [Int: Player]()
static var listeners = [String: Listener]()
static var id: Int = 0;
static var delegate: PlayerDelegate?
var registrar: FlutterPluginRegistrar? = nil
Expand All @@ -207,6 +262,32 @@ public class SwiftOcarinaPlugin: NSObject, FlutterPlugin {
delegate = PlayerDelegate(load: load, dispose: dispose, play: play, pause: pause, resume: resume, stop: stop, volume: volume, seek: seek, position: position)
}

public static func notifyListeners(url: String, isPlaying: Bool) {
listeners.values.forEach { listener in
listener(url, isPlaying)
}
}

public static func addListener(_ id: String, _ listener: @escaping Listener) {
NSLog("SwiftOcarinaPlugin::addListener => id: \(id)")
if listeners.isEmpty && !players.isEmpty {
players.values.forEach { player in
player.addListener()
}
}

listeners[id] = listener
}

public static func removeListener(_ id: String) {
listeners.removeValue(forKey: id)
if listeners.isEmpty {
players.values.forEach { player in
player.removeListener()
}
}
}

public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
if (call.method == "load") {
load(call, result: result)
Expand Down Expand Up @@ -269,6 +350,10 @@ public class SwiftOcarinaPlugin: NSObject, FlutterPlugin {

SwiftOcarinaPlugin.id = SwiftOcarinaPlugin.id + 1

if let player = SwiftOcarinaPlugin.players[id], !SwiftOcarinaPlugin.listeners.isEmpty {
player.addListener()
}

result(id)
}
}
Expand Down

0 comments on commit de50376

Please sign in to comment.