Skip to content
This repository has been archived by the owner on Jul 17, 2020. It is now read-only.

Commit

Permalink
feat: NDI support (#111)
Browse files Browse the repository at this point in the history
  • Loading branch information
2xAA authored May 22, 2020
1 parent c355f51 commit de5a67a
Show file tree
Hide file tree
Showing 9 changed files with 386 additions and 14 deletions.
22 changes: 16 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
# Only build tags
if: tag IS present

matrix:
language: node_js
node_js: '10'
jobs:
include:
- os: osx
- name: "Build on macOS for macOS and Windows"
os: osx
osx_image: xcode10.2
language: node_js
node_js: '10'
env:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
- name: "Build on Linux for Linux"
os: linux
dist: xenial
env:
- ELECTRON_CACHE=$HOME/.cache/electron
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
Expand All @@ -15,7 +21,11 @@ cache:
- node_modules
- "$HOME/.cache/electron"
- "$HOME/.cache/electron-builder"
script: yarn run electron:build -mwl --publish=never
before_install:
- if [ "$TRAVIS_OS_NAME" = "linux"]; then wget https://github.com/Palakis/obs-ndi/releases/download/4.9.0/libndi4_4.5.1-1_amd64.deb && sudo dpkg -i libndi4_4.5.1-1_amd64.deb; fi
script:
- if [ "$TRAVIS_OS_NAME" = "osx"]; then yarn run electron:build -mw --publish=never; fi
- if [ "$TRAVIS_OS_NAME" = "linux"]; then yarn run electron:build -l --publish=never; fi
before_cache:
- rm -rf $HOME/.cache/electron-builder/wine
branches:
Expand Down
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,36 @@ modV 3.0 is a complete rewrite of modV 2.0 with a focus on performance and a sta


## Project setup
```
```bash
yarn
```

### Compiles and hot-reloads for development
```
```bash
yarn run electron:serve
```

### Builds for release
```
```bash
yarn run electron:build
```

## Platform specifics for building

### Windows
Windows Platform tools are required. Install them with:
```bash
npm install --global --production windows-build-tools
```

### Ubuntu/Debian
libndi is required for NDI sources and must be installed for modV to build. You can find that available to download here:
[https://github.com/Palakis/obs-ndi/releases](https://github.com/Palakis/obs-ndi/releases)

Last successful build was with `libndi4_4.5.1-1_amd64.deb`.

### Other Linux flavours
Untested. NDI is provided by grandiose, our fork is here: [https://github.com/vcync/grandiose/](https://github.com/vcync/grandiose/)
This fork of grandiose has other libndi supported platforms, however even on Ubuntu we needed the above libndi package to be installed.

Let us know how you get on (good or bad) and we'll update the repo and docs accordingly.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"core-js": "^2.6.10",
"fluent-ffmpeg": "^2.1.2",
"golden-layout": "^1.5.9",
"grandiose": "github:vcync/grandiose#feat/workerCompatibility",
"interactive-shader-format": "github:vcync/interactive-shader-format-js#feat/ImageBitmap",
"lfo-for-modv": "0.0.1",
"lodash.get": "^4.4.2",
Expand Down
5 changes: 5 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
<gl-component title="BPM" :closable="false">
<BPMConfig />
</gl-component>
<gl-component title="NDI" :closable="false">
<NDIConfig />
</gl-component>
</gl-stack>
</gl-stack>

Expand Down Expand Up @@ -88,6 +91,7 @@ import InputConfig from "@/components/InputConfig";
import AudioDeviceConfig from "@/components/InputDeviceConfig/Audio.vue";
import MIDIDeviceConfig from "@/components/InputDeviceConfig/MIDI.vue";
import BPMConfig from "@/components/InputDeviceConfig/BPM.vue";
import NDIConfig from "@/components/InputDeviceConfig/NDI.vue";
import StatusBar from "@/components/StatusBar";
import Control from "@/components/Control";
Expand All @@ -107,6 +111,7 @@ export default {
AudioDeviceConfig,
MIDIDeviceConfig,
BPMConfig,
NDIConfig,
StatusBar,
Control
},
Expand Down
203 changes: 203 additions & 0 deletions src/application/worker/store/modules/ndi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import grandiose from "grandiose";
import uuidv4 from "uuid/v4";
import Vue from "vue";
import store from "../";

const state = {
discovering: false,

receivers: {
// "receiver-uuidv4": {
// reciver: {}, // the receiver instance,
// outputId: "output-uuidv4", // id of an output canvas
// enabled: false // whether we should do anything with the incoming data from this receiver
// }
},

sources: [
// {
// name: "GINGER (Intel(R) HD Graphics 520 1)",
// urlAddress: "169.254.82.1:5962"
// },
// { name: "GINGER (Test Pattern)", urlAddress: "169.254.82.1:5961" },
// {
// name: "GINGER (TOSHIBA Web Camera - HD)",
// urlAddress: "169.254.82.1:5963"
// }
],

discoveryOptions: {
showLocalSources: true
}
};

function checkCpu() {
if (!grandiose.isSupportedCPU()) {
throw new Error("Your CPU is not supported for NDI");
}
}

async function waitForFrame(receiverContext) {
const { receiver, outputId } = receiverContext;
const {
context,
context: { canvas }
} = store.state.outputs.auxillary[outputId];

let dataFrame;

try {
dataFrame = await receiver.data();
} catch (e) {
console.error(e);

if (receiverContext.enabled) {
waitForFrame(receiverContext);
}
}

if (dataFrame.type === "video") {
const { xres, yres, data: uint8array } = dataFrame;
const ui8c = new Uint8ClampedArray(
uint8array.buffer,
uint8array.byteOffset,
uint8array.byteLength / Uint8ClampedArray.BYTES_PER_ELEMENT
);
const image = new ImageData(ui8c, dataFrame.xres);

if (canvas.width !== xres || canvas.height !== yres) {
canvas.width = xres;
canvas.height = yres;
}

context.putImageData(image, 0, 0);
}

if (receiverContext.enabled) {
waitForFrame(receiverContext);
}
}

const actions = {
async discoverSources({ commit }) {
checkCpu();

commit("SET_DISCOVERING", true);

try {
const sources = await grandiose.find(state.discoveryOptions);
commit("SET_SOURCES", sources);
} catch (e) {
console.log(e);
}

commit("SET_DISCOVERING", false);
},

setDiscoveryOptions({ commit }, options) {
commit("SET_DISCOVERY_OPTIONS", { ...state.discoveryOptions, ...options });
},

async createReceiver({ commit }, receiverOptions) {
receiverOptions.colorFormat = grandiose.COLOR_FORMAT_RGBX_RGBA;
receiverOptions.bandwidth = grandiose.BANDWIDTH_LOWEST;

const receiver = await grandiose.receive(receiverOptions);

const outputContext = await store.dispatch("outputs/getAuxillaryOutput", {
name: receiverOptions.source.name,
group: "NDI",
reactToResize: false
});

const receiverId = uuidv4();
const receiverContext = {
id: receiverId,
outputId: outputContext.id,
receiver,
enabled: false
};

commit("ADD_RECIEVER", receiverContext);

return receiverContext;
},

async enableReceiver({ commit }, { receiverId }) {
const receiverContext = state.receivers[receiverId];

if (!receiverContext) {
throw new Error(`No receiver found with id ${receiverId}`);
}

receiverContext.enabled = true;

commit("UPDATE_RECIEVER", receiverContext);

waitForFrame(receiverContext);
},

disableReceiver({ commit }, { receiverId }) {
const receiverContext = state.receivers[receiverId];

if (!receiverContext) {
throw new Error(`No receiver found with id ${receiverId}`);
}

receiverContext.enabled = false;

commit("UPDATE_RECIEVER", receiverContext);
},

async removeReceiver({ commit }, { receiverId }) {
const receiverContext = state.receivers[receiverId];

if (!receiverContext) {
throw new Error(`No receiver found with id ${receiverId}`);
}

await store.dispatch("ndi/disableReceiver", {
receiverId: receiverContext.id
});

await store.dispatch(
"outputs/removeAuxillaryOutput",
receiverContext.outputId
);

commit("DELETE_RECIEVER", receiverContext);
}
};

const mutations = {
SET_SOURCES(state, sources) {
state.sources = sources;
},

SET_DISCOVERING(state, discovering) {
state.discovering = discovering;
},

SET_DISCOVERY_OPTIONS(state, options) {
state.discoveryOptions = options;
},

ADD_RECIEVER(state, receiverContext) {
Vue.set(state.receivers, receiverContext.id, receiverContext);
},

UPDATE_RECIEVER(state, receiverContext) {
Vue.set(state.receivers, receiverContext.id, receiverContext);
},

DELETE_RECIEVER(state, receiverContext) {
Vue.delete(state.receivers, receiverContext.id);
}
};

export default {
namespaced: true,
state,
actions,
mutations
};
4 changes: 4 additions & 0 deletions src/application/worker/store/modules/outputs.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ const actions = {
return outputContext;
},

removeAuxillaryOutput({ commit }, outputContextId) {
commit("REMOVE_AUXILLARY", outputContextId);
},

setDebugContext({ commit }, debugCanvas) {
const context = debugCanvas.getContext("2d");
commit("SET_DEBUG_CONTEXT", context);
Expand Down
Loading

0 comments on commit de5a67a

Please sign in to comment.