rays-video 0.2.0 → 0.3.0

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 CHANGED
@@ -6,6 +6,10 @@
6
6
  #import <AVFoundation/AVFoundation.h>
7
7
  #import <ImageIO/ImageIO.h>
8
8
  #import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
9
+ #ifdef IOS
10
+ #import <MobileCoreServices/UTCoreTypes.h>
11
+ #endif
12
+ #include <xot/util.h>
9
13
  #include "rays/bitmap.h"
10
14
  #include "rays/exception.h"
11
15
  #include "video_audio_in.h"
@@ -15,63 +19,134 @@ namespace Rays
15
19
  {
16
20
 
17
21
 
18
- struct VideoReader::Data
22
+ struct VideoDecoder::Data
19
23
  {
20
24
 
25
+ String path;
26
+
21
27
  virtual ~Data () {}
22
28
 
23
- virtual Image decode_image (size_t index, float pixel_density) const = 0;
29
+ virtual void get_bitmap (Bitmap* bitmap, size_t index) = 0;
24
30
 
25
31
  virtual VideoAudioInList get_audio_tracks () const
26
32
  {
27
33
  return {};
28
34
  }
29
35
 
30
- virtual coord width () const = 0;
36
+ virtual coord width () const = 0;
31
37
 
32
- virtual coord height () const = 0;
38
+ virtual coord height () const = 0;
33
39
 
34
- virtual float fps () const = 0;
40
+ virtual float fps () const = 0;
35
41
 
36
- virtual size_t size () const = 0;
42
+ virtual size_t size () const = 0;
37
43
 
38
44
  virtual operator bool () const = 0;
39
45
 
40
- };// VideoReader::Data
46
+ };// VideoDecoder::Data
41
47
 
42
48
 
43
- static Bitmap
44
- to_bitmap (CGImageRef cgimage)
49
+ static void
50
+ copy_pixels (Bitmap* bitmap, CMSampleBufferRef sample)
45
51
  {
52
+ if (!bitmap)
53
+ argument_error(__FILE__, __LINE__);
54
+ if (!*bitmap)
55
+ argument_error(__FILE__, __LINE__, "bitmap is empty");
56
+ if (!sample)
57
+ argument_error(__FILE__, __LINE__);
58
+
59
+ CVImageBufferRef pixel_buffer = CMSampleBufferGetImageBuffer(sample);
60
+ if (!pixel_buffer)
61
+ rays_error(__FILE__, __LINE__, "sample has no image buffer");
62
+
63
+ int w = (int) CVPixelBufferGetWidth(pixel_buffer);
64
+ int h = (int) CVPixelBufferGetHeight(pixel_buffer);
65
+ if (bitmap->width() != w || bitmap->height() != h)
66
+ {
67
+ rays_error(
68
+ __FILE__, __LINE__,
69
+ "frame size %dx%d does not match the video size %dx%d",
70
+ w, h, bitmap->width(), bitmap->height());
71
+ }
72
+ if (bitmap->color_space().type() != RGBA)
73
+ argument_error(__FILE__, __LINE__, "bitmap must be RGBA");
74
+
75
+ CVPixelBufferLockBaseAddress(pixel_buffer, kCVPixelBufferLock_ReadOnly);
76
+ {
77
+ // the reader hands out BGRA, the bitmap is RGBA
78
+ const uint8_t* src = (const uint8_t*) CVPixelBufferGetBaseAddress(pixel_buffer);
79
+ size_t src_pitch = CVPixelBufferGetBytesPerRow(pixel_buffer);
80
+ for (int y = 0; y < h; ++y)
81
+ {
82
+ const uint8_t* s = src + y * src_pitch;
83
+ uint8_t* d = bitmap->at<uint8_t>(0, y);
84
+ for (int x = 0; x < w; ++x, s += 4, d += 4)
85
+ {
86
+ d[0] = s[2];
87
+ d[1] = s[1];
88
+ d[2] = s[0];
89
+ d[3] = s[3];
90
+ }
91
+ }
92
+ }
93
+ CVPixelBufferUnlockBaseAddress(pixel_buffer, kCVPixelBufferLock_ReadOnly);
94
+ }
95
+
96
+ static void
97
+ copy_pixels (Bitmap* bitmap, CGImageRef cgimage)
98
+ {
99
+ if (!bitmap)
100
+ argument_error(__FILE__, __LINE__);
101
+ if (!*bitmap)
102
+ argument_error(__FILE__, __LINE__, "bitmap is empty");
46
103
  if (!cgimage)
47
104
  argument_error(__FILE__, __LINE__);
48
105
 
49
106
  int w = (int) CGImageGetWidth(cgimage);
50
107
  int h = (int) CGImageGetHeight(cgimage);
51
- Bitmap bmp(w, h, RGBA);
108
+ if (bitmap->width() != w || bitmap->height() != h)
109
+ {
110
+ rays_error(
111
+ __FILE__, __LINE__,
112
+ "frame size %dx%d does not match the video size %dx%d",
113
+ w, h, bitmap->width(), bitmap->height());
114
+ }
115
+ if (bitmap->color_space().type() != RGBA)
116
+ argument_error(__FILE__, __LINE__, "bitmap must be RGBA");
52
117
 
53
118
  std::shared_ptr<CGColorSpace> colorspace(
54
119
  CGColorSpaceCreateDeviceRGB(),
55
120
  CGColorSpaceRelease);
56
121
  std::shared_ptr<CGContext> context(
57
122
  CGBitmapContextCreate(
58
- bmp.pixels(), w, h, 8, bmp.pitch(), colorspace.get(),
123
+ bitmap->pixels(), w, h, 8, bitmap->pitch(), colorspace.get(),
59
124
  (CGBitmapInfo) kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big),
60
125
  CGContextRelease);
126
+ CGContextSetBlendMode(context.get(), kCGBlendModeCopy);
61
127
  CGContextDrawImage(context.get(), CGRectMake(0, 0, w, h), cgimage);
62
-
63
- return bmp;
64
128
  }
