> [!CAUTION]
> **Important:** Chrome will be removing support for Chrome Apps on all platforms. Chrome browser and the Chrome Web Store will continue to support extensions. [**Read the announcement**](https://blog.chromium.org/2020/08/changes-to-chrome-app-support-timeline.html) and learn more about [**migrating your app**](https://developer.chrome.com/apps/migration).

The goal of this doc is to get you started on building Chrome Apps with the [Sencha Ext JS](http://www.sencha.com/products/extjs)
framework. To achieve this goal, we will dive into a media player app built by Sencha. The [source
code](https://github.com/GoogleChrome/sencha-video-player-app) and [API Documentation](http://senchaprosvcs.github.com/GooglePlayer/docs/output/#!/api) are available on GitHub.

This app discovers a user's available media servers, including media devices connected to the pc and
software that manages media over the network. Users can browse media, play over the network, or save
offline.

Here are the key things you must do to build a media player app using Sencha Ext JS:

- Create manifest, `manifest.json`.
- Create [event page](https://developer.chrome.com/docs/apps/app_lifecycle.md.txt#eventpage), `background.js`.
- [Sandbox](https://developer.chrome.com/docs/apps/app_external.md.txt#sandboxing) app's logic.
- Communicate between Chrome App and sandboxed files.
- Discover media servers.
- Explore and play media.
- Save media offline.

## Create manifest

All Chrome Apps require a [manifest file](https://developer.chrome.com/docs/apps/manifest.md.txt) which contains the information Chrome needs to launch
apps. As indicated in the manifest, the media player app is "offline_enabled"; media assets can be
saved locally, accessed and played regardless of connectivity.

The "sandbox" field is used to sandbox the app's main logic in a unique origin. All sandboxed
content is exempt from the Chrome App [Content Security Policy](https://developer.chrome.com/docs/apps/contentSecurityPolicy.md.txt), but cannot directly access the
Chrome App APIs. The manifest also includes the "socket" permission; the media player app uses the
[socket API](https://developer.chrome.com/docs/apps/socket) to connect to a media server over the network.

```json
{
    "name": "Video Player",
    "description": "Features network media discovery and playlist management",
    "version": "1.0.0",
    "manifest_version": 2,
    "offline_enabled": true,
    "app": {
        "background": {
            "scripts": [
                "background.js"
            ]
        }
    },
    ...

    "sandbox": {
        "pages": ["sandbox.html"]
    },
    "permissions": [
        "experimental",
        "http://*/*",
        "unlimitedStorage",
        {
            "socket": [
                "tcp-connect",
                "udp-send-to",
                "udp-bind"
            ]
        }
    ]
}
```

## Create event page

All Chrome Apps require `background.js` to launch the application. The media player's main page,
`index.html`, opens in a window with the specified dimensions:

```javascript
chrome.app.runtime.onLaunched.addListener(function(launchData) {
    var opt = {
        width: 1000,
        height: 700
    };

    chrome.app.window.create('index.html', opt, function (win) {
        win.launchData = launchData;
    });

});
```

## Sandbox app's logic

Chrome Apps run in a controlled environment that enforces a strict [Content Security Policy
(CSP)](https://developer.chrome.com/docs/apps/contentSecurityPolicy). The media player app needs some higher privileges to render the Ext JS components. To
comply with CSP and execute the app logic, the app's main page, `index.html`, creates an iframe that
acts as a sandbox environment:

```html
<iframe id="sandbox-frame" sandbox="allow-scripts" src="sandbox.html"></iframe>
```

The iframe points to [sandbox.html](https://github.com/GoogleChrome/sencha-video-player-app/blob/master/sandbox.html) which includes the files required for the Ext JS
application:

```html
<html>
<head>
    <link rel="stylesheet" type="text/css" href="resources/css/app.css" />'
    <script src="sdk/ext-all-dev.js"></script>'
    <script src="lib/ext/data/PostMessage.js"></script>'
    <script src="lib/ChromeProxy.js"></script>'
    <script src="app.js"></script>
</head>
<body></body>
</html>
```

The [app.js](http://senchaprosvcs.github.com/GooglePlayer/docs/output/source/app.html#VP-Application) script executes all the Ext JS code and renders the media player views. Since this
script is sandboxed, it cannot directly access the Chrome App APIs. Communication between `app.js`
and non-sandboxed files is done using the [HTML5 Post Message API](https://developer.mozilla.org/docs/DOM/window.postMessage).

## Communicate between files

In order for the media player app to access Chrome App APIs, like query the network for media
servers, `app.js` posts messages to [index.js](https://github.com/GoogleChrome/sencha-video-player-app/blob/master/index.js). Unlike the sandboxed `app.js`, `index.js` can
directly access the Chrome App APIs.

`index.js` creates the iframe:

```javascript
var iframe = document.getElementById('sandbox-frame');

iframeWindow = iframe.contentWindow;
```

And listens for messages from the sandboxed files:

```javascript
window.addEventListener('message', function(e) {
    var data= e.data,
        key = data.key;

    console.log('[index.js] Post Message received with key ' + key);

    switch (key) {
        case 'extension-baseurl':
            extensionBaseUrl(data);
            break;

        case 'upnp-discover':
            upnpDiscover(data);
            break;

        case 'upnp-browse':
            upnpBrowse(data);
            break;

        case 'play-media':
            playMedia(data);
            break;

        case 'download-media':
            downloadMedia(data);
            break;

        case 'cancel-download':
            cancelDownload(data);
            break;

        default:
            console.log('[index.js] unidentified key for Post Message: "' + key + '"');
    }
}, false);
```

In the following example, `app.js` sends a message to `index.js` requesting the key
'extension-baseurl':

```javascript
Ext.data.PostMessage.request({
    key: 'extension-baseurl',
    success: function(data) {
        //...
    }
});
```

`index.js` receives the request, assigns the result, and replies by sending the Base URL back:

```javascript
function extensionBaseUrl(data) {
    data.result = chrome.extension.getURL('/');
    iframeWindow.postMessage(data, '*');
}
```

## Discover media servers

There's a lot that goes into discovering media servers. At a high level, the discovery workflow is
initiated by a user action to search for available media servers. The [MediaServer controller](https://github.com/GoogleChrome/sencha-video-player-app/blob/master/app/controller/MediaServers.js)
posts a message to `index.js`; `index.js` listens for this message and when received, calls
[Upnp.js](https://github.com/GoogleChrome/sencha-video-player-app/blob/master/lib/Upnp.js).

The `Upnp library` uses the Chrome App [socket API](https://developer.chrome.com/docs/apps/app_network.md.txt) to connect the media player app with any
discovered media servers and receive media data from the media server. `Upnp.js` also uses
[soapclient.js](https://github.com/GoogleChrome/sencha-video-player-app/blob/master/lib/soapclient.js) to parse the media server data. The remainder of this section describes this
workflow in more detail.

### Post message

When a user clicks the Media Servers button in the center of the media player app, `MediaServers.js`
calls `discoverServers()`. This function first checks for any outstanding discovery requests, and if
true, aborts them so the new request can be initiated. Next, the controller posts a message to
`index.js` with a key upnp-discovery, and two callback listeners:

```javascript
me.activeDiscoverRequest = Ext.data.PostMessage.request({
    key: 'upnp-discover',
    success: function(data) {
        var items = [];
        delete me.activeDiscoverRequest;

        if (serversGraph.isDestroyed) {
            return;
        }

        mainBtn.isLoading = false;
        mainBtn.removeCls('pop-in');
        mainBtn.setIconCls('ico-server');
        mainBtn.setText('Media Servers');

        //add servers
        Ext.each(data, function(server) {
            var icon,
                urlBase = server.urlBase;

            if (urlBase) {
                if (urlBase.substr(urlBase.length-1, 1) === '/'){
                        urlBase = urlBase.substr(0, urlBase.length-1);
                }
            }

            if (server.icons && server.icons.length) {
                if (server.icons[1]) {
                    icon = server.icons[1].url;
                }
                else {
                    icon = server.icons[0].url;
                }

                icon = urlBase + icon;
            }

            items.push({
                itemId: server.id,
                text: server.friendlyName,
                icon: icon,
                data: server
            });
        });

        ...
    },
    failure: function() {
        delete me.activeDiscoverRequest;

        if (serversGraph.isDestroyed) {
            return;
        }

        mainBtn.isLoading = false;
        mainBtn.removeCls('pop-in');
        mainBtn.setIconCls('ico-error');
        mainBtn.setText('Error...click to retry');
    }
});
```

### Call upnpDiscover()

`index.js` listens for the 'upnp-discover' message from `app.js` and responds by calling
`upnpDiscover()`. When a media server is discovered, `index.js` extracts the media server domain
from the parameters, saves the server locally, formats the media server data, and pushes the data to
the `MediaServer` controller.

### Parse media server data

When `Upnp.js` discovers a new media server, it then retrieves a description of the device and sends
a Soaprequest to browse and parse the media server data; `soapclient.js` parses the media elements
by tag name into a document.

### Connect to media server

`Upnp.js` connects to discovered media servers and receives media data using the Chrome App socket
API:

```javascript
socket.create("udp", {}, function(info) {
    var socketId = info.socketId;

    //bind locally
    socket.bind(socketId, "0.0.0.0", 0, function(info) {

        //pack upnp message
        var message = String.toBuffer(UPNP_MESSAGE);

        //broadcast to upnp
        socket.sendTo(socketId, message, UPNP_ADDRESS, UPNP_PORT, function(info) {

            // Wait 1 second
            setTimeout(function() {

                //receive
                socket.recvFrom(socketId, function(info) {

                    //unpack message
                    var data        = String.fromBuffer(info.data),
                        servers     = [],
                        locationReg = /^location:/i;

                    //extract location info
                    if (data) {
                        data = data.split("\r\n");

                        data.forEach(function(value) {
                            if (locationReg.test(value)){
                                servers.push(value.replace(locationReg, "").trim());
                            }
                        });
                    }

                    //success
                    callback(servers);
                });

            }, 1000);
        });
    });
});
```

## Explore and play media

The [MediaExplorer controller](https://github.com/GoogleChrome/sencha-video-player-app/blob/master/app/controller/MediaExplorer.js) lists all the media files inside a media server folder and is
responsible for updating the breadcrumb navigation in the media player app window. When a user
selects a media file, the controller posts a message to `index.js` with the 'play-media' key:

```javascript
onFileDblClick: function(explorer, record) {
    var serverPanel, node,
        type    = record.get('type'),
        url     = record.get('url'),
        name    = record.get('name'),
        serverId= record.get('serverId');

    if (type === 'audio' || type === 'video') {
        Ext.data.PostMessage.request({
            key     : 'play-media',
            params  : {
                url: url,
                name: name,
                type: type
            }
        });
    }
},
```

`index.js` listens for this post message and responds by calling `playMedia()`:

```javascript
function playMedia(data) {
    var type        = data.params.type,
        url         = data.params.url,
        playerCt    = document.getElementById('player-ct'),
        audioBody   = document.getElementById('audio-body'),
        videoBody   = document.getElementById('video-body'),
        mediaEl     = playerCt.getElementsByTagName(type)[0],
        mediaBody   = type === 'video' ? videoBody : audioBody,
        isLocal     = false;

    //save data
    filePlaying = {
        url : url,
        type: type,
        name: data.params.name
    };

    //hide body els
    audioBody.style.display = 'none';
    videoBody.style.display = 'none';

    var animEnd = function(e) {

        //show body el
        mediaBody.style.display = '';

        //play media
        mediaEl.play();

        //clear listeners
        playerCt.removeEventListener( 'transitionend', animEnd, false );
        animEnd = null;
    };

    //load media
    mediaEl.src = url;
    mediaEl.load();

    //animate in player
    playerCt.addEventListener( 'transitionend', animEnd, false );
    playerCt.style.transform = "translateY(0)";

    //reply postmessage
    data.result = true;
    sendMessage(data);
}
```

## Save media offline

Most of the hard work to save media offline is done by the [filer.js library](https://github.com/GoogleChrome/sencha-video-player-app/blob/master/lib/filer.js). You can read more
this library in [Introducing filer.js](http://ericbidelman.tumblr.com/post/14866798359/introducing-filer-js).

The process kicks off when a user selects one or more files and initiates the 'Take offline' action.
The [MediaExplorer controller](https://github.com/GoogleChrome/sencha-video-player-app/blob/master/app/controller/MediaExplorer.js) posts a message to `index.js` with a key 'download-media';
`index.js` listens for this message and calls the `downloadMedia()` function to initiate the
download process:

```javascript
function downloadMedia(data) {
        DownloadProcess.run(data.params.files, function() {
            data.result = true;
            sendMessage(data);
        });
    }
```

The `DownloadProcess` utility method creates an xhr request to get data from the media server and
waits for completion status. This initiates the onload callback which checks the received content
and saves the data locally using the `filer.js` function:

```javascript
filer.write(
    saveUrl,
    {
        data: Util.arrayBufferToBlob(fileArrayBuf),
        type: contentType
    },
    function(fileEntry, fileWriter) {

        console.log('file saved!');

        //increment downloaded
        me.completedFiles++;

        //if reached the end, finalize the process
        if (me.completedFiles === me.totalFiles) {

            sendMessage({
                key             : 'download-progresss',
                totalFiles      : me.totalFiles,
                completedFiles  : me.completedFiles
            });

            me.completedFiles = me.totalFiles = me.percentage = me.downloadedFiles = 0;
            delete me.percentages;

            //reload local
            loadLocalFiles(callback);
        }
    },
    function(e) {
        console.log(e);
    }
);
```

When the download process is finished, `MediaExplorer` updates the media file list and the media
player tree panel.