Reputation: 4895
Context
I am implementing a video player that should expose play
, pause
mute
and unmute
functions. In my case it would be ideal if these boolean settings are passed in as bindings. I have a basic UIViewRepresentable
working here:
import SwiftUI
import AVKit
import UIKit
//MARK:- video player with play/pause API
struct MediaPlayerFeedItem : UIViewControllerRepresentable {
var urls: [String]
@Binding var shouldPlay: Bool
@Binding var shouldMute: Bool
var vc = AVPlayerViewController()
@State var pauseAll: Bool = false
//MARK:- API
// @use: play from beginning
func play(){
self._goPlay()
}
// @use: pause video
func pause(){
vc.player?.pause()
}
// @use respond to mute event
func mute(){
}
// @use: respond to umute event
func unmute(){
}
//MARK:- view
func makeUIViewController(context: Context) -> AVPlayerViewController {
if shouldPlay {
_goPlay()
} else {
let items = _makeItems()
if items.count > 0 {
let player = AVQueuePlayer(items: items)
vc.player = player
vc.showsPlaybackControls = false
vc.videoGravity = .resizeAspectFill
vc.player?.seek(to: .zero)
vc.player?.pause()
}
}
return vc
}
// @use: this fn will fire when `shouldPlay` is updated. at which point you can
// `play` or `pause` the video as desired
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
}
//MARK:- playback
private func _goPlay(){
let items = _makeItems()
guard items.count > 0 else { return }
let player = AVQueuePlayer(items: items)
vc.player = player
vc.showsPlaybackControls = false
vc.videoGravity = .resizeAspectFill
vc.player?.seek(to: .zero)
vc.player?.play()
NotificationCenter.default
.addObserver(
forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: items[items.count-1],
queue: .main
){_ in
self._goPlay()
}
}
// @use: make a new set of avplayeritems before each
// invocation of _goPlay loop
private func _makeItems() -> [AVPlayerItem] {
return urls
.map{ URL(string: $0) }
.filter{ $0 != nil }
.map{ AVPlayerItem(url: $0!) }
}
}
Use:
MediaPlayerFeedItem(urls: [url1,url2], shouldPlay: $shouldPlay, shouldMute: .constant(true) )
The issue is that I am not sure how to connect the UIViewRepresentable
with its binding values. That is, when shouldPlay
change to true, it would be nice if MediaPlayerFeedItem
just calls play()
by itself.
===================================================
Update
I forgot to mention that in an earlier implementation, I had this:
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
if shouldPlay {
play()
} else {
pause()
}
}
This is where the behavior of MediaPlayerFeedItem
is truly baffling. Both updateViewController
and its respective functions calls for play
or pause
fires as expected, but it doesn't seem to do anything in terms of playing or pausing the video.
For example, if init MediaPlayerFeedItem
with shouldPlay
set to a true value binding, then the video plays regardless of the pause command that happens later. If I init MediaPlayerFeedItem
to shouldPlay
set to false binding, and then set it it true again, the video does not play at all. The video loads, it just remains paused at the first frame.
I am inclined to say that the issue is in the _goPlay
function itself, but the video does play and loop if I just run _goPlay
in the makeUIViewController
function regardless of whether shouldPlay
is set to true.
Per @aheze suggestion, I added print statements in play
, pause
and _goPlay
functions, the behavior is as follows: @aheze when I init MediaFeedItem
with false binding, I am getting 3 pausing
prints on initialization. And then 10 seconds later when i set shouldPlay
to true, I get one play
print statement, and no subsequent pause
statement. The funnhy thing is that I am getting loop play
print statement as well from _goPlay
function in the NotificationCenter
call, so it means the video is playing .. it's just not rendering. Note I am initializing all new AVPlayerItem
list on each loop, this is needed because i cannot share them across different playing loops.
Upvotes: 2
Views: 2101
Reputation: 4895
Figured it out:
struct VideoPlayerView: UIViewRepresentable {
var urls: [URL]
var playOnLoad: Bool
@EnvironmentObject var appState: AppState
func makeUIView(context: Context) -> UIView {
return PlayerUIViewQueue(urls: urls, playOnLoad: playOnLoad, frame: .zero)
}
// @use: on `appState` context change, `play` `pause` or `resume` the video
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<VideoPlayerView>) {
if let uiv = uiView as? PlayerUIViewQueue {
switch appState.getTokenPlayCommand() {
case .pause:
uiv.pause()
case .play:
uiv.playFromBeginning()
case .resume:
uiv.resume()
}
}
}
}
class PlayerUIViewQueue: UIView {
private var URLs: [URL]
// player references
private let playerLayer = AVPlayerLayer()
private var current_mutli : AVQueuePlayer? = nil
private var current_single: AVPlayer? = nil
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
init(urls: [URL], playOnLoad:Bool, frame: CGRect){
self.URLs = urls
super.init(frame: frame)
if self.URLs.count == 1 {
print("if case")
initSinglePlayer()
} else {
print("else case")
loopAll()
}
}
//MARK:- API
func resume(){
print("RESUMNING")
current_mutli?.play()
current_single?.play()
}
func pause(){
print("PAUSING")
current_mutli?.pause()
current_single?.pause()
}
func playFromBeginning(){
print("playFromBeginning")
current_mutli?.seek(to: .zero)
current_mutli?.play()
current_single?.seek(to: .zero)
current_single?.play()
}
//MARK:- player utils
private func initSinglePlayer(){
if URLs.count == 0 { return }
let player = AVPlayer(url: URLs[0])
player.actionAtItemEnd = .none
self.current_single = player
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill
layer.addSublayer(playerLayer)
player.play()
NotificationCenter.default
.addObserver(
self,
selector: #selector(loopPlayerSingle(notification:)),
name: .AVPlayerItemDidPlayToEndTime,
object: player.currentItem
)
}
@objc func loopPlayerSingle(notification: Notification) {
if let playerItem = notification.object as? AVPlayerItem {
playerItem.seek(to: .zero, completionHandler: nil)
}
}
// @use: on each `loopAll()` invocation, reinit
// the player with all video `items`
private func loopAll(){
let items = URLs.map{ AVPlayerItem.init(url: $0) }
let player = AVQueuePlayer(items: items)
self.playerLayer.player = player
self.current_mutli = player
player.seek(to: .zero)
player.play()
playerLayer.videoGravity = .resizeAspectFill
layer.addSublayer(playerLayer)
NotificationCenter.default
.addObserver(
self,
selector: #selector(loopPlayerMulti(notification:)),
name: .AVPlayerItemDidPlayToEndTime,
object: player.items().last
)
}
@objc private func loopPlayerMulti(notification: Notification) {
loopAll()
}
}
Upvotes: 0
Reputation: 30506
Make sure to check out the comments:
// @use: this fn will fire when `shouldPlay` is updated. at which point you can
// `play` or `pause` the video as desired
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
As the comments say, whenever shouldPlay
is updated, updateUIViewController
will be called. So, you could do something like this:
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
if shouldPlay {
play()
} else {
pause()
}
}
Upvotes: 2