Недавно, команда ВК объявила конкурс на разработку мобильного приложения, которое бы расширяло возможности соцсети "ВКонтакте", и я решил принять участие, так как по условиям конкурса можно придумать свою идею приложения. У меня было три идеи, и нужно было выбрать, за какую из них взяться.

image


Уважаемые читатели «Хабрахабра», все ошибки и правки по данной статье присылайте, пожалуйста, в личные сообщения.

Идея 1


Мне очень нравятся групповые чаты в ВК, очень жаль, что нельзя в этих чатах общаться голосом. Такие групповые публичные аудио чаты могут помочь геймерам найти друзей для онлайн iOS игр. Например, я создаю аудиочат с названием «Asphalt 8» — и все, кто хочет сыграть со мной — присоединяются к моей аудиотусовке в приложении, и мы играем вместе, общаясь голосом. Аналогичные «аудиотусовки» есть на консоли PlayStation 4 — и я даже знаю людей, которые включают PS4 не для игр, а только для общения с друзьями, которые сидят в этих аудиотусовках. Зачем делать отдельное приложение, если можно созвониться в Viber или Whatsapp и играть в игры, общаясь в этих приложениях? А вот и нельзя, попробуйте созвониться в Viber и запустить, например, игру Deepworld – звонок в Viber тут же слетит, так как поток из Viber прерверся аудиопотоком из игры. В Skype ситуация для геймеров лучше, аудиосессия Skype останется активной, даже если включить музыку, музыка лишь немного «приглушиться». Но в наши дни считается плохим тоном звонить кому-то без предупреждения, и вдруг я позвоню другу, а он не хочет сейчас играть в игру, которую я предложу? Выход такой – создаем аудиотусовку, и все друзья получат уведомление: «Ваш друг Иван Иванов создал тусовку Hearthstone». Те из друзей, кто захочет присоединиться – жмут на уведомление и выходят на голосовую связь! Один клик на уведомление – и вы в тусовке, больше не нужно обзванивать друзей.

Идея 2


ВКонтакте есть раздел документы, так почему-бы не сделать аналог Dropbox для ВК? Да, придется сделать Windows/Mac клиент, помимо мобильного, на ПК пользователя будет создана папка, все файлы из которой будут синхронизироваться с документами ВКонтакте, и папкой на мобильном устройсте. Получается некий аналог Dropbox с backend ВКонтакте.

Идея 3


Существует «Теория шести рукопожатий» — теория, согласно которой любые два человека на Земле разделены не более чем пятью уровнями общих знакомых (и, соответственно, шестью уровнями связей). Так почему бы не сделать приложение, в котором можно узнать, сколько человек меня разделяют в VK, например, с Павлом Дуровым? То есть мы вписываем двух пользователей в окно мобильного приложения — и получаем цепочку друзей, через которых мы можем выйти на контакт с нужным нам человеком. Для реализации идеи придется скачать все профили пользователей ВКонтакте, перебирая их по ID.

Core Audio


Внимание! Core Audio не зря славится своей сложностью! Попытки загуглить проблемы на stackoverflow.com часто приводят к вопросам, на которые на данном портале никто так и не ответил! Платная поддержка Apple тоже разводит руками! Подводные камни всплывают на каждом шаге разработки!


Выбор пал на первую идею, так как она мне показалась более сложной в реализации, и чтобы усложнить процесс, я решил сделать реализацию на Core Audio, по которой практически отсутствует документация, так что придется экспериментировать. ВКонтакте давно бы уже пора добавить аудиозвонки, ведь даже у Facebook в мобильном клиенте есть возможность позвонить голосом! Чем ВК хуже? Команда ВК уже пыталась запустить видеозвонки в web клиенте, сделала alfa версию, но на этом все и закончилось. Я считаю, что нужно добавлять возможность позвонить в мобильный клиент ВК в обязательном порядке! И в рамках этой статьи я постараюсь рассказать, как это нужно сделать.

Что я вообще знаю о звуке? Как передается звук по сети? С видео все проще, каждый пиксель можно закодировать в RGB и передавать изменения матрицы пикселей в массиве. Но что из себя представляет «слепок звука» за единицу времени? А представляет он собой вот такой массив из чисел типа Float:

image

Причем, если мы сложим (Float 1) + (Float 2) + (Float 3) +… + (Float (n)) и разделим сумму на количество элементов (n) — то мы получим громкость данного слепка!

Чтобы увеличить уроверь звука в два раза, мы должны всего лишь умножить все элементы этого массива на 2:

(Float 1)*2 + (Float 2)*2 + (Float 3)*2 +… + (Float (n))*2

Но что делать, если в нашем случае звук приходит от нескольких пользователей, как нам «склеить» два аудио потока? Ответ прост — нужно просто сложить попарно элементы этих двух массивов.

