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

в 10:46, , рубрики: Viber, voip, windows phone 8, Блог компании Viber, разработка, разработка под 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, но обо всем по порядку.

image

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

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

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

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

image

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

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

1. Транспорт

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

    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(); //передает данные в системный буфер ОС для отправки

я настолько привык к asyncawait, что воспользовался этим же классом и для серверной части (о том, как использовать 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, но, надеюсь, мой пример окажется кому-нибудь полезным.

Исходники:

Автор: Nagg

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js