9

What's the best way for embedding a video into a UITableViewCell? I'm trying to build something sorta like Vine/Instagram.

I'm able to handle asynch image loading really well with SD_WebImage..but unfortunately they don't support video. I also tried embedding with an MPMoviePlayer but it just appears as a black screen. This is what I tried:

override func viewDidLoad() {
    super.viewDidLoad()

    tableView.frame         =   CGRectMake(0, 0, view.bounds.width, view.bounds.height);
    tableView.delegate      =   self
    tableView.dataSource    =   self

    tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")

    self.view.addSubview(tableView)
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    var cell:UITableViewCell = tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
    var moviePlayer : MPMoviePlayerController?

    let url = NSURL (string: "http://jplayer.org/video/m4v/Big_Buck_Bunny_Trailer.m4v")
    moviePlayer = MPMoviePlayerController(contentURL: url)
    if let player = moviePlayer {
        player.view.frame = CGRectMake(0, 100, view.bounds.size.width, 180)
        player.prepareToPlay()
        player.controlStyle = .None
        player.repeatMode = .One
        player.scalingMode = .AspectFit
        cell.addSubview(player.view)
    }

    return cell

}
Chris Jones
  • 856
  • 1
  • 11
  • 24

2 Answers2

24

I have tested a demo for videos only,

Here is how you can achieve it- Create a custom Cell class to hold the video player view, and the handle the play and pause methods for the avplayer here itself.

This is my Custom Cell class -

import UIKit
import AVFoundation

class VideoCellTableViewCell: UITableViewCell {

    // I have put the avplayer layer on this view
    @IBOutlet weak var videoPlayerSuperView: UIView!
    var avPlayer: AVPlayer?
    var avPlayerLayer: AVPlayerLayer?
    var paused: Bool = false

    //This will be called everytime a new value is set on the videoplayer item
    var videoPlayerItem: AVPlayerItem? = nil {
        didSet {
            /*
             If needed, configure player item here before associating it with a player.
             (example: adding outputs, setting text style rules, selecting media options)
             */
            avPlayer?.replaceCurrentItem(with: self.videoPlayerItem)
        }
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        //Setup you avplayer while the cell is created
        self.setupMoviePlayer()
    }

    func setupMoviePlayer(){
        self.avPlayer = AVPlayer.init(playerItem: self.videoPlayerItem)
        avPlayerLayer = AVPlayerLayer(player: avPlayer)
        avPlayerLayer?.videoGravity = AVLayerVideoGravityResizeAspect
        avPlayer?.volume = 3
        avPlayer?.actionAtItemEnd = .none

        //        You need to have different variations
        //        according to the device so as the avplayer fits well
        if UIScreen.main.bounds.width == 375 {
            let widthRequired = self.frame.size.width - 20
            avPlayerLayer?.frame = CGRect.init(x: 0, y: 0, width: widthRequired, height: widthRequired/1.78)
        }else if UIScreen.main.bounds.width == 320 {
            avPlayerLayer?.frame = CGRect.init(x: 0, y: 0, width: (self.frame.size.height - 120) * 1.78, height: self.frame.size.height - 120)
        }else{
            let widthRequired = self.frame.size.width
            avPlayerLayer?.frame = CGRect.init(x: 0, y: 0, width: widthRequired, height: widthRequired/1.78)
        }
        self.backgroundColor = .clear
        self.videoPlayerSuperView.layer.insertSublayer(avPlayerLayer!, at: 0)

        // This notification is fired when the video ends, you can handle it in the method.
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(self.playerItemDidReachEnd(notification:)),
                                               name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
                                               object: avPlayer?.currentItem)
    }

    func stopPlayback(){
        self.avPlayer?.pause()
    }

    func startPlayback(){
        self.avPlayer?.play()
    }

    // A notification is fired and seeker is sent to the beginning to loop the video again
    func playerItemDidReachEnd(notification: Notification) {
        let p: AVPlayerItem = notification.object as! AVPlayerItem
        p.seek(to: kCMTimeZero)
    }

}

Then comes your controller - Dont forget to import the AVFoundation Framework

