AVAssetImageGenerator处理HLS流最后分段时停止生成缩略图问题
HLS流最后分段无法生成缩略图的问题排查与解决
我在iOS应用中尝试为带I-frames的HLS流跳转时生成预览缩略图,使用AVAssetImageGenerator实现后,所有分段都能正常生成图像,唯独最后一个分段不行——只要滑块滑到最后分段的范围,就再也生成不了缩略图。
以下是使用苹果测试HLS流的复现代码:
import AVKit import Combine import SwiftUI let videoURL = URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8")! final class ViewModel: ObservableObject { @Published var duration: Double = 0 @Published var position: Double = 0 @Published var image: UIImage? private let asset: AVAsset private let generator: AVAssetImageGenerator private var subscriber: AnyCancellable? init() { asset = AVAsset(url: videoURL) generator = AVAssetImageGenerator(asset: asset) subscriber = $position.sink { [weak self] in self?.generateImage(for: CMTime(seconds: $0, preferredTimescale: 1000)) } Task { let duration = try! await asset.load(.duration) await MainActor.run { self.duration = CMTimeGetSeconds(duration) } } } private func generateImage(for time: CMTime) { Task { do { let result = try await generator.image(at: time) await MainActor.run { self.image = UIImage(cgImage: result.image) } } catch { print(error) } } } } struct ContentView: View { @StateObject private var model = ViewModel() var body: some View { VStack { if let image = model.image { Image(uiImage: image) } Slider(value: $model.position, in: .init(uncheckedBounds: (lower: 0, upper: model.duration))) } .padding() } }
问题原因与解决思路
这个问题通常是因为HLS流的最后分段存在时长计算误差,AVAsset返回的总时长可能略大于最后一个I-frame的实际时间点,导致AVAssetImageGenerator请求了不存在的时间戳,抛出AVErrorInvalidTimestamp错误。
可以通过以下几个调整解决:
- 修正请求时间,避免超出有效范围
- 调整
AVAssetImageGenerator的属性,提升兼容性 - 针对性处理错误,添加 fallback 逻辑
修改后的代码示例
import AVKit import Combine import SwiftUI let videoURL = URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8")! final class ViewModel: ObservableObject { @Published var duration: Double = 0 @Published var position: Double = 0 @Published var image: UIImage? private let asset: AVAsset private let generator: AVAssetImageGenerator private var subscriber: AnyCancellable? private var lastValidTime: CMTime = .zero init() { asset = AVAsset(url: videoURL) generator = AVAssetImageGenerator(asset: asset) // 配置生成器属性,提升兼容性 generator.apertureMode = .encodedPixels generator.requestedTimeToleranceBefore = .zero generator.requestedTimeToleranceAfter = CMTimeMakeWithSeconds(0.1, preferredTimescale: 1000) subscriber = $position.sink { [weak self] in guard let self = self else { return } var targetTime = CMTime(seconds: $0, preferredTimescale: 1000) // 避免请求超出最后有效时间 if targetTime > self.lastValidTime { targetTime = self.lastValidTime } self.generateImage(for: targetTime) } Task { do { let duration = try await asset.load(.duration) // 预留0.1秒误差,计算最后有效时间点 self.lastValidTime = CMTimeSubtract(duration, CMTimeMakeWithSeconds(0.1, preferredTimescale: 1000)) await MainActor.run { self.duration = CMTimeGetSeconds(duration) } } catch { print("加载元数据失败:\(error)") } } } private func generateImage(for time: CMTime) { Task { do { let result = try await generator.image(at: time) await MainActor.run { self.image = UIImage(cgImage: result.image) } } catch let error as NSError where error.code == AVError.invalidTimestamp.rawValue { // 时间无效时,尝试请求更靠前的 fallback 时间 let fallbackTime = CMTimeSubtract(lastValidTime, CMTimeMakeWithSeconds(0.1, preferredTimescale: 1000)) do { let result = try await generator.image(at: fallbackTime) await MainActor.run { self.image = UIImage(cgImage: result.image) } } catch { print("Fallback生成缩略图失败:\(error)") } } catch { print("生成缩略图失败:\(error)") } } } } struct ContentView: View { @StateObject private var model = ViewModel() var body: some View { VStack { if let image = model.image { Image(uiImage: image) .resizable() .scaledToFit() } Slider(value: $model.position, in: 0...model.duration) } .padding() } }
关键修改点说明
- 新增
lastValidTime变量,提前计算并缓存最后一个有效请求时间 - 配置
generator的属性,提升最后分段的帧获取成功率 - 捕获
invalidTimestamp错误,添加 fallback 逻辑 - 优化滑块的范围绑定,使用安全的
0...model.duration替代uncheckedBounds
内容的提问来源于stack exchange,提问作者Maik
相关产品推荐
相关产品推荐

