Skip to content

Commit

Permalink
Merge branch 'release/v0.10.0' into 'main'
Browse files Browse the repository at this point in the history
Release/v0.10.0

See merge request momentfactory/products/xagora/omniverse/kit-exts-ndi!44
  • Loading branch information
fredericl-mf committed Apr 18, 2023
2 parents 121e26a + c4bb27a commit 0d08b36
Show file tree
Hide file tree
Showing 14 changed files with 533 additions and 40 deletions.
17 changes: 16 additions & 1 deletion exts/mf.ov.ndi/config/extension.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
version = "0.9.0"
version = "0.10.0"

title = "MF NDI® extension"
description = "An extension to enable NDI® live video input in Omniverse."
Expand Down Expand Up @@ -27,3 +27,18 @@ requirements = [
"unidecode"
]
use_online_index = true

[[test]]
args = [
"--/app/window/dpiScaleOverride=1.0",
"--/app/window/scaleToMonitor=false"
]
dependencies = [
"omni.kit.ui_test",
"omni.usd"
]
timeout = 60
stdoutFailPatterns.exclude = [
"*Could not get stage*",
"*Model doesn't have a registered window*"
]
8 changes: 6 additions & 2 deletions exts/mf.ov.ndi/docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]
- Unit and UI test integration
- NDI® source monitoring (dimensions, fps, etc.)

## [0.9.0] - 2023.04-04
## [0.10.0] - 2023-04-12

### Added
- Unit and UI tests

## [0.9.0] - 2023-04-04