import UIKit
import AVFoundation

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

 // The current VisibleIndexPath, 
 //it can be an array, but for now,
 //i am targetting one cell only
 //var visibleIP : IndexPath? 

    var aboutToBecomeInvisibleCell = -1
    var avPlayerLayer: AVPlayerLayer!
    var videoURLs = Array<URL>()
    var firstLoad = true

    @IBOutlet weak var feedTableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        feedTableView.delegate = self
        feedTableView.dataSource = self
   //Your model to hold the videos in the video URL
        for i in 0..<2{
            let url = Bundle.main.url(forResource:"\(i+1)", withExtension: "mp4")
            videoURLs.append(url!)
        }
    // initialized to first indexpath
        visibleIP = IndexPath.init(row: 0, section: 0)
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 5
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 290
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 0
    }

Then provide your URL in the cellForRow delegate

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  //Thats it, just provide the URL from here, it will change with didSet Method in your custom cell class
        let cell = self.feedTableView.dequeueReusableCell(withIdentifier: "videoCell") as! VideoCellTableViewCell
        cell.videoPlayerItem = AVPlayerItem.init(url: videoURLs[indexPath.row % 2])
        return cell
    }

All the part for visible cells is managed here, I have used the calculation of the intersection all the visible cells here,

Find the visible IndexPath, use that to fetch a cell of the custom tablecell type. This can also be achieved with visibleCells but, i have avoided that, as you can have multiple type of cells having image, text or other stuff.

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let indexPaths = self.feedTableView.indexPathsForVisibleRows
        var cells = [Any]()
        for ip in indexPaths!{
            if let videoCell = self.feedTableView.cellForRow(at: ip) as? VideoCellTableViewCell{
                cells.append(videoCell)
            }
        }
        let cellCount = cells.count
        if cellCount == 0 {return}
        if cellCount == 1{
            if visibleIP != indexPaths?[0]{
                visibleIP = indexPaths?[0]
            }
            if let videoCell = cells.last! as? VideoCellTableViewCell{
                self.playVideoOnTheCell(cell: videoCell, indexPath: (indexPaths?.last)!)
            }
        }
        if cellCount >= 2 {
            for i in 0..<cellCount{
                let cellRect = self.feedTableView.rectForRow(at: (indexPaths?[i])!)
                let intersect = cellRect.intersection(self.feedTableView.bounds)
//                curerntHeight is the height of the cell that
//                is visible
                let currentHeight = intersect.height
                print("\n \(currentHeight)")
                let cellHeight = (cells[i] as AnyObject).frame.size.height
//                0.95 here denotes how much you want the cell to display
//                for it to mark itself as visible,
//                .95 denotes 95 percent,
//                you can change the values accordingly
                if currentHeight > (cellHeight * 0.95){
                    if visibleIP != indexPaths?[i]{
                        visibleIP = indexPaths?[i]
                        print ("visible = \(indexPaths?[i])")
                        if let videoCell = cells[i] as? VideoCellTableViewCell{
                            self.playVideoOnTheCell(cell: videoCell, indexPath: (indexPaths?[i])!)
                        }
                    }
                }
                else{
                    if aboutToBecomeInvisibleCell != indexPaths?[i].row{
                        aboutToBecomeInvisibleCell = (indexPaths?[i].row)!
                        if let videoCell = cells[i] as? VideoCellTableViewCell{
                            self.stopPlayBack(cell: videoCell, indexPath: (indexPaths?[i])!)
                        }

                    }
                }
            }
        }
    }

Use these methods to handle the playback.

    func playVideoOnTheCell(cell : VideoCellTableViewCell, indexPath : IndexPath){
        cell.startPlayback()
    }

    func stopPlayBack(cell : VideoCellTableViewCell, indexPath : IndexPath){
        cell.stopPlayback()
    }

    func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        print("end = \(indexPath)")
        if let videoCell = cell as? VideoCellTableViewCell {
            videoCell.stopPlayback()
        }
    }
}

If interested, you can check the demo here

