Записываем видео из Google Street View

Некоторое время назад стала популярной тема Hyperlapse/time-lapse видео. В первую очередь, благодаря небезызвестному ресурсу http://hyperlapse.tllabs.io/[embedded content]Сама по-себе возможность, конечно же замечательная, но сайт не позволяет сохранять результаты экспериментов в виде роликов. Вот эту досадную неприятность решено было исправить, и не просто исправить, а реализовать в виде программки для iOS, помогая тем самым, превратить iPhone или iPad в устройство для создания, а не потребления, контента.Как всё устроеноИтак, на сегодняшний день у нас есть несколько ресурсов, позволяющих снимать Стрит Видео. В первую очередь, это, hyperlapse.tllabs.io/, который позволяет отметить 2 точки, пролодить меежду ними маршрут и наслаждаться зацикленной анимацией.imageВторой сайт, который позволяет смотреть стрит видео это http://track-kit.netimageЭтот сайт позволяет просматривать видео для созданных или импорированных треков. Несмотря на то, что Стрит Видео здесь не является основной функцией, можно сгенерировать прямую ссылку именно на видео для тека. Например, такую: http://track-kit.net/maps_s3/index.php? track=8821.gpx&svv=134Правда на моём Маке более-менее работает только в ХромеТем не менее, ни один из этих ресурсов не позволяет сохранять видео. Эту проблему мы сейчас и будем решать.Для подготовки видео нам необходимо решить несколько задач.

Проложить маршрут от точки А к точке Б. Желательно, отобразить доступность Гугл Стрит Вью. Загрузить кадры панорам Дать возможность пользователю отредактировать панорамы, например, направив камеру на какой-лиобо объект. Сгенерировать видео из набора кадров Решить ряд типичных для iOS проблемм. Прокладываем маршрутДля этого мы используем Google Maps SDK for iOS и Google direction APIС помощью Google direction API запрашиваем у Google набор точек между начальной и конечно точек пути в закодированном виде.Google Maps SDK for iOS (класс GMSPath) понадобится чтобы перевести закодированый список точек который получили от Google в широту и долготу.Для общения с Google используется AFNetworking. static NSString *kLWSDirectionsURL = @«http://maps.googleapis.com/maps/api/directions/json?»;  — (void)loadDirectionsForWaypoints:(NSArray *)waypoints{ NSString *origin = [waypoints objectAtIndex:0]; int waypointCount = [waypoints count]; int destinationPos = waypointCount -1; NSString *destination = [waypoints objectAtIndex: destinationPos]; NSString *sensor = @«false»; NSMutableString *url = [NSMutableString stringWithFormat:@»%@&origin=%@&destination=%@&sensor=%@», kLWSDirectionsURL, origin, destination, sensor]; if (waypointCount>2) { [url appendString:@»&waypoints=optimize: true»]; int wpCount = waypointCount-2; for (int i=1; i

