AVPlayer auto play when enter to foreground from background using RxSwift - ios

I have a video and play in ViewController.
But when I enter to background and then back to foreground, the video sometimes pause.
Have any idea let code add notification to know the user back to foreground and make the video auto playing.
Thanks.
extension UIViewController: AVPlayerViewControllerDelegate {
private func playVideo(url: URL, completeHandler: #escaping () -> Void) -> Void {
let player = AVPlayer(url: url)
let vc = AVPlayerViewController.init()
vc.videoGravity = "AVLayerVideoGravityResizeAspectFill"
vc.showsPlaybackControls = false
vc.player = player
// add child view controller
self.view.addSubview(vc.view)
self.addChildViewController(vc)
vc.didMove(toParentViewController: self)
vc.view.backgroundColor = UIColor.clear
// constraints
vc.view.snp.makeConstraints { (make) in
make.top.equalToSuperview()
make.bottom.equalToSuperview()
make.left.equalToSuperview()
make.right.equalToSuperview()
}
// end play
_ = NotificationCenter.default.rx
.notification(NSNotification.Name.AVPlayerItemDidPlayToEndTime)
.takeUntil(self.rx.deallocated)
.subscribe(onNext: { [weak self] _ in
for vc in self?.childViewControllers ?? [] {
if vc is AVPlayerViewController {
vc.view.removeFromSuperview()
vc.removeFromParentViewController()
}
}
completeHandler()
})
vc.player?.play()
}
}

Well, you have callbacks exactly for that purpose in UIApplicatonDelegate - https://developer.apple.com/documentation/uikit/uiapplicationdelegate
applicationDidEnterBackground(_:)
func applicationWillEnterForeground(UIApplication)
So you will have to pause/resume video from those callback methods.

Related

MPRemoteCommandCenter - Remote controls on lock screen does not show up

I've implemented two functions in View controller (setupRemoteTransportControls() and setupNowPlaying()) and added one function to AppDelegate, but I'm still unable to see background audio controls of my app on the lock screen and also audio interruption function isn't working. This is the live stream from url, as you can spot on in the code. In the general settings I have added background playing:
What I would like to do is to print on the Remote Command Center artist, title and albumArt labes and UIImage (labels an UIImage are taken from my station API) , but i was stuck just displaying the command center. Here is my code:
import UIKit
import AVKit
import MediaPlayer
class ViewController: UIViewController, AVAudioPlayerDelegate {
#IBAction func buttonPressed(_ sender: UIButton){
if isPlaying {
player.pause()
sender.setImage(playImage, for: .normal)
} else {
let url = "https://myradio.com/radio.mp3"
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers, .allowAirPlay])
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, options: [])
print("Playback OK")
try AVAudioSession.sharedInstance().setActive(true)
print("Session is Active")
} catch {
print(error)
}
player = AVPlayer(url: URL(string: url)!)
player.volume = 1.0
player.rate = 1.0
player.play()
sender.setImage(pauseImage, for: .normal)
}
isPlaying.toggle()
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
overrideUserInterfaceStyle = .light
setupRemoteTransportControls()
requestNowPlaying()
setupNowPlaying()
}
// Here is the API data downloading part, so i skipped it
//Command Center audio controls
func setupRemoteTransportControls() {
// Get the shared MPRemoteCommandCenter
let commandCenter = MPRemoteCommandCenter.shared()
// Add handler for Play Command
commandCenter.playCommand.addTarget { [unowned self] event in
if self.player.rate == 1.0 {
self.player.play()
return .success
}
return .commandFailed
}
// Add handler for Pause Command
commandCenter.pauseCommand.addTarget { [unowned self] event in
if self.player.rate == 1.0 {
self.player.pause()
return .success
}
return .commandFailed
}
}
func setupNowPlaying() {
// Define Now Playing Info
var nowPlayingInfo = [String : Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = "Test"
if let image = UIImage(named: "Default_albumart") {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { size in
return image
}
}
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = true
// Set the metadata
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
func updateNowPlaying(isPause: Bool) {
// Define Now Playing Info
let nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo!
//nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player.currentTime
//nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPause ? 0 : 1
// Set the metadata
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
//audio interruption
#objc func handleInterruption(notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}
// Switch over the interruption type.
switch type {
case .began:
print("Interruption began")
case .ended:
// An interruption ended. Resume playback, if appropriate.
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
player.play()
} else {
// An interruption ended. Don't resume playback.
}
default: ()
}
}
}
Here's what I've added in My AppDelegate.swift:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
application.beginReceivingRemoteControlEvents()
// Override point for customization after application launch.
return true
}
Remove .mixWithOthers from your category options.
I think the reasoning is that only the primary iOS audio app can control the remote screen. .mixWithOthers is for secondary audio apps.
Identify yourself as .longForm audio content provider with this code:
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, mode: AVAudioSessionModeDefault, routeSharingPolicy: .longForm)
For whole implementation of AirPlay2 check this Apple WWDC presentation: https://developer.apple.com/videos/play/wwdc2017/509/

