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.
- checksums.yaml +7 -0
- data/.doc/ext/rays-video/native.cpp +17 -0
- data/.doc/ext/rays-video/video.cpp +257 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +12 -0
- data/.github/workflows/release-gem.yml +51 -0
- data/.github/workflows/tag.yml +35 -0
- data/.github/workflows/test.yml +37 -0
- data/.github/workflows/utils.rb +127 -0
- data/CONTRIBUTING.md +7 -0
- data/ChangeLog.md +19 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +147 -0
- data/Rakefile +25 -0
- data/VERSION +1 -0
- data/ext/rays-video/defs.h +17 -0
- data/ext/rays-video/extconf.rb +23 -0
- data/ext/rays-video/native.cpp +17 -0
- data/ext/rays-video/video.cpp +282 -0
- data/include/rays/video.h +102 -0
- data/include/rays-video/ruby/video.h +47 -0
- data/include/rays-video/ruby.h +10 -0
- data/include/rays-video.h +10 -0
- data/lib/rays/video.rb +45 -0
- data/lib/rays-video/ext.rb +1 -0
- data/lib/rays-video/extension.rb +41 -0
- data/lib/rays-video.rb +3 -0
- data/rays-video.gemspec +39 -0
- data/src/ios/video.mm +633 -0
- data/src/ios/video_audio_in.h +22 -0
- data/src/ios/video_audio_in.mm +252 -0
- data/src/osx/video.mm +633 -0
- data/src/osx/video_audio_in.h +22 -0
- data/src/osx/video_audio_in.mm +252 -0
- data/src/sdl/video.cpp +86 -0
- data/src/sdl/video_audio_in.cpp +63 -0
- data/src/video.cpp +278 -0
- data/src/video.h +50 -0
- data/src/video_audio_in.h +57 -0
- data/src/win32/video.cpp +86 -0
- data/src/win32/video_audio_in.cpp +63 -0
- data/test/helper.rb +15 -0
- data/test/test_video.rb +165 -0
- metadata +145 -0
data/src/ios/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) : 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_IOS_VIDEO_AUDIO_IN_H__
|
|
4
|
+
#define __RAYS_VIDEO_SRC_IOS_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
|