Merge branch 'feature/66645-lg-video-qualities' into 'develop'
#66645 Для LG не работает getVideoQualityList See merge request ps/voka/voka-player-js!93
This commit is contained in:
@@ -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
|
||||
+109
@@ -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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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'
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# make lint-staged
|
||||
+1
-2
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@@ -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 <<EOF
|
||||
npm install -g @webos-tools/cli
|
||||
EOF
|
||||
# Утилиты, которые часто нужны: git, openssh для ключей, zip для ares-package, build-base на всякий
|
||||
RUN apk add --no-cache bash git openssh-client zip python3 make g++ \
|
||||
&& npm i -g @webos-tools/cli
|
||||
|
||||
WORKDIR /usr/webOS
|
||||
|
||||
COPY HostedWebApp-main ./app
|
||||
COPY App ./App
|
||||
COPY create.sh .
|
||||
|
||||
ENTRYPOINT ["/bin/sh"]
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# ./Makefile
|
||||
|
||||
# DOCKER = docker compose run --rm webos
|
||||
DOCKER = docker run --rm -v ./App:/usr/webOS/App -v ./Builds:/usr/webOS/Builds -v ./.ssh:/root/.ssh -v ./.webos:/root/.webos --entrypoint '' -it webapp
|
||||
|
||||
# 1) Установка зависимостей фронта (в контейнере)
|
||||
deps:
|
||||
$(DOCKER) bash -lc "corepack enable || true; npm ci || npm i"
|
||||
|
||||
# 2) Dev-сервер внутри контейнера (Vite на 0.0.0.0)
|
||||
dev:
|
||||
$(DOCKER) bash -lc "npm run dev -- --host --port 5173"
|
||||
|
||||
# 3) Устройства (первичная регистрация TV)
|
||||
devices:
|
||||
$(DOCKER) bash -lc "ares-setup-device --list || true; ares-setup-device"
|
||||
|
||||
# 4) Сборка ipk
|
||||
package:
|
||||
$(DOCKER) bash -lc "create.sh"
|
||||
|
||||
# 5) Установка на TV (смените имя устройства и app id при необходимости)
|
||||
install:
|
||||
$(DOCKER) bash -lc "ares-install --device tv Builds/*.ipk"
|
||||
|
||||
# 6) Запуск/остановка
|
||||
launch:
|
||||
$(DOCKER) bash -lc "ares-launch --device tv voka-player-js"
|
||||
close:
|
||||
$(DOCKER) bash -lc "ares-launch --device tv voka-player-js --close"
|
||||
|
||||
# 7) Просмотр логов/консоли без открытия браузера
|
||||
logs:
|
||||
$(DOCKER) bash -lc "ares-inspect --device tv --app voka-player-js"
|
||||
|
||||
# 8) Полезное: список приложений на TV
|
||||
apps:
|
||||
$(DOCKER) bash -lc "ares-launch --device tv --list"
|
||||
|
||||
shell:
|
||||
$(DOCKER) bash
|
||||
|
||||
build-image:
|
||||
docker build -t webapp .
|
||||
+20
-4
@@ -1,13 +1,29 @@
|
||||
Для генерации демо приложения необходимо
|
||||
|
||||
- Сбилдить докер образ
|
||||
|
||||
```shell
|
||||
make build-image
|
||||
```
|
||||
docker build -t webapp:1 .
|
||||
|
||||
или
|
||||
|
||||
```
|
||||
docker build -t webapp .
|
||||
```
|
||||
|
||||
- Запустить контейнер на основе созданного образа и получить результат выполнения.
|
||||
```
|
||||
docker run -v $PWD:/usr/webOS/Result webapp:1
|
||||
|
||||
```shell
|
||||
make package
|
||||
```
|
||||
|
||||
В итоге в текущей директории получим файл с расширением `ipk` которое можно установить на SmartTV.
|
||||
или
|
||||
|
||||
```
|
||||
docker run -v $PWD/App:/usr/webOS/App -v $PWD/Builds:/usr/webOS/Result webapp
|
||||
```
|
||||
|
||||
В итоге в директории `Builds` получим файл с расширением `ipk`, который можно установить на SmartTV.
|
||||
|
||||
Смотри другие команды в `Makefile`
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
ares-package -o "/usr/webOS/Result" ./app
|
||||
ares-package -o "/usr/webOS/Result" ./App
|
||||
+9
-4
@@ -61,9 +61,14 @@
|
||||
},
|
||||
});
|
||||
player.afterInitialize(() => {
|
||||
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 @@
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+50
-4
@@ -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
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Generated
+3868
-28
File diff suppressed because it is too large
Load Diff
+21
-5
@@ -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"
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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 }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+502
-346
File diff suppressed because it is too large
Load Diff
+121
-123
@@ -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<IChangeQuality>()('quality.update')
|
||||
|
||||
export const qualityChange = createEventDefinition<IChangeQuality>()('quality.update')
|
||||
export interface IQualitySet {
|
||||
index: number // Исходный уникальный порядковый идентификатор качества
|
||||
quality: Quality
|
||||
forced: boolean | null
|
||||
noAutoChange: boolean | null
|
||||
withUIUpdate: boolean | null
|
||||
}
|
||||
export const qualitySet = createEventDefinition<IQualitySet>()('quality.set')
|
||||
|
||||
export interface IQualitySet {
|
||||
quality: Quality
|
||||
forced: boolean | null
|
||||
noAutoChange: boolean | null
|
||||
withUIUpdate: boolean | null
|
||||
}
|
||||
export const qualitySet = createEventDefinition<IQualitySet>()('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<ErrorEventData>()('playerError')
|
||||
|
||||
export const error = createEventDefinition<ErrorEventData>()('error')
|
||||
export const close = createEventDefinition()('close')
|
||||
|
||||
export const close = createEventDefinition()('close')
|
||||
// MARK: - Switch content
|
||||
export const switchContent = createEventDefinition<VokaCorePlayer.IContent>()('content.switch')
|
||||
|
||||
// MARK: - Switch content
|
||||
export const switchContent =
|
||||
createEventDefinition<VokaCorePlayer.IContent>()('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<IContextUpdated>()('context.update')
|
||||
|
||||
export const contextUpdated =
|
||||
createEventDefinition<IContextUpdated>()('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
|
||||
export default VokaBusEvent
|
||||
|
||||
@@ -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
|
||||
export default AudioTrackObserver
|
||||
|
||||
@@ -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
|
||||
export default VideoQualityObserver
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
export {
|
||||
AppleSourceProtection,
|
||||
IVokaSource,
|
||||
TizenSourceParams,
|
||||
TizenSourceProtection,
|
||||
VokaOptionsType,
|
||||
VokaSourceHandler,
|
||||
WebOSSourceProtection}
|
||||
|
||||
+1023
-1017
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
+275
@@ -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 {}
|
||||
@@ -1,6 +0,0 @@
|
||||
export default interface AudioTrack {
|
||||
index: number
|
||||
name: string
|
||||
data: any | null
|
||||
isSelected: boolean | 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
|
||||
|
||||
}
|
||||
@@ -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<void>
|
||||
|
||||
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<void> {
|
||||
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
|
||||
export default AVPlayHelper
|
||||
|
||||
@@ -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<typeof VokaBusEvent.audioTracksSet>) => this.changeAudioTrack(
|
||||
event)
|
||||
private changeQualityHandler = (event: ReturnType<typeof VokaBusEvent.qualitySet>) => 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<void> {
|
||||
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<VokaCorePlayer.IQualityData[]>((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<IAudioTrack[]>((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
|
||||
export default VokaTizenTech
|
||||
|
||||
@@ -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<typeof VokaBusEvent.audioTracksSet>) =>
|
||||
this.changeAudioTrack(event)
|
||||
private changeQualityHandler = (event: ReturnType<typeof VokaBusEvent.qualitySet>) => 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<typeof VokaBusEvent.audioTracksSet>) {
|
||||
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<typeof VokaBusEvent.qualitySet>) {
|
||||
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,
|
||||
};
|
||||
};
|
||||
* */
|
||||
@@ -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 }
|
||||
export { AutoplayChecker, AutoplayCheckerCallback }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
export default VokaMetricsPlugin
|
||||
|
||||
+225
-166
@@ -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<void>
|
||||
id: number
|
||||
url: string
|
||||
language: string
|
||||
title: string
|
||||
type: string
|
||||
|
||||
load(httpClient: IHTTPClient): Promise<void>
|
||||
}
|
||||
|
||||
// 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 }
|
||||
export {
|
||||
ICaption,
|
||||
IContent,
|
||||
IContextUpdated,
|
||||
IHeartbeat,
|
||||
IMetrics,
|
||||
IMetricsParams,
|
||||
IOSVersion,
|
||||
VokaErrorMessages,
|
||||
VokaOptions
|
||||
}
|
||||
|
||||
+963
-603
File diff suppressed because it is too large
Load Diff
@@ -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'
|
||||
}
|
||||
RUSSIAN = 'RUSSIAN',
|
||||
ENGLISH = 'ENGLISH',
|
||||
COMMON = 'COMMON'
|
||||
}
|
||||
|
||||
+122
-115
@@ -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<string, any>) {
|
||||
Object.keys(error).forEach((key) => {
|
||||
if (error[key] === null) {
|
||||
delete error[key]
|
||||
}
|
||||
})
|
||||
return error
|
||||
}
|
||||
|
||||
static normalizeHlsError(error: Record<string, any>) {
|
||||
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<string, any>) {
|
||||
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<string, any>) {
|
||||
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 }
|
||||
export {
|
||||
ComponentErrorInfo,
|
||||
ErrorTextType,
|
||||
VokaError,
|
||||
VokaInternalErrorComponent,
|
||||
VokaInternalErrorData,
|
||||
VokaInternalErrorType}
|
||||
|
||||
Reference in New Issue
Block a user