How can I get access to ViewController from MPNowPlayingInfoCenter

I'm working on an audio player and came across this situation:
I have a TrackDetailView that opens to play a track when I click on a TableView cell. Also I have implemented background playback and MPNowPlayingInfoCenter.
When I press Pause or Play button in MPNowPlayingInfoCenter, I want the button image to change on my TrackDetailView as well, but I just can't do it. I will be glad for any help.
Important note(!) TrackDetailView and MPNowPlayingInfoCenter are in different classes. When I put them in one class everything works without problems.
My code:
class TrackDetailView: UIView {
var audioPlayer = AudioPlayer()
...
#IBOutlet var playPauseButton: UIButton!
...
//Loading with view
func set() {
setupMediaPlayerNotificationView()
}
}
class AudioPlayer {
var trackDetailView: TrackDetailView?
func setupMediaPlayerNotificationView() {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.addTarget { [unowned self] event in
if self.player.rate == 0.0 {
self.player.play()
self.trackDetailView?.playPauseButton.setImage(#imageLiteral(resourceName: "pause"), for: .normal)
}
return .commandFailed
}
commandCenter.pauseCommand.addTarget { [unowned self] event in
if self.player.rate == 1.0 {
self.player.pause()
self.trackDetailView?.playPauseButton.setImage(#imageLiteral(resourceName: "play"), for: .normal)
return .success
}
return .commandFailed
}
...
}
}
I think I have a problem with an instance of the TrackDetailView class.
You need to make sure that for this instance
var audioPlayer = AudioPlayer()
you set
audioPlayer.trackDetailView = self
e.x here
func set() {
audioPlayer.trackDetailView = self
audioPlayer.setupMediaPlayerNotificationView()
}

How to create a Cancel button on the screen that selects the file on Swift5?

I am using UIDocumentBrowser to retrieve files. But I am not able to place a back or cancel button in the navigation bar.
I want to make a cancellation button for this but I can't make a cancellation button. How can I solve this problem?
current code
import Foundation
import UIKit
#available(iOS 11.0, *)
class DocumentBrowserViewController : UIDocumentBrowserViewController, UIDocumentBrowserViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
browserUserInterfaceStyle = .dark
view.tintColor = .white
}
func documentBrowser(_ controller: UIDocumentBrowserViewController, didRequestDocumentCreationWithHandler importHandler: #escaping (URL?, UIDocumentBrowserViewController.ImportMode) -> Void) {
let newDocumentURL: URL? = nil
// Set the URL for the new document here. Optionally, you can present a template chooser before calling the importHandler.
// Make sure the importHandler is always called, even if the user cancels the creation request.
if newDocumentURL != nil {
importHandler(newDocumentURL, .move)
} else {
importHandler(nil, .none)
}
}
func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentURLs documentURLs: [URL]) {
guard let sourceURL = documentURLs.first else { return }
do{
try presentDocument(at: sourceURL)
} catch {
Log.Debug("\(error)")
}
}
func documentBrowser(_ controller: UIDocumentBrowserViewController, didImportDocumentAt sourceURL: URL, toDestinationURL destinationURL: URL) {
// Present the Document View Controller for the new newly created document
do{
try presentDocument(at: sourceURL)
} catch {
Log.Debug("\(error)")
}
}
func documentBrowser(_ controller: UIDocumentBrowserViewController, failedToImportDocumentAt documentURL: URL, error: Error?) {
// Make sure to handle the failed import appropriately, e.g., by presenting an error message to the user.
}
func presentDocument(at documentURL: URL) throws {
guard documentURL.startAccessingSecurityScopedResource() else {
throw IXError.fileAcessFailed
}
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let documentViewController = storyBoard.instantiateViewController(withIdentifier: "ViewController") as! ViewController
documentViewController.document = Document(fileURL: documentURL)
}
}
picture of cancellation button that I want
Help me a lot
Thanks in advance.
Do I understand correctly that you want to push a viewController (documentViewController) on the navigation stack and have a back button on the navigationBar that leads you back to your main viewController (DocumentBrowserViewController)? If so first you need to push documentViewController on the current navigation stack.
First of all, does the documentViewController appears?
What I see is that you instantiate a documentViewController, set it's document to Document(...) and end of story. I don't use storyboard but does instantiate presents the viewController?
If you provide more details I will update the answer. But general conclusion is in your presentDocument(...), you need:
self.navigationController?.pushViewController(documentViewController, animated: true)
I learned about the UIDocumentBrowserViewController class and succeeded in adding buttons. But the position of the button is not where I want it to be.
But this has solved my fundamental problem, so I'll end the question.
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
allowsDocumentCreation = false
allowsPickingMultipleItems = false
browserUserInterfaceStyle = .dark
view.tintColor = .white
let cancelbutton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(cancelButton(sender:)))
additionalTrailingNavigationBarButtonItems = [cancelbutton]
}
#objc func cancelButton(sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
}

