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

使用AVPlayer播放m3u8视频时尺寸显示异常的解决咨询

AVPlayer流媒体视频尺寸适配解决方案

要解决不同视频显示尺寸异常的问题,必须动态获取视频的实际宽高比并计算适配容器的frame——因为不同视频的原生宽高比和你的容器视图宽高比不一定匹配,固定设置playerLayer.frame = view.bounds只会适配部分视频。

一、核心步骤

  1. 获取视频的实际显示尺寸:注意要考虑视频轨道的旋转变换(比如竖屏视频可能是横拍后旋转存储的,直接取naturalSize会得到错误的宽高)。
  2. 根据容器尺寸计算适配frame:根据需求选择「完全显示不裁剪」或「填充容器裁剪超出」的适配逻辑。
  3. 监听状态与布局变化:确保视频加载完成后再计算尺寸,同时在容器视图尺寸变化时(如屏幕旋转)重新适配。

二、具体实现代码

1. 获取视频实际显示尺寸

func getVideoActualSize(from playerItem: AVPlayerItem) -> CGSize {
    guard let videoTrack = playerItem.asset.tracks(withMediaType: .video).first else {
        return .zero
    }
    // 应用视频轨道的旋转变换,得到实际显示的宽高
    let transformedSize = videoTrack.naturalSize.applying(videoTrack.preferredTransform)
    return CGSize(width: abs(transformedSize.width), height: abs(transformedSize.height))
}

2. 计算适配容器的frame

根据你的需求选择下面两种计算逻辑:

(1)完全显示不裁剪(对应AVLayerVideoGravity.resizeAspect)
func calculateFitFrame(videoSize: CGSize, containerSize: CGSize) -> CGRect {
    let videoRatio = videoSize.width / videoSize.height
    let containerRatio = containerSize.width / containerSize.height
    
    var targetSize: CGSize
    if videoRatio > containerRatio {
        // 视频更宽,按容器宽度适配,高度自动缩放
        targetSize = CGSize(width: containerSize.width, height: containerSize.width / videoRatio)
    } else {
        // 视频更高,按容器高度适配,宽度自动缩放
        targetSize = CGSize(width: containerSize.height * videoRatio, height: containerSize.height)
    }
    
    // 居中显示
    let originX = (containerSize.width - targetSize.width) / 2
    let originY = (containerSize.height - targetSize.height) / 2
    
    return CGRect(x: originX, y: originY, width: targetSize.width, height: targetSize.height)
}
(2)填充容器裁剪超出(对应AVLayerVideoGravity.resizeAspectFill)
func calculateFillFrame(videoSize: CGSize, containerSize: CGSize) -> CGRect {
    let videoRatio = videoSize.width / videoSize.height
    let containerRatio = containerSize.width / containerSize.height
    
    var targetSize: CGSize
    if videoRatio < containerRatio {
        // 视频更窄,按容器高度适配,宽度超出部分裁剪
        targetSize = CGSize(width: containerSize.height * videoRatio, height: containerSize.height)
    } else {
        // 视频更宽,按容器宽度适配,高度超出部分裁剪
        targetSize = CGSize(width: containerSize.width, height: containerSize.width / videoRatio)
    }
    
    // 居中显示,超出容器的部分会被自动裁剪
    let originX = (containerSize.width - targetSize.width) / 2
    let originY = (containerSize.height - targetSize.height) / 2
    
    return CGRect(x: originX, y: originY, width: targetSize.width, height: targetSize.height)
}

3. 整合到你的播放逻辑中

class VideoPlayerViewController: UIViewController {
    var player: AVPlayer!
    var playerLayer: AVPlayerLayer!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 初始化player(替换成你的流媒体URL)
        guard let url = URL(string: "你的流媒体地址") else { return }
        let playerItem = AVPlayerItem(url: url)
        player = AVPlayer(playerItem: playerItem)
        
        // 初始化playerLayer
        playerLayer = AVPlayerLayer(player: player)
        playerLayer.videoGravity = .resizeAspect // 或.resizeAspectFill,与计算逻辑对应
        view.layer.addSublayer(playerLayer)
        
        // 监听playerItem加载状态
        playerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil)
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        guard let playerItem = object as? AVPlayerItem, keyPath == "status" else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
            return
        }
        
        if playerItem.status == .readyToPlay {
            // 视频加载完成,计算并设置frame
            let videoSize = getVideoActualSize(from: playerItem)
            let containerSize = view.bounds.size
            // 根据需求选择fit或fill逻辑
            playerLayer.frame = calculateFitFrame(videoSize: videoSize, containerSize: containerSize)
            // 开始播放
            player.play()
        }
    }
    
    // 监听容器视图尺寸变化(如屏幕旋转)
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        guard let playerItem = player.currentItem, playerItem.status == .readyToPlay else { return }
        let videoSize = getVideoActualSize(from: playerItem)
        let containerSize = view.bounds.size
        playerLayer.frame = calculateFitFrame(videoSize: videoSize, containerSize: containerSize)
    }
    
    // 移除观察者
    deinit {
        player.currentItem?.removeObserver(self, forKeyPath: "status")
    }
}

三、关键注意点

  • 必须等playerItem.status变为.readyToPlay后再获取视频尺寸,否则可能拿不到有效轨道信息。
  • 视频的旋转变换是核心坑点,直接用naturalSize会导致竖屏视频显示异常,一定要应用preferredTransform。
  • 容器视图尺寸变化时(比如屏幕旋转、父视图布局更新),必须重新计算frame,否则适配会失效。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 23:20:34