diff --git a/configs/lite-server-config.json b/configs/lite-server-config.json index 3ffb085..8ab3a1a 100644 --- a/configs/lite-server-config.json +++ b/configs/lite-server-config.json @@ -1,6 +1,9 @@ { "port": 8080, "server": { - "baseDir": "./demo" + "baseDir": "./demo", + "routes": { + "/v1/devices.json": "v1/devices.json" + } } } \ No newline at end of file diff --git a/demo/example.html b/demo/example.html new file mode 100644 index 0000000..2060b4d --- /dev/null +++ b/demo/example.html @@ -0,0 +1,38 @@ + + + + + Title + + + +

Hello world

+
+ + + \ No newline at end of file diff --git a/demo/index.html b/demo/index.html index 9dcf771..344731f 100644 --- a/demo/index.html +++ b/demo/index.html @@ -9,15 +9,20 @@

Hello world

\ No newline at end of file diff --git a/demo/index_mp4.html b/demo/index_mp4.html new file mode 100644 index 0000000..2060b4d --- /dev/null +++ b/demo/index_mp4.html @@ -0,0 +1,38 @@ + + + + + Title + + + +

Hello world

+
+ + + \ No newline at end of file diff --git a/demo/v1/channels/hls-998f5396-c9dd-4a1e-82c7-0aec531fc015/stream.json b/demo/v1/channels/hls-998f5396-c9dd-4a1e-82c7-0aec531fc015/stream.json new file mode 100644 index 0000000..b20856c --- /dev/null +++ b/demo/v1/channels/hls-998f5396-c9dd-4a1e-82c7-0aec531fc015/stream.json @@ -0,0 +1,6 @@ +{ + "data": { + "url": "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8", + "subtitles": "https://raw.githubusercontent.com/videojs/video.js/c7298d40a4632a6e9dfcd5a2f5cc3bbe92a78744/docs/examples/elephantsdream/captions.ru.vtt" + } +} \ No newline at end of file diff --git a/demo/v1/channels/mp4-0aec531fc015/stream.json b/demo/v1/channels/mp4-0aec531fc015/stream.json new file mode 100644 index 0000000..a7dd115 --- /dev/null +++ b/demo/v1/channels/mp4-0aec531fc015/stream.json @@ -0,0 +1,6 @@ +{ + "data": { + "url": "https://vjs.zencdn.net/v/oceans.mp4", + "subtitles": "https://raw.githubusercontent.com/videojs/video.js/c7298d40a4632a6e9dfcd5a2f5cc3bbe92a78744/docs/examples/elephantsdream/captions.ru.vtt" + } +} \ No newline at end of file diff --git a/demo/v1/devices.json b/demo/v1/devices.json new file mode 100644 index 0000000..0f216a3 --- /dev/null +++ b/demo/v1/devices.json @@ -0,0 +1,5 @@ +{ + "data": { + "device_token": "SOME_DEVICE_TOKEN" + } +} \ No newline at end of file diff --git a/src/internal/drm/dash/VokaDash.ts b/src/internal/drm/dash/VokaDash.ts index 4c21b25..5edbc4e 100644 --- a/src/internal/drm/dash/VokaDash.ts +++ b/src/internal/drm/dash/VokaDash.ts @@ -355,7 +355,7 @@ export default class VokaDash { } }, debug: { - logLevel: options.debug.playerLogs ? Debug.LOG_LEVEL_DEBUG : Debug.LOG_LEVEL_NONE + logLevel: (options.debug != undefined && options.debug.playerLogs) ? Debug.LOG_LEVEL_DEBUG : Debug.LOG_LEVEL_NONE } }) } diff --git a/src/internal/events/events.ts b/src/internal/events/events.ts index 6868f71..a668039 100644 --- a/src/internal/events/events.ts +++ b/src/internal/events/events.ts @@ -1,6 +1,6 @@ import { createEventDefinition } from 'ts-bus' import { Quality } from '@/public/models/ILoadOptions' -import { QualityData } from '../player/VokaPlayerCore' +import VokaCorePlayer from '../player/VokaCorePlayer' import { VokaInternalErrorComponent, VokaInternalErrorData, @@ -17,7 +17,7 @@ export type ErrorEventData = { // Quality export const qualitiesParsed = createEventDefinition<{ - qualities: QualityData[] + qualities: VokaCorePlayer.QualityData[] }>()('quality.parsed') export const qualityChange = createEventDefinition<{ diff --git a/src/internal/player/VokaCorePlayer.ts b/src/internal/player/VokaCorePlayer.ts new file mode 100644 index 0000000..34b50c2 --- /dev/null +++ b/src/internal/player/VokaCorePlayer.ts @@ -0,0 +1,385 @@ +import videojs, { num } from 'video.js' +import Player from 'video.js/dist/types/player' +import { EventBus } from 'ts-bus' +import { VokaPlayerEvent} from '@/public/IVokaPlayer' +import { AutoplayChecker } from '@/internal/utils/AutoplayChecker' +import { Quality } from '@/public/models/ILoadOptions' +import * as languages from '@/languages.json' +import '@/plugins/VokaKeyboardPlugin' +import '@/plugins/VokaHeartbeatPlugin' +import '@/plugins/VokaMetricsPlugin' +import '@/plugins/VokaMagicRemotePlugin' + +import '@/components/Skin' +import { VokaContentType } from '@/public/models/VokaContentType' +import VokaWebOSTech from '@/internal/player/native/webos/tech/VokaWebOSTech' +import VokaTizenTech from '@/internal/player/native/tizen/tech/VokaTizenTech' +import VokaAppleTech from '@/internal/player/native/apple/tech/VokaAppleTech' +import VokaMp4Tech from '@/internal/player/native/mp4/tech/VokaMp4Tech' +import VokaHlsTech from '@/internal/player/native/hls/tech/VokaHlsTech' +import VokaDashTech from '@/internal/player/native/dash/tech/VokaDashTech' +import VokaEmptyTech from '@/internal/player/native/empty/VokaEmptyTech' +import { Promise } from 'es6-promise' + +namespace VokaCorePlayer { + + type VideoJsPlayerOptions = Parameters[1] + type PlayerReadyHandler = (player: CorePlayer) => void + + const playbackRates = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] + const Dom = videojs.dom + const DivID = 'vokaPlayerVideoTag' + + export type QualityData = { + index: number + bitrate: number + quality: Quality + label?: string + } + + export interface IMetrics { + url: string + interval: number | null + params: { [key: string]: string } + } + + export enum DRMType { + FAIRPLAY = 'fairplay', + PLAYREADY = 'playready', + WIDEWINE = 'widewine', + } + + export interface IDRMConfig { + type: DRMType + certificateUrl: string // certificate or serverURL + licenseUrl: string | null, // licenseUrl + headers: [any] | null + } + + export interface IContent { + url: string, + type: VokaContentType, + drmConfig: IDRMConfig | null + subtitlesUrl: string | null + metrics: IMetrics | null + } + + export class CorePlayer { + + private player!: Player + private autoPlaySupported!: boolean + private readonly stateEmitter: EventBus + + // Constructor + constructor(element: HTMLElement, callback?: PlayerReadyHandler) { + const readyCallback = typeof callback === 'undefined' ? null : callback + this.stateEmitter = new EventBus() + /*this.stateEmitter.subscribe( + playerStateChange, + (data) => (this.state_ = data.payload.value) + )*/ + + AutoplayChecker.isAutoplaySupported((supported) => { + this.setupPlayer(element, supported, readyCallback) + }) + } + + // MARK: - Public + + public get isPaused(): boolean { return this.player.paused() } + + public get currentTime(): number { return this.player.currentTime() || 0 } + + public get duration(): number { return this.player.duration() || 0 } + + public get volume(): number { return this.player.volume() || 0 } + + public setVolume(value: number) { this.player.volume(value) } + + public get bufferLength(): number { return this.player.bufferedPercent() } + + public get isBuffering(): boolean { return false } + + public seek(seconds: number): void { this.player.currentTime(seconds) } + + public mute(on: boolean) { this.player.muted(on) } + + public play(): Promise | undefined { return this.player.play() } + + public pause() { return this.player.pause() } + + public stop() { return this.player.pause() } + + public get isMuted(): boolean { return this.player.muted() || false } + + public get isLive(): boolean { + const duration = this.player.duration() + if (duration == undefined) { return true } + if (isNaN(duration)) { return false } + if (isFinite(duration)) { return false } + return true + } + + public async load(content: IContent): Promise { + + let playableContent = null + const drm = content.drmConfig + switch(content.type) { + case VokaContentType.HLS: + playableContent = { + type: "application/x-mpegURL", + } + break + case VokaContentType.MP4: + playableContent = { + type: "video/mp4", + } + break + case VokaContentType.DASH: + playableContent = { + type: "application/dash+xml", + } + break + case VokaContentType.WIDEVINE: + if (drm != null && drm.type == DRMType.WIDEWINE) { + playableContent = { + type: "application/dash+xml", + keySystemOptions: [{ + name: "com.widevine.alpha", + options: { + serverURL: drm.certificateUrl, + httpRequestHeaders: drm.headers, + priority: 0, + } + }] + } + } + break + case VokaContentType.PLAYREADY: + if (drm != null && drm.type == DRMType.PLAYREADY) { + playableContent = { + type: "application/dash+xml", + keySystemOptions: [{ + name: "com.microsoft.playready", + options: { + serverURL: drm.certificateUrl, + httpRequestHeaders: drm.headers, + priority: 0, + } + }] + } + } + break + case VokaContentType.FAIRPLAY: + if (drm != null && drm.type == DRMType.FAIRPLAY) { + playableContent = { + type: "application/x-mpegURL", + protection: { + keySystem: "com.apple.fps.1_0", + certificateUrl: drm.certificateUrl, + licenseUrl: drm.licenseUrl, + }, + } + } + break + case VokaContentType.AES: + playableContent = { + type: "application/x-mpegURL", + } + break + } + + if (playableContent != null) { + playableContent['src'] = content.url + playableContent['content'] = content.type + playableContent['metrics'] = content.metrics + playableContent['subtitlesUrl'] = content.subtitlesUrl + + this.player.src(playableContent) + return Promise.resolve() + } + return Promise.reject() + } + + // MARK: - Private + + private setupPlayer( + element: HTMLElement, + autoPlaySupported: boolean, + callback: PlayerReadyHandler | null + ) { + this.autoPlaySupported = autoPlaySupported + + // Create main container for player. + const playerContainer = Dom.createEl( + 'video', + { id: `${DivID}_${element.id}_` }, + {} + ) + element.appendChild(playerContainer) + + videojs.log.level('debug') + const options = this.initOptions() + const player = videojs(playerContainer, 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 = { } + //plugins.vokaQualityPlugin = {} + //plugins.vokaStatisticsPlugin = {} + //plugins.vokaAdvertisementPlugin = {} + //plugins.vokaCaptionsPlugin = {} + // plugins.vokaKeyboardPlugin = { + // skip: { + // forward: 5, + // backward: 5, + // }, + // } + plugins.vokaMagicRemotePlugin = { + skip: { + forward: 5, + backward: 5, + }, + } + plugins.vokaHeartbeatPlugin = {} // Хартбит Player.Heartbeat + plugins.vokaMetricsPlugin = {} // Метрики Player.Metrics + + const childrenComponents = [ + // Non-visual component, not in UI layer list. + 'mediaLoader' // Required loader, that load tech list! + ] + + const skinChildren = [ + 'Skin', + 'resizeManager', + 'LoadingSpinner', + 'PosterImage', + 'RestrictionBox', + 'liveTracker', + ] + + childrenComponents.push(...skinChildren) + + // Settings for player. + return { + techOrder: [ + VokaWebOSTech.TECH_NAME, + VokaTizenTech.TECH_NAME, + VokaAppleTech.TECH_NAME, + VokaMp4Tech.TECH_NAME, + VokaHlsTech.TECH_NAME, + VokaDashTech.TECH_NAME, + VokaEmptyTech.TECH_NAME + ], // Order for teches important. + plugins, // Attached plugins for player. + languages, + language: 'ru', + children: childrenComponents, + poster: undefined, + responsive: true, + breakpoints: { + tiny: undefined, + xsmall: undefined, + small: 559, + medium: 1002, + large: undefined, + xlarge: undefined, + huge: Infinity + }, + playbackRates, + tracks: [ + { + kind: "subtitles", + src: "https://raw.githubusercontent.com/videojs/video.js/c7298d40a4632a6e9dfcd5a2f5cc3bbe92a78744/docs/examples/elephantsdream/captions.ru.vtt", + srclang: 'ru', + label: 'russian', + }, + { + kind: "subtitles", + src: "https://raw.githubusercontent.com/videojs/video.js/c7298d40a4632a6e9dfcd5a2f5cc3bbe92a78744/docs/examples/shared/example-captions.vtt", + srclang: 'en', + label: 'english', + } + ], + qualities: [ + { + res: 480, + label: "480p", + }, + { + res: 720, + label: "720p", + }, + { + res: 1080, + label: "1080p", + } + ], + audio: [ + { + res: "ru", + label: "Русский", + }, + { + res: "en", + label: "Анлийский", + }, + ], + skip: { + forward: 5, + backward: 5, + }, + controls: { + play: true, + replay: true, + volume: { + inline: true, + }, + progress: true, + zoom: true, + fullscreen: true, + }, + selection: true, + previewPopup: { + imageCallback: (percent: number) => { + return '' + // timeline from voka (for test) + // return `https://streaming.voka.tv/vod_preview/velcom/4W17sRxFu8eCAps3kdTxJrFk7d46DNmum_320x180.jpeg?preview_pos=${percent}`; + }, + }, + enableDocumentPictureInPicture: true, + } + } + + private setupAttachListeners(player: Player) { + // Добавляем листенеры здесь + player.on('loadstart', () => { + // TODO: удалить, после подключения субтитров не из внешенго источника + document.querySelector('.vjs-tech')?.setAttribute('crossOrigin', 'anonymous') + }) + } + } +} + +export default VokaCorePlayer \ No newline at end of file diff --git a/src/internal/player/VokaPlayerCore.ts b/src/internal/player/VokaPlayerCore.ts deleted file mode 100644 index d619910..0000000 --- a/src/internal/player/VokaPlayerCore.ts +++ /dev/null @@ -1,294 +0,0 @@ -import videojs from 'video.js' -import Player from 'video.js/dist/types/player' -import { EventBus } from 'ts-bus' -import { VokaPlayerEvent} from '@/public/IVokaPlayer' -import { AutoplayChecker } from '@/internal/utils/AutoplayChecker' -import { Quality } from '@/public/models/ILoadOptions' -import * as languages from '@/languages.json' -import '@/plugins/VokaKeyboardPlugin' -import '@/plugins/VokaHeartbeatPlugin' -import '@/plugins/VokaMetricsPlugin' -import '@/plugins/VokaMagicRemotePlugin' - -import '@/components/Skin' -import { VokaContentType } from '@/public/models/VokaContentType' -import VokaWebOSTech from '@/internal/player/native/webos/tech/VokaWebOSTech' -import VokaTizenTech from '@/internal/player/native/tizen/tech/VokaTizenTech' -import VokaAppleTech from '@/internal/player/native/apple/tech/VokaAppleTech' -import VokaMp4Tech from '@/internal/player/native/mp4/tech/VokaMp4Tech' -import VokaHlsTech from '@/internal/player/native/hls/tech/VokaHlsTech' -import VokaDashTech from '@/internal/player/native/dash/tech/VokaDashTech' -import VokaEmptyTech from '@/internal/player/native/empty/VokaEmptyTech' - - -type VideoJsPlayerOptions = Parameters[1] -type PlayerReadyHandler = (player: VokaPlayerCore) => void - -const playbackRates = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] -const Dom = videojs.dom - -export type QualityData = { - index: number - bitrate: number - quality: Quality - label?: string -} - -export class VokaPlayerCore { - - static readonly DivID = 'vokaPlayerVideoTag' - - private player!: Player - private autoPlaySupported!: boolean - private readonly stateEmitter: EventBus - - // Constructor - constructor(element: HTMLElement, callback?: PlayerReadyHandler) { - const readyCallback = typeof callback === 'undefined' ? null : callback - this.stateEmitter = new EventBus() - /*this.stateEmitter.subscribe( - playerStateChange, - (data) => (this.state_ = data.payload.value) - )*/ - - AutoplayChecker.isAutoplaySupported((supported) => { - this.setupPlayer(element, supported, readyCallback) - }) - } - - private setupPlayer( - element: HTMLElement, - autoPlaySupported: boolean, - callback: PlayerReadyHandler | null - ) { - this.autoPlaySupported = autoPlaySupported - - // Create main container for player. - const playerContainer = Dom.createEl( - 'video', - { id: `${VokaPlayerCore.DivID}_${element.id}_` }, - {} - ) - element.appendChild(playerContainer) - - videojs.log.level('debug') - const options = this.initOptions() - const player = videojs(playerContainer, 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 = { } - //plugins.vokaQualityPlugin = {} - //plugins.vokaStatisticsPlugin = {} - //plugins.vokaAdvertisementPlugin = {} - //plugins.vokaCaptionsPlugin = {} - // plugins.vokaKeyboardPlugin = { - // skip: { - // forward: 5, - // backward: 5, - // }, - // } - plugins.vokaMagicRemotePlugin = { - skip: { - forward: 5, - backward: 5, - }, - } - plugins.vokaHeartbeatPlugin = {} // Хартбит Player.Heartbeat - plugins.vokaMetricsPlugin = {} // Метрики Player.Metrics - - const childrenComponents = [ - // Non-visual component, not in UI layer list. - 'mediaLoader' // Required loader, that load tech list! - ] - - const skinChildren = [ - 'Skin', - 'resizeManager', - 'LoadingSpinner', - 'PosterImage', - 'RestrictionBox', - 'liveTracker', - ] - - childrenComponents.push(...skinChildren) - - // Settings for player. - return { - techOrder: [ - VokaWebOSTech.TECH_NAME, - VokaTizenTech.TECH_NAME, - VokaAppleTech.TECH_NAME, - VokaMp4Tech.TECH_NAME, - VokaHlsTech.TECH_NAME, - VokaDashTech.TECH_NAME, - VokaEmptyTech.TECH_NAME - ], // Order for teches important. - plugins, // Attached plugins for player. - languages, - language: 'ru', - children: childrenComponents, - poster: undefined, - responsive: true, - breakpoints: { - tiny: undefined, - xsmall: undefined, - small: 559, - medium: 1002, - large: undefined, - xlarge: undefined, - huge: Infinity - }, - playbackRates, - sources: [ - // Dash - /*{ - src: "https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd", - content: VokaContentType.DASH, - type: "application/dash+xml" - }*/ - // { src: "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8", type: "application/x-mpegURL" } - // { src: "https://dai.google.com/linear/hls/event/rtcMlf4RTvOEkaudeany5w/master.m3u8?iu=/4128/CBS.NY.OTT", type: "application/x-mpegURL" } - /*{ - src: "https://vjs.zencdn.net/v/oceans.mp4", - type: "video/mp4", - parameters: { - stream: { - url: "https://vjs.zencdn.net/v/oceans.mp4" - } - } - }*/ - // Apple Fairplay DRM - /*{ - src: "https://codeeducation.akamaized.net/code/fullcycle/devops_20/01/01_introducao.mp4/fp/fairplay.m3u8", - content: VokaContentType.FAIRPLAY, - protection: { - keySystem: "com.apple.fps.1_0", - certificateUrl: "https://codeeducation.akamaized.net/fairplay.cer", - licenseUrl: "https://fps.ezdrm.com/api/licenses/F6B15258-BC92-49EB-9CDF-DE9F121C13A5?customdata=MTQ0OmFyZ2VudGluYWx1aXpAZ21haWwuY29tOjY3MTE6Y291cnNlOmNvZGU=" - } - }*/ - // Dash WideWine DRM - /*{ - src: "https://media.axprod.net/TestVectors/Cmaf/protected_1080p_h264_cbcs/manifest.mpd", - content: VokaContentType.WIDEVINE, - keySystemOptions: [{ - name: "com.widevine.alpha", - options: { - "serverURL": "https://drm-widevine-licensing.axtest.net/AcquireLicense", - "httpRequestHeaders": { - "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICIzMDJmODBkZC00MTFlLTQ4ODYtYmNhNS1iYjFmODAxOGEwMjQiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAicm9LQWcwdDdKaTFpNDNmd3YremZ0UT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ._NfhLVY7S6k8TJDWPeMPhUawhympnrk6WAZHOVjER6M" - }, - priority: 0 - } - }] - }*/ - // Dash Playready DRM - /*{ - src: "https://media.axprod.net/TestVectors/Cmaf/protected_1080p_h264_cbcs/manifest.mpd", - content: VokaContentType.PLAYREADY, - keySystemOptions: [{ - name: "com.microsoft.playready", - options: { - "serverURL": "https://drm-widevine-licensing.axtest.net/AcquireLicense", - "httpRequestHeaders": { - "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICIzMDJmODBkZC00MTFlLTQ4ODYtYmNhNS1iYjFmODAxOGEwMjQiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAicm9LQWcwdDdKaTFpNDNmd3YremZ0UT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ._NfhLVY7S6k8TJDWPeMPhUawhympnrk6WAZHOVjER6M" - }, - priority: 0 - } - }] - }*/ - ], - tracks: [ - { - kind: "subtitles", - src: "https://raw.githubusercontent.com/videojs/video.js/c7298d40a4632a6e9dfcd5a2f5cc3bbe92a78744/docs/examples/elephantsdream/captions.ru.vtt", - srclang: 'ru', - label: 'russian', - }, - { - kind: "subtitles", - src: "https://raw.githubusercontent.com/videojs/video.js/c7298d40a4632a6e9dfcd5a2f5cc3bbe92a78744/docs/examples/shared/example-captions.vtt", - srclang: 'en', - label: 'english', - } - ], - qualities: [ - { - res: 480, - label: "480p", - }, - { - res: 720, - label: "720p", - }, - { - res: 1080, - label: "1080p", - } - ], - audio: [ - { - res: "ru", - label: "Русский", - }, - { - res: "en", - label: "Анлийский", - }, - ], - skip: { - forward: 5, - backward: 5, - }, - controls: { - play: true, - replay: true, - volume: { - inline: true, - }, - progress: true, - zoom: true, - fullscreen: true, - }, - selection: true, - previewPopup: { - imageCallback: (percent: number) => { - return '' - // timeline from voka (for test) - // return `https://streaming.voka.tv/vod_preview/velcom/4W17sRxFu8eCAps3kdTxJrFk7d46DNmum_320x180.jpeg?preview_pos=${percent}`; - }, - }, - enableDocumentPictureInPicture: true, - } - } - - private setupAttachListeners(player: Player) { - // Добавляем листенеры здесь - player.on('loadstart', () => { - // TODO: удалить, после подключения субтитров не из внешенго источника - document.querySelector('.vjs-tech')?.setAttribute('crossOrigin', 'anonymous') - }) - } -} \ No newline at end of file diff --git a/src/internal/player/native/apple/sourcehandler/VokaAppleSourceHandler.ts b/src/internal/player/native/apple/sourcehandler/VokaAppleSourceHandler.ts index 9ad25fd..c59ca38 100644 --- a/src/internal/player/native/apple/sourcehandler/VokaAppleSourceHandler.ts +++ b/src/internal/player/native/apple/sourcehandler/VokaAppleSourceHandler.ts @@ -5,7 +5,7 @@ import { VokaOptionsType } from '../../VokaSourceHandler' import { VokaContentType } from '@/public/models/VokaContentType' -import { isNativePlayback } from '@/internal/utils/PlatformCapabilities' +import PlatformCapabilities from '@/internal/utils/PlatformCapabilities' import VokaAppleTech from '../tech/VokaAppleTech' const Tech = videojs.getTech('Tech') @@ -38,7 +38,7 @@ export default class VokaAppleSourceHandler extends VokaSourceHandler { return '' } - if (isNativePlayback()) { + if (PlatformCapabilities.isNativePlayback()) { return 'probably' } diff --git a/src/internal/player/native/dash/tech/VokaDashTech.ts b/src/internal/player/native/dash/tech/VokaDashTech.ts index 662ec41..4c3a0ce 100644 --- a/src/internal/player/native/dash/tech/VokaDashTech.ts +++ b/src/internal/player/native/dash/tech/VokaDashTech.ts @@ -1,7 +1,7 @@ import videojs from "video.js" import VokaEvent from '@/constants/VokaEvent' import VokaDashSourceHandler from '../sourcehandler/VokaDashSourceHandler' -import { isSupportedMSE } from '@/internal/utils/PlatformCapabilities' +import PlatformCapabilities from '@/internal/utils/PlatformCapabilities' import { DashContext } from '../processors/DashContext' import VokaTech from '@/internal/player/native/VokaTech' import { DashErrorType } from '@/internal/drm/dash/VokaDash' @@ -161,7 +161,7 @@ class VokaDashTech extends VokaTech { } return !!( - isSupportedMSE() && + PlatformCapabilities.isSupportedMSE() && VokaDashSourceHandler.VIDEO_TEST_TAG() && VokaDashSourceHandler.VIDEO_TEST_TAG().canPlayType ) diff --git a/src/internal/player/native/hls/tech/VokaHlsTech.ts b/src/internal/player/native/hls/tech/VokaHlsTech.ts index aab579e..bc1845e 100644 --- a/src/internal/player/native/hls/tech/VokaHlsTech.ts +++ b/src/internal/player/native/hls/tech/VokaHlsTech.ts @@ -7,7 +7,7 @@ import * as Fn from '@/internal/utils/fn' import { wait } from '@/monads/Monoids' import VokaHlsSourceHandler from '@/internal/player/native/hls/sourcehandler/VokaHlsSourceHandler' import HlsLoadContext from '@/internal/player/native/hls/processors/HlsLoadContext' -import { isSupportedHlsJs } from '@/internal/utils/PlatformCapabilities' +import PlatformCapabilities, { isSupportedHlsJs } from '@/internal/utils/PlatformCapabilities' import VokaEvent from '@/constants/VokaEvent' import { IVokaSource, VokaSourceHandler } from '../../VokaSourceHandler' import { bus } from '@/internal/events/bus' @@ -399,7 +399,7 @@ class VokaHlsTech extends VokaTech { } return !!( - isSupportedHlsJs && + PlatformCapabilities.isSupportedHlsJs && VokaHlsSourceHandler.VIDEO_TEST_TAG() && VokaHlsSourceHandler.VIDEO_TEST_TAG().canPlayType ) diff --git a/src/internal/utils/BrowserUtils.ts b/src/internal/utils/BrowserUtils.ts index 13620da..34c1557 100644 --- a/src/internal/utils/BrowserUtils.ts +++ b/src/internal/utils/BrowserUtils.ts @@ -194,7 +194,7 @@ namespace BrowserUtils { const userAgent = (navigator.userAgent || '').toLowerCase() const vendor = (navigator.vendor || '').toLowerCase() - [ + const knownBrowsers =[ { name: 'Edge', prefix: ' edge/' }, { name: 'Edge2', prefix: ' edg/' }, { name: 'Opera', prefix: ' opr/' }, @@ -206,7 +206,9 @@ namespace BrowserUtils { { name: 'Chrome', prefix: ' chrome/', vendor: 'google' }, { name: 'Firefox', prefix: ' firefox/' }, { name: 'IE', prefix: ' trident/' } - ].forEach((element) => { + ] + + knownBrowsers.forEach((element) => { if (result.name != null) { return } if (element.vendor != undefined && vendor.indexOf(element.vendor) < 0) { return } diff --git a/src/internal/utils/EMESystem.ts b/src/internal/utils/EMESystem.ts index 0126619..d032f62 100644 --- a/src/internal/utils/EMESystem.ts +++ b/src/internal/utils/EMESystem.ts @@ -17,6 +17,19 @@ namespace EncryptSystem { return 'com.apple.fps.1_0' } } + + export function toString(system: EncryptSystem | null): string { + switch (system) { + case EncryptSystem.playready: + return 'playready' + case EncryptSystem.widevine: + return 'widevine' + case EncryptSystem.fairplay: + return 'fairplay' + default: + return 'none' + } + } } class EMESystem { diff --git a/src/internal/utils/PlatformCapabilities.ts b/src/internal/utils/PlatformCapabilities.ts index 532b058..e8587b8 100644 --- a/src/internal/utils/PlatformCapabilities.ts +++ b/src/internal/utils/PlatformCapabilities.ts @@ -2,43 +2,46 @@ import Hls from 'hls.js' import videojs from 'video.js' const Browser = videojs.browser -/** - * Помимо проверки на MSE, выполняет также проверку - * - * mediaSource.isTypeSupported('video/mp4 codecs="avc1.42E01E,mp4a.40.2"') - */ -const isSupportedHlsJs: boolean = Hls.isSupported() +namespace PlatformCapabilities { -function getMediaSource(): typeof MediaSource | undefined { - return (window as any).MediaSource || (window as any).WebKitMediaSource -} + /** + * Помимо проверки на MSE, выполняет также проверку + * + * mediaSource.isTypeSupported('video/mp4 codecs="avc1.42E01E,mp4a.40.2"') + */ + export const isSupportedHlsJs: boolean = Hls.isSupported() -/** - * поддержка MediaSourceExtension - необходимое и, возможно, достаточное (maybe, т.к. до проверки type) условие для работы и hls.js, и DASH. - * Код проверки из hls.js isSupported() - * но без проверки на isTypeSupported - эту проверку лучше вырполнять не на захардкоденном кодеке, а в canPlayType теча. - */ -const isSupportedMSE = function (): boolean { - const mediaSource = getMediaSource() - if (!mediaSource) { - return false + export function getMediaSource(): typeof MediaSource | undefined { + return (window as any).MediaSource || (window as any).WebKitMediaSource } - const sourceBuffer: any = - self.SourceBuffer || ((self as any).WebKitSourceBuffer as SourceBuffer) - // if SourceBuffer is exposed ensure its API is valid - // safari and old version of Chrome doe not expose SourceBuffer globally so checking SourceBuffer.prototype is impossible - const sourceBufferValidAPI = - !sourceBuffer || - (sourceBuffer.prototype && - typeof sourceBuffer.prototype.appendBuffer === 'function' && - typeof sourceBuffer.prototype.remove === 'function') - return !!sourceBufferValidAPI + /** + * поддержка MediaSourceExtension - необходимое и, возможно, достаточное (maybe, т.к. до проверки type) условие для работы и hls.js, и DASH. + * Код проверки из hls.js isSupported() + * но без проверки на isTypeSupported - эту проверку лучше вырполнять не на захардкоденном кодеке, а в canPlayType теча. + */ + export const isSupportedMSE = function (): boolean { + const mediaSource = getMediaSource() + if (!mediaSource) { + return false + } + const sourceBuffer: any = + self.SourceBuffer || ((self as any).WebKitSourceBuffer as SourceBuffer) + + // if SourceBuffer is exposed ensure its API is valid + // safari and old version of Chrome doe not expose SourceBuffer globally so checking SourceBuffer.prototype is impossible + const sourceBufferValidAPI = + !sourceBuffer || + (sourceBuffer.prototype && + typeof sourceBuffer.prototype.appendBuffer === 'function' && + typeof sourceBuffer.prototype.remove === 'function') + return !!sourceBufferValidAPI + } + + export const isNativePlayback = function (): boolean { + //return !HlsProcessor.isSupported; + return Browser.IS_IOS && !isSupportedHlsJs + } } -const isNativePlayback = function (): boolean { - //return !HlsProcessor.isSupported; - return Browser.IS_IOS && !isSupportedHlsJs -} - -export { isSupportedMSE, isSupportedHlsJs, isNativePlayback } \ No newline at end of file +export default PlatformCapabilities \ No newline at end of file diff --git a/src/internal/utils/SupportedCodecs.ts b/src/internal/utils/SupportedCodecs.ts index 92fb43c..b3fa713 100644 --- a/src/internal/utils/SupportedCodecs.ts +++ b/src/internal/utils/SupportedCodecs.ts @@ -27,8 +27,8 @@ namespace SupportedCodecs { } export enum NativePlayerType { - hls, - mss + hls = "hls", + mss = "mss" } export class SupportedCodecs { diff --git a/src/public/IVokaPlayer.ts b/src/public/IVokaPlayer.ts index 9601582..d14fe71 100644 --- a/src/public/IVokaPlayer.ts +++ b/src/public/IVokaPlayer.ts @@ -1,3 +1,5 @@ +import { VokaOptions } from '@/public/@types' + export enum VokaPlayerEvent { 'DurationChange' = 'durationchange', 'TimeUpdate' = 'timeupdate', @@ -57,73 +59,171 @@ 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 - /// pause playing - pause(): Promise - stop(): Promise +export interface ITimeRange { + start: number + end: number } -/* -load(url: string, options?: ILoadOptions): Promise -loadWithProvider( - provider: IAPIProvider, - options?: ILoadOptions -): Promise -setCurrentTime(value: number): Promise -setVolume(value: number): Promise -setPlaybackRate(value: PlaybackRate): Promise -setViewType(value: string): Promise -setQuality(value: Quality): Promise*/ +export interface IQuality { + bitrate: number | null + width: number | null + height: number | null +} -/* -* 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 -*/ \ No newline at end of file +export interface IAudioTrack { + index: number + lang: string + label: string +} + +export interface ISubtitle { + index: number + lang: string + label: string +} + +export interface IVokaPlayer { + /// register callback function that will be called after player is initialized (it will be called immediately if player is already initialized) + afterInitialize(callback: () => void): void + + /// check if player is initialized + isInitialized(): boolean + + /// get supported streaming protocol name + getProtocol(): string + + /// get supported drm system name + getDrmSystem(): string + + /// check if HLS protocol is supported + isHlsSupported(): boolean + + /// get list of supported video codecs for dash protocol (only among those that were enabled in config) + getVideoCodecs(): [string] + + /// set stream url and stream options + attachSource(url: string, options: VokaOptions.IStreamOptions): void + + /// get paused state + getPaused(): boolean + + /// get live streaming flag + getIsLive(): boolean + + /// seek stream to specific position, in seconds + seek(seconds: number): void + + /// get current stream position, in seconds + getCurrentTime(): number + + /// get current stream duration, in seconds + getDuration(): number + + /// set audio volume + setVolume(value: number) + + /// get audio volume + getVolume(): number + + /// mute audio + mute(): void + + /// unmute audio + unmute(): void + + /// get muted state + getMuted(): boolean + + /// destroy + destroy() + + /// get length in seconds of buffered data + getBufferLength(): number + + /// wheither or not playback is stalled due to buffering + getBufferingState(): boolean + + /// player is currently loading/playing ads + getAdIsPlaying(): boolean + + /// cancel currently playing advertisement(s) + cancelAdPlayback() + + /// get current utc unixtime for live streams + getAbsoluteCurrentTime(): number | null + + /// get utc start/end range for live streams + getAbsoluteTimeRange(): ITimeRange | null + + /// check if timeshift is available + getTimeshiftAvailable(): boolean + + /// get array with information about available video qualities + getVideoQualityList(): [IQuality] + + /// get index of currently selected video quality (-1 for auto) + getSelectedVideoQuality(): number + + /// get index of currently playing video quality (-1 if unknown) + getPlayingVideoQuality(): number + + /// set index of video quality to play (-1 for auto) + setSelectedVideoQuality(index: number) + + /// get array of available audio tracks + getAudioTrackList(): [IAudioTrack] + + /// get index of currently selected audio track + getCurrentAudioTrack(): number + + /// set index of audio track to play + setCurrentAudioTrack(index: number) + + /// get array of available subtitles tracks + getSubtitlesTrackList(): [ISubtitle] + + /// get index of currently selected subtitles track (-1 for disabled) + getCurrentSubtitlesTrack(): number + + /// set index of subtitles track to display (-1 to disable) + setCurrentSubtitlesTrack(index: number) + + /// mark current position as selection start + setSelectionStartPos(seconds: number) + + /// mark current position as selection end + setSelectionEndPos(seconds: number) + + /// get currently selected range + getSelectionRange(): ITimeRange | null + + /// get zoom button visibility + getZoomButtonVisible(): boolean + + /// check if zoom mode is enabled + getZoomModeEnabled(): boolean + + /// enable/disable zoom mode (boolean argument) + setZoomModeEnabled(isEnabled: boolean) + + /// get current network bandwidth in kbit/s + getNetworkBandwidth(): number + + /// get information about currently playing video track + getCurrentVideoInfo(): any + + /// get visibility state of control bar + getControlbarVisible(): boolean + + /// 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 + + /// pause playing + pause(): void +} \ No newline at end of file diff --git a/src/public/VokaGlobalFunctions.ts b/src/public/VokaGlobalFunctions.ts new file mode 100644 index 0000000..c335455 --- /dev/null +++ b/src/public/VokaGlobalFunctions.ts @@ -0,0 +1,18 @@ +namespace VokaGlobalFunctions { + + export const value = "" + /* + GlobalFuncs.log = playerLog; + GlobalFuncs.enableExternalLog = enableExternalLog; + + GlobalFuncs.getPlayerVersion = getPlayerVersion; + GlobalFuncs.getProtocolAndDrm = getProtocolAndDrm; + + GlobalFuncs.copyOptions = copyOptions; + GlobalFuncs.normalizeOptions = normalizeOptions; + GlobalFuncs.formatTime = formatTime; + * */ + +} + +export default VokaGlobalFunctions \ No newline at end of file diff --git a/src/public/VokaPlayer.ts b/src/public/VokaPlayer.ts index 74b3dfa..ac71b8f 100644 --- a/src/public/VokaPlayer.ts +++ b/src/public/VokaPlayer.ts @@ -1,12 +1,13 @@ -import { VokaErrorMessages, VokaOptions } from './@types' +import { VokaOptions } from './@types' import { IVokaPlayer } from './IVokaPlayer' import { EMESystem, EncryptSystem } from '@/internal/utils/EMESystem' -import { VokaPlayerCore } from '@/internal/player/VokaPlayerCore' +import VokaCorePlayer from '@/internal/player/VokaCorePlayer' import GUIDUtils from '@/internal/utils/GUIDUtils' import { defaultOptions } from './models/VokaDefaultOptions' import { VokaPlayerImpl } from '@/public/VokaPlayerImpl' import SupportedCodecs from '@/internal/utils/SupportedCodecs' import BrowserUtils from '@/internal/utils/BrowserUtils' +import VokaGlobalFunctions from '@/public/VokaGlobalFunctions' namespace VokaPlayer { @@ -23,10 +24,10 @@ namespace VokaPlayer { export function features(): SupportedCodecs.ISelectProtocolResult | null { return this.playerFeatures } - export async function create( - element: HTMLElement | string, + export function create( + element: HTMLElement | string | null, creationOptions: VokaOptions.IOptions | null - ): Promise { + ): IVokaPlayer | VokaGlobalFunctions { let htmlElement: HTMLElement | null = null if (typeof document !== 'undefined') { @@ -38,9 +39,7 @@ namespace VokaPlayer { } if (htmlElement == null || !BrowserUtils.isDomElement(htmlElement)) { - return Promise.reject( - new TypeError(VokaErrorMessages.DocumentUnavailable) - ) + return VokaGlobalFunctions } // TODO: Добавить возможность получить предыдущий инстанс плеера передав этот же элемент @@ -52,29 +51,21 @@ namespace VokaPlayer { }*/ if ((htmlElement as HTMLElement).nodeName == 'IFRAME') { - return Promise.reject( - new TypeError(VokaErrorMessages.IFrameElement) - ) + return VokaGlobalFunctions } const options = { ...defaultOptions, ...creationOptions } - - const result = await detectFeatures(options) - this.playerFeatures = Object.freeze(result) - - return new Promise((resolve) => { - new VokaPlayerCore(htmlElement, (player) => { - resolve( - new VokaPlayerImpl( - GUIDUtils.globalUnique, // id - GUIDUtils.generateGUID(), // SessionGUID - options, // merge options default and passed from outside - result, - player - ) - ) + return new VokaPlayerImpl( + GUIDUtils.globalUnique, // id + GUIDUtils.generateGUID(), // SessionGUID + options, // merge options default and passed from outside + detectFeatures(options), + new Promise((resolve) => { + new VokaCorePlayer.CorePlayer(htmlElement, (player) => { + resolve(player) + }) }) - }) + ) } export async function detectFeatures(options: VokaOptions.IOptions): Promise { @@ -97,8 +88,9 @@ namespace VokaPlayer { try { system = await EMESystem.detect() } catch { } - - return SupportedCodecs.SupportedCodecs.selectProtocol(system, BrowserUtils.isMobile(), options.features.drm, protocolOptions) + const result = SupportedCodecs.SupportedCodecs.selectProtocol(system, BrowserUtils.isMobile(), options.features.drm, protocolOptions) + this.playerFeatures = Object.freeze(result) + return this.playerFeatures } } diff --git a/src/public/VokaPlayerImpl.ts b/src/public/VokaPlayerImpl.ts index 1267219..2923ec0 100644 --- a/src/public/VokaPlayerImpl.ts +++ b/src/public/VokaPlayerImpl.ts @@ -1,67 +1,220 @@ import { VokaOptions } from './@types' -import { IVokaPlayer, VokaPlayerEvent } from './IVokaPlayer' -import { VokaPlayerCore } from '@/internal/player/VokaPlayerCore' +import { IAudioTrack, IQuality, ISubtitle, ITimeRange, IVokaPlayer, VokaPlayerEvent } from './IVokaPlayer' +import VokaCorePlayer from '@/internal/player/VokaCorePlayer' import '../assets/scss/main.scss' import VokaApi from '@/public/network/VokaApi' import BrowserUtils from '@/internal/utils/BrowserUtils' import SupportedCodecs from '@/internal/utils/SupportedCodecs' import { EncryptSystem } from '@/internal/utils/EMESystem' +import { VokaContentType } from '@/public/models/VokaContentType' +import { Promise } from 'es6-promise' +import PlatformCapabilities from '@/internal/utils/PlatformCapabilities' +import IResult = VokaApi.IResult +import CorePlayer = VokaCorePlayer.CorePlayer export class VokaPlayerImpl implements IVokaPlayer { private static readonly version = '0.0.3' private static readonly build = '1' - private readonly player: VokaPlayerCore + private _player: VokaCorePlayer.CorePlayer | null public readonly uniqID: number public readonly sessionGUID: string public readonly options: VokaOptions.IOptions + private readonly initializePromise: Promise + private _features: SupportedCodecs.ISelectProtocolResult | null + public static playerVersion(): string { return `${this.version}.${this.build}` } private constructor( id: number, sessionGUID: string, options: VokaOptions.IOptions, - features: SupportedCodecs.ISelectProtocolResult, - player: VokaPlayerCore + features: Promise, + playerPromise: Promise ) { this.uniqID = id this.sessionGUID = sessionGUID this.options = options - this.player = player + this._player = null - this.initialize(features) + this.initializePromise = new Promise((resolve) => { + + Promise.all([features, playerPromise]).then( + ([features, player]) => { + this._player = player + this._features = features + this.initialize(player, features).then((result) => { resolve() }) + } + ) + }) } // MARK: - IVokaPlayer implementation + public afterInitialize(callback: () => void) { + this.initializePromise.then(() => { callback() }) + } + + public isInitialized(): boolean { + return this._player != null + } + + public getProtocol(): string { + const features = this._features + if (features != null && features.native != null) { + return features.native.toString() + } + return 'dash' + } + + public getDrmSystem(): string { + const features = this._features + if (!this.options.features.drm || features == null) { + return 'none' + } + return EncryptSystem.toString(features.keySystem) + } + + public isHlsSupported(): boolean { + return PlatformCapabilities.isSupportedHlsJs + } + + public getVideoCodecs(): [string] { + const features = this._features + if (features == null) { return [] } + return features.mseCodecs + } + + public attachSource(url: string, options: VokaOptions.IStreamOptions): void { + // TODO!!!! + } + + public getPaused(): boolean { + if (this._player == null) { return false } + return this._player.isPaused + } + + public getIsLive(): boolean { + if (this._player == null) { return false } + return this._player.isLive + } + + public seek(seconds: number): void { + if (this._player == null) { return } + this._player.seek(seconds) + } + + public getCurrentTime(): number { + if (this._player == null) { return 0 } + return this._player.currentTime + } + + public getDuration(): number { + if (this._player == null) { return 0 } + return this._player.duration + } + + public setVolume(value: number) { + if (this._player == null) { return } + this._player.setVolume(value) + } + + public getVolume(): number { + if (this._player == null) { return 0 } + return this._player.volume + } + + public mute() { + if (this._player == null) { return } + this._player.mute(true) + } + + public unmute() { + if (this._player == null) { return } + this._player.mute(false) + } + + public getMuted(): boolean { + if (this._player == null) { return false } + return this._player.isMuted + } + + public destroy() { + // TODO!!! + } + + getBufferLength(): number { + if (this._player == null) { return 0 } + return this._player.bufferLength + } + getBufferingState(): boolean { + if (this._player == null) { return false } + return this._player.isBuffering + } + getAdIsPlaying(): boolean { return false } + cancelAdPlayback() { + // TODO + } + getAbsoluteCurrentTime(): number | null { return null } + getAbsoluteTimeRange(): ITimeRange | null { return null } + getTimeshiftAvailable(): boolean { return false } + getVideoQualityList(): [IQuality] { return [] } + getSelectedVideoQuality(): number { return 0 } + getPlayingVideoQuality(): number { return 0 } + setSelectedVideoQuality(index: number) { } + getAudioTrackList(): [IAudioTrack] { return [] } + getCurrentAudioTrack(): number { return 0 } + setCurrentAudioTrack(index: number) {} + getSubtitlesTrackList(): [ISubtitle] { return [] } + getCurrentSubtitlesTrack(): number { return 0 } + setCurrentSubtitlesTrack(index: number) {} + setSelectionStartPos(seconds: number) {} + setSelectionEndPos(seconds: number) {} + getSelectionRange(): ITimeRange | null { return null } + getZoomButtonVisible(): boolean { return false } + getZoomModeEnabled(): boolean { return false } + setZoomModeEnabled(isEnabled: boolean) {} + getNetworkBandwidth(): number { return 0 } + getCurrentVideoInfo(): any { return {} } + getControlbarVisible(): boolean { return true } + on(event: VokaPlayerEvent, callback: Function): void { throw new Error('Method not implemented.') } off(event: VokaPlayerEvent, callback?: Function): void { throw new Error('Method not implemented.') } - play(): Promise { - throw new Error('Method not implemented.') + play(): Promise | undefined { + if (this._player == null) { return Promise.resolve() } + return this._player.play() } - pause(): Promise { - throw new Error('Method not implemented.') - } - stop(): Promise { - throw new Error('Method not implemented.') + pause(): void { + if (this._player == null) { return } + return this._player.pause() } // MARK: - Private methods - private async initialize(features: SupportedCodecs.ISelectProtocolResult) { + private load(content: IResult, player: CorePlayer) { - if (this.options.features.api) { - const content = this.fetchContent(this.options, features) - } + const iContent = { + url: content.url, + type: VokaContentType.HLS, + subtitlesUrl: content.subtitlesUrl, + } as VokaCorePlayer.IContent + player.load(iContent) + } + private async initialize(player: CorePlayer, features: SupportedCodecs.ISelectProtocolResult): Promise { + if (!this.options.features.api) { return false } + const content = await this.fetchContent(this.options, features) + this.load(content, player) + + return true } private async fetchContent(options: VokaOptions.IOptions, features: SupportedCodecs.ISelectProtocolResult) { @@ -97,6 +250,83 @@ export class VokaPlayerImpl implements IVokaPlayer { } as VokaApi.ISystemCapability const api = new VokaApi.Api(apiOptions, apiCapability) - const result = api.load() + return api.load() } -} \ No newline at end of file +} + +// MP4 +/*const iContent = { + url: "https://vjs.zencdn.net/v/oceans.mp4", + type: VokaContentType.MP4, +} as VokaCorePlayer.IContent*/ + +// HLS +/*const iContent = { + url: "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8", + type: VokaContentType.HLS, +} as VokaCorePlayer.IContent*/ + +// HLS LIVE +/*const iContent = { + url: "https://dai.google.com/linear/hls/event/rtcMlf4RTvOEkaudeany5w/master.m3u8?iu=/4128/CBS.NY.OTT", + type: VokaContentType.HLS, +} as VokaCorePlayer.IContent*/ + +// DASH +/*const iContent = { + url: "https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd", + type: VokaContentType.DASH, +} as VokaCorePlayer.IContent*/ + +// DASH +/*const iContent = { + url: "https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd", + type: VokaContentType.DASH, +} as VokaCorePlayer.IContent*/ + +// FAIRPLAY TODO +/*const iContent = { + url: "https://codeeducation.akamaized.net/code/fullcycle/devops_20/01/01_introducao.mp4/fp/fairplay.m3u8", + type: VokaContentType.FAIRPLAY, + drmConfig: { + type: VokaCorePlayer.DRMType.FAIRPLAY, + certificateUrl: "https://codeeducation.akamaized.net/fairplay.cer", + licenseUrl: "https://fps.ezdrm.com/api/licenses/F6B15258-BC92-49EB-9CDF-DE9F121C13A5?customdata=MTQ0OmFyZ2VudGluYWx1aXpAZ21haWwuY29tOjY3MTE6Y291cnNlOmNvZGU=", + } +} as VokaCorePlayer.IContent*/ + +/*const iContent = { + url: "https://e09f957480c8b1e479a1edb0fabc72d8.egress.mediapackage-vod.eu-west-1.amazonaws.com/out/v1/6f12444e79macdf3e4206ad363f810cb2aead/9ea4e8148b794c8ba2c6295b824e5ad5/46a61bf2c081464bb9476f2a55a06f48/index.m3u8", + type: VokaContentType.FAIRPLAY, + drmConfig: { + type: VokaCorePlayer.DRMType.FAIRPLAY, + certificateUrl: "https://customer-tests.la.drm.cloud/certificate/fairplay?BrandGuid=5a96a0d0-d13f-42b0-ab2b-ba8cfc4aa0a0", + licenseUrl: "https://customer-tests.la.drm.cloud/acquire-license/fairplay?KID=4376a4b3-d8ef-4f21-9a6b-faa81a2e59e3&brandguid=5a96a0d0-d13f-42b0-ab2b-ba8cfc4aa0a0&usertoken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MzU2ODk2MDAsImRybVRva2VuSW5mbyI6eyJleHAiOiIyMDI1LTEyLTA3VDE1OjMwOjA5LjU5MDgxMjUrMDE6MDAiLCJraWQiOlsiKiJdLCJwIjp7InBlcnMiOnRydWUsImVkIjoiMjAyNS0xMi0wN1QxNTozMDowOS41OTExMzA1KzAxOjAwIn19fQ.xEToUttAk9AVFgP3bHyDlcvm6BR-8_hsl8V3n-jrDwM", + } +} as VokaCorePlayer.IContent*/ + +// Dash WideWine DRM +/*const iContent = { + url: "https://media.axprod.net/TestVectors/Cmaf/protected_1080p_h264_cbcs/manifest.mpd", + type: VokaContentType.WIDEVINE, + drmConfig: { + type: VokaCorePlayer.DRMType.WIDEWINE, + certificateUrl: "https://drm-widevine-licensing.axtest.net/AcquireLicense", + headers: { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICIzMDJmODBkZC00MTFlLTQ4ODYtYmNhNS1iYjFmODAxOGEwMjQiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAicm9LQWcwdDdKaTFpNDNmd3YremZ0UT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ._NfhLVY7S6k8TJDWPeMPhUawhympnrk6WAZHOVjER6M", + }, + } +} as VokaCorePlayer.IContent*/ + +// Dash Playready DRM +/*const iContent = { + url: "https://media.axprod.net/TestVectors/Cmaf/protected_1080p_h264_cbcs/manifest.mpd", + type: VokaContentType.PLAYREADY, + drmConfig: { + type: VokaCorePlayer.DRMType.PLAYREADY, + certificateUrl: "https://drm-widevine-licensing.axtest.net/AcquireLicense", + headers: { + "X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICIzMDJmODBkZC00MTFlLTQ4ODYtYmNhNS1iYjFmODAxOGEwMjQiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAicm9LQWcwdDdKaTFpNDNmd3YremZ0UT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ._NfhLVY7S6k8TJDWPeMPhUawhympnrk6WAZHOVjER6M", + }, + } +} as VokaCorePlayer.IContent*/ \ No newline at end of file diff --git a/src/public/network/VokaApi.ts b/src/public/network/VokaApi.ts index f977cd3..e1c4402 100644 --- a/src/public/network/VokaApi.ts +++ b/src/public/network/VokaApi.ts @@ -1,5 +1,6 @@ import { createEventDefinition, EventBus } from 'ts-bus' import { HTTPClient, HTTPMethod, IHTTPClient, RequestOptions } from '../../internal/utils/HTTPClient' +import { VokaOptions } from '@/public/@types' namespace VokaApi { @@ -67,7 +68,8 @@ namespace VokaApi { url: string subtitlesUrl: string | null drmConfig: IDRMConfig | null - metrics: IMetrics | null + metrics: IMetrics | null, + streamOptions: VokaOptions.IStreamOptions | null } export class Api { @@ -135,7 +137,7 @@ namespace VokaApi { } const result = await client.request( - HTTPMethod.POST, + HTTPMethod.GET,// HTTPMethod.POST, "//" + this.getApiHost() + "/v1/devices.json", queryParams, null,