How can I tap to close AVPlayer in Swift 5? - swift

I'm creating an extremely simple game, and I've hidden the video controls.
#IBAction func playVideo1(_ sender: Any) {
// play video connected to button 1
guard let firstVideo = Bundle.main.path(forResource: "Video1", ofType:"mp4") else {
debugPrint("Video not found")
return
}
// create an AVPlayer, passing it mp4
let player = AVPlayer(url: URL(fileURLWithPath: firstVideo))
// Create a new AVPlayerViewController and pass it a reference to the player.
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = false
// Modally present the player and call the player's play() method when complete.
present(controller, animated: true) {
player.play()
}
} // end playVideo1
One of the two options would be OK.
Option 1: Tap to close the video.
Option 2: Have the AVPlayer close automatically at the end of the video.
I appreciate any help.
Thanks!

You can add a tap gesture recognizer to AVPlayerViewController's view (for closing on tap), or you could subscribe to AVPlayerItemDidPlayToEndTime notification (for closing when video ends playing). Something like this:
#IBAction func playVideo1(_ sender: Any) {
// play video connected to button 1
guard let firstVideo = Bundle.main.path(forResource: "Video1", ofType:"mp4") else {
debugPrint("Video not found")
return
}
// create an AVPlayer, passing it mp4
let player = AVPlayer(url: URL(fileURLWithPath: firstVideo))
// Create a new AVPlayerViewController and pass it a reference to the player.
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = false
//for closing when video ends
NotificationCenter.default.addObserver(self, selector: #selector(closePlayer), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: controller.player?.currentItem)
//for closing on tap
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(closePlayer))
controller.view.addGestureRecognizer(tapGestureRecognizer)
// Modally present the player and call the player's play() method when complete.
present(controller, animated: true) {
player.play()
}
} // end playVideo1
#objc func closePlayer() {
dismiss(animated: true)
//if you go notification route, don't forget to remove observer
NotificationCenter.default.removeObserver(self)
}

For closing the videoPlayer when on user tap you can use the following-
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(closePlayerOnTouch))
controller.view.addGestureRecognizer(tapGestureRecognizer)
and then add this after the button -
#objc func closePlayerOnTouch() {
dismiss(animated: true)
NotificationCenter.default.removeObserver(self)
}
#Predrag's answer is really awesome though.

import UIKit
import AVFoundation
class ViewController: UIViewController {
#IBOutlet weak var btnPlay: UIButton!
var player:AVPlayer?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func btnPress(sender: AnyObject) {
if (btnPlay.titleLabel?.text == "Play") {
initPlayer()
btnPlay.setTitle("Stop", forState: UIControlState.Normal)
} else {
stopPlayer()
btnPlay.setTitle("Play", forState: UIControlState.Normal)
}
}
func initPlayer() {
if let play = player {
print("playing")
play.play()
} else {
print("player allocated")
player = AVPlayer(URL: NSURL(string: "http://streaming.radio.rtl.fr/rtl-1-48-192")!)
print("playing")
player!.play()
}
}
func stopPlayer() {
if let play = player {
print("stopped")
play.pause()
player = nil
print("player deallocated")
} else {
print("player was already deallocated")
}
}
}

Related

I keep using this same chunk of code over and over. How do I consolidate it?