Swift 4 & Xcode 10. Play video on app launch, when complete, reveal view controller

Firstly, I'm totally new to Xcode 10 and Swift 4, and I've searched here but haven't found code that works.
What I'm after:
On launching app to play a video which is stored locally (called "launchvideo").
On completion of video to display/move to a UIviewcontroller with a storyboard ID of "menu"
So far I have my main navigation controller with it's linked view controller.
I'm guessing I need a UIview to hold the video to be played in on this page?
Is there someone who can help a new guy out?
Thanks
Firstly change your launch screen storyboard to Main storyboard from project settings in General tab.
Create one view controller with following name and write code to implement AVPlayer to play video.
import UIKit
import AVFoundation
class VideoLaunchVC: UIViewController {
func setupAVPlayer() {
let videoURL = Bundle.main.url(forResource: "Video", withExtension: "mov") // Get video url
let avAssets = AVAsset(url: videoURL!) // Create assets to get duration of video.
let avPlayer = AVPlayer(url: videoURL!) // Create avPlayer instance
let avPlayerLayer = AVPlayerLayer(player: avPlayer) // Create avPlayerLayer instance
avPlayerLayer.frame = self.view.bounds // Set bounds of avPlayerLayer
self.view.layer.addSublayer(avPlayerLayer) // Add avPlayerLayer to view's layer.
avPlayer.play() // Play video
// Add observer for every second to check video completed or not,
// If video play is completed then redirect to desire view controller.
avPlayer.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1) , queue: .main) { [weak self] time in
if time == avAssets.duration {
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ViewController") as! ViewController
self?.navigationController?.pushViewController(vc, animated: true)
}
}
}
//------------------------------------------------------------------------------
override func viewDidLoad() {
super.viewDidLoad()
}
//------------------------------------------------------------------------------
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.setupAVPlayer() // Call method to setup AVPlayer & AVPlayerLayer to play video
}
}
Main.Storyboard:
Project Launch Screen File:
See following video also:
https://youtu.be/dvi0JKEpNTc
You have to load a video on launchvideoVC, like below way in swift 4 and above
import AVFoundation
import AVKit
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
initVideo()
}
func initVideo(){
do {
try AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print(error)
}
let path = Bundle.main.path(forResource: "yourlocalvideo", ofType:"mp4");
player = AVPlayer(url: NSURL(fileURLWithPath: path!) as URL)
NotificationCenter.default.addObserver(self, selector: #selector(launchvideoVC.itemDidFinishPlaying(_:)), name: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem)
DispatchQueue.main.async(execute: {() -> Void in
let playerLayer = AVPlayerLayer(player: self.player)
playerLayer.frame = self.view.bounds
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
playerLayer.zPosition = 1
self.view.layer.addSublayer(playerLayer)
self.player?.seek(to: CMTime.zero)
self.player?.play()
})
}
#objc func itemDidFinishPlaying(_ notification: Notification?) {
//move to whatever UIViewcontroller with a storyboard ID of "menu"
}
I think it's may help you.
Happy coding :)
First you make a new view controller with view and change your launch screen storyboard to Main storyboard from project settings in General tab.
And also add your video in folder.
Then just add my below code to your launch screenView controller:
import UIKit
import MediaPlayer
import AVKit
class LaunchViewController: UIViewController {
fileprivate var rootViewController: UIViewController? = nil
var player: AVPlayer?
var playerController = AVPlayerViewController()
override func viewDidLoad() {
super.viewDidLoad()
showSplashViewController()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func playVideo() {
let videoURL = NSURL(string: "videoplayback")
player = AVPlayer(url: videoURL! as URL)
let playerController = AVPlayerViewController()
playerController.player = player
self.addChildViewController(playerController)
// Add your view Frame
playerController.view.frame = self.view.frame
// Add subview in your view
self.view.addSubview(playerController.view)
player?.play()
}
private func loadVideo() {
//this line is important to prevent background music stop
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryAmbient)
} catch { }
let path = Bundle.main.path(forResource: "videoplayback", ofType:"mp4")
let filePathURL = NSURL.fileURL(withPath: path!)
let player = AVPlayer(url: filePathURL)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.view.frame
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
playerLayer.zPosition = -1
self.view.layer.addSublayer(playerLayer)
player.seek(to: kCMTimeZero)
player.play()
}
func showSplashViewControllerNoPing() {
if rootViewController is LaunchViewController {
return
}
loadVideo()
}
/// Simulates an API handshake success and transitions to MapViewController
func showSplashViewController() {
showSplashViewControllerNoPing()
delay(6.00) {
self.showMenuNavigationViewController()
}
}
public func delay(_ delay:Double, closure:#escaping ()->()) {
DispatchQueue.main.asyncAfter(
deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
}
/// Displays the MapViewController
func showMenuNavigationViewController() {
guard !(rootViewController is Home) else { return }
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let nav = storyboard.instantiateViewController(withIdentifier: "homeTab") as! Home
nav.willMove(toParentViewController: self)
addChildViewController(nav)
if let rootViewController = self.rootViewController {
self.rootViewController = nav
rootViewController.willMove(toParentViewController: nil)
transition(from: rootViewController, to: nav, duration: 0.55, options: [.transitionCrossDissolve, .curveEaseOut], animations: { () -> Void in
}, completion: { _ in
nav.didMove(toParentViewController: self)
rootViewController.removeFromParentViewController()
rootViewController.didMove(toParentViewController: nil)
})
} else {
rootViewController = nav
view.addSubview(nav.view)
nav.didMove(toParentViewController: self)
}
}
override var prefersStatusBarHidden : Bool {
switch rootViewController {
case is LaunchViewController:
return true
case is Home:
return false
default:
return false
}
}
}

