如何获取AVPlayer的视频帧?

15

我有一个PlayerView类用于显示AVPlayer的播放。 代码来自文档

#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>

@interface PlayerView : UIView
@property (nonatomic) AVPlayer *player;
@end

@implementation PlayerView
+ (Class)layerClass {
    return [AVPlayerLayer class];
}
- (AVPlayer*)player {
    return [(AVPlayerLayer *)[self layer] player];
}
- (void)setPlayer:(AVPlayer *)player {
    [(AVPlayerLayer *)[self layer] setPlayer:player];
}
@end

我在这个PlayerView中设置了我的AVPlayer(包含大小为320x240的视频资源),并且我的视频已被调整大小。如何在添加到PlayerView后获取视频的大小?

8个回答

36

iOS 7.0新增了一个新功能:

AVPlayerLayer有一个videoRect属性。


16
对我而言,即使我能在屏幕上看到视频,videoRect仍然返回CGRectZero。我正在使用iOS 8。 - tettoffensive
等待currentItem.status == AVPlayerStatusReadyToPlay,然后你就可以得到帧。 - cohen72
1
即使我已经添加了KVO以等待状态为AVPlayerStatusReadyToPlay,但这仍然无法正常工作。videoRect仍然返回所有零值。 - codingrhythm
5
再试一次后,它可以正常工作。因此,您需要在AVPlayerItem而不是AVPlayer上进行KVO,并检查的状态是AVPlayerItemStatus.ReadyToPlay,而不是AVPlayerStatusReadyToPlay。这种方法是正确的,但@Yocoh的评论有点误导。 - codingrhythm
这适用于fit方面,但不适用于fill方面。根据文档,这是“图层边界内视频图像的显示矩形”。 - Gobe

7

这对我有用。当您没有AVPLayerLayer时。

