Swift 2.0 MPMusicPlayerController will not always play song on initial click - ios

I am new to Swift and have spent days researching, but I can't seem to figure out why my code will only play the selected song about 75% of the time when the app first "starts up". The app has 3 VC's: Artist, Album, Songs. When you get to the songs VC, the user clicks a song but it won't always play. Most of the time it will (and at this moment, there isn't anything else playing).
class ViewController: UIViewController {
var qrySongs = MPMediaQuery()
var myMPMusicPlayerController = MPMusicPlayerController()
override func viewDidLoad() {
super.viewDidLoad()
self.myMPMusicPlayerController = MPMusicPlayerController.systemMusicPlayer()
// Query songs
let predicateByAlbumTitle = MPMediaPropertyPredicate(value: selectedAlbumTitle, forProperty: MPMediaItemPropertyAlbumTitle)
qrySongs = MPMediaQuery.songsQuery()
qrySongs.addFilterPredicate(predicateByAlbumTitle)
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let myCollectionToPlay = MPMediaItemCollection(items: qrySongs.items!)
self.myMPMusicPlayerController.setQueueWithItemCollection(myCollectionToPlay)
self.myMPMusicPlayerController.nowPlayingItem = myCollectionToPlay.items[indexPath.row]
print ("Starting to play selectedSongIndexNum = ", indexPath.row)
self.myMPMusicPlayerController.prepareToPlay()
self.myMPMusicPlayerController.play()
if self.myMPMusicPlayerController.playbackState != .Playing {
self.myMPMusicPlayerController.play() // Try again
}
print (myMPMusicPlayerController.nowPlayingItem?.title)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: UITableViewCell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "songIdCell")
let rowItem = qrySongs.collections![indexPath.row]
cell.textLabel!.text = rowItem.items[0].title
}
}
Output when it doesn't work:
Starting to play selectedSongIndexNum = 7
nil
Output when it does work (plus the song is playing):
Starting to play selectedSongIndexNum = 7
Optional("Last Child")
Keep in mind that if you click again, it will ALWAYS work (no matter which song you click on). If you go back to Artists and then forward to Songs, it will ALWAYS work. There are no other players playing in the background. Thanks ahead of time for any help.

Related

How to handle tableView cell audio play and pause button using Swift?

My scenario, I am recording audio and save into Coredata then listing into tableView with customcell play/pause single button. I cant able to do below things
Detect end of playback and change audio play cell button image pause to play
While first row cell audio playing time, If user click second row play button need to stop first row cell audio play and change image pause to play
then second row cell button play to pause
How to do above two operations?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "Cell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! CustomCell
// Configure the cell...
cell.likeBtn.addTarget(self, action: #selector(TableViewController.liked), for: .touchUpInside)
return cell
}
#objc func liked(sender: UIButton) {
// Here I need to change button icon and handle row
}
Is kind of difficult to explain without knowing more about the rest of the app. It's important to give a little more context. I'll try to give you an answer assuming certain things.
First I'll asume you have Song objects that looks like this:
public struct Song: Equatable {
let name: String
let fileName: String
}
Your database has the following public methods and property:
class DB {
func getSong(_ position: Int) -> Song?
func getPosition(_ song: Song) -> Int?
var count: Int
}
To make it easy for this sample code on the init some predefine data is initialize.
Also there is a Player object that manages playing audio with the following public methods:
class Player {
func currentlyPlaying() -> Song?
func play(this song: Song)
func stop()
}
Now with this previously defined I created a custom cell to display the name and a button for each Song in the database. The definition is this:
class CustomCell : UITableViewCell {
#IBOutlet weak var label: UILabel!
#IBOutlet weak var button: UIButton!
}
And looks like:
Next let's define the tableview's datasource methods. In each cell a target is added for each button's touchUpInside event (as you defined it in the question).
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return DB.shared.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell",
for: indexPath) as! CustomCell
cell.label.text = DB.shared.getSong(indexPath.row)!.name
cell.button.addTarget(self, action: #selector(buttonTapped(_:)),
for: .touchUpInside)
return cell
}
Next let's define a helper method to locate a UIVIew inside a TableView. With this method we can get the IndexPath of any control inside any cell in a TableView. The return value is optional in order to return nil if not found.
func getViewIndexInTableView(tableView: UITableView, view: UIView) -> IndexPath? {
let pos = view.convert(CGPoint.zero, to: tableView)
return tableView.indexPathForRow(at: pos)
}
Another helper method was define to change the image of a button with an animation:
func changeButtonImage(_ button: UIButton, play: Bool) {
UIView.transition(with: button, duration: 0.4,
options: .transitionCrossDissolve, animations: {
button.setImage(UIImage(named: play ? "Play" : "Stop"), for: .normal)
}, completion: nil
}
A method was necessary to stop any current playing song. The first thing is to check wether a song is playing, if so call Player's stop method. Then let's localize the position of the Song in the database, which in my case corresponds to the position in the TableView; having this let's create a IndexPath to get the corresponding cell, finally call changeButtonImage with the cell's button to change the image.
func stopCurrentlyPlaying() {
if let currentSong = Player.shared.currentlyPlaying() {
Player.shared.stop()
if let indexStop = DB.shared.getPosition(currentSong) {
let cell = tableView.cellForRow(at: IndexPath(item: indexStop, section: 0)) as! CustomCell
changeButtonImage(cell.button, play: true)
}
}
}
The buttonTapped method which starts playing a song have some logic inside. First the method signature needs #objc in order to be used in the addTarget method. The logic is a follows:
Localize the button's IndexPath in the table view using the helper method.
Localize the song in the database, the row number in the table corresponds to the order in the database.
If there is a song currently playing and is the same as the one localize for the button tapped it means we just want to stop the song, so stopCurrentlyPlaying is called and the method returns.
If is not the same song or nothing is playing let's call: stopCurrentlyPlaying, start playing the new song and change the tapped button's image to a Stop image.
The code looks like this:
#objc func buttonTapped(_ sender: UIButton) {
// Let's localize the index of the button using a helper method
// and also localize the Song i the database
if let index = getViewIndexInTableView(tableView: tableView, view: sender),
let song = DB.shared.getSong(index.row) {
// If a the song located is the same it's currently playing just stop
// playing it and return.
guard song != Player.shared.currentlyPlaying() else {
stopCurrentlyPlaying()
return
}
// Stop any playing song if necessary
stopCurrentlyPlaying()
// Start playing the tapped song
Player.shared.play(this: song)
// Change the tapped button to a Stop image
changeButtonImage(sender, play: false)
}
}
Here is a little video of the sample app working: sample app

