How to cache .mp4 files in Safari with workbox-webpack-plugin? - safari

I'm having exactly the same issue reported at https://github.com/GoogleChrome/workbox/issues/1663 which describes an issue that occurs exclusively in Safari where mp4 videos are not rendered after being cached by the service worker.
I'm using workbox-webpack-plugin, so the instructions provided in the comment https://github.com/GoogleChrome/workbox/issues/1663#issuecomment-448755945 will not work in my case. I'm not being able to require workbox-range-requests plugin in my webpack config file and pass it to the runtime caching options because I believe this package is intended for browser usage only. My workbox config is precaching .mp4 assets and uses a network first strategy for runtime caching.
How can I setup workbox-range-requests with workbox-webpack-plugin?
EDIT: Following Jeff's answer below, I've adjusted my webpack config to the following:
new WorkboxPlugin.InjectManifest({
swSrc: serviceWorkerSrcPath,
swDest: serviceWorkerBuildPath,
importsDirectory: 'sw',
})
The build produces the following service worker:
importScripts("/_build/sw/precache-manifest.8a0be820b796b153c97ba206d9753bdb.js", "https://storage.googleapis.com/workbox-cdn/releases/3.6.2/workbox-sw.js");
workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
workbox.routing.registerRoute(
/.*\.mp4/,
new workbox.strategies.CacheFirst({
cacheName: 'videos',
plugins: [
new workbox.cacheableResponse.Plugin({ statuses: [200] }),
new workbox.rangeRequests.Plugin(),
],
}),
);
If forgot to mention previously, but I've also added crossOrigin="anonymous" attribute to the video elements.
EDIT:
Repro that demonstrates it does not work as expected on Safari: https://github.com/acostalima/workbox-range-requests-mp4-demo

There's specific guidance for this use case in the "Serve cached audio and video" recipe in the Workbox documentation.
You can continue using the workbox-webpack-plugin, but I'd suggest using it in InjectManifest mode, which will give you control over the top-level service worker file. That will in turn make it possible to follow the recipe.
This documentation has guidance on configuring workbox-webpack-plugin in InjectManifest mode.

I had the same issue with Safari and managed to resolve it by removing my video from the precahe list self.__precacheManifest and instead by adding it in the service worker's install handler:
self.addEventListener('install', (event) => {
const urls = [/* videoUrl */];
const cacheName = 'videos';
event.waitUntil(caches.open(cacheName).then((cache) => cache.addAll(urls)));
});
Looking at the logs, it seemed that otherwise only the precache was used to respond to the request for the video resource and not the router.

Although the docs say that adding mp4s to the precache cache and then configuring the range plugin to handle precache mp4s is supposed to work, in practice, it wasn't. Removing mp4s from the precache and configuring your own video cache with the range plugin did the trick for me. Don't forget to add the crossorigin="anonymous" tag to your videos!
Here's how I did it (webpack 5, workbox 6):
// src/service-worker.js
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import { cacheNames } from 'workbox-core';
import { precacheAndRoute } from 'workbox-precaching';
import { RangeRequestsPlugin } from 'workbox-range-requests';
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
const allEntries = self.__WB_MANIFEST; // Injected by WorkboxWebpackPlugin at compile time
const videoEntries = allEntries.filter((entry) => entry.url.endsWith('.mp4'));
const restEntries = allEntries.filter((entry) => !entry.url.endsWith('.mp4'));
precacheAndRoute(restEntries);
const videoCacheName = `${cacheNames.prefix}-videos-${cacheNames.suffix}`;
self.addEventListener('install', (event) => {
const allVideosAddedToCache = caches.open(videoCacheName).then((videoCache) => {
const videoUrls = videoEntries.map((entry) => entry.url);
return videoCache.addAll(videoUrls);
});
event.waitUntil(allVideosAddedToCache);
});
registerRoute(
(route) => route.url.pathname.endsWith('.mp4'),
new CacheFirst({
cacheName: videoCacheName,
plugins: [new CacheableResponsePlugin({ statuses: [200] }), new RangeRequestsPlugin()],
})
);
// webpack.config.js
plugins: [
new WorkboxWebpackPlugin.InjectManifest({
swSrc: 'src/service-worker.js',
}),
]
// index.tsx
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js');
});
}

