{
17 | let newList: any[] = [];
18 | newList.push(registryAddress)
19 | return newList;
20 | }
21 |
22 | export default getDevices
--------------------------------------------------------------------------------
/Development/src/components/useDebounce.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useRef, useState } from 'react';
2 | import debounce from 'lodash/debounce';
3 |
4 | export const useDebouncedCallback = (callback, delay) =>
5 | useMemo(() => debounce(callback, delay), [callback, delay]);
6 |
7 | const useDebounce = (value, delay) => {
8 | const previousValue = useRef(value);
9 | const [currentValue, setCurrentValue] = useState(value);
10 | const debouncedCallback = useDebouncedCallback(
11 | value => setCurrentValue(value),
12 | delay
13 | );
14 | useEffect(() => {
15 | if (value !== previousValue.current) {
16 | debouncedCallback(value);
17 | previousValue.current = value;
18 | }
19 | }, [debouncedCallback, value]);
20 | return currentValue;
21 | };
22 |
23 | export default useDebounce;
24 |
--------------------------------------------------------------------------------
/is12-client/src/client/DevApp.css:
--------------------------------------------------------------------------------
1 | .tree_wrap {
2 | justify-self: center;
3 | width: 99vw;
4 | height: 90vh;
5 | border: 2px outset black;
6 | background-color: #333333;
7 | }
8 |
9 | .node__root > circle {
10 | fill: rgb(164, 1, 246);
11 | }
12 |
13 | .node__branch > circle {
14 | fill: rgb(99, 1, 246);
15 | }
16 |
17 | .node__leaf > circle {
18 | fill: rgb(102, 150, 255);
19 | }
20 |
21 | .rd3t-label {
22 | outline-style: groove;
23 | outline-color: rgba(148, 148, 148, 0.57);
24 | fill: #d9d9d9;
25 | }
26 |
27 | .rd3t-label > text {
28 | fill: #b0aeae;
29 | }
30 |
31 | .node__branch > g > text > tspan {
32 | text-anchor: middle;
33 | alignment-baseline: central;
34 | font-size: 0.8rem;
35 | }
36 |
37 | .rd3t-label > .rd3t-label__title {
38 | fill: #62c1ce;
39 | text-decoration: underline;
40 | }
--------------------------------------------------------------------------------
/is12-client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
16 |
17 |
18 | NCA Control Client
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Development/src/components/ConnectionEditActions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { ShowButton, TopToolbar } from 'react-admin';
4 | import { useTheme } from '@material-ui/styles';
5 |
6 | export default function ConnectionEditActions({ basePath, id }) {
7 | const theme = useTheme();
8 | return (
9 |
20 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/Development/src/components/CollapseButton.js:
--------------------------------------------------------------------------------
1 | import { IconButton } from '@material-ui/core';
2 | import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
3 | import KeyboardArrowLeftIcon from '@material-ui/icons/KeyboardArrowLeft';
4 | import KeyboardArrowRightIcon from '@material-ui/icons/KeyboardArrowRight';
5 | import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
6 |
7 | const CollapseButton = ({
8 | onClick,
9 | isExpanded,
10 | direction = 'vertical',
11 | title,
12 | }) => (
13 |
14 | {direction === 'horizontal' ? (
15 | isExpanded ? (
16 |
17 | ) : (
18 |
19 | )
20 | ) : isExpanded ? (
21 |
22 | ) : (
23 |
24 | )}
25 |
26 | );
27 |
28 | export default CollapseButton;
29 |
--------------------------------------------------------------------------------
/Development/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "react-app",
5 | "plugin:prettier/recommended",
6 | "prettier"
7 | ],
8 | "plugins": [
9 | "@typescript-eslint",
10 | "import",
11 | "jsx-a11y",
12 | "prettier",
13 | "react",
14 | "react-hooks"
15 | ],
16 | "parserOptions": {
17 | "ecmaVersion": 6,
18 | "ecmaFeatures": {
19 | "jsx": true
20 | }
21 | },
22 | "rules": {
23 | "eqeqeq": "off",
24 | "no-console": "off",
25 | "sort-imports": [
26 | "error",
27 | {
28 | "ignoreCase": false,
29 | "ignoreDeclarationSort": true,
30 | "ignoreMemberSort": false,
31 | "memberSyntaxSortOrder": [ "none", "all", "multiple", "single" ]
32 | }
33 | ],
34 | "no-var": "warn",
35 | "eol-last": [ "error", "always" ],
36 | "no-use-before-define": "off",
37 | "prettier/prettier": "error"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Development/src/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "nmos-js",
3 | "about": "An NMOS Client",
4 | "palette": {
5 | "primary": {
6 | "main": "rgb(45,117,199)",
7 | "contrastText": "#fff"
8 | },
9 | "secondary": {
10 | "main": "rgb(0,47,103)",
11 | "contrastText": "#fff"
12 | }
13 | },
14 | "Query API": {
15 | },
16 | "DNS-SD API": {
17 | },
18 | "Logging API": {
19 | },
20 | "RQL": {
21 | "value": true,
22 | "disabled": false
23 | },
24 | "Paging Limit": {
25 | },
26 | "Friendly Parameters": {
27 | "value": true
28 | },
29 | "Connect Filter": {
30 | "value": {
31 | "$constraint_sets_active": null
32 | }
33 | },
34 | "refresh time": {
35 | "value": 4
36 | },
37 | "theme": {
38 | "value": {
39 | "type": "light"
40 | }
41 | },
42 | "Auth Enabled": {
43 | "value": false
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Development/src/icons/JsonIcon.js:
--------------------------------------------------------------------------------
1 | let _interopRequireDefault = require('@babel/runtime/helpers/interopRequireDefault');
2 |
3 | Object.defineProperty(exports, '__esModule', {
4 | value: true,
5 | });
6 | exports.default = void 0;
7 |
8 | const _react = _interopRequireDefault(require('react'));
9 |
10 | const _createSvgIcon = _interopRequireDefault(
11 | require('@material-ui/icons/utils/createSvgIcon')
12 | );
13 |
14 | const _default = (0, _createSvgIcon.default)(
15 | _react.default.createElement(
16 | 'g',
17 | null,
18 | _react.default.createElement(
19 | 'style',
20 | null,
21 | '.txt { font-size: 14px; font-family: monospace; }'
22 | ),
23 | _react.default.createElement(
24 | 'text',
25 | {
26 | x: 0,
27 | y: 15,
28 | className: 'txt',
29 | },
30 | '{\u2026}'
31 | )
32 | ),
33 | 'json'
34 | );
35 |
36 | exports.default = _default;
37 |
--------------------------------------------------------------------------------
/Development/src/components/MappingButton.js:
--------------------------------------------------------------------------------
1 | import { IconButton, withStyles } from '@material-ui/core';
2 | import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline';
3 | import RadioButtonUncheckedIcon from '@material-ui/icons/RadioButtonUnchecked';
4 |
5 | // de-emphasize the unchecked state
6 | const faded = { opacity: 0.3 };
7 |
8 | const styles = {
9 | unchecked: faded,
10 | checked: {},
11 | };
12 |
13 | // filter out our classes to avoid the Material-UI console warning
14 | const MappingButton = ({
15 | checked,
16 | classes: {
17 | checked: checkedClass,
18 | unchecked: uncheckedClass,
19 | ...inheritedClasses
20 | },
21 | ...props
22 | }) => (
23 |
28 | {checked ? : }
29 |
30 | );
31 |
32 | export default withStyles(styles)(MappingButton);
33 |
--------------------------------------------------------------------------------
/is12-client/README.md:
--------------------------------------------------------------------------------
1 | # IS-12 Device Model Browser
2 |
3 | *A prototype controller for use with IS-12 capable NMOS Nodes*
4 |
5 | ## Overview
6 |
7 | The program is a controller for IS-12 capable NMOS Nodes such as the [NMOS Device Control Mock](https://github.com/AMWA-TV/nmos-device-control-mock).
8 |
9 |
10 |
11 | ## Installation
12 |
13 | ```bash
14 | yarn [install]
15 | ```
16 |
17 | ## Requirements
18 |
19 | ***
20 | - Node.js (install with chocolaty to get all dependencies)
21 | - Python 3.10+
22 |
23 |
24 |
25 | ## Usage
26 |
27 | ***
28 | - On launch there is an edit box where the URL of the control protocol endpoint can be set.
29 | - For example, `ws://127.0.0.1:7002/x-nmos/ncp/v1.0`
30 | - Click the `CONNECT` button to connect to the control protocol endpoint.
31 |
32 | ### Running:
33 |
34 | - With program recompiling every time you save changes
35 |
36 | ```bash
37 | yarn start
38 | ```
39 |
40 | - Alternatively, to simply build and run without effect from changes:
41 |
42 | ```bash
43 | yarn build-and-start
44 | ```
45 |
46 |
--------------------------------------------------------------------------------
/Development/src/components/labelize.js:
--------------------------------------------------------------------------------
1 | // some abbreviations used in IS-04 and IS-05 APIs
2 | // or the nmos-cpp Logging API
3 | const abbreviations = {
4 | api: 'API',
5 | caps: 'Capabilities',
6 | ext: '(Ext)',
7 | fec: 'FEC',
8 | fec1d: 'FEC1D',
9 | fec2d: 'FEC2D',
10 | href: 'Address',
11 | http: 'HTTP',
12 | id: 'ID',
13 | ip: 'IP',
14 | is: 'IS',
15 | mqtt: 'MQTT',
16 | ms: '(ms)',
17 | ptp: 'PTP',
18 | rest: 'REST',
19 | rtcp: 'RTCP',
20 | rtp: 'RTP',
21 | uri: 'URI',
22 | url: 'URL',
23 | ws: 'WebSocket',
24 | };
25 |
26 | const labelize = source => {
27 | // '/meow.$purr.hiss_yowl_ms' => 'Meow Purr Hiss Yowl (ms)'
28 | const label = source
29 | .replace(/[ /$._]+/g, ' ')
30 | .trim()
31 | .replace(
32 | /\S+/g,
33 | word =>
34 | abbreviations[word.toLowerCase()] ||
35 | word.charAt(0).toUpperCase() + word.substr(1).toLowerCase()
36 | );
37 | return label;
38 | };
39 |
40 | export default labelize;
41 |
--------------------------------------------------------------------------------
/Development/src/LoginButton.js:
--------------------------------------------------------------------------------
1 | // in src/LoginButton.js
2 | import * as React from 'react';
3 | import { forwardRef } from 'react';
4 | import { useLogin, useNotify } from 'react-admin';
5 | import Button from '@material-ui/core/Button';
6 | import AccountCircleIcon from '@material-ui/icons/AccountCircle';
7 |
8 | const LoginButton = forwardRef((props, ref) => {
9 | const login = useLogin();
10 | const notify = useNotify();
11 |
12 | const handleClick = () => {
13 | login().catch(error => {
14 | notify(
15 | typeof error === 'string'
16 | ? error
17 | : 'Authentication error: ' + error.message,
18 | 'error'
19 | );
20 | });
21 | };
22 |
23 | return (
24 |
33 | );
34 | });
35 |
36 | export default LoginButton;
37 |
--------------------------------------------------------------------------------
/Documents/Getting-Started.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | The following instructions describe how to build and deploy this software.
4 |
5 | ## Preparation
6 |
7 | Set up the [external dependencies](Dependencies.md#preparation) before proceeding.
8 |
9 | Once packages are installed, run `yarn start` to launch the client on a development server at http://localhost:3000.
10 |
11 | To build for release, simply run `yarn build`. The output will be written to a build directory. Instructions on how to serve the build will be provided at this point.
12 |
13 | ESLint and Prettier can be run using `yarn lint`. If you wish to list the warnings without writing changes `yarn lint-check` can be used instead.
14 |
15 | ## Run as a Docker container
16 |
17 | At this point in time, a prebuilt container is not provided. Luckily it is easy to build it from source!
18 | You will need to have git installed as well as [Docker](https://docs.docker.com/install/).
19 |
20 | ```bash
21 | git clone https://github.com/sony/nmos-js
22 | cd nmos-js
23 | docker build -t nmos-js Development
24 | ```
25 |
26 | Start the Docker container by binding nmos-js to an external port.
27 |
28 | ```bash
29 | docker run -d --name=nmos-js -p 80:80 nmos-js
30 | ```
31 |
--------------------------------------------------------------------------------
/Development/src/components/ListActions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, CreateButton, TopToolbar } from 'react-admin';
3 | import { useTheme } from '@material-ui/styles';
4 | import JsonIcon from '../icons/JsonIcon';
5 |
6 | const ListActions = ({ basePath, hasCreate, url }) => {
7 | const theme = useTheme();
8 | return (
9 |
20 | {url && (
21 |
30 | )}
31 | {hasCreate && }
32 |
33 | );
34 | };
35 |
36 | export default ListActions;
37 |
--------------------------------------------------------------------------------
/Development/src/icons/index.js:
--------------------------------------------------------------------------------
1 | import ActivateImmediateIcon from './ActivateImmediate';
2 | import ActivateScheduledIcon from './ActivateScheduled';
3 | import CancelScheduledActivationIcon from './CancelScheduledActivation';
4 | import ConnectRegistryIcon from './ConnectRegistry';
5 | import ContentCopyIcon from './ContentCopy';
6 | import DeviceIcon from './Device';
7 | import FlowIcon from './Flow';
8 | import JsonIcon from './JsonIcon';
9 | import NodeIcon from './Node';
10 | import ReceiverIcon from './Receiver';
11 | import RegistryIcon from './Registry';
12 | import RegistryLogsIcon from './RegistryLogs';
13 | import SenderIcon from './Sender';
14 | import SourceIcon from './Source';
15 | import StageIcon from './Stage';
16 | import SubscriptionIcon from './Subscription';
17 |
18 | // To optimise the SVG files and convert to the correct syntax:
19 | // svgo --enable=removeDimensions --disable=removeViewBox --pretty -f .\src\ -o .\optimised\
20 | // npx @svgr/cli -d ./out/ --template ./svgr-template.js ./optimised/
21 |
22 | export {
23 | ActivateImmediateIcon,
24 | ActivateScheduledIcon,
25 | CancelScheduledActivationIcon,
26 | ConnectRegistryIcon,
27 | ContentCopyIcon,
28 | DeviceIcon,
29 | FlowIcon,
30 | JsonIcon,
31 | NodeIcon,
32 | ReceiverIcon,
33 | RegistryIcon,
34 | RegistryLogsIcon,
35 | SenderIcon,
36 | SourceIcon,
37 | StageIcon,
38 | SubscriptionIcon,
39 | };
40 |
--------------------------------------------------------------------------------
/Development/src/components/TAIField.js:
--------------------------------------------------------------------------------
1 | import { Tooltip } from '@material-ui/core';
2 | import HintTypography from './HintTypography';
3 | import tai from 't-a-i';
4 | import dayjs from 'dayjs';
5 | import utc from 'dayjs/plugin/utc';
6 | import get from 'lodash/get';
7 | dayjs.extend(utc);
8 |
9 | const TAIField = ({ record, source, mode }) => (
10 |
11 |
17 |
18 | {get(record, source)}
19 |
20 |
21 |
22 | );
23 |
24 | const TAIConversion = (record, source, mode) => {
25 | try {
26 | const taiData = get(record, source).split(':', 2);
27 | if (!taiData[0] || !taiData[1]) return '';
28 | const taiTimeMilliseconds = 1e3 * taiData[0] + taiData[1] / 1e6;
29 | if (get(record, mode) === 'activate_scheduled_relative') {
30 | return dayjs(taiTimeMilliseconds).utc().format('HH:mm:ss.SSS');
31 | }
32 | const unixTimeMilliseconds = tai.atomicToUnix(taiTimeMilliseconds);
33 | return dayjs(unixTimeMilliseconds).format('YYYY-MM-DD HH:mm:ss.SSS Z');
34 | } catch (e) {
35 | return `Conversion Error - ${e}`;
36 | }
37 | };
38 |
39 | TAIField.defaultProps = {
40 | addLabel: true,
41 | };
42 |
43 | export default TAIField;
44 |
--------------------------------------------------------------------------------
/Development/src/components/PaginationButtons.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | ChevronLeft,
4 | ChevronRight,
5 | FirstPage,
6 | LastPage,
7 | } from '@material-ui/icons';
8 | import { Button } from '@material-ui/core';
9 | import { includes, keys } from 'lodash';
10 |
11 | const components = {
12 | prev: ChevronLeft,
13 | next: ChevronRight,
14 | last: LastPage,
15 | first: FirstPage,
16 | };
17 |
18 | export const PaginationButton = ({
19 | pagination,
20 | disabled,
21 | nextPage,
22 | rel,
23 | label = rel,
24 | }) => {
25 | rel.toLowerCase();
26 | const buttons = keys(pagination);
27 |
28 | const enabled = (() => {
29 | if (disabled) return false;
30 | return includes(buttons, rel);
31 | })();
32 |
33 | const getIcon = label => {
34 | const ButtonIcon = components[label];
35 | return ;
36 | };
37 |
38 | return (
39 |
43 | );
44 | };
45 |
46 | const PaginationButtons = props => (
47 | <>
48 |
49 |
50 |
51 |
52 | >
53 | );
54 |
55 | export default PaginationButtons;
56 |
--------------------------------------------------------------------------------
/Development/src/components/ObjectField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Table,
4 | TableBody,
5 | TableCell,
6 | TableHead,
7 | TableRow,
8 | } from '@material-ui/core';
9 | import { get, isEmpty, map } from 'lodash';
10 | import { Parameter } from './ParameterRegisters';
11 |
12 | export const ObjectField = ({ register, record, source }) =>
13 | // no table at all for empty objects
14 | !isEmpty(get(record, source)) && (
15 |
16 |
17 |
18 | Name
19 | Value(s)
20 |
21 |
22 |
23 | {map(get(record, source), (value, key) => (
24 |
25 |
26 |
27 |
28 | {Array.isArray(value) ? (
29 | {value.join(', ')}
30 | ) : (
31 | {value}
32 | )}
33 |
34 | ))}
35 |
36 |
37 | );
38 |
39 | ObjectField.defaultProps = {
40 | addLabel: true,
41 | };
42 |
43 | export default ObjectField;
44 |
--------------------------------------------------------------------------------
/Development/src/components/TransportFileViewer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, CardContent, IconButton, Typography } from '@material-ui/core';
3 | import { Labeled, useNotify } from 'react-admin';
4 | import copy from 'clipboard-copy';
5 | import get from 'lodash/get';
6 | import { ContentCopyIcon } from '../icons';
7 |
8 | const TransportFileViewer = ({ endpoint, ...props }) => {
9 | const notify = useNotify();
10 | const handleCopy = () => {
11 | copy(get(props.record, `${endpoint}`)).then(() => {
12 | notify('Transport file copied');
13 | });
14 | };
15 |
16 | if (!get(props.record, `${endpoint}`)) {
17 | return null;
18 | }
19 |
20 | return (
21 |
22 |
23 |
24 |
29 |
30 |
31 |
32 |
33 | {get(props.record, `${endpoint}`)}
34 |
35 |
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default TransportFileViewer;
43 |
--------------------------------------------------------------------------------
/is12-client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ui_blubber",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@emotion/react": "^11.10.4",
7 | "@emotion/styled": "^11.10.4",
8 | "@mui/icons-material": "^5.10.9",
9 | "@mui/lab": "^5.0.0-alpha.103",
10 | "@mui/material": "^5.10.9",
11 | "@mui/styled-engine-sc": "^5.10.6",
12 | "@mui/x-data-grid-pro": "^5.17.6",
13 | "@testing-library/react": "^13.4.0",
14 | "@testing-library/user-event": "^13.5.0",
15 | "babel-core": "^6.26.3",
16 | "chart.js": "^4.0.1",
17 | "prop-types": "^15.8.1",
18 | "react": "^18.3.1",
19 | "react-chartjs-2": "^5.0.1",
20 | "react-cookie": "^4.1.1",
21 | "react-d3-tree": "^3.3.4",
22 | "react-dom": "^18.2.0",
23 | "react-query": "^3.39.2",
24 | "react-scripts": "^5.0.1",
25 | "styled-components": "^5.3.6"
26 | },
27 | "scripts": {
28 | "start": "react-scripts start",
29 | "build": "react-scripts build",
30 | "test": "react-scripts test",
31 | "eject": "react-scripts eject"
32 | },
33 | "eslintConfig": {
34 | "extends": [
35 | "react-app",
36 | "react-app/jest"
37 | ]
38 | },
39 | "browserslist": {
40 | "production": [
41 | ">0.2%",
42 | "not dead",
43 | "not op_mini all"
44 | ],
45 | "development": [
46 | "last 1 opera version",
47 | "last 1 chrome version",
48 | "last 1 firefox version",
49 | "last 1 safari version"
50 | ]
51 | },
52 | "devDependencies": {
53 | "react-router-dom": "^6.4.2",
54 | "typescript": "^4.8.4"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Development/src/icons/Receiver.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgReceiver = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgReceiver;
17 |
--------------------------------------------------------------------------------
/Development/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
21 |
22 |
23 |
26 |
27 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Development/src/icons/Sender.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgSender = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgSender;
17 |
--------------------------------------------------------------------------------
/Development/src/components/ConstraintField.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import get from 'lodash/get';
3 | import { Typography } from '@material-ui/core';
4 |
5 | const renderConstraintValue = value => {
6 | return value.numerator
7 | ? `${value.numerator} : ${value.denominator ? value.denominator : 1}`
8 | : value;
9 | };
10 |
11 | const ConstraintField = ({ record, source }) => {
12 | const constraint = get(record, source);
13 | const minConstraint = get(constraint, 'minimum');
14 | const maxConstraint = get(constraint, 'maximum');
15 | const enumConstraint = get(constraint, 'enum');
16 | return (
17 | <>
18 | {(minConstraint != null || maxConstraint != null) && (
19 |
20 | {minConstraint != null && (
21 |
22 | {renderConstraintValue(minConstraint)}
23 |
24 | )}
25 | –
26 | {maxConstraint != null && (
27 |
28 | {renderConstraintValue(maxConstraint)}
29 |
30 | )}
31 |
32 | )}
33 | {enumConstraint != null && (
34 |
35 | {enumConstraint
36 | .map(item => renderConstraintValue(item))
37 | .join(', ')}
38 |
39 | )}
40 | >
41 | );
42 | };
43 |
44 | ConstraintField.defaultProps = {
45 | addLabel: true,
46 | };
47 |
48 | export default ConstraintField;
49 |
--------------------------------------------------------------------------------
/Development/src/components/MappingShowActions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, ListButton, TopToolbar, useRecordContext } from 'react-admin';
3 | import JsonIcon from '../icons/JsonIcon';
4 | import { useTheme } from '@material-ui/styles';
5 | import { concatUrl } from '../settings';
6 | import { resourceUrl } from '../dataProvider';
7 |
8 | // cf. ResourceShowActions
9 | export default function MappingShowActions({ basePath, id, resource }) {
10 | const { record } = useRecordContext();
11 | let json_href;
12 | const theme = useTheme();
13 | if (record) {
14 | const tab = window.location.href.split('/').pop();
15 | if (tab === 'active_map' && record.$channelmappingAPI) {
16 | json_href = concatUrl(record.$channelmappingAPI, '/map/active');
17 | } else {
18 | json_href = resourceUrl(resource, `/${id}`);
19 | }
20 | }
21 | return (
22 |
33 | {json_href ? (
34 |
42 | ) : null}
43 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/Development/src/components/ConnectionEditToolbar.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useFormState } from 'react-final-form';
3 | import { Button } from '@material-ui/core';
4 | import { Toolbar } from 'react-admin';
5 | import get from 'lodash/get';
6 | import {
7 | ActivateImmediateIcon,
8 | ActivateScheduledIcon,
9 | CancelScheduledActivationIcon,
10 | StageIcon,
11 | } from '../icons';
12 |
13 | const ConnectionEditToolbar = ({ handleSubmitWithRedirect }) => {
14 | const formState = useFormState().values;
15 | const buttonProps = (() => {
16 | if (get(formState, '$staged.activation.activation_time')) {
17 | return [
18 | 'Cancel Scheduled Activation',
19 | ,
20 | ];
21 | }
22 | switch (get(formState, '$staged.activation.mode')) {
23 | case 'activate_immediate':
24 | return ['Activate', ];
25 | case 'activate_scheduled_relative':
26 | case 'activate_scheduled_absolute':
27 | return ['Activate Scheduled', ];
28 | default:
29 | return ['Stage', ];
30 | }
31 | })();
32 | return (
33 |
34 | <>
35 |
47 | >
48 |
49 | );
50 | };
51 |
52 | export default ConnectionEditToolbar;
53 |
--------------------------------------------------------------------------------
/Development/src/pages/subscriptions/SubscriptionsCreate.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | BooleanInput,
4 | Create,
5 | ListButton,
6 | NumberInput,
7 | SelectInput,
8 | SimpleForm,
9 | Toolbar,
10 | TopToolbar,
11 | } from 'react-admin';
12 | import ObjectInput from '../../components/ObjectInput';
13 | import RawButton from '../../components/RawButton';
14 |
15 | const SubscriptionsCreateActions = ({ basePath, data, resource }) => (
16 |
17 | {data ? : null}
18 |
19 |
20 | );
21 |
22 | const SubscriptionsCreate = props => (
23 | } {...props}>
24 | }
26 | redirect="show"
27 | >
28 | value}
42 | />
43 |
48 |
49 |
50 |
51 |
52 | );
53 |
54 | export default SubscriptionsCreate;
55 |
--------------------------------------------------------------------------------
/TestingFacade/README.md:
--------------------------------------------------------------------------------
1 | # NMOS Controller testing - Fully Automated testing of nmos-js
2 |
3 | ## Installation and usage
4 |
5 | 1. Install flask and selenium
6 | `pip install -r requirements.txt`
7 |
8 | 2. Install the webdriver for the browser you wish to use. [See selenium docs for more info.](https://www.selenium.dev/documentation/en/webdriver/driver_requirements/#quick-reference)
9 |
10 | 3. Update the configuration file `Config.py` if necessary.
11 | - `NCUT_URL` the url of your instance of nmos-js
12 | - `MOCK_REGISTRY_URL` the url of the mock registry set up by the NMOS Controller test suite, included in the `pre_tests_message`
13 | - `BROWSER` the name of the browser for which you installed the driver in step 2
14 |
15 | 4. Run the NMOS Testing tool (https://github.com/AMWA-TV/nmos-testing) and choose a Controller test suite
16 |
17 | 5. Run TestingFacade.py with your chosen test suite specified using the `--suite` command line argument. Eg. `python3 TestingFacade.py --suite 'IS0404'`.
18 | Currently supported test suites are:
19 | - IS0404
20 | - IS0503
21 |
22 | 6. On your NMOS Testing instance enter the IP address and Port where the Automated Testing Facade is running
23 |
24 | 7. Choose tests and click Run. The Testing Facade will launch a new headless browser at the beginning of each test and close it at the end. Note: Set the value of `HEADLESS` in `Config.py` to `False` to have the tests run in visible browser windows
25 |
26 | 8. Test suite `POST`s the Question JSON to the TestingFacade API endpoint `/x-nmos/testquestion/{version}`. TestingFacade will retrieve the data and run the relevant set of selenium instructions defined in the test suite file to complete the test in your chosen browser then `POST`s the Answer JSON back to the test suite via the endpoint given in the `answer_uri` of the Question
27 |
28 | 9. After the last test, test suite will `POST` a clear request to empty the data store
29 |
30 | 10. Results are displayed on NMOS Testing tool
31 |
--------------------------------------------------------------------------------
/Development/src/icons/Node.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgNode = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgNode;
17 |
--------------------------------------------------------------------------------
/Development/src/pages/about.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, CardContent, Typography } from '@material-ui/core';
3 | import { get } from 'lodash';
4 | import { Title } from 'react-admin';
5 | import CONFIG from '../config.json';
6 | import AppLogo from '../components/AppLogo';
7 |
8 | export const About = () => (
9 |
10 |
11 |
12 |
13 |
14 | {get(CONFIG, 'about', 'An NMOS Client')}
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 |
23 | export const RepoLinks = () => (
24 |
25 |
26 | Built on open-source projects
27 | {' '}
28 |
29 |
30 | nmos-js
31 |
32 | {' '}
33 |
34 | and
35 | {' '}
36 |
37 |
38 | nmos-cpp
39 |
40 |
41 |
42 | );
43 |
44 | const Link = ({ to, children }) => (
45 | // eslint-disable-next-line react/jsx-no-target-blank
46 |
52 | {React.cloneElement(children, {
53 | style: { textDecoration: 'underline' },
54 | })}
55 |
56 | );
57 |
58 | export default About;
59 |
--------------------------------------------------------------------------------
/Development/src/pages/logs/LogsShow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | ShowContextProvider,
4 | ShowView,
5 | SimpleShowLayout,
6 | TextField,
7 | useRecordContext,
8 | useShowController,
9 | } from 'react-admin';
10 | import get from 'lodash/get';
11 | import ObjectField from '../../components/ObjectField';
12 | import ResourceShowActions from '../../components/ResourceShowActions';
13 | import ResourceTitle from '../../components/ResourceTitle';
14 | import SanitizedDivider from '../../components/SanitizedDivider';
15 |
16 | export const LogsShow = props => {
17 | const controllerProps = useShowController(props);
18 | return (
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | const LogsShowView = props => {
26 | const { record } = useRecordContext();
27 | return (
28 | }
30 | actions={}
31 | {...props}
32 | >
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export default LogsShow;
57 |
--------------------------------------------------------------------------------
/Development/src/theme/ThemeContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ThemeProvider } from '@material-ui/styles';
3 | import { createMuiTheme, responsiveFontSizes } from '@material-ui/core/styles';
4 | import { get } from 'lodash';
5 | import CONFIG from '../config.json';
6 | import { disabledSetting, useJSONSetting } from '../settings';
7 |
8 | export const ThemeContext = React.createContext({
9 | theme: 'light',
10 | toggleTheme: () => {},
11 | });
12 |
13 | export const AppThemeProvider = ({ children }) => {
14 | const themePalette = get(CONFIG, 'palette', {
15 | primary: {
16 | main: 'rgb(45,117,199)',
17 | contrastText: '#fff',
18 | },
19 | secondary: {
20 | main: 'rgb(0,47,103)',
21 | contrastText: '#fff',
22 | },
23 | });
24 | const [themeState, setThemeState] = useJSONSetting('theme', {
25 | type: 'light',
26 | });
27 |
28 | const theme = responsiveFontSizes(
29 | createMuiTheme({
30 | palette: {
31 | ...themePalette,
32 | type: themeState.type,
33 | },
34 | sidebar: {
35 | width: 240,
36 | closedWidth: 72,
37 | },
38 | overrides: {
39 | MuiTableCell: {
40 | sizeSmall: {
41 | '&:last-child': {
42 | paddingRight: null,
43 | },
44 | },
45 | },
46 | RaReferenceField: {
47 | link: {
48 | color: null,
49 | textDecoration: null,
50 | },
51 | },
52 | },
53 | })
54 | );
55 |
56 | const toggleTheme = () => {
57 | if (disabledSetting('theme')) return;
58 | const toggledType = themeState.type === 'light' ? 'dark' : 'light';
59 | setThemeState({ type: toggledType });
60 | };
61 |
62 | return (
63 |
64 |
65 | {React.cloneElement(children, { theme })}
66 |
67 |
68 | );
69 | };
70 |
71 | export default ThemeContext;
72 |
--------------------------------------------------------------------------------
/Development/src/icons/ActivateScheduled.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgActivateScheduled = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgActivateScheduled;
17 |
--------------------------------------------------------------------------------
/Development/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nmos-js",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": ".",
6 | "dependencies": {
7 | "@material-ui/core": "^4.3.3",
8 | "@material-ui/icons": "^4.2.1",
9 | "@material-ui/lab": "^4.0.0-alpha.57",
10 | "@material-ui/styles": "^4.3.3",
11 | "clipboard-copy": "^4.0.1",
12 | "crypto-js": "^4.0.0",
13 | "dayjs": "^1.8.23",
14 | "deep-diff": "^1.0.2",
15 | "final-form": "^4.18.5",
16 | "history": "^4.7.2",
17 | "immutable": "^3.8.1 || ^4.0.0-rc.1",
18 | "json-ptr": "^3.1.1",
19 | "jwt-decode": "^4.0.0",
20 | "lodash": "~4.17.21",
21 | "prop-types": "^15.6.2",
22 | "react": "^17.0.1",
23 | "react-admin": "^3.11.3",
24 | "react-dom": "^17.0.1",
25 | "react-final-form": "^6.3.3",
26 | "react-redux": "^7.1.0",
27 | "react-router-dom": "^5.1.0",
28 | "react-scripts": "^5.0.1",
29 | "redux": "^3.7.2 || ^4.0.3",
30 | "seamless-immutable": "^7.1.3",
31 | "t-a-i": "^1.0.15"
32 | },
33 | "scripts": {
34 | "start": "react-scripts start",
35 | "build": "react-scripts build",
36 | "test": "react-scripts test --env=jsdom",
37 | "eject": "react-scripts eject",
38 | "lint": "eslint --fix ./ && prettier --config .prettierrc.js --write src/**/*.js",
39 | "lint-check": "eslint --fix-dry-run ./ && prettier --config .prettierrc.js --check src/**/*.js"
40 | },
41 | "devDependencies": {
42 | "@babel/core": "^7.22.10",
43 | "@babel/eslint-parser": "^7.22.10",
44 | "@babel/plugin-proposal-private-property-in-object": "^7.21.0",
45 | "@babel/plugin-syntax-flow": "^7.14.5",
46 | "@babel/plugin-transform-react-jsx": "^7.14.9",
47 | "eslint": "^8.47.0",
48 | "eslint-config-prettier": "^9.0.0",
49 | "eslint-config-react-app": "^7.0.1",
50 | "eslint-plugin-flowtype": "^8.0.3",
51 | "eslint-plugin-import": "^2.28.0",
52 | "eslint-plugin-jsx-a11y": "^6.7.1",
53 | "eslint-plugin-prettier": "^5.0.0",
54 | "eslint-plugin-react": "^7.33.2",
55 | "eslint-plugin-react-hooks": "^4.6.0",
56 | "prettier": "^3.6.2",
57 | "typescript": "^4.1.6"
58 | },
59 | "browserslist": {
60 | "production": [
61 | ">0.2%",
62 | "not dead",
63 | "not op_mini all"
64 | ],
65 | "development": [
66 | "last 1 chrome version",
67 | "last 1 firefox version",
68 | "last 1 safari version"
69 | ]
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Development/src/components/ConnectionShowActions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NavLink } from 'react-router-dom';
3 | import { Button, ListButton, TopToolbar, useRecordContext } from 'react-admin';
4 | import get from 'lodash/get';
5 | import EditIcon from '@material-ui/icons/Edit';
6 | import JsonIcon from '../icons/JsonIcon';
7 | import { useTheme } from '@material-ui/styles';
8 | import { concatUrl } from '../settings';
9 | import { resourceUrl } from '../dataProvider';
10 |
11 | // cf. ResourceShowActions
12 | export default function ConnectionShowActions({ basePath, id, resource }) {
13 | const { record } = useRecordContext();
14 |
15 | let json_href;
16 | if (record) {
17 | const tab = window.location.href.split('/').pop();
18 | if (tab === 'active' || tab === 'staged' || tab === 'transportfile') {
19 | json_href = concatUrl(record.$connectionAPI, `/${tab}`);
20 | } else if (tab === 'connect') {
21 | } else {
22 | json_href = resourceUrl(resource, `/${id}`);
23 | }
24 | }
25 | const theme = useTheme();
26 | return (
27 |
38 | {json_href ? (
39 |
47 | ) : null}
48 |
53 | {get(record, '$connectionAPI') != null ? (
54 |
61 | ) : null}
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/Development/src/icons/ActivateImmediate.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgActivateImmediate = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgActivateImmediate;
17 |
--------------------------------------------------------------------------------
/Development/src/icons/CancelScheduledActivation.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgCancelScheduledActivation = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgCancelScheduledActivation;
17 |
--------------------------------------------------------------------------------
/Development/src/pages/subscriptions/SubscriptionsShow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | BooleanField,
4 | ShowContextProvider,
5 | ShowView,
6 | SimpleShowLayout,
7 | TextField,
8 | Toolbar,
9 | useRecordContext,
10 | useShowController,
11 | } from 'react-admin';
12 | import get from 'lodash/get';
13 | import DeleteButton from '../../components/DeleteButton';
14 | import ObjectField from '../../components/ObjectField';
15 | import ResourceShowActions from '../../components/ResourceShowActions';
16 | import ResourceTitle from '../../components/ResourceTitle';
17 | import SanitizedDivider from '../../components/SanitizedDivider';
18 | import UrlField from '../../components/URLField';
19 | import { queryVersion } from '../../settings';
20 |
21 | export const SubscriptionsShow = props => {
22 | const controllerProps = useShowController(props);
23 | return (
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | const SubscriptionsShowView = props => {
31 | const { record } = useRecordContext();
32 | return (
33 | <>
34 | }
37 | actions={}
38 | >
39 |
40 |
41 |
42 |
43 |
47 |
48 |
49 |
50 |
51 | {queryVersion() >= 'v1.3' && (
52 |
53 | )}
54 |
55 |
56 | {get(record, 'id') && (
57 | // Toolbar will override the DeleteButton resource prop
58 |
59 |
60 |
61 | )}
62 | >
63 | );
64 | };
65 |
66 | export default SubscriptionsShow;
67 |
--------------------------------------------------------------------------------
/TestingFacade/DataStore.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2021 Advanced Media Workflow Association
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | class DataStore:
16 | """
17 | Store json with test question details for use with NMOS Controller
18 | test suite and Testing Facade
19 | """
20 |
21 | def __init__(self):
22 | self.test_type = None
23 | self.question_id = None
24 | self.name = None
25 | self.description = None
26 | self.question = None
27 | self.answers = None
28 | self.timeout = None
29 | self.answer_uri = None
30 | self.answer_response = None
31 | self.metadata = None
32 |
33 | def clear(self):
34 | self.test_type = None
35 | self.question_id = None
36 | self.name = None
37 | self.description = None
38 | self.question = None
39 | self.answers = None
40 | self.timeout = None
41 | self.answer_uri = None
42 | self.answer_response = None
43 | self.metadata = None
44 |
45 | def setJson(self, json):
46 | self.test_type = json["test_type"]
47 | self.question_id = json["question_id"]
48 | self.name = json["name"]
49 | self.description = json["description"]
50 | self.question = json["question"]
51 | self.answers = json["answers"]
52 | self.timeout = json['timeout']
53 | self.answer_uri = json["answer_uri"]
54 | self.metadata = json["metadata"]
55 |
56 | def getAnswerJson(self):
57 | json_data = {
58 | "question_id": self.question_id,
59 | "answer_response": self.answer_response
60 | }
61 | return json_data
62 |
63 | def setAnswer(self, answer):
64 | self.answer_response = answer
65 |
66 | def getQuestionID(self):
67 | return self.question_id
68 |
69 | def getAnswers(self):
70 | return self.answers
71 |
72 | def getUrl(self):
73 | return self.answer_uri
74 |
75 | def getMetadata(self):
76 | return self.metadata
77 |
78 |
79 | dataStore = DataStore()
80 |
--------------------------------------------------------------------------------
/Development/src/pages/queryapis/QueryAPIsShow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | BooleanField,
4 | FunctionField,
5 | ShowContextProvider,
6 | ShowView,
7 | SimpleShowLayout,
8 | TextField,
9 | Toolbar,
10 | useRecordContext,
11 | useShowController,
12 | } from 'react-admin';
13 | import get from 'lodash/get';
14 | import ItemArrayField from '../../components/ItemArrayField';
15 | import ResourceShowActions from '../../components/ResourceShowActions';
16 | import ResourceTitle from '../../components/ResourceTitle';
17 | import SanitizedDivider from '../../components/SanitizedDivider';
18 | import ConnectButton from './ConnectButton';
19 |
20 | export const QueryAPIsShow = props => {
21 | const controllerProps = useShowController(props);
22 | return (
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | const QueryAPIsShowView = props => {
30 | const { record } = useRecordContext();
31 | return (
32 | <>
33 | }
36 | actions={}
37 | >
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | (
51 |
55 | )}
56 | />
57 |
58 |
59 |
60 |
61 | <>
62 |
63 | >
64 |
65 | >
66 | );
67 | };
68 |
69 | export default QueryAPIsShow;
70 |
--------------------------------------------------------------------------------
/Development/src/components/ActiveField.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Switch } from '@material-ui/core';
3 | import { useNotify } from 'react-admin';
4 | import get from 'lodash/get';
5 | import dataProvider from '../dataProvider';
6 | import sanitizeRestProps from './sanitizeRestProps';
7 |
8 | const toggleMasterEnable = (record, resource) => {
9 | return new Promise((resolve, reject) =>
10 | dataProvider('GET_ONE', resource, {
11 | id: record.id,
12 | })
13 | .then(({ data }) => {
14 | if (!data.hasOwnProperty('$staged')) {
15 | throw new Error('No Connection API found');
16 | }
17 | const params = {
18 | id: get(data, 'id'),
19 | data: {
20 | ...data,
21 | $staged: {
22 | ...get(data, '$staged'),
23 | master_enable: !get(data, '$active.master_enable'),
24 | activation: { mode: 'activate_immediate' },
25 | },
26 | },
27 | previousData: data,
28 | };
29 | return dataProvider('UPDATE', resource, params);
30 | })
31 | .then(response => resolve(response))
32 | .catch(error => reject(error))
33 | );
34 | };
35 |
36 | const ActiveField = ({ className, source, record = {}, resource, ...rest }) => {
37 | const notify = useNotify();
38 | const [checked, setChecked] = React.useState(
39 | get(record, 'subscription.active')
40 | );
41 |
42 | const handleChange = (record, resource) => {
43 | toggleMasterEnable(record, resource)
44 | .then(({ data }) => setChecked(get(data, 'master_enable')))
45 | .catch(error => notify(error.toString(), 'warning'));
46 | };
47 |
48 | // When the page refresh button is pressed, the ActiveField will receive a
49 | // new record prop. When this happens we should update the state of the
50 | // switch to reflect the newest IS-04 data
51 | useEffect(() => {
52 | setChecked(get(record, 'subscription.active'));
53 | }, [record]);
54 |
55 | return (
56 | handleChange(record, resource)}
60 | className={className}
61 | value={checked}
62 | {...sanitizeRestProps(rest)}
63 | />
64 | );
65 | };
66 |
67 | ActiveField.defaultProps = {
68 | addLabel: true,
69 | };
70 |
71 | export default ActiveField;
72 |
--------------------------------------------------------------------------------
/Development/src/icons/Source.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgSource = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgSource;
17 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thank you for your interest in this project! We want to encourage individuals and organisations to be involved in its ongoing development as users and contributors.
4 |
5 | ## Issues
6 |
7 | We welcome bug reports and feature requests for the implementation. These should be submitted as Issues on GitHub:
8 |
9 | - [Sign up](https://github.com/join) for GitHub if you haven't already done so.
10 | - Check whether someone has already submitted a similar Issue.
11 | - If necessary, submit a new Issue.
12 |
13 | Good bug reports will include a clear title and description, as much relevant information as possible, and preferably a code sample or an executable test case demonstrating the expected behavior that is not occurring.
14 |
15 | ## Pull Requests
16 |
17 | You can submit bug fixes, patches and other improvements to the code or documentation for review through a GitHub pull request as follows:
18 |
19 | * Fork the repository.
20 | * Make and commit changes.
21 | * Push your changes to a topic branch in your fork.
22 | * Make sure you understand how the repository licence applies to pull requests (see below).
23 | * Submit a pull request.
24 |
25 | Good PRs will include a clear rationale for the patch, and should follow the style of the existing code or documentation in this area.
26 |
27 | ## Licence Agreement
28 |
29 | To make the process of contributing as simple as possible, we do not require contributors to have signed a Contributor Licence Agreement (CLA).
30 |
31 | As GitHub says in its [Terms of Service](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license), *"Whenever you make a contribution to a repository containing notice of a license, you license your contribution under the same terms, and you agree that you have the right to license your contribution under those terms. [...] This is widely accepted as the norm in the open-source community; it's commonly referred to by the shorthand "inbound=outbound"."*
32 |
33 | All submissions are therefore made under the terms and conditions of the widely-used Apache License 2.0 with which this repository is [licensed](LICENSE). In brief, it explains that you must be authorised to submit on behalf of the copyright owner and that you simply grant the project's users, maintainers and contributors the same ability to use your submission as the rest of the repository.
34 |
35 | ## Joining the AMWA Networked Media Incubator
36 |
37 | If you wish to get more involved in creating NMOS specifications, working to implement and improve them, and participating in "plugfests", then your organisation may benefit from joining the [Advanced Media Workflow Association (AMWA)](http://amwa.tv/).
38 |
39 |
--------------------------------------------------------------------------------
/Development/src/components/DeleteButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useHistory } from 'react-router-dom';
3 | import { Button, useDelete, useNotify, useRefresh } from 'react-admin';
4 | import get from 'lodash/get';
5 | import { makeStyles } from '@material-ui/core';
6 | import { fade } from '@material-ui/core/styles/colorManipulator';
7 | import DeleteIcon from '@material-ui/icons/Delete';
8 |
9 | const useStyles = makeStyles(theme => ({
10 | contained: {
11 | color: theme.palette.error.contrastText,
12 | backgroundColor: theme.palette.error.main,
13 | '&:hover': {
14 | backgroundColor: theme.palette.error.dark,
15 | // Reset on touch devices, it doesn't add specificity
16 | '@media (hover: none)': {
17 | backgroundColor: theme.palette.error.main,
18 | },
19 | },
20 | },
21 | text: {
22 | color: theme.palette.error.main,
23 | '&:hover': {
24 | backgroundColor: fade(
25 | theme.palette.error.main,
26 | theme.palette.action.hoverOpacity
27 | ),
28 | // Reset on touch devices, it doesn't add specificity
29 | '@media (hover: none)': {
30 | backgroundColor: 'transparent',
31 | },
32 | },
33 | },
34 | }));
35 |
36 | const DeleteButton = ({
37 | resource,
38 | id,
39 | record,
40 | variant = 'contained',
41 | size,
42 | }) => {
43 | const classes = useStyles();
44 | const notify = useNotify();
45 | const refresh = useRefresh();
46 | const history = useHistory();
47 | const [deleteOne, { loading }] = useDelete(resource, id, record, {
48 | onSuccess: () => {
49 | notify('Element deleted', 'info');
50 | if (window.location.hash.substr(1) === `/${resource}`) {
51 | refresh();
52 | } else {
53 | history.push(`/${resource}`);
54 | }
55 | },
56 | onFailure: error => {
57 | if (error.hasOwnProperty('body')) {
58 | notify(
59 | get(error.body, 'error') +
60 | ' - ' +
61 | get(error.body, 'code') +
62 | ' - ' +
63 | get(error.body, 'debug'),
64 | 'warning'
65 | );
66 | }
67 | notify(error.toString(), 'warning');
68 | },
69 | });
70 | return (
71 |
83 | );
84 | };
85 |
86 | export default DeleteButton;
87 |
--------------------------------------------------------------------------------
/Sandbox/run_controller_tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | nmos_js_dir=$1
4 | shift
5 | testing_facade_dir=$1
6 | shift
7 | testing_dir=$1
8 | shift
9 | results_dir=$1
10 | shift
11 | badges_dir=$1
12 | shift
13 | summary_path=$1
14 | shift
15 | host=$1
16 | shift
17 | build_prefix=$1
18 | shift
19 |
20 | cd `dirname $0`
21 | self_dir=`pwd`
22 | cd -
23 |
24 | expected_disabled_IS_04_04=1
25 | expected_disabled_IS_05_03=0
26 |
27 |
28 | # Run nmos-js
29 | cd ${nmos_js_dir}
30 | yarn start > ${testing_dir}/${results_dir}/nmos_js_output 2>&1 &
31 | NMOS_JS_PID=$!
32 |
33 | function do_run_test() {
34 | suite=$1
35 | shift
36 | max_disabled_tests=$1
37 | shift
38 |
39 | # Run Testing Facade
40 | cd ${testing_facade_dir}
41 | python TestingFacade.py --suite ${suite} > ${testing_dir}/${results_dir}/testing_facade_output 2>&1 &
42 | TESTING_FACADE_PID=$!
43 | # Wait for testing facade to settle
44 | sleep 10s
45 |
46 | cd ${testing_dir}
47 | output_file=${results_dir}/${build_prefix}${suite}.json
48 | echo ${output_file}
49 | result=$(python nmos-test.py suite ${suite} --selection all "$@" --output "${output_file}" >> ${results_dir}/testoutput 2>&1; echo $?)
50 | if [ ! -e ${output_file} ]; then
51 | echo "No output produced"
52 | result=2
53 | else
54 | disabled_tests=`grep -c '"Test Disabled"' ${output_file}`
55 | if [[ $disabled_tests -gt $max_disabled_tests ]]; then
56 | echo "$disabled_tests tests disabled, expected max $max_disabled_tests"
57 | result=2
58 | elif [[ $disabled_tests -gt 0 ]]; then
59 | echo "$disabled_tests tests disabled"
60 | fi
61 | fi
62 | case $result in
63 | [0-1]) echo "Pass" | tee ${badges_dir}/${suite}.txt
64 | echo "${suite} :heavy_check_mark:" >> ${summary_path}
65 | ;;
66 | *) echo "Fail" | tee ${badges_dir}/${suite}.txt
67 | echo "${suite} :x:" >> ${summary_path}
68 | ;;
69 | esac
70 |
71 | # Testing Facade should have already terminates, but just in case
72 | kill $TESTING_FACADE_PID || echo "Testing Facade not running"
73 | }
74 |
75 | # Run Testing Facade
76 | # cd ${testing_facade_dir}
77 | # python TestingFacade.py --suite "IS-04-04" > ${testing_dir}/${results_dir}/testing_facade_output 2>&1 &
78 | # TESTING_FACADE_PID=$!
79 | # Wait for testing facade to run
80 | # sleep 10s
81 |
82 | # cd ${testing_dir}
83 | do_run_test IS-04-04 $expected_disabled_IS_04_04 --host "${host}" null --port 5001 0 --version v1.0 v1.3
84 |
85 | # kill $TESTING_FACADE_PID || echo "Testing Facade not running"
86 |
87 | # Run Testing Facade
88 | # cd ${testing_facade_dir}
89 | # python TestingFacade.py --suite "IS-05-03" > ${testing_dir}/${results_dir}/testing_facade_output 2>&1 &
90 | # TESTING_FACADE_PID=$!
91 | # Wait for testing facade to run
92 | # sleep 10s
93 |
94 | # cd ${testing_dir}
95 | do_run_test IS-05-03 $expected_disabled_IS_05_03 --host "${host}" null null --port 5001 0 0 --version v1.0 v1.3 v1.1
96 |
97 | # kill $TESTING_FACADE_PID || echo "Testing Facade not running"
98 |
99 | # Stop nmos-js
100 | kill $NMOS_JS_PID || echo "nmos-js not running"
101 |
102 |
103 |
--------------------------------------------------------------------------------
/Development/src/pages/menu.js:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, useEffect } from 'react';
2 | import { NavLink, useHistory } from 'react-router-dom';
3 | import {
4 | Divider,
5 | ListItemIcon,
6 | ListItemText,
7 | MenuItem,
8 | MenuList,
9 | } from '@material-ui/core';
10 | import HomeIcon from '@material-ui/icons/Home';
11 | import SettingsIcon from '@material-ui/icons/Settings';
12 | import { makeStyles } from '@material-ui/styles';
13 |
14 | import {
15 | DeviceIcon,
16 | FlowIcon,
17 | NodeIcon,
18 | ReceiverIcon,
19 | RegistryIcon,
20 | RegistryLogsIcon,
21 | SenderIcon,
22 | SourceIcon,
23 | SubscriptionIcon,
24 | } from '../icons';
25 |
26 | import labelize from '../components/labelize';
27 |
28 | const NavLinkRef = forwardRef((props, ref) => (
29 |
30 | ));
31 |
32 | const useStyles = makeStyles(theme => ({
33 | root: {
34 | paddingLeft: '24px',
35 | borderLeftWidth: '3px',
36 | borderLeftStyle: 'solid',
37 | borderLeftColor: 'transparent',
38 | },
39 | active: {
40 | borderLeftColor: theme.palette.primary.main,
41 | color: theme.palette.primary.main,
42 | '& $icon': {
43 | color: theme.palette.primary.main,
44 | },
45 | },
46 | icon: {},
47 | }));
48 |
49 | const CustomMenuItem = ({ to, icon, label = labelize(to), ...props }) => {
50 | const classes = useStyles();
51 | return (
52 |
65 | );
66 | };
67 |
68 | const CustomMenu = () => {
69 | const history = useHistory();
70 | useEffect(() => {
71 | history.block(
72 | (location, action) =>
73 | !(
74 | action === 'PUSH' &&
75 | location.pathname === history.location.pathname
76 | )
77 | );
78 | }, [history]);
79 | return (
80 |
81 | } label={'Home'} />
82 | } />
83 |
84 | } />
85 | } />
86 | } />
87 | } />
88 | } />
89 | } />
90 | } />
91 |
92 | }
95 | label="Query APIs"
96 | />
97 | } />
98 |
99 | );
100 | };
101 |
102 | export default CustomMenu;
103 |
--------------------------------------------------------------------------------
/Development/src/components/CustomNameField.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import { IconButton, TextField, Typography } from '@material-ui/core';
3 | import CreateIcon from '@material-ui/icons/Create';
4 | import DeleteIcon from '@material-ui/icons/Delete';
5 | import ClearIcon from '@material-ui/icons/Clear';
6 | import DoneIcon from '@material-ui/icons/Done';
7 | import useCustomNamesContext from './useCustomNamesContext';
8 |
9 | export const CustomNameField = ({
10 | defaultValue,
11 | source,
12 | label,
13 | autoFocus,
14 | onEditStarted,
15 | onEditStopped,
16 | ...props
17 | }) => {
18 | const { getCustomName, setCustomName, unsetCustomName } =
19 | useCustomNamesContext();
20 | const [editing, setEditing] = useState(false);
21 | const [value, setValue] = useState(
22 | getCustomName(source) || defaultValue || ''
23 | );
24 |
25 | const inputRef = useRef();
26 | useEffect(() => {
27 | const timeout = setTimeout(() => {
28 | if (autoFocus && editing) inputRef.current.focus();
29 | }, 100);
30 | return () => clearTimeout(timeout);
31 | }, [autoFocus, editing]);
32 |
33 | const handleEdit = () => {
34 | setEditing(true);
35 | onEditStarted();
36 | };
37 |
38 | const removeCustomName = () => {
39 | setValue(defaultValue);
40 | unsetCustomName(source);
41 | setEditing(false);
42 | onEditStopped();
43 | };
44 |
45 | const saveCustomName = () => {
46 | if (value !== defaultValue) {
47 | setCustomName(source, value);
48 | } else {
49 | unsetCustomName(source);
50 | }
51 | setEditing(false);
52 | onEditStopped();
53 | };
54 |
55 | const cancelCustomName = () => {
56 | setValue(getCustomName(source) || defaultValue || '');
57 | setEditing(false);
58 | onEditStopped();
59 | };
60 |
61 | return editing ? (
62 |
63 | setValue(event.target.value)}
69 | onFocus={event => event.target.select()}
70 | inputRef={inputRef}
71 | fullWidth={true}
72 | onKeyPress={event => {
73 | if (event.key === 'Enter') {
74 | saveCustomName();
75 | }
76 | }}
77 | {...props}
78 | />
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | ) : (
87 |
88 |
89 | {value}
90 |
91 |
92 |
93 |
94 | {getCustomName(source) && (
95 |
96 |
97 |
98 | )}
99 |
100 | );
101 | };
102 |
103 | export default CustomNameField;
104 |
--------------------------------------------------------------------------------
/Development/src/pages/queryapis/QueryAPIsList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Card,
4 | CardContent,
5 | Table,
6 | TableBody,
7 | TableCell,
8 | TableHead,
9 | TableRow,
10 | } from '@material-ui/core';
11 | import { Loading, ShowButton, Title } from 'react-admin';
12 | import FilterPanel, { StringFilter } from '../../components/FilterPanel';
13 | import ListActions from '../../components/ListActions';
14 | import ConnectButton from './ConnectButton';
15 | import useGetList from '../../components/useGetList';
16 | import { useJSONSetting } from '../../settings';
17 |
18 | const QueryAPIsList = props => {
19 | const [filter, setFilter] = useJSONSetting('Query APIs Filter');
20 | const { data, loaded, url } = useGetList({
21 | ...props,
22 | filter,
23 | });
24 | if (!loaded) return ;
25 |
26 | return (
27 | <>
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
40 |
41 |
42 |
43 |
44 |
49 | Name
50 |
51 | API Versions
52 | Priority
53 |
54 |
55 |
56 |
57 | {data.map(item => (
58 |
59 |
60 |
68 |
69 | {item.txt.api_ver}
70 | {item.txt.pri}
71 |
72 |
76 |
77 |
78 | ))}
79 |
80 |
81 |
82 |
83 | >
84 | );
85 | };
86 |
87 | export default QueryAPIsList;
88 |
--------------------------------------------------------------------------------
/Development/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Admin, Layout, Resource } from 'react-admin';
3 | import { useTheme } from '@material-ui/styles';
4 | import { get } from 'lodash';
5 | import CONFIG from './config.json';
6 | import {
7 | AuthContextProvider,
8 | SettingsContextProvider,
9 | useAuthContext,
10 | } from './settings';
11 | import AdminMenu from './pages/menu';
12 | import AppBar from './pages/appbar';
13 | import About from './pages/about';
14 | import { NodesList, NodesShow } from './pages/nodes';
15 | import { DevicesList, DevicesShow } from './pages/devices';
16 | import { SourcesList, SourcesShow } from './pages/sources';
17 | import { FlowsList, FlowsShow } from './pages/flows';
18 | import { ReceiversEdit, ReceiversList, ReceiversShow } from './pages/receivers';
19 | import { SendersEdit, SendersList, SendersShow } from './pages/senders';
20 | import { LogsList, LogsShow } from './pages/logs';
21 | import {
22 | SubscriptionsCreate,
23 | SubscriptionsList,
24 | SubscriptionsShow,
25 | } from './pages/subscriptions';
26 | import { QueryAPIsList, QueryAPIsShow } from './pages/queryapis';
27 | import Settings from './pages/settings';
28 | import dataProvider from './dataProvider';
29 | import authProvider from './authProvider';
30 |
31 | const AdminAppBar = props => {
32 | const [useAuth] = useAuthContext();
33 | return ;
34 | };
35 |
36 | const AdminLayout = props => (
37 |
38 | );
39 |
40 | const AppAdmin = () => {
41 | const [useAuth] = useAuthContext();
42 |
43 | //if Authentication switch 'off' ensure user is logged out
44 | if (!useAuth) {
45 | authProvider.getIdentity().then(identity => {
46 | if (identity && identity.id) {
47 | authProvider.logout();
48 | }
49 | });
50 | }
51 | return (
52 |
60 |
61 |
62 |
63 |
64 |
65 |
71 |
77 |
83 |
84 |
90 |
91 | );
92 | };
93 |
94 | export const App = () => (
95 |
96 |
97 |
98 |
99 |
100 | );
101 |
102 | export default App;
103 |
--------------------------------------------------------------------------------
/Development/src/pages/queryapis/ConnectButton.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button, Menu, MenuItem } from '@material-ui/core';
3 | import { useNotify } from 'react-admin';
4 | import get from 'lodash/get';
5 | import { QUERY_API, apiUrl, disabledSetting, setApiUrl } from '../../settings';
6 | import { ConnectRegistryIcon } from '../../icons';
7 |
8 | const ConnectButton = ({ record, variant = 'contained', size }) => {
9 | const [anchorEl, setAnchorEl] = useState(null);
10 | const notify = useNotify();
11 |
12 | if (!record) {
13 | return null;
14 | }
15 |
16 | const scheme = get(record, 'txt.api_proto');
17 |
18 | const makeQueryAPI = selectedAddress => {
19 | return (
20 | scheme +
21 | '://' +
22 | selectedAddress +
23 | ':' +
24 | get(record, 'port') +
25 | '/x-nmos/query/' +
26 | get(record, 'txt.api_ver').split(',').slice(-1)[0]
27 | );
28 | };
29 |
30 | const changeQueryAPI = selectedAddress => {
31 | setApiUrl(QUERY_API, makeQueryAPI(selectedAddress));
32 | };
33 |
34 | // heuristic decision about whether to give the user a single-click connect
35 | // or whether to pop up a menu of hosts to select from
36 | const hosts = (() => {
37 | const hostname = get(record, 'host_target');
38 | const addresses = get(record, 'addresses');
39 | if (!hostname.endsWith('local.')) {
40 | // i.e. unicast DNS-SD hostname
41 | return [hostname];
42 | } else if (scheme === 'http' && addresses.length === 1) {
43 | // i.e. mDNS hostname resolved to a single address used with HTTP
44 | return [addresses[0]];
45 | } else {
46 | // i.e. mDNS hostname resolved to multiple addresses or used with HTTPS
47 | return [hostname, ...addresses];
48 | }
49 | })();
50 |
51 | const handleButtonClick = event => {
52 | if (hosts.length > 1) {
53 | setAnchorEl(event.currentTarget);
54 | } else {
55 | changeQueryAPI(hosts[0]);
56 | notify(`Connected to: ${apiUrl(QUERY_API)}`);
57 | }
58 | };
59 |
60 | const handleMenuItemClick = option => {
61 | changeQueryAPI(option);
62 | notify(`Connected to: ${apiUrl(QUERY_API)}`);
63 | setAnchorEl(null);
64 | };
65 |
66 | return (
67 | <>
68 | }
72 | variant={variant}
73 | size={
74 | size ? size : variant === 'contained' ? 'medium' : 'small'
75 | }
76 | disabled={disabledSetting(QUERY_API)}
77 | >
78 | Connect
79 |
80 |
105 | >
106 | );
107 | };
108 |
109 | export default ConnectButton;
110 |
--------------------------------------------------------------------------------
/Development/src/icons/ConnectRegistry.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgConnectRegistry = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgConnectRegistry;
17 |
--------------------------------------------------------------------------------
/Development/src/pages/receivers/ConnectButtons.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button, Menu, MenuItem } from '@material-ui/core';
3 | import { useHistory } from 'react-router-dom';
4 | import get from 'lodash/get';
5 | import { useNotify, useRefresh } from 'react-admin';
6 | import makeConnection from '../../components/makeConnection';
7 | import { ActivateImmediateIcon, StageIcon } from '../../icons';
8 | import dataProvider from '../../dataProvider';
9 |
10 | const ConnectButtons = ({ senderData, receiverData }) => {
11 | const [anchorEl, setAnchorEl] = useState(null);
12 | const [senderLegs, setSenderLegs] = useState(0);
13 | const [endpoint, setEndpoint] = useState('');
14 |
15 | const history = useHistory();
16 | const notify = useNotify();
17 | const refresh = useRefresh();
18 |
19 | const connect = (endpoint, senderLeg) => {
20 | const options = { singleSenderLeg: senderLeg };
21 | makeConnection(senderData.id, receiverData.id, endpoint, options)
22 | .then(() => {
23 | notify('Element updated', 'info');
24 | refresh();
25 | history.push(`/receivers/${receiverData.id}/show/${endpoint}`);
26 | })
27 | .catch(error => {
28 | if (error && error.hasOwnProperty('body'))
29 | notify(
30 | get(error.body, 'error') +
31 | ' - ' +
32 | get(error.body, 'code') +
33 | ' - ' +
34 | get(error.body, 'debug'),
35 | 'warning'
36 | );
37 | notify(error.toString(), 'warning');
38 | });
39 | };
40 |
41 | const handleConnect = (endpoint, event) => {
42 | setEndpoint(endpoint);
43 | if (get(receiverData, '$staged.transport_params').length === 1) {
44 | const ref = event.currentTarget;
45 | dataProvider('GET_ONE', 'senders', {
46 | id: senderData.id,
47 | }).then(({ data: senderData }) => {
48 | if (get(senderData, '$staged.transport_params').length > 1) {
49 | setSenderLegs(
50 | get(senderData, '$staged.transport_params').length
51 | );
52 | setAnchorEl(ref);
53 | } else {
54 | connect(endpoint);
55 | }
56 | });
57 | } else {
58 | connect(endpoint);
59 | }
60 | };
61 |
62 | return (
63 | <>
64 |
72 |
79 |
103 | >
104 | );
105 | };
106 |
107 | export default ConnectButtons;
108 |
--------------------------------------------------------------------------------
/Development/src/icons/Subscription.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgSubscription = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgSubscription;
17 |
--------------------------------------------------------------------------------
/is12-client/src/index.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom/client';
2 | import {createContext, useState} from 'react';
3 | import {CookiesProvider} from "react-cookie";
4 | import './index.css';
5 | import {BrowserRouter, Navigate, Route, Routes} from "react-router-dom";
6 | import DeviceConnect from "./client/DeviceConnect";
7 | import NCAController from "./client/NCAController";
8 | import DevApp from "./client/DevApp";
9 | import {createTheme, ThemeProvider} from "@mui/material/styles";
10 | import getDevices from "./backend/DeviceProvider";
11 |
12 | export const DeviceContext = createContext({}); // Device list from registry
13 |
14 | const darkTheme = createTheme({
15 | palette: {
16 | mode: 'dark',
17 | },
18 | });
19 |
20 | function Index(props) {
21 | const {config} = props;
22 | const [devices, setDevices] = useState([])
23 |
24 | let showClassManager = false
25 | if ("showClassManager" in config) {
26 | showClassManager = config.showClassManager
27 | }
28 |
29 | if (config.address !== undefined && config.port !== undefined) {
30 | const connectionURL = `${config.address}:${config.port}`;
31 |
32 | if (devices.length === 0) {
33 |
34 | /* Get devices from registry */
35 | getDevices(connectionURL)
36 | .then((deviceList) => {
37 |
38 | if (deviceList.length > 0) {
39 | setDevices(deviceList);
40 |
41 | } else {
42 | alert('Connected to registry but no devices found...')
43 |
44 | }
45 |
46 | }).catch((err) => {
47 |
48 | alert(`Could not connect to ${connectionURL}`);
49 | console.error('Problem encountered: ', err);
50 |
51 | });
52 | }
53 |
54 | return (
55 |
56 |
57 |
58 | {devices.length > 0 ? : <>>}
59 |
60 |
61 |
62 | )
63 |
64 | } else {
65 |
66 | return (
67 |
68 |
69 |
70 |
71 |
72 | {/* ROUTER FOR NAVIGATION */}
73 | }/>
74 | 0 ?
76 | :
77 | }/>
78 | 0 ? : }/>
79 |
80 |
81 |
82 |
83 |
84 | )
85 | }
86 | }
87 |
88 |
89 | /* Needs to fetch config like this to automatically re-load doc when config changes */
90 | const getConfig = async () => {
91 | return await fetch('./config.json')
92 | .then((response) => {
93 |
94 | if (!response.ok) {
95 | throw new Error("HTTP error " + response.status);
96 | }
97 |
98 | return response.json()
99 |
100 | })
101 | }
102 |
103 | /* Fetching the config.json file and then rendering the Index component with the config as a prop. */
104 | getConfig().then((config) => {
105 |
106 | const root = ReactDOM.createRoot(document.getElementById('root'));
107 | root.render();
108 |
109 | })
110 |
111 |
112 |
--------------------------------------------------------------------------------
/TestingFacade/TestingFacade.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2021 Advanced Media Workflow Association
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | import os
16 | import requests
17 | import signal
18 | import sys
19 | from threading import Thread
20 | from flask import Flask, request
21 | from selenium.common.exceptions import NoSuchElementException
22 | from DataStore import dataStore
23 | from IS0404AutoTest import IS0404tests
24 | from IS0503AutoTest import IS0503tests
25 | import Config as CONFIG
26 |
27 | TEST_SETS = {'IS-04-04': IS0404tests,
28 | 'IS-05-03': IS0503tests}
29 |
30 | app = Flask(__name__)
31 |
32 | @app.route('/x-nmos/testquestion/', methods=['POST'], strict_slashes=False)
33 | def controller_tests_post(version):
34 | # Should be json from Test Suite with questions
35 | expected_entries = ['test_type', 'name', 'description', 'question', 'answers', 'answer_uri']
36 |
37 | if request.json.get('clear'):
38 | # End of current tests, clear data store
39 | dataStore.clear()
40 | else:
41 | # Should be a new question
42 | for entry in expected_entries:
43 | if entry not in request.json:
44 | return 'Invalid JSON received', 400
45 | # All required entries are present so update data
46 | dataStore.setJson(request.json)
47 | # Run test in new thread
48 | executionThread = Thread(target=execute_test)
49 | executionThread.start()
50 | return '', 202
51 |
52 |
53 | def execute_test():
54 | """
55 | After test data has been sent to x-nmos/testing-facade figure out which
56 | test was sent. Call relevant test method and retrieve answers.
57 | Update json and send back to test suite
58 | """
59 | # Get question details from data store
60 | question_id = dataStore.getQuestionID()
61 | answers = dataStore.getAnswers()
62 | metadata = dataStore.getMetadata()
63 |
64 | # Load specified test suite
65 | tests = TEST_SUITE
66 |
67 | if question_id.startswith("test_"):
68 | # Get method associated with question id, set up test browser,
69 | # run method then tear down and save any answers returned to data store
70 | method = getattr(tests, question_id)
71 | if callable(method):
72 | print(" * Running " + question_id)
73 | try:
74 | tests.set_up_test()
75 | test_result = method(answers, metadata)
76 | except NoSuchElementException:
77 | test_result = None
78 | tests.tear_down_test()
79 | dataStore.setAnswer(test_result)
80 |
81 | elif question_id == 'pre_tests_message':
82 | # Beginning of test set, return to confirm start
83 | dataStore.setAnswer(None)
84 |
85 | elif question_id == 'post_tests_message':
86 | # End of test set, return to confirm end
87 | dataStore.setAnswer(None)
88 |
89 | else:
90 | # Not a recognised part of test suite
91 | dataStore.setAnswer(None)
92 |
93 | # POST answer json back to test suite
94 | requests.post(dataStore.getUrl(), json=dataStore.getAnswerJson())
95 | if question_id == 'post_tests_message':
96 | os.kill(os.getpid(), signal.SIGINT)
97 | return
98 |
99 |
100 | if __name__ == "__main__":
101 | global TEST_SUITE
102 |
103 | if '--suite' not in sys.argv:
104 | sys.exit('You must specify a test suite with --suite')
105 |
106 | for i, arg in enumerate(sys.argv):
107 | if arg == '--suite':
108 | try:
109 | TEST_SUITE = TEST_SETS[sys.argv[i+1]]
110 | except Exception as e:
111 | sys.exit('Invalid test suite selection ' + str(e))
112 |
113 | app.run(host='0.0.0.0', port=CONFIG.TESTING_FACADE_PORT)
114 |
--------------------------------------------------------------------------------
/Development/src/components/CardFormIterator.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | Children,
3 | Component,
4 | cloneElement,
5 | isValidElement,
6 | } from 'react';
7 | import PropTypes from 'prop-types';
8 | import get from 'lodash/get';
9 | import { Card, CardContent, Grid } from '@material-ui/core';
10 | import { FormInput } from 'react-admin';
11 | // Derived from react-admin component
12 | export class CardFormIterator extends Component {
13 | constructor(props) {
14 | super(props);
15 | // we need a unique id for each field for a proper enter/exit animation
16 | // but redux-form doesn't provide one (cf https://github.com/erikras/redux-form/issues/2735)
17 | // so we keep an internal map between the field position and an autoincrement id
18 | this.nextId = props.fields.length
19 | ? props.fields.length
20 | : props.defaultValue
21 | ? props.defaultValue.length
22 | : 0;
23 |
24 | // We check whether we have a defaultValue (which must be an array) before checking
25 | // the fields prop which will always be empty for a new record.
26 | // Without it, our ids wouldn't match the default value and we would get key warnings
27 | // on the CssTransition element inside our render method
28 | this.ids = this.nextId > 0 ? Array.from(Array(this.nextId).keys()) : [];
29 | }
30 |
31 | render() {
32 | const { basePath, children, fields, record, resource, source } =
33 | this.props;
34 | const records = get(record, source);
35 | return fields ? (
36 | <>
37 |
38 |
39 | {fields.map((member, index) => (
40 |
41 |
42 |
43 | {Children.map(children, (input, index2) =>
44 | isValidElement(input) ? (
45 |
71 | ) : null
72 | )}
73 |
74 |
75 |
76 | ))}
77 |
78 | >
79 | ) : null;
80 | }
81 | }
82 |
83 | CardFormIterator.propTypes = {
84 | defaultValue: PropTypes.any,
85 | basePath: PropTypes.string,
86 | children: PropTypes.node,
87 | fields: PropTypes.object,
88 | record: PropTypes.object,
89 | source: PropTypes.string,
90 | resource: PropTypes.string,
91 | };
92 |
93 | export default CardFormIterator;
94 |
--------------------------------------------------------------------------------
/Development/src/pages/logs/LogsList.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Card,
4 | CardContent,
5 | Table,
6 | TableBody,
7 | TableCell,
8 | TableHead,
9 | TableRow,
10 | } from '@material-ui/core';
11 | import { Loading, ShowButton, Title } from 'react-admin';
12 | import FilterPanel, {
13 | AutocompleteFilter,
14 | NumberFilter,
15 | StringFilter,
16 | } from '../../components/FilterPanel';
17 | import PaginationButtons from '../../components/PaginationButtons';
18 | import ListActions from '../../components/ListActions';
19 | import useGetList from '../../components/useGetList';
20 | import { useJSONSetting } from '../../settings';
21 |
22 | const LogsList = props => {
23 | const [filter, setFilter] = useJSONSetting('Logs Filter');
24 | const [paginationURL, setPaginationURL] = useState(null);
25 | const { data, loaded, pagination, url } = useGetList({
26 | ...props,
27 | filter,
28 | paginationURL,
29 | });
30 | if (!loaded) return ;
31 |
32 | const nextPage = label => {
33 | setPaginationURL(pagination[label]);
34 | };
35 |
36 | return (
37 | <>
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
55 |
56 |
57 |
58 |
59 |
64 | Timestamp
65 |
66 | Level
67 | Message
68 | Request URI
69 | HTTP Method
70 |
71 |
72 |
73 | {data.map(item => (
74 |
75 |
76 |
84 |
85 | {item.level}
86 | {item.message}
87 | {item.request_uri}
88 | {item.http_method}
89 |
90 | ))}
91 |
92 |
93 |
94 |
99 |
100 |
101 | >
102 | );
103 | };
104 |
105 | const httpMethods = ['GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'];
106 |
107 | export default LogsList;
108 |
--------------------------------------------------------------------------------
/Development/src/icons/Registry.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgRegistry = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgRegistry;
17 |
--------------------------------------------------------------------------------
/Development/src/pages/sources/SourcesShow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | ArrayField,
4 | ListButton,
5 | ReferenceArrayField,
6 | ReferenceField,
7 | ReferenceManyField,
8 | ShowContextProvider,
9 | ShowView,
10 | SimpleShowLayout,
11 | SingleFieldList,
12 | TextField,
13 | TopToolbar,
14 | useRecordContext,
15 | useShowController,
16 | } from 'react-admin';
17 | import get from 'lodash/get';
18 | import LinkChipField from '../../components/LinkChipField';
19 | import ObjectField from '../../components/ObjectField';
20 | import { FORMATS, ParameterField } from '../../components/ParameterRegisters';
21 | import RateField from '../../components/RateField';
22 | import RawButton from '../../components/RawButton';
23 | import ResourceTitle from '../../components/ResourceTitle';
24 | import SanitizedDivider from '../../components/SanitizedDivider';
25 | import TAIField from '../../components/TAIField';
26 | import UnsortableDatagrid from '../../components/UnsortableDatagrid';
27 | import { queryVersion } from '../../settings';
28 |
29 | const SourcesShowActions = ({ basePath, data, resource }) => (
30 | }>
31 | {data ? : null}
32 |
33 |
34 | );
35 |
36 | export const SourcesShow = props => {
37 | const controllerProps = useShowController(props);
38 | return (
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | const SourcesShowView = props => {
46 | const { record } = useRecordContext();
47 | return (
48 | }
51 | actions={}
52 | >
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | {queryVersion() >= 'v1.1' && (
61 |
62 | )}
63 | {queryVersion() >= 'v1.1' && (
64 |
65 | )}
66 |
67 | {queryVersion() >= 'v1.1' &&
68 | get(record, 'format') === 'urn:x-nmos:format:audio' && (
69 |
70 |
71 |
72 |
73 |
74 |
75 | )}
76 | {queryVersion() >= 'v1.3' &&
77 | get(record, 'format') === 'urn:x-nmos:format:data' && (
78 |
79 | )}
80 |
81 |
86 |
87 |
88 |
89 |
90 |
96 |
97 |
98 |
99 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | );
111 | };
112 |
113 | export default SourcesShow;
114 |
--------------------------------------------------------------------------------
/Development/src/icons/RegistryLogs.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SvgIcon } from '@material-ui/core';
3 |
4 | const SvgRegistryLogs = props => (
5 |
6 |
7 |
11 |
12 |
13 |
14 | );
15 |
16 | export default SvgRegistryLogs;
17 |
--------------------------------------------------------------------------------
/Development/src/pages/sources/SourcesList.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Card,
4 | CardContent,
5 | Table,
6 | TableBody,
7 | TableCell,
8 | TableHead,
9 | TableRow,
10 | } from '@material-ui/core';
11 | import { Loading, ShowButton, Title } from 'react-admin';
12 | import FilterPanel, {
13 | AutocompleteFilter,
14 | RateFilter,
15 | StringFilter,
16 | } from '../../components/FilterPanel';
17 | import {
18 | FORMATS,
19 | ParameterField,
20 | parameterAutocompleteProps,
21 | } from '../../components/ParameterRegisters';
22 | import PaginationButtons from '../../components/PaginationButtons';
23 | import ListActions from '../../components/ListActions';
24 | import useGetList from '../../components/useGetList';
25 | import { queryVersion, useJSONSetting } from '../../settings';
26 |
27 | const SourcesList = props => {
28 | const [filter, setFilter] = useJSONSetting('Sources Filter');
29 | const [paginationURL, setPaginationURL] = useState(null);
30 | const { data, loaded, pagination, url } = useGetList({
31 | ...props,
32 | filter,
33 | paginationURL,
34 | });
35 | if (!loaded) return ;
36 |
37 | const nextPage = label => {
38 | setPaginationURL(pagination[label]);
39 | };
40 |
41 | return (
42 | <>
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | {queryVersion() >= 'v1.1' && (
54 |
55 | )}
56 |
60 | {queryVersion() >= 'v1.3' && (
61 |
66 | )}
67 |
68 |
69 |
70 |
71 |
72 |
77 | Label
78 |
79 | Format
80 | {queryVersion() >= 'v1.3' && (
81 | Event Type
82 | )}
83 |
84 |
85 |
86 | {data.map(item => (
87 |
88 |
89 |
97 |
98 |
99 |
104 |
105 | {queryVersion() >= 'v1.3' && (
106 | {item.event_type}
107 | )}
108 |
109 | ))}
110 |
111 |
112 |
113 |
118 |
119 |
120 | >
121 | );
122 | };
123 |
124 | const eventTypes = ['boolean', 'string', 'number'];
125 |
126 | export default SourcesList;
127 |
--------------------------------------------------------------------------------
/Development/src/components/useGetList.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { shallowEqual, useSelector } from 'react-redux';
3 | import {
4 | CRUD_GET_LIST,
5 | useCheckMinimumRequiredProps,
6 | useDataProvider,
7 | useNotify,
8 | useSafeSetState,
9 | useVersion,
10 | } from 'react-admin';
11 | import isEqual from 'lodash/isEqual';
12 | import useDebounce from './useDebounce';
13 |
14 | const isEmptyList = data =>
15 | Array.isArray(data)
16 | ? data.length === 0
17 | : data &&
18 | Object.keys(data).length === 0 &&
19 | data.hasOwnProperty('fetchedAt');
20 |
21 | // We need a custom hook as the request URL needs to be returned.
22 | const useQueryWithStore = (query, options, dataSelector, totalSelector) => {
23 | const { type, resource, payload } = query;
24 | const data = useSelector(dataSelector);
25 | const total = useSelector(totalSelector);
26 | const [state, setState] = useSafeSetState({
27 | data,
28 | total,
29 | error: null,
30 | loading: true,
31 | loaded: data !== undefined && !isEmptyList(data),
32 | pagination: null,
33 | url: null,
34 | });
35 | if (!isEqual(state.data, data) || state.total !== total) {
36 | setState(
37 | Object.assign(Object.assign({}, state), {
38 | data,
39 | total,
40 | loaded: true,
41 | })
42 | );
43 | }
44 | const dataProvider = useDataProvider();
45 | useEffect(() => {
46 | // If the filter has changed ignore paginationURL
47 | payload.paginationURL = null;
48 | }, [payload.filter]); // eslint-disable-line
49 | useEffect(() => {
50 | setState(prevState =>
51 | Object.assign(Object.assign({}, prevState), { loading: true })
52 | );
53 | dataProvider[type](resource, payload, options)
54 | .then(response => {
55 | // We only care about the dataProvider url response here, because
56 | // the list data was already passed to the SUCCESS redux reducer.
57 | setState(prevState =>
58 | Object.assign(Object.assign({}, prevState), {
59 | error: null,
60 | loading: false,
61 | loaded: true,
62 | pagination: response.pagination,
63 | url: response.url,
64 | })
65 | );
66 | })
67 | .catch(error => {
68 | setState({
69 | error,
70 | loading: false,
71 | loaded: false,
72 | });
73 | });
74 | }, [JSON.stringify({ query, options })]); // eslint-disable-line
75 | return state;
76 | };
77 |
78 | const useGetList = props => {
79 | useCheckMinimumRequiredProps(
80 | 'List',
81 | ['basePath', 'filter', 'resource'],
82 | props
83 | );
84 | const { basePath, resource, paginationURL, filter } = props;
85 | const debouncedFilter = useDebounce(filter, 250);
86 |
87 | const notify = useNotify();
88 | const version = useVersion();
89 |
90 | const { total, error, loading, loaded, pagination, url } =
91 | useQueryWithStore(
92 | {
93 | type: 'getList',
94 | resource,
95 | payload: {
96 | filter: debouncedFilter,
97 | paginationURL,
98 | },
99 | },
100 | {
101 | action: CRUD_GET_LIST,
102 | version,
103 | onFailure: error =>
104 | notify(
105 | typeof error === 'string'
106 | ? error
107 | : error.message || 'ra.notification.http_error',
108 | 'warning'
109 | ),
110 | },
111 | state =>
112 | state.admin.resources[resource]
113 | ? state.admin.resources[resource].list.ids
114 | : null,
115 | state =>
116 | state.admin.resources[resource]
117 | ? state.admin.resources[resource].list.total
118 | : null
119 | );
120 | const data = useSelector(
121 | state =>
122 | state.admin.resources[resource]
123 | ? state.admin.resources[resource].data
124 | : {},
125 | shallowEqual
126 | );
127 | const ids = useSelector(
128 | state =>
129 | state.admin.resources[resource]
130 | ? state.admin.resources[resource].list.ids
131 | : [],
132 | shallowEqual
133 | );
134 |
135 | const listDataObject = {};
136 | ids.forEach(key => (listDataObject[key] = data[key]));
137 |
138 | const listDataArray = Object.keys(listDataObject).map(key => {
139 | return listDataObject[key];
140 | });
141 |
142 | return {
143 | basePath,
144 | data: listDataArray,
145 | error,
146 | ids,
147 | loading,
148 | loaded,
149 | pagination,
150 | resource,
151 | total,
152 | url,
153 | version,
154 | };
155 | };
156 |
157 | export default useGetList;
158 |
--------------------------------------------------------------------------------
/TestingFacade/IS0503AutoTest.py:
--------------------------------------------------------------------------------
1 | import time
2 | from GenericAutoTest import GenericAutoTest
3 |
4 |
5 | class IS0503AutoTest(GenericAutoTest):
6 | """
7 | Automated version of NMOS Controller test suite IS0503
8 | """
9 | def test_01(self, answers, metadata):
10 | """
11 | Identify which Receiver devices are controllable via IS-05
12 | """
13 | # A subset of the Receivers registered with the Registry are controllable via IS-05,
14 | # for instance, allowing Senders to be connected.
15 | # Please refresh your NCuT and select the Receivers
16 | # that have a Connection API from the list below.
17 | # Be aware that if your NCuT only displays Receivers which have a Connection API,
18 | # some of the Receivers in the following list may not be visible.
19 |
20 | self.navigate_to_page('Receivers')
21 | receivers = self.find_resource_labels()
22 |
23 | # Loop through receivers and check if connection tab is disabled
24 | connectable_receivers = []
25 |
26 | for receiver in receivers:
27 | self.navigate_to_page(receiver)
28 | connectable = self.check_connectable()
29 | if connectable:
30 | connectable_receivers.append(receiver)
31 | self.navigate_to_page('Receivers')
32 |
33 | # Get answer ids for connectable receivers to send to test suite
34 | actual_answers = [answer['answer_id'] for answer in answers if answer['resource']['label']
35 | in connectable_receivers]
36 |
37 | return actual_answers
38 |
39 | def test_02(self, answers, metadata):
40 | """
41 | Instruct Receiver to subscribe to a Sender’s Flow via IS-05
42 | """
43 | # All flows that are available in a Sender should be able to be
44 | # connected to a Receiver.
45 | # Use the NCuT to perform an 'immediate' activation between
46 | # sender: x and receiver: y
47 | # Click the 'Next' button once the connection is active.
48 |
49 | # Get sender and receiver details from metadata sent with question
50 | sender = metadata['sender']
51 | receiver = metadata['receiver']
52 |
53 | self.navigate_to_page('Receivers')
54 | self.navigate_to_page(receiver['label'])
55 | self.make_connection(sender['label'])
56 |
57 | def test_03(self, answers, metadata):
58 | """
59 | Disconnecting a Receiver from a connected Flow via IS-05
60 | """
61 | # IS-05 provides a mechanism for removing an active connection
62 | # through its API.
63 | # Use the NCuT to remove the connection between sender: x and
64 | # receiver: y
65 | # Click the 'Next' button once the connection has been removed.'
66 |
67 | receiver = metadata['receiver']
68 | self.navigate_to_page('Receivers')
69 | self.remove_connection(receiver['label'])
70 |
71 | def test_04(self, answers, metadata):
72 | """
73 | Indicating the state of connections via updates received from the
74 | IS-04 Query API
75 | First question
76 | """
77 | # The NCuT should be able to monitor and update the connection status
78 | # of all registered Devices.
79 | # Use the NCuT to identify the receiver that has just been activated.
80 |
81 | self.navigate_to_page('Receivers')
82 | receiver = self.get_active_receiver()
83 |
84 | actual_answer = [answer['answer_id'] for answer in answers if answer['resource']['label'] == receiver][0]
85 |
86 | return actual_answer
87 |
88 | def test_04_1(self, answers, metadata):
89 | """
90 | Indicating the state of connections via updates received from the
91 | IS-04 Query API
92 | Second question
93 | """
94 | # Use the NCuT to identify the sender currently connected to receiver x
95 | receiver = metadata['receiver']
96 | self.navigate_to_page('Receivers')
97 | self.navigate_to_page(receiver['label'])
98 | sender = self.get_connected_sender()
99 |
100 | actual_answer = [answer['answer_id'] for answer in answers if answer['resource']['label'] == sender][0]
101 |
102 | return actual_answer
103 |
104 | def test_04_2(self, answers, metadata):
105 | """
106 | Indicating the state of connections via updates received from the
107 | IS-04 Query API
108 | Third Question
109 | """
110 | # The connection on the following receiver will be disconnected
111 | # at a random moment within the next x seconds.
112 | # receiver x
113 | # As soon as the NCuT detects the connection is inactive please
114 | # press the 'Next' button.
115 | # The button must be pressed within x seconds of the connection
116 | # being removed.
117 | # This includes any latency between the connection being removed
118 | # and the NCuT updating.
119 |
120 | receiver = metadata['receiver']
121 | self.navigate_to_page('Receivers')
122 |
123 | # Periodically refresh until no receiver is active
124 | for i in range(1, 20):
125 | time.sleep(4)
126 | self.refresh_page()
127 | receiver = self.get_active_receiver()
128 | if not receiver:
129 | break
130 |
131 |
132 | IS0503tests = IS0503AutoTest()
133 |
--------------------------------------------------------------------------------
/Development/src/pages/senders/SendersList.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Card,
4 | CardContent,
5 | Table,
6 | TableBody,
7 | TableCell,
8 | TableHead,
9 | TableRow,
10 | } from '@material-ui/core';
11 | import { Loading, ShowButton, Title } from 'react-admin';
12 | import ActiveField from '../../components/ActiveField';
13 | import FilterPanel, {
14 | AutocompleteFilter,
15 | BooleanFilter,
16 | StringFilter,
17 | } from '../../components/FilterPanel';
18 | import {
19 | ParameterField,
20 | TRANSPORTS,
21 | parameterAutocompleteProps,
22 | } from '../../components/ParameterRegisters';
23 | import PaginationButtons from '../../components/PaginationButtons';
24 | import ListActions from '../../components/ListActions';
25 | import useGetList from '../../components/useGetList';
26 | import { queryVersion, useJSONSetting } from '../../settings';
27 |
28 | const SendersList = props => {
29 | const [filter, setFilter] = useJSONSetting('Senders Filter');
30 | const [paginationURL, setPaginationURL] = useState(null);
31 | const { data, loaded, pagination, url } = useGetList({
32 | ...props,
33 | filter,
34 | paginationURL,
35 | });
36 | if (!loaded) return ;
37 |
38 | const nextPage = label => {
39 | setPaginationURL(pagination[label]);
40 | };
41 |
42 | return (
43 | <>
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
58 | {queryVersion() >= 'v1.2' && (
59 |
63 | )}
64 |
65 |
66 |
67 |
68 |
69 |
74 | Label
75 |
76 | Transport
77 | {queryVersion() >= 'v1.2' && (
78 | Active
79 | )}
80 |
81 |
82 |
83 | {data.map(item => (
84 |
85 |
86 |
95 |
96 |
97 |
102 |
103 | {queryVersion() >= 'v1.2' && (
104 |
105 |
109 |
110 | )}
111 |
112 | ))}
113 |
114 |
115 |
116 |
121 |
122 |
123 | >
124 | );
125 | };
126 |
127 | export default SendersList;
128 |
--------------------------------------------------------------------------------
/Development/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (isLocalhost) {
36 | // This is running on localhost. Lets check if a service worker still exists or not.
37 | checkValidServiceWorker(swUrl);
38 |
39 | // Add some additional logging to localhost, pointing developers to the
40 | // service worker/PWA documentation.
41 | navigator.serviceWorker.ready.then(() => {
42 | console.log(
43 | 'This web app is being served cache-first by a service ' +
44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ'
45 | );
46 | });
47 | } else {
48 | // Is not local host. Just register service worker
49 | registerValidSW(swUrl);
50 | }
51 | });
52 | }
53 | }
54 |
55 | function registerValidSW(swUrl) {
56 | navigator.serviceWorker
57 | .register(swUrl)
58 | .then(registration => {
59 | registration.onupdatefound = () => {
60 | const installingWorker = registration.installing;
61 | installingWorker.onstatechange = () => {
62 | if (installingWorker.state === 'installed') {
63 | if (navigator.serviceWorker.controller) {
64 | // At this point, the old content will have been purged and
65 | // the fresh content will have been added to the cache.
66 | // It's the perfect time to display a "New content is
67 | // available; please refresh." message in your web app.
68 | console.log(
69 | 'New content is available; please refresh.'
70 | );
71 | } else {
72 | // At this point, everything has been precached.
73 | // It's the perfect time to display a
74 | // "Content is cached for offline use." message.
75 | console.log('Content is cached for offline use.');
76 | }
77 | }
78 | };
79 | };
80 | })
81 | .catch(error => {
82 | console.error('Error during service worker registration:', error);
83 | });
84 | }
85 |
86 | function checkValidServiceWorker(swUrl) {
87 | // Check if the service worker can be found. If it can't reload the page.
88 | fetch(swUrl)
89 | .then(response => {
90 | // Ensure service worker exists, and that we really are getting a JS file.
91 | if (
92 | response.status === 404 ||
93 | response.headers.get('content-type').indexOf('javascript') ===
94 | -1
95 | ) {
96 | // No service worker found. Probably a different app. Reload the page.
97 | navigator.serviceWorker.ready.then(registration => {
98 | registration.unregister().then(() => {
99 | window.location.reload();
100 | });
101 | });
102 | } else {
103 | // Service worker found. Proceed as normal.
104 | registerValidSW(swUrl);
105 | }
106 | })
107 | .catch(() => {
108 | console.log(
109 | 'No internet connection found. App is running in offline mode.'
110 | );
111 | });
112 | }
113 |
114 | export function unregister() {
115 | if ('serviceWorker' in navigator) {
116 | navigator.serviceWorker.ready.then(registration => {
117 | registration.unregister();
118 | });
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # An NMOS Client in JavaScript [][build-test]
2 | [build-test]: https://github.com/sony/nmos-js/actions?query=workflow%3Abuild-test
3 |
4 | ## Introduction
5 |
6 | This repository contains a client implementation of the [AMWA Networked Media Open Specifications](https://github.com/AMWA-TV/nmos) in JavaScript, [licensed](LICENSE) under the terms of the Apache License 2.0.
7 |
8 | - [AMWA IS-04 NMOS Discovery and Registration Specification](https://amwa-tv.github.io/nmos-discovery-registration)
9 | - [AMWA IS-05 NMOS Device Connection Management Specification](https://amwa-tv.github.io/nmos-device-connection-management)
10 | - [AMWA IS-08 NMOS Audio Channel Mapping Specification](https://specs.amwa.tv/is-08/) (read-only for now)
11 | - [AMWA BCP-004-01 NMOS Receiver Capabilities](https://specs.amwa.tv/bcp-004-01/)
12 |
13 | For more information about AMWA, NMOS and the Networked Media Incubator, please refer to http://amwa.tv/.
14 |
15 | The [repository structure](Documents/Repository-Structure.md), and the [external dependencies](Documents/Dependencies.md), are outlined in the documentation.
16 |
17 | ### Getting Started With NMOS
18 |
19 | The [Easy-NMOS](https://github.com/rhastie/easy-nmos) starter kit allows the user to launch a simple NMOS setup with minimal installation steps.
20 | It relies on nmos-js to provide an NMOS Client that works with an NMOS Registry and a virtual NMOS Node in a Docker Compose network, along with the AMWA NMOS Testing Tool and supporting services.
21 |
22 | ### Getting Started For Developers
23 |
24 | Easy-NMOS is also a great first way to explore the relationship between NMOS services.
25 |
26 | The nmos-js codebase is intended to work with any NMOS Registry, but can take advantage of the features of the [nmos-cpp](https://github.com/sony/nmos-cpp) implementation.
27 |
28 | After setting up the dependencies, follow these [instructions](Documents/Getting-Started.md) to build and deploy the nmos-js client itself.
29 |
30 | The web application can also be packaged and deployed with the [nmos-cpp-registry application](https://github.com/sony/nmos-cpp).
31 | Copy the contents of the nmos-js build directory into the admin directory next to the nmos-cpp-registry executable.
32 |
33 | ## Agile Development
34 |
35 | [
](https://jt-nm.org/jt-nm_tested/)
36 |
37 | The nmos-js client, like the NMOS Specifications, is intended to be always ready, but steadily developing.
38 | The nmos-js client works as both an NMOS Registry browser, using the IS-04 Query API, and provides connection management, using the IS-05 Connection API.
39 | When used with the nmos-cpp-registry, it also provides access to registry log messages.
40 | It has been successfully tested in many AMWA Networked Media Incubator workshops, and in the [JT-NM Tested](https://jt-nm.org/jt-nm_tested/) programme.
41 |
42 | ### Build Status
43 |
44 | The following configurations, defined by the [build-test](.github/workflows/build-test.yml) jobs, are built and unit tested automatically via continuous integration.
45 |
46 | | Platform | Version | Browser | Test Options |
47 | |----------|---------------------------|------------------------------------|--------------------------------------------------------------------|
48 | | Linux | Ubuntu 22.04 | Chrome | |
49 |
50 | The [AMWA NMOS API Testing Tool](https://github.com/AMWA-TV/nmos-testing) controller tests are automatically run against the nmos-js user interface using a fully automated [TestingFacade](TestingFacade/README.md).
51 |
52 | **Test Suite/Status:**
53 | [![IS-04-04][IS-04-04-badge]][IS-04-04-sheet]
54 | [![IS-05-03][IS-05-03-badge]][IS-05-03-sheet]
55 |
56 | [IS-04-04-badge]: https://raw.githubusercontent.com/sony/nmos-js/badges/IS-04-04.svg
57 | [IS-05-03-badge]: https://raw.githubusercontent.com/sony/nmos-js/badges/IS-05-03.svg
58 |
59 | [IS-04-04-sheet]: https://docs.google.com/spreadsheets/d/1104JkWwsOp8ql4x0qc-I7nGZqD7mCSBJ__aUIw5AFfw/edit?gid=1975955785
60 | [IS-05-03-sheet]: https://docs.google.com/spreadsheets/d/1104JkWwsOp8ql4x0qc-I7nGZqD7mCSBJ__aUIw5AFfw/edit?gid=946113228
61 |
62 | ### Recent Activity
63 |
64 | The implementation is designed to be extended. Development is ongoing, following the evolution of the NMOS specifications in the AMWA Networked Media Incubator.
65 |
66 | Recent activity on the project (newest first):
67 |
68 | - Added prototype [IS-12 Device Model browser client](is12-client/README.md).
69 | - Added [automated controller testing](TestingFacade/README.md) for nmos-js.
70 | - Read-only support for IS-08 Audio Channel Mapping
71 | - Support for BCP-004-01 Receiver Capabilities
72 | - JT-NM Tested 03/20 badge (packaged and deployed on a Mellanox SN2010 Switch)
73 | - Improved and simplified connection management
74 | - Periodic refresh
75 | - Lots of other incremental improvements
76 | - Added support for multi-homed Nodes
77 | - Added connection management support for RTP, WebSocket and MQTT transports
78 | - Added a dark theme
79 | - Switched to [react-admin](https://github.com/marmelab/react-admin) framework from [ng-admin](https://github.com/marmelab/ng-admin)
80 |
81 | ## Contributing
82 |
83 | We welcome bug reports, feature requests and contributions to the implementation and documentation.
84 | Please have a look at the simple [Contribution Guidelines](CONTRIBUTING.md).
85 |
86 | Thank you for your interest!
87 |
88 | 
89 |
--------------------------------------------------------------------------------
/Development/src/pages/subscriptions/SubscriptionsList.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Card,
4 | CardContent,
5 | Table,
6 | TableBody,
7 | TableCell,
8 | TableHead,
9 | TableRow,
10 | } from '@material-ui/core';
11 | import { BooleanField, Loading, ShowButton, Title } from 'react-admin';
12 | import get from 'lodash/get';
13 | import DeleteButton from '../../components/DeleteButton';
14 | import FilterPanel, {
15 | AutocompleteFilter,
16 | BooleanFilter,
17 | NumberFilter,
18 | StringFilter,
19 | } from '../../components/FilterPanel';
20 | import PaginationButtons from '../../components/PaginationButtons';
21 | import ListActions from '../../components/ListActions';
22 | import useGetList from '../../components/useGetList';
23 | import { useJSONSetting } from '../../settings';
24 |
25 | const SubscriptionsList = props => {
26 | const [filter, setFilter] = useJSONSetting('Subscriptions Filter');
27 | const [paginationURL, setPaginationURL] = useState(null);
28 | const { data, loaded, pagination, url } = useGetList({
29 | ...props,
30 | filter,
31 | paginationURL,
32 | });
33 | if (!loaded) return ;
34 |
35 | const nextPage = label => {
36 | setPaginationURL(pagination[label]);
37 | };
38 |
39 | return (
40 | <>
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
54 |
55 |
63 |
64 |
65 |
66 |
67 |
68 |
73 | Resource Path
74 |
75 | Persist
76 | Max Update Rate (ms)
77 |
78 |
79 |
80 |
81 | {data.map(item => (
82 |
83 |
84 |
92 |
93 |
94 |
98 |
99 |
100 | {item.max_update_rate_ms}
101 |
102 |
103 | {get(item, 'persist') && (
104 |
109 | )}
110 |
111 |
112 | ))}
113 |
114 |
115 |
120 |
121 |
122 | >
123 | );
124 | };
125 |
126 | const resourcePaths = [
127 | '/nodes',
128 | '/devices',
129 | '/sources',
130 | '/flows',
131 | '/senders',
132 | '/receivers',
133 | ];
134 |
135 | export default SubscriptionsList;
136 |
--------------------------------------------------------------------------------
/Development/src/pages/senders/SendersEdit.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import { Link, Route } from 'react-router-dom';
3 | import { Paper, Tab, Tabs } from '@material-ui/core';
4 | import {
5 | BooleanInput,
6 | Edit,
7 | FormDataConsumer,
8 | SelectInput,
9 | SimpleForm,
10 | TextInput,
11 | } from 'react-admin';
12 | import get from 'lodash/get';
13 | import set from 'lodash/set';
14 | import { useTheme } from '@material-ui/styles';
15 | import ClearIcon from '@material-ui/icons/Clear';
16 | import ConnectionEditActions from '../../components/ConnectionEditActions';
17 | import ConnectionEditToolbar from '../../components/ConnectionEditToolbar';
18 | import ResourceTitle from '../../components/ResourceTitle';
19 | import SenderTransportParamsCardsGrid from './SenderTransportParams';
20 |
21 | const SendersEdit = props => {
22 | const theme = useTheme();
23 | const tabBackgroundColor =
24 | theme.palette.type === 'light'
25 | ? theme.palette.grey[100]
26 | : theme.palette.grey[900];
27 | return (
28 | <>
29 |
30 |
36 |
41 |
46 |
51 |
57 |
62 |
63 |
64 |
65 |
66 |
67 | }
71 | />
72 | >
73 | );
74 | };
75 |
76 | const EditStagedTab = props => (
77 | }
81 | actions={}
82 | >
83 | }
85 | redirect={`/senders/${props.id}/show/staged`}
86 | >
87 |
88 |
92 | },
97 | {
98 | id: 'activate_immediate',
99 | name: 'activate_immediate',
100 | },
101 | {
102 | id: 'activate_scheduled_relative',
103 | name: 'activate_scheduled_relative',
104 | },
105 | {
106 | id: 'activate_scheduled_absolute',
107 | name: 'activate_scheduled_absolute',
108 | },
109 | ]}
110 | translateChoice={false}
111 | />
112 |
113 | {({ formData, ...rest }) => {
114 | switch (get(formData, '$staged.activation.mode')) {
115 | case 'activate_scheduled_relative':
116 | return (
117 |
122 | );
123 | case 'activate_scheduled_absolute':
124 | return (
125 |
130 | );
131 | default:
132 | set(
133 | formData,
134 | '$staged.activation.requested_time',
135 | null
136 | );
137 | return null;
138 | }
139 | }}
140 |
141 |
142 |
143 |
144 | );
145 |
146 | export default SendersEdit;
147 |
--------------------------------------------------------------------------------
/TestingFacade/GenericAutoTest.py:
--------------------------------------------------------------------------------
1 | import time
2 | from selenium import webdriver
3 | from selenium.webdriver.common.keys import Keys
4 | from selenium.common.exceptions import NoSuchElementException
5 | from selenium.webdriver.common.by import By
6 | from selenium.webdriver.support import expected_conditions as EC
7 | from selenium.webdriver.support.ui import WebDriverWait
8 | import Config as CONFIG
9 |
10 | class GenericAutoTest:
11 | """
12 | Base test class for automated version of NMOS Controller test suite without Testing Façade
13 | """
14 | def __init__(self):
15 | self.NCuT_url = CONFIG.NCUT_URL
16 | self.mock_registry_url = CONFIG.MOCK_REGISTRY_URL
17 | self.multipart_question_storage = {}
18 |
19 | def set_up_test(self):
20 | # Set up webdriver
21 | browser = getattr(webdriver, CONFIG.BROWSER)
22 | get_options = getattr(webdriver, CONFIG.BROWSER + 'Options', False)
23 | if get_options:
24 | options = get_options()
25 | if CONFIG.HEADLESS:
26 | options.add_argument("--headless=new")
27 | options.add_argument("--window-size=1920,1080");
28 | options.add_argument("--start-maximized");
29 | self.driver = browser(options=options)
30 | else:
31 | self.driver = browser()
32 | self.driver.implicitly_wait(CONFIG.WAIT_TIME)
33 | # Launch browser, navigate to nmos-js and update query api url to mock registry
34 | self.driver.get(self.NCuT_url + "Settings")
35 | query_api = self.driver.find_element(By.NAME, "queryapi")
36 | query_api.clear()
37 | if query_api.get_attribute('value') != '':
38 | time.sleep(1)
39 | query_api.send_keys(Keys.CONTROL + "a")
40 | query_api.send_keys(Keys.DELETE)
41 | query_api.send_keys(self.mock_registry_url + "x-nmos/query/v1.3")
42 |
43 | # Ensure that RQL is switched off
44 | use_rql = self.driver.find_element(By.NAME, "userql")
45 | if use_rql.get_attribute('checked') == "true":
46 | use_rql.click()
47 |
48 | # Open menu to show link names if not already open
49 | open_menu = self.driver.find_elements(By.XPATH, '//*[@title="Open menu"]')
50 | if open_menu:
51 | open_menu[0].click()
52 |
53 | def tear_down_test(self):
54 | self.driver.close()
55 |
56 | def refresh_page(self):
57 | """
58 | Click refresh button and sleep to allow loading time
59 | """
60 | refresh = WebDriverWait(self.driver, 20).until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[aria-label='Refresh']")))
61 | refresh.click()
62 | time.sleep(1)
63 |
64 | def navigate_to_page(self, page):
65 | """
66 | Navigate to page by link text, refresh page and sleep to allow loading time
67 | """
68 | self.driver.find_element(By.LINK_TEXT, page).click()
69 | self.refresh_page()
70 |
71 | def find_resource_labels(self):
72 | """
73 | Find all resources on a page by label
74 | Returns list of labels
75 | """
76 | resources = self.driver.find_elements(By.NAME, "label")
77 | return [entry.text for entry in resources]
78 |
79 | def next_page(self):
80 | """
81 | Navigate to next page via next button and sleep to allow loading time
82 | """
83 | self.driver.find_element(By.NAME, "next").click()
84 | time.sleep(1)
85 |
86 | def check_connectable(self):
87 | """
88 | Check if connect tab is active
89 | returns True if available, False if disabled
90 | """
91 | connect_button = WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located((By.NAME,
92 | "connect")))
93 | disabled = connect_button.get_attribute("aria-disabled")
94 | return True if disabled == 'false' else False
95 |
96 | def make_connection(self, sender):
97 | """
98 | Navigate to connect tab, activate connection to given sender
99 | """
100 | connect = WebDriverWait(self.driver, 20).until(EC.element_to_be_clickable((By.NAME, "connect")))
101 | connect.click()
102 |
103 | # Find the row containing the correct sender and activate connection
104 | senders = self.find_resource_labels()
105 | row = [i for i, s in enumerate(senders) if s == sender][0]
106 | activate_button = self.driver.find_elements(By.NAME, "activate")[row]
107 | activate_button.click()
108 | time.sleep(2)
109 |
110 | def remove_connection(self, receiver):
111 | """
112 | Deactivate a connection on a given receiver
113 | """
114 | receivers = self.find_resource_labels()
115 | row = [i for i, r in enumerate(receivers) if r == receiver][0]
116 | deactivate_button = self.driver.find_elements(By.NAME, "active")[row]
117 | if deactivate_button.get_attribute('value') == "true":
118 | deactivate_button.click()
119 | time.sleep(2)
120 |
121 | def get_active_receiver(self):
122 | """
123 | Identify an active receiver
124 | Returns string of receiver label or None
125 | """
126 | active_buttons = self.driver.find_elements(By.NAME, 'active')
127 | active_row = [i for i, b in enumerate(active_buttons) if b.get_attribute('value') == "true"]
128 | return None if not active_row else self.driver.find_elements(By.NAME, 'label')[active_row[0]].text
129 |
130 | def get_connected_sender(self):
131 | """
132 | Identify the sender a receiver is connected to
133 | Returns string of sender label
134 | """
135 | active = WebDriverWait(self.driver, 20).until(EC.element_to_be_clickable((By.NAME, "active")))
136 | active.click()
137 |
138 | return self.driver.find_element(By.NAME, "sender").text
--------------------------------------------------------------------------------