UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SwiftUI VideoPlayer leaking (@State management issue)

Forums > SwiftUI

I’m trying to create a looping video player view. Based on Apple’s docs, I did this:

struct
VideoPlayerView: View
{
    let video           :   Video
    let isPlaying       :   Bool

    init(video: Video, isPlaying: Bool)
    {
        Self.logger.info("VideoPlayerView.init: \(video.name)")

        self.video = video
        self.isPlaying = isPlaying

        if let url = self.video.url
        {
            let playerItem = AVPlayerItem(url: url)
            self._player = State(initialValue: AVQueuePlayer(playerItem: playerItem))
            self._looper = State(initialValue: AVPlayerLooper(player: self.player!, templateItem: playerItem))
        }
    }

    var
    body: some View
    {
        let _ = print("VideoPlayerView body requested: “\(self.video.name)” [\(self.namespace)]", terminator: " -- ")
        let _ = Self._printChanges()

        VStack
        {
            if let player = self.player
            {
                VideoPlayer(player: player)
            }
            else
            {
                Text("Unable to create player.")
            }
        }
        .onAppear()
        {
            Self.logger.info("Player appeared: \(self.video.name)")
            self.isPlaying ? self.player?.play() : self.player?.pause()
        }
        .onDisappear()
        {
            Self.logger.info("Player disappeared: \(self.video.name)")
            self.player?.pause()
            self.player?.seek(to: .zero)
        }
        .onChange(of: self.isPlaying)
        { inOld, inNew in
            Self.logger.info("isPlaying changed: \(inNew)")
            inNew ? self.player?.play() : self.player?.pause()
        }
    }

    @State  private var player          :   AVQueuePlayer?
    @State  private var looper          :   AVPlayerLooper?

    @Namespace private var namespace

    static  let logger          =   Logger(subsystem: "<>", category: "VideoPlayerView")
}

I instantiate it as part of a NavigationSplitView detail, based on the current selection:

        detail:
        {
            let sortedSelection = self.selection.sorted(using: SortDescriptor(\.lastViewed))
            if sortedSelection.count > 0
            {
                ScrollView
                {
                    VStack
                    {
                        ForEach(sortedSelection)
                        { video in
                            VideoPlayerView(video: video, isPlaying: self.isPlaying)
                                .id(video)
                                .aspectRatio(4.0/3.0, contentMode: .fit)
                        }
                    }
                }
            }
            else
            {
                Text("Select an Item")
            }
        }
@Model
final
class
Video
{
    var name            :   String
    var bookmark        :   Data
    var lastViewed      :   Date?

    var
    url: URL?
    {
        <get url from bookmark>
    }

There are problems with this approach:

  • If a video is playing when another is selected, the first video’s view disappears, but you can hear the audio continue to play (until I called pause() in .onDisappear(), anyway).
  • Every time something changes, like whether or not videos are playing, this code creates a new suite of AVFoundation objects. I don’t actually know what @State does in this situation. Does it just discard the new initial value, since state already exists for this view?
  • Looking at memory consumption and thread creation, the answer to this is no. Every time I stop and start a video playing, memory goes up, and a few new threads are created.

I can’t think of a better way to manage this player state. Assigning state in .onAppear() and set it to nil in .onDisppear() doesn’t change the behavior; memory still grows and threads continue to be created.

2      

For every item in your NavigationSplitView a new VideoPlayerView is created. The old one isn't reused so to speak. I don't know when the previous instances are killed from memory. There is some mechanic which reuses cells but I don't know how this works. This is done by the OS itself.

What you can do (and IMHO should do) is to refactor your "Player" in an own class and use it as an environment object. This class should be responsible for playing and stopping the video. In this case you only have one instance of the player. You have to implement some logic for swapping the video though. Your VideoPlayerView has to use the newly created player class instead of the video directly.

2      

It's actually not creating a VideoPlayerView for every item. It creates one for every item in the selection. But the problem is that SwiftUI constantly re-creates the view (every time something changes), which means the constructor gets called, which means the suite of AVFoundation objects (AVPlayerItem, AVQueuePlayer, and AVPlayerLooper) get instantiated every time.

I think I need to create a new struct containing those, and instantiate it in the @State declaration. I'll post again if that works.

2      

One thing that greatly improved things: I create the AV objects in .task {}. That seems to more reliably be called only once when the view appears. But I'm still going to package up the AV objects in a struct and keep them in a separate data structure from the view.

2      

TAKE YOUR SKILLS TO THE NEXT LEVEL If you like Hacking with Swift, you'll love Hacking with Swift+ – it's my premium service where you can learn advanced Swift and SwiftUI, functional programming, algorithms, and more. Plus it comes with stacks of benefits, including monthly live streams, downloadable projects, a 20% discount on all books, and free gifts!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.