iOS: AVPlayer - 获取视频当前帧的快照

16

我整天都在翻阅SO答案、Apple参考文献、文档等,但是没有成功。

我想要的是一个简单的东西:我正在使用AVPlayer播放视频,希望能够暂停并获取当前帧作为UIImage。就是这样。

我的视频是位于互联网上的m3u8文件,在AVPlayerLayer中正常播放,没有任何问题。

我尝试了什么:

  1. AVAssetImageGenerator 它不起作用,方法copyCGImageAtTime:actualTime:error:返回空图像引用。根据此处的答案,AVAssetImageGenerator对流媒体视频不起作用。
  2. 拍摄播放器视图的快照。 我首先尝试了AVPlayerLayer上的renderInContext:,但是我意识到它不会渲染这种“特殊”图层。然后我发现iOS 7中引入了一种新方法- drawViewHierarchyInRect:afterScreenUpdates:,应该也能够渲染特殊图层,但是没有运气,仍然得到了带有视频显示的空黑色区域的UI快照。
  3. AVPlayerItemVideoOutput我为我的AVPlayerItem添加了一个视频输出,但是每当我调用hasNewPixelBufferForItemTime:时,它都返回NO。我想问题可能又出在流媒体视频上,与此处的问题相同。
  4. AVAssetReader我想尝试一下,但在找到相关问题这里后,决定不浪费时间。

那么难道没有任何方法可以获取我正在屏幕上看到的东西的快照吗?我简直无法相信。

3个回答

12

AVAssetImageGenerator 是截取视频快照的最佳方法,该方法会异步返回一个 UIImage

import AVFoundation

// ...

var player:AVPlayer? = // ...

func screenshot(handler:@escaping ((UIImage)->Void)) {
    guard let player = player ,
        let asset = player.currentItem?.asset else {
            return
    }
    
    let imageGenerator = AVAssetImageGenerator(asset: asset)
    imageGenerator.appliesPreferredTrackTransform = true
    let times = [NSValue(time:player.currentTime())]
    
    imageGenerator.generateCGImagesAsynchronously(forTimes: times) { _, image, _, _, _ in
        if let img = image {
            handler(UIImage(cgImage: img))
        }
    }
}

(这是Swift 4.2)


@Axel Guilmin 这只捕捉Avplayer。如果我想要同时截取Avplayer和UiView的屏幕截图怎么办? - Raghuram
我认为我的答案不是捕获UIView的正确方法。我没有测试过,但这个答案似乎更好: https://dev59.com/s2855IYBdhLWcg3wcz_y#4334902 - Axel Guilmin
@Axel Guilmin,感谢您的回复。这是我的问题:stackoverflow.com/questions/42085479/… - Raghuram
2
@Anessence,你能找到一个能从直播流中捕获的答案吗? - mfaani

10
AVPlayerItemVideoOutput对于我从m3u8文件中的视频来说运行良好。也许是因为我没有咨询hasNewPixelBufferForItemTime而是直接调用了copyPixelBufferForItemTime?这段代码生成了一个CVPixelBuffer而不是一个UIImage,但有一些答案描述了如何做到这一点
这个答案大部分抄袭自这里
#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>

@interface ViewController ()

@property (nonatomic) AVPlayer *player;
@property (nonatomic) AVPlayerItem *playerItem;
@property (nonatomic) AVPlayerItemVideoOutput *playerOutput;

@end

@implementation ViewController
- (void)setupPlayerWithLoadedAsset:(AVAsset *)asset {
    NSDictionary* settings = @{ (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA) };
    self.playerOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:settings];
    self.playerItem = [AVPlayerItem playerItemWithAsset:asset];
    [self.playerItem addOutput:self.playerOutput];
    self.player = [AVPlayer playerWithPlayerItem:self.playerItem];

    AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
    playerLayer.frame = self.view.frame;
    [self.view.layer addSublayer:playerLayer];

    [self.player play];
}

- (IBAction)grabFrame {
    CVPixelBufferRef buffer = [self.playerOutput copyPixelBufferForItemTime:[self.playerItem currentTime] itemTimeForDisplay:nil];
    NSLog(@"The image: %@", buffer);
}

- (void)viewDidLoad {
    [super viewDidLoad];


    NSURL *someUrl = [NSURL URLWithString:@"http://qthttp.apple.com.edgesuite.net/1010qwoeiuryfg/sl.m3u8"];
    AVURLAsset *asset = [AVURLAsset URLAssetWithURL:someUrl options:nil];

    [asset loadValuesAsynchronouslyForKeys:[NSArray arrayWithObject:@"tracks"] completionHandler:^{

        NSError* error = nil;
        AVKeyValueStatus status = [asset statusOfValueForKey:@"tracks" error:&error];
        if (status == AVKeyValueStatusLoaded)
        {
            dispatch_async(dispatch_get_main_queue(), ^{
                [self setupPlayerWithLoadedAsset:asset];
            });
        }
        else
        {
            NSLog(@"%@ Failed to load the tracks.", self);
        }
    }];
}

@end

我现在会尝试并让您知道。一个简短的问题:我应该从一开始就设置AVPlayerItemVideoOutput对象吗?我的代码正在播放视频,而没有添加输出,然后每当我需要快速创建AVPlayerItemVideoOutput对象的快照时,我将其添加到播放器项目中并尝试读取像素缓冲区。我还尝试在稍早时添加输出 - 每当我的特殊快照手势开始触摸但尚未被识别时。这很重要吗? - frangulyan
我认为你必须从一开始就设置 AVPlayerItemVideoOutput,可能是在开始播放之前。 - Rhythmic Fistman
谢谢你的解决方案,我刚刚检查了一下,它可以工作!诀窍是在开始播放之前添加 AVPlayerItemVideoOutput,就像你说的那样。似乎为了将来某个地方可能不会被拍摄的一个截图而一直添加视频输出有点低效,但至少它能够工作! - frangulyan
不客气。我想你是对的 - 将 ARGB AVPlayerItemVideoOutput 附加到很可能是 YUV 流的内容上可能会很昂贵。我从未想过这一点。 - Rhythmic Fistman
这个解决方案对使用FairPlay保护的HLS有效吗?我已经尝试了copyPixelBufferForItemTime并且它在未受保护的流上运行良好,但一旦你使用FairPlay,它就会返回NULL。http://stackoverflow.com/questions/42839831/fairplay-streaming-calling-copypixelbufferforitemtime-on-avplayeritemvideooutpu - schmittsfn

0

Rhythmic Fistman的解决方案的Swift版本,在Xcode 15和iOS 17中仍然按预期工作:

private var playerOutput: AVPlayerItemVideoOutput?
(...)
self.playerOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)])
(...)
self.playerItem?.add(self.playerOutput!)
(...)
if let playerItem = self?.playerItem, let imageBuffer = self?.playerOutput?.copyPixelBuffer(forItemTime: playerItem.currentTime(), itemTimeForDisplay: nil), let image = UIImage(pixelBuffer: imageBuffer) {
  // do what you want with the `image`
} else {
  print("Failed to grab frame")
}

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