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

    Некоторое время назад стала популярной тема Hyperlapse/time-lapse видео. В первую очередь, благодаря небезызвестному ресурсу http://hyperlapse.tllabs.io/

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

    Тем не менее, ни один из этих ресурсов не позволяет сохранять видео. Эту проблему мы сейчас и будем решать.
    Для подготовки видео нам необходимо решить несколько задач.
    1. Проложить маршрут от точки А к точке Б. Желательно, отобразить доступность Гугл Стрит Вью.
    2. Загрузить кадры панорам
    3. Дать возможность пользователю отредактировать панорамы, например, направив камеру на какой-лиобо объект.
    4. Сгенерировать видео из набора кадров
    5. Решить ряд типичных для 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<wpCount;i++){
                    [url appendString: @"|"];
                    [url appendString:[waypoints objectAtIndex:i]];
                }
            }
            url = [NSMutableString stringWithString:[url stringByAddingPercentEscapesUsingEncoding: NSASCIIStringEncoding]];
            _directionsURL = [NSURL URLWithString:url];
            [self startDownloadDataForURL:_directionsURL];
    }
    
    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 <AVFoundation/AVFoundation.h>
    

    От туда берем всего 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=8
    image
    Вот пример видео, созданного при помощи такой программы:

    Одним из интересных применений стрит видео был ныне покойный сайт 360° Langstrasse. От которого осталость только видео:



    При помощи этой технологии можно создавать интересные проекты дополненной реальности, провдить географические изыскания и, конечно же. Развлекаться. На последок, немножко профессионального Time-lapse от Gunther Wegner
    Share post

    Similar posts

    Comments 13

      +3
      круто, только мутить начинает от таких роликов слегка) может это мой вестибулярный аппарат ни к черту, не знаю
        0
        Тоже обратил на это внимание. Особенно переворот в туннеле.((
        +3
        Видео, которое было сделано в программе под iOs, полагаю может применяться в качестве показа маршрута и объяснения «как добраться из А в Б» на машине/автомобиле. В таком случае просто необходимо замедление скорости увеличение количества кадров на поворотах. Полагаю, это не так трудно реализовать. К тому же, можно задавать степень замедления прямиком в зависимости от угла поворота.
          +1
          Вот ведь буквально на днях искал такой сервис. У меня топографический кретинизм, поэтому очень хочется смотреть маршруты «своими глазами» заранее. Просто был уверен, что подобное уже давно сделано, но не тут то было.
          0
          Спасибо за программу! Испытываю настоящий вау-эффект как когда-то от просмотра простых панорам с гироскопом :) Можно сходу пару пожелалок?
          1. Кнопки без подписей немного путают.
          2. После начала загрузки изображений можно выбрать ещё 3 опции и среди них — использовать широкоугольную камеру. У меня она, кажется, ни на что не повлияла (или это только при сохранении?)
          3. С гироскопом тоже: с очередным кадром камера возвращается на место, так что главной фишкой воспользоваться не удалось (iPhone 5, iOS 7.0.4, jb).
          4. После возврвта на начало перестал работать зум на карте.
          Ну вообще успехов в дальнейшем развитии! Если нужно помочь с логами — не вопрос :)
            –1
            Подпишусь под всеми пунктами, особенно по поводу сохранения направления у гироскопа.
            Так же, хорошо бы иметь еще несколько фич:
            1. Возможность удалять не нужные кадры. Очень часто, бывает точка съемки прыгает на мост, соседнюю дорогу, или вдруг, посреди маршрута 3 кадра сняты в ночное время.
            2. Возможность сразу добавить музыку из библиотеки к ролику.
            3. Указать длину ролика в секундах, хочу 15 секунд, под инстаграм. Но текущей шкалой такой точности добиться не удалось, получалось либо 20, либо 10. Каждый раз перередливать видео накладно.
            4. Ну и собственно, сам экспорт в инстаграм прям из приложения)
            0
            Замечательная идея.
            (Хотя кажется было что-то подобное, где на самолете летать надо)
            Ждем со временем более хорошего качества видео.
            И сделать подобный видеогид по городам мира. (А то самому походить в гуглстрите лень, а видео бы посмотрел)
              0
              Почему качество сохранённого видео получается таким паршивым?? То есть значительно хуже превью.
                0
                Идея отличная. Но как водитель я чувствовал себя неуютно, когда после перекрестка оказывался на встречке.
                  0
                  В приложении очень не хватает функции указания цели для камеры, как это сделано у hyperlapse. Есть это в планах? Купил апп и хотел было «снять» ролик и немного разочаровался, думал это уже реализовано.
                    0
                    Спасибо за статью. По указанному примеру github.com/vaskravchuk/VideoMaker/ не находится библиотека libpods. Не подскажите где ее можно скачать, чтобы все-таки посмотреть пример?
                      0
                      А Вы пример открываете в IDE через файл проекта (VideoMaker.xcodeproj) или воркспейса (VideoMaker.xcworkspace)? попробуйте второе
                        0
                        спасибо, скомпилировалась)

                    Only users with full accounts can post comments. Log in, please.