Determine video codecs

This commit is contained in:
Christoph Hagen
2025-08-31 18:04:00 +02:00
parent 96bd07bdb7
commit 9848de02cb
9 changed files with 93 additions and 17 deletions

View File

@@ -188,6 +188,14 @@ extension VideoBlock {
case .webm: "video/webm" 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 { struct Source {

View File

@@ -30,8 +30,8 @@ struct VideoCommand: CommandProcessor {
} }
results.require(file: file) results.require(file: file)
guard let videoType = file.type.htmlType else { guard let videoType = file.videoType() else {
invalid(markdown) invalid("File \(file.identifier) has an unknown video type")
return "" return ""
} }

View File

@@ -186,6 +186,7 @@ final class ImageGenerator {
let generatedImagePath = storage.outputPath(to: version.outputPath)!.path() let generatedImagePath = storage.outputPath(to: version.outputPath)!.path()
let quality = Int(version.quality * 100) let quality = Int(version.quality * 100)
// TODO: Run in security scope
let process = Process() let process = Process()
#warning("TODO: Move avifenc path to settings") #warning("TODO: Move avifenc path to settings")
process.launchPath = "/opt/homebrew/bin/avifenc" // Adjust based on installation process.launchPath = "/opt/homebrew/bin/avifenc" // Adjust based on installation

View File

@@ -82,7 +82,18 @@ final class PostListPageGenerator {
images.forEach(source.results.require) images.forEach(source.results.require)
media = .images(images) media = .images(images)
} else if localized.hasVideos { } 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) localized.images.forEach(source.results.require)
} else { } else {
media = nil media = nil

View File

@@ -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 // MARK: Paths
func removeFileFromOutputFolder() { func removeFileFromOutputFolder() {

View File

@@ -234,15 +234,4 @@ enum FileType: String {
default: return false default: return false
} }
} }
var htmlType: String? {
switch self {
case .mp4, .m4v:
return "video/mp4"
case .webm:
return "video/webm"
default:
return nil
}
}
} }

View File

@@ -1,13 +1,18 @@
struct PostVideo: HtmlProducer { struct PostVideo: HtmlProducer {
let videos: [FileResource] struct Video {
let path: String
let type: String
}
let videos: [Video]
func populate(_ result: inout String) { func populate(_ result: inout String) {
result += "<video autoplay loop muted playsinline>" result += "<video autoplay loop muted playsinline>"
result += "Video not supported." result += "Video not supported."
for video in videos { for video in videos {
result += "<source src='\(video.absoluteUrl)' type='\(video.type.htmlType!)'>" result += "<source src='\(video.path)' type='\(video.type)'>"
} }
result += "</video>" result += "</video>"
} }

View File

@@ -46,7 +46,7 @@ struct FeedEntryData {
enum Media { enum Media {
case images([ImageSet]) case images([ImageSet])
case video([FileResource]) case video([PostVideo.Video])
} }
var requiresSwiper: Bool { var requiresSwiper: Bool {

View File

@@ -432,6 +432,12 @@ final class Storage: ObservableObject {
return await contentScope.with(relativePath: path, perform: operation) return await contentScope.with(relativePath: path, perform: operation)
} }
func with<T>(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 // MARK: Video thumbnails
func hasVideoThumbnail(for videoId: String) -> Bool { func hasVideoThumbnail(for videoId: String) -> Bool {