Swift: AVPlayer release memory / resources - ios

I am writing an app that need display different video according to the selection of the user. When the user select a video, the function playVideo will be called. And after the video finish playing, then the videoView will be hidden again.
My code is as follows:
var player: AVPlayer?
func playVideo(String: videoFile) {
self.videoView.isHidden = false
let videoURL: NSURL = Bundle.main.url(forResource: videoFile, withExtension: "mp4")! as NSURL
self.player = AVPlayer(url: videoURL as URL)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.videoView.frame
self.videoView.layer.addSublayer(playerLayer)
let duration : Int64 = 0
let preferredTimeScale : Int32 = 1
let seekTime : CMTime = CMTimeMake(duration, preferredTimeScale)
self.player?.seek(to: seekTime)
self.player?.play()
NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemDidReachEnd), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player?.currentItem)
}
#objc func playerItemDidReachEnd()
{
self.player?.pause()
self.videoView.isHidden = true
NotificationCenter.default.removeObserver(self)
}
However, with the code above, i have several question:
How to delete / deallocate the player gracefully? If just using my current code, will it consume lots of memory?
Every time, when the user press a button, the function playVideo will be called, and the corresponding player will be created and play. Is this the right way to do so? Is there any other method or more efficient way or elegant way to do so?
I did try to replace the code on creation of the player by the following, but it fails to play the video.
let playerItem: AVPlayerItem = AVPlayerItem(url: videoURL as URL)
self.player? = AVPlayer(playerItem: playerItem)
Thank you

Related

Play a video from url and validate if whole video was watched

I have this json and I have to play a video
What is the best way to play video and validate or check if user watched whole video?Video could be (Tiktok - Vimeo - Dayli Motion) video but no Youtube Video
I tried to use AVPlayer but it doesn't work :
let videoURL = NSURL(string: "https://vimeo.com/601097657")
let player = AVPlayer(url: videoURL! as URL)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
self.present(playerViewController, animated: true) {
playerViewController.player!.play()
}
I think the possible solution it could be a webView but I'm not sure if its possible to validate if user watched whole video
AVPlayer sends notifications on occasions like that.
Simply subscribe to notifications you need. In your case you need
NSNotification.Name.AVPlayerItemDidPlayToEndTime
implementing this would look something like this:
NotificationCenter.default.addObserver(self,
selector: #selector(itemDidPlayToEnd),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: nil)
And implement a selector for handling notification:
#objc private func itemDidPlayToEnd() {
// do smth
}
This is working for me.
class Player: AVPlayerViewController {
init(url: URL) {
super.init(nibName: nil, bundle: nil)
player = AVPlayer(url: url)
player?.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main, using: { time in
if self.player?.currentItem?.status == .readyToPlay {
let currenTime = CMTimeGetSeconds((self.player?.currentTime())!)
let secs = Int(currenTime)
print(NSString(format: "%02d:%02d", secs/60, secs%60) as String)
}
})
}
This will print out how much of the video they have watched in seconds, which you could then check if this is equal to the total amount of time the video is.
Obviously you would have to implement methods to check if they have skipped part of the video or not, but that's for another question.
Check out more info Time watched. and Total Video Time

How to loop videos with AVQueuePlayer after it completes

I have an array of URLs that I then turn into an array of AVPlayerItems and use AVQueuePlayer to loop through the videos- usually 1-7 videos at a time. However when it stops I am not sure how to start it again to play the same array of videos until the user switches to a different view controller.
in viewDidLoad this creates the array of playerItems
//creates playerItems to play videos in a queue
postURLs?.forEach{(url) in
let asset = AVAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
playerItems.append(playerItem)
}
public func playVideo() {
player = AVQueuePlayer(items: playerItems)
player.seek(to: CMTime.init(value: 0, timescale: 1))
playerLayer = AVPlayerLayer(player:player)
playerLayer.frame = self.lifieView.frame
lifieView.layer.addSublayer(playerLayer)
player.play()
//restart video maybe? Tested but did not work - hits function
NotificationCenter.default.addObserver(
forName: .AVPlayerItemDidPlayToEndTime,
object: nil,
queue: nil) { [weak self] _ in self?.restart2() }
}
//this is test function to restart (works with AVPlayer with single video)
private func restart2(){
player.seek(to: CMTime.zero)
player.play()
}
I got it working after much research and testing.
What I did was change the restart function to first remove all items from the player, then go through the array of playerItems and add them back into the queue- then have the player start back at the beginning.
func restartPlayer(){
player.removeAllItems()
playerItems.forEach{
player.insert($0, after:nil)
}
player.seek(to: .zero)
}