Saurabh Yadav
  • 957
  • 10
  • 20
  • 6
    I LOVE YOU MAN !! – Farid Al Haddad Jun 05 '17 at 17:08
  • One more thing, i have 3 videos that shows at the same time .. sometimes they just dont start. – Farid Al Haddad Jun 05 '17 at 17:10
  • And do you know how can i cache the videos ? – Farid Al Haddad Jun 05 '17 at 17:11
  • Sorry Farid, Haven't tried caching on videos, Will let you know, if i try this. Thanks for the love :) – Saurabh Yadav Jun 06 '17 at 20:11
  • i have used same code in Swift 2.3 but it crashes on setupMoviePlayer method in custom cell with unexpectedly found nil while unwrapping an Optional value. so what's the reason behind that? – Vivek Goswami Aug 10 '17 at 15:24
  • @VivekGoswami Hi , can you tell what is found as nil? – Saurabh Yadav Aug 10 '17 at 17:39
  • func setupMoviePlayer(){ self.avPlayer = AVPlayer.init(playerItem: self.videoPlayerItem)} here self.videoPlayerItem is found nil – Vivek Goswami Aug 10 '17 at 17:46
  • check this line in cellForRow Method, your URL may be wrong. cell.videoPlayerItem = AVPlayerItem.init(url: videoURLs[indexPath.row % 2]) – Saurabh Yadav Aug 10 '17 at 17:53
  • What does you videoURLs array contain? You need to change the logic here, try adding a static url and run – Saurabh Yadav Aug 10 '17 at 17:54
  • yes i have tried with static url, but it crashes on setupMoviePlayer – Vivek Goswami Aug 10 '17 at 18:26
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/151640/discussion-between-saurabh-yadav-and-vivek-goswami). – Saurabh Yadav Aug 10 '17 at 18:34
  • Worked for me, but i need to resume video where user left last video while scroll. any idea? – Singh_Nindi Nov 18 '17 at 18:35
  • @Singh_Nindi You can have a dictionary and store the time of the video when it is changed, and the other one is played, every time you play other video, check for this dictionary for the key, and if it is present, you can use the seek functionality to land it to that place. – Saurabh Yadav Nov 19 '17 at 14:21
  • @SaurabhYadav hmm i was thinking similar, but i also need seekbar with play pause button, is it also possible? – Singh_Nindi Nov 20 '17 at 05:15
  • @Singh_Nindi,not in default way, you will have to create custom controls for that. – Saurabh Yadav Nov 20 '17 at 11:42
  • I am getting a crash "An AVPlayerItem cannot be associated with more than one instance of AVPlayer" while scrolling to top with code. – Gurjit Singh Apr 02 '18 at 07:18
  • @GurjitSingh is that for the github demo? – Saurabh Yadav Apr 02 '18 at 07:24
  • yes, I use the same code in my project and load videos from network url, slow scrolling working fine, but I did add a button which scroll to top of the table view fastally, then this problem arise . I think these should be some deallocations so that cell not reuse the previous cell player item. – Gurjit Singh Apr 02 '18 at 07:47
  • @GurjitSingh, Thanks for checking that out, i will check and get back by the evening. – Saurabh Yadav Apr 02 '18 at 10:44
  • Can you also take a look at how can I play video from last pause position without reloading again from url while deque the cell. My idea is to save PlayerItem to your view-model but didn't implement this. I am sure you can accept this calling. Thanks. – Gurjit Singh Apr 02 '18 at 11:16
  • @GurjitSingh, you will have to provide me some code from your end for that. Sorry but i cannot create that from scratch. Can have a look in your code though. – Saurabh Yadav Apr 02 '18 at 11:49
  • Great solution, thanks! Would just like to know how to add activity indicator in case if connection is slow. – Neeraj Joshi May 08 '18 at 07:23
  • @SaurabhYadav I am using your code in two of my app. Now the problem is video automatically plays even if the cell is not visible. In my case I have video and image both in same cell. Any idea why video is playing while it is not displaying? – Neeraj Joshi Jun 29 '19 at 11:14
  • @Nij, can you share some demo, i will take a look in that case. You can mention this as an issue on the github link mentioned in the end. – Saurabh Yadav Jul 01 '19 at 13:55
  • @SaurabhYadavI cant share a demo. I used the code same as yours. If you scroll slowly it won't play when cell is invisible but if you scroll fast, it will. – Neeraj Joshi Jul 02 '19 at 06:38
  • @Nij, Thanks for mentioning, I will check this and update you. – Saurabh Yadav Jul 03 '19 at 09:36
  • @SaurabhYadav Have you found anything ? I got one case i.e. tableview is inside scrollview at that time this issue is there. Otherwise working fine. – Neeraj Joshi Jul 15 '19 at 09:40
  • @Nij, That is why i was not able to reproduce it, Can you please give me a sample project to work on, i can check for that case. – Saurabh Yadav Jul 15 '19 at 18:12
  • @SaurabhYadav I can give you hierarchy. `| UIScrollView | UIView - For Profile data | UITableView - For posts which contains videos and images` Here currently UITableView is not visible but above that UIView is only visible which contains UILabels and UIButtons. The idea of taking UITableView inside UIScrollView is to make whole screen scrollable. Now you might understand the problem and will able to reproduce. I can't provide the sample hope you understand. :) – Neeraj Joshi Jul 16 '19 at 05:47
  • @SaurabhYadav have you found anything bro? – Neeraj Joshi Jul 19 '19 at 13:06
  • @Nij, sorry, have not got time yet to recreate the project as per your needs, will get back if its done. – Saurabh Yadav Jul 22 '19 at 18:09
  • instead of scrollViewDidScroll(_ scrollView: UIScrollView) we can directly use `func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { if let videoCell = cell as? CommunityFeedTableViewCell { print("playing") videoCell.startPlayback() } }`. It works for me @SaurabhYadav – Khushbu Judal Mar 13 '20 at 10:42
  • @KhushbuJudal go for it then, the idea was to check which was visible and in center. Its ok to have different implementation based on your needs. – Saurabh Yadav Mar 22 '20 at 15:20
  • 1
    Had to make some adjustments to the code because I'm using a collectionView but this definitely works – Lance Samaria Jun 07 '20 at 09:40
  • @Nij I am having the same problem ould you find anything? – Utku Dalmaz Dec 16 '21 at 14:26
