Skip to content

Commit

Permalink
Loop the playback when “Loop Forever” is enabled (#179)
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus committed Mar 9, 2020
1 parent 8b08eb2 commit 99a88e9
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 24 deletions.
6 changes: 6 additions & 0 deletions Gifski/EditVideoViewController.swift
Expand Up @@ -302,6 +302,12 @@ final class EditVideoViewController: NSViewController {
playerViewController = TrimmingAVPlayerViewController(playerItem: AVPlayerItem(asset: asset)) { [weak self] _ in
self?.estimateFileSize()
}

Defaults.observe(.loopGif) { [weak self] in
self?.playerViewController.loopPlayback = $0.newValue
}
.tieToLifetime(of: self)

add(childController: playerViewController, to: playerViewWrapper)
}

Expand Down
14 changes: 12 additions & 2 deletions Gifski/TrimmingAVPlayerViewController.swift
Expand Up @@ -4,6 +4,7 @@ import AVKit
final class TrimmingAVPlayerViewController: NSViewController {
private(set) var timeRange: ClosedRange<Double>?
private let playerItem: AVPlayerItem
private let player: AVPlayer
private let controlsStyle: AVPlayerViewControlsStyle
private let timeRangeDidChange: ((ClosedRange<Double>) -> Void)?

Expand All @@ -16,12 +17,22 @@ final class TrimmingAVPlayerViewController: NSViewController {
}
}

var loopPlayback: Bool {
get {
player.loopPlayback
}
set {
player.loopPlayback = newValue
}
}

init(
playerItem: AVPlayerItem,
controlsStyle: AVPlayerViewControlsStyle = .inline,
timeRangeDidChange: ((ClosedRange<Double>) -> Void)? = nil
) {
self.playerItem = playerItem
self.player = AVPlayer(playerItem: playerItem)
self.controlsStyle = controlsStyle
self.timeRangeDidChange = timeRangeDidChange
super.init(nibName: nil, bundle: nil)
Expand All @@ -34,10 +45,9 @@ final class TrimmingAVPlayerViewController: NSViewController {

override func loadView() {
let playerView = TrimmingAVPlayerView()
playerView.player = AVPlayer(playerItem: playerItem)
playerView.controlsStyle = controlsStyle
playerView.setupTrimmingObserver()

playerView.player = player
view = playerView
}

Expand Down
35 changes: 13 additions & 22 deletions Gifski/VideoValidator.swift
Expand Up @@ -71,7 +71,7 @@ struct VideoValidator {
return .failure
}

guard let videoMetadata = asset.videoMetadata else {
guard asset.videoMetadata != nil else {
NSAlert.showModalAndReportToCrashlytics(
for: window,
message: "The video metadata is not readable.",
Expand All @@ -96,30 +96,21 @@ struct VideoValidator {
return .failure
}

// If the video track duration is shorter than the total asset duration, we extract the video track into a new asset to prevent problems later on. If we don't do this, the video will show as black in the trim view at the duration where there's no video track, and it will confuse users. Also, if the user trims the video to just the black no video track part, the conversion would continue, but there's nothing to convert, so it would be stuck at 0%.
guard firstVideoTrack.isFullDuration else {
guard
let newAsset = firstVideoTrack.extractToNewAsset(),
let newVideoMetadata = newAsset.videoMetadata
else {
NSAlert.showModalAndReportToCrashlytics(
for: window,
message: "Cannot read the video.",
informativeText: "Please open an issue on https://github.com/sindresorhus/Gifski or email sindresorhus@gmail.com. ZIP the video and attach it.\n\nInclude this info:",
debugInfo: asset.debugInfo
)

return .failure
}

Crashlytics.record(
key: "Extracted video to new asset",
value: true
// We extract the video track into a new asset to remove the audio and to prevent problems if the video track duration is shorter than the total asset duration. If we don't do this, the video will show as black in the trim view at the duration where there's no video track, and it will confuse users. Also, if the user trims the video to just the black no video track part, the conversion would continue, but there's nothing to convert, so it would be stuck at 0%.
guard
let newAsset = firstVideoTrack.extractToNewAsset(),
let newVideoMetadata = newAsset.videoMetadata
else {
NSAlert.showModalAndReportToCrashlytics(
for: window,
message: "Cannot read the video.",
informativeText: "This should not happen. Email sindresorhus@gmail.com and include this info:",
debugInfo: asset.debugInfo
)

return .success(newAsset, newVideoMetadata)
return .failure
}

return .success(asset, videoMetadata)
return .success(newAsset, newVideoMetadata)
}
}
37 changes: 37 additions & 0 deletions Gifski/util.swift
Expand Up @@ -2954,3 +2954,40 @@ extension Error {
NSApp.presentError(self)
}
}


extension AVPlayer {
func seekToStart() {
seek(to: .zero)
}
}


extension AVPlayer {
private struct AssociatedKeys {
static let observationToken = ObjectAssociation<NSObjectProtocol>()
static let originalActionAtItemEnd = ObjectAssociation<ActionAtItemEnd>()
}

/// Loop the playback.
var loopPlayback: Bool {
get {
AssociatedKeys.observationToken[self] != nil
}
set {
if newValue {
AssociatedKeys.originalActionAtItemEnd[self] = actionAtItemEnd
actionAtItemEnd = .none

// TODO: Use Combine publisher when targeting macOS 10.15.
AssociatedKeys.observationToken[self] = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: currentItem, queue: nil) { [weak self] _ in
self?.seekToStart()
}
} else {
actionAtItemEnd = AssociatedKeys.originalActionAtItemEnd[self] ?? actionAtItemEnd
AssociatedKeys.originalActionAtItemEnd[self] = nil
AssociatedKeys.observationToken[self] = nil
}
}
}
}

0 comments on commit 99a88e9

Please sign in to comment.