65
129
 
66
130
 
67
- struct VideoFileReader : VideoReader::Data
131
+ typedef std::shared_ptr<opaqueCMSampleBuffer> CMSampleBufferPtr;
132
+
133
+
134
+ struct VideoFileDecoder : VideoDecoder::Data
68
135
  {
69
136
 
70
- AVAsset* asset = nil;
137
+ enum {SKIP_MAX = 10};
138
+
139
+ AVAsset* asset = nil;
140
+
141
+ AVAssetTrack* video_track = nil;
71
142
 
72
- AVAssetTrack* video_track = nil;
143
+ AVAssetReader* reader = nil;
73
144
 
74
- VideoFileReader (const char* path)
145
+ AVAssetReaderOutput* output = nil;
146
+
147
+ ssize_t next_index = -1;
148
+
149
+ VideoFileDecoder (const char* path)
75
150
  {
76
151
  NSURL* url = [NSURL fileURLWithPath: [NSString stringWithUTF8String: path]];
77
152
  if (!url)
@@ -95,33 +170,167 @@ namespace Rays
95
170
  video_track = [track retain];
96
171
  }
97
172
 
98
- ~VideoFileReader ()
173
+ ~VideoFileDecoder ()
99
174
  {
175
+ stop_reading();
100
176
  [video_track release];
101
177
  [asset release];
102
178
  }
103
179
 
104
- Image decode_image (size_t index, float pixel_density) const override
180
+ void get_bitmap (Bitmap* bitmap, size_t index) override
105
181
  {
106
- AVAssetImageGenerator* generator =
107
- [[[AVAssetImageGenerator alloc] initWithAsset: asset] autorelease];
108
- generator.appliesPreferredTrackTransform = YES;
109
- generator.requestedTimeToleranceBefore = kCMTimeZero;
110
- generator.requestedTimeToleranceAfter = kCMTimeZero;
182
+ ssize_t sindex = (ssize_t) index;
183
+ if (next_index < 0 || sindex < next_index || sindex - next_index > SKIP_MAX)
184
+ start_reading(frame_time(index));
111
185
 
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)
186
+ CMSampleBufferPtr sample = read_sample(sindex);
187
+ if (!sample && reader.status == AVAssetReaderStatusFailed)
188
+ {
189
+ // a failed reader can not be reused, so try once more from scratch
190
+ start_reading(frame_time(index));
191
+ sample = read_sample(sindex);
192
+ }
193
+ if (!sample && reader.status == AVAssetReaderStatusCompleted)
194
+ {
195
+ // size() is an estimate, so an index past the end shows the last frame
196
+ start_reading(last_frame_time());
197
+ sample = read_sample(sindex);
198
+ }
199
+ if (!sample)
118
200
  {
119
201
  rays_error(
120
202
  __FILE__, __LINE__, "failed to decode frame %zu: %s",
121
- index, error ? error.localizedDescription.UTF8String : "unknown");
203
+ index, reader_error());
204
+ }
205
+
206
+ copy_pixels(bitmap, sample.get());
207
+ }
208
+
209
+ void start_reading (CMTime time)
210
+ {
211
+ stop_reading();
212
+
213
+ NSError* error = nil;
214
+ AVAssetReader* reader_ =
215
+ [[[AVAssetReader alloc] initWithAsset: asset error: &error] autorelease];
216
+ if (!reader_ || error)
217
+ {
218
+ rays_error(
219
+ __FILE__, __LINE__, "failed to create AVAssetReader: %s",
220
+ error ? error.localizedDescription.UTF8String : "unknown");
221
+ }
222
+
223
+ NSDictionary* settings =
224
+ @{
225
+ (NSString*) kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)
226
+ };
227
+ AVAssetReaderOutput* output_ = nil;
228
+ if (CGAffineTransformIsIdentity(video_track.preferredTransform))
229
+ {
230
+ AVAssetReaderTrackOutput* track_output = [AVAssetReaderTrackOutput
231
+ assetReaderTrackOutputWithTrack: video_track outputSettings: settings];
232
+ track_output.alwaysCopiesSampleData = NO;
233
+ output_ = track_output;
234
+ }
235
+ else
236
+ {
237
+ // let a composition apply the track's rotation to each frame
238
+ AVAssetReaderVideoCompositionOutput* composition_output =
239
+ [AVAssetReaderVideoCompositionOutput
240
+ assetReaderVideoCompositionOutputWithVideoTracks: @[video_track]
241
+ videoSettings: settings];
242
+ composition_output.videoComposition =
243
+ [AVMutableVideoComposition videoCompositionWithPropertiesOfAsset: asset];
244
+ composition_output.alwaysCopiesSampleData = NO;
245
+ output_ = composition_output;
246
+ }
247
+ if (![reader_ canAddOutput: output_])
248
+ rays_error(__FILE__, __LINE__, "cannot add output to AVAssetReader");
249
+ [reader_ addOutput: output_];
250
+
251
+ reader_.timeRange = CMTimeRangeMake(time, kCMTimePositiveInfinity);
252
+ if (![reader_ startReading])
253
+ {
254
+ NSString* desc = reader_.error.localizedDescription;
255
+ rays_error(
256
+ __FILE__, __LINE__, "failed to start reading: %s",
257
+ desc ? desc.UTF8String : "unknown");
122
258
  }
