Get the video duration once the youtube video is loaded - ios

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
}

Related

Unable to display thumbnails in UICollectionView

I am trying to recreate this thing. I've created in Storyboard skeleton. Here's the idea of my code:
Fetch images from URL's array with help of the function getThumbnailFromImage
Add UIImage's with my thumbnails in array webImages
Add in ViewController reusable cell MyCollectionView
...
But here I am with this))) (Don't mind absence of Auto Layout). What am I doing wrong? I think that the problem is with reloadData() but I don't know where to put it.
ViewController:
//
// ViewController.swift
// youtube-clone
//
// Created by мас on 16.08.2022.
//
import Foundation
import UIKit
import YouTubePlayer
import AVFoundation
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var url: [URL?] = [
URL(string: "https://www.youtube.com/watch?v=KhebpuFBD14"),
URL(string: "https://www.youtube.com/watch?v=UfNdNrRHpUw"),
URL(string: "https://www.youtube.com/watch?v=CX-BdDHW0Ho"),
URL(string: "https://www.youtube.com/watch?v=NIOMtSzfpck")
]
var webImages: [UIImage] = []
var currentPage: Int = 0
#IBOutlet var myPage: UIPageControl!
#IBOutlet weak var buttonInfo: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
myPage.currentPage = 0
myPage.numberOfPages = webImages.count
}
// MARK: - Collection View Setup
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return webImages.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCollectionCell
getThumbnailFromImage(url: url[indexPath.row]!, completion: { image in
self.webImages.append(image!)
})
cell.myWebImage.image = webImages[indexPath.row]
cell.myWebImage.layer.cornerRadius = 20
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
myPage.currentPage = indexPath.row
}
// MARK: - Layout Setup // IGNORE IT
func setupLayout() {
buttonInfo.layer.cornerRadius = 25
buttonInfo.imageView!.transform = CGAffineTransform(rotationAngle: 180 * .pi / 180)
self.navigationController?.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
}
// MARK: - Videos Thumbnail Fetcher
func getThumbnailFromImage(url: URL, completion: #escaping ((_ image: UIImage?) -> Void)) {
DispatchQueue.global().async {
let asset = AVAsset(url: url)
let avAssetImageGenerator = AVAssetImageGenerator(asset: asset)
avAssetImageGenerator.appliesPreferredTrackTransform = true
let thumbnailTime = CMTimeMake(value: 7, timescale: 1)
do {
let cgThumbImage = try avAssetImageGenerator.copyCGImage(at: thumbnailTime, actualTime: nil)
let thumbImage = UIImage(cgImage: cgThumbImage)
DispatchQueue.main.async {
completion(thumbImage)
}
}
catch {
print(error.localizedDescription)
}
}
}
}
Reusable Cell AKA MyCollectionCell:
import UIKit
class MyCollectionCell: UICollectionViewCell {
#IBOutlet var myWebImage: UIImageView!
}
P.s.: YouTubePlayer is custom pod from GitHub, it's not currently used.
You do NOT have to use AVAssetImageGenerator, Simply you can use Youtube API to fetch the thumbnail images as .jpg image by video id,
and each YouTube video has four generated images.
https://img.youtube.com/vi/{id}/0.jpg
https://img.youtube.com/vi/{id}/1.jpg
https://img.youtube.com/vi/{id}/2.jpg
https://img.youtube.com/vi/{id}/3.jpg
Example
https://img.youtube.com/vi/KhebpuFBD14/0.jpg
And then it is preferred to use a third party to load this image as its displayed in a list, like https://github.com/SDWebImage/SDWebImage or https://github.com/onevcat/Kingfisher and you will NOT be worry about Concurrency or caching.
A couple of thoughts:
#matt is right in the comment - getThumbnailFromImage will likely not have called the completion block by the time cellForItemAt returns.
From what is visible in the code you posted, webImages.count will still be 0 when your collection view checks numberOfItemsInSection. If the number of items is 0, cellForItemAt may never get called so the call to getThumbnailFromImage wouldn't even be reached. (I'm not sure if the white box in your screenshot is part of a cell or another view element. If a cell is being displayed, I'm assuming you're populating webImages somewhere else before the collection view gets laid out).
One way you could work around these issues is by giving each cell a URL rather than a thumbnail. That way the cell can be displayed while the image is still loading. The cell could look something like this:
class MyCollectionCell: UICollectionViewCell {
#IBOutlet var myWebImage: UIImageView!
func configure(urlString: String) {
guard let self = self, let url = URL(string: urlString) else {
return
}
getThumbnailFromImage(url: url, completion: { [weak self] image in
self?.myWebImage.image = image
})
}
// Move `getThumbnailForImage` function to here, or give the cell a delegate to call back to the VC with if you don't want any networking in the view itself
}
The cellForItemAt function in the VC would need to be changed to something like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCollectionCell
cell.configure(urlString: url[indexPath.row])
cell.myWebImage.layer.cornerRadius = 20 // This should probably live in the cell since the parent doesn't actually need to know about it!
return cell
}
An added benefit of this approach is that you're not referencing a separate array of images that could theoretically end up being in the wrong order if there's a mistake somewhere in the code. You could get rid of the webImages array entirely and use urls.count in numberOfItemsInSection instead - or eventually the number of elements returned from an API somewhere.
Side note - make sure you add [weak self] at the beginning of any closure that references self to avoid trying to access it after it's been deallocated! Currently the call to getThumbnailFromImage doesn't have that :)
Also, note that I changed to a guard statement for checking that the URL exists. This is much safer than force unwrapping a URL(string:) value, especially if you ever end up getting the strings from a dynamic source.

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.

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.

