Playing a media stream in background in a PWA on Windows 10
When a PWA is running on Windows using the Hosted Web App model, it has to follow the same rules and requirements of a native application.
For example, try to install and open Twitter, which is probably the most popular PWA available on the Microsoft Store.
Then open Task Manager and minimize it, so that it isn't in foreground anymore. After a few seconds, you will notice the icon of a leaf appearing in the Status column near the application's name:
This means that the application has been suspended. It's kept in memory, but all the operations are suspended, so the app isn't consuming CPU, network, video memory, etc.
If you have some experience with Windows development, you will recall that this behavior is part of the application's lifecycle enforced by the Universal Windows Platform.
This means that if you have an application which plays a media stream, like music or video, the playback will be suspended and resumed only when it's put back in foreground.
What if I have a PWA which offers music or video playback and I want the streaming to continue even if the user minimizes it? The Universal Windows Platform comes to the rescue!
Interacting with the System Media Transport Controls
System Media Transport Controls is the name of the technology that acts as a middle man between Windows and 3rd party apps which need to play media content.
You can see this control in action simply by pressing on your keyboard one of the keys to control music playback, like Play / Pause or Volume Up / Down. Windows will display a popup like this:
3rd party applications can hook up into this control, in order to handle playback also when they are minimized. In the previous screenshot you can see, as an example, Spotify. If you minimize Spotify, the music will continue to play. Additionally, the control is displaying all the information about the song I'm currently listening to (title, album, artwork, etc.). And if I press one of the buttons, I can pause the playback or move to the previous or next song without having to open Spotify.
PWAs don't make an exception, since when they run on Windows 10 they have access to all the UWP APIs. The first step to achieve this goal is to open the manifest of your PWA project (the Package.appxmanifest) file and move to the Capabilities section. Enable the one called Background Media Playback.
The next step is to add, in the landing page of your web application, the following JavaScript code:
if (typeof Windows !== 'undefined') {
var systemMediaControls = Windows.Media.SystemMediaTransportControls.getForCurrentView();
systemMediaControls.isPlayEnabled = true;
systemMediaControls.isPauseEnabled = true;
systemMediaControls.isStopEnabled = true;
systemMediaControls.playbackStatus = Windows.Media.MediaPlaybackStatus.closed;
}
The first condition should be familiar, since we have already seen it when we have talked about implementing Windows notifications in a PWA. It makes sure that the next snippet of code is invoked only when the web application is running as a PWA on Windows 10. It will be completely ignored if it's running as a regular web app in a browser or as a PWA on another platform.
Once we have verified we are running on Windows, we get access to the System Media Transport Controls by using the getForCurrentView() method exposed by the Windows.Media.SystemMediaTransportControls class.
In the end we set some properties to define the behavior of the various buttons. That's all. If your PWA is playing any kind of media streaming (it could be even an embedded YouTube video), the audio will continue to play even if the application is minimized.
Further interaction with the System Media Transport Control
The code we have previously seen is the bare minimum level to get background playback. However, the System Media Transport Control flyout won't offer any kind of interaction. Pressing any of the buttons won't make any effect and the control won't display any info about the current media.
If you want to provide interaction, you need to subscribe to the buttonpressed event, which returns inside the function's arguments a Button property with the information about selected button. We do this by using the addEventListener() method exposed by the SystemMediaTransportControl object, passing buttonpressed as name of the event we want to subscribe to.
function systemMediaControlsButtonPressed(args) {
if (args.button == Windows.Media.SystemMediaTransportControlsButton.play) {
console.log("Play");
}
}
function doSomething() {
if (typeof Windows !== 'undefined') {
var systemMediaControls = Windows.Media.SystemMediaTransportControls.getForCurrentView();
systemMediaControls.addEventListener("buttonpressed", systemMediaControlsButtonPressed, true);
systemMediaControls.isPlayEnabled = true;
systemMediaControls.isPauseEnabled = true;
systemMediaControls.isStopEnabled = true;
systemMediaControls.playbackStatus = Windows.Media.MediaPlaybackStatus.closed;
}
}
The Button property leverages the SystemMediaTransportControlsButton enumerator (which is documented here). In the previous sample, I'm handling only the Play value, but there are many more like Pause, Next, Previous, etc.
If, instead, you want to update the flyout with information about the media which is being played you can use the DisplayUpdater object.
function doSomething() {
if (typeof Windows !== 'undefined') {
var systemMediaControls = Windows.Media.SystemMediaTransportControls.getForCurrentView();
systemMediaControls.isPlayEnabled = true;
systemMediaControls.isPauseEnabled = true;
systemMediaControls.isStopEnabled = true;
var updater = systemMediaControls.displayUpdater;
updater.type = Windows.Media.MediaPlaybackType.video;
updater.videoProperties.artist = "Channel 9";
updater.videoProperties.title = "MSIX: Inside and Out";
updater.thumbnail = Windows.Storage.Streams.RandomAccessStreamReference.createFromUri(new Windows.Foundation.Uri("https://vignette.wikia.nocookie.net/logopedia/images/a/ac/0383.channel-9-logo.png/revision/latest?cb=20131126132212&format=original"));
updater.update();
}
}
The object is exposed by the displayUpdater property of the SystemMediaTransportControls object.
Once you have a reference to it, you need to specify if the media being played is audio or video, by setting the type property. If you don't set it, you will get an error when you try to set the other properties.
Then you can customize the information being displayed, by using the objects musicProperties (in case the type is audio) or videopProperties (in case the type is video).
Each object exposes a set of properties to describe the media. In the previous sample, we set artist, title and thumbnail.
This is the output of the code:
Wrapping up
In this blog post we have seen how to enable background media playback in a PWA running on Windows. We can opt-in for the a basic integration, which will simply allow our PWA to continue reproducing the media even if it's minimized; or we can provide a more advanced integration, so that the user can interact with the media playback directly from Windows, without having to open the PWA.
A special thanks to Jeff Burtoft from the PAX team who gave me the hint on how to implement this scenario.
Happy coding!