В Mac OS X в обоих форматах kAudioFormatFlagsCanonical и kAudioFormatFlagsAudioUnitCanonical один элемент массива представляет из себя Float с плавающей точкой, но вычисления с плавающей точкой обходились слишком дорого для кристаллов с процессорами ARM, поэтому в iOS отсчеты в формате kAudioFormatFlagsCanonical представлены целыми со знаком, а в формате kAudioFormatFlagsAudioUnitCanonical — целыми числами с фиксированной точкой. «8.24». Это означает, что слева от десятичной точки находятся 8 бит (целая часть), а справа — 24 бита (дробная часть).


Выбираем название приложения и иконку:


У меня в голове было два названия, первое из них — «Tusa», второе «Wassap». Приложение представляет из себя групповой аудиочат, так что было бы здорово, если бы участники здоровались при входе фразой «Wassaaaap!», но из-за схожести названия с «WhatsApp» я выбрал название «Tusa». В качестве иконки я выбрал сначала микрофон, но потом заменил его на камушек:
image

Как работает приложение «Tusa»


image

  • Для начала пользователь попадает на стартовый экран, где ему предлагается авторизоваться с помощью VK кнопки. На этом этапе приложение получает информацию о пользователе и список друзей (только публичная информация).
  • Затем приложение отправляет информация о пользователе и список друзей на PHP сервер, PHP сервер в свою очередь возвращает список аудио чатов друзей пользователя, причем каждой «тусовке» присвоен IP и порт Python сервера, на котором и происходит обмен звуком.
  • Пользователь выбирает «аудио тусовку», и приложение коннектится на нужный Python сервер, либо пользовать выбирает «создать новую тусовку», и уже другие пользователь в дальнейшем заходят в этот чат.


Зачем вообще использовать PHP сервер? Почему нельзя получить список чатов на том же Python сервере? Сделал я PHP сервер для того, чтобы была возможность распараллелить «аудио тусовки» по разным Python серверам, и если интернет канал на одном Python сервере заполниться, то PHP сервер будет создавать аудио комнаты на другом Python сервере с отдельным IP адресом. Так же PHP часть будет ответственна за рассылку IN-APP уведомлений.

Небольшой эксперимент — предыстория


Перед тем, как ознакомиться с Core Audio, я решил провести небольшой эксперимент со своими возможностями. Я представил такую ситуацию — мой самолет потерпел авиакатастрофу, и я с другими пассажирами оказался на необитаемом острове с Macbook, роутером, XCODE из коробки и дюжиной iOS девайсов, заряжающихся от солнечных батарей. Никакой документации по Core Audio у меня бы не было, а так как на тот момент я вообще не знал, как цифруется звук, смог бы я в этих условиях написать аудиочат? Все что я знал на тот момент — это как записывать .wav (.caf) файлы и их же воспроизводить. Недавно я разработал iOS риалтайм мультиплеерную игру «танчики с денди», где на одной карте играют до 100 танчиков вместе. Я решил в несколько строк кода превратить игру в аудиочат, записывая в цикле звук в файл, потом пересылая этот файл другим пользователям, и у пользователей создавать playlist из этих файлов! Это полный идиотизм — слать файлы со звуком, и я провел этот экперимент только благодаря моему уже существующему сетевому движку, хотелось узнать показатели задержки в этом случае и проверить работу моего сетевого кода в условиях пересылки большого количества данных, но в результате, помимо выявленных багов сетевого кода, я получил интересные подробности работы стардатного аудиоплеера в iOS, которые возможно пригодятся и читателям.

Как проиграть звук в iOS? С помощью AVAudioPlayer

AVAudioPlayer *avPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:
    [NSURL fileURLWithPath:@"имя_файла.caf"] error:nil];
[avPlayer play];


Звук от других пользователей у нас приходит в формате NSData и добавляется в массив плейлиста, так что с помощью AVAudioPlayer можно проигрывать не файл из папки, а звук из NSData напрямую:

AVAudioPlayer *avPlayer = [[AVAudioPlayer alloc] initWithData:
    data fileTypeHint:AVFileTypeCoreAudioFormat error:nil];
[avPlayer play];


Как узнать, что AVAudioPlayer закончил воспроизведение? Через callback audioPlayerDidFinishPlaying:

- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player 
    successfully:(BOOL)flag
{
    // AVAudioPlayer завершил проигрывание, 
    // берем следующий звук из плейлиста
}


Я запустил этот вариант на iPhone и iPad — но вот разочарование, звук воспроизводился с прерываниями. Дело в том, что инициализация AVAudioPlayer занимает до 100 миллисекунд, отсюда и лаги со звуком.

Решением оказался AVQueuePlayer, который специально был сделал для воспроизведения плейлистов без задержки между треками, инициализируем AVQueuePlayer:

