1
0
mirror of https://github.com/webrecorder/pywb.git synced 2025-03-15 00:03:28 +01:00

WIP: Add client-side replay option

This commit is contained in:
Tessa Walsh 2025-03-06 15:23:08 -05:00
parent db56fb2df0
commit 8707918269
6 changed files with 117 additions and 0 deletions

View File

@ -29,6 +29,10 @@ framed_replay: true
redirect_to_exact: true redirect_to_exact: true
# Use wabac.js-style client-side replay system
client_side_replay: false
# Uncomment and change to set default locale # Uncomment and change to set default locale
# default_locale: en # default_locale: en

View File

@ -81,6 +81,8 @@ class FrontEndApp(object):
self.debug = config.get('debug', False) self.debug = config.get('debug', False)
self.client_side_replay = config.get('client_side_replay', False)
self.warcserver_server = GeventServer(self.warcserver, port=0) self.warcserver_server = GeventServer(self.warcserver, port=0)
self.proxy_prefix = None # the URL prefix to be used for the collection with proxy mode (e.g. /coll/id_/) self.proxy_prefix = None # the URL prefix to be used for the collection with proxy mode (e.g. /coll/id_/)
@ -130,6 +132,9 @@ class FrontEndApp(object):
coll_prefix = '/<coll>' coll_prefix = '/<coll>'
self.url_map.add(Rule('/', endpoint=self.serve_home)) self.url_map.add(Rule('/', endpoint=self.serve_home))
if self.client_side_replay:
self.url_map.add(Rule('/static/sw.js', endpoint=self.serve_wabac_service_worker))
self._init_coll_routes(coll_prefix) self._init_coll_routes(coll_prefix)
if self.proxy_prefix is not None: if self.proxy_prefix is not None:
@ -818,6 +823,17 @@ class FrontEndApp(object):
response.add_access_control_headers(env=env) response.add_access_control_headers(env=env)
return response return response
def serve_wabac_service_worker(self, env):
"""Serve wabac.js service worker.
:param dict env: The WSGI environment dictionary
:return: WbResponse with service worker
:rtype: WbResponse
"""
response = self.serve_static(env, coll='', filepath='wabacWorker.js')
response.status_headers['Service-Worker-Allowed'] = '/'
return response
# ============================================================================ # ============================================================================
class MetadataCache(object): class MetadataCache(object):

View File

@ -84,6 +84,8 @@ class RewriterApp(object):
self._html_templ('head_insert_html'), self._html_templ('head_insert_html'),
self.custom_banner_view) self.custom_banner_view)
self.client_side_replay = self.config.get('client_side_replay', False)
self.frame_insert_view = TopFrameView(self.jinja_env, self.frame_insert_view = TopFrameView(self.jinja_env,
self._html_templ('frame_insert_html'), self._html_templ('frame_insert_html'),
self.banner_view) self.banner_view)
@ -933,6 +935,7 @@ class RewriterApp(object):
environ, environ,
self.frame_mod, self.frame_mod,
self.replay_mod, self.replay_mod,
self.client_side_replay,
coll='', coll='',
extra_params=extra_params) extra_params=extra_params)

View File

@ -388,6 +388,7 @@ class TopFrameView(BaseInsertView):
env, env,
frame_mod, frame_mod,
replay_mod, replay_mod,
client_side_replay,
coll='', coll='',
extra_params=None): extra_params=None):
""" """
@ -397,6 +398,7 @@ class TopFrameView(BaseInsertView):
:param dict env: The WSGI environment dictionary for the request this template is being rendered for :param dict env: The WSGI environment dictionary for the request this template is being rendered for
:param str frame_mod: The modifier to be used for framing (e.g. if_) :param str frame_mod: The modifier to be used for framing (e.g. if_)
:param str replay_mod: The modifier to be used in the URL of the page being replayed (e.g. mp_) :param str replay_mod: The modifier to be used in the URL of the page being replayed (e.g. mp_)
:param bool client_side_replay: Boolean indicating whether to use wabac.js-based client side replay
:param str coll: The name of the collection this template is being rendered for :param str coll: The name of the collection this template is being rendered for
:param dict extra_params: Additional parameters to be supplied to the Jninja template render method :param dict extra_params: Additional parameters to be supplied to the Jninja template render method
:return: The frame insert string :return: The frame insert string
@ -423,6 +425,7 @@ class TopFrameView(BaseInsertView):
'embed_url': embed_url, 'embed_url': embed_url,
'is_proxy': is_proxy, 'is_proxy': is_proxy,
'client_side_replay': client_side_replay,
'timestamp': timestamp, 'timestamp': timestamp,
'url': wb_url.get_url() 'url': wb_url.get_url()
} }

