Post

swift远端视频第一帧方法及问题

swift远端视频第一帧方法及问题

前言

在往常的业务开发中,对于视频的交互展示,一般都是有一个专门的封面图片地址,一个专门的视频地址两个字段组成,这样做的原因: 1、在列表或默认展示这个封面图片,点击封面图片或播放按钮才正式播放视频,这样可以在不播放时能不用下载视频,节省服务器流量及用户的手机流量。
2、另一个常用场景就是在聊天列表里的视频,由于通常视频和图片都是加密的,没法直接从加密视频里获取第一帧图片,所以需要一个专门的封面图片,否则必须要把全部视频下载完成同时解密后获取第一帧作为封面

业务中的新通点

1、开发过程中要管理图片地址和视频地址两个字段比较繁琐
2、由于一些没有加密的视频,标准的oss存储视频一般提供直接拼接地址获取到视频封面
3、对于一些没有加密的视频,开发时间不够或其他特殊的一些人不配合,希望展示方自己想办法从视频里获取第一帧图


1、如果视频没有加密

方案一、老老实实使用封面图地址 + 视频地址 的方式实现

方案二、 视频没有加密,视频又是存储在支持通过配置链接直接从oss服务器拉去的,直接通过配置链接的方式获取,具体参考各个oss服务的文档,以阿里云oss为例

1
2
3
4
5
6
7
8
假设你的视频 URL 是:
https://your-bucket-name.oss-cn-hangzhou.aliyuncs.com/your-video-file.mp4

在 URL 后拼接以下参数:
?x-oss-process=video/snapshot,t_0,f_jpg,w_0,h_0,m_fast

完整 URL:
https://your-bucket-name.oss-cn-hangzhou.aliyuncs.com/your-video-file.mp4?x-oss-process=video/snapshot,t_0,f_jpg,w_0,h_0,m_fast

方案三、 视频没有加密,但是视频是在不支持的自己服务器上,或者不太确定是否支持通过配置链接获取视频第一帧,直接使用代码方式获取视频第一帧,方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
typealias ActionBlock = (()->Void)
func dispatchGlobalQueue(action: @escaping ActionBlock) -> Void {
    if Thread.isMainThread {
        DispatchQueue.global().async {
            action()
        }
        return
    }
    action()
}
func dispatchMainQueue(action: @escaping ActionBlock) -> Void {
    
    if Thread.isMainThread {
        action()
        return
    }
    DispatchQueue.main.async {
        action()
    }
}
extension String {
    func syncRetrieveImage() -> UIImage? {
        let key = self
        var retrievedImage: UIImage? = nil
        let semaphore = DispatchSemaphore(value: 0)  // 创建一个信号量
        
        // 异步调用,使用信号量同步等待
        KingfisherManager.shared.cache.retrieveImage(forKey: key) { result in
            switch result {
            case .success(let value):
                retrievedImage = value.image
            case .failure:
                retrievedImage = nil
            }
            
            semaphore.signal()  // 信号量发信号,允许线程继续
        }
        
        // 等待信号量完成
        semaphore.wait()
        
        return retrievedImage
    }
    typealias VideoFirstFrameBlock = (UIImage?) -> Void
    func fetchVideoFirstFrame(completion: VideoFirstFrameBlock? = nil) {
        dispatchGlobalQueue {
            self.realFetchVideoFirstFrame(completion: completion)
        }
    }
    private func realFetchVideoFirstFrame(delayTimes: [Double] = [0, 1, 5, 10], completion: VideoFirstFrameBlock? = nil) {
        let key = self
        guard let url = URL(string: key) else {
            dispatchMainQueue {
                completion?(nil)
            }
            return
        }
        // 如果存在已经缓存过的封面,直接使用缓存数据,不再继续下载
        if let image = key.syncRetrieveImage() {
            dispatchMainQueue {
                completion?(image)
            }
            return
        }
        
        let asset = AVURLAsset(url: url)
        let generator = AVAssetImageGenerator(asset: asset)
        generator.appliesPreferredTrackTransform = true // 保证视频方向正确
        
        // 精确获取时间点,减少处理范围
        generator.requestedTimeToleranceBefore = .zero
        generator.requestedTimeToleranceAfter = .zero
        let secondFrom = delayTimes.first ?? 0
        let time = CMTime(seconds: secondFrom, preferredTimescale: 600) // 0秒处
        
        generator.generateCGImagesAsynchronously(forTimes: [NSValue(time: time)]) { _, cgImage, _, result, error in
            switch result {
            case .succeeded:
                guard let cgImage = cgImage else {
                    DispatchQueue.main.async { completion?(nil) }
                    return
                }
                // 如果下载到了视频封面,缓存起来
                let image = UIImage(cgImage: cgImage)
                KingfisherManager.shared.cache.store(image, forKey: key)
                dispatchMainQueue {
                    completion?(image)
                }
            case .failed:
                if delayTimes.count > 1 {
                    print("Error generating secondFrom:\(secondFrom) age: \(error?.localizedDescription ?? "Unknown error") \(url)")
                    let nextTimes = delayTimes.subArray(from: 1, size: delayTimes.count - 1)
                    key.realFetchVideoFirstFrame(delayTimes: nextTimes, completion: completion)
                } else {
                    dispatchMainQueue {
                        completion?(nil)
                    }
                }
            case .cancelled:
                print("Error generating image: \(error?.localizedDescription ?? "Unknown error") \(url)")
                dispatchMainQueue {
                    completion?(nil)
                }
            @unknown default:
                dispatchMainQueue {
                    completion?(nil)
                }
            }
        }
    }
}

// 使用方式
let serverUrl = "https://xxx.xxx.xx/xx/xx.mp4"
serverUrl.fetchVideoFirstFrame()

该方案遇到的一些问题:一般的视频都可以,上传的一些比较短的视频,第1秒是黑的,其实是没有,这个把视频下载到Mac上也能看出来,遇到这种错误,就做了策略继续往后取1,在没有就取5、10这样递归取,都没有就不管了

方案四、将视频完整下载下来,然后再获取下载下来的视频的第一帧(这种方式不推荐,浪费流量,没有播放也把完整视频下载下来了)

代码同方案三,只是传递的地址不一样

1
2
3
// 1 ....下载视频,或者APP里本身包含的视频
let localPath = "file://xx/xx/xx"
localPath.fetchVideoFirstFrame()

2、如果视频加密了

方案一 老老实实使用封面图地址 + 视频地址 两个字段处理

方案二 必须自己把视频全部下载下来,然后解密,解密后再获取视频的第一帧

This post is licensed under CC BY 4.0 by the author.