I've also just run into this same problem. I have two AVPlayer objects, one which plays music and the other plays a narration.
The two AVPlayers stream just fine with Bluetooth and AirPods. But on AirPlay 2 devices like a Sonos speaker, only one or the other will stream, which was an unexpected and not very nice surprise.
The only thing I found to work is to configure your AVAudioSession to not support AirPlay 2 (i.e., don't use the .longFormAudio policy), and make sure to use the .duckOthers option, as follows:
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.duckOthers])
This will stream both AVPlayers' content to an AirPlay 2 device like Sonos. But, the downside is you lose the lock screen / Control Center playback controls, due to the .duckOthers option. Ultimately I guess this is better than the alternative of my app being non-compatible with Sonos/HomePod. But, losing the lock screen controls is a major bummer.
Curious if anyone else has any other options, short of the suggestions above which involve a major and costly rewrite using AVSampleBufferAudioRenderer.
I would think this multiple AVPlayer pattern is a common pattern in meditation apps, as you want the music to continue looping after the narration ends, and you might want to allow the user to change the relative volume between the music AVPlayer and the narration AVPlayer. So, I'm surprised this doesn't work with AirPlay 2.
I looked at the Headspace app and they also don't support AirPlay with simultaneous narration+music, so I'm guessing they couldn't figure out a solution either?
Post
Replies
Boosts
Views
Activity
Yes it's possible to paywall a widget. What I did was:
Design two Views, one which is a paywall view, the other is the actual Widget. (You could in theory also design more views, so that users on higher paying tiers get more widget functionality.)
Highly simplified example:
// Actual widget view
struct WidgetView: View {
var entry: WidgetEntry
var body: some View {
VStack(alignment: .leading) {
Spacer()
Text(entry.widgetText)
.font(.system(size: 12, weight: .semibold, design: .default))
.padding(.leading, 16)
.padding(.trailing, 16)
.padding(.bottom, 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(.white)
}
}
}
// Paywall widget view
struct WidgetPaywallView: View {
var body: some View {
VStack(alignment: .leading) {
Spacer()
Text("Subscribe to use the widget")
.font(.system(size: 12, weight: .semibold, design: .default))
.padding(.leading, 16)
.padding(.trailing, 16)
.padding(.bottom, 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(.white)
}
}
}
In your widget timeline entry model, add a Boolean var such as "isPremiumSubscriber"
struct WidgetEntry: TimelineEntry {
let date: Date
let widgetText: String
var isPremiumSubscriber: Bool
}
In getTimeline() in your Timeline Provider, check to see if the user is subscribed (or, in my case, I use RevenueCat to manage the in-app subscriptions as it's much easier that way). If they are subscribed, create your timeline entries with the timestamp when the widget will be updated (e.g., every 20 mins), the data you want to display at that time (in the code example I pasted, it's just a simple String), and then set the value of isPremiumSubscriber = true.
Then, in your Widget view (which is called each time your widget is about to be refreshed), get the data from your entry, and if entry.isPremiumSubscriber == true, return the actual widget view with all the functionality; if it's false, then return the paywall widget view.
Note that with this approach, if the user's subscription status lapses midway through an already created timeline, then the user will still have widget functionality until the timeline runs out and getTimeline() is called again.
struct WidgetEntryView: View {
var entry: WidgetEntry
var body: some View {
// Display widget if user is subscribed, otherwise return the paywall view
if entry.isPremiumSubscriber {
WidgetView(entry: entry)
} else {
WidgetPaywallView()
}
}
}
Finally, in your actual app, you also need to add a way to force refresh the widget if the user has just subscribed. You can use WidgetCenter.shared.reloadAllTimelines() for this. In my case, I have a global boolean in AppDelegate.swift which keeps track if the user is subscribed. I added a didSet that fires whenever the value changes-- and I add a check to see if the new value is different from the old value. If it's different, I call WidgetCenter.shared.reloadAllTimelines(), which will call getTimeline() in your widget. Make sure to import WidgetKit too before you use reloadAllTimelines().
var isPremiumSubscriber: Bool = false {
didSet {
if #available(iOS 14.0, *) {
if isPremiumSubscriber != oldValue {
// Update widgets because subscription status has changed
WidgetCenter.shared.reloadAllTimelines()
}
}
}
}