AFHTTPRequestOperation *requestOperation; NSMutableArray* coordinatesArr; -(void)startDownloadDataForURL:(NSURL*)url{ [self stopLoadingForUserInfo: userInfo]; requestOperation = [manager GET:[url absoluteString] parameters: nil success:^(AFHTTPRequestOperation *operation, id responseObject) { NSString* status = [responseObject objectForKey:@«status»]; NSArray* routesArr = [responseObject objectForKey:@«routes»]; if ([status isEqualToString:@«OK»] && [routesArr count] > 0) { NSDictionary *routes = [responseObject objectForKey:@«routes»][0]; NSDictionary *route = [routes objectForKey:@«overview_polyline»]; NSString *overview_route = [route objectForKey:@«points»]; GMSPath *path = [GMSPath pathFromEncodedPath: overview_route]; coordinatesArr = [NSMutableArray array]; for (int i = 0; i < [path count]; ++i) { CLLocationCoordinate2D coord = [path coordinateAtIndex:i]; [coordinatesArr addObject:[NSValue valueWithMKCoordinate:coord]]; } } } failure:^(AFHTTPRequestOperation *operation, NSError *error) { }]; } Если загрузка прошла успешно в списке coordinatesArr мы храним набор координат точек нашего пути.P.S. у Google direction API есть 1 нюанс — если необходимо провести маршрут не через 2, а скажем, через 20 точек, то придется делать несколько запросов для интервалов точек пути так как если передать в запрос через «&waypoints» большое количество промежуточных точек, Google может вернуть ошибку.Загружаем панорамы Для загрузки панорамы можно использовать запрос вида cbk0.google.com/cbk?output=json&ll=latitude,longitudeОн нам вернет информацию о ближайшей к точке панораме с координатами latitude,longitude.Самое важное что мы можем получить это «panoId» — id нужной нам панорамы (помимо panoID мы можем получить так же информацию об углах смещения панорамы, которые могут пригодиться если надо будет повернуть панораму в определенном направлении): NSString* panoID; 
-(void)loadMyWebViewForCoord:(CLLocationCoordinate2D)arg{ @try { if (!manager) { manager = [AFHTTPRequestOperationManager manager]; } NSString* urlStr = [NSString stringWithFormat:@"http://cbk0.google.com/cbk?output=json&ll=%f,%f",arg.latitude,arg.longitude]; request = [manager GET:urlStr parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) { id location = [responseObject objectForKey:@"Location"]; id projection = [responseObject objectForKey:@"Projection"]; if (location && projection) { panoID = [location objectForKey:@«panoId»]; } } failure:^(AFHTTPRequestOperation *operation, NSError *error) { }]; } Далее с помощью полученного ID панорамы мы можем через запрос:cbk0.google.com/cbk?output=tile&panoid=panoid&zoom=zoom&x=x&y=yполучить уже необходимые нам тайлы панорамы, где panoId — это полученный ранее идентификатор панорамы, zoom — это масштаб панорамы (ее размер), x и y — это номера тайла панорамы по вертикали и горизонтали, при этом количество тайлов панорамы зависит от введенного нами зума. Например, если мы выберем zoom = 3, то панорама будет состоять из 7 тайлов в ширину и 3 в высоту.То есть чтобы получить целую панораму нам надо загрузить все тайлы: -(void)loadImagesForPanoPoint:(PanoPoint*)currentPanoPointArg { @try { int zoom; int maxX; int maxY; if ([StreetViewSettings instance].hiQualityPano) { zoom = 3; maxX = 7; maxY = 3; } else { zoom = 2; maxX = 4; maxY = 2; } __block int allImages = maxX; for (int x = 0; x < maxX; ++x) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSMutableArray* imForCurrentCoodY = [NSMutableArray array]; for (int y = 0; y < maxY; ++y) { @autoreleasepool { NSString* pathStr = [NSString stringWithFormat:@"http://cbk0.google.com/cbk?output=tile&panoid=%@&zoom=%d&x=%d&y=%d",currentPanoPointArg.panoID,zoom,x,y]; NSString* tempDirectory = NSTemporaryDirectory(); NSString* imPath = [NSString stringWithFormat:@"%@/panoLoadCash/%@zoom=%dx=%dy=%d_%d",tempDirectory,currentPanoPointArg.panoID,zoom,x,y,currentCoordArrIndex]; NSData* im = nil; NSFileManager* fM = [NSFileManager defaultManager]; BOOL isD; if (![fM fileExistsAtPath:imPath isDirectory:&isD]) { im = [self imgByPath:pathStr]; } else { [imForCurrentCoodY addObject:imPath]; } if (im) { pakSize += im.length; if (![fM fileExistsAtPath:[NSTemporaryDirectory() stringByAppendingString:@"panoLoadCash"] isDirectory:&isD]) { NSError* err; [fM createDirectoryAtPath:[NSTemporaryDirectory() stringByAppendingString:@"panoLoadCash"] withIntermediateDirectories:YES attributes:[NSDictionary dictionary] error:&err]; } [im writeToFile:imPath atomically:YES]; [imForCurrentCoodY addObject:imPath]; } } } [imForCurrentCoordV addObject:@(x)]; [imForCurrentCoordTemp addObject:imForCurrentCoodY]; --allImages; }); } } Таким образом, пройдясь по всем точкам полученного от Google маршрута, мы можем загрузить для них панорамы и далее отобразить их пользователю в виде видео.Генерируем видео Для этого нам понадобится библиотека AVFoundation: #import От туда берем всего 3 класса: AVAssetWriter — запись медиа данных в файлAVAssetWriterInput — Добавляет пакет медиаданных в AVAssetWriter для записи в файлAVAssetWriterInputPixelBufferAdaptor — предоставляет пакет видеоданных (CVPixelBuffer) для AVAssetWriterInputСоответственно нам надо их где-то определить: AVAssetWriter* videoWriter; AVAssetWriterInput* writerInput; AVAssetWriterInputPixelBufferAdaptor* adaptor; Далее инициализация: NSError *error = nil; videoWriter = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath: videoPath] fileType: AVFileTypeQuickTimeMovie error:&error]; NSDictionary *videoSettings = [[NSDictionary alloc] initWithObjectsAndKeys: AVVideoCodecH264, AVVideoCodecKey, [NSNumber numberWithInt: videoSize.width], AVVideoWidthKey, [NSNumber numberWithInt: videoSize.height], AVVideoHeightKey, nil]; writerInput = [AVAssetWriterInput assetWriterInputWithMediaType: AVMediaTypeVideo outputSettings: videoSettings]; adaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput: writerInput sourcePixelBufferAttributes: nil]; [videoWriter addInput: writerInput]; [videoWriter startWriting]; [videoWriter startSessionAtSourceTime: kCMTimeZero] После этого все готово к записи видео.В AVAssetWriterInput имеется функция:(void)requestMediaDataWhenReadyOnQueue:(dispatch_queue_t)queue usingBlock:(void (^)(void))blockКоторая, вызывает Block каждый раз когда нужна новая порция данных. [writerInput requestMediaDataWhenReadyOnQueue: assetWriterQueue usingBlock:^ if (buffer == NULL) { CVPixelBufferPoolCreatePixelBuffer (NULL, adaptor.pixelBufferPool, &buffer); } UIImage *image = [self imageForIndex: currentIndexForBuff];

