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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 18:45:54