Change of AVPlayer current item (item url) and start playback - ios

I have a UITableView with a posts that contain URLs to a mp3 files. With a tap on a button in a UITableViewCell I plan to fetch a singleton instance of AVPlayer. How to assign new URL to a AVPlayer and start playback? AVPlayer's currentItem is only a { get } property.
This is what I have now.
import Foundation
import AVFoundation
class StreamMusicPlayer: AVPlayer {
private override init(){
super.init()
}
static func shared() -> AVPlayer{
return AVPlayer()
}
func playItem(at itemURL: URL) {
if StreamMusicPlayer.shared().isPlaying {
StreamMusicPlayer.shared().pause()
}
//Change url of AVPlayerItem
//And and assign it to shared() instance then begin playback
}
}
extension AVPlayer {
var isPlaying: Bool {
return rate != 0 && error == nil
}
}

This might be what you want. It's not perfect.
class StreamMusicPlayer: AVPlayer {
private override init(){
super.init()
}
static var shared = AVPlayer()
static func playItem(at itemURL: URL) {
StreamMusicPlayer.shared = AVPlayer(url: itemURL)
StreamMusicPlayer.shared.play()
}
}
extension AVPlayer {
var isPlaying: Bool {
return rate != 0 && error == nil
}
}
I hade to change the static function to a normal variable. Every time you access .shared() it would initialize and return a new instance.
Then you can do:
StreamMusicPlayer.playItem(at: URL(string: "http://antoon.io/e/mp3/")!)
// Delay so you can hear it changing
StreamMusicPlayer.playItem(at: URL(string: "http://antoon.io/e/mp3/2.mp3")!)

Related

Please help me that there is a problem playing the video with HTTP Live Streaming using AVPlayer

After approximately 1 to 2 seconds of video playback using AVPlayer, the image is crushed like a picture. But the sound comes out. Why is this problem happening?
issue picture:
correct picture:
class ViewController: UIViewController {
#IBOutlet weak var playerView: PlayerView!
let player = AVPlayer()
override func viewDidLoad() {
super.viewDidLoad()
playerView.player = player
let playerItem = AVPlayerItem(url: "HLS URL")
playerItem.preferredForwardBufferDuration = TimeInterval(3.0)
self?.playerView.playerLayer.videoGravity = .resizeAspect
self?.playerView.player?.replaceCurrentItem(with: playerItem)
self?.playerView.player?.play()
}
}
class PlayerView: UIView {
var player: AVPlayer? {
get {
return playerLayer.player
}
set {
playerLayer.player = newValue
}
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
override static var layerClass: AnyClass {
return AVPlayerLayer.self
}
}

Unable to get AVAudioPlayer to stop playing

I have a class defined as Music.swift coded as follows:
import Foundation
import AVFoundation
class Music {
var isPlaying: Bool = false
public var backgrndSound = AVAudioPlayer()
func isMusicPlaying() -> Bool {
isPlaying = UserDefaults.standard.bool(forKey: "isPlaying")
return isPlaying
}
func StartPlaying() {
let path = Bundle.main.path(forResource: "Music.mp3", ofType: nil)!
let url = URL(fileURLWithPath: path)
do {
self.backgrndSound = try AVAudioPlayer(contentsOf: url)
self.backgrndSound.numberOfLoops = -1
self.backgrndSound.play()
UserDefaults.standard.setValue(true, forKey: "isPlaying")
} catch {
// couldn't load file :(
}
}
func StopPlaying() {
self.backgrndSound.pause()
self.backgrndSound.stop()
}
}
On first load of the app, the music is automatically started with a call to StartPlaying(). That works just fine. Afterwards I have a settings menu that has a switch for the music play :
import UIKit
import AVFoundation
class SettingsViewController: UIViewController {
#IBOutlet weak var swMusic: UISwitch!
var myMusic = Music()
override func viewDidLoad() {
super.viewDidLoad()
swMusic.isOn = UserDefaults.standard.bool(forKey: "isPlaying")
}
#IBAction func musicSwitch(_ sender: Any) {
if swMusic.isOn == true {
// turn on music
myMusic.StartPlaying()
} else {
myMusic.StopPlaying()
}
}
}
When I tap the switch it does fire StopPlaying() but the music in the background continues to play despite the tap.
I am not sure why that is, unless the AV object isn't accessible from the original creation and therefore can't stop it properly; but so far I have been unable to figure that out either.
Any help or suggestions would be greatly appreciated.
By instantiating a new instance of the Music class in SettingsViewController you're effectively creating a new AVAudioPlayer instance that knows nothing about the one already instantiated.
Consider this code, which contains static properties and class methods:
import Foundation
import AVFoundation
class Music
{
public static var backgrndSound: AVAudioPlayer?
// AVAudioPlayer already has an isPlaying property
class func isMusicPlaying() -> Bool
{
return backgrndSound?.isPlaying ?? false
}
class func StartPlaying()
{
let path = Bundle.main.path(forResource: "Music.mp3", ofType: nil)!
let url = URL(fileURLWithPath: path)
do
{
backgrndSound = try AVAudioPlayer(contentsOf: url)
backgrndSound?.numberOfLoops = -1
backgrndSound?.play()
}
catch
{
// couldn't load file :(
}
}
class func StopPlaying()
{
backgrndSound?.pause()
backgrndSound?.stop()
}
}
then access this using:
Music.isMusicPlaying()
Music.startPlaying()
Music.stopPlaying()
i.e. you'd not do var myMusic = Music()
This way there will always be a single instance of the AVAudioPlayer, Music.backgrndSound
This sample code changes backgrndSound to an optional ... you're effectively creating an unused AVAudioPlayer instance that is discarded as soon as you startPlaying.
It also removes the unnecessary isPlaying property, as AVAudioPlayer already has a property for this purpose.

