Move media file loading logic to the SDK. (#702)

* Adopt getMediaFile for media previewing.
This commit is contained in:
Doug
2023-03-20 14:51:33 +00:00
committed by GitHub
parent bbd64092e7
commit efdb47a98a
42 changed files with 427 additions and 519 deletions
@@ -17,7 +17,6 @@
import Combine
import Foundation
import UIKit
import UniformTypeIdentifiers
class RoomTimelineController: RoomTimelineControllerProtocol {
private let userId: String
@@ -105,44 +104,34 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
guard let timelineItem = timelineItems.first(where: { $0.id == itemID }) else {
return .none
}
var source: MediaSourceProxy?
var title: String
switch timelineItem {
case let item as ImageRoomTimelineItem:
await loadFileForImageTimelineItem(item)
guard let index = timelineItems.firstIndex(where: { $0.id == itemID }),
let item = timelineItems[index] as? ImageRoomTimelineItem,
let fileURL = item.cachedFileURL else {
return .none
}
return .displayFile(fileURL: fileURL, title: item.body)
source = item.source
title = item.body
case let item as VideoRoomTimelineItem:
await loadVideoForTimelineItem(item)
guard let index = timelineItems.firstIndex(where: { $0.id == itemID }),
let item = timelineItems[index] as? VideoRoomTimelineItem,
let videoURL = item.cachedVideoURL else {
return .none
}
return .displayVideo(videoURL: videoURL, title: item.body)
source = item.source
title = item.body
case let item as FileRoomTimelineItem:
await loadFileForTimelineItem(item)
guard let index = timelineItems.firstIndex(where: { $0.id == itemID }),
let item = timelineItems[index] as? FileRoomTimelineItem,
let fileURL = item.cachedFileURL else {
return .none
}
return .displayFile(fileURL: fileURL, title: item.body)
source = item.source
title = item.body
case let item as AudioRoomTimelineItem:
await loadAudioForTimelineItem(item)
guard let index = timelineItems.firstIndex(where: { $0.id == itemID }),
let item = timelineItems[index] as? AudioRoomTimelineItem,
let audioURL = item.cachedAudioURL else {
return .none
}
// For now we are just displaying audio messages with the File preview until we create a timeline player for them.
return .displayFile(fileURL: audioURL, title: item.body)
source = item.source
title = item.body
default:
return .none
}
guard let source else { return .none }
switch await mediaProvider.loadFileFromSource(source) {
case .success(let file):
return .displayMediaFile(file: file, title: title)
case .failure:
return .none
}
}
func sendMessage(_ message: String, inReplyTo itemID: String?) async {
@@ -305,134 +294,4 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return false
}
private func loadVideoForTimelineItem(_ timelineItem: VideoRoomTimelineItem) async {
if timelineItem.cachedVideoURL != nil {
// already cached
return
}
guard let source = timelineItem.source else {
return
}
let fileExtension = movieFileExtension(for: timelineItem.body)
switch await mediaProvider.loadFileFromSource(source, fileExtension: fileExtension) {
case .success(let fileURL):
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
var item = timelineItems[index] as? VideoRoomTimelineItem else {
return
}
item.cachedVideoURL = fileURL
timelineItems[index] = item
case .failure:
break
}
}
/// Temporary method that generates a file extension for a video file name
/// using `UTType.movie` and falls back to .mp4 if anything goes wrong.
///
/// Ideally Rust should be able to handle this for us, otherwise we should be
/// attempting to detect the file type from the data itself.
private func movieFileExtension(for text: String) -> String {
let fallbackExtension = "mp4"
// This is not great. We could better estimate file extension from the mimetype.
guard let fileExtensionComponent = text.split(separator: ".").last else { return fallbackExtension }
let fileExtension = String(fileExtensionComponent)
// We can't trust that the extension provided is an extension that AVFoundation will accept.
guard let fileType = UTType(filenameExtension: fileExtension),
fileType.isSubtype(of: .movie)
else { return fallbackExtension }
return fileExtension
}
private func loadFileForImageTimelineItem(_ timelineItem: ImageRoomTimelineItem) async {
if timelineItem.cachedFileURL != nil {
// already cached
return
}
guard let source = timelineItem.source else {
return
}
// This is not great. We could better estimate file extension from the mimetype.
guard let fileExtension = timelineItem.body.split(separator: ".").last else {
return
}
switch await mediaProvider.loadFileFromSource(source, fileExtension: String(fileExtension)) {
case .success(let fileURL):
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
var item = timelineItems[index] as? ImageRoomTimelineItem else {
return
}
item.cachedFileURL = fileURL
timelineItems[index] = item
case .failure:
break
}
}
private func loadFileForTimelineItem(_ timelineItem: FileRoomTimelineItem) async {
if timelineItem.cachedFileURL != nil {
// already cached
return
}
guard let source = timelineItem.source else {
return
}
// This is not great. We could better estimate file extension from the mimetype.
guard let fileExtension = timelineItem.body.split(separator: ".").last else {
return
}
switch await mediaProvider.loadFileFromSource(source, fileExtension: String(fileExtension)) {
case .success(let fileURL):
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
var item = timelineItems[index] as? FileRoomTimelineItem else {
return
}
item.cachedFileURL = fileURL
timelineItems[index] = item
case .failure:
break
}
}
private func loadAudioForTimelineItem(_ timelineItem: AudioRoomTimelineItem) async {
if timelineItem.cachedAudioURL != nil {
// already cached
return
}
guard let source = timelineItem.source else {
return
}
// This is not great. We could better estimate file extension from the mimetype.
guard let fileExtension = timelineItem.body.split(separator: ".").last else {
return
}
switch await mediaProvider.loadFileFromSource(source, fileExtension: String(fileExtension)) {
case .success(let audioURL):
guard let index = timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
var item = timelineItems[index] as? AudioRoomTimelineItem else {
return
}
item.cachedAudioURL = audioURL
timelineItems[index] = item
case .failure:
break
}
}
}
@@ -25,8 +25,7 @@ enum RoomTimelineControllerCallback {
}
enum RoomTimelineControllerAction {
case displayVideo(videoURL: URL, title: String?)
case displayFile(fileURL: URL, title: String?)
case displayMediaFile(file: MediaFileHandleProxy, title: String?)
case none
}
@@ -27,7 +27,7 @@ protocol MessageContentProtocol: RoomMessageEventContentProtocol, CustomStringCo
/// A timeline item that represents an `m.room.message` event.
struct MessageTimelineItem<Content: MessageContentProtocol> {
let item: MatrixRustSDK.EventTimelineItem
let item: MatrixRustSDK.EventTimelineItemProtocol
let content: Content
var id: String {
@@ -82,7 +82,7 @@ extension MatrixRustSDK.ImageMessageContent: MessageContentProtocol { }
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.image`.
extension MessageTimelineItem where Content == MatrixRustSDK.ImageMessageContent {
var source: MediaSourceProxy {
.init(source: content.source)
.init(source: content.source, mimeType: content.info?.mimetype)
}
var width: CGFloat? {
@@ -97,8 +97,8 @@ extension MessageTimelineItem where Content == MatrixRustSDK.ImageMessageContent
content.info?.blurhash
}
var type: UTType? {
content.info?.mimetype.flatMap { UTType(mimeType: $0) }
var contentType: UTType? {
UTType(mimeType: content.info?.mimetype, fallbackFilename: content.body)
}
}
@@ -107,14 +107,14 @@ extension MatrixRustSDK.VideoMessageContent: MessageContentProtocol { }
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.video`.
extension MessageTimelineItem where Content == MatrixRustSDK.VideoMessageContent {
var source: MediaSourceProxy {
.init(source: content.source)
.init(source: content.source, mimeType: content.info?.mimetype)
}
var thumbnailSource: MediaSourceProxy? {
guard let src = content.info?.thumbnailSource else {
return nil
}
return .init(source: src)
return .init(source: src, mimeType: content.info?.thumbnailInfo?.mimetype)
}
var duration: UInt64 {
@@ -132,6 +132,10 @@ extension MessageTimelineItem where Content == MatrixRustSDK.VideoMessageContent
var blurhash: String? {
content.info?.blurhash
}
var contentType: UTType? {
UTType(mimeType: content.info?.mimetype, fallbackFilename: content.body)
}
}
extension MatrixRustSDK.FileMessageContent: MessageContentProtocol { }
@@ -139,14 +143,18 @@ extension MatrixRustSDK.FileMessageContent: MessageContentProtocol { }
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.file`.
extension MessageTimelineItem where Content == MatrixRustSDK.FileMessageContent {
var source: MediaSourceProxy {
.init(source: content.source)
.init(source: content.source, mimeType: content.info?.mimetype)
}
var thumbnailSource: MediaSourceProxy? {
guard let src = content.info?.thumbnailSource else {
return nil
}
return .init(source: src)
return .init(source: src, mimeType: content.info?.thumbnailInfo?.mimetype)
}
var contentType: UTType? {
UTType(mimeType: content.info?.mimetype, fallbackFilename: content.body)
}
}
@@ -155,10 +163,14 @@ extension MatrixRustSDK.AudioMessageContent: MessageContentProtocol { }
/// A timeline item that represents an `m.room.message` event with a `msgtype` of `m.audio`.
extension MessageTimelineItem where Content == MatrixRustSDK.AudioMessageContent {
var source: MediaSourceProxy {
.init(source: content.source)
.init(source: content.source, mimeType: content.info?.mimetype)
}
var duration: UInt64 {
content.info?.duration ?? 0
}
var contentType: UTType? {
UTType(mimeType: content.info?.mimetype, fallbackFilename: content.body)
}
}
@@ -15,6 +15,7 @@
//
import Foundation
import UniformTypeIdentifiers
struct AudioRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
@@ -26,7 +27,7 @@ struct AudioRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
let duration: UInt64
let source: MediaSourceProxy?
var cachedAudioURL: URL?
var contentType: UTType?
var properties = RoomTimelineItemProperties()
}
@@ -15,6 +15,7 @@
//
import UIKit
import UniformTypeIdentifiers
struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
@@ -27,7 +28,7 @@ struct FileRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hasha
let source: MediaSourceProxy?
let thumbnailSource: MediaSourceProxy?
var cachedFileURL: URL?
var contentType: UTType?
var properties = RoomTimelineItemProperties()
}
@@ -27,13 +27,12 @@ struct ImageRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
let sender: TimelineItemSender
let source: MediaSourceProxy?
var cachedFileURL: URL?
var width: CGFloat?
var height: CGFloat?
var aspectRatio: CGFloat?
var blurhash: String?
var type: UTType?
var contentType: UTType?
var properties = RoomTimelineItemProperties()
}
@@ -15,6 +15,7 @@
//
import UIKit
import UniformTypeIdentifiers
struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hashable {
let id: String
@@ -28,12 +29,12 @@ struct VideoRoomTimelineItem: EventBasedTimelineItemProtocol, Identifiable, Hash
let duration: UInt64
let source: MediaSourceProxy?
let thumbnailSource: MediaSourceProxy?
var cachedVideoURL: URL?
var width: CGFloat?
var height: CGFloat?
var aspectRatio: CGFloat?
var blurhash: String?
var contentType: UTType?
var properties = RoomTimelineItemProperties()
}
@@ -222,7 +222,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
height: message.height,
aspectRatio: aspectRatio,
blurhash: message.blurhash,
type: message.type,
contentType: message.contentType,
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus))
@@ -250,6 +250,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
height: message.height,
aspectRatio: aspectRatio,
blurhash: message.blurhash,
contentType: message.contentType,
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus))
@@ -265,7 +266,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
isEditable: eventItemProxy.isEditable,
sender: eventItemProxy.sender,
duration: message.duration,
source: message.source)
source: message.source,
contentType: message.contentType,
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus))
}
private func buildFileTimelineItemFromMessage(_ eventItemProxy: EventTimelineItemProxy,
@@ -279,6 +284,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
sender: eventItemProxy.sender,
source: message.source,
thumbnailSource: message.thumbnailSource,
contentType: message.contentType,
properties: RoomTimelineItemProperties(isEdited: message.isEdited,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus))