AVPlayerViewController show black screen some times - ios

I create an player and working fine in most part of times.
In some situations (that I didn't realized why) screen video stays black with a play button that does nothing.
I verified url and is ok, that's not the problem.
In my viewController I can call this block of code multiples times with different urls, that's why I 'restart' AVPlayerViewController.
// Create an var in class...
// ....
self.videoPlayerViewController?.player?.pause()
self.videoPlayerViewController = AVPlayerViewController()
self.videoPlayerViewController?.player = viewModel.avPlayer
if let avController = self.videoPlayerViewController {
self.add(avController, in: self.playerView)
avController.player?.play()
} else {
// Error
}
That's function add:
extension UIViewController {
func add(_ viewController: UIViewController, in view: UIView) {
viewController.view.frame = view.bounds
addChildViewController(viewController)
view.addSubview(viewController.view)
viewController.didMove(toParentViewController: self)
view.clipsToBounds = true
}
}
Someone knows what is wrong?
Thanks in advance!!

After so many time.. I found solution.
The problem was that I wasn't cleaning AVPlayer inside AVPlayerController. And I also added New instance inside a DispachQueue.
That's new code:
self.videoPlayerViewController?.player?.pause()
self.videoPlayerViewController?.player = nil
self.videoPlayerViewController = nil
self.videoPlayerViewController = AVPlayerViewController()
self.videoPlayerViewController?.player = viewModel.avPlayer
And after I added in viewController:
if let avController = self.videoPlayerViewController {
DispatchQueue.main.async { [weak self] in
if let strongSelf = self {
strongSelf.add(avController, in: strongSelf.playerView)
avController.player?.play()
}
}
} else {
// Error
}
I hope it could help someone!!

Related

Looping an iOS live photo programmatically in SwiftUI

I'd like to be able to loop a live photo, for continuous playback.
So far, I'm trying to use the PHLivePhotoViewDelegate to accomplish this.
import Foundation
import SwiftUI
import PhotosUI
import iOSShared
struct LiveImageView: UIViewRepresentable {
let view: PHLivePhotoView
let model:LiveImageViewModel?
let delegate = LiveImageLargeMediaDelegate()
init(fileGroupUUID: UUID) {
let view = PHLivePhotoView()
// Without this, in landscape mode, I don't get proper scaling of the image.
view.contentMode = .scaleAspectFit
self.view = view
// Using this to replay live image repeatedly.
view.delegate = delegate
model = LiveImageViewModel(fileGroupUUID: fileGroupUUID)
guard let model = model else {
return
}
model.getLivePhoto(previewImage: nil) { livePhoto in
view.livePhoto = livePhoto
}
}
func makeUIView(context: Context) -> UIView {
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
guard let model = model else {
return
}
guard !model.started else {
return
}
model.started = true
view.startPlayback(with: .full)
}
}
class LiveImageLargeMediaDelegate: NSObject, PHLivePhotoViewDelegate {
func livePhotoView(_ livePhotoView: PHLivePhotoView, didEndPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) {
livePhotoView.stopPlayback()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
livePhotoView.startPlayback(with: .full)
}
}
}
But without full success. It seems the audio does play again, but not the video. The livePhotoView.stopPlayback and the async aspect are just additional changes I was trying. I've tried it without those too.
Note that I don't want the user to have to manually change the live photo (e.g., see NSPredicate to not include Loop and Bounce Live Photos).
Thoughts?
ChrisPrince I tried your code and it works fine for me, I just add delegate and start playback inside of it and everything runs well and smoothly. I thought that there is no point in using stop playback because the function itself says that the playback ended.
func livePhotoView(_ livePhotoView: PHLivePhotoView, didEndPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) {
livePhotoView.startPlayback(with: .full)
}

How to change the Navigation Bar color for a MFMessageComposeViewController?

