Using VideoNode in CollectionView instead of AVPlayer - ios

I have a UICollectionView which displays photos and videos. I was using an AVPlayer to display videos, however this resulted in very choppy scrolling. To combat this I am using the VideoNode from the AsyncDisplayKit. Currently in my cellForItemAt method I do the following:
cell.viewWithTag(300)?.removeFromSuperview()
if (image) {
//show image
}
else if (video) {
let mainNode = ASDisplayNode()
let videoNode = ASVideoNode()
DispatchQueue.background {
videoNode.frame = CGRect(x: 0.0,y:0.0,width: cell.bounds.width,height: cell.bounds.height)
videoNode.gravity = AVLayerVideoGravityResizeAspectFill
videoNode.shouldAutoplay = true
videoNode.shouldAutorepeat = true
videoNode.muted = true
videoNode.asset = AVAsset(url: cached_url)
}
DispatchQueue.main.async {
mainNode.addSubnode(videoNode)
mainNode.view.tag = 300
cell.addSubview(mainNode.view)
cell.sendSubview(toBack: mainNode.view)
videoNode.play()
cell.backgroundImageView.alpha = 0
cell.gradientOverlay.alpha = 1
}
}
However, with fast scrolling this is still a bit choppy, and the cells containing videos are briefly white before the video shows. Is there a way I could improve this code to further improve scrolling performance and make it as smooth as possible?

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.

Removing the QuickTime icon from AVPlayerViewController

I am building a screen where users can play audio files using an AVPlayerViewController. The problem is that I can't get rid of the QuickTime logo in the player view, see screenshot:
My code:
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)
guard let url = URL(string: filePath!) else {
return
}
let player = AVPlayer(url: url)
let controller = AVPlayerViewController()
controller.modalPresentationStyle = .overFullScreen
controller.player = player
self.present(controller, animated: true) {
player.play()
}
} catch {
}
I've tried adding an UIImageView using controller.contentOverlayView?.addSubView(), but I can't center that properly. How do I customize the layout of the player without having to build my own interface from scratch?
What you tried is correct: just add a subview to the content overlay view. But you left out one step: you must give both the subview and the content overlay view constraints, to make them cover the player controller’s view completely.
Example (my av is your controller):
let iv = UIView()
iv.backgroundColor = .white
av.contentOverlayView!.addSubview(iv)
let v = av.contentOverlayView!
iv.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
iv.bottomAnchor.constraint(equalTo:v.bottomAnchor),
iv.topAnchor.constraint(equalTo:v.topAnchor),
iv.leadingAnchor.constraint(equalTo:v.leadingAnchor),
iv.trailingAnchor.constraint(equalTo:v.trailingAnchor),
])
NSLayoutConstraint.activate([
v.bottomAnchor.constraint(equalTo:av.view.bottomAnchor),
v.topAnchor.constraint(equalTo:av.view.topAnchor),
v.leadingAnchor.constraint(equalTo:av.view.leadingAnchor),
v.trailingAnchor.constraint(equalTo:av.view.trailingAnchor),
])
If all you want to do is remove the "Q" logo without replacing it with anything, you can get rid of this way:
(avPlayerViewController?.view.subviews.first?.subviews.first as? UIImageView)?.image = nil
But, at least for me, the "Q" logo usually only appears about 2 seconds after I've set up the player view controller. So you might need to use a timer to get the above code to run about 2 seconds after creating the player.

How to reuse UICollectionViewCell with nested UIView for video

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.

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
}

How to get a specific video area in Swift AVPlayer?

I am working on Swift base MAC OS-X application. The user will choose a video file and the NSView Controller will play the video using AVPlayerView. The user can draw a rectangle over the AVPlayerView and I have achieved this functionality. How to show the selected area video in a rectangle to a camera Preview Layer at right bottom?
Here is the output:
Here is the Code:
var videoURL = NSURL() // The video file URL by the file chooser
var videoSize = NSSize() // Here the video resolution size will be kept i.e. 720*480
var player:AVPlayerView! // The AVPlayerView to play video
var cameraPreviewLayer:AVPlayerLayer! //The right bottom camera Preview Layer
var rectangleLayer:AVPlayerLayer! // The rectangle that will draw over AVPlayer while mouse dragging
var isClicked = false
var startPoint = NSPoint() // holds the starting point location of mouse pointer while draging
var rect:NSRect!
override func viewDidLoad() {
super.viewDidLoad()
self.player = AVPlayerView()
self.player.frame.origin = CGPoint(x: 0, y: 0)
self.player.setFrameSize(self.videoSize)
self.player.player = AVPlayer.playerWithURL(videoURL) as! AVPlayer
self.view.addSubview(self.player)
setupRectangle()
setupCameraPreviewLayer()
}
// intially setup white color rectangle that will draged over AVPlayer
func setupRectangle() {
self.rectangleLayer = AVPlayerLayer()
self.rectangleLayer.backgroundColor = NSColor.clearColor().CGColor
self.rectangleLayer.borderColor = NSColor.whiteColor().CGColor
self.rectangleLayer.borderWidth = 1.5
self.rectangleLayer.frame = self.player.bounds
}
// intially setup CameraPreview Layer that will show at right bottom of AVPlayer
func setupCameraPreviewLayer(){
cameraPreviewLayer = AVPlayerLayer(player: self.player.player)
//place the cameraPreview layer at right bottom
cameraPreviewLayer.frame.origin.x = self.player.framesize.width-100
cameraPreviewLayer.frame.origin.y = self.player.frame.origin.y-100
cameraPreviewLayer.frame.size.width = 100
cameraPreviewLayer.frame.size.height = 100
}
override func mouseDown(theEvent: NSEvent) {
startPoint = theEvent.locationInWindow
isClicked = true
removeCameraPreviewLayer()
}
override func mouseDragged(theEvent: NSEvent) {
var endPoint = theEvent.locationInWindow
if( isClicked ){
rect = NSRect(x: startPoint.x, y: startPoint.y, width: -(startPoint.x-endPoint.x), height: -(startPoint.y-endPoint.y))
drawCustomRect(rect)
}
}
override func mouseUp(theEvent: NSEvent) {
var endPoint = theEvent.locationInWindow
if( isClicked ){
// I think we have to do some magic code here
addCameraPreviewLayer()
}
isClicked = false;
}
// redraw the white color rectange over avplayer
func drawCustomRect(rect: NSRect) {
self.rectangleLayer.frame = rect
self.player.layer?.addSublayer(self.rectangleLayer)
}
// add Camera PreviewLayer from AVPlayer
func addCameraPreviewLayer() {
self.player.layer?.addSublayer(self.layer)
}
// remove Camera PreviewLayer from AVPlayer
func removeCameraPreviewLayer() {
self.cameraPreviewLayer.removeFromSuperlayer()
}
Here is the desire output picture which I wants.
Suppose Video has a size 720*480, user has draw a rectangle and its points are (x1, y1) (x2,y1) (x3,y3) (x4,y4). How could I crop a video in camera Preview layer(at right bottom) which shows the video of area same as rectangle selected by the user?
Any one help how to achieve this functionality? I have spend many days on this and exhaust.
Note: I can do it in open-CV by video processing using ROI, but the requirement is to do it in native SWIFT Language.

Resources