@@ -10,6 +10,7 @@ struct MessageVideoView: View {
1010
1111 @State private var fileHandle : MediaFileHandle ?
1212 @State private var video : AVPlayer ?
13+ @State private var generatedThumbnail : Image ?
1314
1415 var aspectRatio : CGFloat ? {
1516 guard let info = content. info,
@@ -47,14 +48,60 @@ struct MessageVideoView: View {
4748 }
4849 }
4950
51+ private func generateThumbnail( ) async {
52+ guard let client = appState. matrixClient? . client else { return }
53+
54+ let cacheKey = NSString ( string: " thumb: " + content. source. url ( ) )
55+ if let cached = MatrixClient . imageCache. object ( forKey: cacheKey) {
56+ generatedThumbnail = Image ( nsImage: cached)
57+ return
58+ }
59+
60+ do {
61+ let handle = try await client. getMediaFile (
62+ mediaSource: content. source,
63+ filename: content. filename,
64+ mimeType: content. info? . mimetype ?? " " ,
65+ useCache: true ,
66+ tempDir: NSTemporaryDirectory ( )
67+ )
68+ fileHandle = handle
69+ let path = try handle. path ( )
70+ let url = URL ( filePath: path, directoryHint: . notDirectory)
71+
72+ let asset = AVURLAsset ( url: url)
73+ let generator = AVAssetImageGenerator ( asset: asset)
74+ generator. appliesPreferredTrackTransform = true
75+ generator. maximumSize = CGSize ( width: 600 , height: 600 )
76+
77+ let cgImage = try await generator. image ( at: . zero) . image
78+ let nsImage = NSImage ( cgImage: cgImage, size: NSSize ( width: cgImage. width, height: cgImage. height) )
79+ MatrixClient . imageCache. setObject ( nsImage, forKey: cacheKey)
80+ generatedThumbnail = Image ( nsImage: nsImage)
81+ } catch {
82+ Logger . viewCycle. error ( " Failed to generate video thumbnail: \( error) " )
83+ }
84+ }
85+
86+ @ViewBuilder
87+ var thumbnailView : some View {
88+ if let thumbnailSource = content. info? . thumbnailSource {
89+ MatrixImageView ( mediaSource: thumbnailSource, mimeType: content. info? . thumbnailInfo? . mimetype)
90+ } else if let generatedThumbnail {
91+ generatedThumbnail. resizable ( ) . scaledToFit ( )
92+ } else {
93+ Rectangle ( ) . fill ( Color . gray. opacity ( 0.3 ) )
94+ }
95+ }
96+
5097 var body : some View {
5198 VStack {
5299 if let video {
53100 TimelineVideoPlayer ( videoPlayer: video)
54101 . cornerRadius ( 6 )
55102 } else {
56103 Button ( action: { Task { await loadVideo ( ) } } ) {
57- MatrixImageView ( mediaSource : content . info ? . thumbnailSource , mimeType : content . info ? . thumbnailInfo ? . mimetype )
104+ thumbnailView
58105 . overlay {
59106 Image ( systemName: " play.fill " )
60107 . resizable ( )
@@ -73,5 +120,10 @@ struct MessageVideoView: View {
73120 }
74121 . frame ( maxHeight: maxHeight)
75122 . aspectRatio ( aspectRatio, contentMode: . fit)
123+ . task ( id: content. source. url ( ) , priority: . utility) {
124+ if content. info? . thumbnailSource == nil {
125+ await generateThumbnail ( )
126+ }
127+ }
76128 }
77129}
0 commit comments