Being the Swift noob that I am, I find myself copying and pasting code. I know I'm supposed to use the DRY method and not do this, but this particular bit of code has me stumped. I tried creating a struct to hold it, but the struct threw all sorts of errors. I don't quite understand classes and how I would subclass it, and so maybe that's the solution. I just don't know how to do it? Or maybe an extension?
Anyway, here is the code I keep copying and pasting in each new view controller:
import UIKit
import AVKit
class Step3JobSummaryVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
...
var sourceVCIdentity = "setup"
var initialLaunch = true
let playerVC = AVPlayerViewController()
let video = Video.step3JobsSummary
...
// ------------------------
// Autoplay Video Functions
// ------------------------
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if initialLaunch == true {
showUnplayedVideo()
initialLaunch = false
}
Video.updatePlaybackTime(playerVC: playerVC, videoURL: video.url, firebaseVideoID: video.firebaseID)
}
func showUnplayedVideo() {
// 1. get current video data
Video.getFirebaseData(firebaseVideoID: video.firebaseID) { (playbackTime, watched) in
if !watched {
// 2. show setup video popup on first load
guard let videoURL = URL(string: self.video.url) else { print("url error"); return }
let player = AVPlayer(url: videoURL)
self.playerVC.player = player
// 3. fast forward to where user left off (if applicable)
player.seek(to: CMTimeMakeWithSeconds(playbackTime, 1))
// 4. dismiss the player once the video is over and update Firebase
NotificationCenter.default.addObserver(self,
selector: #selector(self.playerDidFinishPlaying),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: self.playerVC.player?.currentItem)
self.present(self.playerVC, animated: true) {
self.playerVC.player?.play()
}
}
}
}
#objc func playerDidFinishPlaying(note: NSNotification) {
self.playerVC.dismiss(animated: true)
Video.updateFirebase(firebaseVideoID: video.firebaseID)
}
Any help would be great. I'm just trying to learn :-)
EDIT #1
This is my attempt at an extension. I simplified and refactored my code, but as before, it's giving me an error. This time the error is 'extensions must not contain stored properties'. So how do I access the AVPlayerController?!?
extension UIViewController {
let playerVC = AVPlayerViewController()
func showUnplayedVideo(video: Video) {
// 1. get current video data
Video.getFirebaseData(firebaseVideoID: video.firebaseID) { (playbackTime, watched) in
if !watched {
// 2. show setup video popup on first load
guard let videoURL = URL(string: video.url) else { print("url error"); return }
let player = AVPlayer(url: videoURL)
self.playerVC.player = player
// 3. fast forward to where user left off (if applicable)
player.seek(to: CMTimeMakeWithSeconds(playbackTime, 1))
// 4. dismiss the player once the video is over and update Firebase
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime,
object: playerVC.player?.currentItem,
queue: .main) { (notification) in
self.playerDidFinishPlaying(note: notification as NSNotification)
self.present(self.playerVC, animated: true) {
self.playerVC.player?.play()
}
}
}
}
func playerDidFinishPlaying(note: NSNotification, video: Video) {
self.playerVC.dismiss(animated: true)
Video.updateFirebase(firebaseVideoID: video.firebaseID)
}
}
EDIT #2
So I got the code to compile without any errors, but now it's not firing. Aargh.
extension UIViewController {
func showUnplayedVideo(playerVC: AVPlayerViewController, video: Video) {
print("does this code even fire?")
// 1. get current video data
Video.getFirebaseData(firebaseVideoID: video.firebaseID) { (playbackTime, watched) in
if !watched {
// 2. show setup video popup on first load
guard let videoURL = URL(string: video.url) else { print("url error"); return }
let player = AVPlayer(url: videoURL)
playerVC.player = player
// 3. fast forward to where user left off (if applicable)
player.seek(to: CMTimeMakeWithSeconds(playbackTime, 1))
// 4. dismiss the player once the video is over and update Firebase
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime,
object: playerVC.player?.currentItem,
queue: .main) { (notification) in
self.playerDidFinishPlaying(playerVC: playerVC, note: notification as NSNotification, video: video)
self.present(playerVC, animated: true) {
playerVC.player?.play()
}
}
}
}
}
func playerDidFinishPlaying(playerVC: AVPlayerViewController, note: NSNotification, video: Video) {
playerVC.dismiss(animated: true)
Video.updateFirebase(firebaseVideoID: video.firebaseID)
}
}
Why won't this work?
I would start with defining a protocol for your functionality something like this:
protocol VideoPlayable {
func showUnplayedVideo(playerVC: AVPlayerViewController, video: Video)
}
And then add a default implementation to it
extension VideoPlayable where Self: UIViewController {
func showUnplayedVideo(playerVC: AVPlayerViewController, video: Video) {
print("does this code even fire?")
// 1. get current video data
Video.getFirebaseData(firebaseVideoID: video.firebaseID) { (playbackTime, watched) in
if !watched {
// 2. show setup video popup on first load
guard let videoURL = URL(string: video.url) else { print("url error"); return }
let player = AVPlayer(url: videoURL)
playerVC.player = player
// 3. fast forward to where user left off (if applicable)
player.seek(to: CMTimeMakeWithSeconds(playbackTime, 1))
// 4. dismiss the player once the video is over and update Firebase
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime,
object: playerVC.player?.currentItem,
queue: .main) { (notification) in
self.playerDidFinishPlaying(playerVC: playerVC, note: notification as NSNotification, video: video)
}
self.present(playerVC, animated: true) {
playerVC.player?.play()
}
}
}
}
private func playerDidFinishPlaying(playerVC: AVPlayerViewController, note: NSNotification, video: Video) {
playerVC.dismiss(animated: true)
Video.updateFirebase(firebaseVideoID: video.firebaseID)
}
}
Thanks to this when you add the VideoPlayable protocol to a controller you will have your custom functionality available, and other controllers that shouldn't have the functionality won't have the access to this method.
Also if you really want to have access to the method
func playerDidFinishPlaying(playerVC: AVPlayerViewController, note: NSNotification, video: Video)
Add it to the protocol and remove the private statement from the implementation.
And you video player wasn't being shown because you added the presenting of the player into the notification block.
Also, consider adding proper self handling to your blocks. Right now I think its possible that the self can be caught in the blocks.
Just to let you know the statement
where Self: UIViewController
Limits the access of the implementation to UIViewControllers, so if you add the protocol to a UIView subclass you wont get access to the default implementation. You will then need to add a new one :) this prevents the missing of the protocol in places you dont want it to be used.
You can just move all the reused code into a separate class:
class Step3JobSummaryVC: UIViewController {
let videoPlayer = VideoPlayer(video: Video.step3JobsSummary)
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
videoPlayer.start(on: self)
}
}
final
class VideoPlayer {
private var initialLaunch: Bool = true
private let playerVC = AVPlayerViewController()
private let video: Video
init(video: Video) {
self.video = video
}
func start(on viewController: UIViewController) {
if initialLaunch == true {
showUnplayedVideo(on: viewController)
initialLaunch = false
}
Video.updatePlaybackTime(playerVC: playerVC, videoURL: video.url, firebaseVideoID: video.firebaseID)
}
func showUnplayedVideo(on viewController: UIViewController) {
// 1. get current video data
Video.getFirebaseData(firebaseVideoID: video.firebaseID) { (playbackTime, watched) in
if !watched {
// 2. show setup video popup on first load
guard let videoURL = URL(string: self.video.url) else { print("url error"); return }
let player = AVPlayer(url: videoURL)
self.playerVC.player = player
// 3. fast forward to where user left off (if applicable)
player.seek(to: CMTimeMakeWithSeconds(playbackTime, preferredTimescale: 1))
// 4. dismiss the player once the video is over and update Firebase
NotificationCenter.default.addObserver(self,
selector: #selector(self.playerDidFinishPlaying),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: self.playerVC.player?.currentItem)
viewController.present(self.playerVC, animated: true) {
self.playerVC.player?.play()
}
}
}
}
#objc func playerDidFinishPlaying(note: NSNotification) {
self.playerVC.dismiss(animated: true)
Video.updateFirebase(firebaseVideoID: video.firebaseID)
}
}
Have you considered using a more traditional inheritance model?
class VideoPlayingBaseController: : UIViewController, UITableViewDataSource, UITableViewDelegate {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if initialLaunch == true {
showUnplayedVideo()
initialLaunch = false
}
Video.updatePlaybackTime(playerVC: playerVC, videoURL: video.url, firebaseVideoID: video.firebaseID)
}
func showUnplayedVideo() {
// 1. get current video data
Video.getFirebaseData(firebaseVideoID: video.firebaseID) { (playbackTime, watched) in
if !watched {
// 2. show setup video popup on first load
guard let videoURL = URL(string: self.video.url) else { print("url error"); return }
let player = AVPlayer(url: videoURL)
self.playerVC.player = player
// 3. fast forward to where user left off (if applicable)
player.seek(to: CMTimeMakeWithSeconds(playbackTime, 1))
// 4. dismiss the player once the video is over and update Firebase
NotificationCenter.default.addObserver(self,
selector: #selector(self.playerDidFinishPlaying),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: self.playerVC.player?.currentItem)
self.present(self.playerVC, animated: true) {
self.playerVC.player?.play()
}
}
}
}
#objc func playerDidFinishPlaying(note: NSNotification) {
self.playerVC.dismiss(animated: true)
Video.updateFirebase(firebaseVideoID: video.firebaseID)
}
}
Then have your classes that use it:
class Step3JobSummaryVC: VideoPlayingBaseController {
//more code here
}

