home ~ projects ~ socials

Make A Custom Play Button For A macOS SwiftUI Video Player

The default VideoPlayer in macOS SwiftUI tries to scrub left and right if your mouse is over it and you try to scroll up and down. I'm getting around that by removing the native controls and adding my own play button.

import AVKit
import SwiftUI

struct ContentView: View {
    @State var player = AVPlayer(url: Bundle.main.url(forResource: "example-video", withExtension: "mp4")!)
    
    @State var isPlaying: Bool = false
    
    let playerDidFinishNotification = NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime)
    
    var body: some View {
        ScrollView {
            VStack {
                AVPlayerControllerRepresented(player: player)
                    .frame(width: 400, height: 260)
                    .disabled(true)
                Button {
                    if isPlaying == true {
                        player.pause()
                    } else {
                        player.seek(to: .zero)
                        player.play()
                    }
                    isPlaying.toggle()
                } label: {
                    Image(systemName: isPlaying ? "stop" : "play")
                        .padding()
                }.onReceive(playerDidFinishNotification, perform: { _ in
                    isPlaying = false
                })
            }
        }.padding()
    }
}

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

#Preview {
    ContentView()
}

TODO

Add in something like this code which checks to make sure the video file is there (most of the time it's probably not as necessary if you're bundling the videos directly. Still seems like a good backstop to have in place)

if let url = Bundle.main.url(forResource: "\(exampleItem.basename)-video", withExtension: "mp4") {
            VideoPlayer(player: AVPlayer(url: url))
            //                                        .containerRelativeFrame(.vertical, count: 5, span: 2, spacing: 2)
            //                                        .ignoresSafeArea()
            //                                        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
                .frame(height: 280)
                .padding()
        } else {
            Text("No video")
        }
-- end of line --

Endnotes

I don't need scrubbing in the video for my current task of building AutoTyper. There will be times when I do though and this approach would need to have a more complete set of controlls added (or another approach would need to be used)

I'd also want pause as well as stop/start-over if the clips I was working with were longer. The basics in place for this approach should provide a good start for that

References

This is the main one that I used for reference.

This shows a basic play/stop button but the button doesn't change back from stop to play when the end of the video is reached.

This is where I found the sample to get started with switching the play/stop button at the end of the video.

No real help here. Something to investigate for the future to see what else is available

This is about an older version of iOS, but the .disabled(true) was a final piece to prevent the default scrubbing behavior when the scroll wheel is used on the mouse while over the video