Не так давно на хабре обсуждалась новая инициатива от 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