From 75e5c73a46d83cf3e05676670d0a344acacb635c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0=D0=B5=D0=B2?= Date: Thu, 28 Aug 2025 23:22:22 +0000 Subject: [PATCH] =?UTF-8?q?#66575=20WebOS=20:=20=D0=9A=D0=B0=D0=BD=D0=B0?= =?UTF-8?q?=D0=BB=D1=8B=20=D1=81=D1=82=D0=B0=D1=80=D1=82=D1=83=D1=8E=D1=82?= =?UTF-8?q?=20=D0=BC=D0=B5=D0=B4=D0=BB=D0=B5=D0=BD=D0=BD=D0=B5=D0=B5=20?= =?UTF-8?q?=D1=87=D0=B5=D0=BC=20=D0=B2=20=D1=81=D1=82=D0=B0=D1=80=D0=BE?= =?UTF-8?q?=D0=BC=20=D0=BF=D0=BB=D0=B5=D0=B5=D1=80=D0=B5,=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D1=84=D0=B8=D0=BB=D1=8C=D0=BC=D0=B0=D1=85,...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/index.html | 4 +- docker-compose.caddy.yml | 1 + serve.sh | 17 +++ src/internal/player/VokaCorePlayer.ts | 45 +++--- src/public/VokaPlayer.ts | 14 +- src/public/VokaPlayerImpl.ts | 190 ++++++++++++++++++-------- 6 files changed, 186 insertions(+), 85 deletions(-) create mode 100644 serve.sh diff --git a/demo/index.html b/demo/index.html index 87c6154..8e1b383 100644 --- a/demo/index.html +++ b/demo/index.html @@ -25,7 +25,7 @@ var player = spbtvplayer('my-video', { log: true, features: { - api: true, + api: false, drm: false, metrics: true }, @@ -63,7 +63,7 @@ player.afterInitialize(() => { console.log("afterInitialize") // player.setControlbarVisibility(true) - // player.attachSource("https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd") + player.attachSource("https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd", {autoplay: true}) }) player.addEventListener('play', onPlay, window) diff --git a/docker-compose.caddy.yml b/docker-compose.caddy.yml index 185c33c..6fc3491 100644 --- a/docker-compose.caddy.yml +++ b/docker-compose.caddy.yml @@ -8,6 +8,7 @@ services: - USER_UID=1000 - USER_GID=1000 restart: always + command: "serve.sh" volumes: - $PWD:/usr/voka - node_modules:/usr/voka/node_modules diff --git a/serve.sh b/serve.sh new file mode 100644 index 0000000..fbf6065 --- /dev/null +++ b/serve.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +# Собираем production-бандл +echo "[VOKA] Building production bundle..." +#npm run build-prod +nodemon --watch src --watch vendors -e ts,js,scss --exec npm run build & + +# Проверим, что установлен serve, если нет — установим +if ! npx --no-install serve -v >/dev/null 2>&1; then + echo "[VOKA] Installing 'serve'..." + npm install -g serve +fi + +# Запускаем локальный сервер на 5000 порту (или любой другой) +echo "[VOKA] Starting server at http://localhost:8080" +serve -s demo -l 8080 diff --git a/src/internal/player/VokaCorePlayer.ts b/src/internal/player/VokaCorePlayer.ts index 044cecd..5a145ba 100755 --- a/src/internal/player/VokaCorePlayer.ts +++ b/src/internal/player/VokaCorePlayer.ts @@ -407,24 +407,26 @@ namespace VokaCorePlayer { // не подходит для хромкаст. Временное решение для сохранения исходника. this.player._originalSrc = playableContent this.bus.publish(VokaBusEvent.switchContent(content)) - this.bus.publish( - VokaBusEvent.adsMarkersSet({ - items: [ - { - index: 0, - timestamp: 10 - }, - { - index: 1, - timestamp: 60 - }, - { - index: 2, - timestamp: 90 - } - ] - }) - ) + + // MARK: Пример задания рекламных точек + // this.bus.publish( + // VokaBusEvent.adsMarkersSet({ + // items: [ + // { + // index: 0, + // timestamp: 10 + // }, + // { + // index: 1, + // timestamp: 60 + // }, + // { + // index: 2, + // timestamp: 90 + // } + // ] + // }) + // ) return Promise.resolve() } @@ -497,9 +499,6 @@ namespace VokaCorePlayer { private initOptions(options: CorePlayerOptions.IOptions): VideoJsPlayerOptions { // Required player's plugins. const plugins: Record = { } - //plugins.vokaStatisticsPlugin = {} - //plugins.vokaAdvertisementPlugin = {} - //plugins.vokaCaptionsPlugin = {} // plugins.vokaKeyboardPlugin = { // skip: { // forward: 5, @@ -579,7 +578,7 @@ namespace VokaCorePlayer { language: 'ru', children: childrenComponents, // MARK: Пример постера - poster: 'https://i.ytimg.com/vi/u3q7GGLpiqk/maxresdefault.jpg', + // poster: 'https://i.ytimg.com/vi/u3q7GGLpiqk/maxresdefault.jpg', responsive: true, breakpoints: { tiny: undefined, @@ -625,7 +624,7 @@ namespace VokaCorePlayer { previewPopup: { imageCallback: (percent: number) => { return '' - // timeline from voka (for test) + // MARK: пример ссылки на превью // return `https://streaming.voka.tv/vod_preview/velcom/4W17sRxFu8eCAps3kdTxJrFk7d46DNmum_320x180.jpeg?preview_pos=${percent}`; }, }, diff --git a/src/public/VokaPlayer.ts b/src/public/VokaPlayer.ts index e4335da..b569557 100644 --- a/src/public/VokaPlayer.ts +++ b/src/public/VokaPlayer.ts @@ -1,3 +1,4 @@ +import videojs from "video.js"; import { VokaOptions } from './@types' import { IVokaPlayer } from './IVokaPlayer' import EncryptSystem from '@/internal/utils/EncryptSystem' @@ -10,6 +11,11 @@ import BrowserUtils from '@/internal/utils/BrowserUtils' import VokaGlobalFunctions from '@/public/VokaGlobalFunctions' import ObjectUtils from '@/internal/utils/ObjectUtils' +function log(...args) { + const now = new Date().toISOString(); // формат ISO: 2025-08-25T10:15:30.123Z + videojs.log(`[${now}][VokaPlayer]`, ...args); +} + namespace VokaPlayer { let playerFeatures: SupportedCodecs.ISelectProtocolResult | null = null @@ -29,8 +35,9 @@ namespace VokaPlayer { element: HTMLElement | string | null, creationOptions: VokaOptions.IOptions | null ): IVokaPlayer | VokaGlobalFunctions { + log("Player constructor started") + let htmlElement: HTMLElement | null = null - let htmlElement: HTMLElement | null = null if (typeof document !== 'undefined') { if (typeof element === 'string') { htmlElement = document.getElementById(element) as HTMLElement @@ -57,13 +64,16 @@ namespace VokaPlayer { const id = GUIDUtils.globalUnique const options = ObjectUtils.mergeDeep({}, defaultOptions, creationOptions, { loggerId: id } as VokaOptions.IOptions) - return new VokaPlayerImpl( + const player = new VokaPlayerImpl( id, // id GUIDUtils.generateGUID(), // SessionGUID options, // merge options default and passed from outside detectFeatures(options), VokaCorePlayer.createPlayer(htmlElement), ) + log("Player constructor finished") + + return player } export async function detectFeatures(options: VokaOptions.IOptions): Promise { diff --git a/src/public/VokaPlayerImpl.ts b/src/public/VokaPlayerImpl.ts index 28355fc..2af5384 100644 --- a/src/public/VokaPlayerImpl.ts +++ b/src/public/VokaPlayerImpl.ts @@ -1,3 +1,4 @@ +import { BusEvent } from "ts-bus/types"; import videojs from 'video.js' import { VokaOptions } from './@types' import { IAudioTrack, IQuality, ISubtitle, ITimeRange, IVideoInfo, IVokaPlayer } from './IVokaPlayer' @@ -16,6 +17,11 @@ import { EventBus } from 'ts-bus' import { VokaError } from '@/public/models/VokaError' import VokaBusEvent, { adError, adFinished, adStarted } from '@/internal/events/VokaBusEvent' +function log(...args) { + const now = new Date().toISOString() + videojs.log(`[${ now }][VokaPlayerImpl]`, ...args) +} + export class VokaPlayerImpl implements IVokaPlayer { private static readonly version = '0.0.3' @@ -35,11 +41,11 @@ export class VokaPlayerImpl implements IVokaPlayer { public static playerVersion(): string { return `${this.version}.${this.build}` } constructor( - id: number, - sessionGUID: string, - options: VokaOptions.IOptions, - features: Promise, - playerCreationClosure: VokaCorePlayer.PlayerCreationClosure + id: number, + sessionGUID: string, + options: VokaOptions.IOptions, + features: Promise, + playerCreationClosure: VokaCorePlayer.PlayerCreationClosure, ) { if (!options.log) videojs.log.level("off") @@ -53,47 +59,115 @@ export class VokaPlayerImpl implements IVokaPlayer { const playerOptions = this.prepareCoreOptions(options) this.initializePromise = new Promise( - (resolve, reject) => { - Promise.all([features, playerCreationClosure(playerOptions)]).then( - ([features, player]) => { - this._player = player - this._features = features - this.initialize(player, features).then((result) => { resolve() }, reject) - if (!!options.log) { - videojs.log("[OPTIONS PREPARE]:" + this.uniqID, playerOptions); + (resolve, reject) => { + Promise.all([features, playerCreationClosure(playerOptions)]).then( + ([features, player]) => { + this._player = player + this._features = features + this.initialize(player, features).then((result) => { + resolve() + }, reject) - // Применяем декоратор ко всем public методам класса - const allMethods = Object.getOwnPropertyNames(this.__proto__) - const toRemove = ["constructor", "prepareCoreOptions", "load", "initialize", "safeStringify", "logMethodCall"] - for (const propertyName of allMethods.filter(method => !toRemove.includes(method))) { - const descriptor = Object.getOwnPropertyDescriptor(this.__proto__, propertyName); - if (descriptor && typeof descriptor.value === 'function') { - Object.defineProperty(this.__proto__, propertyName, this.logMethodCall(this.__proto__, propertyName, descriptor)); + const INSTANCE_DECORATED = Symbol("instanceDecorated") + const DECORATED_METHODS = Symbol("decoratedMethods") + + if (options.log && !(this as any)[INSTANCE_DECORATED]) { + log("[OPTIONS PREPARE]:" + this.uniqID, playerOptions) + + const proto = Object.getPrototypeOf(this) + const toSkip = new Set([ + "constructor", + "prepareCoreOptions", + "load", + "initialize", + "safeStringify", + "logMethodCall", + ]) + + const decorated: Set = new Set() + Object.defineProperty(this, DECORATED_METHODS, { + value: decorated, + configurable: true, + }) + + for (const name of Object.getOwnPropertyNames(proto)) { + if (toSkip.has(name)) continue + + const d = Object.getOwnPropertyDescriptor(proto, name) + if (!d || typeof d.value !== "function") continue + + if (Object.prototype.hasOwnProperty.call(this, name)) continue + + const instanceDesc: PropertyDescriptor = { + configurable: true, + enumerable: d.enumerable ?? false, + writable: true, + value: d.value, + } + + const wrappedDesc = this.logMethodCall(proto, name, instanceDesc) + wrappedDesc.value.__logged = true + decorated.add(name) + + Object.defineProperty(this, name, wrappedDesc) + } + + for (const name of Object.getOwnPropertyNames(this)) { + if (toSkip.has(name)) continue + if (decorated.has(name)) continue + + const d = Object.getOwnPropertyDescriptor(this, name) + if (!d || typeof d.value !== "function") continue + + if (d.value.__logged) continue + + const instanceDesc: PropertyDescriptor = { + configurable: true, + enumerable: d.enumerable ?? true, + writable: true, + value: d.value, + } + + const wrappedDesc = this.logMethodCall(this, name, instanceDesc) + wrappedDesc.value.__logged = true + decorated.add(name) + + Object.defineProperty(this, name, wrappedDesc) + } + + Object.defineProperty(this, INSTANCE_DECORATED, { + value: true, + configurable: true, + }) } - } - } - } - ) - } + }, + ) + }, ) this.initializePromise.catch(e => { console.error(e) this.destroy() - }); + }) this.bus.subscribe( - VokaBusEvent.adStarted, - event => { this._adIsPlaying = true }, + VokaBusEvent.adStarted, + event => { + this._adIsPlaying = true + }, ) this.bus.subscribe( - VokaBusEvent.adFinished, - event => { this._adIsPlaying = false }, + VokaBusEvent.adFinished, + event => { + this._adIsPlaying = false + }, ) this.bus.subscribe( - VokaBusEvent.adError, - event => { this._adIsPlaying = false }, + VokaBusEvent.adError, + event => { + this._adIsPlaying = false + }, ) } @@ -102,7 +176,7 @@ export class VokaPlayerImpl implements IVokaPlayer { private prepareCoreOptions(options: VokaOptions.IOptions): CorePlayerOptions.IOptions { if (!!options.log) { - videojs.log("[OPTIONS]:" + this.uniqID, options); + log("[OPTIONS]:" + this.uniqID, options) } return { controls: { @@ -247,40 +321,40 @@ export class VokaPlayerImpl implements IVokaPlayer { } private static safeStringify(obj: any, indent = 2): string { - const cache = new Set(); + const cache = new Set() return JSON.stringify(obj, (key, value) => { - if (typeof value === 'object' && value !== null) { - if (cache.has(value)) { - return '[Circular]'; + if (typeof value === "object" && value !== null) { + if (cache.has(value)) { + return "[Circular]" + } + cache.add(value) } - cache.add(value); - } - // Убираем ненужные большие объекты (например, window или DOM-элементы) - if (value instanceof Window) return '[Window]'; - if (value instanceof Document) return '[Document]'; - if (value instanceof HTMLElement) return `[HTMLElement: ${value.tagName}]`; - return value; - }, indent); - } + // Убираем ненужные большие объекты (например, window или DOM-элементы) + if (value instanceof Window) return "[Window]" + if (value instanceof Document) return "[Document]" + if (value instanceof HTMLElement) return `[HTMLElement: ${ value.tagName }]` + return value + }, indent) + } private logMethodCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) { - const originalMethod = descriptor.value; + const originalMethod = descriptor.value const id = this.uniqID - descriptor.value = function(...args: any[]) { - const res = originalMethod.apply(this, args); + descriptor.value = function (...args: any[]) { + const res = originalMethod.apply(this, args) if (this.uniqID === id) { - const logKey ="[VokaPlayerImpl]:[ID=" + this.uniqID + "]:[" + propertyKey + "]:" - videojs.log(logKey, - " Arguments:" + VokaPlayerImpl.safeStringify(args), - " Result:" + VokaPlayerImpl.safeStringify(res) - ); - console.trace(logKey) + const logKey = "[ID=" + this.uniqID + "]:[" + propertyKey + "]:" + log(logKey, + " Arguments:" + VokaPlayerImpl.safeStringify(args), + " Result:" + VokaPlayerImpl.safeStringify(res), + ) + console.trace(logKey, "stacktrace: ") } return res - }; + } - return descriptor; + return descriptor } // MARK: - IVokaPlayer implementation @@ -413,7 +487,7 @@ export class VokaPlayerImpl implements IVokaPlayer { getAdIsPlaying(): boolean { return this._adIsPlaying } cancelAdPlayback() { - this.bus.publish(VokaBusEvent.adCancelPlaybackEvent) + this.bus.publish(VokaBusEvent.adCancelPlaybackEvent as BusEvent) } getAbsoluteCurrentTime(): number | null {