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"
}
}
static func h265(codec: String) -> SourceType? {
switch codec {
case "hvc1": return .h265
case "avc1": return .h264
default: return nil
}
}
}
struct Source {

View File

@@ -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 ""
}

View File

@@ -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

View File

@@ -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

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
func removeFileFromOutputFolder() {

View File

@@ -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
}
}
}

View File

@@ -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 += "<video autoplay loop muted playsinline>"
result += "Video not supported."
for video in videos {
result += "<source src='\(video.absoluteUrl)' type='\(video.type.htmlType!)'>"
result += "<source src='\(video.path)' type='\(video.type)'>"
}
result += "</video>"
}

View File

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

View File

@@ -432,6 +432,12 @@ final class Storage: ObservableObject {
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
func hasVideoThumbnail(for videoId: String) -> Bool {