AVPlayer cannot service a synchronized playback request via setRate:time:atHostTime: until its status is AVPlayerStatusReadyToPlay

I am trying to write a rather simple iOS app synchronizing the playback of a video with AVPlayer (from AVFoundation) to audio playback from multiple AKPlayers (Audiokit 4.0), using the .setRate function of AVPlayer together with CMClockGetHostTimeClock() and mach_absolute_time().
For some reason all my attempts end in the error message
AVPlayer cannot service a synchronized playback request via setRate:time:atHostTime: until its status is AVPlayerStatusReadyToPlay.
Obviously I keep on missing something. The movie plays just fine if I replace everything starting from "let time..." with a simple
videoPlayer.play()
This is a minimal "ViewController.swift" to reproduce the error:
import UIKit
import AudioKit
import AVFoundation
class ViewController: UIViewController {
var videoPlayer = AVPlayer()
override func viewDidLoad() {
super.viewDidLoad()
guard let videopath = Bundle.main.url(forResource: "test", withExtension: "mov") else {
debugPrint("test.mov not found")
return
}
videoPlayer = AVPlayer(url: videopath)
videoPlayer.automaticallyWaitsToMinimizeStalling = false
let playerLayer = AVPlayerLayer(player: videoPlayer)
playerLayer.frame = self.view!.bounds
self.view!.layer.addSublayer(playerLayer)
let time: TimeInterval = 1 // 1 second in the future
videoPlayer.masterClock = CMClockGetHostTimeClock()
let hostTime = mach_absolute_time()
let cmHostTime = CMClockMakeHostTimeFromSystemUnits(hostTime)
let cmVTime = CMTimeMakeWithSeconds(time, preferredTimescale: videoPlayer.currentTime().timescale)
let futureTime = CMTimeAdd(cmHostTime, cmVTime)
videoPlayer.setRate(1, time: CMTime.invalid, atHostTime: futureTime)
}
}

Access variable inside if statement of function

I am trying to make a video play if a certain view is tapped once. The .play() function works well if I directly call it within an if statement inside a function (if which "checks" URL inside the setupPlayerView() function). The first functions (setupPlayerView and defInteractions) I am going to show below are going to be called in an override init which sets the properties and subviews/sublayers etc. for the video player. The last function is triggered by the second function . Pay attention to the declaration of player and my comment in the bottom...
Code: func #1
func setupPlayerView() {
//insert url
let urlString = "https://blurtime.com/images/testvideo.mov"
//check URL if can be converted to NSURL
if let videoURL = NSURL(string: urlString){
//player's video
let player = AVPlayer(url: videoURL as URL)
//add sub-layer
let playerLayer = AVPlayerLayer(player: player)
self.layer.addSublayer(playerLayer)
playerLayer.frame = self.frame
//when are frames actually rendered (when is video loaded)
player.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: nil)
//if I call player.play() here the video plays directly
}
}
As I the UITapGestureRecognizers to detect a single tap I have the following function which is called in the override init as well (shortly after the previous function):
Code: func #2
//set interactions
func defInteractions (){
//enable interaction
controlsContainerView.isUserInteractionEnabled = true
//singletap
let singleTap = UITapGestureRecognizer(target: self, action: #selector(singleTapDetected(_:)))
singleTap.numberOfTapsRequired = 1
//controlsContainerView
controlsContainerView.addGestureRecognizer(singleTap)
}
Now, I would like to call player.play() inside the function singleTapDetected which currently looks like this:
Code: func #3
func singleTapDetected(_ sender: UITapGestureRecognizer) {
player.play()
}
However, it does not work of course as this function is outside the override init as opposed to the others and I get the error use of unresolved identifier 'player'. How can I call player.play() and get the same result as if I would call it in the first function? Can I access it within the if? I could need some help...
Make player an instance variable:
var player: AVPlayer?
Then initialize it like this:
if self.player == nil {
player = AVPlayer(url: videoURL as URL)
}
Then you can easily access it from anywhere in your class.
Make the player property Global like this :
let player : AVPlayer?
Initialize it in setupPlayerView() :
func setupPlayerView() {
//insert url
let urlString = "https://blurtime.com/images/testvideo.mov"
//check URL if can be converted to NSURL
if let videoURL = NSURL(string: urlString){
//player's video
self.player = AVPlayer(url: videoURL as URL)
//add sub-layer
let playerLayer = AVPlayerLayer(player: player)
self.layer.addSublayer(playerLayer)
playerLayer.frame = self.frame
//when are frames actually rendered (when is video loaded)
player.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: nil)
//if I call player.play() here the video plays directly
}
}
and use it like this:
func singleTapDetected(_ sender: UITapGestureRecognizer) {
// with self
self.player.play()
}
Hope it helps
Declare player at the class level with optional.
var player: AVPlayer?
Instead of declaring player as a local variable in setupPlayerView function, you should declare it a an instance variable to be accessible in the scope of the whole class/struct.
For your case, I would suggest that instead of implementing setupPlayerView function, it would be a good practice to declare it as a lazy property , as follows:
lazy var player: AVPlayer? = {
//insert url
let urlString = "https://blurtime.com/images/testvideo.mov"
guard let videoURL = NSURL(string: urlString) else {
return nil
}
let player = AVPlayer(url: videoURL as URL)
//add sub-layer
let playerLayer = AVPlayerLayer(player: player)
self.layer.addSublayer(playerLayer)
playerLayer.frame = self.frame
//when are frames actually rendered (when is video loaded)
player.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: nil)
return player
}()
Usage:
// this means if the urlString is valid
if let player = player {
player.play()
}
That leads to: all the needed setup for player should be ready only if needed. You might want to check this Q&A.

