Пишемо VoIP iOS чат на CORE AUDIO для конкурсу VK Mobile Challenge

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

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 playing audio; 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 дзвінки вже є :)

Потрібні дзвінки в мобільному клієнті ВКонтакте:

/>
/>


<input type=«radio» id=«vv72003»
class=«radio js-field-data»
name=«variant[]»
value=«72003» />
Потрібні
<input type=«radio» id=«vv72005»
class=«radio js-field-data»
name=«variant[]»
value=«72005» />
Не потрібні
<input type=«radio» id=«vv72007»
class=«radio js-field-data»
name=«variant[]»
value=«72007» />
Я не користуюся клієнтом ВК

Проголосував 81 людина. Утрималося 16 осіб.


Тільки зареєстровані користувачі можуть брати участь в опитуванні. Увійдіть, будь ласка.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.