You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义AVPlayer接入SharePlay后如何实现播放控制与进度同步

问题原因

同步失效的核心bug在自定义播放器封装层:

  • CustomVideoPlayer中的player是计算属性,每一次访问都会生成全新的AVPlayer实例
  • SwiftUI刷新视图、updateUIViewController触发时,会不断给AVPlayerViewController替换新创建的player实例,你在configureGroupSession中给旧player绑定的playbackCoordinator同播协调关系会直接失效,新的player没有加入同播会话,自然无法同步暂停、拖动进度等操作。
  • 切换播放资源时如果新建player实例,也会直接断开同播协调链路。
修复步骤
  1. 持久化唯一AVPlayer实例,全程复用,禁止在播放流程中随意新建player
  2. 把AVPlayer的管理逻辑上移到EpisodeDetail层级,避免UIViewControllerRepresentable生命周期变化导致player重建
  3. 同播协调逻辑始终绑定这一个持久化的player实例
  4. 切换播放资源时仅调用replaceCurrentItem替换播放项,不新建player

修正后的代码

1. 重构CustomVideoPlayer

移除内部新建player的逻辑,改为接收外部传入的持久化player实例:

import SwiftUI
import AVFoundation
import AVKit

struct CustomVideoPlayer: UIViewControllerRepresentable {
    let player: AVPlayer
    
    func makeUIViewController(context: Context) -> AVPlayerViewController {
        let controller = AVPlayerViewController()
        controller.modalPresentationStyle = .fullScreen
        controller.allowsPictureInPicturePlayback = true
        controller.canStartPictureInPictureAutomaticallyFromInline = true
        controller.player = player
        return controller
    }
    
    func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {
        // 仅当player实例变化时才重新赋值,避免重复设置
        guard playerController.player !== player else { return }
        playerController.player = player
    }
}

2. 调整EpisodeDetail中的player管理逻辑

把AVPlayer作为单独的状态持久化,初始化时和自定义播放器绑定同一个实例,同播协调逻辑直接绑定这个持久化实例:

import SwiftUI
import AVFoundation
import AVKit
import GroupActivities
import Combine

struct EpisodeDetail: View {
    @State var episode: CommonResponse<Episode>
    // 全局唯一持久化player实例,全程复用不重建
    @State private var player: AVPlayer
    @State private var videoPlayer: CustomVideoPlayer
    @State private var groupSession: GroupSession<EpisodeActivity>?
    @State private var subscriptions = Set<AnyCancellable>()
    
    init(episode: CommonResponse<Episode>) {
        self._episode = State(initialValue: episode)
        let sharedPlayer = AVPlayer()
        self._player = State(initialValue: sharedPlayer)
        self._videoPlayer = State(initialValue: CustomVideoPlayer(player: sharedPlayer))
    }
    
    var body: some View {
        VStack {
            videoPlayer
                .transition(.move(edge: .bottom))
                .edgesIgnoringSafeArea(.all)
            
            ScrollView {
                VStack(alignment: .center) {
                    Button {
                        prepareToPlay(episode.attributes)
                    } label: {
                        Label("同播共享", systemImage: "shareplay")
                    }
                    .buttonStyle(.bordered)
                    .cornerRadius(20)
                }
                
                VStack(alignment: .leading, spacing: 20) {
                    Text(episode.attributes.title)
                        .font(.title2)
                        .fontWeight(.bold)
                    
                    Text(episode.attributes.description)
                        .font(.body)
                        .foregroundColor(.gray)
                }
            }
            .padding(12)
        }
        .task {
            for await session in EpisodeActivity.sessions() {
                configureGroupSession(session)
            }
        }
    }

    private func configureGroupSession(_ session: GroupSession<EpisodeActivity>) {
        groupSession = session
        // 绑定持久化的唯一player实例,协调关系不会因视图刷新断开
        player.playbackCoordinator.coordinateWithSession(session)

        session.$state
            .sink { state in
                if case .invalidated = state {
                    groupSession = nil
                    subscriptions.removeAll()
                }
            }
            .store(in: &subscriptions)

        session.$activity
            .sink { activity in
                print("Activity Changed: \(activity.metadata.title ?? "无标题")")
            }
            .store(in: &subscriptions)

        session.join()
    }

    private func prepareToPlay(_ playerEpisodeData: Episode) {
        let activity = EpisodeActivity(episode: playerEpisodeData)
        // 复用现有player替换播放项,不新建实例
        player.replaceCurrentItem(with: AVPlayerItem(url: playerEpisodeData.videoUrl))

        Task {
            switch await activity.prepareForActivation() {
            case .activationDisabled:
                break
            case .activationPreferred:
                _ = try await activity.activate()
            case .cancelled:
                break
            @unknown default:
                break
            }
        }
    }
}
验证注意事项
  • 确保EpisodeActivity正确实现GroupActivity协议,关联的元数据配置无误
  • 测试设备需升级到iOS 15及以上版本,参与同播的设备登录了有效Apple ID,且已开启同播共享功能
  • 播放流程中不要调用AVPlayer()初始化方法新建实例,所有播放操作都基于持久化的那个player属性执行

内容的提问来源于stack exchange,提问作者Jimmy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 17:27:22