Эта страница переведена с английского. Читать страницу на английском
Zeyn отдаёт медиафайлы с поддержкой HTTP range-запросов и воспроизводит их в компонентах Video и Audio. Декодированием занимаются штатные элементы <video> и <audio>; Zeyn рисует поверх них собственные доступные элементы управления.
Отдача файлов
Зарегистрируйте MediaSource в feature с указанием права:
Feature::new("library")
.media("films", "library.watch", FilmFolder::new())
.media("music", "library.listen", SongTable::new())Браузер запрашивает GET /_zeyn/media/{source}/{id}. Прежде чем источник что-либо увидит, сервер:
- отклоняет идентификаторы, которые не состоят из 1-128 символов
A-Z a-z 0-9 - _ ., начинаются с точки или содержат..(404); - проверяет право источника (
403) и guard-ы feature; - запускает перехватчики (interceptors) (
CallKind::Media, с именем источника вscreen).
open(ctx, id) ищет идентификатор в собственном каталоге приложения и возвращает Media: Media::file(path) для файла на диске (тип по расширению, entity tag по размеру и времени изменения), Media::bytes(data, type) для данных в памяти или Media::new(reader, len, type) для любого Read + Seek. Идентификатор это имя, а не путь: стройте путь из своего поиска, а не из идентификатора.
Ответы:
| Запрос | Ответ |
|---|---|
без Range, другая единица или несколько диапазонов | 200 со всем файлом |
bytes=a-b, bytes=a-, bytes=-n | 206 с Content-Range: bytes a-b/len |
| диапазон bytes с ошибкой в записи или начинающийся за концом | 416 с Content-Range: bytes */len |
If-Range, не совпадающий с entity tag | 200 со всем файлом |
If-None-Match, совпадающий с entity tag | 304 |
В каждом ответе есть Accept-Ranges: bytes, Cache-Control: private, no-cache и X-Content-Type-Options: nosniff. Inline отдаются только аудио, видео, растровые изображения, WebVTT и потоковые плейлисты; любой другой тип (HTML, SVG, скрипты) отправляется как application/octet-stream с Content-Disposition: attachment, поэтому файл никогда не сможет выполниться как страница приложения. Файлы читаются блоками по 64 KiB в blocking-пуле, в очереди не более четырёх блоков, так что память остаётся постоянной для файла любого размера, а ушедший клиент прекращает чтение. Лимиты запросов действуют так же, как для других запросов; чтение медиа не является записью.
Content security policy менять не нужно: default-src 'self' уже разрешает медиа из приложения. Библиотеке адаптивной трансляции, использующей Media Source Extensions, нужен blob: в media-src, который Zeyn не добавляет.
Публичные файлы: изображения, логотипы, загрузки
Файлы, которые может видеть любой, вошедший в систему или нет, помещаются в публичный источник:
Feature::new("catalog").public_media("images", 86_400, ImageFolder::new("data/images"))GET /_zeyn/media/images/{id} пропускает проверку права (идентификаторы, guard-ы и перехватчики по-прежнему работают), не устанавливает cookie и отвечает с Cache-Control: public, max-age=86400 (не более MAX_PUBLIC_MEDIA_CACHE, один год), так что браузеры и CDN перед приложением хранят его. Entity tag по-прежнему позволяет им перепроверять с 304. Используйте путь как src для Image. Для множества крупных изображений отдельный хост изображений за CDN всё же дешевле; разрешите его через App::csp(|c| c.img_src([...])).
openдолжен возвращать одни и те же байты каждому посетителю. Никогда не делайте публичный файл зависимым от пользователя, роли, арендатора (tenant) или языка: CDN или прокси хранит первый ответ и отдаёт его всем. Файлы, различающиеся по пользователям, относятся к источнику, зарегистрированному черезFeature::media.- При большом
max_ageдавайте идентификаторы, которые меняются вместе с содержимым, например хеш содержимого (poster-3f9a2c.jpg). Тогда изменённый файл получает новый адрес, и старые копии не живут в кэшах до истечения срока. - Чтения публичных источников (
GETиHEAD) не учитываются вApp::rate_limit: одна страница может загружать десятки постеров, а многие мобильные посетители делят один адрес за NAT оператора. Приватные источники по-прежнему учитываются, а правила IP allow и deny применяются к обоим. Ограничение злоупотребления трафиком это задача CDN или обратного прокси перед приложением.
Плееры
Video::new("films", t!("films.harbor"))
.subtitle(t!("films.harbor_sub"))
.file("harbor.webm")
.file("harbor.mp4")
.hls("harbor.m3u8")
.poster("harbor.jpg")
.captions("harbor.en.vtt", "en", "English")
.captions("harbor.tr.vtt", "tr", "Türkçe")
Audio::new("music", t!("songs.dawn")).subtitle(t!("songs.artist")).file("dawn.mp3")Элементы управления: воспроизведение и пауза, ползунок перемотки, время, отключение звука, ползунок громкости, меню скорости воспроизведения, кнопка субтитров и (при нескольких файлах субтитров) меню языка, а для видео картинка в картинке (где её поддерживает браузер) и полный экран. Ползунки следуют паттерну WAI-ARIA slider: стрелки сдвигают на 5 секунд (или на 5 процентов громкости), Page Up и Page Down сдвигают на десятую часть, Home и End ведут к краям. Меню следуют паттерну menu button. Пока фокус внутри плеера, k или Space воспроизводит и ставит на паузу, j и l перематывают на 10 секунд, m отключает звук, c переключает субтитры, а f переключает полный экран; в остальных местах страницы эти клавиши ничего не делают.
Субтитры это файлы WebVTT, отдаваемые тем же источником. Плеер рисует активную реплику простым текстом (разметка реплик отбрасывается), поверх картинки для видео и под заголовком для аудио, поэтому субтитры выглядят одинаково во всех браузерах и в полноэкранном режиме.
Продолжение воспроизведения
Плеер сохраняет место, где остановился пользователь: каждые 15 секунд во время воспроизведения, на паузе, когда страница скрывается или закрывается, и 0, когда медиа заканчивается. Воспроизведение продолжается, если сохранённая точка отстоит более чем на 5 секунд от любого из концов.
- Когда источник реализует
positionиsave_position, позиции хранятся на сервере:GET /_zeyn/media/position?source=&id=отвечает{"at": seconds, "kept": true}, аPOST /_zeyn/media/positionс{source, id, at}сохраняет (204). Оба проверяют идентификатор, право и guard-ы и требуют вошедшего пользователя; POST также требует заголовок CSRF и считается записью для лимитов запросов. Позиции большеMAX_POSITION_SECONDSили меньше нуля дают422. - В остальных случаях (методы источника по умолчанию или анонимный пользователь) чтение отвечает
{"at": null, "kept": false}, а плеер хранит позицию в локальном хранилище браузера под ключомzeyn.media.{source}/{id}. Ошибки хранилища (приватные окна, заблокированное хранилище) игнорируются.
Серверные позиции лучше для вошедших пользователей: они следуют за пользователем на каждое устройство, и приложение может читать их для строки «продолжить просмотр». Локальное хранилище не требует кода, но остаётся в одном браузере и общее для всех, кто им пользуется.
HLS и DASH
hls(id) и dash(id) добавляют адаптивный поток. Safari воспроизводит HLS нативно, поэтому использует плейлист. Другим браузерам нужна JavaScript-библиотека (например hls.js или dash.js), которую Zeyn не поставляет. Прежде чем перейти к прогрессивным файлам, плеер отправляет на своём корне отменяемое событие zeyn:media-adaptive:
document.addEventListener('zeyn:media-adaptive', (e) => {
const { media, kind, url } = e.detail;
if (kind !== 'hls' || !window.Hls?.isSupported()) return;
e.preventDefault();
const hls = new window.Hls();
hls.loadSource(url);
hls.attachMedia(media);
});Загружайте этот скрипт из файла вашего приложения (inline-скрипты блокируются content security policy). Когда ни один слушатель не вызывает preventDefault(), записи file воспроизводятся по порядку. Файлы плейлистов и сегментов отдаются тем же источником, поэтому их относительные URL работают, пока их идентификаторы перечислены в нём.
Загруженные файлы
Файлы, загруженные через Form::file, отдаются через GET /_zeyn/files/{store}/{id} с теми же ответами, что и медиафайлы (диапазоны, entity tag, nosniff, inline только для растровых изображений, аудио и видео), после проверки права хранилища, guard-ов feature и арендатора (tenant). См. раздел о загрузке файлов в examples/docs/pages/forms.md.