AVQueuePlayer avPlayer = [[AVQueuePlayer alloc] initWithItems:
    playerItems];
avPlayer.volume = 1;
[avPlayer play];


Чтобы добавить файл в плейлист, используем AVPlayerItem:

NSURL *url = [NSURL fileURLWithPath:pathForResource];
AVPlayerItem *item = [AVPlayerItem playerItemWithURL:url];
NSArray *playerItems = [NSArray arrayWithObjects:item, nil];
[avPlayer insertItem:item afterItem:nil];


Запустив этот вариант я услышал четкий звук между моими устройствами, задержка была около 250 миллисекунд, так как файлы более короткого размера записать не получалось, вылетала ошибка. Ну и конечно, данный вариант был прожорливый до траффика, ведь помимо нужных звуков, по сети несколько раз в секунду передавался .wav (.caf) файл, который содержал заголовок. Так же данный метод не работает в фоновом режиме, так в фоне iOS нельзя начать воспроизводить новые звуки. На этом закончим эксперимент и начнем программировать приложение.

Что мы знаем про Core Audio?


На сайте Apple есть пример записи звука в аудиофайл, используя Core Audio, скачать его можно на странице:

https://developer.apple.com/library/ios/samplecode/AVCaptureToAudioUnit/Introduction/Intro.html

После изучения данного исходника, мне стало понятно, что при записи звука много раз в секунду вызывается Callback

#pragma mark ======== AudioUnit recording callback =========
static OSStatus PushCurrentInputBufferIntoAudioUnit(void * inRefCon,
    AudioUnitRenderActionFlags * ioActionFlags,
    const AudioTimeStamp * inTimeStamp,
    UInt32 inBusNumber,
    UInt32 inNumberFrames,
    AudioBufferList * ioData)
{
    // AudioBufferList *ioData - это и есть наш звук 
    // за промежуток времени
    // Упакуем звук в NSData для отправки на удаленный сервер
    NSMutableData * soundData = [NSMutableData dataWithCapacity:0];
    for( int y=0; y<ioData->mNumberBuffers; y++ )
    {
        AudioBuffer audioBuff = ioData->mBuffers[y];
        // Вот он звук, в виде массива из Float
        Float32 *frame = (Float32*)audioBuff.mData;
        // Упаковываем звук в бинарные данные для пересылки
        [soundData appendBytes:&frame length:sizeof(float)];
    }
    return noErr;
}


Разобрав формат AudioBufferList, который содержал звук в виде списка цифр, я переконвертировал AudioBufferList в NSData, выстроив все цифры в цепочку по 4 байта — и через python сервер в цикле передал этот буффер на удаленное устройство. Но как воспроизвети AudioBufferList на удаленном девайсе? В официальных исходниках на сайте Apple я не нашел ответа, ответ саппорта Apple тоже не дал мне нужной информации. Но проведя достаточно времени по принципу «научного тыка», я понял, что для этой цели существует аналогичный callback, в который нужно подставлять AudioBufferList и он будет воспроизводиться на лету:

#pragma mark ======== AudioUnit playback callback =========
static OSStatus playbackCallback(void *inRefCon,
    AudioUnitRenderActionFlags *ioActionFlags,
     const AudioTimeStamp *inTimeStamp,
     UInt32 inBusNumber,
    UInt32 inNumberFrames,
    AudioBufferList *ioData) 
{
    // Заполняем *ioData нашим массивом из Floats, 
    // который мы получили с удаленного сервера
    return noErr;
}


