AVPlayer not starting until cell is recycled - swift

I have a UITableView that I am using to render out tweets that contain gifs. These are returned from Twitter as MP4's so I am using AVPlayer. Using HanekeSwift I am caching the item and then playing it in the tableview. The issue I am having is that any Tweets on screen when the tableview load, do not auto play, I must scroll them off screen and back on before they play.
I configure my AVPlayer in the following function
private func configureAVPlayer(url: URL) {
cache.fetch(URL: url).onSuccess { [weak self] stream in
guard let path = URL(string: DiskCache.basePath())?.appendingPathComponent("shared-data/original") else { return }
let cached = DiskCache(path: path.absoluteString).path(forKey: url.absoluteString)
let file = URL(fileURLWithPath: cached)
if !FileManager.default.fileExists(atPath: cached) {
try! stream.write(to: file, options: .atomicWrite)
}
self?.player = AVPlayer(url: file)
self?.player?.automaticallyWaitsToMinimizeStalling = false
self?.playerLayer = self?.avPlayerView.layer as? AVPlayerLayer
guard let player = self?.player else { return }
self?.playerLayer?.player = player
self?.player?.play()
}
}
Following something very similar as described here
However for me, the cell is essentially paused until I scroll.

Related

Problem trying to load a set of image using an array of UIImage to slide them

I'm trying to load a set of pictures to an imageView so that they slide alon with a time timer.
My problem is that I have the pictures store in Firebase with their URL as Strings. So I use a function to load a single image throug its URL into an imageView and it works. But now I need to find a way to use a function to put a set of images into the imageView so that the slide with a timer and I donĀ“t now how to do it.
This is the code to load the images from URL
import UIKit
extension UIImageView {
func loadFrom(URLAddress: String) {
guard let url = URL(string: URLAddress) else {
return
}
DispatchQueue.main.async { [weak self] in
if let imageData = try? Data(contentsOf: url) {
if let loadedImage = UIImage(data: imageData) {
self?.image = loadedImage
}
}
}
}
}
Here is the code to load the set of image as an UIImage array.
override func viewWillAppear(_ animated: Bool) {
let even = eventos.eventoPulsado()
var images = [UIImage]()
print(even.imagenes!)
//I load the picture this way
imgFoto.loadFrom(URLAddress: (even.imagenes![0]))
//But I cannot to append my images because I need UIImage data
images.append(imgFoto.image!)
for img in (even.imagenes!) {
imgFoto.loadFrom(URLAddress: (img))
}
imgFoto.animationImages = images
imgFoto.animationDuration = TimeInterval(0.4)
imgFoto.animationRepeatCount = 4
imgFoto.startAnimating()
}

collectionView cell Image change when scrolling - swift - programmatically

