На днях российские пользователи, заказавшие приставку Apple TV на официальном сайте, наконец начали получать долгожданные девайсы.

Как известно, в новом поколении приставок дебютирует магазин App Store с приложениями сторонних разработчиков. Первые российские обзоры с печалью отмечали, что в отечественном сторе пока не очень много приложений (и это надо как-то исправить!). Однако, среди них есть приложение Rutube. В этой статье мы поделимся небольшим опытом, который успели приобрести igor-petrov и Juraldinio за время его разработки.

О платформе tvOS


Платформа нового Apple TV (tvOS) является деривативом iOS и обладает некоторыми уникальными особенностями. В частности, для tvOS возможно как портирование существующего iOS-приложения, написанного на Objective-C / Swift (согласно рекомендациям по реализации интерфейса), так и создание приложения на новом стеке технологий — TVML и TVJS, подключаемых с помощью фреймворка TVMLKit. Этот новый стек позиционируется как хороший выбор для простой и быстрой разработки клиент-серверных приложений (идеально для видео!), что и подтверждается на практике. Именно о TVML и TVJS пойдет речь ниже.

TVMLKit


TVMLKit — фреймворк для Objective-C / Swift. С его помощью управление приложением передается JavaScript-файлу и среде TVML+TVJS. Подробности доступны на странице документации TVMLKit.

TVML


TVML (Television Markup Language) — это подмножество XML, реализующее разметку и стандартные элементы интерфейса tvOS. Этот интерфейс используется не только для приложений — на нем реализованы все страницы tvOS: домашний экран, экран настроек, и сам магазин App Store.

Например, так выглядит кнопка в TVML:

<button>
  <text>Hello world!</text>
</button>

А так кнопка выглядит на экране:



А так выглядит домашний экран устройства, состоящий из элементов TVML:


tvOS предоставляет набор готовых элементов, полный список которых доступен в документации к TVML.

Эти элементы делятся на:

  • Simple elements — простые элементы, состоящие из одного тега, например, text или title;
  • Compound elements — составные элементы, которые могут включать в себя простые элементы. Например, составной элемент banner может содержать в себе простой элемент title;
  • Templates — шаблоны отображения страницы. Задают общую структуру отображения элементов и могут включать в себя составные элементы.

Кроме того, элементам можно задавать атрибуты и стили, например width и src.

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

Из общего списка шаблонов стоит выделить divTemplate, который позволяет конструировать собственный layout, если вас не устроил никакой другой из предложенных.

Возможности создавать собственные элементы нет, можно только комбинировать существующие.

Более того, нужно следить за тем чтобы при парсинге строк в XML не попадался невалидный TVML. Например, ответ нашего API мог содержать HTML-теги, из-за чего возникала ошибка.

Разметка TVML может динамически добавляться на страницу с помощью JavaScript и TVJS.

TVJS


TVJS — JavaScript-фреймворк, предоставляющий API для работы с TVML. Условно, его можно разделить на две части:

  • Стандартный DOM Level 3 API для работы с DOM-элементами, например, конструктор DOMParser для парсинга строки в XML-документ (TVML);
  • Собственные методы и конструкторы tvOS, например, объект NavigationDocument, который управляет стеком отрисованных страниц и используется в качестве навигации по истории.

Каждый из собственных методов описан в документации к TVJS. В остальном это привычный JavaScript. Частично поддерживается ECMAScript 2015, например, классы и template strings.

Не поддерживаются arrow functions, константы. При этом может наблюдаться неработоспосбность некоторых сторонних библиотек. Например, пришлось перебрать несколько npm-пакетов, чтобы найти работающую библиотеку для AJAX-запросов (из-за обращений к window.XMLHttpRequest, который в tvOS реализован не как свойство window). Конечно, было бы проще самостоятельно написать несколько строк кода, но в какой-то момент это стало делом принципа — найти работающий из коробки плагин, и он существует!

Отладка


Для отладки JavaScript-кода можно использовать Safari. Активируем меню «Develop» (Preferences -> Advanced -> Show Develop menu), подключаем Apple TV через USB. Заходим в браузер и, запустив приложение на приставке, выбираем в появившемся выпадающем меню «Develop» девайс «Apple TV», далее выбираем наше приложение.