Как активировать данные callbacks? Для начала переименуйте ваш .m файл проекта в .mm и импортируйте все нужные C++ библиотеки из проекта AVCaptureToAudioUnit. После этого создаем, настраиваем и запускаем наш аудиопоток с помощью данного кода:

    // Объявляем переменные
    OSStatus status;
    AudioComponentInstance audioUnit;

    // Настраиваем аудио компоненты
    AudioComponentDescription desc;
    desc.componentType = kAudioUnitType_Output;
    desc.componentSubType = kAudioUnitSubType_RemoteIO;
    desc.componentFlags = 0;
    desc.componentFlagsMask = 0;
    desc.componentManufacturer = kAudioUnitManufacturer_Apple;
    
    AudioComponent inputComponent = AudioComponentFindNext(NULL, &desc);
    status = AudioComponentInstanceNew(inputComponent, &audioUnit);
    
    // Активируем IO для записи звука
    UInt32 flag = 1;
    status = AudioUnitSetProperty(audioUnit,
        kAudioOutputUnitProperty_EnableIO,
        kAudioUnitScope_Input,
        1, // Input
        &flag,
        sizeof(flag));

    // Активируем IO для проигрывания звука
    status = AudioUnitSetProperty(audioUnit,
        kAudioOutputUnitProperty_EnableIO,
        kAudioUnitScope_Output,
        0, // Output
        &flag,
        sizeof(flag));
    
    AudioStreamBasicDescription audioFormat;
    
    // Описываем формат звука
    audioFormat.mSampleRate = 8000.00;
    audioFormat.mFormatID = kAudioFormatLinearPCM;
    audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger |        
        kAudioFormatFlagIsPacked;
    audioFormat.mFramesPerPacket = 1;
    audioFormat.mChannelsPerFrame = 1;
    audioFormat.mBitsPerChannel = 16;
    audioFormat.mBytesPerPacket = 2;
    audioFormat.mBytesPerFrame = 2;
    
    // Apply format
    status = AudioUnitSetProperty(audioUnit,
        kAudioUnitProperty_StreamFormat,
        kAudioUnitScope_Output,
        1, // Input
        &audioFormat,
        sizeof(audioFormat));
    
    status = AudioUnitSetProperty(audioUnit,
        kAudioUnitProperty_StreamFormat,
        kAudioUnitScope_Input,
        0, // Output
        &audioFormat,
        sizeof(audioFormat));
    
    // Активируем Callback для записи звука
    AURenderCallbackStruct callbackStruct;
    callbackStruct.inputProc = recordingCallback;
    callbackStruct.inputProcRefCon = (__bridge void * _Nullable)(self);
    status = AudioUnitSetProperty(audioUnit,
        kAudioOutputUnitProperty_SetInputCallback,
        kAudioUnitScope_Global,
        1, // Input
        &callbackStruct,
        sizeof(callbackStruct));
    
    // Активируем Callback для воспроизведения звука
    callbackStruct.inputProc = playbackCallback;
    callbackStruct.inputProcRefCon = (__bridge void * _Nullable)(self);
    status = AudioUnitSetProperty(audioUnit,
        kAudioUnitProperty_SetRenderCallback,
        kAudioUnitScope_Global,
        0, // Output
        &callbackStruct,
        sizeof(callbackStruct));
    
    // Отключаем инициализацию буферов для записи
    flag = 0;
    status = AudioUnitSetProperty(audioUnit,
        kAudioUnitProperty_ShouldAllocateBuffer,
        kAudioUnitScope_Output, 
        1, // Input
        &flag, 
        sizeof(flag));
    // Инициализируем
    status = AudioUnitInitialize(audioUnit);
    // Запускаем
    status = AudioOutputUnitStart(audioUnit);


Кстати, в качестве эксперимента, я изучил формат caf файла, просидев тучу времени с HEX редактором и попробовал на удаленном девайсе взять AudioBufferList, добавить к нему побайтово header (заголовок) .caf файла, затем сохранить этот AudioBufferList в .caf файл, и воспроизвести с помощью AVQueuePlayer. И самое странное, что у меня это получилось!

Novocaine


Итак, мы уже разобрались с Core Audio, но как сделать процесс еще проще и нагляднее? И ответ есть, нужно использовать Novocaine!

https://github.com/alexbw/novocaine

Что представляет из себя Novocaine? Три года три кодера оформляли Core Audio в отдельный класс, и у них здорово получилось! Novocaine реализован на C++, так что для подключения C++ класса с нашему Objective C файлу, нужно переименовать его из .m в .mm — и все import производить в начале .mm файла.

Как считать аудио в буффер?

Novocaine *audioManager = [Novocaine audioManager];
[audioManager setInputBlock:^(float *newAudio, UInt32 numSamples, UInt32 numChannels) {
    // Здесь мы получаем аудио с микрофона примерно каждые 20 миллисекунд
    // Если numChannels = 2, значит newAudio[0] это канал 1,
    // newAudio[1] - канал 2, newAudio[2] - канал 1 и т.д.
}];
[audioManager play];


Как воспроизвести буффер?

Novocaine *audioManager = [Novocaine audioManager];
[audioManager setOutputBlock:^(float *audioToPlay, 
    UInt32 numSamples, 
    UInt32 numChannels) 
    {
        // Все, что нужно - это поместить здесь 
        // массив с float звуками в audioToPlay
    }];
[audioManager play]; 


Вот так просто!

Пробуем собрать все это на iPhone и iPad, запускаем аудиозвонок — и… Эхо! Писк! Убийственное эхо проходит многократно через канал связи и врезается писком в мозг! Я рассчитывал, что пользователи будут общаться в том числе и без гарнитуры на громкой связи, но звук шел от меня на удаленное устройство, из динамика удаленного девайса попадал в микрофон и возвращался ко мне. Неприятно. Как реализовать эхоподавление в iOS, используя Core Audio?

Нужно использовать параметр kAudioUnitSubType_VoiceProcessingIO для аудиопотока, вместо стандартного kAudioUnitSubType_RemoteIO. Открываем файл Novocaine.m, находим строку:

