bitmovin-player-ui
Advanced tools
Comparing version 3.31.0 to 3.32.0
@@ -7,8 +7,14 @@ # Change Log | ||
## [3.31.0] | ||
## [3.32.0] - 2021-12-21 - 2021-12-21 | ||
### Fixed | ||
- The scrubber could jump to an old position during a seek operation when it was dragged. | ||
- The Seekbar scrubber could jump to an old position on touch devices when the buffer updates during a seek operation. | ||
## [3.31.0] - 2021-10-12 | ||
### Added | ||
- Style reset for subtitle overlay element to prevent undesired CSS rules collisions. | ||
## [3.30.0] | ||
## [3.30.0] - 2021-09-14 | ||
@@ -18,3 +24,3 @@ ### Added | ||
## [3.29.0] | ||
## [3.29.0] - 2021-08-19 | ||
@@ -24,3 +30,3 @@ ### Fixed | ||
## [3.28.1] | ||
## [3.28.1] - 2021-06-25 | ||
@@ -760,2 +766,3 @@ ### Fixed | ||
[3.32.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.31.0...v3.32.0 | ||
[3.31.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.30.0...v3.31.0 | ||
@@ -762,0 +769,0 @@ [3.30.0]: https://github.com/bitmovin/bitmovin-player-ui/compare/v3.29.0...v3.30.0 |
@@ -96,2 +96,4 @@ import { Component, ComponentConfig } from './component'; | ||
private setAriaSliderValues; | ||
private getPlaybackPositionPercentage; | ||
private updateBufferLevel; | ||
configure(player: PlayerAPI, uimanager: UIInstanceManager, configureSeek?: boolean): void; | ||
@@ -98,0 +100,0 @@ private initializeTimelineMarkers; |
@@ -27,2 +27,3 @@ "use strict"; | ||
var timelinemarkershandler_1 = require("./timelinemarkershandler"); | ||
var seekbarbufferlevel_1 = require("./seekbarbufferlevel"); | ||
/** | ||
@@ -117,2 +118,19 @@ * A seek bar to seek within the player's media. It displays the current playback position, amount of buffed data, seek | ||
}; | ||
SeekBar.prototype.getPlaybackPositionPercentage = function () { | ||
if (this.player.isLive()) { | ||
return 100 - (100 / this.player.getMaxTimeShift() * this.player.getTimeShift()); | ||
} | ||
return 100 / this.player.getDuration() * this.getRelativeCurrentTime(); | ||
}; | ||
SeekBar.prototype.updateBufferLevel = function (playbackPositionPercentage) { | ||
var bufferLoadedPercentageLevel; | ||
if (this.player.isLive()) { | ||
// Always show full buffer for live streams | ||
bufferLoadedPercentageLevel = 100; | ||
} | ||
else { | ||
bufferLoadedPercentageLevel = playbackPositionPercentage + seekbarbufferlevel_1.getMinBufferLevel(this.player); | ||
} | ||
this.setBufferPosition(bufferLoadedPercentageLevel); | ||
}; | ||
SeekBar.prototype.configure = function (player, uimanager, configureSeek) { | ||
@@ -144,2 +162,3 @@ var _this = this; | ||
var isPlaying = false; | ||
var scrubbing = false; | ||
var isUserSeeking = false; | ||
@@ -155,2 +174,11 @@ var isPlayerSeeking = false; | ||
} | ||
var playbackPositionPercentage = _this.getPlaybackPositionPercentage(); | ||
_this.updateBufferLevel(playbackPositionPercentage); | ||
// The segment request finished is used to help the playback position move, when the smooth playback position is not enabled. | ||
// At the same time when the user is scrubbing, we also move the position of the seekbar to display a preview during scrubbing. | ||
// When the user is scrubbing we do not record this as a user seek operation, as the user has yet to finish their seek, | ||
// but we should not move the playback position to not create a jumping behaviour. | ||
if (scrubbing && event.type === player.exports.PlayerEvent.SegmentRequestFinished && playbackPositionPercentage !== _this.playbackPositionPercentage) { | ||
playbackPositionPercentage = _this.playbackPositionPercentage; | ||
} | ||
if (player.isLive()) { | ||
@@ -162,31 +190,18 @@ if (player.getMaxTimeShift() === 0) { | ||
else { | ||
var playbackPositionPercentage = 100 - (100 / player.getMaxTimeShift() * player.getTimeShift()); | ||
_this.setPlaybackPosition(playbackPositionPercentage); | ||
if (!_this.isSeeking()) { | ||
_this.setPlaybackPosition(playbackPositionPercentage); | ||
} | ||
_this.setAriaSliderMinMax(player.getMaxTimeShift().toString(), '0'); | ||
} | ||
// Always show full buffer for live streams | ||
_this.setBufferPosition(100); | ||
} | ||
else { | ||
var playerDuration = player.getDuration(); | ||
var playbackPositionPercentage = 100 / playerDuration * _this.getRelativeCurrentTime(); | ||
var videoBufferLength = player.getVideoBufferLength(); | ||
var audioBufferLength = player.getAudioBufferLength(); | ||
// Calculate the buffer length which is the smaller length of the audio and video buffers. If one of these | ||
// buffers is not available, we set it's value to MAX_VALUE to make sure that the other real value is taken | ||
// as the buffer length. | ||
var bufferLength = Math.min(videoBufferLength != null ? videoBufferLength : Number.MAX_VALUE, audioBufferLength != null ? audioBufferLength : Number.MAX_VALUE); | ||
// If both buffer lengths are missing, we set the buffer length to zero | ||
if (bufferLength === Number.MAX_VALUE) { | ||
bufferLength = 0; | ||
} | ||
var bufferPercentage = 100 / playerDuration * bufferLength; | ||
// Update playback position only in paused state or in the initial startup state where player is neither | ||
// paused nor playing. Playback updates are handled in the Timeout below. | ||
if (_this.config.smoothPlaybackPositionUpdateIntervalMs === SeekBar.SMOOTH_PLAYBACK_POSITION_UPDATE_DISABLED | ||
|| forceUpdate || player.isPaused() || (player.isPaused() === player.isPlaying())) { | ||
var isInInitialStartupState = _this.config.smoothPlaybackPositionUpdateIntervalMs === SeekBar.SMOOTH_PLAYBACK_POSITION_UPDATE_DISABLED | ||
|| forceUpdate || player.isPaused(); | ||
var isNeitherPausedNorPlaying = player.isPaused() === player.isPlaying(); | ||
if ((isInInitialStartupState || isNeitherPausedNorPlaying) && !_this.isSeeking()) { | ||
_this.setPlaybackPosition(playbackPositionPercentage); | ||
} | ||
_this.setBufferPosition(playbackPositionPercentage + bufferPercentage); | ||
_this.setAriaSliderMinMax('0', playerDuration.toString()); | ||
_this.setAriaSliderMinMax('0', player.getDuration().toString()); | ||
} | ||
@@ -204,4 +219,2 @@ if (_this.isUiShown) { | ||
player.on(player.exports.PlayerEvent.StallEnded, playbackPositionHandler); | ||
// update playback position when a seek has finished | ||
player.on(player.exports.PlayerEvent.Seeked, playbackPositionHandler); | ||
// update playback position when a timeshift has finished | ||
@@ -216,6 +229,11 @@ player.on(player.exports.PlayerEvent.TimeShifted, playbackPositionHandler); | ||
_this.setSeeking(true); | ||
scrubbing = false; | ||
}; | ||
var onPlayerSeeked = function () { | ||
var onPlayerSeeked = function (event, forceUpdate) { | ||
if (event === void 0) { event = null; } | ||
if (forceUpdate === void 0) { forceUpdate = false; } | ||
isPlayerSeeking = false; | ||
_this.setSeeking(false); | ||
// update playback position when a seek has finished | ||
playbackPositionHandler(event, forceUpdate); | ||
}; | ||
@@ -250,2 +268,3 @@ var restorePlayingState = function () { | ||
uimanager.onSeekPreview.dispatch(sender, args); | ||
scrubbing = args.scrubbing; | ||
}); | ||
@@ -373,2 +392,5 @@ // Rate-limited scrubbing seek | ||
this.smoothPlaybackPositionUpdater = new timeout_1.Timeout(updateIntervalMs, function () { | ||
if (_this.isSeeking()) { | ||
return; | ||
} | ||
currentTimeSeekBar += currentTimeUpdateDeltaSecs; | ||
@@ -375,0 +397,0 @@ try { |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.version = void 0; | ||
exports.version = '3.31.0'; | ||
exports.version = '3.32.0'; | ||
// Management | ||
@@ -6,0 +6,0 @@ var uimanager_1 = require("./uimanager"); |
{ | ||
"name": "bitmovin-player-ui", | ||
"version": "3.31.0", | ||
"version": "3.32.0", | ||
"description": "Bitmovin Player UI Framework", | ||
@@ -5,0 +5,0 @@ "main": "./dist/js/framework/main.js", |
@@ -55,2 +55,112 @@ import { MockHelper, TestingPlayerAPI } from '../helper/MockHelper'; | ||
}); | ||
describe('playback position', () => { | ||
let setPlaybackPositionSpy: jest.SpyInstance; | ||
beforeEach(() => { | ||
jest.spyOn(playerMock, 'getDuration').mockReturnValue(20); | ||
seekbar.configure(playerMock, uiInstanceManagerMock); | ||
setPlaybackPositionSpy = jest.spyOn(seekbar, 'setPlaybackPosition'); | ||
}) | ||
test.each` | ||
isLive | isSeeking | timesCalled | ||
${false} | ${true} | ${0} | ||
${true} | ${true} | ${0} | ||
${false} | ${false} | ${1} | ||
${true} | ${false} | ${1} | ||
`('will not be set when isLive=$isLive, isSeeking=$isSeeking and a stall ended event is fired', | ||
({isLive, isSeeking, timesCalled}) => { | ||
if (isLive) { | ||
jest.spyOn(playerMock, 'getMaxTimeShift').mockReturnValue(-60); | ||
} | ||
jest.spyOn(seekbar, 'isSeeking').mockReturnValue(isSeeking); | ||
jest.spyOn(playerMock, 'isLive').mockReturnValue(isLive); | ||
playerMock.eventEmitter.fireStallEndedEvent(); | ||
expect(setPlaybackPositionSpy).toHaveBeenCalledTimes(timesCalled); | ||
}) | ||
it('will be moved after a successful seeked event', () => { | ||
playerMock.eventEmitter.fireSeekedEvent(); | ||
expect(setPlaybackPositionSpy).toHaveBeenCalledTimes(1); | ||
}); | ||
it('will move after a successful segment request download', () => { | ||
playerMock.eventEmitter.fireSegmentRequestFinished(); | ||
expect(setPlaybackPositionSpy).toHaveBeenCalledTimes(1); | ||
}); | ||
describe('vod event tracking', () => { | ||
let setBufferPositionSpy: jest.SpyInstance; | ||
beforeEach(() => { | ||
setBufferPositionSpy = jest.spyOn(seekbar, 'setBufferPosition'); | ||
jest.spyOn(playerMock, 'getDuration').mockReturnValue(50); | ||
jest.spyOn(playerMock, 'getSeekableRange').mockImplementation(() => ({start: 30, end: 40})); | ||
const currentTime = 35; | ||
jest.spyOn(playerMock, 'getCurrentTime').mockReturnValue(currentTime); | ||
playerMock.eventEmitter.fireSeekEvent(currentTime); | ||
playerMock.eventEmitter.fireSeekedEvent(); | ||
}) | ||
it('will use the last known playback position location after a successful segment request download and the user is scrubbing', () => { | ||
const firstPlaybackPercentage = seekbar['playbackPositionPercentage']; | ||
jest.spyOn(playerMock, 'getSeekableRange').mockImplementation(() => ({start: 26, end: 30})); | ||
seekbar['onSeekPreviewEvent'](40, true) | ||
playerMock.eventEmitter.fireSegmentRequestFinished(); | ||
expect(setPlaybackPositionSpy).toHaveBeenLastCalledWith(firstPlaybackPercentage); | ||
const expectedPlaybackPercentage = 18; | ||
expect(setBufferPositionSpy).toHaveBeenLastCalledWith(expectedPlaybackPercentage) | ||
}); | ||
it('will update the scrubber location after a successful segment request download and the user is not scrubbing', () => { | ||
jest.spyOn(playerMock, 'getSeekableRange').mockImplementation(() => ({start: 26, end: 30})); | ||
seekbar['onSeekPreviewEvent'](18, false) | ||
playerMock.eventEmitter.fireSegmentRequestFinished(); | ||
expect(setPlaybackPositionSpy).toHaveBeenLastCalledWith(18); | ||
expect(setBufferPositionSpy).toHaveBeenLastCalledWith(18) | ||
}); | ||
}); | ||
}) | ||
describe('buffer levels', () => { | ||
beforeEach(() => { | ||
jest.spyOn(playerMock, 'getDuration').mockReturnValue(20); | ||
jest.spyOn(playerMock, 'getMaxTimeShift').mockReturnValue(-60); | ||
seekbar.configure(playerMock, uiInstanceManagerMock); | ||
}) | ||
test.each` | ||
isLive | ||
${true} | ||
${false} | ||
`('should get updated accordingly when a request has finished and isLive=$isLive', ({isLive}) => { | ||
const setBufferPositionSpy = jest.spyOn(seekbar, 'setBufferPosition'); | ||
jest.spyOn(playerMock, 'isLive').mockReturnValue(isLive); | ||
jest.spyOn(playerMock, 'getCurrentTime').mockReturnValue(35); | ||
jest.spyOn(playerMock, 'getSeekableRange').mockReturnValue({start:30, end: 40}); | ||
playerMock.eventEmitter.fireSegmentRequestFinished(); | ||
expect(setBufferPositionSpy).toHaveBeenCalledTimes(1); | ||
expect(setBufferPositionSpy).toHaveBeenCalledWith(isLive ? 100 : 25); | ||
}) | ||
}); | ||
}); |
@@ -108,2 +108,3 @@ import { PlayerAPI, PlayerEvent } from 'bitmovin-player'; | ||
isViewModeAvailable: jest.fn(), | ||
seek: jest.fn(), | ||
@@ -110,0 +111,0 @@ // Event faker |
@@ -16,2 +16,3 @@ import { | ||
SeekEvent, | ||
SegmentRequestFinishedEvent, | ||
SubtitleCueEvent, | ||
@@ -66,2 +67,17 @@ SubtitleEvent, | ||
public fireSegmentRequestFinished() { | ||
this.fireEvent<SegmentRequestFinishedEvent>({ | ||
timestamp: Date.now(), | ||
type: PlayerEvent.SegmentRequestFinished, | ||
httpStatus: 200, | ||
downloadTime: 0, | ||
mimeType: 'video/mp4', | ||
success: true, | ||
uid: 'unique-id', | ||
size: 1, | ||
duration: 1, | ||
isInit: false, | ||
}); | ||
} | ||
fireAdBreakFinishedEvent(): void { | ||
@@ -68,0 +84,0 @@ this.fireEvent<AdBreakEvent>({ |
@@ -17,2 +17,3 @@ import { Component, ComponentConfig } from './component'; | ||
import { TimelineMarkersHandler } from './timelinemarkershandler'; | ||
import { getMinBufferLevel } from './seekbarbufferlevel'; | ||
@@ -175,2 +176,23 @@ /** | ||
private getPlaybackPositionPercentage(): number { | ||
if (this.player.isLive()) { | ||
return 100 - (100 / this.player.getMaxTimeShift() * this.player.getTimeShift()); | ||
} | ||
return 100 / this.player.getDuration() * this.getRelativeCurrentTime(); | ||
} | ||
private updateBufferLevel(playbackPositionPercentage: number): void { | ||
let bufferLoadedPercentageLevel: number; | ||
if (this.player.isLive()) { | ||
// Always show full buffer for live streams | ||
bufferLoadedPercentageLevel = 100; | ||
} else { | ||
bufferLoadedPercentageLevel = playbackPositionPercentage + getMinBufferLevel(this.player); | ||
} | ||
this.setBufferPosition(bufferLoadedPercentageLevel); | ||
} | ||
configure(player: PlayerAPI, uimanager: UIInstanceManager, configureSeek: boolean = true): void { | ||
@@ -209,2 +231,3 @@ super.configure(player, uimanager); | ||
let isPlaying = false; | ||
let scrubbing = false; | ||
let isUserSeeking = false; | ||
@@ -220,2 +243,14 @@ let isPlayerSeeking = false; | ||
let playbackPositionPercentage = this.getPlaybackPositionPercentage(); | ||
this.updateBufferLevel(playbackPositionPercentage); | ||
// The segment request finished is used to help the playback position move, when the smooth playback position is not enabled. | ||
// At the same time when the user is scrubbing, we also move the position of the seekbar to display a preview during scrubbing. | ||
// When the user is scrubbing we do not record this as a user seek operation, as the user has yet to finish their seek, | ||
// but we should not move the playback position to not create a jumping behaviour. | ||
if (scrubbing && event.type === player.exports.PlayerEvent.SegmentRequestFinished && playbackPositionPercentage !== this.playbackPositionPercentage) { | ||
playbackPositionPercentage = this.playbackPositionPercentage; | ||
} | ||
if (player.isLive()) { | ||
@@ -226,38 +261,20 @@ if (player.getMaxTimeShift() === 0) { | ||
} else { | ||
let playbackPositionPercentage = 100 - (100 / player.getMaxTimeShift() * player.getTimeShift()); | ||
this.setPlaybackPosition(playbackPositionPercentage); | ||
if (!this.isSeeking()) { | ||
this.setPlaybackPosition(playbackPositionPercentage); | ||
} | ||
this.setAriaSliderMinMax(player.getMaxTimeShift().toString(), '0'); | ||
} | ||
// Always show full buffer for live streams | ||
this.setBufferPosition(100); | ||
} else { | ||
const playerDuration = player.getDuration(); | ||
let playbackPositionPercentage = 100 / playerDuration * this.getRelativeCurrentTime(); | ||
let videoBufferLength = player.getVideoBufferLength(); | ||
let audioBufferLength = player.getAudioBufferLength(); | ||
// Calculate the buffer length which is the smaller length of the audio and video buffers. If one of these | ||
// buffers is not available, we set it's value to MAX_VALUE to make sure that the other real value is taken | ||
// as the buffer length. | ||
let bufferLength = Math.min( | ||
videoBufferLength != null ? videoBufferLength : Number.MAX_VALUE, | ||
audioBufferLength != null ? audioBufferLength : Number.MAX_VALUE); | ||
// If both buffer lengths are missing, we set the buffer length to zero | ||
if (bufferLength === Number.MAX_VALUE) { | ||
bufferLength = 0; | ||
} | ||
let bufferPercentage = 100 / playerDuration * bufferLength; | ||
// Update playback position only in paused state or in the initial startup state where player is neither | ||
// paused nor playing. Playback updates are handled in the Timeout below. | ||
if (this.config.smoothPlaybackPositionUpdateIntervalMs === SeekBar.SMOOTH_PLAYBACK_POSITION_UPDATE_DISABLED | ||
|| forceUpdate || player.isPaused() || (player.isPaused() === player.isPlaying())) { | ||
const isInInitialStartupState = this.config.smoothPlaybackPositionUpdateIntervalMs === SeekBar.SMOOTH_PLAYBACK_POSITION_UPDATE_DISABLED | ||
|| forceUpdate || player.isPaused(); | ||
const isNeitherPausedNorPlaying = player.isPaused() === player.isPlaying(); | ||
if ((isInInitialStartupState || isNeitherPausedNorPlaying) && !this.isSeeking()) { | ||
this.setPlaybackPosition(playbackPositionPercentage); | ||
} | ||
this.setBufferPosition(playbackPositionPercentage + bufferPercentage); | ||
this.setAriaSliderMinMax('0', playerDuration.toString()); | ||
this.setAriaSliderMinMax('0', player.getDuration().toString()); | ||
} | ||
@@ -277,4 +294,2 @@ | ||
player.on(player.exports.PlayerEvent.StallEnded, playbackPositionHandler); | ||
// update playback position when a seek has finished | ||
player.on(player.exports.PlayerEvent.Seeked, playbackPositionHandler); | ||
// update playback position when a timeshift has finished | ||
@@ -291,7 +306,11 @@ player.on(player.exports.PlayerEvent.TimeShifted, playbackPositionHandler); | ||
this.setSeeking(true); | ||
scrubbing = false; | ||
}; | ||
let onPlayerSeeked = () => { | ||
let onPlayerSeeked = (event: PlayerEventBase = null, forceUpdate: boolean = false ) => { | ||
isPlayerSeeking = false; | ||
this.setSeeking(false); | ||
// update playback position when a seek has finished | ||
playbackPositionHandler(event, forceUpdate); | ||
}; | ||
@@ -333,2 +352,3 @@ | ||
uimanager.onSeekPreview.dispatch(sender, args); | ||
scrubbing = args.scrubbing; | ||
}); | ||
@@ -489,2 +509,6 @@ | ||
this.smoothPlaybackPositionUpdater = new Timeout(updateIntervalMs, () => { | ||
if (this.isSeeking()) { | ||
return; | ||
} | ||
currentTimeSeekBar += currentTimeUpdateDeltaSecs; | ||
@@ -491,0 +515,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
4429466
438
49510