### Changed
- UI rework: less text, more icons
Expand Down
2 changes: 2 additions & 0 deletions exts/mf.ov.ndi/mf/ov/ndi/NDItools.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def __init__(self, name: str, stream_uri: str, lowbandwidth: bool, tools: NDItoo
self.uri = stream_uri
self.is_ok = False
self._thread: threading.Thread
self._lowbandwidth = lowbandwidth

if not tools.is_ndi_ok():
return
Expand Down Expand Up @@ -212,6 +213,7 @@ def __init__(self, name: str, stream_uri: str, fps: float, lowbandwidth: bool):
self.name = name
self.uri = stream_uri
self.is_ok = False
self._lowbandwidth = lowbandwidth

denominator = 1
if lowbandwidth:
Expand Down
33 changes: 25 additions & 8 deletions exts/mf.ov.ndi/mf/ov/ndi/USDtools.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,26 @@ class USDtools():
ATTR_NDI_NAME = 'ndi:source'
ATTR_BANDWIDTH_NAME = "ndi:lowbandwidth"
PREFIX = "dynamic://"
SCOPE_NAME = "NDI_Looks"

def create_dynamic_material(name: str) -> UsdShade.Material:
def get_stage() -> Usd.Stage:
usd_context = omni.usd.get_context()
stage: Usd.Stage = usd_context.get_stage()
return usd_context.get_stage()

def make_name_valid(name: str) -> str:
return Tf.MakeValidIdentifier(unidecode(name))

def create_dynamic_material(name: str) -> UsdShade.Material:
stage = USDtools.get_stage()
if not stage:
logger = logging.getLogger(__name__)
logger.error("Could not get stage")
return

scope_path: str = f"{stage.GetDefaultPrim().GetPath()}/NDI_Looks"
scope_path: str = f"{stage.GetDefaultPrim().GetPath()}/{USDtools.SCOPE_NAME}"
UsdGeom.Scope.Define(stage, scope_path)

safename = Tf.MakeValidIdentifier(unidecode(name))
safename = USDtools.make_name_valid(name)
if name != safename:
logger = logging.getLogger(__name__)
logger.warn(f"Name \"{name}\" was not a valid USD identifier, changed it to \"{safename}\"")
Expand Down Expand Up @@ -99,8 +110,11 @@ def find_all_dynamic_sources() -> List[DynamicPrim]:
return result

def set_prim_ndi_attribute(path: str, value: str):
usd_context = omni.usd.get_context()
stage: Usd.Stage = usd_context.get_stage()
stage = USDtools.get_stage()
if not stage:
logger = logging.getLogger(__name__)
logger.error("Could not get stage")
return

prim: Usd.Prim = stage.GetPrimAtPath(path)
if not prim.IsValid():
Expand All @@ -111,8 +125,11 @@ def set_prim_ndi_attribute(path: str, value: str):
prim.CreateAttribute(USDtools.ATTR_NDI_NAME, Sdf.ValueTypeNames.String).Set(value)

def set_prim_bandwidth_attribute(path: str, value: bool):
usd_context = omni.usd.get_context()
stage: Usd.Stage = usd_context.get_stage()
stage = USDtools.get_stage()
if not stage:
logger = logging.getLogger(__name__)
logger.error("Could not get stage")
return

prim: Usd.Prim = stage.GetPrimAtPath(path)
if not prim.IsValid():
Expand Down
21 changes: 8 additions & 13 deletions exts/mf.ov.ndi/mf/ov/ndi/comboboxModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def is_active(self):
class ComboboxModel(ui.AbstractItemModel):
NONE_VALUE = "NONE"
PROXY_VALUE = "PROXY (1080p30) - RED"
RUNNING_LABEL_SUFFIX = " - running"
items: List[ComboboxItem] = []
watchers = []

Expand Down Expand Up @@ -65,18 +66,11 @@ def _current_index_changed_fn(self):
self.set_alt_value()

def set_alt_value(self):
self._combobox_alt.text = self.currentvalue() + " - running"
self._combobox_alt.text = self.currentvalue() + ComboboxModel.RUNNING_LABEL_SUFFIX

def currentvalue(self):
self._current_item = ComboboxModel.items[self._current_index.get_value_as_int()]
return self._current_item.value()

def currentItem(self):
self._current_item = ComboboxModel.items[self._current_index.get_value_as_int()]
return self._current_item

def getCurrentItemIndex(self):
return self._current_index.get_value_as_int()
current_item = ComboboxModel.items[self._current_index.get_value_as_int()]
return current_item.value()

def get_item_children(self, item):
return ComboboxModel.items
Expand All @@ -86,8 +80,9 @@ def get_item_value_model(self, item, column_id):
return self._current_index
return item.model

def append_child_item(self, parentItem, text):
ComboboxModel.AddItem(text)

def select_none(self):
self._current_index.set_value(0)

def is_active(self):
current_item = ComboboxModel.items[self._current_index.get_value_as_int()]
return current_item.is_active()
3 changes: 2 additions & 1 deletion exts/mf.ov.ndi/mf/ov/ndi/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import omni.ext
import omni.kit.app
import asyncio
import omni.kit.ui


class MFOVNdiExtension(omni.ext.IExt):
Expand Down Expand Up @@ -40,7 +41,7 @@ def _visibility_changed_fn(self, visible):

def show_window(self, menu, value):
if value:
self._window = NDIWindow(width=800, height=230)
self._window = NDIWindow(width=800, height=275)
self._window.set_visibility_changed_fn(self._visibility_changed_fn)
elif self._window:
self._window.destroy()
Expand Down
10 changes: 9 additions & 1 deletion exts/mf.ov.ndi/mf/ov/ndi/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def on_shutdown(self):

# region streams
def add_stream(self, name: str, uri: str, lowbandwidth: bool) -> bool:
# TODO: Stop the possibility of adding 2 streams with the same name
if uri == ComboboxModel.NONE_VALUE:
logger = logging.getLogger(__name__)
logger.warning("Won't create stream without NDI® source")
Expand All @@ -118,11 +119,18 @@ def _add_stream(self, video_stream, uri) -> bool:
return True

def kill_all_streams(self):
self._window.on_kill_all_streams()
self._kill_all_streams_window()
for stream in self._streams:
stream.destroy()
self._streams = []

def _kill_all_streams_window(self):
if self._window:
self._window.on_kill_all_streams()
else:
logger = logging.getLogger(__name__)
logger.error("Model doesn't have a registered window")

def remove_stream(self, name: str, uri: str):
stream: NDIVideoStream = next((x for x in self._streams if x.name == name and x.uri == uri), None)
if stream is not None: # could be none if already stopped
Expand Down
4 changes: 4 additions & 0 deletions exts/mf.ov.ndi/mf/ov/ndi/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .test_USDtools import *
from .test_model import *
from .test_NDItools import *
from .test_ui import *
25 changes: 25 additions & 0 deletions exts/mf.ov.ndi/mf/ov/ndi/tests/test_NDItools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import omni.kit.test
from ..NDItools import NDIData
from .test_utils import SOURCE1


class NDIDataUnitTest(omni.kit.test.AsyncTestCase):
async def test_source(self):
data = NDIData(SOURCE1, False)
self.assertEqual(data.get_source(), SOURCE1)

async def test_active(self):
data = NDIData(SOURCE1)
self.assertFalse(data.is_active())

data = NDIData(SOURCE1, False)
self.assertFalse(data.is_active())

data = NDIData(SOURCE1, True)
self.assertTrue(data.is_active())

data.set_active(False)
self.assertFalse(data.is_active())

data.set_active(True)
self.assertTrue(data.is_active())
62 changes: 62 additions & 0 deletions exts/mf.ov.ndi/mf/ov/ndi/tests/test_USDtools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import omni.kit.test
from ..USDtools import USDtools
from .test_utils import make_stage, close_stage, create_dynamic_material, create_dynamic_rectlight, SOURCE1


class USDValidNameUnitTest(omni.kit.test.AsyncTestCase):
async def test_name_valid(self):
self.check_name_valid("myDynamicMaterial", "myDynamicMaterial")
self.check_name_valid("789testing123numbers456", "_89testing123numbers456")
self.check_name_valid("", "_")
self.check_name_valid("àâáäãåÀÂÁÃÅÄ", "aaaaaaAAAAAA")
self.check_name_valid("èêéëÈÊÉË", "eeeeEEEE")
self.check_name_valid("ìîíïÌÎÍÏ", "iiiiIIII")
self.check_name_valid("òôóöõøÒÔÓÕÖØ", "ooooooOOOOOO")
self.check_name_valid("ùûúüÙÛÚÜ", "uuuuUUUU")
self.check_name_valid("æœÆŒçÇ°ðÐñÑýÝþÞÿß", "aeoeAEOEcCdegdDnNyYthThyss")
self.check_name_valid("!¡¿@#$%?&*()-_=+/`^~.,'\\<>`;:¤{}[]|\"¦¨«»¬¯±´·¸÷",
"___________________________________________________")
self.check_name_valid("¢£¥§©ªº®¹²³µ¶¼½¾×", "C_PSY_SS_c_ao_r_123uP_1_4_1_2_3_4x")

def check_name_valid(self, source, expected):
v: str = USDtools.make_name_valid(source)
self.assertEqual(v, expected, f"Expected \"{v}\", derived from \"{source}\", to equals \"{expected}\"")


class USDToolsUnitTest(omni.kit.test.AsyncTestCase):
def setUp(self):
self._stage = make_stage()

def tearDown(self):
close_stage()

async def test_create_dynamic_material(self):
material = create_dynamic_material()
prim = self._stage.GetPrimAtPath(material.GetPath())
self.assertIsNotNone(prim)

async def test_find_dynamic_sources(self):
create_dynamic_material()
create_dynamic_rectlight()

sources = USDtools.find_all_dynamic_sources()
self.assertEqual(len(sources), 2)

async def test_set_property_ndi(self):
material = create_dynamic_material()
path = material.GetPath()
USDtools.set_prim_ndi_attribute(path, SOURCE1)

attr = material.GetPrim().GetAttribute(USDtools.ATTR_NDI_NAME)
self.assertEqual(attr.Get(), SOURCE1)

async def test_set_property_bandwidth(self):
material = create_dynamic_material()
path = material.GetPath()
USDtools.set_prim_bandwidth_attribute(path, True)

attr = material.GetPrim().GetAttribute(USDtools.ATTR_BANDWIDTH_NAME)
self.assertTrue(attr.Get())

USDtools.set_prim_bandwidth_attribute(path, False)
self.assertFalse(attr.Get())
96 changes: 96 additions & 0 deletions exts/mf.ov.ndi/mf/ov/ndi/tests/test_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import omni.kit.test
from ..NDItools import NDIData
from ..model import NDIBinding, NDIModel
from ..USDtools import USDtools
from ..comboboxModel import ComboboxModel
from .test_utils import SOURCE1, SOURCE2, DYNAMIC_ID1, DYNAMIC_ID2, DUMMY_PATH


class NDIBindingsUnitTest(omni.kit.test.AsyncTestCase):
async def test_dynamic_id(self):
ndi_data = NDIData(SOURCE1)
ndi_binding = NDIBinding(DYNAMIC_ID1, ndi_data, DUMMY_PATH, False)

self.assertEqual(ndi_binding.get_id(), DYNAMIC_ID1)
self.assertEqual(ndi_binding.get_id_full(), USDtools.PREFIX + DYNAMIC_ID1)

async def test_source(self):
ndi_data1 = NDIData(SOURCE1)
ndi_data2 = NDIData(SOURCE2)

ndi_binding = NDIBinding(DYNAMIC_ID1, ndi_data1, DUMMY_PATH, False)
self.assertEqual(ndi_binding.get_source(), SOURCE1)

ndi_binding.set_ndi_id(ndi_data2)
self.assertEqual(ndi_binding.get_source(), SOURCE2)

async def test_lowbandwidth(self):
ndi_data = NDIData(SOURCE1)
ndi_binding = NDIBinding(DYNAMIC_ID1, ndi_data, DUMMY_PATH, False)
self.assertFalse(ndi_binding.get_lowbandwidth())

ndi_binding = NDIBinding(DYNAMIC_ID1, ndi_data, DUMMY_PATH, True)
self.assertTrue(ndi_binding.get_lowbandwidth())

ndi_binding.set_lowbandwidth(False)
self.assertFalse(ndi_binding.get_lowbandwidth())
ndi_binding.set_lowbandwidth(True)
self.assertTrue(ndi_binding.get_lowbandwidth())
pass


class ModelUnitTest(omni.kit.test.AsyncTestCase):
def setUp(self):
self._model = NDIModel(None)

def tearDown(self):
self._model.on_shutdown()

async def test_no_stream_at_start(self):
streams_length = len(self._model._streams)
self.assertEqual(streams_length, 0)

async def test_add_stream_NONE(self):
streams_base_length = len(self._model._streams)

self._model.add_stream(DYNAMIC_ID1, ComboboxModel.NONE_VALUE, False)
self.assertEqual(len(self._model._streams), streams_base_length)

async def test_add_stream_PROXY(self):
streams_length = len(self._model._streams)

self._model.add_stream(DYNAMIC_ID1, ComboboxModel.PROXY_VALUE, False)
self.assertEqual(len(self._model._streams), streams_length + 1)

async def test_kill_all_streams(self):
self._model.add_stream(DYNAMIC_ID1, ComboboxModel.PROXY_VALUE, False)
self._model.add_stream(DYNAMIC_ID2, ComboboxModel.PROXY_VALUE, False)
self.assertGreater(len(self._model._streams), 0)

self._model.kill_all_streams()
self.assertEqual(len(self._model._streams), 0)

async def test_remove_stream(self):
self._model.add_stream(DYNAMIC_ID1, ComboboxModel.PROXY_VALUE, False)
self._model.add_stream(DYNAMIC_ID2, ComboboxModel.PROXY_VALUE, False)
streams_length = len(self._model._streams)
self.assertGreater(streams_length, 0)

self._model.remove_stream(DYNAMIC_ID1, ComboboxModel.PROXY_VALUE)
self.assertEqual(len(self._model._streams), streams_length - 1)
self._model.remove_stream(DYNAMIC_ID1, ComboboxModel.PROXY_VALUE)
self.assertEqual(len(self._model._streams), streams_length - 1)
self._model.remove_stream(DYNAMIC_ID2, ComboboxModel.PROXY_VALUE)
self.assertEqual(len(self._model._streams), streams_length - 2)


"""
async def test_add_stream(self):
model = NDIModel(None)
streams_length = len(model._streams)
model.add_stream(DYNAMIC_ID, SOURCE1, False)
self.assertEqual(len(model._streams), streams_length + 1)
model.add_stream(DYNAMIC_ID, SOURCE2, False)
self.assertEqual(len(model._streams), streams_length + 2)
"""
Loading

0 comments on commit 0d08b36

Please sign in to comment.