Related

Why Vercel Web vitals is not loading for my Astro Project?

I created a landing page using Astro with Tailwind CSS. And it is currently hosted on Vercel. I wanted to try out the analytics service provided by Vercel. I have been able to avail the Audience analytics service provided by Vercel. However, I cannot avail the web vitals services. After enabling the service and redeploying my project, I am stuck in this screen (screen shot provided).
Please note that I did turn off the ad blocker but that did not resolve the issue.I also added the following meta tag to resolve any CSP issue
<meta http-equiv="Content-Security-Policy"
content="default-src 'self' vitals.vercel-insights.com"/>
But that has not solved the problem.That is why I want to know does Vercel support analytics for Astro projects and if they do, then what am I doing wrong? Thank you.
Vercel’s Web Vitals analytics currently only has out-of-the-box support for Next, Nuxt, and Gatsby.
To track Web Vitals with a different framework like Astro, you need a bit of manual set up work as documented in Vercel’s Web Vitals API docs.
For example in your base Astro layout you could include a script tag that will import their example code and run it:
---
// src/layouts/BaseLayout.astro
---
<script>
import { webVitals } from '../scripts/vitals';
const analyticsId = import.meta.env.PUBLIC_VERCEL_ANALYTICS_ID;
webVitals({
path: window.location.pathname,
analyticsId,
});
</script>
Here’s Vercel’s example vitals.js snippet:
// src/scripts/vitals.js
import { getCLS, getFCP, getFID, getLCP, getTTFB } from 'web-vitals';
const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals';
function getConnectionSpeed() {
return 'connection' in navigator &&
navigator['connection'] &&
'effectiveType' in navigator['connection']
? navigator['connection']['effectiveType']
: '';
}
function sendToAnalytics(metric, options) {
const body = {
dsn: options.analyticsId, // qPgJqYH9LQX5o31Ormk8iWhCxZO
id: metric.id, // v2-1653884975443-1839479248192
page: options.path, // /blog/my-test
href: location.href, // https://my-app.vercel.app/blog/my-test
event_name: metric.name, // TTFB
value: metric.value.toString(), // 60.20000000298023
speed: getConnectionSpeed(), // 4g
};
if (options.debug) {
console.log('[Analytics]', metric.name, JSON.stringify(body, null, 2));
}
const blob = new Blob([new URLSearchParams(body).toString()], {
// This content type is necessary for `sendBeacon`
type: 'application/x-www-form-urlencoded',
});
if (navigator.sendBeacon) {
navigator.sendBeacon(vitalsUrl, blob);
} else
fetch(vitalsUrl, {
body: blob,
method: 'POST',
credentials: 'omit',
keepalive: true,
});
}
export function webVitals(options) {
try {
getFID((metric) => sendToAnalytics(metric, options));
getTTFB((metric) => sendToAnalytics(metric, options));
getLCP((metric) => sendToAnalytics(metric, options));
getCLS((metric) => sendToAnalytics(metric, options));
getFCP((metric) => sendToAnalytics(metric, options));
} catch (err) {
console.error('[Analytics]', err);
}
}
For a slightly more real-world implementation you, check out the <TrackVitals> Astro component in the astro-badge repo.
Vercel analytics has support for frameworks other than Next, Nuxt Gatsby etc. The way to achieve it in Astro (1.6, 2.0 etc.) is to install the #vercel/analytics package and inject a simple <script> tag that imports it and calls its exported function inject():
<script>
import { inject } from '#vercel/analytics'
// #ts-ignore: process.env.NODE_ENV is required by #vercel/analytics internally
// so that it can determine the correct path for importing the analytics script
globalThis.process = { env: { NODE_ENV: import.meta.env.MODE } }
inject()
</script>
You can inject this code in your <head> section in any .astro template file.
Unfortunately, the package is expecting a non-ESM runtime environment and is internally conditionally checking for process.env.NODE_ENV to determine which script to load (local-relative path to JS or from a remote host, fully qualified domain name). This is the reason, the MODE needs to be exposed as process.env.NODE_ENV. I tried to achieve this via Vite using define, but Astro seems to check for process somewhere else internally and fails.

How to turn off console.info messages from Nuxt server?

