diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index d9b3f18..0998e53 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -3,9 +3,11 @@ on: pull_request: branches: - main + - beta push: branches: - main + - beta concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -25,20 +27,20 @@ jobs: run: cmake . -B build && cmake --build build --config Debug && cmake --build build --config Release - name: Parse git commit - if: ${{ github.ref == 'refs/heads/main' }} + if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta' }} run: echo GH_REL_TAG=$(git rev-parse --short HEAD 2> /dev/null | sed "s/\(.*\)/v0-\1/") >> $GITHUB_ENV # Remotely we create and push the tag - name: Create Release Tag run: git tag ${{ env.GH_REL_TAG }} && git push origin ${{ env.GH_REL_TAG }} - if: ${{ github.ref == 'refs/heads/main' }} + if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta' }} # Locally we debug the tag parse - run: echo ${{ env.GH_REL_TAG }} - if: ${{ github.ref == 'refs/heads/main' }} + if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta' }} - name: Package Public - if: ${{ github.ref == 'refs/heads/main' }} + if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta' }} shell: pwsh run: | mkdir rel @@ -48,9 +50,10 @@ jobs: # Remotely we create the release from the tag - name: GH Release - if: ${{ github.ref == 'refs/heads/main' }} + if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/beta' }} uses: softprops/action-gh-release@v0.1.14 with: tag_name: ${{ env.GH_REL_TAG }} + prerelease: ${{ github.ref != 'refs/heads/main' }} generate_release_notes: true files: rel/*.zip diff --git a/CMakeLists.txt b/CMakeLists.txt index a11d26c..9ce9fa5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,5 +2,27 @@ cmake_minimum_required(VERSION 3.22.0) project("rainway-sdk-native-examples") +include(FetchContent) + +# The version of the Rainway SDK to use +set(RAINWAY_SDK_VERSION "0.3.0") + +# The md5 hash of the Rainway SDK zip for this version +# From powershell you can use `Get-FileHash -Algorithm MD5 -Path ` +set(RAINWAY_SDK_MD5_HASH "6F80A5F0B5D019518193E84AA8EF53BE") + +message("Attempting to download Rainway SDK v${RAINWAY_SDK_VERSION} with md5 hash '${RAINWAY_SDK_MD5_HASH}'") + +# Download the sdk +FetchContent_Declare( + rainwaysdk + URL https://sdk-builds.rainway.com/cpp/${RAINWAY_SDK_VERSION}.zip + URL_HASH MD5=${RAINWAY_SDK_MD5_HASH} +) + +# Make the SDK available for use +FetchContent_MakeAvailable(rainwaysdk) + # Add our example subdirectories -add_subdirectory("echo-example") +add_subdirectory("host-example") +add_subdirectory("video-player-example") diff --git a/README.md b/README.md index c4e84f0..9a9c3b2 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,16 @@ For more information about using Rainway, see [our docs](https://docs.rainway.co ## Getting Started +Get [CMake `>=3.22.0`](https://cmake.org/download/) and [a supported Generator](https://cmake.org/cmake/help/v3.22/manual/cmake-generators.7.html) (e.g. [Visual Studio](https://visualstudio.microsoft.com/vs/)). + To build all examples: -``` +```sh # Generate the build system for all examples cmake . -B build # Run the builds using the generated system cmake --build build ``` + +See `README.md` within each example for further instructions. diff --git a/echo-example/README.md b/echo-example/README.md deleted file mode 100644 index ce82e16..0000000 --- a/echo-example/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# echo-example - -This example demonstrates how to build a simple echo server using the Rainway SDK in a C++ application. - -For more information about Rainway, see [our docs](https://docs.rainway.com). To sign up, visit [Rainway.com](https://rainway.com). - -## Getting Started - -> Requires [CMake `>=3.22.0`](https://cmake.org/download/) and [a supported Generator](https://cmake.org/cmake/help/v3.22/manual/cmake-generators.7.html) (e.g. [Visual Studio](https://visualstudio.microsoft.com/vs/)). - -``` -# Generate the build system -cmake . -B build - -# Run the build -cmake --build build - -# Run the built application with your Rainway API Key as the first and only argument -./build/bin/Debug/echo-example.exe -``` - -### Building Release - -The above steps create a `Debug` binary by default. To build a release binary, instruct `cmake` to do so: - -``` -# Generate the build system -cmake . -DCMAKE_BUILD_TYPE=Release -B build - -# Run the build -cmake --build build - -# Run the built application with your Rainway API Key as the first and only argument -./build/bin/Release/echo-example.exe -``` - -For more information about build configuration, see [The CMAKE Docs](https://cmake.org/cmake/help/v3.22/index.html). diff --git a/echo-example/CMakeLists.txt b/host-example/CMakeLists.txt similarity index 69% rename from echo-example/CMakeLists.txt rename to host-example/CMakeLists.txt index a70995f..7c4b170 100644 --- a/echo-example/CMakeLists.txt +++ b/host-example/CMakeLists.txt @@ -1,26 +1,6 @@ # You may install cmake from https://cmake.org/download/ cmake_minimum_required(VERSION 3.22.0) -project("echo-example") -include(FetchContent) - -# The version of the Rainway SDK to use -set(RAINWAY_SDK_VERSION "0.2.8") - -# The md5 hash of the Rainway SDK zip for this version -# From powershell you can use `Get-FileHash -Algorithm MD5 -Path ` -set(RAINWAY_SDK_MD5_HASH "F2F9B0A9C7569FE13EB337858CF01C64") - -message("Attempting to download Rainway SDK v${RAINWAY_SDK_VERSION} with md5 hash '${RAINWAY_SDK_MD5_HASH}'") - -# Download the sdk -FetchContent_Declare( - rainwaysdk - URL https://sdk-builds.rainway.com/cpp/${RAINWAY_SDK_VERSION}.zip - URL_HASH MD5=${RAINWAY_SDK_MD5_HASH} -) - -# Make the SDK available for use -FetchContent_MakeAvailable(rainwaysdk) +project("host-example") # Create an executable target from our source add_executable(${PROJECT_NAME} src/main.cpp) diff --git a/host-example/README.md b/host-example/README.md new file mode 100644 index 0000000..46eced1 --- /dev/null +++ b/host-example/README.md @@ -0,0 +1,20 @@ +# host-example + +This example demonstrates how to build a simple host application using the Rainway SDK in a C++ application. + +It accepts all incoming stream requests from clients (such as the [Web Demo](https://webdemo.rainway.com/)). + +For more information about Rainway, see [our docs](https://docs.rainway.com). To sign up, visit [rainway.com](https://rainway.com). + +## Building and running this example + +See the [parent README](../README.md) for detailed build instructions. + +```ps1 +cd .. +cmake . -B build +cmake --build build -t host-example + +# Get your Rainway API key here: https://hub.rainway.com/keys +.\build\bin\Debug\host-example.exe pk_live_YourRainwayApiKey +``` diff --git a/echo-example/src/main.cpp b/host-example/src/main.cpp similarity index 94% rename from echo-example/src/main.cpp rename to host-example/src/main.cpp index ac9a538..4aeee8c 100644 --- a/echo-example/src/main.cpp +++ b/host-example/src/main.cpp @@ -7,7 +7,7 @@ // Mirrors rainway::RainwayLogLevel indicies for conversion to string const char *LOG_LEVEL_STR_MAP[] = {"Silent", "Error", "Warning", "Info", "Debug", "Trace"}; -// echo-example entry point +// host-example entry point // expects your API_KEY as the first and only argument int main(int argc, char *argv[]) { @@ -24,7 +24,7 @@ int main(int argc, char *argv[]) // The Rainway API Key to authentication with apiKey, // The Rainway External Id to identify ourselves as - "rainway-sdk-native-echo-example", + "rainway-sdk-native-host-example", // The min host port to use (zero is default) 0, // The max host port to use (zero disables limiting the port) @@ -62,11 +62,12 @@ int main(int argc, char *argv[]) [](const rainway::Runtime &runtime, const rainway::StreamRequest req) { // accept all stream requests, granting full input permissions - req.accept(rainway::RainwayStreamConfig{ + req.accept(rainway::StreamConfig{ + rainway::RAINWAY_STREAM_TYPE_FULL_DESKTOP, rainway::RAINWAY_INPUT_LEVEL_ALL, nullptr, // no input filter nullptr, // no isolation pids - 0, // 0 isolation pids + 0, // 0 isolation pids }); }, // Optional callback for when a stream announcement has been received diff --git a/video-player-example/CMakeLists.txt b/video-player-example/CMakeLists.txt new file mode 100644 index 0000000..e881fdb --- /dev/null +++ b/video-player-example/CMakeLists.txt @@ -0,0 +1,34 @@ +# You may install cmake from https://cmake.org/download/ +cmake_minimum_required(VERSION 3.22.0) +project("video-player-example") + +# Create an executable target from our source +add_executable(${PROJECT_NAME} src/main.cpp) +add_compile_definitions(NOMINMAX) + +# Specify the target uses the c++ linker +set_target_properties(${PROJECT_NAME} PROPERTIES LINKER_LANGUAGE CXX) + +# Specify the target binary should output to /bin +set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + +# Specify the target uses c++17 +target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) + +# Link the target against the downloaded rainwaysdk +target_link_libraries(${PROJECT_NAME} rainwaysdk) + +# Include the downloaded rainwaysdk include dir (where the header is) for the target +# Note: rainwaysdk_SOURCE_DIR is autocreated by FetchContent_MakeAvailable() +target_include_directories(${PROJECT_NAME} PRIVATE ${rainwaysdk_SOURCE_DIR}/include) + +# Include the downloaded rainwaysdk root dir (where the dll and lib are) for the target +# Note: rainwaysdk_SOURCE_DIR is autocreated by FetchContent_MakeAvailable() +target_link_directories(${PROJECT_NAME} PRIVATE ${rainwaysdk_SOURCE_DIR}) + +# Add a custom command to copy the rainwaysdk dll to the build directory +# If the build directory has a different version (or no dll) +add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${rainwaysdk_SOURCE_DIR}/rainwaysdk.dll" + $) diff --git a/video-player-example/README.md b/video-player-example/README.md new file mode 100644 index 0000000..287ce89 --- /dev/null +++ b/video-player-example/README.md @@ -0,0 +1,20 @@ +# video-player-example + +This C++ example uses the Rainway SDK's [BYOFB mode](https://docs.rainway.com/docs/byofb) and [MediaFoundation](https://docs.microsoft.com/en-us/windows/win32/medfound/microsoft-media-foundation-sdk) to stream video from a file. + +It accepts all incoming stream requests from clients (such as the [Web Demo](https://webdemo.rainway.com/)), and streams the video to them. Try connecting with multiple clients at once! + +For more information about Rainway, see [our docs](https://docs.rainway.com). To sign up, visit [rainway.com](https://rainway.com). + +## Building and running this example + +See the [parent README](../README.md) for detailed build instructions. + +```ps1 +cd .. +cmake . -B build +cmake --build build -t video-player-example + +# Get your Rainway API key here: https://hub.rainway.com/keys +.\build\bin\Debug\video-player-example.exe pk_live_YourRainwayApiKey C:\path\to\media.mp4 +``` diff --git a/video-player-example/src/main.cpp b/video-player-example/src/main.cpp new file mode 100644 index 0000000..f89e886 --- /dev/null +++ b/video-player-example/src/main.cpp @@ -0,0 +1,722 @@ +// This is a command-line utility that uses MediaFoundation together with +// Rainway's BYOFB mode to stream a video file over Rainway. Use it as: +// +// video-player-example.exe pk_live_YourRainwayApiKey C:\path\to\media.mp4 +// +// Then connect from https://webdemo.rainway.com/ to see your video. +// +// For more info, see: https://docs.rainway.com/docs/byofb + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#pragma comment(lib, "mf.lib") +#pragma comment(lib, "mfplat.lib") +#pragma comment(lib, "mfplay.lib") +#pragma comment(lib, "mfreadwrite.lib") +#pragma comment(lib, "mfuuid.lib") +#pragma comment(lib, "wmcodecdspuuid.lib") + +#pragma comment(lib, "d3d11") +#pragma comment(lib, "d3dcompiler") + +#define WI_VERIFY_SUCCEEDED(x) WINRT_VERIFY((x) == S_OK) + +DEFINE_ENUM_FLAG_OPERATORS(D3D11_CREATE_DEVICE_FLAG); + +constexpr auto AUDIO_SAMPLE_RATE = 44100u; + +namespace dx +{ + winrt::com_ptr create_device() + { + D3D_FEATURE_LEVEL feature_levels[] = { + D3D_FEATURE_LEVEL_12_1, + D3D_FEATURE_LEVEL_12_0, + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL_9_3, + D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL_9_1, + }; + auto feature_levels_len = ARRAYSIZE(feature_levels); + + auto flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT; + +#if !defined(NDEBUG) + flags |= D3D11_CREATE_DEVICE_DEBUG; +#endif + + winrt::com_ptr device = nullptr; + winrt::com_ptr context = nullptr; + + WI_VERIFY_SUCCEEDED(D3D11CreateDevice( + nullptr, + D3D_DRIVER_TYPE_HARDWARE, + nullptr, + flags, + feature_levels, + feature_levels_len, + D3D11_SDK_VERSION, + device.put(), + nullptr, + context.put())); + + // Enable multithread protection: "It is mandatory since Media + // Foundation is heavilty multithreaded by its nature and running + // unprotected you quickly hit a corruption." + // https://stackoverflow.com/a/59760070/257418 + winrt::com_ptr multithread; + WI_VERIFY_SUCCEEDED(device->QueryInterface(IID_PPV_ARGS(multithread.put()))); + multithread->SetMultithreadProtected(true); + + return device; + } + + /// @brief Create a D3D11 texture + /// @param device Device to create texture with + /// @param w Width of texture + /// @param h Height of texture + /// @param format DXGI format of texture to create. + winrt::com_ptr create_texture( + winrt::com_ptr &device, + uint32_t w, + uint32_t h, + DXGI_FORMAT format) + { + D3D11_TEXTURE2D_DESC desc = {}; + desc.Width = w; + desc.Height = h; + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = format; + desc.SampleDesc.Count = 1; + desc.SampleDesc.Quality = 0; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = 0; + desc.CPUAccessFlags = 0; + desc.MiscFlags = D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX | D3D11_RESOURCE_MISC_SHARED_NTHANDLE; + + winrt::com_ptr texture = nullptr; + WI_VERIFY_SUCCEEDED(device->CreateTexture2D(&desc, nullptr, texture.put())); + + return texture; + } +} // namespace dx + +namespace mf +{ + /// @brief Print debug information about the video format of the source reader + /// @param source_reader Source reader + void debug_video_format(winrt::com_ptr &source_reader) + { + winrt::com_ptr native_type = nullptr; + winrt::com_ptr first_output = nullptr; + WI_VERIFY_SUCCEEDED( + source_reader->GetNativeMediaType( + (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, + MF_SOURCE_READER_CURRENT_TYPE_INDEX, + native_type.put())); + + WI_VERIFY_SUCCEEDED( + source_reader->GetCurrentMediaType( + (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, + first_output.put())); + + uint32_t width = 0, height = 0; + WI_VERIFY_SUCCEEDED(MFGetAttributeSize(first_output.get(), MF_MT_FRAME_SIZE, &width, &height)); + + uint64_t val = 0; + WI_VERIFY_SUCCEEDED(native_type->GetUINT64(MF_MT_FRAME_RATE, &val)); + auto fps = float(HI32(val)) / float(LO32(val)); + + printf("VO: %dx%d (@ %f fps)\n", width, height, fps); + } + + /// @brief Print debug information about the audio format of the source reader + /// and resampler + /// @param source_reader Source reader + /// @param resampler Resampler + void debug_audio_format( + winrt::com_ptr &source_reader, + winrt::com_ptr &resampler) + { + winrt::com_ptr native_type = nullptr; + winrt::com_ptr first_output = nullptr; + + WI_VERIFY_SUCCEEDED( + source_reader->GetNativeMediaType( + (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, + MF_SOURCE_READER_CURRENT_TYPE_INDEX, + native_type.put())); + + WI_VERIFY_SUCCEEDED( + source_reader->GetCurrentMediaType( + (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, + first_output.put())); + + uint32_t channels = 0; + uint32_t samples_per_sec = 0; + uint32_t bits_per_sample = 0; + uint32_t block_align = 0; + uint32_t avg_per_sec = 0; + + WI_VERIFY_SUCCEEDED(native_type->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &channels)); + WI_VERIFY_SUCCEEDED(native_type->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, &samples_per_sec)); + WI_VERIFY_SUCCEEDED(native_type->GetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, &bits_per_sample)); + WI_VERIFY_SUCCEEDED(native_type->GetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, &block_align)); + WI_VERIFY_SUCCEEDED(native_type->GetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, &avg_per_sec)); + + printf("AI: c:%d sps:%d bps:%d ba:%d aps:%d\n", channels, samples_per_sec, bits_per_sample, block_align, avg_per_sec); + + WI_VERIFY_SUCCEEDED(first_output->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &channels)); + WI_VERIFY_SUCCEEDED(first_output->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, &samples_per_sec)); + WI_VERIFY_SUCCEEDED(first_output->GetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, &bits_per_sample)); + WI_VERIFY_SUCCEEDED(first_output->GetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, &block_align)); + WI_VERIFY_SUCCEEDED(first_output->GetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, &avg_per_sec)); + + printf("AO: c:%d sps:%d bps:%d ba:%d aps:%d\n", channels, samples_per_sec, bits_per_sample, block_align, avg_per_sec); + + winrt::com_ptr resampler_input = nullptr; + winrt::com_ptr resamper_output = nullptr; + + WI_VERIFY_SUCCEEDED( + resampler->GetInputCurrentType(0, resampler_input.put())); + + WI_VERIFY_SUCCEEDED( + resampler->GetOutputCurrentType(0, resamper_output.put())); + + WI_VERIFY_SUCCEEDED(resampler_input->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &channels)); + WI_VERIFY_SUCCEEDED(resampler_input->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, &samples_per_sec)); + WI_VERIFY_SUCCEEDED(resampler_input->GetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, &bits_per_sample)); + WI_VERIFY_SUCCEEDED(resampler_input->GetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, &block_align)); + WI_VERIFY_SUCCEEDED(resampler_input->GetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, &avg_per_sec)); + + printf("RI: c:%d sps:%d bps:%d ba:%d aps:%d\n", channels, samples_per_sec, bits_per_sample, block_align, avg_per_sec); + + WI_VERIFY_SUCCEEDED(resamper_output->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &channels)); + WI_VERIFY_SUCCEEDED(resamper_output->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, &samples_per_sec)); + WI_VERIFY_SUCCEEDED(resamper_output->GetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, &bits_per_sample)); + WI_VERIFY_SUCCEEDED(resamper_output->GetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, &block_align)); + WI_VERIFY_SUCCEEDED(resamper_output->GetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, &avg_per_sec)); + + printf("RO: c:%d sps:%d bps:%d ba:%d aps:%d\n", channels, samples_per_sec, bits_per_sample, block_align, avg_per_sec); + } + + void debug_media_format( + winrt::com_ptr &source_reader, + winrt::com_ptr &resampler) + { + debug_video_format(source_reader); + debug_audio_format(source_reader, resampler); + } + + struct OpenMediaResult + { + winrt::com_ptr source; + winrt::com_ptr source_reader; + winrt::com_ptr resampler; + winrt::com_ptr device_manager; + }; + + /// @brief Open a media file (.mp4) + /// @param device Device to initialise media foundation source reader with + /// @param utf8_filename filename to open + /// @return result of opening the media file + OpenMediaResult open_media(winrt::com_ptr &device, const char *utf8_filename) + { + // Make sure that MF is loaded + WI_VERIFY_SUCCEEDED(MFStartup(MF_VERSION, MFSTARTUP_NOSOCKET)); + + wchar_t filename[256]; + mbstowcs(filename, utf8_filename, sizeof(filename) / sizeof(wchar_t)); + + winrt::com_ptr source_resolver = nullptr; + WI_VERIFY_SUCCEEDED(MFCreateSourceResolver(source_resolver.put())); + + MF_OBJECT_TYPE object_type = MF_OBJECT_INVALID; + winrt::com_ptr source_unknown = nullptr; + winrt::com_ptr source = nullptr; + + WI_VERIFY_SUCCEEDED( + source_resolver->CreateObjectFromURL( + filename, + MF_RESOLUTION_MEDIASOURCE, + nullptr, + &object_type, + source_unknown.put())); + + WI_VERIFY_SUCCEEDED( + source_unknown->QueryInterface(IID_PPV_ARGS(source.put()))); + + // Set up video + + uint32_t reset_token = 0; + winrt::com_ptr device_manager = nullptr; + WI_VERIFY_SUCCEEDED(MFCreateDXGIDeviceManager(&reset_token, device_manager.put())); + WI_VERIFY_SUCCEEDED(device_manager->ResetDevice(device.get(), reset_token)); + + winrt::com_ptr source_attributes = nullptr; + WI_VERIFY_SUCCEEDED( + MFCreateAttributes(source_attributes.put(), 2)); + + // Set up the source reader to use D3D and produce textures. + // First we set a D3D Manager for the source reader, this allows it to create D3D textures instead of decoding + // to CPU memory buffers + WI_VERIFY_SUCCEEDED(source_attributes->SetUnknown(MF_SOURCE_READER_D3D_MANAGER, (IUnknown *)device_manager.get())); + // We must enable shared without mutex here, it looks like the MF Mpeg2 decoder both does not respect keyed mutex + // sharing AND runs on a separate thread. + WI_VERIFY_SUCCEEDED(source_attributes->SetUINT32(MF_SA_D3D11_SHARED_WITHOUT_MUTEX, 1)); + + // We might want these if we wanted to decode to BGRA8 instead of NV12. + WI_VERIFY_SUCCEEDED(source_attributes->SetUINT32(MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING, 1)); + WI_VERIFY_SUCCEEDED(source_attributes->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, 1)); + WI_VERIFY_SUCCEEDED(source_attributes->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_ARGB32)); + + winrt::com_ptr source_reader = nullptr; + WI_VERIFY_SUCCEEDED(MFCreateSourceReaderFromMediaSource(source.get(), source_attributes.get(), source_reader.put())); + + winrt::com_ptr video_type = nullptr; + WI_VERIFY_SUCCEEDED(MFCreateMediaType(video_type.put())); + WI_VERIFY_SUCCEEDED(video_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video)); + WI_VERIFY_SUCCEEDED(video_type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_ARGB32)); + + WI_VERIFY_SUCCEEDED( + source_reader->SetCurrentMediaType( + (DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM, + nullptr, + video_type.get())); + + // Set up audio + winrt::com_ptr source_output_audio_type = nullptr; + WI_VERIFY_SUCCEEDED(MFCreateMediaType(source_output_audio_type.put())); + + WI_VERIFY_SUCCEEDED(source_output_audio_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio)); + WI_VERIFY_SUCCEEDED(source_output_audio_type->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM)); + + WI_VERIFY_SUCCEEDED(source_reader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, nullptr, source_output_audio_type.get())); + // we grab the type back out of the output so that it populates the other properties that we want + source_output_audio_type = nullptr; + WI_VERIFY_SUCCEEDED(source_reader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, source_output_audio_type.put())); + + // Set up resamplers' output type + winrt::com_ptr resampler_output_type = nullptr; + WI_VERIFY_SUCCEEDED(MFCreateMediaType(resampler_output_type.put())); + + WI_VERIFY_SUCCEEDED(resampler_output_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio)); + WI_VERIFY_SUCCEEDED(resampler_output_type->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM)); + WI_VERIFY_SUCCEEDED(resampler_output_type->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, 2)); + WI_VERIFY_SUCCEEDED(resampler_output_type->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, AUDIO_SAMPLE_RATE)); + WI_VERIFY_SUCCEEDED(resampler_output_type->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, 4)); + WI_VERIFY_SUCCEEDED(resampler_output_type->SetUINT32(MF_MT_AUDIO_AVG_BYTES_PER_SECOND, AUDIO_SAMPLE_RATE * 4)); + WI_VERIFY_SUCCEEDED(resampler_output_type->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, 16)); + WI_VERIFY_SUCCEEDED(resampler_output_type->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, TRUE)); + + // Create our resampler (takes audio in arbitrary formats and converts to AUDIO_SAMPLE_RATEhz PCM) + winrt::com_ptr resampler_unknown = nullptr; + winrt::com_ptr resampler = nullptr; + WI_VERIFY_SUCCEEDED(CoCreateInstance(CLSID_CResamplerMediaObject, nullptr, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)resampler_unknown.put())); + WI_VERIFY_SUCCEEDED(resampler_unknown->QueryInterface(IID_PPV_ARGS(resampler.put()))); + winrt::com_ptr resampler_props = nullptr; + WI_VERIFY_SUCCEEDED(resampler_unknown->QueryInterface(IID_PPV_ARGS(resampler_props.put()))); + resampler_props->SetHalfFilterLength(60); + + // Setup the resamplers input and output types to match our source on one end + // and desktopcapture on the other + resampler->SetInputType(0, source_output_audio_type.get(), 0); + resampler->SetOutputType(0, resampler_output_type.get(), 0); + + WI_VERIFY_SUCCEEDED(resampler->ProcessMessage(MFT_MESSAGE_COMMAND_FLUSH, 0)); + WI_VERIFY_SUCCEEDED(resampler->ProcessMessage(MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, 0)); + WI_VERIFY_SUCCEEDED(resampler->ProcessMessage(MFT_MESSAGE_NOTIFY_START_OF_STREAM, 0)); + + return OpenMediaResult { + source, + source_reader, + resampler, + device_manager, + }; + } +} // namespace mf + +struct Media +{ + // winrt::com_ptr unknown_resampler; + winrt::com_ptr resampler; + winrt::com_ptr source_reader; + + winrt::com_ptr device_manager; + HANDLE device_handle; + + LONGLONG cur_time = 0; + LONGLONG video_timestamp = 0; + LONGLONG audio_timestamp = 0; + + /// @brief Maybe get a video frame + /// @param output texture to copy frame into + /// @return whether a sample was copied + bool video_frame(winrt::com_ptr &output) + { + if (cur_time < video_timestamp) + return false; + + DWORD flags = 0; + winrt::com_ptr sample = nullptr; + + WI_VERIFY_SUCCEEDED( + source_reader->ReadSample( + MF_SOURCE_READER_FIRST_VIDEO_STREAM, + 0, + nullptr, + &flags, + &video_timestamp, + sample.put())); + + // Not sure we have to care about flags... + + if (!sample) + return false; + + WI_VERIFY_SUCCEEDED(sample->SetSampleTime(video_timestamp)); + + // Extract the texture and subresource from the media buffer. + winrt::com_ptr media_buffer = nullptr; + winrt::com_ptr dxgi_buffer = nullptr; + WI_VERIFY_SUCCEEDED(sample->GetBufferByIndex(0, media_buffer.put())); + WI_VERIFY_SUCCEEDED(media_buffer->QueryInterface(IID_PPV_ARGS(dxgi_buffer.put()))); + winrt::com_ptr texture; + uint32_t subresource_index = 0; + WI_VERIFY_SUCCEEDED(dxgi_buffer->GetResource(IID_PPV_ARGS(texture.put()))); + WI_VERIFY_SUCCEEDED(dxgi_buffer->GetSubresourceIndex(&subresource_index)); + + // At this point, we have a texture, and a subresource with which to + // index into that texture. What we want to do now is to copy this texture out of + // the array of textures (that MF uses internally) and into our own texture. + + // Check whether either the input texture or the ouput texture + // are keyed mutex, and accquire accordingly. + // We know that our input isn't keyed mutexed because we set up media foundation to + // to use shared withouy keyed mutex. + + D3D11_TEXTURE2D_DESC in_desc = {}; + D3D11_TEXTURE2D_DESC out_desc = {}; + texture->GetDesc(&in_desc); + output->GetDesc(&out_desc); + + winrt::com_ptr input_mutex; + winrt::com_ptr output_mutex; + + if (in_desc.MiscFlags & D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX) + { + WI_VERIFY_SUCCEEDED(texture->QueryInterface(IID_PPV_ARGS(input_mutex.put()))); + WI_VERIFY_SUCCEEDED(input_mutex->AcquireSync(0, INFINITE)); + } + if (out_desc.MiscFlags & D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX) + { + WI_VERIFY_SUCCEEDED(output->QueryInterface(IID_PPV_ARGS(output_mutex.put()))); + WI_VERIFY_SUCCEEDED(output_mutex->AcquireSync(0, INFINITE)); + } + + winrt::com_ptr device; + winrt::com_ptr context; + texture->GetDevice(device.put()); + device->GetImmediateContext(context.put()); + + auto box = D3D11_BOX {0, 0, 0, out_desc.Width, out_desc.Height, 1}; + context->CopySubresourceRegion(output.get(), 0, 0, 0, 0, texture.get(), subresource_index, &box); + + if (input_mutex) + { + WI_VERIFY_SUCCEEDED(input_mutex->ReleaseSync(0)); + } + if (output_mutex) + { + WI_VERIFY_SUCCEEDED(output_mutex->ReleaseSync(0)); + } + return true; + } + + struct AudioSampleResult + { + winrt::com_ptr sample; + LONGLONG time; + }; + + /// @brief Get an audio sample + /// @return An MF sample, and the time it relates to + AudioSampleResult audio_sample() + { + auto result = AudioSampleResult {}; + + DWORD flags = 0; + winrt::com_ptr sample = nullptr; + WI_VERIFY_SUCCEEDED( + source_reader->ReadSample( + MF_SOURCE_READER_FIRST_AUDIO_STREAM, + 0, + nullptr, + &flags, + &result.time, + sample.put())); + + if (sample) + { + WI_VERIFY_SUCCEEDED(sample->SetSampleTime(result.time)); + } + + result.sample = sample; + + return result; + } + + /// @brief Get an audio frame + /// @param output buffer to copy frame into + void audio_frame(std::vector &output) + { + if (cur_time < audio_timestamp) + return; + + // Feed resampler with samples until it will no longer accept data + while (true) + { + DWORD status = 0; + // Make sure the resampler can take more input before we read and throw away a sample + resampler->GetInputStatus(0, &status); + if (status != MFT_INPUT_STATUS_ACCEPT_DATA) + break; + + auto sample = audio_sample(); + if (sample.sample == nullptr) + break; + + WI_VERIFY_SUCCEEDED(resampler->ProcessInput(0, sample.sample.get(), 0)); + } + + // Create a media buffer and a sample for the resampler + // to output data into + winrt::com_ptr buffer; + auto output_buffer = MFT_OUTPUT_DATA_BUFFER {}; + memset(&output_buffer, 0, sizeof(MFT_OUTPUT_DATA_BUFFER)); + WI_VERIFY_SUCCEEDED(MFCreateSample(&output_buffer.pSample)); + WI_VERIFY_SUCCEEDED(MFCreateMemoryBuffer(1024, buffer.put())); + WI_VERIFY_SUCCEEDED(output_buffer.pSample->AddBuffer(buffer.get())); + output_buffer.dwStreamID = 0; + output_buffer.pEvents = nullptr; + output_buffer.dwStatus = 0; + + DWORD status = 0; + WI_VERIFY_SUCCEEDED(resampler->ProcessOutput(0, 1, &output_buffer, &status)); + auto sample = output_buffer.pSample; + + // Use the resampled output time as the new timestamp + WI_VERIFY_SUCCEEDED(sample->GetSampleTime(&audio_timestamp)); + + winrt::com_ptr media_buffer = nullptr; + WI_VERIFY_SUCCEEDED(sample->ConvertToContiguousBuffer(media_buffer.put())); + sample->Release(); + + uint8_t *begin = nullptr; + DWORD len = 0; + WI_VERIFY_SUCCEEDED(media_buffer->Lock(&begin, nullptr, &len)); + output.resize(len, 0); + memcpy(output.data(), begin, len); + media_buffer->Unlock(); + } + + /// @brief Get media frames if they are ready + /// @param elapsed How much time has elapsed since the last frame + /// @param output_audio Output buffer for audio + /// @param output_texture Output texture for video + /// @param produced_audio Whether a texture has been produced + void frame( + LONGLONG elapsed, + std::vector &output_audio, + winrt::com_ptr &output_texture, + bool &produced_video) + { + cur_time += elapsed; + produced_video = video_frame(output_texture); + audio_frame(output_audio); + } +}; + +#include + +#include + +#include + +using namespace rainway; + +static void log_sink(RainwayLogLevel level, const char *target, const char *message) +{ + const char *LOG_LEVELS[] = { + "silent", + "error", + "warn", + "info", + "debug", + "trace", + }; + printf("[RW] (%8s) %s: %s\n", LOG_LEVELS[level], target, message); +} + +int main(int argc, const char *argv[]) +{ + if (argc < 3) + { + printf("Usage: %s \n", argv[0]); + exit(1); + } + + const auto api_key = argv[1]; + const auto media_path = argv[2]; + + auto alive_mutex = std::mutex {}; + auto alive_streams = std::unordered_set {}; + + auto config = Config {}; + config.apiKey = api_key; + config.externalId = "video-player"; + config.onConnectionRequest = [](const Runtime &runtime, ConnectionRequest request) { + printf("Accepting connection: %s from %s\n", request.id().c_str(), request.externalId().c_str()); + request.accept(); + }; + config.onPeerStateChange = [](const Runtime &runtime, Peer peer, RainwayPeerState state) { + if (state == RAINWAY_PEER_STATE_CONNECTED) + printf("Peer connected: %s\n", peer.externalId().c_str()); + else if (state == RAINWAY_PEER_STATE_FAILED) + printf("Peer failed: %s\n", peer.externalId().c_str()); + }; + config.onPeerDataChannel = [](const Runtime &runtime, Peer peer, const std::string &channel, RainwayChannelMode mode) { + printf("Remote peer created new channel: %s %s", peer.externalId().c_str(), channel.c_str()); + }; + config.onStreamRequest = [&](const Runtime &runtime, StreamRequest request) { + printf("Accepting stream request: from %s\n", request.peer().externalId().c_str()); + request.accept( + rainway::StreamConfig { + RAINWAY_STREAM_TYPE_BYOFB, + (RainwayInputLevel)(RAINWAY_INPUT_LEVEL_ALL)}); + }; + config.onStreamStart = [&](const Runtime &runtime, Stream stream) { + auto streams_lock = std::scoped_lock {alive_mutex}; + alive_streams.insert(stream); + + // Spawn thread for this stream + auto thread = std::thread { + [&, stream = stream]() { + WI_VERIFY_SUCCEEDED(CoInitializeEx(nullptr, COINIT_DISABLE_OLE1DDE)); + + auto device = dx::create_device(); + auto result = mf::open_media(device, media_path); + mf::debug_media_format(result.source_reader, result.resampler); + + auto media = Media { + result.resampler, + result.source_reader, + result.device_manager, + }; + + auto prev = std::chrono::high_resolution_clock::now(); + + std::vector audio = {}; + winrt::com_ptr texture = dx::create_texture(device, 1920, 1080, DXGI_FORMAT_B8G8R8A8_UNORM); + + LONGLONG deadline = 0; + + while (true) + { + { + auto streams_lock = std::scoped_lock {alive_mutex}; + if (auto it = alive_streams.find(stream); it == alive_streams.end()) + { + printf("Stream went down\n"); + return; + } + } + + // This deadline is when we need to get the next samples from the media + // Since the media is at 30 FPS, we often dont have anything new to submit. This + // deadline allows us to not spin the thread, and allow other threads to be scheduled + // whilst we are not doing anything. It also significantly reduces contention on + // the texture keyed mutexes. + if (deadline != 0) + { + using namespace std::chrono_literals; + auto wake_point = std::chrono::time_point {100ns * deadline}; + std::this_thread::sleep_until(wake_point); + } + + auto now = std::chrono::high_resolution_clock::now(); + auto elapsed_nanos = (now - prev).count(); + prev = now; + + audio.resize(0); + + bool produced_video = false; + media.frame(elapsed_nanos / 100, audio, texture, produced_video); + + if (produced_video && texture) + { + stream.submitFrame(texture.get()); + } + + deadline = media.video_timestamp; + + if (audio.size() > 0) + { + auto audio_buffer = RainwayAudioBuffer {RAINWAY_AUDIO_BUFFER_PCM, (int16_t *)audio.data()}; + // Convert from byte length to samples + // 2 bytes per sample, 1 sample per n channels + auto sample_count = audio.size() / 2 / 2; + + auto submission = RainwayAudioSubmit { + AUDIO_SAMPLE_RATE, + (uint16_t)2, + audio_buffer, + sample_count, + }; + stream.submitAudio(submission); + } + + // Use the closest deadline + deadline = std::min(deadline, media.audio_timestamp); + } + }, + }; + + thread.detach(); + }; + config.onStreamEnd = [&](const Runtime &runtime, Stream stream) { + auto streams_lock = std::scoped_lock {alive_mutex}; + alive_streams.erase(stream); + }; + + rainway_set_log_level(RAINWAY_LOG_LEVEL_DEBUG, nullptr); + rainway_set_log_sink(log_sink); + + auto runtime_promise = Runtime::initialize(config); + runtime_promise.wait(); + + auto runtime = std::get>(runtime_promise.get()); + + printf("Connected to Rainway Network\nYour peerId is %llu\n", runtime->peerId()); + + using namespace std::chrono_literals; + std::this_thread::sleep_for(10000h); +}