diff --git a/applications/CMakeLists.txt b/applications/CMakeLists.txt index 2d0469b388..a1f5bbeab2 100644 --- a/applications/CMakeLists.txt +++ b/applications/CMakeLists.txt @@ -38,6 +38,10 @@ add_holohub_application(deltacast_transmitter DEPENDS OPERATORS deltacast_videomaster ) +add_holohub_application(deltacast_receiver DEPENDS + OPERATORS deltacast_videomaster + ) + add_holohub_application(depth_anything_v2) add_subdirectory(distributed) diff --git a/applications/deltacast_receiver/CMakeLists.txt b/applications/deltacast_receiver/CMakeLists.txt new file mode 100644 index 0000000000..043280b800 --- /dev/null +++ b/applications/deltacast_receiver/CMakeLists.txt @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2025 DELTACAST.TV. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.20) +project(deltacast_receiver_apps LANGUAGES NONE) + +add_subdirectory(cpp) diff --git a/applications/deltacast_receiver/README.md b/applications/deltacast_receiver/README.md new file mode 100644 index 0000000000..f403f64928 --- /dev/null +++ b/applications/deltacast_receiver/README.md @@ -0,0 +1,25 @@ +# Deltacast Videomaster Receiver + +This application demonstrates the use of videomaster_source to receive and display video streams from a Deltacast capture card using Holoviz for visualization. + +## Requirements + +This application uses the DELTACAST.TV capture card for input stream. Contact [DELTACAST.TV](https://www.deltacast.tv/) for more details on how to access the SDK and setup your environment. + +## Build Instructions + +See instructions from the top level README on how to build this application. +Note that this application requires to provide the VideoMaster_SDK_DIR if it is not located in a default location on the system. +This can be done with the following command, from the top level Holohub source directory: + +```bash +./holohub build --local deltacast_receiver --configure-args="-DVideoMaster_SDK_DIR=" +``` + +## Run Instructions + +From the build directory, run the command: + +```bash +./applications/deltacast_receiver/cpp/deltacast_receiver +``` diff --git a/applications/deltacast_receiver/cpp/CMakeLists.txt b/applications/deltacast_receiver/cpp/CMakeLists.txt new file mode 100644 index 0000000000..d5e4b7650e --- /dev/null +++ b/applications/deltacast_receiver/cpp/CMakeLists.txt @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2025 DELTACAST.TV. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +cmake_minimum_required(VERSION 3.20) +project(deltacast_receiver CXX) + +find_package(holoscan 1.0 REQUIRED CONFIG + PATHS "/opt/nvidia/holoscan" "/workspace/holoscan-sdk/install") + +# CPP Application +add_executable(deltacast_receiver + main.cpp +) + +target_link_libraries(deltacast_receiver + PRIVATE + holoscan::core + holoscan::ops::format_converter + holoscan::ops::holoviz + holoscan::videomaster +) + +# Copy config file +add_custom_target(deltacast_receiver_yaml + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/deltacast_receiver.yaml" ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "deltacast_receiver.yaml" + BYPRODUCTS "deltacast_receiver.yaml" +) +add_dependencies(deltacast_receiver deltacast_receiver_yaml) diff --git a/applications/deltacast_receiver/cpp/deltacast_receiver.yaml b/applications/deltacast_receiver/cpp/deltacast_receiver.yaml new file mode 100644 index 0000000000..0cc7efa7d8 --- /dev/null +++ b/applications/deltacast_receiver/cpp/deltacast_receiver.yaml @@ -0,0 +1,48 @@ +%YAML 1.2 +# SPDX-FileCopyrightText: Copyright (c) 2022-2025 DELTACAST.TV. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +extensions: + - lib/gxf_extensions/libgxf_videomaster.so + +deltacast: + width: 1920 + height: 1080 + progressive: true + framerate: 30 + board: 0 + input: 0 + rdma: false + +format_converter: + in_dtype: "rgb888" + alpha_value: 255 + out_dtype: "rgba8888" + out_channel_order: [2,1,0,3] + +drop_alpha_channel_converter: + in_dtype: "rgba8888" + out_dtype: "rgb888" + resize_height: 270 + resize_width: 480 + +holoviz: + tensors: + - name: "" + type: color + opacity: 1.0 + priority: 0 + width: 480 + height: 270 \ No newline at end of file diff --git a/applications/deltacast_receiver/cpp/main.cpp b/applications/deltacast_receiver/cpp/main.cpp new file mode 100644 index 0000000000..d9247d7dc1 --- /dev/null +++ b/applications/deltacast_receiver/cpp/main.cpp @@ -0,0 +1,130 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2025 DELTACAST.TV. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include + +class App : public holoscan::Application { + public: + /** Compose function */ + void compose() override { + using namespace holoscan; + + uint32_t width = from_config("deltacast.width").as(); + uint32_t height = from_config("deltacast.height").as(); + bool use_rdma = from_config("deltacast.rdma").as(); + uint64_t source_block_size = width * height * 4 * 4; + uint64_t source_num_blocks = use_rdma ? 3 : 4; + + // Create the VideoMaster source operator (receiver) with explicit arguments + auto source = make_operator( + "deltacast_source", + Arg("rdma") = use_rdma, + Arg("board") = from_config("deltacast.board").as(), + Arg("input") = from_config("deltacast.input").as(), + Arg("width") = width, + Arg("height") = height, + Arg("progressive") = from_config("deltacast.progressive").as(), + Arg("framerate") = from_config("deltacast.framerate").as(), + Arg("pool") = make_resource("pool")); + + // Format converter to prepare for visualization + auto format_converter = + make_operator("format_converter", + from_config("format_converter"), + Arg("pool") = make_resource( + "converter_pool", 1, source_block_size, + source_num_blocks)); + + auto drop_alpha_channel_converter = make_operator( + "drop_alpha_channel_converter", + from_config("drop_alpha_channel_converter"), + Arg("pool") = + make_resource("pool", 1, source_block_size, source_num_blocks)); + auto visualizer = make_operator( + "holoviz", + from_config("holoviz"), + Arg("allocator") = make_resource("holoviz_allocator")); + + // Connect the pipeline: source -> format_converter -> holoviz + add_flow(source, drop_alpha_channel_converter); + add_flow(drop_alpha_channel_converter, format_converter); + add_flow(format_converter, visualizer, {{"", "receivers"}}); + } +}; + +/** Helper function to parse the command line arguments */ +bool parse_arguments(int argc, char** argv, std::string& config_name) { + static struct option long_options[] = { + {"config", required_argument, 0, 'c' }, + {"help", no_argument, 0, 'h' }, + {0, 0, 0, 0 } + }; + + while (int c = getopt_long(argc, argv, "c:h", + long_options, NULL)) { + if (c == -1 || c == '?') break; + + switch (c) { + case 'c': + config_name = optarg; + break; + case 'h': + std::cout << "Usage: " << argv[0] << " [options] [config_file]\n"; + std::cout << "Options:\n"; + std::cout << " -c, --config Configuration file path\n"; + std::cout << " -h, --help Show this help message\n"; + std::cout << "\nExample:\n"; + std::cout << " " << argv[0] << " deltacast_receiver.yaml\n"; + return false; + default: + std::cout << "Unknown arguments returned: " << c << std::endl; + return false; + } + } + + if (optind < argc) { + config_name = argv[optind++]; + } + return true; +} + +int main(int argc, char** argv) { + auto app = holoscan::make_application(); + + // Parse the arguments + std::string config_name = ""; + if (!parse_arguments(argc, argv, config_name)) { + return 1; + } + + if (config_name != "") { + app->config(config_name); + } else { + auto config_path = std::filesystem::canonical(argv[0]).parent_path(); + config_path += "/deltacast_receiver.yaml"; + app->config(config_path); + } + + app->run(); + + return 0; +} diff --git a/applications/deltacast_receiver/cpp/metadata.json b/applications/deltacast_receiver/cpp/metadata.json new file mode 100644 index 0000000000..58c04af04f --- /dev/null +++ b/applications/deltacast_receiver/cpp/metadata.json @@ -0,0 +1,43 @@ +{ + "application": { + "name": "Deltacast Videomaster Receiver", + "authors": [ + { + "name": "Laurent Radoux", + "affiliation": "DELTACAST" + }, + { + "name": "Pierre PERICK", + "affiliation": "DELTACAST" + } + ], + "language": "C++", + "version": "1.0", + "changelog": { + "1.0": "Initial Release - Simplified receiver application for video display without AI processing" + }, + "holoscan_sdk": { + "minimum_required_version": "0.5.0", + "tested_versions": [ + "3.6.0" + ] + }, + "videomaster_sdk": { + "minimum_required_version": "6.26.0", + "tested_versions": [ + "6.32.0" + ] + }, + "platforms": [ + "x86_64", + "aarch64" + ], + "tags": ["Healthcare AI", "Video", "Deltacast", "Receiver", "Display", "Holoviz", "RDMA", "GPUDirect"], + "ranking": 2, + "requirements": {}, + "run": { + "command": "/deltacast_receiver", + "workdir": "holohub_bin" + } + } +} diff --git a/applications/deltacast_receiver/python/CMakeLists.txt b/applications/deltacast_receiver/python/CMakeLists.txt new file mode 100644 index 0000000000..759082f9be --- /dev/null +++ b/applications/deltacast_receiver/python/CMakeLists.txt @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2025 DELTACAST.TV. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Add testing +if(BUILD_TESTING) + # To get the environment path + find_package(holoscan 1.0 REQUIRED CONFIG PATHS "/opt/nvidia/holoscan" "/workspace/holoscan-sdk/install") + + # Add test + add_test(NAME deltacast_receiver_python_test + COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/deltacast_receiver.py + --config ${CMAKE_CURRENT_SOURCE_DIR}/deltacast_receiver.yaml + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) + + set_property(TEST deltacast_receiver_python_test PROPERTY ENVIRONMENT + "PYTHONPATH=${GXF_LIB_DIR}/../python/lib:${CMAKE_BINARY_DIR}/python/lib") + + set_tests_properties(deltacast_receiver_python_test PROPERTIES + PASS_REGULAR_EXPRESSION "Running the graph;" + FAIL_REGULAR_EXPRESSION "[^a-z]Error;ERROR;Failed") +endif() + +# Install application and dependencies into the install/ directory for packaging +install( + FILES deltacast_receiver.py + DESTINATION bin/deltacast_receiver/python +) + +install( + FILES deltacast_receiver.yaml + DESTINATION bin/deltacast_receiver/python +) \ No newline at end of file diff --git a/applications/deltacast_receiver/python/deltacast_receiver.py b/applications/deltacast_receiver/python/deltacast_receiver.py new file mode 100644 index 0000000000..be533b9c3f --- /dev/null +++ b/applications/deltacast_receiver/python/deltacast_receiver.py @@ -0,0 +1,139 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2025 DELTACAST.TV. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from argparse import ArgumentParser + +from holoscan.core import Application +from holoscan.operators import FormatConverterOp, HolovizOp +from holoscan.resources import BlockMemoryPool, MemoryStorageType, UnboundedAllocator + +from holohub.videomaster import VideoMasterSourceOp + + +class DeltacastReceiverApp(Application): + def __init__(self): + """Initialize the deltacast receiver application.""" + super().__init__() + self.name = "Deltacast Receiver" + + def compose(self): + """ + Compose the application by setting up the operators and their connections. + """ + # Retrieve VideoMaster parameters + deltacast_kwargs = self.kwargs("deltacast") + width = deltacast_kwargs.get("width", 1920) + height = deltacast_kwargs.get("height", 1080) + use_rdma = deltacast_kwargs.get("rdma", False) + + # Calculate source block size and count and define the source pool parameters + source_block_size = width * height * 4 * 4 + source_block_count = 3 if use_rdma else 4 + + source_pool_kwargs = dict( + storage_type=MemoryStorageType.DEVICE, + block_size=source_block_size, + num_blocks=source_block_count, + ) + + # Initialize operators + source = VideoMasterSourceOp( + self, + name="deltacast_source", + pool=UnboundedAllocator(self, name="source_pool"), + rdma=use_rdma, + board=deltacast_kwargs.get("board", 0), + input=deltacast_kwargs.get("input", 0), + width=width, + height=height, + progressive=deltacast_kwargs.get("progressive", True), + framerate=deltacast_kwargs.get("framerate", 30), + ) + + # Format converter to prepare for visualization + format_converter = FormatConverterOp( + self, + name="format_converter", + pool=BlockMemoryPool(self, name="converter_pool", **source_pool_kwargs), + **self.kwargs("format_converter"), + ) + + # Drop alpha channel converter (matches C++ implementation exactly) + drop_alpha_channel_converter = FormatConverterOp( + self, + name="drop_alpha_channel_converter", + pool=BlockMemoryPool(self, name="drop_alpha_pool", **source_pool_kwargs), + **self.kwargs("drop_alpha_channel_converter"), + ) + + # Holoviz for visualization + visualizer = HolovizOp( + self, + name="holoviz", + allocator=UnboundedAllocator(self, name="holoviz_allocator"), + **self.kwargs("holoviz"), + ) + + # Connect the pipeline: source -> drop_alpha_channel_converter -> format_converter -> holoviz + # This matches the exact flow from the C++ implementation + self.add_flow(source, drop_alpha_channel_converter) + self.add_flow(drop_alpha_channel_converter, format_converter) + self.add_flow(format_converter, visualizer, {("", "receivers")}) + + +def parse_config(): + """ + Parse command line arguments and validate paths. + + Returns + ------- + args : argparse.Namespace + Parsed command-line arguments. + """ + # Parse command line arguments + parser = ArgumentParser(description="DeltaCast Receiver demo application.") + parser.add_argument( + "-c", + "--config", + type=str, + default=os.path.join(os.path.dirname(__file__), "deltacast_receiver.yaml"), + help="Path to the configuration file to override the default config file location. If not provided, the deltacast_receiver.yaml in root directory will be used. (default location: %(default)s)", + ) + + args = parser.parse_args() + + # Ensure the configuration file exists + if not os.path.exists(args.config): + raise FileNotFoundError( + f"Configuration file {args.config} does not exist at expected location. Use --config to specify the correct path." + ) + + return args + + +def main(): + try: + args = parse_config() + app = DeltacastReceiverApp() + app.config(args.config) + app.run() + except Exception as e: + print(f"Error: {e}") + exit(1) + + +if __name__ == "__main__": + main() diff --git a/applications/deltacast_receiver/python/deltacast_receiver.yaml b/applications/deltacast_receiver/python/deltacast_receiver.yaml new file mode 100644 index 0000000000..0cc7efa7d8 --- /dev/null +++ b/applications/deltacast_receiver/python/deltacast_receiver.yaml @@ -0,0 +1,48 @@ +%YAML 1.2 +# SPDX-FileCopyrightText: Copyright (c) 2022-2025 DELTACAST.TV. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +extensions: + - lib/gxf_extensions/libgxf_videomaster.so + +deltacast: + width: 1920 + height: 1080 + progressive: true + framerate: 30 + board: 0 + input: 0 + rdma: false + +format_converter: + in_dtype: "rgb888" + alpha_value: 255 + out_dtype: "rgba8888" + out_channel_order: [2,1,0,3] + +drop_alpha_channel_converter: + in_dtype: "rgba8888" + out_dtype: "rgb888" + resize_height: 270 + resize_width: 480 + +holoviz: + tensors: + - name: "" + type: color + opacity: 1.0 + priority: 0 + width: 480 + height: 270 \ No newline at end of file diff --git a/applications/deltacast_receiver/python/metadata.json b/applications/deltacast_receiver/python/metadata.json new file mode 100644 index 0000000000..3fadab5b86 --- /dev/null +++ b/applications/deltacast_receiver/python/metadata.json @@ -0,0 +1,39 @@ +{ + "application": { + "name": "Deltacast Videomaster Receiver", + "authors": [ + { + "name": "Pierre PERICK", + "affiliation": "DELTACAST" + } + ], + "language": "Python", + "version": "1.0", + "changelog": { + "1.0": "Initial Release" + }, + "holoscan_sdk": { + "minimum_required_version": "0.5.0", + "tested_versions": [ + "3.6.0" + ] + }, + "videomaster_sdk": { + "minimum_required_version": "6.26.0", + "tested_versions": [ + "6.32.0" + ] + }, + "platforms": [ + "x86_64", + "aarch64" + ], + "tags": ["Healthcare AI", "Video", "Deltacast", "Endoscopy", "RDMA", "GPUDirect", "Receiver"], + "ranking": 2, + "requirements": {}, + "run": { + "command": "python3 /deltacast_receiver.py", + "workdir": "holohub_bin" + } + } +} \ No newline at end of file diff --git a/applications/deltacast_transmitter/cpp/deltacast_transmitter.yaml b/applications/deltacast_transmitter/cpp/deltacast_transmitter.yaml index 5d3b36c639..d1a1844ab3 100644 --- a/applications/deltacast_transmitter/cpp/deltacast_transmitter.yaml +++ b/applications/deltacast_transmitter/cpp/deltacast_transmitter.yaml @@ -36,7 +36,7 @@ deltacast: width: 1920 height: 1080 progressive: true - framerate: 25 + framerate: 30 board: 0 output: 0 rdma: false diff --git a/applications/deltacast_transmitter/cpp/metadata.json b/applications/deltacast_transmitter/cpp/metadata.json index a69e71815a..834d495c00 100644 --- a/applications/deltacast_transmitter/cpp/metadata.json +++ b/applications/deltacast_transmitter/cpp/metadata.json @@ -22,14 +22,16 @@ "tested_versions": [ "0.5.0", "2.9.0", - "3.0.0" + "3.0.0", + "3.6.0" ] }, "videomaster_sdk": { "minimum_required_version": "6.26.0", "tested_versions": [ "6.29.0", - "6.30.0" + "6.30.0", + "6.32.0" ] }, "platforms": [ diff --git a/applications/deltacast_transmitter/python/deltacast_transmitter.yaml b/applications/deltacast_transmitter/python/deltacast_transmitter.yaml index cdb43249f4..6b62553e93 100644 --- a/applications/deltacast_transmitter/python/deltacast_transmitter.yaml +++ b/applications/deltacast_transmitter/python/deltacast_transmitter.yaml @@ -36,7 +36,7 @@ videomaster: width: 1920 height: 1080 progressive: true - framerate: 60 + framerate: 30 board: 0 output: 0 rdma: false diff --git a/applications/deltacast_transmitter/python/metadata.json b/applications/deltacast_transmitter/python/metadata.json index 7bedef47b7..d1609fc93a 100644 --- a/applications/deltacast_transmitter/python/metadata.json +++ b/applications/deltacast_transmitter/python/metadata.json @@ -17,14 +17,16 @@ "tested_versions": [ "0.5.0", "2.9.0", - "3.0.0" + "3.0.0", + "3.6.0" ] }, "videomaster_sdk": { "minimum_required_version": "6.26.0", "tested_versions": [ "6.29.0", - "6.30.0" + "6.30.0", + "6.32.0" ] }, "platforms": [ diff --git a/utilities/cli/container.py b/utilities/cli/container.py index 83e652cdc9..b5c33ca53c 100644 --- a/utilities/cli/container.py +++ b/utilities/cli/container.py @@ -217,6 +217,10 @@ def get_device_mounts() -> List[str]: if os.path.exists(delta_sdi): options.extend(["--device", f"{delta_sdi}:{delta_sdi}"]) + delta_sdi = f"/dev/delta-x370{i}" + if os.path.exists(delta_sdi): + options.extend(["--device", f"{delta_sdi}:{delta_sdi}"]) + # Deltacast HDMI capture board delta_hdmi = f"/dev/delta-x350{i}" if os.path.exists(delta_hdmi):