Hello world и запуск видеоролика


Далее небольшой пример того, как может выглядеть приложение для tvOS.

Для начала необходимо реализовать загрузку JavaScript-файла. Задача сводится к созданию объекта класса TVApplicationController, предоставляющего интерфейс для использования TVML:

Показать код
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
  // объект window
  window = UIWindow(frame: UIScreen.mainScreen().bounds)

  // контекст, необходимый для инициализации объекта класса TVApplicationController
  let appContollerContext = TVApplicationControllerContext()

  #if DEBUG
  // для отладки можно использовать любой хост
  let tvBaseUrl = "http://192.168.xx.xx/"
  let tvBootUrl = "\(tvBaseUrl)js/main.js"
  guard let javaScriptUrl = NSURL(string: tvBootUrl) else {
    fatalError("Unable create js application url")
  }

  // ссылка на js-файл, которому необходимо передать управление
  appContollerContext.javaScriptApplicationURL = javaScriptUrl
  // указываем хост, используемый как BASE для подгрузки ресурсов
  appContollerContext.launchOptions["BASEURL"] = tvBaseUrl;
  #else
  // в боевой версии js-файл подключается в bundle приложения
  appContollerContext.javaScriptApplicationURL = NSBundle.mainBundle().URLForResource("application", withExtension:"js")!
  #endif
  // создаем объект и передаем управление js-файлу
  appController = TVApplicationController(context:appContollerContext, window: window, delegate: self)
    return true
}


После того, как управление перешло к JavaScript-файлу, можно приступать к реализации логики. Содержимое main.js:

App.onLaunch = function (options) {
  console.log('Hello World!');
}

Здесь демонстрируется метод TVJS App. onLauch — входная точка приложения.

Попробуем сделать кое-что посложнее:

App.onLaunch = function () {
  var alertString, alertXML, parser;

  // текстовый шаблон TVML-документа
  alertString = [
    '<document>',
      '<alertTemplate>',
        '<title>Hello World!</title>',
        '<button>',
          '<text>OK</text>',
        '</button>',
      '</alertTemplate>',
    '</document>'
  ].join('');

  // создаем экземпляр DOMParser
  parser = new DOMParser();
  
  // парсим XML-документ из шаблона
  alertXML = parser.parseFromString(alertString, 'application/xml');

  // выводим XML-документ на страницу
  navigationDocument.pushDocument(alertXML);
}

В этом примере демонстрируется конструктор DOMParser, с помощью которого строка преобразуется в XML-документ.

Используя метод TVJS navigationDocument.pushDocument(), можно отобразить получившийся документ на странице. При этом новая страница будет записана в массив navigationDocument.documents, который можно использовать для навигации по истории. В данном случае при запуске приложения на странице будет присутствовать надпись «Hello world» и кнопка «OK».

Перед вставкой XML-документа на страницу можно добавить обработчик пользовательского события:

...
// добавляем обработчик выбора элемента (клик на пульте)
alertXML.addEventListener('select', function () {
  var player, mediaItem;

  // создаем экземпляр MediaItem
  mediaItem = new MediaItem('video', 'http://example.org/path/to.m3u8');

  // создаем экземпляр плеера
  player = new Player();

  // создаем экземпляр Playlist
  player.playlist = new Playlist();

  // добавляем mediaItem в playlist
  player.playlist.push(mediaItem);

  // делаем плеер видимым
  player.present();

  // запускаем проигрывание
  player.play();
});
...

В получившемся примере на элемент XML-документа — кнопку — вешается событие пользовательского выбора, а в коллбеке с помощью методов TVJS создается и запускается видеоплеер. Теперь при нажатии на кнопку «ОК» будет запускаться проигрывание видео.

