Swift UI 自定义视频播放器控件不起作用

问题描述 投票:0回答:1

我有一个自定义视频播放器,可以在 swift ui 视图中使用。我希望能够从 swift ui 视图播放/暂停、静音/取消静音以及快进视频。目前所有控件功能均无法正常工作,视频播放器工作正常,但当我尝试设置 self.videoDuration = item.duration.seconds 时,出现错误“视图更新时无法发布更改”。如何正确实现播放/暂停、静音/取消静音、快进?

import Foundation
import SwiftUI
import AVKit

struct UserStories: View {
    @State private var isPlaying = true
    @State private var videoDuration: Double = 0
    @State private var isMuted = false

    var body: some View {
        VStack {
            LegacyVideoPlayer(url: URL(string: "")!, isPlaying: $isPlaying, videoDuration: $videoDuration, isMuted: $isMuted)

            HStack {
                Button("Play") {
                    isPlaying = true
                }

                Button("Pause") {
                    isPlaying = false
                }
                Button("Toggle Mute") {
                    isMuted.toggle()
                }
            }
            Text("Video Duration: \(videoDuration)")
        }
    }
}

public struct LegacyVideoPlayer: UIViewControllerRepresentable {
    let url: URL
    @Binding var isPlaying: Bool
    @Binding var videoDuration: Double
    @Binding var isMuted: Bool

    public func makeCoordinator() -> CustomPlayerCoordinator {
        CustomPlayerCoordinator(customPlayer: self)
    }

    public func makeUIViewController(context: UIViewControllerRepresentableContext<LegacyVideoPlayer>) -> LegacyAVPlayerViewController {
        let controller = LegacyAVPlayerViewController(videoDuration: $videoDuration, isMuted: $isMuted)
        controller.delegate = context.coordinator
        makeAVPlayer(in: controller, context: context)
        playIfNeeded(controller.player)

        return controller
    }

    public func updateUIViewController(_ uiViewController: LegacyAVPlayerViewController, context: UIViewControllerRepresentableContext<LegacyVideoPlayer>) {
        makeAVPlayer(in: uiViewController, context: context)
        playIfNeeded(uiViewController.player)
        uiViewController.isMuted = isMuted
    }

    private func makeAVPlayer(in controller: LegacyAVPlayerViewController, context: UIViewControllerRepresentableContext<LegacyVideoPlayer>) {
       
        let item = AVPlayerItem(url: url)
        let player = AVQueuePlayer(playerItem: item)
        let loopingPlayer = AVPlayerLooper(player: player, templateItem: item)
        controller.videoGravity = AVLayerVideoGravity.resizeAspectFill
        context.coordinator.loopingPlayer = loopingPlayer
        controller.player = player

        controller.showsPlaybackControls = false
    }

    private func playIfNeeded(_ player: AVPlayer?) {
        if isPlaying { player?.play() }
        else { player?.pause() }
    }
}


public class LegacyAVPlayerViewController: AVPlayerViewController {
    @Binding var videoDuration: Double
    @Binding var isMuted: Bool

    private var rateObserver: NSKeyValueObservation?
    private var durationObserver: NSKeyValueObservation?

    public override var player: AVPlayer? {
        willSet {
            rateObserver?.invalidate()
            durationObserver?.invalidate()
        }
        didSet {
            if rateObserver == nil {
                rateObserver = player?.observe(\AVPlayer.rate, options: [.new], changeHandler: rateHandler(_:change:))
            }
        }
    }

    deinit {
        rateObserver?.invalidate()
        durationObserver?.invalidate()
    }

    private func rateHandler(_ player: AVPlayer, change: NSKeyValueObservedChange<Float>) {
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }

            guard let item = player.currentItem,
                  item.currentTime().seconds > 0.5,
                  player.status == .readyToPlay
            else { return }

            self.videoDuration = item.duration.seconds
        }
    }

    public override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        player?.pause()
    }

    init(videoDuration: Binding<Double>, isMuted: Binding<Bool>) {
        _videoDuration = videoDuration
        _isMuted = isMuted
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

public class CustomPlayerCoordinator: NSObject, AVPlayerViewControllerDelegate, AVPictureInPictureControllerDelegate {
    let customPlayer: LegacyVideoPlayer

    public init(customPlayer: LegacyVideoPlayer) {
        self.customPlayer = customPlayer
        super.init()
    }

    public func playerViewController(_ playerViewController: AVPlayerViewController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
        completionHandler(true)
    }
}
ios swift swiftui avfoundation avplayer
1个回答
0
投票

您的 SwiftUI 视频播放器设置看起来相当复杂:

+---------------------------------------------+
| UserStories (SwiftUI View)                  |
| +-----------------------------------------+ |
| | LegacyVideoPlayer (UIViewControllerRep) | |
| | +-------------------------------------+ | |
| | | LegacyAVPlayerViewController        | | |
| | | (AVPlayerViewController)            | | |
| | | + Player (AVPlayer)                 | | |
| | | + Video Duration (Binding<Double>)  | | |
| | | + Is Muted (Binding<Bool>)          | | |
| | +-------------------------------------+ | |
| +-----------------------------------------+ |
| + HStack for Play/Pause/Mute buttons        |
+---------------------------------------------+

播放/暂停控件似乎在

UserStories
视图中得到了正确处理。
isPlaying
状态绑定到
LegacyVideoPlayer
,更改此状态应播放或暂停视频。
(另请参阅
perimeter-inc/LegacyVideoPlayer
,了解 SwiftUI 视频播放器的另一个示例,以获取想法/灵感。)

isMuted
绑定应控制视频的静音。确保此状态正确反映在
AVPlayer
实例中。

您当前的代码中未实现快进控件。您将需要一种方法来更改视频的播放位置。

错误“

cannot Publish changes when the view updates
”可能意味着在视图更新期间更新SwiftUI状态出现问题。这可以通过确保在主线程上和视图主体之外进行状态更改来解决。


因此,尝试在

UserStories
中添加一个用于快进的按钮,并在
LegacyVideoPlayer
中添加一个方法来处理此问题。
修改持续时间更新逻辑,确保它在主线程上完成,并且以不与 SwiftUI 的视图更新冲突的方式完成。

UserStories

Button("Fast Forward 10s") {
    legacyVideoPlayerRef.fastForward(seconds: 10)
}

LegacyVideoPlayer

// Add a reference to the controller
private var playerController: LegacyAVPlayerViewController?

public func fastForward(seconds: Double) {
    guard let currentPlayer = playerController?.player else { return }
    let currentTime = currentPlayer.currentTime()
    let newTime = CMTimeAdd(currentTime, CMTimeMakeWithSeconds(seconds, preferredTimescale: currentTime.timescale))
    currentPlayer.seek(to: newTime)
}

// In makeUIViewController:
playerController = controller

LegacyAVPlayerViewController
中,修改
rateHandler
以确保持续时间正确更新:

private func rateHandler(_ player: AVPlayer, change: NSKeyValueObservedChange<Float>) {
    DispatchQueue.main.async { [weak self] in
        guard let self = self, let item = player.currentItem, item.currentTime().seconds > 0.5, player.status == .readyToPlay else { return }
        if !self.isDurationSet {
            self.videoDuration = item.duration.seconds
            self.isDurationSet = true
        }
    }
}

添加标记

isDurationSet
以防止多次更新。

© www.soinside.com 2019 - 2024. All rights reserved.