├── .sass-lint.yml ├── .gitignore ├── src ├── panel │ ├── components │ │ ├── Seekbar.scss │ │ ├── Connector.scss │ │ ├── Visualisation.scss │ │ ├── InfoTable.scss │ │ ├── DetachedWarning.scss │ │ ├── Loading.jsx │ │ ├── InfoTable.jsx │ │ ├── Controls.scss │ │ ├── Loading.scss │ │ ├── DetachedWarning.jsx │ │ ├── App.scss │ │ ├── Controls.jsx │ │ ├── TabManager.scss │ │ ├── Connector.jsx │ │ ├── Visualisation.jsx │ │ ├── Seekbar.jsx │ │ ├── TabManager.jsx │ │ └── App.jsx │ ├── io │ │ ├── index.js │ │ ├── MockPageConnection.js │ │ ├── PageConnection.js │ │ └── mock.json │ ├── config │ │ ├── _animations.scss │ │ └── _colours.scss │ ├── img │ │ ├── spinner.svg │ │ ├── play.svg │ │ └── pause.svg │ ├── index.html │ ├── index.js │ ├── index.scss │ └── utils.js ├── devtools.js ├── manifest.json └── devtools.html ├── demo.gif ├── docs ├── videocontext-devtools_0.0.10.crx ├── updates.xml └── index.html ├── .babelrc ├── scripts ├── publish.sh └── build_gh_pages.js ├── README.md ├── .eslintrc.json ├── LICENSE.md ├── package.json └── webpack.config.js /.sass-lint.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | *.log 4 | *.pem 5 | -------------------------------------------------------------------------------- /src/panel/components/Seekbar.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/videocontext-devtools/HEAD/demo.gif -------------------------------------------------------------------------------- /src/devtools.js: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.create( 2 | 'VideoContext', 3 | 'icon.png', 4 | 'panel/index.html', 5 | ) 6 | -------------------------------------------------------------------------------- /docs/videocontext-devtools_0.0.10.crx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbc/videocontext-devtools/HEAD/docs/videocontext-devtools_0.0.10.crx -------------------------------------------------------------------------------- /src/panel/components/Connector.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | box-sizing: border-box; 3 | height: 100%; 4 | padding: 1em; 5 | position: absolute; 6 | width: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /src/panel/io/index.js: -------------------------------------------------------------------------------- 1 | import PageConnection from './PageConnection' 2 | import MockPageConnection from './MockPageConnection' 3 | 4 | export { PageConnection, MockPageConnection } 5 | -------------------------------------------------------------------------------- /src/panel/config/_animations.scss: -------------------------------------------------------------------------------- 1 | %button-animation { 2 | transition-duration: .3s; 3 | transition-property: background-color, fill, color; 4 | transition-timing-function: ease-out; 5 | } 6 | -------------------------------------------------------------------------------- /src/panel/components/Visualisation.scss: -------------------------------------------------------------------------------- 1 | .vis { 2 | height: 100%; 3 | position: absolute; 4 | width: 100%; 5 | } 6 | 7 | .rendering { 8 | height: 100%; 9 | width: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /src/panel/components/InfoTable.scss: -------------------------------------------------------------------------------- 1 | %cell { 2 | padding: .2em; 3 | } 4 | 5 | .key { 6 | @extend %cell; 7 | font-weight: bold; 8 | text-align: right; 9 | } 10 | 11 | .value { 12 | @extend %cell; 13 | } 14 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "version": null, 4 | "name": "VideoContext", 5 | "devtools_page": "devtools.html", 6 | "update_url": "https://bbc.github.io/videocontext-devtools/updates.xml" 7 | } 8 | -------------------------------------------------------------------------------- /src/panel/config/_colours.scss: -------------------------------------------------------------------------------- 1 | $black: #454545; 2 | $white: #fff; 3 | $detached-colour: #ffdfdf; 4 | $body-bg-colour: #fff; 5 | $transparent: rgba(255, 255, 255, 0); 6 | $colour-primary: #4cee7e; 7 | $tab-colour-inactive: #eee; 8 | -------------------------------------------------------------------------------- /src/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/panel/img/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["latest", "react"], 3 | "plugins": [["react-css-modules", { 4 | "filetypes": { 5 | ".scss": { 6 | "syntax": "postcss-scss" 7 | } 8 | } 9 | }]] 10 | } 11 | -------------------------------------------------------------------------------- /src/panel/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/panel/components/DetachedWarning.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | align-items: center; 3 | display: flex; 4 | flex-flow: row nowrap; 5 | } 6 | 7 | .copy { 8 | font-style: italic; 9 | margin: 0; 10 | padding-right: 1em; 11 | } 12 | 13 | .bold { 14 | font-weight: bold; 15 | } 16 | -------------------------------------------------------------------------------- /docs/updates.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/panel/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import IconSVG from 'svg-inline-react' 3 | import spinner from '../img/spinner.svg' 4 | import './Loading.scss' 5 | 6 | export default () => ( 7 |
8 |
9 |
Connecting...
10 |
11 | ) 12 | -------------------------------------------------------------------------------- /src/panel/index.js: -------------------------------------------------------------------------------- 1 | // Ben Robinson, © BBC Research & Development, 2017 2 | 3 | import 'babel-polyfill' 4 | import ReactDOM from 'react-dom' 5 | import React from 'react' 6 | 7 | import Connector from './components/Connector.jsx' 8 | import './index.scss' 9 | import 'rc-slider/assets/index.css' 10 | 11 | ReactDOM.render( 12 | , 13 | document.getElementById('app'), 14 | ) 15 | -------------------------------------------------------------------------------- /src/panel/components/InfoTable.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './InfoTable.scss' 3 | 4 | export default ({ rows }) => ( 5 | 6 | 7 | {rows.map(row => ( 8 | 9 | 10 | 11 | ))} 12 | 13 |
{row[0]}:{row[1]}
14 | ) 15 | -------------------------------------------------------------------------------- /src/panel/img/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/panel/components/Controls.scss: -------------------------------------------------------------------------------- 1 | @import '../config/colours'; 2 | 3 | $playbutton-size: 4em; 4 | 5 | 6 | .main { 7 | align-items: center; 8 | display: flex; 9 | flex-flow: row nowrap; 10 | flex-grow: 0; 11 | } 12 | 13 | .toggleplay { 14 | background-color: $transparent; 15 | border-radius: 0; 16 | fill: $black; 17 | flex-basis: $playbutton-size; 18 | flex-grow: 0; 19 | height: $playbutton-size; 20 | 21 | &:hover { 22 | background-color: $black; 23 | fill: $white; 24 | } 25 | } 26 | 27 | .seekbar { 28 | flex-grow: 1; 29 | padding: 0 1em; 30 | } 31 | -------------------------------------------------------------------------------- /src/panel/components/Loading.scss: -------------------------------------------------------------------------------- 1 | @import '../config/colours'; 2 | 3 | .main { 4 | align-items: center; 5 | color: $black; 6 | display: flex; 7 | flex-flow: column nowrap; 8 | height: 100%; 9 | justify-content: center; 10 | width: 100%; 11 | } 12 | 13 | @keyframes spin { 14 | from { 15 | transform: rotate(0deg); 16 | } 17 | 18 | to { 19 | transform: rotate(359deg); 20 | } 21 | } 22 | 23 | .spinner { 24 | animation: spin 1s linear infinite; 25 | fill: $black; 26 | height: 3em; 27 | width: 3em; 28 | } 29 | 30 | .text { 31 | font-weight: bold; 32 | padding: 1em; 33 | } 34 | -------------------------------------------------------------------------------- /src/panel/img/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/panel/components/DetachedWarning.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './DetachedWarning.scss' 3 | 4 | export default ({ onClick }) => ( 5 |
6 |

7 | Warning:{` You are now in 'detached' mode. This lets you inspect 8 | the VideoContext graph without it constantly re-rendering while 9 | you inspect it. To get the graph updating again, press the 10 | 'Undetach' button.`} 11 |

12 | 17 |
18 | ) 19 | -------------------------------------------------------------------------------- /src/panel/index.scss: -------------------------------------------------------------------------------- 1 | @import './config/colours'; 2 | @import './config/animations'; 3 | 4 | html { 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | body { 10 | background-color: $body-bg-colour; 11 | color: $black; 12 | font-family: sans-serif; 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | button { 18 | @extend %button-animation; 19 | background-color: $tab-colour-inactive; 20 | border: 0; 21 | border-radius: .4em; 22 | cursor: pointer; 23 | font-size: 1.2em; 24 | margin: 0; 25 | outline: none; 26 | padding: 1em; 27 | text-align: center; 28 | 29 | &:hover { 30 | background-color: $colour-primary; 31 | color: $white; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/panel/components/App.scss: -------------------------------------------------------------------------------- 1 | @import '../config/colours'; 2 | @import '../config/animations'; 3 | 4 | .main { 5 | align-items: stretch; 6 | display: flex; 7 | flex-flow: column nowrap; 8 | height: 100%; 9 | justify-content: space-between; 10 | position: absolute; 11 | width: 100%; 12 | } 13 | 14 | %vis { 15 | flex-basis: 1px; 16 | flex-grow: 1; 17 | position: relative; 18 | } 19 | 20 | .vis { 21 | @extend %vis; 22 | } 23 | 24 | .vis-detached { 25 | @extend %vis; 26 | background-color: $detached-colour; 27 | } 28 | 29 | .detached-warning { 30 | align-self: center; 31 | padding: 1em 0; 32 | } 33 | 34 | .other-info { 35 | align-items: center; 36 | align-self: center; 37 | display: flex; 38 | flex-flow: row nowrap; 39 | } 40 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PACKAGE_VERSION=$(node -p "require('./package.json').version") 3 | 4 | echo "Current version is $PACKAGE_VERSION"; 5 | if [[ -z $(git status --porcelain) ]]; then 6 | yarn version --no-git-tag-version 7 | NEW_PACKAGE_VERSION=$(node -p "require('./package.json').version") 8 | 9 | echo "Building JS..." 10 | rimraf dist 11 | yarn run build 12 | 13 | echo "Packing to .crx..." 14 | yarn run build_crx 15 | 16 | echo "Committing the chages..." 17 | git add -A 18 | git commit -m "v$NEW_PACKAGE_VERSION" 19 | git tag v$NEW_PACKAGE_VERSION 20 | 21 | echo "" 22 | echo "Successfully packed version $NEW_PACKAGE_VERSION!" 23 | else 24 | echo "Working tree is not clean! Commit your changes and then try again." 25 | fi 26 | -------------------------------------------------------------------------------- /src/panel/io/MockPageConnection.js: -------------------------------------------------------------------------------- 1 | import mock from './mock.json' 2 | 3 | export default class MockPageConnection { 4 | async requestJSONFromBackground () { 5 | return new Promise((res) => { 6 | setTimeout(() => { 7 | res(mock) 8 | }, 500) 9 | }) 10 | } 11 | 12 | async togglePlay (id) { 13 | console.log(`Toggle play called for id ${id}. This does nothing.`) 14 | } 15 | 16 | async seek (id, time) { 17 | console.log(`seek called with id ${id} and time ${time}. This does nothing.`) 18 | } 19 | 20 | async highlightElement (id) { 21 | console.log(`highlightElement called with id ${id}`) 22 | } 23 | 24 | async unhighlightElement (id) { 25 | console.log(`unhighlightElement called with id ${id}`) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/panel/components/Controls.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import IconSVG from 'svg-inline-react' 3 | 4 | import Seekbar from './Seekbar.jsx' 5 | import play from '../img/play.svg' 6 | import pause from '../img/pause.svg' 7 | import { formatTime } from '../utils' 8 | import './Controls.scss' 9 | 10 | export default ({ currentTime, duration, state, onSeek, togglePlay }) => ( 11 |
12 | 18 |
19 | 23 |
24 | 25 | {formatTime(currentTime)} / {formatTime(duration)} 26 | 27 |
28 | ) 29 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Install VideoContext dev tools 6 | 7 | 8 |

VideoContext Chrome DevTools Extension

9 |

To install the VideoContext DevTools extension (version 0.0.10), follow these steps: 10 |

    11 |
  1. Download this file.
  2. 12 |
  3. Go to chrome://extensions
  4. 13 |
  5. Drag the .crx file you just downloaded onto the extensions page.
  6. 14 |
15 |

16 |

Once you've done this, open your dev tools panel and look for the VideoContext dev tools.

17 |

You only have to do this procedure once - from now on your browser should automatically keep the extension up to date.

18 | 19 | 20 | -------------------------------------------------------------------------------- /src/panel/components/TabManager.scss: -------------------------------------------------------------------------------- 1 | @import '../config/colours'; 2 | 3 | $tab-spacing: 1em; 4 | 5 | .main { 6 | display: flex; 7 | flex-flow: column nowrap; 8 | height: 100%; 9 | position: relative; 10 | } 11 | 12 | .tabbar { 13 | display: flex; 14 | flex-flow: row nowrap; 15 | font-family: monospace; 16 | font-size: 1.2em; 17 | justify-content: space-around; 18 | margin: 0; 19 | padding-bottom: 1em; 20 | } 21 | 22 | %tab { 23 | flex-basis: 100%; 24 | flex-grow: 1; 25 | font-family: monospace; 26 | margin: 0 $tab-spacing; 27 | } 28 | 29 | %tab-active { 30 | background-color: $colour-primary; 31 | color: $white; 32 | } 33 | 34 | .tab { 35 | @extend %tab; 36 | 37 | &:hover { 38 | @extend %tab-active; 39 | } 40 | } 41 | 42 | .tab-active { 43 | @extend %tab; 44 | @extend %tab-active; 45 | cursor: not-allowed; 46 | } 47 | 48 | .app { 49 | flex-grow: 1; 50 | position: relative; 51 | } 52 | -------------------------------------------------------------------------------- /src/panel/utils.js: -------------------------------------------------------------------------------- 1 | export const leftPad = (num, size) => (`000000000${num}`).substr(-size) 2 | 3 | export const toTwoDecimalPlaces = num => Math.round(num * 100) / 100 4 | 5 | export const formatTime = (time) => { 6 | const minutes = Math.floor(time / 60) 7 | const seconds = Math.floor(time - (minutes * 60)) 8 | return `${minutes}:${leftPad(toTwoDecimalPlaces(seconds), 2)}` 9 | } 10 | 11 | export const maxStopTimeForNodes = nodes => Math.max(...nodes 12 | .map(n => n.stop) 13 | .filter(t => t != null)) // filter out any that we don't yet know the time for. 14 | 15 | export const convertStateEnum = (num) => { 16 | switch (num) { 17 | case 0: 18 | return 'Playing' 19 | case 1: 20 | return 'Paused' 21 | case 2: 22 | return 'Stalled' 23 | case 3: 24 | return 'Ended' 25 | case 4: 26 | return 'Broken' 27 | default: 28 | return `ERROR: unknown state with code ${num}` 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/panel/components/Connector.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { PageConnection as LivePageConnection, MockPageConnection } from '../io' 3 | import TabManager from './TabManager.jsx' 4 | import Loading from './Loading.jsx' 5 | import './Connector.scss' 6 | 7 | let PageConnection = LivePageConnection 8 | 9 | if (process.env.NODE_ENV !== 'production') { 10 | PageConnection = MockPageConnection 11 | } 12 | 13 | export default class Connector extends React.Component { 14 | constructor (props) { 15 | super(props) 16 | this.conn = new PageConnection() 17 | this.state = { 18 | json: null, 19 | } 20 | } 21 | componentDidMount () { 22 | this._timer = setInterval(async () => { 23 | const json = await this.conn.requestJSONFromBackground() 24 | this.setState({ json }) 25 | }, 100) 26 | } 27 | componentWillUnmount () { 28 | clearInterval(this._timer) 29 | } 30 | 31 | render () { 32 | return (
33 | {this.state.json ? : 34 | } 35 |
) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VideoContext Dev Tools 2 | 3 | A handy addition to your Chrome Dev Tools for VideoContext. **Requires VideoContext v0.50+**. 4 | 5 | ![demo](./demo.gif) 6 | 7 | To install it, follow the instructions at . Once the extension is installed, open an app with a running instance of VideoContext (you can find a ready-made one [here](http://bbc.github.io/VideoContext/examples/transitions.html)), open your Chrome DevTools and navigate to the VideoContext DevTools tab. 8 | 9 | ## Identifying running instances 10 | 11 | VideoContext automatically registers instances with the Dev Tools and identifies it with an amusing randomly generated name. If you want to manually specify the name that appears in the Dev Tools, then set the VideoContext ID: 12 | 13 | ```js 14 | const ctx = new VideoContext(canvas) 15 | ctx.id = "my chosen id" 16 | ``` 17 | 18 | ## Developing 19 | This extension is formed of two parts - the frontend (the user-facing UI), and the backend (underlying machinery to allow the dev tools extension to pull data from the DOM about running VideoContext instances). 20 | 21 | To develop the frontend, run `yarn start`. This starts a webpack dev server which lets you tweak the frontend (which is backed by a mock data source). 22 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "airbnb" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": true, 9 | "jest": true 10 | }, 11 | "globals": { 12 | "chrome": true 13 | }, 14 | "rules": { 15 | "semi": ["error", "never"], 16 | "indent": ["error", 4], 17 | "space-before-function-paren": ["error", "always"], 18 | "class-methods-use-this": 0, 19 | "no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }], 20 | "no-underscore-dangle": 0, 21 | "no-console": 0, 22 | "no-constant-condition": [1, { "checkLoops": false }], 23 | "no-unused-vars": 1, 24 | "react/jsx-indent": [2, 4], 25 | "react/jsx-indent-props": [2, 4], 26 | "react/sort-comp": [1, { 27 | "order": [ 28 | "type-annotations", 29 | "static-methods", 30 | "lifecycle", 31 | "everything-else", 32 | "render" 33 | ] 34 | }], 35 | "react/jsx-filename-extension": 0, 36 | "react/prop-types": 0, 37 | "import/first": 0, 38 | "import/no-unresolved": 0, 39 | "import/no-extraneous-dependencies": 0, 40 | "import/extensions": 0, 41 | "import/prefer-default-export": 1 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/panel/components/Visualisation.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import VideoContextVisualisation from 'visualise-videocontext' 3 | import './Visualisation.scss' 4 | 5 | export default class Visualisation extends React.Component { 6 | componentDidMount () { 7 | const colours = { 8 | active: '#4CEE7E', 9 | inactive: '#6CA97F', 10 | error: '#F3516C', 11 | processing: '#EE4CBC', 12 | destination: '#000', 13 | } 14 | this._vis = new VideoContextVisualisation(this._ref, colours) 15 | this._vis.setData(this.props.json) 16 | this._vis.render() 17 | } 18 | componentWillUpdate () { 19 | if (!this.props.detached) { 20 | this._vis.setData(this.props.json) 21 | this._vis.render() 22 | } 23 | } 24 | componentWillUnmount () { 25 | this._vis.destroy() 26 | } 27 | render () { 28 | return ( 29 |
34 |
{ this._ref = ref }} 36 | styleName="rendering" 37 | /> 38 |
39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/panel/components/Seekbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Slider from 'rc-slider' 3 | import './Seekbar.scss' 4 | 5 | export default class Seekbar extends React.Component { 6 | constructor (props) { 7 | super(props) 8 | this.state = { 9 | isUserScrubbing: false, 10 | value: this.props.value, 11 | } 12 | 13 | this.handleSeek = this.handleSeek.bind(this) 14 | } 15 | 16 | handleSeek (value) { 17 | this.setState({ value }) 18 | this.props.onUserSeek(value) 19 | } 20 | 21 | render () { 22 | return ( 23 | this.handleSeek(val)} 30 | onBeforeChange={() => this.setState({ isUserScrubbing: true })} 31 | onAfterChange={() => this.setState({ isUserScrubbing: false })} 32 | tipFormatter={null} 33 | trackStyle={{ backgroundColor: '#454545' }} 34 | handleStyle={{ 35 | borderColor: '#454545', 36 | backgroundColor: '#454545', 37 | }} 38 | /> 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 British Broadcasting Corporation 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videocontext-devtools", 3 | "version": "0.0.10", 4 | "description": "Visualise VideoContext", 5 | "main": "index.js", 6 | "author": "Ben Robinson ", 7 | "license": "BSD-3-Clause", 8 | "scripts": { 9 | "start": "webpack-dev-server", 10 | "build": "webpack -p", 11 | "build_crx": "node scripts/build_gh_pages.js", 12 | "pack": "./scripts/publish.sh" 13 | }, 14 | "repository": "https://github.com/bbc/videocontext-devtools", 15 | "devDependencies": { 16 | "babel-core": "^6.24.1", 17 | "babel-loader": "^7.0.0", 18 | "babel-plugin-react-css-modules": "^3.0.0", 19 | "babel-preset-latest": "^6.24.1", 20 | "babel-preset-react": "^6.24.1", 21 | "copy-webpack-plugin": "^4.0.1", 22 | "css-loader": "^0.28.1", 23 | "eslint": "^3.15.0", 24 | "eslint-config-airbnb": "^14.1.0", 25 | "eslint-loader": "^1.6.3", 26 | "eslint-plugin-flowtype": "^2.30.4", 27 | "eslint-plugin-import": "^2.2.0", 28 | "eslint-plugin-jsx-a11y": "^3.0.2 || ^4.0.0", 29 | "eslint-plugin-react": "^6.9.0", 30 | "node-sass": "^4.5.3", 31 | "postcss-scss": "^1.0.1", 32 | "rc-slider": "^8.1.3", 33 | "rimraf": "^2.6.1", 34 | "sass-loader": "^6.0.6", 35 | "style-loader": "^0.17.0", 36 | "svg-inline-loader": "^0.7.1", 37 | "webpack": "^2.4.1", 38 | "webpack-dev-server": "^2.4.5" 39 | }, 40 | "dependencies": { 41 | "babel-polyfill": "^6.23.0", 42 | "react": "^15.5.4", 43 | "react-dom": "^15.5.4", 44 | "svg-inline-react": "1", 45 | "visualise-videocontext": "^1.0.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/panel/components/TabManager.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import App from './App.jsx' 3 | import './TabManager.scss' 4 | 5 | export default class extends React.Component { 6 | constructor (props) { 7 | super(props) 8 | this.state = { 9 | activeId: Object.keys(this.props.json)[0], 10 | detached: false, 11 | } 12 | } 13 | render () { 14 | return ( 15 |
16 |
17 | {Object.keys(this.props.json).map(id => ( 18 | 32 | ))} 33 |
34 |
35 | this.props.conn.togglePlay(this.state.activeId)} 38 | seek={time => this.props.conn.seek(this.state.activeId, time)} 39 | detached={this.state.detached} 40 | setDetached={(detached) => { this.setState({ detached }) }} 41 | /> 42 |
43 |
44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/panel/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import Visualisation from './Visualisation.jsx' 4 | import InfoTable from './InfoTable.jsx' 5 | import DetachedWarning from './DetachedWarning.jsx' 6 | import Controls from './Controls.jsx' 7 | import { maxStopTimeForNodes, toTwoDecimalPlaces, convertStateEnum } from '../utils' 8 | import './App.scss' 9 | 10 | export default ({ json, setDetached, detached, seek, togglePlay }) => { 11 | const ctx = json.videoContext 12 | const nodes = Object.values(json.nodes) 13 | const duration = ctx.duration || maxStopTimeForNodes(nodes) 14 | return ( 15 |
18 |
19 | { setDetached(true) }} 23 | /> 24 |
25 |
29 | { setDetached(false) }} /> 30 |
31 |
32 | 40 |
41 |
42 | seek(val * duration)} 47 | togglePlay={() => { togglePlay() }} 48 | /> 49 |
50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/panel/io/PageConnection.js: -------------------------------------------------------------------------------- 1 | const STORE_VARIABLE = '__VIDEOCONTEXT_REFS__' 2 | 3 | const runScript = script => new Promise((resolve, reject) => { 4 | chrome.devtools.inspectedWindow.eval(`(function () { ${script} })()`, (result, isException) => { 5 | if (isException) { 6 | console.log(isException) 7 | reject() 8 | } else { 9 | resolve(result) 10 | } 11 | }) 12 | }) 13 | 14 | export default class PageConnection { 15 | async requestJSONFromBackground () { 16 | const json = await runScript(` 17 | if (window.${STORE_VARIABLE}) { 18 | var json = {} 19 | Object.keys(window.${STORE_VARIABLE}).map(id => { 20 | json[id] = ${STORE_VARIABLE}[id].snapshot() 21 | }); 22 | return json 23 | } 24 | return null 25 | `) 26 | 27 | return json 28 | } 29 | 30 | async togglePlay (id) { 31 | const script = ` 32 | var ctxId = "${id}"; 33 | if (window.${STORE_VARIABLE} && window.${STORE_VARIABLE}[ctxId]) { 34 | var ctx = ${STORE_VARIABLE}[ctxId]; 35 | if (ctx.state === 0) { 36 | ctx.pause() 37 | } else { 38 | ctx.play() 39 | } 40 | }` 41 | await runScript(script) 42 | } 43 | 44 | async seek (id, time) { 45 | const script = ` 46 | var ctxId = "${id}"; 47 | if (window.${STORE_VARIABLE} && window.${STORE_VARIABLE}[ctxId]) { 48 | var ctx = ${STORE_VARIABLE}[ctxId]; 49 | ctx.currentTime = ${time}; 50 | }` 51 | await runScript(script) 52 | } 53 | 54 | async _setCanvasOpacity (id, opacity) { 55 | const script = ` 56 | var ctxId = "${id}"; 57 | if (window.${STORE_VARIABLE} && window.${STORE_VARIABLE}[ctxId]) { 58 | var ctx = ${STORE_VARIABLE}[ctxId]; 59 | ctx.element.style.opacity = ${opacity}; 60 | }` 61 | await runScript(script) 62 | } 63 | 64 | async highlightElement (id) { 65 | await this._setCanvasOpacity(id, 0.5) 66 | } 67 | 68 | async unhighlightElement (id) { 69 | await this._setCanvasOpacity(id, 1.0) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const CopyWebpackPlugin = require('copy-webpack-plugin') 3 | const VERSION = require('./package.json').version 4 | 5 | module.exports = { 6 | entry: { 7 | panel: './src/panel/index.js', 8 | '../devtools': './src/devtools.js', 9 | }, 10 | output: { 11 | path: path.resolve(__dirname, 'dist', 'panel'), 12 | filename: '[name].js', 13 | publicPath: '/', 14 | }, 15 | devtool: 'cheap-module-source-map', 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.jsx?$/, 20 | use: 'babel-loader', 21 | include: /src/, 22 | }, 23 | { 24 | test: /\.css$/, 25 | use: [ 26 | { 27 | loader: 'style-loader', 28 | }, 29 | { 30 | loader: 'css-loader', 31 | }, 32 | ], 33 | }, 34 | { 35 | test: /\.scss$/, 36 | use: [ 37 | { 38 | loader: 'style-loader', 39 | }, 40 | { 41 | loader: 'css-loader', 42 | options: { 43 | importLoader: 1, 44 | modules: true, 45 | localIdentName: '[path]___[name]__[local]___[hash:base64:5]', 46 | }, 47 | }, 48 | { 49 | loader: 'sass-loader', 50 | }, 51 | ], 52 | include: /src/, 53 | }, 54 | { 55 | test: /\.svg$/, 56 | use: 'svg-inline-loader', 57 | include: /src/, 58 | }, 59 | ], 60 | }, 61 | plugins: [ 62 | new CopyWebpackPlugin([ 63 | { from: 'src/*.html', to: '../[name].html' }, 64 | { 65 | from: 'src/manifest.json', 66 | to: '../manifest.json', 67 | transform: (content) => { 68 | const manifest = JSON.parse(content) 69 | const updatedManifest = Object.assign({}, manifest, { version: VERSION }) 70 | return JSON.stringify(updatedManifest) 71 | }, 72 | }, 73 | { from: 'src/panel/index.html', to: 'index.html' }, 74 | ]), 75 | ], 76 | devServer: { 77 | contentBase: path.resolve(__dirname, 'dist', 'panel'), 78 | publicPath: '/', 79 | }, 80 | } 81 | -------------------------------------------------------------------------------- /scripts/build_gh_pages.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const path = require('path') 3 | const exec = require('child_process').exec 4 | 5 | const EXT_ID = 'jdeljafnglpkijfpgljefaeldobnobpn' 6 | const PRIVATE_KEY_PATH = './scripts/videocontext-devtools.pem' 7 | const UNPACKED_BUILD_FOLDER = './dist' 8 | const BUILD_FOLDER = './docs' 9 | 10 | const VERSION = JSON.parse( 11 | fs.readFileSync('./package.json'), 12 | ).version 13 | 14 | const CRX_FILENAME = `videocontext-devtools_${VERSION}.crx` 15 | 16 | const updatesXML = ` 17 | 18 | 19 | 23 | 24 | 25 | ` 26 | 27 | const indexHTML = ` 28 | 29 | 30 | 31 | Install VideoContext dev tools 32 | 33 | 34 |

VideoContext Chrome DevTools Extension

35 |

To install the VideoContext DevTools extension (version ${VERSION}), follow these steps: 36 |

    37 |
  1. Download this file.
  2. 38 |
  3. Go to chrome://extensions
  4. 39 |
  5. Drag the .crx file you just downloaded onto the extensions page.
  6. 40 |
41 |

42 |

Once you've done this, open your dev tools panel and look for the VideoContext dev tools.

43 |

You only have to do this procedure once - from now on your browser should automatically keep the extension up to date.

44 | 45 | 46 | ` 47 | 48 | const execPromise = cmd => new Promise((res, rej) => { 49 | exec(cmd, (error) => { 50 | if (error) { 51 | rej(error) 52 | } else { 53 | res() 54 | } 55 | }) 56 | }) 57 | 58 | async function main () { 59 | await fs.emptyDir(BUILD_FOLDER) 60 | 61 | console.log(`Packaging videocontext-devtools version ${VERSION}...`) 62 | console.log('') 63 | console.log('Packing crx...') 64 | 65 | const crxPath = path.join(BUILD_FOLDER, CRX_FILENAME) 66 | await execPromise(`extensionator -d ${UNPACKED_BUILD_FOLDER} -i ${PRIVATE_KEY_PATH} -o ${crxPath}`) 67 | console.log(`Created crx at ${crxPath}`) 68 | 69 | console.log('Creating new updates xml...') 70 | const updatesPath = path.join(BUILD_FOLDER, 'updates.xml') 71 | await fs.writeFile(updatesPath, updatesXML) 72 | console.log(`Wrote XML at ${updatesPath}`) 73 | 74 | console.log('Creating homepage xml...') 75 | const indexPath = path.join(BUILD_FOLDER, 'index.html') 76 | await fs.writeFile(indexPath, indexHTML) 77 | console.log(`Wrote HTML at ${indexPath}`) 78 | } 79 | 80 | main() 81 | -------------------------------------------------------------------------------- /src/panel/io/mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "id1": { 3 | "videoContext": { 4 | "currentTime": 5645.67796679, 5 | "duration": 6326.23522, 6 | "state": 1, 7 | "playbackRate": 1 8 | }, 9 | "nodes": { 10 | "source0": { 11 | "type": "VideoNode", 12 | "url": "http://localhost:8080/f13004eed4251c602bbe15737e8a1ecb.mp4", 13 | "start": 0, 14 | "stop": 20, 15 | "currentTime": 5.67, 16 | "state": "playing" 17 | }, 18 | "processor0": { 19 | "type": "TransitionNode", 20 | "definition": { 21 | "title": "Cross-Fade", 22 | "description": "A cross-fade effect. Typically used as a transistion.", 23 | "vertexShader": "\t attribute vec2 a_position;\t attribute vec2 a_texCoord;\t varying vec2 v_texCoord;\t void main() {\t gl_Position = vec4(vec2(2.0,2.0)*a_position-vec2(1.0, 1.0), 0.0, 1.0);\t v_texCoord = a_texCoord;\t }", 24 | "fragmentShader": "\t precision mediump float;\t uniform sampler2D u_image_a;\t uniform sampler2D u_image_b;\t uniform float mix;\t varying vec2 v_texCoord;\t varying float v_mix;\t void main(){\t vec4 color_a = texture2D(u_image_a, v_texCoord);\t vec4 color_b = texture2D(u_image_b, v_texCoord);\t color_a[0] *= (1.0 - mix);\t color_a[1] *= (1.0 - mix);\t color_a[2] *= (1.0 - mix);\t color_a[3] *= (1.0 - mix);\t color_b[0] *= mix;\t color_b[1] *= mix;\t color_b[2] *= mix;\t color_b[3] *= mix;\t gl_FragColor = color_a + color_b;\t }", 25 | "properties": { 26 | "mix": { 27 | "type": "uniform", 28 | "value": 0 29 | } 30 | }, 31 | "inputs": [ 32 | "u_image_a", 33 | "u_image_b" 34 | ] 35 | }, 36 | "inputs": [ 37 | { 38 | "id": "processor1", 39 | "index": 0 40 | }, 41 | { 42 | "id": "processor2", 43 | "index": 1 44 | } 45 | ], 46 | "properties": { 47 | "mix": 1 48 | }, 49 | "transitions": { 50 | "mix": [ 51 | { 52 | "start": 0, 53 | "end": 0, 54 | "current": 0, 55 | "target": 1, 56 | "property": "mix" 57 | }, 58 | { 59 | "start": 5, 60 | "end": 8, 61 | "current": 1, 62 | "target": 0, 63 | "property": "mix" 64 | } 65 | ] 66 | } 67 | }, 68 | "processor1": { 69 | "type": "EffectNode", 70 | "definition": { 71 | "title": "Monochrome", 72 | "description": "Change images to a single chroma (e.g can be used to make a black & white filter). Input color mix and output color mix can be adjusted.", 73 | "vertexShader": "\t attribute vec2 a_position;\t attribute vec2 a_texCoord;\t varying vec2 v_texCoord;\t void main() {\t gl_Position = vec4(vec2(2.0,2.0)*a_position-vec2(1.0, 1.0), 0.0, 1.0);\t v_texCoord = a_texCoord;\t }", 74 | "fragmentShader": "\t precision mediump float;\t uniform sampler2D u_image;\t uniform vec3 inputMix;\t uniform vec3 outputMix;\t varying vec2 v_texCoord;\t varying float v_mix;\t void main(){\t vec4 color = texture2D(u_image, v_texCoord);\t float mono = color[0]*inputMix[0] + color[1]*inputMix[1] + color[2]*inputMix[2];\t color[0] = mono * outputMix[0];\t color[1] = mono * outputMix[1];\t color[2] = mono * outputMix[2];\t gl_FragColor = color;\t }", 75 | "properties": { 76 | "inputMix": { 77 | "type": "uniform", 78 | "value": [ 79 | 0.4, 80 | 0.6, 81 | 0.2 82 | ] 83 | }, 84 | "outputMix": { 85 | "type": "uniform", 86 | "value": [ 87 | 1, 88 | 1, 89 | 1 90 | ] 91 | } 92 | }, 93 | "inputs": [ 94 | "u_image" 95 | ] 96 | }, 97 | "inputs": [ 98 | { 99 | "id": "source0", 100 | "index": 0 101 | } 102 | ], 103 | "properties": { 104 | "inputMix": [ 105 | 0.4, 106 | 0.6, 107 | 0.2 108 | ], 109 | "outputMix": [ 110 | 1, 111 | 1, 112 | 1 113 | ] 114 | } 115 | }, 116 | "processor2": { 117 | "type": "EffectNode", 118 | "definition": { 119 | "title": "Opacity", 120 | "description": "Sets the opacity of an input.", 121 | "vertexShader": "\n attribute vec2 a_position;\n attribute vec2 a_texCoord;\n varying vec2 v_texCoord;\n void main() {\n gl_Position = vec4(vec2(2.0,2.0)*a_position-vec2(1.0, 1.0), 0.0, 1.0);\n v_texCoord = a_texCoord;\n }", 122 | "fragmentShader": "\n precision mediump float;\n uniform sampler2D u_image;\n uniform float opacity;\n varying vec2 v_texCoord;\n varying float v_opacity;\n void main(){\n vec4 color = texture2D(u_image, v_texCoord);\n color[3] *= opacity;\n gl_FragColor = color;\n }", 123 | "properties": { 124 | "opacity": { 125 | "type": "uniform", 126 | "value": 0.7 127 | } 128 | }, 129 | "inputs": [ 130 | "u_image" 131 | ] 132 | }, 133 | "inputs": [ 134 | { 135 | "id": "source0", 136 | "index": 0 137 | } 138 | ], 139 | "properties": { 140 | "opacity": 0.7 141 | } 142 | }, 143 | "destination": { 144 | "type": "Destination", 145 | "inputs": [ 146 | { 147 | "id": "processor0", 148 | "index": 0 149 | } 150 | ] 151 | } 152 | } 153 | }, 154 | "id2": { 155 | "videoContext": { 156 | "currentTime": 3.556, 157 | "duration": 7.446, 158 | "state": 3, 159 | "playbackRate": 1.3 160 | }, 161 | "nodes": { 162 | "source0": { 163 | "type": "VideoNode", 164 | "url": "http://localhost:8080/f13004eed4251c602bbe15737e8a1ecb.mp4", 165 | "start": 0, 166 | "stop": 20, 167 | "currentTime": 5.67, 168 | "state": "playing" 169 | }, 170 | "destination": { 171 | "type": "Destination", 172 | "inputs": [ 173 | { 174 | "id": "source0", 175 | "index": 0 176 | } 177 | ] 178 | } 179 | } 180 | } 181 | } 182 | --------------------------------------------------------------------------------