80
pywb/static/loadWabac.js Normal file
View File

@ -0,0 +1,80 @@
class WabacReplay
{
constructor(prefix, url, ts) {
this.prefix = prefix;
this.url = url;
this.ts = ts;
this.collName = new URL(prefix, "http://dummy").pathname.split('/')[1];
this.adblockUrl = undefined;
this.queryParams = {};
}
async init() {
const scope = '/';
await navigator.serviceWorker.register("/static/sw.js?" + new URLSearchParams(this.queryParams).toString(), {scope});
let initedResolve = null;
const inited = new Promise((resolve) => initedResolve = resolve);
navigator.serviceWorker.addEventListener("message", (event) => {
if (event.data.msg_type === "collAdded") {
// the replay is ready to be loaded when this message is received
initedResolve();
}
});
const baseUrl = new URL(window.location);
baseUrl.hash = "";
const proxyPrefix = "";
const msg = {
msg_type: "addColl",
name: this.collName,
type: "live",
file: {"sourceUrl": `proxy:${proxyPrefix}`},
skipExisting: false,
extraConfig: {
prefix: proxyPrefix,
isLive: false,
baseUrl: baseUrl.href,
baseUrlHashReplay: true,
noPostToGet: false,
archivePrefix: `/${this.collName}/`,
adblockUrl: this.adblockUrl
},
};
if (!navigator.serviceWorker.controller) {
navigator.serviceWorker.addEventListener("controllerchange", () => {
navigator.serviceWorker.controller.postMessage(msg);
});
} else {
navigator.serviceWorker.controller.postMessage(msg);
}
window.addEventListener('message', event => {
let data = event.data;
if (data.wb_type !== 'load') return;
history.replaceState({}, data.title, this.prefix + data.ts + '/' + data.url);
window.WBBanner.onMessage(event);
});
window.cframe = this;
if (inited) {
await inited;
}
this.load_url(this.url, this.ts);
}
// called by the Vue banner when the timeline is clicked
load_url(url, ts) {
const iframe = document.querySelector('#replay_iframe');
iframe.src = `/w/${this.collName}/${ts}mp_/${url}`;
}
}

View File

@ -12,7 +12,15 @@ html, body
} }
</style> </style>
{% if client_side_replay %}
<script src='{{ static_prefix }}/loadWabac.js'></script>
<script>
new WabacReplay("{{ wb_prefix }}", "{{ url }}", "{{ timestamp }}").init();
</script>
{% else %}
<script src='{{ static_prefix }}/wb_frame.js'> </script> <script src='{{ static_prefix }}/wb_frame.js'> </script>
{% endif %}
{% autoescape false %} {% autoescape false %}
@ -45,6 +53,8 @@ html, body
<div id="wb_iframe_div"> <div id="wb_iframe_div">
<iframe id="replay_iframe" frameborder="0" seamless="seamless" scrolling="yes" class="wb_iframe" allow="autoplay; fullscreen"></iframe> <iframe id="replay_iframe" frameborder="0" seamless="seamless" scrolling="yes" class="wb_iframe" allow="autoplay; fullscreen"></iframe>
</div> </div>
{% if not client_side_replay %}
<script> <script>
var cframe = new ContentFrame({"url": "{{ url }}" + window.location.hash, var cframe = new ContentFrame({"url": "{{ url }}" + window.location.hash,
"prefix": "{{ wb_prefix }}", "prefix": "{{ wb_prefix }}",
@ -52,6 +62,7 @@ html, body
"iframe": "#replay_iframe"}); "iframe": "#replay_iframe"});
</script> </script>
{% endif %}
</body> </body>
</html> </html>
{% endautoescape %} {% endautoescape %}