- (CGRect)videoRect {
    // @see https://dev59.com/imw15IYBdhLWcg3whMA1#6565988

    AVAssetTrack *track = [[self.player.currentItem.asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
    if (!track) {
        return CGRectZero;
    }

    CGSize trackSize = [track naturalSize];
    CGSize videoViewSize = self.videoView.bounds.size;

    CGFloat trackRatio = trackSize.width / trackSize.height;
    CGFloat videoViewRatio = videoViewSize.width / videoViewSize.height;

    CGSize newSize;

    if (videoViewRatio > trackRatio) {
        newSize = CGSizeMake(trackSize.width * videoViewSize.height / trackSize.height, videoViewSize.height);
    } else {
        newSize = CGSizeMake(videoViewSize.width, trackSize.height * videoViewSize.width / trackSize.width);
    }

    CGFloat newX = (videoViewSize.width - newSize.width) / 2;
    CGFloat newY = (videoViewSize.height - newSize.height) / 2;

    return CGRectMake(newX, newY, newSize.width, newSize.height);

}

很棒的解决方案!! - ThePedestrian
由于某些原因,这似乎无法处理流媒体,只能处理来自文件系统的下载媒体。看起来顶部的轨道为空。 - Jonny

5

找到了解决方案:

在PlayerView类中添加以下内容:

- (CGRect)videoContentFrame {
    AVPlayerLayer *avLayer = (AVPlayerLayer *)[self layer];
    // AVPlayerLayerContentLayer
    CALayer *layer = (CALayer *)[[avLayer sublayers] objectAtIndex:0];
    CGRect transformedBounds = CGRectApplyAffineTransform(layer.bounds, CATransform3DGetAffineTransform(layer.sublayerTransform));
    return transformedBounds;
}

1
在Xcode4.5,iOS6 sdk,构建目标4.3中对我不起作用。获取大小(0,0)。现在它对你仍然有效吗? - Mingming

4
这是我的解决方案,考虑了AVPlayer在视图中的定位。我将其添加到了自定义类PlayerView中。我不得不解决这个问题,因为似乎在10.7中videoRect无法正常工作。
- (NSRect) videoRect {
    NSRect theVideoRect = NSMakeRect(0,0,0,0);
    NSRect theLayerRect = self.playerLayer.frame;

    NSSize theNaturalSize = NSSizeFromCGSize([[[self.movie asset] tracksWithMediaType:AVMediaTypeVideo][0] naturalSize]);
    float movieAspectRatio = theNaturalSize.width/theNaturalSize.height;
    float viewAspectRatio = theLayerRect.size.width/theLayerRect.size.height;
    if (viewAspectRatio < movieAspectRatio) {
        theVideoRect.size.width = theLayerRect.size.width;
        theVideoRect.size.height = theLayerRect.size.width/movieAspectRatio;
        theVideoRect.origin.x = 0;
        theVideoRect.origin.y = (theLayerRect.size.height/2) - (theVideoRect.size.height/2);
}
    else if (viewAspectRatio > movieAspectRatio) {

        theVideoRect.size.width = movieAspectRatio * theLayerRect.size.height;
        theVideoRect.size.height = theLayerRect.size.height;
        theVideoRect.origin.x = (theLayerRect.size.width/2) - (theVideoRect.size.width/2);
        theVideoRect.origin.y = 0;
    }
    return theVideoRect;
}

videoRect 在 iOS 13 上无法工作,但是这个可以,谢谢! - Pavel Zaitsev

1
这是Eric Badros' answer移植到iOS的代码。我还添加了首选转换处理。假设_player是一个AVPlayer
- (CGRect) videoRect {
    CGRect theVideoRect = CGRectZero;

    // Replace this with whatever frame your AVPlayer is playing inside of:
    CGRect theLayerRect = self.playerLayer.frame;

    AVAssetTrack *track = [_player.currentItem.asset tracksWithMediaType:AVMediaTypeVideo][0];
    CGSize theNaturalSize = [track naturalSize];
    theNaturalSize = CGSizeApplyAffineTransform(theNaturalSize, track.preferredTransform);
    theNaturalSize.width = fabs(theNaturalSize.width);
    theNaturalSize.height = fabs(theNaturalSize.height);

    CGFloat movieAspectRatio = theNaturalSize.width / theNaturalSize.height;
    CGFloat viewAspectRatio = theLayerRect.size.width / theLayerRect.size.height;

    // Note change this *greater than* to a *less than* if your video will play in aspect fit mode (as opposed to aspect fill mode)
    if (viewAspectRatio > movieAspectRatio) {
        theVideoRect.size.width = theLayerRect.size.width;
        theVideoRect.size.height = theLayerRect.size.width / movieAspectRatio;
        theVideoRect.origin.x = 0;
        theVideoRect.origin.y = (theLayerRect.size.height - theVideoRect.size.height) / 2;
    } else if (viewAspectRatio < movieAspectRatio) {
        theVideoRect.size.width = movieAspectRatio * theLayerRect.size.height;
        theVideoRect.size.height = theLayerRect.size.height;
        theVideoRect.origin.x = (theLayerRect.size.width - theVideoRect.size.width) / 2;
        theVideoRect.origin.y = 0;
    }
    return theVideoRect;
}

感谢 @tettoffensive - Sterling Christensen

0

2022 SwiftUI

struct PlayerViewController: UIViewControllerRepresentable {
    
    private let avController =  AVPlayerViewController()
    var videoURL: URL?
    private var player: AVPlayer {
        return AVPlayer(url: videoURL!)
    }
    
    //  HERE 
    func getVideoFrame() -> CGRect {
        self.avController.videoBounds
    }
    //  HERE 

    func makeUIViewController(context: Context) -> AVPlayerViewController {
        avController.modalPresentationStyle = .fullScreen
        avController.player = player
        avController.player?.play()
        return avController
    }

    func updateUIViewController(_ playerController: AVPlayerViewController, context: Context) {}
}

0
这里是一个基于Andrey Banshchikov的答案的Swift解决方案。当您无法访问AVPlayerLayer时,他的解决方案尤其有用。
func currentVideoFrameSize(playerView: AVPlayerView, player: AVPlayer) -> CGSize {
    // See https://dev59.com/t2ct5IYBdhLWcg3wcs-G#40164496
    let track = player.currentItem?.asset.tracks(withMediaType: .video).first
    if let track = track {
        let trackSize      = track.naturalSize
        let videoViewSize  = playerView.bounds.size
        let trackRatio     = trackSize.width / trackSize.height
        let videoViewRatio = videoViewSize.width / videoViewSize.height
        
        var newSize: CGSize

        if videoViewRatio > trackRatio {
            newSize = CGSize(width: trackSize.width * videoViewSize.height / trackSize.height, height: videoViewSize.height)
        } else {
            newSize = CGSize(width: videoViewSize.width, height: trackSize.height * videoViewSize.width / trackSize.width)
        }
        
        return newSize
    }
    return CGSize.zero
}

我看到两个问题:这只返回大小,而我们需要整个框架。而 AVPlayerView 仅适用于 macOS,也许将其更改为 UIView 对于 iOS 是可以的。 - Jonny

0

Andrey的答案适用于文件/本地/下载视频,但不适用于流式HLS视频。作为第二次尝试,我能够在currentItem的“tracks”属性中找到视频轨道。同时已经改写成Swift。

private func videoRect() -> CGRect {
    // Based on https://dev59.com/t2ct5IYBdhLWcg3wcs-G#40164496 - originally objective-c
            
    var trackTop: AVAssetTrack? = nil
    
    if let track1 = self.player?.currentItem?.asset.tracks(withMediaType: AVMediaType.video).first {
        trackTop = track1
    }
    else {
        // For some reason the above way wouldn't find the "track" for streamed HLS video.
        // This seems to work for streamed HLS.
        if let tracks = self.player?.currentItem?.tracks {
            for avplayeritemtrack in tracks {
                if let assettrack = avplayeritemtrack.assetTrack {
                    if assettrack.mediaType == .video {
                        // Found an assetTrack here?
                        trackTop = assettrack
                        break
                    }
                }
            }
        }
    }
    
    guard let track = trackTop else {
        print("Failed getting track")
        return CGRect.zero
    }
    
    let trackSize = track.naturalSize
    let videoViewSize = self.view.bounds.size

    let trackRatio = trackSize.width / trackSize.height
    let videoViewRatio = videoViewSize.width / videoViewSize.height
    
    let newSize: CGSize
    
    if videoViewRatio > trackRatio {
        newSize = CGSize.init(width: trackSize.width * videoViewSize.height / trackSize.height, height: videoViewSize.height)
    }
    else {
        newSize = CGSize.init(width: videoViewSize.width, height: trackSize.height * videoViewSize.width / trackSize.width)
    }

    let newX: CGFloat = (videoViewSize.width - newSize.width) / 2
    let newY: CGFloat = (videoViewSize.height - newSize.height) / 2

    return CGRect.init(x: newX, y: newY, width: newSize.width, height: newSize.height)
}

更简单的方法

我找到了另一种方法。如果你正在使用AVPlayerViewController,这似乎要简单得多。为什么我之前没有发现呢。

return self.videoBounds

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接