Pull to refresh
2677.95
RUVDS.com
VDS/VPS-хостинг. Скидка 15% по коду HABR15

Стриминг видео с помощью Akka Streams

Reading time10 min
Views4.7K
Original author: Bartłomiej Żyliński
Автор статьи, перевод которой мы сегодня публикуем, говорит, что стриминг видео не должен быть такой задачей, с которой у кого-либо возникают сложности. Всё дело — в правильном подборе инструментов, среди которых можно отметить пакет Akka Streams. Использование этого пакета позволяет эффективно разрабатывать приложения для потоковой передачи видео.



Правда, не следует думать, что то, о чём мы будем тут говорить, подобно простому примеру, вроде println(«Hello world»), в котором используется система акторов Akka. Сегодня вы узнаете о том, как создать свой первый сервис для потоковой передачи видео (прошу прощения, если моё предположение неверно, и у вас это уже не первый такой проект). В частности, тут будут использованы пакеты Akka HTTP и Akka Streams, с помощью которых мы создадим REST API, который обладает способностями стриминга видеофайлов в формате MP4. При этом устроен этот API будет так, чтобы то, что он выдаёт, соответствовало бы ожиданиям HTML5-тега <video>. Кроме того, тут я скажу несколько слов о наборе инструментов Akka в целом, и о некоторых его компонентах, вроде Akka Streams. Это даст вам определённый объём теории, которая пригодится вам в работе. Но, прежде чем мы приступим к делу, хочу задать один вопрос.

Почему я решил рассказать о стриминге видео?


У того, что я посвятил эту статью потоковой передаче видео, есть три основных причины.

Первая причина заключается в том, что это, с моей точки зрения, захватывающая и сложная тема. Особенно — если речь идёт о крупномасштабных стриминговых сервисах (вроде Netflix). Мне всегда хотелось получше разобраться в этой теме.

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

И третья причина, последняя в моём списке, но такая же важная, как и другие, кроется в том, что разработка стримингового сервиса — это очень интересный способ знакомства с библиотекой Akka Streams, которая, на самом деле, значительно упрощает задачу создания подобного сервиса.

А теперь мы можем переходить к нашей основной теме.

Что такое набор инструментов Akka? Удобно ли им пользоваться?


Akka — это опенсорсный набор инструментов, который нацелен на упрощение разработки многопоточных и распределённых приложений, и, кроме того, даёт программисту среду выполнения для подобных приложений. Системы, которые базируются на Akka, обычно очень и очень хорошо масштабируются. Проект Akka основан на модели акторов, в нём используется система конкурентного выполнения кода, основанная на акторах. Создатели этого проекта многое почерпнули из Erlang. Инструменты Akka написаны на Scala, но в проекте имеются DSL и для Scala, и для Java.

Если говорить об удобстве работы с Akka, то, полагаю, писать код с использованием этого набора инструментов весьма удобно. Если вы хотите получше его изучить — рекомендую начать с этого материала.

Основной объём кода, который мы будем тут рассматривать, будет использовать пакеты HTTP и Streams. Мы практически не будем пользоваться стандартным пакетом Akka Actors.

Теперь, когда мы разобрались с самыми базовыми сведениями об Akka, пришло время вплотную заняться пакетом Akka Streams.

Что такое Akka Streams?


Akka Streams — это всего лишь пакет, построенный на базе обычных акторов Akka. Он нацелен на то, чтобы облегчить обработку бесконечных (или конечных, но очень больших) последовательностей данных. Главной причиной появления Akka Streams стали сложности правильной настройки акторов в системе акторов, обеспечивающей стабильную работу с данными при их потоковой передаче.

Для нас важен тот факт, что в Akka Streams есть встроенный механизм back-pressure («обратное давление»). Благодаря этому решается одна из самых сложных проблем мира стриминговой передачи данных. Это — настройка правильной реакции поставщика данных на работу в условиях, когда потребитель данных не может справиться с нагрузкой. И эту проблему решает инструмент, которым мы будем пользоваться, а нам остаётся лишь научиться работать с этим инструментом, не вдаваясь в какие-то чрезмерно сложные и запутанные темы.

Пакет Akka Streams, кроме того, даёт в наше распоряжение API, который совместим с интерфейсами, необходимыми для работы с Reactive Streams SPI. И, между прочим, стоит отметить, что сам проект Akka входит в число основателей инициативы Reactive Streams.

Про Akka Streams мы поговорили. Поэтому можем переходить к нашей следующей теоретической теме.

Что такое Akka HTTP?


Akka HTTP — это, как и Akka Streams, пакет, входящий в набор инструментов Akka. Этот пакет основан на пакетах Akka Streams и Akka Actors. Он направлен на то, чтобы упростить работу приложений, в которых используются инструменты Akka, с внешним миром по протоколу HTTP. Этот пакет поддерживает и серверные, и клиентские возможности. Поэтому с его помощью можно создавать и REST API, и HTTP-клиентов, которые отправляют запросы к неким внешним сервисам.

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

HTML5-тег <video>


