rays-video 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/src/osx/video.mm ADDED
@@ -0,0 +1,633 @@
1
+ // -*- mode: objc -*-
2
+ #import "../video.h"
3
+
4
+
5
+ #include <map>
6
+ #import <AVFoundation/AVFoundation.h>
7
+ #import <ImageIO/ImageIO.h>
8
+ #import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
9
+ #include "rays/bitmap.h"
10
+ #include "rays/exception.h"
11
+ #include "video_audio_in.h"
12
+
13
+
14
+ namespace Rays
15
+ {
16
+
17
+
18
+ struct VideoReader::Data
19
+ {
20
+
21
+ virtual ~Data () {}
22
+
23
+ virtual Image decode_image (size_t index, float pixel_density) const = 0;
24
+
25
+ virtual VideoAudioInList get_audio_tracks () const
26
+ {
27
+ return {};
28
+ }
29
+
30
+ virtual coord width () const = 0;
31
+
32
+ virtual coord height () const = 0;
33
+
34
+ virtual float fps () const = 0;
35
+
36
+ virtual size_t size () const = 0;
37
+
38
+ virtual operator bool () const = 0;
39
+
40
+ };// VideoReader::Data
41
+
42
+
43
+ static Bitmap
44
+ to_bitmap (CGImageRef cgimage)
45
+ {
46
+ if (!cgimage)
47
+ argument_error(__FILE__, __LINE__);
48
+
49
+ int w = (int) CGImageGetWidth(cgimage);
50
+ int h = (int) CGImageGetHeight(cgimage);
51
+ Bitmap bmp(w, h, RGBA);
52
+
53
+ std::shared_ptr<CGColorSpace> colorspace(
54
+ CGColorSpaceCreateDeviceRGB(),
55
+ CGColorSpaceRelease);
56
+ std::shared_ptr<CGContext> context(
57
+ CGBitmapContextCreate(
58
+ bmp.pixels(), w, h, 8, bmp.pitch(), colorspace.get(),
59
+ (CGBitmapInfo) kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big),
60
+ CGContextRelease);
61
+ CGContextDrawImage(context.get(), CGRectMake(0, 0, w, h), cgimage);
62
+
63
+ return bmp;
64
+ }
65
+
66
+
67
+ struct VideoFileReader : VideoReader::Data
68
+ {
69
+
70
+ AVAsset* asset = nil;
71
+
72
+ AVAssetTrack* video_track = nil;
73
+
74
+ VideoFileReader (const char* path)
75
+ {
76
+ NSURL* url = [NSURL fileURLWithPath: [NSString stringWithUTF8String: path]];
77
+ if (!url)
78
+ rays_error(__FILE__, __LINE__, "invalid file path");
79
+
80
+ AVURLAsset* asset_ =
81
+ [[[AVURLAsset alloc] initWithURL: url options: nil] autorelease];
82
+ if (!asset_)
83
+ rays_error(__FILE__, __LINE__, "failed to create AVURLAsset");
84
+
85
+ NSArray<AVAssetTrack*>* tracks =
86
+ [asset_ tracksWithMediaType: AVMediaTypeVideo];
87
+ if (!tracks || tracks.count == 0)
88
+ rays_error(__FILE__, __LINE__, "no video tracks found");
89
+
90
+ AVAssetTrack* track = tracks[0];
91
+ if (track.nominalFrameRate <= 0)
92
+ rays_error(__FILE__, __LINE__, "invalid fps");
93
+
94
+ asset = [asset_ retain];
95
+ video_track = [track retain];
96
+ }
97
+
98
+ ~VideoFileReader ()
99
+ {
100
+ [video_track release];
101
+ [asset release];
102
+ }
103
+
104
+ Image decode_image (size_t index, float pixel_density) const override
105
+ {
106
+ AVAssetImageGenerator* generator =
107
+ [[[AVAssetImageGenerator alloc] initWithAsset: asset] autorelease];
108
+ generator.appliesPreferredTrackTransform = YES;
109
+ generator.requestedTimeToleranceBefore = kCMTimeZero;
110
+ generator.requestedTimeToleranceAfter = kCMTimeZero;
111
+
112
+ CMTime time = CMTimeMakeWithSeconds((double) index / fps(), 600);
113
+ NSError* error = nil;
114
+ std::shared_ptr<CGImage> cgimage(
115
+ [generator copyCGImageAtTime: time actualTime: nil error: &error],
116
+ CGImageRelease);
117
+ if (!cgimage || error)
118
+ {
119
+ rays_error(
120
+ __FILE__, __LINE__, "failed to decode frame %zu: %s",
121
+ index, error ? error.localizedDescription.UTF8String : "unknown");
122
+ }
123
+
124
+ return Image(to_bitmap(cgimage.get()), pixel_density);
125
+ }
126
+
127
+ VideoAudioInList get_audio_tracks () const override
128
+ {
129
+ NSArray<AVAssetTrack*>* tracks =
130
+ [asset tracksWithMediaType: AVMediaTypeAudio];
131
+ if (!tracks || tracks.count == 0)
132
+ return {};
133
+
134
+ VideoAudioInList list;
135
+ for (AVAssetTrack* track in tracks)
136
+ list.emplace_back(new VideoAudioIn(VideoAudioIn_Data_create(asset, track)));
137
+
138
+ return list;
139
+ }
140
+
141
+ coord width () const override
142
+ {
143
+ return (int) video_track.naturalSize.width;
144
+ }
145
+
146
+ coord height () const override
147
+ {
148
+ return (int) video_track.naturalSize.height;
149
+ }
150
+
151
+ float fps () const override
152
+ {
153
+ return video_track.nominalFrameRate;
154
+ }
155
+
156
+ size_t size () const override
157
+ {
158
+ double duration = CMTimeGetSeconds(video_track.timeRange.duration);
159
+ return (size_t) std::round(duration * fps());
160
+ }
161
+
162
+ operator bool () const override
163
+ {
164
+ return asset && video_track && video_track.nominalFrameRate > 0;
165
+ }
166
+
167
+ };// VideoFileReader
168
+
169
+
170
+ struct GIFFileReader : VideoReader::Data
171
+ {
172
+
173
+ enum {DEFAULT_FPS = 10};
174
+
175
+ std::shared_ptr<CGImageSource> source;
176
+
177
+ int w = 0, h = 0;
178
+
179
+ float fps_ = 0;
180
+
181
+ GIFFileReader (const char* path)
182
+ {
183
+ NSURL* url = [NSURL fileURLWithPath: [NSString stringWithUTF8String: path]];
184
+ if (!url)
185
+ rays_error(__FILE__, __LINE__, "invalid file path");
186
+
187
+ std::shared_ptr<CGImageSource> source(
188
+ CGImageSourceCreateWithURL((CFURLRef) url, NULL),
189
+ CFRelease);
190
+ if (!source)
191
+ rays_error(__FILE__, __LINE__, "failed to create CGImageSource");
192
+
193
+ size_t count = CGImageSourceGetCount(source.get());
194
+ if (count == 0)
195
+ rays_error(__FILE__, __LINE__, "GIF has no frames");
196
+
197
+ std::shared_ptr<CGImage> first_frame(
198
+ CGImageSourceCreateImageAtIndex(source.get(), 0, NULL),
199
+ CGImageRelease);
200
+ if (!first_frame)
201
+ rays_error(__FILE__, __LINE__, "failed to decode first GIF frame");
202
+
203
+ this->source = source;
204
+ this->w = (int) CGImageGetWidth(first_frame.get());
205
+ this->h = (int) CGImageGetHeight(first_frame.get());
206
+ float delay = get_frame_delay(0);
207
+ this->fps_ = delay > 0 ? std::round(1 / delay) : (float) DEFAULT_FPS;
208
+ }
209
+
210
+ Image decode_image (size_t index, float pixel_density) const override
211
+ {
212
+ std::shared_ptr<CGImage> cgimage(
213
+ CGImageSourceCreateImageAtIndex(source.get(), index, NULL),
214
+ CGImageRelease);
215
+ if (!cgimage)
216
+ {
217
+ rays_error(
218
+ __FILE__, __LINE__, "failed to decode GIF frame %zu", index);
219
+ }
220
+
221
+ return Image(to_bitmap(cgimage.get()), pixel_density);
222
+ }
223
+
224
+ coord width () const override
225
+ {
226
+ return w;
227
+ }
228
+
229
+ coord height () const override
230
+ {
231
+ return h;
232
+ }
233
+
234
+ float fps () const override
235
+ {
236
+ return fps_;
237
+ }
238
+
239
+ size_t size () const override
240
+ {
241
+ return source ? CGImageSourceGetCount(source.get()) : 0;
242
+ }
243
+
244
+ operator bool () const override
245
+ {
246
+ return source && CGImageSourceGetCount(source.get()) > 0;
247
+ }
248
+
249
+ float get_frame_delay (size_t index)
250
+ {
251
+ std::shared_ptr<const __CFDictionary> props(
252
+ CGImageSourceCopyPropertiesAtIndex(source.get(), index, NULL),
253
+ CFRelease);
254
+ if (!props)
255
+ return 0;
256
+
257
+ CFDictionaryRef gif_props = NULL;
258
+ if (!CFDictionaryGetValueIfPresent(
259
+ props.get(), kCGImagePropertyGIFDictionary, (const void**) &gif_props))
260
+ {
261
+ return 0;
262
+ }
263
+
264
+ CFNumberRef num = NULL;
265
+ if (CFDictionaryGetValueIfPresent(
266
+ gif_props, kCGImagePropertyGIFUnclampedDelayTime, (const void**) &num))
267
+ {
268
+ float value = 0;
269
+ CFNumberGetValue(num, kCFNumberFloatType, &value);
270
+ if (value > 0) return value;
271
+ }
272
+ if (CFDictionaryGetValueIfPresent(
273
+ gif_props, kCGImagePropertyGIFDelayTime, (const void**) &num))
274
+ {
275
+ float value = 0;
276
+ CFNumberGetValue(num, kCFNumberFloatType, &value);
277
+ if (value > 0) return value;
278
+ }
279
+
280
+ return 0;
281
+ }
282
+
283
+ };// GIFFileReader
284
+
285
+
286
+ static bool
287
+ is_gif_path (const char* path)
288
+ {
289
+ return String(path).downcase().ends_with(".gif");
290
+ }
291
+
292
+
293
+ VideoReader::VideoReader ()
294
+ : self(NULL)
295
+ {
296
+ }
297
+
298
+ VideoReader::VideoReader (const char* path)
299
+ : self(NULL)
300
+ {
301
+ if (!path || *path == '\0')
302
+ argument_error(__FILE__, __LINE__, "path is empty");
303
+
304
+ if (is_gif_path(path))
305
+ self.reset(new GIFFileReader(path));
306
+ else
307
+ self.reset(new VideoFileReader(path));
308
+ }
309
+
310
+ Image
311
+ VideoReader::decode_image (size_t index, float pixel_density) const
312
+ {
313
+ if (!*this)
314
+ invalid_state_error(__FILE__, __LINE__);
315
+
316
+ return self->decode_image(index, pixel_density);
317
+ }
318
+
319
+ VideoAudioInList
320
+ VideoReader::get_audio_tracks () const
321
+ {
322
+ if (!*this) return {};
323
+ return self->get_audio_tracks();
324
+ }
325
+
326
+ coord
327
+ VideoReader::width () const
328
+ {
329
+ if (!*this) return 0;
330
+ return self->width();
331
+ }
332
+
333
+ coord
334
+ VideoReader::height () const
335
+ {
336
+ if (!*this) return 0;
337
+ return self->height();
338
+ }
339
+
340
+ float
341
+ VideoReader::fps () const
342
+ {
343
+ if (!*this) return 0;
344
+ return self->fps();
345
+ }
346
+
347
+ size_t
348
+ VideoReader::size () const
349
+ {
350
+ if (!*this) return 0;
351
+ return self->size();
352
+ }
353
+
354
+ VideoReader::operator bool () const
355
+ {
356
+ return self && *self;
357
+ }
358
+
359
+ bool
360
+ VideoReader::operator ! () const
361
+ {
362
+ return !operator bool();
363
+ }
364
+
365
+
366
+ static CVPixelBufferRef
367
+ create_pixel_buffer (const Bitmap& bmp)
368
+ {
369
+ CVPixelBufferRef pixel_buffer = NULL;
370
+ CVReturn status = CVPixelBufferCreate(
371
+ kCFAllocatorDefault, bmp.width(), bmp.height(), kCVPixelFormatType_32BGRA,
372
+ (CFDictionaryRef) @{
373
+ (NSString*) kCVPixelBufferCGImageCompatibilityKey: @YES,
374
+ (NSString*) kCVPixelBufferCGBitmapContextCompatibilityKey: @YES,
375
+ },
376
+ &pixel_buffer);
377
+ if (status != kCVReturnSuccess || !pixel_buffer)
378
+ rays_error(__FILE__, __LINE__, "CVPixelBufferCreate() failed");
379
+
380
+ CVPixelBufferLockBaseAddress(pixel_buffer, 0);
381
+
382
+ void* dest = CVPixelBufferGetBaseAddress(pixel_buffer);
383
+ size_t dest_pitch = CVPixelBufferGetBytesPerRow(pixel_buffer);
384
+ const void* src = bmp.pixels();
385
+ int src_pitch = bmp.pitch();
386
+
387
+ // Bitmap is RGBA, CVPixelBuffer is BGRA — need to swizzle
388
+ for (int y = 0, h = bmp.height(); y < h; ++y)
389
+ {
390
+ const uint8_t* s = (const uint8_t*) src + y * src_pitch;
391
+ uint8_t* d = (uint8_t*) dest + y * dest_pitch;
392
+ for (int x = 0, w = bmp.width(); x < w; ++x)
393
+ {
394
+ d[x * 4 + 0] = s[x * 4 + 2]; // B
395
+ d[x * 4 + 1] = s[x * 4 + 1]; // G
396
+ d[x * 4 + 2] = s[x * 4 + 0]; // R
397
+ d[x * 4 + 3] = s[x * 4 + 3]; // A
398
+ }
399
+ }
400
+
401
+ CVPixelBufferUnlockBaseAddress(pixel_buffer, 0);
402
+ return pixel_buffer;
403
+ }
404
+
405
+ static void
406
+ save_as_video (const Video& video, const char* path, CFStringRef file_type)
407
+ {
408
+ NSURL* url = [NSURL fileURLWithPath: [NSString stringWithUTF8String: path]];
409
+
410
+ // Remove existing file
411
+ [[NSFileManager defaultManager] removeItemAtURL: url error: nil];
412
+
413
+ NSError* error = nil;
414
+ AVAssetWriter* writer = [[[AVAssetWriter alloc]
415
+ initWithURL: url fileType: (AVFileType) file_type error: &error]
416
+ autorelease];
417
+ if (!writer || error)
418
+ rays_error(__FILE__, __LINE__, "AVAssetWriter creation failed");
419
+
420
+ AVAssetWriterInput* input = [AVAssetWriterInput
421
+ assetWriterInputWithMediaType: AVMediaTypeVideo outputSettings: @{
422
+ AVVideoCodecKey: AVVideoCodecH264,
423
+ AVVideoWidthKey: @(video.width()),
424
+ AVVideoHeightKey: @(video.height()),
425
+ }];
426
+ input.expectsMediaDataInRealTime = NO;
427
+ if (![writer canAddInput: input])
428
+ rays_error(__FILE__, __LINE__, "cannot add writer input");
429
+
430
+ AVAssetWriterInputPixelBufferAdaptor* adaptor =
431
+ [AVAssetWriterInputPixelBufferAdaptor
432
+ assetWriterInputPixelBufferAdaptorWithAssetWriterInput: input
433
+ sourcePixelBufferAttributes: @{
434
+ (NSString*) kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA),
435
+ (NSString*) kCVPixelBufferWidthKey: @(video.width()),
436
+ (NSString*) kCVPixelBufferHeightKey: @(video.height())
437
+ }];
438
+
439
+ [writer addInput: input];
440
+ [writer startWriting];
441
+ [writer startSessionAtSourceTime: kCMTimeZero];
442
+
443
+ for (size_t i = 0, size = video.size(); i < size; ++i)
444
+ {
445
+ while (!input.readyForMoreMediaData)
446
+ [NSThread sleepForTimeInterval: 0.01];
447
+
448
+ CMTime time = CMTimeMake(i, (int32_t) video.fps());
449
+ CVPixelBufferRef pixel_buffer = create_pixel_buffer(video[i].bitmap());
450
+ bool result =
451
+ [adaptor appendPixelBuffer: pixel_buffer withPresentationTime: time];
452
+ CVPixelBufferRelease(pixel_buffer);
453
+ if (!result)
454
+ rays_error(__FILE__, __LINE__, "appendPixelBuffer failed at frame %zu", i);
455
+ }
456
+
457
+ [input markAsFinished];
458
+
459
+ dispatch_semaphore_t sem = dispatch_semaphore_create(0);
460
+ [writer finishWritingWithCompletionHandler: ^{
461
+ dispatch_semaphore_signal(sem);
462
+ }];
463
+ dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
464
+ dispatch_release(sem);
465
+
466
+ if (writer.status != AVAssetWriterStatusCompleted)
467
+ {
468
+ rays_error(
469
+ __FILE__, __LINE__, "video writing failed: %s",
470
+ writer.error.localizedDescription.UTF8String);
471
+ }
472
+ }
473
+
474
+ static CGImageRef
475
+ create_cgimage_from_bitmap (const Bitmap& bmp)
476
+ {
477
+ std::shared_ptr<CGColorSpace> colorspace(
478
+ CGColorSpaceCreateDeviceRGB(), CGColorSpaceRelease);
479
+ std::shared_ptr<CGDataProvider> provider(
480
+ CGDataProviderCreateWithData(
481
+ NULL, bmp.pixels(), bmp.height() * bmp.pitch(), NULL),
482
+ CGDataProviderRelease);
483
+ return CGImageCreate(
484
+ bmp.width(),
485
+ bmp.height(),
486
+ bmp.color_space().bpc(),
487
+ bmp.color_space().Bpp() * 8,
488
+ bmp.pitch(),
489
+ colorspace.get(),
490
+ (CGBitmapInfo) kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big,
491
+ provider.get(),
492
+ NULL,
493
+ false,
494
+ kCGRenderingIntentDefault);
495
+ }
496
+
497
+ static void
498
+ save_as_gif (const Video& video, const char* path)
499
+ {
500
+ NSURL* url = [NSURL fileURLWithPath: [NSString stringWithUTF8String: path]];
501
+
502
+ std::shared_ptr<CGImageDestination> dest(
503
+ CGImageDestinationCreateWithURL((CFURLRef) url, kUTTypeGIF, video.size(), NULL),
504
+ CFRelease);
505
+ if (!dest)
506
+ rays_error(__FILE__, __LINE__, "CGImageDestinationCreateWithURL() failed");
507
+
508
+ CGImageDestinationSetProperties(dest.get(), (CFDictionaryRef) @{
509
+ (NSString*) kCGImagePropertyGIFDictionary: @{
510
+ (NSString*) kCGImagePropertyGIFLoopCount: @0,// infinite loop
511
+ }
512
+ });
513
+
514
+ NSDictionary* frame_props = @{
515
+ (NSString*) kCGImagePropertyGIFDictionary: @{
516
+ (NSString*) kCGImagePropertyGIFDelayTime: @(1.0 / video.fps()),
517
+ },
518
+ };
519
+ for (size_t i = 0, size = video.size(); i < size; ++i)
520
+ {
521
+ std::shared_ptr<CGImage> cgimage(
522
+ create_cgimage_from_bitmap(video[i].bitmap()),
523
+ CGImageRelease);
524
+ if (!cgimage)
525
+ rays_error(__FILE__, __LINE__, "failed to get CGImage for frame %zu", i);
526
+
527
+ CGImageDestinationAddImage(
528
+ dest.get(), cgimage.get(), (CFDictionaryRef) frame_props);
529
+ }
530
+
531
+ if (!CGImageDestinationFinalize(dest.get()))
532
+ rays_error(__FILE__, __LINE__, "CGImageDestinationFinalize() failed");
533
+ }
534
+
535
+ struct VideoFormats
536
+ {
537
+
538
+ StringList exts;
539
+
540
+ std::map<String, AVFileType> ext2type;
541
+
542
+ };// VideoFormats
543
+
544
+ static const VideoFormats&
545
+ get_video_formats ()
546
+ {
547
+ static VideoFormats formats = []()
548
+ {
549
+ VideoFormats formats;
550
+
551
+ if (@available(macOS 11.0, iOS 14.0, *))
552
+ {
553
+ AVMutableComposition* comp = [AVMutableComposition composition];
554
+ [comp addMutableTrackWithMediaType: AVMediaTypeVideo
555
+ preferredTrackID: kCMPersistentTrackID_Invalid];
556
+
557
+ AVAssetExportSession* session = [AVAssetExportSession
558
+ exportSessionWithAsset: comp
559
+ presetName: AVAssetExportPresetPassthrough];
560
+ UTType* movie_type = [UTType typeWithIdentifier: @"public.movie"];
561
+ for (AVFileType file_type in session.supportedFileTypes)
562
+ {
563
+ UTType* type = [UTType typeWithIdentifier: file_type];
564
+ if (
565
+ !type ||
566
+ !type.preferredFilenameExtension ||
567
+ ![type conformsToType: movie_type])
568
+ {
569
+ continue;
570
+ }
571
+ String ext = type.preferredFilenameExtension.UTF8String;
572
+ formats.exts.push_back(ext);
573
+ formats.ext2type[ext] = file_type;
574
+ }
575
+ }
576
+ else
577
+ {
578
+ formats.exts = {"mp4", "mov", "m4v"};
579
+ formats.ext2type["mp4"] = AVFileTypeMPEG4;
580
+ formats.ext2type["mov"] = AVFileTypeQuickTimeMovie;
581
+ formats.ext2type["m4v"] = AVFileTypeAppleM4V;
582
+ }
583
+
584
+ formats.exts.push_back("gif");
585
+ return formats;
586
+ }();
587
+ return formats;
588
+ }
589
+
590
+ const StringList&
591
+ get_video_exts ()
592
+ {
593
+ return get_video_formats().exts;
594
+ }
595
+
596
+ static CFStringRef
597
+ get_video_file_type (const char* path_)
598
+ {
599
+ String path = String(path_).downcase();
600
+ auto dot = path.rfind('.');
601
+ if (dot == String::npos)
602
+ return nil;
603
+
604
+ String ext = path.substr(dot + 1);
605
+ const auto& map = get_video_formats().ext2type;
606
+ auto it = map.find(ext);
607
+ if (it == map.end())
608
+ return nil;
609
+
610
+ return (CFStringRef) it->second;
611
+ }
612
+
613
+ void
614
+ Video::save (const char* path)
615
+ {
616
+ if (!path || *path == '\0')
617
+ argument_error(__FILE__, __LINE__, "path is empty");
618
+ if (empty())
619
+ invalid_state_error(__FILE__, __LINE__, "no frames to save");
620
+
621
+ if (is_gif_path(path))
622
+ save_as_gif(*this, path);
623
+ else
624
+ {
625
+ CFStringRef file_type = get_video_file_type(path);
626
+ if (!file_type)
627
+ argument_error(__FILE__, __LINE__, "unsupported video format");
628
+ save_as_video(*this, path, file_type);
629
+ }
630
+ }
631
+
632
+
633
+ }// Rays
@@ -0,0 +1,22 @@
1
+ // -*- mode: objc -*-
2
+ #pragma once
3
+ #ifndef __RAYS_VIDEO_SRC_OSX_VIDEO_AUDIO_IN_H__
4
+ #define __RAYS_VIDEO_SRC_OSX_VIDEO_AUDIO_IN_H__
5
+
6
+
7
+ #import <AVFoundation/AVFoundation.h>
8
+ #include "../video_audio_in.h"
9
+
10
+
11
+ namespace Rays
12
+ {
13
+
14
+
15
+ VideoAudioIn::Data* VideoAudioIn_Data_create (
16
+ AVAsset* asset, AVAssetTrack* audio_track);
17
+
18
+
19
+ }// Rays
20
+
21
+
22
+ #endif//EOH