#68064 Абсолютные значения в hls live

This commit is contained in:
Alex Manaev
2025-12-01 22:03:44 +03:00
parent bfc6f1db50
commit 5cdcbea67d
18 changed files with 210 additions and 110 deletions
+15 -16
View File
@@ -178,6 +178,7 @@ export const DashErrorTypeIterable = {
...DashPlaybackErrorType,
...DashDrmErrorType
}
export type DashProgramDateTimeAPI = Pick<VokaDash, 'getProgramDateTimeOffset' | 'getTimeshiftAvailable'>
export interface VokaDashOptions extends VokaOptionsType {
timeout: {
chunk: number
@@ -606,25 +607,23 @@ export default class VokaDash {
return (this.mediaPlayer as MediaPlayerClass).duration()
}
getAbsoluteRange(): any | null {
if (!this.mediaPlayer) return null
const range = { start: 0, end: 0 }
if (!this.mediaPlayer.isDynamic()) {
range.end = this.mediaPlayer.duration()
return range
/**
* Абсолютное время (секунды) для DASH на основе availabilityStartTime (AST).
*/
getProgramDateTimeOffset(): number {
if (!this.mediaPlayer || !this.mediaPlayer.isDynamic()) {
return 0
}
const dashMetrics = this.mediaPlayer?.getDashMetrics()
if (!dashMetrics) return null
if (!dashMetrics) return 0
try {
const dvrInfo = dashMetrics.getCurrentDVRInfo('video')
range.start = dvrInfo.manifestInfo.availableFrom.getTime() / 1000 + dvrInfo.range.start
range.end = this.mediaPlayer?.timeAsUTC() || 0
return range
} catch {
return 0
}
const dvrInfo = dashMetrics.getCurrentDVRInfo('video')
const availableFrom = dvrInfo?.manifestInfo?.availableFrom
if (availableFrom && typeof availableFrom.getTime === 'function') {
return availableFrom.getTime() / 1000
}
} catch {}
return 0
}
getTimeshiftAvailable() {
+58 -26
View File
@@ -1,8 +1,8 @@
import chromecastPlugin from '@silvermine/videojs-chromecast/'
import { Promise } from 'es6-promise'
import { EventBus } from 'ts-bus'
import type { VideoJsPlayer } from 'video.js'
import videojs from 'video.js'
import type Player from 'video.js/dist/types/player'
import VokaEvent from '@/constants/VokaEvent'
import VokaBusEvent from '@/internal/events/VokaBusEvent'
@@ -28,6 +28,7 @@ import type { VokaOptions } from '@/public/@types'
import type { ITimeRange } from '@/public/IVokaPlayer'
import type { Quality } from '@/public/models/ILoadOptions'
import { VokaContentType } from '@/public/models/VokaContentType'
import type { VideoJsPlayer } from 'video.js'
import '@/components/PosterImage'
import '@/components/Skin'
@@ -39,10 +40,10 @@ import '@/plugins/VokaLogPlugin'
import '@/plugins/VokaMagicRemotePlugin'
import '@/plugins/VokaMetricsPlugin'
const log = videojs.log.createLogger('[VokaCorePlayer]') as typeof videojs.log
const log = videojs.log.createLogger('[VokaCorePlayer]')
namespace VokaCorePlayer {
type VideoJsPlayerOptions = Parameters<typeof videojs>[1]
type PlayerReadyHandler = (player: CorePlayer) => void
const playbackRates = [ 0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ]
@@ -115,7 +116,7 @@ namespace VokaCorePlayer {
export class CorePlayer {
private readonly bus: EventBus
private player!: Player
private player!: VideoJsPlayer
private autoPlaySupported: boolean
private autoPlayOption: boolean | 'muted' | any
private readonly stateEmitter: EventBus
@@ -204,25 +205,28 @@ namespace VokaCorePlayer {
}
public getAbsoluteRange(): ITimeRange | null {
if (this.player.liveTracker) {
// Для live возвращаем абсолютный диапазон; для VOD — 0/длительность
const d = this.player.duration()
if (!isFinite(d)) {
const lt = this.player.liveTracker
const startRel = lt ? lt.seekableStart() : 0
const endRel = lt ? lt.seekableEnd() : 0
const tech = this.player.tech({ safeUsage: true }) as VokaTech.IVokaBaseTech
const offset: number = tech.getProgramDateTimeOffset()
return {
start: this.player.liveTracker.seekableStart(),
end: this.player.liveTracker.seekableEnd()
start: offset + startRel,
end: offset + endRel
}
}
return {
start: 0,
end: this.player.duration() || 0
end: d || 0
}
}
public getTimeshiftAvailable(): boolean {
const tech = this.player.tech({ safeUsage: true }) as VokaTech.Tech
// This require because Tizen and WebOS do not inherit from VokaTech
if (typeof tech['getTimeshiftAvailable'] === 'function') {
return tech.getTimeshiftAvailable()
}
return false
const tech = this.player.tech({ safeUsage: true }) as VokaTech.IVokaBaseTech
return tech.getTimeshiftAvailable()
}
public get isPaused(): boolean {
@@ -255,26 +259,44 @@ namespace VokaCorePlayer {
public seek(seconds: number): void {
log('[seek()] called with seconds', seconds)
if (Math.abs(this.currentTime - seconds) < 0.5 || !isFinite(seconds)) {
// Прыжок к live edge по соглашению: Infinity
if (seconds === Infinity || seconds === Number.POSITIVE_INFINITY) {
const lt = this.player.liveTracker
if (lt && this.isLive) {
try { lt.seekToLiveEdge() } catch {}
}
// Для Infinity больше ничего не делаем
return
}
// Избегаем микроскачков
if (Math.abs(this.currentTime - seconds) < 0.5) {
log('[seek()] Skip step less than 0.5 sec')
return
}
const range = this.getAbsoluteRange()
const start = range?.start ?? 0
const end = range?.end || Infinity
if (seconds < start) {
seconds = start
log('[seek()] reset seconds to start value', seconds, start)
} else if (seconds >= end) {
seconds = end - 0.5
log('[seek()] reset seconds under end value', seconds, end)
// Кламп по доступному seekable/длительности
const tr = this.player.seekable()
let target = seconds
if (tr && tr.length) {
const start = tr.start(0)
const end = tr.end(tr.length - 1)
// небольшой отступ от конца окна
const max = isFinite(end) ? Math.max(start, end - 0.5) : end
if (target < start) target = start
else if (isFinite(max) && target > max) target = max
} else {
const d = this.player.duration() || 0
if (target < 0) target = 0
if (isFinite(d) && target > d) target = Math.max(0, d - 0.5)
}
this.player.currentTime(seconds)
this.player.currentTime(target)
}
// ---
public mute(on: boolean) {
this.player.muted(on)
}
@@ -306,6 +328,16 @@ namespace VokaCorePlayer {
return !isFinite(duration)
}
/**
* Абсолютное текущее время (секунды Unix) для live.
*/
public getAbsoluteCurrentTime(): number | null {
if (!this.isLive) return this.player.currentTime() || 0
const tech = this.player.tech({ safeUsage: true }) as VokaTech.IVokaBaseTech
const offset: number = tech.getProgramDateTimeOffset()
return offset + (this.player.currentTime() || 0)
}
public async load(content: IContent): Promise<void> {
if (!content.url) {
throw new Error('Empty content URL')
+24 -9
View File
@@ -1,9 +1,10 @@
import VokaEvent from '@/constants/VokaEvent'
import { IVokaSource } from '@/internal/player/native/VokaSourceHandler'
import { silencePromise } from '@/internal/utils/promise'
import window from 'global'
import videojs from 'video.js'
import VokaEvent from '@/constants/VokaEvent'
import type { IVokaSource } from '@/internal/player/native/VokaSourceHandler'
import { silencePromise } from '@/internal/utils/promise'
import setupSourceset from './setup-sourceset'
const browser = videojs.browser
@@ -15,12 +16,22 @@ namespace VokaTech {
const VideoTech = videojs.getTech('Tech')
export interface ITimeRange {
start: number
end: number
/**
* Базовый интерфейс для поддержки абсолютного времени.
*/
export interface IVokaBaseTech {
/**
* Абсолютное время (секунды), такое что absolute = offset + relative.
* Для VOD — 0; для LIVE — зависит от технологии (HLS/DASH/нативный).
*/
getProgramDateTimeOffset(): number
/**
* Доступен ли таймшифт для текущего контента.
*/
getTimeshiftAvailable(): boolean
}
export class Tech extends VideoTech {
export class Tech extends VideoTech implements IVokaBaseTech {
private rememberedVideoTag: Element | null
private parentNode: Element | null
@@ -770,8 +781,12 @@ namespace VokaTech {
return this.el().offsetHeight
}
getAbsoluteRange(): ITimeRange | null {
return null
/**
* Абсолютное время (секунды), такое что absolute = offset + relative.
* По умолчанию поддержка отсутствует — возвращаем 0.
*/
getProgramDateTimeOffset(): number {
return 0
}
getTimeshiftAvailable(): boolean {
@@ -56,4 +56,4 @@ export default class VokaAppleSourceHandler extends VokaSourceHandler {
}
dispose() {}
}
}
@@ -40,4 +40,4 @@ export default class VokaFairplaySourceHandler extends VokaSourceHandler {
}
dispose() {}
}
}
@@ -1,8 +1,11 @@
import videojs from 'video.js'
import VokaTech from '@/internal/player/native/VokaTech'
import VokaFairplaySourceHandler from '../sourcehandler/VokaFairplaySourceHandler'
import type { IVokaSource} from '../../VokaSourceHandler'
import { VokaSourceHandler } from '../../VokaSourceHandler'
import VokaAppleSourceHandler from '../sourcehandler/VokaAppleSourceHandler'
import { IVokaSource, VokaSourceHandler } from '../../VokaSourceHandler'
import VokaFairplaySourceHandler from '../sourcehandler/VokaFairplaySourceHandler'
const Browser = videojs.browser
const Dom = videojs.dom
@@ -236,6 +239,21 @@ class VokaAppleTech extends VokaTech.Tech {
static get featuresNativeAudioTracks() {
return VokaAppleTech.supportsNativeAudioTracks()
}
seekable() {
return this.el().seekable || videojs.createTimeRanges([])
}
/**
* Абсолютное время (секунды) для нативного HLS: предполагаем seekableEnd ≈ now.
*/
getProgramDateTimeOffset(): number {
const seekable: TimeRanges = this.seekable() as any
if (!seekable || seekable.length < 1) return 0
const endRel = seekable.end(seekable.length - 1)
if (!isFinite(endRel)) return 0
return (Date.now() / 1000) - endRel
}
}
VokaTech.Tech.withSourceHandlers(VokaAppleTech)
@@ -244,4 +262,4 @@ VokaAppleTech.registerSourceHandler(new VokaFairplaySourceHandler()) // FairPlay
VokaAppleTech.registerSourceHandler(new VokaAppleSourceHandler())
VokaAppleTech.registerTech('VokaAppleTech', VokaAppleTech)
export default VokaAppleTech
export default VokaAppleTech
@@ -56,4 +56,4 @@ export default class VokaDashSourceHandler extends VokaSourceHandler {
}
dispose() {}
}
}
@@ -1,10 +1,12 @@
import videojs from "video.js"
import VokaDashSourceHandler from '../sourcehandler/VokaDashSourceHandler'
import PlatformCapabilities from '@/internal/utils/PlatformCapabilities'
import { DashContext } from '../processors/DashContext'
import type { DashErrorType, DashProgramDateTimeAPI } from '@/internal/drm/dash/VokaDash'
import type { IVokaSource } from '@/internal/player/native/VokaSourceHandler'
import VokaTech from '@/internal/player/native/VokaTech'
import { DashErrorType } from '@/internal/drm/dash/VokaDash'
import { IVokaSource } from '@/internal/player/native/VokaSourceHandler'
import PlatformCapabilities from '@/internal/utils/PlatformCapabilities'
import { DashContext } from '../processors/DashContext'
import VokaDashSourceHandler from '../sourcehandler/VokaDashSourceHandler'
const Browser = videojs.browser
@@ -65,12 +67,17 @@ class VokaDashTech extends VokaTech.Tech {
this.sourceHandler_.switchLevel(level)
}
getAbsoluteRange() {
return this.sourceHandler_.getAbsoluteRange()
getTimeshiftAvailable() {
const sh = this.sourceHandler_ as DashProgramDateTimeAPI | null
return sh ? sh.getTimeshiftAvailable() : false
}
getTimeshiftAvailable() {
return this.sourceHandler_.getTimeshiftAvailable()
/**
* Абсолютное время для DASH (секунды).
*/
getProgramDateTimeOffset(): number {
const sh = this.sourceHandler_ as DashProgramDateTimeAPI | null
try { return sh?.getProgramDateTimeOffset?.() ?? 0 } catch { return 0 }
}
/* Static Methods ------------------------------------------------ */
@@ -332,4 +339,4 @@ VokaTech.Tech.withSourceHandlers(VokaDashTech)
VokaDashTech.registerSourceHandler(new VokaDashSourceHandler())
VokaTech.Tech.registerTech('VokaDashTech', VokaDashTech)
export default VokaDashTech
export default VokaDashTech
@@ -1,4 +1,4 @@
import Hls, {
import type {
ErrorData,
Events,
FragLoadedData,
@@ -10,22 +10,25 @@ import Hls, {
ManifestParsedData,
MediaPlaylist,
} from 'hls.js'
import Player from 'video.js/dist/types/player'
import { trigger } from '@/internal/utils/events'
import Hls from 'hls.js'
import type { EventBus } from 'ts-bus'
import type Player from 'video.js/dist/types/player'
import VokaEvent from '@/constants/VokaEvent'
import HlsProcessorContext from './HlsProcessorContext'
import { wait } from '@/monads/Monoids'
import Future from '@/monads/Future'
import { UrlUtilities } from '@/internal/utils/UrlUtilities'
import HlsLoadContext from './HlsLoadContext'
import VokaBusEvent from '@/internal/events/VokaBusEvent'
import { VokaError } from '@/public/models/VokaError'
import { Quality } from '@/public/models/ILoadOptions'
import { IHlsErrorHandler } from './error-handlers/BaseHandler'
import { LevelErrorHandler } from './error-handlers/LevelErrorHandler'
import { ErrorHandler } from './error-handlers/ErrorHandler'
import { trigger } from '@/internal/utils/events'
import { UrlUtilities } from '@/internal/utils/UrlUtilities'
import type Future from '@/monads/Future'
import { wait } from '@/monads/Monoids'
import type { Quality } from '@/public/models/ILoadOptions'
import { QualityMapper } from '@/public/models/QualityMapper'
import { EventBus } from 'ts-bus'
import { VokaError } from '@/public/models/VokaError'
import type { IHlsErrorHandler } from './error-handlers/BaseHandler'
import { ErrorHandler } from './error-handlers/ErrorHandler'
import { LevelErrorHandler } from './error-handlers/LevelErrorHandler'
import HlsLoadContext from './HlsLoadContext'
import HlsProcessorContext from './HlsProcessorContext'
export const LIVE_SYNC_DURATION = 10
export const HLS_LEVEL_AUTO = -1
@@ -322,7 +325,7 @@ export default class HlsProcessor {
({ payload }) => {
const newId = payload.audioTrackId
if (!this.hls || typeof newId !== 'number' ) return
this.hls.audioTrack = newId;
this.hls.audioTrack = newId
}
)
@@ -335,7 +338,7 @@ export default class HlsProcessor {
({ payload }) => {
const newId = payload.trackId
if (!this.hls || typeof newId !== 'number' ) return
this.hls.subtitleTrack = newId;
this.hls.subtitleTrack = newId
}
)
@@ -482,24 +485,21 @@ export default class HlsProcessor {
return isFinite(timestamp) ? timestamp - curr : NaN
}
getAbsoluteRange() {
if (!this.isLive) {
return {
start: 0,
end: this.player.duration()
}
}
const start = this.getVideoStartDate() ?? this.getFakeVideoStartDate()
if (!isFinite(start)) return null
const seekableRange = this.getVideoSeekableRange()
if (!seekableRange) return null
return {
start: seekableRange.start + start,
end: seekableRange.end + start
/**
* Абсолютное время (секунды) для получения абсолютного времени по HLS.
* При наличии PDT используем его; иначе резерв через конец окна (seekableEnd ~ now).
*/
getProgramDateTimeOffset(): number {
if (!this.isLive) return 0
const dt = this.getVideoStartDate()
if (isFinite(dt as number)) {
return dt as number
}
const range = this.getVideoSeekableRange()
if (!range) return 0
const off = (Date.now() / 1000) - range.end
return isFinite(off) ? off : 0
}
getTimeshiftAvailable() {
@@ -512,7 +512,7 @@ export default class HlsProcessor {
return false
}
return true;
return true
}
reload(startSeconds = null) {
@@ -59,4 +59,4 @@ export default class VokaHlsSourceHandler extends VokaSourceHandler {
}
dispose() {}
}
}
@@ -1,4 +1,3 @@
import { IContextUpdated } from '@/public/@types'
import Hls from 'hls.js'
import type { Level, MediaPlaylist } from 'hls.js/lib/types/level'
import type { EventBus } from 'ts-bus'
@@ -14,6 +13,7 @@ import { on, trigger } from '@/internal/utils/events'
import * as Fn from '@/internal/utils/fn'
import PlatformCapabilities from '@/internal/utils/PlatformCapabilities'
import { wait } from '@/monads/Monoids'
import type { IContextUpdated } from '@/public/@types'
import { Environment } from '@/public/models/Environment'
import { QualityLabelVariant } from '@/public/models/ILoadOptions'
import { Quality } from '@/public/models/ILoadOptions'
@@ -312,8 +312,11 @@ class VokaHlsTech extends VokaTech.Tech {
this.hlsProcessor.switchLevel(level)
}
getAbsoluteRange() {
return this.hlsProcessor.getAbsoluteRange()
/**
* Абсолютное время для HLS (в секундах).
*/
getProgramDateTimeOffset(): number {
return this.hlsProcessor.getProgramDateTimeOffset()
}
getTimeshiftAvailable() {
@@ -38,4 +38,4 @@ export default class VokaMp4SourceHandler extends VokaSourceHandler {
}
dispose() {}
}
}
@@ -225,4 +225,4 @@ const setupSourceset = function(tech) {
}
}
export default setupSourceset
export default setupSourceset
@@ -51,4 +51,4 @@ export default class VokaTizenSourceHandler extends VokaSourceHandler {
}
dispose() {}
}
}
@@ -672,6 +672,19 @@ class VokaTizenTech extends Tech {
static get featuresNativeAudioTracks() {
return false
}
/**
* Абсолютное время для AVPlay недоступно — возвращаем offset=0.
*/
getProgramDateTimeOffset(): number {
return 0
}
// Абсолютный диапазон не используем на уровне tech.
getTimeshiftAvailable(): boolean {
return false
}
}
VokaTizenTech.withSourceHandlers(VokaTizenTech)
@@ -115,4 +115,4 @@ export default class VokaWebOSSourceHandler extends VokaSourceHandler {
)
return nativeDRM
}
}
}
@@ -508,6 +508,19 @@ class VokaWebOSTech extends Tech {
static get featuresNativeAudioTracks() {
return false
}
/**
* Абсолютное время для WebOS (нативное HTML5) не вычисляем — offset=0.
*/
getProgramDateTimeOffset(): number {
return 0
}
// Абсолютный диапазон не используем на уровне tech.
getTimeshiftAvailable(): boolean {
return false
}
}
VokaWebOSTech.withSourceHandlers(VokaWebOSTech)
+1 -1
View File
@@ -722,7 +722,7 @@ export class VokaPlayerImpl implements IVokaPlayer {
if (this._player == null) {
return null
}
return this.getCurrentTime()
return this._player.getAbsoluteCurrentTime()
}
getAbsoluteTimeRange(): ITimeRange | null {