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

AVPlayer无法播放本地已保存视频文件的问题求助

问题分析与修复方案

先给你揪出两个核心问题,这就是本地视频黑屏+警告的根源:


1. 致命逻辑错误:文件存在判断完全颠倒了

看你本地播放函数里的这段代码:

if (MediaUtils.fileExists(filePath: filepath)) { 
    Logging.logError("Playing video failed, file not found: \(filepath)") 
    return false 
}

这相当于文件存在的时候,你直接报错说找不到文件,然后返回false终止播放流程!完全搞反了判断逻辑!正确的应该是:如果文件不存在才报错返回,存在的话继续执行。


2. AVPlayerViewController使用违规:同时做了addSubview和present

你在代码里同时执行了:

view.addSubview(playerViewController.view) 
vc.present(playerViewController, animated: true)

这是绝对不允许的——一个视图控制器要么被present出来(作为独立的模态控制器),要么被添加为另一个控制器的子视图(作为容器控制器的一部分),不能同时操作。这就是你看到Unbalanced calls to begin/end appearance transitions警告的直接原因,也会导致视图渲染异常(比如黑屏)。

顺便说一句,你那个网络播放的函数其实也有这个问题,只是网络视频加载有延迟,没立刻触发明显的黑屏,建议也一起修正。


3. 如何获取播放错误信息?

要捕获player.play()的错误,你需要监听AVPlayerItem的状态变化或者相关通知:

  • 监听AVPlayerItemFailedToPlayToEndTimeNotification通知,捕获播放失败的错误
  • 通过KVO监听AVPlayerItem的status属性,获取准备状态和错误信息

修正后的完整代码示例

方式一:用present模态播放本地视频(推荐,符合系统播放器的交互逻辑)

static func playVideo(from filename: String, vc: UIViewController) -> Bool { 
    let filepath = videosDirectoryPath + filename 
    // 修正后的文件存在判断
    guard MediaUtils.fileExists(filePath: filepath) else {
        Logging.logError("Playing video failed, file not found: \(filepath)") 
        return false 
    }
    
    let fileURL = URL(fileURLWithPath: filepath)
    let playerItem = AVPlayerItem(url: fileURL)
    
    // 监听播放失败通知
    NotificationCenter.default.addObserver(forName: .AVPlayerItemFailedToPlayToEndTime, object: playerItem, queue: .main) { notification in
        if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error {
            Logging.logError("Video playback failed with error: \(error.localizedDescription)")
        }
    }
    
    // KVO监听playerItem状态(可选,但能拿到更详细的准备状态)
    playerItem.addObserver(self, forKeyPath: "status", options: [.new], context: nil)
    
    let player = AVPlayer(playerItem: playerItem)
    let playerViewController = AVPlayerViewController() 
    playerViewController.player = player 
    
    vc.present(playerViewController, animated: true) { 
        player.play() 
    } 
    return true 
}

// 记得在合适的时机移除观察者(比如所在控制器的deinit方法)
deinit {
    NotificationCenter.default.removeObserver(self)
    // 如果用了KVO,也要移除对应的观察者
    playerItem?.removeObserver(self, forKeyPath: "status")
}

// KVO回调方法,处理playerItem状态变化
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    guard keyPath == "status", let playerItem = object as? AVPlayerItem else {
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        return
    }
    
    switch playerItem.status {
    case .failed:
        if let error = playerItem.error {
            Logging.logError("Player item failed with error: \(error.localizedDescription)")
        }
    case .readyToPlay:
        Logging.logDebug("Player item is ready to play")
    case .unknown:
        Logging.logDebug("Player item status is unknown (loading...)")
    @unknown default:
        break
    }
}

方式二:将播放器视图添加为子视图(适合嵌入到自定义界面)

如果你需要把播放器嵌入到现有视图中,不要用present,而是遵循容器视图控制器的规范:

static func playVideo(from filename: String, vc: UIViewController, view: UIView) -> Bool { 
    let filepath = videosDirectoryPath + filename 
    guard MediaUtils.fileExists(filePath: filepath) else {
        Logging.logError("Playing video failed, file not found: \(filepath)") 
        return false 
    }
    
    let fileURL = URL(fileURLWithPath: filepath)
    let playerItem = AVPlayerItem(url: fileURL)
    
    // 添加错误监听(同上)
    NotificationCenter.default.addObserver(forName: .AVPlayerItemFailedToPlayToEndTime, object: playerItem, queue: .main) { notification in
        if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error {
            Logging.logError("Video playback failed with error: \(error.localizedDescription)")
        }
    }
    
    let player = AVPlayer(playerItem: playerItem)
    let playerViewController = AVPlayerViewController() 
    playerViewController.player = player 
    
    // 设置播放器视图的frame和自适应规则
    playerViewController.view.frame = view.bounds
    playerViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    
    // 必须调用容器VC的方法,这是规范
    vc.addChild(playerViewController)
    view.addSubview(playerViewController.view)
    playerViewController.didMove(toParent: vc)
    
    player.play()
    return true 
}

最后总结

  • 先修正文件存在的判断逻辑,这是本地视频根本没机会播放的首要原因
  • 二选一使用present或addSubview的方式展示AVPlayerViewController,不要同时操作
  • 通过监听AVPlayerItem的通知或KVO来获取详细的播放错误信息

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 08:04:48