forked from obsproject/obs-studio
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathencoder.c
More file actions
1534 lines (1273 loc) · 48.3 KB
/
encoder.c
File metadata and controls
1534 lines (1273 loc) · 48.3 KB
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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include <obs-module.h>
#include <util/darray.h>
#include <util/platform.h>
#include <obs-avc.h>
#include <CoreFoundation/CoreFoundation.h>
#include <VideoToolbox/VideoToolbox.h>
#include <VideoToolbox/VTVideoEncoderList.h>
#include <CoreMedia/CoreMedia.h>
#include <util/apple/cfstring-utils.h>
#include <assert.h>
#define VT_LOG(level, format, ...) blog(level, "[VideoToolbox encoder]: " format, ##__VA_ARGS__)
#define VT_LOG_ENCODER(encoder, codec_type, level, format, ...) \
blog(level, "[VideoToolbox %s: '%s']: " format, obs_encoder_get_name(encoder), \
codec_type_to_print_fmt(codec_type), ##__VA_ARGS__)
#define VT_BLOG(level, format, ...) VT_LOG_ENCODER(enc->encoder, enc->codec_type, level, format, ##__VA_ARGS__)
enum aq_mode {
AQ_INVALID = 0,
AQ_AUTO,
AQ_DISABLED,
AQ_ENABLED,
};
struct vt_encoder_type_data {
const char *disp_name;
const char *id;
CMVideoCodecType codec_type;
bool hardware_accelerated;
};
struct vt_prores_encoder_data {
FourCharCode codec_type;
CFStringRef encoder_id;
};
static DARRAY(struct vt_prores_encoder_data) vt_prores_hardware_encoder_list;
static DARRAY(struct vt_prores_encoder_data) vt_prores_software_encoder_list;
#ifdef __aarch64__
bool is_apple_silicon = true;
#else
bool is_apple_silicon = false;
#endif
struct vt_encoder {
obs_encoder_t *encoder;
const char *vt_encoder_id;
uint32_t width;
uint32_t height;
uint32_t keyint;
uint32_t fps_num;
uint32_t fps_den;
const char *rate_control;
uint32_t bitrate;
float quality;
bool limit_bitrate;
uint32_t rc_max_bitrate;
double rc_max_bitrate_window;
const char *profile;
CMVideoCodecType codec_type;
bool bframes;
bool spatial_aq;
int vt_pix_fmt;
enum video_colorspace colorspace;
VTCompressionSessionRef session;
CMSimpleQueueRef queue;
bool hw_enc;
DARRAY(uint8_t) packet_data;
DARRAY(uint8_t) extra_data;
};
static const char *codec_type_to_print_fmt(CMVideoCodecType codec_type)
{
switch (codec_type) {
case kCMVideoCodecType_H264:
return "h264";
case kCMVideoCodecType_HEVC:
return "hevc";
case kCMVideoCodecType_AppleProRes4444XQ:
return "ap4x";
case kCMVideoCodecType_AppleProRes4444:
return "ap4h";
case kCMVideoCodecType_AppleProRes422Proxy:
return "apco";
case kCMVideoCodecType_AppleProRes422LT:
return "apcs";
case kCMVideoCodecType_AppleProRes422:
return "apcn";
case kCMVideoCodecType_AppleProRes422HQ:
return "apch";
default:
return "";
}
}
static void log_osstatus(int log_level, struct vt_encoder *enc, const char *context, OSStatus code)
{
char *c_str = NULL;
CFErrorRef err = CFErrorCreate(kCFAllocatorDefault, kCFErrorDomainOSStatus, code, NULL);
CFStringRef str = CFErrorCopyDescription(err);
c_str = cfstr_copy_cstr(str, kCFStringEncodingUTF8);
if (c_str) {
if (enc)
VT_BLOG(log_level, "Error in %s: %s", context, c_str);
else
VT_LOG(log_level, "Error in %s: %s", context, c_str);
}
bfree(c_str);
CFRelease(str);
CFRelease(err);
}
static CFStringRef obs_to_vt_profile(CMVideoCodecType codec_type, const char *profile, enum video_format format)
{
if (codec_type == kCMVideoCodecType_H264) {
if (strcmp(profile, "baseline") == 0)
return kVTProfileLevel_H264_Baseline_AutoLevel;
else if (strcmp(profile, "main") == 0)
return kVTProfileLevel_H264_Main_AutoLevel;
else if (strcmp(profile, "high") == 0)
return kVTProfileLevel_H264_High_AutoLevel;
else
return kVTProfileLevel_H264_Main_AutoLevel;
#ifdef ENABLE_HEVC
} else if (codec_type == kCMVideoCodecType_HEVC) {
if (strcmp(profile, "main") == 0) {
if (format == VIDEO_FORMAT_P010) {
VT_LOG(LOG_WARNING, "Forcing main10 for P010");
return kVTProfileLevel_HEVC_Main10_AutoLevel;
} else {
return kVTProfileLevel_HEVC_Main_AutoLevel;
}
}
if (strcmp(profile, "main10") == 0)
return kVTProfileLevel_HEVC_Main10_AutoLevel;
if (__builtin_available(macOS 12.3, *)) {
if (strcmp(profile, "main42210") == 0)
return kVTProfileLevel_HEVC_Main42210_AutoLevel;
}
return kVTProfileLevel_HEVC_Main_AutoLevel;
#else
(void)format;
#endif // ENABLE_HEVC
} else {
return kVTProfileLevel_H264_Baseline_AutoLevel;
}
}
static CFStringRef obs_to_vt_colorspace(enum video_colorspace cs)
{
switch (cs) {
case VIDEO_CS_601:
return kCVImageBufferYCbCrMatrix_ITU_R_601_4;
case VIDEO_CS_2100_PQ:
case VIDEO_CS_2100_HLG:
return kCVImageBufferYCbCrMatrix_ITU_R_2020;
default:
return kCVImageBufferYCbCrMatrix_ITU_R_709_2;
}
}
static CFStringRef obs_to_vt_primaries(enum video_colorspace cs)
{
switch (cs) {
case VIDEO_CS_601:
return kCVImageBufferColorPrimaries_SMPTE_C;
case VIDEO_CS_2100_PQ:
case VIDEO_CS_2100_HLG:
return kCVImageBufferColorPrimaries_ITU_R_2020;
default:
return kCVImageBufferColorPrimaries_ITU_R_709_2;
}
}
static CFStringRef obs_to_vt_transfer(enum video_colorspace cs)
{
switch (cs) {
case VIDEO_CS_SRGB:
return kCVImageBufferTransferFunction_sRGB;
case VIDEO_CS_2100_PQ:
return kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ;
case VIDEO_CS_2100_HLG:
return kCVImageBufferTransferFunction_ITU_R_2100_HLG;
default:
return kCVImageBufferTransferFunction_ITU_R_709_2;
}
}
/* Adapted from Chromium GenerateMasteringDisplayColorVolume */
static CFDataRef obs_to_vt_masteringdisplay(uint32_t hdr_nominal_peak_level)
{
struct mastering_display_colour_volume {
uint16_t display_primaries[3][2];
uint16_t white_point[2];
uint32_t max_display_mastering_luminance;
uint32_t min_display_mastering_luminance;
};
static_assert(sizeof(struct mastering_display_colour_volume) == 24, "May need to adjust struct packing");
struct mastering_display_colour_volume mdcv;
mdcv.display_primaries[0][0] = __builtin_bswap16(13250);
mdcv.display_primaries[0][1] = __builtin_bswap16(34500);
mdcv.display_primaries[1][0] = __builtin_bswap16(7500);
mdcv.display_primaries[1][1] = __builtin_bswap16(3000);
mdcv.display_primaries[2][0] = __builtin_bswap16(34000);
mdcv.display_primaries[2][1] = __builtin_bswap16(16000);
mdcv.white_point[0] = __builtin_bswap16(15635);
mdcv.white_point[1] = __builtin_bswap16(16450);
mdcv.max_display_mastering_luminance = __builtin_bswap32(hdr_nominal_peak_level * 10000);
mdcv.min_display_mastering_luminance = 0;
UInt8 bytes[sizeof(struct mastering_display_colour_volume)];
memcpy(bytes, &mdcv, sizeof(bytes));
return CFDataCreate(kCFAllocatorDefault, bytes, sizeof(bytes));
}
/* Adapted from Chromium GenerateContentLightLevelInfo */
static CFDataRef obs_to_vt_contentlightlevelinfo(uint16_t hdr_nominal_peak_level)
{
struct content_light_level_info {
uint16_t max_content_light_level;
uint16_t max_pic_average_light_level;
};
static_assert(sizeof(struct content_light_level_info) == 4, "May need to adjust struct packing");
struct content_light_level_info clli;
clli.max_content_light_level = __builtin_bswap16(hdr_nominal_peak_level);
clli.max_pic_average_light_level = __builtin_bswap16(hdr_nominal_peak_level);
UInt8 bytes[sizeof(struct content_light_level_info)];
memcpy(bytes, &clli, sizeof(bytes));
return CFDataCreate(kCFAllocatorDefault, bytes, sizeof(bytes));
}
static OSStatus session_set_prop_float(VTCompressionSessionRef session, CFStringRef key, float val)
{
CFNumberRef n = CFNumberCreate(NULL, kCFNumberFloat32Type, &val);
OSStatus code = VTSessionSetProperty(session, key, n);
CFRelease(n);
return code;
}
static OSStatus session_set_prop_int(VTCompressionSessionRef session, CFStringRef key, int32_t val)
{
CFNumberRef n = CFNumberCreate(NULL, kCFNumberSInt32Type, &val);
OSStatus code = VTSessionSetProperty(session, key, n);
CFRelease(n);
return code;
}
static OSStatus session_set_prop_str(VTCompressionSessionRef session, CFStringRef key, char *val)
{
CFStringRef s = CFStringCreateWithFileSystemRepresentation(NULL, val);
OSStatus code = VTSessionSetProperty(session, key, s);
CFRelease(s);
return code;
}
static OSStatus session_set_prop(VTCompressionSessionRef session, CFStringRef key, CFTypeRef val)
{
return VTSessionSetProperty(session, key, val);
}
static OSStatus session_set_bitrate(VTCompressionSessionRef session, const char *rate_control, int new_bitrate,
float quality, bool limit_bitrate, int max_bitrate, double max_bitrate_window)
{
OSStatus code;
bool can_limit_bitrate;
CFStringRef compressionPropertyKey;
if (strcmp(rate_control, "CBR") == 0) {
compressionPropertyKey = kVTCompressionPropertyKey_AverageBitRate;
can_limit_bitrate = true;
if (__builtin_available(macOS 13.0, *)) {
if (is_apple_silicon) {
compressionPropertyKey = kVTCompressionPropertyKey_ConstantBitRate;
can_limit_bitrate = false;
} else {
VT_LOG(LOG_WARNING, "CBR support for VideoToolbox encoder requires Apple Silicon. "
"Will use ABR instead.");
}
} else {
VT_LOG(LOG_WARNING, "CBR support for VideoToolbox encoder requires macOS 13 or newer. "
"Will use ABR instead.");
}
} else if (strcmp(rate_control, "ABR") == 0) {
compressionPropertyKey = kVTCompressionPropertyKey_AverageBitRate;
can_limit_bitrate = true;
} else if (strcmp(rate_control, "CRF") == 0) {
if (is_apple_silicon) {
compressionPropertyKey = kVTCompressionPropertyKey_Quality;
code = session_set_prop_float(session, compressionPropertyKey, quality);
if (code != noErr) {
return code;
}
} else {
VT_LOG(LOG_WARNING, "CRF support for VideoToolbox encoder requires Apple Silicon. "
"Will use ABR instead.");
compressionPropertyKey = kVTCompressionPropertyKey_AverageBitRate;
}
can_limit_bitrate = true;
} else {
VT_LOG(LOG_ERROR, "Selected rate control method is not supported: %s", rate_control);
return kVTParameterErr;
}
if (compressionPropertyKey != kVTCompressionPropertyKey_Quality) {
code = session_set_prop_int(session, compressionPropertyKey, new_bitrate * 1000);
if (code != noErr) {
return code;
}
}
if (limit_bitrate && can_limit_bitrate) {
double cpb_size = max_bitrate * 125 * max_bitrate_window;
CFNumberRef cf_cpb_size = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &cpb_size);
CFNumberRef cf_cpb_window_size =
CFNumberCreate(kCFAllocatorDefault, kCFNumberFloatType, &max_bitrate_window);
CFTypeRef values[2] = {cf_cpb_size, cf_cpb_window_size};
CFArrayRef rate_control_data = CFArrayCreate(kCFAllocatorDefault, values, 2, &kCFTypeArrayCallBacks);
code = session_set_prop(session, kVTCompressionPropertyKey_DataRateLimits, rate_control_data);
CFRelease(cf_cpb_size);
CFRelease(cf_cpb_window_size);
CFRelease(rate_control_data);
if (code == kVTPropertyNotSupportedErr) {
log_osstatus(LOG_WARNING, NULL, "setting DataRateLimits on session", code);
return noErr;
}
}
return noErr;
}
static OSStatus session_set_colorspace(VTCompressionSessionRef session, enum video_colorspace cs)
{
OSStatus code;
CFTypeRef keys[5] = {kVTCompressionPropertyKey_ColorPrimaries, kVTCompressionPropertyKey_TransferFunction,
kVTCompressionPropertyKey_YCbCrMatrix, NULL, NULL};
CFTypeRef values[5] = {obs_to_vt_primaries(cs), obs_to_vt_transfer(cs), obs_to_vt_colorspace(cs), NULL, NULL};
CFDataRef masteringDisplayColorVolume = NULL;
CFDataRef contentLightLevel = NULL;
if (cs == VIDEO_CS_2100_PQ) {
const uint16_t hdr_nominal_peak_level = (uint16_t)obs_get_video_hdr_nominal_peak_level();
masteringDisplayColorVolume = obs_to_vt_masteringdisplay(hdr_nominal_peak_level);
contentLightLevel = obs_to_vt_contentlightlevelinfo(hdr_nominal_peak_level);
keys[3] = kVTCompressionPropertyKey_MasteringDisplayColorVolume;
keys[4] = kVTCompressionPropertyKey_ContentLightLevelInfo;
values[3] = masteringDisplayColorVolume;
values[4] = contentLightLevel;
} else if (cs == VIDEO_CS_2100_HLG) {
masteringDisplayColorVolume = obs_to_vt_masteringdisplay(1000);
contentLightLevel = obs_to_vt_contentlightlevelinfo(1000);
keys[3] = kVTCompressionPropertyKey_MasteringDisplayColorVolume;
keys[4] = kVTCompressionPropertyKey_ContentLightLevelInfo;
values[3] = masteringDisplayColorVolume;
values[4] = contentLightLevel;
}
CFDictionaryRef session_properties = CFDictionaryCreate(
kCFAllocatorDefault, keys, values, 5, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
code = VTSessionSetProperties(session, session_properties);
CFRelease(session_properties);
if (masteringDisplayColorVolume != NULL) {
CFRelease(masteringDisplayColorVolume);
}
if (contentLightLevel != NULL) {
CFRelease(contentLightLevel);
}
return code;
}
void sample_encoded_callback(void *data, void *source, OSStatus status, VTEncodeInfoFlags info_flags,
CMSampleBufferRef buffer)
{
if (status != noErr) {
log_osstatus(LOG_ERROR, NULL, "encoder callback", status);
return;
}
if (info_flags == kVTEncodeInfo_FrameDropped) {
VT_LOG(LOG_INFO, "Frame dropped by encoder");
}
CMSimpleQueueRef queue = data;
CVPixelBufferRef pixbuf = source;
if (buffer != NULL) {
CFRetain(buffer);
CMSimpleQueueEnqueue(queue, buffer);
}
CFRelease(pixbuf);
}
static inline CFDictionaryRef create_encoder_spec(const char *vt_encoder_id)
{
CFStringRef id = CFStringCreateWithFileSystemRepresentation(NULL, vt_encoder_id);
CFTypeRef keys[1] = {kVTVideoEncoderSpecification_EncoderID};
CFTypeRef values[1] = {id};
CFDictionaryRef encoder_spec = CFDictionaryCreate(
kCFAllocatorDefault, keys, values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFRelease(id);
return encoder_spec;
}
static inline CFDictionaryRef create_prores_encoder_spec(CMVideoCodecType target_codec_type, bool hardware_accelerated)
{
CFStringRef encoder_id = NULL;
size_t size = 0;
struct vt_prores_encoder_data *encoder_list = NULL;
if (hardware_accelerated) {
size = vt_prores_hardware_encoder_list.num;
encoder_list = vt_prores_hardware_encoder_list.array;
} else {
size = vt_prores_software_encoder_list.num;
encoder_list = vt_prores_software_encoder_list.array;
}
for (size_t i = 0; i < size; ++i) {
if (target_codec_type == encoder_list[i].codec_type) {
encoder_id = encoder_list[i].encoder_id;
}
}
CFTypeRef keys[1] = {kVTVideoEncoderSpecification_EncoderID};
CFTypeRef values[1] = {encoder_id};
CFDictionaryRef encoder_spec = CFDictionaryCreate(
kCFAllocatorDefault, keys, values, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
return encoder_spec;
}
static inline CFDictionaryRef create_pixbuf_spec(struct vt_encoder *enc)
{
CFNumberRef PixelFormat = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &enc->vt_pix_fmt);
CFNumberRef Width = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &enc->width);
CFNumberRef Height = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &enc->height);
CFTypeRef keys[3] = {kCVPixelBufferPixelFormatTypeKey, kCVPixelBufferWidthKey, kCVPixelBufferHeightKey};
CFTypeRef values[3] = {PixelFormat, Width, Height};
CFDictionaryRef pixbuf_spec = CFDictionaryCreate(
kCFAllocatorDefault, keys, values, 3, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFRelease(PixelFormat);
CFRelease(Width);
CFRelease(Height);
return pixbuf_spec;
}
static OSStatus create_encoder(struct vt_encoder *enc)
{
OSStatus code;
VTCompressionSessionRef s;
const char *codec_name = obs_encoder_get_codec(enc->encoder);
CFDictionaryRef encoder_spec;
if (strcmp(codec_name, "prores") == 0) {
struct vt_encoder_type_data *type_data =
(struct vt_encoder_type_data *)obs_encoder_get_type_data(enc->encoder);
encoder_spec = create_prores_encoder_spec(enc->codec_type, type_data->hardware_accelerated);
} else {
encoder_spec = create_encoder_spec(enc->vt_encoder_id);
}
CFDictionaryRef pixbuf_spec = create_pixbuf_spec(enc);
code = VTCompressionSessionCreate(kCFAllocatorDefault, enc->width, enc->height, enc->codec_type, encoder_spec,
pixbuf_spec, NULL, &sample_encoded_callback, enc->queue, &s);
if (code != noErr) {
log_osstatus(LOG_ERROR, enc, "VTCompressionSessionCreate", code);
}
CFRelease(encoder_spec);
CFRelease(pixbuf_spec);
CFBooleanRef b = NULL;
code = VTSessionCopyProperty(s, kVTCompressionPropertyKey_UsingHardwareAcceleratedVideoEncoder, NULL, &b);
if (code == noErr && (enc->hw_enc = CFBooleanGetValue(b)))
VT_BLOG(LOG_INFO, "session created with hardware encoding");
else
enc->hw_enc = false;
if (b != NULL)
CFRelease(b);
if (enc->codec_type == kCMVideoCodecType_H264 || enc->codec_type == kCMVideoCodecType_HEVC) {
// This can fail when using GPU hardware encoding
code = session_set_prop_int(s, kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration, enc->keyint);
if (code != noErr)
log_osstatus(LOG_WARNING, enc,
"setting kVTCompressionPropertyKey_MaxKeyFrameIntervalDuration failed, "
"keyframe interval might be incorrect",
code);
CFTypeRef session_keys[4] = {kVTCompressionPropertyKey_MaxKeyFrameInterval,
kVTCompressionPropertyKey_ExpectedFrameRate,
kVTCompressionPropertyKey_AllowFrameReordering,
kVTCompressionPropertyKey_ProfileLevel};
SInt32 key_frame_interval = (SInt32)(enc->keyint * ((float)enc->fps_num / enc->fps_den));
float expected_framerate = (float)enc->fps_num / enc->fps_den;
CFNumberRef MaxKeyFrameInterval =
CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &key_frame_interval);
CFNumberRef ExpectedFrameRate =
CFNumberCreate(kCFAllocatorDefault, kCFNumberFloat32Type, &expected_framerate);
CFTypeRef AllowFrameReordering = enc->bframes ? kCFBooleanTrue : kCFBooleanFalse;
video_t *video = obs_encoder_video(enc->encoder);
const struct video_output_info *voi = video_output_get_info(video);
CFTypeRef ProfileLevel = obs_to_vt_profile(enc->codec_type, enc->profile, voi->format);
CFTypeRef session_values[4] = {MaxKeyFrameInterval, ExpectedFrameRate, AllowFrameReordering,
ProfileLevel};
CFDictionaryRef session_properties =
CFDictionaryCreate(kCFAllocatorDefault, session_keys, session_values, 4,
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
code = VTSessionSetProperties(s, session_properties);
CFRelease(MaxKeyFrameInterval);
CFRelease(ExpectedFrameRate);
CFRelease(AllowFrameReordering);
CFRelease(ProfileLevel);
CFRelease(session_properties);
if (code != noErr) {
return code;
}
code = session_set_bitrate(s, enc->rate_control, enc->bitrate, enc->quality, enc->limit_bitrate,
enc->rc_max_bitrate, enc->rc_max_bitrate_window);
if (code != noErr) {
return code;
}
// if (__builtin_available(macOS 15.0, *)) {
// int spatial_aq = enc->spatial_aq ? kVTQPModulationLevel_Default : kVTQPModulationLevel_Disable;
// CFNumberRef spatialAQ = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &spatial_aq);
//
// code = VTSessionSetProperty(s, kVTCompressionPropertyKey_SpatialAdaptiveQPLevel, spatialAQ);
//
// if (code != noErr) {
// log_osstatus(LOG_WARNING, enc,
// "setting kVTCompressionPropertyKey_SpatialAdaptiveQPLevel failed", code);
// }
//
// CFRelease(spatialAQ);
// }
}
// This can fail depending on hardware configuration
code = session_set_prop(s, kVTCompressionPropertyKey_RealTime, kCFBooleanFalse);
if (code != noErr)
log_osstatus(LOG_WARNING, enc,
"setting kVTCompressionPropertyKey_RealTime failed, "
"frame delay might be increased",
code);
code = session_set_colorspace(s, enc->colorspace);
if (code != noErr) {
return code;
}
code = VTCompressionSessionPrepareToEncodeFrames(s);
if (code != noErr) {
return code;
}
enc->session = s;
return noErr;
}
static void vt_destroy(void *data)
{
struct vt_encoder *enc = data;
if (enc) {
if (enc->session != NULL) {
VTCompressionSessionInvalidate(enc->session);
CFRelease(enc->session);
}
da_free(enc->packet_data);
da_free(enc->extra_data);
bfree(enc);
}
}
static void dump_encoder_info(struct vt_encoder *enc)
{
VT_BLOG(LOG_INFO,
"settings:\n"
"\tvt_encoder_id %s\n"
"\trate_control: %s\n"
"\tbitrate: %d (kbps)\n"
"\tquality: %f\n"
"\tfps_num: %d\n"
"\tfps_den: %d\n"
"\twidth: %d\n"
"\theight: %d\n"
"\tkeyint: %d (s)\n"
"\tlimit_bitrate: %s\n"
"\trc_max_bitrate: %d (kbps)\n"
"\trc_max_bitrate_window: %f (s)\n"
"\thw_enc: %s\n"
"\tspatial_aq: %s\n"
"\tprofile: %s\n"
"\tcodec_type: %.4s\n",
enc->vt_encoder_id, enc->rate_control, enc->bitrate, enc->quality, enc->fps_num, enc->fps_den,
enc->width, enc->height, enc->keyint, enc->limit_bitrate ? "on" : "off", enc->rc_max_bitrate,
enc->rc_max_bitrate_window, enc->hw_enc ? "on" : "off", enc->spatial_aq ? "on" : "off",
(enc->profile != NULL && !!strlen(enc->profile)) ? enc->profile : "default",
codec_type_to_print_fmt(enc->codec_type));
}
typedef enum {
kResultSuccess = 0,
kResultColorFormatUnsupported = 1,
kResultFullRangeUnsupported = 2,
} SetVideoFormatResult;
static SetVideoFormatResult set_video_format(struct vt_encoder *enc, enum video_format format,
enum video_range_type range)
{
bool full_range = range == VIDEO_RANGE_FULL;
switch (format) {
case VIDEO_FORMAT_I420:
enc->vt_pix_fmt = full_range ? kCVPixelFormatType_420YpCbCr8PlanarFullRange
: kCVPixelFormatType_420YpCbCr8Planar;
return kResultSuccess;
case VIDEO_FORMAT_NV12:
enc->vt_pix_fmt = full_range ? kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
return kResultSuccess;
case VIDEO_FORMAT_P010:
if (enc->codec_type == kCMVideoCodecType_HEVC) {
enc->vt_pix_fmt = full_range ? kCVPixelFormatType_420YpCbCr10BiPlanarFullRange
: kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange;
return kResultSuccess;
}
break;
case VIDEO_FORMAT_P216:
if (!full_range) {
enc->vt_pix_fmt = kCVPixelFormatType_422YpCbCr16BiPlanarVideoRange;
return kResultSuccess;
} else {
return kResultFullRangeUnsupported;
}
break;
case VIDEO_FORMAT_P416:
if (!full_range) {
enc->vt_pix_fmt = kCVPixelFormatType_444YpCbCr16BiPlanarVideoRange;
return kResultSuccess;
} else {
return kResultFullRangeUnsupported;
}
break;
default:
return kResultColorFormatUnsupported;
}
return kResultColorFormatUnsupported;
}
static bool update_params(struct vt_encoder *enc, obs_data_t *settings)
{
video_t *video = obs_encoder_video(enc->encoder);
const struct video_output_info *voi = video_output_get_info(video);
const char *codec = obs_encoder_get_codec(enc->encoder);
if (strcmp(codec, "h264") == 0) {
enc->codec_type = kCMVideoCodecType_H264;
obs_data_set_int(settings, "codec_type", enc->codec_type);
#ifdef ENABLE_HEVC
} else if (strcmp(codec, "hevc") == 0) {
enc->codec_type = kCMVideoCodecType_HEVC;
obs_data_set_int(settings, "codec_type", enc->codec_type);
#endif
} else {
enc->codec_type = (CMVideoCodecType)obs_data_get_int(settings, "codec_type");
}
SetVideoFormatResult res = set_video_format(enc, voi->format, voi->range);
if (res == kResultColorFormatUnsupported) {
obs_encoder_set_last_error(enc->encoder, obs_module_text("ColorFormatUnsupported"));
VT_BLOG(LOG_WARNING, "Unsupported color format selected");
return false;
} else if (res == kResultFullRangeUnsupported) {
obs_encoder_set_last_error(enc->encoder, obs_module_text("FullRangeUnsupported"));
VT_BLOG(LOG_WARNING, "Unsupported color range (full) selected");
return false;
}
enc->colorspace = voi->colorspace;
enc->width = obs_encoder_get_width(enc->encoder);
enc->height = obs_encoder_get_height(enc->encoder);
enc->fps_num = voi->fps_num;
enc->fps_den = voi->fps_den;
enc->keyint = (uint32_t)obs_data_get_int(settings, "keyint_sec");
enc->rate_control = obs_data_get_string(settings, "rate_control");
enc->bitrate = (uint32_t)obs_data_get_int(settings, "bitrate");
enc->quality = ((float)obs_data_get_int(settings, "quality")) / 100;
enc->profile = obs_data_get_string(settings, "profile");
enc->limit_bitrate = obs_data_get_bool(settings, "limit_bitrate");
enc->rc_max_bitrate = (uint32_t)obs_data_get_int(settings, "max_bitrate");
enc->rc_max_bitrate_window = obs_data_get_double(settings, "max_bitrate_window");
enc->bframes = obs_data_get_bool(settings, "bframes");
enum aq_mode spatial_aq_mode = obs_data_get_int(settings, "spatial_aq_mode");
if (spatial_aq_mode == AQ_AUTO) {
/* Only enable by default in CRF mode. */
enc->spatial_aq = strcmp(enc->rate_control, "CRF") == 0;
} else {
enc->spatial_aq = spatial_aq_mode == AQ_ENABLED;
}
return true;
}
static bool vt_update(void *data, obs_data_t *settings)
{
struct vt_encoder *enc = data;
uint32_t old_bitrate = enc->bitrate;
bool old_limit_bitrate = enc->limit_bitrate;
update_params(enc, settings);
if (old_bitrate == enc->bitrate && old_limit_bitrate == enc->limit_bitrate)
return true;
OSStatus code = session_set_bitrate(enc->session, enc->rate_control, enc->bitrate, enc->quality,
enc->limit_bitrate, enc->rc_max_bitrate, enc->rc_max_bitrate_window);
if (code != noErr)
VT_BLOG(LOG_WARNING, "Failed to set bitrate to session");
dump_encoder_info(enc);
return true;
}
static void *vt_create(obs_data_t *settings, obs_encoder_t *encoder)
{
struct vt_encoder *enc = bzalloc(sizeof(struct vt_encoder));
OSStatus code;
enc->encoder = encoder;
enc->vt_encoder_id = obs_encoder_get_id(encoder);
if (!update_params(enc, settings))
goto fail;
code = CMSimpleQueueCreate(NULL, 100, &enc->queue);
if (code != noErr) {
goto fail;
}
code = create_encoder(enc);
if (code != noErr) {
goto fail;
}
dump_encoder_info(enc);
return enc;
fail:
vt_destroy(enc);
return NULL;
}
static const uint8_t annexb_startcode[4] = {0, 0, 0, 1};
static void packet_put(struct darray *packet, const uint8_t *buf, size_t size)
{
darray_push_back_array(sizeof(uint8_t), packet, buf, size);
}
static void packet_put_startcode(struct darray *packet, int size)
{
assert(size == 3 || size == 4);
packet_put(packet, &annexb_startcode[4 - size], size);
}
static bool handle_prores_packet(struct vt_encoder *enc, CMSampleBufferRef buffer)
{
OSStatus err = 0;
size_t block_size = 0;
uint8_t *block_buf = NULL;
CMBlockBufferRef block = CMSampleBufferGetDataBuffer(buffer);
if (block == NULL) {
VT_BLOG(LOG_ERROR, "Failed to get block buffer for ProRes frame.");
return false;
}
err = CMBlockBufferGetDataPointer(block, 0, NULL, &block_size, (char **)&block_buf);
if (err != 0) {
VT_BLOG(LOG_ERROR, "Failed to get data buffer pointer for ProRes frame.");
return false;
}
packet_put(&enc->packet_data.da, block_buf, block_size);
return true;
}
static void convert_block_nals_to_annexb(struct vt_encoder *enc, struct darray *packet, CMBlockBufferRef block,
int nal_length_bytes)
{
size_t block_size;
uint8_t *block_buf;
CMBlockBufferGetDataPointer(block, 0, NULL, &block_size, (char **)&block_buf);
size_t bytes_remaining = block_size;
while (bytes_remaining > 0) {
uint32_t nal_size;
if (nal_length_bytes == 1)
nal_size = block_buf[0];
else if (nal_length_bytes == 2)
nal_size = CFSwapInt16BigToHost(((uint16_t *)block_buf)[0]);
else if (nal_length_bytes == 4)
nal_size = CFSwapInt32BigToHost(((uint32_t *)block_buf)[0]);
else
return;
bytes_remaining -= nal_length_bytes;
block_buf += nal_length_bytes;
if (bytes_remaining < nal_size) {
VT_BLOG(LOG_ERROR, "invalid nal block");
return;
}
packet_put_startcode(packet, 3);
packet_put(packet, block_buf, nal_size);
bytes_remaining -= nal_size;
block_buf += nal_size;
}
}
static bool handle_keyframe(struct vt_encoder *enc, CMFormatDescriptionRef format_desc, size_t param_count,
struct darray *packet, struct darray *extra_data)
{
OSStatus code;
const uint8_t *param;
size_t param_size;
for (size_t i = 0; i < param_count; i++) {
if (enc->codec_type == kCMVideoCodecType_H264) {
code = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format_desc, i, ¶m, ¶m_size,
NULL, NULL);
#ifdef ENABLE_HEVC
} else if (enc->codec_type == kCMVideoCodecType_HEVC) {
code = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format_desc, i, ¶m, ¶m_size,
NULL, NULL);
#endif
}
if (code != noErr) {
log_osstatus(LOG_ERROR, enc,
"getting NAL parameter "
"at index",
code);
return false;
}
packet_put_startcode(packet, 4);
packet_put(packet, param, param_size);
}
// if we were passed an extra_data array, fill it with
// SPS, PPS, etc.
if (extra_data != NULL)
packet_put(extra_data, packet->array, packet->num);
return true;
}
static bool convert_sample_to_annexb(struct vt_encoder *enc, struct darray *packet, struct darray *extra_data,
CMSampleBufferRef buffer, bool keyframe)
{
OSStatus code;
CMFormatDescriptionRef format_desc = CMSampleBufferGetFormatDescription(buffer);
size_t param_count;
int nal_length_bytes;
if (enc->codec_type == kCMVideoCodecType_H264) {
code = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format_desc, 0, NULL, NULL, ¶m_count,
&nal_length_bytes);
#ifdef ENABLE_HEVC
} else if (enc->codec_type == kCMVideoCodecType_HEVC) {
code = CMVideoFormatDescriptionGetHEVCParameterSetAtIndex(format_desc, 0, NULL, NULL, ¶m_count,
&nal_length_bytes);
#endif
} else {
log_osstatus(LOG_ERROR, enc, "invalid codec type", kCMFormatDescriptionError_ValueNotAvailable);
return false;
}
// it is not clear what errors this function can return
// so we check the two most reasonable
if (code == kCMFormatDescriptionBridgeError_InvalidParameter ||
code == kCMFormatDescriptionError_InvalidParameter) {
VT_BLOG(LOG_WARNING, "assuming 2 parameter sets "
"and 4 byte NAL length header");
param_count = 2;
nal_length_bytes = 4;
} else if (code != noErr) {
log_osstatus(LOG_ERROR, enc, "getting parameter count from sample", code);
return false;
}
if (keyframe && !handle_keyframe(enc, format_desc, param_count, packet, extra_data))
return false;
CMBlockBufferRef block = CMSampleBufferGetDataBuffer(buffer);
convert_block_nals_to_annexb(enc, packet, block, nal_length_bytes);
return true;
}
static bool is_sample_keyframe(CMSampleBufferRef buffer)
{
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(buffer, false);
if (attachments != NULL) {
CFDictionaryRef attachment;
CFBooleanRef has_dependencies;
attachment = (CFDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
has_dependencies =
(CFBooleanRef)CFDictionaryGetValue(attachment, kCMSampleAttachmentKey_DependsOnOthers);
return has_dependencies == kCFBooleanFalse;
}
return false;
}
static bool parse_sample(struct vt_encoder *enc, CMSampleBufferRef buffer, struct encoder_packet *packet, CMTime off)
{
CMTime pts = CMSampleBufferGetPresentationTimeStamp(buffer);
CMTime dts = CMSampleBufferGetDecodeTimeStamp(buffer);
if (CMTIME_IS_INVALID(dts))
dts = pts;
// imitate x264's negative dts when bframes might have pts < dts
else if (enc->bframes)
dts = CMTimeSubtract(dts, off);
pts = CMTimeMultiply(pts, enc->fps_num);
dts = CMTimeMultiply(dts, enc->fps_num);
const bool is_avc = enc->codec_type == kCMVideoCodecType_H264;
const bool has_annexb = is_avc || (enc->codec_type == kCMVideoCodecType_HEVC);
// All ProRes frames are "keyframes"
const bool keyframe = !has_annexb || is_sample_keyframe(buffer);