123
259
 
124
- return Image(to_bitmap(cgimage.get()), pixel_density);
260
+ reader = [reader_ retain];
261
+ output = [output_ retain];
262
+ next_index = frame_index(time);
263
+ }
264
+
265
+ void stop_reading ()
266
+ {
267
+ if (reader) [reader cancelReading];
268
+ [output release];
269
+ [reader release];
270
+ output = nil;
271
+ reader = nil;
272
+ next_index = -1;
273
+ }
274
+
275
+ CMSampleBufferPtr read_sample (ssize_t index)
276
+ {
277
+ // Reads on until the sample shown at 'index', or the last one when the
278
+ // stream ends before that. NULL on failure.
279
+
280
+ CMSampleBufferPtr last;
281
+ while (true)
282
+ {
283
+ CMSampleBufferPtr sample([output copyNextSampleBuffer], Xot::safe_cfrelease);
284
+ if (!sample)
285
+ return reader.status == AVAssetReaderStatusCompleted ? last : NULL;
286
+
287
+ if (!CMSampleBufferGetImageBuffer(sample.get()))
288
+ continue;
289
+
290
+ ssize_t sample_index =
291
+ frame_index(CMSampleBufferGetPresentationTimeStamp(sample.get()));
292
+ next_index = sample_index + 1;
293
+ if (sample_index >= index) return sample;
294
+
295
+ last = sample;
296
+ }
297
+ }
298
+
299
+ CMTime frame_duration () const
300
+ {
301
+ CMTime duration = video_track.minFrameDuration;
302
+ if (CMTIME_IS_VALID(duration) && duration.value > 0)
303
+ return duration;
304
+
305
+ return CMTimeMakeWithSeconds(1 / fps(), 600);
306
+ }
307
+
308
+ CMTime frame_time (size_t index) const
309
+ {
310
+ return CMTimeMultiply(frame_duration(), (int32_t) index);
311
+ }
312
+
313
+ CMTime last_frame_time () const
314
+ {
315
+ CMTime end = CMTimeRangeGetEnd(video_track.timeRange);
316
+ return CMTimeMaximum(CMTimeSubtract(end, frame_duration()), kCMTimeZero);
317
+ }
318
+
319
+ ssize_t frame_index (CMTime time) const
320
+ {
321
+ // A frame that straddles the start of the time range comes out with its
322
+ // time clipped to the start, so truncate rather than round
323
+
324
+ CMTime duration = frame_duration();
325
+ CMTime t = CMTimeConvertScale(
326
+ time, duration.timescale, kCMTimeRoundingMethod_RoundTowardZero);
327
+ return (ssize_t) (t.value / duration.value);
328
+ }
329
+
330
+ const char* reader_error () const
331
+ {
332
+ NSString* desc = reader ? reader.error.localizedDescription : nil;
333
+ return desc ? desc.UTF8String : "unknown";
125
334
  }
