Develop Your Vega Web App
Send and receive messages from WebView pages
You can send and receive messages with WebView for Vega. To send and receive messages, use the following options:
- React Native for Vega -> Web: The
injectJavaScript
method. - Web -> React Native for Vega: The
onMessage
prop.
Implement Vega device Fire TV features from a web page
The window.ReactNativeWebView.postMessage
method runs the onMessage
prop in React Native for Vega, which you can use to trigger Vega device Fire TV features. window.ReactNativeWebView.postMessage
only accepts one argument, which must be a string.
Set the initial focus to WebView
To set the focus to WebView, do the following.
In the src/App.tsx file, set the initial focus by adding the hasTVPreferredFocus={true}
prop to the webview component.
import { WebView } from "@amazon-devices/webview";
import * as React from "react";
import { useRef } from "react";
import { View } from "react-native";
export const App = () => {
const webRef = useRef(null);
return (
<View >
<WebView
ref={webRef}
hasTVPreferredFocus={true}
source={{
uri: "https://www.example.com",
}}
javaScriptEnabled={true}
onLoadStart={(event) => {
console.log("onLoadStart url: ", event.nativeEvent.url)
}}
onLoad={(event) => {
console.log("onLoad url: ", event.nativeEvent.url)
}}
onError={(event) => {
console.log("onError url: ", event.nativeEvent.url)
}}
/>
</View>
);
};
Enable and use DevTools for debugging
Chrome DevTools is enabled by default when you build the app with the debug version (process.env.NODE_ENV = 'development'
).
To build the DevTools debug app, do the following.
- At the command prompt, run the following command.
kepler build -b Debug
Complete the following steps to use the DevTools effectively. To use the DevTools for debugging, do the following.
- Install and launch the debug app, which opens a WebView.
- Perform port forwarding at the command prompt by running the following command.
vda forward tcp:9229 tcp:9229. DevTools runs on port 9229.
- Open Google Chrome, and then navigate to chrome://inspect/#devices.
- Find DevTools in the Remote Target section, in the list of connected devices.
- Open DevTools, by clicking inspect. Now, you can inspect the WebView.
Enable back-button remote key events in web JavaScript
The allowSystemKeyEvents
property controls whether the web app actively listens for specific system key events, such as the back button (identified by keyCode: 27
).
To enable back-button remote key events, do the following.
- In the src/App.tsx file, set the
allowSystemKeyEvents
property totrue
.
Supported remote key events
The following table shows the supported remote key events and their associated key codes.
Event Key | Key Code | Needs allowSystemKeyEvents Prop |
---|---|---|
GoBack |
27 | Yes |
Enter |
13 | No |
ArrowLeft |
37 | No |
ArrowRight |
39 | No |
ArrowDown |
40 | No |
ArrowUp |
38 | No |
MediaFastForward |
228 | No |
MediaPlayPause |
179 | No |
MediaRewind |
227 | No |
Checking HDR format and codec support in WebView for Vega
WebView currently supports HEVC Main10 (HLG, HDR10, HDR10+) and VP9 Profile2 (HLG, HDR10) on the current Fire TV device. AV1 HDR is not yet supported on the current Fire TV device. H264 and VP8 are not recommended codecs for HDR.
Check for HDR availability by using canPlayType()
or isTypeSupported()
.
Example checking for HDR using canPlayType()
>const video = document.querySelector('video');
>console.log(video.canPlayType('video/webm; codecs="vp09.02.10.10'));
probably
>console.log(video.canPlayType('video/mp4; codecs="hev1.2.4.L153.B0"'));
probably
Example checking for HDR using isTypeSupported()
>MediaSource.isTypeSupported('video/webm; codecs="vp09.02.10.10"');
true
>MediaSource.isTypeSupported('video/mp4; codecs="hev1.2.4.L153.B0"');
true
Not recommended to use MediaCapabilities.decodingInfo()
because of known issues. It reports VP9 Profile 2 support as false.
Example using HEVC
const hevcConfig = {
type: 'media-source',
video: {
contentType: 'video/mp4; codecs="hvc1.2.4.L153.B0"',
width: 3840,
height: 2140,
framerate: 60,
bitrate: 20000000,
transferFunction: 'pq',
colorGamut: 'rec2020'
}
};
navigator.mediaCapabilities.decodingInfo(hevcConfig)
Overriding default media control handlers
Some apps require granular control over media playback that goes beyond default WebView media controls. The following are some possible examples.
- Disabling seek operation during advertisements.
- Disabling all media control during live playback.
To achieve this level of granular control, developers have two options:
- Web-based implementation using
navigator.mediaSession
. To do this, enableallowsDefaultMediaControl
. You might do this for the following reasons.- It enables transport control for play, pause, skip forward, and skip backward.
- It can be implemented directly in the web page without app code changes.
- Direct integration with the
VegaMediaControl
interface in your app code. To do this, disableallowsDefaultMediaControl
. You might do this for the following reasons.- It provides full access to the platform's media control capabilities.
- It is recommended for apps requiring deep system integration.
Web-based implementation using navigator.mediaSession
The navigator.mediaSession
property is a web standard that provides a powerful interface between webpage media content and the platform media control system. It enables web apps to register action handlers for media controls for the currently playing media, and customize how media controls behave across different content.
When allowsDefaultMediaControl
is set to true in WebView, platform-level controls like Alexa voice commands are passed to registered custom handlers if available. Without registered handlers, WebView reverts to its default implementation. This lets apps implement custom handling based on playback context such as during advertisements or live streams.
For example, when users say, “Alexa, fast forward” with allowsDefaultMediaControl
set to true, WebView passes the command to the web page's seekforward handler if it's registered. If no handler is registered, WebView falls back to its default seek implementation. This lets the web page control seeking behavior based on the content playing, such as blocking seeks during advertisements.
Note: Regardless of the value of allowsDefaultMediaControl
, a web page's pause and play handlers will always be called when an app moves from the background and foreground.
Example using navigator.mediaSession
// Web Page JavaScript Code (to be included in the web page)
// Get reference to video element
const video = document.querySelector('video');
if ('mediaSession' in navigator) {
// Override play action
navigator.mediaSession.setActionHandler('play', () => {
// Custom play logic here
video.play();
});
// Override pause action
navigator.mediaSession.setActionHandler('pause', () => {
// Custom pause logic here
video.pause();
});
// Override seek actions
navigator.mediaSession.setActionHandler('seekbackward', (details) => {
// Custom seek logic here
const skipTime = details.seekOffset || 10;
video.currentTime = Math.max(video.currentTime - skipTime, 0);
});
navigator.mediaSession.setActionHandler('seekforward', (details) => {
// Custom seek logic here
const skipTime = details.seekOffset || 10;
video.currentTime = Math.min(video.currentTime + skipTime, video.duration);
});
}
// React Native App Code (to be implemented in your app)
return (
<View style={styles.sectionContainer}>
<WebView
ref={webRef}
hasTVPreferredFocus={true}
allowsDefaultMediaControl={true} // Required for navigator.mediaSession to work
source={{
uri: "https://example.com", // Replace with your URL
}}
javaScriptEnabled={true}
/>
</View>
);
Direct Integration with VegaMediaControl interface in app
VegaMediaControl
provides a powerful interface for managing media playback in Vega apps. This direct integration enables granular control over media features such as playback controls, seeking operations etc.
To integrate your Vega Web App with VegaMediaControl
, follow these key steps:
- Configure the WebView Prop.
- The default media control in the WebView is disabled by default. To keep the controls disabled, either leave the prop unset or explicitly set
allowsDefaultMediaControl
tofalse
. This ensures that yourVegaMediaControl
integration will take precedence over default WebView media controls.
- Implement the
VegaMediaControl
Interface.
- Add
VegaMediaControl
integration to your app
- JavaScript Bridge Communication
- Utilize
injectJavaScript
for web player control in response toVegaMediaControl
handlers.
Example integrating VegaMediaControl in your app code
Below is a detailed example demonstrating how to handle pause, play, seek forward, and seek backward commands using VegaMediaControl
in a Vega Web App.
- Add support for
VegaMediaControl
in manifest.toml. Update the manifest for KMC, along with other entries.[components] [[components.interactive]] categories = ["com.amazon.category.kepler.media"] [[extras]] key = "interface.provider" component-id = "com.amazondeveloper.keplerwebapp.main" [extras.value.application] [[extras.value.application.interface]] interface_name = "com.amazon.kepler.media.IMediaPlaybackServer" command_options = [ "Play", "Pause", "SkipForward", "SkipBackward", ]
- Add the following dependencies to package.json.
"@amazon-devices/kepler-media-controls": "^1.0.0", "@amazon-devices/kepler-media-types": "^1.0.0",
- The following app code examples creates the KMC server and the associated handler. Those handlers are used to inject JavaScript into the webpage with custom behavior.
import { WebView } from "@amazon-devices/webview"; import { useEffect, useRef, useMemo } from "react"; import { StyleSheet, View } from "react-native"; import { Action, IMediaControlHandlerAsync, IMediaMetadata, IMediaSessionId, ITimeValue, ITrack, MediaControlServerComponentAsync, MediaSessionState, RepeatMode, } from "@amazon-devices/kepler-media-controls"; import { IComponentInstance, useComponentInstance } from "@amazon-devices/react-native-kepler"; export const App = () => { const webRef = useRef(null); const componentInstance: IComponentInstance = useComponentInstance(); const mediaControlsHandler: IMediaControlHandlerAsync = useMemo( (): IMediaControlHandlerAsync => ({ handlePlay: function (sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handlePlay"]); // update the JS script based on the webpage implementation. webRef.current?.injectJavaScript("document.querySelector('video')?.play();"); return Promise.resolve(); }, handlePause: function (sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handlePause"]); // update the JS script based on the webpage implementation. webRef.current?.injectJavaScript("document.querySelector('video')?.pause();"); return Promise.resolve(); }, handleTogglePlayPause: function (sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleTogglePlayPause"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleStop: function (sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleStop"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleStartOver: function (sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleStartOver"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleFastForward: function (sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleFastForward"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleRewind: function (sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleRewind"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleSetPlaybackSpeed: function (speed: number, sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleSetPlaybackSpeed"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleSkipForward: function (delta: ITimeValue, sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleSkipForward"]); // update the JS script based on the webpage implementation. webRef.current?.injectJavaScript("document.querySelector('video').currentTime += 30;"); return Promise.resolve(); }, handleSkipBackward: function (delta: ITimeValue, sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleSkipBackward"]); // update the JS script based on the webpage implementation. webRef.current?.injectJavaScript("document.querySelector('video').currentTime -= 30;"); return Promise.resolve(); }, handleSeek: function (position: ITimeValue, sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleSeek"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleSetAudioVolume: function (volume: number, sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleSetAudioVolume"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleSetAudioTrack: function (audioTrack: ITrack, sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleSetAudioTrack"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleEnableTextTrack: function (textTrack: ITrack, sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleEnableTextTrack"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleDisableTextTrack: function (sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleDisableTextTrack"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleNext: function (sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleNext"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handlePrevious: function (sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handlePrevious"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleEnableShuffle: function (enable: boolean, sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleEnableShuffle"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleSetRepeatMode: function (mode: RepeatMode, sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleSetRepeatMode"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleSetRating: function (id: MediaId, rating: number, sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleSetRating", sessionId, id, rating]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleGetMetadataInfo: function (id: MediaId): Promise<IMediaMetadata> { console.log("Media Controls", ["handleGetMetadataInfo", id]); return Promise.resolve({ mediaId: id.contentId, title: 'Sample Title', date: '2024-01-01', artwork: [ { url: 'https://example.com/artwork1.jpg', sizeTag: 'medium', id: 'sample artwork id1', tag: 'fantastic', }, { url: 'https://example.com/artwork2.jpg', sizeTag: 'large', id: 'sample artwork id2', tag: 'wonderful', }, ], }); }, handleCustomAction: function (action: Action, sessionId?: IMediaSessionId): Promise<void> { console.log("Media Controls", ["handleCustomAction"]); // inject javascript based on the webpage implementation return Promise.resolve(); }, handleGetSessionState: function (sessionId?: IMediaSessionId): Promise<MediaSessionState[]> { console.log("Media Controls", ["handleGetSessionState"]); // inject javascript based on the webpage implementation return Promise.resolve([]); }, }), [] ); useEffect(() => { const mediaControlServer = MediaControlServerComponentAsync.getOrMakeServer(); mediaControlServer.setHandlerForComponent(mediaControlsHandler, componentInstance); }, [componentInstance, mediaControlsHandler]); const styles = StyleSheet.create({ sectionContainer: { flex: 1, }, }); return ( <View style={styles.sectionContainer}> <WebView ref={webRef} hasTVPreferredFocus={true} source={{ // Replace with your URL. uri: "https://example.com", }} javaScriptEnabled={true} /> </View> ); };
Load a local file URL stored on the device into a WebView
By default, WebView allows app file loading from the /pkg/assets directory. However, you must set a property to true
if you want to load an app file from a different directory.
To load a local app file URL stored on the device, do the following.
- In the src/App.tsx, set the
allowFileAccess
property totrue
.
Setting the property to true
enables WebView access and the ability to display the app file seamlessly from the specified directory. Failure to set allowFileAccess
to true
when you try to load an app file from a different directory causes an "Access Denied" error.
The file app functions within a sandboxed environment, limiting its access to the device's file system. The directories that the file app can access are detailed in the following table.
Path | Writable | Description |
---|---|---|
/pkg | No | The root of the application package |
/pkg/assets | No | Assets installed by the package |
/pkg/lib | No | The application entry point component and any additional libraries installed by the package |
/data | Yes | Writable location for the application data. Persistent across device reboots and package upgrades. Not shared between applications or services. |
/tmp | Yes | Temporary storage, which cannot be shared with any other application or service. It's nonpersistent, and is removed during the device reboot. |
/proc | No | Only /proc/self is available to the app |
Example: Use WebView to Load an HTML file from the assets directory
<View>
<WebView
source={{
uri: "file:///pkg/assets/sample.html",
}}
/>
</View>
Example: Use WebView to load an HTML file from the app data directory
<View>
<WebView
source={{
uri: "file:///data/sample.html",
}}
allowFileAccess={true}
/>
</View>
D-Pad navigation in WebView
WebView has spatial navigation enabled by default to provide basic D-Pad navigation.
Prevent spatial navigation in WebView
For apps that implement custom focus management rather than the default spatial navigation focus, apps can use Event::preventDefault
when catching the keydown event. Using Event::preventDefault
prevents conflicts between the custom focus logic and built-in WebView spatial navigation.
To manage serialized cookies or to clear cookies for your app, see Vega Web App Cookie Manager. To view other WebView APIs, see Vega Web App Component Reference.
How to prevent a flashing white screen before the page loads
Before a webpage is loaded, the component's background color is used for the view. If no background color has been set, then a white background is chosen.
This can be changed by updating the backgroundColor
style of the WebView component.
import { WebView } from "@amazon-devices/webview";
import * as React from "react";
import { useRef } from "react";
import { View, StyleSheet } from "react-native";
export const App = () => {
const webRef = useRef(null);
return (
<View style={styles.container}>
<WebView
style={styles.webview}
ref={webRef}
hasTVPreferredFocus={true}
source={{
uri: "https://www.example.com",
}}
javaScriptEnabled={true}
onLoadStart={(event) => {
console.log("onLoadStart", event.nativeEvent.description)
}}
onLoad={(event) => {
console.log("onLoad", event.nativeEvent.description)
}}
onError={(event) => {
console.log("onError", event.nativeEvent.description)
}}
/>
</View>
);
};
// Styles for layout and background color
const styles = StyleSheet.create({
container: {
flex: 1,
},
webview: {
backgroundColor: "#000000"
}
});
The background color of the webview style can be modified to whatever is appropriate for your app.
How to remove the icon that appears at the top of the screen
If an icon appears at the top of the screen when playing, it may be due to the disableRemotePlayback
property. The icon may be removed by setting the property to true. True means it's disabled, while false means it remains enabled.
Here's an example:
const obj = document.createElement("audio");
obj.disableRemotePlayback = true;
Related topics
Last updated: Sep 30, 2025