#66645 Для LG не работает getVideoQualityList

This commit is contained in:
Алексей Манаев
2025-09-16 19:52:02 +00:00
parent 448546d34b
commit ed9f7f3047
45 changed files with 9462 additions and 4355 deletions
+14
View File
@@ -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
View File
@@ -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'
}
}
]
}
+72
View File
@@ -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'
]
}
+1
View File
@@ -0,0 +1 @@
# make lint-staged
+1 -2
View File
@@ -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
}
+52
View File
@@ -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

+5 -6
View File
@@ -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"]
+44
View File
@@ -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
View File
@@ -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 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
ares-package -o "/usr/webOS/Result" ./app
ares-package -o "/usr/webOS/Result" ./App
+9 -4
View File
@@ -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
View File
@@ -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
}
```
+3868 -28
View File
File diff suppressed because it is too large Load Diff
+21 -5
View File
@@ -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 }))
}
}
File diff suppressed because it is too large Load Diff
+121 -123
View File
@@ -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
+29 -16
View File
@@ -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
+22 -22
View File
@@ -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
}
+76 -65
View File
@@ -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}
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
View File
@@ -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 dont 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,
};
};
* */
+47 -46
View File
@@ -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 }
+78 -74
View File
@@ -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
+31 -37
View File
@@ -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
View File
@@ -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
}
File diff suppressed because it is too large Load Diff
+19 -18
View File
@@ -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
View File
@@ -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}