126
335
 
127
336
  VideoAudioInList get_audio_tracks () const override
@@ -138,14 +347,22 @@ namespace Rays
138
347
  return list;
139
348
  }
140
349
 
350
+ CGSize frame_size () const
351
+ {
352
+ // the size after the track's rotation is applied
353
+ CGSize size = CGSizeApplyAffineTransform(
354
+ video_track.naturalSize, video_track.preferredTransform);
355
+ return CGSizeMake(fabs(size.width), fabs(size.height));
356
+ }
357
+
141
358
  coord width () const override
142
359
  {
143
- return (int) video_track.naturalSize.width;
360
+ return (int) std::round(frame_size().width);
144
361
  }
145
362
 
146
363
  coord height () const override
147
364
  {
148
- return (int) video_track.naturalSize.height;
365
+ return (int) std::round(frame_size().height);
149
366
  }
150
367
 
151
368
  float fps () const override
@@ -164,10 +381,10 @@ namespace Rays
164
381
  return asset && video_track && video_track.nominalFrameRate > 0;
165
382
  }
166
383
 
167
- };// VideoFileReader
384
+ };// VideoFileDecoder
168
385
 
169
386
 
170
- struct GIFFileReader : VideoReader::Data
387
+ struct GIFFileDecoder : VideoDecoder::Data
171
388
  {
172
389
 
173
390
  enum {DEFAULT_FPS = 10};
@@ -178,7 +395,7 @@ namespace Rays
178
395
 
179
396
  float fps_ = 0;
180
397
 
181
- GIFFileReader (const char* path)
398
+ GIFFileDecoder (const char* path)
182
399
  {
183
400
  NSURL* url = [NSURL fileURLWithPath: [NSString stringWithUTF8String: path]];
184
401
  if (!url)
@@ -186,7 +403,7 @@ namespace Rays
186
403
 
187
404
  std::shared_ptr<CGImageSource> source(
188
405
  CGImageSourceCreateWithURL((CFURLRef) url, NULL),
189
- CFRelease);
406
+ Xot::safe_cfrelease);
190
407
  if (!source)
191
408
  rays_error(__FILE__, __LINE__, "failed to create CGImageSource");
192
409
 
@@ -207,7 +424,7 @@ namespace Rays
207
424
  this->fps_ = delay > 0 ? std::round(1 / delay) : (float) DEFAULT_FPS;
208
425
  }
