How to reuse UICollectionViewCell with nested UIView for video - ios

I am using a UICollectionViewController to display cells with content loaded from an external JSON api. To be specific, these cells include UILabels for the user that posted the video, the title of the video, etc.
In addition to the text labels, there is a large UIView in the middle of the cell to play video.
The following is the code for my UICollectionViewCell.
class PostCell : UICollectionViewCell {
var videoCell: VideoPlayerView
let storyTitleLabel: UILabel = {
let storyTitleLabel = UILabel()
storyTitleLabel.translatesAutoresizingMaskIntoConstraints = false
return storyTitleLabel
}()
let usernameLabel: UILabel = {
let usernameLabel = UILabel()
usernameLabel.translatesAutoresizingMaskIntoConstraints = false
return usernameLabel
}()
func prepare(post: ApiPost) {
self.usernameLabel.text = post.user?.username
self.postTitleLabel.text = post.title!
self.storyTitleLabel.text = post.story?.title
self.videoCell.prepare(post: post)
}
func focus() {
self.videoCell.focus()
}
func unfocus() {
self.videoCell.unfocus()
}
}
When a user is scrolling through the UICollectionView and the middle of the screen is over the visible UICollectionViewCell, the focus method is called.
This loads up the video corresponding to the ApiPost (api data wrapper) for that UICollectionViewCell.
I have also programmed my own subclass of UIView for the videoCell as you can see. The following is the source code of that class.
class VideoPlayerView: UIView {
func prepare(post: ApiPost) {
self.post = post
self.videoUrl = sharedConfig.apiUrl + "/stream/" + post.video!.uuid! + ".mp4"
if let url = URL(string: self.videoUrl!) {
let avPlayerItem = AVPlayerItem(url: url)
if (self.hasReceivedApiData) {
DispatchQueue.global(qos: .background).async {
print("inserting...")
self.player?.replaceCurrentItem(with: avPlayerItem)
print("done")
}
} else {
self.hasReceivedApiData = true
self.player = AVQueuePlayer.init()
self.player?.automaticallyWaitsToMinimizeStalling = false
self.player?.insert(avPlayerItem, after: nil)
self.player?.volume = 1
let playerLayer = AVPlayerLayer(player: self.player)
playerLayer.frame = CGRect(x: 0, y: -70, width: self.frame.width - 32, height: self.frame.height)
self.layer.addSublayer(playerLayer)
}
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.player?.currentItem, queue: nil, using: { (_) in
DispatchQueue.main.async {
self.player?.seek(to: kCMTimeZero)
self.player?.play()
}
})
}
}
func focus() {
self.player?.volume = 1.0
self.player?.play()
}
func unfocus() {
self.player?.seek(to: kCMTimeZero)
self.player?.pause()
}
}
The problem that I am having is that when I scroll over the UICollectionView, it lags really badly when prepare() is called on a reused UICollectionViewCell. It seems that calling self.player?.replaceCurrentItem(with: avPlayerItem) is slowing the UI down on scroll. I have verified this by putting the print calls between. It seems this is a slow operation that is lagging the UI, as you can see, I even tried putting that operation on a different thread, but no luck.
Am I displaying video properly in a UICollectionViewCell? Should I be doing this using some other method? The only way I could figure out to clear the old video is by using an AVQueuePlayer to clear out old AVPlayerItems, and insert the new AVPlayerItem when the cell is reused and prepared for the next video.
Any help on this would be greatly appreciated! Thanks in advance.

Related

Reloading tableview row without interrupting the vide playing inside