Тег <video> — это новый элемент, который появился в HTML5. Он создавался как замена Adobe Video Player. Несложно понять, что главная задача этого HTML-элемента заключается в предоставлении разработчикам возможностей встраивания в HTML-документы медиаплееров, способных воспроизводить видеофайлы. Собственно, этот тег очень похож на <img>.

В теге <video> размещают тег <source>, имеющий два важных атрибута. Первый — это src, который используется для хранения ссылки на видео, которое нужно воспроизвести. Второй — это type, который содержит сведения о формате видео.

Между открывающим и закрывающим тегом <video> </video> можно ввести какой-то текст, который будет использован в роли текста, выводимого вместо элемента <video> в тех случаях, когда этот элемент не поддерживается браузером. Но в наши дни тег <video> поддерживает даже Internet Explorer, поэтому вероятность возникновения ситуации, в которой может понадобиться подобный текст, стремится к нулю.

Как будет работать стриминг с использованием тега <video> и FileIO?


Хотя на первый взгляд кажется, что «стриминг с использованием тега <video> и FileIO» это — нечто архисложное, если в этот вопрос немного вникнуть, окажется, что ничего особенного в нём нет. Дело в том, что то, о чём идёт речь, представлено готовыми блоками, из которых нужно лишь собрать то, что нам нужно.

На стороне сервера основная нагрузка ложится на объект FileIO. Он будет генерировать события, содержащие фрагменты файла, потоковую передачу которого мы осуществляем. Конечно, размеры этих фрагментов поддаются настройке. И, более того, настроить можно и позицию, начиная с которой будет осуществляться стриминг файла. То есть — видео необязательно смотреть сначала — его можно начинать смотреть с любого места, интересующего пользователя. Всё это отлично сочетается с возможностями тега <video>, выполняющего запрос HTTP GET с заголовком Range для того чтобы включить воспроизведение видео без его предварительной загрузки.

Вот — пара примеров запросов, выполняемых элементом <video>:



Если кому интересно — заголовок Range:bytes=x- отвечает за выбор позиции, с которой начинается воспроизведение видео. Первый запрос уходит на сервер в начале воспроизведения видео, а второй может быть отправлен тогда, когда пользователь решит куда-нибудь «перемотать» видео.

Сейчас, после довольно-таки длинного вступления, пришло время заняться кодом.

Пишем стриминговый сервис


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

Я люблю делать предположения, поэтому сделаю ещё одно, которое заключается в том, что я ожидают, что тот, кто это читает, владеет основами Scala и SBT.

▍1


Добавим в файл build.sbt необходимые зависимости. В этом проекте нам понадобится 3 пакета: akka-http, akka-actor-typed (пакета akka-actor, в теории, достаточно, но никогда нельзя забывать о типобезопасности) и akka-stream.

libraryDependencies ++= Seq(
  "com.typesafe.akka" %% "akka-actor-typed" % "2.6.14",
  "com.typesafe.akka" %% "akka-stream" % "2.6.14",
  "com.typesafe.akka" %% "akka-http" % "10.2.4"
)

▍2


Теперь можно создать главный класс, ответственный за запуск приложения. Я решил расширить класс App. Мне кажется, что это удобнее, чем создавать метод main. На следующем шаге мы поместим сюда код, имеющий отношение к созданию системы акторов и HTTP-сервера.

object Runner extends App {

}

▍3


После создания главного класса мы можем добавить в него код, о котором говорили выше.

object Runner extends App {

  val (host, port) = ("localhost", 8090)
  implicit val system: ActorSystem[Nothing] = ActorSystem(Behaviors.empty, "akka-video-stream")
  Http().newServerAt(host, port)

}

Сейчас нас вполне устроит такая конфигурация. На последнем шаге мы добавим в код вызов метода bind, что позволит открыть доступ к нашему REST API. Тут мы создаём объект ActorSystem с именем akka-video-stream и HTTP-сервер, прослушивающий порт 8090 на локальном компьютере. Не забудьте о ключевом слове implicit в определении системы акторов, так как подобный неявный параметр необходим в сигнатуре метода Http.

▍4


А тут мы, наконец, реализуем конечную точку REST API, используемую для обработки запросов от тега <video>.

object Streamer {

  val route: Route =
    path("api" / "files" / "default") {
      get {
        optionalHeaderValueByName("Range") {
          case None =>complete(StatusCodes.RangeNotSatisfiable)
          case Some(range) => complete(HttpResponse(StatusCodes.OK))
        }
      }
    }
}

Как видите, я создал конечную точку с URL api/files/default/. В её коде проверяется, есть ли в запросе заголовок Range. Если он содержит корректные данные — сервер возвращает ответ с кодом 200 (OK). В противном случае возвращается ответ с кодом 416 (Range Not Satisfiable).

▍5


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

