fix epub reader playback and trial navigation
This commit is contained in:
@@ -25,10 +25,22 @@ extension RDEPUBWebView {
|
||||
forMainFrameOnly: true
|
||||
)
|
||||
)
|
||||
userContentController.addUserScript(
|
||||
WKUserScript(
|
||||
source: Self.readerPageMediaLifecycleUserScript,
|
||||
injectionTime: .atDocumentStart,
|
||||
forMainFrameOnly: false
|
||||
)
|
||||
)
|
||||
|
||||
let configuration = WKWebViewConfiguration()
|
||||
configuration.websiteDataStore = .nonPersistent()
|
||||
configuration.userContentController = userContentController
|
||||
// Interactive fixed-layout EPUBs commonly start narration from their
|
||||
// document-load timeline. Allow those page-owned audio elements to play
|
||||
// without requiring an extra tap, and keep playback inside the reader.
|
||||
configuration.allowsInlineMediaPlayback = true
|
||||
configuration.mediaTypesRequiringUserActionForPlayback = []
|
||||
let schemeHandler = RDEPUBResourceURLSchemeHandler(parser: parser)
|
||||
configuration.setURLSchemeHandler(schemeHandler, forURLScheme: RDEPUBResourceURLSchemeHandler.scheme)
|
||||
|
||||
@@ -130,3 +142,110 @@ private final class RDEPUBWeakScriptMessageHandler: NSObject, WKScriptMessageHan
|
||||
target?.userContentController(userContentController, didReceive: message)
|
||||
}
|
||||
}
|
||||
|
||||
extension RDEPUBWebView {
|
||||
|
||||
/// Installed in every frame before EPUB scripts run. Hidden pages start in
|
||||
/// a suspended state, including audio created with `new Audio()` that is
|
||||
/// not attached to the DOM (as used by Tumult Hype publications).
|
||||
static let readerPageMediaLifecycleUserScript = #"""
|
||||
(() => {
|
||||
if (window.__rdReaderMediaLifecycleInstalled) return;
|
||||
window.__rdReaderMediaLifecycleInstalled = true;
|
||||
|
||||
let isActive = false;
|
||||
const trackedMedia = new Set();
|
||||
const command = "rd-reader-page-visibility";
|
||||
const mediaPrototype = window.HTMLMediaElement && window.HTMLMediaElement.prototype;
|
||||
const nativePlay = mediaPrototype && mediaPrototype.play;
|
||||
|
||||
const register = media => {
|
||||
if (media) trackedMedia.add(media);
|
||||
return media;
|
||||
};
|
||||
|
||||
const collectDocumentMedia = () => {
|
||||
if (!document.querySelectorAll) return;
|
||||
document.querySelectorAll("audio,video").forEach(register);
|
||||
};
|
||||
|
||||
const setActive = active => {
|
||||
isActive = !!active;
|
||||
collectDocumentMedia();
|
||||
trackedMedia.forEach(media => {
|
||||
if (isActive) {
|
||||
if (media.__rdReaderReplayWhenVisible) {
|
||||
media.__rdReaderReplayWhenVisible = false;
|
||||
try {
|
||||
media.currentTime = 0;
|
||||
const result = nativePlay && nativePlay.call(media);
|
||||
if (result && result.catch) result.catch(() => {});
|
||||
} catch (_) {}
|
||||
}
|
||||
} else {
|
||||
if (media.__rdReaderWasPlayed || media.autoplay || (!media.paused && !media.ended)) {
|
||||
media.__rdReaderReplayWhenVisible = true;
|
||||
}
|
||||
if (!media.paused) {
|
||||
try { media.pause(); } catch (_) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (mediaPrototype && nativePlay) {
|
||||
mediaPrototype.play = function() {
|
||||
register(this);
|
||||
this.__rdReaderWasPlayed = true;
|
||||
if (!isActive) {
|
||||
this.__rdReaderReplayWhenVisible = true;
|
||||
try { this.pause(); } catch (_) {}
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.__rdReaderReplayWhenVisible = false;
|
||||
return nativePlay.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
const NativeAudio = window.Audio;
|
||||
if (typeof NativeAudio === "function") {
|
||||
const ReaderAudio = function(source) {
|
||||
const media = arguments.length > 0 ? new NativeAudio(source) : new NativeAudio();
|
||||
return register(media);
|
||||
};
|
||||
ReaderAudio.prototype = NativeAudio.prototype;
|
||||
try { Object.setPrototypeOf(ReaderAudio, NativeAudio); } catch (_) {}
|
||||
window.Audio = ReaderAudio;
|
||||
}
|
||||
|
||||
window.addEventListener("message", event => {
|
||||
const message = event.data;
|
||||
if (!message || message.type !== command) return;
|
||||
setActive(message.active);
|
||||
for (let index = 0; index < window.frames.length; index += 1) {
|
||||
try { window.frames[index].postMessage(message, "*"); } catch (_) {}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => setActive(isActive));
|
||||
if (window.MutationObserver) {
|
||||
new MutationObserver(() => setActive(isActive)).observe(document, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
})();
|
||||
"""#
|
||||
|
||||
func setReaderPageVisible(_ isVisible: Bool) {
|
||||
isReaderPageVisible = isVisible
|
||||
applyReaderPageMediaVisibility()
|
||||
}
|
||||
|
||||
func applyReaderPageMediaVisibility() {
|
||||
guard let webView else { return }
|
||||
let active = isReaderPageVisible ? "true" : "false"
|
||||
let script = "window.postMessage({type: 'rd-reader-page-visibility', active: \(active)}, '*');"
|
||||
webView.evaluateJavaScript(script, completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user