diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..944cecf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# .editorconfig +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{js,jsx,ts,tsx,vue}] +indent_style = space +indent_size = 2 +max_line_length = 120 +quote_type = single diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..d2294bf --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,109 @@ +// .eslintrc.cjs + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + + env: { + browser: true, + node: true, + es2022: true + }, + + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + project: ['./tsconfig.json'], + tsconfigRootDir: __dirname + }, + + plugins: ['@typescript-eslint', 'simple-import-sort', 'import'], + + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'prettier' + ], + + rules: { + // стиль без ; + semi: ['error', 'never'], + '@typescript-eslint/semi': ['error', 'never'], + + // type-only импорты + '@typescript-eslint/consistent-type-imports': [ + 'warn', + { prefer: 'type-imports', disallowTypeAnnotations: false } + ], + + // unused vars с поблажками на _ + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' } + ], + + // промисы + '@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: { attributes: false } }], + '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }], + + // длина строки 120 + исключения + 'max-len': ['error', { + code: 120, + comments: 120, + tabWidth: 2, + ignoreUrls: true, + ignoreStrings: true, + ignoreTemplateLiterals: true, + ignoreComments: true, + ignoreTrailingComments: true, + ignorePattern: '^\\s*import\\s.*$' + }], + + // сортировка импортов/экспортов + 'simple-import-sort/imports': ['error', { + groups: [ + ['^react$', '^@?\\w'], // внешние пакеты + ['^@/'], // алиасы проекта + ['^\\.\\.(?!/?$)', '^\\.\\./?$'], // относительные вверх + ['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'], // относительные внутри + ['^.+\\.s?css$'] // стили + ] + }], + 'simple-import-sort/exports': 'error', + 'import/order': 'off', // чтобы не конфликтовало + + '@typescript-eslint/no-namespace': 'off' + }, + + ignorePatterns: [ + 'node_modules/', + 'dist/', + 'build/', + 'coverage/', + 'demo/', + 'vendors/', + '**/*.d.ts' + ], + + overrides: [ + { + files: ['*.config.{ts,js,mjs,cjs}', 'scripts/**'], + rules: { + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-require-imports': 'off' + } + }, + { + files: ['**/*.spec.ts', '**/*.test.ts'], + rules: { + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-explicit-any': 'off' + } + } + ] +} diff --git a/.eslintrc.lenient.cjs b/.eslintrc.lenient.cjs new file mode 100644 index 0000000..b64939f --- /dev/null +++ b/.eslintrc.lenient.cjs @@ -0,0 +1,72 @@ +//.eslintrc.lenient.cjs +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + env: { browser: true, node: true, es2022: true }, + + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + project: ['./tsconfig.json'], + tsconfigRootDir: __dirname + }, + + plugins: ['@typescript-eslint', 'simple-import-sort', 'import'], + extends: [ + 'eslint:recommended', + // без requiring-type-checking + 'plugin:@typescript-eslint/recommended', + 'prettier' + ], + + rules: { + // стиль + semi: ['error', 'never'], + '@typescript-eslint/semi': ['error', 'never'], + 'max-len': ['error', { + code: 120, comments: 120, tabWidth: 2, + ignoreUrls: true, ignoreStrings: true, ignoreTemplateLiterals: true, + ignoreComments: true, ignoreTrailingComments: true, + ignorePattern: '^\\s*import\\s.*$' + }], + + // сортировка импортов + 'simple-import-sort/imports': ['error', { + groups: [ + ['^react$', '^@?\\w'], + ['^@/'], + ['^\\.\\.(?!/?$)', '^\\.\\./?$'], + ['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'], + ['^.+\\.s?css$'] + ] + }], + 'simple-import-sort/exports': 'error', + 'import/order': 'off', + + // «шумные» правила — ослабляем + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/unbound-method': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-redundant-type-constituents': 'off', + '@typescript-eslint/no-namespace': 'off', + 'no-empty': 'off', + + // unused vars — мягче, разрешаем _* + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['warn', { + argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' + }] + }, + + ignorePatterns: [ + 'node_modules/', 'dist/', 'build/', 'coverage/', 'demo/', 'vendors/', '**/*.d.ts', + 'tsup.config.bundled_*.mjs' + ] +} diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..f9c8541 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +# make lint-staged diff --git a/.prettierrc b/.prettierrc index 65220e0..49c9fa3 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,9 +2,8 @@ "semi": false, "trailingComma": "none", "singleQuote": true, - "printWidth": 80, + "printWidth": 120, "arrowParens": "always", - "max-len": ["error", 140, 2], "tabWidth": 2, "useTabs": false } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c2c7d9b --- /dev/null +++ b/Makefile @@ -0,0 +1,52 @@ +##### Make runs with POSIX sh +SHELL := /bin/sh +.SHELLFLAGS := -ec + +# --- config (raw) --- +CONTAINER_NAME_RAW ?= voka-player +IMAGE_NAME_RAW ?= voka-player +WORKDIR_RAW ?= /usr/voka + +# --- sanitized --- +CONTAINER_NAME := $(strip $(CONTAINER_NAME_RAW)) +IMAGE_NAME := $(strip $(IMAGE_NAME_RAW)) +WORKDIR := $(strip $(WORKDIR_RAW)) + +# helper: exec if running, otherwise one-off run; force legacy eslint mode +define run_in_container + @if docker ps --format '{{.Names}}' | grep -q '^$(CONTAINER_NAME)$$'; then \ + echo "→ Using running container: '$(CONTAINER_NAME)'"; \ + echo "+ docker exec -w '$(WORKDIR)' '$(CONTAINER_NAME)' /bin/sh -lc 'export ESLINT_USE_FLAT_CONFIG=0; $(1)'"; \ + docker exec -w "$(WORKDIR)" "$(CONTAINER_NAME)" /bin/sh -lc 'export ESLINT_USE_FLAT_CONFIG=0; $(1)'; \ + else \ + echo "→ Running one-off container from image: '$(IMAGE_NAME)'"; \ + echo "+ docker run --rm -v '$$(pwd):$(WORKDIR)' -w '$(WORKDIR)' --entrypoint /bin/sh '$(IMAGE_NAME)' -lc 'export ESLINT_USE_FLAT_CONFIG=0; $(1)'"; \ + docker run --rm \ + -v "$$(pwd):$(WORKDIR)" \ + -w "$(WORKDIR)" \ + --entrypoint /bin/sh \ + "$(IMAGE_NAME)" \ + -lc 'export ESLINT_USE_FLAT_CONFIG=0; $(1)'; \ + fi +endef + +.PHONY: lint lint-fix format lint-staged ci-check vars-debug + +lint: + $(call run_in_container,npx eslint --ext .ts,.tsx src) + +lint-fix: + $(call run_in_container,npx eslint --ext .ts,.tsx src --fix) + +format: + $(call run_in_container,npx prettier --write .) + +lint-staged: + $(call run_in_container,npx lint-staged --debug) + +ci-check: + $(call run_in_container,npx eslint --ext .ts,.tsx src) + $(call run_in_container,npx prettier --check .) + +vars-debug: + @printf "CONTAINER_NAME='%s'\nIMAGE_NAME='%s'\nWORKDIR='%s'\n" "$(CONTAINER_NAME)" "$(IMAGE_NAME)" "$(WORKDIR)" diff --git a/Toolset/WebOS/HostedWebApp-main/README.md b/Toolset/WebOS/App/README.md similarity index 100% rename from Toolset/WebOS/HostedWebApp-main/README.md rename to Toolset/WebOS/App/README.md diff --git a/Toolset/WebOS/HostedWebApp-main/appinfo.json b/Toolset/WebOS/App/appinfo.json similarity index 100% rename from Toolset/WebOS/HostedWebApp-main/appinfo.json rename to Toolset/WebOS/App/appinfo.json diff --git a/Toolset/WebOS/HostedWebApp-main/icon.png b/Toolset/WebOS/App/icon.png similarity index 100% rename from Toolset/WebOS/HostedWebApp-main/icon.png rename to Toolset/WebOS/App/icon.png diff --git a/Toolset/WebOS/HostedWebApp-main/index.html b/Toolset/WebOS/App/index.html similarity index 100% rename from Toolset/WebOS/HostedWebApp-main/index.html rename to Toolset/WebOS/App/index.html diff --git a/Toolset/WebOS/HostedWebApp-main/largeIcon.png b/Toolset/WebOS/App/largeIcon.png similarity index 100% rename from Toolset/WebOS/HostedWebApp-main/largeIcon.png rename to Toolset/WebOS/App/largeIcon.png diff --git a/Toolset/WebOS/HostedWebApp-main/webOSTV-dev.js b/Toolset/WebOS/App/webOSTV-dev.js similarity index 100% rename from Toolset/WebOS/HostedWebApp-main/webOSTV-dev.js rename to Toolset/WebOS/App/webOSTV-dev.js diff --git a/Toolset/WebOS/HostedWebApp-main/webOSTV.js b/Toolset/WebOS/App/webOSTV.js similarity index 100% rename from Toolset/WebOS/HostedWebApp-main/webOSTV.js rename to Toolset/WebOS/App/webOSTV.js diff --git a/Toolset/WebOS/Dockerfile b/Toolset/WebOS/Dockerfile index 6d03b71..d04c4ec 100644 --- a/Toolset/WebOS/Dockerfile +++ b/Toolset/WebOS/Dockerfile @@ -1,15 +1,14 @@ # Зависит от платформы на которой будет запускаться докер -FROM arm64v8/node:23.6.1-alpine3.21 AS arm +FROM arm64v8/node:16.20.2-alpine AS arm #FROM node:23.6.1-alpine3.21 AS intel -# Global npm dependencies for correct caching docker layers -RUN < { - console.log("afterInitialize") - // player.setControlbarVisibility(true) - player.attachSource("https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd", {autoplay: true}) + console.log('afterInitialize') + // player.setControlbarVisibility(true) + player.attachSource('https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd', { autoplay: 'muted' }) + // player.attachSource("https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/index.m3u8", {autoplay: "muted"}) + // player.attachSource("https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8", {autoplay: true}) + // player.attachSource("https://dash.akamaized.net/dash264/TestCases/10a/1/iis_forest_short_poem_multi_lang_480p_single_adapt_aaclc_sidx.mpd", {autoplay: true}) + // player.attachSource("https://streaming-iptv.voka.tv/eyJvc19uYW1lIjoibWFjIG9zIiwidXNlcl91aWQiOiIzMDE4ZmM2Yi02YmY4LTQ4OGQtOGRhMi02YjZmNWFmMjFjNDQiLCJkYXRhY2VudGVyIjoiaW50IiwiZG9tYWluX25hbWUiOiJzdHJlYW1pbmctaXB0di52b2thLnR2IiwiZHJtIjoic3BidHZjYXMiLCJleHBpcmF0aW9uX2RhdGUiOiIyMDI1LTA5LTA3VDA0OjA1OjI2WiIsImlwX2FkZHJlc3MiOiI4OS4xMTAuMTIyLjE3NSIsInByb2plY3QiOiJ2b2thX3Byb2R1Y3Rpb24iLCJwcm90b2NvbCI6ImhscyIsInNlc3Npb25faWQiOiIxNjU4Nzk0Zi1lNDRhLTRjMzctYmMzZi05YTFiMDc5YTk5YzAiLCJzdHJlYW1fbmFtZSI6IlBUWmNQaHJwTTN6Y0MzQ3RtRlppVEdkYkxuSGpINlBkUiIsInN0cmVhbV9wYXRoIjoiL3ZvZF92NS92ZWxjb20ifQ%3D%3D/MCwCFGxEXtZqOuKJDrL1lQtYVbbtDK4aAhQyj6sfG6I1q3GZ0feSVzzPRSjzbw%3D%3D/vod_v5/velcom/PTZcPhrpM3zcC3CtmFZiTGdbLnHjH6PdR.m3u8?b_app_channel_id=07497ccf-fa3f-473e-a254-9cfdba769ddb&b_app_id=voka_production&b_device_platform=mac%20os&b_device_uid=1d866e05-b0e5-e943-f49c-295ac85c20d7&b_stream_sid=1658794f-e44a-4c37-bc3f-9a1b079a99c0&b_strmr_channel_id=PTZcPhrpM3zcC3CtmFZiTGdbLnHjH6PdR&join_chunks=8", {autoplay: true}) + // player.attachSource("https://streaming-iptv.voka.tv/eyJvc19uYW1lIjoid2Vib3MiLCJ1c2VyX3VpZCI6IjA3YmI4NmYwLTE2NGItNGUzYi04YTg1LTI1ZmNkMDkwYTcwYyIsImRhdGFjZW50ZXIiOiJpbnQiLCJkb21haW5fbmFtZSI6InN0cmVhbWluZy1pcHR2LnZva2EudHYiLCJkcm0iOiJzcGJ0dmNhcyIsImV4cGlyYXRpb25fZGF0ZSI6IjIwMjUtMDktMDdUMTg6NTE6MjBaIiwiaXBfYWRkcmVzcyI6IjE4NS4xMzUuMTUwLjQwIiwicHJvamVjdCI6InZva2FfcHJvZHVjdGlvbiIsInByb3RvY29sIjoiZGFzaCIsInNlc3Npb25faWQiOiJmNzAwYjZiNC05YjM2LTQwOGItYjQwMC1hNWRjNTUwOTAwOTkiLCJzdHJlYW1fbmFtZSI6IlB0UzI3TkRVOEdEcVFaQnQ4VGlZTXBhTUFOMlpRTjZmSCIsInN0cmVhbV9wYXRoIjoiL3ZvZF92NS92ZWxjb20ifQ%3D%3D/MCwCFGUg-eRCCUS8TLoY-ytF6zfQYiH4AhRjhpdoccRw_-G27ppPVN33swBdAw%3D%3D/vod_v5/velcom/PtS27NDU8GDqQZBt8TiYMpaMAN2ZQN6fH.mpd?b_app_channel_id=38488837-7de6-44db-a1bb-848224476f3f&b_app_id=voka_production&b_device_platform=webos&b_device_uid=d9c5-2467-713b-8b9c-b88a&b_stream_sid=f700b6b4-9b36-408b-b400-a5dc55090099&b_strmr_channel_id=PtS27NDU8GDqQZBt8TiYMpaMAN2ZQN6fH", {autoplay: true}) }) player.addEventListener('play', onPlay, window) @@ -180,4 +185,4 @@ } - \ No newline at end of file + diff --git a/doc/options.md b/doc/options.md index e55f770..ed1d983 100644 --- a/doc/options.md +++ b/doc/options.md @@ -9,7 +9,7 @@ /** * Показывать элементы управления * @default false - */ + */ isVisible: true, zoomButton: { /** @@ -51,13 +51,59 @@ ``` ##### **2. Основные настройки воспроизведения (streamOpts)** + ```js -streamOpts: { +/** + * Настройки потока воспроизведения. + * Описывает поведение автозапуска, DRM, метрики, heartbeat, внешние субтитры + * и структуры манифеста (как «сырые» данные, так и результаты парсинга). + */ +interface +IStream +{ /** - * Автоматически начинать воспроизведение после загрузки + * Автоматически начинать воспроизведение после загрузки. + * Может быть булевым значением или строкой 'muted' (для автоплея без звука). * @default false */ - autoplay: false, + autoplay: boolean | 'muted' | any + + /** + * Конфигурация DRM (Widevine / FairPlay / PlayReady). + * Если null — DRM не используется. + */ + drmConfig: IDRMConfig | null + + /** + * Конфигурация метрик (сбор аналитики). + * Если null — метрики отключены. + */ + metrics: IMetrics | null + + /** + * Конфигурация heartbeat (периодические запросы). + * Если null — heartbeat отключён. + */ + heartbeat: IHeartbeat | null + + /** + * Внешние субтитры (загружаемые по URL). + * Если null — внешние субтитры не используются. + */ + externalSubtitles: ISubtitle | null + + /** + * Описание манифеста (список доступных треков видео/аудио). + * Используется течами для телеков для выбора качеств/треков + * Если null — манифест не задан. + */ + manifest: Manifest | null + + /** + * Результат парсинга манифеста (подробная структура HLS/DASH). + * Если null — парсинг не выполнялся. + */ + parsedManifest: IManifestParser | null } ``` diff --git a/package-lock.json b/package-lock.json index ed890fc..0e6d380 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,9 +20,9 @@ "express": "^4.21.2", "hls.js": "^1.5.20", "keycode": "^2.2.1", + "lodash": "^4.17.21", "postcss": "^8.5.1", "postcss-preset-env": "^10.1.3", - "prettier": "^3.5.3", "srt-parser-2": "^1.2.3", "ts-bus": "^2.3.1", "ts-node": "^10.9.2", @@ -32,6 +32,17 @@ }, "devDependencies": { "@swc/core": "^1.11.20", + "@types/lodash": "^4.17.20", + "@types/m3u8-parser": "^7.2.3", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.57.1", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-simple-import-sort": "^12.1.1", + "husky": "^9.1.7", + "lint-staged": "^16.1.6", + "prettier": "3.6.2", "terser": "^5.37.0", "video.js": "^8.23.3" } @@ -1530,6 +1541,225 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", + "integrity": "sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1614,6 +1844,44 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", @@ -2167,6 +2435,13 @@ "win32" ] }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@svta/common-media-library": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.7.4.tgz", @@ -2429,6 +2704,34 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/m3u8-parser": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@types/m3u8-parser/-/m3u8-parser-7.2.3.tgz", + "integrity": "sha512-5qleblo43RShftqx+2bX/xmfSf3r6FQ8OvbHtwxgZcGuJKIDzSw4Hi7nu13k9B5qwPIixKlPN6za/ah4NH7dbg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.10.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz", @@ -2438,6 +2741,373 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, "node_modules/@videojs/http-streaming": { "version": "3.17.0", "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.17.0.tgz", @@ -2511,9 +3181,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2522,6 +3192,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", @@ -2547,6 +3227,39 @@ "pkcs7": "^1.0.4" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -2583,10 +3296,152 @@ "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "license": "MIT" }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "license": "MIT" }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -2624,6 +3479,22 @@ "postcss": "^8.1.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2704,8 +3575,8 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -2790,6 +3661,25 @@ "node": ">=8" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2819,10 +3709,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { - "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "version": "1.0.30001739", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", + "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", "funding": [ { "type": "opencollective", @@ -2839,6 +3739,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chalk": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz", + "integrity": "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2854,6 +3767,64 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/codem-isoboxer": { "version": "0.3.10", "resolved": "https://registry.npmjs.org/codem-isoboxer/-/codem-isoboxer-0.3.10.tgz", @@ -2878,6 +3849,13 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/colorjs.io": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", @@ -2894,6 +3872,13 @@ "node": ">= 6" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/consola": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz", @@ -3108,6 +4093,60 @@ "node": "*" } }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "2.6.9", "license": "MIT", @@ -3115,6 +4154,49 @@ "ms": "2.0.0" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/depd": { "version": "2.0.0", "license": "MIT", @@ -3152,6 +4234,32 @@ "node": ">=0.3.1" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", @@ -3181,9 +4289,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.194", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz", - "integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==", + "version": "1.5.214", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.214.tgz", + "integrity": "sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -3199,6 +4307,88 @@ "node": ">= 0.8" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "license": "MIT", @@ -3223,6 +4413,53 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -3303,6 +4540,548 @@ "version": "1.0.3", "license": "MIT" }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-simple-import-sort": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", + "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "license": "MIT", @@ -3316,6 +5095,13 @@ "integrity": "sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==", "license": "MIT" }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, "node_modules/express": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", @@ -3368,6 +5154,60 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", @@ -3382,12 +5222,25 @@ } } }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3411,6 +5264,107 @@ "node": ">= 0.8" } }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/flat-cache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flat-cache/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -3454,6 +5408,13 @@ "node": ">= 0.6" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3475,6 +5436,50 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.1.tgz", + "integrity": "sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3510,6 +5515,24 @@ "node": ">= 0.4" } }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -3530,6 +5553,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/global": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", @@ -3541,6 +5577,60 @@ "process": "^0.11.10" } }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "license": "MIT", @@ -3551,16 +5641,64 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "license": "MIT", @@ -3571,6 +5709,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "license": "MIT", @@ -3617,6 +5771,22 @@ "node": ">= 0.8" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "license": "MIT", @@ -3627,6 +5797,16 @@ "node": ">=0.10.0" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -3639,6 +5819,33 @@ "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", "license": "MIT" }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/imsc": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/imsc/-/imsc-1.1.5.tgz", @@ -3648,10 +5855,47 @@ "sax": "1.2.1" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "license": "ISC" }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "license": "MIT", @@ -3683,6 +5927,90 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -3698,6 +6026,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-decimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", @@ -3712,12 +6075,28 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3734,12 +6113,31 @@ "dev": true, "license": "MIT" }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -3747,16 +6145,221 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3794,12 +6397,57 @@ "license": "MIT", "peer": true }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/keycode": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz", "integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==", "license": "MIT" }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lie": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", @@ -3827,6 +6475,130 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "license": "MIT" }, + "node_modules/lint-staged": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.1.6.tgz", + "integrity": "sha512-U4kuulU3CKIytlkLlaHcGgKscNfJPNTiDF2avIUGFCv7K95/DCYQ7Ra62ydeRWmgQGg9zJYw2dzdbztwJlqrow==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.0", + "commander": "^14.0.0", + "debug": "^4.4.1", + "lilconfig": "^3.1.3", + "listr2": "^9.0.3", + "micromatch": "^4.0.8", + "nano-spawn": "^1.0.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/lint-staged/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/lint-staged/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.3.tgz", + "integrity": "sha512-0aeh5HHHgmq1KRdMMDHfhMWQmIT/m7nRDTlxlFqni2Sp0had9baqsjJRvDGdlvgd6NmPE0nPloOipiQJGFtTHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/load-tsconfig": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", @@ -3845,12 +6617,121 @@ "lie": "3.1.1" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "license": "MIT" }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3909,6 +6790,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/methods": { "version": "1.1.2", "license": "MIT", @@ -3920,8 +6811,8 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -3934,8 +6825,8 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=8.6" }, @@ -3970,6 +6861,19 @@ "node": ">= 0.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-document": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", @@ -3994,6 +6898,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -4052,6 +6966,19 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nano-spawn": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-1.0.2.tgz", + "integrity": "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -4070,6 +6997,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "license": "MIT", @@ -4109,7 +7043,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.3", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4118,6 +7054,90 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-finished": { "version": "2.4.1", "license": "MIT", @@ -4128,12 +7148,87 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parseurl": { "version": "1.3.3", "license": "MIT", @@ -4147,6 +7242,26 @@ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "license": "MIT" }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4182,6 +7297,16 @@ "version": "0.1.12", "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4200,6 +7325,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -4222,6 +7360,16 @@ "pkcs7": "bin/cli.js" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", @@ -4984,10 +8132,21 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -5054,6 +8213,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "license": "MIT", @@ -5109,6 +8289,29 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -5116,6 +8319,27 @@ "dev": true, "license": "MIT" }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -5145,6 +8369,41 @@ "node": ">=8" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.32.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.0.tgz", @@ -5183,6 +8442,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -5193,6 +8476,26 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "funding": [ @@ -5217,6 +8520,41 @@ "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", "license": "ISC" }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "license": "MIT" @@ -5632,6 +8970,16 @@ "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", "license": "ISC" }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/send": { "version": "0.19.0", "license": "MIT", @@ -5678,6 +9026,55 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "license": "ISC" @@ -5779,6 +9176,46 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map": { "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", @@ -5840,6 +9277,30 @@ "node": ">= 0.8" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -5899,6 +9360,65 @@ "node": ">=8" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -5936,6 +9456,19 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -6035,6 +9568,13 @@ "dev": true, "license": "MIT" }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6079,8 +9619,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "is-number": "^7.0.0" }, @@ -6113,6 +9653,19 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-bus": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/ts-bus/-/ts-bus-2.3.1.tgz", @@ -6174,6 +9727,42 @@ } } }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -6255,6 +9844,32 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "license": "MIT", @@ -6266,6 +9881,84 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -6305,6 +9998,25 @@ "node": "*" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -6348,6 +10060,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6468,6 +10190,105 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -6559,13 +10380,19 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "devOptional": true, "license": "ISC", - "optional": true, - "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -6582,6 +10409,19 @@ "node": ">=6" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "vendors/@silvermine/videojs-chromecast": { "version": "1.5.0", "extraneous": true, diff --git a/package.json b/package.json index 0e7ee9c..8762749 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,20 @@ "version": "0.0.1", "description": "VokaPlayer", "scripts": { - "lint:fix": "npm run lint -- --fix", "build": "tsup --env.NODE_ENV development", "build-prod": "tsup --env.NODE_ENV production", - "postinstall": "npm install ./vendors/@silvermine/videojs-chromecast --no-package-lock --no-save" + "postinstall": "npm install ./vendors/@silvermine/videojs-chromecast --no-package-lock --no-save", + "prepare": "husky", + "lint": "eslint --ext .ts,.tsx src", + "lint:fix": "eslint --ext .ts,.tsx src --fix", + "format": "prettier --write .", + "ci:check": "eslint --ext .ts,.tsx . && prettier --check .", + "lint-staged:fix": "npx lint-staged" }, "lint-staged": { "**/*.ts": [ - "npx prettier --write", - "npx eslint --fix" + "eslint --config .eslintrc.lenient.cjs --fix --ext .ts --fix --no-error-on-unmatched-pattern", + "prettier --write" ] }, "author": "PLSolutions", @@ -27,9 +32,9 @@ "express": "^4.21.2", "hls.js": "^1.5.20", "keycode": "^2.2.1", + "lodash": "^4.17.21", "postcss": "^8.5.1", "postcss-preset-env": "^10.1.3", - "prettier": "^3.5.3", "srt-parser-2": "^1.2.3", "ts-bus": "^2.3.1", "ts-node": "^10.9.2", @@ -39,6 +44,17 @@ }, "devDependencies": { "@swc/core": "^1.11.20", + "@types/lodash": "^4.17.20", + "@types/m3u8-parser": "^7.2.3", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.57.1", + "eslint-config-prettier": "^9.1.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-simple-import-sort": "^12.1.1", + "husky": "^9.1.7", + "lint-staged": "^16.1.6", + "prettier": "3.6.2", "terser": "^5.37.0", "video.js": "^8.23.3" } diff --git a/src/assets/scss/components/_layout.scss b/src/assets/scss/components/_layout.scss index e9525d3..aabcc23 100644 --- a/src/assets/scss/components/_layout.scss +++ b/src/assets/scss/components/_layout.scss @@ -11,7 +11,7 @@ url('data:font/ttf;base64,AAEAAAARAQAABAAQR0RFRiBqII8AAAEcAAAA7kdQT1POA1gqAAACDAAAK6BHU1VCagczrwAALawAAAGoT1MvMqCnsaoAAC9UAAAAYGNtYXDxTp1yAAAvtAAABopjdnQgv4wcAAAANkAAAAEYZnBnbT+uIasAADdYAAAL4mdhc3AAAAAQAABDPAAAAAhnbHlm/xSlmAAAQ0QAAJIwaGVhZAF1UP4AANV0AAAANmhoZWEQqQyUAADVrAAAACRobXR4/FlxHgAA1dAAAAYAbG9jYXJ3TakAANvQAAADAm1heHADUw1wAADe1AAAACBuYW1lr/Sv5QAA3vQAAARocG9zdK5ApKcAAONcAAALy3ByZXDvF1bSAADvKAAAANUAAQAAAAwAAAAAAAAAAgAlACYAPwABAEYAXwABAHUAdQABAHwAgAABAIMAgwABAIcAhwABAIkAiwABAI0AnQABAJ8AnwABAKEAowABAKUApQABAKoAqwABALgAvAABAL4AvgABAMIAxAABAMYAxwADAM0AzgABANYA1wABANkA2QABAN4A3wABAOQA5wABAOoA6wABAO4A8AABAPQA9AABAPcA+wABAQABAQABAQIBBQACAQoBEwABARUBHgABASABNwABATkBOwABAT4BQgABAUQBcgABAXUBdQABAXgBeQABAXsBfAABAX4BfwABAAAAAQAAAAoAMgBMAARERkxUABpjeXJsABpncmVrABpsYXRuABoABAAAAAD//wACAAAAAQACa2VybgAObWFyawAUAAAAAQAAAAAAAQABAAIABiVWAAIAAAACAAoWrgABAWYABAAAAK4CxgLkAuQC6gMIFEwT/hQkFNAUmAMOFDYUNhZYA/QUNhQ2FNAUBAP6BDQVThQSBGoUqBaCBHwEghSCFPAUogSQFcoE9hXKFcoU2hTwBUgFVhVABWwUyhVABXYFhAbmCAgJPhQ2ChwLRhSoDDAUNhQ2FQIUNhQ2FDYMrg2MFDYV0A4GDoAO8g8cD9IVwBAUFMoQuhDEES4U8BHwEiYSrBPaFPAS/hNcE2YTsBPaFEwU2hQSFUAT9BTwFeoVwBSoFMoWWBZYFlgUNhSoFMoUNhQ2FNAVwBSoFMoUmBXqFDYUNhZYFlgVAhRME/4V6hSYFDYUNhTQFAQUJBVOFKgUghSiFNoU8BVAFMoUohXAFUAUmBSiFBIVQBQkFoIVQBQ2FKgUyhQ2FEwUghRMFIIUmBSiFKIUohSoFMoU0BTaFNoU8BUCFUAVAhVAFQIVQBVOFeoVwBXKFdAV6hZYFoIAAQCuAAUABwAMAA0AFAAmACcAKAApACoAKwAtAC4AMAAxADIAMwA0ADUANgA3ADkAOwA8AD0APgA/AEAARgBHAEoASwBNAFAAUgBTAFQAVQBXAFkAWwBcAF0AXgBgAHQAdgB3AHgAeQB6AHsAfAB9AH4AfwCAAIIAgwCEAIUAhgCHAIgAiQCKAIsAjACNAI4AjwCQAJEAkwCYAJkAmgCdAJ4AoAChAKIApQCmAKcArACvALoAuwC8AMQA0QDSANMA1gDXANgA2gDcAN4A5gDnAPcA+QD7AP8BAAEBAQ4BDwERARIBFAEVARYBFwEYARkBGgEcAR0BHgEgASEBIgEjASQBJQEnASgBKgErASwBLQEzATQBNgE6ATsBQAFCAUMBRAFFAUYBRwFIAUkBSgFLAU4BTwFRAVIBUwFUAVsBXAFgAWIBYwFkAWUBZgFnAWgBbQFxAXIBdQF3AXgBegF8AAcAOf/GAHT/xgB4/8YA3P/GAOj/xgEi/8YBbf/GAAEAXAALAAcAOwAUADwAJAA+ABYAuwAUAToAFAFCABYBfAAWAAEAFP8KADkAEf7yABP+8gAm/0IAL/8qADkAFABG/94ASP/rAEn/6wBK/+sATP/rAFT/6wBW/+sAV//mAFr/6gBb/+gAXv/oAHQAFAB4ABQAmf/rAKX/6wCv/0IAuv/rALz/6ADA/+sAxP/rANwAFADl/+sA6AAUARP/KgEX/0IBIgAUAST/3gEl/+sBJ//rASn/6wEq/+gBLP/rATP/6AE2/+sBO//oATz/6wFB/+sBQ//oAUj/QgFJ/94BSv9CAUv/3gFP/+sBUf/rAVL/6wFc/+sBXv/rAWD/6wFk/+gBZv/oAWj/6AFtABQAAQBc/8gADgA5/94AO//kADz/7AA+/90AdP/eAHj/3gC7/+QA3P/eAOj/3gEi/94BOv/kAUL/3QFt/94BfP/dAA0AOf/LADv/7QA+/9AAdP/LAHj/ywC7/+0A3P/LAOj/ywEi/8sBOv/tAUL/0AFt/8sBfP/QAAQADgAPAEIADABX/+sAYgAOAAEAXP/lAAMAL//uADr/7gET/+4AGQAHABAADAAQAA4AFABCABIASP/oAEn/6ABK/+gATP/oAFb/6ABiABMAmf/oAKX/6ADA/+gAxP/oAOX/6AEl/+gBKf/oASz/6AE2/+gBPP/oAUH/6AFP/+gBUf/oAVL/6AFe/+gAFABI/+wASf/sAEr/7ABM/+wAVv/sAJn/7ACl/+wAwP/sAMT/7ADl/+wBJf/sASn/7AEs/+wBNv/sATz/7AFB/+wBT//sAVH/7AFS/+wBXv/sAAMASwAUAFkAMgBcABEABQBU/+MAuv/jASf/4wFc/+MBYP/jAAIAEf+EABP/hAADAC//7AA6/+wBE//sAFgAB//KAAz/ygA5/9IAO//UAD3/9AA+/9MAUv/7AFP/+wBV//sAW//mAF3/7wBe/+YAdP/SAHj/0gB8//QAgP/tAIP/4QCI/9QAjv/7AJD/7wCS//sAk//7AJX/+wCW//sAl//7AJj/yQCa//sAnP/7AJ3/+wCg/9YAov/7AKf/+wCr/+UAu//UALz/5gDC/+MAzv/7ANb/9ADX/+8A2f/7ANz/0gDd/8QA3//7AOH/+wDm//QA5//vAOj/0gDq/+EA7P/hAPb/+wD4//sA+v/7AP//+wEA//QBAf/vARb/7QEi/9IBI//0ASb/+wEo//sBKv/mASv/7wEt//sBMv/7ATP/5gE3//sBOv/UATv/5gFC/9MBQ//mAUX/9AFG/+8BU//0AVT/7wFY//sBWv/7AWP/7QFk/+YBZf/tAWb/5gFn/+0BaP/mAWn/4QFs//sBbf/SAW//4QFy//sBfP/TAEgAB//GAAz/xgA5/6cAO//NAD3/8gA+/7MAUv/XAFP/1wBV/9cAdP+nAHb/9gB4/6cAfP/yAH//9gCA/+wAg//oAIj/xwCO/9cAkv/XAJP/1wCV/9cAlv/XAJf/1wCY/9IAmv/XAJz/1wCd/9cAoP/XAKL/1wCn/9cAu//NAM7/1wDW//IA2f/XANz/pwDd/9EA3//XAOH/1wDm//IA6P+nAOr/6ADs/+gA7//fAPP/9gD2/9cA+P/XAPr/1wD//9cBAP/yARb/7AEi/6cBI//yASb/1wEo/9cBLf/XATL/1wE3/9cBOv/NAUL/swFF//IBU//yAVj/1wFa/9cBY//sAWX/7AFn/+wBaf/oAWz/1wFt/6cBb//oAXL/1wF8/7MATQAH/7kADP+5ADn/qAA7/8sAPf/zAD7/sQBS/9QAU//UAFX/1ABd//EAdP+oAHj/qAB8//MAgP/uAIP/5ACI/8gAjv/UAJD/8QCS/9QAk//UAJX/1ACW/9QAl//UAJj/zACa/9QAnP/UAJ3/1ACg/9QAov/UAKf/1AC7/8sAzv/UANb/8wDX//EA2f/UANz/qADd/8YA3//UAOH/1ADm//MA5//xAOj/qADq/+QA7P/kAO//3wD2/9QA+P/UAPr/1AD//9QBAP/zAQH/8QEW/+4BIv+oASP/8wEm/9QBKP/UASv/8QEt/9QBMv/UATf/1AE6/8sBQv+xAUX/8wFG//EBU//zAVT/8QFY/9QBWv/UAWP/7gFl/+4BZ//uAWn/5AFs/9QBbf+oAW//5AFy/9QBfP+xADcAOf++AFL/5ABT/+QAVf/kAFv/7wBe/+8AdP++AHj/vgCI/8kAjv/kAJL/5ACT/+QAlf/kAJb/5ACX/+QAmP/fAJr/5ACc/+QAnf/kAKD/5ACi/+QAp//kAKv/7QC8/+8Awv/rAM7/5ADZ/+QA3P++AN3/3wDf/+QA4f/kAOj/vgDv/+kA9v/kAPj/5AD6/+QA///kASL/vgEm/+QBKP/kASr/7wEt/+QBMv/kATP/7wE3/+QBO//vAUP/7wFY/+QBWv/kAWT/7wFm/+8BaP/vAWz/5AFt/74Bcv/kAEoAOf/mADv/5wA9//IAPv/nAFL/2gBT/9oAVf/aAF3/8QB0/+YAeP/mAHz/8gCA/+4Ag//oAIj/5gCO/9oAkP/xAJL/2gCT/9oAlf/aAJb/2gCX/9oAmP/QAJr/2gCc/9oAnf/aAKD/2gCi/9oAp//aALv/5wDO/9oA1v/yANf/8QDZ/9oA3P/mAN3/zgDf/9oA4f/aAOb/8gDn//EA6P/mAOr/6ADs/+gA9v/aAPj/2gD6/9oA///aAQD/8gEB//EBFv/uASL/5gEj//IBJv/aASj/2gEr//EBLf/aATL/2gE3/9oBOv/nAUL/5wFF//IBRv/xAVP/8gFU//EBWP/aAVr/2gFj/+4BZf/uAWf/7gFp/+gBbP/aAW3/5gFv/+gBcv/aAXz/5wA6ACYAEAAo/+gALP/oADT/6AA2/+gAOf/gADv/4AA+/98AdP/gAHX/6AB2ABAAeP/gAHsAFAB/ABAAg//hAIj/4ACPABMAlAAQAJv/4ACmABAAqv/oAK8AEAC5/+gAu//gAL3/6AC//+gAwf/oAMP/6ADc/+AA5P/oAOj/4ADq/+EA6//gAOz/4QDt/+AA8P/hAPMAEAD0ABAA+//pARcAEAEe/+gBIf/oASL/4AE6/+ABPf/oAUD/6AFC/98BSAAQAUoAEAFb/+gBXf/oAV//6AFp/+EBav/gAW3/4AFv/+EBcP/gAXz/3wAfABz/8gA5//EAO//0AD3/9AA+//AAdP/xAHb/9QB4//EAfP/0AH//9QCA//MAiP/xALv/9ADW//QA3P/xAOb/9ADo//EA8//1AQD/9AEW//MBIv/xASP/9AE6//QBQv/wAUX/9AFT//QBY//zAWX/8wFn//MBbf/xAXz/8AA3ACYADwA5/+YAO//mAD0ADgA+/+YAdP/mAHYADgB4/+YAewATAHwADgB/AA4AgAALAIP/5QCI/+YAif/0AI8AEgCUAA8AmP/nAJv/6ACg/+kApgAPAK8ADwC7/+YA1gAOANz/5gDd/+cA5gAOAOj/5gDq/+UA6//oAOz/5QDt/+gA7//kAPMADgD0AA8BAAAOARYACwEXAA8BIv/mASMADgE6/+YBQv/mAUUADgFIAA8BSgAPAVMADgFjAAsBZQALAWcACwFp/+UBav/oAW3/5gFv/+UBcP/oAXz/5gAeAAf/xQAM/8UAOf+pADv/zgA+/7UAdP+pAHj/qQCA/+4Ag//pAIj/ygCY/9IAoP/ZALv/zgDc/6kA3f/RAOj/qQDq/+kA7P/pAO//3wEW/+4BIv+pATr/zgFC/7UBY//uAWX/7gFn/+4Baf/pAW3/qQFv/+kBfP+1AB4AOf/jAD3/5QA+/+QAdP/jAHb/5QB4/+MAe//iAHz/5QB//+UAgP/pAJT/6gCm/+oA1v/lANz/4wDm/+UA6P/jAPP/5QD0/+oBAP/lARb/6QEi/+MBI//lAUL/5AFF/+UBU//lAWP/6QFl/+kBZ//pAW3/4wF8/+QAHAA5/+IAPf/kAHT/4gB2/+QAeP/iAHv/4QB8/+QAf//kAID/6QCP/+QAlP/rAKb/6wDW/+QA3P/iAOb/5ADo/+IA8//kAPT/6wEA/+QBFv/pASL/4gEj/+QBRf/kAVP/5AFj/+kBZf/pAWf/6QFt/+IACgA5/+sAPv/zAHT/6wB4/+sA3P/rAOj/6wEi/+sBQv/zAW3/6wF8//MALQBS/+8AU//vAFX/7wBd//AAjv/vAI//7gCQ//AAkv/vAJP/7wCV/+8Alv/vAJf/7wCY/+4Amv/vAJz/7wCd/+8AoP/xAKL/7wCn/+8Aq//0AML/8QDO/+8A1//wANn/7wDd/+8A3//vAOH/7wDn//AA9v/vAPj/7wD6/+8A///vAQH/8AEm/+8BKP/vASv/8AEt/+8BMv/vATf/7wFG//ABVP/wAVj/7wFa/+8BbP/vAXL/7wAQAAf/8gAM//IAW//1AF7/9QCY//QAoP/1AKv/9QC8//UA3f/1ASr/9QEz//UBO//1AUP/9QFk//UBZv/1AWj/9QApAFL/8ABT//AAVf/wAI7/8ACPABQAkv/wAJP/8ACV//AAlv/wAJf/8ACY/+0Amv/wAJv/7QCc//AAnf/wAJ7/0gCg//AAov/wAKf/8ADO//AA2f/wAN3/7QDf//AA4f/wAOv/7QDt/+0A9v/wAPj/8AD6//AA///wASb/8AEo//ABLf/wATL/8AE3//ABWP/wAVr/8AFq/+0BbP/wAXD/7QFy//AAAgAH//UADP/1ABoASP/wAEn/8ABK//AATP/wAFT/ywBW//AAmf/wAKX/8AC6/8sAvv/rAMD/8ADE//AA5f/wASX/8AEn/8sBKf/wASz/8AE2//ABPP/wAUH/8AFP//ABUf/wAVL/8AFc/8sBXv/wAWD/ywAwAAcADQAMAA0ARv/wAEj/vgBJ/74ASv++AEsADQBM/74AVP/hAFb/vgBbAAsAXgALAI//xQCU/9MAmf++AKX/vgCm/9MAuv/hALwACwC+/+sAwP++AMIADADE/74A5f++APT/0wEk//ABJf++ASf/4QEp/74BKgALASz/vgEzAAsBNv++ATsACwE8/74BQf++AUMACwFJ//ABS//wAU//vgFR/74BUv++AVz/4QFe/74BYP/hAWQACwFmAAsBaAALAA0AjwAUAJQAEACY//AAm//wAKD/8gCjABUApgAQAN3/5wDr//AA7f/dAPQAEAFq//ABcP/wACEASP/xAEn/8QBK//EATP/xAFb/8QCPABYAlAAPAJj/4wCZ//EAm//lAJ7/vwCg/+YApf/xAKYADwDA//EAxP/xAN3/vgDl//EA6//lAO3/2gD0AA8BJf/xASn/8QEs//EBNv/xATz/8QFB//EBT//xAVH/8QFS//EBXv/xAWr/5QFw/+UAFABb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AAAAAAAAAAAAAAAAAAAAAwAbAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAHwAYAAAAAAAQABIACwAAAAsAAAALAAAAAAAAAAAAAAAAAAAAAAAAABAAEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAQABIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAfAAAAAAAAAAoACwALACAAAgAaAB8ABQAAAAAAAAABAAAAFAARAA0AEAAGAAgAAAADAAcAFQAJABIACAAYAAAAAAAAAAAAAAAJAAUAAAAIAAAAAAAAABsACQAAAAAAAAAAABEAFQAMAAkAAAAQABIAAAACAAYAAgAGAAAAAAAFAAgAAAAIAAgAEAASAAAAAAAAAAAAAAAAAAEAAwAAAAAAAAADAAAABwAgAAkAIAAJACAACQAAAAAAAAAAAA0AAAAAAAAAHwAYAAAAAAAEAAAAHQAfAAAACwAAAAwAAQAHAXYAEQAAAAAAAAAAABEAAAAAAAAAAAAVABgAFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAgAAAAAAAAACAAAAAAAcAAAAAAAAAAAAAgAAAAIAAAAOAAoACQAXABMADwALABAAAAAAAAAAAAAAAAAABwAAAAEAAQABAAAAAQAAAAAAAAAAAAAAAwADAAQAAwABAAAADAAAAAUACAAAABIACAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAIAFAAAAAoAAAAAAAAADwAAAAAAFAAbAAAAAAAZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAABIAAAADAAMAFgADAAMAAwAAAAEAAwAaAAMAAwAAAAAAAwAAAAMAAAAAAAEAFgADAAAAAAACAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAACAAQAFwAIAAIAAAACAAEAAgAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAADwASAAAAAwAAAAAACgAAAAAAAwAAAAMAAAAAAAIAAQAPABIACgAAABkAGgAAAAAAAAAAAAAAAAAAABQAFgAAAAMAAAADAAAAAwAAAAAAAAAAAAMADwASAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAAbAAYAAAAAAAAAAAAAAAAAAgAAAAAAAgAKAA8ABwABAAMABAADAAEACAASAAEAAwAMAAAAAAAAAAMACAAAAAAAAQADAAAAAAAXAAgAAQACAAAAAAACAAEACwAIAAAADwASAAAABgAHAAYABwAAAAAAAAABAAAAAQABAA8AEgAAAAAAAAADAAAAAwACAAQAAgABAAIABAAAAAAAGwAIABsACAAbAAgAGQAaAAAAAwAKAAAAGQAaAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAALAAQAAAABAAgAAQAMABYAAQDmARoAAgABAMYAygAAAAIAIgAmAD8AAABGAF8AGgB1AHUANAB8AH4ANQCAAIAAOACDAIMAOQCHAIcAOgCJAIsAOwCNAJ0APgCfAJ8ATwChAKMAUAClAKUAUwCqAKsAVAC4ALwAVgC+AL4AWwDCAMQAXADNAM4AXwDkAOUAYQDuAO4AYwD7APsAZAEKAQ8AZQESARIAawEVARYAbAEbARsAbgEkASYAbwEoASkAcgErATAAdAEyATcAegE5ATsAgAFFAUYAgwFIAUsAhQFOAVAAiQFSAVwAjAFfAWwAlwAFAAAAFgAAABwAAAAiAAAAKAAAAC4AAf3SBKYAAf5EBKYAAf3cBQEAAf4aBQEAAf2sBKYApQFMAZoBUgFYAV4BXgFkAWoBcAF2AXwBggGIAY4BlAGaAkgBoAGmAawBsgG4Ab4BxAHKAdADmAHWA7AB3AOeAeIB6AHuA84B9AH6AgACBgIMAhIDqgIYAh4DyAIkAioCMAI2A7YCPAJCAkgCTgJUAloCYAJmAmwCcgJ4An4ChAKKApAClgKcAqICqAKuArQCugLAAsYCzALSAtgC8ALeAuQC6gLwAvYC/AMCAwgDDgMUAxoDIAMmAywDMgM4Az4DRANKA1ADVgNcA2IDaANoA24DbgN0A3oDgAOGA4wDkgOYA54DpAOqA7ADtgO8A8IDyAPOA9QD2gPgA+YD7APyA/gD/gQEBAoEEAQWBBwEIgQoBC4ENAQ6BEAERgRMBFIEWAReBGQEagRwBHYEfASCBIgEjgSUBJoEoASmBKwEsgS4BL4ExATKBNAE1gABAqAGQQABAqgGYQABAmEGTAABAmsGTAABAqAGYQABAsgGTAABARcGTAABA1QGPwABAl0GOwABAQgGPAABA3oGQQABAtYGQQABAsMGQwABAl4GTAABAmIGQQABAm4GQwABAmQGQAABApsGQQABAo8GTAABA5wGQQABAo0GTAABAmoGQAABAmgGQQABAnoGSwABAiUGTAABAfYG2gABAiwFCgABAfUGSwABAN8GQAABAiUGSwABAPkGoQABA48FCgABAj0FCgABAj8FCgABAhkFCgABAZgFCgABATgFyQABAjcFCgABAfkFAAABAvsFCgABAfwFCgABAgMFCgABArwGQAABA6UGTAABAl4GYQABAt0GTAABAoYGTAABArAGTAABA3oGTAABArQGYgABA5QGQAABAooGTAABAiUE9QABAa8E9QABArQE9QABAwwE9QABAgYFCQABAk4E9QABAh0E9AABAoQE9QABAvAE9QABAlIE9QABAlgE9QABAecE9QABAuQGzAABAnME9QABAgwE9QABAywE9QABAxQE9QABAiMFCgABAzsE9QABAg0E9QABAlIFCAABA4IGGgABAwQE9QABAdIGzAABAs8GRwABAi8FDAABAm0GCAABAgcE4AABAkoFUAABAwkGQAABApwGYQABAj4FDAABAt0HYwABAk4GDAABArsGUQABAkIFCgABAoIGTAABAnsGHgABAQoE4QABAPwFCAABAmsHawABAnEHqwABARcHawABAl0HmgABAoYHiAABAt0HiAABAkYFCgABAjUFCgABAk4GMQABAnUFAAABAhUFCgABAfwFAAABAjUGKQABAa8GVAABAjIFCgABAPsGQAABAPwGJwABAh0GUwABAfwGRgABAmsHqwABAt0HqwABAjUGaQABAk4GVAABAscE9QABAm0HOQABAgcGEQABA6UHiAABAwwGMQABAqAHfQABAkYGRgABAqAHYAABAkYGKQABAmsHiAABAjUGRgABAnsHPQABAi8GKgABA6UHawABAwwGFAABAl4HgAABAgYGKAABAt0HcwABAk4GHAABAt0HawABAk4GFAABAsMHYgABAj8GKQABAs8HZgABAi8GKwABArQHgQABAiMGKQABAoYHcwABAfwGMQABAoYHawABAfwGKQABAoYHkAABAfwGTgABArAHawABAgwGFAABA3oHawABAxQGFAABAAAACgB8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uQgACABjYyMgWGIbIVljuAQAYiCwAFBYsEBgWWawAWNgIy4jICA8ijgjIVktsD0ssAAWILAIQyAuRyNHI2EgYLAgYGawAmIgsABQWLBAYFlmsAFjIyAgPIo4LbA+LCMgLkawAiVGUlggPFkusS4BFCstsD8sIyAuRrACJUZQWCA8WS6xLgEUKy2wQCwjIC5GsAIlRlJYIDxZIyAuRrACJUZQWCA8WS6xLgEUKy2wQSywOCsjIC5GsAIlRlJYIDxZLrEuARQrLbBCLLA5K4ogIDywBCNCijgjIC5GsAIlRlJYIDxZLrEuARQrsARDLrAuKy2wQyywABawBCWwBCYgLkcjRyNhsAlDKyMgPCAuIzixLgEUKy2wRCyxCAQlQrAAFrAEJbAEJSAuRyNHI2EgsAQjQrAJQysgsGBQWCCwQFFYswIgAyAbswImAxpZQkIjIEewBEOwAmIgsABQWLBAYFlmsAFjYCCwASsgiophILACQ2BkI7ADQ2FkUFiwAkNhG7ADQ2BZsAMlsAJiILAAUFiwQGBZZrABY2GwAiVGYTgjIDwjOBshICBGI0ewASsjYTghWbEuARQrLbBFLLA4Ky6xLgEUKy2wRiywOSshIyAgPLAEI0IjOLEuARQrsARDLrAuKy2wRyywABUgR7AAI0KyAAEBFRQTLrA0Ki2wSCywABUgR7AAI0KyAAEBFRQTLrA0Ki2wSSyxAAEUE7A1Ki2wSiywNyotsEsssAAWRSMgLiBGiiNhOLEuARQrLbBMLLAII0KwSystsE0ssgAARCstsE4ssgABRCstsE8ssgEARCstsFAssgEBRCstsFEssgAARSstsFIssgABRSstsFMssgEARSstsFQssgEBRSstsFUssgAAQSstsFYssgABQSstsFcssgEAQSstsFgssgEBQSstsFkssgAAQystsFossgABQystsFsssgEAQystsFwssgEBQystsF0ssgAARistsF4ssgABRistsF8ssgEARistsGAssgEBRistsGEssgAAQistsGIssgABQistsGMssgEAQistsGQssgEBQistsGUssDorLrEuARQrLbBmLLA6K7A+Ky2wZyywOiuwPystsGgssAAWsDorsEArLbBpLLA7Ky6xLgEUKy2waiywOyuwPistsGsssDsrsD8rLbBsLLA7K7BAKy2wbSywPCsusS4B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BT8AqQSMAKkEbACpBXMAegW0AKkCLQC3BGoANQUEAKkETgCpBvwAqQW0AKkFgAB2BQwAqQWAAG0E7QCoBL8AUATGADEFMACMBRcAHAcZAD0FBAA5BM4ADwTKAFYCHwCSA0gAKAIfAAkDWABAA5wABAJ5ADkEWgBtBH0AjAQwAFwEgwBfBD0AXQLHADwEfQBgBGgAjAHxAI0B6f+/BA4AjQHxAJwHAwCLBGoAjASQAFsEfQCMBIwAXwK1AIwEIABfAp0ACQRpAIgD4AAhBgMAKwP3ACkDyQAWA/cAWAK1AEAB8wCvArUAEwVxAIMDWABlA6oAjgKCAHsHev/yBsEATgH6AJsCA/+0BDcAYgNqAIEC/ABeAAD8JwRzALEFtQCyBYkAWgQCAC4FmgBfBf8AKgVkAHsIkQAxCKQAsQaCAD4FtACwBQsAogYEADIHQwAbBL8AUAW0ALEFqQAvBQcATQYsAFMF2QCvBXoAlgeHALAHwACwBhIAEAbrALIFBQCjBWQAkwcnALcFGABZBGwAYQSSAJ0DWwCaBNQALgYgABUEEABYBJ4AnARSAJwEoAAsBe8AnQSdAJwEngCcA9gAKAXNAGQEvQCcBFkAZwZ4AJwGngCRBPcAHgY2AJ0EWACdBE0AZAaHAJ0EZAAvBGj/6ARNAGcGyQAnBuQAnASJ//0EngCcBwgAnAYrAIEEVv/cBysAtwX4AJkE0gAoBEYADwcLAMkGCwC8BtEAkwXhAJYJBAC2B9EAmwQjAFAD2wBMBXEAZwSLAFsFCgAWBAMALgVxAGcEiABbBwEAnAYkAH4HCACcBisAgQUyAHUERwBkBP0AdAAA/GcAAPxxAAD9ZgAA/aQAAPwzAAD6GwAA+iwGCQCxBO0AnARW/9wFGwCoBIkAjARjAKIDkACRBNsAsQQFAJEHogAbBmEAFQWaALIEuACcBQkAowR+AJoGjABEBYMAPgX/AKkE2QCcB88AqAW0AJEIMQCwBvQAkQXuAHEE0wBtBRgAOQQqACkHLAA0BVwAHwW8AJYElgBnBW8AlgRqAIMFbwCJBi8APwS9/94FCQCjBFoAmgX+AC8E7wAsBbIAsQSIAJEGEgCpBOwAnAdPAKkGPgCdBYcAXQSoAGgEqABpBLcAOgOrADsFLgA5BEAAKQRuAB8EiwA8BtQAPAbUADwDlgCPBSQAsgGRAGcB/P+2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ZSBMaWNlbnNlLCBWZXJzaW9uIDIuMGh0dHA6Ly93d3cuYXBhY2hlLm9yZy9saWNlbnNlcy9MSUNFTlNFLTIuMABDAG8AcAB5AHIAaQBnAGgAdAAgADIAMAAxADEAIABHAG8AbwBnAGwAZQAgAEkAbgBjAC4AIABBAGwAbAAgAFIAaQBnAGgAdABzACAAUgBlAHMAZQByAHYAZQBkAC4AUgBvAGIAbwB0AG8AUgBlAGcAdQBsAGEAcgBWAGUAcgBzAGkAbwBuACAAMgAuADEAMwA0ADsAIAAyADAAMQA2ADsAIAB0AHQAZgBhAHUAdABvAGgAaQBuAHQAIAAoAHYAMQAuADQALgAxACkAUgBvAGIAbwB0AG8ALQBSAGUAZwB1AGwAYQByAFIAbwBiAG8AdABvACAAaQBzACAAYQAgAHQAcgBhAGQAZQBtAGEAcgBrACAAbwBmACAARwBvAG8AZwBsAGUALgBHAG8AbwBnAGwAZQBHAG8AbwBnAGwAZQAuAGMAbwBtAEMAaAByAGkAcwB0AGkAYQBuACAAUgBvAGIAZQByAHQAcwBvAG4ATABpAGMAZQBuAHMAZQBkACAAdQBuAGQAZQByACAAdABoAGUAIABBAHAAYQBjAGgAZQAgAEwAaQBjAGUAbgBzAGUALAAgAFYAZQByAHMAaQBvAG4AIAAyAC4AMABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBwAGEAYwBoAGUALgBvAHIAZwAvAGwAaQBjAGUAbgBzAGUAcwAvAEwASQBDAEUATgBTAEUALQAyAC4AMAACAAAAAAAA/2oAZAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAABAQIBAwACAAMABAAFAAYABwAIAAkACgALAAwADQAOAA8AEAARABIAEwAUABUAFgAXABgAGQAaABsAHAAdAB4AHwAgACEAIgAjACQAJQAmACcAKAApACoAKwAsAC0ALgAvADAAMQAyADMANAA1ADYANwA4ADkAOgA7ADwAPQA+AD8AQABBAEIAQwBEAEUARgBHAEgASQBKAEsATABNAE4ATwBQAFEAUgBTAFQAVQBWAFcAWABZAFoAWwBcAF0AXgBfAGAAYQCOANoAjQCQAKAA1wEEAQUA2wDfAQYBBwEIAQkBCgELAQwBDQEOAQ8BEAERARIBEwEUARUBFgEXARgBGQEaARsBHAEdAR4BHwEgASEBIgEjASQBJQEmAScBKAEpASoBKwEsAS0BLgEvATABMQEyATMBNAE1ATYBNwE4ATkBOgE7ATwBPQE+AT8BQAFBAUIBQwFEAUUBRgFHAUgBSQFKAUsBTAFNAU4BTwFQAVEBUgFTAVQBVQFWAVcBWAFZAVoBWwFcAV0BXgFfAWABYQFiAWMBZAFlAWYBZwFoAWkBagFrAWwBbQFuAW8BcAFxAXIBcwF0AXUBdgF3AXgBeQF6AXsBfAF9AX4BfwGAAYEBggGDAYQBhQGGAYcBiAGJAYoBiwGMAY0BjgGPAZABkQGSAZMBlAGVAZYBlwGYAZkAwAGaAZsBnAGdAZ4BnwGgAaEBogGjAaQBpQGmAacBqAGpAaoBqwGsAa0BrgGvAbABsQGyAbMBtAG1AbYBtwG4AbkBugG7AbwBvQG+Ab8BwAHBAcIBwwHEAcUBxgHHAcgByQHKAcsBzAHNAc4BzwHQAdEB0gHTAdQB1QHWAdcB2AHZAdoB2wHcAd0B3gHfAeAB4QHiAeMB5AHlAeYB5wHoAekB6gHrAewB7QHuAe8B8AHxAfIB8wH0AfUB9gH3AfgB+QH6AfsB/AH9Af4B/wIAAgECAgIDAgQCBQIGAgcCCAIJAgoCCwIMAg0CDgIPAhACEQISAhMCFAIVAhYHdW5pMDAwMgd1bmkwMDA5B3VuaTAyMzcFc2Nod2EHdW5pMDMwRgVHYW1tYQJQaQNQc2kFZ2FtbWEDcHNpB3VuaTA0MDIHdW5pMDQwNAd1bmkwNDA5B3VuaTA0MEEHdW5pMDQwQgd1bmkwNDBGB3VuaTA0MTEHdW5pMDQxNAd1bmkwNDE2B3VuaTA0MTcHdW5pMDQxOAd1bmkwNDFCB3VuaTA0MjMHdW5pMDQyNAd1bmkwNDI2B3VuaTA0MjcHdW5pMDQyOAd1bmkwNDI5B3VuaTA0MkEHdW5pMDQyQgd1bmkwNDJDB3VuaTA0MkQHdW5pMDQyRQd1bmkwNDJGB3VuaTA0MzEHdW5pMDQzMgd1bmkwNDMzB3VuaTA0MzQHdW5pMDQzNgd1bmkwNDM3B3VuaTA0MzgHdW5pMDQzQQd1bmkwNDNCB3VuaTA0M0MHdW5pMDQzRAd1bmkwNDNGB3VuaTA0NDIHdW5pMDQ0NAd1bmkwNDQ2B3VuaTA0NDcHdW5pMDQ0OAd1bmkwNDQ5B3VuaTA0NEEHdW5pMDQ0Qgd1bmkwNDRDB3VuaTA0NEQHdW5pMDQ0RQd1bmkwNDRGB3VuaTA0NTIHdW5pMDQ1NAd1bmkwNDU5B3VuaTA0NUEHdW5pMDQ1Qgd1bmkwNDVGB3VuaTA0NjAHdW5pMDQ2MQd1bmkwNDYzB3VuaTA0NjQHdW5pMDQ2NQd1bmkwNDY2B3VuaTA0NjcHdW5pMDQ2OAd1bmkwNDY5B3VuaTA0NkEHdW5pMDQ2Qgd1bmkwNDZDB3VuaTA0NkQHdW5pMDQ2RQd1bmkwNDZGB3VuaTA0NzIHdW5pMDQ3Mwd1bmkwNDc0B3VuaTA0NzUHdW5pMDQ3QQd1bmkwNDdCB3VuaTA0N0MHdW5pMDQ3RAd1bmkwNDdFB3VuaTA0N0YHdW5pMDQ4MAd1bmkwNDgxB3VuaTA0ODIHdW5pMDQ4Mwd1bmkwNDg0B3VuaTA0ODUHdW5pMDQ4Ngd1bmkwNDg3B3VuaTA0ODgHdW5pMDQ4OQd1bmkwNDhBB3VuaTA0OEIHdW5pMDQ4RAd1bmkwNDhFB3VuaTA0OEYHdW5pMDQ5MAd1bmkwNDkxB3VuaTA0OTQHdW5pMDQ5NQd1bmkwNDk2B3VuaTA0OTcHdW5pMDQ5QQd1bmkwNDlCB3VuaTA0OUMHdW5pMDQ5RAd1bmkwNEEwB3VuaTA0QTEHdW5pMDRBMgd1bmkwNEEzB3VuaTA0QTQHdW5pMDRBNQd1bmkwNEE2B3VuaTA0QTcHdW5pMDRBOAd1bmkwNEE5B3VuaTA0QjIHdW5pMDRCMwd1bmkwNEI0B3VuaTA0QjUHdW5pMDRCNgd1bmkwNEI3B3VuaTA0QjgHdW5pMDRCOQd1bmkwNEJBB3VuaTA0QkMHdW5pMDRCRAd1bmkwNEMzB3VuaTA0QzQHdW5pMDRDNQd1bmkwNEM2B3VuaTA0QzcHdW5pMDRDOAd1bmkwNEM5B3VuaTA0Q0EHdW5pMDRDRAd1bmkwNENFB3VuaTA0RDgHdW5pMDRFMAd1bmkwNEUxB3VuaTA0RkEHdW5pMDRGQgd1bmkwNEZDB3VuaTA0RkQDZl9sBWZfZl9pBWZfZl9sCGNyb3NzYmFyBUsuYWx0C2N5cmlsbGljdGljDGN5cmlsbGljaG9vawx1bmkwMDZBLmNjbXAMdW5pMDQ1OC5jY21wDHVuaTAwNjkuY2NtcAx1bmkwNDU2LmNjbXAHdW5pMDQwMQd1bmkwNDAzB3VuaTA0MDUHdW5pMDQwNgd1bmkwNDA3B3VuaTA0MDgHdW5pMDQxQQd1bmkwNDBDB3VuaTA0MEUHdW5pMDQxMAd1bmkwNDEyB3VuaTA0MTMHdW5pMDQxNQd1bmkwNDE5B3VuaTA0MUMHdW5pMDQxRAd1bmkwNDFFB3VuaTA0MUYHdW5pMDQyMAd1bmkwNDIxB3VuaTA0MjIHdW5pMDQyNQd1bmkwNDMwB3VuaTA0MzUHdW5pMDQzOQd1bmkwNDNFB3VuaTA0NDAHdW5pMDQ0MQd1bmkwNDQzB3VuaTA0NDUHdW5pMDQ1MQd1bmkwNDUzB3VuaTA0NTUHdW5pMDQ1Ngd1bmkwNDU3B3VuaTA0NTgHdW5pMDQ1Qwd1bmkwNDVFB3VuaTA0MDAHdW5pMDQwRAd1bmkwNDUwB3VuaTA0NUQHdW5pMDQ3MAd1bmkwNDcxB3VuaTA0NzYHdW5pMDQ3Nwd1bmkwNDc5B3VuaTA0NzgHdW5pMDQ5OAd1bmkwNDk5B3VuaTA0QUEHdW5pMDRBQgd1bmkwNEFFB3VuaTA0QUYHdW5pMDRDMAd1bmkwNEMxB3VuaTA0QzIHdW5pMDRDRgd1bmkwNEQwB3VuaTA0RDEHdW5pMDREMgd1bmkwNEQzB3VuaTA0RDQHdW5pMDRENQd1bmkwNEQ2B3VuaTA0RDcHdW5pMDREQQd1bmkwNEQ5B3VuaTA0REIHdW5pMDREQwd1bmkwNEREB3VuaTA0REUHdW5pMDRERgd1bmkwNEUyB3VuaTA0RTMHdW5pMDRFNAd1bmkwNEU1B3VuaTA0RTYHdW5pMDRFNwd1bmkwNEU4B3VuaTA0RTkHdW5pMDRFQQd1bmkwNEVCB3VuaTA0RUMHdW5pMDRFRAd1bmkwNEVFB3VuaTA0RUYHdW5pMDRGMAd1bmkwNEYxB3VuaTA0RjIHdW5pMDRGMwd1bmkwNEY0B3VuaTA0RjUHdW5pMDRGOAd1bmkwNEY5B3VuaTA0QUMHdW5pMDRBRAd1bmkwNENCB3VuaTA0Q0MHdW5pMDRGNgd1bmkwNEY3B3VuaTA0QkUHdW5pMDRCRgd1bmkwNEJCB3VuaTA0OEMHdW5pMDQ2Mgd1bmkwNDkyB3VuaTA0OTMHdW5pMDQ5RQd1bmkwNDlGB3VuaTA0QjAHdW5pMDRCMQd1bmkwNEZFB3VuaTA0RkYAAEu4AMhSWLEBAY5ZsAG5CAAIAGNwsQAHQkAJAIVxXUk1AAcAKrEAB0JAEIsBeAhkCFAIPAgqBx4EBwgqsQAHQkAQjACCBm4GWgZGBjMFJAIHCCqxAA5CQQkjAB5AGUAUQA9ACsAHwAAHAAkqsQAVQkEJAAAAQABAAEAAQABAAEAABwAJKrEDAESxJAGIUViwQIhYsQMARLEmAYhRWLoIgAABBECIY1RYsQNkRFlZWVlAEIwAeghmCFIIPggsByAEBwwquAH/hbAEjbECAESzBWQGAEREAAAA') format("truetype"); } -.video-js { +¡.video-js { // display:inline-block would be closer to the video el's display:inline // but it results in flash reloading when going into fullscreen [#2205] // TODO: Still needed? diff --git a/src/components/control-bar/audio-control/AudioButton.ts b/src/components/control-bar/audio-control/AudioButton.ts index 90bc4c5..cf18c9b 100644 --- a/src/components/control-bar/audio-control/AudioButton.ts +++ b/src/components/control-bar/audio-control/AudioButton.ts @@ -1,13 +1,14 @@ +import type { EventBus } from 'ts-bus' +import type { VideoJsPlayerOptions } from 'video.js' import videojs from 'video.js' -import AudioTrack from 'video.js/dist/types/tracks/audio-track' +import type Player from 'video.js/dist/types/player' +import type AudioTrack from 'video.js/dist/types/tracks/audio-track' -import VokaBusEvent from '@/internal/events/VokaBusEvent'; - -import { SvgType } from '@/constants/VokaSvg' import MenuButton from '@/components/menu/MenuButton' +import { SvgType } from '@/constants/VokaSvg' +import VokaBusEvent from '@/internal/events/VokaBusEvent' + import AudioItem from './AudioItem' -import Player from 'video.js/dist/types/player' -import { EventBus } from 'ts-bus' const Component = videojs.getComponent('component') @@ -77,7 +78,7 @@ class AudioButton extends MenuButton { if (index != -1) { this.items[index].selected(true) this.audioTracks[index].enabled = true - this.bus.publish(VokaBusEvent.audioTracksSet({ audioTrackId: index })) + this.bus.publish(VokaBusEvent.audioTracksSet({ audioTrackId: this.audioTracks[index].id })) } } diff --git a/src/internal/drm/dash/VokaDash.ts b/src/internal/drm/dash/VokaDash.ts index d1a45ce..2de8ab3 100644 --- a/src/internal/drm/dash/VokaDash.ts +++ b/src/internal/drm/dash/VokaDash.ts @@ -1,46 +1,145 @@ +import type { + BitrateInfo, + FragmentLoadingCompletedEvent, + ManifestLoadedEvent, + MediaInfo, + MediaPlayerClass, + MediaPlayerErrorEvent, + QualityChangeRenderedEvent, + Representation, + StreamInitializedEvent +} from 'dashjs' +import { Debug, MediaPlayer, MetricsReporting, Protection } from 'dashjs' +import type { EventBus } from 'ts-bus' import videojs from 'video.js' -import AudioTrack from 'video.js/dist/types/tracks/audio-track' -import TextTrack from 'video.js/dist/types/tracks/text-track' +import type Player from 'video.js/dist/types/player' +import type AudioTrack from 'video.js/dist/types/tracks/audio-track' +import type TextTrack from 'video.js/dist/types/tracks/text-track' + import VokaEvent from '@/constants/VokaEvent' -import { - BitrateInfo, Debug, - FragmentLoadingCompletedEvent, - ManifestLoadedEvent, - MediaInfo, - MediaPlayer, - MediaPlayerClass, - MediaPlayerErrorEvent, - QualityChangeRenderedEvent, Representation, - StreamInitializedEvent, -} from "dashjs"; -import { - ComponentErrorInfo, - VokaError, - VokaInternalErrorComponent, - VokaInternalErrorType -} from '@/public/models/VokaError' -import VokaDashTech from '@/internal/player/native/dash/tech/VokaDashTech' -import { IVokaSource, VokaOptionsType } from '@/internal/player/native/VokaSourceHandler' import VokaBusEvent from '@/internal/events/VokaBusEvent' -import { Quality, QualityLabelVariant } from '@/public/models/ILoadOptions' -import VokaCorePlayer from '@/internal/player/VokaCorePlayer' -import { QualityMapper } from '@/public/models/QualityMapper' +import type VokaDashTech from '@/internal/player/native/dash/tech/VokaDashTech' +import type { IVokaSource, VokaOptionsType } from '@/internal/player/native/VokaSourceHandler' +import type VokaCorePlayer from '@/internal/player/VokaCorePlayer' import ArrayUtils from '@/internal/utils/ArrayUtils' -import Player from 'video.js/dist/types/player' -import { EventBus } from 'ts-bus' +import { IS_TIZEN, IS_WEBOS } from '@/internal/utils/browser' +import type { Quality } from '@/public/models/ILoadOptions' +import { QualityLabelVariant } from '@/public/models/ILoadOptions' +import { QualityMapper } from '@/public/models/QualityMapper' +import type { ComponentErrorInfo } from '@/public/models/VokaError' +import { VokaError, VokaInternalErrorComponent, VokaInternalErrorType } from '@/public/models/VokaError' + +function __isWebOS(): boolean { + return IS_WEBOS +} + +function __webOSVersion(): number | undefined { + if (!IS_WEBOS) return undefined + const m = (navigator.userAgent || '').match(/Web0S\/(\d+\.\d+)/i) + return m ? parseFloat(m[1]) : undefined +} + +// Настройки для WebOS для стабильности работы на слабом железе +function __webOSSoftSettings(isLive: boolean) { + return { + streaming: { + // Никаких LL и агрессивных свитчей + lowLatencyEnabled: false, + fastSwitchEnabled: false, // меньше дерганий и ребуферов на слабом железе + scheduleWhilePaused: false, + + // Чуть реже тикать wallclock (меньше будоражим main thread) + wallclockTimeUpdateInterval: 250, + + // Для live немного больше задержка → стабильнее буфер + liveDelayFragmentCount: isLive ? 4 : undefined, + + buffer: { + // Немного увеличить стабильный буфер (живее на webOS 4.x, но без излишеств) + stableBufferTime: isLive ? 14 : 24, // сек + bufferToKeep: 6, // сек (держим хвост умеренно) + bufferPruningInterval: 15 // сек (реже чистим — меньше лишней работы) + }, + + abr: { + useDefaultABRRules: true, + ABRStrategy: 'abrThroughput', // простая, предсказуемая стратегия + limitBitrateByPortal: true, // не брать профили выше размеров экрана + initialBitrate: { video: 600 } // пониже старт, чтобы быстрее и стабильнее начать + // maxBitrate: { video: 4000 } // если нужно ограничить качества по полосе пропускания + }, + retryIntervals: { + MPD: 3000, + MediaSegment: 1500, + InitializationSegment: 1500, + BitstreamSwitch: 1000 + }, + retryAttempts: { + MPD: 2, + MediaSegment: 2, + InitializationSegment: 2, + BitstreamSwitch: 2 + }, + liveCatchup: { + enabled: false + } + } + } +} + +function __tizenSoftSettings(isLive: boolean) { + return { + streaming: { + lowLatencyEnabled: false, + fastSwitchEnabled: false, + scheduleWhilePaused: false, + wallclockTimeUpdateInterval: 250, + liveDelayFragmentCount: isLive ? 3 : undefined, + jumpGaps: true, + stallThreshold: 2, + stallDetectorEnabled: true, + buffer: { + stableBufferTime: isLive ? 12 : 20, + bufferToKeep: 6, + bufferPruningInterval: 12 + }, + abr: { + useDefaultABRRules: true, + ABRStrategy: 'abrThroughput', + limitBitrateByPortal: true, + initialBitrate: { video: 800 } + }, + text: { defaultEnabled: false } + } + } +} + +function __applyWebOSQuirksAndSettings(mediaPlayer: MediaPlayerClass, guessIsLive: boolean) { + try { + if (!__isWebOS()) return + const ver = __webOSVersion() + if (ver && ver < 4.9) { + // webOS 4.x: подменим FetchLoader на XHRLoader + mediaPlayer.extend('FetchLoader', new MediaPlayer.XHRLoader(), true) + } + // мягкие настройки + mediaPlayer.updateSettings(__webOSSoftSettings(guessIsLive)) + } + catch { + } +} export const enum ExtendedMediaPlayerErrors { - MANIFEST_LOADER_TIMEOUT = 77, - FRAGMENT_LOADER_TIMEOUT = 78 + MANIFEST_LOADER_TIMEOUT = 77, + FRAGMENT_LOADER_TIMEOUT = 78 } enum DashPlaybackErrorType { - //TODO - ManifestDownload = 4000, - ManifestTimeout, - ManifestParsing, - FragmentDownload, - FragmentTimeout + ManifestDownload = 4000, + ManifestTimeout, + ManifestParsing, + FragmentDownload, + FragmentTimeout } enum DashDrmErrorType { @@ -110,52 +209,69 @@ export default class VokaDash { private textTracks: MediaInfo[] | null = null constructor(source: IVokaSource, tech: VokaDashTech, options: VokaDashOptions) { - // Get options from tech if not provided for backwards compatibility - const videoOptions = options || tech['options_'] + // Get options from tech if not provided for backwards compatibility + const videoOptions = options || tech['options_'] - this.bus = source.bus - this.player = videojs(videoOptions.playerId) - this.hasFiniteDuration = false - //хорошо бы ассерт на тип теча - this.tech = tech + this.bus = source.bus + this.player = videojs(videoOptions.playerId) + this.hasFiniteDuration = false + //хорошо бы ассерт на тип теча + this.tech = tech - // Do nothing if the src is falsey - if (!source.src) { - return - } - // While the manifest is loading and Dash.js has not finished initializing - // we must defer events and functions calls with isReady_ and then `triggerReady` - // again later once everything is setup - // @ts-ignore - tech.isReady_ = false + // Do nothing if the src is falsey + if (!source.src) { + return + } + // While the manifest is loading and Dash.js has not finished initializing + // we must defer events and functions calls with isReady_ and then `triggerReady` + // again later once everything is setup + tech.isReady_ = false - // @ts-ignore - const view = tech.el() - let manifestSource = source.src - const keySystemOptions = VokaDash.buildDashJSProtData( - source.keySystemOptions - ) + const view = tech.el() + let manifestSource = source.src + const keySystemOptions = VokaDash.buildDashJSProtData( + source.keySystemOptions + ) - //startSeconds - const startSeconds = source.startSeconds - if (startSeconds) { - //https://github.com/Dash-Industry-Forum/dash.js/issues/173 - manifestSource += '#s=' + startSeconds - } + //startSeconds + const startSeconds = source.startSeconds + if (startSeconds) { + //https://github.com/Dash-Industry-Forum/dash.js/issues/173 + manifestSource += '#s=' + startSeconds + } - this.mediaPlayer = this.createPlayer( - options.dash, - manifestSource, - keySystemOptions, - view - ) + // MARK Защита от конфликта версий в разных бандлах + // Приложение для смартов тянет свою версию dashjs не совместимую с нужной + // Dash.js использует глобальное пространство для подключения опциональных модулей + // На время инициализации плеера явно подсовываем модули из нужной версии в глобал + // После инициализации восстанавливаем статус-кво + let globalProtection: dashjs.Protection | undefined + let globalMetricsReporting: any - // this.mediaPlayer.updateSettings({'debug': {'logLevel': Debug.LOG_LEVEL_DEBUG}}) - this.setupSettings(options) - this.setupListeners() + if (dashjs) { + globalProtection = dashjs.Protection + globalMetricsReporting = dashjs.MetricsReporting + dashjs.Protection = Protection + dashjs.MetricsReporting = MetricsReporting + } - // @ts-ignore - tech.triggerReady() + this.mediaPlayer = this.createPlayer( + options.dash, + manifestSource, + keySystemOptions, + view + ) + + // this.mediaPlayer.updateSettings({'debug': {'logLevel': Debug.LOG_LEVEL_DEBUG}}) + this.setupSettings(options) + this.setupListeners() + + if (dashjs) { + dashjs.Protection = globalProtection + dashjs.MetricsReporting = globalMetricsReporting + } + + tech.triggerReady() } private setupListeners() { @@ -210,9 +326,9 @@ export default class VokaDash { VokaBusEvent.audioTracksSet, ({ payload }) => { if (this.audioTracks && typeof payload.audioTrackId === "number") { - const currentAudioTrack = this.audioTracks[payload.audioTrackId]; + const currentAudioTrack = this.audioTracks[payload.audioTrackId] if (currentAudioTrack) { - this.mediaPlayer?.setCurrentTrack(currentAudioTrack); + this.mediaPlayer?.setCurrentTrack(currentAudioTrack) } } }) @@ -221,11 +337,11 @@ export default class VokaDash { VokaBusEvent.subtitleTracksSet, ({ payload }) => { if (this.textTracks && typeof payload.trackId === "number") { - const currentSubtitleTrack = this.textTracks[payload.trackId]; + const currentSubtitleTrack = this.textTracks[payload.trackId] if (currentSubtitleTrack) { - this.mediaPlayer?.setTextTrack(payload.trackId); + this.mediaPlayer?.setTextTrack(payload.trackId) } else { - this.mediaPlayer?.setTextTrack(-1); + this.mediaPlayer?.setTextTrack(-1) } } }) @@ -294,18 +410,17 @@ export default class VokaDash { } private static getTrackLabel(track?: { labels?: Array<{ text?: string }> }): string | null { - var label; if (!track || !track.labels) { - return null; + return null } if (track.labels.length != 1) { - return null; + return null } - label = track.labels[0]; + const label = track.labels[0] if (!label || !label.text || typeof label.text !== 'string') { - return null; + return null } - return label.text; + return label.text } private static getAudioTrackItems(audioTracks: MediaInfo[], current: MediaInfo | null): videojs.AudioTrack[] { @@ -322,12 +437,12 @@ export default class VokaDash { private static getTextTrackItems(textTracks: MediaInfo[]): any { return textTracks.map( (track, index) => { - let label; + let label if (Array.isArray(track.labels) && track.labels.length === 1) { - label = track.labels[0].text; + label = track.labels[0].text } else { - label = track.lang; + label = track.lang } return { @@ -356,7 +471,7 @@ export default class VokaDash { bandwidth = repSwitch ? Math.round( - // @ts-ignore + dashAdapter.getBandwidthForRepresentation(repSwitch.to, periodIdx) ) : NaN @@ -387,7 +502,7 @@ export default class VokaDash { const closestBitrate = availableQualities .map((quality) => quality.bitrateInKbit) .reduce((prev, curr) => - // @ts-ignore + Math.abs(curr - bandwidth) < Math.abs(prev - bandwidth) ? curr : prev ) parsedLevels.forEach((val, index) => { @@ -441,7 +556,8 @@ export default class VokaDash { } }, debug: { - logLevel: (options.debug != undefined && options.debug.playerLogs) ? Debug.LOG_LEVEL_DEBUG : Debug.LOG_LEVEL_NONE + logLevel: (options.debug != undefined && options.debug.playerLogs) + ? Debug.LOG_LEVEL_DEBUG : Debug.LOG_LEVEL_NONE } }) } @@ -457,6 +573,7 @@ export default class VokaDash { } private setStartQuality(qualities: VokaCorePlayer.IQualityData[]) { + // FIXME тут кажется ошибка v.quality не может быть равно 0 const isFound = qualities.find( (v) => v.quality === Number(0) ) @@ -517,21 +634,21 @@ export default class VokaDash { getAbsoluteRange(): any | null { if (!this.mediaPlayer) return null - const range = { start: 0, end: 0 }; + const range = { start: 0, end: 0 } if (!this.mediaPlayer.isDynamic()) { - range.end = this.mediaPlayer.duration(); - return range; + range.end = this.mediaPlayer.duration() + return range } const dashMetrics = this.mediaPlayer?.getDashMetrics() - if (!dashMetrics) return null; + if (!dashMetrics) return null try { - const dvrInfo = dashMetrics.getCurrentDVRInfo('video'); - range.start = dvrInfo.manifestInfo.availableFrom.getTime() / 1000 + dvrInfo.range.start; - range.end = this.mediaPlayer?.timeAsUTC() || 0; - return range; + const dvrInfo = dashMetrics.getCurrentDVRInfo('video') + range.start = dvrInfo.manifestInfo.availableFrom.getTime() / 1000 + dvrInfo.range.start + range.end = this.mediaPlayer?.timeAsUTC() || 0 + return range } catch { - return 0; + return 0 } } @@ -546,7 +663,7 @@ export default class VokaDash { return false } - return true; + return true } /** @@ -590,106 +707,136 @@ export default class VokaDash { keySystemOptions: any, view: any ): MediaPlayerClass { - const mediaPlayer = MediaPlayer().create() + const mediaPlayer = MediaPlayer().create() - mediaPlayer.setProtectionData(keySystemOptions) + mediaPlayer.setProtectionData(keySystemOptions) - this.attachListeners(mediaPlayer) + this.attachListeners(mediaPlayer) - // Apply all dash options that are set - if (options) { - Object.keys(options).forEach((key) => { - const dashOptionsKey = - 'set' + key.charAt(0).toUpperCase() + key.slice(1) - let value = options[key] + // Apply all dash options that are set + if (options) { + Object.keys(options).forEach((key) => { + const dashOptionsKey = + 'set' + key.charAt(0).toUpperCase() + key.slice(1) + let value = options[key] - if (Object.prototype.hasOwnProperty.call(mediaPlayer, dashOptionsKey)) { - // Set key so it will still work - key = dashOptionsKey - } + if (Object.prototype.hasOwnProperty.call(mediaPlayer, dashOptionsKey)) { + // Set key so it will still work + key = dashOptionsKey + } - if (!Object.prototype.hasOwnProperty.call(mediaPlayer, key)) { - return - } + if (!Object.prototype.hasOwnProperty.call(mediaPlayer, key)) { + return + } - // Guarantee `value` is an array - if (!Array.isArray(value)) { - value = [value] - } + // Guarantee `value` is an array + if (!Array.isArray(value)) { + value = [ value ] + } - // @ts-ignore - mediaPlayer[key](...value) - }) + mediaPlayer[key](...value) + }) + } + + mediaPlayer.initialize(view, manifestSource, false) + try { + const guessIsLive = !!options?.streaming?.liveDelayFragmentCount || false + if (IS_TIZEN) { + mediaPlayer.updateSettings(__tizenSoftSettings(guessIsLive)) + } else if (IS_WEBOS) { + mediaPlayer.updateSettings(__webOSSoftSettings(guessIsLive)) + const ver = __webOSVersion() + if (ver && ver < 4.9) { + // webOS 4.x: подменим FetchLoader на XHRLoader + mediaPlayer.extend('FetchLoader', new MediaPlayer.XHRLoader(), true) + } } + } + catch { + } - mediaPlayer.initialize(view, manifestSource, false) + this.player.trigger(VokaEvent.MasterDashPlaylistLoad, manifestSource) - this.player.trigger(VokaEvent.MasterDashPlaylistLoad, manifestSource) - - return mediaPlayer + return mediaPlayer } private attachListeners(mediaPlayer: MediaPlayerClass) { - // @ts-ignore + mediaPlayer.on(MediaPlayer.events.ERROR, this.listenerError, this) mediaPlayer.on( - MediaPlayer.events.MANIFEST_LOADED, - this.listenerManifestLoaded, - this - ) - mediaPlayer.on( - MediaPlayer.events.FRAGMENT_LOADING_COMPLETED, - this.listenerFragmentLoadingCompleted, - this - ) - mediaPlayer.on( - MediaPlayer.events.STREAM_INITIALIZED, - this.listenerStreamInitialized, - this - ) - mediaPlayer.on( - MediaPlayer.events.QUALITY_CHANGE_RENDERED, - this.listenerQualityChangeRendered, - this - ) - mediaPlayer.on( - MediaPlayer.events.BUFFER_EMPTY, - () => this.updateBufferingMode(true), - this - ) - mediaPlayer.on( - MediaPlayer.events.PLAYBACK_WAITING, - () => this.updateBufferingMode(true), - this - ) - mediaPlayer.on( - MediaPlayer.events.CAN_PLAY, - () => this.updateBufferingMode(false), - this - ) - mediaPlayer.on( - MediaPlayer.events.PLAYBACK_PLAYING, - () => this.updateBufferingMode(false), - this - ) - mediaPlayer.on( - MediaPlayer.events.PLAYBACK_ENDED, - () => this.updateBufferingMode(false), - this - ) - mediaPlayer.on( - MediaPlayer.events.BUFFER_LOADED, - this.setBufferingComplete, - this - ) - mediaPlayer.on( - 'public_keySystemSelected', - e => console.log("Key system selected: ", e), - ) - mediaPlayer.on( - 'public_keyAdded', - e => console.log("Key added:", e), + MediaPlayer.events.MANIFEST_LOADED, + (e: any) => { + try { + const isLive = !!e?.data?.manifestInfo?.isDynamic || !isFinite(e?.data?.mediaPresentationDuration) + if (IS_TIZEN) mediaPlayer.updateSettings(__tizenSoftSettings(isLive)) + else if (IS_WEBOS) { + mediaPlayer.updateSettings(__webOSSoftSettings(isLive)) + } + } + catch { + } + this.listenerManifestLoaded(e) + }, + this ) + mediaPlayer.on( + MediaPlayer.events.FRAGMENT_LOADING_COMPLETED, + this.listenerFragmentLoadingCompleted, + this + ) + mediaPlayer.on( + MediaPlayer.events.STREAM_INITIALIZED, + this.listenerStreamInitialized, + this + ) + mediaPlayer.on( + MediaPlayer.events.QUALITY_CHANGE_RENDERED, + this.listenerQualityChangeRendered, + this + ) + mediaPlayer.on( + MediaPlayer.events.BUFFER_EMPTY, + this.listenerSetBuffering, + this + ) + mediaPlayer.on( + MediaPlayer.events.PLAYBACK_WAITING, + this.listenerSetBuffering, + this + ) + mediaPlayer.on( + MediaPlayer.events.PLAYBACK_STALLED, + this.listenerSetBuffering, + this + ) + mediaPlayer.on( + MediaPlayer.events.CAN_PLAY, + this.listenerUnsetBuffering, + this + ) + mediaPlayer.on( + MediaPlayer.events.PLAYBACK_PLAYING, + this.listenerUnsetBuffering, + this + ) + mediaPlayer.on( + MediaPlayer.events.PLAYBACK_ENDED, + this.listenerUnsetBuffering, + this + ) + mediaPlayer.on( + MediaPlayer.events.BUFFER_LOADED, + this.setBufferingComplete, + this + ) + mediaPlayer.on( + 'public_keySystemSelected', + e => console.log('Key system selected: ', e) + ) + mediaPlayer.on( + 'public_keyAdded', + e => console.log('Key added:', e) + ) } private updateBufferingMode(value: boolean) { @@ -705,84 +852,89 @@ export default class VokaDash { } private detachListeners(mediaPlayer: MediaPlayerClass) { - mediaPlayer.off(MediaPlayer.events.ERROR, this.listenerError, this) - mediaPlayer.off( - MediaPlayer.events.MANIFEST_LOADED, - this.listenerManifestLoaded, - this - ) - mediaPlayer.off( - MediaPlayer.events.FRAGMENT_LOADING_COMPLETED, - this.listenerFragmentLoadingCompleted, - this - ) - mediaPlayer.off( - MediaPlayer.events.STREAM_INITIALIZED, - this.listenerStreamInitialized, - this - ) - mediaPlayer.off( - MediaPlayer.events.QUALITY_CHANGE_RENDERED, - this.listenerQualityChangeRendered, - this - ) - mediaPlayer.off( - MediaPlayer.events.BUFFER_EMPTY, - () => this.updateBufferingMode(true), - this - ) - mediaPlayer.off( - MediaPlayer.events.PLAYBACK_WAITING, - () => this.updateBufferingMode(true), - this - ) - mediaPlayer.off( - MediaPlayer.events.CAN_PLAY, - () => this.updateBufferingMode(false), - this - ) - mediaPlayer.off( - MediaPlayer.events.PLAYBACK_PLAYING, - () => this.updateBufferingMode(false), - this - ) - mediaPlayer.off( - MediaPlayer.events.PLAYBACK_ENDED, - () => this.updateBufferingMode(false), - this - ) - mediaPlayer.off( - MediaPlayer.events.BUFFER_LOADED, - this.setBufferingComplete, - this - ) + mediaPlayer.off(MediaPlayer.events.ERROR, this.listenerError, this) + mediaPlayer.off( + MediaPlayer.events.MANIFEST_LOADED, + this.listenerManifestLoaded, + this + ) + mediaPlayer.off( + MediaPlayer.events.FRAGMENT_LOADING_COMPLETED, + this.listenerFragmentLoadingCompleted, + this + ) + mediaPlayer.off( + MediaPlayer.events.STREAM_INITIALIZED, + this.listenerStreamInitialized, + this + ) + mediaPlayer.off( + MediaPlayer.events.QUALITY_CHANGE_RENDERED, + this.listenerQualityChangeRendered, + this + ) + mediaPlayer.off( + MediaPlayer.events.BUFFER_EMPTY, + this.listenerSetBuffering, + this + ) + mediaPlayer.off( + MediaPlayer.events.PLAYBACK_WAITING, + this.listenerSetBuffering, + this + ) + mediaPlayer.off( + MediaPlayer.events.PLAYBACK_STALLED, + this.listenerSetBuffering, + this + ) + mediaPlayer.off( + MediaPlayer.events.CAN_PLAY, + this.listenerUnsetBuffering, + this + ) + mediaPlayer.off( + MediaPlayer.events.PLAYBACK_PLAYING, + this.listenerUnsetBuffering, + this + ) + mediaPlayer.off( + MediaPlayer.events.PLAYBACK_ENDED, + this.listenerUnsetBuffering, + this + ) + mediaPlayer.off( + MediaPlayer.events.BUFFER_LOADED, + this.setBufferingComplete, + this + ) } // Listeners private listenerManifestLoaded(event: ManifestLoadedEvent) { - // @ts-ignore - const periods = event.data.Period_asArray - const oldHasFiniteDuration = this.hasFiniteDuration - if ( - event.data.mediaPresentationDuration || - (ArrayUtils.isArray(periods) && periods[periods.length - 1].duration) - ) { - this.hasFiniteDuration = true - } else { - // in case we run into a weird situation where we're VOD but then - // switch to live - this.hasFiniteDuration = false - } + const periods = event.data.Period_asArray + const oldHasFiniteDuration = this.hasFiniteDuration - if (this.hasFiniteDuration !== oldHasFiniteDuration) { - this.player.trigger('durationchange') - } + if ( + event.data.mediaPresentationDuration || + (ArrayUtils.isArray(periods) && periods[periods.length - 1].duration) + ) { + this.hasFiniteDuration = true + } else { + // in case we run into a weird situation where we're VOD but then + // switch to live + this.hasFiniteDuration = false + } - //Мы не готовы, триггерим позже. - //this.player.trigger(VokaEvent.MasterHlsManifestParsed, data) - //this.player.trigger(VokaEvent.PlayerMetadataLoaded) + if (this.hasFiniteDuration !== oldHasFiniteDuration) { + this.player.trigger('durationchange') + } + + //Мы не готовы, триггерим позже. + //this.player.trigger(VokaEvent.MasterHlsManifestParsed, data) + //this.player.trigger(VokaEvent.PlayerMetadataLoaded) } private listenerQualityChangeRendered(event: QualityChangeRenderedEvent) { @@ -800,101 +952,105 @@ export default class VokaDash { ) const settings = mediaPlayer.getSettings() const busEvent = { - index: representation.index, - quality: level.quality as Quality, - bitrate: representation.bitrateInKbit, - bandwidth: bandwidth > 0 ? bandwidth : representation.bandwidth, - width: representation.width, - height: representation.height, - isAuto: settings.streaming.abr.autoSwitchBitrate.video, - label: label, + index: representation.index, + quality: level.quality as Quality, + bitrate: representation.bitrateInKbit, + bandwidth: bandwidth > 0 ? bandwidth : representation.bandwidth, + width: representation.width, + height: representation.height, + isAuto: settings.streaming.abr.autoSwitchBitrate.video, + label: label } as VokaBusEvent.IChangeQuality - this.bus.publish( - VokaBusEvent.qualityChange(busEvent), - ) + this.bus.publish( + VokaBusEvent.qualityChange(busEvent) + ) - this.player.trigger( - VokaEvent.DashLevelSwitched, - { level: representation }, - ) + this.player.trigger( + VokaEvent.DashLevelSwitched, + { level: representation } + ) } } - /** - * Ошибки которые нужно логировать (HlsProcessor логирует их): - * MANIFEST_LOAD_TIMEOUT, - * MANIFEST_LOAD_ERROR -> - * MANIFEST_LOADER_LOADING_FAILURE_ERROR_CODE - * DOWNLOAD_ERROR_ID_MANIFEST_CODE - * MANIFEST_PARSING_ERROR -> - * MANIFEST_ERROR_ID_NOSTREAMS_CODE - * MANIFEST_ERROR_ID_PARSE_CODE - * MANIFEST_ERROR_ID_MULTIPLEXED_CODE - * MANIFEST_LOADER_PARSING_FAILURE_ERROR_CODE - * LEVEL_LOAD_ERROR -> нет - * LEVEL_LOAD_TIMEOUT -> нет - * FRAG_LOAD_TIMEOUT, - * FRAG_LOAD_ERROR -> - * DOWNLOAD_ERROR_ID_CONTENT_CODE - * FRAGMENT_LOADER_LOADING_FAILURE_ERROR_CODE - * DOWNLOAD_ERROR_ID_INITIALIZATION_CODE - * (не совсем относится сюда, но добавим:) - * SEGMENT_BASE_LOADER_ERROR_CODE - * URL_RESOLUTION_FAILED_GENERIC_ERROR_CODE - * SEGMENTS_UPDATE_FAILED_ERROR_CODE - * FRAGMENT_LOADER_NULL_REQUEST_ERROR_CODE - * FRAG_PARSING_ERROR -> нет - * - * Подумать, куда деть: - * SEGMENTS_UNAVAILABLE_ERROR_CODE - */ - private listenerError(e: MediaPlayerErrorEvent) { - const { code, message } = e.error - const data = e.error.data - // @ts-ignore - const url = data?.request?.url - // @ts-ignore - const statusCode = data?.response?.status + private readonly listenerSetBuffering = () => this.updateBufferingMode(true) + private readonly listenerUnsetBuffering = () => this.updateBufferingMode(false) - switch (code) { - case MediaPlayer.errors.CAPABILITY_MEDIAKEYS_ERROR_CODE: - return this.triggerError( - DashDrmErrorType.MediaKeysSupport, - url, - message, - statusCode - ) + /** + * Ошибки которые нужно логировать (HlsProcessor логирует их): + * MANIFEST_LOAD_TIMEOUT, + * MANIFEST_LOAD_ERROR -> + * MANIFEST_LOADER_LOADING_FAILURE_ERROR_CODE + * DOWNLOAD_ERROR_ID_MANIFEST_CODE + * MANIFEST_PARSING_ERROR -> + * MANIFEST_ERROR_ID_NOSTREAMS_CODE + * MANIFEST_ERROR_ID_PARSE_CODE + * MANIFEST_ERROR_ID_MULTIPLEXED_CODE + * MANIFEST_LOADER_PARSING_FAILURE_ERROR_CODE + * LEVEL_LOAD_ERROR -> нет + * LEVEL_LOAD_TIMEOUT -> нет + * FRAG_LOAD_TIMEOUT, + * FRAG_LOAD_ERROR -> + * DOWNLOAD_ERROR_ID_CONTENT_CODE + * FRAGMENT_LOADER_LOADING_FAILURE_ERROR_CODE + * DOWNLOAD_ERROR_ID_INITIALIZATION_CODE + * (не совсем относится сюда, но добавим:) + * SEGMENT_BASE_LOADER_ERROR_CODE + * URL_RESOLUTION_FAILED_GENERIC_ERROR_CODE + * SEGMENTS_UPDATE_FAILED_ERROR_CODE + * FRAGMENT_LOADER_NULL_REQUEST_ERROR_CODE + * FRAG_PARSING_ERROR -> нет + * + * Подумать, куда деть: + * SEGMENTS_UNAVAILABLE_ERROR_CODE + */ + private listenerError(e: MediaPlayerErrorEvent) { + const { code, message } = e.error + const data = e.error.data - case MediaPlayer.errors.KEY_SESSION_CREATED_ERROR_CODE: - // Error code returned when key session has not been successfully created - return this.reloadOnce(DashDrmErrorType.CreateKeySession, () => { - this.triggerError( - DashDrmErrorType.CreateKeySession, - url, - message, - statusCode - ) - }) + const url = data?.request?.url - case MediaPlayer.errors.KEY_STATUS_CHANGED_EXPIRED_ERROR_CODE: - // Error code returned when license validity has expired - return this.triggerError( - DashDrmErrorType.InvalidLicense, - url, - message, - statusCode - ) + const statusCode = data?.response?.status - case MediaPlayer.errors.KEY_SYSTEM_ACCESS_DENIED_ERROR_CODE: - // Error code returned when key system access is denied - case MediaPlayer.errors.MEDIA_KEYERR_CLIENT_CODE: - // The Key System could not be installed or updated. - case MediaPlayer.errors.MEDIA_KEYERR_DOMAIN_CODE: - // An error occurred in a multi-device domain licensing configuration. The most common error is a failure to join the domain. - case MediaPlayer.errors.MEDIA_KEYERR_OUTPUT_CODE: - // There is no available output device with the required characteristics for the content protection system. - case MediaPlayer.errors.MEDIA_KEYERR_HARDWARECHANGE_CODE: + switch (code) { + case MediaPlayer.errors.CAPABILITY_MEDIAKEYS_ERROR_CODE: + return this.triggerError( + DashDrmErrorType.MediaKeysSupport, + url, + message, + statusCode + ) + + case MediaPlayer.errors.KEY_SESSION_CREATED_ERROR_CODE: + // Error code returned when key session has not been successfully created + return this.reloadOnce(DashDrmErrorType.CreateKeySession, () => { + this.triggerError( + DashDrmErrorType.CreateKeySession, + url, + message, + statusCode + ) + }) + + case MediaPlayer.errors.KEY_STATUS_CHANGED_EXPIRED_ERROR_CODE: + // Error code returned when license validity has expired + return this.triggerError( + DashDrmErrorType.InvalidLicense, + url, + message, + statusCode + ) + + case MediaPlayer.errors.KEY_SYSTEM_ACCESS_DENIED_ERROR_CODE: + // Error code returned when key system access is denied + case MediaPlayer.errors.MEDIA_KEYERR_CLIENT_CODE: + // The Key System could not be installed or updated. + case MediaPlayer.errors.MEDIA_KEYERR_DOMAIN_CODE: + // An error occurred in a multi-device domain licensing configuration. The most common error is a failure to join + // the domain. + case MediaPlayer.errors.MEDIA_KEYERR_OUTPUT_CODE: + // There is no available output device with the required characteristics for the content protection system. + case MediaPlayer.errors.MEDIA_KEYERR_HARDWARECHANGE_CODE: // A hardware configuration change caused a content protection error. return this.triggerError( DashDrmErrorType.KeySystemSupport, @@ -1034,11 +1190,11 @@ export default class VokaDash { this.levels && this.nextPriorityLevelsWhenError === null ) { - // @ts-ignore - this.levelLoadingErrorGenerate( - data.request.quality, - this.levels as BitrateInfo[] - ) + + this.levelLoadingErrorGenerate( + data.request.quality, + this.levels + ) } if (Array.isArray(this.nextPriorityLevelsWhenError)) { @@ -1053,7 +1209,7 @@ export default class VokaDash { return } - const nextLevel = (this.nextPriorityLevelsWhenError as number[]).shift() + const nextLevel = (this.nextPriorityLevelsWhenError).shift() this.switchLevel(nextLevel as number) @@ -1319,4 +1475,4 @@ export default class VokaDash { } export const DASH_LEVEL_AUTO = -1 -export const TIMESHIFT_PLAYLIST_THRESHOLD = 180 \ No newline at end of file +export const TIMESHIFT_PLAYLIST_THRESHOLD = 180 diff --git a/src/internal/events/VokaBusEvent.ts b/src/internal/events/VokaBusEvent.ts index e0cca79..a0828d9 100644 --- a/src/internal/events/VokaBusEvent.ts +++ b/src/internal/events/VokaBusEvent.ts @@ -1,155 +1,153 @@ -import { createEventDefinition } from 'ts-bus' -import { Quality } from '@/public/models/ILoadOptions' -import VokaCorePlayer from '../player/VokaCorePlayer' -import { - VokaInternalErrorComponent, - VokaInternalErrorData, - VokaInternalErrorType +import type { IContextUpdated } from '@/public/@types' +import type { IAdvertisementMarker } from '@/public/IVokaPlayer' +import type { Quality } from '@/public/models/ILoadOptions' +import type { + VokaInternalErrorComponent, + VokaInternalErrorData, + VokaInternalErrorType } from '@/public/models/VokaError' -import { IContextUpdated } from '@/public/@types' -import AudioTrack from 'video.js/dist/types/tracks/audio-track' -import { IAdvertisementMarker } from '@/public/IVokaPlayer' +import { createEventDefinition } from 'ts-bus' +import type AudioTrack from 'video.js/dist/types/tracks/audio-track' + +import type VokaCorePlayer from '../player/VokaCorePlayer' namespace VokaBusEvent { + export type ErrorEventData = { + type: VokaInternalErrorType + component: VokaInternalErrorComponent + error: VokaInternalErrorData + fatal?: boolean + } - export type ErrorEventData = { - type: VokaInternalErrorType - component: VokaInternalErrorComponent - error: VokaInternalErrorData - fatal?: boolean - } + // MARK: - Quality + export const qualitiesParsed = createEventDefinition<{ + selected: number + qualities: VokaCorePlayer.IQualityData[] + }>()('quality.parsed') - // MARK: - Quality - export const qualitiesParsed = createEventDefinition<{ - selected: number, - qualities: VokaCorePlayer.IQualityData[], - }>()('quality.parsed') + export interface IChangeQuality { + index: number + quality: Quality + bitrate: number + bandwidth: number | null + width: number | null + height: number | null + isAuto: boolean + label: string | null + } - export interface IChangeQuality { - index: number - quality: Quality - bitrate: number - bandwidth: number | null - width: number | null - height: number | null - isAuto: boolean - label: string | null - } + export const qualityChange = createEventDefinition()('quality.update') - export const qualityChange = createEventDefinition()('quality.update') + export interface IQualitySet { + index: number // Исходный уникальный порядковый идентификатор качества + quality: Quality + forced: boolean | null + noAutoChange: boolean | null + withUIUpdate: boolean | null + } + export const qualitySet = createEventDefinition()('quality.set') - export interface IQualitySet { - quality: Quality - forced: boolean | null - noAutoChange: boolean | null - withUIUpdate: boolean | null - } - export const qualitySet = createEventDefinition()('quality.set') + export const qualityUISet = createEventDefinition<{ + index: number + quality: Quality + }>()('quality.setUI') - export const qualityUISet = createEventDefinition<{ - index: number, - quality: Quality, - }>()('quality.setUI') + export const showQualityLoader = createEventDefinition<{ + value: boolean + }>()('showQualityLoader') - export const showQualityLoader = createEventDefinition<{ - value: boolean, - }>()('showQualityLoader') + export const playerReset = createEventDefinition()('playerReset') - export const playerReset = createEventDefinition()('playerReset') + export const error = createEventDefinition()('playerError') - export const error = createEventDefinition()('error') + export const close = createEventDefinition()('close') - export const close = createEventDefinition()('close') + // MARK: - Switch content + export const switchContent = createEventDefinition()('content.switch') - // MARK: - Switch content - export const switchContent = - createEventDefinition()('content.switch') + // MARK: - Audio + export const audioTracksParsed = createEventDefinition<{ + tracks: AudioTrack[] + }>()('audioTracks.parsed') - // MARK: - Audio - export const audioTracksParsed = createEventDefinition<{ - tracks: any - }>()('audioTracks.parsed') + export const audioTracksSet = createEventDefinition<{ + audioTrackId: number + }>()('audioTracks.set') - export const audioTracksSet = createEventDefinition<{ - audioTrackId: number, - }>()('audioTracks.set') + // MARK: - Subtitle + export const subtitleTracksParsed = createEventDefinition<{ + tracks: any + }>()('subtitleTracks.parsed') - // MARK: - Subtitle - export const subtitleTracksParsed = createEventDefinition<{ - tracks: any - }>()('subtitleTracks.parsed') + export const subtitleTracksSet = createEventDefinition<{ + trackId: number + }>()('subtitleTracks.set') - export const subtitleTracksSet = createEventDefinition<{ - trackId: number - }>()('subtitleTracks.set') + export const subtitleTracksItems = createEventDefinition<{ + tracks: any + }>()('subtitleTracks.items') - export const subtitleTracksItems = createEventDefinition<{ - tracks: any - }>()('subtitleTracks.items') + export const subtitleTrackSet = createEventDefinition<{ + url: string + baseurl: string + }>()('subtitleTrack.set') - export const subtitleTrackSet = createEventDefinition<{ - url: string, - baseurl: string, - }>()('subtitleTrack.set') + // MARK: - ADS Рекламные события + export const adsMarkersSet = createEventDefinition<{ + items: IAdvertisementMarker[] + }>()('adsMarkers.set') + // adRequested - before ad loading is initiated + export const adRequested = createEventDefinition<{}>()('adRequested') + // adStarted - ad block started + export const adStarted = createEventDefinition<{}>()('adStarted') + // adFinished - ad block finished successfully + export const adFinished = createEventDefinition<{}>()('adFinished') + // adError - ad finished with error + export const adError = createEventDefinition<{}>()('adError') + // adItemStarted - one ad in ad block started + export const adItemStarted = createEventDefinition<{}>()('adItemStarted') + // adItemFinished - one ad in ad block finished successfully + export const adItemFinished = createEventDefinition<{}>()('adItemFinished') + // adItemError - one ad in ad block finished with error + export const adItemError = createEventDefinition<{}>()('adItemError') + // adImpression - ad impression happened (adId - id of advertisement being played from top-level VAST) + export const adImpression = createEventDefinition<{}>()('adImpression') + // onAdClick - target click + export const adClick = createEventDefinition<{}>()('adClick') + // disableAdsClick - user clicked disable ads button + export const adSkipClick = createEventDefinition<{}>()('disableAdsClick') - // MARK: - ADS Рекламные события - export const adsMarkersSet = createEventDefinition<{ - items: IAdvertisementMarker[], - }>()('adsMarkers.set') - // adRequested - before ad loading is initiated - export const adRequested = createEventDefinition<{}>()('adRequested') - // adStarted - ad block started - export const adStarted = createEventDefinition<{}>()('adStarted') - // adFinished - ad block finished successfully - export const adFinished = createEventDefinition<{}>()('adFinished') - // adError - ad finished with error - export const adError = createEventDefinition<{}>()('adError') - // adItemStarted - one ad in ad block started - export const adItemStarted = createEventDefinition<{}>()('adItemStarted') - // adItemFinished - one ad in ad block finished successfully - export const adItemFinished = createEventDefinition<{}>()('adItemFinished') - // adItemError - one ad in ad block finished with error - export const adItemError = createEventDefinition<{}>()('adItemError') - // adImpression - ad impression happened (adId - id of advertisement being played from top-level VAST) - export const adImpression = createEventDefinition<{}>()('adImpression') - // onAdClick - target click - export const adClick = createEventDefinition<{}>()('adClick') - // disableAdsClick - user clicked disable ads button - export const adSkipClick = createEventDefinition<{}>()('disableAdsClick') + export const adCancelPlaybackEvent = createEventDefinition<{}>()('adCancelPlaybackEvent') - export const adCancelPlaybackEvent = createEventDefinition<{}>()('adCancelPlaybackEvent') + // Others - // Others + export const contextUpdated = createEventDefinition()('context.update') - export const contextUpdated = - createEventDefinition()('context.update') + export const playbackRateMenu = createEventDefinition<{ + isActive: boolean + }>()('playbackRateMenu.isActive') - export const playbackRateMenu = createEventDefinition<{ - isActive: boolean, - }>()('playbackRateMenu.isActive') + export const fragmentLoaded = createEventDefinition<{ + bufferLength?: number + }>()('fragmentLoaded') - export const fragmentLoaded = createEventDefinition<{ - bufferLength?: number - }>()('fragmentLoaded') + export const updateBufferingMode = createEventDefinition<{ + value: boolean + }>()('updateBufferingMode') - export const updateBufferingMode = createEventDefinition<{ - value: boolean - }>()('updateBufferingMode') + export const setBufferingComplete = createEventDefinition<{ + value: boolean + }>()('setBufferingComplete') - export const setBufferingComplete = createEventDefinition<{ - value: boolean - }>()('setBufferingComplete') + export const changeZoomButtonVisible = createEventDefinition<{ + value: boolean + }>()('changeZoomButtonVisible') - export const changeZoomButtonVisible = createEventDefinition<{ - value: boolean - }>()('changeZoomButtonVisible') - - export const updateTimeshift = createEventDefinition<{ - available: boolean - }>()('updateTimeshift') - - export const destroyed = createEventDefinition()('destroyed') + export const updateTimeshift = createEventDefinition<{ + available: boolean + }>()('updateTimeshift') + export const destroyed = createEventDefinition()('destroyed') } -export default VokaBusEvent \ No newline at end of file +export default VokaBusEvent diff --git a/src/internal/observers/AudioTrackObserver.ts b/src/internal/observers/AudioTrackObserver.ts index ab1b87e..b3210be 100644 --- a/src/internal/observers/AudioTrackObserver.ts +++ b/src/internal/observers/AudioTrackObserver.ts @@ -1,13 +1,18 @@ -import AudioTrack from 'video.js/dist/types/tracks/audio-track' -import { IAudioTrack } from '../../public/IVokaPlayer' +import type { EventBus } from 'ts-bus' +import videojs from 'video.js' +import type AudioTrack from 'video.js/dist/types/tracks/audio-track' + +import type { IAudioTrack } from '@/public/IVokaPlayer' import VokaBusEvent from '../events/VokaBusEvent' -import { EventBus } from 'ts-bus' + +const log = videojs.log.createLogger('[AudioTrackObserver]') namespace AudioTrackObserver { - export interface IObserver { get audioTrackList(): IAudioTrack[] + get currentAudioTrackIndex(): number + setCurrentAudioTrack(index: number) } @@ -15,21 +20,19 @@ namespace AudioTrackObserver { return { audioTrackList: [], currentAudioTrackIndex: -1, - setCurrentAudioTrack(index): (value) => void {}, + setCurrentAudioTrack(index): (value) => void {} } as IObserver } export class Observer implements IObserver { - private readonly bus: EventBus private audioTracks: AudioTrack[] = [] constructor(bus: EventBus) { this.bus = bus - bus.subscribe( - VokaBusEvent.audioTracksParsed, - event => { this.audioTracks = event.payload.tracks } - ) + bus.subscribe(VokaBusEvent.audioTracksParsed, (event) => { + this.audioTracks = event.payload.tracks + }) } // MARK: - IObserver @@ -38,22 +41,32 @@ namespace AudioTrackObserver { return this.audioTracks.map((audio, index) => { return { index: index, - lang: audio.language || "", - label: audio.label || "", + lang: audio.language || '', + label: audio.label || '' } as IAudioTrack }) } get currentAudioTrackIndex(): number { - const track = this.audioTracks.find(audio => audio.enabled) - if (track == undefined) { return -1 } + const track = this.audioTracks.find((audio) => audio.enabled) + if (track == undefined) { + return -1 + } return this.audioTracks.indexOf(track) } setCurrentAudioTrack(index: number) { - this.bus.publish(VokaBusEvent.audioTracksSet({audioTrackId: index})) + const track = this.audioTracks[index] + if (!track) { + log.error('Invalid audio tack index', index) + return + } + this.audioTracks.forEach((e, i) => { + e.enabled = i === index + }) + this.bus.publish(VokaBusEvent.audioTracksSet({ audioTrackId: track.id })) } } } -export default AudioTrackObserver \ No newline at end of file +export default AudioTrackObserver diff --git a/src/internal/observers/VideoQualityObserver.ts b/src/internal/observers/VideoQualityObserver.ts index c8d69ff..5d6a1e1 100644 --- a/src/internal/observers/VideoQualityObserver.ts +++ b/src/internal/observers/VideoQualityObserver.ts @@ -1,10 +1,13 @@ +import type { EventBus } from 'ts-bus' +import videojs from 'video.js' + import VokaBusEvent from '@/internal/events/VokaBusEvent' +import type VokaCorePlayer from '@/internal/player/VokaCorePlayer' import { Quality } from '@/public/models/ILoadOptions' -import VokaCorePlayer from '@/internal/player/VokaCorePlayer' -import { EventBus } from 'ts-bus' + +const log = videojs.log.createLogger('[VideoQualityObserver]') namespace VideoQualityObserver { - export interface IQuality { index: number bitrate: number | null @@ -24,12 +27,11 @@ namespace VideoQualityObserver { videoQualityList: [], selectedVideoQuality: -1, playingVideoQuality: -1, - setSelectedVideoQuality(index: number): (value) => void {}, + setSelectedVideoQuality(index: number): (value) => void {} } as IObserver } export class Observer implements IObserver { - private readonly bus: EventBus private qualities: VokaCorePlayer.IQualityData[] private currentIndex: number @@ -41,32 +43,28 @@ namespace VideoQualityObserver { this.currentIndex = -1 this.selectedIndex = -1 - bus.subscribe( - VokaBusEvent.qualitiesParsed, - event => { this.qualities = event.payload.qualities } - ) + bus.subscribe(VokaBusEvent.qualitiesParsed, (event) => { + this.qualities = event.payload.qualities + }) - bus.subscribe( - VokaBusEvent.qualityChange, - event => { this.currentIndex = event.payload.index } - ) - - bus.subscribe( - VokaBusEvent.qualityUISet, - event => { this.selectedIndex = event.payload.index } - ) + bus.subscribe(VokaBusEvent.qualityChange, (event) => { + this.currentIndex = event.payload.index + }) + bus.subscribe(VokaBusEvent.qualityUISet, (event) => { + this.selectedIndex = event.payload.index + }) } // MARK: - IObserver get videoQualityList(): IQuality[] { - return this.qualities.map(quality => { + return this.qualities.map((quality) => { return { index: quality.index, bitrate: quality.bitrate, width: quality.width, - height: quality.height, + height: quality.height } as IQuality }) } @@ -80,16 +78,18 @@ namespace VideoQualityObserver { } setSelectedVideoQuality(index: number) { - const quality = this.qualities.find(quality => quality.index == index) + const quality = this.qualities.find((quality) => quality.index == index) let event: VokaBusEvent.IQualitySet if (quality == undefined) { event = { + index: -1, quality: Quality.AUTO, forced: true, withUIUpdate: true } as VokaBusEvent.IQualitySet } else { event = { + index: quality.index, quality: quality.quality, forced: true, withUIUpdate: true @@ -101,4 +101,4 @@ namespace VideoQualityObserver { } } -export default VideoQualityObserver \ No newline at end of file +export default VideoQualityObserver diff --git a/src/internal/player/VokaCorePlayer.ts b/src/internal/player/VokaCorePlayer.ts index 212fd80..2a8c59d 100755 --- a/src/internal/player/VokaCorePlayer.ts +++ b/src/internal/player/VokaCorePlayer.ts @@ -1,749 +1,766 @@ -import { - IVokaSource, - TizenSourceParams, - TizenSourceProtection, - WebOSSourceProtection, -} from "@/internal/player/native/VokaSourceHandler"; -import { stat } from "fs"; -import videojs from 'video.js' -import Player from 'video.js/dist/types/player' import chromecastPlugin from '@silvermine/videojs-chromecast/' -import { EventBus } from 'ts-bus' -import { ITimeRange} from '@/public/IVokaPlayer' -import { AutoplayChecker } from '@/internal/utils/AutoplayChecker' -import { Quality } from '@/public/models/ILoadOptions' -import * as languages from '@/languages.json' -import '@/plugins/VokaEventsMapperPlugin' -import '@/plugins/VokaKeyboardPlugin' -import '@/plugins/VokaHeartbeatPlugin' -import '@/plugins/VokaMetricsPlugin' -import '@/plugins/VokaMagicRemotePlugin' -import '@/plugins/advertisement/VokaAdvertisementPlugin' -import '@/plugins/VokaLogPlugin' -import '@/components/Skin' -import '@/components/PosterImage' -import { VokaContentType } from '@/public/models/VokaContentType' -import VokaWebOSTech from '@/internal/player/native/webos/tech/VokaWebOSTech' -import VokaTizenTech from '@/internal/player/native/tizen/tech/VokaTizenTech' -import VokaAppleTech from '@/internal/player/native/apple/tech/VokaAppleTech' -import VokaMp4Tech from '@/internal/player/native/mp4/tech/VokaMp4Tech' -import VokaHlsTech from '@/internal/player/native/hls/tech/VokaHlsTech' -import VokaDashTech from '@/internal/player/native/dash/tech/VokaDashTech' -import VokaEmptyTech from '@/internal/player/native/empty/VokaEmptyTech' import { Promise } from 'es6-promise' -import ZoomModeObserver from '@/internal/observers/ZoomModeObserver' -import CorePlayerOptions from '@/internal/player/CorePlayerOptions' +import { EventBus } from 'ts-bus' +import videojs from 'video.js' +import type Player from 'video.js/dist/types/player' + +import VokaEvent from '@/constants/VokaEvent' import VokaBusEvent from '@/internal/events/VokaBusEvent' -import VideoQualityObserver from '@/internal/observers/VideoQualityObserver' import AudioTrackObserver from '@/internal/observers/AudioTrackObserver' +import ControlbarVisibilityObserver from '@/internal/observers/ControlbarVisibilityObserver' import SelectionObserver from '@/internal/observers/SelectionObserver' import SubtitleTracksObserver from '@/internal/observers/SubtitleTrackObserver' -import { VokaOptions } from '@/public/@types' -import VokaTech from '@/internal/player/native/VokaTech' -import VokaEvent from '@/constants/VokaEvent' -import ControlbarVisibilityObserver from '@/internal/observers/ControlbarVisibilityObserver' +import VideoQualityObserver from '@/internal/observers/VideoQualityObserver' +import ZoomModeObserver from '@/internal/observers/ZoomModeObserver' +import type CorePlayerOptions from '@/internal/player/CorePlayerOptions' +import VokaAppleTech from '@/internal/player/native/apple/tech/VokaAppleTech' +import VokaDashTech from '@/internal/player/native/dash/tech/VokaDashTech' +import VokaEmptyTech from '@/internal/player/native/empty/VokaEmptyTech' +import VokaHlsTech from '@/internal/player/native/hls/tech/VokaHlsTech' +import VokaMp4Tech from '@/internal/player/native/mp4/tech/VokaMp4Tech' +import VokaTizenTech from '@/internal/player/native/tizen/tech/VokaTizenTech' +import type { IVokaSource, TizenSourceParams, WebOSSourceProtection } from '@/internal/player/native/VokaSourceHandler' +import type VokaTech from '@/internal/player/native/VokaTech' +import VokaWebOSTech from '@/internal/player/native/webos/tech/VokaWebOSTech' +import { AutoplayChecker } from '@/internal/utils/AutoplayChecker' +import * as languages from '@/languages.json' +import type { VokaOptions } from '@/public/@types' +import type { ITimeRange } from '@/public/IVokaPlayer' +import type { Quality } from '@/public/models/ILoadOptions' +import { VokaContentType } from '@/public/models/VokaContentType' -const log = videojs.log.createLogger("[VokaCorePlayer]") +import '@/components/PosterImage' +import '@/components/Skin' +import '@/plugins/advertisement/VokaAdvertisementPlugin' +import '@/plugins/VokaEventsMapperPlugin' +import '@/plugins/VokaHeartbeatPlugin' +import '@/plugins/VokaKeyboardPlugin' +import '@/plugins/VokaLogPlugin' +import '@/plugins/VokaMagicRemotePlugin' +import '@/plugins/VokaMetricsPlugin' + +const log = videojs.log.createLogger('[VokaCorePlayer]') namespace VokaCorePlayer { + type VideoJsPlayerOptions = Parameters[1] + type PlayerReadyHandler = (player: CorePlayer) => void - type VideoJsPlayerOptions = Parameters[1] - type PlayerReadyHandler = (player: CorePlayer) => void + const playbackRates = [ 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ] + const Dom = videojs.dom + const DivID = 'vokaPlayerVideoTag_player_' - const playbackRates = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] - const Dom = videojs.dom - const DivID = 'vokaPlayerVideoTag_player_' + export type PlayerCreationClosure = (options: CorePlayerOptions.IOptions) => Promise - export type PlayerCreationClosure = (options: CorePlayerOptions.IOptions) => Promise + export interface IQualityData { + index: number + bitrate: number + quality: Quality + width: number | null + height: number | null + label: string | null + } - export interface IQualityData { - index: number - bitrate: number - quality: Quality - width: number | null - height: number | null - label: string | null + export interface IVideoInfo { + width: number | null + height: number | null + bitrate: number | null + } + + export interface IMetrics { + url: string + interval: number | null + params: { [key: string]: string } | null + } + + export interface IHeartbeat { + url: string + interval: number + version: number + } + + export enum DRMType { + NONE = 'none', + FAIRPLAY = 'fairplay', + PLAYREADY = 'playready', + WIDEVINE = 'widewine' + } + + export interface IDRMConfig { + type: DRMType + certificateUrl: string // certificate or serverURL + licenseUrl: string | null // licenseUrl + headers: [any] | null + } + + export interface IContent { + url: string + type: VokaContentType + drmConfig: IDRMConfig | null + subtitlesUrl: string | null + metrics: IMetrics | null + heartbeat: IHeartbeat | null + adv: string | null + streamOptions: VokaOptions.StreamOptions.IStream | null + } + + export function createPlayer(element: HTMLElement): PlayerCreationClosure { + return (options: CorePlayerOptions.IOptions): Promise => { + return new Promise((resolve) => { + new VokaCorePlayer.CorePlayer(element, options, (player) => { + resolve(player) + }) + }) + } + } + + export class CorePlayer { + private readonly bus: EventBus + private player!: Player + private autoPlaySupported: boolean + private autoPlayOption: boolean | 'muted' | any + private readonly stateEmitter: EventBus + private currentQuality: VokaBusEvent.IChangeQuality | null = null + private bufferingComplete: boolean = false + + private readonly controlbarVisibilityObserver: ControlbarVisibilityObserver.Observer + private readonly zoomObserver: ZoomModeObserver.Observer + private readonly videoQualityObserver: VideoQualityObserver.Observer + private readonly audioTrackObserver: AudioTrackObserver.Observer + private readonly selectionObserver: SelectionObserver.Observer + private readonly subtitleTrackObserver: SubtitleTracksObserver.Observer + + // Constructor + constructor(element: HTMLElement, options: CorePlayerOptions.IOptions, callback?: PlayerReadyHandler) { + const readyCallback = typeof callback === 'undefined' ? null : callback + this.stateEmitter = new EventBus() + + this.bus = options.bus + this.autoPlaySupported = false + this.autoPlayOption = options.autoplay + // Create observers + this.controlbarVisibilityObserver = new ControlbarVisibilityObserver.Observer() + this.zoomObserver = new ZoomModeObserver.Observer() + this.videoQualityObserver = new VideoQualityObserver.Observer(this.bus) + this.audioTrackObserver = new AudioTrackObserver.Observer(this.bus) + this.selectionObserver = new SelectionObserver.Observer() + this.subtitleTrackObserver = new SubtitleTracksObserver.Observer(this.bus) + // Check autoplay and init player + AutoplayChecker.isAutoplaySupported((supported) => { + this.setupPlayer(element, supported, options, readyCallback) + }) } - export interface IVideoInfo { - width: number | null - height: number | null - bitrate: number | null + // MARK: - Public + + public getZoomObserver(): ZoomModeObserver.IObserver { + return this.zoomObserver } - export interface IMetrics { - url: string - interval: number | null - params: { [key: string]: string } | null + public getVideoQualityObserver(): VideoQualityObserver.IObserver { + return this.videoQualityObserver } - export interface IHeartbeat { - url: string - interval: number - version: number + public getAudioTrackObserver(): AudioTrackObserver.IObserver { + return this.audioTrackObserver } - export enum DRMType { - NONE = 'none', - FAIRPLAY = 'fairplay', - PLAYREADY = 'playready', - WIDEVINE = 'widewine', + public getSelectionObserver(): SelectionObserver.IObserver { + return this.selectionObserver } - export interface IDRMConfig { - type: DRMType - certificateUrl: string // certificate or serverURL - licenseUrl: string | null, // licenseUrl - headers: [any] | null + public getSubtitleTrackObserver(): SubtitleTracksObserver.IObserver { + return this.subtitleTrackObserver } - export interface IContent { - url: string, - type: VokaContentType, - drmConfig: IDRMConfig | null - subtitlesUrl: string | null - metrics: IMetrics | null - heartbeat: IHeartbeat | null - adv: string | null - streamOptions: VokaOptions.StreamOptions.IStream | null + public getControlbarVisibilityObserver(): ControlbarVisibilityObserver.IObserver { + return this.controlbarVisibilityObserver } - export function createPlayer(element: HTMLElement): PlayerCreationClosure { - return (options: CorePlayerOptions.IOptions): Promise => { - return new Promise( - (resolve) => { - new VokaCorePlayer.CorePlayer(element, options, (player) => { - resolve(player) - }) + public getIsUserActive(): boolean { + if (this.player.paused()) { + return true + } + return this.player.userActive() || false + } + + public getVideoInfo(): IVideoInfo | null { + const quality = this.currentQuality + if (quality != null) { + return { + width: quality.width, + height: quality.height, + bitrate: quality.bitrate + } as IVideoInfo + } + return { + width: null, + height: null, + bitrate: null + } as IVideoInfo + } + + public getCurrentBandwidth(): number | null { + return this.currentQuality?.bandwidth || null + } + + public getAbsoluteRange(): ITimeRange | null { + if (this.player.liveTracker) { + return { + start: this.player.liveTracker.seekableStart(), + end: this.player.liveTracker.seekableEnd() + } + } + return { + start: 0, + end: this.player.duration() || 0 + } + } + + public getTimeshiftAvailable(): boolean { + const tech = this.player.tech({ safeUsage: true }) as VokaTech.Tech + // This require because Tizen and WebOS do not inherit from VokaTech + if (typeof tech['getTimeshiftAvailable'] === 'function') { + return tech.getTimeshiftAvailable() + } + return false + } + + public get isPaused(): boolean { + return this.player.paused() + } + + public get currentTime(): number { + return this.player.currentTime() || 0 + } + + public get duration(): number { + return this.player.duration() || 0 + } + + public get volume(): number { + return this.player.volume() || 0 + } + + public setVolume(value: number) { + this.player.volume(value) + } + + public get bufferLength(): number { + return this.player.bufferedPercent() + } + + public get isBuffering(): boolean { + return false + } + + public seek(seconds: number): void { + log('[seek()] called with seconds', seconds) + if (Math.abs(this.currentTime - seconds) < 0.5 || !isFinite(seconds)) { + log('[seek()] Skip step less than 0.5 sec') + return + } + + const range = this.getAbsoluteRange() + const start = range?.start ?? 0 + const end = range?.end || Infinity + + if (seconds < start) { + seconds = start + log('[seek()] reset seconds to start value', seconds, start) + } else if (seconds >= end) { + seconds = end - 0.5 + log('[seek()] reset seconds under end value', seconds, end) + } + + this.player.currentTime(seconds) + } + + public mute(on: boolean) { + this.player.muted(on) + } + + public play(): Promise | undefined { + return this.player.play() + } + + public pause() { + return this.player.pause() + } + + public stop() { + return this.player.pause() + } + + public get isMuted(): boolean { + return this.player.muted() || false + } + + public get isLive(): boolean { + const duration = this.player.duration() + if (duration == undefined) { + return true + } + if (isNaN(duration)) { + return false + } + return !isFinite(duration) + } + + public async load(content: IContent): Promise { + if (!content.url) { + throw new Error('Empty content URL') + } + + const drmTypes = { + [DRMType.FAIRPLAY]: 'com.apple.fps', + [DRMType.PLAYREADY]: 'com.microsoft.playready', + [DRMType.WIDEVINE]: 'com.widevine.alpha' + } + let playableContent: IVokaSource = null + const drm = content.drmConfig + switch (content.type) { + case VokaContentType.HLS: { + playableContent = { + sourceType: 'application/vnd.apple.mpegurl' + } as IVokaSource + + if (!drm) break + const drmTypeString = drmTypes[drm.type] + if (!drmTypeString) break + + if (drm.type === DRMType.FAIRPLAY) { + // hls.js + playableContent.drmSystems = { + [drmTypeString]: { + certificateUrl: drm.certificateUrl, + licenseUrl: drm.licenseUrl } - ) + } + } else { + // hls.js + playableContent.drmSystems = { + [drmTypeString]: { + licenseUrl: drm.certificateUrl + } + } + // WebOS native + playableContent.webOSProtection = { + licenseServer: drm.certificateUrl + } as WebOSSourceProtection + // Tizen native + playableContent.tizenParams = { + protection: { + type: drm.type, + licenseServer: drm.certificateUrl + } + } as TizenSourceParams + } + break } + case VokaContentType.DASH: + case VokaContentType.WIDEVINE: + case VokaContentType.PLAYREADY: { + playableContent = { + sourceType: 'application/dash+xml' + } as IVokaSource + + if (!drm) break + const allowedDrmTypes: VokaCorePlayer.DRMType[] = [DRMType.WIDEVINE, DRMType.PLAYREADY] + + if (!allowedDrmTypes.includes(drm.type)) break + // dash.js + playableContent.keySystemOptions = [ + { + name: drmTypes[drm.type], + options: { + serverURL: drm.certificateUrl, + // Доп. заголовки, например, авторизация запсроса к серверу лицензий + // httpRequestHeaders: { + // "X-AxDRM-Message": + // "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICI0MDYwYTg2NS04ODc4LTQyNjctOWNiZi05MWFlNWJhZTFlNzIiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAid3QzRW51dVI1UkFybjZBRGYxNkNCQT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ.l8PnZznspJ6lnNmfAE9UQV532Ypzt1JXQkvrk8gFSRw" + // }, + httpRequestHeaders: drm.headers, + priority: 0 + } + } + ] + // WebOS native + playableContent.webOSProtection = { + licenseServer: drm.certificateUrl + } as WebOSSourceProtection + // Tizen native + playableContent.tizenParams = { + protection: { + type: drm.type, + licenseServer: drm.certificateUrl + // Доп. заголовки, например, авторизация запроса к серверу лицензий + // В виде строки, разделитель заголовков - \n + // httpHeader: + // "X-AxDRM-Message:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICI0MDYwYTg2NS04ODc4LTQyNjctOWNiZi05MWFlNWJhZTFlNzIiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAid3QzRW51dVI1UkFybjZBRGYxNkNCQT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ.l8PnZznspJ6lnNmfAE9UQV532Ypzt1JXQkvrk8gFSRw" + } + } as TizenSourceParams + + break + // Нативный FP (Safari & iOS) + } + case VokaContentType.FAIRPLAY: + if (drm != null && drm.type == DRMType.FAIRPLAY) { + playableContent = { + sourceType: 'application/vnd.apple.mpegurl', + protection: { + keySystem: 'com.apple.fps.1_0', + certificateUrl: drm.certificateUrl, + licenseUrl: drm.licenseUrl + } + } as IVokaSource + } + break + case VokaContentType.AES: + playableContent = { + sourceType: 'application/vnd.apple.mpegurl' + } as IVokaSource + break + case VokaContentType.MP4: + playableContent = { + sourceType: 'video/mp4' + } as IVokaSource + break + } + + if (playableContent != null) { + playableContent['bus'] = this.bus + playableContent['src'] = content.url + playableContent['content'] = content.type + playableContent['adv'] = content.adv + playableContent['subtitlesUrl'] = content.subtitlesUrl + playableContent['manifest'] = content.streamOptions?.manifest || { source: '', video: [], audio: [] } + + if (content.streamOptions?.autoplay != null) { + this.autoPlayOption = content.streamOptions?.autoplay + } + + this.player.src(playableContent) + // hls.js в процессе работы может менять исходник на blob, а такое + // не подходит для хромкаст. Временное решение для сохранения исходника. + this.player._originalSrc = playableContent + this.bus.publish(VokaBusEvent.switchContent(content)) + + // MARK: Пример задания рекламных точек + // this.bus.publish( + // VokaBusEvent.adsMarkersSet({ + // items: [ + // { + // index: 0, + // timestamp: 10 + // }, + // { + // index: 1, + // timestamp: 60 + // }, + // { + // index: 2, + // timestamp: 90 + // } + // ] + // }) + // ) + + return Promise.resolve() + } + throw new Error('Unsupported') } - export class CorePlayer { - - private readonly bus: EventBus - private player!: Player - private autoPlaySupported: boolean - private autoPlayOption: boolean | "muted" | any - private readonly stateEmitter: EventBus - private currentQuality: VokaBusEvent.IChangeQuality | null = null - private bufferingComplete: boolean = false - - private readonly controlbarVisibilityObserver: ControlbarVisibilityObserver.Observer - private readonly zoomObserver: ZoomModeObserver.Observer - private readonly videoQualityObserver: VideoQualityObserver.Observer - private readonly audioTrackObserver: AudioTrackObserver.Observer - private readonly selectionObserver: SelectionObserver.Observer - private readonly subtitleTrackObserver: SubtitleTracksObserver.Observer - - // Constructor - constructor(element: HTMLElement, options: CorePlayerOptions.IOptions, callback?: PlayerReadyHandler) { - const readyCallback = typeof callback === 'undefined' ? null : callback - this.stateEmitter = new EventBus() - - this.bus = options.bus - this.autoPlaySupported = false - this.autoPlayOption = options.autoplay - // Create observers - this.controlbarVisibilityObserver = new ControlbarVisibilityObserver.Observer() - this.zoomObserver = new ZoomModeObserver.Observer() - this.videoQualityObserver = new VideoQualityObserver.Observer(this.bus) - this.audioTrackObserver = new AudioTrackObserver.Observer(this.bus) - this.selectionObserver = new SelectionObserver.Observer() - this.subtitleTrackObserver = new SubtitleTracksObserver.Observer(this.bus) - // Check autoplay and init player - AutoplayChecker.isAutoplaySupported((supported) => { - this.setupPlayer(element, supported, options, readyCallback) - }) - } - - // MARK: - Public - - public getZoomObserver(): ZoomModeObserver.IObserver { return this.zoomObserver } - - public getVideoQualityObserver(): VideoQualityObserver.IObserver { return this.videoQualityObserver } - - public getAudioTrackObserver(): AudioTrackObserver.IObserver { return this.audioTrackObserver } - - public getSelectionObserver(): SelectionObserver.IObserver { return this.selectionObserver } - - public getSubtitleTrackObserver(): SubtitleTracksObserver.IObserver { return this.subtitleTrackObserver } - - public getControlbarVisibilityObserver(): ControlbarVisibilityObserver.IObserver { return this.controlbarVisibilityObserver } - - public getIsUserActive(): boolean { - if (this.player.paused()) { return true } - return this.player.userActive() || false - } - - public getVideoInfo(): IVideoInfo | null { - const quality = this.currentQuality - if (quality != null) { - return { - width: quality.width, - height: quality.height, - bitrate: quality.bitrate, - } as IVideoInfo - } - return { - width: null, - height: null, - bitrate: null, - } as IVideoInfo - } - - public getCurrentBandwidth(): number | null { - return this.currentQuality?.bandwidth || null - } - - public getAbsoluteRange(): ITimeRange | null { - if (this.player.liveTracker) { - return { - start: this.player.liveTracker.seekableStart(), - end: this.player.liveTracker.seekableEnd() - } - } - return { - start: 0, - end: this.player.duration() || 0, - } - } - - public getTimeshiftAvailable(): boolean { - const tech = this.player.tech({safeUsage: true}) as VokaTech.Tech - // This require because Tizen and WebOS do not inherit from VokaTech - if (typeof tech['getTimeshiftAvailable'] === 'function') { - return tech.getTimeshiftAvailable() - } - return false - } - - public get isPaused(): boolean { return this.player.paused() } - - public get currentTime(): number { return this.player.currentTime() || 0 } - - public get duration(): number { return this.player.duration() || 0 } - - public get volume(): number { return this.player.volume() || 0 } - - public setVolume(value: number) { this.player.volume(value) } - - public get bufferLength(): number { return this.player.bufferedPercent() } - - public get isBuffering(): boolean { return false } - - public seek(seconds: number): void { - log("[seek()] called with seconds", seconds) - if (Math.abs(this.currentTime - seconds) < 0.5 || !isFinite(seconds)) { - log("[seek()] Skip step less than 0.5 sec") - return - } - - const range = this.getAbsoluteRange() - const start = range?.start ?? 0 - const end = range?.end || Infinity - - if (seconds < start) { - seconds = start - log("[seek()] reset seconds to start value", seconds, start) - } else if (seconds >= end) { - seconds = end - 0.5 - log("[seek()] reset seconds under end value", seconds, end) - } - - this.player.currentTime(seconds) - } - - public mute(on: boolean) { this.player.muted(on) } - - public play(): Promise | undefined { return this.player.play() } - - public pause() { return this.player.pause() } - - public stop() { return this.player.pause() } - - public get isMuted(): boolean { return this.player.muted() || false } - - public get isLive(): boolean { - const duration = this.player.duration() - if (duration == undefined) { return true } - if (isNaN(duration)) { return false } - if (isFinite(duration)) { return false } - return true - } - - public async load(content: IContent): Promise { - - if (!content.url) { - throw new Error('Empty content URL') - } - - const drmTypes = { - [DRMType.WIDEVINE]: 'com.widevine.alpha', - [DRMType.PLAYREADY]: 'com.microsoft.playready', - [DRMType.FAIRPLAY]: 'com.apple.fps', - } - let playableContent: IVokaSource = null - const drm = content.drmConfig - switch(content.type) { - case VokaContentType.HLS: - playableContent = { - sourceType: "application/vnd.apple.mpegurl", - } as IVokaSource - - if (!drm) break - const drmTypeString = drmTypes[drm.type] - if (!drmTypeString) break - - if (drm.type === DRMType.FAIRPLAY) { - // hls.js - playableContent.drmSystems = { - [drmTypeString]: { - certificateUrl: drm.certificateUrl, - licenseUrl: drm.licenseUrl - } - } - } else { - // hls.js - playableContent.drmSystems = { - [drmTypeString]: { - licenseUrl: drm.certificateUrl - } - } - // WebOS native - playableContent.webOSProtection = { - licenseServer: drm.certificateUrl - } as WebOSSourceProtection - // Tizen native - playableContent.tizenParams = { - protection: { - type: drm.type, - licenseServer: drm.certificateUrl - } - } as TizenSourceParams - } - break - case VokaContentType.DASH: - case VokaContentType.WIDEVINE: - case VokaContentType.PLAYREADY: - playableContent = { - sourceType: "application/dash+xml", - } as IVokaSource; - - if (!drm) break; - const allowedDrmTypes = [DRMType.WIDEVINE, DRMType.PLAYREADY]; - - if (!allowedDrmTypes.includes(drm.type)) break; - // dash.js - playableContent.keySystemOptions = [{ - name: drmTypes[drm.type], - options: { - serverURL: drm.certificateUrl, - // Доп. заголовки, например, авторизация запсроса к серверу лицензий - // httpRequestHeaders: { - // "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICI0MDYwYTg2NS04ODc4LTQyNjctOWNiZi05MWFlNWJhZTFlNzIiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAid3QzRW51dVI1UkFybjZBRGYxNkNCQT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ.l8PnZznspJ6lnNmfAE9UQV532Ypzt1JXQkvrk8gFSRw" - // }, - httpRequestHeaders: drm.headers, - priority: 0, - }, - }, - ]; - // WebOS native - playableContent.webOSProtection = { - licenseServer: drm.certificateUrl, - } as WebOSSourceProtection; - // Tizen native - playableContent.tizenParams = { - protection: { - type: drm.type, - licenseServer: drm.certificateUrl, - // Доп. заголовки, например, авторизация запроса к серверу лицензий - // В виде строки, разделитель заголовков - \n - // httpHeader: - // "X-AxDRM-Message:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICI0MDYwYTg2NS04ODc4LTQyNjctOWNiZi05MWFlNWJhZTFlNzIiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAid3QzRW51dVI1UkFybjZBRGYxNkNCQT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ.l8PnZznspJ6lnNmfAE9UQV532Ypzt1JXQkvrk8gFSRw" - }, - } as TizenSourceParams; - - break; - // Нативный FP (Safari & iOS) - case VokaContentType.FAIRPLAY: - if (drm != null && drm.type == DRMType.FAIRPLAY) { - playableContent = { - sourceType: "application/vnd.apple.mpegurl", - protection: { - keySystem: "com.apple.fps.1_0", - certificateUrl: drm.certificateUrl, - licenseUrl: drm.licenseUrl, - }, - } as IVokaSource - } - break - case VokaContentType.AES: - playableContent = { - sourceType: "application/vnd.apple.mpegurl", - } as IVokaSource - break - case VokaContentType.MP4: - playableContent = { - sourceType: "video/mp4", - } as IVokaSource - break - - } - - if (playableContent != null) { - playableContent['bus'] = this.bus - playableContent['src'] = content.url - playableContent['content'] = content.type - playableContent['adv'] = content.adv - playableContent['subtitlesUrl'] = content.subtitlesUrl - - if (content.streamOptions?.autoplay != null) { - this.autoPlayOption = content.streamOptions?.autoplay - } - - this.player.src(playableContent) - // hls.js в процессе работы может менять исходник на blob, а такое - // не подходит для хромкаст. Временное решение для сохранения исходника. - this.player._originalSrc = playableContent - this.bus.publish(VokaBusEvent.switchContent(content)) - - // MARK: Пример задания рекламных точек - // this.bus.publish( - // VokaBusEvent.adsMarkersSet({ - // items: [ - // { - // index: 0, - // timestamp: 10 - // }, - // { - // index: 1, - // timestamp: 60 - // }, - // { - // index: 2, - // timestamp: 90 - // } - // ] - // }) - // ) - - return Promise.resolve() - } - throw new Error('Unsupported') - } - - public destroy() { - this.detachListeners(this.player) - this.player.dispose() - } - - public enable() { - this.playerEl.style.display = "block" - } - - public disable() { - this.playerEl.style.display = "none" - } - - // MARK: - Private - - private get playerEl() { - return this.player.el() as HTMLElement - } - - private setupPlayer( - element: HTMLElement, - autoPlaySupported: boolean, - options: CorePlayerOptions.IOptions, - callback: PlayerReadyHandler | null - ) { - this.autoPlaySupported = autoPlaySupported - - // Create main container for player. - const playerContainer = Dom.createEl( - 'div', - { id: `${DivID}` }, - {} - ) - element.appendChild(playerContainer) - - // Явная регистрация плагина chromecast нужна из-за структуры плагина - chromecastPlugin(videojs) - - const playerOptions = this.initOptions(options) - const player = videojs( - playerContainer, - playerOptions, - () => { - if (callback != null) { - callback(this) - } - - // try load content just right after complete initialization. - /*this.stateEmitter.publish( - playerStateChange({ value: Initialized }) - )*/ - }) - - player.addClass('video-js') - - // Sign to events. - this.attachBusListeners(this.bus) - this.setupAttachListeners(player, options) - this.player = player - - this.initialSetup() - } - - private initOptions(options: CorePlayerOptions.IOptions): VideoJsPlayerOptions { - // Required player's plugins. - const plugins: Record = { } - // plugins.vokaKeyboardPlugin = { - // skip: { - // forward: 5, - // backward: 5, - // }, - // } - // Приложение отвечает полностью за управление воспроизведением с пульта - // См. http://srv-gitlabprod-001.main.velcom.by/Online_and_Web-Services_Group/voka/smarttv/smart-tv-client/-/blob/main/src/app/components/ScenePlayer/Player/controls/usePlayerHotKeysPausePlayStop.tsx - // plugins.vokaMagicRemotePlugin = { - // skip: { - // forward: 5, - // backward: 5, - // }, - // } - plugins.vokaEventsMapperPlugin = { - zoomObserver: this.zoomObserver, - selectionObserver: this.selectionObserver, - bus: this.bus, - } - plugins.vokaHeartbeatPlugin = { - bus: this.bus, - } - plugins.vokaMetricsPlugin = { - bus: this.bus, - } - plugins.vokaAdvertisementPlugin = { - bus: this.bus, - } - plugins.vokaLogPlugin = { - enabled: !!options.log, - id: options.loggerId, - } - plugins.chromecast = { - buttonPositionIndex: -2, - } - - const childrenComponents = [ - // Non-visual component, not in UI layer list. - 'mediaLoader' // Required loader, that load tech list! - ] - - const skinChildren = [ - 'Skin', - 'resizeManager', - 'LoadingSpinner', - 'PosterImage', - 'RestrictionBox', - 'liveTracker', - ] - - childrenComponents.push(...skinChildren) - - // Settings for player. - return { - loggerId: options.loggerId, - log: options.log, - autoplay: options.autoplay == "muted" || options.autoplay == true, - bus: options.bus, - techOrder: [ - 'chromecast', - VokaEmptyTech.TECH_NAME, - VokaWebOSTech.TECH_NAME, - VokaTizenTech.TECH_NAME, - VokaAppleTech.TECH_NAME, - VokaMp4Tech.TECH_NAME, - VokaHlsTech.TECH_NAME, - VokaDashTech.TECH_NAME, - ], // Order for teches important. - // MARK: пример задания заголовков для хромкаст - chromecast: { - requestTitleFn: ( currentSource ) => { log(currentSource); return 'Title' }, - requestSubtitleFn: ( currentSource ) => { log(currentSource); return 'SubTitle' }, - disableAds: true - }, - plugins, // Attached plugins for player. - languages, - language: 'ru', - children: childrenComponents, - // MARK: Пример постера - // poster: 'https://i.ytimg.com/vi/u3q7GGLpiqk/maxresdefault.jpg', - responsive: true, - breakpoints: { - tiny: undefined, - xsmall: undefined, - small: 559, - medium: 1002, - large: undefined, - xlarge: undefined, - huge: Infinity - }, - playbackRates, - tracks: options.controls.tracks, - skip: { - forward: 5, - backward: 5, - }, - controls: { - play: true, - replay: true, - volume: { - inline: true, - }, - progress: true, - zoomButton: { // OUR - observer: this.zoomObserver, - isVisible: options.controls.zoomButton.isVisible, - enable: options.controls.zoomButton.enable, - }, - fullscreen: true, - selection: { - observer: this.selectionObserver, - enable: options.controls.selectionButton.enable, - }, - CentralPanel: { - observer: this.controlbarVisibilityObserver, - isVisible: options.controls.isVisible, - }, - BottomPanel: { - observer: this.controlbarVisibilityObserver, - isVisible: options.controls.isVisible, - }, - }, - previewPopup: { - imageCallback: (percent: number) => { - return '' - // MARK: пример ссылки на превью - // return `https://streaming.voka.tv/vod_preview/velcom/4W17sRxFu8eCAps3kdTxJrFk7d46DNmum_320x180.jpeg?preview_pos=${percent}`; - }, - }, - enableDocumentPictureInPicture: true, - } - } - - private attachBusListeners(bus: EventBus) { - bus.subscribe( - VokaBusEvent.qualityChange, - (event) => { - this.currentQuality = event.payload - this.changeZoomButtonVisible() - }, - ) - } - - private updateBufferingMode(value: boolean) { - this.bus.publish( - VokaBusEvent.updateBufferingMode({ value }) - ) - } - - private changeZoomButtonVisible() { - if (!this.bus) return - this.bus.publish( - VokaBusEvent.changeZoomButtonVisible({ value: this.zoomObserver.isVisible }) - ) - } - - private updateTimeshift(value: boolean) { - if (!this.bus) return - const available = this.isLive && this.getTimeshiftAvailable() && this.player.options?.playback?.supportTimeshift - this.bus.publish( - VokaBusEvent.updateTimeshift({ available }) - ) - } - - private setupAttachListeners(player: Player, options: CorePlayerOptions.IOptions) { - // - // Добавляем листенеры здесь, не забываем удалять в detachListeners() - // - player.on(['loadstart', 'canplay'], () => { - // Video.js поддерживает имитацию автостарта с параметрами - // "muted", "play", "any" через отслеживание событий ready и loadstart (см manualAutoplay_() и play_()) - // hls.js стартует так, что событие loadstart иногда приходит раньше ready, а на - // ready дополнительно завязано состояние changingSrc_, от которого - // зависит запуск воспроизведения - возникает гонка. - // Поэтому здесь дополнительно слушаем canplay, чтобы подтолкнуть "ручной" автоплей, если он не - // сработал с первого раза. - if (player.paused()) this.setupAutoplay(this.autoPlayOption); - }) - player.on('waiting', () => this.updateBufferingMode(true)) - player.on(['playing', 'canplay', 'play'], () => this.updateBufferingMode(false)) - player.on('ended', () => { - if (player.techName_ !== VokaDashTech.TECH_NAME || this.bufferingComplete) { - this.updateBufferingMode(false) - } - }) - player.on('canplay', this.updateTimeshift) - player.one('playing', () => {player.childNameIndex_['PosterImage'].hide()}) - - player.on('canplay', this.changeZoomButtonVisible) - player.on(VokaEvent.MasterDashManifestParsed, this.changeZoomButtonVisible) - window.addEventListener('resize', this.changeZoomButtonVisible, true) - } - - private detachListeners(player: Player) { - window.removeEventListener('resize', this.changeZoomButtonVisible, true) - player.off(VokaEvent.MasterDashManifestParsed) - player.off(['canplay', 'playing', 'ended', 'play', 'waiting', 'loadstart']) - } - - private initialSetup() { - // Zoom settings - if(this.zoomObserver.isVisible) { - this.zoomObserver.isEnabledEmitter.subscribe( - ZoomModeObserver.isEnabledEvent, - (event) => { this.updateZoomCanvas(event.payload.value) }, - ) - this.updateZoomCanvas(this.zoomObserver.isEnabled) - } - } - - private updateZoomCanvas(isZoomed: boolean) { - const videoElement = this.player.el().querySelector('video') - if (videoElement) { - videoElement.style.objectFit = isZoomed ? 'cover' : 'contain' - } - } - - private setupAutoplay(autoplay: boolean | "muted" | any) { - if (!this.autoPlaySupported) { - log("[setupAutoplay] Autoplay is not supported") - return - } - // проверка допустимых значений - if (![true, false, "true", "false", "muted"].includes(autoplay)) { - log("[setupAutoplay] Invalid autoplay value", autoplay) - return - } - - // Определяем значение для передачи в player.autoplay() - let autoplayValue: boolean | "muted"; - - if (autoplay === "muted") { - autoplayValue = "muted"; - } else if (this.isLive) { - // Для лайв всегда форсируем автоплей (бизнес правила Voka) - autoplayValue = true; - } else { - autoplayValue = JSON.parse(autoplay); - } - - log("[setupAutoplay] Set autoplay value", autoplayValue) - this.player.autoplay(autoplayValue); - } + public destroy() { + this.detachListeners(this.player) + this.player.dispose() } + + public enable() { + this.playerEl.style.display = 'block' + } + + public disable() { + this.playerEl.style.display = 'none' + } + + // MARK: - Private + + private get playerEl() { + return this.player.el() as HTMLElement + } + + private setupPlayer( + element: HTMLElement, + autoPlaySupported: boolean, + options: CorePlayerOptions.IOptions, + callback: PlayerReadyHandler | null + ) { + this.autoPlaySupported = autoPlaySupported + + // Create main container for player. + const playerContainer = Dom.createEl('div', { id: `${DivID}` }, {}) + element.appendChild(playerContainer) + + // Явная регистрация плагина chromecast нужна из-за структуры плагина + chromecastPlugin(videojs) + + const playerOptions = this.initOptions(options) + const player = videojs(playerContainer, playerOptions, () => { + if (callback != null) { + callback(this) + } + }) + + player.addClass('video-js') + + this.attachBusListeners(this.bus) + this.setupAttachListeners(player, options) + this.player = player + + this.initialSetup() + } + + private initOptions(options: CorePlayerOptions.IOptions): VideoJsPlayerOptions { + // Required player's plugins. + const plugins: Record = {} + // plugins.vokaKeyboardPlugin = { + // skip: { + // forward: 5, + // backward: 5, + // }, + // } + // Приложение отвечает полностью за управление воспроизведением с пульта + // См. http://srv-gitlabprod-001.main.velcom.by/Online_and_Web-Services_Group/voka/smarttv/smart-tv-client/-/blob/main/src/app/components/ScenePlayer/Player/controls/usePlayerHotKeysPausePlayStop.tsx + // plugins.vokaMagicRemotePlugin = { + // skip: { + // forward: 5, + // backward: 5, + // }, + // } + plugins.vokaEventsMapperPlugin = { + zoomObserver: this.zoomObserver, + selectionObserver: this.selectionObserver, + bus: this.bus + } + plugins.vokaHeartbeatPlugin = { + bus: this.bus + } + plugins.vokaMetricsPlugin = { + bus: this.bus + } + plugins.vokaAdvertisementPlugin = { + bus: this.bus + } + plugins.vokaLogPlugin = { + enabled: !!options.log, + id: options.loggerId + } + plugins.chromecast = { + buttonPositionIndex: -2 + } + + const childrenComponents = [ + // Non-visual component, not in UI layer list. + 'mediaLoader' // Required loader, that load tech list! + ] + + const skinChildren = [ 'Skin', 'resizeManager', 'LoadingSpinner', 'PosterImage', 'RestrictionBox', 'liveTracker' ] + + childrenComponents.push(...skinChildren) + + // Settings for player. + return { + loggerId: options.loggerId, + log: options.log, + autoplay: options.autoplay == 'muted' || options.autoplay == true, + bus: options.bus, + techOrder: [ + 'chromecast', + VokaEmptyTech.TECH_NAME, + VokaDashTech.TECH_NAME, + VokaTizenTech.TECH_NAME, + VokaWebOSTech.TECH_NAME, + // FIXME запретить использовать с лайвом на WebOS и вообще на WebOs + VokaHlsTech.TECH_NAME, + VokaAppleTech.TECH_NAME, + VokaMp4Tech.TECH_NAME + ], // Order for teches important. + // MARK: пример задания заголовков для хромкаст + chromecast: { + requestTitleFn: (currentSource) => { + log(currentSource) + return 'Title' + }, + requestSubtitleFn: (currentSource) => { + log(currentSource) + return 'SubTitle' + }, + disableAds: true + }, + plugins, // Attached plugins for player. + languages, + language: 'ru', + children: childrenComponents, + // MARK: Пример постера + // poster: 'https://i.ytimg.com/vi/u3q7GGLpiqk/maxresdefault.jpg', + responsive: true, + breakpoints: { + tiny: undefined, + xsmall: undefined, + small: 559, + medium: 1002, + large: undefined, + xlarge: undefined, + huge: Infinity + }, + playbackRates, + tracks: options.controls.tracks, + skip: { + forward: 5, + backward: 5 + }, + controls: { + play: true, + replay: true, + volume: { + inline: true + }, + progress: true, + zoomButton: { + // OUR + observer: this.zoomObserver, + isVisible: options.controls.zoomButton.isVisible, + enable: options.controls.zoomButton.enable + }, + fullscreen: true, + selection: { + observer: this.selectionObserver, + enable: options.controls.selectionButton.enable + }, + CentralPanel: { + observer: this.controlbarVisibilityObserver, + isVisible: options.controls.isVisible + }, + BottomPanel: { + observer: this.controlbarVisibilityObserver, + isVisible: options.controls.isVisible + } + }, + previewPopup: { + imageCallback: (percent: number) => { + return '' + // MARK: пример ссылки на превью + // return `https://streaming.voka.tv/vod_preview/velcom/4W17sRxFu8eCAps3kdTxJrFk7d46DNmum_320x180.jpeg?preview_pos=${percent}`; + } + }, + enableDocumentPictureInPicture: true + } + } + + private attachBusListeners(bus: EventBus) { + bus.subscribe(VokaBusEvent.qualityChange, (event) => { + this.currentQuality = event.payload + this.changeZoomButtonVisible() + }) + } + + private updateBufferingMode(value: boolean) { + this.bus.publish(VokaBusEvent.updateBufferingMode({ value })) + } + + private changeZoomButtonVisible() { + if (!this.bus) return + this.bus.publish(VokaBusEvent.changeZoomButtonVisible({ value: this.zoomObserver.isVisible })) + } + + private updateTimeshift(value: boolean) { + if (!this.bus) return + const available = this.isLive && this.getTimeshiftAvailable() && this.player.options?.playback?.supportTimeshift + this.bus.publish(VokaBusEvent.updateTimeshift({ available })) + } + + private setupAttachListeners(player: Player, options: CorePlayerOptions.IOptions) { + // + // Добавляем листенеры здесь, не забываем удалять в detachListeners() + // + player.on(['loadstart', 'canplay'], () => { + // Video.js поддерживает имитацию автостарта с параметрами + // "muted", "play", "any" через отслеживание событий ready и loadstart (см manualAutoplay_() и play_()) + // hls.js стартует так, что событие loadstart иногда приходит раньше ready, а на + // ready дополнительно завязано состояние changingSrc_, от которого + // зависит запуск воспроизведения - возникает гонка. + // Поэтому здесь дополнительно слушаем canplay, чтобы подтолкнуть "ручной" автоплей, если он не + // сработал с первого раза. + if (player.paused()) this.setupAutoplay(this.autoPlayOption) + }) + player.on('waiting', () => this.updateBufferingMode(true)) + player.on(['playing', 'canplay', 'play'], () => this.updateBufferingMode(false)) + player.on('ended', () => { + if (player.techName_ !== VokaDashTech.TECH_NAME || this.bufferingComplete) { + this.updateBufferingMode(false) + } + }) + player.on('canplay', this.updateTimeshift) + player.one('playing', () => { + player.childNameIndex_['PosterImage'].hide() + }) + + player.on('canplay', this.changeZoomButtonVisible) + player.on(VokaEvent.MasterDashManifestParsed, this.changeZoomButtonVisible) + window.addEventListener('resize', this.changeZoomButtonVisible, true) + } + + private detachListeners(player: Player) { + window.removeEventListener('resize', this.changeZoomButtonVisible, true) + player.off(VokaEvent.MasterDashManifestParsed) + player.off(['canplay', 'playing', 'ended', 'play', 'waiting', 'loadstart']) + } + + private initialSetup() { + // Zoom settings + if (this.zoomObserver.isVisible) { + this.zoomObserver.isEnabledEmitter.subscribe(ZoomModeObserver.isEnabledEvent, (event) => { + this.updateZoomCanvas(event.payload.value) + }) + this.updateZoomCanvas(this.zoomObserver.isEnabled) + } + } + + private updateZoomCanvas(isZoomed: boolean) { + const videoElement = this.player.el().querySelector('video') + if (videoElement) { + videoElement.style.objectFit = isZoomed ? 'cover' : 'contain' + } + } + + // https://videojs.com/guides/options/#autoplay + private setupAutoplay(autoplay: boolean | 'muted' | 'play' | 'any' | 'true' | 'false') { + if (!this.autoPlaySupported && typeof autoplay === 'boolean') { + log('[setupAutoplay] Autoplay is not supported') + return + } + + // Определяем значение для передачи в player.autoplay() + let autoplayValue: boolean | 'muted' | 'play' | 'any' + + try { + if (this.isLive) { + // Для лайв всегда (бизнес правила Voka) + autoplayValue = true + } else if ([ 'true', 'false' ].includes(autoplay)) { + autoplayValue = JSON.parse(autoplay) as boolean + } else { + autoplayValue = autoplay + } + } + catch { + autoplayValue = false + } + + log('[setupAutoplay] Set autoplay value', autoplayValue) + this.player.autoplay(autoplayValue) + } + } } -export default VokaCorePlayer \ No newline at end of file +export default VokaCorePlayer diff --git a/src/internal/player/native/PlayerLoadParameters.ts b/src/internal/player/native/PlayerLoadParameters.ts new file mode 100644 index 0000000..98991fe --- /dev/null +++ b/src/internal/player/native/PlayerLoadParameters.ts @@ -0,0 +1,42 @@ +type ManifestVideoTrack = { + name: string + url: string + brandWidth: number | null + width: number | null + height: number | null + source: string[] + group: string | null +} + +type Stream = Readonly<{ + object: 'stream' + //protocol: Protocol; + video_codec: 'h264' | 'mp4v' + audio_codec: 'mp4a' + url: string + session_id: string + latest_video_offset: number | null + //heartbeat: Heartbeat | null; + //heartbeats: Heartbeats[]; + //analytics: Analytics; + //analytics_v2: AnalyticsV2; + //player?: Player | undefined; + //ads: Ads; + device_connection_type: 'wifi' | 'mobile' + content_provider: string +}> + +type DeviceInfo = Readonly<{ + osName: string | null + osVersion: string | null + model: string | null + vendor: string | null + deviceId: string | null + isUHDSupported: boolean | null +}> + +export { + DeviceInfo, + ManifestVideoTrack, + Stream +} diff --git a/src/internal/player/native/VokaSourceHandler.ts b/src/internal/player/native/VokaSourceHandler.ts index b18383d..6757123 100644 --- a/src/internal/player/native/VokaSourceHandler.ts +++ b/src/internal/player/native/VokaSourceHandler.ts @@ -1,95 +1,106 @@ +import type { EventBus } from 'ts-bus' import videojs from 'video.js' -import { VokaContentType } from '@/public/models/VokaContentType' -import { DeviceInfo, ManifestParser, Stream } from '@/internal/player/native/tizen/models/PlayerLoadParameters' -import { EventBus } from 'ts-bus' + +import type { DeviceInfo, Stream } from '@/internal/player/native/PlayerLoadParameters' +import type { VokaOptions } from '@/public/@types' +import type { VokaContentType } from '@/public/models/VokaContentType' + +type VokaManifest = VokaOptions.StreamOptions.Manifest + const Dom = videojs.dom const Tech = videojs.getTech('Tech') interface AppleSourceProtection { - keySystem: string - certificateUrl: string - licenseUrl: string - useBase64: boolean | null + keySystem: string + certificateUrl: string + licenseUrl: string + useBase64: boolean | null } interface WebOSSourceProtection { - licenseServer: string - streamId: String // parameters.mediaItem.id, - clientIp: string // parameters.clientIP - deviceId: string // parameters.platform.deviceId - assetId: string // parameters.stream.drm.drm_asset_id + licenseServer: string + streamId: string // parameters.mediaItem.id, + clientIp: string // parameters.clientIP + deviceId: string // parameters.platform.deviceId + assetId: string // parameters.stream.drm.drm_asset_id } interface TizenSourceProtection { - type: string - licenseServer: string - // ex: "X-AxDRM-Message:" + DRMToken - httpHeader: string + type: string + licenseServer: string + // ex: "X-AxDRM-Message:" + DRMToken + httpHeader: string } interface TizenSourceParams { - protection: TizenSourceProtection | null - isUHDSupported: boolean - parsedManifest: ManifestParser | null - platform: DeviceInfo - stream: Stream - isLiveStream: boolean - language: string - percentsWatched: number | null + protection: TizenSourceProtection | null + isUHDSupported: boolean + // parsedManifest: ManifestParser | null + platform: DeviceInfo + stream: Stream + isLiveStream: boolean + language: string + percentsWatched: number | null } interface IVokaSource { - bus: EventBus - content: VokaContentType - sourceType: string - src: string - startSeconds: number | null - // dash.js DRM options (widevine, playready only) - keySystemOptions: [{ [key: string]: any }] | null - // FirePlay для Apple native - protection: AppleSourceProtection | null - webOSProtection: WebOSSourceProtection | null - tizenParams: TizenSourceParams | null - // hls.js DRM options (widevine, playready, fireplay) - drmSystems: any + bus: EventBus + content: VokaContentType + sourceType: string + src: string + startSeconds: number | null + // dash.js DRM options (widevine, playready only) + keySystemOptions: [{ [key: string]: any }] | null + // FirePlay для Apple native + protection: AppleSourceProtection | null + webOSProtection: WebOSSourceProtection | null + tizenParams: TizenSourceParams | null + // hls.js DRM options (widevine, playready, fireplay) + drmSystems: any + adv: string | null + subtitlesUrl: string | null + manifest: VokaManifest } type VokaOptionsType = { [key: string]: any } abstract class VokaSourceHandler { - abstract canPlayType(type: string): string + abstract canPlayType(type: string): string - abstract canHandleSource(source: IVokaSource, options: VokaOptionsType): string + abstract canHandleSource(source: IVokaSource, options: VokaOptionsType): string - abstract handleSource( - source: IVokaSource, - tech: typeof Tech, - options: VokaOptionsType - ): void + abstract handleSource(source: IVokaSource, tech: typeof Tech, options: VokaOptionsType): void - /** - * A noop for the native dispose function, as cleanup is not needed. - */ - abstract dispose(): void + /** + * A noop for the native dispose function, as cleanup is not needed. + */ + abstract dispose(): void - /* + /* Support tech playable */ - private static VIDEO_TAG: HTMLMediaElement | null = null - static VIDEO_TEST_TAG(): HTMLMediaElement | null { - if (VokaSourceHandler.VIDEO_TAG != null) { - return VokaSourceHandler.VIDEO_TAG - } - if (Dom.isReal()) { - VokaSourceHandler.VIDEO_TAG = document.createElement('video') - const track = document.createElement('track') - - track.kind = 'captions' - track.srclang = 'en' - track.label = 'English' - VokaSourceHandler.VIDEO_TAG.appendChild(track) - } - return VokaSourceHandler.VIDEO_TAG + private static VIDEO_TAG: HTMLMediaElement | null = null + static VIDEO_TEST_TAG(): HTMLMediaElement | null { + if (VokaSourceHandler.VIDEO_TAG != null) { + return VokaSourceHandler.VIDEO_TAG } + if (Dom.isReal()) { + VokaSourceHandler.VIDEO_TAG = document.createElement('video') + const track = document.createElement('track') + + track.kind = 'captions' + track.srclang = 'en' + track.label = 'English' + VokaSourceHandler.VIDEO_TAG.appendChild(track) + } + return VokaSourceHandler.VIDEO_TAG + } } -export { AppleSourceProtection, WebOSSourceProtection, TizenSourceProtection, TizenSourceParams, VokaOptionsType, VokaSourceHandler, IVokaSource } \ No newline at end of file +export { + AppleSourceProtection, + IVokaSource, + TizenSourceParams, + TizenSourceProtection, + VokaOptionsType, + VokaSourceHandler, + WebOSSourceProtection} diff --git a/src/internal/player/native/VokaTech.ts b/src/internal/player/native/VokaTech.ts index e341d6b..be5f6ca 100644 --- a/src/internal/player/native/VokaTech.ts +++ b/src/internal/player/native/VokaTech.ts @@ -1,1061 +1,1067 @@ +import VokaEvent from '@/constants/VokaEvent' +import { IVokaSource } from '@/internal/player/native/VokaSourceHandler' +import { silencePromise } from '@/internal/utils/promise' import window from 'global' import videojs from 'video.js' -import VokaEvent from '@/constants/VokaEvent' import setupSourceset from './setup-sourceset' -import { silencePromise } from '@/internal/utils/promise' -import { IVokaSource } from '@/internal/player/native/VokaSourceHandler' -import { ITimeRange } from '@/public/IVokaPlayer' const browser = videojs.browser const Dom = videojs.dom - -const log = videojs.log.createLogger("[VokaTech]") +const log = videojs.log.createLogger('[VokaTech]') namespace VokaTech { - const VideoTech = videojs.getTech('Tech') + const VideoTech = videojs.getTech('Tech') - export interface ITimeRange { - start: number - end: number + export interface ITimeRange { + start: number + end: number + } + + export class Tech extends VideoTech { + + private rememberedVideoTag: Element | null + private parentNode: Element | null + private playPromise: null + private lastCurrentTime: number + private isLive_: boolean + protected cacheTime: number + + // protected qualityMapper: QualityMapper + + constructor(options?: any, ready?: any) { + super(options, ready) + + // this.playerError_ = null + this.lastCurrentTime = -1 + this.cacheTime = 0 + this.isLive_ = false + // this.isDVR_ = false + // this.isDRM_ = false + this.rememberedVideoTag = null + // this.isLocked = false + this.parentNode = null + + this.playPromise = null + + // this.proxyNativeTracks_() + + // prevent iOS Safari from disabling metadata text tracks during native playback + this.restoreMetadataTracksInIOSNativePlayer_() + + // Determine if native controls should be used + // Our goal should be to get the custom controls on mobile solid everywhere + // so we can remove this all together. Right now this will block custom + // controls on touch enabled laptops like the Chrome Pixel + if ((browser.TOUCH_ENABLED || browser.IS_IPHONE || + browser.IS_ANDROID) && options?.nativeControlsForTouch === true) { + this.setControls(true) + } + + // on iOS, we want to proxy `webkitbeginfullscreen` and `webkitendfullscreen` + // into a `fullscreenchange` event + this.proxyWebkitFullscreen_() + + setupSourceset(this) } - export class Tech extends VideoTech { + finishSetup(options?: any) { + this.triggerReady() + } - private rememberedVideoTag: Element | null - private parentNode: Element | null - private playPromise: null - private lastCurrentTime: number - private isLive_: boolean - protected cacheTime: number - // protected qualityMapper: QualityMapper + /* -------------------------------------------------------------- */ - constructor(options?: any, ready?: any) { - super(options, ready) + static get movingMediaElementInDOM(): boolean { + return !browser.IS_IOS + } - // this.playerError_ = null - //FIXME неиспользуемое поле - // this.isActive = true - this.lastCurrentTime = -1 - this.cacheTime = 0 - this.isLive_ = false - // this.isDVR_ = false - // this.isDRM_ = false - this.rememberedVideoTag = null - // this.isLocked = false - this.parentNode = null + createEl(): Element { + const player = this.player_ + let el = player ? player.el() : null + const playerId = this.options_.playerId + this.options_.techId - this.playPromise = null + if ( + !el || + !(this.options_.playerElIngest || Tech.movingMediaElementInDOM) + ) { + if (el && el.tagName === this.elementTagName) { + const clone = el.cloneNode(true) - // this.proxyNativeTracks_() + if (el.parentNode) { + el.parentNode.insertBefore(clone, el) + } + Tech.disposeMediaElement(el) + el = clone + } else { + el = document.createElement(this.elementTagName) - // prevent iOS Safari from disabling metadata text tracks during native playback - this.restoreMetadataTracksInIOSNativePlayer_() + Dom.setAttributes(el, { + id: playerId, + class: 'vjs-tech', + 'x-webkit-airplay': 'allow', + playsinline: 'true', + 'webkit-playsinline': 'true', + width: '100%', + height: '100%', + oncontextmenu: 'return false' + }) - // Determine if native controls should be used - // Our goal should be to get the custom controls on mobile solid everywhere - // so we can remove this all together. Right now this will block custom - // controls on touch enabled laptops like the Chrome Pixel - // if ((browser.TOUCH_ENABLED || browser.IS_IPHONE || - // browser.IS_ANDROID) && options?.nativeControlsForTouch === true) { - // this.setControls(true) - // } + const tagAttributes = + this.options_.tag && Dom.getAttributes(this.options_.tag) + const attributes = videojs.obj.merge({}, tagAttributes) - // on iOS, we want to proxy `webkitbeginfullscreen` and `webkitendfullscreen` - // into a `fullscreenchange` event - this.proxyWebkitFullscreen_() - - // this.qualityMapper = new QualityMapper( - // PlayerOptionsContext.qualityLabelVariant - // ) - - setupSourceset(this) + if ( + !browser.TOUCH_ENABLED || + this.options_.nativeControlsForTouch !== true + ) { + delete attributes.controls + } } - finishSetup(options?: any) { - this.triggerReady() + el.playerId = playerId + } + + if (typeof this.options_.preload !== 'undefined') { + Dom.setAttribute(el, 'preload', this.options_.preload) + } + + const settingsAttrs = [ 'loop', 'muted', 'playsinline', 'autoplay' ] + for (let i = 0; i < settingsAttrs.length; i++) { + const attr = settingsAttrs[i] + const value = this.options_[attr] + + if (typeof value !== 'undefined') { + if (value) { + Dom.setAttribute(el, attr, attr) + } else { + Dom.removeAttribute(el, attr) + } + el[attr] = value + } + } + + // on(el, 'error', Fn.bind(this, this.handleError)) + + this.el_ = el + + return el + } + + dispose() { + if (this.isLocked) { + return + } + + if (this.el() && this.el().resetSourceset_) { + this.el().resetSourceset_() + } + Tech.disposeMediaElement(this.el()) + this.options_ = null + + // tech will handle clearing of the emulated track list + super.dispose() + } + + /* Business Logic ------------------------------------------------ */ + + // lock() { + // this.isLocked = true + // + // const el = this.el() + // if (el && el.parentNode) { + // this.parentNode = el.parentNode + // this.parentNode.removeChild(el) + // } + // } + // + // unlock() { + // if (!this.isLocked) { return } + // + // const el = this.el() + // if (el && this.parentNode) { + // Dom.prependTo(el, this.parentNode) + // } + // + // this.parentNode = null + // this.isLocked = false + // } + + // saveCurrentTime() { + // const currentTime = this.currentTime() + // const isSeekable = (!this.isLive || this.isDVR) + // if (isSeekable && + // !isNaN(currentTime) && currentTime > 0) + // { + // this.lastCurrentTime = currentTime + // } + // } + + // setIsActive(active) { + // this.isActive = active + // } + // setIsDVR(dvr) { + // this.isDVR_ = dvr + // } + // + // setIsDRM(drm) { + // this.isDRM_ = drm + // } + + set isLive(value: boolean) { + this.isLive_ = value + } + + get isLive(): boolean { + return this.isLive_ + } + + // get isDVR() { + // return this.isDVR_ + // } + // + // get isDRM() { + // return this.isDRM_ + // } + + handleError(event, data) { + this.playerError_ = data + } + + switchLevel(level: number) { + } + + /* Static Methods ------------------------------------------------ */ + + get elementTagName() { + return 'video' + } + + setVideoTag(add: boolean) { + const playerEl = this.player_.el() + if (add && this.rememberedVideoTag) { + if (playerEl.parentNode != null) { + playerEl.parentNode.insertBefore(this.rememberedVideoTag, playerEl) + } + this.rememberedVideoTag = null + this.addSrcElement() + } else if (!add && !this.rememberedVideoTag) { + let temp = playerEl.getElementsByTagName('video') + if (temp.length > 0) { + temp = temp[0] + } else { + temp = null } - /* -------------------------------------------------------------- */ - - static get movingMediaElementInDOM(): boolean { - return !browser.IS_IOS + if (temp) { + this.rememberedVideoTag = temp + playerEl.removeChild(this.rememberedVideoTag) } + } + } - createEl(): Element { - const player = this.player_ - let el = player ? player.el() : null - const playerId = this.options_.playerId + this.options_.techId + addSrcElement() { + } - if ( - !el || - !(this.options_.playerElIngest || Tech.movingMediaElementInDOM) - ) { - if (el && el.tagName === this.elementTagName) { - const clone = el.cloneNode(true) + removeSrcElement() { + } - if (el.parentNode) { - el.parentNode.insertBefore(clone, el) - } - Tech.disposeMediaElement(el) - el = clone - } else { - el = document.createElement(this.elementTagName) + updateTechSettings(params) { + // this.hlsProcessor.updateSettings(params) + } - Dom.setAttributes(el, { - id: playerId, - class: 'vjs-tech', - 'x-webkit-airplay': 'allow', - playsinline: 'true', - 'webkit-playsinline': 'true', - width: '100%', - height: '100%', - oncontextmenu: 'return false' - }) + /* + * On methods + */ - const tagAttributes = - this.options_.tag && Dom.getAttributes(this.options_.tag) - const attributes = videojs.obj.merge({}, tagAttributes) + /* ---------------------------------А----------------------------- */ - if ( - !browser.TOUCH_ENABLED || - this.options_.nativeControlsForTouch !== true - ) { - delete attributes.controls - } - } + overrideNative_(type: string, override: boolean) { + if (override !== this[`featuresNative${ type }Tracks`]) { + return + } - el.playerId = playerId - } + const lowerCaseType = type.toLowerCase() - if (typeof this.options_.preload !== 'undefined') { - Dom.setAttribute(el, 'preload', this.options_.preload) - } + if (this[`${ lowerCaseType }TracksListeners_`]) { + Object.keys(this[`${ lowerCaseType }TracksListeners_`]).forEach( + (eventName) => { + const elTracks = this.el()[`${ lowerCaseType }Tracks`] - const settingsAttrs = ['loop', 'muted', 'playsinline', 'autoplay'] - for (let i = 0; i < settingsAttrs.length; i++) { - const attr = settingsAttrs[i] - const value = this.options_[attr] - - if (typeof value !== 'undefined') { - if (value) { - Dom.setAttribute(el, attr, attr) - } else { - Dom.removeAttribute(el, attr) - } - el[attr] = value - } - } - - // on(el, 'error', Fn.bind(this, this.handleError)) - - this.el_ = el - - return el - } - - dispose() { - if (this.isLocked) { - return - } - - if (this.el() && this.el().resetSourceset_) { - this.el().resetSourceset_() - } - Tech.disposeMediaElement(this.el()) - this.options_ = null - - // tech will handle clearing of the emulated track list - super.dispose() - } - - /* Business Logic ------------------------------------------------ */ - - // lock() { - // this.isLocked = true - // - // const el = this.el() - // if (el && el.parentNode) { - // this.parentNode = el.parentNode - // this.parentNode.removeChild(el) - // } - // } - // - // unlock() { - // if (!this.isLocked) { return } - // - // const el = this.el() - // if (el && this.parentNode) { - // Dom.prependTo(el, this.parentNode) - // } - // - // this.parentNode = null - // this.isLocked = false - // } - - // saveCurrentTime() { - // const currentTime = this.currentTime() - // const isSeekable = (!this.isLive || this.isDVR) - // if (isSeekable && - // !isNaN(currentTime) && currentTime > 0) - // { - // this.lastCurrentTime = currentTime - // } - // } - - // setIsActive(active) { - // this.isActive = active - // } - // setIsDVR(dvr) { - // this.isDVR_ = dvr - // } - // - // setIsDRM(drm) { - // this.isDRM_ = drm - // } - - set isLive(value: boolean) { - this.isLive_ = value - } - - get isLive(): boolean { - return this.isLive_ - } - - // get isDVR() { - // return this.isDVR_ - // } - // - // get isDRM() { - // return this.isDRM_ - // } - - handleError(event, data) { - this.playerError_ = data - } - - switchLevel(level: number) {} - - /* Static Methods ------------------------------------------------ */ - - - get elementTagName() { - return 'video' - } - - setVideoTag(add: boolean) { - const playerEl = this.player_.el() - if (add && this.rememberedVideoTag) { - if (playerEl.parentNode != null) { - playerEl.parentNode.insertBefore(this.rememberedVideoTag, playerEl) - } - this.rememberedVideoTag = null - this.addSrcElement() - } else if (!add && !this.rememberedVideoTag) { - let temp = playerEl.getElementsByTagName('video') - if (temp.length > 0) { - temp = temp[0] - } else { - temp = null - } - - if (temp) { - this.rememberedVideoTag = temp - playerEl.removeChild(this.rememberedVideoTag) - } - } - } - - addSrcElement() {} - - removeSrcElement() {} - - updateTechSettings(params) { - // this.hlsProcessor.updateSettings(params) - } - - /* - * On methods - */ - - /* ---------------------------------А----------------------------- */ - - overrideNative_(type: string, override: boolean) { - if (override !== this[`featuresNative${type}Tracks`]) { - return - } - - const lowerCaseType = type.toLowerCase() - - if (this[`${lowerCaseType}TracksListeners_`]) { - Object.keys(this[`${lowerCaseType}TracksListeners_`]).forEach( - (eventName) => { - const elTracks = this.el()[`${lowerCaseType}Tracks`] - - elTracks.removeEventListener( - eventName, - this[`${lowerCaseType}TracksListeners_`][eventName] - ) - } - ) - } - - this[`featuresNative${type}Tracks`] = !override - this[`${lowerCaseType}TracksListeners_`] = null - - this.proxyNativeTracksForType_(lowerCaseType) - } - - proxyNativeTracksForType_(name: string) { - const props = TRACK_TYPES[name] - const elTracks = this.el()[props.getterName] - const techTracks = this[props.getterName]() - - if ( - !this[`featuresNative${props.capitalName}Tracks`] || - !elTracks || - !elTracks.addEventListener - ) { - return - } - const listeners = { - change: (e: Event) => { - const event = { - type: 'change', - target: techTracks, - currentTarget: techTracks, - srcElement: techTracks - } - techTracks.trigger(event) - - if (name === 'text') { - this[REMOTE.remoteText.getterName]().trigger(event) - } - }, - addtrack(e) { - techTracks.addTrack(e.track) - }, - removetrack(e) { - techTracks.removeTrack(e.track) - } - } - const removeOldTracks = function () { - const removeTracks = [] - - for (let i = 0; i < techTracks.length; i++) { - let found = false - - for (let j = 0; j < elTracks.length; j++) { - if (elTracks[j] === techTracks[i]) { - found = true - break - } - } - - if (!found) { - removeTracks.push(techTracks[i]) - } - } - - while (removeTracks.length) { - techTracks.removeTrack(removeTracks.shift()) - } - } - - this[props.getterName + 'Listeners_'] = listeners - - Object.keys(listeners).forEach((eventName) => { - const listener = listeners[eventName] - - elTracks.addEventListener(eventName, listener) - this.on('dispose', (e: Event) => - elTracks.removeEventListener(eventName, listener) - ) - }) - - this.on('loadstart', removeOldTracks) - this.on('dispose', (e: Event) => this.off('loadstart', removeOldTracks)) - } - - get allowAirplay() { - return true - } - - overrideNativeAudioTracks(override: boolean) { - this.overrideNative_('Audio', override) - } - - overrideNativeVideoTracks(override: boolean) { - this.overrideNative_('Video', override) - } - - proxyWebkitFullscreen_() { - if (!('webkitDisplayingFullscreen' in this.el())) { - return - } - - const endFn = function () { - this.trigger('fullscreenchange', { isFullscreen: false }) - } - - const beginFn = function () { - if ( - 'webkitPresentationMode' in this.el() && - this.el().webkitPresentationMode !== 'picture-in-picture' - ) { - this.one('webkitendfullscreen', endFn) - - this.trigger('fullscreenchange', { - isFullscreen: true, - // set a flag in case another tech triggers fullscreenchange - nativeIOSFullscreen: true - }) - } - } - - this.on('webkitbeginfullscreen', beginFn) - this.on('dispose', () => { - this.off('webkitbeginfullscreen', beginFn) - this.off('webkitendfullscreen', endFn) - }) - } - - proxyNativeTracks_() { - TRACK_TYPES.names.forEach((name) => { - this.proxyNativeTracksForType_(name) - }) - } - - restoreMetadataTracksInIOSNativePlayer_() { - const textTracks = this.textTracks() - let metadataTracksPreFullscreenState - - // captures a snapshot of every metadata track's current state - const takeMetadataTrackSnapshot = () => { - metadataTracksPreFullscreenState = [] - - for (let i = 0; i < textTracks.length; i++) { - const track = textTracks[i] - - if (track.kind === 'metadata') { - metadataTracksPreFullscreenState.push({ - track, - storedMode: track.mode - }) - } - } - } - - takeMetadataTrackSnapshot() - textTracks.addEventListener('change', takeMetadataTrackSnapshot) - - this.on('dispose', () => - textTracks.removeEventListener('change', takeMetadataTrackSnapshot) + elTracks.removeEventListener( + eventName, + this[`${ lowerCaseType }TracksListeners_`][eventName] ) + } + ) + } - const restoreTrackMode = () => { - for (let i = 0; i < metadataTracksPreFullscreenState.length; i++) { - const storedTrack = metadataTracksPreFullscreenState[i] + this[`featuresNative${ type }Tracks`] = !override + this[`${ lowerCaseType }TracksListeners_`] = null - if ( - storedTrack.track.mode === 'disabled' && - storedTrack.track.mode !== storedTrack.storedMode - ) { - storedTrack.track.mode = storedTrack.storedMode - } - } - textTracks.removeEventListener('change', restoreTrackMode) - } - - this.on('webkitbeginfullscreen', () => { - textTracks.removeEventListener('change', takeMetadataTrackSnapshot) - - textTracks.removeEventListener('change', restoreTrackMode) - textTracks.addEventListener('change', restoreTrackMode) - }) - - this.on('webkitendfullscreen', () => { - textTracks.removeEventListener('change', takeMetadataTrackSnapshot) - textTracks.addEventListener('change', takeMetadataTrackSnapshot) - - textTracks.removeEventListener('change', restoreTrackMode) - }) - } - - isVokaTech(): boolean { return true } - - pause() { - if (this.playPromise != null) { - this.playPromise.then((_) => { - this.el().pause() - }) - } else { - this.el().pause() - } - } - - load() { - if (!browser.IS_WINDOWS) { - if (typeof this.el().load === 'function') { - const src = this.currentSrc() - if (typeof src !== 'undefined' && src.indexOf('blob') !== -1) { - return - } - return this.el().load() - } - } else { - // console.log('⛈ Try load on Windows devices!') - } - } - - play(opts: { [key: string]: any }) { - if (this.lastCurrentTime > 0) { - this.setCurrentTime(this.lastCurrentTime) - this.lastCurrentTime = -1 - } - - this.playPromise = null - - // в этом promise на некоторых os нет finally! - const promise = this.el().play() - if (promise != null) { - // включая undefined - - const onFinally = () => { - this.playPromise = null - this.trigger(VokaEvent.PlaybackStarted) - } - - // Сохраняем промис для отслеживания. - this.playPromise = promise.then(onFinally, onFinally) - } else { - this.trigger(VokaEvent.PlaybackStarted) - } - - return this.playPromise - } - - reset() { - Tech.resetMediaElement(this.el()) - } - - setSrc(source: IVokaSource) {} - - get srcElement() { - return this.el().tagSource ? this.el().tagSource : this.el() - } - - //FIXME корректно только для HLS! оставить более безопасную проверку здесь, явно переопределять. - switchType(src: string) { - if (typeof src === 'undefined' || !src) { - return - } - const _src = src.toLowerCase() - if (_src.indexOf('m3u8') > 0) { - this.type('application/x-mpegURL') // 'application/x-mpegURL' 'application/vnd.apple.mpegurl' - } else { - this.type('video/mp4') - } - } - - type(type: string) { - if (type /*&& _allowType */) { - this.srcElement.setAttribute('type', type) - } else { - return this.srcElement.getAttribute('type') - } - } - - currentSrc() { - if (this.currentSource_) { - return this.currentSource_.src - } - return this.el().currentSrc - } - - controls(): boolean { - return this.el().controls || this.el().hasAttribute('controls') - } - - setControls(val: string) { - this.el().controls = !!val - } - - duration() { - return NaN - } - - currentTime() { - const current = this.el().currentTime - const isSeekable = !this.isLive || this.isDVR - if (!isSeekable) { - this.cacheTime = Math.max(this.cacheTime, current) - } else { - this.cacheTime = current - } - return this.cacheTime - } - - setCurrentTime(seconds: number) { - log("[setCurrentTime] called with seconds", seconds) - try { - this.el().currentTime = seconds - } catch (e) { - log.error('Video is not ready', e) - } - } - - supportsFullScreen(): boolean { - if (typeof this.el().webkitEnterFullScreen === 'function') { - const userAgent = (window.navigator && window.navigator.userAgent) || '' - - // Seems to be broken in Chromium/Chrome && Safari in Leopard - if ( - /Android/.test(userAgent) || - !/Chrome|Mac OS X 10.5/.test(userAgent) - ) { - return true - } - } - return false - } - - enterFullScreen() { - const video = this.el() - - if (video.paused && video.networkState <= video.HAVE_METADATA) { - // attempt to prime the video element for programmatic access - // this isn't necessary on the desktop but shouldn't hurt - silencePromise(this.player_.play()) - - // playing and pausing synchronously during the transition to fullscreen - // can get iOS ~6.1 devices into a play/pause loop - this.setTimeout(function () { - video.pause() - try { - video.webkitEnterFullScreen() - } catch (e) { - this.trigger('fullscreenerror', e) - } - }, 0) - } else { - try { - video.webkitEnterFullScreen() - } catch (e) { - this.trigger('fullscreenerror', e) - } - } - } - - exitFullScreen() { - if (!this.el() || !this.el().webkitExitFullScreen) { - return - } - if (!this.el().webkitDisplayingFullscreen) { - this.trigger('fullscreenerror', new Error('The video is not fullscreen')) - return - } - this.el().webkitExitFullScreen() - } - - playbackRate(): number { - return this.el().playbackRate - } - - setPlaybackRate(value: number) { - this.el().playbackRate = value - } - - defaultPlaybackRate() { - return this.el().defaultPlaybackRate - } - - setDefaultPlaybackRate(value: number) { - this.el().defaultPlaybackRate = value - } - - setVolume(percentAsDecimal: number) { - this.el().volume = percentAsDecimal - } - - volume(): number { - return this.el().volume - } - - buffered() { - return this.el().buffered - } - - muted(): boolean { - return this.el().muted || this.el().hasAttribute('muted') - } - - setMuted(muted: boolean) { - this.el().muted = muted - - if (muted) { - this.el().setAttribute('muted', muted) - } else { - this.el().removeAttribute('muted') - } - } - - loop(): boolean { - return this.el().loop || this.el().hasAttribute('loop') - } - - setLoop(loop: boolean) { - this.el().loop = loop - - if (loop) { - this.el().setAttribute('loop', loop) - } else { - this.el().removeAttribute('loop') - } - } - - playsinline(): boolean { - return this.el().playsinline || this.el().hasAttribute('playsinline') - } - - setPlaysinline(playsinline: boolean) { - this.el().playsinline = playsinline - - if (playsinline) { - this.el().setAttribute('playsinline', playsinline) - } else { - this.el().removeAttribute('playsinline') - } - } - - autoplay(): boolean { - return this.el().autoplay || this.el().hasAttribute('autoplay') - } - - setAutoplay(autoplay: boolean) { - this.el().autoplay = autoplay - - if (autoplay) { - this.el().setAttribute('autoplay', autoplay) - } else { - this.el().removeAttribute('autoplay') - } - } - - defaultMuted(): boolean { - return this.el().defaultMuted || this.el().hasAttribute('defaultMuted') - } - - setDefaultMuted(defaultMuted: boolean) { - this.el().defaultMuted = defaultMuted - - if (defaultMuted) { - this.el().setAttribute('defaultMuted', defaultMuted) - } else { - this.el().removeAttribute('defaultMuted') - } - } - - paused(): boolean { - return this.el().paused - } - - width(): number { - return this.el().offsetWidth - } - - height(): number { - return this.el().offsetHeight - } - - getAbsoluteRange(): ITimeRange | null { - return null - } - - getTimeshiftAvailable(): boolean { - return false - } - - addTextTrack(kind: string, label: string, language: string) { - if (!this.featuresNativeTextTracks) { - return super.addTextTrack(kind, label, language) - } - - return this.el().addTextTrack(kind, label, language) - } - - createRemoteTextTrack(options) { - if (!this.featuresNativeTextTracks) { - return super.createRemoteTextTrack(options) - } - const htmlTrackElement = document.createElement('track') - - if (options.kind) { - htmlTrackElement.kind = options.kind - } - if (options.label) { - htmlTrackElement.label = options.label - } - if (options.language || options.srclang) { - htmlTrackElement.srclang = options.language || options.srclang - } - if (options.default) { - htmlTrackElement.default = options.default - } - if (options.id) { - htmlTrackElement.id = options.id - } - if (options.src) { - htmlTrackElement.src = options.src - } - - return htmlTrackElement - } - - addRemoteTextTrack(options, manualCleanup: boolean) { - const htmlTrackElement = super.addRemoteTextTrack(options, manualCleanup) - - if (this.featuresNativeTextTracks) { - this.el().appendChild(htmlTrackElement) - } - - return htmlTrackElement - } - - removeRemoteTextTrack(track) { - super.removeRemoteTextTrack(track) - - if (this.featuresNativeTextTracks) { - const tracks = this.$$('track') - - let i = tracks.length - - while (i--) { - if (track === tracks[i] || track === tracks[i].track) { - this.el().removeChild(tracks[i]) - } - } - } - } - - getVideoPlaybackQuality() { - if (typeof this.el().getVideoPlaybackQuality === 'function') { - return this.el().getVideoPlaybackQuality() - } - - const videoPlaybackQuality = {} - - if ( - typeof this.el().webkitDroppedFrameCount !== 'undefined' && - typeof this.el().webkitDecodedFrameCount !== 'undefined' - ) { - videoPlaybackQuality.droppedVideoFrames = - this.el().webkitDroppedFrameCount - videoPlaybackQuality.totalVideoFrames = this.el().webkitDecodedFrameCount - } - - if (window.performance && typeof window.performance.now === 'function') { - videoPlaybackQuality.creationTime = window.performance.now() - } else if ( - window.performance && - window.performance.timing && - typeof window.performance.timing.navigationStart === 'number' - ) { - videoPlaybackQuality.creationTime = - window.Date.now() - window.performance.timing.navigationStart - } - - return videoPlaybackQuality - } - - /** - * Не используем работу с `poster` потому что за ошибки у нас отвечает VokaLockScreenPlugin - */ - poster() { - return null - } - - setPoster(poster) {} - - preload(): string { - return this.el().preload - } - - setPreload(preload: string) { - this.el().preload = preload - } - - error(error) { - return this.el().error || this.playerError_ - } - - seeking(): boolean { - return this.el().seeking - } - - seekable() { - return !this.isLive && this.el().seekable - } - - ended(): boolean { - return this.el().ended // || this._forsedStop - } - - played() { - return this.el().played - } - - /** - * Get the value of `networkState` from the media element. `networkState` indicates - * the current network state. It returns an enumeration from the following list: - * - 0: NETWORK_EMPTY - * - 1: NETWORK_IDLE - * - 2: NETWORK_LOADING - * - 3: NETWORK_NO_SOURCE - * - * @method Html5#networkState - * @return {number} - * The value of `networkState` from the media element. This will be a number - * from the list in the description. - * - * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-networkstate} - */ - networkState(): number { - return this.el().networkState - } - - /** - * Get the value of `readyState` from the media element. `readyState` indicates - * the current state of the media element. It returns an enumeration from the - * following list: - * - 0: HAVE_NOTHING - * - 1: HAVE_METADATA - * - 2: HAVE_CURRENT_DATA - * - 3: HAVE_FUTURE_DATA - * - 4: HAVE_ENOUGH_DATA - * - * @method Html5#readyState - * @return {number} - * The value of `readyState` from the media element. This will be a number - * from the list in the description. - * - * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#ready-states} - */ - readyState(): number { - return this.el().readyState - } - - videoWidth(): number { - return this.el().videoWidth - } - - videoHeight(): number { - return this.el().videoHeight - } - - /* -------------------------------------------------------------- */ - /* - * Static methods - */ - - static resetMediaElement(el) { - if (!el) { - return - } - - const sources = el.querySelectorAll('source') - let i = sources.length - - while (i--) { - el.removeChild(sources[i]) - } - - // remove any src reference. - // not setting `src=''` because that throws an error - el.removeAttribute('src') - - if (typeof el.load === 'function') { - // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) - const func = () => { - try { - el.load() - } catch (e) { - // satisfy linter - } - } - func() - } - } - - static disposeMediaElement(el) { - if (!el) { - return - } - - if (el.parentNode) { - el.parentNode.removeChild(el) - } - - // remove any child track or source nodes to prevent their loading - while (el.hasChildNodes()) { - el.removeChild(el.firstChild) - } - - // remove any src reference. not setting `src=''` because that causes a warning - // in firefox - el.removeAttribute('src') - - // force the media element to update its loading state by calling load() - // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793) - if (typeof el.load === 'function') { - // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) - const func = () => { - try { - el.load() - } catch (e) { - // not supported - } - } - func() - } - } - - static canOverrideAttributes(): boolean { - // if we cannot overwrite the src/innerHTML property, there is no support - // iOS 7 safari for instance cannot do this. - try { - const noop = () => {} - - Object.defineProperty(document.createElement('video'), 'src', { - get: noop, - set: noop - }) - Object.defineProperty(document.createElement('audio'), 'src', { - get: noop, - set: noop - }) - Object.defineProperty(document.createElement('video'), 'innerHTML', { - get: noop, - set: noop - }) - Object.defineProperty(document.createElement('audio'), 'innerHTML', { - get: noop, - set: noop - }) - } catch (e) { - return false - } - - return true - } - - static supportsNativeTextTracks(): boolean { - return true - } + this.proxyNativeTracksForType_(lowerCaseType) } + proxyNativeTracksForType_(name: string) { + const props = TRACK_TYPES[name] + const elTracks = this.el()[props.getterName] + const techTracks = this[props.getterName]() + + if ( + !this[`featuresNative${ props.capitalName }Tracks`] || + !elTracks || + !elTracks.addEventListener + ) { + return + } + const listeners = { + change: (e: Event) => { + const event = { + type: 'change', + target: techTracks, + currentTarget: techTracks, + srcElement: techTracks + } + techTracks.trigger(event) + + if (name === 'text') { + this[REMOTE.remoteText.getterName]().trigger(event) + } + }, + addtrack(e) { + techTracks.addTrack(e.track) + }, + removetrack(e) { + techTracks.removeTrack(e.track) + } + } + const removeOldTracks = function() { + const removeTracks = [] + + for (let i = 0; i < techTracks.length; i++) { + let found = false + + for (let j = 0; j < elTracks.length; j++) { + if (elTracks[j] === techTracks[i]) { + found = true + break + } + } + + if (!found) { + removeTracks.push(techTracks[i]) + } + } + + while (removeTracks.length) { + techTracks.removeTrack(removeTracks.shift()) + } + } + + this[props.getterName + 'Listeners_'] = listeners + + Object.keys(listeners).forEach((eventName) => { + const listener = listeners[eventName] + + elTracks.addEventListener(eventName, listener) + this.on('dispose', (e: Event) => + elTracks.removeEventListener(eventName, listener) + ) + }) + + this.on('loadstart', removeOldTracks) + this.on('dispose', (e: Event) => this.off('loadstart', removeOldTracks)) + } + + get allowAirplay() { + return true + } + + overrideNativeAudioTracks(override: boolean) { + this.overrideNative_('Audio', override) + } + + overrideNativeVideoTracks(override: boolean) { + this.overrideNative_('Video', override) + } + + proxyWebkitFullscreen_() { + if (!('webkitDisplayingFullscreen' in this.el())) { + return + } + + const endFn = function() { + this.trigger('fullscreenchange', { isFullscreen: false }) + } + + const beginFn = function() { + if ( + 'webkitPresentationMode' in this.el() && + this.el().webkitPresentationMode !== 'picture-in-picture' + ) { + this.one('webkitendfullscreen', endFn) + + this.trigger('fullscreenchange', { + isFullscreen: true, + // set a flag in case another tech triggers fullscreenchange + nativeIOSFullscreen: true + }) + } + } + + this.on('webkitbeginfullscreen', beginFn) + this.on('dispose', () => { + this.off('webkitbeginfullscreen', beginFn) + this.off('webkitendfullscreen', endFn) + }) + } + + proxyNativeTracks_() { + TRACK_TYPES.names.forEach((name) => { + this.proxyNativeTracksForType_(name) + }) + } + + restoreMetadataTracksInIOSNativePlayer_() { + const textTracks = this.textTracks() + let metadataTracksPreFullscreenState + + // captures a snapshot of every metadata track's current state + const takeMetadataTrackSnapshot = () => { + metadataTracksPreFullscreenState = [] + + for (let i = 0; i < textTracks.length; i++) { + const track = textTracks[i] + + if (track.kind === 'metadata') { + metadataTracksPreFullscreenState.push({ + track, + storedMode: track.mode + }) + } + } + } + + takeMetadataTrackSnapshot() + textTracks.addEventListener('change', takeMetadataTrackSnapshot) + + this.on('dispose', () => + textTracks.removeEventListener('change', takeMetadataTrackSnapshot) + ) + + const restoreTrackMode = () => { + for (let i = 0; i < metadataTracksPreFullscreenState.length; i++) { + const storedTrack = metadataTracksPreFullscreenState[i] + + if ( + storedTrack.track.mode === 'disabled' && + storedTrack.track.mode !== storedTrack.storedMode + ) { + storedTrack.track.mode = storedTrack.storedMode + } + } + textTracks.removeEventListener('change', restoreTrackMode) + } + + this.on('webkitbeginfullscreen', () => { + textTracks.removeEventListener('change', takeMetadataTrackSnapshot) + + textTracks.removeEventListener('change', restoreTrackMode) + textTracks.addEventListener('change', restoreTrackMode) + }) + + this.on('webkitendfullscreen', () => { + textTracks.removeEventListener('change', takeMetadataTrackSnapshot) + textTracks.addEventListener('change', takeMetadataTrackSnapshot) + + textTracks.removeEventListener('change', restoreTrackMode) + }) + } + + isVokaTech(): boolean { + return true + } + + pause() { + if (this.playPromise != null) { + this.playPromise.then((_) => { + this.el().pause() + }) + } else { + this.el().pause() + } + } + + load() { + if (!browser.IS_WINDOWS) { + if (typeof this.el().load === 'function') { + const src = this.currentSrc() + if (typeof src !== 'undefined' && src.indexOf('blob') !== -1) { + return + } + return this.el().load() + } + } else { + // console.log('⛈ Try load on Windows devices!') + } + } + + play(opts: { [key: string]: any }) { + if (this.lastCurrentTime > 0) { + this.setCurrentTime(this.lastCurrentTime) + this.lastCurrentTime = -1 + } + + this.playPromise = null + + // в этом promise на некоторых os нет finally! + const promise = this.el().play() + if (promise != null) { + // включая undefined + + const onFinally = () => { + this.playPromise = null + this.trigger(VokaEvent.PlaybackStarted) + } + + // Сохраняем промис для отслеживания. + this.playPromise = promise.then(onFinally, onFinally) + } else { + this.trigger(VokaEvent.PlaybackStarted) + } + + return this.playPromise + } + + reset() { + Tech.resetMediaElement(this.el()) + } + + setSrc(source: IVokaSource) { + } + + get srcElement() { + return this.el().tagSource ? this.el().tagSource : this.el() + } + + switchType(src: string) { + if (typeof src === 'undefined' || !src) { + return + } + const _src = src.toLowerCase() + if (_src.indexOf('m3u8') > 0) { + this.type('application/x-mpegURL') // 'application/x-mpegURL' 'application/vnd.apple.mpegurl' + } else { + this.type('video/mp4') + } + } + + type(type: string) { + if (type /*&& _allowType */) { + this.srcElement.setAttribute('type', type) + } else { + return this.srcElement.getAttribute('type') + } + } + + currentSrc() { + if (this.currentSource_) { + return this.currentSource_.src + } + return this.el().currentSrc + } + + controls(): boolean { + return this.el().controls || this.el().hasAttribute('controls') + } + + setControls(val: string) { + this.el().controls = !!val + } + + duration() { + return NaN + } + + currentTime() { + const current = this.el().currentTime + const isSeekable = !this.isLive || this.isDVR + if (!isSeekable) { + this.cacheTime = Math.max(this.cacheTime, current) + } else { + this.cacheTime = current + } + return this.cacheTime + } + + setCurrentTime(seconds: number) { + log('[setCurrentTime] called with seconds', seconds) + try { + this.el().currentTime = seconds + } + catch (e) { + log.error('Video is not ready', e) + } + } + + supportsFullScreen(): boolean { + if (typeof this.el().webkitEnterFullScreen === 'function') { + const userAgent = (window.navigator && window.navigator.userAgent) || '' + + // Seems to be broken in Chromium/Chrome && Safari in Leopard + if ( + /Android/.test(userAgent) || + !/Chrome|Mac OS X 10.5/.test(userAgent) + ) { + return true + } + } + return false + } + + enterFullScreen() { + const video = this.el() + + if (video.paused && video.networkState <= video.HAVE_METADATA) { + // attempt to prime the video element for programmatic access + // this isn't necessary on the desktop but shouldn't hurt + silencePromise(this.player_.play()) + + // playing and pausing synchronously during the transition to fullscreen + // can get iOS ~6.1 devices into a play/pause loop + this.setTimeout(function() { + video.pause() + try { + video.webkitEnterFullScreen() + } + catch (e) { + this.trigger('fullscreenerror', e) + } + }, 0) + } else { + try { + video.webkitEnterFullScreen() + } + catch (e) { + this.trigger('fullscreenerror', e) + } + } + } + + exitFullScreen() { + if (!this.el() || !this.el().webkitExitFullScreen) { + return + } + if (!this.el().webkitDisplayingFullscreen) { + this.trigger('fullscreenerror', new Error('The video is not fullscreen')) + return + } + this.el().webkitExitFullScreen() + } + + playbackRate(): number { + return this.el().playbackRate + } + + setPlaybackRate(value: number) { + this.el().playbackRate = value + } + + defaultPlaybackRate() { + return this.el().defaultPlaybackRate + } + + setDefaultPlaybackRate(value: number) { + this.el().defaultPlaybackRate = value + } + + setVolume(percentAsDecimal: number) { + this.el().volume = percentAsDecimal + } + + volume(): number { + return this.el().volume + } + + buffered() { + return this.el().buffered + } + + muted(): boolean { + return this.el().muted || this.el().hasAttribute('muted') + } + + setMuted(muted: boolean) { + this.el().muted = muted + + if (muted) { + this.el().setAttribute('muted', muted) + } else { + this.el().removeAttribute('muted') + } + } + + loop(): boolean { + return this.el().loop || this.el().hasAttribute('loop') + } + + setLoop(loop: boolean) { + this.el().loop = loop + + if (loop) { + this.el().setAttribute('loop', loop) + } else { + this.el().removeAttribute('loop') + } + } + + playsinline(): boolean { + return this.el().playsinline || this.el().hasAttribute('playsinline') + } + + setPlaysinline(playsinline: boolean) { + this.el().playsinline = playsinline + + if (playsinline) { + this.el().setAttribute('playsinline', playsinline) + } else { + this.el().removeAttribute('playsinline') + } + } + + autoplay(): boolean { + return this.el().autoplay || this.el().hasAttribute('autoplay') + } + + setAutoplay(autoplay: boolean) { + this.el().autoplay = autoplay + + if (autoplay) { + this.el().setAttribute('autoplay', autoplay) + } else { + this.el().removeAttribute('autoplay') + } + } + + defaultMuted(): boolean { + return this.el().defaultMuted || this.el().hasAttribute('defaultMuted') + } + + setDefaultMuted(defaultMuted: boolean) { + this.el().defaultMuted = defaultMuted + + if (defaultMuted) { + this.el().setAttribute('defaultMuted', defaultMuted) + } else { + this.el().removeAttribute('defaultMuted') + } + } + + paused(): boolean { + return this.el().paused + } + + width(): number { + return this.el().offsetWidth + } + + height(): number { + return this.el().offsetHeight + } + + getAbsoluteRange(): ITimeRange | null { + return null + } + + getTimeshiftAvailable(): boolean { + return false + } + + addTextTrack(kind: string, label: string, language: string) { + if (!this.featuresNativeTextTracks) { + return super.addTextTrack(kind, label, language) + } + + return this.el().addTextTrack(kind, label, language) + } + + createRemoteTextTrack(options) { + if (!this.featuresNativeTextTracks) { + return super.createRemoteTextTrack(options) + } + const htmlTrackElement = document.createElement('track') + + if (options.kind) { + htmlTrackElement.kind = options.kind + } + if (options.label) { + htmlTrackElement.label = options.label + } + if (options.language || options.srclang) { + htmlTrackElement.srclang = options.language || options.srclang + } + if (options.default) { + htmlTrackElement.default = options.default + } + if (options.id) { + htmlTrackElement.id = options.id + } + if (options.src) { + htmlTrackElement.src = options.src + } + + return htmlTrackElement + } + + addRemoteTextTrack(options, manualCleanup: boolean) { + const htmlTrackElement = super.addRemoteTextTrack(options, manualCleanup) + + if (this.featuresNativeTextTracks) { + this.el().appendChild(htmlTrackElement) + } + + return htmlTrackElement + } + + removeRemoteTextTrack(track) { + super.removeRemoteTextTrack(track) + + if (this.featuresNativeTextTracks) { + const tracks = this.$$('track') + + let i = tracks.length + + while (i--) { + if (track === tracks[i] || track === tracks[i].track) { + this.el().removeChild(tracks[i]) + } + } + } + } + + getVideoPlaybackQuality() { + if (typeof this.el().getVideoPlaybackQuality === 'function') { + return this.el().getVideoPlaybackQuality() + } + + const videoPlaybackQuality = {} + + if ( + typeof this.el().webkitDroppedFrameCount !== 'undefined' && + typeof this.el().webkitDecodedFrameCount !== 'undefined' + ) { + videoPlaybackQuality.droppedVideoFrames = + this.el().webkitDroppedFrameCount + videoPlaybackQuality.totalVideoFrames = this.el().webkitDecodedFrameCount + } + + if (window.performance && typeof window.performance.now === 'function') { + videoPlaybackQuality.creationTime = window.performance.now() + } else if ( + window.performance && + window.performance.timing && + typeof window.performance.timing.navigationStart === 'number' + ) { + videoPlaybackQuality.creationTime = + window.Date.now() - window.performance.timing.navigationStart + } + + return videoPlaybackQuality + } + + /** + * Не используем работу с `poster` потому что за ошибки у нас отвечает VokaLockScreenPlugin + */ + poster() { + return null + } + + setPoster(poster) { + } + + preload(): string { + return this.el().preload + } + + setPreload(preload: string) { + this.el().preload = preload + } + + error(error) { + return this.el().error || this.playerError_ + } + + seeking(): boolean { + return this.el().seeking + } + + seekable() { + return !this.isLive && this.el().seekable + } + + ended(): boolean { + return this.el().ended // || this._forsedStop + } + + played() { + return this.el().played + } + + /** + * Get the value of `networkState` from the media element. `networkState` indicates + * the current network state. It returns an enumeration from the following list: + * - 0: NETWORK_EMPTY + * - 1: NETWORK_IDLE + * - 2: NETWORK_LOADING + * - 3: NETWORK_NO_SOURCE + * + * @method Html5#networkState + * @return {number} + * The value of `networkState` from the media element. This will be a number + * from the list in the description. + * + * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-networkstate} + */ + networkState(): number { + return this.el().networkState + } + + /** + * Get the value of `readyState` from the media element. `readyState` indicates + * the current state of the media element. It returns an enumeration from the + * following list: + * - 0: HAVE_NOTHING + * - 1: HAVE_METADATA + * - 2: HAVE_CURRENT_DATA + * - 3: HAVE_FUTURE_DATA + * - 4: HAVE_ENOUGH_DATA + * + * @method Html5#readyState + * @return {number} + * The value of `readyState` from the media element. This will be a number + * from the list in the description. + * + * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#ready-states} + */ + readyState(): number { + return this.el().readyState + } + + videoWidth(): number { + return this.el().videoWidth + } + + videoHeight(): number { + return this.el().videoHeight + } + + /* -------------------------------------------------------------- */ + + /* + * Static methods + */ + + static resetMediaElement(el) { + if (!el) { + return + } + + const sources = el.querySelectorAll('source') + let i = sources.length + + while (i--) { + el.removeChild(sources[i]) + } + + // remove any src reference. + // not setting `src=''` because that throws an error + el.removeAttribute('src') + + if (typeof el.load === 'function') { + // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) + const func = () => { + try { + el.load() + } + catch (e) { + // satisfy linter + } + } + func() + } + } + + static disposeMediaElement(el) { + if (!el) { + return + } + + if (el.parentNode) { + el.parentNode.removeChild(el) + } + + // remove any child track or source nodes to prevent their loading + while (el.hasChildNodes()) { + el.removeChild(el.firstChild) + } + + // remove any src reference. not setting `src=''` because that causes a warning + // in firefox + el.removeAttribute('src') + + // force the media element to update its loading state by calling load() + // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793) + if (typeof el.load === 'function') { + // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) + const func = () => { + try { + el.load() + } + catch (e) { + // not supported + } + } + func() + } + } + + static canOverrideAttributes(): boolean { + // if we cannot overwrite the src/innerHTML property, there is no support + // iOS 7 safari for instance cannot do this. + try { + const noop = () => { + } + + Object.defineProperty(document.createElement('video'), 'src', { + get: noop, + set: noop + }) + Object.defineProperty(document.createElement('audio'), 'src', { + get: noop, + set: noop + }) + Object.defineProperty(document.createElement('video'), 'innerHTML', { + get: noop, + set: noop + }) + Object.defineProperty(document.createElement('audio'), 'innerHTML', { + get: noop, + set: noop + }) + } + catch (e) { + return false + } + + return true + } + + static supportsNativeTextTracks(): boolean { + return true + } + } + } VokaTech.Tech.registerTech('VokaTech', VokaTech.Tech) diff --git a/src/internal/player/native/dash/modules/HTTPLoader.ts b/src/internal/player/native/dash/modules/HTTPLoader.ts deleted file mode 100644 index 1d3ef50..0000000 --- a/src/internal/player/native/dash/modules/HTTPLoader.ts +++ /dev/null @@ -1,332 +0,0 @@ -import EventBus from "../../../../vendors/dash.js/src/core/EventBus"; -import Settings from "../../../../vendors/dash.js/src/core/Settings"; -import {HTTPRequest} from "../../../../vendors/dash.js/src/streaming/vo/metrics/HTTPRequest"; -import Events from "../../../../vendors/dash.js/src/core/events/Events"; -import DashJSError from "../../../../vendors/dash.js/src/streaming/vo/DashJSError"; -import FetchLoader from "../../../../vendors/dash.js/src/streaming/net/FetchLoader"; -import Constants from "../../../../vendors/dash.js/src/streaming/constants/Constants"; -import XHRLoader from "../../../../vendors/dash.js/src/streaming/net/XHRLoader"; -import Utils from "../../../../vendors/dash.js/src/core/Utils"; -import CmcdModel from "../../../../vendors/dash.js/src/streaming/models/CmcdModel"; -import LowLatencyThroughputModel from "../../../../vendors/dash.js/src/streaming/models/LowLatencyThroughputModel"; -import CustomParametersModel from "../../../../vendors/dash.js/src/streaming/models/CustomParametersModel"; -import {ExtendedMediaPlayerErrors} from "../../../../drm/dash/VokaDash"; -import {Environment} from "../../../../models/Environment"; - -export default function HTTPLoader(cfg) { - const context = this.context; - const errHandler = cfg.errHandler; - const dashMetrics = cfg.dashMetrics; - const mediaPlayerModel = cfg.mediaPlayerModel; - const requestModifier = cfg.requestModifier; - const boxParser = cfg.boxParser; - const errors = cfg.errors; - const requestTimeout = cfg.requestTimeout || 0; - const eventBus = EventBus(context).getInstance(); - const settings = Settings(context).getInstance(); - let isManifestTimeoutError = false - let isFragmentTimeoutError = false - - let instance, - requests, - delayedRequests, - retryRequests, - downloadErrorToRequestTypeMap, - cmcdModel, - customParametersModel, - lowLatencyThroughputModel; - - function internalLoad(config, remainingAttempts) { - - const request = config.request; - const traces = []; - let firstProgress = true; - let needFailureReport = true; - let requestStartTime = new Date(); - let lastTraceTime = requestStartTime; - let lastTraceReceivedCount = 0; - let fileLoaderType = null; - let httpRequest; - - if (!requestModifier || !dashMetrics || !errHandler) { - throw new Error('config object is not correct or missing'); - } - - const handleLoaded = function (success) { - needFailureReport = false; - - request.requestStartDate = requestStartTime; - request.requestEndDate = new Date(); - request.firstByteDate = request.firstByteDate || requestStartTime; - request.fileLoaderType = fileLoaderType; - - if (!request.checkExistenceOnly) { - const responseUrl = httpRequest.response ? httpRequest.response.responseURL : null; - const responseStatus = httpRequest.response ? httpRequest.response.status : null; - const responseHeaders = httpRequest.response && httpRequest.response.getAllResponseHeaders ? httpRequest.response.getAllResponseHeaders() : - httpRequest.response ? httpRequest.response.responseHeaders : []; - - dashMetrics.addHttpRequest(request, responseUrl, responseStatus, responseHeaders, success ? traces : null); - - if (request.type === HTTPRequest.MPD_TYPE) { - dashMetrics.addManifestUpdate(request); - } - } - }; - - const onloadend = function () { - if (requests.indexOf(httpRequest) === -1) { - return; - } else { - requests.splice(requests.indexOf(httpRequest), 1); - } - - if (needFailureReport) { - handleLoaded(false); - - if (remainingAttempts > 0) { - - // If we get a 404 to a media segment we should check the client clock again and perform a UTC sync in the background. - try { - if (settings.get().streaming.utcSynchronization.enableBackgroundSyncAfterSegmentDownloadError && request.type === HTTPRequest.MEDIA_SEGMENT_TYPE) { - // Only trigger a sync if the loading failed for the first time - const initialNumberOfAttempts = mediaPlayerModel.getRetryAttemptsForType(HTTPRequest.MEDIA_SEGMENT_TYPE); - if (initialNumberOfAttempts === remainingAttempts) { - eventBus.trigger(Events.ATTEMPT_BACKGROUND_SYNC); - } - } - } catch (e) { - - } - - remainingAttempts--; - let retryRequest = { config: config }; - retryRequests.push(retryRequest); - retryRequest.timeout = setTimeout(function () { - if (retryRequests.indexOf(retryRequest) === -1) { - return; - } else { - retryRequests.splice(retryRequests.indexOf(retryRequest), 1); - } - internalLoad(config, remainingAttempts); - }, mediaPlayerModel.getRetryIntervalsForType(request.type)); - } else { - if (request.type === HTTPRequest.MSS_FRAGMENT_INFO_SEGMENT_TYPE) { - return; - } - - let errorType = downloadErrorToRequestTypeMap[request.type] - - if (isManifestTimeoutError) { - errorType = downloadErrorToRequestTypeMap['MPD_TYPE_TIMEOUT'] - } else if (isFragmentTimeoutError) { - errorType = downloadErrorToRequestTypeMap['FRAGMENT_TYPE_TIMEOUT'] - } - - errHandler.error(new DashJSError( - errorType, - request.url + ' is not available', { - request: request, - response: httpRequest.response - })); - - if (config.error) { - config.error(request, 'error', httpRequest.response.statusText); - } - - if (config.complete) { - config.complete(request, httpRequest.response.statusText); - } - } - } - }; - - const progress = function (event) { - const currentTime = new Date(); - - if (firstProgress) { - firstProgress = false; - if (!event.lengthComputable || - (event.lengthComputable && event.total !== event.loaded)) { - request.firstByteDate = currentTime; - } - } - - if (event.lengthComputable) { - request.bytesLoaded = event.loaded; - request.bytesTotal = event.total; - } - - if (!event.noTrace) { - traces.push({ - s: lastTraceTime, - d: event.time ? event.time : currentTime.getTime() - lastTraceTime.getTime(), - b: [event.loaded ? event.loaded - lastTraceReceivedCount : 0] - }); - - lastTraceTime = currentTime; - lastTraceReceivedCount = event.loaded; - } - - if (config.progress && event) { - config.progress(event); - } - }; - - const onload = function () { - if (httpRequest.response.status >= 200 && httpRequest.response.status <= 299) { - handleLoaded(true); - - if (config.success) { - config.success(httpRequest.response.response, httpRequest.response.statusText, httpRequest.response.responseURL); - } - - if (config.complete) { - config.complete(request, httpRequest.response.statusText); - } - } - }; - - const onabort = function () { - if (config.abort) { - config.abort(request); - } - }; - - const ontimeout = function (event) { - if (request.type === HTTPRequest.MPD_TYPE) { - isManifestTimeoutError = true - } - if ([ - HTTPRequest.INIT_SEGMENT_TYPE, - HTTPRequest.MEDIA_SEGMENT_TYPE - ].includes(request.type)) { - isFragmentTimeoutError = true - } - }; - - let loader; - if (request.hasOwnProperty('availabilityTimeComplete') && request.availabilityTimeComplete === false && window.fetch && request.responseType === 'arraybuffer' && request.type === HTTPRequest.MEDIA_SEGMENT_TYPE) { - loader = FetchLoader(context).create({ - requestModifier: requestModifier, - lowLatencyThroughputModel, - boxParser: boxParser - }); - loader.setup({ - dashMetrics - }); - fileLoaderType = Constants.FILE_LOADER_TYPES.FETCH; - } else { - loader = XHRLoader(context).create({ - requestModifier: requestModifier - }); - fileLoaderType = Constants.FILE_LOADER_TYPES.XHR; - } - - let headers = null; - let modifiedUrl = requestModifier.modifyRequestURL(request.url); - if (settings.get().streaming.cmcd && settings.get().streaming.cmcd.enabled) { - const cmcdMode = settings.get().streaming.cmcd.mode; - if (cmcdMode === Constants.CMCD_MODE_QUERY) { - const additionalQueryParameter = _getAdditionalQueryParameter(request); - modifiedUrl = Utils.addAditionalQueryParameterToUrl(modifiedUrl, additionalQueryParameter); - } else if (cmcdMode === Constants.CMCD_MODE_HEADER) { - headers = cmcdModel.getHeaderParameters(request); - } - } - request.url = modifiedUrl; - const verb = request.checkExistenceOnly ? HTTPRequest.HEAD : HTTPRequest.GET; - const withCredentials = customParametersModel.getXHRWithCredentialsForType(request.type); - - let timeout = requestTimeout - - if (request.type === HTTPRequest.MPD_TYPE) { - timeout = Environment.shared().timeout.manifest - } else if (request.type === HTTPRequest.MEDIA_SEGMENT_TYPE) { - timeout = Environment.shared().timeout.chunk - } - - httpRequest = { - url: modifiedUrl, - method: verb, - withCredentials: withCredentials, - request: request, - onload: onload, - onend: onloadend, - onerror: onloadend, - progress: progress, - onabort: onabort, - ontimeout: ontimeout, - loader: loader, - timeout, - headers: headers - }; - - // Adds the ability to delay single fragment loading time to control buffer. - let now = new Date().getTime(); - if (isNaN(request.delayLoadingTime) || now >= request.delayLoadingTime) { - // no delay - just send - requests.push(httpRequest); - loader.load(httpRequest); - } else { - // delay - let delayedRequest = { httpRequest: httpRequest }; - delayedRequests.push(delayedRequest); - delayedRequest.delayTimeout = setTimeout(function () { - if (delayedRequests.indexOf(delayedRequest) === -1) { - return; - } else { - delayedRequests.splice(delayedRequests.indexOf(delayedRequest), 1); - } - try { - requestStartTime = new Date(); - lastTraceTime = requestStartTime; - requests.push(delayedRequest.httpRequest); - loader.load(delayedRequest.httpRequest); - } catch (e) { - delayedRequest.httpRequest.onerror(); - } - }, (request.delayLoadingTime - now)); - } - } - - function setup() { - requests = []; - delayedRequests = []; - retryRequests = []; - cmcdModel = CmcdModel(context).getInstance(); - lowLatencyThroughputModel = LowLatencyThroughputModel(context).getInstance(); - customParametersModel = CustomParametersModel(context).getInstance(); - - downloadErrorToRequestTypeMap = { - [HTTPRequest.MPD_TYPE]: errors.DOWNLOAD_ERROR_ID_MANIFEST_CODE, - MPD_TYPE_TIMEOUT : 77, - FRAGMENT_TYPE_TIMEOUT : 78, - [HTTPRequest.XLINK_EXPANSION_TYPE]: errors.DOWNLOAD_ERROR_ID_XLINK_CODE, - [HTTPRequest.INIT_SEGMENT_TYPE]: errors.DOWNLOAD_ERROR_ID_INITIALIZATION_CODE, - [HTTPRequest.MEDIA_SEGMENT_TYPE]: errors.DOWNLOAD_ERROR_ID_CONTENT_CODE, - [HTTPRequest.INDEX_SEGMENT_TYPE]: errors.DOWNLOAD_ERROR_ID_CONTENT_CODE, - [HTTPRequest.BITSTREAM_SWITCHING_SEGMENT_TYPE]: errors.DOWNLOAD_ERROR_ID_CONTENT_CODE, - [HTTPRequest.OTHER_TYPE]: errors.DOWNLOAD_ERROR_ID_CONTENT_CODE - }; - } - - setup() - - return { - load: function(config) { - if (config.request) { - internalLoad( - config, - mediaPlayerModel.getRetryAttemptsForType( - config.request.type - ) - ); - } else { - if (config.error) { - config.error(config.request, 'error'); - } - } - }, - } -} \ No newline at end of file diff --git a/src/internal/player/native/tizen/TizenAVPlay.d.ts b/src/internal/player/native/tizen/TizenAVPlay.d.ts new file mode 100644 index 0000000..bda809e --- /dev/null +++ b/src/internal/player/native/tizen/TizenAVPlay.d.ts @@ -0,0 +1,275 @@ +// tizen-avplay.d.ts +// Types for Samsung Tizen AVPlay (webapis.avplay) +// Source: Samsung Developer — AVPlay API +// https://developer.samsung.com/smarttv/develop/api-references/samsung-product-api-references/avplay-api.html + +/* ========================= + * Enums (as string unions) + * ========================= */ + +export type AVPlayPlayerState = 'NONE' | 'IDLE' | 'READY' | 'PLAYING' | 'PAUSED'; + +export type AVPlayDisplayMode = + | 'PLAYER_DISPLAY_MODE_LETTER_BOX' + | 'PLAYER_DISPLAY_MODE_FULL_SCREEN' + | 'PLAYER_DISPLAY_MODE_AUTO_ASPECT_RATIO'; + +export type AVPlayBufferOption = 'PLAYER_BUFFER_FOR_PLAY' | 'PLAYER_BUFFER_FOR_RESUME'; + +export type AVPlayBufferSizeUnit = +// 'PLAYER_BUFFER_SIZE_IN_BYTE' is deprecated since Tizen 5.0 + 'PLAYER_BUFFER_SIZE_IN_SECOND'; + +export type AVPlayStreamingPropertyType = + | 'COOKIE' + | 'USER_AGENT' + | 'PREBUFFER_MODE' + | 'ADAPTIVE_INFO' + // 'SET_MODE_4K' deprecated on Retail TV since Tizen 5.0 (use ADAPTIVE_INFO: FIXED_MAX_RESOLUTION) + | 'SET_MODE_4K' + | 'LISTEN_SPARSE_TRACK' + // getters: + | 'IS_LIVE' + | 'AVAILABLE_BITRATE' + | 'GET_LIVE_DURATION' + | 'CURRENT_BANDWIDTH' + // B2B only: + | 'USE_VIDEOMIXER' + | 'SET_MIXEDFRAME' + | 'PORTRAIT_MODE' + // Since Tizen 7.0: + | 'IN_APP_MULTIVIEW'; + +export type AVPlayStreamType = 'VIDEO' | 'AUDIO' | 'TEXT'; + +export type AVPlayDrmType = 'PLAYREADY' | 'VERIMATRIX' | 'WIDEVINE_CDM'; + +export type AVPlayDrmOperation = + | 'SetProperties' + | 'InstallLicense' + // deprecated in 2019 but still appears in enum: + | 'ProcessInitiator' + | 'widevine_license_data'; + +export type AVPlayEvent = +// not fully enumerated in docs; keep as string to be forward-compatible + string; + +export type AVPlayError = + | 'PLAYER_ERROR_NONE' + | 'PLAYER_ERROR_INVALID_PARAMETER' + | 'PLAYER_ERROR_NO_SUCH_FILE' + | 'PLAYER_ERROR_INVALID_OPERATION' + | 'PLAYER_ERROR_SEEK_FAILED' + | 'PLAYER_ERROR_INVALID_STATE' + | 'PLAYER_ERROR_NOT_SUPPORTED_FILE' + | 'PLAYER_ERROR_NOT_SUPPORTED_FORMAT' + | 'PLAYER_ERROR_INVALID_URI' + | 'PLAYER_ERROR_CONNECTION_FAILED' + | 'PLAYER_ERROR_GENEREIC'; + +/* =============== + * Helper structs + * =============== */ + +export interface AVPlayStreamInfo { + /** 0-based track index or -1 if invalid */ + index: number; + /** Track type: VIDEO | AUDIO | TEXT */ + type: AVPlayStreamType; + /** + * Extra info JSON string; examples: + * VIDEO: {"fourCC":"H264","Width":"1920","Height":"1080","Bit_rate":"477000"} + * AUDIO: {"language":"en","channels":"2","sample_rate":"44100","bit_rate":"96000","fourCC":"AACL"} + * TEXT : {"track_num":"0","track_lang":"en","subtitle_type":"-1","fourCC":"TTML"} + */ + extra_info: string | null; +} + +// Minimal shape; API returns an object describing seamless switching capability. +export interface AVPlayVideoSeamlessInfo { + // The public docs don’t standardize fields; keep as bag of data. + [key: string]: unknown; +} + +/** Subtitle attribute pair (name/value), as provided into onsubtitlechange */ +export interface AVPlaySubtitleAttribute { + name: string; + value: string; +} + +/* ========================= + * Callback interfaces + * ========================= */ + +export interface AVPlayPlaybackCallback { + onbufferingstart?(): void; + + onbufferingprogress?(percent: number): void; + + onbufferingcomplete?(): void; + + /** ms */ + oncurrentplaytime?(currentTime: number): void; + + onstreamcompleted?(): void; + + /** Generic player events (ID + data string) */ + onevent?(eventid: AVPlayEvent, data: string): void; + + /** Error without/with message */ + onerror?(eventid: AVPlayError): void; + + onerrormsg?(eventid: AVPlayError, errorMsg: string): void; + + /** DRM challenge / messages */ + ondrmevent?(type: AVPlayDrmType, drmData: unknown): void; + + /** Subtitle cue callback */ + onsubtitlechange?( + duration: string, + subtitles: string, + type: string, + attributes: AVPlaySubtitleAttribute[] + ): void; +} + +export interface AVPlaySoundAnalysisCallback { + ongetexception?(err: Error): void; + + onsetexception?(err: Error): void; + + ongetbandsarray?(bands: number[]): void; +} + +/* ========================= + * Main AVPlay interface + * ========================= */ + +export interface TizenAVPlay { + /* Lifecycle & prep */ + open(url: string): void; // states: NONE, IDLE + close(): void; // states: ANY (incl. READY/PLAYING/PAUSED) + prepare(): void; // states: IDLE, READY + prepareAsync( + successCallback?: () => void | null, + errorCallback?: (errType: + | 'NotSupportedError' + | 'InvalidValuesError' + | 'InvalidAccessError' + | 'InvalidStateError' + | 'UnknownError') => void | null + ): void; // states: IDLE, READY + + /* Display */ + setDisplayRect(x: number, y: number, width: number, height: number): void; // 1920x1080 reference grid + setDisplayMethod(mode: AVPlayDisplayMode): void; + + setDisplayRotation(rotation: string): void; // vendor string; documented but not enumerated + setVideoRoi(x_ratio: number, y_ratio: number, w_ratio: number, h_ratio: number): void; + + /* Playback control */ + play(): void; // READY, PLAYING (resume), PAUSED (resume) + pause(): void; + + stop(): void; + + seekTo(milliseconds: number, successCallback?: () => void | null, + errorCallback?: (err: unknown) => void | null + ): void; + + jumpForward(milliseconds: number, successCallback?: () => void | null, + errorCallback?: (err: unknown) => void | null + ): void; + + jumpBackward(milliseconds: number, successCallback?: () => void | null, + errorCallback?: (err: unknown) => void | null + ): void; + + setSpeed(playbackSpeed: number): void; // -16,-8,-4,-2,1,2,4,8,16 (limits depend on protocol) + + /* State & time */ + getState(): AVPlayPlayerState; + + getDuration(): number; // ms + getCurrentTime(): number; // ms + + /* Buffering */ + setTimeoutForBuffering(seconds: number): void; // default 20s; completion fires onbufferingcomplete + setBufferingParam(option: AVPlayBufferOption, unTit: AVPlayBufferSizeUnit, amount: number): void; // IDLE; amount >= + // 4s + + /* Tracks & stream info */ + setSelectTrack(type: AVPlayStreamType, trackIndex: number): void; + + getCurrentStreamInfo(): AVPlayStreamInfo[]; // READY, PLAYING, PAUSED + getTotalTrackInfo(): AVPlayStreamInfo[]; // READY(when prepare sync), PLAYING, PAUSED + + /* Streaming properties */ + setStreamingProperty(propertyType: AVPlayStreamingPropertyType, propertyParam: string): void; // IDLE + getStreamingProperty(propertyType: AVPlayStreamingPropertyType): string; + + /* Subtitles */ + setSilentSubtitle(onoff: boolean): void; // soft-hide + setExternalSubtitlePath(filePath: string): void; + + setSubtitlePosition(position: number): void; + + /* DRM */ + setDrm(drmType: AVPlayDrmType, drmOperation: AVPlayDrmOperation, jsonParam: string): string; + + getUID(drmType: AVPlayDrmType): string; + + /* Listeners */ + setListener(callbacks: AVPlayPlaybackCallback): void; + + setSoundAnalysisListener(callbacks: AVPlaySoundAnalysisCallback): void; + + unsetSoundAnalysisListener(): void; + + /* Misc / power-state */ + getVersion(): string; + + suspend(): void; + + restore(URL?: string | null, resumeTime?: number | null, bPrepare?: boolean | null): void; + + restoreAsync( + URL?: string | null, + resumeTime?: number | null, + bPrepare?: boolean | null, + successCallback?: () => void | null, + errorCallback?: (err: unknown) => void | null + ): void; + + setLooping(isLooping: boolean): void; + + setVideoStillMode(mode: string): void; + + /* Extras */ + getVideoSeamlessInfo(): AVPlayVideoSeamlessInfo; + + /* Audio on/off (since newer API) */ + enableAudioStream(): void; + + disableAudioStream(): void; +} + +/* ========================= + * webapis global shim + * ========================= */ + +declare global { + interface Window { + webapis: { + avplay: TizenAVPlay; + // Some platforms expose an avplay store/multiview APIs; + // keep it optional to avoid breaking builds. + avplaystore?: { + getPlayer(): TizenAVPlay; + }; + }; + } +} + +export {} diff --git a/src/internal/player/native/tizen/models/AudioTrack.ts b/src/internal/player/native/tizen/models/AudioTrack.ts deleted file mode 100644 index 0aaf0f8..0000000 --- a/src/internal/player/native/tizen/models/AudioTrack.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default interface AudioTrack { - index: number - name: string - data: any | null - isSelected: boolean | null -} \ No newline at end of file diff --git a/src/internal/player/native/tizen/models/PlayerLoadParameters.ts b/src/internal/player/native/tizen/models/PlayerLoadParameters.ts deleted file mode 100644 index 7b791a4..0000000 --- a/src/internal/player/native/tizen/models/PlayerLoadParameters.ts +++ /dev/null @@ -1,108 +0,0 @@ -type Manifest = { - source: string | null - video: ManifestVideoTrack[] | null - audio: ManifestAudioTrack[] | null -}; - -type ManifestVideoTrack = { - name: string - url: string - brandWidth: number | null - width: number | null - height: number | null - source: string[]; - group: string | null -} - -type ManifestAudioTrack = { - name: string - url: string - source: string - group: string | null -} - -type Stream = Readonly<{ - object: 'stream' - //protocol: Protocol; - video_codec: 'h264' | 'mp4v' - audio_codec: 'mp4a' - url: string - session_id: string - latest_video_offset: number | null - //heartbeat: Heartbeat | null; - //heartbeats: Heartbeats[]; - //analytics: Analytics; - //analytics_v2: AnalyticsV2; - //player?: Player | undefined; - //ads: Ads; - device_connection_type: 'wifi' | 'mobile' - content_provider: string -}> - -interface ManifestParserPlayList { - attributes: { - RESOLUTION: { - width: number | null - height: number | null - }; - BANDWIDTH: number | null - } -} - -interface ManifestParser { - allowCache: boolean | null - endList: boolean | null - mediaSequence: number | null - discontinuitySequence: number | null - playlistType: string | null - custom: any | null - playlists: ManifestParserPlayList[] | null - mediaGroups: any | null - dateTimeString: string | null - dateTimeObject: string | null - targetDuration: number | null - totalDuration: number | null - discontinuityStarts: any | null - segments: any[] | null -} - -interface TimeshiftAvailability { - available: boolean - object: 'timeshift_availability' - // period: Period; -} - -type DeviceInfo = Readonly<{ - osName: string | null - osVersion: string | null - model: string | null - vendor: string | null - deviceId: string | null - isUHDSupported: boolean | null -}> - -export { - ManifestAudioTrack, - ManifestVideoTrack, - Stream, - ManifestParser, - ManifestParserPlayList, - TimeshiftAvailability, - DeviceInfo, -} - -export default interface PlayerLoadParameters { - - manifest: Manifest - - container: HTMLDivElement | null - - timeShiftParams: TimeshiftAvailability | null - is4KStream: boolean - - NPAWParams: any | null - // mediaItem: PlayerMediaItem - - clientIP: string - -} \ No newline at end of file diff --git a/src/internal/player/native/tizen/tech/AVPlayHelper.ts b/src/internal/player/native/tizen/tech/AVPlayHelper.ts index ae8632b..a7e866e 100755 --- a/src/internal/player/native/tizen/tech/AVPlayHelper.ts +++ b/src/internal/player/native/tizen/tech/AVPlayHelper.ts @@ -1,12 +1,11 @@ -import videojs from "video.js"; -import VokaBusEvent from "@/internal/events/VokaBusEvent"; -import { EventBus, createEventDefinition } from 'ts-bus' +import type { AVPlayStreamInfo, TizenAVPlay } from '@/internal/player/native/tizen/TizenAVPlay' +import { createEventDefinition, EventBus } from 'ts-bus' +import videojs from 'video.js' namespace AVPlayHelper { - const log = videojs.log.createLogger("[AVPlayHelper]") + const log = videojs.log.createLogger('[AVPlayHelper]') - import setBufferingComplete = VokaBusEvent.setBufferingComplete; let avPlayer: IAVPlayer | null export enum State { @@ -27,34 +26,62 @@ namespace AVPlayHelper { time: number, player: IAVPlayer, }>()('timeChangeAVPlayEvent'), - } export interface IAVPlayer { get lastError(): any + get avState(): State + get state(): State + get eventsEmitter(): EventBus + get currentTime(): number + get duration(): number + get isPaused(): boolean + get isPlaying(): boolean + get isCompleted(): boolean + get isLive(): boolean + open(url: string) + prepare(): Promise + play() + pause() - seekTo(seconds: number) + + seekTo(seconds: number, successCallback?: () => void | null, errorCallback?: (err: unknown) => void | null) + jumpBackward(seconds: number) + jumpForward(seconds: number) + stop(close: boolean) + drmOptions(properties: any) + setDisplayRect(x: number, y: number, width: number, heigth: number) + setDisplayMethod(method: string) + getStreamingProperty(property: string): string + setStreamingProperty(property: string, value: string) - getLivePeriod(): [number, number] + + getAvailableBitrates(): number[] + + getTotalTrackInfo(): AVPlayStreamInfo[] + + setSelectTrack(type: 'AUDIO' | 'TEXT' | 'VIDEO', index: number): void + + getLivePeriod(): [ number, number ] } export function canCreate(): Boolean { @@ -63,15 +90,26 @@ namespace AVPlayHelper { } export function getInstance(): IAVPlayer { - if (avPlayer != null) { return avPlayer } + if (avPlayer != null) { + return avPlayer + } const instance = new AVPlayer(window['webapis']) avPlayer = instance return instance } + /** + * - setStreamingProperty('ADAPTIVE_INFO', 'BITRATES=5000~10000|STARTBITRATE=HIGHEST|SKIPBITRATE=LOWEST') — + * задаёт ABR-параметры (IDLE-state only). SET_MODE_4K депрекейтед на розничных ТВ с Tizen 5.0; вместо него + * используйте ADAPTIVE_INFO: FIXED_MAX_RESOLUTION=3840X2160.  + * - setBufferingParam (IDLE) и setTimeoutForBuffering (IDLE/READY/PLAYING/PAUSED) управляют стартовым/резюмным + * буфером и таймаутом ребуферинга; единицы — секунды.  + * - Список колбэков setListener (включая onerrormsg, ondrmevent, onsubtitlechange) дан в таблице API; вешайте + * в состоянии IDLE, чтобы не пропускать ивенты. + */ class AVPlayer implements IAVPlayer { - private readonly avPlayer: any + private readonly avPlayer: TizenAVPlay private _lastError: any private _state: State private _bus: EventBus @@ -87,28 +125,30 @@ namespace AVPlayHelper { this._isCompleted = false this._isPrebufferPlaying = false - log("AVplay initialized " + this.avPlayer.getVersion()) + log('AVplay initialized ' + this.avPlayer.getVersion()) } private addListener(avPlayer: any) { - if (AVPlayer.isAllowedState(this.state, State.idle) == null) { return } + if (AVPlayer.isAllowedState(this.state, State.idle) == null) { + return + } avPlayer.setListener({ oncurrentplaytime: (currentTime) => { - log("oncurrentplaytime : ", currentTime) + // log('oncurrentplaytime : ', currentTime) this._isCompleted = false try { - this._currentTime = this.avPlayer.getCurrentTime(); + this._currentTime = this.avPlayer.getCurrentTime() } catch { - this._currentTime = currentTime; + this._currentTime = currentTime } - this._bus.publish(Events.time({ time: this.currentTime, player: this })); + this._bus.publish(Events.time({ time: this.currentTime, player: this })) }, onevent: (eventType, eventData) => { - log("event type: " + eventType + ", data: " + eventData) + log('event type: ' + eventType + ', data: ' + eventData) }, onbufferingstart: () => { - log("Buffering start.") + log('Buffering start.') if (this.requiredState(State.idle)) { this.switchState(State.ready) } else if (this.requiredState(State.playing)) { @@ -116,50 +156,50 @@ namespace AVPlayHelper { } }, onbufferingprogress: (percent) => { - log("Buffering progress data : " + percent) + log('Buffering progress data : ' + percent) }, onbufferingcomplete: () => { if (this._isPrebufferPlaying) { this._isPrebufferPlaying = false if (this.avState === State.paused) this.play() } - log("Buffering complete. avState, avPlayer ", this.avState, this.avPlayer) + log('Buffering complete. avState, avPlayer ', this.avState, this.avPlayer) }, onrenderingstart: () => { - log('onRenderingStart'); + log('onRenderingStart') }, onstreamcompleted: () => { - log("Stream Completed") + log('Stream Completed') this._isCompleted = true this._isPrebufferPlaying = false this.stop(false) }, - onsubtitlechange: (duration, text, data3, data4) => { - log("subtitleText: " + text) + onsubtitlechange: (duration, text) => { + log('subtitleText: ' + text) }, onerror: (eventType) => { this._lastError = eventType - log("event type error : " + eventType) + log('[onerror] event type ', eventType) }, ondrmevent: (drmEvent, drmData) => { - log("DRM callback: ", drmEvent, drmData) + log('[ondrmevent] event, data : ', drmEvent, drmData) }, - onerrormsg: (err, msg) => log.error("onerrormsg:", err, msg), + onerrormsg: (err, msg) => log.error('[onerrormsg]:', err, msg) }) } private switchState(state: State) { const arrived = AVPlayer.isAllowedState(this._state, state) if (arrived == null) { - log("switchState Not allowed transition from " + this._state + " to " + state) + log('switchState Not allowed transition from ' + this._state + ' to ' + state) return } - log("Transition from " + this._state + " to " + state) + log('Transition from ' + this._state + ' to ' + state) this._state = arrived this._bus.publish( - Events.state({state: arrived, player: this}) + Events.state({ state: arrived, player: this }) ) } @@ -170,57 +210,56 @@ namespace AVPlayHelper { if (arrive == State.idle) return arrive break case State.idle: - if ([State.idle, State.ready, State.none].includes(arrive)) return arrive + if ([ State.idle, State.ready, State.none ].includes(arrive)) return arrive break case State.ready: - if ([State.ready, State.playing, State.paused].includes(arrive)) return arrive + if ([ State.ready, State.playing, State.paused ].includes(arrive)) return arrive break case State.playing: - if ([State.idle, State.playing, State.paused].includes(arrive)) return arrive + if ([ State.idle, State.playing, State.paused ].includes(arrive)) return arrive break case State.paused: - if ([State.playing, State.paused].includes(arrive)) return arrive + if ([ State.playing, State.paused ].includes(arrive)) return arrive break } - log("isAllowedState Not allowed state transition: " + depart + " -> " + arrive) + log('[isAllowedState] Not allowed state transition: ' + depart + ' -> ' + arrive) return null } private requiredState(state: State): boolean { - if (this.state != state) { - log.error("requiredState incorrect state: ", this.state, state) - return false - } - return true + return this.requireAllowedStates([ state ]) } private requireAllowedStates(states: State[]): boolean { - if (!states.includes(this.state)) { - log.error("requireAllowedStates incorrect state: ", this.state, states, ) - return false - } - return true + // log.error('[requireAllowedStates] incorrect state: current, required ', this.state, states) + return states.includes(this.state) } // MARK: - IAVPlay - get lastError(): any { return this._lastError } + get lastError(): any { + return this._lastError + } get avState(): State { - // log("avState: ", this.avPlayer.getState()) return this.avPlayer.getState() } get state(): State { - // log("state: _state, avPlayer.getState()", this._state, this.avPlayer.getState()) return this._state } - get eventsEmitter(): EventBus { return this._bus } + get eventsEmitter(): EventBus { + return this._bus + } get currentTime(): number { - try { return this.avPlayer.currentTime() / 1000 } - catch { return this._currentTime / 1000 } + try { + return this.avPlayer.currentTime() / 1000 + } + catch { + return this._currentTime / 1000 + } } get duration(): number { @@ -232,14 +271,25 @@ namespace AVPlayHelper { } get isLive() { - const isLive = this.avPlayer.getStreamingProperty("IS_LIVE") || ""; - log("IS_LIVE, result", isLive, typeof isLive, isLive === "1") - return isLive === "1" + if (!this.requireAllowedStates([ State.ready, State.paused, State.playing ])) { + return false + } + const isLive = this.avPlayer.getStreamingProperty('IS_LIVE') || '' + // log('IS_LIVE, result', isLive, typeof isLive, isLive === '1') + return isLive === '1' } - get isPaused(): boolean { return this.state != State.playing } - get isPlaying(): boolean { return this.state == State.playing } - get isCompleted() { return this._isCompleted; } + get isPaused(): boolean { + return this.state != State.playing + } + + get isPlaying(): boolean { + return this.state == State.playing + } + + get isCompleted() { + return this._isCompleted + } open(url: string) { if (AVPlayer.isAllowedState(this.state, State.idle) == null) { @@ -252,7 +302,7 @@ namespace AVPlayHelper { } prepare(): Promise { - log('prepare'); + log('prepare') if (!this.requiredState(State.idle)) { return Promise.reject('incorrect state') } @@ -261,12 +311,12 @@ namespace AVPlayHelper { this.avPlayer.prepareAsync( () => { if (this.requiredState(State.idle)) this.switchState(State.ready) - log('prepareAsync completed: _state, avState', this._state, this.avState); + log('prepareAsync completed: _state, avState', this._state, this.avState) resolve() }, (error) => { - log('prepareFailed: ', error, this.avState); - log('prepareFailed stack: ', error.stack); + log('prepareFailed: ', error, this.avState) + log('prepareFailed stack: ', error.stack) reject('prepare failed: ' + error) } ) @@ -275,84 +325,109 @@ namespace AVPlayHelper { } play() { - log('play'); + log('play') if (AVPlayer.isAllowedState(this.state, State.playing) == null) { - log('play invalid transition', this.state, State.playing); + log('play invalid transition', this.state, State.playing) return } try { this.avPlayer.play() this.switchState(State.playing) - } catch (error) { - log.error("play error: ", error) + } + catch (error) { + log.error('play error: ', error) } } pause() { - if (AVPlayer.isAllowedState(this.state, State.paused) == null) { return } + if (AVPlayer.isAllowedState(this.state, State.paused) == null) { + return + } try { this.avPlayer.pause() this.switchState(State.paused) - } catch (error) { - log.error("pause error: ",error) + } + catch (error) { + log.error('pause error: ', error) } } - seekTo(seconds: number) { - if (!this.requireAllowedStates([State.ready, State.paused, State.playing, State.idle])) { return } + seekTo(seconds: number, + ...args: [ successCallback?: () => void | null, errorCallback?: (err: unknown) => void | null ] + ) { + log('seekTo seconds: ', seconds) + if (!this.requireAllowedStates([ State.ready, State.paused, State.playing, State.idle ])) { + return + } try { - this.avPlayer.seekTo(seconds * 1000) - } catch (e) { - log.error("seekTo error: ", e) + this.avPlayer.seekTo(seconds * 1000, ...args) + } + catch (e) { + log.error('seekTo error: ', e) } } stop(close: boolean) { - if (!this.requiredState(State.playing)) { return } + if (!this.requiredState(State.playing)) { + return + } this._currentTime = 0 this.avPlayer.stop() this.switchState(State.idle) if (close) { - if (!this.requiredState(State.idle)) { return } + if (!this.requiredState(State.idle)) { + return + } this.avPlayer.close() this.switchState(State.none) } } drmOptions(properties: any) { - if (!this.requiredState(State.idle)) { return } - log("[drmOptions] setDrm properties:", properties) + if (!this.requiredState(State.idle)) { + return + } + log('[drmOptions] setDrm properties:', properties) try { this.avPlayer.setDrm( - 'PLAYREADY', - 'SetProperties', - JSON.stringify(properties) + 'PLAYREADY', + 'SetProperties', + JSON.stringify(properties) ) - } catch (e) { - log("[drmOptions] setDrm error", e) + } + catch (e) { + log('[drmOptions] setDrm error', e) } } setDisplayRect(x: number, y: number, width: number, height: number) { - if (!this.requiredState(State.idle)) { return } - log('setDisplayRect', x, y, width, height); + if (!this.requiredState(State.idle)) { + return + } + log('setDisplayRect', x, y, width, height) this.avPlayer.setDisplayRect(x, y, width, height) } setDisplayMethod(method: string) { - if (!this.requiredState(State.idle)) { return } + if (!this.requiredState(State.idle)) { + return + } this.avPlayer.setDisplayMethod(method) } setStreamingProperty(property: string, value: string) { - if (!this.requiredState(State.idle)) { return } + if (!this.requiredState(State.idle)) { + return + } this.avPlayer.setStreamingProperty(property, value) } getStreamingProperty(property: string): string { - if (!this.requiredState(State.idle)) { return '' } + if (!this.requireAllowedStates([ State.ready, State.playing, State.paused ])) { + return '' + } return this.avPlayer.getStreamingProperty(property) } @@ -364,26 +439,85 @@ namespace AVPlayHelper { this.avPlayer.jumpForward(seconds * 1000) } - getLivePeriod() { + getLivePeriod(): [ number, number ] { + if (!this.requireAllowedStates([ State.ready, State.paused, State.playing, State.idle ])) { + return [ 0, 0 ] + } if (!this.isLive) { - log.warn("cant get live period – stream is not LIVE") - return [0, 0] + log.warn('cant get live period – stream is not LIVE') + return [ 0, 0 ] } let period try { - period = this.avPlayer.getStreamingProperty("GET_LIVE_DURATION").split("|") || [0, 0] - period = [parseInt(period[0]) / 1000, parseInt(period[1]) / 1000] - log('live period', period) + period = this.avPlayer.getStreamingProperty('GET_LIVE_DURATION').split('|') || [ 0, 0 ] + period = [ parseInt(period[0]) / 1000 || 0, parseInt(period[1]) / 1000 || 0 ] + // log('live period', period) return period - } catch (e) { - log.error("cant get live period") - return [0, 0] + } + catch (e) { + log.error('cant get live period') + return [ 0, 0 ] } } + getAvailableBitrates() { + const value = this.getStreamingProperty('AVAILABLE_BITRATE') + return (value ?? '').split(':').map((v) => { + return num(v) || 0 + }) + } + + getTotalTrackInfo() { + if (!this.requireAllowedStates([ State.ready, State.playing, State.paused ])) { + return [] + } + try { + const totalTrackInfo = this.avPlayer.getTotalTrackInfo() + log('[getTotalTrackInfo] totalTrackInfo ', totalTrackInfo) + return totalTrackInfo + } + catch (e) { + log.error('[getTotalTrackInfo] error ', e) + } + return [] + } + + // Здесь index - сквозное значение по всем трекам из `getTotalTrackInfo()` + setSelectTrack(type: 'AUDIO' | 'TEXT' | 'VIDEO', index: number): void { + if (!this.requireAllowedStates([ State.ready, State.playing, State.paused ])) { + return + } + try { + this.avPlayer.setSelectTrack(type, index) + } + catch { + log('Can`t set track', type, index) + } + } } + /* ========= + * helpers + * ========= */ + export function safeJSON(s: unknown): any { + if (typeof s !== 'string') return undefined + try { + return JSON.parse(s) + } + catch { + return undefined + } + } + + export function num(v: unknown): number | undefined { + const n = typeof v === 'string' ? Number(v) : typeof v === 'number' ? v : NaN + return Number.isFinite(n) ? n : undefined + } + + export function str(v: unknown): string | undefined { + return typeof v === 'string' && v.length ? v : undefined + } } -export default AVPlayHelper \ No newline at end of file +export default AVPlayHelper diff --git a/src/internal/player/native/tizen/tech/VokaTizenTech.ts b/src/internal/player/native/tizen/tech/VokaTizenTech.ts index 5e86c6e..4c627b9 100755 --- a/src/internal/player/native/tizen/tech/VokaTizenTech.ts +++ b/src/internal/player/native/tizen/tech/VokaTizenTech.ts @@ -1,68 +1,80 @@ -import { DeviceInfo, Stream } from "@/internal/player/native/tizen/models/PlayerLoadParameters"; -import { hkdf } from "crypto"; +import type { EventBus } from 'ts-bus' import videojs from 'video.js' -import VokaTizenSourceHandler from '../sourcehandler/VokaTizenSourceHandler' -import AVPlayHelper from './AVPlayHelper' -import AudioTrack from '../models/AudioTrack' -import { IS_TIZEN } from '@/internal/utils/browser' -import { IVokaSource, TizenSourceParams } from '@/internal/player/native/VokaSourceHandler' +import type AudioTrack from 'video.js/dist/types/tracks/audio-track' + +import VokaBusEvent from '@/internal/events/VokaBusEvent' +import type { DeviceInfo, Stream } from '@/internal/player/native/PlayerLoadParameters' +import VokaTizenSourceHandler from '@/internal/player/native/tizen/sourcehandler/VokaTizenSourceHandler' import ScreenSaverHelper from '@/internal/player/native/tizen/tech/ScreenSaverHelper' +import type { IVokaSource, TizenSourceParams } from '@/internal/player/native/VokaSourceHandler' +import type VokaCorePlayer from '@/internal/player/VokaCorePlayer' +import { IS_TIZEN } from '@/internal/utils/browser' +import type { IAudioTrack } from '@/public/IVokaPlayer' + +import AVPlayHelper from './AVPlayHelper' +import safeJSON = AVPlayHelper.safeJSON const log = videojs.log.createLogger('[VokaTizenTech]') -const Tech= videojs.getTech('Tech') +const Tech = videojs.getTech('Tech') class VokaTizenTech extends Tech { static TECH_NAME = 'VokaTizenTech' - private lastCurrentTime: number | null - private playPromise: null + private playPromise: null = null private avPlayer: AVPlayHelper.IAVPlayer private screenSaver: ScreenSaverHelper.IScreenSaver - private params: TizenSourceParams | null - private currentSrcUrl: string | null + private params: TizenSourceParams | null = null + private sourceUrl: string | null = null - private is4KSupported: boolean - private selectedAudioTrack: AudioTrack | null - private selectedVideoTrack: any | null - private startTime: number | null - private switchToThisTime: number | null + private bus: EventBus | null + private is4KSupported: boolean = false + + private videoQualities: VokaCorePlayer.IQualityData[] = [] + private selectedQuality: VokaCorePlayer.IQualityData[] | null = null + private audioTracks: videojs.AudioTrack[] = [] + private selectedAudioTrackIndex: number | null = null + + private startTime: number | null = null + private switchToThisTime: number | null = null + + private source: IVokaSource | null + + private changeAudioTrackHandler = (event: ReturnType) => this.changeAudioTrack( + event) + private changeQualityHandler = (event: ReturnType) => this.changeQuality(event) + private changingTrack: boolean = false constructor(options, ready) { const opt = options || {} opt.techId = VokaTizenTech.TECH_NAME super(opt, ready) this.name_ = VokaTizenTech.TECH_NAME - this.playPromise = null - this.lastCurrentTime = null if (!AVPlayHelper.canCreate()) { - log.error("Can't create instance of avplay") - return + throw Error('Can\'t create instance of avplay') } this.avPlayer = AVPlayHelper.getInstance() this.screenSaver = ScreenSaverHelper.create() - this.is4KSupported = false - this.startTime = null - this.selectedVideoTrack = null - this.selectedAudioTrack = null - this.switchToThisTime = null + // Event bus bridge for UI/observers + this.source = (opt.source || null) as IVokaSource | null + this.bus = (this.source && (this.source as any).bus) ? (this.source as any).bus as EventBus : null this.attachListeners(this.avPlayer) if (options.source) { - this.setSrc(options.source); + this.setSrc(options.source) } this.triggerReady() // from Component } dispose() { - this.avPlayer.stop(true) - this.avPlayer.eventsEmitter.emitter.removeAllListeners() - super.dispose() + this.avPlayer.stop(true) + this.avPlayer.eventsEmitter.emitter.removeAllListeners() + super.dispose() } private attachListeners(player: AVPlayHelper.IAVPlayer) { @@ -73,7 +85,7 @@ class VokaTizenTech extends Tech { { type: 'timeupdate', target: this, - manuallyTriggered: true, + manuallyTriggered: true } ) } @@ -82,6 +94,7 @@ class VokaTizenTech extends Tech { player.eventsEmitter.subscribe( AVPlayHelper.Events.state as string, event => { + if (this.changingTrack) return const state = event.payload.state switch (state) { case AVPlayHelper.State.playing: @@ -91,11 +104,11 @@ class VokaTizenTech extends Tech { this.trigger('pause') break case AVPlayHelper.State.ready: - log("this.autoplay() = ", this.autoplay()); + log('this.autoplay() = ', this.autoplay()) if (this.autoplay() === true) { log('try to start autoplaying') this.play().catch((reason) => { - log.error("can`t autoplay with reason", reason) + log.error('can`t autoplay with reason', reason) }) } this.trigger('loadstart') @@ -106,9 +119,26 @@ class VokaTizenTech extends Tech { } } ) + + this.on('loadedmetadata', () => { + if (this.changingTrack) return + try { + this.setVideoQualities() + this.setAudioTracks() + } + catch (e) { + log.error('Error while setting tracks', e) + } + }) + + if (this.bus) { + this.bus.subscribe(VokaBusEvent.audioTracksSet, this.changeAudioTrackHandler) + this.bus.subscribe(VokaBusEvent.qualitySet, this.changeQualityHandler) + } } /* API ------------------------------------------------ */ + createEl(): Element { const playerId = this.options_.playerId const el = document.createElement(this.elementTagName) @@ -127,33 +157,32 @@ class VokaTizenTech extends Tech { } load() { - log('LOAD') } play(): Promise { - log("play() started") + log('play() started') this.screenSaver.disable() - return new Promise((resolve, reject) => { const playTimeout = setTimeout(() => { - log.error("play() timeout") - reject(new Error("play timeout")) + log.error('play() timeout') + reject(new Error('play timeout')) }, 10000) const onPlay = () => { clearTimeout(playTimeout) this.off('play', onPlay as any) - log("play() resolved") + log('play() resolved') resolve() } this.one('play', onPlay) try { this.avPlayer.play() - } catch (e) { + } + catch (e) { clearTimeout(playTimeout) - log.error("play() error", e) + log.error('play() error', e) reject(e) } }) @@ -167,7 +196,10 @@ class VokaTizenTech extends Tech { reset() { this.screenSaver.enable() this.startTime = null - this.selectedAudioTrack = null + this.videoQualities = [] + this.selectedQuality = null + this.audioTracks = [] + this.selectedAudioTrackIndex = null this.params = null const el = this.el() @@ -183,37 +215,52 @@ class VokaTizenTech extends Tech { this.avPlayer.stop(true) } - loop() { return false } - setLoop(value) { } + loop() { + return false + } + + setLoop(value) { + } + + seeking() { + return false + } - seeking() { return false } seekable() { if (this.avPlayer.isLive) { try { - return videojs.createTimeRanges([this.avPlayer.getLivePeriod()]) - } catch { + return videojs.createTimeRanges([ this.avPlayer.getLivePeriod() ]) + } + catch { return videojs.createTimeRanges([]) } } const d = this.duration() - return videojs.createTimeRanges(d ? [[0, d]] : []) + return videojs.createTimeRanges(d ? [ [ 0, d ] ] : []) } - preload() { } - setPreload(preload) { } + preload() { + } - poster() { return null } - setPoster(poster) { } + setPreload(preload) { + } + + poster() { + return null + } + + setPoster(poster) { + } setSrc(source: IVokaSource) { const src = typeof source.src === 'undefined' ? '' : source.src const sourceType = typeof source.sourceType === 'undefined' ? '' : source.sourceType let parameters = source.tizenParams - log("[setSrc] source ", source) + log('[setSrc] source ', source) if (parameters == null) { parameters = {} as TizenSourceParams } - if (typeof parameters.isUHDSupported !== "boolean") { + if (typeof parameters.isUHDSupported !== 'boolean') { parameters.isUHDSupported = true } if (!parameters.platform) { @@ -237,11 +284,16 @@ class VokaTizenTech extends Tech { if (!this.is4KSupported) { if (supportedBandWidthCount === 0) { - log("No supported BandWidth", playListsAttributes) + log('No supported BandWidth', playListsAttributes) return } } + this.videoQualities = [] + this.selectedQuality = null + this.audioTracks = [] + this.selectedAudioTrackIndex = null + this.params = parameters const osVersionNumber = parseInt( (parameters.platform.osVersion || '') @@ -250,21 +302,21 @@ class VokaTizenTech extends Tech { .split('.')[0], 10) const playerUrl = parameters.stream.url - this.currentSrcUrl = playerUrl + this.sourceUrl = playerUrl const protection = parameters.protection const hasDRM = protection != null && protection.type == 'playready' - log("setSrc: protection, hasDRM", protection, hasDRM) + log('setSrc: protection, hasDRM', protection, hasDRM) this.avPlayer.open(playerUrl) /* - https://developer.samsung.com/smarttv/develop/guides/multimedia/media-playback/using-avplay.html#drm-contents-playback-sequence - */ + https://developer.samsung.com/smarttv/develop/guides/multimedia/media-playback/using-avplay.html#drm-contents-playback-sequence + */ if (hasDRM && protection != null) { const properties = { DeleteLicenseAfterUse: true, - LicenseServer: protection.licenseServer, + LicenseServer: protection.licenseServer } if (protection.httpHeader) { properties.HttpHeader = protection.httpHeader @@ -276,7 +328,9 @@ class VokaTizenTech extends Tech { try { window.addEventListener('resize', () => this.setFullScreenRect()) document.addEventListener('visibilitychange', () => this.setFullScreenRect()) - } catch {} + } + catch { + } if (this.avPlayer.setDisplayMethod) { try { this.avPlayer.setDisplayMethod('PLAYER_DISPLAY_MODE_AUTO_ASPECT_RATIO') @@ -289,99 +343,54 @@ class VokaTizenTech extends Tech { item?.RESOLUTION?.width && item?.RESOLUTION?.width > 3800 ))?.BANDWIDTH || null - log('osVersionNumber:', osVersionNumber); - log('hasUHDTracks:', hasUHDTracks); + log('osVersionNumber:', osVersionNumber) + log('hasUHDTracks:', hasUHDTracks) - // SET_MODE_4K Deprecated Snice 5 Version - if (osVersionNumber < 5 && hasUHDTracks) { - this.avPlayer.setStreamingProperty('SET_MODE_4K', 'TRUE') //for 4K contents - } - } - - const adaptiveInfo: string[] = [] - /*if (videoTrack && videoTrack !== 'auto') { - if (videoTrack?.attributes?.BANDWIDTH) { - // "450001", "830002", "1500003", "4000004", "6500005" - adaptiveInfo.push(`BITRATES=500~${videoTrack?.attributes?.BANDWIDTH}`); - adaptiveInfo.push(`STARTBITRATE=${videoTrack?.attributes?.BANDWIDTH}`); - adaptiveInfo.push(`SKIPBITRATE=HIGHEST`); - - // adaptiveInfo.push(`SKIPBITRATE=830002~6500005`); - // adaptiveInfo.push(`STARTBITRATE=${videoTrack?.attributes?.BANDWIDTH}`); - - this.avPlayer.setStreamingProperty( - 'ADAPTIVE_INFO', - `BITRATES=${videoTrack?.attributes?.BANDWIDTH}`, - ) - this.selectedVideoTrack = videoTrack - //handleTrackChanged(); - } - } else { - const minBandWidth = NumberUtils.minBy(playListsAttributes, (item) => item?.BANDWIDTH)?.BANDWIDTH || 0 - let maxBandWidth = NumberUtils.maxBy(playListsAttributes, (item) => item?.BANDWIDTH)?.BANDWIDTH || 0 - if (!this.is4KSupported) { - const firstUnsupportedBandWidth = playListsAttributes - .find((item) => ( - item?.RESOLUTION?.width && item?.RESOLUTION?.width > 1920 - ))?.BANDWIDTH || null; - - if (firstUnsupportedBandWidth) { - maxBandWidth = firstUnsupportedBandWidth - 1; + if (hasUHDTracks) { + // SET_MODE_4K Deprecated Snice 5 Version + if (osVersionNumber < 5 && hasUHDTracks) { + this.avPlayer.setStreamingProperty('SET_MODE_4K', 'TRUE') //for 4K contents + } else { + this.avPlayer.setStreamingProperty('ADAPTIVE_INFO', 'FIXED_MAX_RESOLUTION=3840x2160') } } - - adaptiveInfo.push(`BITRATES=${minBandWidth}~${maxBandWidth}`) - }*/ - - if (this.switchToThisTime) { - adaptiveInfo.push(`START_TIME=${this.switchToThisTime * 1000}`) - this.switchToThisTime = null } - if (adaptiveInfo.length > 0) { - this.avPlayer.setStreamingProperty('ADAPTIVE_INFO', adaptiveInfo.join('|')) - } - - this.avPlayer.setStreamingProperty('PREBUFFER_MODE', '3000') - this.avPlayer.setStreamingProperty('ADAPTIVE_INFO', 'FIXED_MAX_RESOLUTION=3840x2160') - - this.avPlayer.prepare().then(() => { - if (this.switchToThisTime != null) { - this.avPlayer.seekTo(this.switchToThisTime) - this.switchToThisTime = null - } else if (parameters.percentsWatched) { - this.avPlayer.seekTo(parameters.percentsWatched * this.duration() || 0) - } - }) + this.avPlayer.prepare() } + currentSrc() { - return this.currentSrcUrl || '' + return this.sourceUrl || '' } duration() { - log("duration() started") + // log('duration() started') try { if (this.avPlayer.isLive) { - log("duration Infinity") + // log('duration Infinity') return Infinity } - log("duration = ", this.avPlayer.duration) + // log('duration = ', this.avPlayer.duration) return this.avPlayer.duration - } catch (e) { - log.error("Cant get duration", e) + } + catch (e) { + log.error('Cant get duration', e) return Infinity } } setCurrentTime(seconds) { - log("[setCurrentTime] called with seconds", seconds) + log('[setCurrentTime] called with seconds', seconds) this.avPlayer.seekTo(seconds) } + currentTime() { return this.avPlayer.currentTime } - setControls(val) { } + setControls(val) { + } + controls() { return false } @@ -390,6 +399,7 @@ class VokaTizenTech extends Tech { log('setAutoplay():', autoplay) this.options_.autoplay = autoplay } + autoplay(): boolean | string | null { return this.options_.autoplay } @@ -397,79 +407,91 @@ class VokaTizenTech extends Tech { supportsFullScreen() { return false } - enterFullScreen() { } - exitFullScreen() { } + + enterFullScreen() { + } + + exitFullScreen() { + } private setFullScreenRect() { - const apply = (w:number,h:number) => { - try { this.avPlayer.setDisplayRect(0, 0, Math.round(w), Math.round(h)) } catch {} + const apply = (w: number, h: number) => { + try { + this.avPlayer.setDisplayRect(0, 0, Math.round(w), Math.round(h)) + } + catch { + } } try { // @ts-ignore tizen.systeminfo.getPropertyValue( 'DISPLAY', - (d:any)=>apply(d.resolutionWidth, d.resolutionHeight), - ()=>apply(window.screen.width, window.screen.height) + (d: any) => apply(d.resolutionWidth, d.resolutionHeight), + () => apply(window.screen.width, window.screen.height) ) - } catch { + } + catch { apply(window.screen.width, window.screen.height) } } setVolume(value: number) { - if (typeof value === 'number') { - try { - const tizenValue = Math.round(value * 100) - log.error("[setVolume] tizen volume value", tizenValue) - // @ts-ignore - tizen.tvaudiocontrol.getVolume(tizenValue) - } catch { - log.error("[setVolume] cant set volume", value) - } - } else { - log.error("[setVolume] invalid volume value", value) - } } - + volume() { - // @ts-ignore - const volume = tizen.tvaudiocontrol.getVolume() / 100 - log("volume() = ", volume) - // @ts-ignore - return tizen.tvaudiocontrol.getVolume() / 100 + try { + const volume = tizen.tvaudiocontrol.getVolume() / 100 + log('[volume] result ', volume) + return volume + } + catch { + return 1 + } } setMuted(muted) { - log("setMuted() ", muted) - // @ts-ignore - tizen.tvaudiocontrol.setMute(muted) } + muted() { - // @ts-ignore - const muted = tizen.tvaudiocontrol.isMute() - log("muted() = ", muted) - return muted + try { + const muted = tizen.tvaudiocontrol.isMute() + log('[muted] result ', muted) + return muted + } + catch { + return false + } } - paused() { return this.avPlayer.isPaused } + paused() { + return this.avPlayer.isPaused + } - ended() { return this.avPlayer.isCompleted } + ended() { + return this.avPlayer.isCompleted + } played() { - return [0, 0] + return [ 0, 0 ] } playbackRate() { return this.paused() ? 0.0 : 1.0 } - setPlaybackRate(value: number) { } - playsinline() { return false } - setPlaysinline(value) { } + setPlaybackRate(value: number) { + } + + playsinline() { + return false + } + + setPlaysinline(value) { + } networkState() { - return 3 - } // 3: NETWORK_NO_SOURCE + return 3 // 3: NETWORK_NO_SOURCE + } get elementTagName() { return 'object' @@ -482,18 +504,23 @@ class VokaTizenTech extends Tech { static get featuresVolumeControl() { return false } + static get featuresMuteControl() { return false } + get featuresPlaybackRate() { return false } + static get featuresSourceset() { return false } + static get featuresFullscreenResize() { return false } + get featuresProgressEvents() { return true } @@ -502,12 +529,155 @@ class VokaTizenTech extends Tech { return true } + private setVideoQualities() { + if (!this.bus) return + if (this.videoQualities.length != 0) return + this.videoQualities = this.buildVideoQualities() + if (this.videoQualities.length) { + this.bus.publish(VokaBusEvent.qualitiesParsed({ selected: -1, qualities: this.videoQualities })) + } + } + + private setAudioTracks() { + if (!this.bus) return + if (this.audioTracks.length != 0) return + this.audioTracks = this.buildAudioTracks() + if (this.audioTracks.length) { + this.bus.publish(VokaBusEvent.audioTracksParsed({ tracks: this.audioTracks })) + } + } + + private buildVideoQualities(): VokaCorePlayer.IQualityData[] { + const bitrates = this.avPlayer.getAvailableBitrates() || [] + log('[buildVideoQualities] bitrates', bitrates) + if ((!bitrates || bitrates.length === 0)) { + log.warn('Can`t get available bitrate', bitrates) + return [] + } + + return bitrates.reduce((acc, bitrate, index) => { + acc.push({ + index: index, + bitrate: bitrate, + quality: undefined, // QualityMapper.getQualityTypeByBitrate если будет нужно + width: 0, + height: 0, + label: `Video ${ bitrate }` + } as unknown as IAudioTrack) + return acc + }, []) + + } + + private buildAudioTracks(): AudioTrack[] { + const tracks = this.avPlayer.getTotalTrackInfo?.() ?? [] + return tracks.reduce((acc, track, index) => { + if (track.type !== 'AUDIO') return acc + const ext = safeJSON(track.extra_info) + acc.push({ + id: index, + label: ext?.language ? String(ext.language).toUpperCase() : `Audio ${ ext.index ?? ext.index }`, + language: ext?.language || '', + kind: 'AUDIO', + enabled: -1 // getCurrentTrack + } as unknown as IAudioTrack) + return acc + }, []) + } + + changeQuality(ev: { payload?: { index?: number } } | any) { + try { + this.changingTrack = true + let idx = ev?.payload?.index + + const wasPaused = this.paused() + const ct = this.avPlayer.currentTime + + let bitrate = 0 + if (idx > -1) { + const q = this.videoQualities[idx] || this._lastQuality + bitrate = q.bitrate + idx = q.index + } + + if (wasPaused) this.avPlayer.play() + this.avPlayer.stop(false) + + // Если нижний предел выставить в 0, то будет ABR от минимального до заданного качества + const btString = bitrate ? `${ bitrate - 1000 }~${ bitrate + 1000 }|STARTBITRATE=LOWEST` : '0' + this.avPlayer.setStreamingProperty('ADAPTIVE_INFO', `BITRATES=${ btString }`) + + if (this.selectedAudioTrackIndex) { + const unsubs = this.avPlayer.eventsEmitter.subscribe( + AVPlayHelper.Events.state as string, + event => { + const state = event.payload.state + switch (state) { + case AVPlayHelper.State.playing: + this.avPlayer.setSelectTrack('AUDIO', this.selectedAudioTrackIndex) + unsubs() + break + } + } + ) + } + + ;(async () => { + + await this.avPlayer.prepare() + + if (!wasPaused && this.avPlayer.isPaused) { + this.avPlayer.play() + } + if (ct) { + this.avPlayer.seekTo(ct) + } + this.changingTrack = false + // В ABR режиме можно при желании отслеживать внутренние переключения потоков + // [AVPlayHelper]: event type: PLAYER_MSG_BITRATE_CHANGE, data: BITRATE:8489356 + this.bus?.publish( + VokaBusEvent.qualityChange({ + bandwidth: bitrate, + bitrate, + height: 0, + isAuto: idx < 0, + label: '', + quality: undefined, // QualityMapper.getQualityByBitrate если понадобится + width: 0, + index: idx + }) + ) + })() + } + catch (e) { + this.changingTrack = false + log.error('[changeQuality] failed', e) + } + } + + changeAudioTrack(ev: { payload?: { audioTrackId?: number } } | any) { + const idx = ev?.payload?.audioTrackId + if (typeof idx !== 'number' || idx < 0) { + log('Invalid audio track index', idx) + return + } + try { + this.avPlayer.setSelectTrack('AUDIO', idx) + this.selectedAudioTrackIndex = idx + } + catch (e) { + log('Can`t select audio track', e) + } + } + get featuresNativeTextTracks() { return false } + static get featuresNativeVideoTracks() { return false } + static get featuresNativeAudioTracks() { return false } @@ -516,4 +686,4 @@ class VokaTizenTech extends Tech { VokaTizenTech.withSourceHandlers(VokaTizenTech) VokaTizenTech.registerSourceHandler(new VokaTizenSourceHandler()) VokaTizenTech.registerTech('VokaTizenTech', VokaTizenTech) -export default VokaTizenTech \ No newline at end of file +export default VokaTizenTech diff --git a/src/internal/player/native/webos/tech/VokaWebOSTech.ts b/src/internal/player/native/webos/tech/VokaWebOSTech.ts index 86f2327..c5e57ec 100755 --- a/src/internal/player/native/webos/tech/VokaWebOSTech.ts +++ b/src/internal/player/native/webos/tech/VokaWebOSTech.ts @@ -1,30 +1,56 @@ +import type { EventBus } from 'ts-bus' import videojs from 'video.js' -import VokaTech from '@/internal/player/native/VokaTech' -import VokaWebOSSourceHandler from '../sourcehandler/VokaWebOSSourceHandler' -import { IVokaSource } from '@/internal/player/native/VokaSourceHandler' +import type AudioTrack from 'video.js/dist/types/tracks/audio-track' + import VokaEvent from '@/constants/VokaEvent' +import VokaBusEvent from '@/internal/events/VokaBusEvent' +import type { ManifestVideoTrack } from '@/internal/player/native/PlayerLoadParameters' +import type { IVokaSource } from '@/internal/player/native/VokaSourceHandler' +import VokaTech from '@/internal/player/native/VokaTech' +import type VokaCorePlayer from '@/internal/player/VokaCorePlayer' +import { IS_WEBOS } from '@/internal/utils/browser' +import type { VokaOptions } from '@/public/@types' +import type { Quality } from '@/public/models/ILoadOptions' +import { QualityMapper } from '@/public/models/QualityMapper' + +import VokaWebOSSourceHandler from '../sourcehandler/VokaWebOSSourceHandler' + +type VokaManifestVideoTrack = VokaOptions.StreamOptions.ManifestVideoTrack const Tech = videojs.getTech('Tech') const Dom = videojs.dom -const log = videojs.log.createLogger("[VokaWebOSTech]") +const log = videojs.log.createLogger('[VokaWebOSTech]') class VokaWebOSTech extends Tech { static TECH_NAME = 'VokaWebOSTech' + private source: IVokaSource + private bus: EventBus private lastCurrentTime: number | null private playPromise: null + private selectedAudioTrackId: string | null = null + + private videoTracks: VokaManifestVideoTrack[] constructor(options, ready) { const opt = options || {} opt.techId = VokaWebOSTech.TECH_NAME + super(opt, ready) + this.name_ = VokaWebOSTech.TECH_NAME + + this.source = (opt.source || {}) as IVokaSource + + this.bus = this.source.bus this.playPromise = null this.lastCurrentTime = null - if (options.source) { - this.setSrc(options.source); + this.attachListeners() + + if (this.source) { + this.setSrc(this.source) } this.triggerReady() @@ -37,70 +63,64 @@ class VokaWebOSTech extends Tech { } createEl(): Element { - const player = this.player_; - let el = player ? player.el() : null; - const playerId = this.options_.playerId; + const player = this.player_ + let el = player ? player.el() : null + const playerId = this.options_.playerId if (el && el.tagName === this.elementTagName) { - const clone = el.cloneNode(true); + const clone = el.cloneNode(true) if (el.parentNode) { - el.parentNode.insertBefore(clone, el); + el.parentNode.insertBefore(clone, el) } - VokaTech.Tech.disposeMediaElement(el); - el = clone; + VokaTech.Tech.disposeMediaElement(el) + el = clone } else { - el = document.createElement(this.elementTagName); + el = document.createElement(this.elementTagName) Dom.setAttributes(el, { id: playerId, - class: "vjs-tech", - width: "100%", - height: "100%", - }); + class: 'vjs-tech', + width: '100%', + height: '100%' + }) // set native autoplay only if player option === true try { - const ap = this.player_?.autoplay?.(); + const ap = this.player_?.autoplay?.() if (ap === true) { - el.setAttribute("autoplay", ""); + el.setAttribute('autoplay', '') } else { - el.removeAttribute("autoplay"); + el.removeAttribute('autoplay') } + } catch { + log.error('cant change autoplay') } - catch { - log.error("cant change autoplay"); - } - } - el.playerId = playerId; - this.el_ = el; + el.playerId = playerId + this.el_ = el - return el; + return el } dispose() { + this.detachListeners() + if (this.el() && this.el().resetSourceset_) { this.el().resetSourceset_() } VokaTech.Tech.disposeMediaElement(this.el()) this.options_ = null - + super.dispose() } - error(error) { + error(_error) { return null } - load() { } + load() {} play() { - const lastTime = this.lastCurrentTime - if (lastTime != null) { - this.lastCurrentTime = null - this.setCurrentTime(lastTime) - } - this.playPromise = null const promise = this.el().play() @@ -129,22 +149,36 @@ class VokaWebOSTech extends Tech { } } - reset() { } + reset() { + } + loop() { return false } - setLoop(value) { } - seeking() { return !!this.el().seeking } - seekable() { return this.el().seekable || videojs.createTimeRanges([]) } + setLoop(_value) { + } - preload() { } - setPreload(preload) { } + seeking() { + return !!this.el().seeking + } + + seekable() { + return this.el().seekable || videojs.createTimeRanges([]) + } + + preload() { + } + + setPreload(_preload) { + } poster() { return null } - setPoster(poster) { } + + setPoster(_poster) { + } setSrc(source: IVokaSource) { const src = typeof source.src === 'undefined' ? '' : source.src @@ -156,63 +190,82 @@ class VokaWebOSTech extends Tech { if (sourceType !== '') { srcElement.setAttribute('type', sourceType) } + srcElement.addEventListener('error', (err) => console.error(err)) + + if (source.manifest.video) { + this.videoTracks = source.manifest.video + } - srcElement.addEventListener('error', err => console.error(err)) this.el().appendChild(srcElement) + this.selectedAudioTrackId = null + } + + currentSrc() { + return this.el().currentSrc || '' } - currentSrc() { return this.el().currentSrc || '' } duration() { return this.el().duration || 0 } setCurrentTime(seconds) { - log("[setCurrentTime] called with seconds", seconds) + log('[setCurrentTime] called with seconds', seconds) try { - if (this.el().currentTime !== undefined) { - this.play() - this.el().currentTime = seconds - } else { - log.error('[setCurrentTime] this.el().currentTime !== undefined') - } + this.el().currentTime = seconds } catch (e) { log.error('[setCurrentTime] Video is not ready', e) } } - currentTime() { + currentTime(): number { return this.el().currentTime || 0 } - setControls(val) { } + setControls(val) {} + controls() { return false } setAutoplay(autoplay) { - try { - if (autoplay === true) this.el().setAttribute('autoplay','') - else this.el().removeAttribute('autoplay'); - } catch { + try { + if (autoplay === true) this.el().setAttribute('autoplay', '') + else this.el().removeAttribute('autoplay') + } catch {} + } - } } autoplay() { - try { return this.player_?.autoplay?.() ?? false } catch { return false } } + try { + return this.player_?.autoplay?.() ?? false + } catch { + return false + } + } supportsFullScreen() { return false } - enterFullScreen() { } - exitFullScreen() { } - setVolume(percentAsDecimal) { if (typeof percentAsDecimal === 'number') this.el().volume = Math.max(0, Math.min(1, percentAsDecimal)); } - volume() { return this.el().volume } + enterFullScreen() {} - setMuted(muted) { this.el().muted = !!muted } - muted() { return !!this.el().muted } + exitFullScreen() {} - paused() { - return this.el().paused + setVolume(percentAsDecimal) { + } + + volume() { + return this.el().volume + } + + setMuted(muted) { + } + + muted() { + return !!this.el().muted + } + + paused(): boolean { + return this.el()?.paused as boolean } ended() { @@ -226,49 +279,232 @@ class VokaWebOSTech extends Tech { playbackRate() { return this.paused() ? 0.0 : 1.0 } - setPlaybackRate(value: number) { } + + setPlaybackRate(value: number) {} playsinline() { return true } - setPlaysinline(value) { } + + setPlaysinline(value) {} networkState() { return 3 } + private selectedVideoTrack: ManifestVideoTrack | null = null + + private changeAudioTrackHandler = (event: ReturnType) => + this.changeAudioTrack(event) + private changeQualityHandler = (event: ReturnType) => this.changeQuality(event) + private detachHandlers: Funtion[] = [] + private emitVideoQualitiesHandler = () => this.emitVideoQualities() + private emitAudioTracksHandler = () => this.emitAudioTracks() + + private attachListeners() { + if (this.el()) { + this.on('loadedmetadata', this.emitVideoQualitiesHandler) + this.on('loadedmetadata', this.emitAudioTracksHandler) + } + + this.detachHandlers.push(this.bus.subscribe(VokaBusEvent.audioTracksSet, this.changeAudioTrackHandler)) + this.detachHandlers.push(this.bus.subscribe(VokaBusEvent.qualitySet, this.changeQualityHandler)) + } + + private detachListeners() { + if (this.el()) { + this.off(this.el(), 'loadedmetadata', this.emitVideoQualitiesHandler) + this.off(this.el(), 'loadedmetadata', this.emitAudioTracksHandler) + } + this.detachHandlers.forEach((detachHandler) => { + detachHandler() + }) + } + + private emitVideoQualities() { + const videoQualities = this.getVideoQualities() + if (videoQualities.length > 0) { + this.bus.publish( + VokaBusEvent.qualitiesParsed({ + selected: -1, + qualities: videoQualities + }) + ) + } + } + + private emitAudioTracks() { + const audioTracks = this.getAudioTracks() + if (audioTracks.length > 0) { + this.bus.publish(VokaBusEvent.audioTracksParsed({ tracks: audioTracks })) + } + } + + private changeAudioTrack(event: ReturnType) { + const video = this.el() + const at = video?.audioTracks + const id = event.payload.audioTrackId + if (!at || at.length <= 1) { + log.warn('[changeAudioTrack] cant change audio track', at) + return + } + try { + Array.from(at).forEach((t) => { + t.enabled = t.id === id + if (t.enabled) this.selectedAudioTrackId = id + }) + } + catch (e) { + log.error('[changeAudioTrack] cant change audio track', e) + } + } + + private changeQuality(event: ReturnType) { + try { + log('[changeQuality] payload', event.payload) + const el = this.el() as HTMLMediaElement + if (!el) { + log('[changeQuality] video element doesnt ready', el) + return + } + if (!this.hasVideoTracks()) { + log('[changeQuality] there are not tracks to change', this.videoTracks) + return + } + + let index = event.payload.index + const track = this.videoTracks[index] + let url = track?.url + + if (!url) { + index = -1 + url = this.source.src + } + + let paused = this.paused() + if (!paused) { + this.pause() + } + let ct: number | null = this.currentTime() + + const resumeContext = () => { + if (!paused) { + paused = true + this.play() + } + } + this.one(el, [ 'canplay', 'canplaythrough' ], () => resumeContext) + // watchdog на случай, если события потерялись + setTimeout(() => resumeContext(), 3000) + this.one(el, 'loadedmetadata', () => { + if (this.selectedAudioTrackId) { + Array.from(this.el()?.audioTracks).forEach((t) => { + t.enabled = t.id === this.selectedAudioTrackId + }) + } + }) + + el.src = url + if (ct !== null && this.duration() !== Infinity) { + this.setCurrentTime(ct) + ct = null + } + + this.bus.publish( + VokaBusEvent.qualityChange({ + bandwidth: track?.brandWidth ?? 0, + bitrate: track?.brandWidth ?? 0, + height: track?.height ?? 0, + index: index, + isAuto: index < 0, + label: track?.name ?? track?.brandWidth ?? `Video#${ index }`, + quality: QualityMapper.getQualityTypeByResolution(track?.width, track?.height) as unknown as Quality, + width: track?.width + }) + ) + } catch (e) { + log('[changeQuality] error', e) + } + } + + private getAudioTracks(): AudioTrack[] { + if (this.el()) { + return Array.from(this.el().audioTracks) + } else { + log('[getAudioTracks] video element is not ready') + } + return [] as AudioTrack[] + } + + getVideoQualities() { + const videoQualities: VokaCorePlayer.IQualityData[] = [] + try { + if (this.hasVideoTracks()) { + this.videoTracks.map((video, index) => { + const w = video.width || 0 + const h = video.height || 0 + const q = QualityMapper.getQualityTypeByResolution(w, h) + videoQualities.push({ + index, + bitrate: video.brandWidth || 0, + quality: q as unknown as Quality, + width: w, + height: h, + label: video.name + } as VokaCorePlayer.IQualityData) + }) + } + } catch (e) { + log('[getVideoTracks] can`t get video tracks', e) + } + + return videoQualities + } + + private hasVideoTracks() { + return (this.videoTracks || []).length > 0 + } + static isSupported() { - const userAgent = window.navigator.userAgent.toLowerCase() - return userAgent.includes("web0s") || userAgent.includes("webos") + return IS_WEBOS } static get featuresVolumeControl() { return false } + static get featuresMuteControl() { return false } + get featuresPlaybackRate() { return false } + static get featuresSourceset() { return true } + static get featuresFullscreenResize() { return false } + get featuresProgressEvents() { return true } + get featuresTimeupdateEvents() { return true } + get featuresNativeTextTracks() { return false } + static get featuresNativeVideoTracks() { return false } + static get featuresNativeAudioTracks() { return false } @@ -278,148 +514,3 @@ VokaWebOSTech.withSourceHandlers(VokaWebOSTech) VokaWebOSTech.registerSourceHandler(new VokaWebOSSourceHandler()) VokaWebOSTech.registerTech('VokaWebOSTech', VokaWebOSTech) export default VokaWebOSTech - -/* -const webosPlayer = ({ videoContainer, event }: Props): IPlayer => { - let selectedVideoTrack: ManifestVideoTrack | null = null; - let params: PlayerLoadParameters | null = null; - let switchToThisTime: number | null = null; - let videoElement: HTMLVideoElement | null = null; - - const isTracksFromManifest = () => ((params?.manifest?.video || []).length > 0); - const handleTrackChanged = () => { - event.emit(Events.TRACK_CHANGED); - }; - -const on: PlayerEventSubscribe = (target, listener) => event.on(target, listener); - -const removeListener: PlayerEventSubscribe = (target, listener) => { - event.removeListener(target, listener); -}; - -const stop = () => { - reset(); -}; - -const changeAudioTrack = (audioTrack: AudioTrack) => { - if (videoElement?.audioTracks) { - try { - for (var index = 0; index < videoElement?.audioTracks.length; index += 1) { - videoElement.audioTracks[index].enabled = (index === audioTrack.index); - } - handleTrackChanged(); - } catch (ignore) { - // ignore - } - } -}; -const changeVideoTrack = (videoTrack: VideoTrack) => { - try { - if (isTracksFromManifest()) { - switchToThisTime = getCurrentTime(); - - pause(); - - selectedVideoTrack = videoTrack.data as ManifestVideoTrack; - - // @ts-ignore - videoElement?.src = selectedVideoTrack.url; - - seekTo(switchToThisTime); - switchToThisTime = null; - } - if (videoElement?.videoTracks) { - for (var index = 0; index < videoElement?.videoTracks.length; index += 1) { - videoElement.videoTracks[index].selected = (index === videoTrack.index); - } - } - handleTrackChanged(); - } catch (ignore) { - // ignore - } -}; -const getAudioTracks = () => { - const audioTracks: AudioTrack[] = []; - - if (videoElement?.audioTracks) { - try { - for (var index = 0; index < videoElement?.videoTracks.length; index += 1) { - audioTracks.push({ - index, - isSelected: videoElement.audioTracks[index].enabled, - name: bringingAudioTrackNameToStandard( - videoElement.audioTracks[index].language - || videoElement.audioTracks[index].label - ), - }); - } - } catch (ignore) { - // ignore - } - } - - return audioTracks; -}; -const getVideoTracks = () => { - const videoTracks: VideoTrack[] = []; - - try { - if (isTracksFromManifest()) { - (params?.manifest?.video || []).map((video, index) => { - videoTracks.push({ - index, - isSelected: (selectedVideoTrack?.url === video.url), - name: video.name, - data: video, - }); - }) - } else if (videoElement?.videoTracks) { - for (let index = 0; index < videoElement?.videoTracks.length; index += 1) { - const name = ( - videoElement.videoTracks[index].language || videoElement.videoTracks[index].label - ).replace('`', ''); - videoTracks.push({ - index, - isSelected: videoElement.videoTracks[index].selected, - name, - }); - } - } - } catch (ignore) { - // ignore - } - - return sortVideoTracks(videoTracks); -}; -const getSelectedAudioTrack = () => { - find(getAudioTracks(), { isSelected: true }); -}; -const getSelectedVideoTrack = () => { - find(getVideoTracks(), { isSelected: true }); -}; - -const getProperties = () => { - if (!videoElement) { return null; } - - return { - videoPlaybackQuality: videoElement?.getVideoPlaybackQuality ? - videoElement.getVideoPlaybackQuality() : null, - }; -}; - -return { - load, - on, - removeListener, - reset, - stop, - changeAudioTrack, - changeVideoTrack, - getAudioTracks, - getVideoTracks, - getSelectedAudioTrack, - getSelectedVideoTrack, - getProperties, -}; -}; -* */ \ No newline at end of file diff --git a/src/internal/utils/AutoplayChecker.ts b/src/internal/utils/AutoplayChecker.ts index 6c32d14..84afc8f 100644 --- a/src/internal/utils/AutoplayChecker.ts +++ b/src/internal/utils/AutoplayChecker.ts @@ -1,4 +1,4 @@ -import { IS_SMART_TV } from "@/internal/utils/browser" +import { IS_SMART_TV } from '@/internal/utils/browser' type AutoplayCheckerCallback = (supported: boolean) => void class AutoplayChecker { @@ -13,40 +13,41 @@ class AutoplayChecker { * @param callback Callback for receive check result. */ static isAutoplaySupported(callback: AutoplayCheckerCallback) { - if (typeof callback !== 'function') { - console.warn('isAutoplaySupported: Callback must be a function.') - return + if (typeof callback !== 'function') { + console.warn('isAutoplaySupported: Callback must be a function.') + return + } + + if (IS_SMART_TV) { + callback(true) + return + } + + const storageValue = AutoplayChecker.sessionStorageRead() + if (storageValue != null) { + callback(storageValue) + return + } + + let video = null + try { + // Create video element to test autoplay + video = AutoplayChecker.addVideoElement() + if (typeof video.load === 'function') { + video.load() } - if (IS_SMART_TV) { - callback(true) - return - } - - const storageValue = AutoplayChecker.sessionStorageRead() - if (storageValue != null) { - callback(storageValue) - return - } - - let video = null - try { - // Create video element to test autoplay - video = AutoplayChecker.addVideoElement() - if (typeof video.load === 'function') { - video.load() - } - - const promise = video.play() - if (!AutoplayChecker.tryPromise(promise, video, callback)) { - AutoplayChecker.tryOnplay(video, callback) - } - } catch (e) { - // On old browsers we can catch surprises and magic. - AutoplayChecker.removeVideoElement(video) - AutoplayChecker.sessionStoreSave('false') - callback(false) + const promise = video.play() + if (!AutoplayChecker.tryPromise(promise, video, callback)) { + AutoplayChecker.tryOnplay(video, callback) } + } + catch (e) { + // On old browsers we can catch surprises and magic. + AutoplayChecker.removeVideoElement(video) + AutoplayChecker.sessionStoreSave('false') + callback(false) + } } // Save and read values from session @@ -131,19 +132,19 @@ class AutoplayChecker { video: HTMLVideoElement, callback: AutoplayCheckerCallback ): boolean { - if (!promise) { - return false - } - // Без промиса мы не знаем стартовали ли фактически или нет. - promise - .then(() => { - AutoplayChecker.sessionStoreSave('true') - AutoplayChecker.removeVideoElement(video) - callback(true) - }) - .catch(() => { - AutoplayChecker.sessionStoreSave('false') - AutoplayChecker.removeVideoElement(video) + if (promise == null) { + return false + } + // Без промиса мы не знаем стартовали ли фактически или нет. + promise + .then(() => { + AutoplayChecker.sessionStoreSave('true') + AutoplayChecker.removeVideoElement(video) + callback(true) + }) + .catch(() => { + AutoplayChecker.sessionStoreSave('false') + AutoplayChecker.removeVideoElement(video) callback(false) }) return true @@ -172,4 +173,4 @@ class AutoplayChecker { } } -export { AutoplayChecker, AutoplayCheckerCallback } \ No newline at end of file +export { AutoplayChecker, AutoplayCheckerCallback } diff --git a/src/internal/utils/browser.ts b/src/internal/utils/browser.ts index 1b9a32b..ce5e97c 100644 --- a/src/internal/utils/browser.ts +++ b/src/internal/utils/browser.ts @@ -2,8 +2,9 @@ * @file browser.js * @module browser */ -import * as Dom from './dom'; -import window from 'global/window'; +import window from 'global/window' + +import * as Dom from './dom' /** * Whether or not this device is an iPod. @@ -11,7 +12,7 @@ import window from 'global/window'; * @static * @type {Boolean} */ -export let IS_IPOD = false; +export let IS_IPOD = false /** * The detected iOS version - or `null`. @@ -19,7 +20,7 @@ export let IS_IPOD = false; * @static * @type {string|null} */ -export let IOS_VERSION = null; +export let IOS_VERSION = null /** * Whether or not this is an Android device. @@ -27,7 +28,7 @@ export let IOS_VERSION = null; * @static * @type {Boolean} */ -export let IS_ANDROID = false; +export let IS_ANDROID = false /** * The detected Android version - or `null` if not Android or indeterminable. @@ -35,7 +36,7 @@ export let IS_ANDROID = false; * @static * @type {number|string|null} */ -export let ANDROID_VERSION; +export let ANDROID_VERSION /** * Whether or not this is Mozilla Firefox. @@ -43,7 +44,7 @@ export let ANDROID_VERSION; * @static * @type {Boolean} */ -export let IS_FIREFOX = false; +export let IS_FIREFOX = false /** * Whether or not this is Microsoft Edge. @@ -51,7 +52,7 @@ export let IS_FIREFOX = false; * @static * @type {Boolean} */ -export let IS_EDGE = false; +export let IS_EDGE = false /** * Whether or not this is any Chromium Browser @@ -59,7 +60,7 @@ export let IS_EDGE = false; * @static * @type {Boolean} */ -export let IS_CHROMIUM = false; +export let IS_CHROMIUM = false /** * Whether or not this is any Chromium browser that is not Edge. @@ -75,7 +76,7 @@ export let IS_CHROMIUM = false; * @deprecated * @type {Boolean} */ -export let IS_CHROME = false; +export let IS_CHROME = false /** * The detected Chromium version - or `null`. @@ -83,7 +84,7 @@ export let IS_CHROME = false; * @static * @type {number|null} */ -export let CHROMIUM_VERSION = null; +export let CHROMIUM_VERSION = null /** * The detected Google Chrome version - or `null`. @@ -94,7 +95,7 @@ export let CHROMIUM_VERSION = null; * @deprecated * @type {number|null} */ -export let CHROME_VERSION = null; +export let CHROME_VERSION = null /** * Whether or not this is a Chromecast receiver application. @@ -102,7 +103,9 @@ export let CHROME_VERSION = null; * @static * @type {Boolean} */ -export const IS_CHROMECAST_RECEIVER = Boolean(window.cast && window.cast.framework && window.cast.framework.CastReceiverContext); +export const IS_CHROMECAST_RECEIVER = Boolean( + window.cast && window.cast.framework && window.cast.framework.CastReceiverContext +) /** * The detected Internet Explorer version - or `null`. @@ -111,7 +114,7 @@ export const IS_CHROMECAST_RECEIVER = Boolean(window.cast && window.cast.framewo * @deprecated * @type {number|null} */ -export let IE_VERSION = null; +export let IE_VERSION = null /** * Whether or not this is desktop Safari. @@ -119,7 +122,7 @@ export let IE_VERSION = null; * @static * @type {Boolean} */ -export let IS_SAFARI = false; +export let IS_SAFARI = false /** * Whether or not this is a Windows machine. @@ -127,7 +130,7 @@ export let IS_SAFARI = false; * @static * @type {Boolean} */ -export let IS_WINDOWS = false; +export let IS_WINDOWS = false /** * Whether or not this device is an iPad. @@ -135,7 +138,7 @@ export let IS_WINDOWS = false; * @static * @type {Boolean} */ -export let IS_IPAD = false; +export let IS_IPAD = false /** * Whether or not this device is an iPhone. @@ -143,10 +146,10 @@ export let IS_IPAD = false; * @static * @type {Boolean} */ -// The Facebook app's UIWebView identifies as both an iPhone and iPad, so -// to identify iPhones, we need to exclude iPads. -// http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/ -export let IS_IPHONE = false; + // The Facebook app's UIWebView identifies as both an iPhone and iPad, so + // to identify iPhones, we need to exclude iPads. + // http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/ +export let IS_IPHONE = false /** * Whether or not this is a Tizen device. @@ -154,7 +157,7 @@ export let IS_IPHONE = false; * @static * @type {Boolean} */ -export let IS_TIZEN = false; +export let IS_TIZEN = false /** * Whether or not this is a WebOS device. @@ -162,7 +165,7 @@ export let IS_TIZEN = false; * @static * @type {Boolean} */ -export let IS_WEBOS = false; +export let IS_WEBOS = false /** * Whether or not this is a Smart TV (Tizen or WebOS) device. @@ -170,7 +173,7 @@ export let IS_WEBOS = false; * @static * @type {Boolean} */ -export let IS_SMART_TV = false; +export let IS_SMART_TV = false /** * Whether or not this device is touch-enabled. @@ -179,108 +182,109 @@ export let IS_SMART_TV = false; * @const * @type {Boolean} */ -export const TOUCH_ENABLED = Boolean(Dom.isReal() && ( - 'ontouchstart' in window || - window.navigator.maxTouchPoints || - window.DocumentTouch && window.document instanceof window.DocumentTouch)); +export const TOUCH_ENABLED = Boolean( + Dom.isReal() && + ('ontouchstart' in window || + window.navigator.maxTouchPoints || + (window.DocumentTouch && window.document instanceof window.DocumentTouch)) +) -const UAD = window.navigator && window.navigator.userAgentData; +const UAD = window.navigator && window.navigator.userAgentData if (UAD && UAD.platform && UAD.brands) { // If userAgentData is present, use it instead of userAgent to avoid warnings // Currently only implemented on Chromium // userAgentData does not expose Android version, so ANDROID_VERSION remains `null` - IS_ANDROID = UAD.platform === 'Android'; - IS_EDGE = Boolean(UAD.brands.find(b => b.brand === 'Microsoft Edge')); - IS_CHROMIUM = Boolean(UAD.brands.find(b => b.brand === 'Chromium')); - IS_CHROME = !IS_EDGE && IS_CHROMIUM; - CHROMIUM_VERSION = CHROME_VERSION = (UAD.brands.find(b => b.brand === 'Chromium') || {}).version || null; - IS_WINDOWS = UAD.platform === 'Windows'; + IS_ANDROID = UAD.platform === 'Android' + IS_EDGE = Boolean(UAD.brands.find((b) => b.brand === 'Microsoft Edge')) + IS_CHROMIUM = Boolean(UAD.brands.find((b) => b.brand === 'Chromium')) + IS_CHROME = !IS_EDGE && IS_CHROMIUM + CHROMIUM_VERSION = CHROME_VERSION = (UAD.brands.find((b) => b.brand === 'Chromium') || {}).version || null + IS_WINDOWS = UAD.platform === 'Windows' } // If the browser is not Chromium, either userAgentData is not present which could be an old Chromium browser, // or it's a browser that has added userAgentData since that we don't have tests for yet. In either case, // the checks need to be made agiainst the regular userAgent string. if (!IS_CHROMIUM) { - const USER_AGENT = window.navigator && window.navigator.userAgent || ''; + const USER_AGENT = (window.navigator && window.navigator.userAgent) || '' - IS_IPOD = (/iPod/i).test(USER_AGENT); + IS_IPOD = /iPod/i.test(USER_AGENT) IOS_VERSION = (function() { - const match = USER_AGENT.match(/OS (\d+)_/i); + const match = USER_AGENT.match(/OS (\d+)_/i) if (match && match[1]) { - return match[1]; + return match[1] } - return null; - }()); + return null + })() - IS_ANDROID = (/Android/i).test(USER_AGENT); + IS_ANDROID = /Android/i.test(USER_AGENT) ANDROID_VERSION = (function() { // This matches Android Major.Minor.Patch versions // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned - const match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i); + const match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i) if (!match) { - return null; + return null } - const major = match[1] && parseFloat(match[1]); - const minor = match[2] && parseFloat(match[2]); + const major = match[1] && parseFloat(match[1]) + const minor = match[2] && parseFloat(match[2]) if (major && minor) { - return parseFloat(match[1] + '.' + match[2]); + return parseFloat(match[1] + '.' + match[2]) } else if (major) { - return major; + return major } - return null; - }()); + return null + })() - IS_FIREFOX = (/Firefox/i).test(USER_AGENT); + IS_FIREFOX = /Firefox/i.test(USER_AGENT) - IS_EDGE = (/Edg/i).test(USER_AGENT); + IS_EDGE = /Edg/i.test(USER_AGENT) - IS_CHROMIUM = ((/Chrome/i).test(USER_AGENT) || (/CriOS/i).test(USER_AGENT)); + IS_CHROMIUM = /Chrome/i.test(USER_AGENT) || /CriOS/i.test(USER_AGENT) - IS_CHROME = !IS_EDGE && IS_CHROMIUM; + IS_CHROME = !IS_EDGE && IS_CHROMIUM CHROMIUM_VERSION = CHROME_VERSION = (function() { - const match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/); + const match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/) if (match && match[2]) { - return parseFloat(match[2]); + return parseFloat(match[2]) } - return null; - }()); + return null + })() IE_VERSION = (function() { - const result = (/MSIE\s(\d+)\.\d/).exec(USER_AGENT); - let version = result && parseFloat(result[1]); + const result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT) + let version = result && parseFloat(result[1]) - if (!version && (/Trident\/7.0/i).test(USER_AGENT) && (/rv:11.0/).test(USER_AGENT)) { + if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) { // IE 11 has a different user agent string than other IE versions - version = 11.0; + version = 11.0 } - return version; - }()); + return version + })() - IS_TIZEN = (/Tizen/i).test(USER_AGENT); + IS_TIZEN = /Tizen/i.test(USER_AGENT) - IS_WEBOS = (/Web0S/i).test(USER_AGENT); + IS_WEBOS = /Web0S|WebOS|LG Browser/i.test(USER_AGENT) - IS_SMART_TV = IS_TIZEN || IS_WEBOS; + IS_SMART_TV = IS_TIZEN || IS_WEBOS - IS_SAFARI = (/Safari/i).test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE && !IS_SMART_TV; + IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE && !IS_SMART_TV - IS_WINDOWS = (/Windows/i).test(USER_AGENT); + IS_WINDOWS = /Windows/i.test(USER_AGENT) - IS_IPAD = (/iPad/i).test(USER_AGENT) || - (IS_SAFARI && TOUCH_ENABLED && !(/iPhone/i).test(USER_AGENT)); + IS_IPAD = /iPad/i.test(USER_AGENT) || (IS_SAFARI && TOUCH_ENABLED && !/iPhone/i.test(USER_AGENT)) - IS_IPHONE = (/iPhone/i).test(USER_AGENT) && !IS_IPAD; + IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD } /** @@ -290,7 +294,7 @@ if (!IS_CHROMIUM) { * @const * @type {Boolean} */ -export const IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD; +export const IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD /** * Whether or not this is any flavor of Safari - including iOS. @@ -299,4 +303,4 @@ export const IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD; * @const * @type {Boolean} */ -export const IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME; +export const IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME diff --git a/src/plugins/VokaMetricsPlugin.ts b/src/plugins/VokaMetricsPlugin.ts index 9df0ce6..133e080 100644 --- a/src/plugins/VokaMetricsPlugin.ts +++ b/src/plugins/VokaMetricsPlugin.ts @@ -1,13 +1,15 @@ -import Player from 'video.js/dist/types/player' -import VokaBusEvent from '@/internal/events/VokaBusEvent' -import GUIDUtils from '@/internal/utils/GUIDUtils' -import TimerUtils from '@/internal/utils/TimerUtils' -import BrowserUtils from '@/internal/utils/BrowserUtils' -import { IHTTPClient, HTTPClient } from '@/internal/utils/HTTPClient' -import DateUtils from '@/internal/utils/DateUtils' -import VokaCorePlayer from '@/internal/player/VokaCorePlayer' +import type { EventBus } from 'ts-bus' import videojs from 'video.js' -import { EventBus } from 'ts-bus' +import type Player from 'video.js/dist/types/player' + +import VokaBusEvent from '@/internal/events/VokaBusEvent' +import type VokaCorePlayer from '@/internal/player/VokaCorePlayer' +import BrowserUtils from '@/internal/utils/BrowserUtils' +import DateUtils from '@/internal/utils/DateUtils' +import GUIDUtils from '@/internal/utils/GUIDUtils' +import type { IHTTPClient } from '@/internal/utils/HTTPClient' +import { HTTPClient } from '@/internal/utils/HTTPClient' +import TimerUtils from '@/internal/utils/TimerUtils' namespace VokaMetricsPlugin { @@ -27,8 +29,8 @@ namespace VokaMetricsPlugin { export class Plugin extends VideoPlugin { private player: Player - private playbackInitialized: Boolean - private playbackStarted: Boolean + private playbackInitialized: boolean + private playbackStarted: boolean private prevPlayerState: string private prevStateTime: number private metrics: VokaCorePlayer.IMetrics | null @@ -206,14 +208,6 @@ namespace VokaMetricsPlugin { break } - // TODO! - let bandwidth = NaN//player.getNetworkBandwidth(); - if (isNaN(bandwidth)) { - bandwidth = 0 - } else { - bandwidth *= 1000 - } - const data = { application_id: params.application_id, application_version: params['application_version'], @@ -227,21 +221,12 @@ namespace VokaMetricsPlugin { watch_session_id: this.getWatchSessionId(), resource_uid: params['resource_uid'], resource_type: params.resource_type, - // TODO! - // buffered_duration: Math.floor(player.getBufferLength() * 1000), - bandwidth: Math.floor(bandwidth), + buffered_duration: 0, + bandwidth: 0, player_state: playerState, time_spent: this.timeSpent, network_type: 'unknown' - }; - - /*const request = this.httpClient.request( - HTTPMethod.POST, - this.getApiUrl(), - null, - data, - { timeout: timeout, withCredentials: true } - )*/ + } this.timeSpent = null } @@ -267,11 +252,20 @@ namespace VokaMetricsPlugin { } private getPlayerState(): string { - if (!this.playbackInitialized) { return 'initial_buffering' } - if (!this.playbackStarted) { return 'paused' } - if (this.player.paused()) { return 'paused' } - // TODO! - // if (player.getBufferingState()) { return 'freezed' } + try { + if (!this.playbackInitialized) { + return 'initial_buffering' + } + if (!this.playbackStarted) { + return 'paused' + } + if (this.player?.paused()) { + return 'paused' + } + // if (player.getBufferingState()) { return 'freezed' } + } + catch { + } return 'playing' } @@ -323,4 +317,4 @@ namespace VokaMetricsPlugin { } videojs.registerPlugin('vokaMetricsPlugin', VokaMetricsPlugin.Plugin) -export default VokaMetricsPlugin \ No newline at end of file +export default VokaMetricsPlugin diff --git a/src/public/@types.ts b/src/public/@types.ts index e768e59..17896c2 100644 --- a/src/public/@types.ts +++ b/src/public/@types.ts @@ -1,213 +1,272 @@ -import { ILogger } from '@/public/logger/ILogger' -import { LogLevel } from '@/public/logger/Logger' -import { IHTTPClient } from '@/internal/utils/HTTPClient' +import type { IHTTPClient } from '@/internal/utils/HTTPClient' +import type { Quality } from '@/public/models/ILoadOptions' interface IOSVersion { - name: string - version: string | null + name: string + version: string | null } interface IContent { - title: string | null - description: string | null - url: string - adConfig: string | null - heartbeat: IHeartbeat | null - metrics: IMetrics | null - //statistics?: StatisticsUnit[] - //subtitles?: SubtitlesItem[] + title: string | null + description: string | null + url: string + adConfig: string | null + heartbeat: IHeartbeat | null + metrics: IMetrics | null + //statistics?: StatisticsUnit[] + //subtitles?: SubtitlesItem[] } type IContextUpdated = { - quality: Quality | null - expectedQuality: Quality | null - volume: number | null - isAutoQuality: boolean | null - isLive: boolean | null - loop: boolean | null - enableCaptions: boolean | null + quality: Quality | null + expectedQuality: Quality | null + volume: number | null + isAutoQuality: boolean | null + isLive: boolean | null + loop: boolean | null + enableCaptions: boolean | null } interface IHeartbeat { - url: string - version: number - interval: number + url: string + version: number + interval: number } interface IMetricsParams { - watch_session_id: string | null - application_id: string | null - application_version: string | null - user_type: string | null - user_id: string | null - resource_uid: string | null - resource_type: string | null + watch_session_id: string | null + application_id: string | null + application_version: string | null + user_type: string | null + user_id: string | null + resource_uid: string | null + resource_type: string | null } interface IMetrics { - url: string - params: IMetricsParams | null - interval: number + url: string + params: IMetricsParams | null + interval: number } interface ICaption { - id: number - url: string - language: string - title: string - type: string - load(httpClient: IHTTPClient): Promise + id: number + url: string + language: string + title: string + type: string + + load(httpClient: IHTTPClient): Promise } // Options that passed to player on initialization enum VokaErrorMessages { - DocumentUnavailable = "DocumentUnavailable", - IFrameElement = "IFrameElementNotAllowed", + DocumentUnavailable = 'DocumentUnavailable', + IFrameElement = 'IFrameElementNotAllowed' } namespace VokaOptions { - - namespace UIControls { - export interface IControls { - externalSubtitles: ISubtitle - zoomButton: IZoomButton - editing: IEditing - } - - export interface IZoomButton { - enable: boolean - isVisible: boolean - } - - export interface IEditing { - enable: boolean - } - - export interface ISubtitle { - url: string | null - lang: string - } - + namespace UIControls { + export interface IControls { + externalSubtitles: ISubtitle + zoomButton: IZoomButton + editing: IEditing } - export interface IOptions { - features: IFeatures - apiConfig: IAPIConfig - uiConfig: IUIConfig | null - globalOpts: IGlobalOptions | null - streamOpts: StreamOptions.IStream | null - codecs: ICodecs - tweaks: ITweaks - controls: UIControls.IControls - log?: Boolean - loggerId?: Number + export interface IZoomButton { + enable: boolean + isVisible: boolean } - export interface ICodecs { - h264: boolean - h265: boolean - vp9: boolean - av1: boolean + export interface IEditing { + enable: boolean } - export interface ITweaks { - forceNative: string | null - forceHls: boolean - resumeReloadsLive: boolean + export interface ISubtitle { + url: string | null + lang: string + } + } + + export interface IOptions { + features: IFeatures + apiConfig: IAPIConfig + uiConfig: IUIConfig | null + globalOpts: IGlobalOptions | null + streamOpts: StreamOptions.IStream | null + codecs: ICodecs + tweaks: ITweaks + controls: UIControls.IControls + log?: boolean + loggerId?: number + } + + export interface ICodecs { + h264: boolean + h265: boolean + vp9: boolean + av1: boolean + } + + export interface ITweaks { + forceNative: string | null + forceHls: boolean + resumeReloadsLive: boolean + } + + export interface IFeatures { + api: boolean + drm: boolean + ads: boolean + heartbeat: boolean + metrics: boolean + } + + export interface IAPIConfig { + channelId: string | null + clientId: string | null + urlGetParams: string | null + movieId: string | null + episodeId: string | null + newsId: string | null + apiHost: string | null + } + + export interface IUIConfig { + initAsLive: boolean + focusAtInit: boolean + autoshowToolbox: boolean + emptyPoster: boolean + waitingPoster: boolean + hideControls: boolean + } + + export interface IGlobalOptions { + uiLanguage: string + } + + export namespace StreamOptions { + export interface IDRMWideVine { + proxy_url: string | null } - export interface IFeatures { - api: boolean - drm: boolean - ads: boolean - heartbeat: boolean - metrics: boolean + export interface IDRMPlayReady { + la_url: string | null } - export interface IAPIConfig { - channelId: string | null - clientId: string | null - urlGetParams: string | null - movieId: string | null - episodeId: string | null - newsId: string | null - apiHost: string | null + export interface IDRMFairplay { + certificate_url: string + ksm_url: string + ksm_protocol: string + add_no_cache_headers: boolean } - export interface IUIConfig { - initAsLive: boolean - focusAtInit: boolean, - autoshowToolbox: boolean - emptyPoster: boolean - waitingPoster: boolean - hideControls: boolean + export interface IDRMConfig { + widevine: IDRMWideVine | null + fairplay: IDRMFairplay | null + playready: IDRMPlayReady | null } - export interface IGlobalOptions { - uiLanguage: string + export interface IHeartbeat { + url: string | null + interval: number + version: number } - export namespace StreamOptions { - - export interface IDRMWideVine { - proxy_url: string | null - } - - export interface IDRMPlayReady { - la_url: string | null - } - - export interface IDRMFairplay { - certificate_url: string, - ksm_url: string, - ksm_protocol: string, - add_no_cache_headers: boolean - } - - export interface IDRMConfig { - widevine: IDRMWideVine | null - fairplay: IDRMFairplay | null - playready: IDRMPlayReady | null - } - - export interface IHeartbeat { - url: string | null - interval: number - version: number - } - - export interface IMetricsParams { - application_id: string | null - application_version: string | null - user_type: string | null - user_id: string | null - resource_uid: string | null - resource_type: string | null - watch_session_id: string | null - } - - export interface ISubtitle { - url: string | null - lang: string | null - } - - export interface IMetrics { - apiHost: string | null - apiUrl: string | null - interval: number - params: IMetricsParams | null - } - - export interface IStream { - autoplay: boolean | "muted" | any - drmConfig: IDRMConfig | null - metrics: IMetrics | null - heartbeat: IHeartbeat | null - externalSubtitles: ISubtitle | null - } + export interface IMetricsParams { + application_id: string | null + application_version: string | null + user_type: string | null + user_id: string | null + resource_uid: string | null + resource_type: string | null + watch_session_id: string | null } + + export interface ISubtitle { + url: string | null + lang: string | null + } + + export interface IMetrics { + apiHost: string | null + apiUrl: string | null + interval: number + params: IMetricsParams | null + } + + export type ManifestVideoTrack = { + name: string + url: string + // Унаcледованная опечатка, изменять нельзя - поломается совместимость с приложением Voka + brandWidth: number | null + width: number | null + height: number | null + source: string[] + group: string | null + } + + export type ManifestAudioTrack = { + name: string + url: string + source: string + group: string | null + } + + export type Manifest = { + source: string + video: ManifestVideoTrack[] + audio: ManifestAudioTrack[] + } + + export interface IManifestParserPlayList { + attributes: { + RESOLUTION: { + width: number | null + height: number | null + } + BANDWIDTH: number | null + } + } + + export interface IManifestParser { + allowCache?: boolean + endList?: boolean + mediaSequence?: number + discontinuitySequence?: number + playlistType?: string + custom?: any + playlists: IManifestParserPlayList[] + mediaGroups?: any + dateTimeString?: string + dateTimeObject?: string + targetDuration?: number + totalDuration?: number + discontinuityStarts?: any + segments?: any[] + } + + export interface IStream { + autoplay: boolean | 'muted' | any + drmConfig: IDRMConfig | null + metrics: IMetrics | null + heartbeat: IHeartbeat | null + externalSubtitles: ISubtitle | null + manifest: Manifest | null + parsedManifest: IManifestParser | null + } + } } -export { IOSVersion, IContent, IContextUpdated, IHeartbeat, IMetricsParams, IMetrics, ICaption, - VokaOptions, VokaErrorMessages } \ No newline at end of file +export { + ICaption, + IContent, + IContextUpdated, + IHeartbeat, + IMetrics, + IMetricsParams, + IOSVersion, + VokaErrorMessages, + VokaOptions +} diff --git a/src/public/VokaPlayerImpl.ts b/src/public/VokaPlayerImpl.ts index 653c1b6..5600452 100644 --- a/src/public/VokaPlayerImpl.ts +++ b/src/public/VokaPlayerImpl.ts @@ -1,696 +1,1056 @@ -import { BusEvent } from "ts-bus/types"; -import videojs from 'video.js' -import { VokaOptions } from './@types' -import { IAudioTrack, IQuality, ISubtitle, ITimeRange, IVideoInfo, IVokaPlayer } from './IVokaPlayer' -import VokaCorePlayer from '@/internal/player/VokaCorePlayer' -import '../assets/scss/main.scss' -import VokaApi from '@/public/network/VokaApi' -import BrowserUtils from '@/internal/utils/BrowserUtils' -import SupportedCodecs from '@/internal/utils/SupportedCodecs' -import EncryptSystem from '@/internal/utils/EncryptSystem' -import { VokaContentType } from '@/public/models/VokaContentType' import { Promise } from 'es6-promise' -import PlatformCapabilities from '@/internal/utils/PlatformCapabilities' -import CorePlayerOptions from '@/internal/player/CorePlayerOptions' -import EventUtils from '@/internal/utils/EventUtils' +import * as m3u8Parser from 'm3u8-parser' import { EventBus } from 'ts-bus' +import type { BusEvent } from 'ts-bus/types' +import videojs from 'video.js' + +import VokaBusEvent from '@/internal/events/VokaBusEvent' +import type CorePlayerOptions from '@/internal/player/CorePlayerOptions' +import VokaCorePlayer from '@/internal/player/VokaCorePlayer' +import BrowserUtils from '@/internal/utils/BrowserUtils' +import EncryptSystem from '@/internal/utils/EncryptSystem' +import EventUtils from '@/internal/utils/EventUtils' +import PlatformCapabilities from '@/internal/utils/PlatformCapabilities' +import SupportedCodecs from '@/internal/utils/SupportedCodecs' +import { VokaContentType } from '@/public/models/VokaContentType' import { VokaError } from '@/public/models/VokaError' -import VokaBusEvent, { adError, adFinished, adStarted } from '@/internal/events/VokaBusEvent' +import VokaApi from '@/public/network/VokaApi' + +import type { VokaOptions } from './@types' +import type { IAudioTrack, IQuality, ISubtitle, ITimeRange, IVideoInfo, IVokaPlayer } from './IVokaPlayer' + +import '../assets/scss/main.scss' + +type VokaManifest = VokaOptions.StreamOptions.Manifest +type VokaManifestAudioTrack = VokaOptions.StreamOptions.ManifestAudioTrack +type VokaManifestVideoTrack = VokaOptions.StreamOptions.ManifestVideoTrack function log(...args) { - const now = new Date().toISOString() - videojs.log(`[${ now }][VokaPlayerImpl]`, ...args) + const now = new Date().toISOString() + videojs.log(`[${now}][VokaPlayerImpl]`, ...args) } export class VokaPlayerImpl implements IVokaPlayer { + private static readonly version = '0.0.3' + private static readonly build = '1' - private static readonly version = '0.0.3' - private static readonly build = '1' + private _player: VokaCorePlayer.CorePlayer | null + private _adIsPlaying: boolean + public readonly uniqID: number + public readonly sessionGUID: string + public readonly options: VokaOptions.IOptions - private _player: VokaCorePlayer.CorePlayer | null - private _adIsPlaying: boolean - public readonly uniqID: number - public readonly sessionGUID: string - public readonly options: VokaOptions.IOptions + private readonly initializePromise: Promise + private _features: SupportedCodecs.ISelectProtocolResult | null - private readonly initializePromise: Promise - private _features: SupportedCodecs.ISelectProtocolResult | null + private readonly bus: EventBus - private readonly bus: EventBus + public static playerVersion(): string { + return `${this.version}.${this.build}` + } - public static playerVersion(): string { return `${this.version}.${this.build}` } - - constructor( - id: number, - sessionGUID: string, - options: VokaOptions.IOptions, - features: Promise, - playerCreationClosure: VokaCorePlayer.PlayerCreationClosure, - ) { - if (!options.log) videojs.log.level("off") - - this.uniqID = id - this.sessionGUID = sessionGUID - this.options = options - this._player = null - this._adIsPlaying = false - this.bus = new EventBus() - VokaError.bus = this.bus - - const playerOptions = this.prepareCoreOptions(options) - this.initializePromise = new Promise( - (resolve, reject) => { - Promise.all([features, playerCreationClosure(playerOptions)]).then( - ([features, player]) => { - this._player = player - this._features = features - this.initialize(player, features).then((result) => { - resolve() - }, reject) - - const INSTANCE_DECORATED = Symbol("instanceDecorated") - const DECORATED_METHODS = Symbol("decoratedMethods") - - if (options.log && !(this as any)[INSTANCE_DECORATED]) { - log("[OPTIONS PREPARE]:" + this.uniqID, playerOptions) - - const proto = Object.getPrototypeOf(this) - const toSkip = new Set([ - "constructor", - "prepareCoreOptions", - "load", - "initialize", - "safeStringify", - "logMethodCall", - ]) - - const decorated: Set = new Set() - Object.defineProperty(this, DECORATED_METHODS, { - value: decorated, - configurable: true, - }) - - for (const name of Object.getOwnPropertyNames(proto)) { - if (toSkip.has(name)) continue - - const d = Object.getOwnPropertyDescriptor(proto, name) - if (!d || typeof d.value !== "function") continue - - if (Object.prototype.hasOwnProperty.call(this, name)) continue - - const instanceDesc: PropertyDescriptor = { - configurable: true, - enumerable: d.enumerable ?? false, - writable: true, - value: d.value, - } - - const wrappedDesc = this.logMethodCall(proto, name, instanceDesc) - wrappedDesc.value.__logged = true - decorated.add(name) - - Object.defineProperty(this, name, wrappedDesc) - } - - for (const name of Object.getOwnPropertyNames(this)) { - if (toSkip.has(name)) continue - if (decorated.has(name)) continue - - const d = Object.getOwnPropertyDescriptor(this, name) - if (!d || typeof d.value !== "function") continue - - if (d.value.__logged) continue - - const instanceDesc: PropertyDescriptor = { - configurable: true, - enumerable: d.enumerable ?? true, - writable: true, - value: d.value, - } - - const wrappedDesc = this.logMethodCall(this, name, instanceDesc) - wrappedDesc.value.__logged = true - decorated.add(name) - - Object.defineProperty(this, name, wrappedDesc) - } - - Object.defineProperty(this, INSTANCE_DECORATED, { - value: true, - configurable: true, - }) - } - }, - ) - }, - ) - this.initializePromise.catch(e => { - console.error(e) - this.destroy() - }) - - this.bus.subscribe( - VokaBusEvent.adStarted, - event => { - this._adIsPlaying = true - }, - ) - - this.bus.subscribe( - VokaBusEvent.adFinished, - event => { - this._adIsPlaying = false - }, - ) - - this.bus.subscribe( - VokaBusEvent.adError, - event => { - this._adIsPlaying = false - }, - ) + constructor( + id: number, + sessionGUID: string, + options: VokaOptions.IOptions, + features: Promise, + playerCreationClosure: VokaCorePlayer.PlayerCreationClosure + ) { + if (!options.log) videojs.log.level('off') + // FIXME debug + if (!window.players) { + window.players = {} } + window.players[id] = this - // MARK: - Private methods + this.uniqID = id + this.sessionGUID = sessionGUID + this.options = options + this._player = null + this._adIsPlaying = false + this.bus = new EventBus() + VokaError.bus = this.bus - private prepareCoreOptions(options: VokaOptions.IOptions): CorePlayerOptions.IOptions { - if (!!options.log) { - log("[OPTIONS]:" + this.uniqID, options) + const playerOptions = this.prepareCoreOptions(options) + this.initializePromise = new Promise((resolve, reject) => { + Promise.all([features, playerCreationClosure(playerOptions)]).then(([features, player]) => { + this._player = player + this._features = features + this.initialize(player, features).then((result) => { + resolve() + }, reject) + + const INSTANCE_DECORATED = Symbol('instanceDecorated') + const DECORATED_METHODS = Symbol('decoratedMethods') + + if (options.log && !(this as any)[INSTANCE_DECORATED]) { + log('[OPTIONS PREPARE]:' + this.uniqID, playerOptions) + + const proto = Object.getPrototypeOf(this) + const toSkip = new Set([ + 'constructor', + 'prepareCoreOptions', + 'load', + 'initialize', + 'safeStringify', + 'logMethodCall' + ]) + + const decorated: Set = new Set() + Object.defineProperty(this, DECORATED_METHODS, { + value: decorated, + configurable: true + }) + + for (const name of Object.getOwnPropertyNames(proto)) { + if (toSkip.has(name)) continue + + const d = Object.getOwnPropertyDescriptor(proto, name) + if (!d || typeof d.value !== 'function') continue + + if (Object.prototype.hasOwnProperty.call(this, name)) continue + + const instanceDesc: PropertyDescriptor = { + configurable: true, + enumerable: d.enumerable ?? false, + writable: true, + value: d.value + } + + const wrappedDesc = this.logMethodCall(proto, name, instanceDesc) + wrappedDesc.value.__logged = true + decorated.add(name) + + Object.defineProperty(this, name, wrappedDesc) + } + + for (const name of Object.getOwnPropertyNames(this)) { + if (toSkip.has(name)) continue + if (decorated.has(name)) continue + + const d = Object.getOwnPropertyDescriptor(this, name) + if (!d || typeof d.value !== 'function') continue + + if (d.value.__logged) continue + + const instanceDesc: PropertyDescriptor = { + configurable: true, + enumerable: d.enumerable ?? true, + writable: true, + value: d.value + } + + const wrappedDesc = this.logMethodCall(this, name, instanceDesc) + wrappedDesc.value.__logged = true + decorated.add(name) + + Object.defineProperty(this, name, wrappedDesc) + } + + Object.defineProperty(this, INSTANCE_DECORATED, { + value: true, + configurable: true + }) } - return { - controls: { - isVisible: options.uiConfig?.hideControls !== true, - tracks: options.controls.externalSubtitles?.url && [ - { - kind: "subtitles", - src: options.controls.externalSubtitles.url, - srclang: options.controls.externalSubtitles.lang || "", - label: options.controls.externalSubtitles.lang || "", - }, - ], - zoomButton: { - enable: options.controls.zoomButton.enable, - isVisible: options.controls.zoomButton.isVisible, - }, - selectionButton: { - enable: options.controls.editing.enable - } - }, - autoplay: (BrowserUtils.getLocationParam('autoplay') || (options.streamOpts && options.streamOpts.autoplay) || false), - log: options.log, - bus: this.bus, - loggerId: options.loggerId - } as CorePlayerOptions.IOptions + }) + }) + this.initializePromise.catch((e) => { + console.error(e) + this.destroy() + }) + + this.bus.subscribe(VokaBusEvent.adStarted, (event) => { + this._adIsPlaying = true + }) + + this.bus.subscribe(VokaBusEvent.adFinished, (event) => { + this._adIsPlaying = false + }) + + this.bus.subscribe(VokaBusEvent.adError, (event) => { + this._adIsPlaying = false + }) + } + + // MARK: - Private methods + + private prepareCoreOptions(options: VokaOptions.IOptions): CorePlayerOptions.IOptions { + if (options.log) { + log('[OPTIONS]:' + this.uniqID, options) } + return { + controls: { + isVisible: options.uiConfig?.hideControls !== true, + tracks: options.controls.externalSubtitles?.url && [ + { + kind: 'subtitles', + src: options.controls.externalSubtitles.url, + srclang: options.controls.externalSubtitles.lang || '', + label: options.controls.externalSubtitles.lang || '' + } + ], + zoomButton: { + enable: options.controls.zoomButton.enable, + isVisible: options.controls.zoomButton.isVisible + }, + selectionButton: { + enable: options.controls.editing.enable + } + }, + autoplay: + BrowserUtils.getLocationParam('autoplay') || (options.streamOpts && options.streamOpts.autoplay) || false, + log: options.log, + bus: this.bus, + loggerId: options.loggerId + } as CorePlayerOptions.IOptions + } - private load(content: VokaApi.IResult, player: VokaCorePlayer.CorePlayer) { + private lastToken = 0 + private inflightAbort?: AbortController - const drmConfig = { type: VokaCorePlayer.DRMType.NONE } as VokaCorePlayer.IDRMConfig - if (content.drmConfig != null) { - switch (content.drmConfig.type) { - case VokaApi.DRMType.PLAYREADY: - drmConfig.type = VokaCorePlayer.DRMType.PLAYREADY - break - case VokaApi.DRMType.FAIRPLAY: - drmConfig.type = VokaCorePlayer.DRMType.FAIRPLAY - break - case VokaApi.DRMType.WIDEVINE: - drmConfig.type = VokaCorePlayer.DRMType.WIDEVINE - break - } - drmConfig.certificateUrl = content.drmConfig.certificateUrl - drmConfig.licenseUrl = content.drmConfig.keyServerUrl - } else if (content.streamOptions?.drmConfig != null) { - const config = content.streamOptions?.drmConfig - if (config?.fairplay != null) { - drmConfig.type = VokaCorePlayer.DRMType.FAIRPLAY - drmConfig.certificateUrl = config.fairplay.certificate_url - drmConfig.licenseUrl = config.fairplay.ksm_url - } else if (config?.widevine != null) { - drmConfig.type = VokaCorePlayer.DRMType.WIDEVINE - drmConfig.certificateUrl = config.widevine.proxy_url || "" - } else if (config?.playready != null) { - drmConfig.type = VokaCorePlayer.DRMType.PLAYREADY - drmConfig.certificateUrl = config.playready.la_url || "" - } + // Синхронный интерфейс — не меняем + public load(content: VokaApi.IResult, player: VokaCorePlayer.CorePlayer): void { + const token = ++this.lastToken + + if (this.inflightAbort) this.inflightAbort.abort() + const abort = new AbortController() + this.inflightAbort = abort + + ;(async () => { + const drmConfig = this.buildDrmConfig(content) + + const iContent: VokaCorePlayer.IContent = { + url: content.url, + type: VokaPlayerImpl.contentType(content.url, drmConfig), + drmConfig: drmConfig.type !== VokaCorePlayer.DRMType.NONE ? drmConfig : null, + subtitlesUrl: content.subtitlesUrl || content.streamOptions?.externalSubtitles?.url, + metrics: content.metrics || content.streamOptions?.metrics, + heartbeat: content.streamOptions?.heartbeat, + streamOptions: content.streamOptions, + adv: content.adv + } + + try { + if (!iContent.streamOptions?.manifest) { + const manifestText = await this.fetchWithTimeout(content.url, 1000, abort.signal) + + const dir = VokaPlayerImpl.getDir(content.url) + const manifestMeta = this.parseManifest(manifestText, dir) + iContent.streamOptions = { + ...iContent.streamOptions, + manifest: manifestMeta + } as VokaOptions.StreamOptions.IStream } - const iContent = { - url: content.url, - type: VokaPlayerImpl.contentType(content.url, drmConfig), - drmConfig: drmConfig.type != VokaCorePlayer.DRMType.NONE ? drmConfig : null, - subtitlesUrl: content.subtitlesUrl || content.streamOptions?.externalSubtitles?.url, - metrics: content.metrics || content.streamOptions?.metrics, - heartbeat: content.streamOptions?.heartbeat, - streamOptions: content.streamOptions, - adv: content.adv, - } as VokaCorePlayer.IContent + if (abort.signal.aborted || token !== this.lastToken) return + await player.load(iContent) + if (abort.signal.aborted || token !== this.lastToken) return + player.enable() + } + catch (error) { + if (abort.signal.aborted || token !== this.lastToken) return + console.error('Error content loading, disable player', error) + player.disable() + } + finally { + // Чистим только если это самый свежий запуск + if (token === this.lastToken) this.inflightAbort = undefined + } + })() + } - // Логика для сайта voka - если нечего играть, плеер скрываем; - player.load(iContent) - .then(() => player.enable()) - .catch((error) => { - console.error("Error content loading, disable player", error) - player.disable() - }) + // ---------------- helpers ---------------- + + private static getDir(url: string): string { + const u = new URL(url) + return u.origin + u.pathname.substring(0, u.pathname.lastIndexOf('/') + 1) + } + + private buildDrmConfig(content: VokaApi.IResult): VokaCorePlayer.IDRMConfig { + const drmConfig: VokaCorePlayer.IDRMConfig = { type: VokaCorePlayer.DRMType.NONE } + + if (content.drmConfig) { + switch (content.drmConfig.type) { + case VokaApi.DRMType.PLAYREADY: + drmConfig.type = VokaCorePlayer.DRMType.PLAYREADY + break + case VokaApi.DRMType.FAIRPLAY: + drmConfig.type = VokaCorePlayer.DRMType.FAIRPLAY + break + case VokaApi.DRMType.WIDEVINE: + drmConfig.type = VokaCorePlayer.DRMType.WIDEVINE + break + } + drmConfig.certificateUrl = content.drmConfig.certificateUrl + drmConfig.licenseUrl = content.drmConfig.keyServerUrl + return drmConfig } - private async initialize(player: VokaCorePlayer.CorePlayer, features: SupportedCodecs.ISelectProtocolResult): Promise { - if (!this.options.features.api) { return false } - - const content = await VokaPlayerImpl.fetchContent(this.options, features) - this.load(content, player) - - return true + const sc = content.streamOptions?.drmConfig + if (sc?.fairplay) { + drmConfig.type = VokaCorePlayer.DRMType.FAIRPLAY + drmConfig.certificateUrl = sc.fairplay.certificate_url + drmConfig.licenseUrl = sc.fairplay.ksm_url + } else if (sc?.widevine) { + drmConfig.type = VokaCorePlayer.DRMType.WIDEVINE + drmConfig.certificateUrl = sc.widevine.proxy_url || '' + } else if (sc?.playready) { + drmConfig.type = VokaCorePlayer.DRMType.PLAYREADY + drmConfig.certificateUrl = sc.playready.la_url || '' } + return drmConfig + } - private static async fetchContent(options: VokaOptions.IOptions, features: SupportedCodecs.ISelectProtocolResult) { - const apiOptions = { - apiHost: options.apiConfig.apiHost, - movieId: options.apiConfig.movieId, - episodeId: options.apiConfig.episodeId, - newsId: options.apiConfig.newsId, - channelId: options.apiConfig.channelId, - clientId: options.apiConfig.clientId, - urlGetParams: options.apiConfig.urlGetParams - } as VokaApi.IOptions + private async fetchWithTimeout(url: string, timeoutMs: number, signal?: AbortSignal): Promise { + const ctrl = new AbortController() + const timeout = setTimeout(() => ctrl.abort(), timeoutMs) - const apiCapability = { - getOSVersion: () => BrowserUtils.getOSVersion(), - getDeviceId: () => BrowserUtils.deviceID(), - supportedProtocols: () => { - if (features.native != null) { - return features.native == SupportedCodecs.NativePlayerType.hls ? 'hls' : 'mss' - } - return 'dash' - }, - supportedDrm: () => { - switch (features.keySystem) { - case EncryptSystem.Encryption.widevine: return 'widevine' - case EncryptSystem.Encryption.fairplay: return 'fairplay' - case EncryptSystem.Encryption.playready: return 'playready' - default: return 'spbtvcas' - } - }, - getPlayerVersion: () => `${this.version}.${this.build}`, - getBrowserName: () => BrowserUtils.browserInfo().name - } as VokaApi.ISystemCapability + const onAbort = () => ctrl.abort() + signal?.addEventListener('abort', onAbort) - const api = new VokaApi.Api(apiOptions, apiCapability) - return api.load() + try { + const res = await fetch(url, { signal: ctrl.signal, credentials: 'omit' }) + if (!res.ok) throw new Error(`HTTP ${res.status} on ${url}`) + return await res.text() + } finally { + clearTimeout(timeout) + signal?.removeEventListener('abort', onAbort) } + } - private static contentType(url: string, drmConfig: VokaCorePlayer.IDRMConfig): VokaContentType { - const value = url.toLowerCase() - if (value.indexOf('m3u8') > -1) { - return drmConfig.type == VokaCorePlayer.DRMType.FAIRPLAY - ? VokaContentType.FAIRPLAY - : VokaContentType.HLS + private parseManifest(text: string, dir: string): VokaManifest { + if (text.startsWith('#EXTM3U')) return this.parseHls(text, dir) + if (text.trimStart().startsWith('<') && text.includes(' { + const a = pl.attributes ?? {} + const res = (a.RESOLUTION as { width: number; height: number }) ?? {} + const width = res.width ?? null + const height = res.height ?? null + + const name = + width && height ? `${width}x${height}` : typeof a.NAME === 'string' && a.NAME.length ? a.NAME : 'Variant' + + const url = this.resolveUrl(dir, (pl.uri as string) ?? '') + + const codecs = + typeof a.CODECS === 'string' && a.CODECS.length + ? a.CODECS.split(',') + .map((s) => s.trim()) + .filter(Boolean) + : [] + + const brandWidth = + typeof a.BANDWIDTH === 'number' ? a.BANDWIDTH : a.BANDWIDTH != null ? Number(a.BANDWIDTH) : null + + const group = typeof a.AUDIO === 'string' && a.AUDIO.length ? a.AUDIO : null + + const track: VokaManifestVideoTrack = { + name, + url, + brandWidth, + width, + height, + source: codecs, // В реализации Voka тут кусок исходника, относящийся к текущему треку + group } - if (value.indexOf('dash') > -1 || value.indexOf('mpd') > -1) { - if (drmConfig.type == VokaCorePlayer.DRMType.WIDEVINE) { - return VokaContentType.WIDEVINE - } - if (drmConfig.type == VokaCorePlayer.DRMType.PLAYREADY) { - return VokaContentType.PLAYREADY - } - return VokaContentType.DASH + return track + }) + + video.sort((a, b) => { + const hA = a.height ?? -1, + hB = b.height ?? -1 + if (hA !== hB) return hB - hA + const bwA = a.brandWidth ?? -1, + bwB = b.brandWidth ?? -1 + return bwB - bwA + }) + + // Дорожки из mediaGroups.AUDIO + const audio: VokaManifestAudioTrack[] = [] + const groups = m.mediaGroups?.AUDIO ?? {} + for (const groupId of Object.keys(groups)) { + const renditions = groups[groupId] ?? {} + for (const renditionKey of Object.keys(renditions)) { + const entry: any = renditions[renditionKey] ?? {} + + const url = this.resolveUrl(dir, entry.uri ?? '') + const rawName: string = + typeof entry.language === 'string' && entry.language + ? entry.language + : typeof entry.name === 'string' && entry.name + ? entry.name + : renditionKey + + const name = this.bringingAudioTrackNameToStandard(rawName) + + audio.push({ + name, + url, + source: entry.language ?? '', + group: groupId || null + }) } - return VokaContentType.MP4 + } + + return { + source: manifestTextData, + video, + audio + } + } catch { + return {} + } + } + + /* =============== helpers =============== */ + + // Универсальное приведение URI к абсолютному с учётом dir + private resolveUrl(dir: string, uri: string): string { + const u = (uri || '').trim() + if (!u) return u + if (/^https?:\/\//i.test(u)) return u + try { + return new URL(u, dir.endsWith('/') ? dir : dir + '/').toString() + } catch { + // fallback на конкатенацию, если dir не абсолютный URL + return (dir ?? '') + u + } + } + + // У тебя уже есть эта функция — оставляю вызов. + private bringingAudioTrackNameToStandard(name: string): string { + return name + } + + private parseDash(text: string): VokaManifest { + // минимальный разбор без внешних зависимостей + const profiles = /profiles="([^"]+)"/.exec(text)?.[1] + const timeShift = /timeShiftBufferDepth="PT([0-9.]+)S?"/.exec(text)?.[1] + return { + source: text, + video: [], + audio: [] + } + } + + private async initialize( + player: VokaCorePlayer.CorePlayer, + features: SupportedCodecs.ISelectProtocolResult + ): Promise { + if (!this.options.features.api) { + return false } - private static safeStringify(obj: any, indent = 2): string { - const cache = new Set() - return JSON.stringify(obj, (key, value) => { - if (typeof value === "object" && value !== null) { - if (cache.has(value)) { - return "[Circular]" - } - cache.add(value) - } - // Убираем ненужные большие объекты (например, window или DOM-элементы) - if (value instanceof Window) return "[Window]" - if (value instanceof Document) return "[Document]" - if (value instanceof HTMLElement) return `[HTMLElement: ${ value.tagName }]` - return value - }, indent) - } + const content = await VokaPlayerImpl.fetchContent(this.options, features) + this.load(content, player) - private logMethodCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) { - const originalMethod = descriptor.value - const id = this.uniqID + return true + } - descriptor.value = function (...args: any[]) { - const res = originalMethod.apply(this, args) - if (this.uniqID === id) { - const logKey = "[ID=" + this.uniqID + "]:[" + propertyKey + "]:" - log(logKey, - " Arguments:" + VokaPlayerImpl.safeStringify(args), - " Result:" + VokaPlayerImpl.safeStringify(res), - ) - console.trace(logKey, "stacktrace: ") - } - return res - } + private static async fetchContent(options: VokaOptions.IOptions, features: SupportedCodecs.ISelectProtocolResult) { + const apiOptions = { + apiHost: options.apiConfig.apiHost, + movieId: options.apiConfig.movieId, + episodeId: options.apiConfig.episodeId, + newsId: options.apiConfig.newsId, + channelId: options.apiConfig.channelId, + clientId: options.apiConfig.clientId, + urlGetParams: options.apiConfig.urlGetParams + } as VokaApi.IOptions - return descriptor - } - - // MARK: - IVokaPlayer implementation - - public afterInitialize(callback: () => void) { - this.initializePromise.then(() => { callback() }) - } - - public isInitialized(): boolean { - return this._player != null - } - - public getProtocol(): string { - const features = this._features - if (features != null && features.native != null) { - return features.native.toString() + const apiCapability = { + getOSVersion: () => BrowserUtils.getOSVersion(), + getDeviceId: () => BrowserUtils.deviceID(), + supportedProtocols: () => { + if (features.native != null) { + return features.native == SupportedCodecs.NativePlayerType.hls ? 'hls' : 'mss' } return 'dash' - } - - public getDrmSystem(): string { - const features = this._features - if (!this.options.features.drm || features == null) { - return 'none' + }, + supportedDrm: () => { + switch (features.keySystem) { + case EncryptSystem.Encryption.widevine: + return 'widevine' + case EncryptSystem.Encryption.fairplay: + return 'fairplay' + case EncryptSystem.Encryption.playready: + return 'playready' + default: + return 'spbtvcas' } - return EncryptSystem.toString(features.keySystem) - } + }, + getPlayerVersion: () => `${this.version}.${this.build}`, + getBrowserName: () => BrowserUtils.browserInfo().name + } as VokaApi.ISystemCapability - public isHlsSupported(): boolean { - return PlatformCapabilities.isSupportedHlsJs - } + const api = new VokaApi.Api(apiOptions, apiCapability) + return api.load() + } - public getVideoCodecs(): [string] { - const features = this._features - if (features == null) { return [] } - return features.mseCodecs + private static contentType(url: string, drmConfig: VokaCorePlayer.IDRMConfig): VokaContentType { + const value = url.toLowerCase() + if (value.indexOf('m3u8') > -1) { + return drmConfig.type == VokaCorePlayer.DRMType.FAIRPLAY ? VokaContentType.FAIRPLAY : VokaContentType.HLS } + if (value.indexOf('dash') > -1 || value.indexOf('mpd') > -1) { + if (drmConfig.type == VokaCorePlayer.DRMType.WIDEVINE) { + return VokaContentType.WIDEVINE + } + if (drmConfig.type == VokaCorePlayer.DRMType.PLAYREADY) { + return VokaContentType.PLAYREADY + } + return VokaContentType.DASH + } + return VokaContentType.MP4 + } - public attachSource( - url: string | null, - options: VokaOptions.StreamOptions.IStream | null - ): void { - if (!this._player) { return } - if (url == null) { - this._player.stop() - return; + private static safeStringify(obj: any, indent = 2): string { + const cache = new Set() + return JSON.stringify( + obj, + (key, value) => { + if (typeof value === 'object' && value !== null) { + if (cache.has(value)) { + return '[Circular]' + } + cache.add(value) } + // Убираем ненужные большие объекты (например, window или DOM-элементы) + if (value instanceof Window) return '[Window]' + if (value instanceof Document) return '[Document]' + if (value instanceof HTMLElement) return `[HTMLElement: ${value.tagName}]` + return value + }, + indent + ) + } - const content = { - url: url, - subtitlesUrl: null, - drmConfig: null, - metrics: null, - streamOptions: options, - } as VokaApi.IResult + private logMethodCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value + const id = this.uniqID - this.load(content , this._player) + descriptor.value = function (...args: any[]) { + const res = originalMethod.apply(this, args) + if (this.uniqID === id) { + const logKey = '[ID=' + this.uniqID + ']:[' + propertyKey + ']:' + log(logKey, ' Arguments:' + VokaPlayerImpl.safeStringify(args), ' Result:' + VokaPlayerImpl.safeStringify(res)) + console.trace(logKey, 'stacktrace: ') + } + return res } - public getPaused(): boolean { - if (this._player == null) { return false } - return this._player.isPaused + return descriptor + } + + // MARK: - IVokaPlayer implementation + + public afterInitialize(callback: () => void) { + this.initializePromise.then(() => { + callback() + }) + } + + public isInitialized(): boolean { + return this._player != null + } + + public getProtocol(): string { + const features = this._features + if (features != null && features.native != null) { + return features.native.toString() + } + return 'dash' + } + + public getDrmSystem(): string { + const features = this._features + if (!this.options.features.drm || features == null) { + return 'none' + } + return EncryptSystem.toString(features.keySystem) + } + + public isHlsSupported(): boolean { + return PlatformCapabilities.isSupportedHlsJs + } + + public getVideoCodecs(): [string] { + const features = this._features + if (features == null) { + return [] + } + return features.mseCodecs + } + + public attachSource(url: string | null, options: VokaOptions.StreamOptions.IStream | null): void { + if (!this._player) { + return + } + if (url == null) { + this._player.stop() + return } - public getIsLive(): boolean { - if (this._player == null) { return false } - return this._player.isLive + const content = { + url: url, + subtitlesUrl: null, + drmConfig: null, + metrics: null, + streamOptions: options + } as VokaApi.IResult + + this.load(content, this._player) + } + + public getPaused(): boolean { + if (this._player == null) { + return false + } + return this._player.isPaused + } + + public getIsLive(): boolean { + if (this._player == null) { + return false + } + return this._player.isLive + } + + public seek(seconds: number): void { + if (this._player == null) { + return + } + this._player.seek(seconds) + } + + public getCurrentTime(): number { + if (this._player == null) { + return 0 + } + return this._player.currentTime + } + + public getDuration(): number { + if (this._player == null) { + return 0 + } + return this._player.duration + } + + public setVolume(value: number) { + if (this._player == null) { + return + } + this._player.setVolume(value) + } + + public getVolume(): number { + if (this._player == null || this._player.isMuted) { + return 0 + } + return this._player.volume + } + + public mute() { + if (this._player == null) { + return + } + this._player.mute(true) + } + + public unmute() { + if (this._player == null) { + return + } + this._player.mute(false) + } + + public getMuted(): boolean { + if (this._player == null) { + return false + } + return this._player.isMuted + } + + public destroy() { + const events = this.bus.emitter.eventNames() + events.forEach((event) => { + this.bus.emitter.removeAllListeners(event) + }) + if (this._player != null) { + this._player.destroy() + this._player = null + this.bus.publish(VokaBusEvent.destroyed()) + } + } + + getBufferLength(): number { + if (this._player == null) { + return 0 + } + return this._player.bufferLength + } + + getBufferingState(): boolean { + if (this._player == null) { + return false + } + return this._player.isBuffering + } + + getAdIsPlaying(): boolean { + return this._adIsPlaying + } + + cancelAdPlayback() { + this.bus.publish(VokaBusEvent.adCancelPlaybackEvent as BusEvent) + } + + getAbsoluteCurrentTime(): number | null { + if (this._player == null) { + return null + } + return this.getCurrentTime() + } + + getAbsoluteTimeRange(): ITimeRange | null { + if (this._player == null) { + return null + } + return this._player.getAbsoluteRange() + } + + getTimeshiftAvailable(): boolean { + if (this._player == null) { + return false + } + return this._player.getTimeshiftAvailable() + } + + getVideoQualityList(): IQuality[] { + if (this._player == null) { + return [] + } + return this._player.getVideoQualityObserver().videoQualityList + } + + getSelectedVideoQuality(): number { + if (this._player == null) { + return 0 + } + return this._player.getVideoQualityObserver().selectedVideoQuality + } + + getPlayingVideoQuality(): number { + if (this._player == null) { + return 0 + } + return this._player.getVideoQualityObserver().playingVideoQuality + } + + setSelectedVideoQuality(index: number) { + if (this._player == null) { + return 0 + } + this._player.getVideoQualityObserver().setSelectedVideoQuality(index) + } + + getAudioTrackList(): IAudioTrack[] { + if (this._player == null) { + return [] + } + return this._player.getAudioTrackObserver().audioTrackList + } + + getCurrentAudioTrack(): number { + if (this._player == null) { + return 0 + } + return this._player.getAudioTrackObserver().currentAudioTrackIndex + } + + setCurrentAudioTrack(index: number) { + if (this._player == null) { + return 0 + } + this._player.getAudioTrackObserver().setCurrentAudioTrack(index) + } + + setSelectionStartPos(seconds: number) { + if (this._player == null) { + return false + } + return this._player.getSelectionObserver().setStartPosition(seconds) + } + + setSelectionEndPos(seconds: number) { + if (this._player == null) { + return false + } + return this._player.getSelectionObserver().setEndPosition(seconds) + } + + getSelectionRange(): ITimeRange | null { + if (this._player == null) { + return null + } + return this._player.getSelectionObserver().range + } + + getSubtitlesTrackList(): ISubtitle[] { + if (this._player == null) { + return [] + } + return this._player.getSubtitleTrackObserver().subtitleTrackList + } + + getCurrentSubtitlesTrack(): number { + if (this._player == null) { + return 0 + } + return this._player.getSubtitleTrackObserver().currentSubtitleTrackIndex + } + + setCurrentSubtitlesTrack(index: number) { + if (this._player == null) { + return 0 + } + this._player.getSubtitleTrackObserver().setCurrentSubtitleTrack(index) + } + + getZoomButtonVisible(): boolean { + if (this._player == null) { + return false + } + return this._player.getZoomObserver().isVisible + } + + getZoomModeEnabled(): boolean { + if (this._player == null) { + return false + } + return this._player.getZoomObserver().isEnabled + } + + setZoomModeEnabled(isEnabled: boolean) { + if (this._player == null) { + return false + } + this._player.getZoomObserver().setEnabled(isEnabled) + } + + getNetworkBandwidth(): number { + if (this._player == null) { + return 0 + } + return this._player.getCurrentBandwidth() || 0 + } + + getCurrentVideoInfo(): IVideoInfo { + const result = { + width: null, + height: null, + bitrate: null + } as IVideoInfo + + if (this._player == null) { + return result } - public seek(seconds: number): void { - if (this._player == null) { return } - this._player.seek(seconds) + const videoInfo = this._player.getVideoInfo() + if (videoInfo == null) { + return result } + result.width = videoInfo.width + result.height = videoInfo.height + result.bitrate = videoInfo.bitrate + return result + } - public getCurrentTime(): number { - if (this._player == null) { return 0 } - return this._player.currentTime + getControlbarVisible(): boolean { + if (this._player == null) { + return false } + if (!this._player.getControlbarVisibilityObserver().visibility()) { + return false + } + return this._player.getIsUserActive() + } - public getDuration(): number { - if (this._player == null) { return 0 } - return this._player.duration + setControlbarVisibility(visible: boolean) { + if (this._player == null) { + return } + this._player.getControlbarVisibilityObserver().visibility(visible) + } - public setVolume(value: number) { - if (this._player == null) { return } - this._player.setVolume(value) - } + addEventListener(name: string, func: Function, obj: any): void { + EventUtils.addEventListener(name, func, obj) + } - public getVolume(): number { - if (this._player == null || this._player.isMuted) { return 0 } - return this._player.volume - } + removeEventListener(name: string, func: Function, obj: any): void { + EventUtils.removeEventListener(name, func, obj) + } - public mute() { - if (this._player == null) { return } - this._player.mute(true) + play(): Promise | undefined { + if (this._player == null) { + return Promise.resolve() } + return this._player.play() + } - public unmute() { - if (this._player == null) { return } - this._player.mute(false) - } - - public getMuted(): boolean { - if (this._player == null) { return false } - return this._player.isMuted - } - - public destroy() { - const events = this.bus.emitter.eventNames() - events.forEach(event => { this.bus.emitter.removeAllListeners(event) }) - if (this._player != null) { - this._player.destroy() - this._player = null - this.bus.publish( - VokaBusEvent.destroyed() - ) - } - } - - getBufferLength(): number { - if (this._player == null) { return 0 } - return this._player.bufferLength - } - getBufferingState(): boolean { - if (this._player == null) { return false } - return this._player.isBuffering - } - - getAdIsPlaying(): boolean { return this._adIsPlaying } - cancelAdPlayback() { - this.bus.publish(VokaBusEvent.adCancelPlaybackEvent as BusEvent) - } - - getAbsoluteCurrentTime(): number | null { - if (this._player == null) { return null } - return this.getCurrentTime() - } - getAbsoluteTimeRange(): ITimeRange | null { - if (this._player == null) { return null } - return this._player.getAbsoluteRange(); - } - getTimeshiftAvailable(): boolean { - if (this._player == null) { return false } - return this._player.getTimeshiftAvailable(); - } - getVideoQualityList(): IQuality[] { - if (this._player == null) { return [] } - return this._player.getVideoQualityObserver().videoQualityList - } - getSelectedVideoQuality(): number { - if (this._player == null) { return 0 } - return this._player.getVideoQualityObserver().selectedVideoQuality - } - getPlayingVideoQuality(): number { - if (this._player == null) { return 0 } - return this._player.getVideoQualityObserver().playingVideoQuality - } - setSelectedVideoQuality(index: number) { - if (this._player == null) { return 0 } - this._player.getVideoQualityObserver().setSelectedVideoQuality(index) - } - getAudioTrackList(): IAudioTrack[] { - if (this._player == null) { return [] } - return this._player.getAudioTrackObserver().audioTrackList - } - getCurrentAudioTrack(): number { - if (this._player == null) { return 0 } - return this._player.getAudioTrackObserver().currentAudioTrackIndex - } - setCurrentAudioTrack(index: number) { - if (this._player == null) { return 0 } - this._player.getAudioTrackObserver().setCurrentAudioTrack(index) - } - - setSelectionStartPos(seconds: number) { - if (this._player == null) { return false } - return this._player.getSelectionObserver().setStartPosition(seconds) - } - setSelectionEndPos(seconds: number) { - if (this._player == null) { return false } - return this._player.getSelectionObserver().setEndPosition(seconds) - } - getSelectionRange(): ITimeRange | null { - if (this._player == null) { return null } - return this._player.getSelectionObserver().range - } - - getSubtitlesTrackList(): ISubtitle[] { - if (this._player == null) { return [] } - return this._player.getSubtitleTrackObserver().subtitleTrackList - } - getCurrentSubtitlesTrack(): number { - if (this._player == null) { return 0 } - return this._player.getSubtitleTrackObserver().currentSubtitleTrackIndex - } - setCurrentSubtitlesTrack(index: number) { - if (this._player == null) { return 0 } - this._player.getSubtitleTrackObserver().setCurrentSubtitleTrack(index) - } - - getZoomButtonVisible(): boolean { - if (this._player == null) { return false } - return this._player.getZoomObserver().isVisible - } - getZoomModeEnabled(): boolean { - if (this._player == null) { return false } - return this._player.getZoomObserver().isEnabled - } - setZoomModeEnabled(isEnabled: boolean) { - if (this._player == null) { return false } - this._player.getZoomObserver().setEnabled(isEnabled) - } - getNetworkBandwidth(): number { - if (this._player == null) { return 0 } - return this._player.getCurrentBandwidth() || 0 - } - getCurrentVideoInfo(): IVideoInfo { - const result = { - width: null, - height: null, - bitrate: null, - } as IVideoInfo - - if (this._player == null) { return result } - - const videoInfo = this._player.getVideoInfo() - if (videoInfo == null) { return result } - result.width = videoInfo.width - result.height = videoInfo.height - result.bitrate = videoInfo.bitrate - return result - } - getControlbarVisible(): boolean { - if (this._player == null) { return false } - if (!this._player.getControlbarVisibilityObserver().visibility()) { return false } - return this._player.getIsUserActive() - } - - setControlbarVisibility(visible: boolean) { - if (this._player == null) { return } - this._player.getControlbarVisibilityObserver().visibility(visible) - } - - addEventListener(name: string, func: Function, obj: any): void { - EventUtils.addEventListener(name, func, obj) - } - - removeEventListener(name: string, func: Function, obj: any): void { - EventUtils.removeEventListener(name, func, obj) - } - - play(): Promise | undefined { - if (this._player == null) { return Promise.resolve() } - return this._player.play() - } - pause(): void { - if (this._player == null) { return } - return this._player.pause() + pause(): void { + if (this._player == null) { + return } + return this._player.pause() + } } +// Пример качеств из Dash +// private getQualityItems(levels: IDashQuality[]): IQualityLevel[] { +// return levels +// .map((value: IDashQuality, index: number) => { +// const qualityType = QualityMapper.getQualityTypeByResolution( +// value.width, +// value.height +// ) +// +// if (qualityType == null) { return null } +// +// return { +// index, +// bitrate: value.bitrateInKbit, +// width: value.width, +// height: value.height, +// quality: QualityMapper.getValueByQualityType(qualityType), +// label: QualityMapper.getLabelByQualityType( +// qualityType, +// QualityLabelVariant.COMMON, +// ) +// } +// } +// ) +// .filter(value => value != null) as IQualityLevel[] +// } + +// private loadAndParseHlsManifest(url: string) { +// const client = new HTTPClient() +// +// // Скачиваем плейлист +// const result = client.request('get', url, null, null, { +// // FIXME взять тип из this.options_.source.type +// headers: { +// Accept: +// 'application/vnd.apple.mpegurl, application/x-mpegURL, text/plain' +// }, +// timeout: 10000 +// }) +// +// // ....... +// result.then() +// +// if (!result.ok) { +// throw new Error( +// `Failed to load manifest: ${result.status} ${result.statusText}` +// ) +// } +// +// const manifestText = result.data +// +// const parser = new M3U8Parser() +// parser.push(manifestText) +// parser.end() +// +// const manifest = parser.manifest as ManifestParser +// return { manifest, raw: manifestText } +// } + // MP4 /*const iContent = { - url: "https://vjs.zencdn.net/v/oceans.mp4", - type: VokaContentType.MP4, -} as VokaCorePlayer.IContent*/ + url: "https://vjs.zencdn.net/v/oceans.mp4", + type: VokaContentType.MP4, + } as VokaCorePlayer.IContent*/ // HLS /*const iContent = { - url: "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8", - type: VokaContentType.HLS, -} as VokaCorePlayer.IContent*/ + url: "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8", + type: VokaContentType.HLS, + } as VokaCorePlayer.IContent*/ // HLS LIVE /*const iContent = { - url: "https://dai.google.com/linear/hls/event/rtcMlf4RTvOEkaudeany5w/master.m3u8?iu=/4128/CBS.NY.OTT", - type: VokaContentType.HLS, -} as VokaCorePlayer.IContent*/ + url: "https://dai.google.com/linear/hls/event/rtcMlf4RTvOEkaudeany5w/master.m3u8?iu=/4128/CBS.NY.OTT", + type: VokaContentType.HLS, + } as VokaCorePlayer.IContent*/ // DASH /*const iContent = { - url: "https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd", - type: VokaContentType.DASH, -} as VokaCorePlayer.IContent*/ + url: "https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd", + type: VokaContentType.DASH, + } as VokaCorePlayer.IContent*/ // DASH /*const iContent = { - url: "https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd", - type: VokaContentType.DASH, -} as VokaCorePlayer.IContent*/ + url: "https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd", + type: VokaContentType.DASH, + } as VokaCorePlayer.IContent*/ // FAIRPLAY TODO /*const iContent = { - url: "https://codeeducation.akamaized.net/code/fullcycle/devops_20/01/01_introducao.mp4/fp/fairplay.m3u8", - type: VokaContentType.FAIRPLAY, - drmConfig: { - type: VokaCorePlayer.DRMType.FAIRPLAY, - certificateUrl: "https://codeeducation.akamaized.net/fairplay.cer", - licenseUrl: "https://fps.ezdrm.com/api/licenses/F6B15258-BC92-49EB-9CDF-DE9F121C13A5?customdata=MTQ0OmFyZ2VudGluYWx1aXpAZ21haWwuY29tOjY3MTE6Y291cnNlOmNvZGU=", - } -} as VokaCorePlayer.IContent*/ + url: "https://codeeducation.akamaized.net/code/fullcycle/devops_20/01/01_introducao.mp4/fp/fairplay.m3u8", + type: VokaContentType.FAIRPLAY, + drmConfig: { + type: VokaCorePlayer.DRMType.FAIRPLAY, + certificateUrl: "https://codeeducation.akamaized.net/fairplay.cer", + licenseUrl: "https://fps.ezdrm.com/api/licenses/F6B15258-BC92-49EB-9CDF-DE9F121C13A5?customdata=MTQ0OmFyZ2VudGluYWx1aXpAZ21haWwuY29tOjY3MTE6Y291cnNlOmNvZGU=", + } + } as VokaCorePlayer.IContent*/ /*const iContent = { - url: "https://e09f957480c8b1e479a1edb0fabc72d8.egress.mediapackage-vod.eu-west-1.amazonaws.com/out/v1/6f12444e79macdf3e4206ad363f810cb2aead/9ea4e8148b794c8ba2c6295b824e5ad5/46a61bf2c081464bb9476f2a55a06f48/index.m3u8", - type: VokaContentType.FAIRPLAY, - drmConfig: { - type: VokaCorePlayer.DRMType.FAIRPLAY, - certificateUrl: "https://customer-tests.la.drm.cloud/certificate/fairplay?BrandGuid=5a96a0d0-d13f-42b0-ab2b-ba8cfc4aa0a0", - licenseUrl: "https://customer-tests.la.drm.cloud/acquire-license/fairplay?KID=4376a4b3-d8ef-4f21-9a6b-faa81a2e59e3&brandguid=5a96a0d0-d13f-42b0-ab2b-ba8cfc4aa0a0&usertoken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzU2ODk2MDAsImRybVRva2VuSW5mbyI6eyJleHAiOiIyMDI1LTEyLTA3VDE1OjMwOjA5LjU5MDgxMjUrMDE6MDAiLCJraWQiOlsiKiJdLCJwIjp7InBlcnMiOnRydWUsImVkIjoiMjAyNS0xMi0wN1QxNTozMDowOS41OTExMzA1KzAxOjAwIn19fQ.xEToUttAk9AVFgP3bHyDlcvm6BR-8_hsl8V3n-jrDwM", - } -} as VokaCorePlayer.IContent*/ + url: "https://e09f957480c8b1e479a1edb0fabc72d8.egress.mediapackage-vod.eu-west-1.amazonaws.com/out/v1/6f12444e79macdf3e4206ad363f810cb2aead/9ea4e8148b794c8ba2c6295b824e5ad5/46a61bf2c081464bb9476f2a55a06f48/index.m3u8", + type: VokaContentType.FAIRPLAY, + drmConfig: { + type: VokaCorePlayer.DRMType.FAIRPLAY, + certificateUrl: "https://customer-tests.la.drm.cloud/certificate/fairplay?BrandGuid=5a96a0d0-d13f-42b0-ab2b-ba8cfc4aa0a0", + licenseUrl: "https://customer-tests.la.drm.cloud/acquire-license/fairplay?KID=4376a4b3-d8ef-4f21-9a6b-faa81a2e59e3&brandguid=5a96a0d0-d13f-42b0-ab2b-ba8cfc4aa0a0&usertoken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzU2ODk2MDAsImRybVRva2VuSW5mbyI6eyJleHAiOiIyMDI1LTEyLTA3VDE1OjMwOjA5LjU5MDgxMjUrMDE6MDAiLCJraWQiOlsiKiJdLCJwIjp7InBlcnMiOnRydWUsImVkIjoiMjAyNS0xMi0wN1QxNTozMDowOS41OTExMzA1KzAxOjAwIn19fQ.xEToUttAk9AVFgP3bHyDlcvm6BR-8_hsl8V3n-jrDwM", + } + } as VokaCorePlayer.IContent*/ // Dash WIDEVINE DRM /*const iContent = { - url: "https://media.axprod.net/TestVectors/Cmaf/protected_1080p_h264_cbcs/manifest.mpd", - type: VokaContentType.WIDEVINE, - drmConfig: { - type: VokaCorePlayer.DRMType.WIDEVINE, - certificateUrl: "https://drm-widevine-licensing.axtest.net/AcquireLicense", - headers: { - "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICIzMDJmODBkZC00MTFlLTQ4ODYtYmNhNS1iYjFmODAxOGEwMjQiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAicm9LQWcwdDdKaTFpNDNmd3YremZ0UT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ._NfhLVY7S6k8TJDWPeMPhUawhympnrk6WAZHOVjER6M", - }, - } -} as VokaCorePlayer.IContent*/ + url: "https://media.axprod.net/TestVectors/Cmaf/protected_1080p_h264_cbcs/manifest.mpd", + type: VokaContentType.WIDEVINE, + drmConfig: { + type: VokaCorePlayer.DRMType.WIDEVINE, + certificateUrl: "https://drm-widevine-licensing.axtest.net/AcquireLicense", + headers: { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICIzMDJmODBkZC00MTFlLTQ4ODYtYmNhNS1iYjFmODAxOGEwMjQiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAicm9LQWcwdDdKaTFpNDNmd3YremZ0UT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ._NfhLVY7S6k8TJDWPeMPhUawhympnrk6WAZHOVjER6M", + }, + } + } as VokaCorePlayer.IContent*/ // Dash Playready DRM /*const iContent = { - url: "https://media.axprod.net/TestVectors/Cmaf/protected_1080p_h264_cbcs/manifest.mpd", - type: VokaContentType.PLAYREADY, - drmConfig: { - type: VokaCorePlayer.DRMType.PLAYREADY, - certificateUrl: "https://drm-widevine-licensing.axtest.net/AcquireLicense", - headers: { - "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICIzMDJmODBkZC00MTFlLTQ4ODYtYmNhNS1iYjFmODAxOGEwMjQiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAicm9LQWcwdDdKaTFpNDNmd3YremZ0UT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ._NfhLVY7S6k8TJDWPeMPhUawhympnrk6WAZHOVjER6M", - }, - } -} as VokaCorePlayer.IContent*/ \ No newline at end of file + url: "https://media.axprod.net/TestVectors/Cmaf/protected_1080p_h264_cbcs/manifest.mpd", + type: VokaContentType.PLAYREADY, + drmConfig: { + type: VokaCorePlayer.DRMType.PLAYREADY, + certificateUrl: "https://drm-widevine-licensing.axtest.net/AcquireLicense", + headers: { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICIzMDJmODBkZC00MTFlLTQ4ODYtYmNhNS1iYjFmODAxOGEwMjQiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAicm9LQWcwdDdKaTFpNDNmd3YremZ0UT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ._NfhLVY7S6k8TJDWPeMPhUawhympnrk6WAZHOVjER6M", + }, + } + } as VokaCorePlayer.IContent*/ diff --git a/src/public/models/ILoadOptions.ts b/src/public/models/ILoadOptions.ts index 5346a2a..78265b4 100644 --- a/src/public/models/ILoadOptions.ts +++ b/src/public/models/ILoadOptions.ts @@ -1,25 +1,26 @@ export interface ILoadOptions { - startPosition?: number - playbackRate?: number - volume?: number - autoplay?: boolean - quality?: Quality - adConfig: string + startPosition?: number + playbackRate?: number + volume?: number + autoplay?: boolean + quality?: Quality + adConfig: string } export enum Quality { - AUTO = -1, - _240p = 240, - _360p = 360, - _480p = 480, - _720p = 720, - _1080p = 1080, - _1440p = 1440, - _2160p = 2160 + AUTO = -1, + _240p = 240, + _144p = 144, + _360p = 360, + _480p = 480, + _720p = 720, + _1080p = 1080, + _1440p = 1440, + _2160p = 2160 } export enum QualityLabelVariant { - RUSSIAN = 'RUSSIAN', - ENGLISH = 'ENGLISH', - COMMON = 'COMMON' -} \ No newline at end of file + RUSSIAN = 'RUSSIAN', + ENGLISH = 'ENGLISH', + COMMON = 'COMMON' +} diff --git a/src/public/models/VokaError.ts b/src/public/models/VokaError.ts index 74f4bdb..9312b0f 100644 --- a/src/public/models/VokaError.ts +++ b/src/public/models/VokaError.ts @@ -1,144 +1,151 @@ -import { ErrorData } from 'hls.js' +import type { ErrorData } from 'hls.js' +import type { EventBus } from 'ts-bus' + import VokaBusEvent from '@/internal/events/VokaBusEvent' -import { Environment } from './Environment' -import { EventBus } from 'ts-bus' /** * Компонент, где произошла ошибка (err_type) */ enum VokaInternalErrorComponent { - System = 'SYSTEM', - Api = 'API', - DRM = 'DRM', - Playlist = 'PLAYLIST', // мастера-плейлиста - Track = 'TRACK', - Chunk = 'CHUNK', // чанков - // Chunk_ts = 'chunk_ts', // фрагмента чанка - Player = 'PLAYER', // плеера - Stream = 'STREAM', - Fairplay = 'FAIRPLAY', // FairPlay DRM - DashDrm = 'DASH_DRM', // Dash Widevine / Playready DRM - AdvertXml = 'XML', // рекламный xml - AdvertCreative = 'CREATIVE', // рекламный creative - Statistics = 'STATISTICS' + System = 'SYSTEM', + Api = 'API', + DRM = 'DRM', + Playlist = 'PLAYLIST', // мастера-плейлиста + Track = 'TRACK', + Chunk = 'CHUNK', // чанков + // Chunk_ts = 'chunk_ts', // фрагмента чанка + Player = 'PLAYER', // плеера + Stream = 'STREAM', + Fairplay = 'FAIRPLAY', // FairPlay DRM + DashDrm = 'DASH_DRM', // Dash Widevine / Playready DRM + AdvertXml = 'XML', // рекламный xml + AdvertCreative = 'CREATIVE', // рекламный creative + Statistics = 'STATISTICS' } enum VokaInternalErrorType { - System = 'SYSTEM', - Http = 'HTTP', - Parsing = 'PARSING', - Timeout = 'TIMEOUT', - Exception = 'EXCEPTION' + System = 'SYSTEM', + Http = 'HTTP', + Parsing = 'PARSING', + Timeout = 'TIMEOUT', + Exception = 'EXCEPTION' } enum ErrorTextType { - TITLE, - DESCRIPTION + TITLE, + DESCRIPTION } type VokaInternalErrorData = { - url: string | null - message: string | null - code: string | null + url: string | null + message: string | null + code: string | null } type ComponentErrorInfo = { - component: VokaInternalErrorComponent - type: VokaInternalErrorType + component: VokaInternalErrorComponent + type: VokaInternalErrorType } class VokaError { + static bus!: EventBus - static bus!: EventBus + static normalizeHlsError(error: Record) { + Object.keys(error).forEach((key) => { + if (error[key] === null) { + delete error[key] + } + }) + return error + } - static normalizeHlsError(error: Record) { - Object.keys(error).forEach((key) => { - if (error[key] === null) { - delete error[key] - } + static getMessageByCode(code: number): string | undefined { + switch (code) { + case 400: + return 'Bad Request' + case 401: + return 'Unauthorized' + case 403: + return 'Forbidden' + case 404: + return 'Not found' + case 405: + return 'Method Not Allowed' + case 500: + return 'Internal Server Error' + case 502: + return 'Bad Gateway' + case 503: + return 'Service Unavailable' + case 504: + return 'Gateway Timeout' + } + } + + static getHlsErrorMessage(data: ErrorData): string | undefined { + const code = data.response?.code + if (code) { + return VokaError.getMessageByCode(code) + } + + if (data.reason) { + return data.reason + } + return data.response?.text + } + + static normalize(error: Record) { + const err: VokaInternalErrorData = {} + if (error.message) { + err['message'] = error.message + } else { + err['message'] = error.details + } + if (error.statusCode) { + err['code'] = error.statusCode + } else if (error.message && typeof error.message === 'object' && typeof error.message.statusCode !== 'undefined') { + err['code'] = error.message.statusCode + } + return err + } + + static fire( + type: VokaInternalErrorType, + component: VokaInternalErrorComponent, + data: VokaInternalErrorData, + fatal = false + ) { + try { + this.bus.publish( + VokaBusEvent.error({ + type, + component, + error: data, + fatal }) - return error + ) } - - static getMessageByCode(code: number): string | undefined { - switch (code) { - case 400: - return 'Bad Request' - case 401: - return 'Unauthorized' - case 403: - return 'Forbidden' - case 404: - return 'Not found' - case 405: - return 'Method Not Allowed' - case 500: - return 'Internal Server Error' - case 502: - return 'Bad Gateway' - case 503: - return 'Service Unavailable' - case 504: - return 'Gateway Timeout' - } + catch { } + } - static getHlsErrorMessage(data: ErrorData): string | undefined { - const code = data.response?.code - if (code) { - return VokaError.getMessageByCode(code) - } - - if (data.reason) { return data.reason } - return data.response?.text - } - - static normalize(error: Record) { - const err: VokaInternalErrorData = {} - if (error.message) { - err['message'] = error.message - } else { - err['message'] = error.details - } - if (error.statusCode) { - err['code'] = error.statusCode - } else if ( - error.message && - typeof error.message === 'object' && - typeof error.message.statusCode !== 'undefined' - ) { - err['code'] = error.message.statusCode - } - return err - } - - static fire( - type: VokaInternalErrorType, - component: VokaInternalErrorComponent, - data: VokaInternalErrorData, - fatal = false - ) { - this.bus.publish( - VokaBusEvent.error({ - type, - component, - error: data, - fatal - }) - ) - } - - static processText(text: string, type: ErrorTextType) { - /*if (type === ErrorTextType.DESCRIPTION) { - return Environment.shared().ui?.text?.error?.description !== undefined - ? Environment.shared().ui?.text?.error?.description - : text - } else if (type === ErrorTextType.TITLE) { - return Environment.shared().ui?.text?.error?.title !== undefined - ? Environment.shared().ui?.text?.error?.title - : text - }*/ - } + static processText(text: string, type: ErrorTextType) { + /*if (type === ErrorTextType.DESCRIPTION) { + return Environment.shared().ui?.text?.error?.description !== undefined + ? Environment.shared().ui?.text?.error?.description + : text + } else if (type === ErrorTextType.TITLE) { + return Environment.shared().ui?.text?.error?.title !== undefined + ? Environment.shared().ui?.text?.error?.title + : text + }*/ + } } -export { VokaError, VokaInternalErrorComponent, VokaInternalErrorType, ErrorTextType, VokaInternalErrorData, ComponentErrorInfo } \ No newline at end of file +export { + ComponentErrorInfo, + ErrorTextType, + VokaError, + VokaInternalErrorComponent, + VokaInternalErrorData, + VokaInternalErrorType}