inputDescription.componentSubType = kAudioUnitSubType_RemoteIO;

заменяем на:

inputDescription.componentSubType = kAudioUnitSubType_VoiceProcessingIO;


Пробуем собрать и видим ошибку. Дело в том, что по умолчанию наш аудиопоток работал на частоте 44100.0 hz, а я для работы kAudioUnitSubType_VoiceProcessingIO нужна более низкая частота.

Я поменял значение 44100.0 на 8000.0 — во всех файлах, но аудиопоток продолжал создаваться с частотой 44100.0. После парсинга информации на просторах интернета, я обнаружил, что у проекта Novocaine на github есть три «Pull request» от сторонних пользователей, и один из них имел описание:

Fixed Crash when launching from background while audio playing; Ability to manage Sample Rate


Скопировав все измененные строки из этого запроса, мне удалось запустить аудиопоток на частоте 8000.0 и эхоподавление работало! Задержка звука составляла 15-25 мс! Приложение работало в свернутом виде, даже с выключенным экраном на заблокированном iPhone!

Дело в том, что iOS не позволяет запускать новые звуки, когда приложение свернуто, для проверки можете запустить песню в Safari из ВК и свернуть браузер. Как только трек закончится, новый трек из плейлиста не включится до тех пор, пока вы не сделаете браузер активным! Если использовать аудиопотоки в iOS — то приложение отлично справится с задачей воспроизведения новых звуков из бэкграунда!

Как передается звук от устройства к устройству в приложении «Tusa»


На удаленном сервере я открываю с помощью python скрипта TCP порт 7878 и из iOS приложения создаю TCP соединение с этим сервером:

Затем, собрав звук в массив float — я конвертирую его в NSMutableData, выстраивая float в
цепочку по 4 байта:

NSMutableData * soundData = [NSMutableData dataWithCapacity:0];
for (int i=0; i < numFrames; ++i) 
{
    for (int iChannel = 0; iChannel < numChannels; ++iChannel) 
    {
        float theta = data[i*numChannels + iChannel];
        [soundData appendBytes:&theta length:sizeof(float)];
    }
}


Теперь звук находится в soundData, мы передаем его на сервер в формате:

LENGTH(soundData)+A+soundData

где A — байт-индификатор того, что на сервер пришел звук, LENGTH(soundData) — длина пакета (4 байта), soundData — сами данные в формате NSData.

Так же я попробовал шифровать весь аудиопоток по секретному ключу, объем трафика увеличился на 50-100% — но по производительности iOS девайсы справляются с этим на ура. Хотя для тех, кто пользуется 3G в условиях плохого приема – такой прирост интернет канала может оказаться неподъемным.

Самое неприятное, что весь проект изначально я реализовал на библиотеке Cocos2D, предназначенной для игр, и выяснилось, что VK SDK не работает с Cocos2D проектами, а поддерживает только ARC режим (Automatic Reference Counting), в котором происходит автоматическая работа с освобождением памяти. В одну из прошлых игр я тоже пытался встроить VK кнопку, но из-за ошибок пришлось заменить ее на Facebook кнопку. Надеюсь, что следующие версии VK SDK будут работать с Cocos2D, а пока мне пришлось переписать весь код на стандартные Storyboard интерфейсы, удалив все освобождения памяти "release" из кода. И если еще несколько дней назад я искал, где бы вставить «release» для того, чтобы избежать утечек памяти, то в ARC режиме вообще этой проблемы нет. Приложение стало занимать всего 10мб оперативной памяти, вместо 30мб на Cocos2D.

Примечание: Мне все-таки удалось «подружить» Storyboard интерфейсы с Cocos2D, и запустить Cocos2D игру прямо в UIViewController, причем Cocos2D запускается в ARC режиме, но это тема для отдельной статьи


Сомнительные инновации или UDP против TCP


Вместо привычного для VoIP протокола UDP — в качестве эксперимента я использовал протокол передачи данных TCP. При передаче звука по TCP, при каждой потере пакетов создается небольшая задержка (из-за повторной пересылки данных). В итоге, из-за нестабильного интернета у клиента во входящем плейлисте аудио сообщений иногда оказывается слишком много данных, длина входящего плейлиста начинает превышать несколько секунд, и что-то с этим нужно делать. Я решил попробовать исправить ситуацию следующим путем:
  • Если длина входящего плейлиста превышает 2 секунды — то я просто пропускаю «тихие» аудио слепки, вырезая молчание между фразами
  • Если длина входящего плейлиста превышает критический показатель, то я просто увеличиваю скорость аудио потока в 2 раза до тех пор, пока плейлист не будет удовлетворительной длины. В итоге, входящий голос в данной ситуации звучит «ускоренно».


Плюсы использования TCP — все пакеты и фразы будут гарантированно доставлены, и, если шифровать аудио пакеты, то не будет проблем с их расшифровкой. Так же не нужны дополнительные STUN и TURN серверы, через которые «проксируется» весь UDP траффик для обхода NAT (не стоит забывать, что почти все пользователи iOS не имеют внешнего IP), в случае TCP обмен происходит напрямую между сервером и клиентом.

Плюсы использования UDP — отсутствие задержек при потере пакетов, если пакеты теряются, то приложение это проигнорирует.

Итог: При потере пакетов при плохом интернет соединении нам в любом случае придется отказаться от части аудио-данных, в случае традиционного для VoIP UDP соединения — это будут произвольные аудио-данные, в том числе и аудио-дата с голосом, а в случае TCP соединения — мы можем выбрать от каких аудио-данных отказаться, и мы выбираем отсечение «тихих» аудио-данных — так мы компенсируем задержку.

Ну вот и все, если вам интересно следить за развитием проекта в рамках конкурса, предоставляю ссылку на VK страничку проекта: http://vk.com/id232953074

В данный момент одна из моих игр («Танчики Онлайн») попала на главную страницу App Store ( УРА! ), все мои сервера заполнились тысячами игроков, так что запуск «Тусы» пришлось отложить на несколько дней. Всю информацию о запуске, я выложу ВКонтакте.


Исходный код приложения Tusa я тоже постараюсь выложить ВКонтакте, как только придам ему более оптимизированный вид.

В комментариях к статье я хотел бы услышать альтернативные бесплатные(!) варианты передачи аудиопотока в iOS через собственные сервера(!), которые можно было бы использовать для передачи звука ВКонтакте.

Так же в комментариях пишите, есть ли аналоги приложения Tusa в App Store ( Помимо платного «Тимспик» ).

Так как конкурс VK был затеян для расширения возможностей клиента ВК, и мое приложение демонстрирует, как добавить звонки в ВК, то прошу принять участие в голосовании, нужны ли аудио/видео звонки в мобильном клиенте ВК по-Вашему мнению? Голосование не сыграет ключевую роль в принятии решения, но разработчики ВК точно заметят его, ведь в Facebook звонки уже есть :)
Нужны ли звонки в мобильном клиенте ВКонтакте:

Проголосовало 154 человека. Воздержалось 25 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Комментарии (35)


  1. CRivlaldo
    26.03.2016 19:54
    +1

    Почему бы не использовать pjsip для мобильного приложения? Под iOS собирается на ура. Есть демонстрационное приложение в пару сотен строк на pjsua (high-level api). На борту уже несколько кодеков, не нужно гонять wav. VoIP сервер Kamalio поднимается за пол часа. И можно звонить.
    Вот тут можно почитать как это всё делается по шагам.


    1. megabraza
      26.03.2016 21:18

      К сожалению по вашему линку устаревшая инструкция по сборке, в xcode 7.3 — pjsip не собирается на iOS, огромное количество ошибок. Часть ошибок я попробовал исправить, но потратив 10 минут, скомпилировать на iOS мне не удалось. Возможно, потратив больше времени, можно собрать pjsip на iOS 9, если у вас есть данный опыт, поделитесь готовым архивом с проектом. Тут еще дело в том, что мой сервер, написанный с нуля, помимо распределения аудио данных, рассылает IN-APP нотификации и синхронизирует информацию о VK друзьях с базой данных. В принципе можно модифицировать Kamailio сервер, но не уверен, что это тривиальная задача.


    1. selenite
      26.03.2016 21:18
      +1

      Можно сразу WebRTC из кода хромиума взять, ага-ага.

      Про задержку, задержка не проблема, проблема — синхронизация. В гугл — за decoding time stamp (DTS), и за presentation time stamp (PTS).


      1. megabraza
        26.03.2016 21:38

        Как я понимаю, "временный штампы" нужны для синхронизации видео потока с аудио потоком и субтитрами, а так же для обнаружения пропущенных семплов.
        Если вы посмотрите на код активации Callback в статье, вы увидите эту метку — AudioTimeStamp * inTimeStamp:

        #pragma mark ======== AudioUnit recording callback =========
        static OSStatus PushCurrentInputBufferIntoAudioUnit(void * inRefCon,
            AudioUnitRenderActionFlags * ioActionFlags,
            const AudioTimeStamp * inTimeStamp,
            UInt32 inBusNumber,
            UInt32 inNumberFrames,
            AudioBufferList * ioData)
        {
            //inTimeStamp - это и есть наша временная метка
        } 

        struct AudioTimeStamp {
            Float64 mSampleTime;   // временная метка в количестве семплов
            UInt64  mHostTime;   // временная метка в абсолютном системном времени
            // ...
            UInt32 mFlags;   // маска, указывающая на то, какие из полей заполнены
         };


  1. fleaump
    26.03.2016 20:55
    +2

    Переводи на UDP и еще немного выжмешь задержку


    1. megabraza
      26.03.2016 21:25

      Абсолютно верный и уместный комментарий, я планировал на сервере открыть 2 порта, один TCP: для авторизации и обменом ВК информацией, второй UDP: для пересылки аудио пакетов. Возможно, попробую UDP позже.


  1. aylarov
    26.03.2016 21:48

    Возьмите voximplant и будет вам и UDP, и нормальный звук и даже видео :)


    1. megabraza
      26.03.2016 22:21
      -1

      Платные решения я не рассматриваю по трем причинам (помимо той, что большинство этих решений написано на том же Core Audio):

      • Есть в наличии свои сервера.
      • Я планирую сделать приватные чаты с шифрованием по паролю. Использовать сторонние сервера — удар по приватности пользователей.
      • Работая со звуком на «низком уровне», есть возможность создавать голосовые фильтры, модифицировать голос, что я и хочу сделать в дальнейших обновлениях.
      Добавить поддержку видео в мое приложение на данном этапе — довольно просто, но тема для отдельной статьи.


      1. aylarov
        26.03.2016 22:25
        -4

        Из вашей статьи видно, что знаний по работе с real-time у вас почти нет, то же самое кодирование звука mp3 говорит о многом, как и передача данных по TCP. Voximplant поддерживает бесплатные P2P звонки, это к вопросу про приватность, бесплатность и прочие ваши требования. Не хотите Voximplant, который уже решил за вас большинство проблем, погуглите хотя бы про WebRTC.


        1. megabraza
          26.03.2016 22:57

          Я понимаю, что, как сотрудник Voximplant, вы пытаетесь рекламировать свой продукт, возможно это и правильно, но не понимаю, как iOS пользователи без внешнего IP могут приватно совершать P2P звонки, не пропуская весь трафик через ваши сервера. Я понимаю, что большинство VoIP работают на UDP, но мне хотелось провести эксперимент с гарантированной доставкой пакетов, поэтому я попробовал использовать TCP для гарантированной передачи зашифрованных пакетов, чтобы принимающая сторона могла их гарантированно расшифровать.


          1. aylarov
            27.03.2016 09:32
            -5

            но не понимаю, как iOS пользователи без внешнего IP могут приватно совершать P2P звонки, не пропуская весь трафик через ваши сервера.

            мда, все еще хуже чем я думал, удачи вам)


  1. StrangerInRed
    26.03.2016 22:02
    +3

    Орнул с передачи пакетов по TCP. Гугли RTP, SIP.


    1. Obramko
      27.03.2016 01:16
      +2

      Зря минусите человека. Действительно — UDP. И действительно брать RTP. А собственно SIP — это как раз то, что автор хочет гонять по TCP, т.е. всякая разная авторизация и прочие не-голосовые данные.


      1. megabraza
        27.03.2016 02:33
        -1

        Все верно, в интернете есть примеры iOS RTP проектов, я так же пытался собрать и XMPP проект, но при выходе новых версий iOS — эти проекты у меня просто перестали компилироваться. Причем в старых версиях XCODE эти проекты собирались, но Apple запретил нам пользоваться старыми версиями XCODE, скаченными со сторонних ресурсов, под угрозой исключения приложений из App Store. Лично у меня не хватило терпения разобраться со всеми ошибками компилятора, если у вас есть рабочие iOS примеры — поделитесь с нами, пожалуйста.


        1. eugeneego
          27.03.2016 15:39
          +1

          Старые версии Xcode всегда можно скачать с дев портала эппл developer.apple.com/downloads


          1. megabraza
            27.03.2016 15:58

            Действительно, спасибо за ссылку


    1. megabraza
      27.03.2016 15:48

      Спасибо, я к сожалению в SIP мало понимаю, но постараюсь разобраться. Получается такая ситуация: у нас есть два iPhone, без выделенных IP, которые нужно соединить по UDP протоколу. Открыть UDP порт для принятия данных телефон не может. Каждый из них сначала опросит STUN сервер, чтобы узнать, что у него нет выделенного IP, хотя нужно ли это действие, когда и так понятно, что выделенного IP нету? Затем каждый из iPhone создаст постоянное TCP(!) соединение со своим TURN сервером, для того, чтобы иметь возможность через TURN сервер принимать (проксировать) UDP данные. В итоге, для того, чтобы соединить два телефона, у нас висит 2 TCP соединения, 2 UDP соединения (от клиентов на TURN сервер противоположного iPhone), да и еще коммуникация между TURN серверами + первоначальный опрос STUN серверов? И для чего такая сложная топология нужна — только для того, чтобы при потере пакетов не заморачиваться на повторную отправку данных? Верно я понимаю данную схему или допустил ошибку? Буду благодарен, если объясните.


      1. Tirael78
        27.03.2016 20:08
        +1

        даже заимев STUN проблемы не решаются.
        начинаются более интересные вещи:

        односторонняя слышимость
        симметричный NAT
        в некоторых случаях ни как без проксирования (а значит нужен и media proxy)

        Проблемы с проброской канала — это основная сложность, с которой борются в voip ip.
        без решения как минимум этих проблем — это сферический конь в вакуме


      1. Yan169
        27.03.2016 22:28
        +1

        Неверно.
        Клиент отправляет UDP-пакет на STUN-сервер, NAT открывает внешний порт, по которому клиент сможет получать входящие UDP-пакеты, STUN-сервер видит этот порт, а также адрес NAT, и отправляет их другому клиенту, который на этот адрес и порт начинает транслировать разговор по UDP. На пальцах так, подробнее в RFC, Wiki и Google. Схема работает не для всех видов NAT, поэтому всегда предусматривают TURN-сервер как запасной вариант. Где-то видел статистику, что p2p работает для ~90% (точно не помню, но порядок такой), остальное проксируется через TURN.
        P.S. Шифровать без проблем можно и по UDP, см. хотя бы DTLS.


        1. Tirael78
          27.03.2016 22:40
          +1

          когда с обоих сторон будет симметричный NAT — только проксирование.
          насчет p2p, вы, очевидно, путаете его с децентрализованой сетью
          p2p это просто одноранговая сеть, для тех показателей, о которых вы говорите, такая сеть нуждается в координаторах, которыми могут являться и сами клиенты, но такая сеть уже минимум являться гибридной


          1. Yan169
            27.03.2016 22:48
            +1

            Согласен с Вашим комментарием, он уточняет мой упрощенный посыл, в каком направлении копать. :)


      1. StrangerInRed
        28.03.2016 09:43
        +1

        SIP и RTP не относятся к методу резолвинга NAT. Для этого как вы и написали есть TURN, STUN. RTP хорош как идея, но когда я педалил нечто похожее (2012) то никаких достойных реализация протокола / аналогов небыло. Не знаю как сейчас, но скорее всего прийдется либо реализовывать стандарты самому либо поливелосипедить. Но по уменьшению потере пакетов и задежке оно будет того стоить. (Если велосипедить то возьмите pcap и впилите свой протокол транспортного уровня чисто поверх IP (так у вас отпадет ненужная прослойка в виде UDP))


  1. zemavo
    27.03.2016 17:15
    +1

    " Так почему бы не сделать приложение, в котором можно узнать, сколько человек меня разделяют в VK, например, с Павлом Дуровым?" — но ведь именно такое давно уже в разных приложениях?


    1. megabraza
      27.03.2016 17:17

      Скиньте ссылку, пожалуйста, или название приложения, где это можно сделать.


      1. zemavo
        27.03.2016 17:25
        +1

        https://vk.com/app662272 — раньше например тут такое делали


        1. megabraza
          27.03.2016 18:39

          Спасибо за ссылку, поигрался с приложением, работает :)


          1. zemavo
            27.03.2016 18:40
            +1

            Не за что


  1. alekssamos
    27.03.2016 18:23

    Дело в том, что iOS не позволяет запускать новые звуки, когда приложение свернуто, для проверки можете запустить песню в Safari из ВК и свернуть браузер. Как только трек закончится, новый трек из плейлиста не включится до тех пор, пока вы не сделаете браузер активным!

    Извините, что немного не по теме пишу, но как можно сделать, чтобы вот как раз в этом Safari песни переключались? А то я когда слушаю аудиозаписи через ВК мне приходится всегда держать экран включенным и браузер открытым на этой страничке с музыкой, а если проходит более 30 минут, то тогда если экран всё ещё включен, айфон начинает нагреваться и зарядка быстрее уходит, и ещё есть вираятность случайно куда-то нажать…


  1. alexanderMykhailenko
    28.03.2016 11:46

    А что используется из вконтакте АПИ? Логин и получение друзей?


    1. megabraza
      28.03.2016 11:47

      Да, только публичная информация, ФИО и список друзей.


      1. alexanderMykhailenko
        28.03.2016 11:57
        +1

        А Вам не кажется что этого маловато для участия в конкурсе Vkontakte? Вы используете только два общедоступных метода, а как же комментарии, лайки, репосты и тп. Или участие в конкурсе не первостепенная причина для написания приложения?


        1. megabraza
          28.03.2016 12:02
          -1

          Первостепенная, в идеале хотелось бы, чтобы Core Audio стало частью новых общественных методов и аудио звонки вошли бы в состав стандартного VK SDK