How to stream a video with AVURLAsset and save to disk the cached data

Some days ago I was asked to check how difficult is to play a video while downloading it from Internet. I know it's an easy task because someone told me a while ago. So, I checked and it was super easy.
The problem was that I wanted to save to disk the video to do not force the user to download it again and again.
The problem was to access the buffer and store it to disk.
Many answers in Stackoverflow says it is nor possible. Specially with videos.
My original code to play the video:
import AVFoundation
....
//MARK: - Accessors
lazy var player: AVPlayer = {
var player: AVPlayer = AVPlayer(playerItem: self.playerItem)
player.actionAtItemEnd = AVPlayerActionAtItemEnd.None
return player
}()
lazy var playerItem: AVPlayerItem = {
var playerItem: AVPlayerItem = AVPlayerItem(asset: self.asset)
return playerItem
}()
lazy var asset: AVURLAsset = {
var asset: AVURLAsset = AVURLAsset(URL: self.url)
return asset
}()
lazy var playerLayer: AVPlayerLayer = {
var playerLayer: AVPlayerLayer = AVPlayerLayer(player: self.player)
playerLayer.frame = UIScreen.mainScreen().bounds
playerLayer.backgroundColor = UIColor.clearColor().CGColor
return playerLayer
}()
var url: NSURL = {
var url = NSURL(string: "https://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4")
return url!
}()
//MARK: - ViewLifeCycle
override func viewDidLoad() {
super.viewDidLoad()
view.layer.addSublayer(playerLayer)
player.play()
}
The solution for this problem is to use AVAssetExportSession and AVAssetResourceLoaderDelegate:
First step is to add a notification to know when the video finish. Then we can start saving it to disk.
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(playerItemDidReachEnd(_:)), name: AVPlayerItemDidPlayToEndTimeNotification, object: nil)
...
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
The implementation of our function:
func playerItemDidReachEnd(notification: NSNotification) {
if notification.object as? AVPlayerItem == player.currentItem {
let exporter = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality)
let filename = "filename.mp4"
let documentsDirectory = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.DocumentDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).last!
let outputURL = documentsDirectory.URLByAppendingPathComponent(filename)
exporter?.outputURL = outputURL
exporter?.outputFileType = AVFileTypeMPEG4
exporter?.exportAsynchronouslyWithCompletionHandler({
print(exporter?.status.rawValue)
print(exporter?.error)
})
}
}
Finally we need to make our AVURLAsset delegate of AVAssetResourceLoaderDelegate:
lazy var asset: AVURLAsset = {
var asset: AVURLAsset = AVURLAsset(URL: self.url)
asset.resourceLoader.setDelegate(self, queue: dispatch_get_main_queue())
return asset
}()
And:
extension ViewController : AVAssetResourceLoaderDelegate {
}
I created a small demo with this code in GitHub.
The team at Calm has open-sourced our implementation to this. It's available as a CocoaPod. It's called PersistentStreamPlayer.
Features include:
streaming of audio file, starting playback as soon as first data is available
also saves streamed data to a file URL as soon as the buffer completes
exposes timeBuffered, helpful for displaying buffer progress bars in the UI
handles re-starting the audio file after the buffer stream stalls (e.g. slow network)
simple play, pause and destroy methods (destroy clears all memory resources)
does not keep audio file data in memory, so that it supports large files that don't fit in RAM
You can find it here: https://github.com/calmcom/PersistentStreamPlayer

Resources