I'm using the MFMessageComposeViewController and the MFMailComposeViewController. For some reason only the Mail VC is being styled with the colors I want. Here is how I am styling the Navigation bar in the AppDelegate inside the didFinish func.
let navigationBarAppearace = UINavigationBar.appearance()
navigationBarAppearace.tintColor = Styles.whiteColor()
navigationBarAppearace.barTintColor = Styles.inputColor()
navigationBarAppearace.titleTextAttributes = [NSForegroundColorAttributeName:Styles.whiteColor()]
navigationBarAppearace.isTranslucent = false
But the Message VC is not being styled by the AppDelegate but I'm not sure why not.
I tried this but nothing changed.
let controller = MFMessageComposeViewController()
controller.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: Styles.positiveColor()]
controller.navigationBar.barTintColor = Styles.negativeColor()
controller.messageComposeDelegate = self
Is the Message VC styled differently? It still shows up with the default white nav bar and the default blue cancel button.
Here is a photo of the Email VC and the Message VC navigations bars.
As you can see the Message VC is not being styled like the Email VC Navigation bar, but I'm not sure why.
You can create a subclass of UINavigationBar (MyNavigationBar) where you set all needed properties.
Then, as MFMessageComposeViewController inherits from UINavigationController, you can use its initialization method
init(navigationBarClass: AnyClass?, toolbarClass: AnyClass?)
and provide MyNavigationBar class as a parameter.
The following is for Swift 3/4.
I tried many ways shown on StackOverflow and other sites, including the subclass way mentioned in the above answer. But could not get success in changing color or changing font color of UIBarButtons.
Then tried different way of presenting the MFMessageComposeViewController.
// Configures and returns a MFMessageComposeViewController instance. This is same with no change.
func configuredMessageComposeViewController() -> MFMessageComposeViewController {
let messageComposeVC = MFMessageComposeViewController()
let fileManager:FileManager = FileManager.default
messageComposeVC.messageComposeDelegate = self // Make sure to set this property to self, so that the controller can be dismissed!
messageComposeVC.recipients = [myContactPhone]
if fileManager.fileExists(atPath: mySendImagePath) {
if let image = UIImage(contentsOfFile: mySendImagePath) {
if UIImagePNGRepresentation(image) != nil
{
let imageData1: Data = UIImagePNGRepresentation(image)!
let success = messageComposeVC.addAttachmentData(imageData1, typeIdentifier: "public.data", filename: "image.JPG")
if(success)
{
}
else{
}
}
}
}
return messageComposeVC
}
// Following code is usage of above.
if (MFMessageComposeViewController.canSendText()) {
myMessageComposeVC = configuredMessageComposeViewController()
// old code - Instead of using following way
//present(messageComposeVC, animated: true, completion: nil)
// Used this way to use existing navigation bar.
if let messageComposeVC = myMessageComposeVC {
messageComposeVC.willMove(toParentViewController: self)
messageComposeVC.view.frame = self.view.frame
self.view.addSubview(messageComposeVC.view)
self.addChildViewController(messageComposeVC)
messageComposeVC.didMove(toParentViewController: self)
}
} else {
showSendMMSErrorAlert()
return
}
// Following code to remove it when returned through delegate.
func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
// old code
//controller.dismiss(animated: true, completion: nil)
controller.willMove(toParentViewController: nil)
controller.view.removeFromSuperview()
controller.removeFromParentViewController()
if(result.rawValue == 0)
{
... error ...
} else {
... success ...
}
}
Hope, this is useful for persons like me.
Regards.

CNContactViewController setEditing true before appear

