as

Settings
Sign out
Notifications
Alexa
Amazon Appstore
AWS
Documentation
Support
Contact Us
My Cases
Get Started
Design and Develop
Publish
Reference
Support

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.

Copied to clipboard.

  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.

Copied to clipboard.

kepler build -b Debug

Complete the following steps to use the DevTools effectively. To use the DevTools for debugging, do the following.

  1. Install and launch the debug app, which opens a WebView.
  2. Perform port forwarding at the command prompt by running the following command.

    Copied to clipboard.

     vda forward tcp:9229 tcp:9229.
     DevTools runs on port 9229.
    
  3. Open Google Chrome, and then navigate to chrome://inspect/#devices.
  4. Find DevTools in the Remote Target section, in the list of connected devices.
  5. 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 to true.

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()

Copied to clipboard.

>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()

Copied to clipboard.

>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

Copied to clipboard.

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, enable allowsDefaultMediaControl. 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, disable allowsDefaultMediaControl. 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

Copied to clipboard.

// 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:

  1. 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 to false. This ensures that your VegaMediaControl integration will take precedence over default WebView media controls.
  1. Implement the VegaMediaControl Interface.
  • Add VegaMediaControl integration to your app
  1. JavaScript Bridge Communication
  • Utilize injectJavaScript for web player control in response to VegaMediaControl 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.

  1. Add support for VegaMediaControl in manifest.toml. Update the manifest for KMC, along with other entries.

    Copied to clipboard.

     [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",
     ]
    
  2. Add the following dependencies to package.json.

    Copied to clipboard.

     "@amazon-devices/kepler-media-controls": "^1.0.0",
     "@amazon-devices/kepler-media-types": "^1.0.0",
    
  3. 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.

    Copied to clipboard.

     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 to true.

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

Copied to clipboard.

<View>
    <WebView
        source={{
            uri: "file:///pkg/assets/sample.html",
        }}
    />
</View>

Example: Use WebView to load an HTML file from the app data directory

Copied to clipboard.

<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.

Copied to clipboard.


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:

Copied to clipboard.

const obj = document.createElement("audio");
obj.disableRemotePlayback = true;

Last updated: Sep 30, 2025