AVMutableComposition: small jump between video segments when playing - ios

I'm using AVMutableComposition to play a video which is split in different continuous files. However, when playing, there is a small jump (short black screen) between 2 consecutive segments.
Is there any way to prevent that?
import UIKit
import AVFoundation
class KLMasterPlayerViewController: KLPlayerViewController {
let comp:AVMutableComposition = AVMutableComposition()
var playerItem:AVPlayerItem!
init() {
super.init(nibName: nil, bundle: nil)
self.initComp()
self.playerItem = AVPlayerItem(asset: self.comp)
self.player = AVPlayer(playerItem: self.playerItem)
}
private func initComp() {
let segments = MasterVideo.sharedInstance.videoSegments
var insertedTime:Double = 0.0
for segment in segments {
do {
let asset = segment.getURLAsset()
try comp.insertTimeRange(CMTimeRangeMake(kCMTimeZero,asset!.duration), ofAsset: asset!, atTime: CMTimeMake(Int64(insertedTime * 10000), 10000))
insertedTime += segment.getDuration()
} catch {
}
}
}
}

I've found the answer: Swift wasn't calculating the duration of my video assets properly. For each one of them, there was a different of 1 image.
By forcing the video segments duration, that was fine and flawless.
Glad it's solved!
Many thanks to vaibhav for trying to help.

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

How to control AVPlayer buffering

I'm using an AVPlayer to play a remote progressive download (i.e. non-HLS) video. But, I can't figure out how to control its buffering behavior.
I would like to pre-fetch 2 seconds of the video before it's ready to play, and also to stop buffering when the video is paused.
Here's my setup:
let asset = AVURLAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
let player = AVPlayer()
I tried the following, without success:
// doesn't start buffering
playerItem.preferredForwardBufferDuration = 2.0
// doesn't stop buffering
playerItem.preferredForwardBufferDuration = 2.0
player.replaceCurrentItem(with: playerItem)
I tried player.automaticallyWaitsToMinimizeStalling = true in both cases, and in combination with various player.pause() or player.rate = 0 - doesn't work.
A potential approach that comes to mind is to observe for loadedTimeRanges until the first 2 seconds loaded and set current item of the player to nil.
let c = playerItem.publisher(for: \.loadedTimeRanges, options: .new)
.compactMap { $0.first as? CMTimeRange }
.sink {
if $0.duration.seconds - $0.start.seconds > 2 {
player.replaceCurrentItem(with: nil)
}
}
This would work for pre-buffer, but it doesn't work for pausing, because it makes the video blank instead of paused. (And at this point, I feel I'm attempting to reimplement/interfere with some core buffering functionality)

How to tell when AVPlayer has been played for three seconds Swift

I have an application that contains videos that play automatically in an UIImageView in a UITableView when the cell is visible, and all I am trying to do is allow the application to know when the video has been played for three seconds. I wrote this code.
class PostCell: UITableViewCell {
var player: AVPlayer?
var playerLayer: AVPlayerLayer?
var post: Post? {
didSet {
updateView()
}
}
func updateView() {
self.viewcount()
if let videoUrlString = post?.videoUrl, let videoUrl = URL(string: videoUrlString) {
player = AVPlayer(url: videoUrl)
playerLayer = AVPlayerLayer(player: player)
playerLayer?.frame = postImageView.frame
playerLayer?.frame.size.width = postImageView.frame.size.width
playerLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
self.contentView.layer.addSublayer(playerLayer!)
player?.play()
}
func viewcount() {
if let currentitem = player?.currentItem {
if currentitem.currentTime() == CMTimeMake(3, 1) {
print ("VIDEO PLAYED FOR THREE SECONDS")
}
}
}
}
but it is not printing out my message once the video starts playing. I have searched the web for help but couldn't find anything on this subject. So could anyone please help with my issue and tell me what I am doing wrong ?
You are searching for observer of player here is how you can check and track the current position of AVPlayer
Here is function that is adding observer to cell
private func addObserversForVideoPlayer(cell:CustomCell) {
let observer = cell.player?.addPeriodicTimeObserver(forInterval: CMTime.init(seconds: 1, preferredTimescale: 1), queue: .main, using: {[weak self,weak cell] (time) in
guard let cell = cell else {return}
if cell.player?.currentItem?.status == .readyToPlay {
// print("Inside Will DISPLAY\(cell.video.currentTime)")
let timeDuration : Float64 = CMTimeGetSeconds((cell.player?.currentItem?.asset.duration)!)
cell.lblDuration.text = self?.getDurationFromTime(time: timeDuration)
let currentTime : Float64 = CMTimeGetSeconds((cell.player?.currentTime())!)
cell.lblStart.text = self?.getDurationFromTime(time: currentTime)
cell.slider.maximumValue = Float(timeDuration.rounded())
cell.slider.value = Float(currentTime.rounded())
}
})
NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: cell.player?.currentItem, queue: .main, using: {[weak cell,weak self] (notification) in
if cell?.player != nil {
cell?.player?.seek(to: kCMTimeZero)
cell?.player?.play()
}
})
}
so that addPeriodicTimeObserver will notify you when the player start playing.
And NSNotification.Name.AVPlayerItemDidPlayToEndTime will notify you when your AVPlayer stops.
Note1: If your cell.player?.currentItem is nil while you are adding AVPlayerItemDidPlayToEndTime it will be cause bug see this One AVPlayer's AVPlayerItemDidPlayToEndTime action executed for all Currently playing videos , If . you don't need it don't add it :)
Note2: You should keep observer so after time you can remove it so that can not take extra load on memory
Hope it is helpful
Try calling the view count after player had started playing
func updateView() {
/// Not here Because at this time player current item is not initiated yet
/// if you use Breakpoints in viewCount code you will see it won't enter
/// in if condition created
self.viewcount() /// Comment this line
if let videoUrlString = post?.videoUrl, let videoUrl = URL(string: videoUrlString) {
player = AVPlayer(url: videoUrl)
playerLayer = AVPlayerLayer(player: player)
playerLayer?.frame = postImageView.frame
playerLayer?.frame.size.width = postImageView.frame.size.width
playerLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
self.contentView.layer.addSublayer(playerLayer!)
/// Player is initiated with a item to play
player?.play()
/// Call current time here
/// Now it will Enter in if Condition
/// Also try using else statement so you know Do control enter in if or in Else
self.viewcount()
}
func viewcount()
{
if let currentitem = player?.currentItem
{
///Yes Player have a item whose time can be Detected
if currentitem.currentTime() == CMTimeMake(3, 1)
{
print ("VIDEO PLAYED FOR THREE SECONDS")
}
}
else
{
/// Check do Control reach here in case 1 When you are calling before player.play()
}
}

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