使用AVFoundation AVPlayer循环播放视频?

152

有没有一种相对简单的方法在AVFoundation中循环播放视频?

我已经这样创建了我的AVPlayer和AVPlayerLayer:

avPlayer = [[AVPlayer playerWithURL:videoUrl] retain];
avPlayerLayer = [[AVPlayerLayer playerLayerWithPlayer:avPlayer] retain];

avPlayerLayer.frame = contentView.layer.bounds;
[contentView.layer addSublayer: avPlayerLayer];

然后我使用以下代码播放视频:

[avPlayer play];
视频播放正常,但在结尾处停止。对于MPMoviePlayerController而言,你只需将其repeatMode属性设置为正确的值即可。AVPlayer似乎没有类似的属性。也似乎没有回调可以告诉我什么时候电影已经播放完毕,以便我可以跳到开头并再次播放。
我不使用MPMoviePlayerController,因为它有一些严重的限制。我想能够同时播放多个视频流。

1
请查看此答案,其中包含实际可工作的代码链接:https://dev59.com/61zUa4cB1Zd3GeqP2mCV - MoDJ
22个回答

287

当播放器结束时,您可以获得通知。请检查AVPlayerItemDidPlayToEndTimeNotification

设置播放器时:

ObjC

  avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone; 

  [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(playerItemDidReachEnd:)
                                               name:AVPlayerItemDidPlayToEndTimeNotification
                                             object:[avPlayer currentItem]];

这将防止播放器在结尾时暂停。

在通知中:

- (void)playerItemDidReachEnd:(NSNotification *)notification {
    AVPlayerItem *p = [notification object];
    [p seekToTime:kCMTimeZero];
}

这将倒回电影。

在释放播放器时不要忘记取消注册通知。

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)
    }
}

6
如果你想在[p seekToTime:kCMTimeZero](一种“倒带”)之后立即播放它,只需再次执行[p play]即可。 - thomax
26
如果您执行 avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;,就不需要这样做了,因为它不会停止,所以无需设置它再次播放。 - Bastian
13
这个解决方案可以运行,但不是完全无缝的。我有一个非常小的暂停。我做错了什么吗? - Joris van Liempd iDeveloper
这个答案看起来很棒,但我发现我的 currentItem 在播放后会被释放。因此,即使我执行 [p seekToTime:kCMTimeZero]; ,我的 AVPlayer 也无法再次播放该项目。我该如何保留我的 player.currentItem - Brian
3
@Praxiteles 当视图被销毁时,或者当您删除视频播放器或执行其他操作时,您需要注销它。例如,当self正在监听通知时,您可以使用[[NSNotificationCenter defaultCenter] removeObserver:self]; - Bastian
显示剩余10条评论

83
在iOS / tvOS 10中,有一个新的AVPlayerLooper()可用于创建无缝循环播放视频(Swift):

在iOS / tvOS 10中,有一个新的AVPlayerLooper()可用于创建无缝循环播放视频(Swift):

player = AVQueuePlayer()
playerLayer = AVPlayerLayer(player: player)
playerItem = AVPlayerItem(url: videoURL)
playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)
player.play()    

这是在2016年WWDC“AVFoundation播放的进展”报告中提出的:https://developer.apple.com/videos/play/wwdc2016/503/

即使使用了这段代码,我仍然遇到了麻烦,直到我向苹果公司提交错误报告并得到以下回复:

电影文件的电影时长比音频/视频轨道长是问题所在。FigPlayer_File禁用无缝过渡,因为音频轨道编辑的时间比电影时长短(15.682对15.787)。

您需要修复电影文件,使电影持续时间和轨道持续时间长度相同,或者您可以使用AVPlayerLooper的时间范围参数(将时间范围从0设置为音频轨道的持续时间)

事实证明,Premiere导出的文件的音频轨道与视频略有不同的长度。在我的情况下,完全删除音频是可行的,这就解决了问题。


4
其他方法对我都没用。我正在使用AVPlayerLooper,遇到了这个问题,调整视频和音频长度的不一致解决了这个问题。 - Kevin Heap
1
谢谢您提供有关Premiere的信息。我向循环器添加了一个timeRange,这解决了我的“闪烁视频”问题。 - Alexander Flenniken
1
@Nabha 这个能不能在视频的某个时间范围内使用?例如,视频长度为60秒,但我只想循环播放前10秒。 - Lance Samaria
@LanceSamaria 看起来是这样!init接受一个时间范围来循环播放,请参见:https://developer.apple.com/documentation/avfoundation/avplayerlooper/1643626-init - Nabha Cosley
@Nabha 感谢你的帮助,非常感激。干杯! - Lance Samaria

