Skip to content

Commit 76d4fdc

Browse files
committed
Add video compression controls to writer
1 parent 56e4c64 commit 76d4fdc

8 files changed

Lines changed: 679 additions & 32 deletions

File tree

Planet.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@
179179
2A95E6892A19A3CA001288B8 /* WriterVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7211403BD64F1D6E5B14D69E /* WriterVideoView.swift */; };
180180
D7A100012F6AFA1100AA0001 /* VideoAttachmentInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A100052F6AFA1100AA0001 /* VideoAttachmentInfo.swift */; };
181181
D7A100032F6AFA1100AA0001 /* VideoInfoRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A100062F6AFA1100AA0001 /* VideoInfoRow.swift */; };
182+
D7A100072F6AFA1100AA0001 /* VideoCompressionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A100092F6AFA1100AA0001 /* VideoCompressionJob.swift */; };
182183
2A95E68B2A19A3CA001288B8 /* WriterAudioView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72114341F4E465B909F76B33 /* WriterAudioView.swift */; };
183184
2A95E68C2A19A3CA001288B8 /* WriterTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF4003827F2321F005DF1A9 /* WriterTextView.swift */; };
184185
2A95E68D2A19A3CA001288B8 /* WriterWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF4006527F379BF005DF1A9 /* WriterWebView.swift */; };
@@ -518,6 +519,7 @@
518519
721145BB829F2234DC279697 /* WriterVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7211403BD64F1D6E5B14D69E /* WriterVideoView.swift */; };
519520
D7A100022F6AFA1100AA0001 /* VideoAttachmentInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A100052F6AFA1100AA0001 /* VideoAttachmentInfo.swift */; };
520521
D7A100042F6AFA1100AA0001 /* VideoInfoRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A100062F6AFA1100AA0001 /* VideoInfoRow.swift */; };
522+
D7A100082F6AFA1100AA0001 /* VideoCompressionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A100092F6AFA1100AA0001 /* VideoCompressionJob.swift */; };
521523
721145EEDE381A8BDED43E93 /* FeedUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72114982114B99197B3B0756 /* FeedUtils.swift */; };
522524
7211466B9B1D3BAF1FC23CA1 /* PlanetArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7211459D5283D74D29C8F0DC /* PlanetArticle.swift */; };
523525
72114683927B9B11EFE02DD4 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72114939C0E92FC2CFBD5F11 /* OnboardingView.swift */; };
@@ -858,6 +860,7 @@
858860
6AF8B0272C42BD400007E2AE /* ArticleListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleListViewModel.swift; sourceTree = "<group>"; };
859861
D7A100052F6AFA1100AA0001 /* VideoAttachmentInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoAttachmentInfo.swift; sourceTree = "<group>"; };
860862
D7A100062F6AFA1100AA0001 /* VideoInfoRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoInfoRow.swift; sourceTree = "<group>"; };
863+
D7A100092F6AFA1100AA0001 /* VideoCompressionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCompressionJob.swift; sourceTree = "<group>"; };
861864
7211403BD64F1D6E5B14D69E /* WriterVideoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WriterVideoView.swift; sourceTree = "<group>"; };
862865
7211404DFC1B4D23F84EA279 /* PlanetError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlanetError.swift; sourceTree = "<group>"; };
863866
721140CF1813A5B4340B90F3 /* FollowingPlanetAvatarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FollowingPlanetAvatarView.swift; sourceTree = "<group>"; };
@@ -1346,6 +1349,7 @@
13461349
7211403BD64F1D6E5B14D69E /* WriterVideoView.swift */,
13471350
D7A100052F6AFA1100AA0001 /* VideoAttachmentInfo.swift */,
13481351
D7A100062F6AFA1100AA0001 /* VideoInfoRow.swift */,
1352+
D7A100092F6AFA1100AA0001 /* VideoCompressionJob.swift */,
13491353
72114341F4E465B909F76B33 /* WriterAudioView.swift */,
13501354
);
13511355
path = Writer;
@@ -2077,6 +2081,7 @@
20772081
2A95E6892A19A3CA001288B8 /* WriterVideoView.swift in Sources */,
20782082
D7A100012F6AFA1100AA0001 /* VideoAttachmentInfo.swift in Sources */,
20792083
D7A100032F6AFA1100AA0001 /* VideoInfoRow.swift in Sources */,
2084+
D7A100072F6AFA1100AA0001 /* VideoCompressionJob.swift in Sources */,
20802085
2A996AD92A1DA6CF00BEF898 /* PlanetDownloadModel.swift in Sources */,
20812086
2A95E6E52A19A64F001288B8 /* HelpLinkButton.swift in Sources */,
20822087
2A95E67C2A19A3C4001288B8 /* PodcastUtils.swift in Sources */,
@@ -2392,6 +2397,7 @@
23922397
721145BB829F2234DC279697 /* WriterVideoView.swift in Sources */,
23932398
D7A100022F6AFA1100AA0001 /* VideoAttachmentInfo.swift in Sources */,
23942399
D7A100042F6AFA1100AA0001 /* VideoInfoRow.swift in Sources */,
2400+
D7A100082F6AFA1100AA0001 /* VideoCompressionJob.swift in Sources */,
23952401
6A8A6A592BA5B0CA00FBF67D /* MyArticleModel+Views.swift in Sources */,
23962402
2AE44EAC28CD218200944786 /* PlanetSettingsGeneralView.swift in Sources */,
23972403
6A03269F2C81DF2100FEDD55 /* String+EthereumChecksumAddress.swift in Sources */,

