What's the trick to getting AVPlayer video content to show up in one's view?
We are using the following AVPlayer code but nothing is appearing on screen. We know the video is there because we were able to show it using an MPMoviePlayerController.
Here is the code we are using:
AVAsset *asset = [AVAsset assetWithURL:videoTempURL];
AVPlayerItem *item = [[AVPlayerItem alloc] initWithAsset:asset];
AVPlayer *player = [[AVPlayer alloc] initWithPlayerItem:item];
player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
AVPlayerLayer *layer = [AVPlayerLayer playerLayerWithPlayer:player];
// layer.frame = self.view.frame;
[self.view.layer addSublayer:layer];
layer.backgroundColor = [UIColor clearColor].CGColor;
//layer.backgroundColor = [UIColor greenColor].CGColor;
[layer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
[player play];
Are we setting the layer improperly for the current view?
You need to set the layer's frame property. e.g.:
self.playerLayer.frame = CGRectMake(0, 0, 100, 100)
If you tried this and it didn't work in the view controller's view, it is likely that you tried to set the layer's frame property to the view-controllers frame or bounds property which was {0, 0, 0, 0} at the time the AVPlayerLayer was created. You will need to set the frame of the player during layout-passes, at which point the view-controller's frame will be set to something other than {0, 0, 0, 0}. To do this properly:
If you're using auto-layout in a custom UIView (including IB):
override func layoutSubviews() {
super.layoutSubviews()
//Match size of view
CATransaction.begin()
CATransaction.setDisableActions(true)
self.playerLayer.frame = self.bounds
CATransaction.commit()
}
If you're using auto-layout in a custom UIViewController:
override fun viewDidLayoutSubviews() {
//Match size of view-controller
CATransaction.begin()
CATransaction.setDisableActions(true)
self.playerLayer.frame = self.view.bounds
CATransaction.commit()
}
The CATransaction lines are to disable implicit animations on the layer's frame change. If you're wondering why this normally not needed, it's because layers that back UIView's will not implicitly animate by default. In this case, we are using a non-view backed layer (AVPlayerLayer)
The best route to take will be to add a new view to your view-controller through interface build and set a custom class on the newly added view. Then create that custom view class and implement the layoutSubviews code.
It turns out that AVPlayer needs its own contextual view in order to play.
We added this code and now the video plays. Unfortunately, AVPlayer has no built in controls unlike MPMoviePlayerController. It is unclear why Apple is deprecating towards a tool that will have non-standardized video playback options.
UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0, 320.0f, 200.0f)];
layer.frame = self.view.frame;
[containerView.layer addSublayer:layer];
[self.view addSubview:containerView];
layer.backgroundColor = [UIColor greenColor].CGColor;
[layer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
[player play];
Related
I am developing an application that include functionality to play video with per-frame animation.
You can see an example of such functionality.
I already tried to add CAKeyFrameAnimation to sublayer of AVSynchronizedLayer and have some troubles with it.
I also already tried to pre-render video with AVAssetExportSession, and it is working perfectly. But it's very slow. It needa up to 3 minutes to render such video.
Maybe there are other approaches to make it?
Update:
This is how I implement animation with AVSynchronizedLayer:
let fullScreenAnimationLayer = CALayer()
fullScreenAnimationLayer.frame = videoRect
fullScreenAnimationLayer.geometryFlipped = true
values: [NSValue] = [], times: [NSNumber] = []
// fill values array with positions of face center for each frame
values.append(NSValue(CATransform3D: t))
// fill times with corresoinding time for each frame
times.append(NSNumber(double: (Double(j) / fps) / videoDuration)) // where fps = 25 (according to video file fps)
...
let transform = CAKeyframeAnimation(keyPath: "transform")
transform.duration = videoDuration
transform.calculationMode = kCAAnimationDiscrete
transform.beginTime = AVCoreAnimationBeginTimeAtZero
transform.values = values
transform.keyTimes = times
transform.removedOnCompletion = false
transform.fillMode = kCAFillModeForwards
fullScreenAnimationLayer.addAnimation(transform, forKey: "a_transform")
...
if let syncLayer = AVSynchronizedLayer(playerItem: player.currentItem) {
syncLayer.frame = CGRect(origin: CGPointZero, size: videoView.bounds.size)
syncLayer.addSublayer(fullScreenAnimationLayer)
videoView.layer.addSublayer(syncLayer)
}
Here's my opinion,
add AVPlayer layer property(AVPlayerLayer class) to a sublayer to a UIView layer, then manipulate the view animation.
for example,
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:blahURL options:nil];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:urlAsset];
AVPlayer *player = [AVPlayer playerWithPlayerItem:playerItem];
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
playerLayer.frame = yourFrame;
UIView *videoView = [UIView new];
[videoView addSublayer:playerLayer];
then
give animations to videoView
I'm trying to make a circle-masked image view with animated mask, and played with different solutions. The example below does the job but I've got two problems with it:
1) Why is it that I cannot make the image tappable? Adding eg. a UITapGestureRecognizer does not work. My guess is that the mask prevents the touch actions being propagated to the lower level in the view hierarchy.
2) Animating the mask is running very fast and I cannot adjust the duration using UIView block animation
How can I solve these?
- (void) addCircle {
// this is the encapsulating view
//
base = [[UIView alloc] init];
//
// this is the button background
//
base_bgr = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"c1_bgr.png"]];
base_bgr.center = CGPointMake(60, 140);
[base addSubview:base_bgr];
//
// icon image
//
base_icon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"c1_ico.png"]];
base_icon.center = CGPointMake(186*0.3/2, 182*0.3/2);
base_icon.transform = CGAffineTransformMakeScale(0.3, 0.3);
[base addSubview:base_icon];
//
// the drawn circle mask layer
//
circleLayer = [CAShapeLayer layer];
// Give the layer the same bounds as your image view
[circleLayer setBounds:CGRectMake(0.0f, 0.0f, [base_icon frame].size.width,
[base_icon frame].size.height)];
// Position the circle
[circleLayer setPosition:CGPointMake(186*0.3/2-7, 182*0.3/2-10)];
// Create a circle path.
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:
CGRectMake(0.0f, 0.0f, 70.0f, 70.0f)];
// Set the path on the layer
[circleLayer setPath:[path CGPath]];
[[base layer] setMask:circleLayer];
[self.view addSubview:base];
base.center = CGPointMake(100, 100);
base.userInteractionEnabled = YES;
base_bgr.userInteractionEnabled = YES;
base_icon.userInteractionEnabled = YES;
//
// NOT working: UITapGestureRecognizer
//
UITapGestureRecognizer *tapgesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapit:)];
[base_icon addGestureRecognizer:tapgesture];
//
// BAD but WORKS :) properly positioned UIButton over the masked image
//
base_btn = [UIButton buttonWithType:UIButtonTypeCustom];
base_btn.frame = CGRectMake(base.frame.origin.x, base.frame.origin.y, base_icon.frame.size.width, base_icon.frame.size.height);
[base_btn addTarget:self action:#selector(tapit:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:base_btn];
}
This is the tap handler, and here's the mask animation. Whatever number I tried in duration, it is animating fast - approximately 0.25 second, and I cannot adjust it.
- (void) tapit:(id) sender {
//...
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^ {
[circleLayer setTransform:CATransform3DMakeScale(10.0, 10.0, 1.0)];
}
completion:^(BOOL finished) {
// it is not necessary if I manage to make the icon image tappable
base_btn.frame = [base convertRect:base_icon.frame toView:self.view];
}];
}
}
1) touches are not propagated down from base because it's initiated without a frame, so its frame will be CGRectZero. Views don't get touch events that start outside of their bounds. Simply set a valid frame on base that includes entire tap target.
2) setTransform: on a layer invokes an implicit animation which uses Core Animation's default duration of 0.25 (you guessed it right :)). The best solution would be to use CABasicAnimation instead of UIView-based animation. Something like this:
CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
scaleAnimation.toValue = #(10.0f);
scaleAnimation.duration = 2;
[circleLayer addAnimation:scaleAnimation forKey:nil];
Note: by default, CABasicAnimation will remove itself from a layer when complete and the layer will snap back to old values. You can prevent it by for example setting that animation's removedOnCompletion property to NO and removing it yourself later using CALayer's removeAnimationForKey: method (just set a key instead of passing nil wheen adding the animation), but that depends on what exactly you want to accomplish with this.
I'm having issues making the animation use the AVPlayer time instead of the system time. the synchronized layer does not work properly and animations stay synchronized on the system time instead of the player time. I know the player do play. and if I pass CACurrentMediaTime() to the beginTime, the animation start right away as it should when not synchronized.
EDIT
I can see the red square in its final state since the beginning, which mean the animation has reach its end at the beginning because it is synchronized on the system time and not the AVPlayerItem time.
// play composition live in order to modifier
AVPlayerItem * playerItem = [AVPlayerItem playerItemWithAsset:composition];
AVPlayer * player = [AVPlayer playerWithPlayerItem:playerItem];
AVPlayerLayer * playerLayer = [AVPlayerLayer playerLayerWithPlayer:player ];
playerLayer.frame = [UIScreen mainScreen].bounds;
if (!playerItem) {
NSLog(#"playerItem empty");
}
// dummy time
playerItem.forwardPlaybackEndTime = totalDuration;
playerItem.videoComposition = videoComposition;
CALayer * aLayer = [CALayer layer];
aLayer.frame = CGRectMake(100, 100, 60, 60);
aLayer.backgroundColor = [UIColor redColor].CGColor;
aLayer.opacity = 0.f;
CAKeyframeAnimation * keyframeAnimation2 = [CAKeyframeAnimation animationWithKeyPath:#"opacity"];
keyframeAnimation2.removedOnCompletion = NO;
keyframeAnimation2.beginTime = 0.1;
keyframeAnimation2.duration = 4.0;
keyframeAnimation2.fillMode = kCAFillModeBoth;
keyframeAnimation2.keyTimes = #[#0.0, #1.0];
keyframeAnimation2.values = #[#0.f, #1.f];
NSLog(#"%f current media time", CACurrentMediaTime());
[aLayer addAnimation:keyframeAnimation2
forKey:#"opacity"];
[self.parentLayer addSublayer:aLayer];
AVSynchronizedLayer * synchronizedLayer =
[AVSynchronizedLayer synchronizedLayerWithPlayerItem:playerItem];
synchronizedLayer.frame = [UIScreen mainScreen].bounds;
[synchronizedLayer addSublayer:self.parentLayer];
[playerLayer addSublayer:synchronizedLayer];
The solution was that AVSynchronizedLayer doesn't work on the Simulator but works fine on a device.
I've set up a project as a test using AVSyncronizedLayer to move a red line (CALayer) across the screen as a movie plays.
When doing this I referenced the answer given here and have included that solution, but the animation doesn't start when the video does.
If anyone has any ideas where I'm going wrong that would be really helpful. The code is:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//create the red line sublayer
redLine = [CALayer layer];
redLine.backgroundColor = [UIColor redColor].CGColor;
redLine.frame = CGRectMake(engravingControl.frame.origin.x, engravingControl.center.y - (engravingControl.frame.size.height/2), 3, engravingControl.frame.size.height);
//create the AVplayer object
NSURL *fileURL = [[NSBundle mainBundle] URLForResource:#"IMG_1306" withExtension:#"mov"];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:fileURL options:nil];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];
AVPlayer *player = [AVPlayer playerWithPlayerItem:playerItem];
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
playerLayer.frame = CGRectMake(0, 0,400,300);
playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
//add the subViews
[self.view.layer addSublayer:playerLayer];
[self.view.layer addSublayer:redLine];
CABasicAnimation *redLineMove;
redLineMove=[CABasicAnimation animationWithKeyPath:#"transform.translation.x"];
redLineMove.duration=10;
redLineMove.fromValue=[NSNumber numberWithFloat:engravingControl.frame.origin.x];
redLineMove.toValue=[NSNumber numberWithFloat:engravingControl.frame.size.width + engravingControl.frame.origin.x];
redLineMove.beginTime = AVCoreAnimationBeginTimeAtZero;
//create the sync layer
AVSynchronizedLayer *syncLayer = [AVSynchronizedLayer synchronizedLayerWithPlayerItem:playerItem];
[syncLayer addSublayer:redLine];
[redLine addAnimation:redLineMove forKey:#"redLineMove"];
[self.view.layer addSublayer:syncLayer];
[player play];
}
I discovered a solution to this. If you don't have the following line of code:
animation.removedOnCompletion = NO;
Then the animation never starts (I presume it is removed by CA before the movie starts to play).
Add that in, and it works as you would expect.
Whenever I change the frame of my AVPlayerLayer, the video is not resized immediately, but animated to the new size.
For example: I change the frame from (0, 0, 100, 100) to (0, 0, 400, 400), the view's frame is changed immediately, but the video's size is animated to the new size.
Has anyone encountered this issue? And if yes does someone know a way to disable the default animation?
Thanks!
You can try disabling implicit actions and using zero length animations:
CALayer *videolayer = <# AVPlayerLayer #>
[CATransaction begin];
[CATransaction setAnimationDuration:0];
[CATransaction setDisableActions:YES];
CGRect rect = videolayer.bounds;
rect.size.width /= 3;
rect.size.height /= 3;
videolayer.bounds = rect;
[CATransaction commit];
This is what I used:
AVPlayerLayer * playerLayer = <# AVPlayerLayer #>;
playerLayer.frame = <# CGRect #>;
[playerLayer removeAllAnimations];
I hope this helps. I don't know if its best practices, but it works for me.
It seems that whenever ".frame" or "setFrame" is used, it adds animation to the layer.
The easiest and cleanest way to deal with this is to create a UIView subclass that has AVPlayerLayer as its layerClass. When doing this the AVPlayerLayer will behave just like a regular UIView layer. You can change the frame of the view instead of the layer and no implicit animations will happen.
AVPlayerLayerView.h
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#interface AVPlayerLayerView : UIView
#property (nonatomic, readonly) AVPlayerLayer *playerLayer;
#end
AVPlayerLayerView.m
#import "AVPlayerLayerView.h"
#implementation AVPlayerLayerView
+ (Class)layerClass {
return [AVPlayerLayer class];
}
- (AVPlayerLayer *)playerLayer {
return (AVPlayerLayer *)self.layer;
}
#end
You can now do this:
playerLayerView.frame = CGRectMake(0, 0, 400, 400);
To associate the AVPlayerLayer with an AVPlayer simply do this:
playerLayerView.playerLayer.player = player;
Do you use ?:
- (void)setPlayer:(AVPlayer *)player {
[(AVPlayerLayer *)[self layer] setPlayer:player];
[(AVPlayerLayer *)[self layer] setVideoGravity:AVLayerVideoGravityResize];
}
Here is what I do in Swift right after changing the playerLayer's frame:
playerLayer.removeAllAnimations()
I got this working in Swift 4 via the following:
var videoLayer = AVPlayerLayer()
CATransaction.begin()
CATransaction.setAnimationDuration(0)
CATransaction.setDisableActions(true)
var rect = videoLayer.bounds
rect.size.width /= 3
rect.size.height /= 3
videoLayer.bounds = rect
CATransaction.commit()
Probably this, what will help in some cases:
adding custom 'setFrame:' setter in view that holds the player layer
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
self.playerLayer.frame = CGRectMake(0.0f, 0.0f, frame.size.width, frame.size.height);
}