swiftui dealloc and realloc AVplayer with many videos - swift

I have many simultaneous videos. Through a Int var (var test1 and var test2) I would like to be able to add only a certain video and remove all the others so as not to have memory problems
As soon as the view is loaded, the value "nil" is assigned to each player and when test1 == test 2 it should load the video in a certain player and in the other "nil"
The problem is that despite being the testing variable in binding (struct VideoPlayer #Binding var testing) it does not update the state of the player which always remains in Nil
Below the best solution I have obtained so far
Some idea? Thank you all
struct CustomPlayer: View {
#Binding var test1:Int
#Binding var test2:Int
#State var path:String
#State var testing:AVPlayer? = nil
var body: some View {
if(test1 == test2 ) {
self.testing? = AVPlayer(url: URL(fileURLWithPath: Bundle.main.path(forResource: "\(path)", ofType: "mp4")!) )
self.testing?.play()
} else {
self.testing?.replaceCurrentItem(with: nil)
}
return ZStack{
VideoPlayer(testing: self.$testing)
}
struct VideoPlayer : UIViewControllerRepresentable {
#Binding var testing : AVPlayer?
func makeUIViewController(context: UIViewControllerRepresentableContext<VideoPlayer>) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = testing
controller.showsPlaybackControls = false
controller.view.backgroundColor = UIColor.white
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<VideoPlayer>) {
}
}

Here an example how to load multiple videos simultaneous (i added an autoplay feuature), and remove all other, but not the selected one.
To remove videos tap on video you want to save
And i'm not sure that i solve your initial problem, but this can give you a clue where to look next
Code can be copy/pasted, as i declare it in one file to convenient stackoverflow use
import SwiftUI
import AVKit
struct VideoModel: Identifiable {
let id: Int
let name: String
let type: String = ".mp4"
}
final class VideoData: ObservableObject {
#Published var videos = [VideoModel(id: 100, name: "video"),
VideoModel(id: 101, name: "wow"),
VideoModel(id: 102, name: "okay")]
}
//Multiple item player
struct MultipleVideoPlayer: View {
#EnvironmentObject var userData: VideoData
var body: some View {
VStack(alignment: .center, spacing: 8) {
ForEach(userData.videos) { video in
VideoPlayer(video: .constant(video))
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 250, maxHeight: 250, alignment: .center)
}
}
}
}
//Single item player
struct VideoPlayer : UIViewControllerRepresentable {
#EnvironmentObject var userData: VideoData
#Binding var video: VideoModel
func makeUIViewController(context: Context) -> AVPlayerViewController {
guard let path = Bundle.main.path(forResource: video.name, ofType: video.type) else {
fatalError("\(video.name)\(video.type) not found")
}
let url = URL(fileURLWithPath: path)
let playerItem = AVPlayerItem(url: url)
context.coordinator.player = AVPlayer(playerItem: playerItem)
context.coordinator.player?.isMuted = true
context.coordinator.player?.actionAtItemEnd = .none
NotificationCenter.default.addObserver(context.coordinator,
selector: #selector(Coordinator.playerItemDidReachEnd(notification:)),
name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
object: context.coordinator.player?.currentItem)
let controller = AVPlayerViewController()
controller.player = context.coordinator.player
controller.showsPlaybackControls = false
controller.view.backgroundColor = UIColor.white
controller.delegate = context.coordinator
let tapRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.playerDidTap))
controller.view.addGestureRecognizer(tapRecognizer)
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
uiViewController.player?.play()
}
func makeCoordinator() -> Coordinator {
let coord = Coordinator(self)
return coord
}
class Coordinator: NSObject, AVPlayerViewControllerDelegate {
var parent: VideoPlayer
var player: AVPlayer?
init(_ playerViewController: VideoPlayer) {
self.parent = playerViewController
}
#objc func playerItemDidReachEnd(notification: NSNotification) {
if let playerItem: AVPlayerItem = notification.object as? AVPlayerItem {
playerItem.seek(to: CMTime.zero, completionHandler: nil)
}
}
#objc func playerDidTap(){
parent.userData.videos = parent.userData.videos.filter { videoItem in
return videoItem.id == parent.video.id
}
}
}
}
//preview
struct AnotherEntry_Previews: PreviewProvider {
static var previews: some View {
MultipleVideoPlayer()
}
}
And in 'SceneDelegate.swift' replace app entry point with
window.rootViewController = UIHostingController(rootView: MultipleVideoPlayer().environmentObject(VideoData()))
The key thing of this move is to have "VideoData" resource, you can achieve it with EnvironmentObject, or with some other shared data example
Videos below i added to project, including them to Target Membership of a project
#Published var videos = [VideoModel(id: 100, name: "video"),
VideoModel(id: 101, name: "wow"),
VideoModel(id: 102, name: "okay")]