Swift: multiple functions working at the same time

In my app I have 2 UIButtons, each button plays 2 sounds (plays one sound, pauses, plays the other sound), and the UIButtons image changes every time it is pressed, my app also has a few tabs.the problem is that when I press a UIButton (which takes about 4 seconds for the sounds to be played and be done) I can not press another button, or even switch between tabs, the app freezes until the action of UIbutton gets done with, then I can press other buttons or switch tabs.
here's my code:
import UIKit
import AVFoundation
import CoreData
class FirstViewController: UIViewController {
var player:AVAudioPlayer = AVAudioPlayer()
var player2:AVAudioPlayer = AVAudioPlayer()
#IBAction func play(_ sender: Any) {
player.play()
sleep(1)
player2.play()
}
#IBAction func play2(_ sender: Any) {
player.play()
sleep(1)
player2.play()
}
#IBOutlet weak var play: UIButton!
#IBOutlet weak var play2: UIButton!
var buttonActive = false
#IBAction func pPressed(_ sender: Any) {
if buttonActive {
play.setBackgroundImage(UIImage(named: "SwitchD-Icon-40"), for: .normal)
} else {
play.setBackgroundImage(UIImage(named: "Switch-Icon-40"), for: .normal)
}
buttonActive = !buttonActive
}
#IBAction func p2Pressed(_ sender: Any) {
if buttonActive {
play2.setBackgroundImage(UIImage(named: "SwitchD-Icon-40"), for: .normal)
} else {
play2.setBackgroundImage(UIImage(named: "Switch-Icon-40"), for: .normal)
}
buttonActive = !buttonActive
}
override func viewDidLoad() {
super.viewDidLoad()
play.setBackgroundImage(UIImage(named: "SwitchD-Icon-40"), for: .normal)
play2.setBackgroundImage(UIImage(named: "SwitchD-Icon-40"), for: .normal)
do
{
let audioPath = Bundle.main.path(forResource: "DTMF-1", ofType: "mp3")
try player = AVAudioPlayer(contentsOf: NSURL(fileURLWithPath: audioPath!) as URL)
let audioPath2 = Bundle.main.path(forResource: "DTMF-2", ofType: "mp3")
try player2 = AVAudioPlayer(contentsOf: NSURL(fileURLWithPath: audioPath2!) as URL)
}
Is there any way to make my app be faster?
One famous way to implement do something, pause and do other thing is to use Timer.
The following code also includes:
How to properly instantiate AVAudioPlayer
How to invalidate working Timer
How to stop AVAudioPlayer when it may be reused soon
How to stop playing on transition
Please try:
class FirstViewController: UIViewController {
var player: AVAudioPlayer? //### You should not instantiate unused objects
var player2: AVAudioPlayer?
var secondSoundTimer: Timer?
private func startPlaying() {
stopPlaying()
player?.play()
//### Using `Timer` is one way to achieve pause-like thing.
//### If you need to target older iOSs than 10.0, you need to modify this part.
secondSoundTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) {timer in
self.player2?.play()
self.secondSoundTimer?.invalidate()
self.secondSoundTimer = nil
}
}
private func stopPlaying() {
secondSoundTimer?.invalidate()
secondSoundTimer = nil
if let player = player, player.isPlaying {
//### Stop playing (keeping internal `prepared` state) and rewind to the first place.
player.pause()
player.currentTime = 0
}
if let player2 = player2, player2.isPlaying {
player2.pause()
player2.currentTime = 0
}
}
#IBOutlet weak var play: UIButton!
#IBOutlet weak var play2: UIButton!
var buttonActive = false
#IBAction func pPressed(_ sender: Any) {
startPlaying()
if buttonActive {
play.setBackgroundImage(UIImage(named: "SwitchD-Icon-40"), for: .normal)
} else {
play.setBackgroundImage(UIImage(named: "Switch-Icon-40"), for: .normal)
}
buttonActive = !buttonActive
}
//### Don't you need `button2Active`?
#IBAction func p2Pressed(_ sender: Any) {
startPlaying()
if buttonActive {
play2.setBackgroundImage(UIImage(named: "SwitchD-Icon-40"), for: .normal)
} else {
play2.setBackgroundImage(UIImage(named: "Switch-Icon-40"), for: .normal)
}
buttonActive = !buttonActive
}
override func viewDidLoad() {
super.viewDidLoad()
play.setBackgroundImage(UIImage(named: "SwitchD-Icon-40"), for: .normal)
play2.setBackgroundImage(UIImage(named: "SwitchD-Icon-40"), for: .normal)
do {
//### You can get URL directly from Bundle
//### It's better to apply forced-unwrapping as soon as possible, if you need it eventually.
let audioURL = Bundle.main.url(forResource: "DTMF-1", withExtension: "mp3")!
player = try AVAudioPlayer(contentsOf: audioURL)
//### `prepareToPlay()` improves response for the first play.
player?.prepareToPlay()
let audioURL2 = Bundle.main.url(forResource: "DTMF-2", withExtension: "mp3")!
player2 = try AVAudioPlayer(contentsOf: audioURL2)
player2?.prepareToPlay()
} catch {
print(error)
//... or any other things you need...
}
//...
}
//### Stop playing on transition to other ViewConrollers.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stopPlaying()
}
}

