For my radio app Cuterdio I implemented CarPlay. There was no example for this in Xamarin, so I decided to create one (GitHub).
First of all you need a special entitlement for CarPlay. See here for a navigation app and audio app.
Add the entitlement to your project and don’t forget to link the entitlements in your project.
In this example we create a simple music player which plays audio streams via the amazing XamarinMediaManager.
In our iOS project we init the delegates in the AppDelegate.cs:
public override bool FinishedLaunching(UIApplication app, NSDictionary options) { Forms.Forms.Init(); LoadApplication(new App()); CrossMediaManager.Current.Init(); CarIntegrationBridge.Init(); return base.FinishedLaunching(app, options); }
The delegates are registered as follow:
public class CarIntegrationBridge : ICarIntegrationBridge { public static void Init() { PlayableContentDelegate playableContentDelegate = new PlayableContentDelegate(); MPPlayableContentManager.Shared.Delegate = playableContentDelegate; PlayableContentDataSource playableContentDataSource = new PlayableContentDataSource(); MPPlayableContentManager.Shared.DataSource = playableContentDataSource; } }
Now we define our datasource. In my example I added radio stations with name and url. We have to define the menu items count and how a menu item is displayed (name, icon, …):
internal class PlayableContentDataSource : MPPlayableContentDataSource { public static List<Station> Stations = new List<Station> { new Station{Name = "Rainbow radio", Url = "https://stream.rockantenne.de/rockantenne/stream/mp3"}, new Station{Name = "Unicorn radio", Url = "http://play.rockantenne.de/heavy-metal.m3u"} }; public override MPContentItem ContentItem(NSIndexPath indexPath) { var station = Stations[indexPath.Section]; var item = new MPContentItem(station.Url); item.Title = station.Name; item.Playable = true; item.StreamingContent = true; var artWork = GetImageFromUrl("station.png"); if (artWork != null) { item.Artwork = artWork; } return item; } public override nint NumberOfChildItems(NSIndexPath indexPath) { if (indexPath.GetIndexes().Length == 0) { return Stations.Count; } throw new NotImplementedException(); } private MPMediaItemArtwork GetImageFromUrl(string imagePath) { MPMediaItemArtwork result = null; try { using (var nsUrl = new NSUrl(imagePath)) { using (var data = NSData.FromUrl(nsUrl)) { var image = UIImage.LoadFromData(data); result = new MPMediaItemArtwork(image); } } } catch { UIImage image = UIImage.FromBundle(imagePath); if (image != null) { result = new MPMediaItemArtwork(image); } } return result; } }
Now we have to decide what is todo, if an item is taped.
The simulator have an other behavior than a real device. So I hacked a solution for calling the NowPlayingScene.
internal class PlayableContentDelegate : MPPlayableContentDelegate { public override void InitiatePlaybackOfContentItem( MPPlayableContentManager contentManager, NSIndexPath indexPath, Action<NSError> completionHandler) { Execute(contentManager, indexPath); completionHandler?.Invoke(null); } private void Execute(MPPlayableContentManager contentManager, NSIndexPath indexPath) { DispatchQueue.MainQueue.DispatchAsync(async () => await ItemSelectedAsync(contentManager, indexPath)); } private async Task ItemSelectedAsync(MPPlayableContentManager contentManager, NSIndexPath indexPath) { // Play var station = PlayableContentDataSource.Stations[indexPath.Section]; await CrossMediaManager.Current.Play(station.Url); // Set playing identifier MPContentItem item = contentManager.DataSource.ContentItem(indexPath); contentManager.NowPlayingIdentifiers = new[] { item.Identifier }; // Update on simulator if (DeviceInfo.DeviceType == DeviceType.Virtual) { InvokeOnMainThread(() => { UIApplication.SharedApplication.EndReceivingRemoteControlEvents(); UIApplication.SharedApplication.BeginReceivingRemoteControlEvents(); }); } } }
To reload the data (e.g. if you change the stations), you have to call this:
public void ReloadStations() { MPPlayableContentManager.Shared?.ReloadData(); }