Using WebViews With Firebase Database

Recently in my app I have been using Firebase to store information for my app and it has worked well. Now I am using it to stream videos with a web view being used in the tableview to display Youtube videos. When trying to link the WebView to the database, I get an error that says:
Type 'video' has no subscript members
What would be causing this?
Here is the code:
import UIKit
import Firebase
class videoController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var ref = DatabaseReference()
var video = [UIWebView]()
var databaseHandle:DatabaseHandle = 0
#IBOutlet var videoController: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
ref = Database.database().reference()
databaseHandle = ref.child("Videos").observe(.childAdded) { (snapshot) in
let post = snapshot.value as? UIWebView
if let actualPost = post {
self.video.append(actualPost)
self.videoController.reloadData()
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return video.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let video = tableView.dequeueReusableCell(withIdentifier: "video") as! video
video.videos.allowsInlineMediaPlayback = video[indexPath.row]
return(video)
}
}
This line:
let video = tableView.dequeueReusableCell(withIdentifier: "video") as! video
is your problem. This creates a new, local variable named video and it hides your video array property. Change it to:
let videoCell = tableView.dequeueReusableCell(withIdentifier: "video") as! video
Here's the whole method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let videoCell = tableView.dequeueReusableCell(withIdentifier: "video") as! video
videoCell.videos.allowsInlineMediaPlayback = video[indexPath.row]
return videoCell
}
But on top of all of that, why do you have an array of web views? You certainly are not getting web views from Firebase.
And please fix your naming conventions. Class, struct, and enum names should start with uppercase letters. Variable, function, and case names start with lowercase letters. And use descriptive names. Naming everything simply video is confusing.
And change your video array to videos.

Get the video duration once the youtube video is loaded

Im working on a project where I load list of youtube videos to a UITableView. As to load and play them I use the YouTube-Player-iOS-Helper pod. Based on my understanding I use "playerViewDidBecomeReady" delegate method to determine if the video is loaded and once that calls I update my label with the video duration. However for some reason it doesnt get update all the time. My code as bellow. What am I missing
func playerViewDidBecomeReady(_ playerView: YTPlayerView) {
print("player is ready to play")
self.updateLabel(cell:cell,videoView:YTPlayer)
}
static func updateLabel(cell:UITableViewCell,videoView:YTPlayerView) {
var videoView = cell.viewWithTag(TABLE_CELL_TAGS.webView) as! YTPlayerView!
let durationSecs = String(describing: videoView?.duration())
var time = videoView?.duration()
cell.textLabel.text = time
}
My UITableViewDelegates as bellow
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier:Utils.isIpad() ? "iPadVideosCell":"videosCell", for: indexPath)
self.cell = cell
var videoView = cell.viewWithTag(5) as! YTPlayerView!
self.YTPlayer = videoView
videoView?.delegate = self
return cell
}

