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

如何使用Combine开发AVPlayerItem观察者?寻求通用实现方案

通用Combine风格的AVPlayerItem观察者实现

一、基于KVO的通用Publisher扩展

利用Combine原生的KVO支持,为AVPlayerItem封装通用的属性监听Publisher,无需手动维护PassthroughSubject的生命周期:

import Combine
import AVFoundation

extension AVPlayerItem {
    // 快捷监听播放状态
    var statusPublisher: AnyPublisher<AVPlayerItem.Status, Never> {
        publisher(for: \.status, options: [.initial, .new])
            .eraseToAnyPublisher()
    }
    
    // 通用属性监听方法,支持任意KVO兼容的属性
    func publisher<Value>(for keyPath: KeyPath<AVPlayerItem, Value>) -> AnyPublisher<Value, Never> {
        publisher(for: keyPath, options: [.initial, .new])
            .map { $0 }
            .eraseToAnyPublisher()
    }
    
    // 监听播放进度(结合播放状态自动启停定时更新)
    var playbackProgressPublisher: AnyPublisher<CMTime, Never> {
        let statusTrigger = publisher(for: \.timeControlStatus)
        let periodicUpdates = Timer.publish(every: 0.1, on: .main, in: .common)
            .autoconnect()
            .map { [weak self] _ in self?.currentTime() ?? .zero }
        
        return statusTrigger
            .flatMap { [weak self] status -> AnyPublisher<CMTime, Never> in
                guard let self = self else { return Just(.zero).eraseToAnyPublisher() }
                switch status {
                case .playing:
                    return periodicUpdates.eraseToAnyPublisher()
                default:
                    return Just(self.currentTime()).eraseToAnyPublisher()
                }
            }
            .eraseToAnyPublisher()
    }
}

二、AVPlayerItem通知的通用封装

针对AVPlayerItem发送的系统通知,封装成绑定当前item的专属Publisher:

extension AVPlayerItem {
    // 通用通知监听方法
    func notificationPublisher(for name: Notification.Name) -> AnyPublisher<Notification, Never> {
        NotificationCenter.default.publisher(for: name, object: self)
            .eraseToAnyPublisher()
    }
    
    // 快捷监听播放结束
    var didPlayToEndTimePublisher: AnyPublisher<Notification, Never> {
        notificationPublisher(for: .AVPlayerItemDidPlayToEndTime)
    }
    
    // 快捷监听加载进度变化
    var loadedTimeRangesChangedPublisher: AnyPublisher<Notification, Never> {
        notificationPublisher(for: .AVPlayerItemNewLoadedTimeRanges)
    }
}

三、使用示例

SwiftUI场景

import SwiftUI

struct PlayerStatusView: View {
    let playerItem: AVPlayerItem
    @State private var status: AVPlayerItem.Status = .unknown
    @State private var currentProgress: Double = 0
    private var cancellables = Set<AnyCancellable>()
    
    var body: some View {
        VStack(spacing: 16) {
            Text("播放状态:\(status.displayText)")
            Text("当前进度:\(String(format: "%.1f", currentProgress))秒")
        }
        .onAppear {
            // 绑定播放状态
            playerItem.statusPublisher
                .assign(to: \.status, on: self)
                .store(in: &cancellables)
            
            // 监听播放进度
            playerItem.playbackProgressPublisher
                .map { $0.seconds }
                .assign(to: \.currentProgress, on: self)
                .store(in: &cancellables)
            
            // 监听播放结束
            playerItem.didPlayToEndTimePublisher
                .sink { _ in
                    print("播放完成,可执行重播/切换逻辑")
                }
                .store(in: &cancellables)
        }
    }
}

extension AVPlayerItem.Status {
    var displayText: String {
        switch self {
        case .unknown: return "加载中"
        case .readyToPlay: return "就绪"
        case .failed: return "加载失败"
        @unknown default: return "未知状态"
        }
    }
}

UIKit场景

import UIKit

class PlayerViewController: UIViewController {
    let playerItem: AVPlayerItem
    private var cancellables = Set<AnyCancellable>()
    
    init(playerItem: AVPlayerItem) {
        self.playerItem = playerItem
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 监听加载进度变化
        playerItem.loadedTimeRangesChangedPublisher
            .sink { [weak self] _ in
                guard let self = self else { return }
                let loadedDuration = self.playerItem.loadedTimeRanges.reduce(CMTime.zero) { result, value in
                    CMTimeAdd(result, value.timeRangeValue.duration)
                }
                print("已加载时长:\(loadedDuration.seconds)秒")
            }
            .store(in: &cancellables)
    }
}

四、方案优势

  • 完全基于Combine原生API,无需手动管理Subject的创建、发送和销毁
  • 通用方法支持快速扩展到任意KVO属性或系统通知,复用性强
  • 自动处理内存泄漏(通过weak self和Combine的内存管理机制)
  • 播放进度监听结合播放状态自动启停,避免无效的定时更新

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 12:03:14