209
426
 
210
- Image decode_image (size_t index, float pixel_density) const override
427
+ void get_bitmap (Bitmap* bitmap, size_t index) override
211
428
  {
212
429
  std::shared_ptr<CGImage> cgimage(
213
430
  CGImageSourceCreateImageAtIndex(source.get(), index, NULL),
@@ -218,7 +435,7 @@ namespace Rays
218
435
  __FILE__, __LINE__, "failed to decode GIF frame %zu", index);
219
436
  }
220
437
 
221
- return Image(to_bitmap(cgimage.get()), pixel_density);
438
+ copy_pixels(bitmap, cgimage.get());
222
439
  }
223
440
 
224
441
  coord width () const override
@@ -250,7 +467,7 @@ namespace Rays
250
467
  {
251
468
  std::shared_ptr<const __CFDictionary> props(
252
469
  CGImageSourceCopyPropertiesAtIndex(source.get(), index, NULL),
253
- CFRelease);
470
+ Xot::safe_cfrelease);
254
471
  if (!props)
255
472
  return 0;
256
473
 
@@ -280,7 +497,7 @@ namespace Rays
280
497
  return 0;
281
498
  }
282
499
 
283
- };// GIFFileReader
500
+ };// GIFFileDecoder
284
501
 
285
502
 
286
503
  static bool
@@ -290,74 +507,83 @@ namespace Rays
290
507
  }
291
508
 
292
509
 
293
- VideoReader::VideoReader ()
510
+ VideoDecoder::VideoDecoder ()
294
511
  : self(NULL)
295
512
  {
296
513
  }
297
514
 
298
- VideoReader::VideoReader (const char* path)
515
+ VideoDecoder::VideoDecoder (const char* path)
299
516
  : self(NULL)
300
517
  {
301
518
  if (!path || *path == '\0')
302
519
  argument_error(__FILE__, __LINE__, "path is empty");
303
520
 
304
521
  if (is_gif_path(path))
305
- self.reset(new GIFFileReader(path));
522
+ self.reset(new GIFFileDecoder(path));
306
523
  else
307
- self.reset(new VideoFileReader(path));
524
+ self.reset(new VideoFileDecoder(path));
525
+
526
+ self->path = path;
308
527
  }
309
528
 
310
- Image
311
- VideoReader::decode_image (size_t index, float pixel_density) const
529
+ void
530
+ VideoDecoder::get_bitmap (Bitmap* bitmap, size_t index)
312
531
  {
313
532
  if (!*this)
314
533
  invalid_state_error(__FILE__, __LINE__);
315
534
 
316
- return self->decode_image(index, pixel_density);
535
+ self->get_bitmap(bitmap, index);
317
536
  }
318
537
 
319
538
  VideoAudioInList
320
- VideoReader::get_audio_tracks () const
539
+ VideoDecoder::get_audio_tracks () const
321
540
  {
322
541
  if (!*this) return {};
323
542
  return self->get_audio_tracks();
324
543
  }
325
544
 
545
+ const char*
546
+ VideoDecoder::path () const
547
+ {
548
+ if (!*this) return "";
549
+ return self->path.c_str();
550
+ }
551
+
326
552
  coord
327
- VideoReader::width () const
553
+ VideoDecoder::width () const
328
554
  {
329
555
  if (!*this) return 0;
330
556
  return self->width();
331
557
  }
332
558
 
333
559
  coord
334
- VideoReader::height () const
560
+ VideoDecoder::height () const
335
561
  {
336
562
  if (!*this) return 0;
337
563
  return self->height();
338
564
  }
339
565
 
340
566
  float
341
- VideoReader::fps () const
567
+ VideoDecoder::fps () const
342
568
  {
343
569
  if (!*this) return 0;
344
570
  return self->fps();
345
571
  }