I am running tests and receive unnecessary console.info texts in terminal, I would like to get rid of:
console.info
Download the Vue Devtools extension for a better development experience:
https://github.com/vuejs/vue-devtools
at node_modules/vue/dist/vue.common.dev.js:9051:47
console.info
You are running Vue in development mode.
Make sure to turn on production mode when deploying for production.
See more tips at https://vuejs.org/guide/deployment.html
at node_modules/vue/dist/vue.common.dev.js:9060:45
const { Nuxt } = require('nuxt')
const nuxtConfig = require('../../../../nuxt.config.js')
let nuxt = null
beforeAll(async () => {
nuxt = new Nuxt({
...nuxtConfig,
buildDir: constants.buildDir
})
await nuxt.server.listen(constants.port, 'localhost')
}, 300000)
I've tried to put vue.config silent property in various places in code above, but also into nuxt.config.js, but I got no luck doing so. I've tried this snippet: https://nuxtjs.org/docs/2.x/configuration-glossary/configuration-vue-config/
export default {
vue: {
config: {
productionTip: true,
devtools: false
}
}
}
How to turn off console.info messages?
You could right click on the message in your devtools console and go with Hide messages from vue.runtime.esm.js. It will hide it from your console thanks to a filter. Do not solves the real problem, but a nice and quick fix.
Pretty much as here: https://superuser.com/a/995289/850722

PDFTron full api is not getting loaded in WebViewer