8

MPMoviePlayerController was deprecated in iOS 9, you should use instead AVPlayer like in the following way:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)

    let videoURL = NSURL(string: "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")
    let player = AVPlayer(URL: videoURL!)

    let playerLayer = AVPlayerLayer(player: player)
    playerLayer.frame = cell.bounds

    cell.layer.addSublayer(playerLayer)
    player.play()

    return cell
}

You need to include the two frameworks :

import AVKit
import AVFoundation

To get the visibles cell/rows in the UITableView you can use the two read-only properties:

I hope this help you.

Victor Sigler
  • 23,243
  • 14
  • 88
  • 105
  • this works, but it's really glitchy when I try doing more than one cell...and especially when I try scrolling. Any ideas on how to fix that? – Chris Jones Nov 14 '15 at 03:00
  • You need to handle in the `cellForRowAtIndexPath` what you need to show. What exactly do you need to happen? – Victor Sigler Nov 15 '15 at 18:03
  • I need something like vine where once the user scrolls to a cell, the video starts playing. I was thinking that originally I could just show an initial frame of the video (as an image), and then when the cell becomes visible, start playing the video. I'm having trouble figuring out how to do that though. Is there any tableView function that lets me check for visibleRow? – Chris Jones Nov 15 '15 at 20:02
  • Yes indeed there are two read-only properties you can use, please read updated answer – Victor Sigler Nov 15 '15 at 20:08
  • Ok so I'm able to detect which cell is visible, but then how do I update the cell? Should I use a scroll view delegate and detect when scrolling has stopped? – Chris Jones Nov 15 '15 at 20:29
  • Keep in mind that every time a cell is enter in the range of visible the method `cellForRowAtIndexPath` it's called and the cell is populated, remenber that the cells are reusable. – Victor Sigler Nov 15 '15 at 21:05