28

In Swift:

当播放器结束时,你可以通过检查 AVPlayerItemDidPlayToEndTimeNotification 来获取通知。

在设置播放器时:

avPlayer.actionAtItemEnd = AVPlayerActionAtItemEnd.None

NSNotificationCenter.defaultCenter().addObserver(self, 
                                                 selector: "playerItemDidReachEnd:", 
                                                 name: AVPlayerItemDidPlayToEndTimeNotification, 
                                                 object: avPlayer.currentItem)
这将防止播放器在结束时暂停。
在通知中:
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)

这将倒带电影。

释放播放器时不要忘记注销通知。


5
我发现在使用这种方法时,循环之间出现了小问题。我在Adobe Premiere中打开了我的视频并确认视频中没有重复的帧,所以这个短暂的问题肯定是在播放时出现的。有人找到了一种在Swift中使视频无缝循环的方法吗? - SpaceManGalaxy
@SpaceManGalaxy,我也注意到了这个小故障。你找到解决方法了吗? - Lance Samaria

21

这是我最终采取的措施,以防止暂停-打嗝问题:

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];
                    }];

注意:我没有使用avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone,因为不需要。



2
@KostiaDombrovsky 你是在实际设备上尝试过还是不同的视频? - Islam
@IslamQ。我录制了一个MP4文件,然后尝试像Snapchat一样循环播放它。 - Kostia Dombrovsky
@KostiaDombrovsky 你有没有将你的播放与 Snapchat 并排比较过?我认为由于开头和结尾的帧不匹配,它看起来像是暂停了,但实际上并没有暂停。 - Islam
我也试过了,对我也没用。我的视频长度为6秒,音频是连续的,但是使用这种方法仍然会听到一瞬间的静音。 - Cbas
我在使用这种方法时发现了内存泄漏问题。这与[weakSelf.avPlayer seekToTime:kCMTimeZero]; [weakSelf.avPlayer play];这两行代码有关 - 当我注释掉这些代码时,就不再出现内存泄漏了。我已经在Instruments中进行了分析。 - Solsma Dev

10

Swift 5:

我对之前的回答进行了微小的调整,例如在将playerItem添加到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()

将playerLooper作为您的UIViewController的属性,否则视频可能仅播放一次。


4

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()
    })
}

3
为了避免视频倒带时出现间隙,我建议在合成中使用多个相同资产的副本。我在这里找到了解决方法:www.developers-life.com/avplayer-looping-video-without-hiccupdelays.html(链接已失效)。
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];

我猜你指的是这个链接 http://devbrief.blogspot.se/2011/12/avplayer-looping-video-without.html?m=1 - flame3

3
我推荐使用 AVQueuePlayer 来无缝循环播放视频。添加通知观察器。
AVPlayerItemDidPlayToEndTimeNotification

在选择器中,循环播放您的视频

AVPlayerItem *video = [[AVPlayerItem alloc] initWithURL:videoURL];
[self.player insertItem:video afterItem:nil];
[self.player play];

我尝试了这个方法,但是它并没有比@Bastian建议的方法有任何改进。你能够完全解决这个问题吗? - amadour
2
@amadour 你可以做的是在初始化 AVQueuePlayer 播放器时添加两个相同的视频,当播放器发布 AVPlayerItemDidPlayToEndTimeNotification 通知时,将相同的视频添加到播放器的队列中。 - kevinnguy

1
这对我来说很顺利,没有问题,关键在于在调用seekToTime方法之前暂停播放器:
  1. 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()
    
  2. registering notification

    NSNotificationCenter.defaultCenter().addObserver(self, selector: "videoLoop", name: AVPlayerItemDidPlayToEndTimeNotification, object: self.backgroundPlayer!.currentItem)
    
  3. videoLoop function

    func videoLoop() {
      self.backgroundPlayer?.pause()
      self.backgroundPlayer?.currentItem?.seekToTime(kCMTimeZero)
      self.backgroundPlayer?.play()
    }
    

5
谢谢 - 我尝试了这个方法,但对我来说还是有一些停顿。 - Nabha Cosley

1

我的解决方案是使用AVQueuePlayer在Objective-C中 - 似乎你需要复制AVPlayerItem,并在第一个元素播放完毕后立即添加另一个副本。这“有点”合理,对我来说没有任何问题。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接