if (image) { buffer = [self pixelBufferFromCGImage: image.CGImage];

CMTime presentationTime= CMTimeMakeWithSeconds (speed*currentIndexForBuff, 33); if (![adaptor appendPixelBuffer: buffer withPresentationTime: presentationTime]) { [self finishVideo]; return; } CVPixelBufferRelease (buffer); if (currentIndexForBuff < imagesPathsForVideo.count) { } else { [self finishVideo]; } } else { if (currentIndexForBuff < imagesPathsForVideo.count) { } else { [self finishVideo]; } return; } ++currentIndexForBuff; }]; Скорость проигрывания видео контролируется с помощью переменной presentationTime, которая указывает время кадра в выходном файлеUIImage *image — это текущий кадрКогда все кадры записаны в видео, мы сообщаем videoWriter и writerInput о том что необходимо остановить запись видео: -(void)finishVideo { [writerInput markAsFinished]; [videoWriter finishWritingWithCompletionHandler:^(){}]; } Функция получения CVPixelBufferRef с изображения: - (CVPixelBufferRef) pixelBufferFromCGImage: (CGImageRef) image { if (image) { NSDictionary *options = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey, [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey, nil]; CVPixelBufferRef pxbuffer = NULL; CVPixelBufferCreate(kCFAllocatorDefault, CGImageGetWidth(image), CGImageGetHeight(image), kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options, &pxbuffer); CVPixelBufferLockBaseAddress(pxbuffer, 0); void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer); CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(pxdata, CGImageGetWidth(image), CGImageGetHeight(image), 8, 4*CGImageGetWidth(image), rgbColorSpace, kCGImageAlphaNoneSkipFirst); CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)), image); CGColorSpaceRelease(rgbColorSpace); CGContextRelease(context); CVPixelBufferUnlockBaseAddress(pxbuffer, 0); return pxbuffer; } else { return nil; } } Работа в фоновом режиме Чтобы видео продолжало генерироваться когда наше приложение находится в фоновом режиме, можно использовать long-running background task для этого советую использовать неплохой классhttps://github.com/vaskravchuk/VideoMaker/Добавляя немножко опций, получаем такой вот программный продукт.itunes.apple.com/us/app/street-video-maker-free-create/id788610126?mt=8imageВот пример видео, созданного при помощи такой программы:[embedded content]Одним из интересных применений стрит видео был ныне покойный сайт 360° Langstrasse. От которого осталость только видео: 

[embedded content]При помощи этой технологии можно создавать интересные проекты дополненной реальности, провдить географические изыскания и, конечно же. Развлекаться. На последок, немножко профессионального Time-lapse от Gunther Wegner
http://vimeo.com/50238512

© Habrahabr.ru