How To Make Play/Pause Button on Apple TV Remote Play/Pause AVAudioPlayer

I am making a music app for tvOS which has a AVAudioPlayer. I was wondering how to make the Play/Pause Button on Apple TV remote Play/Pause the AVAudioPlayer? Here's my current code:
import UIKit
import AVFoundation
class MusicViewController: UIViewController, AVAudioPlayerDelegate {
#IBOutlet weak var progressView: UIProgressView!
var audioPlayer = AVAudioPlayer()
override func viewDidLoad() {
super.viewDidLoad()
do {
audioPlayer = try AVAudioPlayer(contentsOf: URL.init(fileURLWithPath: Bundle.main.path(forResource: "Roots", ofType: "mp3")!))
audioPlayer.prepareToPlay()
var audioSession = AVAudioSession.sharedInstance()
Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(updateAudioProgressView), userInfo: nil, repeats: true)
progressView.setProgress(Float(audioPlayer.currentTime/audioPlayer.duration), animated: false)
do {
try audioSession.setCategory(AVAudioSessionCategoryPlayback)
}
}
catch {
print(error)
}
audioPlayer.delegate = self
}
// Set the music to automaticly play and stop
override func viewDidAppear(_ animated: Bool) {
audioPlayer.play()
}
override func viewDidDisappear(_ animated: Bool) {
audioPlayer.stop()
}
func updateAudioProgressView()
{
if audioPlayer.isPlaying
{
// Update progress
progressView.setProgress(Float(audioPlayer.currentTime/audioPlayer.duration), animated: true)
}
}
}
I've been looking arround trying to figure this out. I haven't worked with tvOS before, so this is new for me. Thanks so much for the help!
These functions have been working for us. They add a gesture recognizer that listens for the play/pause button on the remote. You can add this to your app delegate.
func initializePlayButtonRecognition() {
addPlayButtonRecognizer(#selector(AppDelegate.handlePlayButton(_:)))
}
func addPlayButtonRecognizer(_ selector: Selector) {
let playButtonRecognizer = UITapGestureRecognizer(target: self, action:selector)
playButtonRecognizer.allowedPressTypes = [NSNumber(value: UIPressType.playPause.rawValue as Int)]
self.window?.addGestureRecognizer(playButtonRecognizer)
}
func handlePlayButton(_ sender: AnyObject) {
if audioPlayer.isPlaying {
audioPlayer.pause() {
} else {
audioPlayer.play()
}
}

Stop video playback on Siri Remote Menu Button press

In my application I do play a video with that function
func easyRandom_play(episodeTitle:String){
self.backgroundMusic?.stop()
backgroundMusic?.stop()
let link = "http://46.235.26.87/\(__TITLENAME_FOLDERNAME_DOWNLOAD__)/\(episodeTitle).m4v"
print(link)
let url:NSURL = NSURL(string: link)!
let player = AVPlayer(URL: url)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
self.presentViewController(playerViewController, animated: true) {
playerViewController.player!.play()
}
}
Now there is no way to stop the video - normally the menu button should take me back and end the playback.
Is there a way to implement this?
You can add a UITapGestureRecognizer to handle the Menu button.
override func viewDidLoad() {
// Setup Menu Button recognizer
let menuGesture = UITapGestureRecognizer(target: self, action: #selector(ViewController.handleMenuGesture(_:)))
menuGesture.allowedPressTypes = [NSNumber(integer: UIPressType.Menu.rawValue)]
self.view.addGestureRecognizer(menuGesture)
}
func handleMenuGesture(tap: UITapGestureRecognizer) {
print("Menu Gesture")
// Stop video and dismiss view controller here
}

AVPlayer will not stop playing when going back to the previous view controller (SWIFT)

I have two View Controllers a TableViewController where i have a list of musics and a UIViewController where it displays the music details and plays the music. The music automatically plays when the view is loaded and pauses when the pause button is pressed. However whenever I go back to the previous TableViewController to select another music, the music continues to play. And if i select another music, both of them are playing together
override func viewDidLoad() {
super.viewDidLoad()
timeLabel.text = "00:00"
if let object = currentObject {
audioTitle.text = object["audioTitle"] as? String
let days = object["daysActive"] as! Int
daysActive.text = "Powertalks: Day \(days)"
var initialThumbnail = UIImage(named: "trc_app_icon.png")
audioImage.image = initialThumbnail
if let thumbnail = object["image"] as? PFFile {
audioImage.file = thumbnail
audioImage.loadInBackground()
}
if let audioFile = object["audioFile"] as? PFFile {
if let audioPath: String = audioFile.url {
audioPlayer = AVPlayer(URL: NSURL(string: audioPath))
audioSlider.minimumValue = 0
audioSlider.maximumValue = Float(CMTimeGetSeconds(audioPlayer.currentItem.asset.duration))
audioSlider.value = Float(CMTimeGetSeconds(audioPlayer.currentTime()))
audioPlayer.volume = volumeSlider.value
playAudio()
}
}
}
timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: Selector("updateSlider"), userInfo: nil, repeats: true )
}
You have to pause the player when the view disappears. Although AVPlayer doesn't have a stop method, you can set the rate to 0.0 (or use pause()) and set currentItem to nil to achieve the same effect. Try using the below code (not tested)
override func viewWillDisappear(animated: Bool) {
audioPlayer.pause()
audioPlayer.currentItem = nil
}
I tested the code below which works fine:
override func viewWillDisappear(animated: Bool) {
audioPlayer.pause()
audioPlayer.currentItem = nil
}
You could also use:
audioPlayer.stop()