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

feat: NDI support #111

Merged
merged 5 commits into from
May 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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