Skip to content

Commit 4ce7a62

Browse files
committed
feat(export): add gif export option available using gifski lib, progressbar and eta also implemented
1 parent 22abde3 commit 4ce7a62

8 files changed

Lines changed: 719 additions & 25 deletions

File tree

Reframed.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,7 +960,10 @@
960960
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
961961
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
962962
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
963+
LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/Reframed/Libraries/gifski";
964+
OTHER_LDFLAGS = "-lgifski";
963965
SWIFT_EMIT_LOC_STRINGS = YES;
966+
SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/Reframed/Libraries/gifski";
964967
SWIFT_VERSION = 6.0;
965968
};
966969
name = Debug;
@@ -1002,7 +1005,10 @@
10021005
RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO;
10031006
RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO;
10041007
RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO;
1008+
LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/Reframed/Libraries/gifski";
1009+
OTHER_LDFLAGS = "-lgifski";
10051010
SWIFT_EMIT_LOC_STRINGS = YES;
1011+
SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/Reframed/Libraries/gifski";
10061012
SWIFT_VERSION = 6.0;
10071013
};
10081014
name = Release;

Reframed/Compositor/ExportSettings.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,43 @@ struct ExportSettings: Sendable {
88
var codec: ExportCodec = .h265
99
var audioBitrate: ExportAudioBitrate = .kbps320
1010
var mode: ExportMode = .normal
11+
var gifQuality: GIFQuality = .high
12+
}
13+
14+
enum GIFQuality: Sendable, CaseIterable, Identifiable {
15+
case low
16+
case medium
17+
case high
18+
case maximum
19+
20+
var id: Self { self }
21+
22+
var label: String {
23+
switch self {
24+
case .low: "Low"
25+
case .medium: "Medium"
26+
case .high: "High"
27+
case .maximum: "Maximum"
28+
}
29+
}
30+
31+
var description: String {
32+
switch self {
33+
case .low: "Smallest file size. Noticeable quality loss."
34+
case .medium: "Good balance of size and quality."
35+
case .high: "High quality. Recommended for most use cases."
36+
case .maximum: "Best quality. Larger file size."
37+
}
38+
}
39+
40+
var value: UInt8 {
41+
switch self {
42+
case .low: 50
43+
case .medium: 70
44+
case .high: 90
45+
case .maximum: 100
46+
}
47+
}
1148
}
1249