Swift 2.0 MPMusicPlayerController will not always play song on initial click

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.

Given indexPath for UITableViewCell, load that specific cell

If I know which cell I need to load (from an indexPath), how do I perform an action for only that cell?
I have a class for my UITableViewCell where I set up a few things, most importantly I position an MPMoviePlayer with an empty URL.
class TableViewCell: UITableViewCell {
#IBOutlet weak var titleLabel:UILabel!
#IBOutlet weak var movieView:UIView! //Set up in storyboard
var moviePlayer:MPMoviePlayerController!
var videoURL:NSURL!
override func awakeFromNib() {
super.awakeFromNib()
//initialize movie player
moviePlayer = MPMoviePlayerController(contentURL: videoURL)
}
override func layoutSubviews() {
//layout movieplayer
moviePlayer.view.frame = movieView.bounds
moviePlayer.view.center = CGPointMake(CGRectGetMidX(movieView.bounds), CGRectGetMidY(movieView.bounds))
movieView.addSubview(moviePlayer.view)
}
//Action to load video
func displayVideo() {
println("Should display Video at specified indexPath")
moviePlayer = MPMoviePlayerController(contentURL: videoURL)
moviePlayer.movieSourceType = MPMovieSourceType.File
moviePlayer.repeatMode = MPMovieRepeatMode.One
moviePlayer.controlStyle = MPMovieControlStyle.None
moviePlayer.prepareToPlay()
moviePlayer.play()
}
}
displayVideo is the vital function here. It needs to load ONLY when the tableViewCell is taking up a majority of the view. Therefore, I can't call it in cellForRowAtIndexPath.
All I do in cellForRowAtIndexPath is load a label into each cell and set a height variable for adjusting the heightForRowAtIndexPath:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let myCell:TableViewCell = tableView.dequeueReusableCellWithIdentifier("tableViewCell", forIndexPath: indexPath) as TableViewCell
//Get height of movieView so we can adjust height of row
if didSetRowHeight == false {
movieViewHeight = myCell.movieView.frame.height
didSetRowHeight = true
}
//Set label in each cell to the right college
myCell.titleLabel.text = titleLabels[indexPath.row]
//This does NOT WORK; loads movies that are not taking majority of view
//myCell.videoURL = NSURL(string: videoFiles[indexPath.row].url)
return myCell
}
Next, I determine the indexPath for the cell that is in the majority of the view when scrolling stops. This value is held in indexPathToLoad
override func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
//Array to hold distance of visible cells to top of screen
var distancesToTop = [CGFloat]()
//Clean out array from previous scroll
distancesToTop.removeAll(keepCapacity: true)
//Array of visible cell indexPaths
var indexPaths = tableView.indexPathsForVisibleRows()!
for visibleCell in tableView.visibleCells() { //for each visible cell...
//Append the distance to top of screen
distancesToTop.append(abs((visibleCell.frame.minY - tableView.contentOffset.y) - 64))
}
//Find the lowest distance to top
let numMin = distancesToTop.reduce(CGFloat.max, { min($0, $1) })
//Determine the objectForIndexPath that the minimum number was in
let num = find(distancesToTop, numMin)!
//Use that to determine the indexPathToLoad from the array of indexPaths
indexPathToLoad = indexPaths[num]
//This successfully prints the indexPath that I need to load a movie
println("indexPath to load: \(indexPathToLoad.row)")
//Here's where it gets funky:
//Attempt to access cell from this function so we can load the video at the proper indexPath
var cell:TableViewCell = tableView.dequeueReusableCellWithIdentifier("tableViewCell", forIndexPath: indexPathToLoad as NSIndexPath) as TableViewCell
//Load the proper video...
cell.videoURL = NSURL(string: videoFiles[indexPathToLoad.row].url)
cell.displayVideo()
}
So I know precisely which tableViewCell that displayVideo() needs to be applied to, but it seems to choose a totally random indexPath, rather than the one specified in indexPathToLoad.
Any help is GREATLY APPRECIATED. I have been struggling with this for days.
The line
var cell:TableViewCell = tableView.dequeueReusableCellWithIdentifier("tableViewCell", forIndexPath: indexPathToLoad as NSIndexPath) as TableViewCell
should look something like
if let cell = tableView.cellForRowAtIndexPath(indexPathToLoad) as? TableViewCell {
cell.displayVideo()
}
There is a UITableViewDelegate protocol method
optional func tableView(_ tableView: UITableView,
willDisplayCell cell: UITableViewCell,
forRowAtIndexPath indexPath: NSIndexPath)
Implement this method and inside it check if the indexPath is the one that you want and if it is do the work that needs to be done.

Resources