Код примера целиком
App.onLaunch = function () {
  var alertString, alertXML, parser;

  // текстовый шаблон TVML-документа
  alertString = [
    '<document>',
    '<alertTemplate>',
    '<title>Hello World!</title>',
    '<button>',
    '<text>OK</text>',
    '</button>',
    '</alertTemplate>',
    '</document>'
  ].join('');

  // создаем экземпляр DOMParser
  parser = new DOMParser();

  // парсим XML-документ из шаблона
  alertXML = parser.parseFromString(alertString, 'application/xml');

  // добавляем обработчик выбора элемента (клик на пульте)
  alertXML.addEventListener('select', function () {
    var player, mediaItem;

    // создаем экземпляр MediaItem
    mediaItem = new MediaItem('video', 'http://example.org/path/to.m3u8');

    // создаем экземпляр плеера
    player = new Player();

    // создаем экземпляр Playlist
    player.playlist = new Playlist();

    // добавляем mediaItem в свойство playlist плеера
    player.playlist.push(mediaItem);

    // делаем плеер видимым
    player.present();

    // запускаем проигрывание
    player.play();
  });

  // выводим XML-документ на страницу
  navigationDocument.pushDocument(alertXML);
};


Конечно, это совсем простой пример, но, он вполне отражает общий принцип работы с TVML и TVJS и показывает, что это решение — не какой-то Дивный Зверь, а близкая к классическому фронтэнду технология.

В заключение


В заключение пара слов о том, как на данный момент реализовано приложение Rutube.

Код написан на CoffeeScript, в качестве модульного загрузчика используется Browserify. Для получения данных используется XMLHttpRequest и открытый API Rutube. Для доставки видео используется протокол HLS.

Полюбуйтесь на главный экран приложения


В целом, разработка на TVML и TVJS понравилась — быстро и эффективно. Приложение хоть и создается в стандартизированном интерфейсе, но этот интерфейс выглядит весьма эффектно. При этом экономится время на верстке и создании визуальных эффектов, и не приходится задумываться об их производительности.

Из минусов хотелось бы отметить ограничения в дизайне. Ну и реализация поиска (текстового ввода) показалась неоднозначной. Даже не знаем, стоит ли его без Siri добавлять в приложение?

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

В будущем ожидается выход плагина для React, который пока (ноябрь 2015) находится в статусе «very alpha».

А пока надо отметить, что за прошедшую с релиза неделю, не напрягаясь, «за чашкой чая», добавили в наше приложение некоторое количество приятных глазу «фишечек» и готовим обновление.

Полезные ссылки


Документация по TVMLKit
Документация по TVJS
Документация по TVML
Beginning tvOS Development with TVML Tutorial

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


  1. pepelsbey
    09.11.2015 13:28
    -1

    Платформа нового Apple TV является деривативом iOS

    Кто-нибудь кроме вас здесь знает, что такое дериватив?


    1. PavelPV
      09.11.2015 13:40
      +3

      Не посчитайте оскорблением, но если ваше образование или память подводят, то всегда есть Google.


  1. ad1Dima
    09.11.2015 14:56

    Хорошо, что к 2015 году Apple разродилась более-менее нормальным ML.

    Но что-то он местами все равно выглядит странно. Зачем нужен тег внутри Button, например? Если это какой-то объект UI (с точки зрения системы), то можно было бы и опускать его, как в том же XAML.


    1. igor-petrov
      09.11.2015 15:22

      Логика в том, что тексту можно задавать определенные атрибуты. Если бы разрешили опускать text, то пришлось бы разрешить вешать эти атрибуты на button и другие элементы.


      1. ad1Dima
        09.11.2015 15:48

        >то пришлось бы разрешить вешать эти атрибуты на button и другие элементы.
        Или писать тег text, если хотите задать атрибуты.
        Если аттрибуты текста нужны в подавляющем большинстве случаев — вы правы, как есть лучший вариант. А если хотя бы в половине случаев они не нужны, то почему бы и не опускать тег text без атрибутов?


        1. igor-petrov
          09.11.2015 16:12

          Да, соглашусь, это было бы удобней, но видимо они посчитали что в этом слишком много вольностей.


          1. ad1Dima
            09.11.2015 16:19
            +1

            ну, не plist и на том спасибо.


  1. asave
    09.11.2015 14:58
    +1

    SmartTV зверинец пополнился очередным жывотным. Ура!