1350
enum ExportMode: Sendable, CaseIterable, Identifiable {
@@ -34,29 +71,37 @@ enum ExportMode: Sendable, CaseIterable, Identifiable {
3471
enum ExportFormat: Sendable, CaseIterable, Identifiable {
3572
case mp4
3673
case mov
74+
case gif
3775

3876
var id: Self { self }
3977

4078
var label: String {
4179
switch self {
4280
case .mp4: "MP4"
4381
case .mov: "MOV"
82+
case .gif: "GIF"
4483
}
4584
}
4685

4786
var fileType: AVFileType {
4887
switch self {
4988
case .mp4: .mp4
5089
case .mov: .mov
90+
case .gif: .mp4
5191
}
5292
}
5393

5494
var fileExtension: String {
5595
switch self {
5696
case .mp4: "mp4"
5797
case .mov: "mov"
98+
case .gif: "gif"
5899
}
59100
}
101+
102+
var isGIF: Bool {
103+
self == .gif
104+
}
60105
}
61106

62107
enum ExportFPS: Sendable, CaseIterable, Identifiable {

Reframed/Compositor/ExportSheet.swift

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,41 @@ struct ExportSheet: View {
2626
.labelsHidden()
2727
}
2828

29-
settingsRow(label: "Codec") {
30-
Picker("", selection: $settings.codec) {
31-
ForEach(ExportCodec.allCases) { codec in
32-
Text(codec.label).tag(codec)
29+
if settings.format.isGIF {
30+
settingsRow(label: "Quality") {
31+
Picker("", selection: $settings.gifQuality) {
32+
ForEach(GIFQuality.allCases) { quality in
33+
Text(quality.label).tag(quality)
34+
}
3335
}
36+
.pickerStyle(.segmented)
37+
.labelsHidden()
38+
}
39+
40+
Text(settings.gifQuality.description)
41+
.font(.system(size: 11))
42+
.foregroundStyle(ReframedColors.dimLabel)
43+
.padding(.top, -10)
44+
} else {
45+
settingsRow(label: "Codec") {
46+
Picker("", selection: $settings.codec) {
47+
ForEach(ExportCodec.allCases) { codec in
48+
Text(codec.label).tag(codec)
49+
}
50+
}
51+
.pickerStyle(.segmented)
52+
.labelsHidden()
3453
}
35-
.pickerStyle(.segmented)
36-
.labelsHidden()
37-
}
3854

39-
Text(settings.codec.description)
40-
.font(.system(size: 11))
41-
.foregroundStyle(ReframedColors.dimLabel)
42-
.padding(.top, -10)
55+
Text(settings.codec.description)
56+
.font(.system(size: 11))
57+
.foregroundStyle(ReframedColors.dimLabel)
58+
.padding(.top, -10)
59+
}
4360

4461
settingsRow(label: "Frame Rate") {
4562
Picker("", selection: $settings.fps) {
46-
ForEach(ExportFPS.allCases) { fps in
63+
ForEach(gifAllowedFPSCases) { fps in
4764
Text(fps.label).tag(fps)
4865
}
4966
}
@@ -74,7 +91,7 @@ struct ExportSheet: View {
7491
.labelsHidden()
7592
}
7693

77-
if hasAudio {
94+
if hasAudio && !settings.format.isGIF {
7895
settingsRow(label: "Audio Bitrate (kbps)") {
7996
Picker("", selection: $settings.audioBitrate) {
8097
ForEach(ExportAudioBitrate.allCases) { bitrate in
@@ -86,22 +103,31 @@ struct ExportSheet: View {
86103
}
87104
}
88105

89-
settingsRow(label: "Renderer") {
90-
Picker("", selection: $settings.mode) {
91-
ForEach(ExportMode.allCases) { mode in
92-
Text(mode.label).tag(mode)
106+
if !settings.format.isGIF {
107+
settingsRow(label: "Renderer") {
108+
Picker("", selection: $settings.mode) {
109+
ForEach(ExportMode.allCases) { mode in
110+
Text(mode.label).tag(mode)
111+
}
93112
}
113+
.pickerStyle(.segmented)
114+
.labelsHidden()
94115
}
95-
.pickerStyle(.segmented)
96-
.labelsHidden()
97-
}
98116

99-
Text(settings.mode.description)
100-
.font(.system(size: 11))
101-
.foregroundStyle(ReframedColors.dimLabel)
102-
.padding(.top, -10)
117+
Text(settings.mode.description)
118+
.font(.system(size: 11))
119+
.foregroundStyle(ReframedColors.dimLabel)
120+
.padding(.top, -10)
121+
}
103122
}
104123
.padding(.horizontal, 28)
124+
.onChange(of: settings.format) { _, newFormat in
125+
if newFormat.isGIF {
126+
if let fpsVal = settings.fps.numericValue, fpsVal > 30 {
127+
settings.fps = .fps24
128+
}
129+
}
130+
}
105131

106132
Spacer().frame(height: 28)
107133

@@ -123,6 +149,16 @@ struct ExportSheet: View {
123149
.background(ReframedColors.panelBackground)
124150
}
125151

152+
private var gifAllowedFPSCases: [ExportFPS] {
153+
if settings.format.isGIF {
154+
return ExportFPS.allCases.filter { fps in
155+
guard let val = fps.numericValue else { return true }
156+
return val <= 30
157+
}
158+
}
159+
return ExportFPS.allCases
160+
}
161+
126162
private func settingsRow<Content: View>(label: String, @ViewBuilder content: () -> Content) -> some View {
127163
VStack(alignment: .leading, spacing: 6) {
128164
Text(label)

0 commit comments

Comments
 (0)