I need to load an ImageView inside UIcollectionViewcell using a URL that I pass during initialisation:
func configureCellWith(messageModel : MessageModel){
guard let url = URL(string: messageModel.contentUrl!) else { return }
if url.isURLPhoto(){
likedImageView.sd_setImage(with: url, placeholderImage: nil)
}
else if url.isURLVideo(){
getThumbnailImageFromVideoUrl(url: url) { (image) in
self.likedImageView.image = image
}
}
If url is video I need to load the image in this way using this method:
func getThumbnailImageFromVideoUrl(url: URL, completion: #escaping ((_ image: UIImage?)->Void)) {
DispatchQueue.global().async {
let asset = AVAsset(url: url)
let avAssetImageGenerator = AVAssetImageGenerator(asset: asset)
avAssetImageGenerator.appliesPreferredTrackTransform = true
let thumnailTime = CMTimeMake(value: 2, timescale: 1)
do {
let cgThumbImage = try avAssetImageGenerator.copyCGImage(at: thumnailTime, actualTime: nil)
let thumbNailImage = UIImage(cgImage: cgThumbImage)
DispatchQueue.main.async {
completion(thumbNailImage)
}
} catch {
print(error.localizedDescription)
DispatchQueue.main.async {
completion(nil)
}
}
}
}
As visible I retrieve the initial frame of the video and I load it inside the cell, obviously since it's an asynchronous function it will take some time for loading the image, there's no problem In that.
The problem occurs when I scroll through the collection and I see that some cells display images which don't correspond to the correct ones.
Searching online I found out that I need to clear the image in prepareForReuse of the cell and so I did (both in case the image is loaded through sd_setImage and though getThumbnailImageFromVideoUrl function):
override func prepareForReuse() {
super.prepareForReuse()
self.likedImageView.image = UIImage()
self.likedImageView.image = nil
self.likedImageView.sd_cancelCurrentImageLoad()
}
but I still get images mismatched when scrolling thought the collection view, what could be the problem?
I think the issue is not with images, i guess its with video thumbnail. You generate a thumbnail on background thread synchronously but while setting it back to imageView you never bothered to find if the cell is reused and the image u just created is outdated or not.
So in your cell
var currentModel: MessageModel! = nil //declare a instance variable to hold model
... other code
func configureCellWith(messageModel : MessageModel){
self.currentModel = messageModel //keep a copy of model passed to u as argument
guard let url = URL(string: messageModel.contentUrl!) else { return }
if url.isURLPhoto(){
likedImageView.sd_setImage(with: url, placeholderImage: nil)
}
else if url.isURLVideo(){
getThumbnailImageFromVideoUrl(url: url) { (image) in
self.likedImageView.image = image
}
}
Finally in getThumbnailImageFromVideoUrl
func getThumbnailImageFromVideoUrl(url: URL, completion: #escaping ((_ image: UIImage?)->Void)) {
DispatchQueue.global().async {
let asset = AVAsset(url: url)
let avAssetImageGenerator = AVAssetImageGenerator(asset: asset)
avAssetImageGenerator.appliesPreferredTrackTransform = true
let thumnailTime = CMTimeMake(value: 2, timescale: 1)
do {
let cgThumbImage = try avAssetImageGenerator.copyCGImage(at: thumnailTime, actualTime: nil)
let thumbNailImage = UIImage(cgImage: cgThumbImage)
if url.absoluteString == currentModel.contentUrl { //check if image you generated is still valid or its no longer needed
DispatchQueue.main.async {
completion(thumbNailImage)
}
}
} catch {
print(error.localizedDescription)
DispatchQueue.main.async {
completion(nil)
}
}
}

Switching subtitles of hls on tvOS/swift

I am using AVplayer of swift for my tvOSApp.
When I use default player, subtitles of the video will be shown on "subtitle" bar on top and can easily switch subtitles.
So, how can I switch subtitles like that, without using the default playback controls?
I have buttons for switching subtitles on the screen and I'd like to switch during target method of these.
Here is my code for AVPlayer.
let asset = AVAsset(url: URL(string: videoUrl)!)
let playerItem = AVPlayerItem(asset: asset)
let player = AVPlayer(playerItem: playerItem)
player.play()
self.bgMovieLayer = AVPlayerLayer(player: player)
self.bgMovieLayer.frame = view.bounds
self.bgMovieLayer.videoGravity = .resizeAspectFill
self.bgMovieLayer.zPosition = -1
self.view.layer.insertSublayer(self.bgMovieLayer, at: 0)
I am using the video below for a test.
http://184.72.239.149/vod/smil:BigBuckBunny.smil/playlist.m3u8
I read the documents regarding AVPlayer/AVPlayerItem and came into no answer. I would appreciate if someone can help this.
You have to load availableMediaCharacteristics first and after that look for AVMediaCharacteristic.legible.
Something like this:
let asset:AVAsset
let key = #keyPath(AVAsset.availableMediaCharacteristicsWithMediaSelectionOptions)
asset.loadValuesAsynchronously(forKeys: [key]) {
var error: NSError? = nil
let status = asset.statusOfValue(forKey: key, error: &error)
switch status {
case .loaded:
// Sucessfully loaded, continue processing
case .failed:
// Examine NSError pointer to determine failure
case .cancelled:
// Loading cancelled
default:
// Handle all other cases
}
}
let subtitlesGroup = asset.mediaSelectionGroup(forMediaCharacteristic: AVMediaCharacteristic.legible)

Can't able to get Video Tracks from AVURLAsset for HLS videos(.m3u8 format) for AVPlayer?

I am developing a custom video player to stream HLS videos from server. I can successfully play HLS videos using AVPlayerItem and AVPlayer.
After that I want to add subtitle track and audio tracks for my video player. So I used AVMutableComposition to do so. So now the issue is when I am creating AVURLAsset for HLS Videos, I can't able to get video tracks from AVURLAsset. It is giving me always 0 tracks. I tried "loadValuesAsynchronously" of AVURLAsset and I tried adding KVO for "tracks" of AVPlayerItem. But None of these producing me any positive result.
I am using the following code.
func playVideo() {
let videoAsset = AVURLAsset(url: videoURL!)
let composition = AVMutableComposition()
// Video
let videoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
do {
let tracks = videoAsset.tracks(withMediaType: .video)
guard let track = tracks.first else {
print("Can't get first video track")
return
}
try videoTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, videoAsset.duration), of: track, at: kCMTimeZero)
} catch {
print(error)
return
}
guard let subtitlesUrl = Bundle.main.url(forResource: "en", withExtension: "vtt") else {
print("Can't load en.vtt from bundle")
return
}
//Subtitles
let subtitleAsset = AVURLAsset(url: subtitlesUrl)
let subtitleTrack = composition.addMutableTrack(withMediaType: .text, preferredTrackID: kCMPersistentTrackID_Invalid)
do {
let subTracks = subtitleAsset.tracks(withMediaType: AVMediaType.text)
guard let subTrack = subTracks.first else {
print("Can't get first subtitles track")
return
}
try subtitleTrack?.insertTimeRange(CMTimeRangeMake(kCMTimeZero, videoAsset.duration), of: subTrack, at: kCMTimeZero)
} catch {
print(error)
return
}
// Prepare item and play it
let item = AVPlayerItem(asset: composition)
self.player = AVPlayer(playerItem: item)
self.playerLayer = AVPlayerLayer.init()
self.playerLayer.frame = self.bounds
self.playerLayer.contentsGravity = kCAGravityResizeAspect
self.playerLayer.player = player
self.layer.addSublayer(self.playerLayer)
self.player.addObserver(self, forKeyPath: "currentItem.loadedTimeRanges", options: .new, context: nil)
self.player.play()
}
This procedure working well for .mp4 videos but not for HLS Videos(.m3u8). Anyone have some working solution for this?
or
How can we get tracks from HLS videos using AVURLAsset? If this is not possible then How can achieve similar result ?
Please let me know you feedback.
Many more thanks in advance.
For HLS video tracks(withMediaType: .video) will return an empty array.
Use this instead: player.currentItem.presentationSize.width and player.currentItem.presentationSize.height.
Pls let me know if it works.
I didn't have the exact same problem as you. But I got around a similar problem (querying for HDR) by instead of querying the tracks on the AVURLAsset, I queried the tracks on the AVPlayerItem.
Set up an observer on the item status:
player?.observe(\AVPlayer.currentItem?.status,
options: [.new, .initial], changeHandler: { [weak self] player, _ in
DispatchQueue.main.async {
self?.observedItemStatus(from: player)
}
})
Then query the AVMediaType of your choice (in your case text).
func observedItemStatus(from avPlayer: AVPlayer) {
guard let currentItem = avPlayer.currentItem else { return }
// ideally execute code based on currentItem.status...for the brevity of this example I won't.
let hasLegibleMedia = currentItem.tracks.first(where: {
$0.assetTrack?.mediaType == AVMediaType.text
})?.assetTrack.hasMediaCharacteristic(.legible)
}
Alternatively if you need more than just a Bool, you could do a loop to access the assetTrack you really want.

for loop in swift step by step?

I have the following array with urls:
let KStorePlayURL = [
https://source.s3-us-west-2.amazonaws.com/ENVOI/2018/07/19/ATASTEOFDANCE_S1_EP3.mp4,
https://source.s3-us-west-2.amazonaws.com/ENVOI/2018/05/23/ATasteOfDance_S1E1_Episode.mp4,
https://source.s3-us-west-2.amazonaws.com/ENVOI/2018/05/23/ATasteOfDance_S1E1_Episode.mp4,
https://source.s3-us-west-2.amazonaws.com/ENVOI/2018/05/23/ATasteOfDance_S1E1_Episode.mp4
]
How can I get the URLs one after the other in the for loop? Also when one video is played and followed by other videos in this Bitmovin player?
Here is the code I have already tried:
#objc func setUpPlayerVideos1() {
print(KStorePlayURL)
for i in 0..<KStorePlayURL.count {
let streamURL = URL(string: KStorePlayURL[i])
playlist.append(PlaylistItem(url: streamURL!, title: "player"))
// Create player based with a default configuration
let player = BitmovinPlayer()
// Create player view and pass the player instance to it
let playerView = BMPBitmovinPlayerView(player: player, frame: .zero)
// Listen to player events
player.add(listener: self)
playerView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
playerView.frame = view.bounds
view.addSubview(playerView)
view.bringSubview(toFront: playerView)
// store the reference to the player
self.Bitmovinplayer = player
}
// Start the playlist
playNextItem()
}
You shouldn't be using a for-loop, because you are going to be overriding the player in each iteration. What you need to do is fill the playlistusing a for-loop or a more functional style like this :
KStorePlayURL.forEach { urlString in
//Make sure that the url address is correct
guard let streamURL = URL(string: urlString) else {
fatalError("Error in stream url")
}
playlist.append(PlaylistItem(url: streamURL!, title: "player"))
}
Here I am supposing that KStorePlayURL is an array of strings:
let KStorePlayURL = [
"https://source.s3-us-west-2.amazonaws.com/ENVOI/2018/07/19/ATASTEOFDANCE_S1_EP3.mp4",
"https://source.s3-us-west-2.amazonaws.com/ENVOI/2018/05/23/ATasteOfDance_S1E1_Episode.mp4",
"https://source.s3-us-west-2.amazonaws.com/ENVOI/2018/05/23/ATasteOfDance_S1E1_Episode.mp4",
"https://source.s3-us-west-2.amazonaws.com/ENVOI/2018/05/23/ATasteOfDance_S1E1_Episode.mp4"
]
Your final code should look like this:
#objc func setUpPlayerVideos1() {
KStorePlayURL.forEach { urlString in
guard let streamURL = URL(string: urlString) else {
fatalError("Error in stream url")
}
playlist.append(PlaylistItem(url: streamURL!, title: "player"))
}
let player = BitmovinPlayer()
// Create player view and pass the player instance to it
let playerView = BMPBitmovinPlayerView(player: player, frame: .zero)
// Listen to player events
player.add(listener: self)
playerView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
playerView.frame = view.bounds
view.addSubview(playerView)
view.bringSubview(toFront: playerView)
// store the reference to the player
self.Bitmovinplayer = player
}
// Start the playlist
playNextItem()
}
You can find a complete sample code for playing a playlist with the bitmovin player here.