Reuse AVPlayer's in TableView - ios

Can anyone help in creating a pool of AVPlayer's to be reused in a tableView? So the idea is to have a pool of AVPlayer's, let's say 3, and continue to reuse them as the user scrolls through the table.
Here's a quick synopsis of the app and what we want to build:
Each video is an mp4 and is only 5 seconds long. So each cell will play a 5 second video and loop it.
All videos are local, they will be downloaded to disk before the table is even shown to the user. This will help in terms of smooth scroll performance of the tableView.
Right now I am creating too many AVPlayer's and not reusing them which is having a bad effect on performance such as scrolling is a bit choppy. Also, Apple does not allow an infinite amount of AVPlayer's to be created.
Any ideas? Thanks
Update 1:
import UIKit
import VIMVideoPlayer
var cache = NSCache<NSString, VIMVideoPlayerView>()
class FeedTableViewCell: UITableViewCell {
// MARK: - Properties
#IBOutlet weak var containerView: UIView!
static let reuseIdentifier = "FeedTableViewCell"
var video: Video? {
didSet {
if let cachedVideoPlayerView = cache.object(forKey: video!.preview!.remoteURL as NSString) {
// We have a cached video player view!
containerView.addSubview(cachedVideoPlayerView)
} else {
// There is nothing cached.
let previewURL = FileManager.applicationDocumentsDirectory.appendingPathComponent(video!.preview!.fileName!)
let newVideoPlayer = VIMVideoPlayer()
newVideoPlayer.setURL(previewURL)
newVideoPlayer.isLooping = true
newVideoPlayer.isMuted = true
newVideoPlayer.disableAirplay()
let newVideoPlayerView = VIMVideoPlayerView()
newVideoPlayerView.frame = contentView.bounds
newVideoPlayerView.delegate = self
newVideoPlayerView.setVideoFillMode(AVLayerVideoGravityResizeAspectFill)
newVideoPlayerView.player = newVideoPlayer
containerView.addSubview(newVideoPlayerView)
cache.setObject(newVideoPlayerView, forKey: video!.preview!.remoteURL as NSString)
}
}
}
// MARK: - Life Cycle
override func awakeFromNib() {
super.awakeFromNib()
print("AWAKE FROM NIB CELL")
}
override func prepareForReuse() {
super.prepareForReuse()
}
}
// MARK: - VIMVideoPlayerViewDelegate
extension FeedTableViewCell: VIMVideoPlayerViewDelegate {
func videoPlayerViewIsReady(toPlayVideo videoPlayerView: VIMVideoPlayerView!) {
videoPlayerView.player.play()
}
}

Extending your idea of using a pool, instead of reinventing the wheel, why not leverage UITableViewCell's current implementation of reusability? At any given time, n 'reusable' UITableViewCells exist, acting as your pool.
If each one of these cells contains a single AVPlayer subview, then the management is done for you by the table view. Therefore in the UITableViewCell's reuse identifier, stop the player (if needed), update its MP4 (from an in-memory cache ideally), and start it again.
If needed, you can cache the position of the video when the cell disappears in order to make it seem like the video never stopped playing while scrolling was in progress.
-- As a side note, theoretically this will work, but has not been tested with a live app. The obvious caveat is the size of the video being loaded into the AVPlayer on the main thread, while attempting to maintain 60fps.
Edit:
Please see https://stackoverflow.com/a/35514126/556479 for more info.

Related

AVPlayer cause TableView To Lag