I implemented AVQueuePlayer inside of each tableview cell and those cells showing a video.
When I reload any of these rows, the video inside gets interrupted and plays from the beginning.
My question is that how can I reload the row without touching the video at all.
UIView.performWithoutAnimation {
self.tableView.reloadRows(at: [indexPath], with: .none)
}
CellForRowAt
let cell: PostsWithVideoCustom = tableView.dequeueReusableCell(withIdentifier: "CellWithVideo", for: indexPath) as! PostsWithVideoCustom
let scale = UIScreen.main.bounds.width / CGFloat(release.photoWidth)
let bestHeight = CGFloat(release.photoHeight) * scale
cell.videoViewHeight.constant = bestHeight
cell.videoView.frame.size.height = bestHeight
let soundMuteTap = UITapGestureRecognizer(target: self, action: #selector(videoSoundMute))
cell.videoView.addGestureRecognizer(soundMuteTap)
cell.videoView.layer.sublayers?
.filter { $0 is AVPlayerLayer }
.forEach { $0.removeFromSuperlayer() }
CacheManager.shared.getFileWith(stringUrl: release.photoFilename) { result in
switch result {
case let .success(url):
let playerItem = AVPlayerItem(url: url)
cell.videoView.player = AVQueuePlayer(playerItem: playerItem)
cell.videoView.looper = AVPlayerLooper(player: cell.videoView.player!, templateItem: playerItem)
var layer = AVPlayerLayer()
layer = AVPlayerLayer(player: cell.videoView.player)
layer.backgroundColor = UIColor.white.cgColor
layer.frame = cell.videoView.bounds
layer.videoGravity = .resizeAspectFill
cell.videoView.layer.addSublayer(layer)
if self.userDefaults.string(forKey: "videoSound") == "1" {
cell.videoView.player?.isMuted = false
} else {
cell.videoView.player?.isMuted = true
}
case let .failure(error):
print(error)
}
}
return cell
Assuming you only play one video at a time, you could programatically create an AVQueuePlayer and give it desired frame and position instead of putting it in your cells. Doing this have two merits:
It achieves your goal.
Less memory usage.
You would also have to implement UIScrollViewDelegate to update AVPlayer's current position & playing state.

AVPlayerViewController ignores all touches after returning from fullscreen

I faced a very strange behaviour of AVPlayerViewController.
I use this code to add play video inside view:
class ViewShowTest:UIViewController
{
let view_for_media = UIView()
override func viewDidLoad()
{
super.viewDidLoad()
self.view.backgroundColor = .red
self.view.addSubview(view_for_media)
view_for_media.snp.makeConstraints(
{ make in
make.top.equalToSuperview().offset(100)
make.left.right.equalToSuperview()
make.height.equalTo(300)
})
setVideo()
}
func setVideo()
{
let url = URL(string: "https://www.radiantmediaplayer.com/media/bbb-360p.mp4")!
let player = AVPlayer(url: url)
let player_vc = AVPlayerViewController(nibName: nil, bundle: nil)
player_vc.player = player
self.view_for_media.addSubview(player_vc.view)
self.addChild(player_vc)
player_vc.didMove(toParent: self)
player_vc.view.frame = view_for_media.bounds
}
}
Video is playing as expected inside view. But after going to fullScreen and returning back (no matter with swipe or close button) ViewShowTest ignores all touches. I still can drag default ios Menus from top and bottom but ViewShowTest view and player_vc.view does not reacts any touches. This bug appears only on ios 12.4 and below. How can i solve this problem?

Add layer underneath subviews

I am trying to add a sublayer of the function setupPlayerView below subviews which have been loaded previously. The subviews are loaded faster than the layer as the function creating the layer uses a string which is modified from another class, a UIViewController (I want to load individual videos). I found an answer on stackoverflow but the suggested solution of .insertSublayer(playerLayer, below: self.controlsContainerView.layer) did not work for me. How can I ensure the videolayer is under all my other subviews like controlsContainerView and its subviews (e.g. activityIndicatorView) in turn?
Here is what I got right now:
class PlayerView: UIView {
var urlString: String? {
didSet {
setupPlayerView()
}
}
override init(frame: CGRect){
super.init(frame: frame)
//although the function is called immediately it is not the "lower" than everything else
setupPlayerView()
//add subview with controls (e.g. spinner)
controlsContainerView.frame = frame
addSubview(controlsContainerView)
//add to subview and center spinner in subview
controlsContainerView.addSubview(activityIndicatorView) // (spinner)
controlsContainerView.addSubview(whiteView)
//...
}
//... irrelevant code (closures like `activityIndicatorView` and `whiteView`)
//setup video player
func setupPlayerView() {
if let urlString = self.urlString, let videoURL = NSURL(string: urlString){
print(urlString)
//player's video
if self.player == nil {
player = AVPlayer(url: videoURL as URL)
}
//add sub-layer
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.frame
self.controlsContainerView.layer.insertSublayer(playerLayer, below: self.controlsContainerView.layer)
//when are frames actually rendered (when is video loaded so one can remove spinner)
player?.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: nil)
}
}
For those interested: I change urlString the following way in another class (a ViewController):
videoPlayerView.urlString = videoUrl //videoUrl being some link to a video source

Swift: Delay loading animations in collection view?

I have not seen this topic posted anywhere else - I have built an iMessage app that uses a collecitonview to hold a bunch of MSStickerViews. These stickers are animated.
My problem is that while the app itself appears to load when you first open, there is a noticeable delay before one is able to touch a sticker/interact with the app, and before the MSStickers begin animating. I am not sure whether this is a matter of the order of functions being called but I can't find a way to improve/fix this.
My MSStickers are added to collection view cells here:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// get a reference to our storyboard cell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "stickerCell", for: indexPath as IndexPath) as! StickerCollectionViewCell
and in a separate Cell class I then add the sticker view and set it to .startAnimating().
The delay is about 5-6 seconds. How can I reduce this? Is this possible?
Animation/sticker code:
for (animal, atlas) in animalsAnim {
animalsAnim[animal] = getImagesForAnimals(animal: animal, allSticks: allStickers)
addSticker(images: animalsAnim[animal]!, name: animal)
}
func addSticker(images: [UIImage], name: String)
{
let sticker: MSSticker
do {
try sticker=MSSticker(images: images, format: .apng, frameDelay: 0.95/14.0, numberOfLoops: 0, localizedDescription: name)
}catch MSStickerAnimationInputError.InvalidDimensions {
fatalError("ERROR: Dimens")
}catch MSStickerAnimationInputError.InvalidStickerFileSize {
fatalError("ERROR: Size")
} catch { fatalError("other error:\(error)") }
var stickerSize = CGSize()
if (UIDevice.current.iPhone5orBefore)
{
stickerSize = CGSize(width: view.bounds.width*0.38, height: view.bounds.width*0.38)
}
else {
stickerSize = CGSize(width: view.bounds.width*0.4, height: view.bounds.width*0.4)
}
let stickerView = InstrumentedStickerView(frame: CGRect(origin: CGPoint(x: 0,y :0), size: stickerSize))
stickerView.sticker = sticker
stickerView.delegate = self
stickerPack.append(stickerView)
}
The sticker view is then added and begun animating in a separate cell class.

AVPlayer not full screen in landscape mode using iOS swift

I have a video view set to full screen. However while playing in the simulator, it is not running full screen.
This issue is only for iPads and not iPhones.
Here is my code:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
let bundle: Bundle = Bundle.main
let videoPlayer: String = bundle.path(forResource: "normalnewer", ofType: "mp4")!
let movieUrl : NSURL = NSURL.fileURL(withPath: videoPlayer) as NSURL
// var fileURL = NSURL(fileURLWithPath: "/Users/Mantas/Desktop/123/123/video-1453562323.mp4.mp4")
playerView = AVPlayer(url: movieUrl as URL)
NotificationCenter.default.addObserver(self,selector: #selector(playerItemDidReachEnd),name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,object: self.playerView.currentItem) // Add observer
var playerLayer=AVPlayerLayer(player: playerView)
// self.avPlayerLayer.playerLayer = AVLayerVideoGravityResizeAspectFill
playerLayer.frame = viewVideo.bounds
// self.playerLayer.frame = self.videoPreviewLayer.bounds
viewVideo.layer.addSublayer(playerLayer)
playerView.play()
UserDefaults.standard.set("normalnewer", forKey: "video")
}
I have tried checking landscape mode in target's general.
Gone through the threads below:
how to play a video in fullscreen mode using swift ios?
Play video fullscreen in landscape mode, when my entire application is in lock in portrait mode
Setting device orientation in Swift iOS
But these did not resolve my issue.
Here is a screenshot.
Can somebody help me resolve this?
I see you solved your issue, but I noticed you were using an AVPlayerLayer. The orientation rotation is handled with the AVPlayerViewController, but not with a custom view using a player layer. It is useful to be able to put the layer in fullscreen without rotating the device. I answered this question elsewhere, but I will put it here as well.
Transforms and frame manipulation can solve this issue:
extension CGAffineTransform {
static let ninetyDegreeRotation = CGAffineTransform(rotationAngle: CGFloat(M_PI / 2))
}
extension AVPlayerLayer {
var fullScreenAnimationDuration: TimeInterval {
return 0.15
}
func minimizeToFrame(_ frame: CGRect) {
UIView.animate(withDuration: fullScreenAnimationDuration) {
self.setAffineTransform(.identity)
self.frame = frame
}
}
func goFullscreen() {
UIView.animate(withDuration: fullScreenAnimationDuration) {
self.setAffineTransform(.ninetyDegreeRotation)
self.frame = UIScreen.main.bounds
}
}
}
Setting the frame of the AVPlayerLayer changes it's parent's frame. Save the original frame in your view subclass, to minimize the AVPLayerLayer back to where it was. This allows for autolayout.
IMPORTANT - This only works if the player is in the center of your view subclass.
Incomplete example:
class AVPlayerView: UIView {
fileprivate var avPlayerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
fileprivate var hasGoneFullScreen = false
fileprivate var isPlaying = false
fileprivate var originalFrame = CGRect.zero
func togglePlayback() {
if !hasGoneFullScreen {
originalFrame = frame
hasGoneFullScreen = true
}
isPlaying = !isPlaying
if isPlaying {
avPlayerLayer.goFullscreen()
avPlayerLayer.player?.play()
} else {
avPlayerLayer.player?.pause()
avPlayerLayer.minimizeToFrame(originalFrame)
}
}
}
After spending some time taking a good look at the Storyboard, I tried changing the fill to Aspect fill & fit and that resolved the problem :)
The best thing you can do is put this code
In viewDidLayoutSubviews and enjoy it!
DispatchQueue.main.async {
self.playerLayer?.frame = self.videoPlayerView.bounds
}

Resources