Is there a relatively easy way of looping a video in AVFoundation?
I've created my AVPlayer and AVPlayerLayer like so:
avPlayer = [[AVPlayer playerWithURL:videoUrl] retain];
avPlayerLayer = [[AVPlayerLayer playerLayerWithPlayer:avPlayer] retain];开发者_如何转开发
avPlayerLayer.frame = contentView.layer.bounds;
[contentView.layer addSublayer: avPlayerLayer];
and then I play my video with:
[avPlayer play];
The video plays fine but stops at the end. With the MPMoviePlayerController all you have to do is set its repeatMode
property to the right value. There doesn't appear to be a similar property on AVPlayer. There also doesn't seem to be a callback that will tell me when the movie has finished so I can seek to the beginning and play it again.
I'm not using MPMoviePlayerController because it has some serious limitations. I want to be able to play back multiple video streams at once.
You can get a Notification when the player ends. Check AVPlayerItemDidPlayToEndTimeNotification
When setting up the player:
ObjC
avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playerItemDidReachEnd:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:[avPlayer currentItem]];
this will prevent the player to pause at the end.
in the notification:
- (void)playerItemDidReachEnd:(NSNotification *)notification {
AVPlayerItem *p = [notification object];
[p seekToTime:kCMTimeZero];
}
this will rewind the movie.
Don't forget un unregister the notification when releasing the player.
Swift
avPlayer?.actionAtItemEnd = .none
NotificationCenter.default.addObserver(self,
selector: #selector(playerItemDidReachEnd(notification:)),
name: .AVPlayerItemDidPlayToEndTime,
object: avPlayer?.currentItem)
@objc func playerItemDidReachEnd(notification: Notification) {
if let playerItem = notification.object as? AVPlayerItem {
playerItem.seek(to: kCMTimeZero)
}
}
Swift 4+
@objc func playerItemDidReachEnd(notification: Notification) {
if let playerItem = notification.object as? AVPlayerItem {
playerItem.seek(to: CMTime.zero, completionHandler: nil)
}
}
In iOS / tvOS 10, there's a new AVPlayerLooper() that you can use to create seamless looping of video (Swift):
player = AVQueuePlayer()
playerLayer = AVPlayerLayer(player: player)
playerItem = AVPlayerItem(url: videoURL)
playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)
player.play()
This was presented at WWDC 2016 in "Advances in AVFoundation Playback": https://developer.apple.com/videos/play/wwdc2016/503/
Even using this code, I had a hiccup until I filed a bug report with Apple and got this response:
The movie file having movie duration longer than audio/video tracks is the problem. FigPlayer_File is disabling gapless transition because audio track edit is shorter than the movie duration (15.682 vs 15.787).
You need to either fix the movie files to have the movie duration and track durations to be same length or you can use the time range parameter of AVPlayerLooper (set time range from 0 to duration of audio track)
It turns out that Premiere had been exporting files with an audio track of a slightly different length than the video. In my case it was fine to remove the audio entirely, and that fixed the problem.
In Swift:
You can get a Notification when the player ends... check AVPlayerItemDidPlayToEndTimeNotification
when setting up the player:
avPlayer.actionAtItemEnd = AVPlayerActionAtItemEnd.None
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "playerItemDidReachEnd:",
name: AVPlayerItemDidPlayToEndTimeNotification,
object: avPlayer.currentItem)
this will prevent the player to pause at the end.
in the notification:
func playerItemDidReachEnd(notification: NSNotification) {
if let playerItem: AVPlayerItem = notification.object as? AVPlayerItem {
playerItem.seekToTime(kCMTimeZero)
}
}
Swift3
NotificationCenter.default.addObserver(self,
selector: #selector(PlaylistViewController.playerItemDidReachEnd),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: avPlayer?.currentItem)
this will rewind the movie.
Do not forget to unregister the notification when releasing the player.
Here's what I ended up doing to prevent the pause-hiccup issue:
Swift:
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime,
object: nil,
queue: nil) { [weak self] note in
self?.avPlayer.seek(to: kCMTimeZero)
self?.avPlayer.play()
}
Objective C:
__weak typeof(self) weakSelf = self; // prevent memory cycle
NSNotificationCenter *noteCenter = [NSNotificationCenter defaultCenter];
[noteCenter addObserverForName:AVPlayerItemDidPlayToEndTimeNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
[weakSelf.avPlayer seekToTime:kCMTimeZero];
[weakSelf.avPlayer play];
}];
NOTE: I didn't use avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone
as it's not needed.
Swift 5:
I've made some slight adjustments from previous answers like adding the playerItem to the queue before add it to the playerLayer.
let playerItem = AVPlayerItem(url: url)
let player = AVQueuePlayer(playerItem: playerItem)
let playerLayer = AVPlayerLayer(player: player)
playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)
playerLayer.frame = cell.eventImage.bounds
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
// Add the playerLayer to a UIView.layer
player.play()
And make playerLooper a property of your UIViewController, otherwise the video may only play once.
I recommend using AVQueuePlayer to loop your videos seamlessly. Add the notification observer
AVPlayerItemDidPlayToEndTimeNotification
and in its selector, loop your video
AVPlayerItem *video = [[AVPlayerItem alloc] initWithURL:videoURL];
[self.player insertItem:video afterItem:nil];
[self.player play];
To avoid the gap when the video is rewound, using multiple copies of the same asset in a composition worked well for me. I found it here: www.developers-life.com/avplayer-looping-video-without-hiccupdelays.html (link now dead).
AVURLAsset *tAsset = [AVURLAsset assetWithURL:tURL];
CMTimeRange tEditRange = CMTimeRangeMake(CMTimeMake(0, 1), CMTimeMake(tAsset.duration.value, tAsset.duration.timescale));
AVMutableComposition *tComposition = [[[AVMutableComposition alloc] init] autorelease];
for (int i = 0; i < 100; i++) { // Insert some copies.
[tComposition insertTimeRange:tEditRange ofAsset:tAsset atTime:tComposition.duration error:nil];
}
AVPlayerItem *tAVPlayerItem = [[AVPlayerItem alloc] initWithAsset:tComposition];
AVPlayer *tAVPlayer = [[AVPlayer alloc] initWithPlayerItem:tAVPlayerItem];
SWIFT 5:
private var player: AVPlayer?
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(restartVideo),
name: .AVPlayerItemDidPlayToEndTime,
object: self.player?.currentItem)
}
@objc func restartVideo() {
player?.pause()
player?.currentItem?.seek(to: CMTime.zero, completionHandler: { _ in
self.player?.play()
})
}
this worked for me without hiccup issues, point is in pausing the player before calling seekToTime method:
init AVPlayer
let url = NSBundle.mainBundle().URLForResource("loop", withExtension: "mp4") let playerItem = AVPlayerItem(URL: url!) self.backgroundPlayer = AVPlayer(playerItem: playerItem) let playerLayer = AVPlayerLayer(player: self.backgroundPlayer) playerLayer.frame = CGRectMake(0, 0, UIScreen.mainScreen().bounds.width, UIScreen.mainScreen().bounds.height) self.layer.addSublayer(playerLayer) self.backgroundPlayer!.actionAtItemEnd = .None self.backgroundPlayer!.play()
registering notification
NSNotificationCenter.defaultCenter().addObserver(self, selector: "videoLoop", name: AVPlayerItemDidPlayToEndTimeNotification, object: self.backgroundPlayer!.currentItem)
videoLoop function
func videoLoop() { self.backgroundPlayer?.pause() self.backgroundPlayer?.currentItem?.seekToTime(kCMTimeZero) self.backgroundPlayer?.play() }
my solution in objective-c wth AVQueuePlayer - it seems you have to duplicate the AVPlayerItem and upon finishing playback of first element instantly add another copy. "Kind of" makes sense and works for me without any hiccup
NSURL *videoLoopUrl;
// as [[NSBundle mainBundle] URLForResource:@"assets/yourVideo" withExtension:@"mp4"]];
AVQueuePlayer *_loopVideoPlayer;
+(void) nextVideoInstance:(NSNotification*)notif
{
AVPlayerItem *currItem = [AVPlayerItem playerItemWithURL: videoLoopUrl];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(nextVideoInstance:)
name:AVPlayerItemDidPlayToEndTimeNotification
object: currItem];
[_loopVideoPlayer insertItem:currItem afterItem:nil];
[_loopVideoPlayer advanceToNextItem];
}
+(void) initVideoPlayer {
videoCopy1 = [AVPlayerItem playerItemWithURL: videoLoopUrl];
videoCopy2 = [AVPlayerItem playerItemWithURL: videoLoopUrl];
NSArray <AVPlayerItem *> *dummyArray = [NSArray arrayWithObjects: videoCopy1, videoCopy2, nil];
_loopVideoPlayer = [AVQueuePlayer queuePlayerWithItems: dummyArray];
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(nextVideoInstance:)
name: AVPlayerItemDidPlayToEndTimeNotification
object: videoCopy1];
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(nextVideoInstance:)
name: AVPlayerItemDidPlayToEndTimeNotification
object: videoCopy2];
}
https://gist.github.com/neonm3/06c3b5c911fdd3ca7c7800dccf7202ad
For Swift 3 & 4
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.avPlayer?.currentItem, queue: .main) { _ in
self.avPlayer?.seek(to: kCMTimeZero)
self.avPlayer?.play()
}
I couldn't find my solution among answers. It can be helpful to observe boundary time specified to asset duration. When observer is triggered, seek to the start and replay.
player?.addBoundaryTimeObserver(forTimes: [NSValue(time: asset.duration)], queue: .main) { [weak self] in
self?.player?.seek(to: .zero, completionHandler: { [weak self] _ in
self?.player?.play()
})
}
After loading the video into the AVPlayer (via its AVPlayerItem, of course):
[self addDidPlayToEndTimeNotificationForPlayerItem:item];
The addDidPlayToEndTimeNotificationForPlayerItem method:
- (void)addDidPlayToEndTimeNotificationForPlayerItem:(AVPlayerItem *)item
{
if (_notificationToken)
_notificationToken = nil;
/*
Setting actionAtItemEnd to None prevents the movie from getting paused at item end. A very simplistic, and not gapless, looped playback.
*/
_player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
_notificationToken = [[NSNotificationCenter defaultCenter] addObserverForName:AVPlayerItemDidPlayToEndTimeNotification object:item queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
// Simple item playback rewind.
[[_player currentItem] seekToTime:kCMTimeZero];
}];
}
In your viewWillDisappear method:
if (_notificationToken) {
[[NSNotificationCenter defaultCenter] removeObserver:_notificationToken name:AVPlayerItemDidPlayToEndTimeNotification object:_player.currentItem];
_notificationToken = nil;
}
In your view controller's interface declaration within the implementation file:
id _notificationToken;
Need to see this up-and-running before you try? Download and run this sample app:
https://developer.apple.com/library/prerelease/ios/samplecode/AVBasicVideoOutput/Listings/AVBasicVideoOutput_APLViewController_m.html#//apple_ref/doc/uid/DTS40013109-AVBasicVideoOutput_APLViewController_m-DontLinkElementID_8
In my app, which uses this very code, there is no pause whatsoever between the end of the video and the beginning. In fact, depending on the video, there's no way for me to tell the video is at the beginning again, save the timecode display.
you can add a AVPlayerItemDidPlayToEndTimeNotification observer and replay video from start in selector, code like below
//add observer
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackFinished:) name:AVPlayerItemDidPlayToEndTimeNotification
object:_aniPlayer.currentItem];
-(void)playbackFinished:(NSNotification *)notification{
[_aniPlayer seekToTime:CMTimeMake(0, 1)];//replay from start
[_aniPlayer play];
}
The following is working for me in WKWebView in swift 4.1 The main part of the WKWebView in WKwebviewConfiguration
wkwebView.navigationDelegate = self
wkwebView.allowsBackForwardNavigationGestures = true
self.wkwebView = WKWebView(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height))
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
wkwebView = WKWebView(frame: wkwebView.frame, configuration: config)
self.view.addSubview(wkwebView)
self.wkwebView.load(NSURLRequest(url: URL(string: self.getUrl())!) as URLRequest)
What I did is to make it loop playing, like my code below:
[player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0)
queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
float current = CMTimeGetSeconds(time);
float total = CMTimeGetSeconds([playerItem duration]);
if (current >= total) {
[[self.player currentItem] seekToTime:kCMTimeZero];
[self.player play];
}
}];
Swift 4.2 in Xcode 10.1.
Yes, there is a relatively easy way of looping a video in AVKit
/AVFoundation
using AVQueuePlayer()
, Key-Value Observation (KVO) technique and a token for it.
This definitely works for a bunch of H.264/HEVC videos with a minimal burden for CPU.
Here's a code:
import UIKit
import AVFoundation
import AVKit
class ViewController: UIViewController {
private let player = AVQueuePlayer()
let clips = ["01", "02", "03", "04", "05", "06", "07"]
private var token: NSKeyValueObservation?
var avPlayerView = AVPlayerViewController()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
self.addAllVideosToPlayer()
present(avPlayerView, animated: true, completion: { self.player.play() })
}
func addAllVideosToPlayer() {
avPlayerView.player = player
for clip in clips {
let urlPath = Bundle.main.path(forResource: clip, ofType: "m4v")!
let url = URL(fileURLWithPath: urlPath)
let playerItem = AVPlayerItem(url: url)
player.insert(playerItem, after: player.items().last)
token = player.observe(\.currentItem) { [weak self] player, _ in
if self!.player.items().count == 1 { self?.addAllVideosToPlayer() }
}
}
}
}
Swift 5
import UIKit
import AVKit
import AVFoundation
class VideoViewControler: UIViewController {
// init video background and its path
var player: AVPlayer?
let videoURL: NSURL = Bundle.main.url(forResource: "farmer_watering", withExtension: "mp4")! as NSURL
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// begin implementing the avplayer
player = AVPlayer(url: videoURL as URL)
player?.actionAtItemEnd = .none
player?.isMuted = true
let playerLayer = AVPlayerLayer(player: player)
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspect
playerLayer.zPosition = -1
playerLayer.frame = view.frame
view.layer.addSublayer(playerLayer)
player?.play()
// add observer to watch for video end in order to loop video
NotificationCenter.default.addObserver(
self,
selector: #selector(loopVideo),
name: .AVPlayerItemDidPlayToEndTime,
object: self.player?.currentItem
)
}
// if video ends, will restart
func playerItemDidReachEnd() {
player?.seek(to: CMTime.zero)
}
// add this loop at the end, after viewDidLoad
@objc func loopVideo() {
playerItemDidReachEnd()
player?.play()
}
}
SwiftUI example:
// VideoLooper.swift
import SwiftUI
import AVKit
struct VideoLooper: UIViewRepresentable {
private let player: AVQueuePlayer
private let videoURL: URL
init(resourceInBundle name: String, ofType type: String = "mp4") {
self.init(url: URL(fileURLWithPath: Bundle.main.path(forResource: name, ofType: type)!))
}
init(url: URL) {
self.videoURL = url
self.player = AVQueuePlayer()
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<VideoLooper>) {
}
func makeUIView(context: Context) -> UIView {
return PlayerUIView(player: player, videoURL: videoURL)
}
}
class PlayerUIView: UIView {
private let playerLayer: AVPlayerLayer
private let playerItem: AVPlayerItem
private let playerLooper: AVPlayerLooper
init(player: AVQueuePlayer, videoURL: URL) {
playerLayer = AVPlayerLayer(player: player)
playerItem = AVPlayerItem(url: videoURL)
playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)
super.init(frame: .zero)
playerLayer.player = player
layer.addSublayer(playerLayer)
player.play()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
Usage:
// Video-Resource in Bundle
VideoLooper(resourceInBundle: "moviename", ofType: "mp4")
// Video-Resource from URL
VideoLooper(url: URL(string: "https://my-video-server.local/christmas.mp4")!)
use AVPlayerViewController below code, its working for me
let type : String! = "mp4"
let targetURL : String? = NSBundle.mainBundle().pathForResource("Official Apple MacBook Air Video YouTube", ofType: "mp4")
let videoURL = NSURL(fileURLWithPath:targetURL!)
let player = AVPlayer(URL: videoURL)
let playerController = AVPlayerViewController()
playerController.player = player
self.addChildViewController(playerController)
self.playView.addSubview(playerController.view)
playerController.view.frame = playView.bounds
player.play()
All controls to be showed, hope its helpful
/* "numberOfLoops" is the number of times that the sound will return to the beginning upon reaching the end.
A value of zero means to play the sound just once.
A value of one will result in playing the sound twice, and so on..
Any negative number will loop indefinitely until stopped.
*/
@property NSInteger numberOfLoops;
This property is already defined inside AVAudioPlayer
. Hope this can help you.
I'm using Xcode 6.3.
精彩评论