346
572
 
347
573
  size_t
348
- VideoReader::size () const
574
+ VideoDecoder::size () const
349
575
  {
350
576
  if (!*this) return 0;
351
577
  return self->size();
352
578
  }
353
579
 
354
- VideoReader::operator bool () const
580
+ VideoDecoder::operator bool () const
355
581
  {
356
582
  return self && *self;
357
583
  }
358
584
 
359
585
  bool
360
- VideoReader::operator ! () const
586
+ VideoDecoder::operator ! () const
361
587
  {
362
588
  return !operator bool();
363
589
  }
@@ -501,7 +727,7 @@ namespace Rays
501
727
 
502
728
  std::shared_ptr<CGImageDestination> dest(
503
729
  CGImageDestinationCreateWithURL((CFURLRef) url, kUTTypeGIF, video.size(), NULL),
504
- CFRelease);
730
+ Xot::safe_cfrelease);
505
731
  if (!dest)
506
732
  rays_error(__FILE__, __LINE__, "CGImageDestinationCreateWithURL() failed");
507
733
 
@@ -3,6 +3,7 @@
3
3
 
4
4
 
5
5
  #import <AVFoundation/AVFoundation.h>
6
+ #include <xot/util.h>
6
7
  #include "rays/exception.h"
7
8
 
8
9
 
@@ -134,7 +135,7 @@ namespace Rays
134
135
 
135
136
  std::shared_ptr<opaqueCMSampleBuffer> samples(
136
137
  [output copyNextSampleBuffer],
137
- CFRelease);
138
+ Xot::safe_cfrelease);
138
139
  if (!samples)
139
140
  return false;
140
141
 
data/src/sdl/video.cpp CHANGED
@@ -8,63 +8,69 @@ namespace Rays
8
8
  {
9
9
 
10
10
 
11
- struct VideoReader::Data
11
+ struct VideoDecoder::Data
12
12
  {
13
- };// VideoReader::Data
13
+ };// VideoDecoder::Data
14
14
 
15
15
 
16
- VideoReader::VideoReader ()
16
+ VideoDecoder::VideoDecoder ()
17
17
  {
18
18
  }
19
19
 
20
- VideoReader::VideoReader (const char*)
20
+ VideoDecoder::VideoDecoder (const char*)
21
21
  {
22
22
  not_implemented_error(__FILE__, __LINE__);
23
23
  }
24
24
 
25
- Image
26
- VideoReader::decode_image (size_t, float) const
25
+ void
26
+ VideoDecoder::get_bitmap (Bitmap*, size_t)
27
27
  {
28
28
  not_implemented_error(__FILE__, __LINE__);
29
29
  }
30
30
 
31
31
  VideoAudioInList
32
- VideoReader::get_audio_tracks () const
32
+ VideoDecoder::get_audio_tracks () const
33
33
  {
34
34
  return {};
35
35
  }
36
36
 
37
+ const char*
38
+ VideoDecoder::path () const
39
+ {
40
+ not_implemented_error(__FILE__, __LINE__);
41
+ }
42
+
37
43
  coord
38
- VideoReader::width () const
44
+ VideoDecoder::width () const
39
45
  {
40
46
  return 0;
41
47
  }
42
48
 
43
49
  coord
44
- VideoReader::height () const
50
+ VideoDecoder::height () const
45
51
  {
46
52
  return 0;
47
53
  }
48
54
 
49
55
  float
50
- VideoReader::fps () const
56
+ VideoDecoder::fps () const
51
57
  {
52
58
  return 0;
53
59
  }
54
60
 
55
61
  size_t
56
- VideoReader::size () const
62
+ VideoDecoder::size () const
57
63
  {
58
64
  return 0;
59
65
  }
60
66
 
61
- VideoReader::operator bool () const
67
+ VideoDecoder::operator bool () const
62
68
  {
63
69
  return false;
64
70
  }
65
71
 
66
72
  bool
67
- VideoReader::operator ! () const
73
+ VideoDecoder::operator ! () const
68
74
  {
69
75
  return !operator bool();
70
76
  }