I am trying to setup a UITableView that can play videos. Many of the previous SO questions on this used MPMoviePlayer(Playing Video into UITableView, Playing video in UItableView in SWIFT, Playing Video From UITableView), which is now deprecated in iOS 9. One of the few that used AVFoundation (what i'm using), is this one: Play video on UITableViewCell when it is completely visible and is where I'm getting most of my code from. Here is my code, inside cellForRowAtIndexPath:
VideoCell *videoCell = (VideoCell *)[self.tableView dequeueReusableCellWithIdentifier:@"VideoCell"];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
NSURL *url = [[NSURL alloc] initWithString:urlString];
dispatch_async(queue, ^{
videoCell.item = [AVPlayerItem playerItemWithURL:url];
dispatch_sync(dispatch_get_main_queue(), ^{
videoCell.player = [[AVPlayer alloc] initWithPlayerItem:videoCell.item];
videoCell.player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:videoCell.player];
playerLayer.frame = CGRectMake(0, 0, videoCell.contentView.frame.size.width, videoCell.contentView.frame.size.height);
[videoCell.contentView.layer addSublayer:playerLayer];
playerLayer.videoGravity = AVLayerVideoGravityResize;
[videoCell.player play];
});
});
return videoCell;
From what I understand, I need to asynchronously download the videos before I play them. I've asynchronously downloaded images before, and the "download" part of that always involves converting NSURL -> NSData -> UIImage. And then when you have the UIImage, you're ready to display it in the cell, so you bring the main queue up and dispatch_async and perform the cell.imageView.image = yourImage; on the main queue.
Here, I have an NSURL, but I don't quite get which steps here should be on the main queue and which should be off the main thread. I tried what was suggested in the aforementioned SO question, but so far it isn't working. The table cell just loads, and the video never plays. I just see the first frame. Sometimes the first 1 second of it will play, but then it buffers after that and won't. I'm working with a table view that has only 1 object right now, so there is just 1 video to play, and it still isn't playing.
What am I doing wrong, and could someone help me explain exactly which parts need to be on the main thread and which off? A responder in the thread I mentioned at the top said there were "lots of tutorials on this out there," but upon scanning google I didn't see any. the terms "asynchronous" and "iOS" together almost always get search results about image downloading, not video. But if any tutorials exist it would be nice to see one.
thanks
The following is the method that I have used to play video in the tableview cells. I'm not sure it is the best method for this, anyway it may help you :)
First I have created a custom cell. In the setter method I called a method to set AVPlayer.
- (void)setUpAVPlayer
{
@try {
self.videoPlayer = [[AVPlayer alloc]initWithPlayerItem:self.videoPlayerItem];
}
@catch (NSException *exception) {
NSLog(@"Exception : %@",exception.description);
[self.videoPlayer replaceCurrentItemWithPlayerItem:self.videoPlayerItem];
}
// Setting player properties.
playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.videoPlayer];
playerLayer.videoGravity = AVLayerVideoGravityResize;
[self.previewImageView.layer addSublayer:playerLayer];
self.previewImageView.clipsToBounds = YES;
playerLayer.hidden = YES;
[playerLayer addObserver:self forKeyPath:@"readyForDisplay" options:0 context:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(pauseAllRunningPlayers) name:@"pause" object:nil];
// AVPlayer KVO
[self.videoPlayer addObserver:self forKeyPath:@"rate" options:NSKeyValueObservingOptionNew context:kRateDidChangeKVO];
[self.videoPlayer addObserver:self forKeyPath:@"currentItem.status" options:NSKeyValueObservingOptionNew context:kStatusDidChangeKVO];
[self.videoPlayer addObserver:self forKeyPath:@"currentItem.duration" options:NSKeyValueObservingOptionNew context:kDurationDidChangeKVO];
[self.videoPlayer addObserver:self forKeyPath:@"currentItem.loadedTimeRanges" options:NSKeyValueObservingOptionNew context:kTimeRangesKVO];
[self.videoPlayer addObserver:self forKeyPath:@"currentItem.playbackBufferFull" options:NSKeyValueObservingOptionNew context:kBufferFullKVO];
[self.videoPlayer addObserver:self forKeyPath:@"currentItem.playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:kBufferEmptyKVO];
[self.videoPlayer addObserver:self forKeyPath:@"currentItem.error" options:NSKeyValueObservingOptionNew context:kDidFailKVO];
[videoLoadingIndicator stopAnimating];
}
I have found that in your code you have written theb play function like this [videoCell.player play];
You should call the play function only when the player state becomes 'AVPlayerStatusReadyToPlay'. Thats why I used KVOs.
Hope this may helps you :)
Here i have prepared a demo in which Once your cell will be visible as per your requirement player will start play a video, you can design your storyboard with the reference of below image.
//
// ViewController.swift
// RTLDemo
//
// Created by iOS Test User on 06/01/18.
// Copyright © 2018 iOS Test User. All rights reserved.
//
import UIKit
import AVKit
public struct VideoName {
static let video1 = "video1"
static let video2 = "video2"
}
public struct MediaType {
static let video = 1
static let image = 2
}
class ViewController: UIViewController {
@IBOutlet weak var collView: UICollectionView!
var timer: Timer?
var isVideoPlaying: Bool = false
// ------------------------------------------------------------------------------------------
// MARK: -
// MARK: - Memory management method
// ------------------------------------------------------------------------------------------
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// ------------------------------------------------------------------------------------------
// MARK:
// MARK: - Custom Methods
// ------------------------------------------------------------------------------------------
func initialSetup() {
if self.timer == nil {
self.timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(checkForTheVisibleVideo), userInfo: nil, repeats: true)
}
timer?.fire()
}
// ------------------------------------------------------------------------------------------
@objc func checkForTheVisibleVideo() {
if !isVideoPlaying {
let visibleCell = self.collView.indexPathsForVisibleItems
if visibleCell.count > 0 {
for indexPath in visibleCell {
if self.isVideoPlaying {
break
}
if let cell = self.collView.cellForItem(at: indexPath) as? CustomCell,cell.mediaType == MediaType.video {
if cell.player == nil{
cell.player = AVPlayer(url: URL.init(fileURLWithPath: Bundle.main.path(forResource: VideoName.video2, ofType: "mp4")!))
cell.playerLayer = AVPlayerLayer.init(player: cell.player)
cell.playerLayer?.frame = cell.imgView.frame
cell.imgView.layer.addSublayer(cell.playerLayer!)
NotificationCenter.default.addObserver(self, selector: #selector(videoDidFinishPlaying), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: cell.player?.currentItem)
cell.player?.addPeriodicTimeObserver(forInterval: CMTime.init(seconds: 1, preferredTimescale: 1), queue: .main, using: { (time) in
if cell.player?.currentItem?.status == .readyToPlay {
let timeDuration : Float64 = CMTimeGetSeconds((cell.player?.currentItem?.asset.duration)!)
cell.lblDuration.text = self.getDurationFromTime(time: timeDuration)
let currentTime : Float64 = CMTimeGetSeconds((cell.player?.currentTime())!)
cell.lblStart.text = self.getDurationFromTime(time: currentTime)
cell.slider.maximumValue = Float(timeDuration.rounded())
cell.slider.value = Float(currentTime.rounded())
}
})
}
cell.player?.play()
cell.btnPlay.setImage(#imageLiteral(resourceName: "pause_video"), for: .normal)
self.isVideoPlaying = true
}
}
}
}
}
// ------------------------------------------------------------------------------------------
@objc func videoDidFinishPlaying() {
self.isVideoPlaying = false
let visibleItems: Array = self.collView.indexPathsForVisibleItems
if visibleItems.count > 0 {
for currentCell in visibleItems {
guard let cell = self.collView.cellForItem(at: currentCell) as? CustomCell else {
return
}
if cell.player != nil {
cell.player?.seek(to: kCMTimeZero)
cell.player?.play()
}
}
}
}
// ------------------------------------------------------------------------------------------
@objc func onSliderValChanged(slider: UISlider, event: UIEvent) {
if let touchEvent = event.allTouches?.first {
guard let cell = self.collView.cellForItem(at: IndexPath.init(item: slider.tag, section: 0)) as? CustomCell else {
return
}
switch touchEvent.phase {
case .began:
cell.player?.pause()
case .moved:
cell.player?.seek(to: CMTimeMake(Int64(slider.value), 1))
case .ended:
cell.player?.seek(to: CMTimeMake(Int64(slider.value), 1))
cell.player?.play()
default:
break
}
}
}
// ------------------------------------------------------------------------------------------
func getDurationFromTime(time: Float64)-> String {
let date : Date = Date(timeIntervalSince1970: time)
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone.init(identifier: "UTC")
dateFormatter.dateFormat = time < 3600 ? "mm:ss" : "HH:mm:ss"
return dateFormatter.string(from: date)
}
// ------------------------------------------------------------------------------------------
@IBAction func btnPlayTapped(_ sender: UIButton) {
let indexPath = IndexPath.init(item: sender.tag, section: 0)
guard let cell = self.collView.cellForItem(at: indexPath) as? CustomCell else {
return
}
if isVideoPlaying {
self.isVideoPlaying = false
cell.btnPlay.setImage(#imageLiteral(resourceName: "play_video"), for: .normal)
cell.player?.pause()
}else{
if cell.player == nil {
cell.player = AVPlayer(url: URL.init(fileURLWithPath: Bundle.main.path(forResource: VideoName.video2, ofType: "mp4")!))
cell.playerLayer = AVPlayerLayer(player: cell.player!)
cell.playerLayer?.frame = CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.size.width - 50, height: (UIScreen.main.bounds.size.height - 64) * 0.3)
cell.imgView.layer.addSublayer(cell.playerLayer!)
NotificationCenter.default.addObserver(self, selector: #selector(videoDidFinishPlaying), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: cell.player?.currentItem)
}
cell.btnPlay.setImage(#imageLiteral(resourceName: "pause_video"), for: .normal)
cell.player?.play()
self.isVideoPlaying = true
}
}
// ------------------------------------------------------------------------------------------
// MARK: -
// MARK: - View life cycle methods
// ------------------------------------------------------------------------------------------
override func viewDidLoad() {
super.viewDidLoad()
self.initialSetup()
}
}
extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate,UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
// ------------------------------------------------------------------------------------------
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = self.collView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell
if indexPath.row % 2 == 0 {
cell.mediaType = MediaType.image
cell.btnPlay.isHidden = true
cell.lblDuration.isHidden = true
cell.lblStart.isHidden = true
cell.slider.isHidden = true
cell.imgView.isHidden = false
}else{
cell.mediaType = MediaType.video
cell.btnPlay.isHidden = false
cell.lblDuration.isHidden = false
cell.lblStart.isHidden = false
cell.slider.isHidden = false
cell.imgView.isHidden = false
}
cell.btnPlay.tag = indexPath.row
cell.slider.tag = indexPath.row
cell.btnPlay.addTarget(self, action: #selector(btnPlayTapped(_:)), for: .touchUpInside)
cell.slider.addTarget(self, action: #selector(self.onSliderValChanged(slider:event:)), for: .valueChanged)
return cell
}
// ------------------------------------------------------------------------------------------
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize.init(width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height / 3)
}
// ------------------------------------------------------------------------------------------
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard let cellToHide = cell as? CustomCell else {
return
}
if cellToHide.player != nil {
cellToHide.player?.pause()
cellToHide.playerLayer?.removeFromSuperlayer()
cellToHide.player = nil
cellToHide.btnPlay.setImage(#imageLiteral(resourceName: "play_video"), for: .normal)
self.isVideoPlaying = false
}
}
}
// Custom Cell Class
class CustomCell: UICollectionViewCell {
@IBOutlet weak var btnPlay: UIButton!
@IBOutlet weak var lblDuration: UILabel!
@IBOutlet weak var imgView: UIImageView!
@IBOutlet weak var viewBack: UIView!
@IBOutlet weak var lblStart: UILabel!
@IBOutlet weak var slider: UISlider!
var player: AVPlayer?
var playerLayer: AVPlayerLayer?
var mediaType: Int!
}
Storyboard reference image in which i created simple custom video player with cell of collection view:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With