private def stream(rangeHeader: String): HttpResponse = {
    val path = "path/to/file"
    val file = new File(path)
    val fileSize = file.length()

    val range = rangeHeader.split("=")(1).split("-")
    val start = range(0).toInt
    val end = fileSize - 1
   
    val headers = List(
      RawHeader("Content-Range", s"bytes ${start}-${end}/${fileSize}"),
      RawHeader("Accept-Ranges", s"bytes")
    )
   
    val fileSource: Source[ByteString, Future[IOResult]] = FileIO.fromPath(file.toPath, 1024, start)
    val responseEntity = HttpEntity(MediaTypes.`video/mp4`, fileSource)
    HttpResponse(StatusCodes.PartialContent, headers, responseEntity)
  }

Тут я сделал следующее:

  • Загрузил файл, потоковую передачу которого я хочу организовать, а затем, учитывая заголовок из запроса, и данные о файле, нашёл позицию в файле, с которой начнётся стриминг, а так же сформировал заголовок Content Range.
  • С помощью FileIO создал поток из ранее загруженного файла. Затем я использовал этот поток в роли данных в HttpEntity.
  • Я создал ответ, HttpResponse, с кодом 206 (Partial Content), с соответствующими заголовками и с телом в виде responseEntity.

Ещё мне хочется подробнее поговорить о FileIO, так как это — самый удивительный механизм во всей статье. Что, на самом деле, происходит при выполнении строки FileIO.fromPath(file.toPath, 1024, start)?

Тут, из содержимого файла, находящегося по заданному пути, создаётся объект Source (Akka-аналог Producer из Reactive Streams). Каждый элемент, выдаваемый этим объектом, имеет размер, в точности равный 1 Мб. Первый элемент будет взят из позиции, указанной в параметре start. Поэтому, если в start будет 0 — первый элемент окажется первым мегабайтом файла.

▍6


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

Начнём с внесения изменений в определение REST API:

complete(HttpResponse(StatusCodes.Ok)) => complete(stream(range))

Получается, что вместо того, чтобы просто возвращать OK, мы вызываем метод stream с передачей ему параметра range и начинаем стриминг.

Нельзя забывать о том, что наш API всё ещё недоступен для внешнего мира. Поэтому нам нужно модифицировать соответствующий фрагмент кода, ответственный за запуск HTTP-сервера:

Http().newServerAt(host, port) =>Http().newServerAt(host, port).bind(Streamer.route)

Готово! Теперь у нас есть рабочий бэкенд, а наш REST API ждёт подключений от любых программ, которым он нужен. Осталось лишь всё протестировать.

Тестирование стримингового сервиса


Мы, чтобы протестировать приложение, создадим простую HTTP-страницу, единственным достойным внимания элементом которой будет тег <video>. Причём, обо всём, что надо знать для понимания работы этой страницы, мы уже говорили. Поэтому я просто приведу ниже полный код соответствующего HTML-документа.

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

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Akka streaming example</title>
</head>
<body style="background: black">

<div style="display: flex; justify-content: center">
    <video width="1280" height="960" muted autoplay controls>

        <source src="http://localhost:8090/api/files/default" type="video/mp4">

        Update your browser to one from XXI century
    </video>
</div>
</body>
</html>

Тут мне хотелось бы обратить ваше внимание на 5 важных вещей:

  1. Я использовал возможности тега <source> вместо использования соответствующих атрибутов тега <video>.
  2. В атрибуте src тега <source> я указал путь, при обращении к которому бэкенд начнёт потоковую передачу видеофайла.
  3. В атрибуте type тега <source> я указал тип файла.
  4. Я добавил к тегу <video> атрибуты autoplay и muted для того чтобы видео начинало бы воспроизводиться автоматически.
  5. К тегу <video> я добавил и атрибут controls, благодаря чему будут выводиться элементы управления видеоплеера, встроенного в страницу.

На элемент <div> можете особого внимания не обращать. Он тут нужен лишь для стилизации плеера.

Для проверки правильности работы системы достаточно запустить бэкенд и открыть вышеописанный HTML-документ в любом современном браузере.


Правильная работа стримингового сервиса

Обратите внимание на то, что автоматическое воспроизведение видео не начнётся до тех пор, пока к тегу <video> не будут добавлены атрибуты muted и autoplay. Если не оснастить тег <video> этими атрибутами — воспроизведение придётся включать вручную, нажимая на соответствующую кнопку.

Как и в любом обычном программном проекте, мы, после создания и тестирования первого варианта программы, можем переосмыслить решения, принятые в самом начале, и подумать о том, как улучшить её код.

Что можно улучшить?


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

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

Итоги


Сегодня я постарался доказать то, что реализация простого стримингового сервиса — это, при условии использования правильных инструментов, проще, чем кажется. Использование инструментов Akka и подходящего HTML-тега способно значительно сократить объём работы. Правда, не забывайте о том, что тут показан очень простой пример. Для реализации реального стримингового сервиса этого может быть недостаточно.

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

Вот GitHub-репозиторий с кодом проекта.

Какие инструменты вы выбрали бы для создания собственного стримингового сервиса?


Tags:
Hubs:
+34
Comments6

Articles

Information

Website
ruvds.com
Registered
Founded
Employees
11–30 employees
Location
Россия
Representative
ruvds