I have a task to show the contact editing screen at once during his appearance (such as WhatsApp), I show him the following way.
#objc private func presentContactEditController() {
guard var contact = contactModel.contact else { return }
if !contact.areKeysAvailable([CNContactViewController.descriptorForRequiredKeys()]) {
do {
let contactStore = CNContactStore()
contact = try contactStore.unifiedContact(withIdentifier: contact.identifier, keysToFetch: [CNContactViewController.descriptorForRequiredKeys()])
} catch {
debugPrint("presentContactEditController error", error.localizedDescription)
}
}
let cnContactViewController = CNContactViewController(for: contact)
cnContactViewController.delegate = self
cnContactViewController.setEditing(true, animated: false)
let contactNaviController = UINavigationController(rootViewController: cnContactViewController)
present(contactNaviController, animated: true, completion: nil)
}
But there is a screen with information about this contact. So I tried to do it through the heir of CNContactViewController, different methods ViewController life cycle, but it works only in viewDidAppear method, but it will be visible to the user. How can I solve this problem? Thank you.
just change let cnContactViewController = CNContactViewController(for: contact)
to
let cvc = CNContactViewController(forNewContact: contact)
it will work for you
I came to the conclusion that that WhatsApp create a custom screen for this purpose. Just I saw the same screen in Telegram only with a modified design.

Label does not update using Swift

I'm trying to improve a GitHub project I forked (https://github.com/giacmarangoni/Swift-Radio-Pro/tree/xcode8).
After some fixes and changes everything seems to work good but suddenly I noticed a really strange behavior.
When I open "NowPlayingViewController" for the first time and station starts to stream, everything is working and AVPlayer delegate updates user interface as expected (songLabel, titleLabel and albumArtwork).
After that, without stopping radio streaming, I tried to go back to "StationsViewController" and immediately to reopen "NowPlayingViewController" using "Now playing" button.
At this point delegation is still active, streaming is going on, but when song changes all variables in this view controller are updated but I can't say the same for the user interface. I tried to debug and I noticed that labels are populated but not updated. UI updates in the main thread and setNeedDisplay didn't help.
NowPlayingViewController
AVPlayer setup:
func setUpPlayer(){
radioPlayer = Player.radio
radioPlayer.rate = 1
NotificationCenter.default.addObserver(
self,
selector: #selector(self.playerItemDidReachEnd),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: self.radioPlayer.currentItem
)
}
Here you can find func onMetaData(_ metaData: [AVMetadataItem]?)).
//*****************************************************************
// MARK: - AVPlayerItem Delegate (for metadata)
//*****************************************************************
extension NowPlayingViewController: CustomAVPlayerItemDelegate {
func onMetaData(_ metaData: [AVMetadataItem]?) {
if let metaDatas = metaData{
startNowPlayingAnimation()
let firstMeta: AVMetadataItem = metaDatas.first!
let metaData = firstMeta.value as! String
var stringParts = [String]()
if metaData.range(of: " - ") != nil {
stringParts = metaData.components(separatedBy: " - ")
} else {
stringParts = metaData.components(separatedBy: "-")
}
// Set artist & songvariables
let currentSongName = track.title
track.artist = stringParts[0].decodeAllChars()
track.title = stringParts[0].decodeAllChars()
if stringParts.count > 1 {
track.title = stringParts[1].decodeAllChars()
}
if track.artist == "" && track.title == "" {
track.artist = currentStation.stationDesc
track.title = currentStation.stationName
}
DispatchQueue.main.async {
if currentSongName != self.track.title {
if kDebugLog {
print("METADATA artist: \(self.track.artist) | title: \(self.track.title)")
}
// Update Labels
self.artistLabel.text = self.track.artist
self.songLabel.text = self.track.title
self.updateUserActivityState(self.userActivity!)
// songLabel animation
self.songLabel.animation = "zoomIn"
self.songLabel.duration = 1.5
self.songLabel.damping = 1
self.songLabel.animate()
// Update Stations Screen
self.delegate?.songMetaDataDidUpdate(self.track)
// Query API for album art
self.resetAlbumArtwork()
self.queryAlbumArt()
}
}
}
}
}
This method is observed in "CustomAVPlayerItem" according to timedMetaData key path; It's fired every time AVPlayer metadatas change. This class is a subclass of AVPlayerItem:
import MediaPlayer
import Foundation
protocol CustomAVPlayerItemDelegate {
func onMetaData(_ metaData:[AVMetadataItem]?)
}
//*****************************************************************
// Makes sure that observers are removed before deallocation
//*****************************************************************
class CustomAVPlayerItem: AVPlayerItem {
var delegate : CustomAVPlayerItemDelegate?
init(url URL:URL)
{
if kDebugLog {print("CustomAVPlayerItem.init")}
super.init(asset: AVAsset(url: URL) , automaticallyLoadedAssetKeys:[])
addObserver(self, forKeyPath: "timedMetadata", options: NSKeyValueObservingOptions.new, context: nil)
}
deinit{
if kDebugLog {print("CustomAVPlayerItem.deinit")}
removeObserver(self, forKeyPath: "timedMetadata")
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let avpItem: AVPlayerItem = object as? AVPlayerItem {
if keyPath == "timedMetadata" {
delegate?.onMetaData(avpItem.timedMetadata)
}
}
}
}
The following is my AVPlayer:
import MediaPlayer
//*****************************************************************
// This is a singleton struct using Swift
//*****************************************************************
struct Player {
static var radio = AVPlayer()
}
This is the segue function I use to open to "NowPlayingViewController". StationsViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "NowPlaying" {
self.title = ""
firstTime = false
let nowPlayingVC = segue.destination as! NowPlayingViewController
nowPlayingVC.delegate = self
if let indexPath = (sender as? IndexPath) {
// User clicked on row, load/reset station
if searchController.isActive {
currentStation = searchedStations[indexPath.row]
} else {
currentStation = stations[indexPath.row]
}
nowPlayingVC.currentStation = currentStation
nowPlayingVC.newStation = true
} else {
// User clicked on a now playing button
if let currentTrack = currentTrack {
// Return to NowPlaying controller without reloading station
nowPlayingVC.track = currentTrack
nowPlayingVC.currentStation = currentStation
nowPlayingVC.newStation = false
} else {
// Issue with track, reload station
nowPlayingVC.currentStation = currentStation
nowPlayingVC.newStation = true
}
}
}
}
Here's what I think you're not understanding and what's actually going on.
Normally, when you "go back" from a pushed view controller, the pushed view controller is popped and destroyed. Your pushed view controller is a NowPlayingViewController. It should be destroyed when you "go back" from it to the StationsViewController. Thus, when you show the NowPlayingViewController again, you would have to create a new, different NowPlayingViewController.
Okay, so far so good, provided you understand all of that. But in your case there is a further complication: you have a leak! Your old NowPlayingViewController is not being destroyed. Thus, when you "go back" to the StationsViewController and show the NowPlayingViewController for a second time, there are now two NowPlayingViewControllers — the new one that you see, and the old one that is leaking.
Okay, so your logging continues to show the old NowPlayingViewController, which is still observing and updating. But your eyes are seeing the new NowPlayingViewController, which is doing nothing. And that explains the phenomena you have described.
If this is right — and, from what you've said, I'm pretty sure it is — then you need to reorganize your architecture either so that you don't get this leak or so that when you show the NowPlayingViewController the second time you show the same NowPlayingViewController rather than creating a different one. (The first approach would be better.)

