dropLin 3 өдөр өмнө
commit
ad780f763e
100 өөрчлөгдсөн 7756 нэмэгдсэн , 0 устгасан
  1. 227 0
      .admin/copyright
  2. 2 0
      .gitattributes
  3. 53 0
      .gitignore
  4. 128 0
      CODE_OF_CONDUCT.md
  5. 3 0
      LICENSE
  6. 28 0
      Makefile
  7. 52 0
      README.md
  8. 28 0
      demos/README.md
  9. 163 0
      demos/bash/ota_update/.gitignore
  10. 71 0
      demos/bash/ota_update/README.md
  11. 164 0
      demos/bash/ota_update/send_ota.sh
  12. 12 0
      demos/bash/ota_update/test/Dockerfile
  13. 74 0
      demos/bash/ota_update/test/get_test_zip.py
  14. 31 0
      demos/bash/ota_update/test/test.sh
  15. 53 0
      demos/c_c++/GoProC_C++Demo/.gitignore
  16. 14 0
      demos/c_c++/GoProC_C++Demo/CMakeLists.txt
  17. 159 0
      demos/c_c++/GoProC_C++Demo/README.md
  18. 41 0
      demos/c_c++/GoProC_C++Demo/build.sh
  19. 6 0
      demos/c_c++/GoProC_C++Demo/conanfile.txt
  20. 471 0
      demos/c_c++/GoProC_C++Demo/src/media.cpp
  21. 211 0
      demos/c_c++/GoProC_C++Demo/src/stream.cpp
  22. 394 0
      demos/c_c++/GoProStreamDemo/.gitignore
  23. 139 0
      demos/c_c++/GoProStreamDemo/BufferNegotiator.cpp
  24. 29 0
      demos/c_c++/GoProStreamDemo/BufferNegotiator.h
  25. 67 0
      demos/c_c++/GoProStreamDemo/GPWNetwork.cpp
  26. 35 0
      demos/c_c++/GoProStreamDemo/GPWNetwork.h
  27. 353 0
      demos/c_c++/GoProStreamDemo/GoProStreamDemo.cpp
  28. 15 0
      demos/c_c++/GoProStreamDemo/GoProStreamDemo.h
  29. BIN
      demos/c_c++/GoProStreamDemo/GoProStreamDemo.ico
  30. BIN
      demos/c_c++/GoProStreamDemo/GoProStreamDemo.rc
  31. 31 0
      demos/c_c++/GoProStreamDemo/GoProStreamDemo.sln
  32. 215 0
      demos/c_c++/GoProStreamDemo/GoProStreamDemo.vcxproj
  33. 82 0
      demos/c_c++/GoProStreamDemo/GoProStreamDemo.vcxproj.filters
  34. 168 0
      demos/c_c++/GoProStreamDemo/HTTPRequest.cpp
  35. 34 0
      demos/c_c++/GoProStreamDemo/HTTPRequest.h
  36. 9 0
      demos/c_c++/GoProStreamDemo/README.md
  37. 30 0
      demos/c_c++/GoProStreamDemo/Resource.h
  38. 336 0
      demos/c_c++/GoProStreamDemo/UDPSocketCapture.cpp
  39. 40 0
      demos/c_c++/GoProStreamDemo/UDPSocketCapture.h
  40. 419 0
      demos/c_c++/GoProStreamDemo/demuxing_decoding.c
  41. 10 0
      demos/c_c++/GoProStreamDemo/demuxing_decoding.h
  42. 4 0
      demos/c_c++/GoProStreamDemo/packages.config
  43. 0 0
      demos/c_c++/GoProStreamDemo/raw.img
  44. 0 0
      demos/c_c++/GoProStreamDemo/raw720.img
  45. BIN
      demos/c_c++/GoProStreamDemo/small.ico
  46. 24 0
      demos/c_c++/GoProStreamDemo/stdafx.h
  47. 11 0
      demos/c_c++/GoProStreamDemo/targetver.h
  48. 388 0
      demos/csharp/GoProCSharpSample/.gitignore
  49. 6 0
      demos/csharp/GoProCSharpSample/App.config
  50. 9 0
      demos/csharp/GoProCSharpSample/App.xaml
  51. 20 0
      demos/csharp/GoProCSharpSample/App.xaml.cs
  52. 111 0
      demos/csharp/GoProCSharpSample/GoProCSharpSample.csproj
  53. 25 0
      demos/csharp/GoProCSharpSample/GoProCSharpSample.sln
  54. 52 0
      demos/csharp/GoProCSharpSample/MainWindow.xaml
  55. 712 0
      demos/csharp/GoProCSharpSample/MainWindow.xaml.cs
  56. 58 0
      demos/csharp/GoProCSharpSample/Properties/AssemblyInfo.cs
  57. 74 0
      demos/csharp/GoProCSharpSample/Properties/Resources.Designer.cs
  58. 117 0
      demos/csharp/GoProCSharpSample/Properties/Resources.resx
  59. 33 0
      demos/csharp/GoProCSharpSample/Properties/Settings.Designer.cs
  60. 7 0
      demos/csharp/GoProCSharpSample/Properties/Settings.settings
  61. 26 0
      demos/csharp/GoProCSharpSample/README.md
  62. 134 0
      demos/csharp/webcam/.gitignore
  63. 35 0
      demos/csharp/webcam/App.config
  64. 9 0
      demos/csharp/webcam/App.xaml
  65. 20 0
      demos/csharp/webcam/App.xaml.cs
  66. 148 0
      demos/csharp/webcam/GoProWebcamViewer.csproj
  67. 25 0
      demos/csharp/webcam/GoProWebcamViewer.sln
  68. 58 0
      demos/csharp/webcam/Properties/AssemblyInfo.cs
  69. 74 0
      demos/csharp/webcam/Properties/Resources.Designer.cs
  70. 117 0
      demos/csharp/webcam/Properties/Resources.resx
  71. 89 0
      demos/csharp/webcam/Properties/Settings.Designer.cs
  72. 21 0
      demos/csharp/webcam/Properties/Settings.settings
  73. 32 0
      demos/csharp/webcam/README.md
  74. 63 0
      demos/csharp/webcam/StreamViewer.xaml
  75. 337 0
      demos/csharp/webcam/StreamViewer.xaml.cs
  76. 7 0
      demos/csharp/webcam/packages.config
  77. 17 0
      demos/ionic/file_transfer/.browserslistrc
  78. 16 0
      demos/ionic/file_transfer/.editorconfig
  79. 47 0
      demos/ionic/file_transfer/.eslintrc.json
  80. 31 0
      demos/ionic/file_transfer/.gitignore
  81. 96 0
      demos/ionic/file_transfer/android/.gitignore
  82. 3 0
      demos/ionic/file_transfer/android/.idea/.gitignore
  83. 6 0
      demos/ionic/file_transfer/android/.idea/compiler.xml
  84. 30 0
      demos/ionic/file_transfer/android/.idea/jarRepositories.xml
  85. 9 0
      demos/ionic/file_transfer/android/.idea/misc.xml
  86. 2 0
      demos/ionic/file_transfer/android/app/.gitignore
  87. 51 0
      demos/ionic/file_transfer/android/app/build.gradle
  88. 27 0
      demos/ionic/file_transfer/android/app/capacitor.build.gradle
  89. 21 0
      demos/ionic/file_transfer/android/app/proguard-rules.pro
  90. 29 0
      demos/ionic/file_transfer/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
  91. 76 0
      demos/ionic/file_transfer/android/app/src/main/AndroidManifest.xml
  92. 6 0
      demos/ionic/file_transfer/android/app/src/main/assets/capacitor.config.json
  93. 38 0
      demos/ionic/file_transfer/android/app/src/main/assets/capacitor.plugins.json
  94. 8 0
      demos/ionic/file_transfer/android/app/src/main/java/io/numbersprotocol/capturelite/experiments/gopro/MainActivity.java
  95. BIN
      demos/ionic/file_transfer/android/app/src/main/res/drawable-land-hdpi/splash.png
  96. BIN
      demos/ionic/file_transfer/android/app/src/main/res/drawable-land-mdpi/splash.png
  97. BIN
      demos/ionic/file_transfer/android/app/src/main/res/drawable-land-xhdpi/splash.png
  98. BIN
      demos/ionic/file_transfer/android/app/src/main/res/drawable-land-xxhdpi/splash.png
  99. BIN
      demos/ionic/file_transfer/android/app/src/main/res/drawable-land-xxxhdpi/splash.png
  100. BIN
      demos/ionic/file_transfer/android/app/src/main/res/drawable-port-hdpi/splash.png

+ 227 - 0
.admin/copyright

