Организация хостинга зашифрованного видеоконтента с помощью HTML5

в 20:24, , рубрики: html, html5, javascript, w3c, Веб-разработка, видео, шифрование, метки: , , , ,

Не так давно на хабре обсуждалась новая инициатива от W3С — Encrypted Media Extensions или просто EME. Попробуем же разобраться на практике, что нового и интересного нам предлагают.

Итак, задача: предположим, что есть куча интересного видеоматериала, которую хочется выложить в интернет, но не для всех, а только для хороших людей (для друзей, знакомых,… или просто за денежку). Так вот, инициатива Encrypted Media Extensions как раз и предлагает добавить весь необходимый API для клиентской части в стандарт HTML. Помимо этого в предложении от W3C рассматриваются и другие вопросы, такие как злополучный DRM, но мы их пока отложим, остановившись исключительно на клиентской части организации нашего гипотетического сервиса. Обязательное условие — поддержка браузером спецификации EME, а значит, поддерживается и базовая ключевая система Clear Key.

Итак, есть тег video и файл foo.webm, видео и аудио в котором зашифровано с помощью алгоритма AES (такая возможность поддерживается в контейнере WebM, и авторы EME воспользовались этим):

<video src="foo.webm" autoplay></video>

В данной ситуации EME-браузер при попытке воспроизвести foo.webm обнаружит, что контент зашифрован, остановится и будет ждать, когда ему предоставят ключ для дешифровки. Вообще, основной вариант использования (use case) состоит в том, что информация о том, как зашифрован контент, описывается прямо в контейнере; воспользуемся именно таким вариантом. Кроме того, что браузер остановится, он еще и возбудит событие needkey на самом элементе HTMLMediaElement, а приложению следует его обработать и, в конечном итоге, предоставить ключ:

<video src="foo.webm" autoplay onneedkey="handleKeyNeeded(event)"></video>

Функция handleKeyNeeded() будет не очень сложной — она получает идентификатор ключа, который нужен браузеру, затем делает XHR-запрос на сервер и отдает ответ=ключ браузеру обратно (или не отдает, если того требует ситуация — это уже бизнес-логика). Мы немного упростим — пусть ключ уже у нас есть; для схемы Clear Key+AES, которую мы используем, это будет массив Uint8Array из 16 байт; тогда функция выглядит так:

function handleKeyNeeded(event) {
  var video = event.target;
  var initData = event.initData;

  //var message = initData;
  //var xmlhttp = new XMLHttpRequest();
  //xmlhttp.open("POST", "http://.../getkey", false);
  //xmlhttp.send(message);
  //var key = new Uint8Array(xmlhttp.response);
  var key = new Uint8Array([0xeb, 0xdd, 0x62, 0xf1, 0x68, 0x14, 0xd2, 0x7b,
                            0x68, 0xef, 0x12, 0x2a, 0xfc, 0xe4, 0xae, 0x3c]);

  if (!video.keys)
    video.keys = video.MediaKeys("org.w3.clearkey");
  if (!video.keys)
    throw "Could not create MediaKeys";

  var keySession = video.keys.createSession(mimeType, initData);
  if (!keySession)
    throw "Could not create key session";
  keySession.update(key);
}

Основные действующие лица здесь:

  • event.initData: идентификатор, соответствующий ему ключ нужен браузеру;
  • key: собственно ключ для дешифровки;
  • keySession.update(key): этим вызовом браузер получает тот самый ключ; большего от страницы и не требуется.

Те, кому интересен остальной код функции handleKeyNeeded() (классы MediaKeys, MediaKeySession и т.д.), могут воспользоваться свежим переводом будущей спецификации EME, выполненной с помощью сервиса Catbo.

Таким образом, если бы у нас был браузер с поддержкой EME (текущей версии 2), то на странице encrypted_media_player_v2.html мы бы обнаружили на видео черного медведя, прохаживающегося по клетке (образец зашифрованного видео bear-320x240-av-enc_av.webm взят из кода Chromium). На момент написания этой статьи такого браузера, с высокой долей вероятности, нет. Однако, если чуть доработать код страницы, то медведя все-таки можно будет увидеть, воспользовавшись браузерами Google Chrome или Chromium версии 26 или выше (пока поддержка обнаружена только в них): encrypted_media_player.html.

Далее мы рассмотрим, какие доработки пришлось сделать, чтобы наш пример заработал. Во-первых, сейчас в Chrome/Chromium поддерживается 1-я версия будущей спецификации EME, от которой уже отказались авторы EME в пользу версии 2, сделав последнюю объектно-ориентированной. Однако в Chrome/Chromium включена пока только первая версия EME; кроме того, во многих местах надо навесить префикс webkit; из-за этого функция handleKeyNeeded() изменится следующим образом:

function handleKeyNeeded(event) {
  ...

  var keySystem = "webkit-org.w3.clearkey";
  video.webkitGenerateKeyRequest(keySystem, event.initData);
  // добавление ключа
  video.webkitAddKey(keySystem, key, event.initData, null);
}

Во-вторых, если сейчас напрямую устанавливать атрибут src тега video, то события needkey мы не дождемся. Дело в том, что в этом случае видео воспроизводится стоковым функционалом ffmpeg, который задействован в браузере, а тот не обращает внимания на всякие флаги зашифрованности в контейнере (далее идет немного технических подробностей о реализации медиа-стека именно в Chrome/Chromium). А вот если воспользоваться другим способом предоставления контента проигрывателю, то получим нужный нам результат (возбуждение события needkey). Этот другой способ имеет название Media Source Extensions; его суть заключается в том, что коду JavaScript позволяется генерировать медиапоток для последующего воспроизведения. На деле это означает, что в браузерах от Google появилась альтернативная реализация demuxer-ов, таких как WebM и mp4; вот их-то и научили возбуждению необходимого нам события needkey. Вот соответствующий код загрузки видео с медведем в наш тег video:

function load() {
  var video = document.getElementById("video");

  var mediaFile = "bear-320x240-av-enc_av.webm";
  //video.src = mediaFile;
  var sourceOpened = false;
  function onSourceOpen(e) {
    if (sourceOpened)
      return;
    sourceOpened = true;

    var mediaType = 'video/webm; codecs="vorbis, vp8"';
    var srcBuffer = mediaSource.addSourceBuffer(mediaType);
    var xhr = new XMLHttpRequest();
    xhr.open('GET', mediaFile);
    xhr.responseType = 'arraybuffer';
    xhr.addEventListener('load', function(e) {
      srcBuffer.append(new Uint8Array(e.target.response));
      mediaSource.endOfStream();
    });
    xhr.send();
  }
  var mediaSource = new WebKitMediaSource();
  mediaSource.addEventListener('webkitsourceopen', onSourceOpen);
  video.src = window.URL.createObjectURL(mediaSource);

  ...
}

Конечно же, этот код менее универсален, чем простое присваивание

video.src = "bear-320x240-av-enc_av.webm";

; более того, будущая спецификация EME не требует подобных ухищрений (но пока добиться работоспособности возможно только таким путем). Ждем дальнейшего развития событий.


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

Ссылки на тему EME:
Google, Microsoft и Netflix хотят добавить DRM в HTML5
Перевод спецификации Encrypted Media Extensions / Зашифрованный медиаконтент в HTML5

Автор: ilil

Источник

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


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