AVPlayer layer inside a view does not resize when UIView frame changes
Objective CUiviewCalayerAvplayerObjective C Problem Overview
I have a UIView
which contains an AVPlayer
to show a video. When changing orientation, I need to change the size and location of the video.
I'm not very experienced with resizing layers, so I'm having problems making the video resize.
I start by creating the AVPlayer
and adding its player to my videoHolderView's layer:
NSURL *videoURL = [NSURL fileURLWithPath:videoPath];
self.avPlayer = [AVPlayer playerWithURL:videoURL];
AVPlayerLayer* playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.avPlayer];
playerLayer.frame = videoHolderView.bounds;
playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
playerLayer.needsDisplayOnBoundsChange = YES;
[videoHolderView.layer addSublayer:playerLayer];
videoHolderView.layer.needsDisplayOnBoundsChange = YES;
Then, at a later point, I change the size and location of the videoHolderView's frame:
[videoHolderView setFrame:CGRectMake(0, 0, 768, 502)];
At this point, I need the avPlayer to resize to these same dimension. This doesn't happen automatically - the avPlayer stays at it's small size within the videoHolderView.
If anyone can help, I'd really appreciate any advice.
Thanks guys.
Objective C Solutions
Solution 1 - Objective C
playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFit;
Solution 2 - Objective C
Converting @Suran's solution to Swift:
First, create a class inheriting UIView where you override only 1 variable:
import UIKit
import AVFoundation
class AVPlayerView: UIView {
override class var layerClass: AnyClass {
return AVPlayerLayer.self
}
}
Then add a simple UIView using the interface builder and change its class to the one you just created: AVPlayerView
Then, make an outlet for that view. Call it avPlayerView
@IBOutlet weak var avPlayerView: AVPlayerView!
Now, you can use that view inside your viewcontroller and access its avlayer like this:
let avPlayer = AVPlayer(url: video)
let castLayer = avPlayerView.layer as! AVPlayerLayer
castLayer.player = avPlayer
avPlayer.play()
The layer will now follow the constraints just like a regular layer would do. No need to manually change bounds or sizes.
Solution 3 - Objective C
Actually you shouldn't add AVPlayer's layer as a sublayer. Instead of that you should use the following method, in the subclass of view in which you want to display AVPlayer.
+ (Class)layerClass
{
return [AVPlayerLayer class];
}
And use the following line to add(set) the player layer.
[(AVPlayerLayer *)self.layer setPlayer:self.avPlayer];
Hope it helps;-)
Solution 4 - Objective C
In the end I solved this by re-adding the AVPlayerLayer to the UIView. I'm not sure why changing the frame removed the layer, but it did. Here's the final solution:
//amend the frame of the view
[videoHolderView setFrame:CGRectMake(0, 0, 768, 502)];
//reset the layer's frame, and re-add it to the view
AVPlayerLayer* playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.avPlayer];
playerLayer.frame = videoHolderView.bounds;
[videoHolderView.layer addSublayer:playerLayer];
Solution 5 - Objective C
Found a great article by Marco Santarossa that shows multiple approaches to fixing. https://marcosantadev.com/calayer-auto-layout-swift/
I used his first suggestion to reset the layer frame during viewDidLayoutSubViews() event.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
playerLayer.frame = view.layer.bounds
}
Solution 6 - Objective C
You can change the layer's frame by overriding the -layoutSubviews
method:
- (void)layoutSubviews
{
[super layoutSubviews];
self.playerLayer.frame = self.bounds;
}
Solution 7 - Objective C
In Swift 4 use
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
or whichever property of AVLayerVideoGravity
you desire.
Solution 8 - Objective C
theDunc's answer did not work for me. I found a solution that is more simple: I just needed to adjust the frame of the AVPlayerLayer after changing it in the UIView:
avPlayerLayer.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
In this blog is stated, that a View's frame also affect that of the layer in it.
> When you change a view’s frame, it’s simply changing the layer’s frame.
For this case, it is not true.
Solution 9 - Objective C
I had this problem in Swift 2.3, and I solved writing a proper PlayerView class and setting it as subview:
import UIKit
import AVKit
import AVFoundation
class PlayerPreviewView: UIView {
override class func layerClass() -> AnyClass {
return AVPlayerLayer.self
}
var player: AVPlayer? {
get {
return playerLayer.player
}
set {
playerLayer.player = newValue
}
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
override init(frame: CGRect) {
super.init(frame: frame)
playerLayer.videoGravity = AVLayerVideoGravityResize
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
playerLayer.videoGravity = AVLayerVideoGravityResize
}
}
In the presenting ViewController:
private func play(asset asset: AVURLAsset){
let playerItem = AVPlayerItem(asset: asset)
player = AVPlayer(playerItem: playerItem)
player?.actionAtItemEnd = .None
player?.muted = true
playerPreviewView = PlayerPreviewView(frame: CGRectZero)
view.addSubview(playerPreviewView)
playerPreviewView.player = player
playerPreviewView.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[subview]-0-|", options: .DirectionLeadingToTrailing, metrics: nil, views: ["subview": playerPreviewView]))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[subview]-0-|", options: .DirectionLeadingToTrailing, metrics: nil, views: ["subview": playerPreviewView]))
player?.play()
NSNotificationCenter.defaultCenter().addObserver(self,
selector: #selector(SplashScreenViewController.videoDidFinish),
name: AVPlayerItemDidPlayToEndTimeNotification,
object: nil)
}
Solution 10 - Objective C
First step: check out UIView's autoresizing property in UIVIew.h
> @property(nonatomic) UIViewAutoresizing autoresizingMask; // simple resize. default is UIViewAutoresizingNone
This property correspondents to the "springs and struts" controls in IB, though it will take you some experimentation to get the results you want.
Solution 11 - Objective C
To get to playerLayer
, you need to loop through videoHolderView.layer.sublayers
and change each one.
this is what I did in Swift 2.2
if let sublayers = videoHolderView.layer.sublayers{
for layer in sublayers where layer is AVPlayerLayer{
layer.frame.size = ... // change size of the layer here
}
}
Solution 12 - Objective C
For those of you who are only concerned with resizing the AVPlayer during device rotation, you can alter your layer frame in the viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) method as shown below.
//Swift 4
//Method in UIViewController
//Variable definitions are:
//self.layer is the AVPlayerLayer
//self.videoPlayerView is the view that the AVPlayerLayer is the sublayer of
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { context in
self.layer.layoutIfNeeded()
UIView.animate(
withDuration: context.transitionDuration,
animations: {
self.layer.frame = self.videoPlayerView.bounds
}
)
}, completion: nil)
}
This will animate your layer frame along with all the other views during the rotation.
Solution 13 - Objective C
Any one searching for Xamarin Version as i was searching
don't add the AVPlayerLayer as sublayer but set the layer to Avplayer Layer
[Export("layerClass")]
public static Class LayerClass()
{
return new Class(typeof(AVPlayerLayer));
}
The following line set the layer
(this.Layer as AVPlayerLayer).Player = _player; // Acplayer