Determine video codecs
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>"
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ struct FeedEntryData {
|
||||
|
||||
enum Media {
|
||||
case images([ImageSet])
|
||||
case video([FileResource])
|
||||
case video([PostVideo.Video])
|
||||
}
|
||||
|
||||
var requiresSwiper: Bool {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user