Planet/Entities/AttachmentModel.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ enum AttachmentType: String, Codable {
4444
class Attachment: Codable, Equatable, Hashable, ObservableObject {
4545
let name: String
4646
@Published var type: AttachmentType
47+
@Published var videoCompressionPreset: String?
4748
let created: Date
4849

4950
@Published var thumbnail: NSImage? = nil
@@ -119,6 +120,7 @@ class Attachment: Codable, Equatable, Hashable, ObservableObject {
119120
func hash(into hasher: inout Hasher) {
120121
hasher.combine(name)
121122
hasher.combine(type)
123+
hasher.combine(videoCompressionPreset)
122124
hasher.combine(draft)
123125
hasher.combine(created)
124126
}
@@ -133,32 +135,37 @@ class Attachment: Codable, Equatable, Hashable, ObservableObject {
133135
return lhs.name == rhs.name
134136
&& lhs.draft == rhs.draft
135137
&& lhs.type == rhs.type
138+
&& lhs.videoCompressionPreset == rhs.videoCompressionPreset
136139
&& lhs.created == rhs.created
137140
}
138141

139142
enum CodingKeys: String, CodingKey {
140143
case name
141144
case type
145+
case videoCompressionPreset
142146
case created
143147
}
144148

145149
required init(from decoder: Decoder) throws {
146150
let container = try decoder.container(keyedBy: CodingKeys.self)
147151
name = try container.decode(String.self, forKey: .name)
148152
type = try container.decode(AttachmentType.self, forKey: .type)
153+
videoCompressionPreset = try container.decodeIfPresent(String.self, forKey: .videoCompressionPreset)
149154
created = try container.decode(Date.self, forKey: .created)
150155
}
151156

152157
func encode(to encoder: Encoder) throws {
153158
var container = encoder.container(keyedBy: CodingKeys.self)
154159
try container.encode(name, forKey: .name)
155160
try container.encode(type, forKey: .type)
161+
try container.encodeIfPresent(videoCompressionPreset, forKey: .videoCompressionPreset)
156162
try container.encode(created, forKey: .created)
157163
}
158164

159165
init(name: String, type: AttachmentType) {
160166
self.name = name
161167
self.type = type
168+
videoCompressionPreset = nil
162169
created = Date()
163170
}
164171

Planet/Entities/DraftModel.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,30 @@ class DraftModel: Identifiable, Equatable, Hashable, Codable, ObservableObject {
337337
)
338338
}
339339

340+
@discardableResult
341+
func replaceVideoAttachment(
342+
_ attachment: Attachment,
343+
withCompressedVideoAt compressedURL: URL,
344+
compressionPreset: String
345+
) throws -> Attachment {
346+
let oldName = attachment.name
347+
let oldPath = attachment.path
348+
let newAttachment = try addAttachment(path: compressedURL, type: .video)
349+
newAttachment.videoCompressionPreset = compressionPreset
350+
351+
if oldName != newAttachment.name {
352+
content = content.replacingOccurrences(of: oldName, with: newAttachment.name)
353+
}
354+
355+
if oldPath != newAttachment.path, FileManager.default.fileExists(atPath: oldPath.path) {
356+
try FileManager.default.removeItem(at: oldPath)
357+
}
358+
359+
try renderPreview()
360+
try save()
361+
return newAttachment
362+
}
363+
340364
func deleteAttachment(name: String) {
341365
if let attachment = attachments.first(where: { $0.name == name }) {
342366
do {

Planet/Writer/VideoAttachmentInfo.swift

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import Foundation
44
struct VideoAttachmentInfo {
55
let duration: String
66
let resolution: String
7+
let pixelWidth: Int?
8+
let pixelHeight: Int?
79
let codec: String
10+
let colorSpace: String
11+
let containsHDR: Bool
812
let bitrate: String
913
let frameRate: String
1014
let fileSize: String
@@ -14,9 +18,10 @@ struct VideoAttachmentInfo {
1418
let durationSeconds = await durationSeconds(for: asset)
1519
let videoTrack = await firstVideoTrack(for: asset)
1620
let fileSizeBytes = fileSizeBytes(for: url)
17-
let resolutionText = await resolution(for: videoTrack)
21+
let resolutionDetails = await resolutionDetails(for: videoTrack)
1822
let trackBitrate = await trackBitrate(for: videoTrack)
1923
let trackFrameRate = await trackFrameRate(for: videoTrack)
24+
let colorSpaceDetails = await colorSpaceDetails(for: videoTrack)
2025
let durationText: String = formattedDuration(durationSeconds)
2126
let codecText: String = await codecName(for: videoTrack) ?? "Unknown"
2227
let bitrateText: String = formattedBitrate(
@@ -29,8 +34,12 @@ struct VideoAttachmentInfo {
2934

3035
return VideoAttachmentInfo(
3136
duration: durationText,
32-
resolution: resolutionText,
37+
resolution: resolutionDetails.text,
38+
pixelWidth: resolutionDetails.width,
39+
pixelHeight: resolutionDetails.height,
3340
codec: codecText,
41+
colorSpace: colorSpaceDetails.text,
42+
containsHDR: colorSpaceDetails.containsHDR,
3443
bitrate: bitrateText,
3544
frameRate: frameRateText,
3645
fileSize: fileSizeText
@@ -60,22 +69,67 @@ struct VideoAttachmentInfo {
6069
return Int64(fileSize)
6170
}
6271

63-
private static func resolution(for track: AVAssetTrack?) async -> String {
72+
private static func resolutionDetails(
73+
for track: AVAssetTrack?
74+
) async -> (text: String, width: Int?, height: Int?) {
6475
guard let track,
6576
let naturalSize = try? await track.load(.naturalSize)
6677
else {
67-
return "Unknown"
78+
return ("Unknown", nil, nil)
6879
}
6980

7081
let preferredTransform = (try? await track.load(.preferredTransform)) ?? .identity
7182
let transformedSize = naturalSize.applying(preferredTransform)
7283
let width = Int(abs(transformedSize.width).rounded())
7384
let height = Int(abs(transformedSize.height).rounded())
7485
guard width > 0, height > 0 else {
75-
return "Unknown"
86+
return ("Unknown", nil, nil)
87+
}
88+
89+
return ("\(width)x\(height)", width, height)
90+
}
91+
92+
private static func colorSpaceDetails(
93+
for track: AVAssetTrack?
94+
) async -> (text: String, containsHDR: Bool) {
95+
guard let track else {
96+
return ("Unknown", false)
97+
}
98+
99+
let formatDescriptions = (try? await track.load(.formatDescriptions)) ?? []
100+
let extensions: NSDictionary? = formatDescriptions.first.flatMap { formatDescription in
101+
guard let rawExtensions = CMFormatDescriptionGetExtensions(formatDescription) else {
102+
return nil
103+
}
104+
return rawExtensions as NSDictionary
105+
}
106+
107+
let colorPrimaries =
108+
extensions?[kCMFormatDescriptionExtension_ColorPrimaries as String] as? String
109+
let transferFunction =
110+
extensions?[kCMFormatDescriptionExtension_TransferFunction as String] as? String
111+
let containsHDR =
112+
track.hasMediaCharacteristic(.containsHDRVideo)
113+
|| transferFunction == AVVideoTransferFunction_SMPTE_ST_2084_PQ
114+
|| transferFunction == AVVideoTransferFunction_ITU_R_2100_HLG
115+
116+
let primariesName = colorPrimariesName(for: colorPrimaries)
117+
let transferFunctionName = hdrTransferFunctionName(for: transferFunction)
118+
let details = [primariesName, transferFunctionName].compactMap { $0 }
119+
120+
if containsHDR {
121+
if details.isEmpty {
122+
return ("HDR", true)
123+
} else {
124+
return ("HDR (\(details.joined(separator: ", ")))", true)
125+
}
76126
}
77127

78-
return "\(width)x\(height)"
128+
if let primariesName {
129+
return (primariesName, false)
130+
}
131+
132+
return ("Unknown", false)
79133
}
80134

81135
private static func formattedDuration(_ durationSeconds: Double?) -> String {
@@ -219,6 +273,30 @@ struct VideoAttachmentInfo {
219273
return ByteCountFormatter.string(fromByteCount: fileSizeBytes, countStyle: .file)
220274
}
221275

276+
private static func colorPrimariesName(for value: String?) -> String? {
277+
switch value {
278+
case AVVideoColorPrimaries_ITU_R_709_2:
279+
return "BT.709"
280+
case AVVideoColorPrimaries_ITU_R_2020:
281+
return "BT.2020"
282+
case AVVideoColorPrimaries_P3_D65:
283+
return "P3 D65"
284+
default:
285+
return nil
286+
}
287+
}
288+
289+
private static func hdrTransferFunctionName(for value: String?) -> String? {
290+
switch value {
291+
case AVVideoTransferFunction_SMPTE_ST_2084_PQ:
292+
return "PQ"
293+
case AVVideoTransferFunction_ITU_R_2100_HLG:
294+
return "HLG"
295+
default:
296+
return nil
297+
}
298+
}
299+
222300
private static func fourCCString(_ value: FourCharCode) -> String {
223301
let bytes: [UInt8] = [
224302
UInt8(truncatingIfNeeded: value >> 24),

0 commit comments

Comments
 (0)