I am trying to get the full api of PDFTron working from the WEbViewer. I followed the steps in the link below.
https://www.pdftron.com/documentation/web/guides/full-api/setup/
But I am getting the error given below in console while loading the webviewer.
Uncaught (in promise) Error: Full version of PDFNetJS has not been loaded. Please pass the "fullAPI: true" option in your WebViewer constructor to use the PDFNet APIs.
at Object.get (CoreControls.js:1694)
at z.docViewer.on ((index):43)
at CoreControls.js:398
at Array.forEach (<anonymous>)
at z.O (CoreControls.js:398)
at CoreControls.js:213
This is my code.
WebViewer({
path: 'WebViewer-6.0.2/lib', // path to the PDFTron 'lib' folder on your server
type: 'html5',
initialDoc: 'forms/local.pdf', // You can also use documents on your server
fullAPI: true,
}, document.getElementById('viewer'))
.then(instance => {
const docViewer = instance.docViewer;
const annotManager = instance.annotManager;
const Annotations = instance.Annotations;
Annotations.ChoiceWidgetAnnotation.FORCE_SELECT=true;
const Actions = instance.Actions;
docViewer.on('documentLoaded', async () => {
const PDFNet = instance.PDFNet;
await PDFNet.Initialize();
// This part requires the full API: https://www.pdftron.com/documentation/web/guides/full-api/setup/
alert('async');
const doc = docViewer.getDocument();
// Get document from worker
const pdfDoc = await doc.getPDFDoc();
pdfDoc.getAcroForm().putBool("NeedAppearances", true);
});
docViewer.on('documentLoaded', () => {
docViewer.on('annotationsLoaded', () => {
const annotations = annotManager.getAnnotationsList();
annotations.forEach(annot => {
console.log('fieldName => '+annot.fieldName);
});
});
Please help me resolve this.
EDIT
Modified the code as suggested by #Andy.
The updated code in index.html file looks like below,
<!DOCTYPE html>
<html>
<head>
<title>Basic WebViewer</title>
</head>
<!-- Import WebViewer as a script tag -->
<script src='WebViewer-6.0.2/lib/webviewer.min.js'></script>
<body>
<div id='viewer' style='width: 1024px; height: 600px; margin: 0 auto;'>
<script>
WebViewer({
path: 'WebViewer-6.0.2/lib', // path to the PDFTron 'lib' folder on your server
type: 'html5',
fullAPI: true,
// licenseKey: 'Insert commercial license key here after purchase',
}, document.getElementById('viewer'))
.then(async instance => {
const { Annotations, Tools, CoreControls, PDFNet, PartRetrievers, docViewer, annotManager } = instance;
await PDFNet.Initialize();
Annotations.ChoiceWidgetAnnotation.FORCE_SELECT=true;
const Actions = instance.Actions;
docViewer.on('documentLoaded', async () => {
// This part requires the full API: https://www.pdftron.com/documentation/web/guides/full-api/setup/
const doc = docViewer.getDocument();
// Get document from worker
const pdfDoc = await doc.getPDFDoc();
const acroFrom = await pdfDoc.getAcroForm();
acroform.putBool("NeedAppearances", true);
});
instance.loadDocument('forms/test.pdf');
});
</script>
</div>
</body>
</html>
I am loading the file from a http server in my project folder.
http-server -a localhost -p 7080
Unfortunately, I am getting the same error.
Error: Full version of PDFNetJS has not been loaded. Please pass the "fullAPI: true" option in your WebViewer constructor to use the PDFNet APIs.
We are currently evaluating PDFTron, so the licenseKey option is not passed in the WebViewer constructor.
Kindly help me on this.
I have tried out the code you have provided and was still not able to reproduce the issue you are encountering. I do typically perform the initialize outside WebViewer events so the initialization occurs only once:
WebViewer(...)
.then(instance => {
const { Annotations, Tools, CoreControls, PDFNet, PartRetrievers, docViewer } = instance;
const annotManager = docViewer.getAnnotationManager();
await PDFNet.initialize(); // Only needs to be initialized once
docViewer.on('documentLoaded', ...);
docViewer.on('annotationsLoaded', ...);
});
Also, I noticed that you attach an an event handler to annotationsLoaded every time documentLoaded is triggered. I am not sure if that is intentional or desirable but this can lead to the handler triggering multiple times (when switching documents).
This may not matter but instead of using initialDoc, you could try instance.loadDocument after the initialize instead.
await PDFNet.initialize();
docViewer.on('documentLoaded', ...);
docViewer.on('annotationsLoaded', ...);
instance.loadDocument('http://...');
There is one last thing to mention about the full API. The APIs will return a promise most of the time as the result so you will have to await the return value most of the time.
const acroFrom = await pdfDoc.getAcroForm();
// You can await this too. Especially if you need a reference to the new bool object that was
acroform.putBool("NeedAppearances", true);
Let me know if this helps!

How do I cache other routes for offline browsing?

I am setting up a Progressive Web App supporting offline browsing.
I have already set up offline browsing for my main route ('domainsample.com/') and it responds 200 even if offline.
But when I navigate to other routes ('domainsample.com/about') I receive a No Internet Page error.
Here is a sample I deployed in Heroku the URL: https://pwa-hehe.herokuapp.com
I used Vue CLI 3 to set up the project and Node.js and Express.js to run my dist folder in the server.
// server.js
const express = require('express')
const path = require('path')
const history = require('connect-history-api-fallback')
const app = express()
const staticFileMiddleware = express.static(path.join(__dirname + '/dist'))
app.use(staticFileMiddleware)
app.use(history({
disableDotRule: true,
verbose: true
}))
app.use(staticFileMiddleware)
app.get('/', function (req, res) {
res.render(path.join(__dirname + '/dist/'))
})
var server = app.listen(process.env.PORT || 3000, function () {
var port = server.address().port
console.log("App now running on port", port)
})
// manifest.json
{
"name": "pwa-offline",
"short_name": "pwa-offline",
"icons": [
{
"src": "./img/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./img/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": "./index.html",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#4DBA87"
}
// service-worker.js
/**
* Welcome to your Workbox-powered service worker!
*
* You'll need to register this file in your web app and you should
* disable HTTP caching for this file too.
*
*
* The rest of the code is auto-generated. Please don't update this file
* directly; instead, make changes to your Workbox build configuration
* and re-run your build process.
*
*/
importScripts("https://storage.googleapis.com/workbox-cdn/releases/3.6.3/workbox-sw.js");
importScripts(
"/precache-manifest.d3f1ce5d8331bddc555348f44cfba9d8.js"
);
workbox.core.setCacheNameDetails({prefix: "pwa-offline"});
/**
* The workboxSW.precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
*
*/
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.suppressWarnings();
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
There's a simpler solution that doesn't involve using InjectManifest.
Simply add this to your vue.config.js file:
pwa: {
workboxOptions: {
navigateFallback: 'index.html'
}
}
It will automatically add the necessary code to your service worker:
workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("index.html"));
I had the same problem and I found the solution here: https://github.com/vuejs/vue-cli/issues/717#issuecomment-382079361
To anyone who is also using the vue/cli-pligin-pwa and vue-cli 3, I have solved this problem.
The vue/cli-pligin-pwa does its job in caching your js files for offline use of your web app.
but since you're using a Single Page App, you'll have to set a some kind of fallback.
The request for the page e.g.(sample.com/about) will be a navigation request and it will serve the cached page for /index.html
Source: https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route
So what I did is, I made a custom service worker by entering this line of code in my
vue.config.js file
// vue.config.js
module.exports = {
pwa: {
// configure the workbox plugin
workboxPluginMode: 'InjectManifest',
workboxOptions: {
swSrc: 'public/service-worker.js'
}
}
}
and now, create a service-worker.js in your public folder directory, and write up the the generated line of codes from vue/cli-pligin-pwa and add the line :
workbox.routing.registerNavigationRoute('/index.html');
//service-worker.js
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.suppressWarnings();
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
workbox.routing.registerNavigationRoute('/index.html');
// install new service worker when ok, then reload page.
self.addEventListener("message", msg => {
if (msg.data.action == 'skipWaiting') {
self.skipWaiting()
}
})
The problem is that your service worker is not aware of which assets to cache. What is the value of self.__precacheManifest? (probably the web manifest file).
You would need to configure workbox in order to have some assets for the precacheAndRoute method, as this will instruct the service worker on which files to cache and when. Something like:
workbox.precaching.precacheAndRoute([
'/styles/example.ac29.css',
'/app/offlinePage.js',
'/app/offlinePage.html',
// ... other entries ...
]);
Per default a service worker does not know which assets or HTTP responses to cache, therefore we have to define and implement a caching strategy according to our requirements.
I wrote an article about service workers and the available caching strategies. Have a look at it if you want to deepen the topic.

Ignoring a new file with vue's dev server

I'm using the pre-build-webpack plugin to merge several json files into 1 json array every time I start my app (npm run serve or npm run build), but the problem is that it gets caught in an infinite webpack compile loop in when I start the development server. I managed to find a solution to the problem by using the watch-ignore-webpack-plugin plugin, which initially seemed to have resolved the issue - webpack will now compile everything twice (it seems) and then it's good to go and I can access my local server. But the problem now is that when I visit localhost:8080 there's nothing. The screen's blank and there's nothing being console.log()ed, so I don't know what to do anymore.
If anyone's seen anything like this or know how to fix it, please let me know. If you require any additional info, also let me know.
Versions:
vue: 2.6.10 (as seen in package.json)
vue-cli: 3.11.0 (running vue -V in cmd)
pre-build-webpack: 0.1.0
watch-ignore-webpack-plugin: 1.0.0
webpack-log: 3.0.1
vue.config.js (with everything irrelevant removed):
const WebpackPreBuildPlugin = require('pre-build-webpack');
const WatchIgnorePlugin = require('watch-ignore-webpack-plugin');
module.exports = {
configureWebpack: {
plugins: [
new WebpackPreBuildPlugin(() => {
const fs = require('fs');
const glob = require('glob');
const log = require('webpack-log')({ name: 'ATTENTION!' });
const output = [];
const exclude = [];
glob('./src/components/mods/**/*.json', (err, paths) => {
paths.forEach(path => {
const content = JSON.parse(fs.readFileSync(path, 'utf-8'));
const pathSplit = path.split('/');
const modFolderName = pathSplit[pathSplit.length - 2]
if(!output.filter(val => val.id === content.id)[0]) {
if(exclude.indexOf(modFolderName) === -1) {
output.push(content);
} else {
log.warn(`SKIPPING CONTENTS OF "${modFolderName}"`);
}
} else {
log.error(`MOD WITH ID "${content.id}" ALREADY EXISTS!`);
process.exit(0);
}
});
// If I take out this line, the infinite loop doesn't occur, but then, of
// course, I don't get my merged json file either.
fs.writeFileSync('./src/config/modules/layoutConfig.json', JSON.stringify(output));
});
}),
// Neither of the blow paths work.
new WatchIgnorePlugin([/\layoutConfig.json$/]),
// new WatchIgnorePlugin(['./src/config/modules/layoutConfig.json']),
]
}
};