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

如何在SwiftUI中结合VLCKit与AVPlayerLayer实现画中画?

解决方案:VLCKit 实现画中画(PIP)功能

你的核心问题在于:AVPlayerLayer 仅兼容 AVFoundation 框架的 AVPlayer,无法直接与 VLCKit 的 VLCMediaPlayer 配合使用。要实现画中画,推荐使用 VLCKit 原生的画中画支持,以下是完整的实现步骤和代码:

一、前置配置

首先在项目的 Info.plist 中添加画中画权限描述:

<key>NSPictureInPictureUsageDescription</key>
<string>需要使用画中画功能播放视频</string>

二、完整实现代码

替换你原有的 AVPlayer 相关代码,改用 VLCKit 的原生 API:

import UIKit
import SwiftUI
import MobileVLCKit

// 自定义VLC播放器视图,支持画中画
class VLCPlayerView: UIView, VLCMediaPlayerDelegate {
    private let mediaPlayer: VLCMediaPlayer
    private var pipController: AVPictureInPictureController?
    
    init(url: URL) {
        mediaPlayer = VLCMediaPlayer()
        super.init(frame: .zero)
        
        setupPlayer(url: url)
        setupPIP()
        backgroundColor = .black
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupPlayer(url: URL) {
        let media = VLCMedia(url: url)
        mediaPlayer.media = media
        mediaPlayer.delegate = self
        
        // 将VLC的输出层添加到视图
        if let playerLayer = mediaPlayer.drawable as? CALayer {
            playerLayer.frame = bounds
            layer.addSublayer(playerLayer)
        }
    }
    
    private func setupPIP() {
        // 检查设备是否支持画中画
        guard AVPictureInPictureController.isPictureInPictureSupported() else { return }
        
        // 创建AVPlayerLayer的占位(用于桥接VLC到PIP)
        let pipPlayerLayer = AVPlayerLayer()
        pipPlayerLayer.frame = bounds
        layer.addSublayer(pipPlayerLayer)
        
        // 初始化PIP控制器
        let pipContentSource = AVPictureInPictureController.ContentSource(
            playerLayer: pipPlayerLayer,
            playbackDelegate: self
        )
        pipController = AVPictureInPictureController(contentSource: pipContentSource)
        pipController?.delegate = self
    }
    
    // MARK: - 播放控制
    func play() {
        mediaPlayer.play()
    }
    
    func pause() {
        mediaPlayer.pause()
    }
    
    func togglePIP() {
        guard let pipController = pipController else { return }
        if pipController.isPictureInPictureActive {
            pipController.stopPictureInPicture()
        } else {
            pipController.startPictureInPicture()
        }
    }
    
    // MARK: - VLCMediaPlayerDelegate
    func mediaPlayerStateChanged(_ aNotification: Notification) {
        // 监听播放状态变化,同步到PIP控制器
        pipController?.updatePlaybackState()
    }
}

// 让VLCPlayerView遵守PIP播放代理
extension VLCPlayerView: AVPictureInPictureControllerPlaybackDelegate {
    func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didStartPictureInPictureWithAnimationCoordinator coordinator: AVPictureInPictureAnimationCoordinator) {
        // PIP启动时的处理
    }
    
    func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didStopPictureInPictureWithAnimationCoordinator coordinator: AVPictureInPictureAnimationCoordinator) {
        // PIP停止时的处理
    }
    
    func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
        // 恢复界面时的处理
        completionHandler(true)
    }
}

// SwiftUI包装器
struct VLCPlayerContainer: UIViewRepresentable {
    let url: URL
    @Binding var isPlaying: Bool
    @Binding var showPIP: Bool
    
    func makeUIView(context: Context) -> VLCPlayerView {
        let playerView = VLCPlayerView(url: url)
        return playerView
    }
    
    func updateUIView(_ uiView: VLCPlayerView, context: Context) {
        if isPlaying {
            uiView.play()
        } else {
            uiView.pause()
        }
        
        if showPIP {
            uiView.togglePIP()
            context.coordinator.parent.showPIP = false
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator {
        let parent: VLCPlayerContainer
        
        init(_ parent: VLCPlayerContainer) {
            self.parent = parent
        }
    }
}

// 视图模型
class VLCPlayerViewModel: ObservableObject {
    @Published var isPlaying = false
    @Published var showPIP = false
    let videoURL: URL
    
    init(fileName: String) {
        guard let url = Bundle.main.url(forResource: fileName, withExtension: "mp4") else {
            fatalError("视频文件不存在")
        }
        videoURL = url
    }
}

// 主视图
struct ContentView: View {
    @StateObject var model = VLCPlayerViewModel(fileName: "video")
    
    var body: some View {
        ZStack {
            VStack {
                VLCPlayerContainer(url: model.videoURL, isPlaying: $model.isPlaying, showPIP: $model.showPIP)
                    .frame(height: 200)
                
                HStack {
                    Button(action: {
                        model.isPlaying.toggle()
                    }) {
                        Image(systemName: model.isPlaying ? "pause" : "play")
                            .font(.largeTitle)
                            .padding()
                    }
                    
                    Button(action: {
                        model.showPIP = true
                    }) {
                        Image(systemName: "pip")
                            .font(.largeTitle)
                            .padding()
                    }
                }
            }
        }
        .ignoresSafeArea()
    }
}

三、关键说明

  1. 为什么不能直接用AVPlayerLayer?
    AVPlayerLayer是AVFoundation专属的图层类,内部仅支持AVPlayer实例,而VLCMediaPlayer是基于libvlc的独立播放器,两者没有兼容接口,所以无法直接替换。

  2. 原生PIP的优势
    上述方案利用VLCKit的播放能力,同时通过AVPictureInPictureController实现画中画,既保留了VLC的解码优势,又能系统级支持PIP,无需复杂的帧桥接。

  3. 可选的复杂方案(不推荐)
    如果必须使用AVPlayerLayer,需要将VLC的视频帧输出转换为CVPixelBuffer,再包装成AVPlayerItem的数据源(如使用AVAssetWriter或自定义AVPlayerItem),但这种方式需要处理帧同步、格式转换、性能优化等问题,开发成本极高,除非有特殊需求不建议采用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 23:47:17