Determine video codecs
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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 ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user