How can I stop video when user is scrolling

I have a tableView which gets data from the database. Some of the cells in my TableView contain videos in them and when we come across a video it displays properly.
My issue is that if someone keeps scrolling then that video still keeps playing. I would like to change that and if the user watches part of a video then keeps scrolling then I would like to end the video right there. I think I'm close to solving that issue but not quite there yet.
This is my code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Home", for: indexPath) as! Home
let filename = streamsModel.video_image[indexPath.row] as NSString
if filename.pathExtension == "mov" {
let movieURL = URL(string: streamsModel.video_image[indexPath.row])
cell.videoCapHeight.constant = CGFloat(Float(cell.pic_height!))
cell.playerView = AVPlayer(url: movieURL!)
cell.MyAVPlayer.player = cell.playerView
cell.MyAVPlayer.showsPlaybackControls = false
cell.MyAVPlayer.view.frame = cell.videoView.bounds
cell.videoView.addSubview(cell.MyAVPlayer.view)
controller.addChildViewController(cell.MyAVPlayer)
cell.playerView?.isMuted = false
cell.MyAVPlayer.player?.play()
}
return cell
}
That code above simply checks to see if the streamsModel.video_image has a .mov extension (JSON). If it does, then it gets the movie/video and displays it in a AVPlayer.
As you can see from the code, the Play() method is used so videos start playing automatically. The problem is that if a video is, for instance, 1 minute long and you watch 20 seconds of it and you scroll down, you still hear that video playing and I would like to stop the video as soon as the user scrolls away.
This is my scrollView code now I just need to stop the videos here on scroll or maybe in UITableViewCell:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (self.lastContentOffset > scrollView.contentOffset.y + 0) {
print("scrolling up")
}
else if (self.lastContentOffset < scrollView.contentOffset.y) {
print("scrolling down")
}
self.lastContentOffset = scrollView.contentOffset.y
}
Dan's suggestion of using the table view's delegate method would work well for you here meaning you can pause the video as the cell moves off screen.
Something like this should work:
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
// Dequeue the cell
let cell = tableView.dequeueReusableCell(withIdentifier: "Home", for: indexPath) as! Home
// Check the player object is set (unwrap it)
if let player = cell.MyAVPlayer.player {
// Check if the player is playing
if player.rate != 0 {
// Pause the player
player.pause()
}
}
}
I would suggest that automatically pausing and playing again could be a little bit laggy depending on if there is buffering going on etc... so you may want to consider allowing the user to play the videos on a tap.

What is the best place to add AVPlayer or MPMoviePlayerController in UITableViewCell?

