Handling AVPlayerLayer when wrapping AVPlayer behaviour - ios

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.

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

Playback controls in swiftui

Attempting to use AVKit's AVPlayer to play a video without playback controls:
private let player = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "videoToPlay", ofType: "mp4")!))
player.showsPlaybackControls = false
The above results in an error message that declarations must be separated by ';'.
I've also tried:
VideoPlayer(player: player)
.onAppear {
player.showsPlaybackControls = false
}
Which results in a different error.
Any advice on hiding playback controls with swiftui?
VideoPlayer does not appear to have any methods on it that I can find in the documentation to control whether the playback controls are shown.
showPlaybackControls doesn't work because AVPlayer doesn't have that property either.
It looks like for now you'll have to do something like wrap an AVPlayerViewController:
Note that this is a very limited example and doesn't take into account a lot of scenarios you may need to consider, like what happens when AVPlayerControllerRepresented gets reloaded because it's parent changes -- will you need to use updateUIViewController to update its properties? You will also probably need a more stable solution to storing your AVPlayer than what I used, which will also get recreated any time the parent view changes. But, all of these are relatively easily solved architectural decisions (look into ObservableObject, StateObject, Equatable, etc)
struct ContentView: View {
let player = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "IMG_0226", ofType: "mp4")!))
var body: some View {
AVPlayerControllerRepresented(player: player)
.onAppear {
player.play()
}
}
}
struct AVPlayerControllerRepresented : UIViewControllerRepresentable {
var player : AVPlayer
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = false
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
}
}
For me, setting allowsHitTesting false hides play back controls in iOS 16:
private let player = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "videoToPlay", ofType: "mp4")!))
...
// Disable user interaction on the video player to hide playback controls
VideoPlayer(player: player)
.allowsHitTesting(false)
There may be some corner cases I'm missing where the UI could still appear, but I have yet to encounter that.
I was having problems with playing segments with VideoPlayer, so I had to resort to using AVPlayerLayer which does not have the controls.
class PlayerView: UIView {
// Override the property to make AVPlayerLayer the view's backing layer.
override static var layerClass: AnyClass { AVPlayerLayer.self }
// The associated player object.
var player: AVPlayer? {
get { playerLayer.player }
set { playerLayer.player = newValue }
}
private var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
}
struct CustomVideoPlayer: UIViewRepresentable {
let player: AVQueuePlayer
func makeUIView(context: Context) -> PlayerView {
let view = PlayerView()
view.player = player
return view
}
func updateUIView(_ uiView: PlayerView, context: Context) { }
}
Usage:
CustomVideoPlayer(player: player)
.onAppear() {
player.play()
}
.onDisappear() {
player.pause()
}
.ignoresSafeArea(.all)
Another great option is to use a video layer instead of an AVPlayerViewController. This is especially useful if the video needs to repeat, for example, since that seems to be the only way to support that use-case.
Since you're just displaying a video layer, there are no controls by default.
How to loop video with AVPlayerLooper
import Foundation
import SwiftUI
import AVKit
import AVFoundation
struct GifyVideoView : View {
let videoName: String
var body: some View {
if let url = Bundle.main.url(forResource: videoName, withExtension: "mp4") {
GifyVideoViewWrapped(url: url)
} else {
Text("No video for some reason")
}
}
}
fileprivate struct GifyVideoViewWrapped: View {
let player : AVPlayer
var pub = NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime)
init(url: URL) {
player = AVPlayer(url: url)
}
var body: some View {
AVPlayerControllerRepresented(player: player)
.onAppear {
player.play()
}
.onReceive(pub) { (output) in
player.play()
}
}
}
fileprivate struct AVPlayerControllerRepresented : NSViewRepresentable {
var player : AVPlayer
func makeNSView(context: Context) -> AVPlayerView {
let view = AVPlayerView()
view.controlsStyle = .none
view.player = player
return view
}
func updateNSView(_ nsView: AVPlayerView, context: Context) { }
}
usage:
GifyVideoView(VideoName: "someMp4VideoName")
.frame(width: 314, height: 196 * 2)
.clipShape( RoundedRectangle(cornerRadius: 15, style: .continuous) )
video will be played looped ant without controls.
Video will be taken from project resources

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

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")!)

AVPlayer audio isn't always muted

I want my AVPlayer video to be muted unless the user's phone isn't on silent mode. This generally works by default, however sometimes the sound still plays even when on silent mode. How can I fix this? My code is:
let player = AVPlayer(playerItem: AVPlayerItem(asset: attachmentVideo))
videoView.playerLayer.player = player
player.play()
Where videoView is a PlayerView defined as:
class PlayerView: UIView {
var player: AVPlayer? {
get {
return playerLayer.player
}
set {
playerLayer.player = newValue
}
}
var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}
override class var layerClass : AnyClass {
return AVPlayerLayer.self
}
}
I don't specifically specify anywhere that the AVPlayer should be muted...

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