Your code seems to be ok. Are you sure the AVPlayer(...) path is correct
and testing is not nil. Put
.....
self.testing?.play()
print("-----> testing: \(testing.debugDescription)")
is testing = nil at that point?

Related

SwiftUI pass parameter to Class from UIViewRepresentable

I have a question that I think is quite simple and yet I get stuck, so I come to ask for help and maybe it could be other people !
I want to loop a video, but i've problem, i need to pass a var to a struct then a class like :
struct FinalPreview: View {
var url: URL
PlayerView(url: url)
.aspectRatio(contentMode: .fill)
.frame(width: size.width, height: size.height)
.clipShape(RoundedRectangle(cornerRadius: 30, style: .continuous))
.onAppear{
if player.currentItem == nil {
let item = AVPlayerItem(url: url)
player.replaceCurrentItem(with: item)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
player.play()
})
}
}
struct PlayerView: UIViewRepresentable {
var url: URL
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) {
}
func makeUIView(context: Context) -> UIView {
return PlayerUIView(frame: .zero)
}
}
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
// Load the resource
let fileUrl = "MY URL VAR FROM PlayerView"
// Setup the player
let player = AVPlayer(url: fileUrl)
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill
layer.addSublayer(playerLayer)
// Setup looping
player.actionAtItemEnd = .none
// Start the movie
player.play()
}
#objc
func playerItemDidReachEnd(notification: Notification) {
playerLayer.player?.seek(to: CMTime.zero)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
It's to loop a video.
I'm also looking to hide AVPlayer controls by using :
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) {
}
}
To hide the controls, it works very well, I don't know how to combine both...
Thank you.
You actually don't need 3 different types, just a single UIViewControllerRepresentable with a url:
import AVKit
import SwiftUI
struct PlayerView: UIViewControllerRepresentable {
var url: URL
func makeUIViewController(context: Context) -> AVPlayerViewController {
// create a looping auto-starting video player:
let player = AVPlayer(url: url)
player.actionAtItemEnd = .none
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main) { [weak player] _ in
player?.seek(to: .zero)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak player] in
player?.play()
}
// use that player in a view controller:
let controller = AVPlayerViewController()
controller.player = player
controller.videoGravity = .resizeAspectFill
controller.showsPlaybackControls = false
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {}
}
struct PlayerView_Previews: PreviewProvider {
static var previews: some View {
PlayerView(url: someVideoURL) // replace the URL with your own
}
}

AVPlayer audio only works when ringer is on

With SwiftUI, I have a custom avplayer that auto plays and loops the video. The problem is whether or not I specifically tell avplayer to unmute, it is still muted. The physical volume buttons have no effect. The only way to toggle mute on/off is to physically switch the ringer to silent (muted) or not silent (unmute).
Here is the parent view:
struct VideoCacheView: View {
#State private var avPlayer: AVPlayer? = nil
public let url: String
public let thumbnailURL: String
var body: some View {
if self.avPlayer != nil {
CustomVideoPlayer(player: Binding(self.$avPlayer)!)
.onAppear {
self.avPlayer?.isMuted = false
self.avPlayer?.play()
}
}
}
}
and the child:
struct CustomVideoPlayer: UIViewControllerRepresentable {
#EnvironmentObject var cvm: CameraViewModel
#Binding var player: AVPlayer
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.player = self.player
controller.showsPlaybackControls = false
controller.videoGravity = self.cvm.videoGravity
player.actionAtItemEnd = .none
NotificationCenter.default.addObserver(context.coordinator, selector: #selector(context.coordinator.restartPlayback), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
return controller
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { }
class Coordinator: NSObject {
public var parent: CustomVideoPlayer
init(_ parent: CustomVideoPlayer) {
self.parent = parent
}
#objc func restartPlayback () {
self.parent.player.seek(to: .zero)
}
}
}
Why is the only volume control my avplayer has is with the physicaly silent switch?
https://developer.apple.com/documentation/avfoundation/avplayer/1390127-volume
Turns out that the volume is set to 0.0 when ringer is in silent mode. By setting the volume to 1.0 by default, there is volume all the time.
Added this:
self.player?.volume = 1.0
inside of the child view below the videoGravity line