I try to play video in UITableViewCell when a user clicks on play button in cell.
Scene 1:
If you are willing to auto play video's like Facebook or Instagram in each cell then consider adding AVPlayer in cellForRowAtIndexPath.
AVPlayer will not allow you to change the url being played (AVURLAsset url) once created. So If you are using AVPlayer and providing ur own view to player I afraid creating AVPlayer instance every time in cellForRowAtIndexPath is the only solution.
On the other hand if you are planning to use AVPlayerViewController, you can create AVPlayer viewController instance in cell's awakeFromNib so you will have only one instance of player per cell and in cellForRowAtIndexPath you can pass different url each time. You can use prepareForReuse method of cell to pause the player and resetting all your player properties.
Scene 2:
If you are planning to play once user taps on button in cell, consider creating AVPlayer/AVPlayerViewController on button tap and play the video. Use prepareForReuse to reset the properties of ur player to ensure you don't play wrong video when u scroll and cell gets reused.
EDIT:
As OP has asked for some code snippet, providing skeleton code. This is not a copy paste code, idea is just to give idea of how to use AVPlayerViewController inside cell the code might have compiler issues.
//create a custom tableViewCell subclass
class ImageTableViewCell: UITableViewCell {
#IBOutlet weak var carsImageView: UIImageView!
var playerController : AVPlayerViewController? = nil
var passedURL : URL! = nil
override func awakeFromNib() {
super.awakeFromNib()
playerController = AVPlayerViewController()
// Initialization code
}
func configCell(with url : URL) {
//something like this
self.passedURL = url
//show video thumbnail with play button on it.
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func playOrPauseVideo(_ sender: UIButton) {
let player = AVPlayer(url: url)
playerController.player = player
playerController?.showsPlaybackControls = true
playerController.player.play()
//add playerController view as subview to cell
}
override func prepareForReuse() {
//this way once user scrolls player will pause
self.playerController.player?.pause()
self.playerController.player = nil
}
}
The code above sets the PlayerController to nil in prepareForReuse() so when user scrolls the tableView and cell goes out of tableView Frame and gets reused, player will pause and will not retain the status. If you simply want to pause and replay when user scrolls back to the same cell u will have to save player somewhere outside the cell and find a way to map player instance to cell.
Finally in cellForRowAtIndexPath call,
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : ImageTableViewCell = tableView.dequeueReusableCell(withIdentifier: "imageCell") as! ImageTableViewCell
cell.configCell(with: URL(string: "https://google.com")!)
....
}
EDIT 2:
As OP has completely filped the question to play only one video at any point in time and pause other playing videos on tapping play button my earlier answer will no longer hold true.
Earlier answer allows user to play multiple video.
Here is a modified answer to make it work.
Modify your custom cell class as below.
protocol VideoPlayingCellProtocol : NSObjectProtocol {
func playVideoForCell(with indexPath : IndexPath)
}
class ImageTableViewCell: UITableViewCell {
#IBOutlet weak var carsImageView: UIImageView!
var playerController : AVPlayerViewController? = nil
var passedURL : URL! = nil
var indexPath : IndexPath! = nil
var delegate : VideoPlayingCellProtocol = nil
override func awakeFromNib() {
super.awakeFromNib()
playerController = AVPlayerViewController()
// Initialization code
}
func configCell(with url : URL,shouldPlay : Bool) {
//something like this
self.passedURL = url
if shouldPlay == true {
let player = AVPlayer(url: url)
if self.playerController == nil {
playerController = AVPlayerViewController()
}
playerController.player = player
playerController?.showsPlaybackControls = true
playerController.player.play()
}
else {
if self.playerController != nil {
self.playerController.player?.pause()
self.playerController.player = nil
}
//show video thumbnail with play button on it.
}
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func playOrPauseVideo(_ sender: UIButton) {
self.delegate.playVideoForCell(self.indexPath)
//add playerController view as subview to cell
}
override func prepareForReuse() {
//this way once user scrolls player will pause
self.playerController.player?.pause()
self.playerController.player = nil
}
}
In your TableView VC create a property called
var currentlyPlayingIndexPath : IndexPath? = nil
Make your TableView VC to confirm VideoPlayingCellProtocol
extension ViewController : VideoPlayingCellProtocol {
func playVideoForCell(with indexPath: IndexPath) {
self.currentlyPlayingIndexPath = indexPath
//reload tableView
self.tableView.reloadRows(at: self.tableView.indexPathsForVisibleRows!, with: .none)
}
}
Finally modify your cellForRowAtIndexPath as
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : ImageTableViewCell = tableView.dequeueReusableCell(withIdentifier: "imageCell") as! ImageTableViewCell
//delegate setting here which u missed
cell.delegate = self
//let the cell know its indexPath
cell.indexPath = indexPath
cell.configCell(with: URL(string: "https://google.com")!, shouldPlay: self.currentlyPlayingIndexPath == indexPath)
....
}
Thats all you need.
I had a similar requirement as you and created global AVPlayer and AVPlayerLayer variables in my UITableViewController. I then attached them to a cell when a person selected the cell. From what I've seen, my scrolling is smooth, memory usage is minimal, and it also enforces that only one video is played at a time.
First step was to create the variables in my UITableViewController:
lazy var avPlayer : AVPlayer = {
let player = AVPlayer()
return player
}()
lazy var avPlayerLayer : AVPlayerLayer = {
let layer = AVPlayerLayer(player: self.avPlayer)
return layer
}()
I then created two functions in the UITableViewController to handle linking and removing the AVPlayerLayer from a cell as such.
private func removePlayerFromCell()
{
if (self.avPlayerLayer.superlayer != nil)
{
self.avPlayerLayer.removeFromSuperlayer()
self.avPlayer.pause()
self.avPlayer.replaceCurrentItem(with: nil)
}
}
private func attachPlayerToCell(indexPath : IndexPath)
{
if let cell = tableView.cellForRow(at: indexPath)
{
self.removePlayerFromCell()
//create url
let url = ...
//create player item
let playerItem = AVPlayerItem(url: url)
self.avPlayer.replaceCurrentItem(with: playerItem)
self.avPlayerLayer.frame = cell.contentView.frame
cell.contentView.layer.addSublayer(self.avPlayerLayer)
self.avPlayer.play()
}
}
You can then call attachPlayerToCell() in didSelectRowAt().
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
attachPlayerToCell(indexPath: indexPath)
}
Lastly, to ensure videos don't play during scrolling, use the table's UIScrollViewDelegate to stop playback:
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.removePlayerFromCell()
}
Hope this helps and is clear.

Resources