diff --git a/MiniCut.playground/Sources/Model/Clip.swift b/MiniCut.playground/Sources/Model/Clip.swift index 307fa22..e6084be 100644 --- a/MiniCut.playground/Sources/Model/Clip.swift +++ b/MiniCut.playground/Sources/Model/Clip.swift @@ -12,15 +12,25 @@ struct Clip: Identifiable { var content: ClipContent private var _start: TimeInterval - private var _length: TimeInterval + private var _originalLength: TimeInterval + /// Start offset within the clip (i.e. nonzero means that the clip is trimmed). var start: TimeInterval { get { _start } set { _start = max(0, newValue) } } + /// Original length of the clip's content. + var originalLength: TimeInterval { + get { _originalLength } + set { _originalLength = min(max(0, newValue), content.duration.map { $0 - start } ?? .infinity) } + } + /// Playback speed factor, 1 corresponds to normal playback rate. + /// Only applies to audiovisual content. Shall never be 0. + var speed: Double = 1 + /// Playback length, i.e. original length divided by speed. var length: TimeInterval { - get { _length } - set { _length = min(max(0, newValue), content.duration.map { $0 - start } ?? .infinity) } + get { originalLength / speed } + set { originalLength = newValue * speed } } var visualOffsetDx: Double = 0 // Normalized @@ -41,7 +51,7 @@ struct Clip: Identifiable { self.category = category self.content = content _start = max(0, start) - _length = max(0, length ?? content.duration ?? Self.defaultLength) + _originalLength = max(0, length ?? content.duration ?? Self.defaultLength) } init(url: URL) { diff --git a/MiniCut.playground/Sources/View/InspectorClipView.swift b/MiniCut.playground/Sources/View/InspectorClipView.swift index a4bce2f..7b84fc5 100644 --- a/MiniCut.playground/Sources/View/InspectorClipView.swift +++ b/MiniCut.playground/Sources/View/InspectorClipView.swift @@ -51,6 +51,14 @@ final class InspectorClipView: SKNode { } }) ] + case .audiovisual(_): + props += [ + ("Speed", { [weak self] in + Slider(value: self?.clip?.clip.speed ?? 1, range: 0.25..<4, width: $0) { + self?.clip?.clip.speed = $0 + } + }) + ] default: break } diff --git a/MiniCut.playground/Sources/View/VideoClipView.swift b/MiniCut.playground/Sources/View/VideoClipView.swift index 0197f82..97890fd 100644 --- a/MiniCut.playground/Sources/View/VideoClipView.swift +++ b/MiniCut.playground/Sources/View/VideoClipView.swift @@ -36,24 +36,29 @@ final class VideoClipView: SKNode { switch clip.clip.content { case .audiovisual(let content): player = AVPlayer(playerItem: AVPlayerItem(asset: content.asset)) + let video = SKVideoNode(avPlayer: player) video.size = size addChild(video) let updatePlayer = { [weak self] in guard let currentClip = state.timeline[trackId]?[id] else { return } - let relative = (state.cursor - currentClip.offset) + currentClip.clip.start + let originalRelative = (state.cursor - currentClip.offset) + currentClip.clip.start + let relative = originalRelative * currentClip.clip.speed + self?.player.rate = Float(currentClip.clip.speed) self?.player.seek(to: CMTime(seconds: relative, preferredTimescale: 1000)) } clipSubscription = state.timelineDidChange.subscribeFiring(state.timeline) { _ in updatePlayer() } cursorSubscription = state.cursorDidChange.subscribeFiring(state.cursor) { _ in updatePlayer() } - isPlayingSubscription = state.isPlayingDidChange.subscribeFiring(state.isPlaying) { + isPlayingSubscription = state.isPlayingDidChange.subscribeFiring(state.isPlaying) { [weak self] in + guard let speed = state.timeline[trackId]?[id]?.clip.speed else { return } if $0 { - video.play() + self?.player.play() + self?.player.rate = Float(speed) } else { - video.pause() + self?.player.pause() } } case .text(let text):