VLCMediaThumbnailer.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. /*****************************************************************************
  2. * VLCKit: VLCMediaThumbnailer
  3. *****************************************************************************
  4. * Copyright (C) 2010-2012 Pierre d'Herbemont and VideoLAN
  5. *
  6. * Authors: Pierre d'Herbemont
  7. *
  8. * This program is free software; you can redistribute it and/or modify it
  9. * under the terms of the GNU Lesser General Public License as published by
  10. * the Free Software Foundation; either version 2.1 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Lesser General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Lesser General Public License
  19. * along with this program; if not, write to the Free Software Foundation,
  20. * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
  21. *****************************************************************************/
  22. #import <vlc/vlc.h>
  23. #import "VLCMediaThumbnailer.h"
  24. #import "VLCLibVLCBridging.h"
  25. @interface VLCMediaThumbnailer ()
  26. {
  27. NSObject<VLCMediaThumbnailerDelegate>* __weak _thumbnailingDelegate;
  28. VLCMedia *_media;
  29. void *_mp;
  30. CGImageRef _thumbnail;
  31. void *_data;
  32. NSTimer *_parsingTimeoutTimer;
  33. NSTimer *_thumbnailingTimeoutTimer;
  34. CGFloat _thumbnailHeight,_thumbnailWidth;
  35. float _snapshotPosition;
  36. CGFloat _effectiveThumbnailHeight,_effectiveThumbnailWidth;
  37. int _numberOfReceivedFrames;
  38. BOOL _shouldRejectFrames;
  39. void * _internalLibVLCInstance;
  40. }
  41. - (void)didFetchThumbnail;
  42. - (void)notifyDelegate;
  43. - (void)fetchThumbnail;
  44. - (void)startFetchingThumbnail;
  45. @property (readonly, assign, nonatomic) void *dataPointer;
  46. @property (readonly, assign, nonatomic) BOOL shouldRejectFrames;
  47. @end
  48. static void *lock(void *opaque, void **pixels)
  49. {
  50. VLCMediaThumbnailer *thumbnailer = (__bridge VLCMediaThumbnailer *)(opaque);
  51. *pixels = [thumbnailer dataPointer];
  52. assert(*pixels);
  53. return NULL;
  54. }
  55. static const size_t kDefaultImageWidth = 320;
  56. static const size_t kDefaultImageHeight = 240;
  57. static const float kSnapshotPosition = 0.3;
  58. static const long long kStandardStartTime = 150000;
  59. void unlock(void *opaque, void *picture, void *const *p_pixels)
  60. {
  61. (void)picture;
  62. (void)p_pixels;
  63. }
  64. static void display(void *opaque, void *picture)
  65. {
  66. VLCMediaThumbnailer *thumbnailer = (__bridge VLCMediaThumbnailer *)(opaque);
  67. assert(!picture);
  68. // We may already have a thumbnail if we are receiving picture after the first one.
  69. // Just ignore.
  70. if ([thumbnailer thumbnail] || [thumbnailer shouldRejectFrames])
  71. return;
  72. [thumbnailer performSelectorOnMainThread:@selector(didFetchThumbnail) withObject:nil waitUntilDone:YES];
  73. }
  74. @implementation VLCMediaThumbnailer
  75. @synthesize media=_media;
  76. @synthesize delegate=_thumbnailingDelegate;
  77. @synthesize thumbnail=_thumbnail;
  78. @synthesize dataPointer=_data;
  79. @synthesize thumbnailWidth=_thumbnailWidth;
  80. @synthesize thumbnailHeight=_thumbnailHeight;
  81. @synthesize snapshotPosition=_snapshotPosition;
  82. @synthesize shouldRejectFrames=_shouldRejectFrames;
  83. + (VLCMediaThumbnailer *)thumbnailerWithMedia:(VLCMedia *)media andDelegate:(id<VLCMediaThumbnailerDelegate>)delegate
  84. {
  85. id obj = [[[self class] alloc] init];
  86. [obj setMedia:media];
  87. [obj setDelegate:delegate];
  88. [obj setLibVLCinstance:[VLCLibrary sharedInstance]];
  89. return obj;
  90. }
  91. + (VLCMediaThumbnailer *)thumbnailerWithMedia:(VLCMedia *)media delegate:(id<VLCMediaThumbnailerDelegate>)delegate andVLCLibrary:(VLCLibrary *)library
  92. {
  93. id obj = [[[self class] alloc] init];
  94. [obj setMedia:media];
  95. [obj setDelegate:delegate];
  96. if (library)
  97. [obj setLibVLCinstance:library.instance];
  98. else
  99. [obj setLibVLCinstance:[VLCLibrary sharedInstance]];
  100. return obj;
  101. }
  102. - (void)dealloc
  103. {
  104. NSAssert(!_thumbnailingTimeoutTimer, @"Timer not released");
  105. NSAssert(!_parsingTimeoutTimer, @"Timer not released");
  106. NSAssert(!_data, @"Data not released");
  107. NSAssert(!_mp, @"Not properly retained");
  108. if (_thumbnail)
  109. CGImageRelease(_thumbnail);
  110. if (_internalLibVLCInstance)
  111. libvlc_release(_internalLibVLCInstance);
  112. }
  113. - (void)setLibVLCinstance:(void *)libVLCinstance
  114. {
  115. _internalLibVLCInstance = libVLCinstance;
  116. libvlc_retain(_internalLibVLCInstance);
  117. }
  118. - (void *)libVLCinstance
  119. {
  120. return _internalLibVLCInstance;
  121. }
  122. - (void)fetchThumbnail
  123. {
  124. NSAssert(!_data, @"We are already fetching a thumbnail");
  125. VLCMediaParsedStatus parsedStatus = [_media parsedStatus];
  126. if (!(parsedStatus == VLCMediaParsedStatusFailed || parsedStatus == VLCMediaParsedStatusDone)) {
  127. [_media addObserver:self forKeyPath:@"parsedStatus" options:0 context:NULL];
  128. [_media parseWithOptions:VLCMediaParseLocal | VLCMediaParseNetwork];
  129. NSAssert(!_parsingTimeoutTimer, @"We already have a timer around");
  130. _parsingTimeoutTimer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(mediaParsingTimedOut) userInfo:nil repeats:NO];
  131. return;
  132. }
  133. [self startFetchingThumbnail];
  134. }
  135. - (void)startFetchingThumbnail
  136. {
  137. NSArray *tracks = [_media tracksInformation];
  138. // Find the video track
  139. NSDictionary *videoTrack = nil;
  140. for (NSDictionary *track in tracks) {
  141. NSString *type = track[VLCMediaTracksInformationType];
  142. if ([type isEqualToString:VLCMediaTracksInformationTypeVideo]) {
  143. videoTrack = track;
  144. break;
  145. }
  146. }
  147. unsigned imageWidth = _thumbnailWidth > 0 ? _thumbnailWidth : kDefaultImageWidth;
  148. unsigned imageHeight = _thumbnailHeight > 0 ? _thumbnailHeight : kDefaultImageHeight;
  149. float snapshotPosition = _snapshotPosition > 0 ? _snapshotPosition : kSnapshotPosition;
  150. /* optimize rendering if we know what's ahead, if not, well not too bad either */
  151. if (videoTrack) {
  152. int videoHeight = [videoTrack[VLCMediaTracksInformationVideoHeight] intValue];
  153. int videoWidth = [videoTrack[VLCMediaTracksInformationVideoWidth] intValue];
  154. // Constraining to the aspect ratio of the video.
  155. double ratio;
  156. if ((double)imageWidth / imageHeight < (double)videoWidth / videoHeight)
  157. ratio = (double)imageHeight / videoHeight;
  158. else
  159. ratio = (double)imageWidth / videoWidth;
  160. int newWidth = round(videoWidth * ratio);
  161. int newHeight = round(videoHeight * ratio);
  162. imageWidth = newWidth > 0 ? newWidth : imageWidth;
  163. imageHeight = newHeight > 0 ? newHeight : imageHeight;
  164. }
  165. _numberOfReceivedFrames = 0;
  166. NSAssert(!_shouldRejectFrames, @"Are we still running?");
  167. _effectiveThumbnailHeight = imageHeight;
  168. _effectiveThumbnailWidth = imageWidth;
  169. _snapshotPosition = snapshotPosition;
  170. _data = calloc(1, imageWidth * imageHeight * 4);
  171. NSAssert(_data, @"Can't create data");
  172. NSAssert(!_mp, @"We are already fetching a thumbnail");
  173. _mp = libvlc_media_player_new(self.libVLCinstance);
  174. if (_mp == NULL) {
  175. NSAssert(0, @"%s: creating the player instance failed", __PRETTY_FUNCTION__);
  176. [self endThumbnailing];
  177. }
  178. libvlc_media_add_option([_media libVLCMediaDescriptor], "no-audio");
  179. libvlc_media_add_option([_media libVLCMediaDescriptor], "no-spu");
  180. libvlc_media_add_option([_media libVLCMediaDescriptor], "avcodec-threads=1");
  181. libvlc_media_add_option([_media libVLCMediaDescriptor], "avcodec-skip-idct=4");
  182. libvlc_media_add_option([_media libVLCMediaDescriptor], "avcodec-skiploopfilter=3");
  183. libvlc_media_add_option([_media libVLCMediaDescriptor], "deinterlace=-1");
  184. libvlc_media_add_option([_media libVLCMediaDescriptor], "avi-index=3");
  185. libvlc_media_add_option([_media libVLCMediaDescriptor], "codec=avcodec,none");
  186. libvlc_media_player_set_media(_mp, [_media libVLCMediaDescriptor]);
  187. libvlc_video_set_format(_mp, "RGBA", imageWidth, imageHeight, 4 * imageWidth);
  188. libvlc_video_set_callbacks(_mp, lock, unlock, display, (__bridge void *)(self));
  189. if (snapshotPosition == kSnapshotPosition) {
  190. int length = _media.length.intValue;
  191. if (length < kStandardStartTime) {
  192. VKLog(@"short file detected");
  193. if (length > 1000) {
  194. VKLog(@"attempting seek to %is", (length * 25 / 100000));
  195. libvlc_media_add_option([_media libVLCMediaDescriptor], [[NSString stringWithFormat:@"start-time=%i", (length * 25 / 100000)] UTF8String]);
  196. }
  197. } else
  198. libvlc_media_add_option([_media libVLCMediaDescriptor], [[NSString stringWithFormat:@"start-time=%lli", (kStandardStartTime / 1000)] UTF8String]);
  199. }
  200. libvlc_media_player_play(_mp);
  201. NSURL *url = _media.url;
  202. NSTimeInterval timeoutDuration = 10;
  203. if (![url.scheme isEqualToString:@"file"]) {
  204. VKLog(@"media is remote, will wait longer");
  205. timeoutDuration = 45;
  206. }
  207. NSAssert(!_thumbnailingTimeoutTimer, @"We already have a timer around");
  208. _thumbnailingTimeoutTimer = [NSTimer scheduledTimerWithTimeInterval:timeoutDuration target:self selector:@selector(mediaThumbnailingTimedOut) userInfo:nil repeats:NO];
  209. }
  210. - (void)mediaParsingTimedOut
  211. {
  212. VKLog(@"WARNING: media thumbnailer media parsing timed out");
  213. [_media removeObserver:self forKeyPath:@"parsedStatus"];
  214. [self startFetchingThumbnail];
  215. }
  216. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  217. {
  218. if (object == _media && [keyPath isEqualToString:@"parsedStatus"]) {
  219. [_parsingTimeoutTimer invalidate];
  220. _parsingTimeoutTimer = nil;
  221. [_media removeObserver:self forKeyPath:@"parsedStatus"];
  222. [self startFetchingThumbnail];
  223. return;
  224. }
  225. return [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
  226. }
  227. - (void)didFetchThumbnail
  228. {
  229. if (_shouldRejectFrames)
  230. return;
  231. // The video thread is blocking on us. Beware not to do too much work.
  232. _numberOfReceivedFrames++;
  233. float position = libvlc_media_player_get_position(_mp);
  234. long long length = libvlc_media_player_get_length(_mp);
  235. // Make sure we are getting the right frame
  236. if (position < self.snapshotPosition && _numberOfReceivedFrames < 2) {
  237. libvlc_media_player_set_position(_mp, self.snapshotPosition, YES);
  238. return;
  239. }
  240. if ((length < kStandardStartTime * 2 && _numberOfReceivedFrames < 5) && self.snapshotPosition == kSnapshotPosition) {
  241. libvlc_media_player_set_position(_mp, kSnapshotPosition, YES);
  242. return;
  243. }
  244. if ((position <= 0.05 && _numberOfReceivedFrames < 8) && length > 1000) {
  245. // Arbitrary choice to work around broken files.
  246. libvlc_media_player_set_position(_mp, kSnapshotPosition, YES);
  247. return;
  248. }
  249. // it isn't always best what comes first
  250. if (_numberOfReceivedFrames < 4) {
  251. return;
  252. }
  253. NSAssert(_data, @"We have no data");
  254. CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
  255. const CGFloat width = _effectiveThumbnailWidth;
  256. const CGFloat height = _effectiveThumbnailHeight;
  257. const CGFloat pitch = 4 * width;
  258. CGContextRef bitmap = CGBitmapContextCreate(_data,
  259. width,
  260. height,
  261. 8,
  262. pitch,
  263. colorSpace,
  264. kCGImageAlphaNoneSkipLast);
  265. CGColorSpaceRelease(colorSpace);
  266. NSAssert(bitmap, @"Can't create bitmap");
  267. // Create the thumbnail image
  268. //NSAssert(!_thumbnail, @"We already have a thumbnail");
  269. if (_thumbnail)
  270. CGImageRelease(_thumbnail);
  271. _thumbnail = CGBitmapContextCreateImage(bitmap);
  272. _thumbnailWidth = _effectiveThumbnailWidth;
  273. _thumbnailHeight = _effectiveThumbnailHeight;
  274. // Put a new context there.
  275. CGContextRelease(bitmap);
  276. // Make sure we don't block the video thread now
  277. [self performSelector:@selector(notifyDelegate) withObject:nil afterDelay:0];
  278. }
  279. - (void)stopAsync
  280. {
  281. if (_mp) {
  282. libvlc_media_player_stop_async(_mp);
  283. libvlc_media_player_release(_mp);
  284. _mp = NULL;
  285. }
  286. // Now release data
  287. if (_data)
  288. free(_data);
  289. _data = NULL;
  290. _shouldRejectFrames = NO;
  291. }
  292. - (void)endThumbnailing
  293. {
  294. _shouldRejectFrames = YES;
  295. [_thumbnailingTimeoutTimer invalidate];
  296. _thumbnailingTimeoutTimer = nil;
  297. [self performSelectorInBackground:@selector(stopAsync) withObject:nil];
  298. }
  299. - (void)notifyDelegate
  300. {
  301. [self endThumbnailing];
  302. // Call delegate
  303. [_thumbnailingDelegate mediaThumbnailer:self didFinishThumbnail:_thumbnail];
  304. }
  305. - (void)mediaThumbnailingTimedOut
  306. {
  307. VKLog(@"WARNING: media thumbnailer media thumbnailing timed out");
  308. [self endThumbnailing];
  309. // Call delegate
  310. [_thumbnailingDelegate mediaThumbnailerDidTimeOut:self];
  311. }
  312. @end