63297 - draft player implementation

This commit is contained in:
Juraldinio
2025-02-17 20:18:01 +03:00
parent 3d1de441b1
commit ab07792963
19 changed files with 748 additions and 16 deletions
+1
View File
@@ -1,3 +1,4 @@
.DS_Store
.idea
distribution
node_modules
+1
View File
@@ -5,6 +5,7 @@ FROM arm64v8/node:23.6.1-alpine3.21 AS arm
WORKDIR /usr/voka
COPY package.json .
COPY package-lock.json .
COPY develop.sh .
RUN <<EOF
+4
View File
@@ -10,6 +10,10 @@
docker build -t voka-player .
```
Обязательно перед запуском докера создать директорию `node_modules`!
Ни в коем случае не вносить в нее какие-либо изменения.
Директория реплицируется на хост машину для удобства.
Далее требуется запустить докер композ.
```
+1 -1
View File
@@ -5,7 +5,7 @@
<title>Title</title>
</head>
<body>
<h1>Hello world3</h1>
<h1>Hello world</h1>
<div id="my-video"/>
<script language="JavaScript" src="./distribution/vokaPlayer.global.js"></script>
</body>
+10 -4
View File
@@ -1,5 +1,3 @@
version: "3"
services:
server:
image: docker.io/library/voka-player
@@ -10,6 +8,14 @@ services:
restart: always
volumes:
- $PWD:/usr/voka
- /usr/voka/node_modules
- node_modules:/usr/voka/node_modules
ports:
- "8080:8080"
- "8080:8080"
volumes:
node_modules:
driver: local
driver_opts:
type: none
o: bind
device: $PWD/node_modules
+73
View File
@@ -17,6 +17,7 @@
"express": "^4.21.2",
"postcss": "^8.5.1",
"postcss-preset-env": "^10.1.3",
"ts-bus": "^2.3.1",
"ts-node": "^10.9.2",
"tsup": "^8.3.6",
"typescript": "^5.7.3",
@@ -2970,6 +2971,12 @@
"node": ">= 0.6"
}
},
"node_modules/eventemitter2": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz",
"integrity": "sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==",
"license": "MIT"
},
"node_modules/express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
@@ -3360,6 +3367,13 @@
"node": ">=10"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT",
"peer": true
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -3393,6 +3407,19 @@
"integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@@ -4511,6 +4538,18 @@
"node": ">= 0.6.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"license": "MIT",
@@ -4564,6 +4603,28 @@
"node": ">= 0.8"
}
},
"node_modules/react": {
"version": "16.14.0",
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT",
"peer": true
},
"node_modules/readdirp": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz",
@@ -5562,6 +5623,18 @@
"tree-kill": "cli.js"
}
},
"node_modules/ts-bus": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/ts-bus/-/ts-bus-2.3.1.tgz",
"integrity": "sha512-ZoiVKzctxeJvQ0ZB7Fc/wpbEEA5NEjdZWebisXk8raFfEFigDmwl1fKTcr+jNRfEcwFl1QJ4oiirxGR0YnPq1w==",
"license": "MIT",
"dependencies": {
"eventemitter2": "^5.0.1"
},
"peerDependencies": {
"react": "^16.8.6"
}
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+1
View File
@@ -24,6 +24,7 @@
"express": "^4.21.2",
"postcss": "^8.5.1",
"postcss-preset-env": "^10.1.3",
"ts-bus": "^2.3.1",
"ts-node": "^10.9.2",
"tsup": "^8.3.6",
"typescript": "^5.7.3",
+126
View File
@@ -0,0 +1,126 @@
import videojs from 'video.js'
import { VideoJsPlayer, VideoJsPlayerOptions } from '@types/video.js'
import { EventBus } from 'ts-bus'
import { IVokaPlayer } from '@/public/IVokaPlayer'
import { AutoplayChecker } from '@/internal/utils/AutoplayChecker'
import * as languages from '@/languages.json'
type PlayerReadyHandler = (player: IVokaPlayer) => void
const playbackRates = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]
export class VokaPlayerImpl implements IVokaPlayer {
private player!: VideoJsPlayer
private autoPlaySupported!: boolean
private readonly stateEmitter: EventBus
// Constructor
constructor(element: HTMLElement, callback?: PlayerReadyHandler/*, options?: ICreationOptions*/) {
const readyCallback = typeof callback === 'undefined' ? null : callback
this.stateEmitter = new EventBus()
/*this.stateEmitter.subscribe(
playerStateChange,
(data) => (this.state_ = data.payload.value)
)*/
/*if (options) {
window.vokaEnvironment = options
}*/
AutoplayChecker.isAutoplaySupported((supported) => {
this.setupPlayer(element, supported, readyCallback)
})
}
private setupPlayer(
element: HTMLElement,
autoPlaySupported: boolean,
callback: PlayerReadyHandler | null
) {
this.autoPlaySupported = autoPlaySupported
videojs.log.level('debug')
const options = this.initOptions()
const player = videojs(element, options, () => {
if (callback != null) {
callback(this)
}
this.player.play()
// try load content just right after complete initialization.
/*this.stateEmitter.publish(
playerStateChange({ value: Initialized })
)*/
})
player.addClass('video-js')
/*if (userAgent.getDevice().type === DEVICE.MOBILE) {
player.addClass('vjs-mobile')
}*/
// Sign to events.
this.setupAttachListeners(player)
this.player = player
}
private initOptions(): VideoJsPlayerOptions {
// Required player's plugins.
const plugins: Record<string, any> = { }
//plugins.vokaQualityPlugin = {}
//plugins.vokaStatisticsPlugin = {}
//plugins.vokaAdvertisementPlugin = {}
//plugins.vokaCaptionsPlugin = {}
const childrenComponents = [
// Non-visual component, not in UI layer list.
'mediaLoader' // Required loader, that load tech list!
]
const skinChildren = [
'resizeManager',
'LoadingSpinner',
'PosterImage',
'RestrictionBox',
'liveTracker',
'textTrackDisplay'
]
// Settings for player.
return {
controls: true, // Show controls.
techOrder: [
/*'VokaEmptyTech',
'VokaAppleTech',
'VokaMp4Tech',
'VokaHlsTech',
'VokaDashTech'*/
'html5'
], // Order for teches important.
plugins, // Attached plugins for player.
languages,
language: 'ru',
children: childrenComponents,
poster: null,
responsive: true,
breakpoints: {
tiny: undefined,
xsmall: undefined,
small: 559,
medium: 1002,
large: undefined,
xlarge: undefined,
huge: Infinity
},
playbackRates,
sources: [
{src: "//vjs.zencdn.net/v/oceans.mp4", type: "video/mp4"}
]
}
}
private setupAttachListeners(player: VideoJsPlayer) {
// Добавляем листенеры здесь
}
}
File diff suppressed because one or more lines are too long
+15
View File
@@ -0,0 +1,15 @@
/**
* Check to see if the object is a DOM Element.
*
* @param {*} element The object to check.
* @return {boolean}
*/
export function isDomElement(element: any): boolean {
return Boolean(
element &&
element.nodeType === 1 &&
'nodeName' in element &&
element.ownerDocument &&
element.ownerDocument.defaultView
)
}
+40
View File
@@ -0,0 +1,40 @@
{
"ru": {
"Play": "Смотреть",
"Pause": "Пауза",
"Replay": "Повторить",
"Settings": "Настройки",
"Fullscreen": "Развернуть",
"Non-Fullscreen": "Свернуть",
"Mute": "Отключить звук",
"Unmute": "Включить звук",
"Speed": "Скорость",
"Subtitles": "Субтитры",
"Off": "Выкл.",
"Normal": "Обычная",
"Auto": "Авто",
"Quality": "Качество",
"Error": "Ошибка",
"Playback-error": "Ошибка проигрывания",
"Seconds-short": "Cек"
},
"en": {
"Play": "Play",
"Pause": "Pause",
"Replay": "Repeat",
"Settings": "Settings",
"Fullscreen": "Full screen",
"Non-Fullscreen": "Exit full screen",
"Mute": "Mute",
"Unmute": "Unmute",
"Speed": "Speed",
"Subtitles": "Subtitles",
"Off": "Off",
"Normal": "Normal",
"Auto": "Auto",
"Quality": "Quality",
"Error": "Error",
"Playback-error": "Playback error",
"Seconds-short": "Sec"
}
}
+11 -10
View File
@@ -1,11 +1,12 @@
import videojs from 'video.js';
import { VokaPlayer } from '@/public/VokaPlayer'
import { IVokaPlayer } from '@/public/IVokaPlayer'
const player = videojs('my-video', {
controls: true,
autoplay: false,
preload: 'auto'
});
player.ready(() => {
console.log('Your Video.js player is ready?!!');
});
console.log(VokaPlayer.version)
const playerPromise = VokaPlayer.create('my-video')
playerPromise
.then((player) => {
console.log(player)
})
.catch((error) => {
console.error(error)
})
+9
View File
@@ -0,0 +1,9 @@
import { ILogger } from '@/public/logger/ILogger'
import { LogLevel } from '@/public/logger/Logger'
export interface ICreationOptions {
// Включен ли дебаг режим
debug?: boolean
logger?: ILogger
logLevel?: LogLevel
}
+129
View File
@@ -0,0 +1,129 @@
export enum VokaPlayerEvent {
'DurationChange' = 'durationchange',
'TimeUpdate' = 'timeupdate',
'Pause' = 'pause',
'Play' = 'play',
'Playing' = 'playing',
'Error' = 'error',
'Ended' = 'ended',
'VolumeChange' = 'volumechange',
'LoadedMetaData' = 'loadedmetadata',
'LoadStart' = 'loadstart',
'LoadedData' = 'loadeddata',
'FullscreenChange' = 'fullscreenchange',
'RateChange' = 'ratechange',
'Seeking' = 'seeking',
'Seeked' = 'seeked',
'Log' = 'log',
'Waiting' = 'waiting',
'CanPlay' = 'canplay',
'CanPlayThrough' = 'canplaythrough',
'QualitiesParsed' = 'qualitiesparsed',
'QualityChange' = 'qualitychange'
}
/*
destroyed - object was destroyed
sourceAttached - new stream source was set
canplay - player is ready to play video
play - video playback started/resumed
pause - video playback paused
ended - video playback finished (played until end)
error - video playback error
timeupdate - current stream position changed
timeshiftUpdate - timeshift availability changed
volumechange - audio volume changed
bufferLengthUpdate - buffer length changed
bufferingUpdate - buffering state changed
qualityChange - current video quality changed
trackChange - current audio/subtitles track changed
zoomButtonChange - update visibility of zoom button
zoomModeChange - update state of zoom mode
timeNotify - reached stream/wall-clock position configured in timeNotify section of options
adRequested - before ad loading is initiated
adStarted - ad block started
adFinished - ad block finished successfully
adError - ad finished with error
adItemStarted - one ad in ad block started
adItemFinished - one ad in ad block finished successfully
adItemError - one ad in ad block finished with error
adImpression - ad impression happened
disableAdsClick - user clicked disable ads button
logoClick - user clicked on logo button
controlbarShow - control bar is shown
controlbarHide - control bar is hidden
toolboxStartSel - toolbox start selection button was clicked
toolboxEndSel - toolbox end selection button was clicked
toolboxProcessSel - toolbox process selection button was clicked
* */
export interface IVokaPlayer {
/// addEventListener - add listener of specific event
on(event: VokaPlayerEvent, callback: Function): void
/// removeEventListener - remove listener of event
off(event: VokaPlayerEvent, callback?: Function): void
/// start/resume playing
play(): Promise<void>
/// pause playing
pause(): Promise<void>
stop(): Promise<void>
}
/*
load(url: string, options?: ILoadOptions): Promise<void>
loadWithProvider(
provider: IAPIProvider,
options?: ILoadOptions
): Promise<void>
setCurrentTime(value: number): Promise<void>
setVolume(value: number): Promise<void>
setPlaybackRate(value: PlaybackRate): Promise<void>
setViewType(value: string): Promise<void>
setQuality(value: Quality): Promise<void>*/
/*
* afterInitialize - register callback function that will be called after player is initialized (it will be called immediately if player is already initialized)
* isInitialized - check if player is initialized
* getProtocol - get supported streaming protocol name
* getDrmSystem - get supported drm system name
* isHlsSupported - check if HLS protocol is supported
* getVideoCodecs - get list of supported video codecs for dash protocol (only among those that were enabled in config)
* attachSource - set stream url and stream options
* getPaused - get paused state
* getIsLive - get live streaming flag
* seek - seek stream to specific position, in seconds
* getCurrentTime - get current stream position, in seconds
* getDuration - get current stream duration, in seconds
* getAbsoluteCurrentTime - get current utc unixtime for live streams
* getAbsoluteTimeRange - get utc start/end range for live streams
* getTimeshiftAvailable - check if timeshift is available
* getVideoQualityList - get array with information about available video qualities
* getSelectedVideoQuality - get index of currently selected video quality (-1 for auto)
* getPlayingVideoQuality - get index of currently playing video quality (-1 if unknown)
* setSelectedVideoQuality - set index of video quality to play (-1 for auto)
* getAudioTrackList - get array of available audio tracks
* getCurrentAudioTrack - get index of currently selected audio track
* setCurrentAudioTrack - set index of audio track to play
* getSubtitlesTrackList - get array of available subtitles tracks
* getCurrentSubtitlesTrack - get index of currently selected subtitles track (-1 for disabled)
* setCurrentSubtitlesTrack - set index of subtitles track to display (-1 to disable)
* setSelectionStartPos - mark current position as selection start
* setSelectionEndPos - mark current position as selection end
* getSelectionRange - get currently selected range
* getZoomButtonVisible - get zoom button visibility
* getZoomModeEnabled - check if zoom mode is enabled
* setZoomModeEnabled - enable/disable zoom mode (boolean argument)
* setVolume - set audio volume
* getVolume - get audio volume
* mute - mute audio
* unmute - unmute audio
* getMuted - get muted state
* getBufferLength - get length in seconds of buffered data
* getNetworkBandwidth - get current network bandwidth in kbit/s
* getBufferingState - wheither or not playback is stalled due to buffering
* getCurrentVideoInfo - get information about currently playing video track
* getControlbarVisible - get visibility state of control bar
* getAdIsPlaying - player is currently loading/playing ads
* cancelAdPlayback - cancel currently playing advertisement(s)
* destroy - destroy "player" object
*/
+57
View File
@@ -0,0 +1,57 @@
import { ICreationOptions } from './@types'
import { IVokaPlayer } from './IVokaPlayer'
import { VokaPlayerImpl } from '@/internal/player/VokaPlayerImpl'
import { LogLevel, Logger } from '@/public/logger/Logger'
import { ConsoleLogger } from '@/public/logger/ConsoleLogger'
import { isDomElement } from '../internal/utils/functions'
/*
* player = spbtvplayer('some_id', {});
player.afterInitialize(readyFunc);
function readyFunc() {
}
* */
export class VokaPlayer {
static readonly version = '0.0.1'
public static create(
element: HTMLElement | string,
options?: ICreationOptions
): Promise<IVokaPlayer> {
let elem = element
// Если указанный элемент - строка, ищем html-элемент по id
if (typeof document !== 'undefined' && typeof elem === 'string') {
elem = document.getElementById(elem) as HTMLElement
}
// Если не валидный DOM-элемент
if (!isDomElement(elem)) {
return Promise.reject(
new TypeError('You must pass either a valid element or a valid id.')
)
}
if (options && options.debug) {
const logger = options.logger ? options.logger : new ConsoleLogger()
const logLevel = options.logLevel ? options.logLevel : LogLevel.Debug
Logger.getInstance().setHandler(logger, logLevel)
}
// Если элемент - iframe
if ((elem as HTMLElement).nodeName == 'IFRAME') {
return Promise.reject(
new TypeError('Can not use iframe as player container.')
)
}
return new Promise<IVokaPlayer>((resolve) => {
new VokaPlayerImpl(elem, (player) => {
resolve(player)
})
})
}
}
+19
View File
@@ -0,0 +1,19 @@
import { ILogger } from '@/public/logger/ILogger'
export class ConsoleLogger implements ILogger {
debug(message: string): void {
console.debug(message)
}
error(message: string): void {
console.error(message)
}
info(message: string): void {
console.info(message)
}
warning(message: string): void {
console.warn(message)
}
}
+6
View File
@@ -0,0 +1,6 @@
export interface ILogger {
debug(message: string): void
info(message: string): void
warning(message: string): void
error(message: string): void
}
+77
View File
@@ -0,0 +1,77 @@
import { ILogger } from './ILogger'
export enum LogLevel {
'Debug' = 'debug',
'Info' = 'info',
'Warning' = 'warning',
'Error' = 'error'
}
export class Logger {
private handler?: ILogger
private logLevel: LogLevel
private static instance: Logger
private constructor() {
this.handler = undefined
}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger()
}
return Logger.instance
}
setHandler(logger: ILogger, logLevel: LogLevel): void {
this.handler = logger
this.logLevel = logLevel
window.addEventListener('message', (e: MessageEvent) => {
try {
if (!e.data) return
if (typeof e.data !== 'string') return
const pmData = JSON.parse(e.data)
if (pmData.type === 'player:log') {
if (!this.checkLevel(pmData?.data?.level)) return
// @ts-ignore
this.handler[pmData?.data?.level](pmData?.data?.message)
}
} catch (e) {
console.error(e)
}
})
}
debug(message: string): void {
if (!this.checkLevel(LogLevel.Debug)) return
this.handler?.debug(message)
}
error(message: string): void {
if (!this.checkLevel(LogLevel.Error)) return
this.handler?.error(message)
}
info(message: string): void {
if (!this.checkLevel(LogLevel.Info)) return
this.handler?.info(message)
}
warning(message: string): void {
if (!this.checkLevel(LogLevel.Warning)) return
this.handler?.warning(message)
}
private checkLevel(logLevel: LogLevel): boolean {
const levels = Object.values(LogLevel)
const availableLevelIndex = levels.findIndex((value: LogLevel) => {
return value === this.logLevel
})
const checkedLevelIndex = levels.findIndex((value: LogLevel) => {
return value === logLevel
})
return checkedLevelIndex >= availableLevelIndex
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": [
"es2016",
"dom"