@@ -0,0 +1,227 @@
+#!/usr/bin/env bash
+
+# copyright/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro).
+# This copyright was auto-generated on Wed, Sep  1, 2021  5:06:27 PM
+
+COPYRIGHT="##FILE##/Open GoPro, Version ##VERSION## (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro)."
+TIME="This copyright was auto-generated on ##TIME##"
+
+INPUT=
+CHECK_ONLY=false
+
+num_wrong=0
+
+help() {
+    cat <<EOF
+
+Usage: copyright [OPTIONS] VERSION
+Check for or apply copyrights to source files.
+
+Required positional arguments:
+    VERSION          version to check for / apply in the copyright
+
+Optional arguments:
+    -i INPUT         INPUT is directory to analyze. By default if not passed, git changed / added files will be analyzed.
+    -h               Print this Help.
+    -c               Check only (and output problems). Do not apply changes. Returns 3 if any missing copyrights found
+
+EOF
+}
+
+# MacOS doesn't have realpath. This is taken from https://stackoverflow.com/questions/3572030/bash-script-absolute-path-with-os-x
+realpath_support_macos() {
+    [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
+}
+
+main() {
+    # Build array of input files
+    IFS=$'\n' # Change interframe separator to handle files with spaces
+    if [ -z "$INPUT" ]; then
+        echo "Analyzing added / modified files."
+        files=($(git diff --cached --name-only --diff-filter=ACM)) # Added / modified files
+    else
+        echo "Analyzing all files recursively at $(realpath_support_macos $INPUT)."
+        # All files except for those in .git and
+        # Lunr files which must contain front matter at first line
+        files=($(find $INPUT -type f ! -path "./.git/*" ! -path "./docs/assets/js/lunr/*"))
+    fi
+
+    # Ensure there are any files to analyze
+    if [[ "${#files[@]}" == 0 ]]; then
+        echo "No files to analyze."
+        exit 0
+    fi
+
+    FORMAT="%-120s | %-7s | %-7s | %-7s | %-7s |"
+    printf "\n$FORMAT\n" "FILE" "CORRECT" "WRONG" "MISSING" "N/A"
+    printf '=%.0s' {1..162}
+    echo
+
+    # Analyze each file for a copyright
+    for file in ${files[@]}; do
+        copyright=
+        line_start=
+        line_end=
+        found_version=
+        incorrect="MISSING"
+
+        # Is there a copyright?
+        # Note! We're only looking at the first 7 lines as an optimization to avoid reading large files.
+        # In actuality, there is no reason for it be after the 3rd line.
+        copyright=$(head -n 7 $file | cat -n | grep -m1 '(C) Copyright.*GoPro' | sed -e 's/^[[:space:]]*//') # Add line numbers when cat'ing, then trim leading whitespace.
+        generic_copyright=$(head -n 7 $file | cat -n | grep -i -m1 'copyright' | sed -e 's/^[[:space:]]*//')
+        if [[ "$copyright" ]]; then
+            # If this file explicitely does not get a copyright, continue
+            if  [[ $(echo "$copyright" | grep -c "No (C) Copyright") > 0 ]]; then
+                printf "$FORMAT\n" "$file" "" "" "" "*"
+                continue
+            fi
+            # Is this the correct version, continue
+            found_version=$(echo "$copyright" | sed 's/.*Version \(.*\) (C).*/\1/')
+            if [[ "$VERSION" == "$found_version" ]]; then
+                printf "$FORMAT\n" "$file" "*"
+                continue
+            fi
+            # The version is incorrect. If we are to make changes (i.e. we are not only checking)...
+            incorrect="WRONG_VERSION"
+            if [[ "$CHECK_ONLY" == false ]]; then
+                # Get line number where copyright starts
+                line_start=$(echo "$copyright" | cut -f1)
+                # And where it ends (line 1 = copyright, line 2 = timestamp, line 3 = newline)
+                line_end=$((line_start + 2))
+                # Strip the lines since we will add the correct copyright below
+                lines="$line_start,$line_end"
+                sed -i.bak "$lines"'d' $file && rm "$file".bak # The .bak is to appease MacOS
+            fi
+        elif [[ "$generic_copyright" ]]; then
+            #this file has a copyright that is potentially not our own
+            printf "$FORMAT\n" "$file" "" "" "" "*"
+            continue
+        fi
+
+        # The copyright was either missing or had an incorrect version. Now see if this file deserves a copyright.
+        # First check known filenames that don't have extensions
+        case $(basename "$file") in
+        Makefile | Dockerfile)
+            start="#"
+            stop=""
+            ;;
+        *)
+            # Now check file extensions
+            extension="${file##*.}"
+            case $extension in
+            py | yml | r | R | rb)
+                start="#"
+                stop=""
+                ;;
+            c | h | cpp | cxx | hpp | js | ts | swift | css | proto | java | cs | kt | kts | ktm | m | php | rs | go | dart)
+                start="/*"
+                stop=" */"
+                ;;
+            lua)
+                start="--"
+                stop=""
+                ;;
+            *)
+                # Special case for shell files. We use the file command since they might not have an extension
+                if [[ $(echo $(file $file) | grep -c "shell script") > 0 ]]; then
+                    start="#"
+                    stop=""
+                else
+                    printf "$FORMAT\n" "$file" "" "" "" "*"
+                    continue
+                fi
+                ;;
+            esac
+            ;;
+        esac
+
+        num_wrong=$((num_wrong + 1))
+        if [[ "$incorrect" == "MISSING" ]]; then
+            printf "$FORMAT" "$file" "" "" "X"
+        else
+            printf "$FORMAT" "$file" "" "X"
+        fi
+
+        # Build the copyright string if desired
+        if [[ "$CHECK_ONLY" == false ]]; then
+            # Replace template strings with actual values and make into comment
+            new_copyright="${COPYRIGHT/"##FILE##"/$(basename "$file")}"
+            new_copyright="${new_copyright/"##VERSION##"/$VERSION}"
+            new_copyright="$start $new_copyright$stop"
+            # Build the time string
+            time="${TIME/"##TIME##"/$(date)}"
+            time="$start $time$stop"
+
+            # Now write it. First we need to find where to put the copyright if there was not already one.
+            if [ -z "$line_start" ]; then
+                # If shebang exists, write on the line after the shebang. Shebang is always on the first line.
+                if [ "$(head -c 2 $file)" = "#!" ]; then
+                    line_start=2
+                # Otherwise, use the first line
+                else
+                    line_start=1
+                fi
+            fi
+
+            # Now build the new file
+            head -n $((line_start - 1)) $file >temp 2>/dev/null # Original file up until the line where the copyright was (or is to be)
+            # MacOS warns about 0 so pipe sdterr to null
+            echo "$new_copyright" >>temp      # Add copyright
+            echo "$time" >>temp               # Add timestamp
+            echo >>temp                       # Add new line
+            tail -n +$line_start $file >>temp # Lines after where the copyright was (or will be)
+            mv temp "$file"                   # Replace the original file
+            echo " --> Added copyright ✔️"
+        else
+            echo
+        fi
+    done
+
+    if [[ "$CHECK_ONLY" == true ]]; then
+        echo
+        echo "Total found $num_wrong missing or incorrect copyrights"
+        if [[ "$num_wrong" > 0 ]]; then
+            exit 3
+        fi
+    fi
+
+    exit 0
+}
+
+# Parse optional arguments
+while getopts "hci:" option; do
+    case ${option} in
+    h) #For option h
+        help
+        exit 0
+        ;;
+    c) #For option c
+        CHECK_ONLY=true
+        ;;
+    i) #For option i
+        INPUT=$OPTARG
+        ;;
+    \? | :)
+        help
+        exit 1
+        ;;
+    *)
+        echo "Unexpected error occurred." >&2
+        help
+        exit 1
+        ;;
+    esac
+done
+
+# Jump to positional arguments
+shift $((OPTIND - 1))
+if [[ $# < 1 ]]; then
+    echo "Missing VERSION parameter" >&2
+    help
+    exit 1
+fi
+
+VERSION=$1
+
+main

+ 2 - 0
.gitattributes

@@ -0,0 +1,2 @@
+capabilities/capabilities.xlsx filter=lfs diff=lfs merge=lfs -text
+capabilities/capabilities.json filter=lfs diff=lfs merge=lfs -text

+ 53 - 0
.gitignore

@@ -0,0 +1,53 @@
+**/*.vscode/
+
+**/*.crt
+/.build
+
+/.env
+**.log
+
+/test.html
+/link_results
+.vscode
+**/venv
+**/.venv
+**/__pycache__/
+**/dist/
+xcuserdata/
+.git
+**/temp
+/*.jpg
+/*.gpmf
+/docs/_config-temp.yml
+# This is built and stored in the Docker image
+/docs/Gemfile.lock
+
+# Vim
+*~
+*.sw[p_]
+
+# Sublime Text
+*.sublime-project
+*.sublime-workspace
+
+# Ruby Gem
+*.gem
+.bundle
+**/vendor/bundle
+
+# Node.js and NPM
+node_modules
+npm-debug.log*
+package-lock.json
+codekit-config.json
+
+# macOS
+.DS_Store
+
+# Jekyll generated files
+/docs/.jekyll-cache
+/docs/.jekyll-metadata
+/docs/.sass-cache
+/docs/_asset_bundler_cache
+/docs/_site
+!/docs/assets/images/demos

+ 128 - 0
CODE_OF_CONDUCT.md

@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+  and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+  overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+  advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+  address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+  professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior,  harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.

+ 3 - 0
LICENSE

@@ -0,0 +1,3 @@
+Some components within the OpenGoPro project include third party libraries which are subject to open source licenses
+specified by their  respective copyright holders.  These third party libraries and associated licenses are identified in
+the relevant thirdPartyDependencies.csv file located in the applicable directory.

+ 28 - 0
Makefile

@@ -0,0 +1,28 @@
+# Makefile/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro).
+# This copyright was auto-generated on Wed, Sep  1, 2021  5:06:25 PM
+
+VERSION:=2.0
+
+PROTO_BUILD_DIR=.build/protobuf
+PYTHON_TUTORIAL_PROTO_DIR=demos/python/tutorial/tutorial_modules/tutorial_5_ble_protobuf/proto
+PYTHON_SDK_PROTO_DIR=demos/python/sdk_wireless_camera_control/open_gopro/models/proto
+KOTLIN_SDK_PROTO_DIR=demos/kotlin/kmp_sdk/wsdk/src/commonMain/kotlin/com/gopro/open_gopro/entity/operation/proto
+
+.PHONY: help
+help: ## Display this help which is generated from Make goal comments
+	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+
+.PHONY: protos
+protos: ## Build generated code from protobuf files
+	@docker compose run --build --rm proto-build
+	@rm -rf ${PYTHON_TUTORIAL_PROTO_DIR}/*pb2.py* && mkdir -p ${PYTHON_TUTORIAL_PROTO_DIR}
+	@cp ${PROTO_BUILD_DIR}/python/* ${PYTHON_TUTORIAL_PROTO_DIR}
+	@rm -rf ${PYTHON_SDK_PROTO_DIR}/*pb2.py* && mkdir -p ${PYTHON_SDK_PROTO_DIR}
+	@cp ${PROTO_BUILD_DIR}/python/* ${PYTHON_SDK_PROTO_DIR}
+	@rm -rf ${KOTLIN_SDK_PROTO_DIR}/* && mkdir -p ${KOTLIN_SDK_PROTO_DIR}
+	@cp ${PROTO_BUILD_DIR}/kotlin/* ${KOTLIN_SDK_PROTO_DIR}
+
+.PHONY: copyright
+copyright: ## Check for and add missing copyrights
+	@echo "©️ Verifying / adding copyrights..."
+	@.admin/copyright -i . $(VERSION)

+ 52 - 0
README.md

@@ -0,0 +1,52 @@
+# Open GoPro Developer README
+
+Current Version: 2.0
+
+<img src="https://raw.githubusercontent.com/gopro/OpenGoPro/gh-pages/assets/images/logos/logo.png" alt="GoPro Logo" style="width: 35%;"/>
+
+This README is only for a developer who wants to modify or contribute to this repo. If you are a user (i.e.
+you don't want to make changes to this repo), see the Open GoPro [Github Pages site](https://gopro.github.io/OpenGoPro/)
+for specs and getting started with Open GoPro.
+
+If you are just looking for demos, you can browse the `demos` folder here.
+
+If you are looking for the [Python SDK](https://pypi.org/project/open_gopro/),
+see its [documentation](https://gopro.github.io/OpenGoPro/python_sdk/).
+
+If you are looking for the [Kotlin SDK](TODO_LINK),
+see its [documentation](https://gopro.github.io/OpenGoPro/kotlin_sdk/).
+
+## Overview
+
+This repo consists of the following types of content:
+
+## Demos
+
+Demos are runnable examples in various languages / frameworks and can be found in the `demos` folder. Demos exist,
+from their own perspective, independent to the Jekyll-based documentation described below. To create a demo,
+follow the "Contributing" section of the [README](demos/README.md) in the `demos` folder.
+
+## Capabilities
+
+These are various specifications to describe specific camera / firmware version capabilities. For more information,
+see the [BLE](https://gopro.github.io/OpenGoPro/ble/features/settings.html#camera-capabilities) or
+[HTTP](https://gopro.github.io/OpenGoPro/http#tag/settings) spec.
+
+## Tools
+
+These are utilities to perform various functionality needed by demos or tutorials. See the README of each tool
+directory for more information.
+
+## Copyright
+
+All relevant source files shall contain a copyright. This is managed via the "Pre Merge Checks" Github Action.
+When a pull request is opened (or updated), this action will search for any missing / incorrect copyrights,
+add / fix them, and update the branch. If for some reason this needs to be done manually, it can be done via:
+
+```
+make copyright
+```
+
+A file can be excluded from this process by adding the following in a comment on the first line:
+
+`No (C) Copyright`

+ 28 - 0
demos/README.md

@@ -0,0 +1,28 @@
+# Demos
+
+This directory contains demos using the Open GoPro API with varying language and frameworks.
+
+This file will also provide a summary of each demo.
+
+## Demos Summary
+
+| Language | Name                                                                   | Supported Platforms   | Notes                                                                                             |
+| -------- | ---------------------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------- |
+| bash     | [Over the Air Firmware Update](./bash/ota_update/README.md)            | Windows, Mac, Linux   |                                                                                                   |
+| C++      | [Basic Media and Stream](./c_c++/GoProC_C++Demo/README.md)             | Windows, Mac, Linux   | Unoptimized Preview Stream                                                                        |
+| C++      | [Low Latency Stream](./c_c++/GoProStreamDemo/README.md)                | Windows, Mac, Linux   | Low Latency Preview and Webcam Streams                                                            |
+| C#       | [BLE and Wifi Connect / Control](./csharp/GoProCSharpSample/README.md) | Windows               |                                                                                                   |
+| C#       | [Webcam Stream](./csharp/webcam/README.md)                             | Windows               | Unoptimized webcam streaming using VLC                                                            |
+| Ionic JS | [BLE and WiFi Connect / Control](./ionic/file_transfer/readme.md)      | iOS, Android          |                                                                                                   |
+| Kotlin   | [Kotlin SDK](./kotlin/kmp_sdk/README.md)                               | iOS (partly), Android | This is a fully featured SDK. See its [home page](https://gopro.github.io/OpenGoPro/kotlin_sdk/). |
+| Python   | [Multi Webcam](./python/multi_webcam/README.md)                        | Windows, Mac, Linux   | Unoptimized streaming using OpenCV                                                                |
+| Python   | [Python SDK](./python/sdk_wireless_camera_control/README.md)           | Windows, Mac, Linux   | This is a fully featured SDK. See its [home page](https://gopro.github.io/OpenGoPro/python_sdk/). |
+| Swift    | [BLE and WiFi Connect / Control](./swift/EnableWiFiDemo/README.md)     | iOS                   |                                                                                                   |
+
+## Contributing
+
+To add a new demo:
+
+1. Add your demo in the appropriate language subfolder of this directory. Create one if it does not exist.
+1. Add a README.md in the demos directory describing what it does and how to use it
+1. Add a summary row to the [summary table](#demos-summary) above.

+ 163 - 0
demos/bash/ota_update/.gitignore

@@ -0,0 +1,163 @@
+**.json
+**.zip
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/

+ 71 - 0
demos/bash/ota_update/README.md

@@ -0,0 +1,71 @@
+# Over the Air Firmware Update Demo
+
+- [Over the Air Firmware Update Demo](#over-the-air-firmware-update-demo)
+  - [Assumptions](#assumptions)
+  - [Prerequisites](#prerequisites)
+  - [OTA Procedure](#ota-procedure)
+  - [Usage](#usage)
+  - [Testing](#testing)
+
+This directory contains a bash script to perform an over-the-air (OTA) firmware (FW) update to the camera.
+
+## Assumptions
+
+-   The host PC is already connected via WiFi to the target camera
+-   The target FW images are already provided from GoPro as a .zip file. These can be found on the
+    [Update Page](https://gopro.com/en/us/update) or programmatically from the
+    [Firmware Catalog](https://api.gopro.com/firmware/v2/catalog) JSON information.
+
+## Prerequisites
+
+Alternatively to satisfying these prerequisites, the script can be run with the Docker option (see [usage](#usage))
+
+-   The script uses [open ssl](https://www.openssl.org/) to calculate the hash so `openssl` must be
+    installed and available on the system path.
+-   The script uses [curl](https://curl.se/) to send HTTP commands so `curl` must be installed and
+    available on the system path.
+
+## OTA Procedure
+
+The OTA procedure requires the following steps:
+
+1. Calculate the SHA1 hash of the target .zip file.
+1. Delete any partially stored data.
+1. Show the update UI
+1. Upload the target firmware to the camera
+1. Notify the camera that the upload is complete
+1. Instruct the camera to load the new firmware
+
+## Usage
+
+Call the `send_ota.sh` script here with the `-h` option for a detailed usage:
+
+```
+Usage: ./send_ota.sh [-d] OTA_UPDATE_FILE
+
+Given a target FW .zip file, calculate its SHA1 hash, then send it over-the-air to an already connected camera.
+Required positional arguments:
+    OTA_UPDATE_FILE  target .zip file to send over-the-air. If using docker, must be passed as relative path
+                     from the directory of this script
+Optional arguments:
+    -d               Use docker for openssl and curl commands.
+    -h               Print this Help.
+```
+
+## Testing
+
+There is a test script that will use a Python script (contained in a Docker container) to:
+
+-   get the FW catalog JSON
+-   get the UPDATE.zip from the link in the JSON
+-   exercise the send_ota.sh script to send this firmware to the camera (outside of the container)
+
+The test script is located `./test/test.sh` and should be passed the camera that is connected, i.e.:
+
+```
+./test.sh "HERO_11"
+```
+
+> Note! The test script needs to be run from the `test` folder
+
+You can also run with the `--help` parameter to get a list of cameras.

+ 164 - 0
demos/bash/ota_update/send_ota.sh

@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+# send_ota.sh/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro).
+# This copyright was auto-generated on Wed Oct 19 15:33:20 UTC 2022
+
+set -e
+
+BASE_URL=http://10.5.5.9:8080
+USE_DOCKER=false
+FILE=
+POLLING_PERIOD_SEC=0.500
+
+help() {
+    cat <<EOF
+Usage: $0 [-d] OTA_UPDATE_FILE
+
+Given a target FW .zip file, calculate its SHA1 hash, then send it over-the-air to an already connected camera.
+Required positional arguments:
+    OTA_UPDATE_FILE  target .zip file to send over-the-air. If using docker, must be passed as relative path
+                     from the directory of this script
+Optional arguments:
+    -d               Use docker for openssl and curl commands.
+    -h               Print this Help.
+
+EOF
+}
+
+##############################################################################################################
+#                          Argument parsing and Setup
+##############################################################################################################
+
+# Parse optional arguments
+while getopts "hd" option; do
+    case ${option} in
+    h) #For option h (help)
+        help
+        exit 0
+        ;;
+    d) #For option d (use docker)
+        USE_DOCKER=true
+        ;;
+    \? | :)
+        echo
+        help
+        exit 1
+        ;;
+    *)
+        echo "Unexpected error occurred." >&2
+        help
+        exit 1
+        ;;
+    esac
+done
+
+# Jump to positional arguments and parse
+shift $((OPTIND - 1))
+if [[ $# < 1 ]]; then
+    echo "ERROR: Missing OTA_UPDATE_FILE argument" >&2
+    echo
+    help
+    exit 1
+fi
+FILE="$1"
+if [ ! -f "$FILE" ]; then
+    echo "ERROR: Cannot find file: \"$FILE\""
+    echo
+    help
+    exit 1
+fi
+
+# Configure curl and openssl commands to be native or docker
+IN_FILE="$FILE" # Store original file name since it might change if using Docker (to the docker image absolute path)
+if $USE_DOCKER; then
+    # Check to see if we need sudo
+    case "$(uname -sr)" in
+    Linux*Microsoft*) ;; # WSL does not need sudo
+    Linux*)
+        SUDO=sudo
+        ;;
+    *) ;;
+    esac
+
+    export MSYS_NO_PATHCONV=1 # Handle variable expansion in Windows MSYS / Git Bash
+    DOCKER_BASE_CMD="$SUDO docker run --rm --mount type=bind,source=$(pwd)/$IN_FILE,target=/test_image"
+    OPENSSL="$DOCKER_BASE_CMD alpine/openssl"
+    CURL="$DOCKER_BASE_CMD curlimages/curl"
+    FILE="/test_image"
+else
+    OPENSSL="openssl"
+    CURL="curl"
+fi
+
+##############################################################################################################
+#                          Local Functions
+##############################################################################################################
+
+function log() {
+    echo
+    echo =====================================================================================================
+    echo $1
+}
+
+# Get the camera statuses and extract status 8 (System Busy)
+# Return 1 (busy) or 0 (not busy)
+function is_system_busy {
+    $CURL -s "$BASE_URL/gopro/camera/state" |
+        grep -v 'settings' |
+        grep -o '\"8\":[0-9]\+' |
+        cut -d':' -f'2'
+}
+
+# Takes curl parameters as input. Then performs the curl command, validates the return, and reads the Busy
+# status until the camera is ready
+# Exits with error 1 if curl command fails
+function curl_validate_pend() {
+    # Send command and check error code
+    if ! $CURL "$@"; then
+        echo "❌ Error!! Command failed. ❌"
+        exit 1
+    fi
+
+    # Wait for system to be ready for next command
+    while [ "$(is_system_busy)" = '1' ]; do
+        sleep $POLLING_PERIOD_SEC
+    done
+}
+
+##############################################################################################################
+#                          Main script
+##############################################################################################################
+
+echo "Calculating SHA1 hash of file: $IN_FILE..."
+sha1_hash=$($OPENSSL dgst -sha1 -hex "$FILE" | sed 's|.*= \(.*\)|\1|g')
+if [ $? -ne 0 ]; then
+    echo "ERROR: Unable to get sha1 digest from file: \"$IN_FILE\""
+    exit 1
+fi
+echo "SHA1 is $sha1_hash"
+
+log "Deleting any partially stored OTA data..."
+curl_validate_pend "$BASE_URL/gp/gpSoftUpdate?request=delete"
+
+log "Showing OTA update UI..."
+curl_validate_pend "$BASE_URL/gp/gpSoftUpdate?request=showui"
+
+log "Sending $IN_FILE to the camera..."
+curl_validate_pend "$BASE_URL/gp/gpSoftUpdate" \
+    -X POST \
+    -H "Content-Type: multipart/form-data" \
+    -F "sha1=$sha1_hash" \
+    -F "offset=0" \
+    -F "file=@$FILE"
+
+log "Notifying camera that transfer is complete..."
+curl_validate_pend "$BASE_URL/gp/gpSoftUpdate" \
+    -X POST \
+    -H "Content-Type: multipart/form-data" \
+    -F "sha1=$sha1_hash" \
+    -F "complete=true"
+
+log "Notifying camera that it should start loading the new firmware..."
+curl_validate_pend "$BASE_URL/gp/gpSoftUpdate?request=start"
+
+log "👍 Image has been transferred succesfully! The camera will now apply the firmware update."
+exit 0

+ 12 - 0
demos/bash/ota_update/test/Dockerfile

@@ -0,0 +1,12 @@
+# Dockerfile/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro).
+# This copyright was auto-generated on Wed Oct 19 15:33:20 UTC 2022
+
+FROM python:3.10
+
+WORKDIR /workdir
+COPY ./test/get_test_zip.py /workdir
+
+RUN pip install --upgrade pip
+RUN pip install requests==2.28.1
+
+ENTRYPOINT ["python", "./get_test_zip.py"]

+ 74 - 0
demos/bash/ota_update/test/get_test_zip.py

@@ -0,0 +1,74 @@
+# get_test_zip.py/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro).
+# This copyright was auto-generated on Wed Oct 19 15:33:20 UTC 2022
+
+import sys
+import json
+import enum
+import logging
+import argparse
+import subprocess
+from pathlib import Path
+from typing import Final, Optional
+
+import requests
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+
+class Camera(enum.Enum):
+    HERO_9 = "HERO9 Black"
+    HERO_10 = "HERO10 Black"
+    HERO_11 = "HERO11 Black"
+
+
+cameras: Final[list[str]] = [camera.name for camera in Camera]
+
+FW_CATALOG: Final = r"https://api.gopro.com/firmware/v2/catalog"
+
+
+def parse_arguments() -> argparse.Namespace:
+    parser = argparse.ArgumentParser(
+        description="Get the firmware catalog JSON, extract the camera OTA URL, and download the .zip"
+    )
+    parser.add_argument("camera", type=str, choices=cameras, help=f"Camera to get FW for")
+    parser.add_argument("-d", "--debug", action="store_true", help="Set to output debug information")
+    return parser.parse_args()
+
+
+def main(args: argparse.Namespace) -> None:
+    # Configure logging
+    if args.debug:
+        logger.setLevel(logging.DEBUG)
+
+    # Get firmware catalog
+    target = eval(f"Camera.{args.camera}").value
+    logger.info(f"Getting .zip for camera: {target}")
+    with requests.get(FW_CATALOG, timeout=10) as response:
+        response.raise_for_status()
+        catalog = json.loads(response.text)
+        if logger.level == logging.DEBUG:
+            logger.debug(json.dumps(catalog, indent=4))
+
+    # Extract link for camera-specific firmware
+    fw_url: Optional[str] = None
+    for camera in catalog["cameras"]:
+        if camera["name"] == target:
+            fw_url = camera["url"]
+            logger.info(f"FW URL is: {fw_url}")
+            break
+    else:
+        raise RuntimeError("Firmware URL not found in firmware catalog")
+
+    # Download zip
+    assert fw_url
+    local_filename = Path("UPDATE.zip")
+    logger.info(f"Downloading zip to {local_filename}")
+    with requests.get(fw_url, stream=True) as response:
+        response.raise_for_status()
+        with open(local_filename, "wb") as fp:
+            for chunk in response.iter_content(chunk_size=8192):
+                fp.write(chunk)
+
+if __name__ == "__main__":
+    main(parse_arguments())

+ 31 - 0
demos/bash/ota_update/test/test.sh

@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+# test.sh/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro).
+# This copyright was auto-generated on Wed Oct 19 15:33:20 UTC 2022
+
+set -e
+
+# Check to see if we need sudo
+case "$(uname -sr)" in
+Linux*Microsoft*) ;; # WSL does not need sudo
+Linux*)
+    SUDO=sudo
+    ;;
+*) ;;
+esac
+
+# Handle variable expansion in Windows MSYS / Git Bash
+export MSYS_NO_PATHCONV=1
+
+pushd ..
+echo "Building Docker image..."
+$SUDO docker build -t ota-test -f ./test/Dockerfile .
+
+# Get the test Zip
+$SUDO docker run --rm --mount type=bind,source=$(pwd)/test,target=/workdir ota-test $@
+
+if [[ $1 != "--help" && $1 != "-h" ]]; then
+    # Now test the script
+    ./send_ota.sh -d ./test/UPDATE.zip
+fi
+
+popd >/dev/null

+ 53 - 0
demos/c_c++/GoProC_C++Demo/.gitignore

@@ -0,0 +1,53 @@
+# Output
+build/
+**/*.log
+
+# Prerequisites
+*.d
+
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Fortran module files
+*.mod
+*.smod
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
+
+# Cmake
+CMakeLists.txt.user
+CMakeCache.txt
+CMakeFiles
+CMakeScripts
+Testing
+Makefile
+cmake_install.cmake
+install_manifest.txt
+compile_commands.json
+CTestTestfile.cmake
+_deps
+
+# Python
+venv/
+.venv/

+ 14 - 0
demos/c_c++/GoProC_C++Demo/CMakeLists.txt

@@ -0,0 +1,14 @@
+project(open_gopro_demo)
+cmake_minimum_required(VERSION 3.1)
+
+add_definitions(-D_CRT_SECURE_NO_WARNINGS)
+
+include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
+conan_basic_setup(TARGETS)
+
+add_executable(stream src/stream.cpp)
+target_link_libraries(stream CONAN_PKG::libcurl)
+
+add_executable(media src/media.cpp)
+target_link_libraries(media CONAN_PKG::libcurl)
+target_link_libraries(media CONAN_PKG::cjson)

+ 159 - 0
demos/c_c++/GoProC_C++Demo/README.md

@@ -0,0 +1,159 @@
+# GoPro C/C++ Demos
+
+- [GoPro C/C++ Demos](#gopro-cc-demos)
+  - [Build](#build)
+    - [Requirements](#requirements)
+    - [Steps](#steps)
+  - [Run](#run)
+    - [Requirements](#requirements-1)
+    - [Steps](#steps-1)
+      - [Media Commands](#media-commands)
+      - [Stream Commands](#stream-commands)
+
+This folder contains C and C++ examples to perform some Open GoPro functionality. There are two examples,
+each of which are detailed in a section below.
+
+1. [**Media Commands**](#media-commands)
+2. [**Stream Commands**](#stream-commands)
+
+## Build
+
+### Requirements
+
+This demo depends on the following external libraries:
+
+-   [libCurl](https://curl.se/download.html): a client-side URL transfer library used to make command requests to the camera over WiFi and get the JSON response
+-   [cJSON](https://github.com/DaveGamble/cJSON): an ultra-light JSON parser that can be used to parse the JSON responses from the WiFi commands
+
+To use the build system contained here, the following programs are required to be installed:
+
+-   [Conan](https://docs.conan.io/en/latest/installation.html): a python-based C / C++ package manager
+    -   If a local python 3 is found, this will be automatically installed
+-   [CMake](https://cmake.org/install/): a project configuration and build tool
+
+### Steps
+
+1. Run the `build.sh` file. This will:
+    - verify existence of requirements (and install Conan if applicable)
+    - use Conan to download and install libCurl and cJson
+    - use CMake to configure build system and build executables
+1. The output binaries will then be available in `build/bin`
+
+## Run
+
+### Requirements
+
+Before running the executables built here, you must first be connected to the camera's WiFi Access Point. This can be
+done via:
+
+1. Connect [BLE](https://gopro.github.io/OpenGoPro/ble) to turn on AP and get [WiFi](https://gopro.github.io/OpenGoPro/http) SSID/PASSPHRASE
+2. Use retrieved WiFi SSID/PASSPHRASE to connect system to GoPro WiFi
+
+A programmatic example of this process can be found in the Open GoPro Python SDK's
+[Connect Wifi Demo](https://gopro.github.io/OpenGoPro/python_sdk/quickstart.html#wifi-demo). This can be run
+(assuming a local Python 3.8.x installation exists) via:
+
+```
+pip install open-gopro
+gopro-wifi
+```
+
+### Steps
+
+#### Media Commands
+
+This demo shows one way to get the media list and download the first media file. It also supports
+requests to get the media list, media info and downloading specific media files.
+
+For a list of possible commands, do:
+
+```
+$ ./build/bin/media_commands --help
+```
+
+Media List:
+
+```bash
+$ ./build/bin/media_commands <-l, --list_files>
+```
+
+Media List(Pretty Print):
+
+```bash
+$ ./build/bin/media_commands <-f, --list_files_pretty>
+```
+
+Media Info:
+
+```bash
+$ ./build/bin/media_commands <-i, --info> <camera_file_path>
+```
+
+Media Info(Pretty Print):
+
+```bash
+$ ./build/bin/media_commands <-p, --info_pretty> <camera_file_path>
+```
+
+Media Download:
+
+```bash
+$ ./build/bin/media_commands <-g, --download> <camera_file_path> <output_path/output_file_name>
+```
+
+Media Hilight Moment:
+
+```bash
+$ ./build/bin/media_commands --tag
+```
+
+Media Hilight File:
+
+```bash
+$ ./build/bin/media_commands --tag-video <video_file_path> <offset_ms>
+```
+
+```bash
+$ ./build/bin/media_commands --tag-photo <photo_file_path>
+```
+
+Media Hilight Remove:
+
+```bash
+$ ./build/bin/media_commands --tag-video-remove <video_file_path> <offset_ms>
+```
+
+```bash
+$ ./build/bin/media_commands --tag-photo-remove <photo_file_path>
+```
+
+Media Demo:
+
+```bash
+$ ./build/bin/media_commands <-d, --demo> <output_path>
+```
+
+#### Stream Commands
+
+This demo demonstrates one way to start and stop the preview stream.
+
+> Note: To run the Preview Stream demo. A media player (i.e: [VLC](https://www.videolan.org/)) that supports UDP is needed to view the preview stream.
+> The UDP address is **_udp://0.0.0.0:8554_**
+
+Start Stream:
+
+```bash
+$ ./build/bin/stream_commands <-s, --start>
+```
+
+Stop Stream:
+
+```bash
+$ ./build/bin/stream_commands <-e, --end>
+```
+
+Preview Stream Demo:
+
+```bash
+$ ./build/bin/stream_commands <-d, --demo>
+```

+ 41 - 0
demos/c_c++/GoProC_C++Demo/build.sh

@@ -0,0 +1,41 @@
+#! /bin/bash
+# build.sh/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro).
+# This copyright was auto-generated on Tue Feb  8 01:22:35 UTC 2022
+
+set -e
+cd "$(dirname "$0")"
+
+# Verify Prerequisites
+if ! command -v cmake &>/dev/null; then
+    echo "cmake can not be found."
+    echo "Please install: https://cmake.org/install/"
+    exit
+fi
+
+CONAN="python3 -m conans.conan"
+if ! $CONAN --version &>/dev/null; then
+    echo "conan can not be found."
+
+    if ! command -v python3 &>/dev/null; then
+        echo "Please install: https://docs.conan.io/en/latest/installation.html"
+        exit
+    else
+        echo "Trying to install conan with discovered python3"
+        pip3 install conan
+    fi
+fi
+
+# Install Conan packages
+mkdir -p build
+cd build
+$CONAN install .. --build=missing
+
+if [ "$(uname)" == "Darwin" ]; then
+    cmake -DCMAKE_BUILD_TYPE=Release ..
+elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
+    cmake -DCMAKE_BUILD_TYPE=Release ..
+else # Windows. Force to 64 bit.
+    cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_GENERATOR_PLATFORM=x64 ..
+fi
+
+cmake --build . --config Release

+ 6 - 0
demos/c_c++/GoProC_C++Demo/conanfile.txt

@@ -0,0 +1,6 @@
+[requires]
+libcurl/7.80.0
+cjson/1.7.15
+
+[generators]
+cmake

+ 471 - 0
demos/c_c++/GoProC_C++Demo/src/media.cpp

@@ -0,0 +1,471 @@
+/* media.cpp/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Wed, Sep  1, 2021  5:05:39 PM */
+
+#include <iostream>
+#include <curl/curl.h>
+#include <cjson/cJSON.h>
+#include <cstring>
+#include <stdio.h>
+/**
+ * MEDIA COMMANDS DEMO:
+ * Commands used in this demo can be found in the WiFi documentation here: https://github.com/gopro/OpenGoPro/tree/main/docs/wifi
+ * List of HTTP Error codes and their meanings can be found here: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
+ */
+
+#define HTTP_ERR_CODE_OK 200
+#define ERR_FAILURE -1
+#define ERR_SUCCESS 0
+
+using namespace std;
+
+enum MediaRequest
+{
+    eUnknown = -1,
+    eListFiles,
+    eListFilesPretty,
+    eMediaInfo,
+    eMediaInfoPretty,
+    eDownloadMedia,
+    eHilightFile,
+    eHilightMoment,
+    eHilightRemove,
+    eDemo
+};
+
+/**
+ * Function to get response code of curl request
+ * @param curlReq - curl request
+ * @return - return http error code
+ */
+long get_response_code(CURL *curlReq)
+{
+    long resp_code;
+    curl_easy_getinfo(curlReq, CURLINFO_RESPONSE_CODE, &resp_code);
+    return resp_code;
+}
+
+/**
+ * Function to send curl request and check response code
+ * @param curl - curl request
+ * @return - 0, if successful. -1 if any step failed
+ */
+long perform_request(CURL *curlReq)
+{
+    CURLcode curlCode = curl_easy_perform(curlReq);
+    // Check if curl request succeeded
+    if(curlCode != CURLE_OK)
+    {
+        printf("\nFailed to perform curl request - received error code:%d\n", curlCode);
+        return ERR_FAILURE;
+    }
+
+    // Check response code
+    long resp_code = get_response_code(curlReq);
+    if(resp_code != HTTP_ERR_CODE_OK)
+    {
+        printf("\nRequest returned http error code:%ld\n", resp_code);
+        return ERR_FAILURE;
+    }
+    return ERR_SUCCESS;
+}
+
+size_t json_response_callback(char *contents, size_t size, size_t mem, void *data)
+{
+    size_t json_size = size * mem;
+    char *json_data  = (char*)data;
+    strcat(json_data, contents);
+    return json_size;
+}
+
+size_t file_response_callback(char *contents, size_t size, size_t mem, void *data)
+{
+    FILE *file  = (FILE*)data;
+    size_t file_size = fwrite(contents, size, mem, file);
+    return file_size;
+}
+
+MediaRequest check_user_request(int num_inputs, char *input_array[])
+{
+    char* input = input_array[1];
+    if(num_inputs == 2)
+    {
+        if((strcmp(input, "--list_files") == 0) || (strcmp(input, "-l") == 0))
+        {
+            return eListFiles;
+        }
+        if((strcmp(input, "--list_files_pretty") == 0) || (strcmp(input, "-f") == 0))
+        {
+            return eListFilesPretty;
+        }
+        if(strcmp(input, "--tag-moment") == 0)
+        {
+            return eHilightMoment;
+        }
+        return eUnknown;
+    }
+
+    if(num_inputs == 3)
+    {
+        if((strcmp(input, "--info") == 0) || (strcmp(input, "-i") == 0))
+        {
+            return eMediaInfo;
+        }
+        if((strcmp(input, "--info_pretty") == 0) || (strcmp(input, "-p") == 0))
+        {
+            return eMediaInfoPretty;
+        }
+        if((strcmp(input, "--demo") == 0) || (strcmp(input, "-d") == 0))
+        {
+            return eDemo;
+        }
+        if(strcmp(input, "--tag-photo") == 0)
+        {
+            return eHilightFile;
+        }
+        if(strcmp(input, "--tag-photo-remove") == 0)
+        {
+            return eHilightRemove;
+        }
+    }
+
+    if(num_inputs >= 4)
+    {
+        if((strcmp(input, "--download") == 0) || (strcmp(input, "-g") == 0))
+        {
+            return eDownloadMedia;
+        }
+
+        if(strcmp(input, "--tag-video") == 0)
+        {
+            return eHilightFile;
+        }
+        if(strcmp(input, "--tag-video-remove") == 0)
+        {
+            return eHilightRemove;
+        }
+    }
+    return eUnknown;
+}
+
+int send_write_request(CURL *curl, const char *path, const char* curl_response, FILE *file)
+{
+    if(curl_response == NULL && file == NULL)
+    {
+        printf("\nBoth inputs to write data are NULL");
+        return ERR_FAILURE;
+    }
+
+    CURLcode code = curl_easy_setopt(curl, CURLOPT_URL, path);
+    if(code != CURLE_OK)
+    {
+        printf("\nFailed to perform curl operation - received error code:%d", code);
+        curl_easy_cleanup(curl);
+        return ERR_FAILURE;
+    }
+
+    // check if we should write the data to a const char* or FILE*
+    if(file == NULL)
+    {
+        code = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, json_response_callback);
+        if(code != CURLE_OK)
+        {
+            printf("\nFailed to setup callback function - received error code:%d", code);
+            curl_easy_cleanup(curl);
+            return ERR_FAILURE;
+        }
+
+        code = curl_easy_setopt(curl, CURLOPT_WRITEDATA, curl_response);
+        if (code != CURLE_OK) {
+            printf("\nFailed to write json data");
+            curl_easy_cleanup(curl);
+            return ERR_FAILURE;
+        }
+    }
+    else
+    {
+        code = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, file_response_callback);
+        if(code != CURLE_OK)
+        {
+            printf("\nFailed to setup callback function - received error code:%d", code);
+            curl_easy_cleanup(curl);
+            return ERR_FAILURE;
+        }
+        code = curl_easy_setopt(curl, CURLOPT_WRITEDATA, file);
+        if (code != CURLE_OK)
+        {
+            printf("\nFailed to write json data");
+            curl_easy_cleanup(curl);
+            return ERR_FAILURE;
+        }
+    }
+
+    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
+
+    if(perform_request(curl) == ERR_FAILURE)
+    {
+        printf("\nFailed to perform request:%s", path);
+        curl_easy_cleanup(curl);
+        return ERR_FAILURE;
+    }
+    return ERR_SUCCESS;
+}
+
+int download_media_file(CURL *curl, const char *media_file, const char *out_file)
+{
+    if(curl == NULL || media_file == NULL || out_file == NULL)
+    {
+        printf("\ncurl:%p, media_file:%s or out_file:%s is NULL", curl, media_file, out_file);
+        return ERR_FAILURE;
+    }
+
+    FILE *file = fopen(out_file, "w");
+    if(file == NULL)
+    {
+        printf("\nCouldn't open %s\n", out_file);
+        curl_easy_cleanup(curl);
+        return ERR_FAILURE;
+    }
+    string path = "http://10.5.5.9:8080/videos/DCIM/" + string(media_file);
+    printf("\nDownloading %s to %s\n", media_file, out_file);
+    if(send_write_request(curl, path.c_str(), NULL, file) != ERR_SUCCESS)
+    {
+        fclose(file);
+        curl_easy_cleanup(curl);
+        return ERR_FAILURE;
+    }
+    fclose(file);
+    return ERR_SUCCESS;
+}
+
+void help()
+{
+    printf("\nUsage");
+    printf("\n\t./media_commands <-l, --list_files>");
+    printf("\n\t./media_commands <-f, --list_files_pretty>");
+    printf("\n\t./media_commands <-i, --info> <camera_file_path>(i.e: 100GOPRO/GH010433.MP4");
+    printf("\n\t./media_commands <-p, --info_pretty> <camera_file_path>(i.e: 100GOPRO/GH010433.MP4");
+    printf("\n\t./media_commands <-g, --download> <camera_file_path> <output_path/output_file_name>");
+    printf("\n\t./media_commands --tag-moment");
+    printf("\n\t./media_commands --tag-video <video_file_path> <offset_ms>");
+    printf("\n\t./media_commands --tag-photo <photo_file_path>");
+    printf("\n\t./media_commands --tag-video-remove <video_file_path> <offset_ms>");
+    printf("\n\t./media_commands --tag-photo-remove <photo_file_path>");
+    printf("\n\t./media_commands <-d, --demo> <output_path>\n");
+}
+
+int download_first_file(CURL *curl, cJSON *fs_array, const char* directory, const char* output_path)
+{
+    cJSON *media_item = fs_array->child;
+    if(cJSON_GetArraySize(fs_array) <= 0)
+    {
+        printf("\nNo media files found on camera");
+        return ERR_SUCCESS;
+    }
+
+    // loop through the first media item
+    while(media_item != NULL)
+    {
+        if((media_item->type == cJSON_String) && (strcmp(media_item->string, "n") == 0))
+        {
+            string dir(directory);
+            string file(media_item->valuestring);
+            string src_path = dir + "/" + file;
+            string dst_path = string(output_path) + file;
+            return download_media_file(curl, src_path.c_str(), dst_path.c_str());
+        }
+        media_item = media_item->next;
+    }
+    printf("\nFailed to parse media file in file system array");
+    return ERR_FAILURE;
+}
+
+int main(int argc, char* argv[])
+{
+    if(argc < 2 || argc > 5)
+    {
+        help();
+        return ERR_FAILURE;
+    }
+
+    MediaRequest request = check_user_request(argc, argv);
+    if(request == eUnknown)
+    {
+        help();
+        return ERR_FAILURE;
+    }
+
+    CURL *curl = curl_easy_init();
+    int ret = ERR_SUCCESS;
+
+    if(curl != NULL)
+    {
+        char curl_response[CURL_MAX_WRITE_SIZE];
+        memset(&curl_response,'\0', CURL_MAX_WRITE_SIZE);
+        switch(request)
+        {
+            case eListFiles:
+            {
+                const string& path = "http://10.5.5.9:8080/gopro/media/list";
+                if(send_write_request(curl, path.c_str(), curl_response, NULL) != ERR_SUCCESS)
+                {
+                    ret =  ERR_FAILURE;
+                    break;
+                }
+                printf("\n%s\n", curl_response);
+                break;
+            }
+            case eListFilesPretty:
+            {
+                const string& path = "http://10.5.5.9:8080/gopro/media/list";
+                if(send_write_request(curl, path.c_str(), curl_response, NULL) != ERR_SUCCESS)
+                {
+                    return ERR_FAILURE;
+                }
+                cJSON *json = cJSON_Parse(curl_response);
+                const char *json_pretty = cJSON_Print(json);
+                printf("\n%s\n", json_pretty);
+                break;
+            }
+            case eMediaInfo:
+            {
+                string file(argv[2]);
+                string path = "http://10.5.5.9:8080/gopro/media/info?path=" + file;
+                printf("\n%s info:", file.c_str());
+                if(send_write_request(curl, path.c_str(), curl_response, NULL) != ERR_SUCCESS)
+                {
+                    return ERR_FAILURE;
+                }
+                printf("\n%s\n", curl_response);
+                break;
+            }
+            case eMediaInfoPretty:
+            {
+                string file(argv[2]);
+                string path = "http://10.5.5.9:8080/gopro/media/info?path=" + file;
+                printf("\n%s info:", file.c_str());
+                if(send_write_request(curl, path.c_str(), curl_response, NULL) != ERR_SUCCESS)
+                {
+                    return ERR_FAILURE;
+                }
+                cJSON *json = cJSON_Parse(curl_response);
+                if(json == NULL)
+                {
+                    printf("\nCouldn't parse json response");
+                    ret = ERR_FAILURE;
+                    break;
+                }
+                const char *json_pretty = cJSON_Print(json);
+                printf("\n%s\n", json_pretty);
+                break;
+            }
+            case eDownloadMedia:
+            {
+                string media_file(argv[2]);
+                string out_file(argv[3]);
+                ret = download_media_file(curl, media_file.c_str(), out_file.c_str());
+                break;
+            }
+            case eHilightFile:
+            {
+                string file(argv[2]);
+                // Get the offset for the tag. Value should be in ms.
+                // Note: if file is a photo, no offset should be given
+                string offset_ms = "";
+                if(argc >= 4)
+                    offset_ms = "&ms=" + string(argv[3]);
+                const string& path = "http://10.5.5.9:8080/gopro/media/hilight/file?path=" + file + offset_ms;
+                if(send_write_request(curl, path.c_str(), curl_response, NULL) != ERR_SUCCESS)
+                {
+                    return ERR_FAILURE;
+                }
+                printf("\n%s\n", curl_response);
+                break;                break;
+            }
+            case eHilightRemove:
+            {
+                string file(argv[2]);
+                // Get the offset for the tag. Value should be in ms
+                // Note: if file is a photo, no offset should be given
+                string offset_ms = "";
+                if(argc >= 4)
+                    offset_ms = "&ms=" + string(argv[3]);
+                const string& path = "http://10.5.5.9:8080/gopro/media/hilight/remove?path=" + file + offset_ms;
+                if(send_write_request(curl, path.c_str(), curl_response, NULL) != ERR_SUCCESS)
+                {
+                    return ERR_FAILURE;
+                }
+                printf("\n%s\n", curl_response);
+                break;                break;
+            }
+            case eHilightMoment:
+            {
+                const string& path = "http://10.5.5.9:8080/gopro/media/hilight/moment";
+                if(send_write_request(curl, path.c_str(), curl_response, NULL) != ERR_SUCCESS)
+                {
+                    return ERR_FAILURE;
+                }
+                printf("\n%s\n", curl_response);
+                break;                break;
+            }
+            case eDemo:
+            {
+                /*
+                 * Demo will gather media list and download first file
+                 */
+                const string& path = "http://10.5.5.9:8080/gopro/media/list";
+                if(send_write_request(curl, path.c_str(), curl_response, NULL) != ERR_SUCCESS)
+                {
+                    return ERR_FAILURE;
+                }
+                cJSON *json = cJSON_Parse(curl_response);
+                if(json == NULL)
+                {
+                    printf("\nCouldn't parse json response");
+                    ret =  ERR_FAILURE;
+                    break;
+                }
+                cJSON *media_list = NULL;
+                char* directory = NULL;
+                // Find file system array in media list json response
+                for(media_list = json->child; media_list != NULL;)
+                {
+                    if((media_list->type == cJSON_String) && (strcmp(media_list->string, "d") == 0))
+                    {
+                        directory = media_list->valuestring;
+                        media_list = media_list->next;
+                        continue;
+                    }
+                    if((media_list->type == cJSON_Array) && (strcmp(media_list->string, "fs") == 0))
+                    {
+                        media_list = media_list->child;
+                        break;
+                    }
+                    if((media_list->type == cJSON_Array) || (media_list->type == cJSON_Object))
+                    {
+                        media_list = media_list->child;
+                        continue;
+                    }
+                    media_list = media_list->next;
+                }
+
+                cJSON *fs_array = media_list;
+                if(fs_array == NULL)
+                {
+                    printf("\nfs_array is null");
+                    ret =  ERR_FAILURE;
+                    break;
+                }
+                ret = download_first_file(curl, fs_array, directory, argv[2]);
+                break;
+            }
+            default:
+            {
+                printf("\nUnknown media request:%d\n", request);
+                ret =  ERR_FAILURE;
+                break;
+            }
+        }
+    }
+    curl_easy_cleanup(curl);
+    return ret;
+}

+ 211 - 0
demos/c_c++/GoProC_C++Demo/src/stream.cpp

@@ -0,0 +1,211 @@
+/* stream.c/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Wed, Sep  1, 2021  5:05:40 PM */
+
+#include <curl/curl.h>
+#include <string.h>
+#include <stdint.h>
+#include <chrono>
+#include <thread>
+
+/**
+ * PREVIEW STREAM COMMANDS DEMO:
+ * Commands used in this demo can be found in the WiFi documentation here: https://github.com/gopro/OpenGoPro/tree/main/docs/wifi
+ * List of HTTP Error codes and their meanings can be found here: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
+ */
+
+#define HTTP_ERR_CODE_OK 200
+#define ERR_FAILURE -1
+#define ERR_SUCCESS 0
+
+enum PreviewStreamRequest
+{
+    eUnknown = -1,
+    eStartStreamRequest,
+    eEndStreamRequest,
+    eDemo
+};
+
+/**
+ * Function to get response code of curl request
+ * @param curlReq - curl request
+ * @return - return http error code
+ */
+long get_response_code(CURL *curlReq)
+{
+    long resp_code;
+    curl_easy_getinfo(curlReq, CURLINFO_RESPONSE_CODE, &resp_code);
+    return resp_code;
+}
+
+/**
+ * Function to send curl request and check response code
+ * @param curl - curl request
+ * @return - 0, if successful. -1 if any step failed
+ */
+long perform_request(CURL *curlReq)
+{
+    CURLcode curlCode = curl_easy_perform(curlReq);
+    // Check if curl request succeeded
+    if(curlCode != CURLE_OK)
+    {
+        curl_easy_cleanup(curlReq);
+        printf("\nFailed to perform curl request - received CURLcode error:%d\n", curlCode);
+        return ERR_FAILURE;
+    }
+
+    // Check HTTP response code
+    long resp_code = get_response_code(curlReq);
+    if(resp_code != HTTP_ERR_CODE_OK)
+    {
+        curl_easy_cleanup(curlReq);
+        printf("\nRequest returned HTTP error code:%ld\n", resp_code);
+        return ERR_FAILURE;
+    }
+    return ERR_SUCCESS;
+}
+
+enum PreviewStreamRequest check_user_request(int num_inputs, char *input)
+{
+    if(num_inputs == 2)
+    {
+        if((strcmp(input, "-s") == 0) || (strcmp(input, "--start") == 0))
+        {
+            return eStartStreamRequest;
+        }
+        if((strcmp(input, "-e") == 0) || (strcmp(input, "--end") == 0))
+        {
+            return eEndStreamRequest;
+        }
+        if((strcmp(input, "-d") == 0) || (strcmp(input, "--demo") == 0))
+        {
+            return eDemo;
+        }
+    }
+    return eUnknown;
+}
+
+void help()
+{
+    printf("\nUsage");
+    printf("\n\t./stream_commands -s, --start");
+    printf("\n\t./stream_commands -e, --end");
+    printf("\n\t./stream_commands -d, --demo");
+}
+
+int start_stream(CURL *curl)
+{
+    printf("\nStarting preview stream");
+    curl_easy_setopt(curl, CURLOPT_URL, "http://10.5.5.9:8080/gopro/camera/stream/start");
+    if (perform_request(curl) == ERR_FAILURE)
+    {
+        curl_easy_cleanup(curl);
+        return ERR_FAILURE;
+    }
+    return ERR_SUCCESS;
+}
+
+int stop_stream(CURL *curl)
+{
+    printf("\nStopping preview stream");
+    curl_easy_setopt(curl, CURLOPT_URL, "http://10.5.5.9:8080/gopro/camera/stream/stop");
+    if (perform_request(curl) == ERR_FAILURE)
+    {
+        curl_easy_cleanup(curl);
+        return ERR_FAILURE;
+    }
+    return ERR_SUCCESS;
+}
+
+int main(int argc, char *argv[])
+{
+    /**
+     * To test this demo, a media player that supports UDP is necessary.
+     * VLC is a good option, which can be found here https://www.videolan.org/
+     * The UDP address:port is udp://0.0.0.0:8554
+     */
+
+    enum PreviewStreamRequest user_request = check_user_request(argc, argv[1]);
+    if(user_request == eUnknown)
+    {
+        printf("\nCouldn't run command");
+        help();
+        return ERR_FAILURE;
+    }
+
+    CURL *curl = curl_easy_init();
+    if(curl != NULL)
+    {
+        switch(user_request)
+        {
+            case eStartStreamRequest:
+            {
+                if(start_stream(curl) == ERR_FAILURE)
+                {
+                    curl_easy_cleanup(curl);
+                    return ERR_FAILURE;
+                }
+                break;
+            }
+            case eEndStreamRequest:
+            {
+                if(stop_stream(curl) == ERR_FAILURE)
+                {
+                    curl_easy_cleanup(curl);
+                    return ERR_FAILURE;
+                }
+                break;
+            }
+            case eDemo:
+            {
+                uint32_t duration = 0;
+                // Prompt to see how long the user wants the preview stream to run
+                printf("\nEnter stream duration(seconds): ");
+                int result = scanf("%u", &duration);
+
+                if (result != 1)
+                {
+                    printf("\nStream duration was invalid, exiting demo");
+                    curl_easy_cleanup(curl);
+                    return ERR_FAILURE;
+                }
+                // stop preview stream in case it was previously running
+                if(stop_stream(curl) == ERR_FAILURE)
+                {
+                    curl_easy_cleanup(curl);
+                    return ERR_FAILURE;
+                }
+
+                if(start_stream(curl) == ERR_FAILURE)
+                {
+                    curl_easy_cleanup(curl);
+                    return ERR_FAILURE;
+                }
+
+                printf("\nStream will run for %u seconds\n", duration);
+                std::this_thread::sleep_for(std::chrono::seconds(duration));
+
+                if(stop_stream(curl) == ERR_FAILURE)
+                {
+                    curl_easy_cleanup(curl);
+                    return ERR_FAILURE;
+                }
+                printf("\nSuccessfully exited demo");
+                break;
+            }
+            default:
+            {
+                curl_easy_cleanup(curl);
+                printf("\nUnknown request type used:%d", user_request);
+                return ERR_FAILURE;
+            }
+        }
+    }
+    else
+    {
+        printf("\nFailed to init curl object") ;
+        return ERR_FAILURE;
+    }
+
+    curl_easy_cleanup(curl);
+    return ERR_SUCCESS;
+}

+ 394 - 0
demos/c_c++/GoProStreamDemo/.gitignore

@@ -0,0 +1,394 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml

+ 139 - 0
demos/c_c++/GoProStreamDemo/BufferNegotiator.cpp

@@ -0,0 +1,139 @@
+/* BufferNegotiator.cpp/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Sat Mar  5 01:05:52 UTC 2022 */
+
+
+#include "BufferNegotiator.h"
+#include "GoProStreamDemo.h"
+#include <mutex>
+#include <condition_variable>
+#include <algorithm>
+#include "stdafx.h"
+
+std::mutex locker;
+
+std::mutex inputlock;
+std::condition_variable sleepy;
+
+extern "C" uint8_t* video_dst_data[4];
+extern "C" long video_dst_bufsize;
+#define MAX_INPUT_BUF_SIZE 232768
+uint8_t inputbuffer[MAX_INPUT_BUF_SIZE] = { 0 };
+int inputbuffersize = 0;
+static int quit_now = 0;
+
+long GetBuffer(uint8_t* dest, long buflen)
+{
+    long ret = 0;
+    int blackbuf = 0;
+    locker.lock();
+
+#ifndef USE_PREVIEW_STREAM
+    //for 1080
+    if (video_dst_bufsize == 0 || 
+        (video_dst_data[0][0] == 0 //upper left
+            && video_dst_data[0][1035840] == 0 //center
+            && video_dst_data[0][1919] == 0     //upper right
+            && video_dst_data[0][2071680] == 0     //lower left
+            && video_dst_data[0][2073599] == 0)) //lower right
+        blackbuf = 1;
+#else
+    //for 720
+    if (video_dst_bufsize == 0 ||
+        (video_dst_data[0][0] == 0 //upper left
+            && video_dst_data[0][460800] == 0 //center
+            && video_dst_data[0][1279] == 0     //upper right
+            && video_dst_data[0][920320] == 0     //lower left
+            && video_dst_data[0][921599] == 0)) //lower right
+        blackbuf = 1;
+#endif
+
+
+    if (video_dst_bufsize>0 && buflen >= video_dst_bufsize && !blackbuf)
+    {
+        memcpy(dest, video_dst_data[0], video_dst_bufsize);
+        ret = video_dst_bufsize;
+    }
+    else
+    {
+        ret = 0;
+    }
+
+    locker.unlock();
+    return ret;
+}
+
+void LockBuffer()
+{
+    locker.lock();
+}
+
+void UnlockBuffer()
+{
+    locker.unlock();
+}
+
+/*
+This function stores the buffer for consumption by the decoding/demuxing thread
+*/
+void WriteInputBuffer(uint8_t* buf, int buf_size)
+{
+    inputlock.lock();
+    if (buf_size <= MAX_INPUT_BUF_SIZE)
+    {
+        //buffer is full
+        if (inputbuffersize + buf_size > MAX_INPUT_BUF_SIZE)
+        {
+            //clear the buffer
+            inputbuffersize = 0;
+            OutputDebugStringA("clearing the buffer\n");
+        }
+        if (buf[0] == 'G' && (buf_size % 188 == 0))
+        {
+            memcpy(&inputbuffer[inputbuffersize], buf, buf_size);
+            inputbuffersize += buf_size;
+
+        }
+
+        sleepy.notify_all();
+    }
+    inputlock.unlock();
+}
+
+void StopInput()
+{
+    inputlock.lock();
+    quit_now = 1;
+    sleepy.notify_all();
+    inputlock.unlock();
+}
+
+void StartInput()
+{
+    inputlock.lock();
+    quit_now = 0;
+    inputlock.unlock();
+}
+
+int ReadInputBuffer(uint8_t* buf, int buf_size)
+{
+    if (quit_now)
+    {
+        return -1;
+    }
+    std::unique_lock<std::mutex> lck(inputlock);
+    while (inputbuffersize == 0 && quit_now==0)
+    {
+        sleepy.wait(lck);
+    }
+    if (quit_now)
+    {
+        return -1;
+    }
+    int new_size = min(buf_size, inputbuffersize);
+    /* copy internal buffer data to buf */
+    memcpy(buf, inputbuffer, new_size);
+    memmove(inputbuffer, &inputbuffer[new_size], inputbuffersize - new_size);
+    inputbuffersize -= new_size;
+
+    return new_size;
+}

+ 29 - 0
demos/c_c++/GoProStreamDemo/BufferNegotiator.h

@@ -0,0 +1,29 @@
+/* BufferNegotiator.h/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Sat Mar  5 01:05:53 UTC 2022 */
+
+#pragma once
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+    long GetBuffer(uint8_t* dest, long buflen);
+
+    void LockBuffer();
+
+    void UnlockBuffer();
+
+    void StartInput();
+
+    void StopInput();
+
+    void WriteInputBuffer(uint8_t* buf, int buf_size);
+
+    int ReadInputBuffer(uint8_t* buf, int buf_size);
+
+#ifdef __cplusplus
+}
+#endif

+ 67 - 0
demos/c_c++/GoProStreamDemo/GPWNetwork.cpp

@@ -0,0 +1,67 @@
+/* GPWNetwork.cpp/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Sat Mar  5 01:05:53 UTC 2022 */
+
+
+#include "GPWNetwork.h"
+
+WSASession::WSASession()
+{
+    int ret = WSAStartup(MAKEWORD(2, 2), &data);
+    if (ret != 0)
+        throw std::system_error(WSAGetLastError(), std::system_category(), "WSAStartup Failed");
+}
+WSASession::~WSASession()
+{
+    WSACleanup();
+}
+
+UDPSocket::UDPSocket()
+{
+    sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+    if (sock == INVALID_SOCKET)
+    {
+        int blah = WSAGetLastError();
+        printf("error opening socket %d", blah);
+        throw std::system_error(WSAGetLastError(), std::system_category(), "Error opening socket");
+    }
+}
+UDPSocket::~UDPSocket()
+{
+    closesocket(sock);
+}
+
+void UDPSocket::SendTo(const std::string& address, unsigned short port, const char* buffer, int len, int flags)
+{
+    sockaddr_in add;
+    add.sin_family = AF_INET;
+    InetPtonA(AF_INET, (PCSTR)(address.c_str()), &add.sin_addr.s_addr);
+    
+    add.sin_port = htons(port);
+    int ret = sendto(sock, buffer, len, flags, reinterpret_cast<SOCKADDR *>(&add), sizeof(add));
+    if (ret < 0)
+        throw std::system_error(WSAGetLastError(), std::system_category(), "sendto failed");
+}
+
+sockaddr_in UDPSocket::RecvFrom(char* buffer, int len, int& received, int flags)
+{
+    sockaddr_in from;
+    int size = sizeof(from);
+    int ret = recvfrom(sock, buffer, len, flags, reinterpret_cast<SOCKADDR *>(&from), &size);
+    if (ret < 0)
+        throw std::system_error(WSAGetLastError(), std::system_category(), "recvfrom failed");
+    received = ret;
+
+    return from;
+}
+void UDPSocket::Bind(unsigned short port)
+{
+    sockaddr_in add;
+    add.sin_family = AF_INET;
+    add.sin_addr.s_addr = htonl(INADDR_ANY);
+    add.sin_port = htons(port);
+
+    int ret = bind(sock, reinterpret_cast<SOCKADDR *>(&add), sizeof(add));
+    if (ret < 0)
+        throw std::system_error(WSAGetLastError(), std::system_category(), "Bind failed");
+}
+

+ 35 - 0
demos/c_c++/GoProStreamDemo/GPWNetwork.h

@@ -0,0 +1,35 @@
+/* GPWNetwork.h/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Sat Mar  5 01:05:53 UTC 2022 */
+
+#pragma once
+
+#include <WinSock2.h>
+#include <WS2tcpip.h>
+#include <system_error>
+#include <string>
+#include <iostream>
+
+
+class WSASession
+{
+public:
+    WSASession();
+    virtual ~WSASession();
+
+private:
+    WSAData data;
+};
+
+class UDPSocket
+{
+public:
+    UDPSocket();
+    virtual ~UDPSocket();
+
+    void SendTo(const std::string& address, unsigned short port, const char* buffer, int len, int flags = 0);
+    sockaddr_in RecvFrom(char* buffer, int len, int& received, int flags = 0);
+    void Bind(unsigned short port);
+
+private:
+    SOCKET sock;
+};

+ 353 - 0
demos/c_c++/GoProStreamDemo/GoProStreamDemo.cpp

@@ -0,0 +1,353 @@
+/* GoProStreamDemo.cpp/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Sat Mar  5 01:05:52 UTC 2022 */
+
+// GoProStreamDemo.cpp : Defines the entry point for the application.
+//
+
+#include "stdafx.h"
+#include "GoProStreamDemo.h"
+#include <thread>
+#include "UDPSocketCapture.h"
+#include <d2d1.h>
+extern "C"
+{
+#include "libavcodec\avcodec.h"
+}
+
+#define BUF_SIZE_1080 3110400 // 1920 * 1080 + (1920 *1080)/2
+#define BUF_SIZE_720  1382400 // 1280 * 720 + (1280 *720)/2
+
+int BUF_SIZE = BUF_SIZE_1080;
+int HEIGHT = 1080;
+int WIDTH = 1920;
+
+#ifndef max
+#define max(a,b)            (((a) > (b)) ? (a) : (b))
+#endif
+
+#ifndef min
+#define min(a,b)            (((a) < (b)) ? (a) : (b))
+#endif
+
+int quitNow = 0;
+HWND myWin = 0;
+std::thread myThread;
+std::thread capThread;
+
+HBITMAP myBitmap;
+COLORREF* imgBuf = (COLORREF*)calloc(2073600, sizeof(COLORREF));
+void yuv2rgb(uint8_t yValue, uint8_t uValue, uint8_t vValue,
+    uint8_t* r, uint8_t* g, uint8_t* b);
+
+void CaptureFrames();
+
+#define MAX_LOADSTRING 100
+
+// Global Variables:
+HINSTANCE hInst;                                // current instance
+WCHAR szTitle[MAX_LOADSTRING];                  // The title bar text
+WCHAR szWindowClass[MAX_LOADSTRING];            // the main window class name
+
+// Forward declarations of functions included in this code module:
+ATOM                MyRegisterClass(HINSTANCE hInstance);
+BOOL                InitInstance(HINSTANCE, int);
+LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
+INT_PTR CALLBACK    About(HWND, UINT, WPARAM, LPARAM);
+
+UDPSocketCapture capper;
+
+int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
+                     _In_opt_ HINSTANCE hPrevInstance,
+                     _In_ LPWSTR    lpCmdLine,
+                     _In_ int       nCmdShow)
+{
+    UNREFERENCED_PARAMETER(hPrevInstance);
+    UNREFERENCED_PARAMETER(lpCmdLine);
+
+
+    // Initialize global strings
+    LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
+    LoadStringW(hInstance, IDC_GOPROSTREAMDEMO, szWindowClass, MAX_LOADSTRING);
+    MyRegisterClass(hInstance);
+
+    // Perform application initialization:
+    if (!InitInstance (hInstance, nCmdShow))
+    {
+        return FALSE;
+    }
+
+    HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_GOPROSTREAMDEMO));
+#ifdef USE_PREVIEW_STREAM
+    HEIGHT = 720;
+    WIDTH = 1280;
+    BUF_SIZE = BUF_SIZE_720;
+#endif
+    MSG msg;
+    capper.Start();
+
+    capThread = std::thread(CaptureFrames);
+    capThread.detach();
+
+    // Main message loop:
+    while (GetMessage(&msg, nullptr, 0, 0))
+    {
+        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
+        {
+            TranslateMessage(&msg);
+            DispatchMessage(&msg);
+        }
+    }
+    capper.Stop();
+    return (int) msg.wParam;
+}
+
+//
+//  FUNCTION: MyRegisterClass()
+//
+//  PURPOSE: Registers the window class.
+//
+ATOM MyRegisterClass(HINSTANCE hInstance)
+{
+    WNDCLASSEXW wcex;
+
+    wcex.cbSize = sizeof(WNDCLASSEX);
+
+    wcex.style          = CS_HREDRAW | CS_VREDRAW;
+    wcex.lpfnWndProc    = WndProc;
+    wcex.cbClsExtra     = 0;
+    wcex.cbWndExtra     = 0;
+    wcex.hInstance      = hInstance;
+    wcex.hIcon          = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_GOPROSTREAMDEMO));
+    wcex.hCursor        = LoadCursor(nullptr, IDC_ARROW);
+    wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
+    wcex.lpszMenuName   = MAKEINTRESOURCEW(IDC_GOPROSTREAMDEMO);
+    wcex.lpszClassName  = szWindowClass;
+    wcex.hIconSm        = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
+
+    return RegisterClassExW(&wcex);
+}
+
+//
+//   FUNCTION: InitInstance(HINSTANCE, int)
+//
+//   PURPOSE: Saves instance handle and creates main window
+//
+//   COMMENTS:
+//
+//        In this function, we save the instance handle in a global variable and
+//        create and display the main program window.
+//
+BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
+{
+   hInst = hInstance; // Store instance handle in our global variable
+
+   HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
+      CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
+
+   if (!hWnd)
+   {
+      return FALSE;
+   }
+   myWin = hWnd;
+   ShowWindow(hWnd, nCmdShow);
+   UpdateWindow(hWnd);
+
+   return TRUE;
+}
+
+//
+//  FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
+//
+//  PURPOSE: Processes messages for the main window.
+//
+//  WM_COMMAND  - process the application menu
+//  WM_PAINT    - Paint the main window
+//  WM_DESTROY  - post a quit message and return
+//
+//
+LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
+{
+    switch (message)
+    {
+    case WM_COMMAND:
+        {
+            int wmId = LOWORD(wParam);
+            // Parse the menu selections:
+            switch (wmId)
+            {
+            case IDM_ABOUT:
+                DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
+                break;
+            case IDM_EXIT:
+                DestroyWindow(hWnd);
+                break;
+            default:
+                return DefWindowProc(hWnd, message, wParam, lParam);
+            }
+        }
+        break;
+    case WM_PAINT:
+        {
+            RECT rect;
+            int width;
+            int height;
+            
+            if (GetWindowRect(hWnd, &rect))
+            {
+                width = rect.right - rect.left;
+                height = rect.bottom - rect.top;
+            }
+            PAINTSTRUCT ps;
+            HDC hdc = BeginPaint(hWnd, &ps);
+            // TODO: Add any drawing code that uses hdc here...
+
+            /////////////////
+#ifdef SHOW_CAM_OUTPUT
+            // Creating temp bitmap
+            HBITMAP map = CreateBitmap(WIDTH, 
+                HEIGHT, 
+                1, 
+                32, 
+                (void*)imgBuf); 
+
+            HDC src = CreateCompatibleDC(hdc); 
+            SelectObject(src, map); 
+
+            BitBlt(hdc, 
+                10,  
+                10,  
+                WIDTH, 
+                HEIGHT, 
+                src, 
+                0,   
+                0,   
+                SRCCOPY); 
+            DeleteObject(map);
+            DeleteDC(src);
+#endif
+            
+            EndPaint(hWnd, &ps);
+        }
+        break;
+    case WM_DESTROY:
+        quitNow = 1;
+        PostQuitMessage(0);
+        break;
+    default:
+        return DefWindowProc(hWnd, message, wParam, lParam);
+    }
+    return 0;
+}
+
+// Message handler for about box.
+INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
+{
+    UNREFERENCED_PARAMETER(lParam);
+    switch (message)
+    {
+    case WM_INITDIALOG:
+        return (INT_PTR)TRUE;
+
+    case WM_COMMAND:
+        if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
+        {
+            EndDialog(hDlg, LOWORD(wParam));
+            return (INT_PTR)TRUE;
+        }
+        break;
+    }
+    return (INT_PTR)FALSE;
+}
+
+void CaptureFrames()
+{
+    uint8_t* buf = (uint8_t*)malloc(BUF_SIZE_1080);
+    static int countstart = 0;
+    long bytesReturned = 0;
+    //std::this_thread::sleep_for(std::chrono::seconds(6));
+    while (!quitNow)
+    {
+        capper.GetBuffer(buf, BUF_SIZE, bytesReturned);
+        
+        if (bytesReturned > 100)
+        {
+            //for 1080 frames
+            //0-2073599 = Y
+            //2073600 - 2591999 = U
+            //2592000 - 3110399 = V
+            uint8_t r, g, b;
+            int uindex = HEIGHT*WIDTH, vindex = HEIGHT*WIDTH + ((HEIGHT*WIDTH)/4);
+            int strider = 0;
+#ifdef SHOW_CAM_OUTPUT
+#ifndef SHOW_CAM_OUTPUT_ORIGINAL
+            for (int i = 0; i < HEIGHT * WIDTH; i++)
+            {
+                int lineNumber = i / WIDTH;
+                int place = i % WIDTH;
+                int oddline = ((lineNumber) % 2);
+                strider = ((place) / 2) + (((lineNumber / 2) + (lineNumber%2)) * (WIDTH/2));
+                if (oddline)
+                    strider -= (WIDTH/2);
+                if (place == WIDTH-1)
+                    strider += 0;
+                yuv2rgb(buf[i], buf[uindex + strider], buf[vindex+strider], &r, &g, &b);
+                imgBuf[i] = RGB(b, g, r);
+                //imgBuf[i] = RGB(buf[i], buf[i], buf[i]);
+            }
+#endif
+#endif
+#ifdef SHOW_CAM_OUTPUT
+#ifdef SHOW_CAM_OUTPUT_ORIGINAL
+            //this is the Y channel
+            for (int j = 0, i = 0; i < HEIGHT*WIDTH; j++, i++)
+            {
+                imgBuf[j] = RGB(buf[i], buf[i], buf[i]);
+            }
+            //uncomment this to see UV channels
+            /*
+            for (int j = 0, i = HEIGHT*WIDTH; i < BUF_SIZE; j++, i++)
+            {
+                imgBuf[j] = RGB(buf[i], buf[i], buf[i]);
+            }
+            */
+#endif
+#endif
+        
+#ifdef SHOW_CAM_OUTPUT
+            InvalidateRect(myWin, NULL, false);
+            UpdateWindow(myWin);
+#endif
+
+        }
+        else
+        {
+            bytesReturned = 0;
+        }
+        
+        std::this_thread::sleep_for(std::chrono::milliseconds(20));
+    }
+
+    free(buf);
+}
+
+void yuv2rgb(uint8_t yValue, uint8_t uValue, uint8_t vValue,
+                        uint8_t* r, uint8_t* g, uint8_t* b) 
+{
+    /*int rTmp = yValue + (1.403 * vValue);
+    int gTmp = yValue - (0.344 * uValue) - (0.714 * vValue);
+    int bTmp = yValue + (1.770 * uValue);*/
+        
+    /*int rTmp = yValue + (1.402 * (vValue - 128));
+    int gTmp = yValue - (0.34414 * (uValue - 128)) - (0.71414 * (vValue - 128));
+    int bTmp = yValue + (1.772 * (uValue - 128));*/
+
+    int rTmp = yValue + (1.370705 * (vValue - 128));
+    int gTmp = yValue - (0.698001 * (vValue - 128)) - (0.337633 * (uValue - 128));
+    int bTmp = yValue + (1.732446 * (uValue - 128));
+
+    *r = min(max(rTmp, 0), 255);
+    *g = min(max(gTmp, 0), 255);
+    *b = min(max(bTmp, 0), 255);
+}
+
+

+ 15 - 0
demos/c_c++/GoProStreamDemo/GoProStreamDemo.h

@@ -0,0 +1,15 @@
+/* GoProStreamDemo.h/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Sat Mar  5 01:05:53 UTC 2022 */
+
+#pragma once
+
+#include "resource.h"
+
+//show cam output in directx using bad bitmap display
+#define SHOW_CAM_OUTPUT
+
+//show the original YUV image
+//#define SHOW_CAM_OUTPUT_ORIGINAL
+
+//Use preview stream (720p) instead of webcam 
+//#define USE_PREVIEW_STREAM

BIN
demos/c_c++/GoProStreamDemo/GoProStreamDemo.ico


BIN
demos/c_c++/GoProStreamDemo/GoProStreamDemo.rc


+ 31 - 0
demos/c_c++/GoProStreamDemo/GoProStreamDemo.sln

@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.28307.572
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GoProStreamDemo", "GoProStreamDemo.vcxproj", "{1E3DC643-1E3D-4EC1-B7A7-3720A97EFC6D}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|x64 = Debug|x64
+		Debug|x86 = Debug|x86
+		Release|x64 = Release|x64
+		Release|x86 = Release|x86
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{1E3DC643-1E3D-4EC1-B7A7-3720A97EFC6D}.Debug|x64.ActiveCfg = Debug|x64
+		{1E3DC643-1E3D-4EC1-B7A7-3720A97EFC6D}.Debug|x64.Build.0 = Debug|x64
+		{1E3DC643-1E3D-4EC1-B7A7-3720A97EFC6D}.Debug|x86.ActiveCfg = Debug|Win32
+		{1E3DC643-1E3D-4EC1-B7A7-3720A97EFC6D}.Debug|x86.Build.0 = Debug|Win32
+		{1E3DC643-1E3D-4EC1-B7A7-3720A97EFC6D}.Release|x64.ActiveCfg = Release|x64
+		{1E3DC643-1E3D-4EC1-B7A7-3720A97EFC6D}.Release|x64.Build.0 = Release|x64
+		{1E3DC643-1E3D-4EC1-B7A7-3720A97EFC6D}.Release|x86.ActiveCfg = Release|Win32
+		{1E3DC643-1E3D-4EC1-B7A7-3720A97EFC6D}.Release|x86.Build.0 = Release|Win32
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {756D0A0F-0730-4B11-BF9D-7770FF93A682}
+	EndGlobalSection
+EndGlobal

+ 215 - 0
demos/c_c++/GoProStreamDemo/GoProStreamDemo.vcxproj

@@ -0,0 +1,215 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup Label="ProjectConfigurations">
+    <ProjectConfiguration Include="Debug|Win32">
+      <Configuration>Debug</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|Win32">
+      <Configuration>Release</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Debug|x64">
+      <Configuration>Debug</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|x64">
+      <Configuration>Release</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+  </ItemGroup>
+  <PropertyGroup Label="Globals">
+    <VCProjectVersion>15.0</VCProjectVersion>
+    <ProjectGuid>{1E3DC643-1E3D-4EC1-B7A7-3720A97EFC6D}</ProjectGuid>
+    <Keyword>Win32Proj</Keyword>
+    <RootNamespace>GoProStreamDemo</RootNamespace>
+    <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
+    <ProjectName>GoProStreamDemo</ProjectName>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <UseDebugLibraries>true</UseDebugLibraries>
+    <PlatformToolset>v141</PlatformToolset>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <UseDebugLibraries>false</UseDebugLibraries>
+    <PlatformToolset>v141</PlatformToolset>
+    <WholeProgramOptimization>true</WholeProgramOptimization>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <UseDebugLibraries>true</UseDebugLibraries>
+    <PlatformToolset>v142</PlatformToolset>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
+    <ConfigurationType>Application</ConfigurationType>
+    <UseDebugLibraries>false</UseDebugLibraries>
+    <PlatformToolset>v143</PlatformToolset>
+    <WholeProgramOptimization>true</WholeProgramOptimization>
+    <CharacterSet>Unicode</CharacterSet>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+  <ImportGroup Label="ExtensionSettings">
+  </ImportGroup>
+  <ImportGroup Label="Shared">
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+  </ImportGroup>
+  <PropertyGroup Label="UserMacros" />
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <LinkIncremental>true</LinkIncremental>
+    <IncludePath>..\ffmpeg32\include;..\Filters\Network;$(IncludePath)</IncludePath>
+    <LibraryPath>..\ffmpeg32\lib;$(LibraryPath)</LibraryPath>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <LinkIncremental>true</LinkIncremental>
+    <IncludePath>$(IncludePath)</IncludePath>
+    <LibraryPath>$(LibraryPath)</LibraryPath>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <LinkIncremental>false</LinkIncremental>
+    <IncludePath>..\ffmpeg32\include;..\Filters\Network;$(IncludePath)</IncludePath>
+    <LibraryPath>..\ffmpeg32\lib;$(LibraryPath)</LibraryPath>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <LinkIncremental>false</LinkIncremental>
+    <IncludePath>$(IncludePath)</IncludePath>
+    <LibraryPath>$(LibraryPath)</LibraryPath>
+  </PropertyGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <ClCompile>
+      <PrecompiledHeader>NotUsing</PrecompiledHeader>
+      <WarningLevel>Level3</WarningLevel>
+      <Optimization>Disabled</Optimization>
+      <SDLCheck>true</SDLCheck>
+      <PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;_WINSOCK_DEPRECATED_NO_WARNINGS;WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+    </ClCompile>
+    <Link>
+      <SubSystem>Windows</SubSystem>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+      <AdditionalDependencies>bcrypt.lib;Ws2_32.lib;avcodec.lib;avutil.lib;avformat.lib;%(AdditionalDependencies)</AdditionalDependencies>
+      <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
+      <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <ClCompile>
+      <PrecompiledHeader>NotUsing</PrecompiledHeader>
+      <WarningLevel>Level3</WarningLevel>
+      <Optimization>Disabled</Optimization>
+      <SDLCheck>true</SDLCheck>
+      <PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;_WINSOCK_DEPRECATED_NO_WARNINGS;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+    </ClCompile>
+    <Link>
+      <SubSystem>Windows</SubSystem>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+      <AdditionalDependencies>Ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
+      <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
+    </Link>
+    <PostBuildEvent>
+      <Command>copy "$(ProjectDir)raw.img" "$(OutputPath)"
+copy "$(ProjectDir)raw720.img" "$(OutputPath)"</Command>
+    </PostBuildEvent>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <ClCompile>
+      <PrecompiledHeader>NotUsing</PrecompiledHeader>
+      <WarningLevel>Level3</WarningLevel>
+      <Optimization>MaxSpeed</Optimization>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <SDLCheck>true</SDLCheck>
+      <PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;_WINSOCK_DEPRECATED_NO_WARNINGS;WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+    </ClCompile>
+    <Link>
+      <SubSystem>Windows</SubSystem>
+      <EnableCOMDATFolding>true</EnableCOMDATFolding>
+      <OptimizeReferences>true</OptimizeReferences>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+      <AdditionalDependencies>bcrypt.lib;Ws2_32.lib;avcodec.lib;avutil.lib;avformat.lib;%(AdditionalDependencies)</AdditionalDependencies>
+      <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
+      <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
+    </Link>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <ClCompile>
+      <PrecompiledHeader>NotUsing</PrecompiledHeader>
+      <WarningLevel>Level3</WarningLevel>
+      <Optimization>MaxSpeed</Optimization>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <SDLCheck>true</SDLCheck>
+      <PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;_WINSOCK_DEPRECATED_NO_WARNINGS;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <ConformanceMode>true</ConformanceMode>
+    </ClCompile>
+    <Link>
+      <SubSystem>Windows</SubSystem>
+      <EnableCOMDATFolding>true</EnableCOMDATFolding>
+      <OptimizeReferences>true</OptimizeReferences>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+      <AdditionalDependencies>Ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
+      <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
+    </Link>
+    <PostBuildEvent>
+      <Command>copy "$(ProjectDir)raw.img" "$(OutputPath)"
+copy "$(ProjectDir)raw720.img" "$(OutputPath)"</Command>
+    </PostBuildEvent>
+  </ItemDefinitionGroup>
+  <ItemGroup>
+    <ClInclude Include="BufferNegotiator.h" />
+    <ClInclude Include="demuxing_decoding.h" />
+    <ClInclude Include="GPWNetwork.h" />
+    <ClInclude Include="HTTPRequest.h" />
+    <ClInclude Include="UDPSocketCapture.h" />
+    <ClInclude Include="GoProStreamDemo.h" />
+    <ClInclude Include="Resource.h" />
+    <ClInclude Include="stdafx.h" />
+    <ClInclude Include="targetver.h" />
+  </ItemGroup>
+  <ItemGroup>
+    <ClCompile Include="BufferNegotiator.cpp" />
+    <ClCompile Include="demuxing_decoding.c" />
+    <ClCompile Include="GPWNetwork.cpp" />
+    <ClCompile Include="HTTPRequest.cpp" />
+    <ClCompile Include="UDPSocketCapture.cpp" />
+    <ClCompile Include="GoProStreamDemo.cpp" />
+  </ItemGroup>
+  <ItemGroup>
+    <ResourceCompile Include="GoProStreamDemo.rc" />
+  </ItemGroup>
+  <ItemGroup>
+    <Image Include="GoProStreamDemo.ico" />
+    <Image Include="small.ico" />
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+  <ImportGroup Label="ExtensionTargets">
+    <Import Project="packages\FFmpeg.Nightly.20200831.1.0\build\native\FFmpeg.Nightly.targets" Condition="Exists('packages\FFmpeg.Nightly.20200831.1.0\build\native\FFmpeg.Nightly.targets')" />
+  </ImportGroup>
+  <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
+    <PropertyGroup>
+      <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
+    </PropertyGroup>
+    <Error Condition="!Exists('packages\FFmpeg.Nightly.20200831.1.0\build\native\FFmpeg.Nightly.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\FFmpeg.Nightly.20200831.1.0\build\native\FFmpeg.Nightly.targets'))" />
+  </Target>
+</Project>

+ 82 - 0
demos/c_c++/GoProStreamDemo/GoProStreamDemo.vcxproj.filters

@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup>
+    <Filter Include="Source Files">
+      <UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
+      <Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
+    </Filter>
+    <Filter Include="Header Files">
+      <UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
+      <Extensions>h;hh;hpp;hxx;hm;inl;inc;ipp;xsd</Extensions>
+    </Filter>
+    <Filter Include="Resource Files">
+      <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
+      <Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
+    </Filter>
+  </ItemGroup>
+  <ItemGroup>
+    <ClInclude Include="stdafx.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="targetver.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="Resource.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="GoProStreamDemo.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="GPWNetwork.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="BufferNegotiator.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="demuxing_decoding.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="UDPSocketCapture.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+    <ClInclude Include="HTTPRequest.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
+  </ItemGroup>
+  <ItemGroup>
+    <ClCompile Include="GoProStreamDemo.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="demuxing_decoding.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="BufferNegotiator.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="GPWNetwork.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="HTTPRequest.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+    <ClCompile Include="UDPSocketCapture.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
+  </ItemGroup>
+  <ItemGroup>
+    <ResourceCompile Include="GoProStreamDemo.rc">
+      <Filter>Resource Files</Filter>
+    </ResourceCompile>
+  </ItemGroup>
+  <ItemGroup>
+    <Image Include="small.ico">
+      <Filter>Resource Files</Filter>
+    </Image>
+    <Image Include="GoProStreamDemo.ico">
+      <Filter>Resource Files</Filter>
+    </Image>
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
+</Project>

+ 168 - 0
demos/c_c++/GoProStreamDemo/HTTPRequest.cpp

@@ -0,0 +1,168 @@
+/* HTTPRequest.cpp/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Sat Mar  5 01:05:53 UTC 2022 */
+
+#include "HTTPRequest.h"
+
+HTTPRequest::HTTPRequest(const std::string& host, const short port)
+    : Host(host), Port(port)
+{
+    InitWinsock();
+    if ((Sock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
+    {
+        int err = WSAGetLastError();
+        throw std::exception("Couldn't create socket " + err);
+    }
+}
+
+HTTPRequest::~HTTPRequest()
+{
+    WSACleanup();
+    closesocket(Sock);
+}
+
+
+std::string HTTPRequest::get_response()
+{
+    return Response;
+}
+
+bool HTTPRequest::InitWinsock()
+{
+    WSADATA wsaData;
+
+    if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0)
+    {
+        return false;
+    }
+
+    return true;
+}
+
+bool HTTPRequest::loop_recieve()
+{
+    while (true)
+    {
+        char recvBuf[256];
+
+        auto nret = recv(Sock, recvBuf, sizeof(recvBuf), 0);
+        if (nret == -1)
+        {
+            return false;
+        }
+        else if (nret == 0)
+        {
+            break;
+        }
+
+        Response.append(recvBuf, nret);
+    }
+
+    return true;
+}
+
+bool HTTPRequest::resolve_and_connect()
+{
+    bool retbool = true;
+    if (connected)
+        return retbool;
+    sockaddr_in add;
+    add.sin_family = AF_INET;
+    InetPtonA(AF_INET, Host.c_str(), &add.sin_addr.s_addr);
+    add.sin_port = htons(Port);
+    int ret = -1;
+
+    // Set non-blocking 
+    unsigned long modeB = 1;
+    ret = ioctlsocket(Sock, FIONBIO, &modeB);
+    if (ret != NO_ERROR)
+        OutputDebugStringA("ioctlsocket failed\n");
+
+    try
+    {
+        ret = connect(Sock, reinterpret_cast<SOCKADDR*>(&add), sizeof(add));
+
+        if (ret < 0)
+        {
+            int boo = WSAGetLastError();
+            if (boo == WSAEWOULDBLOCK)
+            {
+                OutputDebugStringA("connect in progress - selecting\n");
+                fd_set setW, setE;
+
+                FD_ZERO(&setW);
+                FD_SET(Sock, &setW);
+                FD_ZERO(&setE);
+                FD_SET(Sock, &setE);
+
+                timeval time_out = { 0 };
+                time_out.tv_sec = 2;
+                time_out.tv_usec = 0;
+
+                ret = select(0, NULL, &setW, &setE, &time_out);
+                if (ret <= 0)
+                {
+                    // select() failed or connection timed out
+                    closesocket(Sock);
+                    if (ret == 0)
+                    {
+                        TimedOut = true;
+                        WSASetLastError(WSAETIMEDOUT);
+                    }
+                    ret = -1;
+                }
+                if (FD_ISSET(Sock, &setE))
+                {
+                    // connection failed
+                    closesocket(Sock);
+                    ret = -1;
+                }
+                // put socked in blocking mode...
+                modeB = 0;
+                if (ioctlsocket(Sock, FIONBIO, &modeB) == SOCKET_ERROR)
+                {
+                    closesocket(Sock);
+                    ret = -1;
+                }
+            }
+            else
+            {
+                ret = -1;
+            }
+        }
+    }
+    catch(...)
+    {
+
+    }
+    if (ret < 0)
+    {
+        int err = WSAGetLastError();
+        char buf[128] = { 0 };
+        sprintf(buf, "error opening socket %d", err);
+        OutputDebugStringA(buf);
+    }
+    else
+    {
+        connected = true;
+    }
+
+    return retbool;
+}
+
+bool HTTPRequest::get_request(const std::string& path)
+{
+    if (!resolve_and_connect())
+        return false;
+
+    std::string request = "GET " + path + " HTTP/1.1" + "\r\n";
+    request += "Host: " + Host + "\r\n";
+    request += "Connection: close\r\n";
+    request += "\r\n";
+
+    if (send(Sock, request.c_str(), request.length(), 0) == SOCKET_ERROR)
+    {
+        return false;
+    }
+
+    return loop_recieve();
+}

+ 34 - 0
demos/c_c++/GoProStreamDemo/HTTPRequest.h

@@ -0,0 +1,34 @@
+/* HTTPRequest.h/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Sat Mar  5 01:05:53 UTC 2022 */
+
+#include <winsock2.h>
+#include <ws2tcpip.h>
+#include <string>
+
+class HTTPRequest
+{
+
+public:
+    HTTPRequest(const std::string& host, const short port);
+
+    virtual ~HTTPRequest();
+    std::string get_response();
+
+    bool get_request(const std::string& path);
+
+    bool TimedOut = false;
+
+private:
+    std::string Host;
+    short Port;
+    bool connected = false;
+    SOCKET Sock;
+    
+
+    std::string Response;
+
+    bool loop_recieve();
+    bool resolve_and_connect();
+    bool InitWinsock();
+};
+

+ 9 - 0
demos/c_c++/GoProStreamDemo/README.md

@@ -0,0 +1,9 @@
+# GoPro Low Latency Stream Demo
+
+This demonstrates how to decode the GoPro webcam or 16x9 video preview stream using the ffmpeg library.
+To use this demo, plug in a hero 9 or hero 10 camera while in a standard video mode and run the app. The webcam stream at 1080p should be displayed in the window. The display output is not optimized.
+To switch to preview stream (720p non processed stream), uncomment the following define in `GoProStreamDemo.h`:
+
+```c
+//#define USE_PREVIEW_STREAM
+```

+ 30 - 0
demos/c_c++/GoProStreamDemo/Resource.h

@@ -0,0 +1,30 @@
+/* Resource.h/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Wed Mar  9 19:50:27 UTC 2022 */
+
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by GoProStreamDemo.rc
+//
+#define IDC_MYICON                      2
+#define IDD_GOPROSTREAMDEMO_DIALOG    102
+#define IDS_APP_TITLE                   103
+#define IDD_ABOUTBOX                    103
+#define IDM_ABOUT                       104
+#define IDM_EXIT                        105
+#define IDI_GOPROSTREAMDEMO           107
+#define IDI_SMALL                       108
+#define IDC_GOPROSTREAMDEMO           109
+#define IDR_MAINFRAME                   128
+#define IDC_STATIC                      -1
+
+// Next default values for new objects
+// 
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NO_MFC                     1
+#define _APS_NEXT_RESOURCE_VALUE        132
+#define _APS_NEXT_COMMAND_VALUE         32771
+#define _APS_NEXT_CONTROL_VALUE         1000
+#define _APS_NEXT_SYMED_VALUE           110
+#endif
+#endif

+ 336 - 0
demos/c_c++/GoProStreamDemo/UDPSocketCapture.cpp

@@ -0,0 +1,336 @@
+/* UDPSocketCapture.cpp/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Sat Mar  5 01:05:53 UTC 2022 */
+
+#include "UDPSocketCapture.h"
+#include "HTTPRequest.h"
+#include "GoProStreamDemo.h"
+
+#ifndef snprintf
+#define snprintf(buf,len, format,...) _snprintf_s(buf, len,len, format, __VA_ARGS__)
+#endif
+
+#define READ_BUFFER_SIZE 16384
+
+extern "C"
+{
+#include "demuxing_decoding.h"
+}
+#include "BufferNegotiator.h"
+
+UDPSocketCapture::UDPSocketCapture()
+{
+
+}
+
+UDPSocketCapture::~UDPSocketCapture()
+{
+    Stop();
+}
+
+LONG GetStringRegKey(HKEY hKey, const std::string& strValueName, std::string& strValue)
+{
+    CHAR szBuffer[512] = { 0 };
+    DWORD dwBufferSize = sizeof(szBuffer);
+    ULONG nError;
+    nError = RegQueryValueExA(hKey, strValueName.c_str(), 0, NULL, (LPBYTE)szBuffer, &dwBufferSize);
+    if (ERROR_SUCCESS == nError)
+    {
+        strValue = szBuffer;
+    }
+    return nError;
+}
+int UDPSocketCapture::FindCamera(std::string& address)
+{
+    int ret = -1;
+    address = "";
+    WSAData wsaData;
+    if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0)
+    {
+        return 255;
+    }
+
+    std::string myaddr = "";
+    address = "";
+
+    char logbuf[100] = { 0 };
+    char ac[80];
+    if (gethostname(ac, sizeof(ac)) == SOCKET_ERROR)
+    {
+        
+        snprintf(logbuf, 100, "Error %d when getting local host name.\n", WSAGetLastError());
+        OutputDebugStringA(logbuf);
+        WSACleanup();
+        return -1;
+    }
+    snprintf(logbuf, 100, "Host name is %s\n", ac);
+    OutputDebugStringA(logbuf);
+
+    struct hostent* phe = gethostbyname(ac);
+    struct addrinfo* result = NULL;
+    struct addrinfo* res = NULL;
+    getaddrinfo(ac, NULL, NULL, &result);
+    if (phe == 0)
+    {
+        OutputDebugStringA("Bad host lookup.\n");
+        WSACleanup();
+        return -1;
+    }
+
+    for (int i = 0; phe->h_addr_list[i] != 0; ++i)
+    {
+        struct in_addr addr;
+        memcpy(&addr, phe->h_addr_list[i], sizeof(struct in_addr));
+        snprintf(logbuf, 100, "Address %d: %s\n ", i , inet_ntoa(addr));
+        OutputDebugStringA(logbuf);
+        if ((int)addr.S_un.S_un_b.s_b1 == 172 && ((int)addr.S_un.S_un_b.s_b2 >= 20 && (int)addr.S_un.S_un_b.s_b2 <= 29) &&
+            ((int)addr.S_un.S_un_b.s_b4 >= 50 && (int)addr.S_un.S_un_b.s_b4 <= 70))
+        {
+            address = inet_ntoa(addr);
+            address.replace(address.length() - 1, 1, "1");
+            ret = 0;
+        }
+    }
+
+    WSACleanup();
+
+    return ret;
+}
+
+
+void UDPSocketCapture::Start(int height)
+{
+    if (!mStarted)
+    {
+        mHeight = height;
+        this->mCaptureThread = std::thread(CaptureThread, this);
+        this->mOutputThread = std::thread(OutputThread, this);
+        mStarted = true;
+    }
+}
+
+void UDPSocketCapture::Stop()
+{
+    if (mStarted)
+    {
+        StopDecoding();
+        StopCapture();
+        mQuitNow = true;
+        if (mCaptureThread.joinable())
+            mCaptureThread.join();
+        if(mOutputThread.joinable())
+            mOutputThread.join();
+        mStarted = false;
+    }
+}
+
+void UDPSocketCapture::GetBuffer(uint8_t* buf, long bufsize, long& bytesReturned)
+{
+    mBufMutex.lock();
+    static size_t actualbytes = 0;
+    if (buf == NULL || bufsize == 0)
+    {
+        mBufMutex.unlock();
+        return;
+    }
+    bytesReturned = ::GetBuffer(buf, bufsize);
+
+    if (bytesReturned == 0)
+    {
+        if (mBigBuf == NULL)
+        {
+            char path[MAX_PATH];
+            std::string spath = "";
+            HMODULE hm = NULL;
+
+            if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
+                GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
+                (LPCSTR)&GetStringRegKey, &hm) == 0)
+            {
+                int err = GetLastError();
+                fprintf(stderr, "GetModuleHandle failed, error = %d\n", err);
+            }
+            if (GetModuleFileNameA(hm, path, sizeof(path)) == 0)
+            {
+                int err = GetLastError();
+                fprintf(stderr, "GetModuleFileName failed, error = %d\n", err);
+            }
+            spath = path;
+            size_t slashpos = spath.find_last_of("\\", spath.length()) + 1;
+#ifdef USE_PREVIEW_STREAM
+            spath.replace(slashpos, spath.length() - slashpos, "raw720.img");
+#else
+            spath.replace(slashpos, spath.length() - slashpos, "raw.img");
+#endif
+
+            mBigBuf = (uint8_t*)malloc(BIG_BUF_SIZE);
+            if (mBigBuf != NULL)
+            {
+                FILE* ou = fopen(spath.c_str(), "rb");
+                if (ou)
+                {
+                    actualbytes = fread(mBigBuf, 1, BIG_BUF_SIZE, ou);
+                    fclose(ou);
+                    bytesReturned = actualbytes;
+                }
+                else
+                {
+                    free(mBigBuf);
+                    mBigBuf = NULL;
+                }
+            }
+        }
+        if (mBigBuf != NULL)
+        {
+            if (bufsize!=0 && bufsize >= actualbytes)
+            {
+                memcpy(buf, mBigBuf, actualbytes);
+                bytesReturned = actualbytes;
+            }
+        }
+        
+    }
+
+    mBufMutex.unlock();
+}
+
+void UDPSocketCapture::WriteBuffer(uint8_t* buf, int bufsize)
+{
+    mBufMutex.lock();
+    mBufMutex.unlock();
+}
+
+/*
+This function is responsible for calling decode on the captured buffer
+*/
+void UDPSocketCapture::OutputThread(UDPSocketCapture* me)
+{
+    std::string addr = "";
+    
+    while (FindCamera(addr) != 0 && !me->mQuitNow)
+    {
+        Sleep(500);
+    }
+
+    if (!me->mQuitNow)
+    {
+        bool alreadyStarted = false;
+
+        {
+            HTTPRequest req(addr, 8080);
+            if (req.get_request("/gopro/webcam/status"))
+            {
+                std::string stat = req.get_response();
+                std::string findme = "\"status\": 2";
+                if (stat.find(findme, 0) != std::string::npos)
+                    alreadyStarted = true;
+            }
+            if (alreadyStarted)
+            {
+                HTTPRequest req(addr, 8080);
+                if (req.get_request("/gopro/webcam/stop"))
+                {
+                    std::string stat = req.get_response();
+                }
+                Sleep(200);
+            }
+        }
+        {
+            HTTPRequest req(addr, 8080);
+#ifdef USE_PREVIEW_STREAM
+            std::string startaddr = "/gopro/camera/stream/start";
+#else
+            std::string startaddr = "/gopro/webcam/start?res=%d&fov=0";
+#endif
+            
+            char bufff[256] = { 0 };
+            int parm = 12;
+            if(me->mHeight == 720)
+            {
+                parm = 7;
+            }
+            snprintf(bufff, 256, startaddr.c_str(), parm);
+            std::string buf = bufff;
+            if (req.get_request(buf))
+            {
+                std::string stat = req.get_response();
+            }
+        }
+
+        const char* url = "udp://@0.0.0.0:8554";
+
+        while (!me->mQuitNow)
+        {
+            try
+            {
+                DecodeStream();
+            }
+            catch (...)
+            {
+                OutputDebugStringA("Decode crashed, quitting everything\n");
+            }
+        }
+
+        {
+
+            HTTPRequest req(addr, 8080);
+#ifdef USE_PREVIEW_STREAM
+            req.get_request("/gopro/camera/stream/stop");
+#else
+            req.get_request("/gopro/webcam/stop");
+            if (!req.TimedOut)
+            {
+                HTTPRequest req2(addr, 8080);
+                req2.get_request("/gopro/webcam/exit");
+            }
+#endif
+
+        }
+    }
+    
+    OutputDebugStringA("LEAVING THREAD\n");
+}
+
+
+/*
+This is the main function that captures TS data from the camera and saves it to a buffer
+*/
+void UDPSocketCapture::CaptureThread(UDPSocketCapture* me)
+{
+    WSASession session;
+    char buf[READ_BUFFER_SIZE] = { 0 };
+    OutputDebugStringA("Starting receive thread\n");
+    UDPSocket* mysock = new UDPSocket();
+    mysock->Bind(8554);
+
+    while (true)
+    {
+        if (mysock == NULL)
+        {
+            mysock = new UDPSocket();
+            mysock->Bind(8554);
+        }
+        int received = 0;
+        sockaddr_in blop = mysock->RecvFrom(buf, READ_BUFFER_SIZE, received);
+        //LOGF::Instance()->LOG("Received %d bytes\n", received);
+
+        if (received == READ_BUFFER_SIZE)
+            OutputDebugStringA("Maxed out the buffer\n");
+        if (received == 1 || me->mQuitNow)
+        {
+            //fclose(myfile);
+            break;
+        }
+
+        WriteInputBuffer((uint8_t*)buf, received);
+        
+        memset(buf, 0, READ_BUFFER_SIZE);
+    }
+}
+
+void UDPSocketCapture::StopCapture()
+{
+    UDPSocket soc;
+    char buf = 'q';
+    soc.SendTo("127.0.0.1", 8554, &buf, 1);
+    StopInput();
+}

+ 40 - 0
demos/c_c++/GoProStreamDemo/UDPSocketCapture.h

@@ -0,0 +1,40 @@
+/* UDPSocketCapture.h/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Sat Mar  5 01:05:53 UTC 2022 */
+
+#pragma once
+
+#include "GPWNetwork.h"
+#include <thread>
+#include <mutex>
+
+#define BIG_BUF_SIZE 3110400
+
+class UDPSocketCapture
+{
+public:
+    UDPSocketCapture();
+    virtual ~UDPSocketCapture();
+
+    void Start(int height = 1080);
+    void Stop();
+
+    void GetBuffer(uint8_t* buf, long bufsize, long&bytesReturned);
+    void WriteBuffer(uint8_t* buf, int bufsize);
+    
+protected:
+    static void CaptureThread(UDPSocketCapture* me);
+    static void OutputThread(UDPSocketCapture* me);
+    static int FindCamera(std::string& address);
+    void StopCapture();
+    std::thread mCaptureThread;
+    std::thread mOutputThread;
+
+    bool mStarted = false;
+    bool mQuitNow = false;
+    uint8_t* mBigBuf = NULL;
+    int mBigBufPos = 0;
+    std::mutex mBufMutex;
+    int mHeight = 1080;
+
+};
+

+ 419 - 0
demos/c_c++/GoProStreamDemo/demuxing_decoding.c

@@ -0,0 +1,419 @@
+#pragma warning( disable : 4996)
+/*
+ * Copyright (c) 2012 Stefano Sabatini
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/**
+ * @file
+ * Demuxing and decoding example.
+ *
+ * Show how to use the libavformat and libavcodec API to demux and
+ * decode audio and video data.
+ * @example demuxing_decoding.c
+ */
+
+#ifdef BUSTED_COMPILER
+#define inline __inline
+#ifndef snprintf
+#define snprintf(buf,len, format,...) _snprintf_s(buf, len,len, format, __VA_ARGS__)
+#endif
+#endif
+
+#include <libavutil/imgutils.h>
+#include <libavutil/samplefmt.h>
+#include <libavutil/timestamp.h>
+#include <libavformat/avformat.h>
+#include <libavformat/avio.h>
+
+#include "BufferNegotiator.h"
+
+static AVFormatContext *fmt_ctx = NULL;
+static AVCodecContext *video_dec_ctx = NULL, *audio_dec_ctx = NULL;
+static AVIOContext* avio_ctx = NULL;
+static uint8_t* avio_ctx_buffer = NULL;
+static size_t avio_ctx_buffer_size = 48128;
+static int width, height;
+static enum AVPixelFormat pix_fmt;
+static AVStream *video_stream = NULL, *audio_stream = NULL;
+static int stopDecoding = 0;
+
+uint8_t *video_dst_data[4] = {NULL};
+static int      video_dst_linesize[4];
+long video_dst_bufsize;
+
+static int video_stream_idx = -1, audio_stream_idx = -1;
+static AVFrame *frame = NULL;
+static AVPacket pkt;
+static int video_frame_count = 0;
+static int audio_frame_count = 0;
+
+
+static int read_packet(void* opaque, uint8_t* buf, int buf_size);
+
+/* Enable or disable frame reference counting. You are not supposed to support
+ * both paths in your application but pick the one most appropriate to your
+ * needs. Look for the use of refcount in this example to see what are the
+ * differences of API usage between them. */
+static int refcount = 0;
+
+static int decode_packet(int *got_frame, int cached)
+{
+    int ret = 0;
+    int decoded = pkt.size;
+
+    *got_frame = 0;
+
+    if (pkt.stream_index == video_stream_idx) {
+        /* decode video frame */
+        ret = avcodec_decode_video2(video_dec_ctx, frame, got_frame, &pkt);
+        if (ret < 0) {
+            fprintf(stderr, "Error decoding video frame (%s)\n", av_err2str(ret));
+            return ret;
+        }
+
+        if (*got_frame) {
+
+            if (frame->width != width || frame->height != height ||
+                frame->format != pix_fmt) {
+                /* To handle this change, one could call av_image_alloc again and
+                 * decode the following frames into another rawvideo file. */
+                fprintf(stderr, "Error: Width, height and pixel format have to be "
+                        "constant in a rawvideo file, but the width, height or "
+                        "pixel format of the input video changed:\n"
+                        "old: width = %d, height = %d, format = %s\n"
+                        "new: width = %d, height = %d, format = %s\n",
+                        width, height, av_get_pix_fmt_name(pix_fmt),
+                        frame->width, frame->height,
+                        av_get_pix_fmt_name(frame->format));
+                return -1;
+            }
+
+            printf("video_frame%s n:%d coded_n:%d\n",
+                   cached ? "(cached)" : "",
+                   video_frame_count++, frame->coded_picture_number);
+
+            /* copy decoded frame to destination buffer:
+             * this is required since rawvideo expects non aligned data */
+            LockBuffer();
+            av_image_copy(video_dst_data, video_dst_linesize,
+                          (const uint8_t **)(frame->data), frame->linesize,
+                          pix_fmt, width, height);
+            UnlockBuffer();
+            /* write to rawvideo file */
+            //fwrite(video_dst_data[0], 1, video_dst_bufsize, video_dst_file);
+        }
+    } else if (pkt.stream_index == audio_stream_idx) {
+        /* decode audio frame */
+        ret = avcodec_decode_audio4(audio_dec_ctx, frame, got_frame, &pkt);
+        if (ret < 0) {
+            fprintf(stderr, "Error decoding audio frame (%s)\n", av_err2str(ret));
+            return ret;
+        }
+        /* Some audio decoders decode only part of the packet, and have to be
+         * called again with the remainder of the packet data.
+         * Sample: fate-suite/lossless-audio/luckynight-partial.shn
+         * Also, some decoders might over-read the packet. */
+        decoded = FFMIN(ret, pkt.size);
+
+        if (*got_frame) {
+            size_t unpadded_linesize = frame->nb_samples * av_get_bytes_per_sample(frame->format);
+            //printf("audio_frame%s n:%d nb_samples:%d pts:%s\n",
+            //       cached ? "(cached)" : "",
+            //       audio_frame_count++, frame->nb_samples,
+            //       av_ts2timestr(frame->pts, &audio_dec_ctx->time_base));
+
+            /* Write the raw audio data samples of the first plane. This works
+             * fine for packed formats (e.g. AV_SAMPLE_FMT_S16). However,
+             * most audio decoders output planar audio, which uses a separate
+             * plane of audio samples for each channel (e.g. AV_SAMPLE_FMT_S16P).
+             * In other words, this code will write only the first audio channel
+             * in these cases.
+             * You should use libswresample or libavfilter to convert the frame
+             * to packed data. */
+            //fwrite(frame->extended_data[0], 1, unpadded_linesize, audio_dst_file);
+        }
+    }
+
+    /* If we use frame reference counting, we own the data and need
+     * to de-reference it when we don't use it anymore */
+    if (*got_frame && refcount)
+        av_frame_unref(frame);
+
+    return decoded;
+}
+
+static int open_codec_context(int *stream_idx,
+                              AVCodecContext **dec_ctx, AVFormatContext *fmt_ctx, enum AVMediaType type)
+{
+    int ret, stream_index;
+    AVStream *st;
+    AVCodec *dec = NULL;
+    AVDictionary *opts = NULL;
+
+    ret = av_find_best_stream(fmt_ctx, type, -1, -1, NULL, 0);
+    if (ret < 0) {
+        fprintf(stderr, "Could not find %s stream in input\n",
+                av_get_media_type_string(type));
+        return ret;
+    } else {
+        stream_index = ret;
+        st = fmt_ctx->streams[stream_index];
+
+        /* find decoder for the stream */
+        dec = avcodec_find_decoder(st->codecpar->codec_id);
+        if (!dec) {
+            fprintf(stderr, "Failed to find %s codec\n",
+                    av_get_media_type_string(type));
+            return AVERROR(EINVAL);
+        }
+
+        /* Allocate a codec context for the decoder */
+        *dec_ctx = avcodec_alloc_context3(dec);
+        if (!*dec_ctx) {
+            fprintf(stderr, "Failed to allocate the %s codec context\n",
+                    av_get_media_type_string(type));
+            return AVERROR(ENOMEM);
+        }
+
+        /* Copy codec parameters from input stream to output codec context */
+        if ((ret = avcodec_parameters_to_context(*dec_ctx, st->codecpar)) < 0) {
+            fprintf(stderr, "Failed to copy %s codec parameters to decoder context\n",
+                    av_get_media_type_string(type));
+            return ret;
+        }
+
+        /* Init the decoders, with or without reference counting */
+        av_dict_set(&opts, "refcounted_frames", refcount ? "1" : "0", 0);
+        if ((ret = avcodec_open2(*dec_ctx, dec, &opts)) < 0) {
+            fprintf(stderr, "Failed to open %s codec\n",
+                    av_get_media_type_string(type));
+            return ret;
+        }
+        *stream_idx = stream_index;
+    }
+
+    return 0;
+}
+
+static int get_format_from_sample_fmt(const char **fmt,
+                                      enum AVSampleFormat sample_fmt)
+{
+    int i;
+    struct sample_fmt_entry {
+        enum AVSampleFormat sample_fmt; const char *fmt_be, *fmt_le;
+    } sample_fmt_entries[] = {
+        { AV_SAMPLE_FMT_U8,  "u8",    "u8"    },
+        { AV_SAMPLE_FMT_S16, "s16be", "s16le" },
+        { AV_SAMPLE_FMT_S32, "s32be", "s32le" },
+        { AV_SAMPLE_FMT_FLT, "f32be", "f32le" },
+        { AV_SAMPLE_FMT_DBL, "f64be", "f64le" },
+    };
+    *fmt = NULL;
+
+    for (i = 0; i < FF_ARRAY_ELEMS(sample_fmt_entries); i++) {
+        struct sample_fmt_entry *entry = &sample_fmt_entries[i];
+        if (sample_fmt == entry->sample_fmt) {
+            *fmt = AV_NE(entry->fmt_be, entry->fmt_le);
+            return 0;
+        }
+    }
+
+    fprintf(stderr,
+            "sample format %s is not supported as output format\n",
+            av_get_sample_fmt_name(sample_fmt));
+    return -1;
+}
+
+int StopDecoding()
+{
+    stopDecoding = 1;
+
+    return 0;
+}
+
+static int shouldInterrupt(void * junk)
+{
+    return stopDecoding;
+}
+
+int DecodeStream()
+{
+    int ret = 0, got_frame = 0;
+    stopDecoding = 0;
+    char* format = "mpegts";
+    
+    if (stopDecoding)
+    {
+        return -1;
+    }
+    
+    do
+    {
+        AVInputFormat* fmt = NULL;
+
+        if (stopDecoding == 1)
+            return -1;
+        fmt = av_find_input_format(format);
+        fmt_ctx = avformat_alloc_context();
+        fmt_ctx->interrupt_callback.callback = shouldInterrupt;
+        fmt_ctx->interrupt_callback.opaque = NULL;
+        AVDictionary* format_opts = NULL;
+        av_dict_set(&format_opts, "probesize", "4194304", 0);
+
+        avio_ctx_buffer = av_malloc(avio_ctx_buffer_size);
+        if (!avio_ctx_buffer)
+        {
+            ret = AVERROR(ENOMEM);
+            goto end;
+        }
+        avio_ctx = avio_alloc_context(avio_ctx_buffer, avio_ctx_buffer_size,
+            0, NULL, &read_packet, NULL, NULL);
+        if (!avio_ctx)
+        {
+            ret = AVERROR(ENOMEM);
+            goto end;
+        }
+        fmt_ctx->pb = avio_ctx;
+
+        int averr = 0;
+        /* open input file, and allocate format context */
+        while ((averr = avformat_open_input(&fmt_ctx, NULL, fmt, &format_opts)) < 0)
+        {
+            if (stopDecoding == 1)
+            {
+                return -1;
+            }
+            _sleep(200);
+        }
+
+        int tries = 0;
+        /* retrieve stream information */
+        while (tries < 3)
+        {
+            if (avformat_find_stream_info(fmt_ctx, NULL) < 0)
+            {
+                fprintf(stderr, "Could not find stream information\n");
+                tries++;
+            }
+            else
+            {
+                break;
+            }
+        }
+
+        if (open_codec_context(&video_stream_idx, &video_dec_ctx, fmt_ctx, AVMEDIA_TYPE_VIDEO) >= 0)
+        {
+            video_stream = fmt_ctx->streams[video_stream_idx];
+
+            /* allocate image where the decoded image will be put */
+            width = video_dec_ctx->width;
+            height = video_dec_ctx->height;
+            pix_fmt = video_dec_ctx->pix_fmt;
+            LockBuffer();
+            ret = av_image_alloc(video_dst_data, video_dst_linesize, width, height, pix_fmt, 1);
+            UnlockBuffer();
+            if (ret < 0)
+            {
+                fprintf(stderr, "Could not allocate raw video buffer\n");
+                goto end;
+            }
+            LockBuffer();
+            video_dst_bufsize = ret;
+            UnlockBuffer();
+        }
+
+        if (!video_stream)
+        {
+            fprintf(stderr, "Could not find video stream in the input, aborting\n");
+            ret = 1;
+            goto end;
+        }
+
+        frame = av_frame_alloc();
+        if (!frame)
+        {
+            fprintf(stderr, "Could not allocate frame\n");
+            ret = AVERROR(ENOMEM);
+            goto end;
+        }
+
+        /* initialize packet, set data to NULL, let the demuxer fill it */
+        av_init_packet(&pkt);
+        pkt.data = NULL;
+        pkt.size = 0;
+        avio_flush(fmt_ctx->pb);
+        avformat_flush(fmt_ctx);
+        /* read frames from the file */
+        while (av_read_frame(fmt_ctx, &pkt) >= 0 && !stopDecoding)
+        {
+            AVPacket orig_pkt = pkt;
+            do
+            {
+                ret = decode_packet(&got_frame, 0);
+                if (ret < 0)
+                    break;
+                pkt.data += ret;
+                pkt.size -= ret;
+            } while (pkt.size > 0);
+            av_packet_unref(&orig_pkt);
+        }
+
+        /* flush cached frames */
+        pkt.data = NULL;
+        pkt.size = 0;
+        do
+        {
+            decode_packet(&got_frame, 1);
+        } while (got_frame);
+end:
+        LockBuffer();
+        video_dst_bufsize = 0;
+
+        avcodec_free_context(&video_dec_ctx);
+        avcodec_free_context(&audio_dec_ctx);
+        avformat_close_input(&fmt_ctx);
+        av_freep(&avio_ctx->buffer);
+        av_freep(&avio_ctx);
+        avio_ctx = NULL;
+        video_dec_ctx = NULL;
+        audio_dec_ctx = NULL;
+        fmt_ctx = NULL;
+
+        av_frame_free(&frame);
+        frame = NULL;
+        av_free(video_dst_data[0]);
+        video_dst_data[0] = NULL;
+        UnlockBuffer();
+
+        
+    } while (1);
+
+    return ret < 0;
+}
+
+static int read_packet(void* opaque, uint8_t* buf, int buf_size)
+{
+    int ret = ReadInputBuffer(buf, buf_size);
+
+    return ret;
+}
+
+

+ 10 - 0
demos/c_c++/GoProStreamDemo/demuxing_decoding.h

@@ -0,0 +1,10 @@
+/* demuxing_decoding.h/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Sat Mar  5 01:05:52 UTC 2022 */
+
+#pragma once
+
+#include <stdint.h>
+
+int DecodeStream();
+
+int StopDecoding();

+ 4 - 0
demos/c_c++/GoProStreamDemo/packages.config

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="FFmpeg.Nightly" version="20200831.1.0" targetFramework="native" />
+</packages>

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
demos/c_c++/GoProStreamDemo/raw.img


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
demos/c_c++/GoProStreamDemo/raw720.img


BIN
demos/c_c++/GoProStreamDemo/small.ico


+ 24 - 0
demos/c_c++/GoProStreamDemo/stdafx.h

@@ -0,0 +1,24 @@
+/* stdafx.h/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Sat Mar  5 01:05:53 UTC 2022 */
+
+// stdafx.h : include file for standard system include files,
+// or project specific include files that are used frequently, but
+// are changed infrequently
+//
+
+#pragma once
+
+#include "targetver.h"
+
+#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
+// Windows Header Files
+#include <windows.h>
+
+// C RunTime Header Files
+#include <stdlib.h>
+#include <malloc.h>
+#include <memory.h>
+#include <tchar.h>
+
+
+// reference additional headers your program requires here

+ 11 - 0
demos/c_c++/GoProStreamDemo/targetver.h

@@ -0,0 +1,11 @@
+/* targetver.h/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Sat Mar  5 01:05:52 UTC 2022 */
+
+#pragma once
+
+// Including SDKDDKVer.h defines the highest available Windows platform.
+
+// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
+// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.
+
+#include <SDKDDKVer.h>

+ 388 - 0
demos/csharp/GoProCSharpSample/.gitignore

@@ -0,0 +1,388 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Nuget personal access tokens and Credentials
+nuget.config
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+.idea/
+*.sln.iml

+ 6 - 0
demos/csharp/GoProCSharpSample/App.config

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+    <startup> 
+        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
+    </startup>
+</configuration>

+ 9 - 0
demos/csharp/GoProCSharpSample/App.xaml

@@ -0,0 +1,9 @@
+<Application x:Class="GoProCSharpSample.App"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:local="clr-namespace:GoProCSharpSample"
+             StartupUri="MainWindow.xaml">
+    <Application.Resources>
+         
+    </Application.Resources>
+</Application>

+ 20 - 0
demos/csharp/GoProCSharpSample/App.xaml.cs

@@ -0,0 +1,20 @@
+/* App.xaml.cs/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Wed, Sep  1, 2021  5:05:37 PM */
+
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace GoProCSharpSample
+{
+    /// <summary>
+    /// Interaction logic for App.xaml
+    /// </summary>
+    public partial class App : Application
+    {
+    }
+}

+ 111 - 0
demos/csharp/GoProCSharpSample/GoProCSharpSample.csproj

@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{0B7AB7DB-A5D1-49BC-A83B-5EE143160D3E}</ProjectGuid>
+    <OutputType>WinExe</OutputType>
+    <RootNamespace>GoProCSharpSample</RootNamespace>
+    <AssemblyName>GoProCSharpSample</AssemblyName>
+    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+    <WarningLevel>4</WarningLevel>
+    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
+    <Deterministic>true</Deterministic>
+    <NuGetPackageImportStamp>
+    </NuGetPackageImportStamp>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Xaml">
+      <RequiredTargetFramework>4.0</RequiredTargetFramework>
+    </Reference>
+    <Reference Include="WindowsBase" />
+    <Reference Include="PresentationCore" />
+    <Reference Include="PresentationFramework" />
+  </ItemGroup>
+  <ItemGroup>
+    <ApplicationDefinition Include="App.xaml">
+      <Generator>MSBuild:Compile</Generator>
+      <SubType>Designer</SubType>
+    </ApplicationDefinition>
+    <Page Include="MainWindow.xaml">
+      <Generator>MSBuild:Compile</Generator>
+      <SubType>Designer</SubType>
+    </Page>
+    <Compile Include="App.xaml.cs">
+      <DependentUpon>App.xaml</DependentUpon>
+      <SubType>Code</SubType>
+    </Compile>
+    <Compile Include="MainWindow.xaml.cs">
+      <DependentUpon>MainWindow.xaml</DependentUpon>
+      <SubType>Code</SubType>
+    </Compile>
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Properties\AssemblyInfo.cs">
+      <SubType>Code</SubType>
+    </Compile>
+    <Compile Include="Properties\Resources.Designer.cs">
+      <AutoGen>True</AutoGen>
+      <DesignTime>True</DesignTime>
+      <DependentUpon>Resources.resx</DependentUpon>
+    </Compile>
+    <Compile Include="Properties\Settings.Designer.cs">
+      <AutoGen>True</AutoGen>
+      <DependentUpon>Settings.settings</DependentUpon>
+      <DesignTimeSharedInput>True</DesignTimeSharedInput>
+    </Compile>
+    <EmbeddedResource Include="Properties\Resources.resx">
+      <Generator>ResXFileCodeGenerator</Generator>
+      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
+    </EmbeddedResource>
+    <None Include="Properties\Settings.settings">
+      <Generator>SettingsSingleFileGenerator</Generator>
+      <LastGenOutput>Settings.Designer.cs</LastGenOutput>
+    </None>
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="App.config" />
+  </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Windows.SDK.Contracts">
+      <Version>10.0.19041.1</Version>
+    </PackageReference>
+    <PackageReference Include="System.Runtime.WindowsRuntime">
+      <Version>4.6.0</Version>
+    </PackageReference>
+    <PackageReference Include="System.Runtime.WindowsRuntime.UI.Xaml">
+      <Version>4.6.0</Version>
+    </PackageReference>
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project>

+ 25 - 0
demos/csharp/GoProCSharpSample/GoProCSharpSample.sln

@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30002.166
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoProCSharpSample", "GoProCSharpSample.csproj", "{0B7AB7DB-A5D1-49BC-A83B-5EE143160D3E}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{0B7AB7DB-A5D1-49BC-A83B-5EE143160D3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{0B7AB7DB-A5D1-49BC-A83B-5EE143160D3E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{0B7AB7DB-A5D1-49BC-A83B-5EE143160D3E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{0B7AB7DB-A5D1-49BC-A83B-5EE143160D3E}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {BA3A81ED-EBCF-424C-A43F-F475BDEF4D9D}
+	EndGlobalSection
+EndGlobal

+ 52 - 0
demos/csharp/GoProCSharpSample/MainWindow.xaml

@@ -0,0 +1,52 @@
+<Window x:Class="GoProCSharpSample.MainWindow"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:local="clr-namespace:GoProCSharpSample"
+        mc:Ignorable="d"
+        DataContext="{Binding RelativeSource={RelativeSource Self}}"
+        Title="GoPro C# Sample" Height="378" Width="400" ResizeMode="NoResize">
+    <Window.Resources>
+        <local:BrushBoolColorConverter x:Key="BConverter"></local:BrushBoolColorConverter>
+    </Window.Resources>
+    <Grid>
+        <Grid.ColumnDefinitions>
+            <ColumnDefinition/>
+            <ColumnDefinition Width="200"/>
+        </Grid.ColumnDefinitions>
+        <Label Content="Devices" HorizontalAlignment="Left" Margin="10,6,0,0" VerticalAlignment="Top"/>
+        <ListBox x:Name="lbDevices" ItemsSource="{Binding Devices}" DisplayMemberPath="DeviceInfo.Name" HorizontalAlignment="Left" Height="167" Margin="10,33,0,0" VerticalAlignment="Top" Width="174"/>
+        <GroupBox Grid.Column="1" Header="BLE" HorizontalAlignment="Left" Height="190" Margin="10,10,0,0" VerticalAlignment="Top" Width="180"/>
+
+        <Button x:Name="btnScanBLE" Content="Scan" Grid.Column="1" HorizontalAlignment="Left" Margin="20,33,0,0" VerticalAlignment="Top" Width="75" Click="BtnScanBLE_Click"/>
+        <Button x:Name="btnPair" Content="Pair" Grid.Column="1" HorizontalAlignment="Right" Margin="0,33,20,0" VerticalAlignment="Top" Width="75" Click="BtnPair_Click"/>
+        <Button x:Name="btnConnect" Content="Connect" Grid.Column="1" HorizontalAlignment="Left" Margin="20,58,0,0" VerticalAlignment="Top" Width="75" Click="BtnConnect_Click" />
+        <Button x:Name="btnReadAPName" Content="Read AP Name" Grid.Column="1" HorizontalAlignment="Left" Margin="20,83,0,0" VerticalAlignment="Top" Width="160" Click="BtnReadAPName_Click"/>
+        <Button x:Name="btnReadAPPass" Content="Read AP Pass" Grid.Column="1" HorizontalAlignment="Left" Margin="20,108,0,0" VerticalAlignment="Top" Width="160" Click="BtnReadAPPass_Click" />
+        <Button x:Name="btnTurnWifiOn" Content="Wifi AP on" Grid.Column="1" HorizontalAlignment="Left" Margin="20,133,0,0" VerticalAlignment="Top" Width="75" Click="BtnTurnWifiOn_Click"  />
+        <Button x:Name="btnTurnWifiOff" Content="Wifi AP off" Grid.Column="1" HorizontalAlignment="Left" Margin="105,133,0,0" VerticalAlignment="Top" Width="75" Click="BtnTurnWifiOff_Click"  />
+        <Button x:Name="btnShutterOn" Content="Shutter on" Grid.Column="1" HorizontalAlignment="Left" Margin="20,158,0,0" VerticalAlignment="Top" Width="75" Click="BtnShutterOn_Click" />
+        <Button x:Name="btnShutterOff" Content="Shutter off" Grid.Column="1" HorizontalAlignment="Left" Margin="105,158,0,0" VerticalAlignment="Top" Width="75" Click="BtnShutterOff_Click" />
+
+        
+        <GroupBox Grid.Column="1" Header="Status" HorizontalAlignment="Left" Height="117" Margin="10,205,0,0" VerticalAlignment="Top" Width="180"/>
+        <Label Content="Encoding" HorizontalAlignment="Left" Margin="117,222,0,0" VerticalAlignment="Top" Padding="0" Grid.Column="1"/>
+        <Rectangle Fill="{Binding Encoding, Converter={StaticResource BConverter}}" HorizontalAlignment="Left" Height="23" Margin="117,238,0,0" Stroke="Black" VerticalAlignment="Top" Width="63" Grid.Column="1"/>
+        <Label Content="Battery Level" HorizontalAlignment="Left" Margin="20,228,0,0" VerticalAlignment="Top" Padding="0" Grid.Column="1"/>
+        <ProgressBar x:Name="prgBatteryLevel" HorizontalAlignment="Left" Height="11" Margin="20,245,0,0" VerticalAlignment="Top" Width="75" Value="{Binding BatteryLevel, Mode=OneWay}" ToolTip="{Binding BatteryLevel, Mode=OneWay}" Maximum="100" Grid.Column="1"/>
+        <Label Content="Wifi On" HorizontalAlignment="Left" Margin="117,273,0,0" VerticalAlignment="Top" Padding="0" Grid.Column="1"/>
+        <Rectangle Fill="{Binding WifiOn, Converter={StaticResource BConverter}}" HorizontalAlignment="Left" Height="23" Margin="117,289,0,0" Stroke="Black" VerticalAlignment="Top" Width="63" Grid.Column="1"/>
+
+        <Label Content="Camera Wifi AP Name" HorizontalAlignment="Left" Margin="10,207,0,0" VerticalAlignment="Top"/>
+        <TextBox x:Name="txtAPName" HorizontalAlignment="Left" Height="23" Margin="10,233,0,0" TextWrapping="NoWrap" Text="" VerticalAlignment="Top" Width="174"/>
+        <Label Content="Camera Wifi AP Password" HorizontalAlignment="Left" Margin="10,263,0,0" VerticalAlignment="Top"/>
+        <TextBox x:Name="txtAPPassword" HorizontalAlignment="Left" Height="23" Margin="10,289,0,0" TextWrapping="NoWrap" Text="" VerticalAlignment="Top" Width="174"/>
+
+        <StatusBar Grid.ColumnSpan="2" Height="20" Margin="0,0,0,0" VerticalAlignment="Bottom">
+            <StatusBarItem Padding="0">
+                <TextBlock Name="txtStatusBar" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" FontSize="12" Padding="0,0,0,0"></TextBlock>
+            </StatusBarItem>
+        </StatusBar>
+    </Grid>
+</Window>

+ 712 - 0
demos/csharp/GoProCSharpSample/MainWindow.xaml.cs

@@ -0,0 +1,712 @@
+/* MainWindow.xaml.cs/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Wed, Sep  1, 2021  5:05:38 PM */
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+using Windows.Devices.Bluetooth;
+using Windows.Devices.Bluetooth.GenericAttributeProfile;
+using Windows.Devices.Enumeration;
+using Windows.Media.MediaProperties;
+using Windows.Storage.Streams;
+
+namespace GoProCSharpSample
+{
+    /// <summary>
+    /// Interaction logic for MainWindow.xaml
+    /// </summary>
+    public partial class MainWindow : Window, INotifyPropertyChanged
+    {
+
+        public class GDeviceInformation
+        {
+            public GDeviceInformation(DeviceInformation inDeviceInformation, bool inPresent, bool inConnected)
+            {
+                DeviceInfo = inDeviceInformation;
+                IsPresent = inPresent;
+                IsConnected = inConnected;
+            } 
+            public DeviceInformation DeviceInfo { get; set; } = null;
+            public bool IsPresent { get; set; } = false;
+            public bool IsConnected { get; set; } = false;
+            public bool IsVisible { get { return IsPresent || IsConnected; } }
+
+            private GDeviceInformation() { }
+        }
+
+        #region Binded Properties
+        public ObservableCollection<GDeviceInformation> Devices
+        {
+            get; set;
+        } = new ObservableCollection<GDeviceInformation>();
+
+        private bool mEncoding = false;
+        public bool Encoding
+        {
+            get
+            {
+                return mEncoding;
+            }
+            set
+            {
+                mEncoding = value;
+                if (this.PropertyChanged != null)
+                {
+                    PropertyChanged(this, new PropertyChangedEventArgs("Encoding"));
+                }
+            }
+        }
+
+        private int mBatterylevel = 0;
+        public int BatteryLevel
+        {
+            get
+            {
+                return mBatterylevel;
+            }
+            set
+            {
+                mBatterylevel = value;
+                if (this.PropertyChanged != null)
+                {
+                    PropertyChanged(this, new PropertyChangedEventArgs("BatteryLevel"));
+                }
+            }
+        }
+
+        private bool mWifiOn = false;
+        public bool WifiOn
+        {
+            get
+            {
+                return mWifiOn;
+            }
+            set
+            {
+                mWifiOn = value;
+                if (this.PropertyChanged != null)
+                {
+                    PropertyChanged(this, new PropertyChangedEventArgs("WifiOn"));
+                }
+            }
+        }
+
+        #endregion
+
+        #region Bluetooth Device Members
+        private BluetoothLEDevice mBLED = null;
+        public GattCharacteristic mNotifyCmds = null;
+        public GattCharacteristic mSendCmds = null;
+        public GattCharacteristic mSetSettings = null;
+        public GattCharacteristic mNotifySettings = null;
+        public GattCharacteristic mSendQueries = null;
+        public GattCharacteristic mNotifyQueryResp = null;
+        public GattCharacteristic mReadAPName = null;
+        public GattCharacteristic mReadAPPass = null;
+        #endregion
+
+        public event PropertyChangedEventHandler PropertyChanged;
+
+        DeviceWatcher mDeviceWatcher = null;
+        private readonly Dictionary<string, DeviceInformation> mAllDevices = new Dictionary<string, DeviceInformation>();
+
+        public MainWindow()
+        {
+            InitializeComponent();
+            WindowStartupLocation = WindowStartupLocation.CenterScreen;
+        }
+
+        #region Button Click Handlers
+
+        private void BtnScanBLE_Click(object sender, RoutedEventArgs e)
+        {
+            string BLESelector = "System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-76eaa7084d49}\"";
+            DeviceInformationKind deviceInformationKind = DeviceInformationKind.AssociationEndpoint;
+            string[] requiredProperties = { "System.Devices.Aep.Bluetooth.Le.IsConnectable", "System.Devices.Aep.IsConnected" };
+
+            mDeviceWatcher = DeviceInformation.CreateWatcher(BLESelector, requiredProperties, deviceInformationKind);
+            mDeviceWatcher.Added += MDeviceWatcher_Added; ;
+            mDeviceWatcher.Updated += MDeviceWatcher_Updated; ;
+            mDeviceWatcher.Removed += MDeviceWatcher_Removed; ;
+            mDeviceWatcher.EnumerationCompleted += MDeviceWatcher_EnumerationCompleted; ;
+            mDeviceWatcher.Stopped += MDeviceWatcher_Stopped; ;
+
+            this.txtStatusBar.Text = "Scanning for devices...";
+            mDeviceWatcher.Start();
+        }
+        private async void BtnPair_Click(object sender, RoutedEventArgs e)
+        {
+            GDeviceInformation lDevice = (GDeviceInformation)lbDevices.SelectedItem;
+            if (lDevice != null)
+            {
+                StatusOutput("Pairing started");
+
+                mBLED = await BluetoothLEDevice.FromIdAsync(lDevice.DeviceInfo.Id);
+                mBLED.DeviceInformation.Pairing.Custom.PairingRequested += Custom_PairingRequested;
+                if (mBLED.DeviceInformation.Pairing.CanPair)
+                {
+                    DevicePairingProtectionLevel dppl = mBLED.DeviceInformation.Pairing.ProtectionLevel;
+                    DevicePairingResult dpr = await mBLED.DeviceInformation.Pairing.Custom.PairAsync(DevicePairingKinds.ConfirmOnly, dppl);
+
+                    StatusOutput("Pairing result = " + dpr.Status.ToString());
+                }
+                else
+                {
+                    StatusOutput("Pairing failed");
+                }
+            }
+            else
+            {
+                StatusOutput("Select a device");
+            }
+        }
+        private async void BtnConnect_Click(object sender, RoutedEventArgs e)
+        {
+            StatusOutput("Connecting...");
+            GDeviceInformation mDI = (GDeviceInformation)lbDevices.SelectedItem;
+            if (mDI == null)
+            {
+                StatusOutput("No device selected");
+                return;
+            }
+            mBLED = await BluetoothLEDevice.FromIdAsync(mDI.DeviceInfo.Id);
+            if(!mBLED.DeviceInformation.Pairing.IsPaired)
+            {
+                StatusOutput("Device not paired");
+                return;
+            }
+            GattDeviceServicesResult result = await mBLED.GetGattServicesAsync();
+            mBLED.ConnectionStatusChanged += MBLED_ConnectionStatusChanged;
+
+            if (result.Status == GattCommunicationStatus.Success)
+            {
+                IReadOnlyList<GattDeviceService> services = result.Services;
+                foreach (GattDeviceService gatt in services)
+                {
+                    GattCharacteristicsResult res = await gatt.GetCharacteristicsAsync();
+                    if (res.Status == GattCommunicationStatus.Success)
+                    {
+                        IReadOnlyList<GattCharacteristic> characteristics = res.Characteristics;
+                        foreach (GattCharacteristic characteristic in characteristics)
+                        {
+                            GattCharacteristicProperties properties = characteristic.CharacteristicProperties;
+                            if (properties.HasFlag(GattCharacteristicProperties.Read))
+                            {
+                                // This characteristic supports reading from it.
+                            }
+                            if (properties.HasFlag(GattCharacteristicProperties.Write))
+                            {
+                                // This characteristic supports writing to it.
+                            }
+                            if (properties.HasFlag(GattCharacteristicProperties.Notify))
+                            {
+                                // This characteristic supports subscribing to notifications.
+                            }
+                            if (characteristic.Uuid.ToString() == "b5f90002-aa8d-11e3-9046-0002a5d5c51b")
+                            {
+                                mReadAPName = characteristic;
+                            }
+                            if (characteristic.Uuid.ToString() == "b5f90003-aa8d-11e3-9046-0002a5d5c51b")
+                            {
+                                mReadAPPass = characteristic;
+                            }
+                            if (characteristic.Uuid.ToString() == "b5f90072-aa8d-11e3-9046-0002a5d5c51b")
+                            {
+                                mSendCmds = characteristic;
+                            }
+                            if (characteristic.Uuid.ToString() == "b5f90073-aa8d-11e3-9046-0002a5d5c51b")
+                            {
+                                mNotifyCmds = characteristic;
+                                GattCommunicationStatus status = await mNotifyCmds.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
+                                if (status == GattCommunicationStatus.Success)
+                                {
+                                    mNotifyCmds.ValueChanged += MNotifyCmds_ValueChanged;
+                                }
+                                else
+                                {
+                                    //failure
+                                    StatusOutput("Failed to attach notify cmd " + status);
+                                }
+                            }
+                            if (characteristic.Uuid.ToString() == "b5f90074-aa8d-11e3-9046-0002a5d5c51b")
+                            {
+                                mSetSettings = characteristic;
+                            }
+                            if (characteristic.Uuid.ToString() == "b5f90075-aa8d-11e3-9046-0002a5d5c51b")
+                            {
+                                mNotifySettings = characteristic;
+                                GattCommunicationStatus status = await mNotifySettings.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
+                                if (status == GattCommunicationStatus.Success)
+                                {
+                                    mNotifySettings.ValueChanged += MNotifySettings_ValueChanged;
+                                }
+                                else
+                                {
+                                    //failure
+                                    StatusOutput("Failed to attach notify settings " + status);
+                                }
+                            }
+                            if (characteristic.Uuid.ToString() == "b5f90076-aa8d-11e3-9046-0002a5d5c51b")
+                            {
+                                mSendQueries = characteristic;
+                            }
+                            if (characteristic.Uuid.ToString() == "b5f90077-aa8d-11e3-9046-0002a5d5c51b")
+                            {
+                                mNotifyQueryResp = characteristic;
+                                GattCommunicationStatus status = await mNotifyQueryResp.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
+                                if (status == GattCommunicationStatus.Success)
+                                {
+                                    mNotifyQueryResp.ValueChanged += MNotifyQueryResp_ValueChanged;
+                                    if (mSendQueries != null)
+                                    {
+                                        //Register for settings and status updates
+                                        DataWriter mm = new DataWriter();
+                                        mm.WriteBytes(new byte[] { 1, 0x52 });
+                                        GattCommunicationStatus gat = await mSendQueries.WriteValueAsync(mm.DetachBuffer());
+                                        mm = new DataWriter();
+                                        mm.WriteBytes(new byte[] { 1, 0x53 });
+                                        gat = await mSendQueries.WriteValueAsync(mm.DetachBuffer());
+                                    }
+                                    else
+                                    {
+                                        StatusOutput("send queries was null!");
+                                    }
+                                }
+                                else
+                                {
+                                    //failure
+                                    StatusOutput("Failed to attach notify query " + status);
+                                }
+                            }
+                        }
+                    }
+                }
+                SetThirdPartySource();
+            }
+            else if (result.Status == GattCommunicationStatus.Unreachable)
+            {
+                //couldn't find camera
+                StatusOutput("Connection failed");
+            }
+        }
+        private async void BtnReadAPName_Click(object sender, RoutedEventArgs e)
+        {
+            if (mReadAPName != null)
+            {
+                GattReadResult res = await mReadAPName.ReadValueAsync();
+                if (res.Status == GattCommunicationStatus.Success)
+                {
+                    DataReader dataReader = Windows.Storage.Streams.DataReader.FromBuffer(res.Value);
+                    string output = dataReader.ReadString(res.Value.Length);
+                    txtAPName.Text = output;
+                }
+                else
+                {
+                    StatusOutput("Failed to read ap name");
+                }
+            }
+            else
+            {
+                StatusOutput("Not connected");
+            }
+        }
+        private async void BtnReadAPPass_Click(object sender, RoutedEventArgs e)
+        {
+            if (mReadAPPass != null)
+            {
+                GattReadResult res = await mReadAPPass.ReadValueAsync();
+                if (res.Status == GattCommunicationStatus.Success)
+                {
+                    DataReader dataReader = Windows.Storage.Streams.DataReader.FromBuffer(res.Value);
+                    string output = dataReader.ReadString(res.Value.Length);
+                    txtAPPassword.Text = output;
+                }
+                else
+                {
+                    StatusOutput("Failed to read password");
+                }
+            }
+            else
+            {
+                StatusOutput("Not connected");
+            }
+        }
+        private void BtnTurnWifiOn_Click(object sender, RoutedEventArgs e)
+        {
+            TogglefWifiAP(1);
+        }
+        private void BtnTurnWifiOff_Click(object sender, RoutedEventArgs e)
+        {
+            TogglefWifiAP(0);
+        }
+        private void BtnShutterOn_Click(object sender, RoutedEventArgs e)
+        {
+            ToggleShutter(1);
+        }
+        private void BtnShutterOff_Click(object sender, RoutedEventArgs e)
+        {
+            ToggleShutter(0);
+        }
+
+        #endregion
+
+        #region Device Watcher Event Handlers
+        private void MDeviceWatcher_Stopped(DeviceWatcher sender, object args)
+        {
+            Application.Current.Dispatcher.BeginInvoke(new Action(() =>
+            {
+                this.txtStatusBar.Text = "Scan Stopped!";
+            }));
+        }
+
+        private void MDeviceWatcher_EnumerationCompleted(DeviceWatcher sender, object args)
+        {
+            Application.Current.Dispatcher.BeginInvoke(new Action(() =>
+            {
+                this.txtStatusBar.Text = "Scan Complete";
+            }));
+        }
+
+        private void MDeviceWatcher_Removed(DeviceWatcher sender, DeviceInformationUpdate args)
+        {
+            for (int i = 0; i < Devices.Count; i++)
+            {
+                if (Devices[i].DeviceInfo.Id == args.Id)
+                {
+                    Application.Current.Dispatcher.BeginInvoke(new Action(() =>
+                    {
+                        Devices.RemoveAt(i);
+                    }));
+                    break;
+                }
+            }
+        }
+
+        private void MDeviceWatcher_Updated(DeviceWatcher sender, DeviceInformationUpdate args)
+        {
+            bool isPresent = false, isConnected = false, found = false;
+
+            if (args.Properties.ContainsKey("System.Devices.Aep.Bluetooth.Le.IsConnectable"))
+            {
+                isPresent = (bool)args.Properties["System.Devices.Aep.Bluetooth.Le.IsConnectable"];
+            }
+            if (args.Properties.ContainsKey("System.Devices.Aep.IsConnected"))
+            {
+                isConnected = (bool)args.Properties["System.Devices.Aep.IsConnected"];
+            }
+
+            for (int i = 0; i < Devices.Count; i++)
+            {
+                if (Devices[i].DeviceInfo.Id == args.Id)
+                {
+                    found = true;
+                    Application.Current.Dispatcher.BeginInvoke(new Action(() =>
+                    {
+                        Devices[i].DeviceInfo.Update(args);
+                        Devices[i].IsPresent = isPresent;
+                        Devices[i].IsConnected = isConnected;
+                    }));
+                    break;
+                }
+            }
+            if(!found && (isPresent || isConnected))
+            {
+                if (mAllDevices.ContainsKey(args.Id))
+                {
+                    mAllDevices[args.Id].Update(args);
+                    Application.Current.Dispatcher.BeginInvoke(new Action(() =>
+                    {
+                        Devices.Add(new GDeviceInformation(mAllDevices[args.Id], isPresent, isConnected));
+                    }));
+                }
+            }
+        }
+
+        private void MDeviceWatcher_Added(DeviceWatcher sender, DeviceInformation args)
+        {
+            bool isPresent = false;
+            bool isConnected = false;
+
+            if (args.Properties.ContainsKey("System.Devices.Aep.Bluetooth.Le.IsConnectable"))
+            {
+                isPresent = (bool)args.Properties["System.Devices.Aep.Bluetooth.Le.IsConnectable"];
+            }
+            if (args.Properties.ContainsKey("System.Devices.Aep.IsConnected"))
+            {
+                isConnected = (bool)args.Properties["System.Devices.Aep.IsConnected"];
+            }
+
+            if (args.Name != "" && args.Name.Contains("GoPro"))
+            {
+                bool found = false;
+                if (!mAllDevices.ContainsKey(args.Id))
+                {
+                    mAllDevices.Add(args.Id, args);
+                }
+                for (int i = 0; i < Devices.Count; i++)
+                {
+                    if (Devices[i].DeviceInfo.Id == args.Id)
+                    {
+                        found = true;
+                        Application.Current.Dispatcher.BeginInvoke(new Action(() =>
+                        {
+                            Devices[i].DeviceInfo = args;
+                            Devices[i].IsPresent = isPresent;
+                            Devices[i].IsConnected = isConnected;
+                        }));
+                        break;
+                    }
+                }
+                if (!found && (isPresent || isConnected))
+                {
+                    Application.Current.Dispatcher.BeginInvoke(new Action(() =>
+                    {
+                        Devices.Add(new GDeviceInformation(args, isPresent, isConnected));
+                    }));
+                }
+            }
+        }
+
+        #endregion
+
+        #region BLE Device Handlers
+
+        private void MBLED_ConnectionStatusChanged(BluetoothLEDevice sender, object args)
+        {
+            if (sender.ConnectionStatus == BluetoothConnectionStatus.Connected)
+                StatusOutput("CONNECTED");
+            else
+                StatusOutput("DISCONNECTED");
+        }
+        private void Custom_PairingRequested(DeviceInformationCustomPairing sender, DevicePairingRequestedEventArgs args)
+        {
+            StatusOutput("Pairing request...");
+            args.Accept();
+        }
+
+        #endregion
+
+        #region Gatt Characteristic Notification Handlers
+
+        private readonly List<byte> mBufQ = new List<byte>();
+        private int mExpectedLengthQ = 0;
+        private void MNotifyQueryResp_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args)
+        {
+            var reader = DataReader.FromBuffer(args.CharacteristicValue);
+            byte[] myBytes = new byte[reader.UnconsumedBufferLength];
+            reader.ReadBytes(myBytes);
+            int newLength = ReadBytesIntoBuffer(myBytes, mBufQ);
+            if (newLength > 0)
+                mExpectedLengthQ = newLength;
+
+            if (mExpectedLengthQ == mBufQ.Count)
+            {
+                if ((mBufQ[0] == 0x53 || mBufQ[0] == 0x93) && mBufQ[1] == 0)
+                {
+                    //status messages
+                    for (int k = 0; k < mBufQ.Count;)
+                    {
+                        if (mBufQ[k] == 10)
+                        {
+                            Encoding = mBufQ[k + 2] > 0;
+                        }
+                        if (mBufQ[k] == 70)
+                        {
+                            BatteryLevel = mBufQ[k + 2];
+                        }
+                        if(mBufQ[k] == 69)
+                        {
+                            WifiOn = mBufQ[k + 2] == 1;
+                        }
+                        k += 2 + mBufQ[k + 1];
+                    }
+                }
+                else
+                {
+                    //Unhandled Query Message
+                }
+                mBufQ.Clear();
+                mExpectedLengthQ = 0;
+            }
+        }
+
+        private readonly List<byte> mBufSet = new List<byte>();
+        private int mExpectedLengthSet = 0;
+        private void MNotifySettings_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args)
+        {
+            var reader = DataReader.FromBuffer(args.CharacteristicValue);
+            byte[] myBytes = new byte[reader.UnconsumedBufferLength];
+            reader.ReadBytes(myBytes);
+            int newLength = ReadBytesIntoBuffer(myBytes, mBufSet);
+            if (newLength > 0)
+                mExpectedLengthSet = newLength;
+
+            if (mExpectedLengthSet == mBufSet.Count)
+            {
+                /*
+                if (mBufSet[0] == 0xXX)
+                {
+
+                }
+                */
+                mBufSet.Clear();
+            }
+        }
+
+        private readonly List<byte> mBufCmd = new List<byte>();
+        private int mExpectedLengthCmd = 0;
+        private void MNotifyCmds_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args)
+        {
+            var reader = DataReader.FromBuffer(args.CharacteristicValue);
+            byte[] myBytes = new byte[reader.UnconsumedBufferLength];
+            reader.ReadBytes(myBytes);
+            int newLength = ReadBytesIntoBuffer(myBytes, mBufCmd);
+            if (newLength > 0)
+                mExpectedLengthCmd = newLength;
+
+            if (mExpectedLengthCmd == mBufCmd.Count)
+            {
+                /*
+                if (mBufCmd[0] == 0xXX)
+                {
+
+                }
+                */
+                mBufCmd.Clear();
+            }
+        }
+
+        #endregion
+
+        #region Private Helper Functions
+
+        private async void SetThirdPartySource()
+        {
+            DataWriter mm = new DataWriter();
+            mm.WriteBytes(new byte[] { 0x01, 0x50 });
+            GattCommunicationStatus res = GattCommunicationStatus.Unreachable;
+
+            if (mSendCmds != null)
+            {
+                res = await mSendCmds.WriteValueAsync(mm.DetachBuffer());
+            }
+            if (res != GattCommunicationStatus.Success && mSendCmds != null)
+            {
+                StatusOutput("Failed to set command source: " + res.ToString());
+            }
+        }
+
+        private void StatusOutput(string status)
+        {
+            Application.Current.Dispatcher.BeginInvoke(new Action(() =>
+            {
+                this.txtStatusBar.Text = status;
+            }));
+        }
+        private int ReadBytesIntoBuffer(byte[] bytes, List<byte> mBuf)
+        {
+            int returnLength = -1;
+            int startbyte = 1;
+            int theseBytes = bytes.Length;
+            if ((bytes[0] & 32) > 0)
+            {
+                //extended 13 bit header
+                startbyte = 2;
+                int len = ((bytes[0] & 0xF) << 8) | bytes[1];
+                returnLength = len;
+            }
+            else if ((bytes[0] & 64) > 0)
+            {
+                //extended 16 bit header
+                startbyte = 3;
+                int len = (bytes[1] << 8) | bytes[2];
+                returnLength = len;
+            }
+            else if ((bytes[0] & 128) > 0)
+            {
+                //its a continuation packet
+            }
+            else
+            {
+                //8 bit header
+                returnLength = bytes[0];
+            }
+            for (int k = startbyte; k < theseBytes; k++)
+                mBuf.Add(bytes[k]);
+
+            return returnLength;
+        }
+        private async void TogglefWifiAP(int onOff)
+        {
+            DataWriter mm = new DataWriter();
+            mm.WriteBytes(new byte[] { 0x03, 0x17, 0x01, (byte)onOff });
+            GattCommunicationStatus res = GattCommunicationStatus.Unreachable;
+
+            if (onOff != 1 && onOff != 0)
+            {
+                res = GattCommunicationStatus.AccessDenied;
+            }
+            else if (mSendCmds != null)
+            {
+                res = await mSendCmds.WriteValueAsync(mm.DetachBuffer());
+            }
+            if (res != GattCommunicationStatus.Success)
+            {
+                StatusOutput("Failed to turn on wifi: " + res.ToString());
+            }
+        }
+        private async void ToggleShutter(int onOff)
+        {
+            DataWriter mm = new DataWriter();
+            mm.WriteBytes(new byte[] { 3, 1, 1, (byte)onOff });
+            GattCommunicationStatus res = GattCommunicationStatus.Unreachable;
+
+            if (onOff != 1 && onOff != 0)
+            {
+                res = GattCommunicationStatus.AccessDenied;
+            }
+            else if (mSendCmds != null)
+            {
+                res = await mSendCmds.WriteValueAsync(mm.DetachBuffer());
+            }
+            if (res != GattCommunicationStatus.Success)
+            {
+                StatusOutput("Failed to send shutter: " + res.ToString());
+            }
+        }
+
+        #endregion
+
+    }
+
+    public class BrushBoolColorConverter : IValueConverter
+    {
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            if (!(bool)value)
+            {
+                return new SolidColorBrush(Color.FromRgb(100, 100, 100));
+            }
+            return new SolidColorBrush(Color.FromRgb(255, 100, 100));
+        }
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 58 - 0
demos/csharp/GoProCSharpSample/Properties/AssemblyInfo.cs

@@ -0,0 +1,58 @@
+/* AssemblyInfo.cs/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Wed, Sep  1, 2021  5:05:38 PM */
+
+using System.Reflection;
+using System.Resources;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("GoProCSharpSample")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("GoProCSharpSample")]
+[assembly: AssemblyCopyright("Copyright ©  2021")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components.  If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+//In order to begin building localizable applications, set
+//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
+//inside a <PropertyGroup>.  For example, if you are using US english
+//in your source files, set the <UICulture> to en-US.  Then uncomment
+//the NeutralResourceLanguage attribute below.  Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly: ThemeInfo(
+    ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+                                     //(used if a resource is not found in the page,
+                                     // or application resource dictionaries)
+    ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+                                              //(used if a resource is not found in the page,
+                                              // app, or any theme specific resource dictionaries)
+)]
+
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 74 - 0
demos/csharp/GoProCSharpSample/Properties/Resources.Designer.cs

@@ -0,0 +1,74 @@
+/* Resources.Designer.cs/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Wed, Sep  1, 2021  5:05:38 PM */
+
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//     Runtime Version:4.0.30319.42000
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace GoProCSharpSample.Properties
+{
+
+
+    /// <summary>
+    ///   A strongly-typed resource class, for looking up localized strings, etc.
+    /// </summary>
+    // This class was auto-generated by the StronglyTypedResourceBuilder
+    // class via a tool like ResGen or Visual Studio.
+    // To add or remove a member, edit your .ResX file then rerun ResGen
+    // with the /str option, or rebuild your VS project.
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    internal class Resources
+    {
+
+        private static global::System.Resources.ResourceManager resourceMan;
+
+        private static global::System.Globalization.CultureInfo resourceCulture;
+
+        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+        internal Resources()
+        {
+        }
+
+        /// <summary>
+        ///   Returns the cached ResourceManager instance used by this class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Resources.ResourceManager ResourceManager
+        {
+            get
+            {
+                if ((resourceMan == null))
+                {
+                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GoProCSharpSample.Properties.Resources", typeof(Resources).Assembly);
+                    resourceMan = temp;
+                }
+                return resourceMan;
+            }
+        }
+
+        /// <summary>
+        ///   Overrides the current thread's CurrentUICulture property for all
+        ///   resource lookups using this strongly typed resource class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Globalization.CultureInfo Culture
+        {
+            get
+            {
+                return resourceCulture;
+            }
+            set
+            {
+                resourceCulture = value;
+            }
+        }
+    }
+}

+ 117 - 0
demos/csharp/GoProCSharpSample/Properties/Resources.resx

@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+</root>

+ 33 - 0
demos/csharp/GoProCSharpSample/Properties/Settings.Designer.cs

@@ -0,0 +1,33 @@
+/* Settings.Designer.cs/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Wed, Sep  1, 2021  5:05:39 PM */
+
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//     Runtime Version:4.0.30319.42000
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace GoProCSharpSample.Properties
+{
+
+
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
+    internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
+    {
+
+        private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+        public static Settings Default
+        {
+            get
+            {
+                return defaultInstance;
+            }
+        }
+    }
+}

+ 7 - 0
demos/csharp/GoProCSharpSample/Properties/Settings.settings

@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='utf-8'?>
+<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
+  <Profiles>
+    <Profile Name="(Default)" />
+  </Profiles>
+  <Settings />
+</SettingsFile>

+ 26 - 0
demos/csharp/GoProCSharpSample/README.md

@@ -0,0 +1,26 @@
+# GoProCSharpSample
+
+This sample demonstrates how to discover, pair, and connect to a GoPro camera via Bluetooth LE (BLE). Once a connection is established, the code shows how to:
+
+1. Enable Wi-Fi on the GoPro camera
+2. Read the camera wifi name and password
+3. Get status and notification of camera's battery level, encoding flag, and wifi ap on flag.
+4. Start and stop camera shutter
+
+# Requirements
+
+Visual Studio is required to run the solution. Visit https://visualstudio.microsoft.com/downloads/ to download.
+
+The target .NET framework is v4.7.2
+
+GoPro camera must be paired before any other operations will succeed. Put the camera in pairing mode before attempting pairing with the app.
+
+# Usage
+
+1. Open and run the demo in Visual Studio to show the GUI
+2. `Scan` for GoPro devices
+3. `Pair` to the discovered device that is not `GoPro Cam`. In the .gif below, this is `GoPro 0456` (Only needs to be done once, or if camera is factory reset)
+4. After pairing is successful, `connect` to the same GoPro device
+5. Now use any of the GUI buttons to read WiFi info, enable WiFi AP, set shutter, etc.
+
+![Demo Steps](../../../docs/assets/images/demos/csharp_demo.gif)

+ 134 - 0
demos/csharp/webcam/.gitignore

@@ -0,0 +1,134 @@
+.vs/
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+
+[Dd]ebug/
+[Rr]elease/
+x64/
+[Bb]in/
+[Oo]bj/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.log
+*.svclog
+*.scc
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.Publish.xml
+*.pubxml
+*.azurePubxml
+
+# NuGet Packages Directory
+## TODO: If you have NuGet Package Restore enabled, uncomment the next line
+packages/
+## TODO: If the tool you use requires repositories.config, also uncomment the next line
+!packages/repositories.config
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Others
+sql/
+*.Cache
+ClientBin/
+[Ss]tyle[Cc]op.*
+![Ss]tyle[Cc]op.targets
+~$*
+*~
+*.dbmdl
+*.[Pp]ublish.xml
+
+*.publishsettings
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file to a newer
+# Visual Studio version. Backup files are not needed, because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+App_Data/*.mdf
+App_Data/*.ldf
+
+# =========================
+# Windows detritus
+# =========================
+
+# Windows image file caches
+Thumbs.db
+ehthumbs.db
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Mac desktop service store files
+.DS_Store
+
+_NCrunch*

+ 35 - 0
demos/csharp/webcam/App.config

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<configuration>
+    <configSections>
+        <sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
+            <section name="GoProWebcamViewer.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
+        </sectionGroup>
+    </configSections>
+    <startup> 
+        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
+    </startup>
+    <system.net>
+        <settings>
+            <httpWebRequest useUnsafeHeaderParsing="true" />
+        </settings>
+    </system.net>
+    <userSettings>
+        <GoProWebcamViewer.Properties.Settings>
+            <setting name="FTop" serializeAs="String">
+                <value>0</value>
+            </setting>
+            <setting name="FLeft" serializeAs="String">
+                <value>0</value>
+            </setting>
+            <setting name="FHeight" serializeAs="String">
+                <value>0</value>
+            </setting>
+            <setting name="FWidth" serializeAs="String">
+                <value>0</value>
+            </setting>
+            <setting name="IPAddress" serializeAs="String">
+                <value />
+            </setting>
+        </GoProWebcamViewer.Properties.Settings>
+    </userSettings>
+</configuration>

+ 9 - 0
demos/csharp/webcam/App.xaml

@@ -0,0 +1,9 @@
+<Application x:Class="GoProWebcamViewer.App"
+             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:local="clr-namespace:GoProWebcamViewer"
+             StartupUri="StreamViewer.xaml">
+    <Application.Resources>
+         
+    </Application.Resources>
+</Application>

+ 20 - 0
demos/csharp/webcam/App.xaml.cs

@@ -0,0 +1,20 @@
+/* App.xaml.cs/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Wed Oct 20 21:41:18 UTC 2021 */
+
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace GoProWebcamViewer
+{
+    /// <summary>
+    /// Interaction logic for App.xaml
+    /// </summary>
+    public partial class App : Application
+    {
+    }
+}

+ 148 - 0
demos/csharp/webcam/GoProWebcamViewer.csproj

@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{A5FF4FD0-113A-4312-AA45-147107DEB06B}</ProjectGuid>
+    <OutputType>WinExe</OutputType>
+    <RootNamespace>GoProWebcamViewer</RootNamespace>
+    <AssemblyName>GoProWebcamViewer</AssemblyName>
+    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
+    <WarningLevel>4</WarningLevel>
+    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
+    <Deterministic>true</Deterministic>
+    <PublishUrl>publish\</PublishUrl>
+    <Install>true</Install>
+    <InstallFrom>Disk</InstallFrom>
+    <UpdateEnabled>false</UpdateEnabled>
+    <UpdateMode>Foreground</UpdateMode>
+    <UpdateInterval>7</UpdateInterval>
+    <UpdateIntervalUnits>Days</UpdateIntervalUnits>
+    <UpdatePeriodically>false</UpdatePeriodically>
+    <UpdateRequired>false</UpdateRequired>
+    <MapFileExtensions>true</MapFileExtensions>
+    <ApplicationRevision>0</ApplicationRevision>
+    <ApplicationVersion>1.0.0.%2a</ApplicationVersion>
+    <IsWebBootstrapper>false</IsWebBootstrapper>
+    <UseApplicationTrust>false</UseApplicationTrust>
+    <BootstrapperEnabled>true</BootstrapperEnabled>
+    <NuGetPackageImportStamp>
+    </NuGetPackageImportStamp>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>bin\Release\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+  </PropertyGroup>
+  <PropertyGroup>
+    <ApplicationIcon>
+    </ApplicationIcon>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Xml" />
+    <Reference Include="Microsoft.CSharp" />
+    <Reference Include="System.Core" />
+    <Reference Include="System.Xml.Linq" />
+    <Reference Include="System.Data.DataSetExtensions" />
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Xaml">
+      <RequiredTargetFramework>4.0</RequiredTargetFramework>
+    </Reference>
+    <Reference Include="Vlc.DotNet.Core, Version=3.1.0.0, Culture=neutral, PublicKeyToken=84529da31f4eb963, processorArchitecture=MSIL">
+      <HintPath>packages\Vlc.DotNet.Core.3.1.0\lib\net45\Vlc.DotNet.Core.dll</HintPath>
+    </Reference>
+    <Reference Include="Vlc.DotNet.Core.Interops, Version=3.1.0.0, Culture=neutral, PublicKeyToken=84529da31f4eb963, processorArchitecture=MSIL">
+      <HintPath>packages\Vlc.DotNet.Core.Interops.3.1.0\lib\net45\Vlc.DotNet.Core.Interops.dll</HintPath>
+    </Reference>
+    <Reference Include="Vlc.DotNet.Wpf, Version=3.1.0.0, Culture=neutral, PublicKeyToken=84529da31f4eb963, processorArchitecture=MSIL">
+      <HintPath>packages\Vlc.DotNet.Wpf.3.1.0\lib\net45\Vlc.DotNet.Wpf.dll</HintPath>
+    </Reference>
+    <Reference Include="WindowsBase" />
+    <Reference Include="PresentationCore" />
+    <Reference Include="PresentationFramework" />
+  </ItemGroup>
+  <ItemGroup>
+    <ApplicationDefinition Include="App.xaml">
+      <Generator>MSBuild:Compile</Generator>
+      <SubType>Designer</SubType>
+    </ApplicationDefinition>
+    <Compile Include="App.xaml.cs">
+      <DependentUpon>App.xaml</DependentUpon>
+      <SubType>Code</SubType>
+    </Compile>
+    <Page Include="StreamViewer.xaml">
+      <Generator>MSBuild:Compile</Generator>
+      <SubType>Designer</SubType>
+    </Page>
+    <Compile Include="StreamViewer.xaml.cs">
+      <DependentUpon>StreamViewer.xaml</DependentUpon>
+      <SubType>Code</SubType>
+    </Compile>
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Properties\AssemblyInfo.cs">
+      <SubType>Code</SubType>
+    </Compile>
+    <Compile Include="Properties\Resources.Designer.cs">
+      <AutoGen>True</AutoGen>
+      <DesignTime>True</DesignTime>
+      <DependentUpon>Resources.resx</DependentUpon>
+    </Compile>
+    <Compile Include="Properties\Settings.Designer.cs">
+      <AutoGen>True</AutoGen>
+      <DependentUpon>Settings.settings</DependentUpon>
+      <DesignTimeSharedInput>True</DesignTimeSharedInput>
+    </Compile>
+    <EmbeddedResource Include="Properties\Resources.resx">
+      <Generator>ResXFileCodeGenerator</Generator>
+      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
+    </EmbeddedResource>
+    <None Include="packages.config" />
+    <None Include="Properties\Settings.settings">
+      <Generator>SettingsSingleFileGenerator</Generator>
+      <LastGenOutput>Settings.Designer.cs</LastGenOutput>
+    </None>
+  </ItemGroup>
+  <ItemGroup>
+    <None Include="App.config" />
+  </ItemGroup>
+  <ItemGroup>
+    <BootstrapperPackage Include=".NETFramework,Version=v4.7.2">
+      <Visible>False</Visible>
+      <ProductName>Microsoft .NET Framework 4.7.2 %28x86 and x64%29</ProductName>
+      <Install>true</Install>
+    </BootstrapperPackage>
+    <BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
+      <Visible>False</Visible>
+      <ProductName>.NET Framework 3.5 SP1</ProductName>
+      <Install>false</Install>
+    </BootstrapperPackage>
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+  <Import Project="packages\VideoLAN.LibVLC.Windows.3.0.11\build\VideoLAN.LibVLC.Windows.targets" Condition="Exists('packages\VideoLAN.LibVLC.Windows.3.0.11\build\VideoLAN.LibVLC.Windows.targets')" />
+  <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
+    <PropertyGroup>
+      <ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them.  For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
+    </PropertyGroup>
+    <Error Condition="!Exists('packages\VideoLAN.LibVLC.Windows.3.0.11\build\VideoLAN.LibVLC.Windows.targets')" Text="$([System.String]::Format('$(ErrorText)', 'packages\VideoLAN.LibVLC.Windows.3.0.11\build\VideoLAN.LibVLC.Windows.targets'))" />
+  </Target>
+</Project>

+ 25 - 0
demos/csharp/webcam/GoProWebcamViewer.sln

@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30002.166
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoProWebcamViewer", "GoProWebcamViewer.csproj", "{A5FF4FD0-113A-4312-AA45-147107DEB06B}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{A5FF4FD0-113A-4312-AA45-147107DEB06B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{A5FF4FD0-113A-4312-AA45-147107DEB06B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{A5FF4FD0-113A-4312-AA45-147107DEB06B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{A5FF4FD0-113A-4312-AA45-147107DEB06B}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {0AE62D15-9618-49CC-A5AA-863F4D95E0A8}
+	EndGlobalSection
+EndGlobal

+ 58 - 0
demos/csharp/webcam/Properties/AssemblyInfo.cs

@@ -0,0 +1,58 @@
+/* AssemblyInfo.cs/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Wed Oct 20 21:41:18 UTC 2021 */
+
+using System.Reflection;
+using System.Resources;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("GoProWebcamViewer")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("GoProWebcamViewer")]
+[assembly: AssemblyCopyright("Copyright ©  2020")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components.  If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+//In order to begin building localizable applications, set
+//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
+//inside a <PropertyGroup>.  For example, if you are using US english
+//in your source files, set the <UICulture> to en-US.  Then uncomment
+//the NeutralResourceLanguage attribute below.  Update the "en-US" in
+//the line below to match the UICulture setting in the project file.
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly: ThemeInfo(
+    ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+                                     //(used if a resource is not found in the page,
+                                     // or application resource dictionaries)
+    ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+                                              //(used if a resource is not found in the page,
+                                              // app, or any theme specific resource dictionaries)
+)]
+
+
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]

+ 74 - 0
demos/csharp/webcam/Properties/Resources.Designer.cs

@@ -0,0 +1,74 @@
+/* Resources.Designer.cs/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Wed Oct 20 21:41:18 UTC 2021 */
+
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//     Runtime Version:4.0.30319.42000
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace GoProWebcamViewer.Properties
+{
+
+
+    /// <summary>
+    ///   A strongly-typed resource class, for looking up localized strings, etc.
+    /// </summary>
+    // This class was auto-generated by the StronglyTypedResourceBuilder
+    // class via a tool like ResGen or Visual Studio.
+    // To add or remove a member, edit your .ResX file then rerun ResGen
+    // with the /str option, or rebuild your VS project.
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    internal class Resources
+    {
+
+        private static global::System.Resources.ResourceManager resourceMan;
+
+        private static global::System.Globalization.CultureInfo resourceCulture;
+
+        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+        internal Resources()
+        {
+        }
+
+        /// <summary>
+        ///   Returns the cached ResourceManager instance used by this class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Resources.ResourceManager ResourceManager
+        {
+            get
+            {
+                if ((resourceMan == null))
+                {
+                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GoProWebcamViewer.Properties.Resources", typeof(Resources).Assembly);
+                    resourceMan = temp;
+                }
+                return resourceMan;
+            }
+        }
+
+        /// <summary>
+        ///   Overrides the current thread's CurrentUICulture property for all
+        ///   resource lookups using this strongly typed resource class.
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Globalization.CultureInfo Culture
+        {
+            get
+            {
+                return resourceCulture;
+            }
+            set
+            {
+                resourceCulture = value;
+            }
+        }
+    }
+}

+ 117 - 0
demos/csharp/webcam/Properties/Resources.resx

@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+</root>

+ 89 - 0
demos/csharp/webcam/Properties/Settings.Designer.cs

@@ -0,0 +1,89 @@
+/* Settings.Designer.cs/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Wed Oct 20 21:41:18 UTC 2021 */
+
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//     Runtime Version:4.0.30319.42000
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace GoProWebcamViewer.Properties {
+    
+    
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.5.0.0")]
+    internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
+        
+        private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+        
+        public static Settings Default {
+            get {
+                return defaultInstance;
+            }
+        }
+        
+        [global::System.Configuration.UserScopedSettingAttribute()]
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+        [global::System.Configuration.DefaultSettingValueAttribute("0")]
+        public double FTop {
+            get {
+                return ((double)(this["FTop"]));
+            }
+            set {
+                this["FTop"] = value;
+            }
+        }
+        
+        [global::System.Configuration.UserScopedSettingAttribute()]
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+        [global::System.Configuration.DefaultSettingValueAttribute("0")]
+        public double FLeft {
+            get {
+                return ((double)(this["FLeft"]));
+            }
+            set {
+                this["FLeft"] = value;
+            }
+        }
+        
+        [global::System.Configuration.UserScopedSettingAttribute()]
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+        [global::System.Configuration.DefaultSettingValueAttribute("0")]
+        public double FHeight {
+            get {
+                return ((double)(this["FHeight"]));
+            }
+            set {
+                this["FHeight"] = value;
+            }
+        }
+        
+        [global::System.Configuration.UserScopedSettingAttribute()]
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+        [global::System.Configuration.DefaultSettingValueAttribute("0")]
+        public double FWidth {
+            get {
+                return ((double)(this["FWidth"]));
+            }
+            set {
+                this["FWidth"] = value;
+            }
+        }
+        
+        [global::System.Configuration.UserScopedSettingAttribute()]
+        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+        [global::System.Configuration.DefaultSettingValueAttribute("")]
+        public string IPAddress {
+            get {
+                return ((string)(this["IPAddress"]));
+            }
+            set {
+                this["IPAddress"] = value;
+            }
+        }
+    }
+}

+ 21 - 0
demos/csharp/webcam/Properties/Settings.settings

@@ -0,0 +1,21 @@
+<?xml version='1.0' encoding='utf-8'?>
+<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="GoProWebcamViewer.Properties" GeneratedClassName="Settings">
+  <Profiles />
+  <Settings>
+    <Setting Name="FTop" Type="System.Double" Scope="User">
+      <Value Profile="(Default)">0</Value>
+    </Setting>
+    <Setting Name="FLeft" Type="System.Double" Scope="User">
+      <Value Profile="(Default)">0</Value>
+    </Setting>
+    <Setting Name="FHeight" Type="System.Double" Scope="User">
+      <Value Profile="(Default)">0</Value>
+    </Setting>
+    <Setting Name="FWidth" Type="System.Double" Scope="User">
+      <Value Profile="(Default)">0</Value>
+    </Setting>
+    <Setting Name="IPAddress" Type="System.String" Scope="User">
+      <Value Profile="(Default)" />
+    </Setting>
+  </Settings>
+</SettingsFile>

+ 32 - 0
demos/csharp/webcam/README.md

@@ -0,0 +1,32 @@
+# CSharp Webcam Demo
+
+This demo implements a simple GUI to interact with a GoPro camera that supports [Open GoPro 2.0](https://gopro.github.io/OpenGoPro/http).
+
+# Requirements
+
+This demo will only run on Windows.
+
+Visual Studio is required to run the solution. Visit [VisualStudio](https://visualstudio.microsoft.com/downloads/) to download.
+
+The target .NET framework is v4.7.2
+
+GoPro camera must be paired before any other operations will succeed. Put the camera in
+[pairing mode](https://gopro.github.io/OpenGoPro/tutorials/connect-ble#advertise) before attempting
+pairing with the app.
+
+# Prerequisites
+
+The correct GoPro Webcam drivers must be installed. To verify this, ensure that you can first use your desired
+GoPro as a webcam following the steps [here](https://community.gopro.com/s/article/GoPro-Webcam?language=en_US).
+
+# Usage
+
+1. Connect the GoPro to your computer using the USB cable
+1. Open the solution (GoProWebCamViewer.sln) in Visual Studio, build and run io to show the Webcam GUI
+1. Select `Start Player` to start the VLC backend. Note that the log and status bar have updated. This will be
+   true for all functionality.
+
+   <img src="../../../docs/assets/images/demos/webcam_start_player.png" alt="Start Player" width="600"/>
+2. Select `Show Preview` to start a low quality preview stream
+3. While in preview, feel free to update the FOV or change the zoom
+4. Once you are ready, select `Start Webcam` to start full resolution streaming

+ 63 - 0
demos/csharp/webcam/StreamViewer.xaml

@@ -0,0 +1,63 @@
+<Window x:Class="GoProWebcamViewer.StreamViewer"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:local="clr-namespace:GoProWebcamViewer"
+        xmlns:Vlc="clr-namespace:Vlc.DotNet.Wpf;assembly=Vlc.DotNet.Wpf"
+        DataContext="{Binding RelativeSource={RelativeSource Self}}"
+        mc:Ignorable="d"
+        Title="Stream Viewer" Height="593" Width="640" Closing="Window_Closing">
+    <DockPanel LastChildFill="True">
+        <StatusBar Name="statusBar" Height="15" Margin="0" VerticalAlignment="Top" HorizontalAlignment="Stretch" DockPanel.Dock="Bottom" Padding="0" FontSize="10">
+            <StatusBarItem Padding="0">
+                <TextBlock Name="txtStatusBar" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" FontSize="12" Padding="0,0,0,0"></TextBlock>
+            </StatusBarItem>
+        </StatusBar>
+        <Grid>
+            <Grid.RowDefinitions>
+                <RowDefinition Height="*" />
+            </Grid.RowDefinitions>
+            <Grid.ColumnDefinitions>
+                <ColumnDefinition Width="*"/>
+                <ColumnDefinition Width="5" />
+                <ColumnDefinition x:Name="ExpWidth" Width="200" />
+            </Grid.ColumnDefinitions>
+            <DockPanel Grid.Column="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="Auto">
+                <Vlc:VlcControl Name="mPlayer" Grid.Column="0" HorizontalAlignment="Stretch" Height="300" Margin="10,10,10,10" DockPanel.Dock="Top"></Vlc:VlcControl>
+                <TextBox x:Name="txtGeneral" HorizontalAlignment="Stretch" Height="Auto" Margin="10,10,10,10" 
+                 VerticalScrollBarVisibility="Visible" TextWrapping="Wrap" Text="" VerticalAlignment="Stretch" />
+            </DockPanel>
+
+            <GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" />
+            <Grid Grid.Column="2">
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="*" />
+                    <RowDefinition Height="*" />
+                </Grid.RowDefinitions>
+                <Button x:Name="btnStart" Content="Start Webcam" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="180" Click="btnStart_Click"/>
+                <Button x:Name="btnPreview" Content="Show Preview" HorizontalAlignment="Left" Margin="10,108,0,0" VerticalAlignment="Top" Width="180" Click="btnPreview_Click"/>
+                <Button x:Name="btnStop" Content="Stop Webcam" HorizontalAlignment="Left" Margin="10,36,0,0" VerticalAlignment="Top" Width="180" Click="btnStop_Click" />
+                <Button x:Name="btnExit" Content="Exit" HorizontalAlignment="Left" Margin="10,62,0,0" VerticalAlignment="Top" Width="180" Click="btnExit_Click" />
+                <Button x:Name="btnStatusStream" Content="Stream Status" Grid.Row="0" HorizontalAlignment="Left" Margin="10,200,0,0" VerticalAlignment="Top" Width="180" Click="btnStatusStream_Click" />
+                <Button x:Name="btnClearText" Content="Clear Log" Grid.Row="1" HorizontalAlignment="Left" Margin="10,70,0,0" VerticalAlignment="Top" Width="180" Click="btnClearText_Click" />
+                <ComboBox x:Name="cmbFOV" HorizontalAlignment="Left" Margin="63,225,0,0" VerticalAlignment="Top" Width="79" Height="21">
+                    <ComboBoxItem Content="0" IsSelected="True" />
+                    <ComboBoxItem Content="4"></ComboBoxItem>
+                </ComboBox>
+                <Label Content="Zoom" HorizontalAlignment="Left" Margin="10,8,0,0" Grid.Row="1" VerticalAlignment="Top"/>
+                <Slider x:Name="sldZoom" HorizontalAlignment="Left" Margin="10,34,0,0" Grid.Row="1" VerticalAlignment="Top" Width="180" ValueChanged="sldZoom_ValueChanged" Maximum="100" LargeChange="10" SmallChange="1"
+                        Thumb.DragStarted="Thumb_DragStarted" Thumb.DragCompleted="Thumb_DragCompleted" IsEnabled="False"/>
+                <Button x:Name="btnFOV" Content="FOV" HorizontalAlignment="Left" Margin="10,225,0,0" VerticalAlignment="Top" Width="41" RenderTransformOrigin="0.521,-0.15" Height="21" Click="btnFOV_Click" IsEnabled="False"/>
+                <TextBox x:Name="txtIPAddr" IsEnabled="False" HorizontalAlignment="Left" Height="23" Margin="10,147,0,0" Grid.Row="1" TextWrapping="Wrap" Text="{Binding IPAddr}"  VerticalAlignment="Top" Width="177"/>
+                <Label Content="Camera IP:" HorizontalAlignment="Left" Margin="10,121,0,0" Grid.Row="1" VerticalAlignment="Top"/>
+                <Button x:Name="btnStartPlayer" Content="Start Player" HorizontalAlignment="Left" Margin="10,134,0,0" VerticalAlignment="Top" Width="87" Click="btnStartPlayer_Click"/>
+                <Button x:Name="btnStopPlayer" Content="Stop Player" HorizontalAlignment="Left" Margin="102,134,0,0" VerticalAlignment="Top" Width="88" Click="btnStopPlayer_Click"/>
+                <Button x:Name="btnMute" Content="Toggle Mute" HorizontalAlignment="Left" Margin="10,159,0,0" VerticalAlignment="Top" Width="87" Click="btnMute_Click"/>
+
+            </Grid>
+
+        </Grid>
+
+    </DockPanel>
+</Window>

+ 337 - 0
demos/csharp/webcam/StreamViewer.xaml.cs

@@ -0,0 +1,337 @@
+/* StreamViewer.xaml.cs/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Wed Oct 20 21:41:18 UTC 2021 */
+
+using Microsoft.Win32;
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace GoProWebcamViewer
+{
+    enum Status
+    {
+        SUCCESS,
+        FAILURE
+    }
+
+    /// <summary>
+    /// Interaction logic for StreamViewer.xaml
+    /// </summary>
+    public partial class StreamViewer : Window, INotifyPropertyChanged
+    {
+        bool bQuit = false;
+        bool zoomMan = false;
+
+        private string playerEnabledText, webcamEnabledText, previewEnabledText;
+        private bool playerEnabled
+        {
+            get { return playerEnabledText == "enabled" ? true : false; }
+            set
+            {
+                playerEnabledText = value == true ? "enabled" : "disabled";
+                UpdateStatusBar();
+            }
+        }
+        private bool webcamEnabled
+        {
+            get { return webcamEnabledText == "enabled" ? true : false; }
+            set
+            {
+                webcamEnabledText = value == true ? "enabled" : "disabled";
+                UpdateStatusBar();
+            }
+        }
+        private bool previewEnabled
+        {
+            get { return previewEnabledText == "enabled" ? true : false; }
+            set
+            {
+                previewEnabledText = value == true ? "enabled" : "disabled";
+                UpdateStatusBar();
+            }
+        }
+
+        private string ipaddr = "unknown";
+
+        public event PropertyChangedEventHandler PropertyChanged;
+
+        public string IPAddr
+        {
+            get { return ipaddr; }
+            set
+            {
+                ipaddr = value;
+                OnPropertyChanged("IPAddr");
+            }
+        }
+
+        public StreamViewer()
+        {
+            InitializeComponent();
+            this.Top = Properties.Settings.Default.FTop;
+            this.Left = Properties.Settings.Default.FLeft;
+            this.Height = Properties.Settings.Default.FHeight;
+            this.Width = Properties.Settings.Default.FWidth;
+
+            if (this.Height == 0)
+            {
+                this.Height = 600;
+                this.Width = 800;
+            }
+            ipaddr = Properties.Settings.Default.IPAddress;
+
+            this.txtIPAddr.Text = ipaddr;
+            DataContext = this;
+
+            var vlcLibDirectory = new DirectoryInfo(System.IO.Path.Combine("./", "libvlc", IntPtr.Size == 4 ? "win-x86" : "win-x64"));
+
+            var options = new string[]
+            {
+                // VLC options can be given here. Please refer to the VLC command line documentation.
+            };
+
+            mPlayer.SourceProvider.CreatePlayer(vlcLibDirectory, options);
+
+            new Timer(IPAddrCheck, this, 50, 10000);
+
+            playerEnabled = false;
+            webcamEnabled = false;
+            previewEnabled = false;
+        }
+
+        private void Log(string text)
+        {
+            Dispatcher.Invoke((Action)delegate ()
+            { /* update UI */
+                this.txtGeneral.Text += text;
+            });
+        }
+
+        private void UpdateStatusBar()
+        {
+            Dispatcher.Invoke((Action)delegate ()
+            { /* update UI */
+                this.txtStatusBar.Text = $"Player: {playerEnabledText,-20}Webcam: {webcamEnabledText,-20}Preview: {previewEnabledText,-20}";
+            });
+        }
+
+        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
+        {
+            bQuit = true;
+            ThreadPool.QueueUserWorkItem(_ => mPlayer.SourceProvider.MediaPlayer.Stop());
+
+            if (WindowState == WindowState.Maximized)
+            {
+                // Use the RestoreBounds as the current values will be 0, 0 and the size of the screen
+                Properties.Settings.Default.FTop = RestoreBounds.Top;
+                Properties.Settings.Default.FLeft = RestoreBounds.Left;
+                Properties.Settings.Default.FHeight = RestoreBounds.Height;
+                Properties.Settings.Default.FWidth = RestoreBounds.Width;
+            }
+            else
+            {
+                Properties.Settings.Default.FTop = this.Top;
+                Properties.Settings.Default.FLeft = this.Left;
+                Properties.Settings.Default.FHeight = this.Height;
+                Properties.Settings.Default.FWidth = this.Width;
+            }
+            Properties.Settings.Default.IPAddress = ipaddr;
+            Properties.Settings.Default.Save();
+        }
+
+        private Status SendHTTPRequest(string endpoint)
+        {
+            Status status = Status.FAILURE;
+            if (ipaddr == "not found")
+                return status;
+            string responseString;
+            string requestString = "http://" + ipaddr + ":8080/gopro/" + endpoint;
+            HttpWebResponse resp;
+            HttpWebRequest req = HttpWebRequest.CreateHttp(requestString);
+            req.Method = "GET";
+            req.KeepAlive = false;
+            try
+            {
+                Log(requestString + " ==> \n");
+                resp = (HttpWebResponse)req.GetResponse();
+                responseString = new StreamReader(resp.GetResponseStream()).ReadToEnd();
+                resp.Close();
+                status = Status.SUCCESS;
+            }
+            catch (WebException ep)
+            {
+                responseString = "Failed url " + endpoint + ": " + ep.Message;
+                HttpWebResponse respy = (HttpWebResponse)ep.Response;
+                if (respy != null)
+                {
+                    responseString = new StreamReader(respy.GetResponseStream()).ReadToEnd();
+                }
+            }
+            if (!bQuit)
+            {
+                Log(responseString);
+            }
+
+            return status;
+        }
+
+        private Status SetSetting(int id, int value)
+        {
+            return SendHTTPRequest("camera/setting?setting=" + id.ToString() + "&option=" + value.ToString());
+        }
+
+        private void btnStatusStream_Click(object sender, RoutedEventArgs e)
+        {
+            SendHTTPRequest("webcam/status");
+        }
+
+        private void btnClearText_Click(object sender, RoutedEventArgs e)
+        {
+            txtGeneral.Clear();
+            txtStatusBar.Text = "";
+        }
+
+        private void HideStream()
+        {
+            ThreadPool.QueueUserWorkItem(_ => mPlayer.SourceProvider.MediaPlayer.Stop());
+            playerEnabled = false;
+        }
+
+        private void sldZoom_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
+        {
+            if (!zoomMan)
+            {
+                SendHTTPRequest("camera/digital_zoom?percent=" + Convert.ToInt32(sldZoom.Value));
+            }
+        }
+
+        private void Thumb_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e)
+        {
+            zoomMan = true;
+        }
+
+        private void Thumb_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
+        {
+            zoomMan = false;
+            SendHTTPRequest("camera/digital_zoom?percent=" + Convert.ToInt32(sldZoom.Value));
+        }
+
+        private void btnFOV_Click(object sender, RoutedEventArgs e)
+        {
+            int value = Convert.ToInt32(((ComboBoxItem)cmbFOV.SelectedItem).Content.ToString());
+            SetSetting(43, value);
+        }
+
+        private void txtIPAddr_TextChanged(object sender, TextChangedEventArgs e)
+        {
+            ipaddr = txtIPAddr.Text;
+        }
+
+        private void IPAddrCheck(object state)
+        {
+            String strHostName = Dns.GetHostName();
+            bool found = false;
+            // Find host by name
+            IPHostEntry iphostentry = Dns.GetHostEntry(strHostName);
+
+            // Enumerate IP addresses
+            foreach (IPAddress ipaddress in iphostentry.AddressList)
+            {
+                if (ipaddress.AddressFamily == AddressFamily.InterNetwork)
+                {
+                    byte[] abytes = ipaddress.GetAddressBytes();
+                    if (abytes[0] == 172 && abytes[1] >= 20 && abytes[1] <= 29 && abytes[3] >= 50 && abytes[3] <= 70)
+                    {
+                        ipaddr = ipaddress.ToString();
+                        StringBuilder sb = new StringBuilder(ipaddress.ToString());
+                        sb[ipaddress.ToString().Length - 1] = '1';
+                        IPAddr = sb.ToString();
+                        found = true;
+                        break;
+                    }
+                }
+            }
+            if (!found)
+                IPAddr = "not found";
+
+        }
+        protected void OnPropertyChanged(string name = null)
+        {
+            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+        }
+
+        private void btnPreview_Click(object sender, RoutedEventArgs e)
+        {
+            if (SendHTTPRequest("webcam/preview") == Status.SUCCESS)
+            {
+                btnFOV.IsEnabled = true;
+                sldZoom.IsEnabled = true;
+                previewEnabled = true;
+                webcamEnabled = false;
+            }
+        }
+
+        private void btnStart_Click(object sender, RoutedEventArgs e)
+        {
+            if (SendHTTPRequest("webcam/start") == Status.SUCCESS)
+            {
+                btnFOV.IsEnabled = false;
+                sldZoom.IsEnabled = false;
+                webcamEnabled = true;
+                previewEnabled = false;
+            }
+        }
+
+        private void btnStop_Click(object sender, RoutedEventArgs e)
+        {
+            if (SendHTTPRequest("webcam/stop") == Status.SUCCESS)
+            {
+                HideStream();
+                btnFOV.IsEnabled = false;
+                sldZoom.IsEnabled = false;
+                webcamEnabled = false;
+            }
+        }
+
+        private void btnExit_Click(object sender, RoutedEventArgs e)
+        {
+            if (SendHTTPRequest("webcam/exit") == Status.SUCCESS)
+            {
+                HideStream();
+                btnFOV.IsEnabled = false;
+                sldZoom.IsEnabled = false;
+                webcamEnabled = false;
+                previewEnabled = false;
+            }
+        }
+
+        private void btnStartPlayer_Click(object sender, RoutedEventArgs e)
+        {
+            Log("Starting video player...\n");
+            ThreadPool.QueueUserWorkItem(_ =>
+            {
+                mPlayer.SourceProvider.MediaPlayer.Play(new Uri("udp://@0.0.0.0:8554", UriKind.Absolute), new string[] { "--network-caching=10", "--no-audio" });
+            });
+            playerEnabled = true;
+        }
+
+        private void btnStopPlayer_Click(object sender, RoutedEventArgs e)
+        {
+            Log("Stopping video player...\n");
+            HideStream();
+        }
+
+        private void btnMute_Click(object sender, RoutedEventArgs e)
+        {
+            Log("Toggling mute...\n");
+            ThreadPool.QueueUserWorkItem(_ => mPlayer.SourceProvider.MediaPlayer.Audio.ToggleMute());
+        }
+    }
+}

+ 7 - 0
demos/csharp/webcam/packages.config

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="VideoLAN.LibVLC.Windows" version="3.0.11" targetFramework="net472" />
+  <package id="Vlc.DotNet.Core" version="3.1.0" targetFramework="net472" />
+  <package id="Vlc.DotNet.Core.Interops" version="3.1.0" targetFramework="net472" />
+  <package id="Vlc.DotNet.Wpf" version="3.1.0" targetFramework="net472" />
+</packages>

+ 17 - 0
demos/ionic/file_transfer/.browserslistrc

@@ -0,0 +1,17 @@
+# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
+# For additional information regarding the format and rule options, please see:
+# https://github.com/browserslist/browserslist#queries
+
+# For the full list of supported browsers by the Angular framework, please see:
+# https://angular.io/guide/browser-support
+
+# You can see what browsers were selected by your queries by running:
+#   npx browserslist
+
+last 1 Chrome version
+last 1 Firefox version
+last 2 Edge major versions
+last 2 Safari major versions
+last 2 iOS major versions
+Firefox ESR
+not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

+ 16 - 0
demos/ionic/file_transfer/.editorconfig

@@ -0,0 +1,16 @@
+# Editor configuration, see https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.ts]
+quote_type = single
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false

+ 47 - 0
demos/ionic/file_transfer/.eslintrc.json

@@ -0,0 +1,47 @@
+{
+  "root": true,
+  "ignorePatterns": ["projects/**/*"],
+  "overrides": [
+    {
+      "files": ["*.ts"],
+      "parserOptions": {
+        "project": ["tsconfig.json", "e2e/tsconfig.json"],
+        "createDefaultProgram": true
+      },
+      "extends": [
+        "plugin:@angular-eslint/ng-cli-compat",
+        "plugin:@angular-eslint/ng-cli-compat--formatting-add-on",
+        "plugin:@angular-eslint/template/process-inline-templates"
+      ],
+      "rules": {
+        "@angular-eslint/component-class-suffix": [
+          "error",
+          {
+            "suffixes": ["Page", "Component"]
+          }
+        ],
+        "@angular-eslint/component-selector": [
+          "error",
+          {
+            "type": "element",
+            "prefix": "app",
+            "style": "kebab-case"
+          }
+        ],
+        "@angular-eslint/directive-selector": [
+          "error",
+          {
+            "type": "attribute",
+            "prefix": "app",
+            "style": "camelCase"
+          }
+        ]
+      }
+    },
+    {
+      "files": ["*.html"],
+      "extends": ["plugin:@angular-eslint/template/recommended"],
+      "rules": {}
+    }
+  ]
+}

+ 31 - 0
demos/ionic/file_transfer/.gitignore

@@ -0,0 +1,31 @@
+# Specifies intentionally untracked files to ignore when using Git
+# http://git-scm.com/docs/gitignore
+
+*~
+*.sw[mnpcod]
+.tmp
+*.tmp
+*.tmp.*
+*.sublime-project
+*.sublime-workspace
+.DS_Store
+Thumbs.db
+UserInterfaceState.xcuserstate
+$RECYCLE.BIN/
+
+*.log
+log.txt
+npm-debug.log*
+
+/.idea
+/.ionic
+/.sass-cache
+/.sourcemaps
+/.versions
+/.vscode
+/coverage
+/dist
+/node_modules
+/platforms
+/plugins
+/www

+ 96 - 0
demos/ionic/file_transfer/android/.gitignore

@@ -0,0 +1,96 @@
+# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
+
+# Built application files
+*.apk
+*.aar
+*.ap_
+*.aab
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+#  Uncomment the following line in case you need and you don't have the release build type files in your app
+# release/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# IntelliJ
+*.iml
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/gradle.xml
+.idea/assetWizardSettings.xml
+.idea/dictionaries
+.idea/libraries
+# Android Studio 3 in .gitignore file.
+.idea/caches
+.idea/modules.xml
+# Comment next line if keeping position of elements in Navigation Editor is relevant for you
+.idea/navEditor.xml
+
+# Keystore files
+# Uncomment the following lines if you do not want to check your keystore files in.
+#*.jks
+#*.keystore
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
+.cxx/
+
+# Google Services (e.g. APIs or Firebase)
+# google-services.json
+
+# Freeline
+freeline.py
+freeline/
+freeline_project_description.json
+
+# fastlane
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+fastlane/readme.md
+
+# Version control
+vcs.xml
+
+# lint
+lint/intermediates/
+lint/generated/
+lint/outputs/
+lint/tmp/
+# lint/reports/
+
+# Android Profiling
+*.hprof
+
+# Cordova plugins for Capacitor
+capacitor-cordova-android-plugins
+
+# Copied web assets
+app/src/main/assets/public

+ 3 - 0
demos/ionic/file_transfer/android/.idea/.gitignore

@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml

+ 6 - 0
demos/ionic/file_transfer/android/.idea/compiler.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <bytecodeTargetLevel target="11" />
+  </component>
+</project>

+ 30 - 0
demos/ionic/file_transfer/android/.idea/jarRepositories.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="RemoteRepositoriesConfiguration">
+    <remote-repository>
+      <option name="id" value="central" />
+      <option name="name" value="Maven Central repository" />
+      <option name="url" value="https://repo1.maven.org/maven2" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="jboss.community" />
+      <option name="name" value="JBoss Community repository" />
+      <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="BintrayJCenter" />
+      <option name="name" value="BintrayJCenter" />
+      <option name="url" value="https://jcenter.bintray.com/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="Google" />
+      <option name="name" value="Google" />
+      <option name="url" value="https://dl.google.com/dl/android/maven2/" />
+    </remote-repository>
+    <remote-repository>
+      <option name="id" value="MavenRepo" />
+      <option name="name" value="MavenRepo" />
+      <option name="url" value="https://repo.maven.apache.org/maven2/" />
+    </remote-repository>
+  </component>
+</project>

+ 9 - 0
demos/ionic/file_transfer/android/.idea/misc.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Embedded JDK" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/build/classes" />
+  </component>
+  <component name="ProjectType">
+    <option name="id" value="Android" />
+  </component>
+</project>

+ 2 - 0
demos/ionic/file_transfer/android/app/.gitignore

@@ -0,0 +1,2 @@
+/build/*
+!/build/.npmkeep

+ 51 - 0
demos/ionic/file_transfer/android/app/build.gradle

@@ -0,0 +1,51 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion rootProject.ext.compileSdkVersion
+    defaultConfig {
+        applicationId "io.numbersprotocol.capturelite.experiments.gopro"
+        minSdkVersion rootProject.ext.minSdkVersion
+        targetSdkVersion rootProject.ext.targetSdkVersion
+        versionCode 1
+        versionName "1.0"
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        aaptOptions {
+             // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+             // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
+            ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
+        }
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+repositories {
+    flatDir{
+        dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
+    }
+}
+
+dependencies {
+    implementation fileTree(include: ['*.jar'], dir: 'libs')
+    implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
+    implementation project(':capacitor-android')
+    testImplementation "junit:junit:$junitVersion"
+    androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
+    androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
+    implementation project(':capacitor-cordova-android-plugins')
+}
+
+apply from: 'capacitor.build.gradle'
+
+try {
+    def servicesJSON = file('google-services.json')
+    if (servicesJSON.text) {
+        apply plugin: 'com.google.gms.google-services'
+    }
+} catch(Exception e) {
+    logger.warn("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
+}

+ 27 - 0
demos/ionic/file_transfer/android/app/capacitor.build.gradle

@@ -0,0 +1,27 @@
+// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
+
+android {
+  compileOptions {
+      sourceCompatibility JavaVersion.VERSION_1_8
+      targetCompatibility JavaVersion.VERSION_1_8
+  }
+}
+
+apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
+dependencies {
+    implementation project(':capacitor-community-bluetooth-le')
+    implementation project(':capacitor-community-http')
+    implementation project(':capacitor-community-wifi')
+    implementation project(':capacitor-app')
+    implementation project(':capacitor-filesystem')
+    implementation project(':capacitor-haptics')
+    implementation project(':capacitor-keyboard')
+    implementation project(':capacitor-status-bar')
+    implementation project(':capacitor-storage')
+
+}
+
+
+if (hasProperty('postBuildExtras')) {
+  postBuildExtras()
+}

+ 21 - 0
demos/ionic/file_transfer/android/app/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 29 - 0
demos/ionic/file_transfer/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java

@@ -0,0 +1,29 @@
+/* ExampleInstrumentedTest.java/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Wed Sep  8 21:37:26 UTC 2021 */
+
+package com.getcapacitor.myapp;
+
+import static org.junit.Assert.*;
+
+import android.content.Context;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+
+    @Test
+    public void useAppContext() throws Exception {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        assertEquals("com.getcapacitor.app", appContext.getPackageName());
+    }
+}

+ 76 - 0
demos/ionic/file_transfer/android/app/src/main/AndroidManifest.xml

@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.numbersprotocol.capturelite.experiments.gopro">
+
+    <application
+        android:networkSecurityConfig="@xml/network_security_config"
+        android:requestLegacyExternalStorage="true"
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+
+        <activity
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
+            android:name="io.numbersprotocol.capturelite.experiments.gopro.MainActivity"
+            android:label="@string/title_activity_main"
+            android:theme="@style/AppTheme.NoActionBarLaunch"
+            android:launchMode="singleTask">
+
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+
+        </activity>
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="${applicationId}.fileprovider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/file_paths"></meta-data>
+        </provider>
+    </application>
+
+    <!-- Permissions -->
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    
+    <!-- Storage Permissions -->
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <!--  Wi-Fi Permissions -->
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+
+    <!-- Bluetooth Permissions -->
+    <!-- Request legacy Bluetooth permissions on older devices. -->
+    <uses-permission
+        android:name="android.permission.BLUETOOTH"
+        android:maxSdkVersion="30" />
+    <uses-permission
+        android:name="android.permission.BLUETOOTH_ADMIN"
+        android:maxSdkVersion="30" />
+    <!-- Needed only if your app looks for Bluetooth devices.
+        You must add an attribute to this permission, or declare the
+        ACCESS_FINE_LOCATION permission, depending on the results when you
+        check location usage in your app. -->
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
+    <!-- Needed only if your app makes the device discoverable to Bluetooth
+        devices. -->
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+    <!-- Needed only if your app communicates with already-paired Bluetooth
+        devices. -->
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+
+</manifest>

+ 6 - 0
demos/ionic/file_transfer/android/app/src/main/assets/capacitor.config.json

@@ -0,0 +1,6 @@
+{
+	"appId": "io.numbersprotocol.capturelite.experiments.gopro",
+	"appName": "go-pro-demo-ionic3",
+	"webDir": "www",
+	"bundledWebRuntime": false
+}

+ 38 - 0
demos/ionic/file_transfer/android/app/src/main/assets/capacitor.plugins.json

@@ -0,0 +1,38 @@
+[
+	{
+		"pkg": "@capacitor-community/bluetooth-le",
+		"classpath": "com.capacitorjs.community.plugins.bluetoothle.BluetoothLe"
+	},
+	{
+		"pkg": "@capacitor-community/http",
+		"classpath": "com.getcapacitor.plugin.http.Http"
+	},
+	{
+		"pkg": "@capacitor-community/wifi",
+		"classpath": "com.digaus.capacitor.wifi.Wifi"
+	},
+	{
+		"pkg": "@capacitor/app",
+		"classpath": "com.capacitorjs.plugins.app.AppPlugin"
+	},
+	{
+		"pkg": "@capacitor/filesystem",
+		"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
+	},
+	{
+		"pkg": "@capacitor/haptics",
+		"classpath": "com.capacitorjs.plugins.haptics.HapticsPlugin"
+	},
+	{
+		"pkg": "@capacitor/keyboard",
+		"classpath": "com.capacitorjs.plugins.keyboard.KeyboardPlugin"
+	},
+	{
+		"pkg": "@capacitor/status-bar",
+		"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
+	},
+	{
+		"pkg": "@capacitor/storage",
+		"classpath": "com.capacitorjs.plugins.storage.StoragePlugin"
+	}
+]

+ 8 - 0
demos/ionic/file_transfer/android/app/src/main/java/io/numbersprotocol/capturelite/experiments/gopro/MainActivity.java

@@ -0,0 +1,8 @@
+/* MainActivity.java/Open GoPro, Version 2.0 (C) Copyright 2021 GoPro, Inc. (http://gopro.com/OpenGoPro). */
+/* This copyright was auto-generated on Wed Sep  8 21:37:26 UTC 2021 */
+
+package io.numbersprotocol.capturelite.experiments.gopro;
+
+import com.getcapacitor.BridgeActivity;
+
+public class MainActivity extends BridgeActivity {}

BIN
demos/ionic/file_transfer/android/app/src/main/res/drawable-land-hdpi/splash.png


BIN
demos/ionic/file_transfer/android/app/src/main/res/drawable-land-mdpi/splash.png


BIN
demos/ionic/file_transfer/android/app/src/main/res/drawable-land-xhdpi/splash.png


BIN
demos/ionic/file_transfer/android/app/src/main/res/drawable-land-xxhdpi/splash.png


BIN
demos/ionic/file_transfer/android/app/src/main/res/drawable-land-xxxhdpi/splash.png


BIN
demos/ionic/file_transfer/android/app/src/main/res/drawable-port-hdpi/splash.png


Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно