diff --git a/README.md b/README.md
index 5ff8deefd..14676fb1c 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,21 @@ Note: This requires you to use Ubuntu 16.04 or 18.04.
Clone the [SDL Core repository](https://github.com/smartdevicelink/sdl_core) and follow the setup instructions for the project. After the project is built, run an instance of SDL Core in your terminal.
+### Dependencies
+
+- nvm
+- chromium-browser
+- python3 and pip
+- ffmpeg
+- ffmpeg-python
+
+```
+sudo apt install chromium-browser ffmpeg python3 python3-pip -y
+python3 -m pip install ffmpeg-python
+```
+
+Check out [nvm on github](https://github.com/nvm-sh/nvm#installing-and-updating) to learn how to install and use nvm!
+
### Build and Run the HMI
Once SDL Core is running, follow these steps to set up the Generic HMI.
diff --git a/deploy_server.sh b/deploy_server.sh
index cc076966e..6909a6751 100755
--- a/deploy_server.sh
+++ b/deploy_server.sh
@@ -39,7 +39,7 @@ DeployServer() {
StartServer() {
cp ${SOURCE_DIR}/${TARGET_SCRIPT} ${TARGET_DIR}
- python3 ${TARGET_DIR}/${TARGET_SCRIPT} --host 127.0.0.1 --port 8081
+ python3 ${TARGET_DIR}/${TARGET_SCRIPT} --host 127.0.0.1 --ws-port 8081
rm ${TARGET_DIR}/${TARGET_SCRIPT}
}
diff --git a/src/css/_config.scss b/src/css/_config.scss
index 7ffa04f24..711906335 100644
--- a/src/css/_config.scss
+++ b/src/css/_config.scss
@@ -9,4 +9,9 @@ $accent-color: white;
$secondary-accent-color: bright-blue;
$font-primary-color: white;
-$font-secondary-color: slate;
\ No newline at end of file
+$font-secondary-color: slate;
+
+:export {
+ masterWidth: $master-width;
+ masterHeight: $master-height;
+}
\ No newline at end of file
diff --git a/src/css/base/_base.scss b/src/css/base/_base.scss
index 3202226bf..dfb597976 100644
--- a/src/css/base/_base.scss
+++ b/src/css/base/_base.scss
@@ -61,7 +61,7 @@ body {
.driver-distraction-button {
margin-top: 22px;
margin-left: 20px;
- width: 210px;
+ width: 150px;
border-radius: 2px;
padding: 10px 0;
float: left;
@@ -70,6 +70,13 @@ body {
}
}
+.resolution-selector {
+ margin-top: 18px;
+ margin-left: 20px;
+ border-radius: 2px;
+ float: left;
+}
+
.webengine-app-container {
iframe {
position: absolute;
diff --git a/src/index.js b/src/index.js
index 95b467562..8daf4883c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,6 +3,7 @@ import * as serviceWorker from './serviceWorker';
// import css
import './css/main.scss';
+import config from './css/_config.scss';
// import react and js
import MediaPlayer from './js/MediaPlayer';
@@ -17,6 +18,7 @@ import TilesOnly from './js/Templates/TilesOnly/TilesOnly';
import TextWithGraphic from './js/Templates/TextWithGraphic/TextWithGraphic'
import GraphicWithText from './js/Templates/GraphicWithText/GraphicWithText'
import DoubleGraphicWithSoftbuttons from './js/Templates/DoubleGraphicWithSoftbuttons/DoubleGraphicWithSoftbuttons'
+import NavFullscreenMap from './js/Templates/NavFullscreenMap/NavFullscreenMap'
import HMIMenu from './js/HMIMenu';
import InAppMenu from './js/InAppMenu';
import InAppList from './js/InAppList';
@@ -50,12 +52,19 @@ class HMIApp extends React.Component {
constructor(props) {
super(props);
this.state = {
- dark: true
+ dark: true,
+ resolution: "960x600 Scale 1",
+ scale: 1
}
this.sdl = new Controller();
this.handleClick = this.handleClick.bind(this);
this.togglePTUWithModem = this.togglePTUWithModem.bind(this);
this.handleDDToggle = this.handleDDToggle.bind(this);
+ this.pickResolution = this.pickResolution.bind(this);
+ this.onTouchBegin = this.onTouchBegin.bind(this);
+ this.onTouchMove = this.onTouchMove.bind(this);
+ this.onTouchEnd = this.onTouchEnd.bind(this);
+ this.onTouchEvent = this.onTouchEvent.bind(this);
}
handleClick() {
var theme = !this.state.dark
@@ -74,8 +83,82 @@ class HMIApp extends React.Component {
store.dispatch(setDDState(newDDState));
uiController.onDriverDistraction((newDDState) ? "DD_ON" : "DD_OFF");
}
+ pickResolution(event) {
+ var match = event.target.value.match(/(\d+)x(\d+) Scale (\d+.?\d*)/);
+ var capability = {
+ systemCapabilityType: 'VIDEO_STREAMING',
+ videoStreamingCapability: {
+ scale: parseFloat(match[3]),
+ preferredResolution: {
+ resolutionWidth: parseInt(match[1]),
+ resolutionHeight: parseInt(match[2])
+ }
+ }
+ }
+ this.setState({ resolution: event.target.value, scale: match[3] });
+
+ bcController.onSystemCapabilityUpdated(capability, this.props.activeAppId);
+ }
+
+ onTouchEvent(type, event) {
+ if (!this.videoRect) {
+ var video = document.getElementById('navi_stream');
+ this.videoRect = video.getBoundingClientRect();
+ }
+
+ if (event.pageX < this.videoRect.left
+ || event.pageX > this.videoRect.right
+ || event.pageY < this.videoRect.top
+ || event.pageY > this.videoRect.bottom) {
+ return;
+ }
+
+ uiController.onTouchEvent(type, [{
+ id: 0,
+ ts: [ parseInt(event.timeStamp) ],
+ c: [{
+ x: event.pageX - this.videoRect.x,
+ y: event.pageY - this.videoRect.y
+ }]
+ }]);
+ }
+
+ onTouchBegin(event) {
+ this.touchInProgress = true;
+ this.onTouchEvent('BEGIN', event);
+ }
+
+ onTouchMove(event) {
+ if (this.touchInProgress) {
+ this.onTouchEvent('MOVE', event);
+ }
+ }
+
+ onTouchEnd(event) {
+ if (this.touchInProgress) {
+ this.touchInProgress = false;
+ this.onTouchEvent('END', event);
+ }
+ }
+
render() {
const themeClass = this.state.dark ? 'dark-theme' : 'light-theme';
+ var videoStyle = { position: 'absolute', width: config.masterWidth, height: config.masterHeight - 75, top: 75, left: 0, backgroundColor: 'transparent', objectFit: 'fill' };
+ if (!this.props.videoStreamVisible || !this.props.videoStreamUrl) {
+ videoStyle.display = 'none';
+ }
+
+ var resolutionSelector = undefined;
+ if (this.props.activeAppState && this.props.activeAppState.videoStreamingCapability.length) {
+ resolutionSelector = (
+
+
+
);
+ }
+
return(
+
{
this.props.webEngineApps.map((app) => {
let query = `?sdl-host=${flags.CoreHost}&sdl-port=${flags.CoreWebEngineAppPort}&sdl-transport-role=${app.transportType.toLowerCase()}-server`;
@@ -176,7 +264,10 @@ const mapStateToProps = (state) => {
webEngineApps: state.appStore.installedApps.filter(app => app.runningAppId),
showWebView: state.appStore.webViewActive,
activeAppId: state.activeApp,
- dd: state.ddState
+ dd: state.ddState,
+ videoStreamUrl: state.system.videoStreamUrl,
+ videoStreamVisible: state.system.navigationActive && state.activeApp === state.system.videoStreamingApp,
+ activeAppState: state.ui[state.activeApp]
}
}
HMIApp = connect(mapStateToProps)(HMIApp)
@@ -199,6 +290,7 @@ ReactDOM.render((
+
diff --git a/src/js/Controllers/BCController.js b/src/js/Controllers/BCController.js
index 6577e4761..d7675ba8e 100644
--- a/src/js/Controllers/BCController.js
+++ b/src/js/Controllers/BCController.js
@@ -1,6 +1,6 @@
import RpcFactory from './RpcFactory'
import store from '../store'
-import { updateAppList, activateApp, deactivateApp, registerApplication, unregisterApplication, policyUpdate, onPutFile, updateColorScheme, setAppIsConnected, onSystemCapabilityUpdated, updateInstalledAppStoreApps, appStoreAppInstalled, appStoreAppUninstalled } from '../actions'
+import { updateAppList, activateApp, deactivateApp, registerApplication, unregisterApplication, policyUpdate, onPutFile, updateColorScheme, setAppIsConnected, onSystemCapabilityUpdated, updateInstalledAppStoreApps, appStoreAppInstalled, appStoreAppUninstalled, setVideoStreamingCapability } from '../actions'
import sdlController from './SDLController'
import externalPolicies from './ExternalPoliciesController'
import {flags} from '../Flags'
@@ -48,6 +48,10 @@ class BCController {
if (rpc.params.application.appType.includes("WEB_VIEW")) {
store.dispatch(registerApplication(rpc.params.application.appID, "web-view"));
this.listener.send(RpcFactory.OnSystemCapabilityDisplay("WEB_VIEW", rpc.params.application.appID));
+ } else if (rpc.params.application.appType.includes("NAVIGATION")
+ || rpc.params.application.appType.includes("PROJECTION")) {
+ store.dispatch(registerApplication(rpc.params.application.appID, "nav-fullscreen-map"));
+ this.listener.send(RpcFactory.OnSystemCapabilityDisplay("NAV_FULLSCREEN_MAP", rpc.params.application.appID));
} else {
var templates = rpc.params.application.isMediaApplication ? ["media","MEDIA"] : ["nonmedia","NON-MEDIA"];
store.dispatch(registerApplication(rpc.params.application.appID, templates[0]));
@@ -98,6 +102,17 @@ class BCController {
case "GetSystemTime":
this.listener.send(RpcFactory.GetSystemTime(rpc.id))
return null
+
+ case "OnAppCapabilityUpdated":
+ if (rpc.params.appCapability.appCapabilityType === 'VIDEO_STREAMING'
+ && rpc.params.appCapability.videoStreamingCapability) {
+ var vsc = rpc.params.appCapability.videoStreamingCapability;
+ if (!vsc.additionalVideoStreamingCapabilities) {
+ vsc.additionalVideoStreamingCapabilities = [];
+ }
+ store.dispatch(setVideoStreamingCapability(rpc.params.appID, vsc.additionalVideoStreamingCapabilities));
+ }
+ return null;
default:
return false;
}
@@ -178,6 +193,9 @@ class BCController {
getAppProperties(policyAppID){
this.listener.send(RpcFactory.GetAppProperties(policyAppID))
}
+ onSystemCapabilityUpdated(capability, appID) {
+ this.listener.send(RpcFactory.OnSystemCapabilityUpdated(capability, appID));
+ }
}
let controller = new BCController()
diff --git a/src/js/Controllers/Controller.js b/src/js/Controllers/Controller.js
index 24290b6ff..02683178c 100644
--- a/src/js/Controllers/Controller.js
+++ b/src/js/Controllers/Controller.js
@@ -134,9 +134,11 @@ export default class Controller {
this.subscribeToNotification("BasicCommunication.OnAppUnregistered")
this.subscribeToNotification("BasicCommunication.OnPutFile")
this.subscribeToNotification("Navigation.OnVideoDataStreaming")
+ this.subscribeToNotification("Navigation.OnAudioDataStreaming")
this.subscribeToNotification("SDL.OnStatusUpdate")
this.subscribeToNotification("BasicCommunication.OnSystemCapabilityUpdated")
this.subscribeToNotification("AppService.OnAppServiceData")
+ this.subscribeToNotification("BasicCommunication.OnAppCapabilityUpdated")
var onSystemTimeReady = {
"jsonrpc": "2.0",
diff --git a/src/js/Controllers/DisplayCapabilities.js b/src/js/Controllers/DisplayCapabilities.js
index 3601f94b6..66c62ea2d 100644
--- a/src/js/Controllers/DisplayCapabilities.js
+++ b/src/js/Controllers/DisplayCapabilities.js
@@ -1,3 +1,8 @@
+import config from '../../css/_config.scss';
+const masterWidth = parseInt(config.masterWidth);
+const masterHeight = parseInt(config.masterHeight);
+const templateHeight = masterHeight - 75;
+
let softButtonCapability = {
"shortPressAvailable": true,
"longPressAvailable": true,
@@ -15,16 +20,16 @@ let imageOnlySoftButtonCapability = {
}
let templatesAvailable = [
- "DEFAULT", "MEDIA", "NON-MEDIA", "LARGE_GRAPHIC_WITH_SOFTBUTTONS",
- "LARGE_GRAPHIC_ONLY", "GRAPHIC_WITH_TEXTBUTTONS", "TEXTBUTTONS_WITH_GRAPHIC",
- "TEXTBUTTONS_ONLY", "TEXT_WITH_GRAPHIC", "GRAPHIC_WITH_TEXT",
- "DOUBLE_GRAPHIC_WITH_SOFTBUTTONS", "TILES_ONLY", "WEB_VIEW"
+ "DEFAULT", "MEDIA", "NON-MEDIA", "LARGE_GRAPHIC_WITH_SOFTBUTTONS", "LARGE_GRAPHIC_ONLY",
+ "GRAPHIC_WITH_TEXTBUTTONS", "TEXTBUTTONS_WITH_GRAPHIC", "TEXTBUTTONS_ONLY",
+ "TEXT_WITH_GRAPHIC", "GRAPHIC_WITH_TEXT", "DOUBLE_GRAPHIC_WITH_SOFTBUTTONS", "WEB_VIEW",
+ "NAV_FULLSCREEN_MAP", "TILES_ONLY"
]
let screenParams = {
"resolution": {
- "resolutionWidth": 960,
- "resolutionHeight": 675
+ "resolutionWidth": masterWidth,
+ "resolutionHeight": masterHeight
},
"touchEventAvailable": {
"pressAvailable": true,
@@ -571,6 +576,39 @@ let capabilities = {
softButtonCapability
]
},
+ "NAV_FULLSCREEN_MAP": {
+ "displayCapabilities": {
+ "displayType": "SDL_GENERIC",
+ "displayName": "GENERIC_DISPLAY",
+ "textFields": [
+ textField("templateTitle", 50),
+ textField("alertText1"),
+ textField("alertText2"),
+ textField("alertText3"),
+ textField("subtleAlertText1"),
+ textField("subtleAlertText2"),
+ textField("subtleAlertSoftButtonText"),
+ textField("menuName"),
+ textField("secondaryText"),
+ textField("tertiaryText")
+ ],
+ "imageFields": [
+ imageField("choiceImage", 40),
+ imageField("menuIcon", 40),
+ imageField("cmdIcon", 150),
+ imageField("appIcon", 50),
+ imageField("alertIcon", 225),
+ imageField("subtleAlertIcon", 225)
+ ],
+ "mediaClockFormats": [],
+ "graphicSupported": false,
+ "templatesAvailable": templatesAvailable,
+ "screenParams": screenParams,
+ "imageCapabilities": ["DYNAMIC", "STATIC"],
+ "menuLayoutsAvailable": ["LIST", "TILES"]
+ },
+ "softButtonCapabilities": []
+ },
"COMMON": {
"audioPassThruCapabilities": {
"samplingRate": "44KHZ",
@@ -595,7 +633,7 @@ let capabilities = {
"hmiCapabilities": {
"navigation": false,
"phoneCall": false,
- "videoStreaming": false
+ "videoStreaming": true
},
"systemCapabilities": {
"navigationCapability": {
@@ -608,6 +646,65 @@ let capabilities = {
"driverDistractionCapability": {
"menuLength": 3,
"subMenuDepth": 2
+ },
+ videoStreamingCapability: {
+ preferredResolution: {
+ resolutionWidth: masterWidth,
+ resolutionHeight: templateHeight
+ },
+ maxBitrate: 400000,
+ supportedFormats: [
+ { protocol: "RAW", codec: "H264" },
+ { protocol: "RTP", codec: "H264" },
+ { protocol: "RTSP", codec: "Theora" },
+ { protocol: "RTMP", codec: "VP8" },
+ { protocol: "WEBM", codec: "VP9" }
+ ],
+ hapticSpatialDataSupported: true,
+ diagonalScreenSize: 7,
+ pixelPerInch: 96,
+ scale: 1,
+ preferredFPS: 30,
+ additionalVideoStreamingCapabilities: [
+ {
+ preferredResolution: {
+ resolutionWidth: masterWidth,
+ resolutionHeight: templateHeight
+ },
+ maxBitrate: 400000,
+ supportedFormats: [
+ { protocol: "RAW", codec: "H264" },
+ { protocol: "RTP", codec: "H264" },
+ { protocol: "RTSP", codec: "Theora" },
+ { protocol: "RTMP", codec: "VP8" },
+ { protocol: "WEBM", codec: "VP9" }
+ ],
+ hapticSpatialDataSupported: true,
+ diagonalScreenSize: 7,
+ pixelPerInch: 72,
+ scale: 1.5,
+ preferredFPS: 30
+ },
+ {
+ preferredResolution: {
+ resolutionWidth: masterWidth,
+ resolutionHeight: templateHeight
+ },
+ maxBitrate: 400000,
+ supportedFormats: [
+ { protocol: "RAW", codec: "H264" },
+ { protocol: "RTP", codec: "H264" },
+ { protocol: "RTSP", codec: "Theora" },
+ { protocol: "RTMP", codec: "VP8" },
+ { protocol: "WEBM", codec: "VP9" }
+ ],
+ hapticSpatialDataSupported: true,
+ diagonalScreenSize: 7,
+ pixelPerInch: 48,
+ scale: 2,
+ preferredFPS: 30
+ }
+ ]
}
}
}
@@ -674,7 +771,8 @@ const getDisplayCapability = (template) => {
var capability = {
displayName: templateCapability.displayCapabilities.displayName,
windowTypeSupported: [mainWindowTypeCapability],
- windowCapabilities: [getWindowCapability(template)]
+ windowCapabilities: [getWindowCapability(template)],
+ screenParams: screenParams
}
return capability;
diff --git a/src/js/Controllers/NavController.js b/src/js/Controllers/NavController.js
index a0102dc76..127225083 100644
--- a/src/js/Controllers/NavController.js
+++ b/src/js/Controllers/NavController.js
@@ -1,4 +1,8 @@
import RpcFactory from './RpcFactory'
+import FileSystemController from './FileSystemController'
+import store from '../store';
+import { setVideoStreamUrl, setVideoStreamingApp } from '../actions'
+import AudioPlayer from '../Utils/AudioPlayer'
class NavController {
constructor () {
@@ -9,22 +13,79 @@ class NavController {
}
handleRPC(rpc) {
let methodName = rpc.method.split(".")[1]
- var message = "";
switch (methodName) {
case "IsReady":
- return {"rpc": RpcFactory.IsReadyResponse(rpc, false)}
+ return {"rpc": RpcFactory.IsReadyResponse(rpc, true)}
+ case "OnAudioDataStreaming":
+ if (rpc.params.available) {
+ FileSystemController.subscribeToEvent('StartAudioStream', (success, params) => {
+ if (!success || !params.endpoint) {
+ console.error('Error encountered while starting audio stream');
+ return;
+ }
+
+ AudioPlayer.play(params.endpoint);
+ });
+
+ FileSystemController.sendJSONMessage({
+ method: 'StartAudioStream',
+ params: {
+ url: this.audioStreamUrl
+ }
+ });
+ } else {
+ AudioPlayer.pause();
+ }
+ return null
+ case "OnVideoDataStreaming":
+ if (rpc.params.available) {
+ FileSystemController.subscribeToEvent('StartVideoStream', (success, params) => {
+ if (!success) {
+ console.error('Error encountered while starting video stream');
+ return;
+ }
+
+ if (params.endpoint) {
+ store.dispatch(setVideoStreamUrl(params.endpoint));
+ var video = document.getElementById('navi_stream');
+ video.play();
+ }
+ });
+
+ FileSystemController.sendJSONMessage({
+ method: 'StartVideoStream',
+ params: {
+ url: this.videoStreamUrl,
+ config: this.videoConfig,
+ webm: this.webmSupport
+ }
+ });
+ } else {
+ document.getElementById('navi_stream').pause();
+ store.dispatch(setVideoStreamUrl(null));
+ }
+ return null
case "StartStream":
- message = "This system does not support video streaming."
- return {"rpc": RpcFactory.UnsupportedResourceResponse(rpc, message)};
+ this.videoStreamUrl = rpc.params.url;
+ store.dispatch(setVideoStreamingApp(rpc.params.appID));
+ return { "rpc": RpcFactory.StartStreamSuccess(rpc.id) };
+ case "StopStream":
+ store.dispatch(setVideoStreamUrl(null));
+ return { "rpc": RpcFactory.StopStreamSuccess(rpc.id) };
case "StartAudioStream":
- message = "This system does not support audio streaming."
- return {"rpc": RpcFactory.UnsupportedResourceResponse(rpc, message)};
+ this.audioStreamUrl = rpc.params.url;
+ return {"rpc": RpcFactory.StartAudioStreamSuccess(rpc.id)};
case "SetVideoConfig":
- message = "This system does not support video streaming."
- return {"rpc": RpcFactory.UnsupportedResourceResponse(rpc, message)};
+ this.videoConfig = rpc.params.config;
+
+ if (this.webmSupport === undefined) {
+ var video = document.getElementById('navi_stream');
+ this.webmSupport = 'probably' === video.canPlayType('video/webm; codecs="vp8"');
+ }
+
+ return { "rpc": RpcFactory.SetVideoConfigSuccess(rpc.id) };
default:
- message = "This RPC is not supported."
- return {"rpc": RpcFactory.UnsupportedResourceResponse(rpc, message)};
+ return {"rpc": RpcFactory.UnsupportedResourceResponse(rpc, "This RPC is not supported.")};
}
}
}
diff --git a/src/js/Controllers/RpcFactory.js b/src/js/Controllers/RpcFactory.js
index 2c9b4c1de..a9244361c 100644
--- a/src/js/Controllers/RpcFactory.js
+++ b/src/js/Controllers/RpcFactory.js
@@ -710,21 +710,26 @@ class RpcFactory {
}
})
}
- static OnSystemCapabilityDisplay(template, appID) {
- var systemCapability = {
- systemCapabilityType: "DISPLAYS",
- displayCapabilities: [getDisplayCapability(template)]
- }
+
+ static OnSystemCapabilityUpdated(capability, appID) {
return ({
"jsonrpc": "2.0",
"method": "BasicCommunication.OnSystemCapabilityUpdated",
"params": {
- "systemCapability": systemCapability,
+ "systemCapability": capability,
"appID": appID
}
})
}
+ static OnSystemCapabilityDisplay(template, appID) {
+ var systemCapability = {
+ systemCapabilityType: "DISPLAYS",
+ displayCapabilities: [getDisplayCapability(template)]
+ }
+ return this.OnSystemCapabilityUpdated(systemCapability, appID);
+ }
+
static OnUpdateFile(appID, fileName) {
return ({
"jsonrpc": "2.0",
@@ -782,6 +787,61 @@ class RpcFactory {
}
return message;
}
+
+ static OnTouchEvent(type, events) {
+ return ({
+ 'jsonrpc': '2.0',
+ 'method': 'UI.OnTouchEvent',
+ 'params': {
+ 'type': type,
+ 'event': events
+ }
+ })
+ }
+
+ static StartStreamSuccess(id) {
+ return {
+ "jsonrpc": "2.0",
+ "id": id,
+ "result": {
+ "code": 0,
+ "method": "Navigation.StartStream"
+ }
+ };
+ }
+
+ static StartAudioStreamSuccess(id) {
+ return {
+ "jsonrpc": "2.0",
+ "id": id,
+ "result": {
+ "code": 0,
+ "method": "Navigation.StartAudioStream"
+ }
+ };
+ }
+
+ static StopStreamSuccess(id) {
+ return {
+ "jsonrpc": "2.0",
+ "id": id,
+ "result": {
+ "code": 0,
+ "method": "Navigation.StopStream"
+ }
+ };
+ }
+
+ static SetVideoConfigSuccess(id) {
+ return {
+ "jsonrpc": "2.0",
+ "id": id,
+ "result": {
+ "code": 0,
+ "method": "Navigation.SetVideoConfig"
+ }
+ };
+ }
}
export default RpcFactory
diff --git a/src/js/Controllers/UIController.js b/src/js/Controllers/UIController.js
index 4a03cf901..b0337c67d 100644
--- a/src/js/Controllers/UIController.js
+++ b/src/js/Controllers/UIController.js
@@ -610,6 +610,10 @@ class UIController {
onKeyboardInput(value, event) {
this.listener.send(RpcFactory.OnKeyboardInput(value, event))
}
+
+ onTouchEvent(type, events) {
+ this.listener.send(RpcFactory.OnTouchEvent(type, events));
+ }
}
let controller = new UIController ()
diff --git a/src/js/Templates/NavFullscreenMap/NavFullscreenMap.js b/src/js/Templates/NavFullscreenMap/NavFullscreenMap.js
new file mode 100644
index 000000000..b18820ba9
--- /dev/null
+++ b/src/js/Templates/NavFullscreenMap/NavFullscreenMap.js
@@ -0,0 +1,61 @@
+import React from 'react';
+import { connect } from 'react-redux'
+import AppHeader from '../../containers/Header';
+import store from '../../store';
+import { systemNavigationViewActive, systemNavigationViewInactive } from '../../actions';
+
+class NavFullscreenMap extends React.Component {
+ componentWillMount() {
+ store.dispatch(systemNavigationViewActive());
+ }
+
+ componentWillUnmount() {
+ store.dispatch(systemNavigationViewInactive());
+ }
+
+ getColorScheme() {
+ var activeApp = this.props.activeApp
+ var colorScheme = null;
+ if (activeApp) {
+ if (this.props.theme === true) { //Dark Theme
+ if (this.props.ui[activeApp].nightColorScheme && this.props.ui[activeApp].nightColorScheme.backgroundColor) {
+ colorScheme = this.props.ui[activeApp].nightColorScheme.backgroundColor
+ }
+ } else { //Light Theme
+ if (this.props.ui[activeApp].dayColorScheme && this.props.ui[activeApp].dayColorScheme.backgroundColor) {
+ colorScheme = this.props.ui[activeApp].dayColorScheme.backgroundColor
+ }
+ }
+ }
+
+ if (colorScheme) {
+ var redInt = colorScheme.red;
+ var blueInt = colorScheme.blue;
+ var greenInt = colorScheme.green;
+ var cssColorScheme = {
+ backgroundColor: `rgb(${redInt}, ${greenInt}, ${blueInt})`
+ }
+ return cssColorScheme;
+ } else {
+ return null;
+ }
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
+
+const mapStateToProps = (state) => {
+ return {
+ activeApp: state.activeApp,
+ theme: state.theme,
+ ui: state.ui
+ };
+};
+
+export default connect(mapStateToProps)(NavFullscreenMap);
\ No newline at end of file
diff --git a/src/js/Templates/NonMedia/NonMedia.js b/src/js/Templates/NonMedia/NonMedia.js
index d815f4106..df54e250d 100644
--- a/src/js/Templates/NonMedia/NonMedia.js
+++ b/src/js/Templates/NonMedia/NonMedia.js
@@ -48,7 +48,7 @@ const mapStateToProps = (state) => {
return {
activeApp: state.activeApp,
theme: state.theme,
- ui: state.ui
+ ui: state.ui
};
};
diff --git a/src/js/Utils/AudioPlayer.js b/src/js/Utils/AudioPlayer.js
new file mode 100644
index 000000000..953ef3922
--- /dev/null
+++ b/src/js/Utils/AudioPlayer.js
@@ -0,0 +1,24 @@
+class AudioPlayer {
+ constructor() {
+ this.audio = new Audio();
+ }
+
+ play(url) {
+ this.audio.src = url;
+ this.audio.play().then(_ =>
+ console.log('audio streaming starting @ ', url)
+ ).catch(err =>
+ console.error('failed to stream audio: ', err)
+ )
+ }
+
+ pause() {
+ if (this.audio.src !== '') {
+ this.audio.pause();
+ this.audio.src = '';
+ }
+ }
+}
+
+const audio = new AudioPlayer();
+export default audio;
\ No newline at end of file
diff --git a/src/js/actions.js b/src/js/actions.js
index 27c08203a..312e6b0ae 100644
--- a/src/js/actions.js
+++ b/src/js/actions.js
@@ -43,7 +43,11 @@ export const Actions = {
WEBENGINE_APP_LAUNCH: "WEBENGINE_APP_LAUNCH",
APPSTORE_BEGIN_INSTALL: "APPSTORE_BEGIN_INSTALL",
WEB_VIEW_ACTIVE: "WEB_VIEW_ACTIVE",
- SET_DD_STATE: "SET_DD_STATE"
+ SET_DD_STATE: "SET_DD_STATE",
+ SET_VIDEO_STREAM_URL: "SET_VIDEO_STREAM_URL",
+ SET_VIDEO_STREAM_APP: "SET_VIDEO_STREAM_APP",
+ NAVIGATION_VIEW_ACTIVE: "NAVIGATION_VIEW_ACTIVE",
+ SET_VIDEO_STREAM_CAPABILITY: "SET_VIDEO_STREAM_CAPABILITY"
}
export const updateAppList = (applications) => {
@@ -432,3 +436,39 @@ export const setDDState = (ddState) => {
dd: ddState
}
}
+
+export const setVideoStreamUrl = (url) => {
+ return {
+ type: Actions.SET_VIDEO_STREAM_URL,
+ url: url
+ }
+}
+
+export const setVideoStreamingApp = (appID) => {
+ return {
+ type: Actions.SET_VIDEO_STREAM_APP,
+ appID: appID
+ }
+}
+
+export const systemNavigationViewActive = () => {
+ return {
+ type: Actions.NAVIGATION_VIEW_ACTIVE,
+ active: true
+ }
+}
+
+export const systemNavigationViewInactive = () => {
+ return {
+ type: Actions.NAVIGATION_VIEW_ACTIVE,
+ active: false
+ }
+}
+
+export const setVideoStreamingCapability = (appID, capability) => {
+ return {
+ type: Actions.SET_VIDEO_STREAM_CAPABILITY,
+ appID: appID,
+ capability: capability
+ }
+}
diff --git a/src/js/reducers.js b/src/js/reducers.js
index c25945061..38846f450 100644
--- a/src/js/reducers.js
+++ b/src/js/reducers.js
@@ -50,7 +50,63 @@ function newAppState () {
msgID: null
},
dayColorScheme: null,
- nightColorScheme: null
+ nightColorScheme: null,
+ videoStreamingCapability: [
+ {
+ preferredResolution: {
+ resolutionWidth: 960,
+ resolutionHeight: 600
+ },
+ maxBitrate: 400000,
+ supportedFormats: [
+ { protocol: "RAW", codec: "H264" },
+ { protocol: "RTP", codec: "H264" },
+ { protocol: "RTSP", codec: "Theora" },
+ { protocol: "RTMP", codec: "VP8" },
+ { protocol: "WEBM", codec: "VP9" }
+ ],
+ hapticSpatialDataSupported: true,
+ diagonalScreenSize: 7,
+ pixelPerInch: 96,
+ scale: 1
+ },
+ {
+ preferredResolution: {
+ resolutionWidth: 960,
+ resolutionHeight: 600
+ },
+ maxBitrate: 400000,
+ supportedFormats: [
+ { protocol: "RAW", codec: "H264" },
+ { protocol: "RTP", codec: "H264" },
+ { protocol: "RTSP", codec: "Theora" },
+ { protocol: "RTMP", codec: "VP8" },
+ { protocol: "WEBM", codec: "VP9" }
+ ],
+ hapticSpatialDataSupported: true,
+ diagonalScreenSize: 7,
+ pixelPerInch: 48,
+ scale: 2
+ },
+ {
+ preferredResolution: {
+ resolutionWidth: 960,
+ resolutionHeight: 600
+ },
+ maxBitrate: 400000,
+ supportedFormats: [
+ { protocol: "RAW", codec: "H264" },
+ { protocol: "RTP", codec: "H264" },
+ { protocol: "RTSP", codec: "Theora" },
+ { protocol: "RTMP", codec: "VP8" },
+ { protocol: "WEBM", codec: "VP9" }
+ ],
+ hapticSpatialDataSupported: true,
+ diagonalScreenSize: 7,
+ pixelPerInch: 72,
+ scale: 1.5
+ }
+ ]
}
}
@@ -538,7 +594,10 @@ function ui(state = {}, action) {
case "DOUBLE_GRAPHIC_WITH_SOFTBUTTONS":
app.displayLayout = "double-graphic-with-softbuttons"
break
- default:
+ case "NAV_FULLSCREEN_MAP":
+ app.displayLayout = "nav-fullscreen-map"
+ break
+ default:
break
}
if (action.dayColorScheme) {
@@ -613,6 +672,9 @@ function ui(state = {}, action) {
app.keyboardProperties, action.keyboardProperties);
}
return newState
+ case Actions.SET_VIDEO_STREAM_CAPABILITY:
+ app.videoStreamingCapability = action.capability
+ return newState
case Actions.DEACTIVATE_APP:
app.backSeekIndicator = {type: "TRACK", seekTime: null}
app.forwardSeekIndicator = {type: "TRACK", seekTime: null}
@@ -636,6 +698,18 @@ function system(state = {}, action) {
case Actions.SET_PTU_WITH_MODEM:
newState.ptuWithModemEnabled = action.enabled
return newState
+ case Actions.SET_VIDEO_STREAM_URL:
+ if (state.videoStreamUrl === action.url) {
+ return state;
+ }
+ newState.videoStreamUrl = action.url
+ return newState
+ case Actions.SET_VIDEO_STREAM_APP:
+ newState.videoStreamingApp = action.appID;
+ return newState;
+ case Actions.NAVIGATION_VIEW_ACTIVE:
+ newState.navigationActive = action.active
+ return newState
default:
return state
diff --git a/tools/start_server.py b/tools/start_server.py
index 5177e394a..c152d227d 100644
--- a/tools/start_server.py
+++ b/tools/start_server.py
@@ -43,16 +43,20 @@
from http.server import SimpleHTTPRequestHandler
import threading
import argparse
+import ffmpeg
+import pexpect.fdpexpect
class Flags():
"""Used to define global properties"""
FILE_SERVER_HOST = '127.0.0.1'
FILE_SERVER_PORT = 4000
FILE_SERVER_URI = 'http://127.0.0.1:4000'
+ VIDEO_SERVER_PORT = 8085
+ AUDIO_SERVER_PORT = 8086
class WebengineFileServer():
"""Used to handle routing file server requests for webengine apps.
-
+
Routes are added/removed via the add_app_mapping/remove_app_mapping functions
"""
def __init__(self, _host, _port, _uri=None):
@@ -92,13 +96,13 @@ class Handler(SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
super().end_headers()
-
+
def do_GET(self):
path_parts = self.path.split('/')
key = path_parts[1]
- file_path = '/'.join(path_parts[2:])
+ file_path = '/'.join(path_parts[2:])
- # Check if the secret key is valid
+ # Check if the secret key is valid
if key not in app_dir_mapping:
super().send_error(403, "Using invalid key %s" % key)
return
@@ -118,17 +122,17 @@ def do_GET(self):
class WSServer():
"""Used to create a Websocket Connection with the HMI.
-
+
Has a SampleRPCService class to handle incoming and outgoing messages.
"""
def __init__(self, _host, _port, _service_class=None):
self.HOST = _host
self.PORT = _port
self.service_class = _service_class if _service_class != None else WSServer.SampleRPCService
-
+
async def serve(self, _stop_func):
async with websockets.serve(self.on_connect, self.HOST, self.PORT):
- await _stop_func
+ await _stop_func
def start_server(self):
loop = asyncio.get_event_loop()
@@ -144,22 +148,22 @@ def start_server(self):
async def on_connect(self, _websocket, _path):
print('\033[1;2mClient %s connected\033[0m' % str(_websocket.remote_address))
rpc_service = self.service_class(_websocket, _path)
-
+
async for message in _websocket:
await rpc_service.on_receive(message)
-
+
class SampleRPCService():
def __init__(self, _websocket, _path):
print('\033[1;2mCreated RPC service\033[0m')
self.websocket = _websocket
self.path = _path
-
+
async def on_receive(self, _msg):
print('\033[1;2mMessage received: %s\033[0m' % _msg)
class RPCService(WSServer.SampleRPCService):
"""Used to handle receiving RPC requests and send RPC responses.
-
+
An implementation of the SampleRPCService class. RPC requests are handled in the `handle_*` functions.
"""
webengine_manager = None
@@ -174,12 +178,14 @@ def __init__(self, _websocket, _path):
"InstallApp": RPCService.webengine_manager.handle_install_app,
"GetInstalledApps": RPCService.webengine_manager.handle_get_installed_apps,
"UninstallApp": RPCService.webengine_manager.handle_uninstall_app,
+ "StartVideoStream": self.handle_start_video_stream,
+ "StartAudioStream": self.handle_start_audio_stream,
}
async def send(self, _msg):
print('\033[1;2m***Sending message: %s\033[0m' % _msg)
await self.websocket.send(_msg)
-
+
async def send_error(self, _error_msg):
err = self.gen_error_msg(_error_msg)
print(json.dumps(err))
@@ -217,7 +223,7 @@ def handle_request(self, _method_name, _params):
return self.rpc_mapping[_method_name](_method_name, _params)
def handle_get_pts_file_content(self, _method_name, _params):
-
+
if 'fileName' not in _params:
return self.gen_error_msg('Missing mandatory param \'fileName\'')
@@ -245,14 +251,14 @@ def handle_save_ptu_to_file(self, _method_name, _params):
# Validate file path
def isFileNameValid(_file_name):
- path = os.path.abspath(os.path.normpath(_file_name))
+ path = os.path.abspath(os.path.normpath(_file_name))
if os.path.commonpath([path, os.getcwd()]) != os.getcwd(): # Trying to save outside the working directory
return False
return True
-
+
if not isFileNameValid(file_name):
return self.gen_error_msg('Invalid file name: %s. Cannot save PTU' % file_name)
-
+
try:
json_file = open(file_name, 'w')
json.dump(content, json_file)
@@ -263,6 +269,54 @@ def isFileNameValid(_file_name):
"success": True
}
+ def handle_start_video_stream(self, _method, _params):
+ if 'url' not in _params:
+ return self.gen_error_msg('Missing mandatory param \'url\'')
+
+ if 'config' not in _params:
+ return self.gen_error_msg('Missing mandatory param \'config\'')
+
+ if 'webm' not in _params:
+ return self.gen_error_msg('Missing mandatory param \'webm\'')
+
+ if _params['config'].get('protocol') == 'RTP':
+ print('\033[33mSDL does not support RTP video in browser\033[0m')
+ if _params['config'].get('codec') == 'H264':
+ print('\033[1mYou may view your video with gstreamer:\033[0m')
+ print('gst-launch-1.0 souphttpsrc location=' + _params['url'] + ' ! "application/x-rtp-stream" ! rtpstreamdepay ! "application/x-rtp,media=(string)video,clock-rate=90000,encoding-name=(string)H264" ! rtph264depay ! "video/x-h264, stream-format=(string)avc, alignment=(string)au" ! avdec_h264 ! videoconvert ! ximagesink sync=false')
+
+ elif not _params['webm']:
+ print('\033[33mYour browser does not support WEBM video\033[0m')
+ if _params['config'].get('protocol') == 'RAW' and _params['config'].get('codec') == 'H264':
+ print('\033[1mYou may view your video with gstreamer:\033[0m')
+ print('gst-launch-1.0 souphttpsrc location=' + _params['url'] + ' ! decodebin ! videoconvert ! xvimagesink sync=false')
+
+ else:
+ server_endpoint = 'http://' + Flags.FILE_SERVER_HOST + ':' + str(Flags.VIDEO_SERVER_PORT)
+ ffmpeg_process = ffmpeg.input(_params['url']).output(server_endpoint, vcodec='vp8', format='webm', listen=1, multiple_requests=1).run_async(pipe_stderr=True)
+ o = pexpect.fdpexpect.fdspawn(ffmpeg_process.stderr.fileno(), logfile=sys.stdout.buffer)
+ index = o.expect(["Input", pexpect.EOF, pexpect.TIMEOUT])
+
+ if index != 0:
+ return self.gen_error_msg('Streaming data not available from SDL')
+ return { 'success': True, 'params': { 'endpoint': server_endpoint } }
+
+ return { 'success': True }
+
+ def handle_start_audio_stream(self, _method, _params):
+ if 'url' not in _params:
+ return self.gen_error_msg('Missing mandatory param \'url\'')
+
+ server_endpoint = 'http://' + Flags.FILE_SERVER_HOST + ':' + str(Flags.AUDIO_SERVER_PORT)
+ ffmpeg_process = ffmpeg.input(_params['url'], ar='16000', ac='1', f='s16le').output(server_endpoint, format="wav", listen=1, multiple_requests=1).run_async(pipe_stderr=True)
+ o = pexpect.fdpexpect.fdspawn(ffmpeg_process.stderr.fileno(), logfile=sys.stdout.buffer)
+ index = o.expect(["Input", pexpect.EOF, pexpect.TIMEOUT])
+
+ if index != 0:
+ return self.gen_error_msg('Streaming data not available from SDL')
+
+ return { 'success': True, 'params': { 'endpoint': server_endpoint } }
+
@staticmethod
def gen_error_msg(_error_msg):
return {'success': False, 'info': _error_msg}
@@ -270,7 +324,7 @@ def gen_error_msg(_error_msg):
class WebEngineManager():
"""Used to specifically handle WebEngine RPCs.
- All the `handle_*` functions parse the RPC requests and the non `handle_*` functions implement
+ All the `handle_*` functions parse the RPC requests and the non `handle_*` functions implement
the actual webengine operations(downloading the app zip, creating directories, etc.).
"""
def __init__(self):
@@ -291,7 +345,7 @@ def handle_install_app(self, _method_name, _params):
return RPCService.gen_error_msg('Missing manadatory param \'policyAppID\'')
if 'packageUrl' not in _params:
return RPCService.gen_error_msg('Missing manadatory param \'packageUrl\'')
-
+
resp = self.install_app(_params['policyAppID'], _params['packageUrl'])
return resp
@@ -344,7 +398,7 @@ def add_app_to_file_server(self, _app_storage_folder):
print('\033[2mSecret key is %s\033[0m' % (secret_key))
self.file_server.add_app_mapping(secret_key, _app_storage_folder)
return {
- "key": secret_key,
+ "key": secret_key,
"url": '%s/%s/' % (self.file_server.URI, secret_key)
}
@@ -366,10 +420,10 @@ def get_installed_apps(self):
"appKey": file_server_info['key']
}
apps_info.append({
- "policyAppID": app_id,
+ "policyAppID": app_id,
"appUrl": self.webengine_apps[app_id]['appURL']
})
-
+
return {
"success": True,
"params":{
@@ -408,7 +462,9 @@ def uninstall_app(self, _app_id):
def main():
parser = argparse.ArgumentParser(description="Handle backend operations for the hmi")
parser.add_argument('--host', type=str, required=True, help="Backend server hostname")
- parser.add_argument('--port', type=int, required=True, help="Backend server port number")
+ parser.add_argument('--ws-port', type=int, required=True, help="Backend server port number")
+ parser.add_argument('--video-port', type=int, default=8085, help="Video streaming server port number")
+ parser.add_argument('--audio-port', type=int, default=8086, help="Audio streaming server port number")
parser.add_argument('--fs-port', type=int, default=4000, help="File server port number")
parser.add_argument('--fs-uri', type=str, help="File server's URI (to be sent back to the client hmi)")
@@ -416,8 +472,10 @@ def main():
Flags.FILE_SERVER_HOST = args.host
Flags.FILE_SERVER_PORT = args.fs_port
Flags.FILE_SERVER_URI = args.fs_uri if args.fs_uri else 'http://%s:%s' % (Flags.FILE_SERVER_HOST, Flags.FILE_SERVER_PORT)
+ Flags.VIDEO_SERVER_PORT = args.video_port
+ Flags.AUDIO_SERVER_PORT = args.audio_port
- backend_server = WSServer(args.host, args.port, RPCService)
+ backend_server = WSServer(args.host, args.ws_port, RPCService)
print('Starting server')
backend_server.start_server()