From c0487144bb14e1c8e1437f053efe7f876888ec06 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Thu, 5 Mar 2026 17:42:49 +0100 Subject: [PATCH 1/3] Start on video playback --- .../MatrixRustSDK+MessageContent.swift | 13 ++++ Mactrix/Views/ChatView/ChatMessageView.swift | 2 +- Mactrix/Views/ChatView/MessageVideoView.swift | 67 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 Mactrix/Extensions/MatrixRustSDK+MessageContent.swift create mode 100644 Mactrix/Views/ChatView/MessageVideoView.swift diff --git a/Mactrix/Extensions/MatrixRustSDK+MessageContent.swift b/Mactrix/Extensions/MatrixRustSDK+MessageContent.swift new file mode 100644 index 0000000..fc13dd8 --- /dev/null +++ b/Mactrix/Extensions/MatrixRustSDK+MessageContent.swift @@ -0,0 +1,13 @@ +import MatrixRustSDK + +protocol MessageContent { + var filename: String { get } + var caption: String? { get } + var formattedCaption: FormattedBody? { get } + var source: MediaSource { get } +} + +extension FileMessageContent: MessageContent {} +extension AudioMessageContent: MessageContent {} +extension VideoMessageContent: MessageContent {} +extension ImageMessageContent: MessageContent {} diff --git a/Mactrix/Views/ChatView/ChatMessageView.swift b/Mactrix/Views/ChatView/ChatMessageView.swift index 8a4baf6..5937e2d 100644 --- a/Mactrix/Views/ChatView/ChatMessageView.swift +++ b/Mactrix/Views/ChatView/ChatMessageView.swift @@ -71,7 +71,7 @@ struct ChatMessageView: View, UI.MessageEventActions { case let .audio(content: content): Text("Audio: \(content.caption ?? "no caption") \(content.filename)").textSelection(.enabled) case let .video(content: content): - Text("Video: \(content.caption ?? "no caption") \(content.filename)").textSelection(.enabled) + MessageVideoView(content: content) case let .file(content: content): MessageFileView(content: content) case let .gallery(content: content): diff --git a/Mactrix/Views/ChatView/MessageVideoView.swift b/Mactrix/Views/ChatView/MessageVideoView.swift new file mode 100644 index 0000000..279155f --- /dev/null +++ b/Mactrix/Views/ChatView/MessageVideoView.swift @@ -0,0 +1,67 @@ +import AVKit +import MatrixRustSDK +import Models +import OSLog +import SwiftUI + +struct MessageVideoView: View { + @Environment(AppState.self) private var appState + let content: VideoMessageContent + + @State private var fileHandle: MediaFileHandle? + @State private var video: AVPlayer? + + var aspectRatio: CGFloat? { + guard let info = content.info, + let height = info.height, + let width = info.width else { return nil } + + return CGFloat(width) / CGFloat(height) + } + + var maxHeight: CGFloat { + guard let height = content.info?.height else { return 300 } + return min(CGFloat(height), 300) + } + + func loadVideo() async { + guard let client = appState.matrixClient?.client else { return } + + do { + let handle = try await client.getMediaFile( + mediaSource: content.source, + filename: content.filename, + mimeType: content.info?.mimetype ?? "", + useCache: true, + tempDir: NSTemporaryDirectory() + ) + + fileHandle = handle + let path = try handle.path() + let url = URL(filePath: path, directoryHint: .notDirectory) + + video = AVPlayer(url: url) + } catch { + Logger.viewCycle.error("Failed to load video: \(error)") + } + } + + var body: some View { + VStack { + if let video { + VideoPlayer(player: video) + .cornerRadius(6) + } else { + Button(action: { Task { await loadVideo() } }) { + Text("Load video") + } + } + if let caption = content.caption, !caption.isEmpty { + Text(caption.formatAsMarkdown) + .textSelection(.enabled) + } + } + .frame(maxHeight: maxHeight) + .aspectRatio(aspectRatio, contentMode: .fit) + } +} From b108e0212f13ab213bc4c03f434db1f67db5fbe2 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Fri, 6 Mar 2026 14:58:57 +0100 Subject: [PATCH 2/3] Add more controls to video and start on thumbnail --- Mactrix/Views/ChatView/MessageVideoView.swift | 9 +++- .../Views/ChatView/TimelineVideoPlayer.swift | 28 +++++++++++ Mactrix/Views/MatrixImageView.swift | 50 +++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 Mactrix/Views/ChatView/TimelineVideoPlayer.swift create mode 100644 Mactrix/Views/MatrixImageView.swift diff --git a/Mactrix/Views/ChatView/MessageVideoView.swift b/Mactrix/Views/ChatView/MessageVideoView.swift index 279155f..979fdf0 100644 --- a/Mactrix/Views/ChatView/MessageVideoView.swift +++ b/Mactrix/Views/ChatView/MessageVideoView.swift @@ -41,6 +41,7 @@ struct MessageVideoView: View { let url = URL(filePath: path, directoryHint: .notDirectory) video = AVPlayer(url: url) + video?.play() } catch { Logger.viewCycle.error("Failed to load video: \(error)") } @@ -49,12 +50,16 @@ struct MessageVideoView: View { var body: some View { VStack { if let video { - VideoPlayer(player: video) + TimelineVideoPlayer(videoPlayer: video) .cornerRadius(6) } else { Button(action: { Task { await loadVideo() } }) { - Text("Load video") + MatrixImageView(mediaSource: content.info?.thumbnailSource, mimeType: content.info?.thumbnailInfo?.mimetype) + .overlay { + Image(systemName: "play.fill") + } } + .buttonStyle(.plain) } if let caption = content.caption, !caption.isEmpty { Text(caption.formatAsMarkdown) diff --git a/Mactrix/Views/ChatView/TimelineVideoPlayer.swift b/Mactrix/Views/ChatView/TimelineVideoPlayer.swift new file mode 100644 index 0000000..1e6327e --- /dev/null +++ b/Mactrix/Views/ChatView/TimelineVideoPlayer.swift @@ -0,0 +1,28 @@ +import AppKit +import AVKit +import SwiftUI + +struct TimelineVideoPlayer: NSViewRepresentable { + let videoPlayer: AVPlayer + + func makeNSView(context: Context) -> AVPlayerView { + let playerView = AVPlayerView() + playerView.allowsPictureInPicturePlayback = true + playerView.showsFullScreenToggleButton = true + playerView.showsSharingServiceButton = true + + playerView.player = videoPlayer + + return playerView + } + + func updateNSView(_ playerView: AVPlayerView, context: Context) { + playerView.player = videoPlayer + } + + func makeCoordinator() -> Coordinator { + return Coordinator() + } + + class Coordinator {} +} diff --git a/Mactrix/Views/MatrixImageView.swift b/Mactrix/Views/MatrixImageView.swift new file mode 100644 index 0000000..6a2e614 --- /dev/null +++ b/Mactrix/Views/MatrixImageView.swift @@ -0,0 +1,50 @@ +import MatrixRustSDK +import SwiftUI +import UniformTypeIdentifiers + +struct MatrixImageView: View { + let mediaSource: MediaSource? + let mimeType: String? + + @Environment(AppState.self) private var appState + @State private var image: Image? = nil + @State private var errorMessage: String? = nil + + @ViewBuilder + var content: some View { + if let image { + image + } else if let errorMessage { + VStack { + ContentUnavailableView("Error loading image", image: "photo.badge.exclamationmark") + Text(errorMessage) + } + } else { + ProgressView { + Text("Fetching image") + } + } + } + + var body: some View { + content + .task(id: mediaSource?.url(), priority: .utility) { + guard let matrixClient = appState.matrixClient else { + errorMessage = "Matrix client not available" + return + } + + guard let mediaSource else { + return + } + + do { + let data = try await matrixClient.client.getMediaContent(mediaSource: mediaSource) + let contentType = mimeType.flatMap(UTType.init) + image = try await Image(importing: data, contentType: contentType) + } catch { + errorMessage = error.localizedDescription + } + } + } +} From e0738ea6f1e2368686f449a83be763394e098996 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Fri, 6 Mar 2026 15:08:50 +0100 Subject: [PATCH 3/3] Fix thumbnail --- Mactrix/Views/ChatView/MessageVideoView.swift | 5 +++++ Mactrix/Views/MatrixImageView.swift | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Mactrix/Views/ChatView/MessageVideoView.swift b/Mactrix/Views/ChatView/MessageVideoView.swift index 979fdf0..95d3e12 100644 --- a/Mactrix/Views/ChatView/MessageVideoView.swift +++ b/Mactrix/Views/ChatView/MessageVideoView.swift @@ -57,6 +57,11 @@ struct MessageVideoView: View { MatrixImageView(mediaSource: content.info?.thumbnailSource, mimeType: content.info?.thumbnailInfo?.mimetype) .overlay { Image(systemName: "play.fill") + .resizable() + .foregroundStyle(.white) + .shadow(radius: 4) + .frame(width: 48, height: 48) + .opacity(0.9) } } .buttonStyle(.plain) diff --git a/Mactrix/Views/MatrixImageView.swift b/Mactrix/Views/MatrixImageView.swift index 6a2e614..180379b 100644 --- a/Mactrix/Views/MatrixImageView.swift +++ b/Mactrix/Views/MatrixImageView.swift @@ -33,14 +33,14 @@ struct MatrixImageView: View { errorMessage = "Matrix client not available" return } - + guard let mediaSource else { return } do { let data = try await matrixClient.client.getMediaContent(mediaSource: mediaSource) - let contentType = mimeType.flatMap(UTType.init) + let contentType = mimeType.flatMap { UTType(mimeType: $0) } image = try await Image(importing: data, contentType: contentType) } catch { errorMessage = error.localizedDescription