Sync video in AVPlayerLayer and AVPlayerViewController

I'm working with AVPlayerto show the playing video using a URL in it. There are 2 parts to it:
1. Firstly, I've embedded the AVPlayer to a view's sublayer using AVPlayerLayer, i.e.
var player: AVPlayer?
func configure() {
let urlString = "https://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"
if let url = URL(string: urlString) {
self.player = AVPlayer(url: url)
let playerLayer = AVPlayerLayer(player: self.player)
playerLayer.frame = self.view.bounds
self.view.layer.addSublayer(playerLayer)
player.play()
}
}
The above code is working fine and the video is playing.
2. Secondly, on a UIButton tap, I'm presenting a AVPlayerViewController using the same AVPlayer instance that I created earlier, i.e.
#IBAction func onTapVideoButton(_ sender: UIButton) {
self.player?.pause()
let controller = AVPlayerViewController()
controller.player = self.player
self.present(controller, animated: true) {
self.player?.play()
}
}
The problem I'm facing here is, after the AVPlayerViewController opens, the video stops playing but the audio still plays on.
What I want is to sync the video in both AVPlayerLayer and AVPlayerViewController.
I think there is a problem when sharing a player already created to the AVPlayerViewController. I'm not sure why is stoping but it wont happen if you create a new AVPlayer for that controller. A way to sync your player and your AVPlayerViewController could be like this:
First, you create a Notification Name that you'll use when the AVPlayerViewController is dismiss (apple does not give you a way to know when the user dismiss the AVPlayerViewController):
extension Notification.Name {
static let avPlayerDidDismiss = Notification.Name("avPlayerDidDismiss")
}
Then, you extend AVPlayerViewController to post this notification when is going to be dismiss and to send the time when the user left the video:
extension AVPlayerViewController {
open override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if let seekTime = player?.currentTime() {
let userInfo = ["seekTime": seekTime]
NotificationCenter.default.post(name: .avPlayerDidDismiss, object: nil, userInfo: userInfo)
}
}
}
And in your ViewController you observe that notification, get the seekTime you want to go and use it to setup your avPlayer:
class ViewController: UIViewController {
var player: AVPlayer?
deinit {
NotificationCenter.default.removeObserver(self)
}
override func viewDidLoad() {
super.viewDidLoad()
configure()
NotificationCenter.default.addObserver(self, selector: #selector(avPlayerDidDismiss), name: .avPlayerDidDismiss, object: nil)
}
func configure() {
self.player = getPlayer()
let playerLayer = AVPlayerLayer(player: self.player)
playerLayer.frame = self.view.bounds
self.view.layer.addSublayer(playerLayer)
self.player?.play()
}
#IBAction func onTapVideoButton(_ sender: UIButton) {
self.player?.pause()
let controllerPlayer = getPlayer()
controllerPlayer.currentItem?.seek(to: self.player!.currentTime(), completionHandler: nil)
let controller = AVPlayerViewController()
controller.player = controllerPlayer
self.present(controller, animated: true, completion: {
controller.player?.play()
})
}
func getPlayer() -> AVPlayer {
let url = URL(string: "https://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")!
return AVPlayer(url: url)
}
#objc
private func avPlayerDidDismiss(_ notification: Notification) {
if let seekTime = notification.userInfo?["seekTime"] as? CMTime {
player?.currentItem?.seek(to: seekTime, completionHandler: nil)
player?.play()
}
}
}
Cons: It will send the notification for every AVPlayerViewController. You use add you as an observer when you need this info. Hope it can help.

Resources