redirect для playlist .m3u8 у nginx для анти кешу браузера у HLS player
Стала задача як форсувати отримання свіжої версії файлу playlist .m3u8 для застуванні у HLS (HTTP Live Streaming) медіа плеєру. Свіжу версію мається на увазі те що вона не буде зчитана з кешу браузера у будь-якому випадку незалежно від заголовків expire.
Вирішення задачі це додавати до імені файлу поточну дату у вигляді додаткового аргументу на кшталт: index.m3u8?m=202303191711.
Використовується Web сервер - nginx.
nginx
###
time_iso8601
Сформуємо змінну $formatted_date з датою у необхідному форматі. Є вбудована змінна у nginx для дати у форматі ISO8601 $time_iso8601.
Для формування дати у потрібному форматі застосовується послідовність map у контексті http.
map $time_iso8601 $year {
default 'year';
'~^(?<yyyy>\d{4})-' $yyyy;
}
map $time_iso8601 $month {
default 'month';
'~^\d{4}-(?<mm>\d{2})-' $mm;
}
map $time_iso8601 $day {
default 'day';
'~^\d{4}-\d{2}-(?<dd>\d{2})' $dd;
}
map $time_iso8601 $hour {
default 'hour';
'~^\d{4}-\d{2}-\d{2}T(?<hh>\d{2})' $hh;
}
map $time_iso8601 $min {
default 'minute';
'~^\d{4}-\d{2}-\d{2}T\d{2}:(?<mn>\d{2})' $mn;
}
map $time_iso8601 $sec {
default 'seconds';
'~^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:(?<se>\d{2})' $se;
}
map $time_iso8601 $formatted_date {
default 'date-not-found';
'~^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})' $year$month$day$hour$min$sec;
}
Тепер ми маємо змінну $formatted_date у вигляді $year$month$day$hour$min$sec.
Але є одне але. Спільне використання змінної у регулярному виразі location.
location
Наприклад “location ~ ^(*..m3u8)$” і спробі використати результат пошуку змінної “$1” у location для rewrite.
location ~ ^/t/(.*\.m3u8)$ {
rewrite ^ $1?m=$formatted_date&$args? permanent;
}
Надасть ось такий результат: “http://lexxai.pp.ua/t/09?m=20230319083509&”.
Тому що використана буде остання змінна “$1” з використання команди map, вже після location.
Для вирішення цієї проблеми використаємо таку конструкцію регулярного виразу:
location ~ ^/t/(?<filename>.+\.m3u8) {
rewrite ^ $filename?m=$formatted_date&$args? permanent;
}
Зараз все буде добре з заміною, але буде інша проблема - зациклювання:
“http://lexxai.pp.ua/t/index.m3u8?m=20230319084535&m=20230319084535&m=20230319084535&m=20230319084535&m=20230319084535&m=20230319084534&m=20230319084534&m=20230319084534&m=20230319084534&m=20230319084534&m=20230319084533&m=20230319084533&m=20230319084533&m=20230319084533&m=20230319084532&m=20230319084532&m=20230319084532&m=20230319084532&m=20230319084532&m=20230319084531&….”
Тому додаємо умову на наявність аргументу “m=”:
location ~ ^/t/(?<filename>.+\.m3u8) {
if ($args !~ m=){
rewrite ^ $filename?m=$formatted_date&$args? permanent;
}
}
Надасть ось такий результат: “http://lexxai.pp.ua/t/index.m3u8?m=20230319085241&”.
Але є знову одне але. Використання “redirect 301” для HLS плеєра.
HLS плеєр.
Для програвання на сторінці браузера файлів у форматі HLS має бути його підтримка. Так як корні формату належать Apple (application/vnd.apple.mpegurl), тому повноцінну підтримку на сьогодні має тільки браузер (Safari). Для інших браузерів має бути стороннє рішення. Для себе я знайшов проєкт JavaScript - hls.js. Він використовує стандартний HTML5 video tag, і у випадку вбудованої підтримки HLS, більше нічого не застосовується. Для інших браузерів вже застосовується додатковий javascript код котрий завантажує маленькі блоки відео і з’єднує їх у пам’яті для програвання відео через об’єкт javascript blob.
Так от, використання redirect 301, не дозволено у HLS, можливо з метою безпеки. Тому було винайдено інший шлях - згенерувати новий master m3u8 playlist засобами nginx.
###
master m3u8 playlist
Все доволі просто, у location:
return 200 "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH\n$filename?m=$formatted_date&$args";
І в результаті буде таке повернення від сервера на запит: http://lexxai.pp.ua/t/index.m3u8
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH
/t/index.m3u8?m=20230319093225&
CORS
Не забуваємо про CORS заголовки для програвання HLS.
location ~ ^(?.+\.m3u8)$ {
if ($args !~ m=){
add_header Pragma "public";
add_header Cache-Control "public";
add_header Storm-Control "public";
add_header X-Cache $upstream_cache_status;
# CORS setup
add_header 'Access-Control-Allow-Origin' "$cors_origin_header" always;
add_header 'Access-Control-Expose-Headers' 'Content-Length';
add_header 'Access-Control-Allow-Methods' 'GET, HEAD';
expires 1s;
return 200 "#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH\n$filename?m=$formatted_date";
}
add_header Pragma "public";
add_header Cache-Control "public";
add_header Storm-Control "public";
add_header X-Cache $upstream_cache_status;
# CORS setup
add_header 'Access-Control-Allow-Origin' "$cors_origin_header" always;
add_header 'Access-Control-Expose-Headers' 'Content-Length';
add_header 'Access-Control-Allow-Methods' 'GET, HEAD';
expires 5m;
}
Де $cors_origin_header визначається через map для Ваших дозволенних доменів:
map $http_origin $cors_origin_header {
default '';
# all your domains
https://cdn1.domain1.com "$http_origin";
https://cdn2.domain1.com "$http_origin";
https://cdn.domain2.com "$http_origin";
}
Приклад готового проєкту:


