Как написать своё VoIP-приложение с работой в фоне под Windows Phone

    В этой статье я бы хотел рассказать о том, как в минимум усилий написать своё простое VoIP-приложение с бэкэндом и работой в фоне на платформе Windows Phone 8.

    До выхода Windows Phone 8 пользователей voip-приложений очень разочаровывала работа в фоне, которая, собственно, практически отсутствовала — максимум из того, что могли сделать разработчики, чтобы показать пользователю входящий звонок пока приложение в бэкграунде — это показать toast notification, который слабозаметен, еле слышен и быстро исчезает. С одной стороны, это не позволяло поедать батарейку как если бы приложение работало полноценно в фоне, но с другой — делало его малополезным инструментом. До выхода WP8, Microsoft подогревала интерес публики к новой версии платформы обещаниями интегрировать Skype в операционную систему и работу в фоне. Что ж, обещания они свои выполнили — теперь стало возможно:

    • инициировать звонок на Skype через контактную книгу телефона
    • продолжать разговор по Skype даже если вы целенаправленно или случайно свернете приложение (раньше если при разговоре вы случайно заденете кнопку поиска — разговор обрывался)
    • и самое интересное: принимать входящие звонки с интерфейсом а-ля обычный gsm-звонок в условиях когда Skype не запущен (не в foreground) и более того — он в фоне ничего не делает (не поедает батарейку)

    Microsoft не стало делать это эксклюзивными возможностями (кроме интеграции в контактную книгу) для своего продукта и открыло API, что дает возможность сторонним разработчикам реализовывать такие же сценарии, не будучи при этом привилегированным партнером (как было в WP7 с native sdk). И хотя так же красиво интегрироваться в контактную книгу не получится — можно воспользоваться ContactStore и Protocol handlers, чтобы изменить в контакте поле URL и сделать открытие приложение по клику).

    В конце статьи приложены исходники двух проектов: один из них пример Microsoft Chatterbox, в котором объясняется, как работают бэкграунд процессы с симуляцией бэк-энда с входящими звонками и даже с видео; второй — мой проект с простым бэкэндом, который позволяет общаться по voip на двух устройствах и использует voip push notifications, но обо всем по порядку.



    Архитектура VoIP приложения с работой в фоне


    Если вы задались целью написать полноценное voip приложение, то к сожалению (или к счастью) вам не обойтись без native компонента на C++ (потому как нормальное API для работы с аудио-девайсами не доступно из managed части) Если кратко, то voip — приложение, которое умеет работать в фоне, должно состоять из двух процессов:

    • Foreground — собственно обычный процесс, в котором «бежит» интерфейс приложения
    • Background — второй процесс, который, по сути, состоит из четырех агентов:
      • VoipHttpIncomingCallTask — запускается когда к нам приходит входящий звонок по пуш каналу (особый вид push уведомлений, будет описан ниже).
      • VoipForegroundLifetimeAgent – запускается когда наше приложение становится активным и работает до тех пор, пока приложение не свернули или закрыли.
      • VoipCallInProgressAgent – Запускается при звонке сигнализируя о том, что процессу выделено больше ресурсов процессора для поддержки звонка. Таким образом (де)кодирование видео и аудио надо начинать после этого события.
      • VoipKeepAliveTask – запускается периодически каждые 6 часов. По сути, он нужен для того, чтобы периодически напоминать вашему серверу, что приложение всё ещё установлено на телефоне
    • Out-of-process — межпроцессный компонент, призванный решить проблему коммуникации между первыми двумя. На самом деле это всё тот же второй процесс.

    Графически это выглядит так:

    image

    Как же написать своё VoIP приложение?


    Начнем по порядку:

    1. Транспорт


    Сперва разберемся с уровнем транспорта наших данных. Конечно, это очень простой пример, который я соорудил за день, поэтому никаких мегаумных штук тут не будет — вы сами понимаете: мало написать транспорт, запись и воспроизведение аудио — надо ещё чтобы это работало быстро без задержек даже на слабых каналах связи — но это тема ни одной книги. И так, для транспорта мы будем использовать очень удобный класс из нового API — DatagramSocket (он прост и работает по UDP что логичнее аудио\видео потоками (нам же не надо ожидать подтверждения доставки каждого аудио пакета, правда?). Благодаря async\await работа с ним очень проста:

        const string host = "192.168.1.12";
        const string port = "12398";
        var socket = new DatagramSocket();
        socket.MessageReceived += (s, e) =>
            {
                //читаем входящие данные в строку
                var reader = e.GetDataReader();
                string message = reader.ReadString(reader.UnconsumedBufferLength);                     
            };
        await socket.BindServiceNameAsync(host);
        var stream = await socket.GetOutputStreamAsync(new HostName(host), port);
        var dataWriter = new DataWriter(stream);
        //отправляем строку на удаленный хост
        dataWriter.WriteString("Hello!");
        await dataWriter.StoreAsync(); //передает данные в системный буфер ОС для отправки
    

    я настолько привык к async\await, что воспользовался этим же классом и для серверной части (о том, как использовать WinRT API в десктопном дотнете смотрите тут). Проткол тоже очень прост: COMMAND!BODY – для нашего примера хватит.

    2. Запись голоса


    В Managed части для записи данных с микрофона есть два класса:

    • XNA Microphone
    • AudioVideoCaptureDevice

    В нашем примере мы будем использовать первый (он доступен ещё с WP7) так как лично я не смог разобраться в том, как проиграть аудио со второго без использования native api), но, понятное дело, для реализации серьезного voip приложения вам придется использовать второй способ (StartRecordingToSinkAsync, который отдает чистый несжатый поток данных с микрофона). И так, запись данных с микрофона организована всего парой строк:

        _microphone = Microphone.Default;
        _microphone.BufferDuration = TimeSpan.FromMilliseconds(500);
        _microphoneBuffer = new byte[_microphone.GetSampleSizeInBytes(_microphone.BufferDuration)];
        _microphone.BufferReady += (s, e) =>
            {
                _microphone.GetData(_microphoneBuffer);
                //отправка байтов на сервер
            };
        _microphone.Start();
    

    3. Проигрывание аудио


    В нашем примере мы будем использовать очень неоптимальный, но рабочий и небольшой код:

        _soundEffect = new SoundEffect(e.Data, _microphone.SampleRate, AudioChannels.Mono);
        _soundEffect.Play();
    

    К сожалению, альтернатив нет и через managed часть нет возможности проигрывать аудио на динамике для звонков, а только на спикере, поэтому возможно появление эхо и прочих шумов (это же простой пример).

    4. VoIP push уведомления


    Киллер-фичей нашего примера будет является то, что если вы установите это приложение на два девайса — вы сможете звонить через приложение на другой девайс без необходимости нахождения в foreground приложения на том девайсе. Сперва необходимо зарегистрировать Push URI для обоих девайсов на сервере вместе с каким-нибудь идентификатором пользователя (в Skype это произвольное имя, в Viber — номер телефона пользователя). Затем, когда девайс А захочет позвонить девайсу Б — он отправит на сервере команду, сервер найдет push uri для девайса Б и отправит на MPNS xml c некоторыми данными о звонящем с обязательным условием наличия в хедере запроса X-NotificationClass=4. До выхода WP8 было всего три класса Push notifications
    • Tile
    • Raw
    • Toast

    но как видите, с WP8 добавился новый четвертый класс — VoIP. MPNS по своим каналам отправляет этот пакет на клиента и поднимает специально запущенный для этих целей ScheduledTaskAgent. Если этот агент правильно отработает — пользователю отобразится экран входящего звонка (аналогичный обычному GSM-звонку). И так, что же должен сделать ScheduledTaskAgent?

        var incomingCallTask = task as VoipHttpIncomingCallTask;
        if (incomingCallTask != null)
        {
            //десериализуем XML с номером и именем звонящего
            Notification pushNotification;
            using (var ms = new MemoryStream(incomingCallTask.MessageBody))
            {
                var xs = new XmlSerializer(typeof(Notification));
                pushNotification = (Notification)xs.Deserialize(ms);
            }
    
            VoipPhoneCall callObj;
            var callCoordinator = VoipCallCoordinator.GetDefault();
                    
            //запрос на отображения gsm-call-like интерфейса
            callCoordinator.RequestNewIncomingCall("/MainPage.xaml?incomingCall=" + pushNotification.Number,
                pushNotification.Name, pushNotification.Number, new Uri(defaultContactImageUri), "Voip.Client.Phone", 
                new Uri(appLogoUri), "Я VoIP-push!", new Uri(logoUrl), VoipCallMedia.Audio, 
                TimeSpan.FromMinutes(5), out callObj);
    
            callObj.AnswerRequested += (s, e) =>
                {
                    s.NotifyCallActive(); //запустит наше приложение
                    //далее небольшой воркэрунд для примера:
                    //как я писал выше, у нас нет возможности проигрывать
                    //аудио на внутреннем динамике используя managed code, а NotifyCallActive включает
                    //именно его без возможности проигрывать звуки на внешнем,
                    //так что таким способом мы отключаем внутренний, и включаем внешний
                    await Task.Delay(3000);
                    s.NotifyCallEnded();                    
                };
            callObj.RejectRequested += (s, e) => s.NotifyCallEnded();
        }
    

    Стоит заметить, что VoIP-пуши в отличие от всех остальных видов, могут прилетать как в открытое приложение, так и если оно закрыто — Skype принимает входящие звонки только через пуши даже если он в данный момент в foreground — на самом деле спорное решение, т.к. voip пуши иногда тормозят. Увы, в нашем примере мы не сможем поднять разговор, если voip пуш прилетит когда приложение запущено — у нас в нашем примере нет нативного межпроцессного компонента, чтобы сообщить основному процессу об этом (и да, OnNavigatedTo,From не сработают при появлении UI входящего звонка, хотя возможно будет вызов события Obscured у фрейма, но мы не сможем достать номер звонящего) — поэтому в моем примере при звонке принимающая сторона должна выйти из приложения, чтобы корректно подхватить разговор.

    Заключение


    Всего это хватило, чтобы написать простое VoIP-приложение за день. Увы, оно умеет разговаривать только через спикер, не умеет выключать экран при поднесении к уху (proximity sensor) и продолжать разговор, если приложение сворачивается — для всего этого необходим native компонент, который очень подробно описан в примере Microsoft Chatterbox — мой же пример попроще, но зато с серверной частью. Изначально я хотел рассказать только о VoIP-пушах, но получилось немного больше. Конечно, для реализации полноценных VoIP-приложений лучше смотреть в сторону быстро развивающегося WebRTC, который, к слову, уже официально работает в хроме в Android, но, надеюсь, мой пример окажется кому-нибудь полезным.

    Исходники:
    Viber
    Company
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 9

      +2
      А телефончик со скриншота рабочий?
        +2
        Пример замечательный. Жаль попробовать не получилось. Виндовс фоун 8 сдк не хотят ставиться…
        И если не затруднит, можете продолжить эту тему на несколько статей, что б можно было "«подучить» более широко написание приложений под ВФ8.
          0
          Спасибо! Вообще можете глянуть по блогу «разработка под Windows Phone» — там есть несколько полезных статей. Касательно WP8 SDK — я так понимаю вы не под Windows 8?
            0
            Лицензионная Windows 8 Pro x64,
            Подключение к интернету нормальное. Все что дополнительно нужно — ставится в фоновом режиме без проблем.
            Зарегистрирован на майкрософте.
            Правда визуалка ломанная. Зато версия Алтимейт 2012. Попробую поставить Экспрес с оф сайта.
            Просто не дождусь когда приедет моя новенькая нокиа люмия с ВФ8 — уже куча идей приложений…
              +1
              Без годовой платной подписки вы не сможете дебажить приложения на телефоне (правда стоит она не дорого и студентам бесплатно)
                0
                Я знаю.
                Но у меня сам WP8 SDK криво ставится. Проверял на 2 машинах…
          +1
          Что-то я Skype тестировал на предмет входящего вызова и не заработало.
            0
            Вопрос снимается. Скайп работает как нужно, просто нужно иногда заходить в скайп для обновления токена.
            –5
            Странно, уже 3 коммента, и никто не спросил «что за баба?» :-D

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