fix: video player playback rate
Pull Request Type
- [x] Bugfix
- [ ] Feature Implementation
- [ ] Documentation
- [ ] Other
Related issue
closes #8226
Description
Fixes a bug with the playback rate jumping to a wrong value (bug described in issue #8226).
Bug details
When loading the video player the playback rate is set based on a prop, which keeps track of the playback rate of a session. So, when playing the next video in a playlist or selecting a video from the next videos section, this correctly allows for keeping the same playback rate. While the current playback rate is set correctly, it is also used to set the default playback rate:
// ft-shaka-video-player.js line 2576-2577
videoElement.playbackRate = props.currentPlaybackRate
videoElement.defaultPlaybackRate = props.currentPlaybackRate
Now, when we set the playback rate back to the actual default playback rate, we correctly stop the trick play mode, but this makes the video player use the default playback rate of the video element, which is wrong from the snippet above, and propagate the value through an event update (update value in UI).
// ft-shaka-video-player.js line 1955-1970
function changePlayBackRate(step) {
const newPlaybackRateString = (player.getPlaybackRate() + step).toFixed(2)
const newPlaybackRate = parseFloat(newPlaybackRateString)
// The following error is thrown if you go below 0.07:
// The provided playback rate (0.05) is not in the supported playback range.
if (newPlaybackRate > 0.07 && newPlaybackRate <= maxVideoPlaybackRate.value) {
if (newPlaybackRate === defaultPlaybackRate.value) {
player.cancelTrickPlay()
} else {
player.trickPlay(newPlaybackRate, false)
}
showValueChange(`${newPlaybackRateString}x`)
}
}
So we can fix this first bug by correctly providing the default playback rate, but this introduces a second bug where after loading the video player, the current playback rate is always reset to the default playback rate. This is because the current playback rate value of the video element is reset to the default value set when the video element is attached to the local player.
// ft-shaka-video-player.js line 2576-2590
videoElement.playbackRate = props.currentPlaybackRate
videoElement.defaultPlaybackRate = props.currentPlaybackRate
const localPlayer = new shaka.Player()
ui = new shaka.ui.Overlay(
localPlayer,
container.value,
videoElement,
vrCanvas.value
)
await localPlayer.attach(videoElement) // videoElement.playbackRate is reset to default value
I assume this is the reason, why the default playback rate of the video element was set to the current playback rate and not the actual default playback rate.
Fixes
- set the default playback rate value of the video element (
videoElement.defaultPlaybackRate) based on global default playback rate (defaultPlaybackRate.value) instead of the current playback rate (props.currentPlaybackRate) - set the playback rate and default playback rate on the video element after calling
.attach()to avoid the playback rate to be overridden- an alternative solution would be to figure out why shaka player resets the value and maybe fix it there
Testing
- open a video
- set a non-default playback rate
- select a new video
- either via "Up next" section
- or via a playlist
- set playback rate back to the default playback rate (not via the UI button)
- either via ctrl+mouse scroll
- or via hotkeys
PorO
- when reaching the default playback rate, is now should not jump to the playback rate of the previous video, but correctly stay at the default playback rate
Desktop
- OS: Ubuntu
- OS Version: 24.04.2 LTS
- FreeTube version: v0.23.12 Beta
Additional context
The reason why await localPlayer.attach(videoElement) resets the playback rate to the default value of the video element is, that the source of the HTMLMediaElement is changed, which resets the playback rate to the set default value.
Excerpts from the code of the shaka video player:
// lib/player.js
async attach(mediaElement, initializeMediaSource = true) {
// ...
this.video_ = mediaElement;
// ...
await this.initializeMediaSourceEngineInner_(); // <-- calls
}
// ...
async initializeMediaSourceEngineInner_() {
// ...
const mediaSourceEngine = this.createMediaSourceEngine( // <-- calls
this.video_,
// ...
)
// ...
}
// ...
createMediaSourceEngine(mediaElement, textDisplayer, playerInterface,
lcevcDec, config) {
return new shaka.media.MediaSourceEngine( // <-- calls
mediaElement,
textDisplayer,
playerInterface,
config,
lcevcDec);
}
// lib/media/media_source_engine.js
shaka.media.MediaSourceEngine = class {
constructor(video, textDisplayer, playerInterface, config, lcevcDec) {
// ...
this.mediaSource_ = this.createMediaSource(this.mediaSourceOpen_); // <-- calls
// ...
}
createMediaSource(p) {
// ...
// Store the object URL for releasing it later.
this.url_ = shaka.media.MediaSourceEngine.createObjectURL(mediaSource);
if (this.config_.useSourceElements) {
this.video_.removeAttribute('src');
if (this.source_) {
this.video_.removeChild(this.source_);
}
if (this.secondarySource_) {
this.video_.removeChild(this.secondarySource_);
}
this.source_ = shaka.util.Dom.createSourceElement(this.url_);
this.video_.appendChild(this.source_); // source replace here
if (this.secondarySource_) {
this.video_.appendChild(this.secondarySource_);
}
this.video_.load();
} else {
this.video_.src = this.url_; // or source attribute replaced here
}
}
}
This is therefore not directly the fault of the shaka player, but based on the HTMLMediaElement. This can also be tested with this example:
const video = document.createElement("video");
video.playbackRate = 2;
video.src = "media/video.mp4"; // some valid url
video.playbackRate; // now reset to 1
Alternatively to the second commit, this could be fixed in the shaka player by storing the playback rate before setting a new (or first) source. But the fix would work either way.