I have to play video in my App inside tableView cell. According to view Screen size my Table Can show 2 cell in small screen device and 3 cell in large screen device like iPhone XSMax or Xr
Flow
AVPlayerController Class Created
class CustomAVPlayerViewController: AVPlayerViewController {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("touchesBegan")
}
}
Instances Created in cell class
var myImgArray=[String]()
var myVideoArray=[String]()
var myVideoPlayer : AVPlayer?
var videoPlayerController : CustomAVPlayerViewController?
var myPlayerItems=[AVPlayerItem]()
Prepare for Reuse Functions
override func prepareForReuse() {
myVideoPlayer?.replaceCurrentItem(with: nil)
videoPlayerController=nil
myVideoPlayer=nil
self.count=0
myPlayerItems.removeAll()
myImgArray.removeAll()
myVideoArray.removeAll()
NotificationCenter.default.removeObserver(NSNotification.Name.AVPlayerItemDidPlayToEndTime)
for vieww in myScrollView.subviews {
vieww.removeFromSuperview()
}
}
I am using AVPLayer instance one for a cell. A single cell can contain multiple videos so used an scrollview for that
if !videoArray.isEmpty {
/// We've Videos
for i in 0..<videoArray.count {
///Get Origin
let xOrigin : CGFloat = CGFloat(i) * myScrollView.frame.size.width + (CGFloat(imgArray.count)*myScrollView.frame.size.width)
let newPlayerItem = AVPlayerItem(url: URL(string: videoArray[i])!)
myPlayerItems.append(newPlayerItem)
if i == 0 {
self.myVideoPlayer=AVPlayer(playerItem: newPlayerItem)
self.videoPlayerController = CustomAVPlayerViewController()
NotificationCenter.default.removeObserver(NSNotification.Name.AVPlayerItemDidPlayToEndTime)
NotificationCenter.default.addObserver(self, selector: #selector(self.playerDidFinishPlaying(note:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.myVideoPlayer!.currentItem)
self.videoPlayerController?.player = self.myVideoPlayer!
self.videoPlayerController?.view.frame = CGRect(x: xOrigin+10, y: 10, width: self.myScrollView.frame.size.width-20, height: self.myScrollView.frame.size.height-20)
self.myVideoPlayer?.play()
myScrollView.addSubview(self.videoPlayerController!.view)
myScrollView.bringSubviewToFront(self.videoPlayerController!.view)
}
}
}
As shown In above code I am looping through Video Array and adding its subview in scrollView
Issue -
it start to lag when there are multiple cell on screen which have video
or Next cell to be loaded which have video
I am using prepareForReuse to avoid High Memory Usage.
App cycles unto 40 MB in run of 4 Min which include other Features too
What I can do ?
Tried Things
Checking IndexPathForVisibleCell it include three cell which are displayed as soon as my cell goes off screen I remove scrollView subviews and AVPLayer instance.
Using Global AVPLayer Instance but didn't worked. It doesn't play video
Reason - When three cell are on screen it won't be dequeued again so Video won't be loaded as global instance will be added in one only and prepareForReuse cause no video to play
It may lag because you add and remove the AVPlayer view.
Instead of removing them, you can reuse them so you can update the link when the cell is visible on screen.
You can refer any article like this: Best way to check if UITableViewCell is completely visible

What is the simplest way to insert resized images from Core Data into a UICollectionView?

When I set images from a local array into each collection view cell, scrolling is laggy. This is because the full scaled UIImage is being used when the cell will be displayed.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoCell", for: indexPath) as! PhotoCell
let image = images[indexPath.row]
cell.imageView.image = image
return cell
}
In order to try and solve this, I wrote a method that resizes images to their proper size at runtime.
private func resizeImages(images: [UIImage], size: CGSize) -> [UIImage?] {//...}
But resizing all the images in the array in viewDidLoad took a considerable amount of time.
This is a simple photo album app so I would prefer avoiding any use of activity or loading indicators. Given access to image data fetched from Core Data, how can I set the images in a collection view such that it won't lag while scrolling?
Instead of resizing images on runtime you should do this task at time of saving in CoreData. Instead of saving one image, you should store two images. One with full scale (high resolution) and other thumbnail (resized). Load only thumbnails in your CollectionViewCell to avoid scroll lagging.
I have used a LazyImageView (originally taken from cocoanetics.com) for networking which probably work without many changes in your case:
import UIKit
class LazyImageView: UIImageView {
var loading = false
weak var rawImage: UIImage?
deinit {
image = nil
loading = false
}
override func didMoveToSuperview() {
if !loading && image == nil{
loading = true
DispatchQueue.global().async {
self.resizeImage()
}
}
}
func resizeImage() {
let resizedImage: UIImage? = nil
// do the resizing
DispatchQueue.main.async {
self.image = resizedImage
self.loading = false
}
}
}
As you have asked for simple I would consider it done.
If you want to spend some more time, you could/should think about caching (i.e. to the file system's cache folder).
Depending on the app, maybe you have another nice UX way to give the user a hint what will happen (i.e. setting correct(?) placeholder color as a background for the image before loading).
Considering #Bilal answer to save a correct thumb version sound also well. It really depends on your app, i.e. if the user has a choice to resize the overall image size in the collection view (now or as a future feature).

How CATiledLayer works internally

I'm a newbie to the iOS development and I'm trying to figure out how CATiledLayer works. NOT how to use it!
There are two key features of CATiledLayer I want to know how it works.
Visible bounds automatic reporting. How does CATiledLayer know its visible bounds have changed? Can I create a custom CALayer and register to know the same information?
Tile drawing. How are these tiles drawn? Are they drawn as sublayers?
The reason I want to find out how these two key points work is that I'm building a custom UIView that can have a very large width and a CALayer will crash due to having such a large backed layer. CATiledLayer has a light memory usage and its' almost perfect!
The reason I don't want to use CATiledLayer is how it works internally. All drawing happens on a background thread. I know you can even have the drawing logic be called on the main thread but this would happen on the next drawing cycle. Because drawing doesn't happen on the same draw cycle, during a view resize our drawing logic has a delay updating the drawings thus causing the UIView content to shake during updates by the user.
Just to add a little more to this. I'm building an audio editor where it shows the audio waveform and the user can resize this clip. The clip is shown inside a collection view. The mentioned issue above with CATiledLayer seems to be the same issue Garage band has. I can barely notice it when resizing an audio clip to the left. They are likely using CATiledLayer.
I know one way to do question number one but I am not sure of the consequences and efficiency. This is more theoretical except it works. I am using a CADisplayLink to run a check to see if the frame of the layer is in the main window. I did notice a small bit of CPU (1% or less) being used so I would test it more compared to the CATiledLayer. CATiledLayer just breaks the drawing up but operates on the same premise that only what is visible can be drawn. drawRect I think fundamentally works when visible or the visible bounds change. As far as subclass I tested I used it inside a UICollectionView and know that it works. I could even get logs of when a cell was created and not on screen. Here is the working subclass of CALayer. I don't know if this helps you but it is possible.
import UIKit
protocol OnScreenLayerDelegate:class {
func layerIsOnScreen(currentScreenSpace:CGRect)
func layerIsNotOnScreen(currentScreenSpace:CGRect)
}
class OnScreenLayer: CALayer {
var displayLink : CADisplayLink?
weak var onScreenDelegate : OnScreenLayerDelegate?
override init() {
super.init()
commonSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonSetup()
}
func commonSetup(){
displayLink = CADisplayLink(target: self, selector: #selector(checkOnScreen))
displayLink?.add(to: .main, forMode: .commonModes)
}
#objc func checkOnScreen(){
if let window = UIApplication.shared.keyWindow{
let currentRect = self.convert(self.bounds, to: window.layer)
if window.bounds.intersects(currentRect){
onScreenDelegate?.layerIsOnScreen(currentScreenSpace: currentRect)
}else{
onScreenDelegate?.layerIsNotOnScreen(currentScreenSpace: currentRect)
}
}
}
}
As to question 2 I think CATiledLayers probably does not use sublayers and instead slowly draws all the contents into a single contents image but by tiling and probably easier math for them. It might be something that takes the visible area draws it in the background and provides the layer contents. Then caches that section and adds another until it is complete.This is only a guess.
Here is the code from my test in a collectionView cell.
import UIKit
class AlbumCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
var onScreenLayer = OnScreenLayer()
var currentIndex : Int = 0
override func awakeFromNib() {
super.awakeFromNib()
onScreenLayer.frame = self.bounds
self.layer.addSublayer(onScreenLayer)
onScreenLayer.onScreenDelegate = self
}
override func layoutSubviews() {
super.layoutSubviews()
onScreenLayer.frame = self.bounds
}
}
extension AlbumCollectionViewCell : OnScreenLayerDelegate{
func layerIsOnScreen(currentScreenSpace: CGRect) {
//or more proof
print("it is on screen \(currentIndex)")
}
func layerIsNotOnScreen(currentScreenSpace: CGRect) {
print("not on screen \(currentIndex)")
}
}
The current index was set in cellForItem so I could monitor it. Let me know if this helps. Also the check could modify the frame to catch it right before it comes on screen by a margin that way you are drawing prior to that.

Swift WebKitView inside UITableViewCell - Cell reloads webkit view every time it comes on screen

I'm trying to load a WebKitView inside a UITableViewCell. Although the cell is repeating, I always only load one cell of this type. The WebKitView loads an embedded youtube video URL.
So currently, the code is working, but there is an issue that is causing me to second guess my code, and wonder if there is something I'm missing.
The behavior is as follows:
1. The table loads all cells properly, with no issues
2. The cell containing the web kit view loads properly, and the embedded youtube video link loads properly in the cell. The video is able to play / go full screen, and seems to be working just fine.
Here's where the problem starts:
3. As I scroll up and down through the table cells, every time the video cell comes on screen, there is a significant lag spike, and I can tell that the cell is reloaded as there is a print statement inside cellForRowAt that is printing every time.
4. An error is produced along with the print statement. The error is as follows:
2018-05-17 08:46:07.305058-0500 Storefront[15267:9513857] WF:
_WebFilterIsActive returning: NO
2018-05-17 08:46:08.817680-0500 Storefront[15267:9513857] WF:
_userSettingsForUser mobile: {
filterBlacklist = (
);
filterWhitelist = (
);
restrictWeb = 1;
useContentFilter = 0;
useContentFilterOverrides = 0;
whitelistEnabled = 0;
}
I did some research on the error specifically, and from what I've gathered it's caused by the secure URL that I specified loading insecure content. This is a youtube url, so this is very strange.
I tried adding youtube.com to my NSExceptionDomains inside my plist, with no change. However, from my research, I'm not convinced that the error being printed is related to the lag I'm experiencing, and the reloading of the webkitview every time it comes on screen. It must be something to do with how using a reusable cell containing a webkitview interacts with the cell loading.
Here's my cellForRow code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if product.productHasVideo == true {
let cell = tableView.deque(VideoTableViewCell.self, configureFrom: self.product, at: indexPath)
if let id = product.embededYoutubeVideoId {
print("has id")
if let url = URL(string: "https://www.youtube.com/embed/\(id)") {
let urlRequest = URLRequest(url: url)
cell.videoWebKitView.load(urlRequest)
}
} else {
print("no id")
}
return cell
} else {
return UITableViewCell()
}
}
}
Any thoughts or ideas? Thanks.
EDIT:
I thought to try removing the entire url loading section from the code and just load the webkit view. This was successful. The cell loaded the webkitview, but never loaded a URL. I still received the same error listed above, and the same lag listed above, so the issue definately has nothing to do with my plist or youtube or the URL I'm specifying.
Here is the code inside the tableViewCell:
import UIKit
import WebKit
class VideoTableViewCell: UITableViewCell, ViewModelConfigurable {
typealias ViewModelType = ProductViewModel
var viewModel: ViewModelType?
#IBOutlet var videoWebKitView: WKWebView!
func configureFrom(_ viewModel: ViewModelType) {
self.viewModel = viewModel
}
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Implementation of UITableView is making cells reusable, it is not holding all cells always, it makes how many it will fit on screen +/- some buffer. When You scroll it is making table view reuse cells -> reloading happens.
You can use just UIScrollView to instantiate custom views and then it is up to You when You want to destroy/reuse/remove them.

how to redraw non-visible UICollectionViewCell's after rotation ready for when reuse occurs?

How to redraw non-visible UICollectionViewCell's ready for when reuse occurs???
One approach I thought of was per the code in the Layout Cell prepareForReuse function, however whilst it works it non-optimal as it causes more re-drawing then required.
Background: Need to trigger drawRect for cells after an orientation change that are not current visible, but pop up to be used and haven't been redraw, so so far I can only see that prepareForReuse would be appropriate. Issue is I'm re-drawing all "reuse" cells, whereas I really only want to redraw those that initially pop up that were created during the previous orientation position of the device.
ADDITIONAL INFO: So currently I'm doing this:
In ViewController:
override func viewWillLayoutSubviews() {
// Clear cached layout attributes (to ensure new positions are calculated)
(self.cal.collectionViewLayout as! GCCalendarLayout).resetCache()
self.cal.collectionViewLayout.invalidateLayout()
// Trigger cells to redraw themselves (to get new widths etc)
for cell in self.cal?.visibleCells() as! [GCCalendarCell] {
cell.setNeedsDisplay()
}
// Not sure how to "setNeedsDisplay" on non visible cells here?
}
In Layout Cell class:
override func prepareForReuse() {
super.prepareForReuse()
// Ensure "drawRect" is called (only way I could see to handle change in orientation
self.setNeedsDisplay()
// ISSUE: It does this also for subsequent "prepareForReuse" after all
// non-visible cells have been re-used and re-drawn, so really
// not optimal
}
Example of what happens without the code in prepareForReuse above. Snapshot taken after an orientation change, and just after scrolling up a little bit:
I think I have it now here:
import UIKit
#IBDesignable class GCCalendarCell: UICollectionViewCell {
var prevBounds : CGRect?
override func layoutSubviews() {
if let prevBounds = prevBounds {
if !( (prevBounds.width == bounds.width) && (prevBounds.height == bounds.height) ) {
self.setNeedsDisplay()
}
}
}
override func drawRect(rect: CGRect) {
// Do Stuff
self.prevBounds = self.bounds
}
}
Noted this check didn't work in "prepareForReuse" as at this time the cell had not had the rotation applied. Seems to work in "layoutSubviews" however.
You can implement some kind of communication between the cells and the view controller holding the collection view ( protocol and delegate or passed block or even direct reference to the VC ). Then You can ask the view controller for rotation changes.
Its a bit messy, but if You have some kind of rotation tracking in Your view controller You can filter the setNeedsDisplay with a simple if statement.
I had similar challenged updating cells that were already displayed and off the screen. While cycling through ALLL cells may not be possible - refreshing / looping through non-visible ones is.
IF this is your use case - then read on. Pre - Warning - if you're adding this sort of code - explain why you're doing it. It's kind of anti pattern - but can help fix that bug and help ship your app albeit adding needless complexity. Don't use this in multiple spots in app.
Any collectionviewcell that's de-initialized (off the screen and being recylced) should be unsubscribed automatically.
Notification Pattern
let kUpdateButtonBarCell = NSNotification.Name("kUpdateButtonBarCell")
class Notificator {
static func fireNotification(notificationName: NSNotification.Name) {
NotificationCenter.default.post(name: notificationName, object: nil)
}
}
extension UICollectionViewCell{
func listenForBackgroundChanges(){
NotificationCenter.default.removeObserver(self, name: kUpdateButtonBarCell, object: nil)
NotificationCenter.default.addObserver(forName:kUpdateButtonBarCell, object: nil, queue: OperationQueue.main, using: { (note) in
print( " contentView: ",self.contentView)
})
}
}
override func collectionView(collectionView: UICollectionView!, cellForItemAtIndexPath indexPath: NSIndexPath!) -> UICollectionViewCell! {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("die", forIndexPath: indexPath) as UICollectionViewCell
cell.listenForBackgroundChanges()
return cell
}
// Where appropriate broadcast notification to hook into all cells past and present
Notificator.fireNotification(notificationName: kUpdateButtonBarCell)
Delegate Pattern
It's possible to simplify this.... an exercise for the reader. just do not retain the cells (use a weak link) - otherwise you'll have memory leaks.

Resources