Stream 2 videos simultaneously Swift

I'm trying to stream two videos at the same time with swift on iphone. I already know that the AV Player can only stream one video at a time, but googling it I saw that it's still possible to stream different tracks at the same time. I also saw the picture in picture implementation. The real problem is that is all in objective-c and the code is quite old. I tried to understand it running the code as it is, but there are errors and some of the functions are deprecated.
Does someone know how to do that in swift? Also, I'm streaming video from the internet so merging them before playing is not an option.
Thank you!
Swift Version
The article you referenced is an interesting method of handling multiple video playback in iOS. The article appears to be related to Apple's Stitched Stream Player Sample Project. As an exercise in Swift 2.2, I've rewritten the code on the iOS Guy article.
You can find both the view and the view controller in Swift on my gist. I'm copying below as it is best practices to not use link only answers on SO.
Custom View Player
This custom view player allows one to swap out multiple AVPlayerItem(s).
import UIKit
import AVFoundation
class MNGVideoPlayerView: UIView {
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
}
*/
override class func layerClass () -> AnyClass {
return AVPlayerLayer.self
}
func player () -> AVPlayer {
return (self.layer as! AVPlayerLayer).player!
}
func setPlayer(player:AVPlayer) {
(self.layer as! AVPlayerLayer).player = player
}
func setVideoFillMode(fillMode:String) {
let playerLayer = (self.layer as! AVPlayerLayer)
playerLayer.videoGravity = fillMode
}
}
Multiple Video Player View Controller
This controller manages the distribution and presentation of the different AVPlayerItem(s). Check my gist repos for additional updates. (ported from original objc source #iOS Guy)
import UIKit
import AVFoundation
class MNGVideoPlayerViewController: UIViewController {
let kTracksKey = "tracks";
let kStatusKey = "status";
let kRateKey = "rate";
let kPlayableKey = "playable";
let kCurrentItemKey = "currentItem";
let kTimedMetadataKey = "currentItem.timedMetadata";
var _URL:NSURL? = nil
var player:AVPlayer? = nil
var playerItem:AVPlayerItem? = nil
var playerView:MNGVideoPlayerView? = nil
var AVPlayerDemoPlaybackViewControllerStatusObservationContext = UnsafeMutablePointer<Void>()
var AVPlayerDemoPlaybackViewControllerCurrentItemObservationContext = UnsafeMutablePointer<Void>()
var AVPlayerDemoPlaybackViewControllerStatusObservation = UnsafeMutablePointer<Void>()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Public methods
func setURL(url:NSURL) {
self._URL = url
let asset = AVURLAsset(URL: self._URL!)
let requestedKeys = [kTracksKey,kPlayableKey]
asset.loadValuesAsynchronouslyForKeys(requestedKeys) { () -> Void in
self.prepareToPlayAsset(asset, withKeys: requestedKeys)
}
}
func prepareToPlayAsset(asset:AVURLAsset, withKeys requestedKeys:NSArray) {
var error:NSErrorPointer = nil
for thisKey in requestedKeys {
let keyStatus = asset.statusOfValueForKey(thisKey as! String, error: error)
if keyStatus == .Failed {
return
}
}
if !asset.playable {
return
}
if (self.playerItem != nil) {
self.playerItem?.removeObserver(self, forKeyPath: kStatusKey)
NSNotificationCenter.defaultCenter().removeObserver(self, name: AVPlayerItemDidPlayToEndTimeNotification, object: self.playerItem)
}
self.playerItem = AVPlayerItem(asset: asset)
self.playerItem?.addObserver(self, forKeyPath: kStatusKey, options: [.Initial,.New], context: AVPlayerDemoPlaybackViewControllerStatusObservationContext)
if (self.player == nil) {
self.player = AVPlayer(playerItem: self.playerItem!)
self.player?.addObserver(self, forKeyPath: kCurrentItemKey, options: [.Initial,.New], context: AVPlayerDemoPlaybackViewControllerCurrentItemObservationContext)
}
if self.player?.currentItem! != self.playerItem! {
self.player?.replaceCurrentItemWithPlayerItem(self.playerItem!)
}
}
// MARK: - Key Value Observing
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if context == AVPlayerDemoPlaybackViewControllerStatusObservation {
let status:Int = (change![NSKeyValueChangeNewKey]?.integerValue)!
if status == AVPlayerStatus.ReadyToPlay.rawValue {
self.player?.play()
}
} else if context == AVPlayerDemoPlaybackViewControllerCurrentItemObservationContext {
let newPlayerItem = change![NSKeyValueChangeNewKey] as? AVPlayerItem
if newPlayerItem != nil {
self.playerView?.setPlayer(self.player!)
self.playerView?.setVideoFillMode(AVLayerVideoGravityResizeAspect)
}
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
It looks like http://iosguy.com/2012/01/11/multiple-video-playback-on-ios/ is the best solution at the moment. Nobody tried to convert it in swift yet.
I play multiple videos in my app at once, I just use multiple instances of AVPlayer.

Resources