Handling AVPlayerLayer when wrapping AVPlayer behaviour

I'm trying to wrap AVPlayer in my own class so I can provide a nicer API to use throughout my app and so I can mock player behaviour for testing with other objects (and because the AVPlayer KVOs are quite ugly to use!). Here's a simplified model of what I'm trying to do with just the play and pause functionality:
protocol VideoPlayerProtocol {
func play()
func pause()
}
class AVPlayerWrapped: VideoPlayerProtocol {
private let player = AVPlayer()
init(playerItem: AVPlayerItem) {
self.player.replaceCurrentItem(with: playerItem)
}
func play() {
player.play()
}
func pause() {
player.pause()
}
}
I also have a PlayerView which adds an AVPlayerLayer to a view. From the Apple docs, this is set by providing the view an AVPlayer:
class PlayerView: UIView {
override class var layerClass: AnyClass {
return AVPlayerLayer.self
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
var player: AVPlayer? {
get { playerLayer.player }
set { playerLayer.player = newValue }
}
}
The problem is that when I setup an AVPlayerWrapped object, in order to display the playback in a view I need to reveal the underlying AVPlayer to the player property on PlayerView which defeats the purpose of me wrapping the player.
Is there a way for me to somehow use an AVPlayerLayer without my AVPlayerWrapped having to reveal its underlying player please? Or am I taking the wrong approach?
Any guidance much appreciated!
class AVPlayerWrapped: VideoPlayerProtocol {
fileprivate let player = AVPlayer()
init(playerItem: AVPlayerItem) {
self.player.replaceCurrentItem(with: playerItem)
}
func play() {
player.play()
}
func pause() {
player.pause()
}
}
extension AVPlayerLayer {
func setPlayerWrapper(_ playerWrapped: AVPlayerWrapped) {
player = playerWrapped.player
}
}
And
class PlayerView: UIView {
override class var layerClass: AnyClass {
return AVPlayerLayer.self
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
func setPlayerWrapper(_ playerWrapped: AVPlayerWrapped) {
playerLayer.setPlayerWrapper(playerWrapped)
}
}
I believe you don't need a getter for your view - in my practice I haven't used it. But in case you do, you can do it with an associatedObject, but it's much slower than a real property and I would advise you to use this approach only in special cases.

Avplayer AVPlayerItemDidPlayToEndTime notificaton not sending message to observer

I am using AVQueuePlayer to play a list of remote audio files. I want to implement repeat all by default.
My approach, I am observing AVPlayerItemDidPlayToEndTime notification and I add the playerItem to the back of the Queue when it has finished playing.
The nextAudio(notification: Notification) is not running at all. Need help on this or better still a better way to implement infinite play.
func playAudio(_ items: [AVPlayerItem]) {
let avPlayerVC = AVPlayerViewController()
let player = AVQueuePlayer(items: items)
player.actionAtItemEnd = .pause
avPlayerVC.player = player
for item in items {
NotificationCenter.default.addObserver(self,
selector: #selector(FilesViewController.nextAudio(notification:)),
name: .AVPlayerItemDidPlayToEndTime, object: item)
}
present(avPlayerVC, animated: true) {
self.player.play()
}
}
#objc func nextAudio(notification: Notification) {
debugPrint("nextAudio was called")
guard player != nil else { return }
debugPrint("AVPlayerItemDidPlayToEndTime notif info \(notification.userInfo)")
if let currentItem = notification.userInfo!["object"] as? AVPlayerItem {
currentItem.seek(to: kCMTimeZero)
self.player.advanceToNextItem()
self.player.insert(currentItem, after: nil)
}
}
I am sure you already figured it out but I just encountered myself and decided to answer anyways:
It looks like it is not delivered when you specify an object of notification (which should be a proper way BTW). may be a bug in iOS...
You need to pass nil instead:
NotificationCenter.default.addObserver(self, selector: #selector(videoDidPlayToEnd), name: .AVPlayerItemDidPlayToEndTime, object: nil)
Then in the method you should check in the notification's object is your player item. Documentation is actually not consistent because Apple states that notification's object is a AVplayer but it is a AVPlayerItem:
#objc
private func videoDidPlayToEnd(_ notification: Notification) {
guard let playerItem = notification.object as? AVPlayerItem, let urlAsset = playerItem.asset as? AVURLAsset else { return }
gpLog("Sender urlAsset: \(urlAsset.url.absoluteString)")
// Compare an asset URL.
}
Thank you #Lukasz for your great answer! I was having an issue with multiple videos firing the notification at the wrong time. Your answer helped me fix it.
If anyone is looking for examples of how to use this with SwiftUI here's my code below:
First create a player:
import SwiftUI
import AVKit
struct VideoPlayer : UIViewControllerRepresentable {
func makeCoordinator() -> VideoPlayer.Coordinator {
return VideoPlayer.Coordinator(parent1: self)
}
#Binding var didFinishVideo : Bool
#Binding var player : AVPlayer
var play: Bool
var loop: Bool
var videoName: String
var controller = AVPlayerViewController()
func makeUIViewController(context: UIViewControllerRepresentableContext<VideoPlayer>) -> AVPlayerViewController {
controller.player = player
controller.showsPlaybackControls = false
controller.videoGravity = .resize
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(context.coordinator.playerDidFinishPlaying(_:)), name: .AVPlayerItemDidPlayToEndTime, object: nil)
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<VideoPlayer>) {
if play {
player.play()
}
}
class Coordinator : NSObject{
var parent : VideoPlayer
init(parent1 : VideoPlayer) {
parent = parent1
}
#objc func playerDidFinishPlaying(_ notification: Notification) {
guard let playerItem = notification.object as? AVPlayerItem, let urlAsset = playerItem.asset as? AVURLAsset else { return }
print("Sender urlAsset: \(urlAsset.url.absoluteString)")
if urlAsset.url.absoluteString.contains(parent.videoName) {
if parent.loop {
parent.player.seek(to: CMTime.zero)
parent.player.play()
} else {
parent.didFinishVideo = true
}
}
}
}
}
The you can use it to create multiple videos like this:
import SwiftUI
import AVKit
struct ExampleVideo: View {
#Binding var didFinishVideo : Bool
var play: Bool
#State private var player = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "exampleVideoFileName", ofType: "mp4")!))
var body: some View {
VideoPlayer(didFinishVideo: $didFinishVideo, player: $player, play: play, loop: false, videoName: "exampleVideoFileName")
}
}
struct ExampleVideo_Previews: PreviewProvider {
static var previews: some View {
CeveraIntroVideo(didFinishVideo: .constant(true), play: true)
}
}
Here's an example of using it in a view after something loads:
struct IntroScreens: View {
#State var loadingComplete = false
#State var didFinishVideo = false
var body: some View {
ZStack{
ExampleVideo(didFinishVideo: $didFinishVideo, play: loadingComplete)
.zIndex(loadingComplete ? 3 : 0)
.animation(Animation.easeInOut(duration: 1))
}
}
}

How to properly make background audio playback on iOS work?

I'm trying to build an Radio Streaming app. I have created a Singleton class for may RadioPlayer as described bellow and I have turned on Background Modes > Audio, AirPlay and Picture in Picture.
However, when my App goes into background mode, the audio stops playing. What am I missing here?
Appreciate any help! Thanks!
import Foundation
import AVFoundation
class RadioPlayer {
static let sharedInstance = RadioPlayer()
var player = AVPlayer(playerItem: RadioPlayer.radioPlayerItem())
var isPlaying = false
class func radioPlayerItem() -> AVPlayerItem {
return AVPlayerItem(URL: urlRadio())
}
class func urlRadio() -> NSURL {
let roRadio = Repository.realm.objects(RORadio)
let url: NSURL = NSURL(string: roRadio[0].streaming)!
return url
}
func toggle() {
if isPlaying == true {
pause()
} else {
play()
}
}
func play() {
player.play()
isPlaying = true
}
func pause() {
player.pause()
isPlaying = false
}
func currentlyPlaying() -> Bool {
return isPlaying
}
}
The application should be allowed to continue running while in the background. Open your Info.plist file and add the key of UIBackgroundModes. There will be only one string "audio" for your aim.

Resources