0

Using Swift5.3.2, iOS14.4.1, Xcode 12.4,

I am successfully running a VideoPlayer in SwiftUI.

I am calling the Player view with this code: VideoPlayer(player: AVPlayer(url: url)).

The problem is that the video stops playing whenever a parent-View of the VideoPlayer updates (i.e. re-renders).

Since in SwiftUI I don't have any control over when such a re-render moment takes place, I don't know how to overcome this problem.

Any ideas ?

Here is the entire Code:

The VideoPlayer View is called as such:

struct MediaTabView: View {
    
    @State private var url: URL
    
    var body: some View {
        
        // CALL TO VIDEOPLAYER IS HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        VideoPlayer(player: AVPlayer(url: url))
    }
}

The MediaTabView is called as such:

import SwiftUI

struct PageViewiOS: View {
    
    var body: some View {
        
        ZStack {
            
            Color.black
            
            // CALL TO MEDIATABVIEW IS HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!d
            MediaTabView(url: URL(string: "https://someurel.com"))
                
            CloseButtonView()
        }
    }
}

The PageViewiOS View is called as such:

struct MainView: View {
    
    @EnvironmentObject var someState: AppStateService
    
    var body: some View {
        NavigationView {
            Group {                                                    
                if someState = .stateOne {

                    // CALL TO PAGEVIEWIOS IS HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
                    PageViewiOS()
                } else {
                    Text("hello")
                }
            }
        }
    }
}
iKK
  • 6,394
  • 10
  • 58
  • 131
  • can you show the codes make this issue please? – ios coder Mar 17 '21 at 23:55
  • @swiftPunk, please see entire code in original post – iKK Mar 18 '21 at 00:03
  • The MainView is the parent-View at question that re-renders when an Environment-property changes. However, during that time the video is still playing (inside a child of a child-View). And that is the moment the Video stops playing, unfortunately. Any ideas on how to overcome this ?? – iKK Mar 18 '21 at 00:04
  • Any chance you could make a minimal reproducible example? – jnpdx Mar 18 '21 at 00:46
  • @jnpdx, I changed the example to be much more compact. And I found a workaround in the meantime (see answer below). Maybe you know why this workaround works ?? – iKK Mar 18 '21 at 00:56

3 Answers3

2

With the solution from @jnpdx, everything works now.

Here is the final solution (full credit to @jnpdx):

import SwiftUI
import AVKit

class PlayerViewModel: ObservableObject {
    
    @Published var avPlayer: AVPlayer?
    
    func loadFromUrl(url: URL) {
        avPlayer = AVPlayer(url: url)
    }
}

struct CustomPlayerView: View {
    
    var url : URL
    @StateObject private var playerViewModel = PlayerViewModel()

    var body: some View {
        ZStack {
            if let avPlayer = playerViewModel.avPlayer {
                VideoPlayer(player: avPlayer)
            }
        }.onAppear {
            playerViewModel.loadFromUrl(url: url)
        }
    }
}

With that in hand, it is enough to call the CustomPlayerVideo like that:

CustomPlayerView(url: url)

Remark: I needed to use ZStack instead of Group in my CustomPlayerView in order for it to work.

iKK
  • 6,394
  • 10
  • 58
  • 131
0

I have found a solution.

Call the following :

CustomPlayerView(url: url)

...instead of :

VideoPlayer(player: AVPlayer(url: url))

Not sure why this works, tough.

Maybe somebody can explain further ?

Here is the CustomVideoPlayer code:

struct CustomPlayerView: View {

    private let url: URL

    init(url: URL) {
        self.url = url
    }

    var body: some View {
        VideoPlayer(player: AVPlayer(url: url))
    }
}

With this minor change, the Video keeps on playing even tough the parent-View gets re-rendered. Still, not sure why ???

----------- Answer with the hint of @jnpdx --------

I changed the CustomVideoPlayer even more :

CustomPlayerView(playerViewModel: PlayerViewModel(avPlayer: AVPlayer(url: url)))
import SwiftUI
import AVKit

class PlayerViewModel: ObservableObject {
    
    @Published var avPlayer: AVPlayer
    
    init(avPlayer: AVPlayer) {
        self.avPlayer = avPlayer
    }
}

struct CustomPlayerView: View {
    
    @StateObject var playerViewModel: PlayerViewModel

