struct VideoBlock: OrderedKeyBlockProcessor { static let blockId: ContentBlock = .video let content: Content let results: PageGenerationResults let language: ContentLanguage init(content: Content, results: PageGenerationResults, language: ContentLanguage) { self.content = content self.results = results self.language = language } func process(_ arguments: [(key: Key, value: String)], markdown: Substring) -> String { var options: [Option] = [] var sources: [Source] = [] for (key, value) in arguments { guard let sourceType = key.sourceType else { guard let option = makeOption(key: key, value: value) else { invalid(markdown) continue } options.append(option) continue } let fileId = value.removingSurroundingQuotes guard let file = content.file(fileId) else { results.missing(file: fileId, source: "Video Block: \(key)") continue } guard file.type.isVideo else { invalid(markdown) continue } results.require(file: file) let source = Source(file: file, type: sourceType) sources.append(source) } guard !sources.isEmpty else { invalid(markdown) return "" } return VersionedVideo(sources: sources, options: options).content } private func makeOption(key: Key, value: String) -> Option? { switch key { case .controls: return .controls case .autoplay: return .autoplay case .muted: return .muted case .loop: return .loop case .playsinline: return .playsinline default: break } let value = value.removingSurroundingQuotes guard value != "" else { return nil } switch key { case .height: guard let height = Int(value) else { return nil } return .height(height) case .width: guard let width = Int(value) else { return nil } return .width(width) case .preload: guard let preloadOption = Option.Preload(rawValue: value) else { return nil } return .preload(preloadOption) case .poster: guard let image = content.image(value) else { results.missing(file: value, source: "Video Block: poster") return nil } let width = 2 * content.settings.pages.contentWidth let version = image.imageVersion(width: width, height: width, type: .jpg) results.require(image: version) return .poster(image: version.outputPath) case .src: guard let file = content.file(value) else { results.missing(file: value, source: "Video Block: src") return nil } results.warning("Use 'h264' and 'h265' instead of 'src'") return .src(file.absoluteUrl) default: return nil } } } extension VideoBlock { enum Key: String { /// The H264 video file to use case h264 /// The H265 video file to use case h265 /// The WebM video file to use case webm // MARK: Video options /// Specifies that video controls should be displayed (such as a play/pause button etc). case controls /// Specifies that the video will start playing as soon as it is ready case autoplay /// Specifies that the video will start over again, every time it is finished case loop /// Specifies that the audio output of the video should be muted case muted /// Mobile browsers will play the video right where it is instead of the default, which is to open it up fullscreen while it plays case playsinline /// Sets the height of the video player case height /// Sets the width of the video player case width /// Specifies if and how the author thinks the video should be loaded when the page loads case preload /// Specifies an image to be shown while the video is downloading, or until the user hits the play button case poster /// Specifies the URL of the video file case src var isOption: Bool { switch self { case .controls, .autoplay, .loop, .muted, .playsinline, .height, .width, .preload, .poster, .src: return true default: return false } } var sourceType: SourceType? { switch self { case .h264: .h264 case .h265: .h265 case .webm: .webm default: nil } } } enum SourceType { case h264 case h265 case webm var order: Int { switch self { case .h265: 1 case .webm: 2 case .h264: 3 } } var mimeType: String { switch self { case .h265, .h264: "video/mp4" case .webm: "video/webm" } } } struct Source { let file: FileResource let type: SourceType } } extension VideoBlock { enum Option { /// Specifies that video controls should be displayed (such as a play/pause button etc). case controls /// Specifies that the video will start playing as soon as it is ready case autoplay /// Specifies that the video will start over again, every time it is finished case loop /// Specifies that the audio output of the video should be muted case muted /// Mobile browsers will play the video right where it is instead of the default, which is to open it up fullscreen while it plays case playsinline /// Sets the height of the video player case height(Int) /// Sets the width of the video player case width(Int) /// Specifies if and how the author thinks the video should be loaded when the page loads case preload(Preload) /// Specifies an image to be shown while the video is downloading, or until the user hits the play button case poster(image: String) /// Specifies the URL of the video file case src(String) var rawValue: String { switch self { case .controls: return "controls" case .autoplay: return "autoplay" case .muted: return "muted" case .loop: return "loop" case .playsinline: return "playsinline" case .height(let height): return "height='\(height)'" case .width(let width): return "width='\(width)'" case .preload(let option): return "preload='\(option)'" case .poster(let image): return "poster='\(image)'" case .src(let url): return "src='\(url)'" } } } } extension VideoBlock.Option { /** The `preload` attribute specifies if and how the author thinks that the video should be loaded when the page loads. The `preload` attribute allows the author to provide a hint to the browser about what he/she thinks will lead to the best user experience. This attribute may be ignored in some instances. Note: The `preload` attribute is ignored if `autoplay` is present. */ enum Preload: String { /// The author thinks that the browser should load the entire video when the page loads case auto /// The author thinks that the browser should load only metadata when the page loads case metadata /// The author thinks that the browser should NOT load the video when the page loads case none } }