From 9848de02cb8e66ae460240d0cada963ae856ba9a Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Sun, 31 Aug 2025 18:04:00 +0200 Subject: [PATCH] Determine video codecs --- .../Generator/Blocks/VideoBlock.swift | 8 +++ .../Generator/Commands/VideoCommand.swift | 4 +- .../Generator/ImageGenerator.swift | 1 + .../Post Lists/PostListPageGenerator.swift | 13 ++++- CHDataManagement/Model/FileResource.swift | 56 +++++++++++++++++++ CHDataManagement/Model/FileType.swift | 11 ---- .../ContentElements/PostVideo.swift | 9 ++- .../Page Elements/FeedEntryData.swift | 2 +- CHDataManagement/Storage/Storage.swift | 6 ++ 9 files changed, 93 insertions(+), 17 deletions(-) diff --git a/CHDataManagement/Generator/Blocks/VideoBlock.swift b/CHDataManagement/Generator/Blocks/VideoBlock.swift index acd88e5..8206ec4 100644 --- a/CHDataManagement/Generator/Blocks/VideoBlock.swift +++ b/CHDataManagement/Generator/Blocks/VideoBlock.swift @@ -188,6 +188,14 @@ extension VideoBlock { case .webm: "video/webm" } } + + static func h265(codec: String) -> SourceType? { + switch codec { + case "hvc1": return .h265 + case "avc1": return .h264 + default: return nil + } + } } struct Source { diff --git a/CHDataManagement/Generator/Commands/VideoCommand.swift b/CHDataManagement/Generator/Commands/VideoCommand.swift index aade462..f990b24 100644 --- a/CHDataManagement/Generator/Commands/VideoCommand.swift +++ b/CHDataManagement/Generator/Commands/VideoCommand.swift @@ -30,8 +30,8 @@ struct VideoCommand: CommandProcessor { } results.require(file: file) - guard let videoType = file.type.htmlType else { - invalid(markdown) + guard let videoType = file.videoType() else { + invalid("File \(file.identifier) has an unknown video type") return "" } diff --git a/CHDataManagement/Generator/ImageGenerator.swift b/CHDataManagement/Generator/ImageGenerator.swift index f2a8185..abf8983 100644 --- a/CHDataManagement/Generator/ImageGenerator.swift +++ b/CHDataManagement/Generator/ImageGenerator.swift @@ -186,6 +186,7 @@ final class ImageGenerator { let generatedImagePath = storage.outputPath(to: version.outputPath)!.path() let quality = Int(version.quality * 100) + // TODO: Run in security scope let process = Process() #warning("TODO: Move avifenc path to settings") process.launchPath = "/opt/homebrew/bin/avifenc" // Adjust based on installation diff --git a/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift index d8dd02f..19aedac 100644 --- a/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift +++ b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift @@ -82,7 +82,18 @@ final class PostListPageGenerator { images.forEach(source.results.require) media = .images(images) } else if localized.hasVideos { - media = .video(localized.images) + let videos: [PostVideo.Video] = localized.images.compactMap { file -> PostVideo.Video? in + guard file.type.isVideo else { + self.source.results.warning("File \(file.identifier) ignored due to videos present in the post") + return nil + } + guard let type = file.videoType() else { + self.source.results.warning("Video \(file.identifier) ignored due to unknown video type") + return nil + } + return .init(path: file.absoluteUrl, type: type) + } + media = .video(videos) localized.images.forEach(source.results.require) } else { media = nil diff --git a/CHDataManagement/Model/FileResource.swift b/CHDataManagement/Model/FileResource.swift index 23e6b8e..2d57e2b 100644 --- a/CHDataManagement/Model/FileResource.swift +++ b/CHDataManagement/Model/FileResource.swift @@ -343,6 +343,62 @@ final class FileResource: Item, LocalizedItem { } } + private var _videoType: String? + + func videoType() -> String? { + if let _videoType { + return _videoType + } + _videoType = determineVideoType() + return _videoType + } + + private func determineVideoType() -> String? { + #warning("TODO: Move ffmpeg path to settings") + switch type { + case .webm: + return "video/webm" + case .mp4, .m4v: + if isExternallyStored { + return "video/mp4" + } + break + default: + return nil + } + + return content.storage.with(file: identifier) { path in + let process = Process() + let arguments = "-v error -select_streams v:0 -show_entries stream=codec_tag_string -of default=noprint_wrappers=1:nokey=1 \(path.path())" + .components(separatedBy: " ") + process.launchPath = "/opt/homebrew/bin/ffprobe" + process.arguments = Array(arguments) + + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = pipe + + process.launch() + process.waitUntilExit() + + let outputData = pipe.fileHandleForReading.readDataToEndOfFile() + let outputString = String(data: outputData, encoding: .utf8) ?? "" + + if process.terminationStatus != 0 { + print("Failed to determine video type for \(identifier)") + print(outputString) + return nil + } + let firstLine = outputString.components(separatedBy: .newlines).first!.trimmed + guard let type = VideoBlock.SourceType.h265(codec: firstLine) else { + print("Unknown codec type for \(identifier): \(firstLine)") + print(outputString) + return "video/mp4" + } + return type.mimeType + } + } + // MARK: Paths func removeFileFromOutputFolder() { diff --git a/CHDataManagement/Model/FileType.swift b/CHDataManagement/Model/FileType.swift index e6f01af..adab248 100644 --- a/CHDataManagement/Model/FileType.swift +++ b/CHDataManagement/Model/FileType.swift @@ -234,15 +234,4 @@ enum FileType: String { default: return false } } - - var htmlType: String? { - switch self { - case .mp4, .m4v: - return "video/mp4" - case .webm: - return "video/webm" - default: - return nil - } - } } diff --git a/CHDataManagement/Page Elements/ContentElements/PostVideo.swift b/CHDataManagement/Page Elements/ContentElements/PostVideo.swift index 50cf3c9..3d8bf6c 100644 --- a/CHDataManagement/Page Elements/ContentElements/PostVideo.swift +++ b/CHDataManagement/Page Elements/ContentElements/PostVideo.swift @@ -1,13 +1,18 @@ struct PostVideo: HtmlProducer { - let videos: [FileResource] + struct Video { + let path: String + let type: String + } + + let videos: [Video] func populate(_ result: inout String) { result += "" } diff --git a/CHDataManagement/Page Elements/FeedEntryData.swift b/CHDataManagement/Page Elements/FeedEntryData.swift index 7a7937a..facac4d 100644 --- a/CHDataManagement/Page Elements/FeedEntryData.swift +++ b/CHDataManagement/Page Elements/FeedEntryData.swift @@ -46,7 +46,7 @@ struct FeedEntryData { enum Media { case images([ImageSet]) - case video([FileResource]) + case video([PostVideo.Video]) } var requiresSwiper: Bool { diff --git a/CHDataManagement/Storage/Storage.swift b/CHDataManagement/Storage/Storage.swift index f49522d..417fdb8 100644 --- a/CHDataManagement/Storage/Storage.swift +++ b/CHDataManagement/Storage/Storage.swift @@ -432,6 +432,12 @@ final class Storage: ObservableObject { return await contentScope.with(relativePath: path, perform: operation) } + func with(file fileId: String, perform operation: (URL) -> T?) -> T? { + guard let contentScope else { return nil } + let path = filePath(file: fileId) + return contentScope.with(relativePath: path, perform: operation) + } + // MARK: Video thumbnails func hasVideoThumbnail(for videoId: String) -> Bool {