eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import AVFoundation
3import UniformTypeIdentifiers
4#if canImport(AppKit) && !targetEnvironment(macCatalyst)
5import AppKit
6#else
7import UIKit
8#endif
9
10// Plays a decrypted video WITHOUT writing plaintext to disk: an AVURLAsset on
11// a custom scheme whose resource loader serves byte ranges straight from the
12// in-memory Data. AVPlayer can't otherwise play from memory — handing it a
13// file would mean silently persisting decrypted bytes, the same posture
14// problem QuickLook has (see the photo lightbox).
15//
16// NB: AVAssetResourceLoader holds its delegate WEAKLY — whoever presents the
17// player must keep this loader alive for the asset's lifetime.
18/// A poster frame for a clip held in memory (staged-chip previews, and
19/// anything else that wants a quick look at second zero) — the same in-memory
20/// loader the player uses, so the plaintext never touches disk.
21public func videoPosterImage(data: Data, mimeType: String?,
22 maxDim: Double = 288) async -> PlatformImage? {
23 let loader = MemoryVideoAsset(data: data, mimeType: mimeType)
24 let generator = AVAssetImageGenerator(asset: loader.makeAsset())
25 generator.appliesPreferredTrackTransform = true
26 generator.maximumSize = CGSize(width: maxDim, height: maxDim)
27 guard let cg = try? await generator.image(at: .zero).image else { return nil }
28 #if canImport(AppKit) && !targetEnvironment(macCatalyst)
29 return NSImage(cgImage: cg, size: NSSize(width: cg.width, height: cg.height))
30 #else
31 return UIImage(cgImage: cg)
32 #endif
33}
34
35public final class MemoryVideoAsset: NSObject, AVAssetResourceLoaderDelegate {
36 private let data: Data
37 private let utiType: String
38 private let queue = DispatchQueue(label: "lchat.memory-video")
39
40 public init(data: Data, mimeType: String?) {
41 self.data = data
42 self.utiType = mimeType.flatMap { UTType(mimeType: $0)?.identifier }
43 ?? UTType.mpeg4Movie.identifier
44 super.init()
45 }
46
47 /// The playable asset. The custom scheme forces every load through the
48 /// resource loader; the bogus host/path are never used.
49 public func makeAsset() -> AVURLAsset {
50 let asset = AVURLAsset(url: URL(string: "lchat-mem://video/clip")!)
51 asset.resourceLoader.setDelegate(self, queue: queue)
52 return asset
53 }
54
55 public func resourceLoader(_ resourceLoader: AVAssetResourceLoader,
56 shouldWaitForLoadingOfRequestedResource
57 loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
58 if let info = loadingRequest.contentInformationRequest {
59 info.contentType = utiType
60 info.contentLength = Int64(data.count)
61 info.isByteRangeAccessSupported = true
62 }
63 if let request = loadingRequest.dataRequest {
64 let from = Int(request.requestedOffset)
65 let length = request.requestsAllDataToEndOfResource
66 ? data.count - from
67 : min(request.requestedLength, data.count - from)
68 if from >= 0, length > 0, from + length <= data.count {
69 request.respond(with: data.subdata(in: from ..< from + length))
70 }
71 }
72 loadingRequest.finishLoading()
73 return true
74 }
75}
76