Feature/63538 voka api

This commit is contained in:
Юрий Шикин
2025-06-02 05:35:03 +00:00
parent 7accdb335b
commit aee395ff9d
16 changed files with 972 additions and 812 deletions
@@ -1,6 +1,16 @@
{
"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"
"subtitles": "https://raw.githubusercontent.com/videojs/video.js/c7298d40a4632a6e9dfcd5a2f5cc3bbe92a78744/docs/examples/elephantsdream/captions.ru.vtt",
"analytics_v2": {
"url": "https://juraldinio.com/analytics/",
"interval": 5000,
"additional_parameters": {
"application_id": "42",
"user_id": "12321",
"resource_type": "video",
"watch_session_id": "100500"
}
}
}
}
+1 -1
View File
@@ -38,6 +38,6 @@
"devDependencies": {
"@swc/core": "^1.11.20",
"terser": "^5.37.0",
"video.js": "^8.21.0"
"video.js": "^8.23.3"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
enum WebOSDRMClientType {
PLAYREADY = 'playready',
WIDEWINE = 'widevine'
WIDEVINE = 'widevine'
}
interface WebOSDRMClient {
@@ -173,7 +173,7 @@ class WebOSPlayerNativeDRMImpl {
new Error(`Player.sendDrmMessage onSuccess: [${resultCode}] DRM message error`)
)
}
} else if (client.type == WebOSDRMClientType.WIDEWINE) {
} else if (client.type == WebOSDRMClientType.WIDEVINE) {
resolve(result)
}
},
@@ -247,7 +247,7 @@ class WebOSPlayerNativeDRMImpl {
message += '</LicenseServerUriOverride>'
message += '</PlayReadyInitiator>'
} else if (client.type == WebOSDRMClientType.WIDEWINE) {
} else if (client.type == WebOSDRMClientType.WIDEVINE) {
drmMessageType = 'application/widevine+xml' // Message type for widevine 'xml'
drmSystemId = 'urn:dvb:casystemid:19156' // Unique ID of DRM system
+13 -6
View File
@@ -6,7 +6,7 @@ import {
VokaInternalErrorData,
VokaInternalErrorType
} from '@/public/models/VokaError'
import { IContent, IContextUpdated } from '@/public/@types'
import { IContextUpdated } from '@/public/@types'
namespace VokaBusEvent {
@@ -33,6 +33,13 @@ namespace VokaBusEvent {
label: string | null
}
export interface ICon {
id: string
version: number
}
export const taskCreated = createEventDefinition<ICon>()("task.created")
export const qualityChange = createEventDefinition<IChangeQuality>()('quality.update')
export const qualitySet = createEventDefinition<{
@@ -56,7 +63,11 @@ namespace VokaBusEvent {
export const close = createEventDefinition()('close')
// audio
// MARK: - Switch content
export const switchContent =
createEventDefinition<VokaCorePlayer.IContent>()('content.switch')
// MARK: - Audio
export const audioTracksParsed = createEventDefinition<{
audioTracks: any
}>()('audioTracks.parsed')
@@ -67,10 +78,6 @@ namespace VokaBusEvent {
// Others
export const contentRetrieved = createEventDefinition<{
content: IContent
}>()('content.loaded')
export const contextUpdated =
createEventDefinition<IContextUpdated>()('context.update')
+16 -6
View File
@@ -1,6 +1,6 @@
import videojs from 'video.js'
import Player from 'video.js/dist/types/player'
import { EventBus } from 'ts-bus'
import { createEventDefinition, EventBus } from 'ts-bus'
import { VokaPlayerEvent} from '@/public/IVokaPlayer'
import { AutoplayChecker } from '@/internal/utils/AutoplayChecker'
import { Quality } from '@/public/models/ILoadOptions'
@@ -23,7 +23,7 @@ import { Promise } from 'es6-promise'
import ZoomModeObserver from '@/internal/observers/ZoomModeObserver'
import CorePlayerOptions from '@/internal/player/CorePlayerOptions'
import { bus } from '@/internal/events/bus'
import VokaBusEvent from '@/internal/events/VokaBusEvent'
import VokaBusEvent, { switchContent } from '@/internal/events/VokaBusEvent'
namespace VokaCorePlayer {
@@ -54,13 +54,20 @@ namespace VokaCorePlayer {
export interface IMetrics {
url: string
interval: number | null
params: { [key: string]: string }
params: { [key: string]: string } | null
}
export interface IHeartbeat {
url: string
interval: number
version: number
}
export enum DRMType {
NONE = 'none',
FAIRPLAY = 'fairplay',
PLAYREADY = 'playready',
WIDEWINE = 'widewine',
WIDEVINE = 'widewine',
}
export interface IDRMConfig {
@@ -76,6 +83,7 @@ namespace VokaCorePlayer {
drmConfig: IDRMConfig | null
subtitlesUrl: string | null
metrics: IMetrics | null
heartbeat: IHeartbeat | null
}
export function createPlayer(element: HTMLElement): PlayerCreationClosure {
@@ -193,7 +201,7 @@ namespace VokaCorePlayer {
}
break
case VokaContentType.WIDEVINE:
if (drm != null && drm.type == DRMType.WIDEWINE) {
if (drm != null && drm.type == DRMType.WIDEVINE) {
playableContent = {
type: "application/dash+xml",
keySystemOptions: [{
@@ -244,10 +252,12 @@ namespace VokaCorePlayer {
if (playableContent != null) {
playableContent['src'] = content.url
playableContent['content'] = content.type
playableContent['metrics'] = content.metrics
playableContent['subtitlesUrl'] = content.subtitlesUrl
this.player.src(playableContent)
bus.publish(VokaBusEvent.switchContent(content))
return Promise.resolve()
}
return Promise.reject('Unsupported')
@@ -27,7 +27,7 @@ export default class VokaDashSourceHandler extends VokaSourceHandler {
return ''
}
// отключаем widewine на ios и в safari
// отключаем widevine на ios и в safari
if (source.content === VokaContentType.WIDEVINE && (Browser.IS_SAFARI || Browser.IS_IOS)) {
return ''
}
@@ -67,7 +67,7 @@ export default class VokaWebOSSourceHandler extends VokaSourceHandler {
const nativeDrm = this.createDRMPlayer(source, tech)
VokaWebOSSourceHandler.nativeDrm = nativeDrm
const type = source.content == VokaContentType.WIDEVINE
? WebOSDRMClientType.WIDEWINE
? WebOSDRMClientType.WIDEVINE
: WebOSDRMClientType.PLAYREADY
nativeDrm.loadClient(type)
} else {
+111 -103
View File
@@ -1,123 +1,131 @@
import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js'
import VokaEvent from "@/constants/VokaEvent"
import videojs from 'video.js'
import Player from 'video.js/dist/types/player'
import { bus } from '@/internal/events/bus'
import { IContent, IHeartbeat } from '@/public/@types'
import { IHeartbeat } from '@/public/@types'
import VokaBusEvent from '@/internal/events/VokaBusEvent'
import TimerUtils from '@/internal/utils/TimerUtils'
import { HTTPMethod, IHTTPClient, HTTPClient, RequestOptions } from '@/internal/utils/HTTPClient'
const Plugin = videojs.getPlugin('plugin')
import { HTTPMethod, IHTTPClient, HTTPClient } from '@/internal/utils/HTTPClient'
export class VokaHeartbeatPlugin extends Plugin {
private player!: VideoJsPlayer
private heartbeat: IHeartbeat | null
private playbackStarted: boolean
private scheduledProgress: ReturnType<typeof setInterval> | null
private httpClient: IHTTPClient
namespace VokaHeartbeatPlugin {
constructor(player: VideoJsPlayer, options: VideoJsPlayerOptions) {
super(player, options)
this.player = player
this.playbackStarted = false
this.httpClient = new HTTPClient()
this.setupListeners()
}
const VideoPlugin = videojs.getPlugin('plugin')
private setupListeners() {
bus.subscribe(
VokaBusEvent.contentRetrieved,
(event) => {
this.heartbeat = event.payload.content.heartbeat
export class Plugin extends VideoPlugin {
private player!: Player
private heartbeat: IHeartbeat | null
private playbackStarted: boolean
private scheduledProgress: ReturnType<typeof setInterval> | null
private httpClient: IHTTPClient
constructor(player: Player) {
super(player)
this.player = player
this.playbackStarted = false
this.httpClient = new HTTPClient()
this.setupListeners()
}
private setupListeners() {
bus.subscribe(
VokaBusEvent.switchContent,
(event) => {
this.unscheduleProgressRequest()
this.heartbeat = event.payload.heartbeat
}
)
this.player.on('play', this.onPlaybackStarted.bind(this))
this.player.on('ended', this.onPlaybackEnded.bind(this))
}
private onPlaybackStarted(event) {
if (this.heartbeat == null || this.playbackStarted) { return }
this.playbackStarted = true
this.createRequest(this.heartbeat, 'start')
this.scheduleProgressRequest(this.heartbeat)
}
private onPlaybackEnded(event) {
this.unscheduleProgressRequest()
if (this.heartbeat == null || !this.playbackStarted) { return }
this.playbackStarted = false
this.createRequest(this.heartbeat, 'end')
}
private createRequest(heartbeat: IHeartbeat, type: string) {
let needTime = false
let needDuration = false
let params = {}
switch (type) {
case 'start':
params['action'] = 'start'
needTime = true
needDuration = true
break
case 'end':
params['action'] = 'end'
needTime = true
needDuration = true
break
case 'progress':
params['action'] = 'watch'
needTime = true
needDuration = true
break
default:
return
}
)
this.player.on('play', this.onPlaybackStarted.bind(this))
this.player.on('ended', this.onPlaybackEnded.bind(this))
}
private onPlaybackStarted(event) {
if (this.heartbeat == null || this.playbackStarted) { return }
this.playbackStarted = true
this.createRequest('start')
this.scheduleProgressRequest()
}
private onPlaybackEnded(event) {
if (this.heartbeat == null || !this.playbackStarted) { return }
this.playbackStarted = false
this.createRequest('end')
this.unscheduleProgressRequest()
}
private createRequest(type: string) {
let needTime = false
let needDuration = false
let params = {}
switch (type) {
case 'start':
params['action'] = 'start'
needTime = true
needDuration = true
break
case 'end':
params['action'] = 'end'
needTime = true
needDuration = true
break
case 'progress':
params['action'] = 'watch'
needTime = true
needDuration = true
break
default:
return
}
if (!isNaN(this.heartbeat.version)) {
params['v'] = this.heartbeat.version.toString()
}
if (needTime) {
const value = this.player.currentTime()
if (!isNaN(value)) {
params['timestamp'] = Math.round(value * 1000).toString()
if (!isNaN(heartbeat.version)) {
params['v'] = heartbeat.version.toString()
}
}
if (needDuration) {
const value = this.player.duration()
if (!isNaN(value)) {
params['duration'] = Math.round(value * 1000).toString()
if (needTime) {
const value = this.player.currentTime()
if (value != undefined && !isNaN(value)) {
params['timestamp'] = Math.round(value * 1000).toString()
}
}
if (needDuration) {
const value = this.player.duration()
if (value != undefined && !isNaN(value)) {
params['duration'] = Math.round(value * 1000).toString()
}
}
const request = this.httpClient.request(
HTTPMethod.GET,
heartbeat.url,
params,
null,
{ timeout: 60000 }
)
}
const request = this.httpClient.request(
HTTPMethod.GET,
this.heartbeat.url,
params,
null,
{ timeout: 60000 }
)
}
private scheduleProgressRequest(heartbeat: IHeartbeat) {
this.unscheduleProgressRequest()
this.scheduledProgress = TimerUtils.setInterval(
() => { this.createProgressRequest(heartbeat) },
heartbeat.interval * 1000,
)
}
private scheduleProgressRequest() {
this.unscheduleProgressRequest()
this.scheduledProgress = TimerUtils.setInterval(() => { this.createProgressRequest() }, this.heartbeat.interval * 1000)
}
private unscheduleProgressRequest() {
if (this.scheduledProgress == null) { return }
clearInterval(this.scheduledProgress)
this.scheduledProgress = null
}
private unscheduleProgressRequest() {
if (this.scheduledProgress == null) { return }
clearInterval(this.scheduledProgress)
this.scheduledProgress = null
}
private createProgressRequest(heartbeat: IHeartbeat) {
this.createRequest(heartbeat, 'progress')
}
private createProgressRequest() {
this.createRequest('progress')
}
}
videojs.registerPlugin('vokaHeartbeatPlugin', VokaHeartbeatPlugin)
videojs.registerPlugin('vokaHeartbeatPlugin', VokaHeartbeatPlugin.Plugin)
export default VokaHeartbeatPlugin
+121 -115
View File
@@ -1,129 +1,135 @@
import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js'
import keycode from 'keycode'
const Plugin = videojs.getPlugin('plugin')
export class VokaKeyboardPlugin extends Plugin {
private player!: VideoJsPlayer
private options: VideoJsPlayerOptions
namespace VokaKeyboardPlugin {
constructor(player: VideoJsPlayer, options: VideoJsPlayerOptions) {
super(player, options)
this.player = player
this.options = options
this.setupListeners()
}
const VideoPlugin = videojs.getPlugin('plugin')
private setupListeners() {
document.addEventListener('keyup', this.listenerKeyUp.bind(this))
}
export class Plugin extends VideoPlugin {
private player!: VideoJsPlayer
private options: VideoJsPlayerOptions
private listenerKeyUp(event: KeyboardEvent) {
if (event.defaultPrevented || !this.player) {
return
}
switch (keycode(event)) {
case 'space':
case 'enter':
this.operationTogglePlay()
break
case 'up':
this.operationChangeVolume(true)
break
case 'down':
this.operationChangeVolume(false)
break
case 'm':
this.operationMuteSound()
break
case 'left':
this.operationStepBackward()
break
case 'right':
this.operationStepForward()
break
case 'f':
this.operationFullscreenToggle()
break
constructor(player: VideoJsPlayer, options: VideoJsPlayerOptions) {
super(player, options)
this.player = player
this.options = options
this.setupListeners()
}
// Отменяем действие по умолчанию в плеере.
event.preventDefault()
}
private setupListeners() {
document.addEventListener('keyup', this.listenerKeyUp.bind(this))
}
private operationTogglePlay() {
if (this.player.paused()) {
this.player.play()
} else {
this.player.pause()
private listenerKeyUp(event: KeyboardEvent) {
if (event.defaultPrevented || !this.player) {
return
}
switch (keycode(event)) {
case 'space':
case 'enter':
this.operationTogglePlay()
break
case 'up':
this.operationChangeVolume(true)
break
case 'down':
this.operationChangeVolume(false)
break
case 'm':
this.operationMuteSound()
break
case 'left':
this.operationStepBackward()
break
case 'right':
this.operationStepForward()
break
case 'f':
this.operationFullscreenToggle()
break
}
// Отменяем действие по умолчанию в плеере.
event.preventDefault()
}
private operationTogglePlay() {
if (this.player.paused()) {
this.player.play()
} else {
this.player.pause()
}
}
private operationChangeVolume(rise: Boolean) {
const step = 0.1
const value = this.player.volume() + (rise ? step : -step)
this.player.volume(value)
}
private operationMuteSound() {
this.player.muted(!this.player.muted())
}
private operationFullscreenToggle() {
if (!this.player.isFullscreen()) {
this.player.requestFullscreen()
} else {
this.player.exitFullscreen()
}
}
private operationStepBackward() {
const skipTime = this.options.skip?.forward
if (!skipTime) return
const currentVideoTime = this.player.currentTime()
const liveTracker = this.player.liveTracker
const seekableStart =
liveTracker && liveTracker.isLive() && liveTracker.seekableStart()
let newTime
if (seekableStart && currentVideoTime - skipTime <= seekableStart) {
newTime = seekableStart
} else if (currentVideoTime >= skipTime) {
newTime = currentVideoTime - skipTime
} else {
newTime = 0
}
this.player.currentTime(newTime)
}
private operationStepForward() {
const skipTime = this.options.skip?.forward
if (isNaN(this.player.duration()) || !skipTime) {
return
}
const currentVideoTime = this.player.currentTime()
const liveTracker = this.player.liveTracker
const duration =
liveTracker && liveTracker.isLive()
? liveTracker.seekableEnd()
: this.player.duration()
let newTime
if (currentVideoTime + skipTime <= duration) {
newTime = currentVideoTime + skipTime
} else {
newTime = duration
}
this.player.currentTime(newTime)
}
}
private operationChangeVolume(rise: Boolean) {
const step = 0.1
const value = this.player.volume() + (rise ? step : -step)
this.player.volume(value)
}
private operationMuteSound() {
this.player.muted(!this.player.muted())
}
private operationFullscreenToggle() {
if (!this.player.isFullscreen()) {
this.player.requestFullscreen()
} else {
this.player.exitFullscreen()
}
}
private operationStepBackward() {
const skipTime = this.options.skip?.forward
if (!skipTime) return
const currentVideoTime = this.player.currentTime()
const liveTracker = this.player.liveTracker
const seekableStart =
liveTracker && liveTracker.isLive() && liveTracker.seekableStart()
let newTime
if (seekableStart && currentVideoTime - skipTime <= seekableStart) {
newTime = seekableStart
} else if (currentVideoTime >= skipTime) {
newTime = currentVideoTime - skipTime
} else {
newTime = 0
}
this.player.currentTime(newTime)
}
private operationStepForward() {
const skipTime = this.options.skip?.forward
if (isNaN(this.player.duration()) || !skipTime) {
return
}
const currentVideoTime = this.player.currentTime()
const liveTracker = this.player.liveTracker
const duration =
liveTracker && liveTracker.isLive()
? liveTracker.seekableEnd()
: this.player.duration()
let newTime
if (currentVideoTime + skipTime <= duration) {
newTime = currentVideoTime + skipTime
} else {
newTime = duration
}
this.player.currentTime(newTime)
}
}
videojs.registerPlugin('vokaKeyboardPlugin', VokaKeyboardPlugin)
videojs.registerPlugin('vokaKeyboardPlugin', VokaKeyboardPlugin.Plugin)
export default VokaKeyboardPlugin
+113 -107
View File
@@ -1,128 +1,134 @@
import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js'
const Plugin = videojs.getPlugin('plugin')
const KEY_CODES = {
ENTER: 13,
namespace VokaMagicRemotePlugin {
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
const VideoPlugin = videojs.getPlugin('plugin')
STOP: 413,
PLAY: 415,
PAUSE: 19,
const KEY_CODES = {
ENTER: 13,
NEXT: 417,
PREV: 412
}
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
export class VokaMagicRemotePlugin extends Plugin {
private player!: VideoJsPlayer
private options: VideoJsPlayerOptions
STOP: 413,
PLAY: 415,
PAUSE: 19,
constructor(player: VideoJsPlayer, options: VideoJsPlayerOptions) {
super(player, options)
this.player = player
this.options = options
this.setupListeners()
NEXT: 417,
PREV: 412
}
private setupListeners() {
document.addEventListener('keyup', this.listenerKeyUp.bind(this))
}
export class Plugin extends VideoPlugin {
private player!: VideoJsPlayer
private options: VideoJsPlayerOptions
private listenerKeyUp(event: KeyboardEvent) {
if (event.defaultPrevented || !this.player) {
return
}
switch (event.keyCode) {
case KEY_CODES.PLAY:
this.operationPlay()
break
case KEY_CODES.PAUSE:
this.operationPause()
break
case KEY_CODES.STOP:
this.operationPause()
break
case KEY_CODES.ENTER:
this.operationTogglePlay()
break
case KEY_CODES.LEFT:
case KEY_CODES.PREV:
this.operationStepBackward()
break
case KEY_CODES.RIGHT:
case KEY_CODES.NEXT:
this.operationStepForward()
break
constructor(player: VideoJsPlayer, options: VideoJsPlayerOptions) {
super(player, options)
this.player = player
this.options = options
this.setupListeners()
}
// Отменяем действие по умолчанию в плеере.
event.preventDefault()
}
private setupListeners() {
document.addEventListener('keyup', this.listenerKeyUp.bind(this))
}
private operationTogglePlay() {
if (this.player.paused()) {
this.player.play()
} else {
private listenerKeyUp(event: KeyboardEvent) {
if (event.defaultPrevented || !this.player) {
return
}
switch (event.keyCode) {
case KEY_CODES.PLAY:
this.operationPlay()
break
case KEY_CODES.PAUSE:
this.operationPause()
break
case KEY_CODES.STOP:
this.operationPause()
break
case KEY_CODES.ENTER:
this.operationTogglePlay()
break
case KEY_CODES.LEFT:
case KEY_CODES.PREV:
this.operationStepBackward()
break
case KEY_CODES.RIGHT:
case KEY_CODES.NEXT:
this.operationStepForward()
break
}
// Отменяем действие по умолчанию в плеере.
event.preventDefault()
}
private operationTogglePlay() {
if (this.player.paused()) {
this.player.play()
} else {
this.player.pause()
}
}
private operationPlay() {
if (this.player.paused()) {
this.player.play()
}
}
private operationPause() {
this.player.pause()
}
}
private operationPlay() {
if (this.player.paused()) {
this.player.play()
private operationStepBackward() {
const skipTime = this.options.skip?.forward
if (!skipTime) return
const currentVideoTime = this.player.currentTime()
const liveTracker = this.player.liveTracker
const seekableStart =
liveTracker && liveTracker.isLive() && liveTracker.seekableStart()
let newTime
if (seekableStart && currentVideoTime - skipTime <= seekableStart) {
newTime = seekableStart
} else if (currentVideoTime >= skipTime) {
newTime = currentVideoTime - skipTime
} else {
newTime = 0
}
this.player.currentTime(newTime)
}
private operationStepForward() {
const skipTime = this.options.skip?.forward
if (isNaN(this.player.duration()) || !skipTime) {
return
}
const currentVideoTime = this.player.currentTime()
const liveTracker = this.player.liveTracker
const duration =
liveTracker && liveTracker.isLive()
? liveTracker.seekableEnd()
: this.player.duration()
let newTime
if (currentVideoTime + skipTime <= duration) {
newTime = currentVideoTime + skipTime
} else {
newTime = duration
}
this.player.currentTime(newTime)
}
}
private operationPause() {
this.player.pause()
}
private operationStepBackward() {
const skipTime = this.options.skip?.forward
if (!skipTime) return
const currentVideoTime = this.player.currentTime()
const liveTracker = this.player.liveTracker
const seekableStart =
liveTracker && liveTracker.isLive() && liveTracker.seekableStart()
let newTime
if (seekableStart && currentVideoTime - skipTime <= seekableStart) {
newTime = seekableStart
} else if (currentVideoTime >= skipTime) {
newTime = currentVideoTime - skipTime
} else {
newTime = 0
}
this.player.currentTime(newTime)
}
private operationStepForward() {
const skipTime = this.options.skip?.forward
if (isNaN(this.player.duration()) || !skipTime) {
return
}
const currentVideoTime = this.player.currentTime()
const liveTracker = this.player.liveTracker
const duration =
liveTracker && liveTracker.isLive()
? liveTracker.seekableEnd()
: this.player.duration()
let newTime
if (currentVideoTime + skipTime <= duration) {
newTime = currentVideoTime + skipTime
} else {
newTime = duration
}
this.player.currentTime(newTime)
}
}
videojs.registerPlugin('vokaMagicRemotePlugin', VokaMagicRemotePlugin)
videojs.registerPlugin('vokaMagicRemotePlugin', VokaMagicRemotePlugin.Plugin)
export default VokaMagicRemotePlugin
+280 -281
View File
@@ -1,316 +1,315 @@
import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js'
import VokaEvent from "@/constants/VokaEvent"
import Player from 'video.js/dist/types/player'
import { bus } from '@/internal/events/bus'
import { IContent, IMetrics, IMetricsParams } from '@/public/@types'
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 { HTTPMethod, IHTTPClient, HTTPClient, RequestOptions } from '@/internal/utils/HTTPClient'
import { IHTTPClient, HTTPClient } from '@/internal/utils/HTTPClient'
import DateUtils from '@/internal/utils/DateUtils'
import VokaCorePlayer from '@/internal/player/VokaCorePlayer'
import videojs from 'video.js'
const Plugin = videojs.getPlugin('plugin')
const VideoPlugin = videojs.getPlugin('plugin')
interface ITimeSpend {
initializing_player: number
initializing_buffer: number
playing: number
paused: number
buffering: number
}
namespace VokaMetricsPlugin {
export class VokaMetricsPlugin extends Plugin {
private player!: VideoJsPlayer
private playbackInitialized: Boolean
private playbackStarted: Boolean
private prevPlayerState: string
private prevStateTime: number
private metrics: IMetrics | null
private httpClient: IHTTPClient
private defaultWatchSessionId: string
private timeSpent: ITimeSpend | null
private scheduledRequest: ReturnType<typeof setInterval> | null
private timeout: number = 2000
constructor(player: VideoJsPlayer, options: VideoJsPlayerOptions) {
super(player, options)
this.player = player
this.httpClient = new HTTPClient()
this.playbackInitialized = false
this.playbackStarted = false
this.prevPlayerState = ''
this.prevStateTime = 0
this.metrics = null
this.timeSpent = null
this.setupListeners()
interface ITimeSpend {
initializing_player: number
initializing_buffer: number
playing: number
paused: number
buffering: number
}
private setupListeners() {
bus.subscribe(
VokaBusEvent.contentRetrieved,
(event) => {
this.onSourceAttached(event.payload.content)
}
)
this.player.on('play', this.onPlaybackStarted.bind(this))
this.player.on('pause', this.onPlaybackPaused.bind(this))
this.player.on('ended', this.onPlaybackEnded.bind(this))
this.player.on('canplay', this.onPlayerCanplay.bind(this))
// TODO!
// player.addEventListener('bufferingUpdate', onPlayerBufferingUpdate)
}
export class Plugin extends VideoPlugin {
private player!: Player
private playbackInitialized: Boolean
private playbackStarted: Boolean
private prevPlayerState: string
private prevStateTime: number
private metrics: VokaCorePlayer.IMetrics | null
private httpClient: IHTTPClient
private defaultWatchSessionId: string
private timeSpent: ITimeSpend | null
private scheduledRequest: ReturnType<typeof setInterval> | null
private timeout: number = 2000
// Listeners
constructor(player: Player) {
super(player)
private onPlayerBufferingUpdate(event) {
if (this.metrics == null) { return }
this.createRequest('update')
}
this.player = player
this.httpClient = new HTTPClient()
this.playbackInitialized = false
this.playbackStarted = false
this.prevPlayerState = ''
this.prevStateTime = 0
this.metrics = null
this.timeSpent = null
private onSourceAttached(content: IContent) {
this.onPlaybackEnded(null)
this.defaultWatchSessionId = GUIDUtils.generateGUID()
this.metrics = content.metrics
this.playbackInitialized = false
this.playbackStarted = false
this.prevPlayerState = ''
this.prevStateTime = 0
this.timeSpent = null
if (this.metrics == null) { return }
this.scheduleRequestIfNeeded()
this.createRequest('init')
this.createRequest('buffering')
}
private onPlaybackStarted(event) {
if (this.metrics == null) { return }
this.playbackInitialized = true
this.playbackStarted = true
this.scheduleRequestIfNeeded()
this.createRequest('play')
}
private onPlaybackEnded(event) {
this.unscheduleRequest()
this.playbackStarted = false
}
private onPlayerCanplay(event) {
if (this.metrics == null) { return }
this.playbackInitialized = true
this.createRequest('update')
this.scheduleRequestIfNeeded()
}
private onPlaybackPaused(event) {
if (this.metrics == null) { return }
this.scheduleRequestIfNeeded()
this.createRequest('pause')
}
private scheduleRequestIfNeeded() {
if (this.scheduledRequest !== undefined) { return }
this.scheduleRequest()
}
private scheduleRequest() {
this.unscheduleRequest()
if (this.metrics == null) { return }
this.scheduledRequest = TimerUtils.setInterval(
() => { this.createRequest('periodic') },
this.metrics.interval * 1000
)
}
private unscheduleRequest() {
if (this.scheduledRequest == null) { return }
clearInterval(this.scheduledRequest)
this.scheduledRequest = null
}
private createRequest(state: string) {
if (this.metrics == null) { return }
const params = this.metrics.params || {}
let playerState = this.getPlayerState()
switch (state) {
case 'init':
playerState = 'initialization'
break
case 'buffering':
playerState = 'initial_buffering'
break
case 'play':
playerState = 'playing'
break
case 'pause':
playerState = 'paused'
break
case 'periodic':
case 'update':
default:
break
this.setupListeners()
}
if (state === 'update' && playerState === this.prevPlayerState) { return }
private setupListeners() {
this.updateTimeSpent()
this.prevPlayerState = playerState
bus.subscribe(
VokaBusEvent.switchContent,
(event) => {
this.onSourceAttached(event.payload)
}
)
if (state != 'periodic') { return }
if (state == 'periodic') {
this.timeout = this.metrics.interval * 2 * 1000
} else {
this.timeout = 20000
}
const osVersion = BrowserUtils.getOSVersion()
let deviceOS = osVersion.name.toLowerCase()
switch (deviceOS) {
case 'ios':
case 'android':
case 'windows':
case 'macos':
case 'linux':
break
default:
deviceOS = 'other'
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,
user_type: params.user_type,
user_id: params.user_id,
device_id: BrowserUtils.deviceID,
device_os: deviceOS,
device_type: 'browser',
device_player_type: 'native',
application_session_id: GUIDUtils.sessionGUID,
watch_session_id: this.getWatchSessionId(),
resource_uid: params.resource_uid,
resource_type: params.resource_type,
this.player.on('play', this.onPlaybackStarted.bind(this))
this.player.on('pause', this.onPlaybackPaused.bind(this))
this.player.on('ended', this.onPlaybackEnded.bind(this))
this.player.on('canplay', this.onPlayerCanplay.bind(this))
// TODO!
// buffered_duration: Math.floor(player.getBufferLength() * 1000),
bandwidth: Math.floor(bandwidth),
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
}
private getApiUrl(): string | null {
if (this.metrics == null) { return null }
if (this.metrics.apiUrl != null) {
return this.metrics.apiUrl
// player.addEventListener('bufferingUpdate', onPlayerBufferingUpdate)
}
let result = this.metrics.apiHost
if (result == null) {
result = '127.0.0.1'
}
if (result.indexOf('//') < 0) {
result = 'https://' + result
}
return result + '/v2/player'
}
// Listeners
private getWatchSessionId() {
if (this.metrics != null) {
const params = this.metrics.params
if (params != null && params.watch_session_id != null) {
return params.watch_session_id
private onPlayerBufferingUpdate(event) {
if (this.metrics == null) { return }
this.createRequest(this.metrics, 'update')
}
private onSourceAttached(content: VokaCorePlayer.IContent) {
this.onPlaybackEnded(null)
this.defaultWatchSessionId = GUIDUtils.generateGUID()
this.metrics = content.metrics
this.playbackInitialized = false
this.playbackStarted = false
this.prevPlayerState = ''
this.prevStateTime = 0
this.timeSpent = null
if (this.metrics == null) { return }
this.scheduleRequestIfNeeded()
this.createRequest(this.metrics, 'init')
this.createRequest(this.metrics, 'buffering')
}
private onPlaybackStarted(event) {
if (this.metrics == null) { return }
this.playbackInitialized = true
this.playbackStarted = true
this.scheduleRequestIfNeeded()
this.createRequest(this.metrics, 'play')
}
private onPlaybackEnded(event) {
this.unscheduleRequest()
this.playbackStarted = false
}
private onPlayerCanplay(event) {
if (this.metrics == null) { return }
this.playbackInitialized = true
this.createRequest(this.metrics, 'update')
this.scheduleRequestIfNeeded()
}
private onPlaybackPaused(event) {
if (this.metrics == null) { return }
this.scheduleRequestIfNeeded()
this.createRequest(this.metrics, 'pause')
}
private scheduleRequestIfNeeded() {
if (this.scheduledRequest !== undefined) { return }
this.scheduleRequest()
}
private scheduleRequest() {
this.unscheduleRequest()
const metrics = this.metrics
if (metrics == null) { return }
this.scheduledRequest = TimerUtils.setInterval(
() => { this.createRequest(metrics, 'periodic') },
metrics * 1000,
)
}
private unscheduleRequest() {
if (this.scheduledRequest == null) { return }
clearInterval(this.scheduledRequest)
this.scheduledRequest = null
}
private createRequest(metrics: VokaCorePlayer.IMetrics, state: string) {
const params = metrics.params || {}
let playerState = this.getPlayerState()
switch (state) {
case 'init':
playerState = 'initialization'
break
case 'buffering':
playerState = 'initial_buffering'
break
case 'play':
playerState = 'playing'
break
case 'pause':
playerState = 'paused'
break
case 'periodic':
case 'update':
default:
break
}
}
return this.defaultWatchSessionId
}
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' }
return 'playing'
}
if (state === 'update' && playerState === this.prevPlayerState) { return }
private updateTimeSpent() {
if (this.timeSpent == null) {
this.timeSpent = {
initializing_player: 0,
initializing_buffer: 0,
playing: 0,
paused: 0,
buffering: 0
this.updateTimeSpent()
this.prevPlayerState = playerState
if (state != 'periodic') { return }
if (state == 'periodic') {
this.timeout = metrics.interval * 2 * 1000
} else {
this.timeout = 20000
}
const osVersion = BrowserUtils.getOSVersion()
let deviceOS = osVersion.name.toLowerCase()
switch (deviceOS) {
case 'ios':
case 'android':
case 'windows':
case 'macos':
case 'linux':
break
default:
deviceOS = 'other'
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'],
user_type: params['user_type'],
user_id: params.user_id,
device_id: BrowserUtils.deviceID,
device_os: deviceOS,
device_type: 'browser',
device_player_type: 'native',
application_session_id: GUIDUtils.sessionGUID,
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),
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
}
const currTime = DateUtils.getCurrTimeMS()
if (this.prevStateTime == 0) {
private getApiUrl(): string | null {
if (this.metrics == null) { return null }
if (this.metrics.url != null) {
return this.metrics.url
}
return 'https://127.0.0.1/v2/player'
}
private getWatchSessionId() {
if (this.metrics != null) {
const params = this.metrics.params
if (params != null && params.watch_session_id != null) {
return params.watch_session_id
}
}
return this.defaultWatchSessionId
}
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' }
return 'playing'
}
private updateTimeSpent() {
if (this.timeSpent == null) {
this.timeSpent = {
initializing_player: 0,
initializing_buffer: 0,
playing: 0,
paused: 0,
buffering: 0
}
}
const currTime = DateUtils.getCurrTimeMS()
if (this.prevStateTime == 0) {
this.prevStateTime = currTime
}
const diffTime = currTime - this.prevStateTime
this.prevStateTime = currTime
let propName = null
switch (this.prevPlayerState) {
case 'initialization':
propName = 'initializing_player'
break
case 'initial_buffering':
propName = 'initializing_buffer'
break
case 'paused':
propName = 'paused'
break
case 'freezed':
propName = 'buffering'
break
case 'playing':
propName = 'playing'
break
default:
return
}
this.timeSpent[propName] += diffTime
}
const diffTime = currTime - this.prevStateTime
this.prevStateTime = currTime
let propName = null
switch (this.prevPlayerState) {
case 'initialization':
propName = 'initializing_player'
break
case 'initial_buffering':
propName = 'initializing_buffer'
break
case 'paused':
propName = 'paused'
break
case 'freezed':
propName = 'buffering'
break
case 'playing':
propName = 'playing'
break
default:
return
}
this.timeSpent[propName] += diffTime
}
}
videojs.registerPlugin('vokaMetricsPlugin', VokaMetricsPlugin)
videojs.registerPlugin('vokaMetricsPlugin', VokaMetricsPlugin.Plugin)
export default VokaMetricsPlugin
+175 -170
View File
@@ -20,205 +20,210 @@ import MenuItem from "@/components/menu/MenuItem"
import SettingsMenuButton from "../components/control-bar/menu/SettingsMenuButton"
import SettingsQualityMenu from "../components/control-bar/menu/SettingsQualityMenu"
const Plugin = videojs.getPlugin("plugin")
namespace VokaQualityPlugin {
export class VokaQualityPlugin extends Plugin {
constructor(player: VideoJsPlayer) {
super(player)
this.setupListeners(player)
}
const VideoPlugin = videojs.getPlugin("plugin")
addQualityMenuItem(menu: Component) {
const qualityMenuItem = new MenuItem(this.player, {
name: "QualityMenuItem",
value:
QualityMapper.getLabelByQualityType(
QualityType.AUTO,
PlayerOptionsContext.qualityLabelVariant
) || this.player.localize("Auto"),
label: "Quality",
id: "menu-quality-item"
})
export class Plugin extends VideoPlugin {
constructor(player: VideoJsPlayer) {
super(player)
this.setupListeners(player)
}
// @ts-ignore
menu.addChild(qualityMenuItem, {}, 0)
const qualityParsedEvent = bus.subscribe(qualitiesParsed, () => {
const qualityMenuItem = menu.getChild("QualityMenuItem")
if (qualityMenuItem) {
;(qualityMenuItem as MenuItem).valueLabel =
addQualityMenuItem(menu: Component) {
const qualityMenuItem = new MenuItem(this.player, {
name: "QualityMenuItem",
value:
QualityMapper.getLabelByQualityType(
QualityType.AUTO,
PlayerOptionsContext.qualityLabelVariant
) || this.player.localize("Auto")
}
const qualityChangeEvent = bus.subscribe(qualityChange, ({ payload }) => {
this.player.removeClass("vjs-quality-loader")
if (PlayerOptionsContext.isAutoQuality) return
if (qualityMenuItem) {
qualityMenuItem.valueLabel = QualityMapper.getLabelByValue(
payload.quality,
PlayerOptionsContext.qualityLabelVariant
) as string
}
) || this.player.localize("Auto"),
label: "Quality",
id: "menu-quality-item"
})
const qualitySetEvent = bus.subscribe(qualitySet, ({ payload }) => {
if (payload.quality !== Quality.AUTO) return
qualityMenuItem.valueLabel =
QualityMapper.getLabelByQualityType(
QualityType.AUTO,
PlayerOptionsContext.qualityLabelVariant
) || this.player.localize("Auto")
})
// @ts-ignore
menu.addChild(qualityMenuItem, {}, 0)
const qualityUISetEvent = bus.subscribe(qualityUISet, ({ payload }) => {
if (PlayerOptionsContext.isAutoQuality) return
const qualityParsedEvent = bus.subscribe(qualitiesParsed, () => {
const qualityMenuItem = menu.getChild("QualityMenuItem")
if (qualityMenuItem) {
qualityMenuItem.valueLabel = QualityMapper.getLabelByValue(
payload.quality,
PlayerOptionsContext.qualityLabelVariant
) as string
;(qualityMenuItem as MenuItem).valueLabel =
QualityMapper.getLabelByQualityType(
QualityType.AUTO,
PlayerOptionsContext.qualityLabelVariant
) || this.player.localize("Auto")
}
const qualityChangeEvent = bus.subscribe(qualityChange, ({ payload }) => {
this.player.removeClass("vjs-quality-loader")
if (PlayerOptionsContext.isAutoQuality) return
if (qualityMenuItem) {
qualityMenuItem.valueLabel = QualityMapper.getLabelByValue(
payload.quality,
PlayerOptionsContext.qualityLabelVariant
) as string
}
})
const qualitySetEvent = bus.subscribe(qualitySet, ({ payload }) => {
if (payload.quality !== Quality.AUTO) return
qualityMenuItem.valueLabel =
QualityMapper.getLabelByQualityType(
QualityType.AUTO,
PlayerOptionsContext.qualityLabelVariant
) || this.player.localize("Auto")
})
const qualityUISetEvent = bus.subscribe(qualityUISet, ({ payload }) => {
if (PlayerOptionsContext.isAutoQuality) return
if (qualityMenuItem) {
qualityMenuItem.valueLabel = QualityMapper.getLabelByValue(
payload.quality,
PlayerOptionsContext.qualityLabelVariant
) as string
}
})
this.player.one("playerreset", () => {
qualitySetEvent()
qualityChangeEvent()
qualityUISetEvent()
})
})
this.player.one("playerreset", () => {
qualitySetEvent()
qualityChangeEvent()
qualityUISetEvent()
qualityParsedEvent()
})
})
this.player.one("playerreset", () => {
qualityParsedEvent()
})
}
public handleSettingsButton(menuButton: Component) {
bus.subscribe(qualityChange, ({ payload }) => {
this.processMenuButtonBadges(menuButton, payload.quality)
})
bus.subscribe(qualitySet, ({ payload }) => {
if (payload.quality === Quality.AUTO) {
this.removeSettingsButtonBadges(menuButton)
}
})
bus.subscribe(qualityUISet, ({ payload }) => {
this.processMenuButtonBadges(menuButton, payload.quality)
})
}
private processMenuButtonBadges(button: Component, quality: Quality) {
this.removeSettingsButtonBadges(button)
if (PlayerOptionsContext.isAutoQuality) return
if (quality === Quality.P720) {
button.addClass("vjs-menu-button--quality-hd")
return
}
if (quality === Quality.P1080) {
button.addClass("vjs-menu-button--quality-fhd")
return
public handleSettingsButton(menuButton: Component) {
bus.subscribe(qualityChange, ({ payload }) => {
this.processMenuButtonBadges(menuButton, payload.quality)
})
bus.subscribe(qualitySet, ({ payload }) => {
if (payload.quality === Quality.AUTO) {
this.removeSettingsButtonBadges(menuButton)
}
})
bus.subscribe(qualityUISet, ({ payload }) => {
this.processMenuButtonBadges(menuButton, payload.quality)
})
}
}
private removeSettingsButtonBadges(button: Component) {
button.removeClass("vjs-menu-button--quality-hd")
button.removeClass("vjs-menu-button--quality-fhd")
}
private processMenuButtonBadges(button: Component, quality: Quality) {
this.removeSettingsButtonBadges(button)
private setupListeners(player: VideoJsPlayer) {
bus.subscribe(qualityChange, (data) => {
if (PlayerOptionsContext.expectedQuality) {
if (data.payload.quality === PlayerOptionsContext.expectedQuality) {
bus.publish(
contextOptionsUpdated({
expectedQuality: null,
isAutoQuality: false
})
)
bus.publish(
qualityUISet({
quality: data.payload.quality
})
)
}
}
if (PlayerOptionsContext.isAutoQuality) return
bus.publish(
contextOptionsUpdated({
quality: data.payload.quality
})
)
})
bus.subscribe(qualitySet, (data) => {
// Если текущее качество равно проставляемому
if (PlayerOptionsContext.quality === data.payload.quality) {
// Если текущее качество не авто, то не обрабатываем клик
if (!PlayerOptionsContext.isAutoQuality) {
return
} else {
// Если авто, обновляем UI
bus.publish(
contextOptionsUpdated({
isAutoQuality: false
})
)
bus.publish(
qualityUISet({
quality: data.payload.quality
})
)
return
}
}
if (data.payload.quality !== Quality.AUTO) {
player.addClass("vjs-quality-loader")
}
if (data.payload.noAutoChange) {
bus.publish(
contextOptionsUpdated({
expectedQuality: data.payload.quality
})
)
if (quality === Quality.P720) {
button.addClass("vjs-menu-button--quality-hd")
return
}
if (quality === Quality.P1080) {
button.addClass("vjs-menu-button--quality-fhd")
return
}
}
bus.publish(
contextOptionsUpdated({
isAutoQuality: Quality.AUTO === data.payload.quality
})
)
private removeSettingsButtonBadges(button: Component) {
button.removeClass("vjs-menu-button--quality-hd")
button.removeClass("vjs-menu-button--quality-fhd")
}
private setupListeners(player: VideoJsPlayer) {
bus.subscribe(qualityChange, (data) => {
if (PlayerOptionsContext.expectedQuality) {
if (data.payload.quality === PlayerOptionsContext.expectedQuality) {
bus.publish(
contextOptionsUpdated({
expectedQuality: null,
isAutoQuality: false
})
)
bus.publish(
qualityUISet({
quality: data.payload.quality
})
)
}
}
if (data.payload.withUIUpdate) {
bus.publish(
qualityUISet({
contextOptionsUpdated({
quality: data.payload.quality
})
)
}
})
})
bus.subscribe(qualitySet, (data) => {
// Если текущее качество равно проставляемому
if (PlayerOptionsContext.quality === data.payload.quality) {
// Если текущее качество не авто, то не обрабатываем клик
if (!PlayerOptionsContext.isAutoQuality) {
return
} else {
// Если авто, обновляем UI
bus.publish(
contextOptionsUpdated({
isAutoQuality: false
})
)
bus.publish(
qualityUISet({
quality: data.payload.quality
})
)
return
}
}
bus.subscribe(showQualityLoader, ({ payload }) => {
payload.value
? player.addClass("vjs-quality-loader")
: player.removeClass("vjs-quality-loader")
})
player.on("playerreset", () => {
bus.publish(
qualitySet({
quality: -1,
forced: true
})
)
})
if (data.payload.quality !== Quality.AUTO) {
player.addClass("vjs-quality-loader")
}
if (data.payload.noAutoChange) {
bus.publish(
contextOptionsUpdated({
expectedQuality: data.payload.quality
})
)
return
}
bus.publish(
contextOptionsUpdated({
isAutoQuality: Quality.AUTO === data.payload.quality
})
)
if (data.payload.withUIUpdate) {
bus.publish(
qualityUISet({
quality: data.payload.quality
})
)
}
})
bus.subscribe(showQualityLoader, ({ payload }) => {
payload.value
? player.addClass("vjs-quality-loader")
: player.removeClass("vjs-quality-loader")
})
player.on("playerreset", () => {
bus.publish(
qualitySet({
quality: -1,
forced: true
})
)
})
}
}
}
videojs.registerPlugin("vokaQualityPlugin", VokaQualityPlugin)
videojs.registerPlugin("vokaQualityPlugin", VokaQualityPlugin.Plugin)
export default VokaQualityPlugin
+54 -7
View File
@@ -46,8 +46,7 @@ interface IMetricsParams {
}
interface IMetrics {
apiUrl: string | null
apiHost: string | null
url: string
params: IMetricsParams | null
interval: number
}
@@ -86,7 +85,7 @@ namespace VokaOptions {
apiConfig: IAPIConfig
uiConfig: IUIConfig | null
globalOpts: IGlobalOptions | null
streamOpts: IStreamOptions | null
streamOpts: StreamOptions.IStream | null
codecs: ICodecs
tweaks: ITweaks
controls: UIControls.IControls
@@ -136,12 +135,60 @@ namespace VokaOptions {
uiLanguage: string
}
export interface IStreamOptions {
autoplay: boolean
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 IMetrics {
apiHost: string | null
apiUrl: string | null
interval: number
params: IMetricsParams | null
}
export interface IStream {
autoplay: boolean
drmConfig: IDRMConfig | null
metrics: IMetrics | null
heartbeat: IHeartbeat | null
}
}
}
export { IOSVersion, IContent, IContextUpdated, IHeartbeat, IMetricsParams, IMetrics, ICaption,
VokaOptions, Error, VokaErrorMessages }
+68 -7
View File
@@ -69,10 +69,43 @@ export class VokaPlayerImpl implements IVokaPlayer {
private load(content: IResult, player: CorePlayer) {
const drmConfig = { type: VokaCorePlayer.DRMType.NONE } as VokaCorePlayer.IDRMConfig
if (content.drmConfig != null) {
switch (content.drmConfig.type) {
case VokaApi.DRMType.PLAYREADY:
drmConfig.type = VokaCorePlayer.DRMType.PLAYREADY
break
case VokaApi.DRMType.FAIRPLAY:
drmConfig.type = VokaCorePlayer.DRMType.FAIRPLAY
break
case VokaApi.DRMType.WIDEVINE:
drmConfig.type = VokaCorePlayer.DRMType.WIDEVINE
break
}
drmConfig.certificateUrl = content.drmConfig.certificateUrl
drmConfig.licenseUrl = content.drmConfig.keyServerUrl
} else if (content.streamOptions?.drmConfig != null) {
const config = content.streamOptions?.drmConfig
if (config?.fairplay != null) {
drmConfig.type = VokaCorePlayer.DRMType.FAIRPLAY
drmConfig.certificateUrl = config.fairplay.certificate_url
drmConfig.licenseUrl = config.fairplay.ksm_url
} else if (config?.widevine != null) {
drmConfig.type = VokaCorePlayer.DRMType.WIDEVINE
drmConfig.certificateUrl = config.widevine.proxy_url || ""
} else if (config?.playready != null) {
drmConfig.type = VokaCorePlayer.DRMType.PLAYREADY
drmConfig.certificateUrl = config.playready.la_url || ""
}
}
const iContent = {
url: content.url,
type: VokaContentType.HLS,
type: VokaPlayerImpl.contentType(content.url, drmConfig),
drmConfig: drmConfig.type != VokaCorePlayer.DRMType.NONE ? drmConfig : null,
subtitlesUrl: content.subtitlesUrl,
metrics: content.metrics || content.streamOptions?.metrics,
heartbeat: content.streamOptions?.heartbeat
} as VokaCorePlayer.IContent
player.load(iContent)
@@ -81,13 +114,13 @@ export class VokaPlayerImpl implements IVokaPlayer {
private async initialize(player: CorePlayer, features: SupportedCodecs.ISelectProtocolResult): Promise<boolean> {
if (!this.options.features.api) { return false }
const content = await this.fetchContent(this.options, features)
const content = await VokaPlayerImpl.fetchContent(this.options, features)
this.load(content, player)
return true
}
private async fetchContent(options: VokaOptions.IOptions, features: SupportedCodecs.ISelectProtocolResult) {
private static async fetchContent(options: VokaOptions.IOptions, features: SupportedCodecs.ISelectProtocolResult) {
const apiOptions = {
apiHost: options.apiConfig.apiHost,
movieId: options.apiConfig.movieId,
@@ -115,7 +148,7 @@ export class VokaPlayerImpl implements IVokaPlayer {
default: return 'spbtvcas'
}
},
getPlayerVersion: () => `${VokaPlayerImpl.version}.${VokaPlayerImpl.build}`,
getPlayerVersion: () => `${this.version}.${this.build}`,
getBrowserName: () => BrowserUtils.browserInfo().name
} as VokaApi.ISystemCapability
@@ -123,6 +156,25 @@ export class VokaPlayerImpl implements IVokaPlayer {
return api.load()
}
private static contentType(url: string, drmConfig: VokaCorePlayer.IDRMConfig): VokaContentType {
const value = url.toLowerCase()
if (value.indexOf('m3u8') > -1) {
return drmConfig.type == VokaCorePlayer.DRMType.FAIRPLAY
? VokaContentType.FAIRPLAY
: VokaContentType.HLS
}
if (value.indexOf('dash') > -1 || value.indexOf('mpd') > -1) {
if (drmConfig.type == VokaCorePlayer.DRMType.WIDEVINE) {
return VokaContentType.WIDEVINE
}
if (drmConfig.type == VokaCorePlayer.DRMType.PLAYREADY) {
return VokaContentType.PLAYREADY
}
return VokaContentType.DASH
}
return VokaContentType.MP4
}
// MARK: - IVokaPlayer implementation
public afterInitialize(callback: () => void) {
@@ -159,8 +211,17 @@ export class VokaPlayerImpl implements IVokaPlayer {
return features.mseCodecs
}
public attachSource(url: string, options: VokaOptions.IStreamOptions): void {
public attachSource(
url: string,
options: VokaOptions.StreamOptions.IStream
): void {
//private load(content: IResult, player: CorePlayer) {
// TODO!!!!
// REQUIRE CONVERT TO IContent protocol!
}
public getPaused(): boolean {
@@ -352,12 +413,12 @@ export class VokaPlayerImpl implements IVokaPlayer {
}
} as VokaCorePlayer.IContent*/
// Dash WideWine DRM
// Dash WIDEVINE DRM
/*const iContent = {
url: "https://media.axprod.net/TestVectors/Cmaf/protected_1080p_h264_cbcs/manifest.mpd",
type: VokaContentType.WIDEVINE,
drmConfig: {
type: VokaCorePlayer.DRMType.WIDEWINE,
type: VokaCorePlayer.DRMType.WIDEVINE,
certificateUrl: "https://drm-widevine-licensing.axtest.net/AcquireLicense",
headers: {
"X-AxDRM-Message": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJ2ZXJzaW9uIjogMSwKICAiY29tX2tleV9pZCI6ICI2OWU1NDA4OC1lOWUwLTQ1MzAtOGMxYS0xZWI2ZGNkMGQxNGUiLAogICJtZXNzYWdlIjogewogICAgInR5cGUiOiAiZW50aXRsZW1lbnRfbWVzc2FnZSIsCiAgICAidmVyc2lvbiI6IDIsCiAgICAibGljZW5zZSI6IHsKICAgICAgImFsbG93X3BlcnNpc3RlbmNlIjogdHJ1ZQogICAgfSwKICAgICJjb250ZW50X2tleXNfc291cmNlIjogewogICAgICAiaW5saW5lIjogWwogICAgICAgIHsKICAgICAgICAgICJpZCI6ICIzMDJmODBkZC00MTFlLTQ4ODYtYmNhNS1iYjFmODAxOGEwMjQiLAogICAgICAgICAgImVuY3J5cHRlZF9rZXkiOiAicm9LQWcwdDdKaTFpNDNmd3YremZ0UT09IiwKICAgICAgICAgICJ1c2FnZV9wb2xpY3kiOiAiUG9saWN5IEEiCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjogWwogICAgICB7CiAgICAgICAgIm5hbWUiOiAiUG9saWN5IEEiLAogICAgICAgICJwbGF5cmVhZHkiOiB7CiAgICAgICAgICAibWluX2RldmljZV9zZWN1cml0eV9sZXZlbCI6IDE1MCwKICAgICAgICAgICJwbGF5X2VuYWJsZXJzIjogWwogICAgICAgICAgICAiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3IgogICAgICAgICAgXQogICAgICAgIH0KICAgICAgfQogICAgXQogIH0KfQ._NfhLVY7S6k8TJDWPeMPhUawhympnrk6WAZHOVjER6M",
+4 -3
View File
@@ -55,7 +55,8 @@ namespace VokaApi {
export enum DRMType {
FAIRPLAY = 'fairplay',
PLAYREADY = 'playready'
PLAYREADY = 'playready',
WIDEVINE = 'widevine',
}
export interface IDRMConfig {
@@ -69,7 +70,7 @@ namespace VokaApi {
subtitlesUrl: string | null
drmConfig: IDRMConfig | null
metrics: IMetrics | null,
streamOptions: VokaOptions.IStreamOptions | null
streamOptions: VokaOptions.StreamOptions.IStream | null
}
export class Api {
@@ -306,7 +307,7 @@ namespace VokaApi {
'resource_type',
'watch_session_id'
].forEach((item) => {
if ((item in params.additional_parameters) && typeof params.additional_parameters[item] !== 'string') {
if ((item in params.additional_parameters) && typeof params.additional_parameters[item] == 'string') {
metrics.params[item] = params.additional_parameters[item]
}
})