VideoPlayer Autoplay SwiftUI Issue

I am trying to get the VideoPlayer to autoplay with the video controller but I can't seem to figure it out! What should I do? I am fairly new to SwiftUI.
struct VideoView: View {
// MARK: - PROPERTIES
//: BINDING
#Binding var videoView: Bool
//: STATE
#State var player = AVPlayer()
#State var isplaying = true
#State var showcontrols = true
//: VAR
var allowsPictureInPicturePlayback : Bool = true
var wrkflw: Wrkflw
// MARK: - BODY
var body: some View {
VideoPlayer(wrkflw: wrkflw, player: $player)
.edgesIgnoringSafeArea(.all)
.onAppear {
player.play()
}
}
}
MARK: - VIDEO CONTROLLER
struct VideoPlayer : UIViewControllerRepresentable {
var wrkflw: Wrkflw
#Binding var player : AVPlayer
func makeUIViewController(context: Context) -> UIViewController {
let player = AVPlayer(url: wrkflw.wrkvideo)
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = true
controller.exitsFullScreenWhenPlaybackEnds = true
controller.allowsPictureInPicturePlayback = true
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}
The simplest solution would be to just pass the AVPlayer reference from your VideoView to the VideoPlayer. You can use player.replaceCurrentItem so that you can set the correct URL but maintain the same player reference.
Make sure you remove the let player... lines in VideoPlayer
struct VideoView: View { // MARK: - PROPERTIES
//: BINDING
#Binding var videoView: Bool
//: STATE
#State var player = AVPlayer()
#State var isplaying = true
#State var showcontrols = true
//: VAR
var allowsPictureInPicturePlayback : Bool = true
var wrkflw: Wrkflw
// MARK: - BODY
var body: some View {
VideoPlayer(wrkflw: wrkflw, player: player)
.edgesIgnoringSafeArea(.all)
.onAppear {
player.replaceCurrentItem(with: AVPlayerItem(url: wrkflw.wrkvideo)) //<-- Here
player.play()
}
}
}
// MARK: - VIDEO CONTROLLER
struct VideoPlayer : UIViewControllerRepresentable {
var wrkflw: Wrkflw
var player : AVPlayer
func makeUIViewController(context: Context) -> UIViewController {
//<-- Here
let controller = AVPlayerViewController()
controller.player = player
controller.showsPlaybackControls = true
controller.exitsFullScreenWhenPlaybackEnds = true
controller.allowsPictureInPicturePlayback = true
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}

When using combine in SwiftUI to update a Lottie Animation, it fails to update at the right time and gives an unexpected result

I have this ViewModel that uses combine along with a timer and I want this ViewModel to update a LottieView and filename with a new animation. When the timer counts down I want it to publish and send specific strings, these strings will be the json Lottie filenames. When my ContentView receives these filenames I want it to dynamically update the LottieViews animation.
So I made an #State variable called name inside ContentView and made that equal to the received value that is passed in. However, what confuses me is that the filename that is being published and sent from the ViewModels timer at the 10 second mark is suppose to be received and used inside LottieView(filename: name).
However this LottieView instantly runs this file when I launch the app. How so? The only place that filename exists in the whole app is when the timer reaches 10 seconds and it shouldn't even exist when LottieView(name) is called. It also ignores the previous file name that should have been run at the 19 second mark. If I were to ignore LottieView(name) all together and run a Text View instead, so in this case Text(name), when I run the app the Text properly changes when the timer reaches 10.
So how come LottieView(name) runs like this? I verified these files properly match their animations as well.
import SwiftUI
import Combine
class ViewModel: ObservableObject {
var anyCancellable: AnyCancellable?
let publish = PassthroughSubject<String, Never>()
private var timer: Timer?
private var scheduleTime = 20
init() {
fire()
anyCancellable = publish.sink { str in
print("Value that is being passed over: \(str)")
}
}
func fire() {
print("Fire timer")
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
self.scheduleTime -= 1
print(self.scheduleTime)
if self.scheduleTime == 19 {
self.publish.send("13865-sign-for-error-flat-style")
}
if self.scheduleTime == 10 {
self.publish.send("4174-unlock-to-premium")
timer.invalidate()
}
}
}
}
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var vm = ViewModel()
#State var name: String = ""
var body: some View {
VStack {
LottieView(filename: $name)
Text(name)
}
.onReceive(vm.publish, perform: { value in
print("passed over : \(value)")
name = value
print(name)
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
import Lottie
struct LottieView: UIViewRepresentable {
typealias UIViewType = UIView
#Binding var filename: String
func makeUIView(context: UIViewRepresentableContext<LottieView>) -> UIView {
let view = UIView(frame: .zero)
let animationView = AnimationView()
let animation = Animation.named(filename)
animationView.animation = animation
animationView.contentMode = .scaleAspectFit
animationView.play()
animationView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(animationView)
NSLayoutConstraint.activate([
animationView.widthAnchor.constraint(equalTo: view.widthAnchor),
animationView.heightAnchor.constraint(equalTo: view.heightAnchor),
animationView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
animationView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LottieView>) {
}
}
After a day of searching, without finding an answer, I probably came up with a not very correct solution, but it seems to work(saving temp fileName and after update LottieView checked name):
import SwiftUI
import Lottie
struct LottieView: UIViewRepresentable {
typealias UIViewType = UIView
var filename: String
var animationView = AnimationView()
let isPaused: Bool
var needUpdate: Bool = false {
didSet {
if needUpdate {
let animation = Animation.named(filename)
animationView.animation = animation
animationView.contentMode = .scaleAspectFit
animationView.loopMode = .loop
animationView.play()
needUpdate = false
}
}
}
func makeUIView(context: UIViewRepresentableContext<LottieView>) -> UIView {
let view = UIView(frame: .zero)
let animation = Animation.named(filename)
animationView.animation = animation
animationView.contentMode = .scaleAspectFit
animationView.loopMode = .loop
animationView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(animationView)
NSLayoutConstraint.activate([
animationView.widthAnchor.constraint(equalTo: view.widthAnchor),
animationView.heightAnchor.constraint(equalTo: view.heightAnchor),
])
animationView.play()
return view
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LottieView>) {
let tempFileName = context.coordinator.parent.filename
DispatchQueue.main.async {
context.coordinator.parent.filename = filename
if tempFileName != filename {
context.coordinator.parent.needUpdate = true
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject {
var parent: LottieView
init(_ parent: LottieView) {
self.parent = parent
}
}
}

Can't pause AVPlayer with custom button

I am trying to make a detailed view in swift, but I just can't figure out a way to pause the video with a custom button. And also when I go back to my list I can still hear the video playing in the background. Here is my code for the AVPlayer and for the button.
import SwiftUI
import AVKit
struct Workdetail: View {
var work: WorkoutDe
#State var player = AVPlayer()
#State var isplaying = true
var body: some View {
VStack {
ZStack {
VideoPlayer(player: $player, work: work)
.frame(height: UIScreen.main.bounds.height / 3.5)
Butto(player: $player, isplaying: $isplaying)
}
Spacer()
}
}
}
struct Butto : View {
#Binding var player : AVPlayer
#Binding var isplaying : Bool
var body : some View {
Button(action: {
if self.isplaying {
self.player.pause()
self.isplaying = false
} else {
self.player.play()
self.isplaying = true
}
}) {
Image(systemName: self.isplaying ? "pause.fill" : "play.fill")
.font(.title)
.foregroundColor(.white)
.padding(20)
}
}
}
struct VideoPlayer : UIViewControllerRepresentable {
var work : WorkoutDe
#Binding var player : AVPlayer
var playerLayer = AVPlayerLayer()
public func makeUIViewController(context: Context) -> AVPlayerViewController {
player = AVPlayer(url: URL(fileURLWithPath: String(work.url)))
let controller = AVPlayerViewController()
controller.player = player
controller.videoGravity = .resizeAspectFill
player.actionAtItemEnd = .none
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main) { _ in
player.seek(to: CMTime.zero)
player.play()
}
player.play()
return controller
}
func rewindVideo(notification: Notification) {
playerLayer.player?.seek(to: .zero)
}
public func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<VideoPlayer>) {
}
}
The AVPlayer works but when I press the button nothing happens. The image for the button changes but the video won't stop playing. Can someone please explain to me how I can bind the button, because I can't figure it out
You work with different players in your sub views, try
public func makeUIViewController(context: Context) -> AVPlayerViewController {
let player = AVPlayer(url: URL(fileURLWithPath: String(work.url)))
let controller = AVPlayerViewController()
DispatchQueue.main.async {
self.player = player
}
binding should update parent state, which will update Butto, so it should work.