    var body: some View {
        VideoPlayer(player: playerViewModel.avPlayer)
    }
}
iKK
  • 6,394
  • 10
  • 58
  • 131
  • 1
    In your original example, you were recreating the AVPlayer object every time the view rendered (which could potentially happen every time the parent renders, depending on if the parameters change). In your new version, because `CustomPlayerView` only takes a `URL` as a parameter, SwiftUI would not rerender it in most circumstances because it does a simple diff on the parameters and sees that `url` hasn't changed -- thus, no rerender. – jnpdx Mar 18 '21 at 00:59
  • wow - excellent explanation. I did not know about the diff-mechanism in SwiftUI or how re-render decisions are made since I just started with SwiftUI. Makes a lot of sense to me and I will give it a good go to learn more about this new approach in iOS. Thank you for your helpful answer. – iKK Mar 18 '21 at 01:02
  • You might want a more robust solution for this. For example, my guess your video would restart changing from landscape to portrait orientation (I could be wrong). I'd store the `AVPlayer` in a `@State` object or (more likely) in an ObservableObject stored as a `@StateObject` – jnpdx Mar 18 '21 at 01:07
  • Thank you, I will try this. (...as for Landscape/Portrait: no it keeps playing with the CustomVideoPlayer....). Can you make an example on how this would look like ? – iKK Mar 18 '21 at 01:10
  • @jnpdx, did you mean as such (see my additional answer).... – iKK Mar 18 '21 at 01:33
  • That's one way -- I might choose to do it with still just passing `CustomVideoPlayer` a URL but then calling a `loadVideo` function or something like that on the video model on `onAppear` to create the AVPlayer object. And conditionally display the VideoPlayer (only if there's been an `AVPlayer` created) – jnpdx Mar 18 '21 at 01:36
  • now you lost me. Can you please make an example. I am not sure where the AVPlayer is created and also, what exactly needs to be placed inside onAppear ?? – iKK Mar 18 '21 at 01:49
  • Thank you very much - makes much more sense now. – iKK Mar 18 '21 at 01:56
  • If that other answer is helpful, perhaps consider upvoting it – jnpdx Mar 18 '21 at 01:57
  • actually, I just implemented it and it is not working - with your solution, the play button is no longer visible ! Maybe the `onAppear()` is too late or why is it that the play button is lost now ?? – iKK Mar 18 '21 at 01:59
  • 1
    My answer doesn't have anything to do with a play button... I just ran it in a test on my device and a play button appears just fine. – jnpdx Mar 18 '21 at 02:02
  • on my iPad the play button no longer appears, unfortunately... I get an all black screen. – iKK Mar 18 '21 at 02:02
  • But the entire video frame is still there? – jnpdx Mar 18 '21 at 02:04
  • nope - there is only a black screen. No video frame anymore... – iKK Mar 18 '21 at 02:04
  • 1
    Sounds like a layout issue to me -- it must be laying it out before the video appears. Can't debug that unless I have the code necessary to reproduce it. It sounds like you have a solution anyway. – jnpdx Mar 18 '21 at 02:05
  • Yes, I am going back to the solution I placed below the dotted line. Thank you so much for your time. It all comes a bit clearer now. Thank you – iKK Mar 18 '21 at 02:09
  • Oh, I just found the issue with your last solution. Please find my extra answer from your code just a minute ago. The solution was to use `ZStack` instead of `Group`. No more all-black screen with `ZStack` - and also, the play button is there again.... Not sure why this difference occurs in my View-Stack. – iKK Mar 18 '21 at 02:19
  • Yeah, like I suspected, just a layout issue. I've undeleted my answer -- perhaps you can upvote now. – jnpdx Mar 18 '21 at 02:36
0

This is in response to our comment thread on the other answer:

class PlayerViewModel: ObservableObject {
    
    @Published var avPlayer: AVPlayer?
    
    func loadFromUrl(url: URL) {
        avPlayer = AVPlayer(url: url)
    }
}

struct CustomPlayerView: View {
    
    var url : URL
    @StateObject private var playerViewModel = PlayerViewModel()

    var body: some View {
        ZStack {
            if let avPlayer = playerViewModel.avPlayer {
                VideoPlayer(player: avPlayer)
            }
        }.onAppear {
            playerViewModel.loadFromUrl(url: url)
        }
    }
}

I'm not sure that this is definitively better, so it's worth testing. But, it does control when AVPlayer gets created and avoids re-creating PlayerViewModel on every render of the parent as well.

jnpdx
  • 45,847
  • 6
  • 64
  • 94