|
@@ -734,7 +734,8 @@ body.always-show-volume #volume-slider { display: block; }
|
|
|
transition: opacity 0.3s, transform 0.3s;
|
|
transition: opacity 0.3s, transform 0.3s;
|
|
|
pointer-events: none;
|
|
pointer-events: none;
|
|
|
z-index: 1000;
|
|
z-index: 1000;
|
|
|
- white-space: nowrap;
|
|
|
|
|
|
|
+ max-width: min(90vw, 30em);
|
|
|
|
|
+ text-align: center; line-height: 1.45;
|
|
|
}
|
|
}
|
|
|
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
|
|
|
|
|
@@ -1157,7 +1158,7 @@ body.always-show-volume #volume-slider { display: block; }
|
|
|
.set-seg-btn:hover { color: var(--text); }
|
|
.set-seg-btn:hover { color: var(--text); }
|
|
|
.set-seg-btn.active { background: var(--surface); color: var(--text); }
|
|
.set-seg-btn.active { background: var(--surface); color: var(--text); }
|
|
|
|
|
|
|
|
-.speed-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
|
|
|
|
|
|
|
+.speed-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; }
|
|
|
.speed-btn {
|
|
.speed-btn {
|
|
|
padding: 5px 0; font-size: 12px; font-family: inherit;
|
|
padding: 5px 0; font-size: 12px; font-family: inherit;
|
|
|
border: none; border-radius: 5px; cursor: pointer;
|
|
border: none; border-radius: 5px; cursor: pointer;
|
|
@@ -1761,14 +1762,11 @@ img.ctx-icon { height: 16px; opacity: 0.85; display: block; }
|
|
|
<div class="set-section">
|
|
<div class="set-section">
|
|
|
<div class="set-head"><img src="img/icons/play_white.svg" alt="">Playback speed</div>
|
|
<div class="set-head"><img src="img/icons/play_white.svg" alt="">Playback speed</div>
|
|
|
<div class="speed-grid" id="set-speed-grid">
|
|
<div class="speed-grid" id="set-speed-grid">
|
|
|
- <button class="speed-btn" data-speed="0.25">0.25×</button>
|
|
|
|
|
- <button class="speed-btn" data-speed="0.5">0.5×</button>
|
|
|
|
|
- <button class="speed-btn" data-speed="0.75">0.75×</button>
|
|
|
|
|
- <button class="speed-btn active" data-speed="1">Normal</button>
|
|
|
|
|
- <button class="speed-btn" data-speed="1.25">1.25×</button>
|
|
|
|
|
- <button class="speed-btn" data-speed="1.5">1.5×</button>
|
|
|
|
|
- <button class="speed-btn" data-speed="1.75">1.75×</button>
|
|
|
|
|
- <button class="speed-btn" data-speed="2">2×</button>
|
|
|
|
|
|
|
+ <button class="speed-btn" data-speed="0.5">0.5×</button>
|
|
|
|
|
+ <button class="speed-btn active" data-speed="1">1×</button>
|
|
|
|
|
+ <button class="speed-btn" data-speed="1.2">1.2×</button>
|
|
|
|
|
+ <button class="speed-btn" data-speed="1.5">1.5×</button>
|
|
|
|
|
+ <button class="speed-btn" data-speed="2">2×</button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="set-section">
|
|
<div class="set-section">
|
|
@@ -1778,6 +1776,14 @@ img.ctx-icon { height: 16px; opacity: 0.85; display: block; }
|
|
|
<button class="set-seg-btn" data-volbar="always">Always show</button>
|
|
<button class="set-seg-btn" data-volbar="always">Always show</button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div class="set-section">
|
|
|
|
|
+ <div class="set-head"><img src="img/icons/movie_white.svg" alt="">Streaming mode</div>
|
|
|
|
|
+ <div class="set-seg" id="set-stream-seg">
|
|
|
|
|
+ <button class="set-seg-btn active" data-stream="auto">Auto</button>
|
|
|
|
|
+ <button class="set-seg-btn" data-stream="mp4">MP4</button>
|
|
|
|
|
+ <button class="set-seg-btn" data-stream="hls">HLS</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
<div class="set-section" style="padding-left:0;padding-right:0;padding-bottom:2px;">
|
|
<div class="set-section" style="padding-left:0;padding-right:0;padding-bottom:2px;">
|
|
|
<div class="set-link" id="set-load-subtitle">
|
|
<div class="set-link" id="set-load-subtitle">
|
|
|
<img src="img/icons/movie_white.svg" alt="">Load SRT subtitle…
|
|
<img src="img/icons/movie_white.svg" alt="">Load SRT subtitle…
|
|
@@ -1901,7 +1907,13 @@ var countdownTimer = null;
|
|
|
var repeatSingle = (localStorage.getItem('movie_repeat_one') === '1');
|
|
var repeatSingle = (localStorage.getItem('movie_repeat_one') === '1');
|
|
|
|
|
|
|
|
// Playback speed (persisted, reapplied whenever a new source loads)
|
|
// Playback speed (persisted, reapplied whenever a new source loads)
|
|
|
-var playbackSpeed = parseFloat(localStorage.getItem('movie_playback_speed') || '1') || 1;
|
|
|
|
|
|
|
+// Snapped to the nearest preset on load: an earlier build let this be dragged
|
|
|
|
|
+// to any value, and the popup highlights presets only.
|
|
|
|
|
+// The preset rates offered in the settings popup. Declared here rather than
|
|
|
|
|
+// beside setPlaybackSpeed because the initialiser below runs on load, and a
|
|
|
|
|
+// `var` further down the file would still be undefined at that point.
|
|
|
|
|
+var PLAYBACK_SPEEDS = [0.5, 1, 1.2, 1.5, 2];
|
|
|
|
|
+var playbackSpeed = nearestPlaybackSpeed(localStorage.getItem('movie_playback_speed') || '1');
|
|
|
|
|
|
|
|
// Force the volume slider visible even on portrait phones (see the media query)
|
|
// Force the volume slider visible even on portrait phones (see the media query)
|
|
|
var alwaysShowVolumeBar = (localStorage.getItem('movie_always_volume_bar') === '1');
|
|
var alwaysShowVolumeBar = (localStorage.getItem('movie_always_volume_bar') === '1');
|
|
@@ -1995,6 +2007,10 @@ function connectCast() {
|
|
|
if (playingIndex >= 0 && currentEpisodes[playingIndex]) {
|
|
if (playingIndex >= 0 && currentEpisodes[playingIndex]) {
|
|
|
var ep = currentEpisodes[playingIndex];
|
|
var ep = currentEpisodes[playingIndex];
|
|
|
var ext = ep.ext ? ep.ext.toLowerCase().replace(/^\./, '') : '';
|
|
var ext = ep.ext ? ep.ext.toLowerCase().replace(/^\./, '') : '';
|
|
|
|
|
+ // Always hand the receiver the MP4 stream: it is a separate
|
|
|
|
|
+ // client whose HLS support we cannot probe from here, and
|
|
|
|
|
+ // this browser's streaming-mode preference says nothing
|
|
|
|
|
+ // about it.
|
|
|
var src = isWebPlayable(ext)
|
|
var src = isWebPlayable(ext)
|
|
|
? MEDIA_API + '?file=' + encodeURIComponent(ep.filepath)
|
|
? MEDIA_API + '?file=' + encodeURIComponent(ep.filepath)
|
|
|
: TRANSCODE_API + '?file=' + encodeURIComponent(ep.filepath);
|
|
: TRANSCODE_API + '?file=' + encodeURIComponent(ep.filepath);
|
|
@@ -2064,14 +2080,17 @@ function _castResumeLocally() {
|
|
|
var pos = castCurrentTime;
|
|
var pos = castCurrentTime;
|
|
|
var vid = document.getElementById('main-video');
|
|
var vid = document.getElementById('main-video');
|
|
|
if (isTranscodedVideo && pos > 0) {
|
|
if (isTranscodedVideo && pos > 0) {
|
|
|
- // Seek-by-reload: restart the transcode stream at the cast position
|
|
|
|
|
|
|
+ // Restart the transcode at the cast position, on whichever transport
|
|
|
|
|
+ // this browser is using
|
|
|
transcodeSeekOffset = pos;
|
|
transcodeSeekOffset = pos;
|
|
|
- vid.src = TRANSCODE_API + '?file=' + encodeURIComponent(ep.filepath) + '&start=' + pos.toFixed(3);
|
|
|
|
|
|
|
+ attachTranscodeStream(vid, transcodeStreamURL(ep.filepath, pos));
|
|
|
} else {
|
|
} else {
|
|
|
var ext = ep.ext ? ep.ext.toLowerCase().replace(/^\./, '') : '';
|
|
var ext = ep.ext ? ep.ext.toLowerCase().replace(/^\./, '') : '';
|
|
|
- vid.src = isWebPlayable(ext)
|
|
|
|
|
- ? MEDIA_API + '?file=' + encodeURIComponent(ep.filepath)
|
|
|
|
|
- : TRANSCODE_API + '?file=' + encodeURIComponent(ep.filepath);
|
|
|
|
|
|
|
+ if (isWebPlayable(ext)) {
|
|
|
|
|
+ vid.src = MEDIA_API + '?file=' + encodeURIComponent(ep.filepath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ attachTranscodeStream(vid, transcodeStreamURL(ep.filepath, 0));
|
|
|
|
|
+ }
|
|
|
if (pos > 0) {
|
|
if (pos > 0) {
|
|
|
$(vid).one('loadedmetadata.castresume', function() { vid.currentTime = pos; });
|
|
$(vid).one('loadedmetadata.castresume', function() { vid.currentTime = pos; });
|
|
|
}
|
|
}
|
|
@@ -2520,24 +2539,49 @@ function hideSeekFreeze() {
|
|
|
$('#seek-spinner').removeClass('active');
|
|
$('#seek-spinner').removeClass('active');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// Restart the transcode stream at `seconds`, holding the last frame until the
|
|
|
|
|
-// new segment plays. Clamped to the known duration.
|
|
|
|
|
|
|
+// Seek to `seconds` (absolute, whole-file time), clamped to the known duration.
|
|
|
|
|
+//
|
|
|
|
|
+// An HLS stream keeps every segment produced so far in its playlist, so a seek
|
|
|
|
|
+// landing inside the transcoded window is an ordinary <video> seek — no reload,
|
|
|
|
|
+// no re-transcode, no black frame. Only a seek past that window (or an MP4
|
|
|
|
|
+// stream, which has no seekable history at all) has to restart the transcode at
|
|
|
|
|
+// the new offset.
|
|
|
function transcodeSeekTo(seconds) {
|
|
function transcodeSeekTo(seconds) {
|
|
|
if (playingIndex < 0 || !currentEpisodes[playingIndex]) { return; }
|
|
if (playingIndex < 0 || !currentEpisodes[playingIndex]) { return; }
|
|
|
var vid = document.getElementById('main-video');
|
|
var vid = document.getElementById('main-video');
|
|
|
var pos = Math.max(0, seconds);
|
|
var pos = Math.max(0, seconds);
|
|
|
if (transcodeDuration > 0) { pos = Math.min(transcodeDuration, pos); }
|
|
if (transcodeDuration > 0) { pos = Math.min(transcodeDuration, pos); }
|
|
|
|
|
|
|
|
|
|
+ if (usingHLS() && seekWithinTranscodedWindow(vid, pos)) {
|
|
|
|
|
+ setSubtitleSeekFloor(pos);
|
|
|
|
|
+ vid.currentTime = pos - transcodeSeekOffset;
|
|
|
|
|
+ playVideo(vid);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
showSeekFreeze();
|
|
showSeekFreeze();
|
|
|
setSubtitleSeekFloor(pos);
|
|
setSubtitleSeekFloor(pos);
|
|
|
transcodeSeekOffset = pos;
|
|
transcodeSeekOffset = pos;
|
|
|
- vid.src = TRANSCODE_API + '?file='
|
|
|
|
|
- + encodeURIComponent(currentEpisodes[playingIndex].filepath)
|
|
|
|
|
- + '&start=' + pos.toFixed(3);
|
|
|
|
|
- vid.load();
|
|
|
|
|
|
|
+ if (!attachTranscodeStream(vid, transcodeStreamURL(currentEpisodes[playingIndex].filepath, pos))) {
|
|
|
|
|
+ hideSeekFreeze();
|
|
|
|
|
+ showToast('HLS playback needs hls.js, which is not installed. Switch streaming mode to MP4 in settings.');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
playVideo(vid);
|
|
playVideo(vid);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Whether `pos` (absolute time) falls inside what the current stream has
|
|
|
|
|
+// already produced and can therefore be reached without reloading.
|
|
|
|
|
+function seekWithinTranscodedWindow(vid, pos) {
|
|
|
|
|
+ var relative = pos - transcodeSeekOffset;
|
|
|
|
|
+ if (relative < 0) { return false; }
|
|
|
|
|
+ if (!vid.seekable || vid.seekable.length === 0) { return false; }
|
|
|
|
|
+ var available = vid.seekable.end(vid.seekable.length - 1);
|
|
|
|
|
+ // Stay a segment clear of the live edge; seeking right onto it stalls until
|
|
|
|
|
+ // the next segment lands.
|
|
|
|
|
+ return relative <= available - 4;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// Current playback position in whole-file terms (transcode streams restart at 0)
|
|
// Current playback position in whole-file terms (transcode streams restart at 0)
|
|
|
function effectivePlaybackTime() {
|
|
function effectivePlaybackTime() {
|
|
|
var vid = document.getElementById('main-video');
|
|
var vid = document.getElementById('main-video');
|
|
@@ -3364,30 +3408,36 @@ function startPlayback(index) {
|
|
|
var ep = currentEpisodes[index];
|
|
var ep = currentEpisodes[index];
|
|
|
|
|
|
|
|
var ext = ep.ext ? ep.ext.toLowerCase().replace(/^\./, '') : '';
|
|
var ext = ep.ext ? ep.ext.toLowerCase().replace(/^\./, '') : '';
|
|
|
- var src = isWebPlayable(ext)
|
|
|
|
|
- ? MEDIA_API + '?file=' + encodeURIComponent(ep.filepath)
|
|
|
|
|
- : TRANSCODE_API + '?file=' + encodeURIComponent(ep.filepath);
|
|
|
|
|
|
|
+ var webPlayable = isWebPlayable(ext);
|
|
|
|
|
+ var directSrc = MEDIA_API + '?file=' + encodeURIComponent(ep.filepath);
|
|
|
|
|
|
|
|
// Reset transcode seek state for the new episode
|
|
// Reset transcode seek state for the new episode
|
|
|
transcodeSeekOffset = 0;
|
|
transcodeSeekOffset = 0;
|
|
|
- isTranscodedVideo = !isWebPlayable(ext);
|
|
|
|
|
|
|
+ isTranscodedVideo = !webPlayable;
|
|
|
transcodeDuration = 0;
|
|
transcodeDuration = 0;
|
|
|
|
|
|
|
|
var vid = document.getElementById('main-video');
|
|
var vid = document.getElementById('main-video');
|
|
|
|
|
|
|
|
if (castMode && _castConnected()) {
|
|
if (castMode && _castConnected()) {
|
|
|
- // Don't load locally — avoids transcoding the same file twice
|
|
|
|
|
|
|
+ // Don't load locally — avoids transcoding the same file twice.
|
|
|
|
|
+ // The receiver is a separate client, so it always gets the MP4 stream
|
|
|
|
|
+ // rather than this browser's streaming-mode preference.
|
|
|
vid.pause();
|
|
vid.pause();
|
|
|
_castSend('media.load', {
|
|
_castSend('media.load', {
|
|
|
filepath: ep.filepath, name: ep.name, type: 'video',
|
|
filepath: ep.filepath, name: ep.name, type: 'video',
|
|
|
- src: src, startTime: 0
|
|
|
|
|
|
|
+ src: webPlayable ? directSrc : TRANSCODE_API + '?file=' + encodeURIComponent(ep.filepath),
|
|
|
|
|
+ startTime: 0
|
|
|
});
|
|
});
|
|
|
// Volume after load, before play — guarantees device volume overrides
|
|
// Volume after load, before play — guarantees device volume overrides
|
|
|
// whatever Arozcast was previously playing at.
|
|
// whatever Arozcast was previously playing at.
|
|
|
_castSend('media.volume', { volume: vid.volume * 100, muted: vid.muted });
|
|
_castSend('media.volume', { volume: vid.volume * 100, muted: vid.muted });
|
|
|
_castSend('media.play', {});
|
|
_castSend('media.play', {});
|
|
|
|
|
+ } else if (webPlayable) {
|
|
|
|
|
+ vid.src = directSrc;
|
|
|
|
|
+ playVideo(vid);
|
|
|
|
|
+ } else if (!attachTranscodeStream(vid, transcodeStreamURL(ep.filepath, 0))) {
|
|
|
|
|
+ showToast('HLS playback needs hls.js, which is not installed. Switch streaming mode to MP4 in settings.');
|
|
|
} else {
|
|
} else {
|
|
|
- vid.src = src;
|
|
|
|
|
playVideo(vid);
|
|
playVideo(vid);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -3883,6 +3933,44 @@ function setRepeatSingle(on) {
|
|
|
.filter('[data-repeat="' + (repeatSingle ? 'one' : 'off') + '"]').addClass('active');
|
|
.filter('[data-repeat="' + (repeatSingle ? 'one' : 'off') + '"]').addClass('active');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Apply a streaming mode. Changing it mid-playback re-opens the current file on
|
|
|
|
|
+// the other transport, resuming where the viewer was.
|
|
|
|
|
+function applyStreamMode(mode, reloadCurrent) {
|
|
|
|
|
+ var previous = getStreamMode();
|
|
|
|
|
+ setStreamMode(mode);
|
|
|
|
|
+ $('#set-stream-seg .set-seg-btn').removeClass('active')
|
|
|
|
|
+ .filter('[data-stream="' + getStreamMode() + '"]').addClass('active');
|
|
|
|
|
+
|
|
|
|
|
+ if (!reloadCurrent || previous === getStreamMode()) { return; }
|
|
|
|
|
+ // Warn only when the viewer actively picks a mode this browser cannot play
|
|
|
|
|
+ warnIfStreamModeUnplayable();
|
|
|
|
|
+ if (!isTranscodedVideo || playingIndex < 0 || !currentEpisodes[playingIndex]) { return; }
|
|
|
|
|
+
|
|
|
|
|
+ // Restart the current file on the newly chosen transport, at the same spot
|
|
|
|
|
+ var resumeAt = effectivePlaybackTime();
|
|
|
|
|
+ transcodeSeekOffset = 0;
|
|
|
|
|
+ var vid = document.getElementById('main-video');
|
|
|
|
|
+ if (!attachTranscodeStream(vid, transcodeStreamURL(currentEpisodes[playingIndex].filepath, 0))) {
|
|
|
|
|
+ showToast('HLS playback needs hls.js, which is not installed.');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ playVideo(vid);
|
|
|
|
|
+ if (resumeAt > 1) { transcodeSeekTo(resumeAt); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function playerVideoElement() { return document.getElementById('main-video'); }
|
|
|
|
|
+
|
|
|
|
|
+// Surface the two streaming modes that cannot work on a given browser. Anything
|
|
|
|
|
+// that is simply working says nothing at all.
|
|
|
|
|
+function warnIfStreamModeUnplayable() {
|
|
|
|
|
+ var mode = getStreamMode();
|
|
|
|
|
+ if (mode === 'mp4' && isWebKitClient()) {
|
|
|
|
|
+ showToast('This browser cannot play the MP4 stream. Use Auto or HLS.');
|
|
|
|
|
+ } else if (mode === 'hls' && !hlsPlaybackSupported(playerVideoElement())) {
|
|
|
|
|
+ showToast('HLS needs hls.js, which is not installed on this server.');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function setPlaybackSpeed(rate) {
|
|
function setPlaybackSpeed(rate) {
|
|
|
playbackSpeed = parseFloat(rate) || 1;
|
|
playbackSpeed = parseFloat(rate) || 1;
|
|
|
document.getElementById('main-video').playbackRate = playbackSpeed;
|
|
document.getElementById('main-video').playbackRate = playbackSpeed;
|
|
@@ -3891,10 +3979,22 @@ function setPlaybackSpeed(rate) {
|
|
|
.filter('[data-speed="' + playbackSpeed + '"]').addClass('active');
|
|
.filter('[data-speed="' + playbackSpeed + '"]').addClass('active');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Snap an arbitrary stored rate onto the nearest preset, so the popup always
|
|
|
|
|
+// has exactly one button highlighted. Needed because an earlier build persisted
|
|
|
|
|
+// free-form rates from a drag slider.
|
|
|
|
|
+function nearestPlaybackSpeed(rate) {
|
|
|
|
|
+ var value = parseFloat(rate);
|
|
|
|
|
+ if (!value || value <= 0) { return 1; }
|
|
|
|
|
+ return PLAYBACK_SPEEDS.reduce(function (best, preset) {
|
|
|
|
|
+ return Math.abs(preset - value) < Math.abs(best - value) ? preset : best;
|
|
|
|
|
+ }, PLAYBACK_SPEEDS[0]);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function openSettingsPopup() {
|
|
function openSettingsPopup() {
|
|
|
setRepeatSingle(repeatSingle);
|
|
setRepeatSingle(repeatSingle);
|
|
|
setPlaybackSpeed(playbackSpeed);
|
|
setPlaybackSpeed(playbackSpeed);
|
|
|
setAlwaysShowVolumeBar(alwaysShowVolumeBar);
|
|
setAlwaysShowVolumeBar(alwaysShowVolumeBar);
|
|
|
|
|
+ applyStreamMode(getStreamMode(), false);
|
|
|
$('#player-ctx').hide();
|
|
$('#player-ctx').hide();
|
|
|
$('#settings-popup').addClass('active');
|
|
$('#settings-popup').addClass('active');
|
|
|
showControls();
|
|
showControls();
|
|
@@ -3930,6 +4030,10 @@ function initSettingsPopup() {
|
|
|
setAlwaysShowVolumeBar($(this).data('volbar') === 'always');
|
|
setAlwaysShowVolumeBar($(this).data('volbar') === 'always');
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ $('#set-stream-seg').on('click', '.set-seg-btn', function () {
|
|
|
|
|
+ applyStreamMode(String($(this).data('stream')), true);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
$('#set-load-subtitle').on('click', function () { closeSettingsPopup(); pickSubtitleFile(); });
|
|
$('#set-load-subtitle').on('click', function () { closeSettingsPopup(); pickSubtitleFile(); });
|
|
|
$('#set-subtitle-settings').on('click', function () { closeSettingsPopup(); openSubtitleSettings(); });
|
|
$('#set-subtitle-settings').on('click', function () { closeSettingsPopup(); openSubtitleSettings(); });
|
|
|
$('#set-video-props').on('click', function () { closeSettingsPopup(); openVideoInfo('props'); });
|
|
$('#set-video-props').on('click', function () { closeSettingsPopup(); openVideoInfo('props'); });
|
|
@@ -3938,6 +4042,7 @@ function initSettingsPopup() {
|
|
|
setRepeatSingle(repeatSingle);
|
|
setRepeatSingle(repeatSingle);
|
|
|
setPlaybackSpeed(playbackSpeed);
|
|
setPlaybackSpeed(playbackSpeed);
|
|
|
setAlwaysShowVolumeBar(alwaysShowVolumeBar);
|
|
setAlwaysShowVolumeBar(alwaysShowVolumeBar);
|
|
|
|
|
+ applyStreamMode(getStreamMode(), false);
|
|
|
|
|
|
|
|
// Changing episode — or a transcode seek — replaces the media source, which
|
|
// Changing episode — or a transcode seek — replaces the media source, which
|
|
|
// resets playbackRate. Reapply the chosen speed every time media loads.
|
|
// resets playbackRate. Reapply the chosen speed every time media loads.
|