-
-
Notifications
You must be signed in to change notification settings - Fork 289
/
VideoValidator.swift
190 lines (159 loc) · 6.44 KB
/
VideoValidator.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import AVKit
import Crashlytics
struct VideoValidator {
enum Result {
case failure
case success(AVAsset, AVAsset.VideoMetadata)
}
func validate(_ inputUrl: URL, in window: NSWindow?) -> Result {
Crashlytics.record(
key: "Does input file exist",
value: inputUrl.exists
)
Crashlytics.record(
key: "Is input file reachable",
value: try? inputUrl.checkResourceIsReachable()
)
Crashlytics.record(
key: "Is input file readable",
value: inputUrl.isReadable
)
Crashlytics.record(
key: "File size",
value: inputUrl.fileSize
)
guard inputUrl.fileSize > 0 else {
// TODO: Make this not report to Crashlytics at some point.
NSAlert.showModalAndReportToCrashlytics(
for: window,
message: "The selected file is empty.",
informativeText: "Try selecting a different file.",
debugInfo: ""
)
return .failure
}
// This is very unlikely to happen. We have a lot of file type filters in place, so the only way this can happen is if the user right-clicks a non-video in Finder, chooses "Open With", then "Other…", chooses "All Applications", and then selects Gifski. Yet, some people are doing this…
guard inputUrl.isVideo else {
NSAlert.showModal(
for: window,
message: "The selected file could not be converted because it's not a video.",
informativeText: "Try again with a video file, usually with the file extension “mp4” or “mov”."
)
return .failure
}
let asset = AVURLAsset(
url: inputUrl,
options: [
AVURLAssetPreferPreciseDurationAndTimingKey: true
]
)
Crashlytics.record(key: "AVAsset debug info", value: asset.debugInfo)
guard asset.videoCodec != .appleAnimation else {
NSAlert.showModal(
for: window,
message: "The QuickTime Animation format is not supported.",
informativeText: "Re-export or convert the video to ProRes 4444 XQ instead. It's more efficient, more widely supported, and like QuickTime Animation, it also supports alpha channel. To convert an existing video, open it in QuickTime Player, which will automatically convert it, and then save it."
)
return .failure
}
if asset.hasAudio, !asset.hasVideo {
NSAlert.showModal(
for: window,
message: "Audio files are not supported.",
informativeText: "Gifski converts video files but the provided file is audio-only. Please provide a file that contains video."
)
return .failure
}
guard let firstVideoTrack = asset.firstVideoTrack else {
NSAlert.showModal(
for: window,
message: "Could not read any video from the video file.",
informativeText: "Either the video format is unsupported by macOS or the file is corrupt."
)
return .failure
}
guard !asset.hasProtectedContent else {
NSAlert.showModal(
for: window,
message: "The video is DRM-protected and cannot be converted."
)
return .failure
}
// We already specify the UTIs we support, so this can only happen on invalid video files or unsupported codecs.
guard asset.isVideoDecodable else {
if
let codec = firstVideoTrack.codec,
codec.isSupported
{
NSAlert.showModalAndReportToCrashlytics(
for: window,
message: "The video could not be decoded even though its codec “\(codec)” is supported.",
informativeText: "This could happen if the video is corrupt or the codec profile level is not supported. macOS unfortunately doesn't provide Gifski a reason for why the video could not be decoded. Try re-exporting using a different configuration or try converting the video to HEVC (MP4) with the free HandBrake app.",
showDebugInfo: false,
debugInfo: asset.debugInfo
)
return .failure
}
guard let codecTitle = firstVideoTrack.codecTitle else {
NSAlert.showModalAndReportToCrashlytics(
for: window,
message: "The video file is not supported.",
informativeText: "I'm trying to figure out why this happens. It would be amazing if you could email the below details to sindresorhus@gmail.com",
debugInfo: asset.debugInfo
)
return .failure
}
guard codecTitle != "hev1" else {
NSAlert.showModal(
for: window,
message: "This variant of the HEVC video codec is not supported by macOS.",
informativeText: "The video uses the “hev1” variant of HEVC while macOS only supports “hvc1”. Try re-exporting the video using a different configuration or use the free HandBrake app to convert the video to the supported HEVC variant."
)
return .failure
}
NSAlert.showModalAndReportToCrashlytics(
for: window,
message: "The video codec “\(codecTitle)” is not supported.",
informativeText: "Re-export or convert the video to a supported format. For the best possible quality, export to ProRes 4444 XQ (supports alpha). Alternatively, use the free HandBrake app to convert the video to HEVC (MP4).",
showDebugInfo: false,
debugInfo: asset.debugInfo
)
return .failure
}
guard asset.videoMetadata != nil else {
NSAlert.showModalAndReportToCrashlytics(
for: window,
message: "The video metadata is not readable.",
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
}
guard
let dimensions = asset.dimensions,
dimensions.width >= 4,
dimensions.height >= 4
else {
NSAlert.showModal(
for: window,
message: "The video dimensions must be at least 4×4.",
informativeText: "The dimensions of the video are \(asset.dimensions?.formatted ?? "0×0")."
)
return .failure
}
// 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: "Could not read the video.",
informativeText: "This should not happen. Email sindresorhus@gmail.com and include this info:",
debugInfo: asset.debugInfo
)
return .failure
}
return .success(newAsset, newVideoMetadata)
}
}