├── .gitignore ├── .idea ├── .gitignore └── discord.xml ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src └── main └── java └── codes └── shiftmc └── streaming ├── ImageProcessor.java ├── ParticleLib.java ├── client ├── BlazeNetClient.java ├── BufferedImageClient.java ├── Clients.java ├── LocalClient.java ├── RTMPClient.java └── UrlClient.java ├── data ├── BlazeNetData.java ├── Connection.java ├── Connections.java ├── Keypoint.java └── Pose.java ├── renderer ├── Renderers.java ├── map │ ├── ImageBundleRenderer.java │ ├── ItemMapFrame.java │ ├── MapRenderer.java │ └── model │ │ └── Map.java └── particle │ ├── ParticleData.java │ └── ParticleImage.java └── socket └── SocketServer.java /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | 41 | ### Mac OS ### 42 | .DS_Store -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Streaming Framework 3 | 4 | Uma api criada para o Minestom, feita para facilitar a criação de qualquer tipo de display no Minecraft. 5 | 6 | ## Demo 7 | 8 | ### Renderizar uma imagem estática em um mapa 9 | ```java 10 | new MapRenderer( 11 | Point, 12 | Instance, 13 | Orientation, 14 | 128, // Width 15 | 128, // Height 16 | 20, // Frame rate 17 | 100, // Similarity 18 | false, // Bundle Packet (Apenas funciona se slowSend tiver desativado) 19 | false, // Slow Send (Enviar imagens em diversos ticks, pode acelerar usando o método MapRenderer#setAmount(int)) 20 | true // Arb Encode (https://github.com/JNNGL/vanilla-shaders?tab=readme-ov-file#rgb-maps) 21 | ).render(BufferedImage) 22 | ``` 23 | 24 | ### Fazer Streaming em tempo real da sua tela com partículas 25 | ```java 26 | new LocalClient(new ParticleImage( 27 | Vec.ZERO, 28 | WorldLoader.instance, 29 | 16 * 8, 9 * 8, 30 | 0.3f, 1f 31 | )).start(); 32 | ``` 33 | ## Gradle 34 | ```kts 35 | repositories { 36 | maven { 37 | name = "craftsapiensRepoReleases" 38 | isAllowInsecureProtocol = true 39 | url = uri("http://node.craftsapiens.com.br:50021/releases") 40 | } 41 | } 42 | 43 | dependencies { 44 | // Não recomendado usar versões onde o último parâmetro não é zero 45 | implementation("codes.shiftmc:streaming:1.x.0") 46 | } 47 | ``` 48 | ## Arb Encode 49 | 50 | Com Arb 51 | ![With arb](https://i.imgur.com/iOVFZTo.jpeg) 52 | Sem Arb 53 | ![Without arb](https://i.imgur.com/5gt5WqO.jpeg) 54 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java") 3 | `maven-publish` 4 | } 5 | 6 | repositories { 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | implementation("net.minestom:minestom-snapshots:7b180172ce") 12 | implementation("ch.qos.logback:logback-classic:1.5.6") 13 | implementation("com.corundumstudio.socketio:netty-socketio:2.0.11") 14 | implementation("uk.co.caprica:vlcj:4.8.3") 15 | } 16 | 17 | java { 18 | withSourcesJar() 19 | withJavadocJar() 20 | } 21 | 22 | publishing { 23 | repositories { 24 | maven { 25 | name = "craftsapiens" 26 | url = uri("http://node.craftsapiens.com.br:50021/releases") 27 | credentials(PasswordCredentials::class) 28 | isAllowInsecureProtocol = true 29 | authentication { 30 | create("basic") 31 | } 32 | } 33 | } 34 | publications { 35 | create("maven") { 36 | groupId = "codes.shiftmc" 37 | artifactId = "streaming" 38 | version = "1.5.6" 39 | from(components["java"]) 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 2 | 3 | # Releases token & secret 4 | craftsapiensReleasesUsername={token} 5 | craftsapiensReleasesPassword={secret} -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShiftSad/Streaming/fee48a6a606ba5ef716f8a6415dff0e14210a4e8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Aug 04 12:36:19 BRT 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" 3 | } 4 | rootProject.name = "Streaming" 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/ImageProcessor.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | public class ImageProcessor { 6 | 7 | private static final int[] mapColorPalette = { 8 | 0xFF597D27, 0xFF6D9930, 0xFF7FB238, 0xFF435E1D, 9 | 0xFFAEA473, 0xFFD5C98C, 0xFFF7E9A3, 0xFF827B56, 10 | 0xFF8C8C8C, 0xFFABABAB, 0xFFC7C7C7, 0xFF696969, 11 | 0xFFB40000, 0xFFDC0000, 0xFFFF0000, 0xFF870000, 12 | 0xFF7070B4, 0xFF8A8ADC, 0xFFA0A0FF, 0xFF545487, 13 | 0xFF757575, 0xFF909090, 0xFFA7A7A7, 0xFF585858, 14 | 0xFF005700, 0xFF006A00, 0xFF007C00, 0xFF004100, 15 | 0xFFB4B4B4, 0xFFDCDCDC, 0xFFFFFFFF, 0xFF878787, 16 | 0xFF737681, 0xFF8D909E, 0xFFA4A8B8, 0xFF565861, 17 | 0xFF6A4C36, 0xFF825E42, 0xFF976D4D, 0xFF4F3928, 18 | 0xFF4F4F4F, 0xFF606060, 0xFF707070, 0xFF3B3B3B, 19 | 0xFF2D2DB4, 0xFF3737DC, 0xFF4040FF, 0xFF212187, 20 | 0xFF645432, 0xFF7B663E, 0xFF8F7748, 0xFF4B3F26, 21 | 0xFFB4B1AC, 0xFFDCD9D3, 0xFFFFFFF5, 0xFF878581, 22 | 0xFF985924, 0xFFBA6D2C, 0xFFD87F33, 0xFF72431B, 23 | 0xFF7D3598, 0xFF9941BA, 0xFFB24CD8, 0xFF5E2872, 24 | 0xFF486C98, 0xFF5884BA, 0xFF6699D8, 0xFF365172, 25 | 0xFFA1A124, 0xFFC5C52C, 0xFFE5E533, 0xFF79791B, 26 | 0xFF599011, 0xFF6DB015, 0xFF7FCC19, 0xFF436C0D, 27 | 0xFFAA5974, 0xFFD06D8E, 0xFFF27FA5, 0xFF804357, 28 | 0xFF353535, 0xFF414141, 0xFF4C4C4C, 0xFF282828, 29 | 0xFF6C6C6C, 0xFF848484, 0xFF999999, 0xFF515151, 30 | 0xFF35596C, 0xFF416D84, 0xFF4C7FB2, 0xFF284351, 31 | 0xFF592C7D, 0xFF6D3699, 0xFF7F3FB2, 0xFF43215E, 32 | 0xFF24357D, 0xFF2C4199, 0xFF334CB2, 0xFF1B285E, 33 | 0xFF483524, 0xFF58412C, 0xFF664C33, 0xFF36281B, 34 | 0xFF485924, 0xFF586D2C, 0xFF667F33, 0xFF36431B, 35 | 0xFF6C2424, 0xFF842C2C, 0xFF993333, 0xFF511B1B, 36 | 0xFF111111, 0xFF151515, 0xFF191919, 0xFF0D0D0D, 37 | 0xFFB0A836, 0xFFD7CD42, 0xFFFAEE4D, 0xFF847E28, 38 | 0xFF409A96, 0xFF4FBCCB, 0xFF5CD1D5, 0xFF307370, 39 | 0xFF345AB4, 0xFF3F6EDC, 0xFF4A80FF, 0xFF274387, 40 | 0xFF009928, 0xFF00BB32, 0xFF00D93A, 0xFF00721E, 41 | 0xFF5B3C22, 0xFF6F4A2A, 0xFF815631, 0xFF442D19, 42 | 0xFF4F0100, 0xFF600100, 0xFF700200, 0xFF3B0100, 43 | 0xFF937C71, 0xFFB4988A, 0xFFD1B1A1, 0xFF6E5D55, 44 | 0xFF703919, 0xFF89461F, 0xFF9F5224, 0xFF542B13, 45 | 0xFF693D4C, 0xFF804B5D, 0xFF95576C, 0xFF4E2E39, 46 | 0xFF4F4C61, 0xFF605D77, 0xFF706C8A, 0xFF3B3949, 47 | 0xFF835D19, 0xFFA07220, 0xFFBA8527, 0xFF624613, 48 | 0xFF485225, 0xFF58642D, 0xFF677535, 0xFF363D1C, 49 | 0xFF703637, 0xFF8A4243, 0xFFA04D4E, 0xFF542829, 50 | 0xFF281C18, 0xFF31231E, 0xFF392923, 0xFF1E1512, 51 | 0xFF5F4B45, 0xFF745C54, 0xFF876B62, 0xFF473833, 52 | 0xFF3D4040, 0xFF4B4F4F, 0xFF575C5C, 0xFF2E3030, 53 | 0xFF56333E, 0xFF693E4B, 0xFF7A4958, 0xFF40262E, 54 | 0xFF352B40, 0xFF41354F, 0xFF4C3E5C, 0xFF282030, 55 | 0xFF352318, 0xFF412B1E, 0xFF4C3223, 0xFF281A12, 56 | 0xFF35391D, 0xFF414624, 0xFF4C522A, 0xFF282B16, 57 | 0xFF642A20, 0xFF7A3327, 0xFF8E3C2E, 0xFF4B2018, 58 | 0xFF1A0F0B, 0xFF1F120D, 0xFF251610, 0xFF130B08, 59 | 0xFF852122, 0xFFA3292A, 0xFFBD3031, 0xFF641919, 60 | 0xFF682C44, 0xFF7F3653, 0xFF943F61, 0xFF4E2133, 61 | 0xFF401114, 0xFF4F1519, 0xFF5C191D, 0xFF300D11, 62 | 0xFF0F585E, 0xFF126C73, 0xFF167E86, 0xFF0B4246, 63 | 0xFF286462, 0xFF327A78, 0xFF3A8E8C, 0xFF1E4B4A, 64 | 0xFF3C1F2B, 0xFF4A2535, 0xFF562C3E, 0xFF2D1720, 65 | 0xFF0E7F5D, 0xFF119B72, 0xFF14B485, 0xFF0A5F46, 66 | 0xFF464646, 0xFF565656, 0xFF646464, 0xFF343434, 67 | 0xFF987B67, 0xFFBA967E, 0xFFD8AF93, 0xFF726C5D, 68 | 0xFF597569, 0xFF6D8F7D, 0xFF7FB796, 0xFF43584F 69 | }; 70 | 71 | public static BufferedImage resizeAndEncodeImage(BufferedImage inputImage) { 72 | int width = inputImage.getWidth(); 73 | int height = inputImage.getHeight(); 74 | 75 | if (width != 128 || height != 128) { 76 | throw new IllegalArgumentException("Image must be 128x128"); 77 | } 78 | 79 | // Create a 64x64 image (downsampled) 80 | BufferedImage resizedImage = new BufferedImage(64, 64, BufferedImage.TYPE_3BYTE_BGR); 81 | for (int y = 0; y < 64; y++) { 82 | for (int x = 0; x < 64; x++) { 83 | int rgb = inputImage.getRGB(x * 2, y * 2); 84 | resizedImage.setRGB(x, y, rgb); 85 | } 86 | } 87 | 88 | // Encode the 64x64 image into the 128x128 map 89 | BufferedImage mapImage = new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB); 90 | for (int y = 0; y < 64; y++) { 91 | for (int x = 0; x < 64; x++) { 92 | int rgb = resizedImage.getRGB(x, y); 93 | int r = (rgb >> 16) & 0xFF; 94 | int g = (rgb >> 8) & 0xFF; 95 | int b = rgb & 0xFF; 96 | 97 | int b1 = b & 0x7F; 98 | int msb1 = b >> 7; 99 | 100 | int b2 = g & 0x7F; 101 | int msb2 = g >> 7; 102 | 103 | int b3 = r & 0x7F; 104 | int msb3 = r >> 7; 105 | 106 | int b4 = (msb3 << 2) | (msb2 << 1) | msb1; 107 | 108 | mapImage.setRGB(x * 2, y * 2, mapColorPalette[b1]); 109 | mapImage.setRGB(x * 2 + 1, y * 2, mapColorPalette[b2]); 110 | mapImage.setRGB(x * 2, y * 2 + 1, mapColorPalette[b3]); 111 | mapImage.setRGB(x * 2 + 1, y * 2 + 1, mapColorPalette[b4]); 112 | } 113 | } 114 | 115 | return mapImage; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/ParticleLib.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming; 2 | 3 | import codes.shiftmc.streaming.data.Connections; 4 | import codes.shiftmc.streaming.data.Keypoint; 5 | import codes.shiftmc.streaming.data.Pose; 6 | import codes.shiftmc.streaming.renderer.particle.ParticleData; 7 | import net.minestom.server.color.Color; 8 | import net.minestom.server.coordinate.Vec; 9 | import net.minestom.server.instance.Instance; 10 | import net.minestom.server.network.packet.server.play.ParticlePacket; 11 | import net.minestom.server.particle.Particle; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import java.util.List; 16 | 17 | public class ParticleLib { 18 | 19 | private static final Logger LOGGER = LoggerFactory.getLogger(ParticleLib.class); 20 | 21 | public static void drawLine(Vec start, Vec end, ParticleData data, Instance instance) { 22 | Vec direction = start.sub(end); 23 | for (double i = 0; i < start.distance(end); i += 0.1) { 24 | Vec pos = start.add(direction.normalize().mul(i)); 25 | 26 | instance.sendGroupedPacket(new ParticlePacket( 27 | Particle.DUST 28 | .withColor(data.color()) 29 | .withScale(0.25f), 30 | pos, 31 | Vec.ZERO, 32 | 0f, 33 | 0 34 | )); 35 | } 36 | } 37 | 38 | public static void drawKeypoints3D(List poses, Instance instance, Vec offset) { 39 | for (Pose pose : poses) { 40 | var keypoints3D = pose.getKeypoints3D(); 41 | var connections = new Connections().getConnections(); 42 | 43 | connections.forEach(connection -> { 44 | // var from = keypoints3D.stream().filter(keypoint -> keypoint.getName().equals(connection.getFrom())).findFirst(); 45 | // var to = keypoints3D.stream().filter(keypoint -> keypoint.getName().equals(connection.getTo())).findFirst(); 46 | // 47 | // if (from.isEmpty() || to.isEmpty()) { 48 | // LOGGER.error("Connection not found: {} -> {}", connection.getFrom(), connection.getTo()); 49 | // return; 50 | // } 51 | // 52 | // Vec fromPos = from.get().getPosition().mul(-1).add(offset); 53 | // Vec toPos = to.get().getPosition().mul(-1).add(offset); 54 | // 55 | // drawLine(fromPos, toPos, new ParticleData(new Color(0, 255, 255), 0.25f, Vec.ZERO), instance); 56 | 57 | var names = keypoints3D.stream().map(Keypoint::getName).toList(); 58 | System.out.println(names); 59 | }); 60 | 61 | keypoints3D.forEach(keypoint -> { 62 | Vec pos = keypoint.getPosition().mul(-1).add(offset); 63 | spawnParticle(pos, new Color(0, 255, 255), instance); 64 | }); 65 | } 66 | } 67 | 68 | private static void spawnParticle(Vec pos, Color color, Instance instance) { 69 | instance.sendGroupedPacket(new ParticlePacket( 70 | Particle.DUST 71 | .withColor(color) 72 | .withScale(0.4f), 73 | pos, 74 | Vec.ZERO, 75 | 0f, 76 | 0 77 | )); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/client/BlazeNetClient.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.client; 2 | 3 | import codes.shiftmc.streaming.ParticleLib; 4 | import codes.shiftmc.streaming.data.BlazeNetData; 5 | import codes.shiftmc.streaming.data.Pose; 6 | import net.minestom.server.coordinate.Pos; 7 | import net.minestom.server.coordinate.Vec; 8 | import net.minestom.server.instance.Instance; 9 | import net.minestom.server.network.packet.server.play.ParticlePacket; 10 | import net.minestom.server.particle.Particle; 11 | 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.concurrent.Executors; 15 | import java.util.concurrent.ScheduledExecutorService; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | public class BlazeNetClient implements Clients { 19 | 20 | private final Instance instance; 21 | private final Vec start; 22 | 23 | private final HashMap> poses = new HashMap<>(); 24 | 25 | public BlazeNetClient(Instance instance, Vec start) { 26 | this.instance = instance; 27 | this.start = start; 28 | } 29 | 30 | @Override 31 | public void start() { 32 | ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); 33 | scheduler.scheduleAtFixedRate(this::render, 50, 50, TimeUnit.MILLISECONDS); 34 | } 35 | 36 | public void update(BlazeNetData data) { 37 | poses.put(data.getPlayerName(), data.getPoses()); 38 | } 39 | 40 | public void render() { 41 | poses.forEach((playerName, poses) -> { 42 | ParticleLib.drawKeypoints3D(poses, instance, start); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/client/BufferedImageClient.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.client; 2 | 3 | import codes.shiftmc.streaming.renderer.Renderers; 4 | 5 | import java.awt.image.BufferedImage; 6 | 7 | public class BufferedImageClient implements Clients { 8 | 9 | private final Renderers renderer; 10 | 11 | public BufferedImageClient(Renderers renderer) { 12 | this.renderer = renderer; 13 | } 14 | 15 | @Override 16 | public void start() { 17 | 18 | } 19 | 20 | public void render(BufferedImage image) { 21 | renderer.render(image); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/client/Clients.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.client; 2 | 3 | public interface Clients { 4 | 5 | void start(); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/client/LocalClient.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.client; 2 | 3 | 4 | import codes.shiftmc.streaming.renderer.Renderers; 5 | 6 | import java.awt.*; 7 | import java.util.concurrent.Executors; 8 | import java.util.concurrent.ScheduledExecutorService; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | public class LocalClient implements Clients { 12 | 13 | private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); 14 | private final Renderers renderer; 15 | 16 | public LocalClient(Renderers renderer) { 17 | this.renderer = renderer; 18 | } 19 | 20 | @Override 21 | public void start() { 22 | try { 23 | var robot = new Robot(); 24 | var screenDevices = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices(); 25 | var screenDevice = screenDevices[1]; // Change the index to select the desired monitor 26 | var bounds = screenDevice.getDefaultConfiguration().getBounds(); 27 | var rectangle = new Rectangle(bounds); 28 | 29 | scheduler.scheduleAtFixedRate(() -> frame(robot, rectangle), 0, 50, TimeUnit.MILLISECONDS); 30 | } catch (Exception e) { 31 | e.printStackTrace(); 32 | } 33 | } 34 | 35 | private void frame(Robot robot, Rectangle rectangle) { 36 | var image = robot.createScreenCapture(rectangle); 37 | renderer.render(image); 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/client/RTMPClient.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.client; 2 | 3 | import codes.shiftmc.streaming.renderer.Renderers; 4 | import org.jetbrains.annotations.NotNull; 5 | import uk.co.caprica.vlcj.factory.MediaPlayerFactory; 6 | import uk.co.caprica.vlcj.player.base.MediaPlayer; 7 | import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer; 8 | import uk.co.caprica.vlcj.player.embedded.videosurface.VideoSurface; 9 | import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormat; 10 | import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormatCallback; 11 | import uk.co.caprica.vlcj.player.embedded.videosurface.callback.RenderCallbackAdapter; 12 | import uk.co.caprica.vlcj.player.embedded.videosurface.callback.format.RV32BufferFormat; 13 | 14 | import java.awt.image.BufferedImage; 15 | import java.nio.ByteBuffer; 16 | 17 | public class RTMPClient implements Clients { 18 | 19 | private final Renderers renderers; 20 | private final String rmtpUrl; 21 | 22 | private RenderCallbackAdapter renderCallback; 23 | private BufferFormatCallback bufferCallback; 24 | private VideoSurface videoSurface; 25 | 26 | private final MediaPlayerFactory mediaPlayerFactory; 27 | private final EmbeddedMediaPlayer mediaPlayer; 28 | private final int[] videoBuffer; 29 | private final int frameRate; 30 | 31 | public RTMPClient(Renderers renderers, String rmtpUrl, int frameRate) { 32 | this.mediaPlayerFactory = new MediaPlayerFactory("--no-audio"); 33 | this.mediaPlayer = mediaPlayerFactory.mediaPlayers().newEmbeddedMediaPlayer(); 34 | 35 | this.renderers = renderers; 36 | this.rmtpUrl = rmtpUrl; 37 | this.videoBuffer = new int[1920 * 1080]; 38 | this.frameRate = frameRate; 39 | 40 | setupVideoSurface(); 41 | } 42 | 43 | private void setupVideoSurface() { 44 | // Create a render callback adapter using the buffer 45 | renderCallback = new RenderCallbackAdapter(videoBuffer) { 46 | private long lastFrameTime = 0; 47 | @Override 48 | protected void onDisplay(MediaPlayer mediaPlayer, int[] buffer) { 49 | if (frameRate != -1) { 50 | long currentTime = System.currentTimeMillis(); 51 | float frameDuration = (float) 1000 / frameRate; 52 | if (currentTime - lastFrameTime < frameDuration) return; // Skip the frame it is too soon 53 | 54 | lastFrameTime = currentTime; 55 | } 56 | 57 | // Convert the raw RGB data to a BufferedImage 58 | BufferedImage image = new BufferedImage(1920, 1080, BufferedImage.TYPE_INT_RGB); 59 | image.setRGB(0, 0, 1920, 1080, buffer, 0, 1920); 60 | 61 | renderers.render(image); 62 | } 63 | }; 64 | 65 | // Create an anonymous inner class for BufferFormatCallback 66 | videoSurface = getVideoSurface(renderCallback); 67 | mediaPlayer.videoSurface().set(videoSurface); 68 | mediaPlayer.videoSurface().attachVideoSurface(); 69 | } 70 | 71 | private @NotNull VideoSurface getVideoSurface(RenderCallbackAdapter renderCallback) { 72 | bufferCallback = new BufferFormatCallback() { 73 | @Override 74 | public BufferFormat getBufferFormat(int sourceWidth, int sourceHeight) { 75 | return new RV32BufferFormat(1920, 1080); 76 | } 77 | 78 | @Override 79 | public void allocatedBuffers(ByteBuffer[] buffers) {} 80 | }; 81 | 82 | return mediaPlayerFactory.videoSurfaces().newVideoSurface(bufferCallback, renderCallback, true); 83 | } 84 | 85 | @Override 86 | public void start() { 87 | mediaPlayer.media().play(rmtpUrl); 88 | } 89 | 90 | public void stop() { 91 | mediaPlayer.controls().stop(); 92 | mediaPlayer.release(); 93 | mediaPlayerFactory.release(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/client/UrlClient.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.client; 2 | 3 | import codes.shiftmc.streaming.renderer.Renderers; 4 | 5 | import javax.imageio.ImageIO; 6 | import java.awt.image.BufferedImage; 7 | import java.io.IOException; 8 | import java.net.URI; 9 | import java.util.function.Predicate; 10 | import java.util.regex.Pattern; 11 | 12 | public class UrlClient implements Clients { 13 | 14 | private static final Predicate URL_TEST = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]").asPredicate(); 15 | private Renderers renderer; 16 | 17 | public UrlClient(Renderers renderer) { 18 | this.renderer = renderer; 19 | } 20 | 21 | @Override 22 | public void start() { 23 | 24 | } 25 | 26 | public void render(String url) throws IOException { 27 | if (!URL_TEST.test(url)) { 28 | throw new RuntimeException("Invalid URL: " + url); 29 | } 30 | 31 | BufferedImage image = ImageIO.read(URI.create(url).toURL()); 32 | renderer.render(image); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/data/BlazeNetData.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.data; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | import java.util.List; 7 | 8 | public class BlazeNetData { 9 | private String model; 10 | private String playerName; 11 | private List poses; 12 | 13 | @JsonCreator 14 | public BlazeNetData( 15 | @JsonProperty("model") String model, 16 | @JsonProperty("playerName") String playerName, 17 | @JsonProperty("poses") List poses 18 | ) { 19 | this.model = model; 20 | this.playerName = playerName; 21 | this.poses = poses; 22 | } 23 | 24 | // Getters and setters 25 | public String getModel() { return model; } 26 | public void setModel(String model) { this.model = model; } 27 | 28 | public String getPlayerName() { return playerName; } 29 | public void setPlayerName(String playerName) { this.playerName = playerName; } 30 | 31 | public List getPoses() { return poses; } 32 | public void setPoses(List poses) { this.poses = poses; } 33 | } -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/data/Connection.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.data; 2 | 3 | public class Connection { 4 | 5 | private final String from; 6 | private final String to; 7 | 8 | public Connection(String from, String to) { 9 | this.from = from; 10 | this.to = to; 11 | } 12 | 13 | public String getFrom() { 14 | return from; 15 | } 16 | 17 | public String getTo() { 18 | return to; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/data/Connections.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.data; 2 | 3 | import java.util.ArrayList; 4 | 5 | public class Connections { 6 | public static final ArrayList connections = new ArrayList<>(); 7 | 8 | public Connections() { 9 | connections.add(new Connection("left_shoulder", "left_hip")); 10 | connections.add(new Connection("right_shoulder", "right_hip")); 11 | // connections.add(new Connection("left_hip", "left_knee")); 12 | // connections.add(new Connection("left_knee", "left_ankle")); 13 | // connections.add(new Connection("left_ankle", "left_heel")); 14 | // connections.add(new Connection("left_heel", "left_foot_index")); 15 | // connections.add(new Connection("left_foot_index", "left_ankle")); 16 | // connections.add(new Connection("right_shoulder", "left_shoulder")); 17 | // connections.add(new Connection("right_shoulder", "right_hip")); 18 | // connections.add(new Connection("right_hip", "left_hip")); 19 | // connections.add(new Connection("right_hip", "right_knee")); 20 | // connections.add(new Connection("right_knee", "right_ankle")); 21 | // connections.add(new Connection("right_ankle", "right_heel")); 22 | // connections.add(new Connection("right_heel", "right_foot_index")); 23 | } 24 | 25 | public ArrayList getConnections() { 26 | return connections; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/data/Keypoint.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.data; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import net.minestom.server.coordinate.Pos; 5 | import net.minestom.server.coordinate.Vec; 6 | 7 | public class Keypoint { 8 | @JsonProperty("x") 9 | private double x; 10 | 11 | @JsonProperty("y") 12 | private double y; 13 | 14 | @JsonProperty("z") 15 | private double z; 16 | 17 | @JsonProperty("score") 18 | private double score; 19 | 20 | @JsonProperty("name") 21 | private String name; 22 | 23 | // Getters and setters 24 | public double getX() { return x; } 25 | public void setX(double x) { this.x = x; } 26 | 27 | public double getY() { return y; } 28 | public void setY(double y) { this.y = y; } 29 | 30 | public double getZ() { return z; } 31 | public void setZ(double z) { this.z = z; } 32 | 33 | public double getScore() { return score; } 34 | public void setScore(double score) { this.score = score; } 35 | 36 | public String getName() { return name; } 37 | public void setName(String name) { this.name = name; } 38 | 39 | public Vec getPosition() { 40 | return new Vec(x, y, z); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/data/Pose.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.data; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | import java.util.List; 6 | 7 | public class Pose { 8 | @JsonProperty("keypoints") 9 | private List keypoints2D; 10 | 11 | @JsonProperty("keypoints3D") 12 | private List keypoints3D; 13 | 14 | private double score; 15 | 16 | // Getters and setters 17 | public List getKeypoints2D() { return keypoints2D; } 18 | public void setKeypoints2D(List keypoints2D) { this.keypoints2D = keypoints2D; } 19 | 20 | public List getKeypoints3D() { return keypoints3D; } 21 | public void setKeypoints3D(List keypoints3D) { this.keypoints3D = keypoints3D; } 22 | 23 | public double getScore() { 24 | return score; 25 | } 26 | 27 | public void setScore(double score) { 28 | this.score = score; 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/renderer/Renderers.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.renderer; 2 | 3 | import java.awt.image.BufferedImage; 4 | 5 | public interface Renderers { 6 | 7 | void render(BufferedImage image); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/renderer/map/ImageBundleRenderer.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.renderer.map; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public class ImageBundleRenderer { 8 | 9 | private final ArrayList cachedImages = new ArrayList<>(); 10 | private final List mapRenderers = new ArrayList<>(); 11 | 12 | private final List ids = new ArrayList<>(); 13 | 14 | private int currentIndex = 0; 15 | 16 | public ImageBundleRenderer( 17 | List images, 18 | List mapRenderers 19 | ) { 20 | cachedImages.addAll(images); 21 | this.mapRenderers.addAll(mapRenderers); 22 | 23 | ids.add((int) System.currentTimeMillis() + 10000); 24 | for (int i = 0; i < mapRenderers.size(); i++) { 25 | final var renderer = mapRenderers.get(i); 26 | renderer.render(images.get(i)); 27 | ids.add(renderer.getId()); 28 | } 29 | ids.add((int) System.currentTimeMillis() + 100000); 30 | } 31 | 32 | public void step(int number) { 33 | if (currentIndex + number >= cachedImages.size()) currentIndex = 0; 34 | else if (currentIndex + number < 0) return; 35 | else currentIndex += number; 36 | 37 | for (final MapRenderer renderer : mapRenderers) { 38 | for (int i = 0; i < renderer.getItemMapFrames().length; i++) { 39 | final var item = renderer.getItemMapFrames()[i]; 40 | for (int j = 0; j < item.length; j++) { 41 | final var k = renderer.getItemMapFrames()[i][j]; 42 | k.changeId(MapRenderer.generateUniqueId(ids.get(i), i, j)); 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/renderer/map/ItemMapFrame.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.renderer.map; 2 | 3 | import net.minestom.server.coordinate.Pos; 4 | import net.minestom.server.entity.Entity; 5 | import net.minestom.server.entity.EntityType; 6 | import net.minestom.server.entity.metadata.other.ItemFrameMeta; 7 | import net.minestom.server.instance.Instance; 8 | import net.minestom.server.item.ItemComponent; 9 | import net.minestom.server.item.ItemStack; 10 | import net.minestom.server.item.Material; 11 | 12 | public class ItemMapFrame extends Entity { 13 | public ItemMapFrame(int mapId, Instance instance, Pos position, ItemFrameMeta.Orientation orientation) { 14 | super(EntityType.ITEM_FRAME); 15 | 16 | var meta = (ItemFrameMeta) getEntityMeta(); 17 | meta.setNotifyAboutChanges(false); 18 | changeId(mapId); 19 | setInstance(instance, position); 20 | meta.setOrientation(orientation); 21 | meta.setNotifyAboutChanges(true); 22 | } 23 | 24 | public void changeId(int mapId) { 25 | var meta = (ItemFrameMeta) getEntityMeta(); 26 | var itemStack = ItemStack.of(Material.FILLED_MAP).builder() 27 | .set(ItemComponent.MAP_ID, mapId) 28 | .build(); 29 | 30 | meta.setItem(itemStack); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/renderer/map/MapRenderer.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.renderer.map; 2 | 3 | import codes.shiftmc.streaming.ImageProcessor; 4 | import codes.shiftmc.streaming.renderer.Renderers; 5 | import net.minestom.server.coordinate.Vec; 6 | import net.minestom.server.entity.Player; 7 | import net.minestom.server.entity.metadata.other.ItemFrameMeta; 8 | import net.minestom.server.event.instance.AddEntityToInstanceEvent; 9 | import net.minestom.server.instance.Instance; 10 | import net.minestom.server.map.MapColors; 11 | import net.minestom.server.map.framebuffers.DirectFramebuffer; 12 | import net.minestom.server.network.packet.server.play.BundlePacket; 13 | import net.minestom.server.network.packet.server.play.MapDataPacket; 14 | 15 | import java.awt.image.BufferedImage; 16 | import java.util.LinkedList; 17 | import java.util.concurrent.*; 18 | 19 | import static codes.shiftmc.streaming.renderer.particle.ParticleImage.resize; 20 | 21 | public class MapRenderer implements Renderers { 22 | 23 | private final Instance instance; 24 | private final int width; 25 | private final int height; 26 | private boolean bundlePacket; 27 | private boolean slowSend; 28 | private float frameRate; 29 | private float similarity; 30 | private final boolean arbEncode; 31 | 32 | private boolean destroyed = false; 33 | 34 | private int amount = 1; 35 | 36 | private final int id = Math.toIntExact(System.currentTimeMillis() % Integer.MAX_VALUE); 37 | 38 | private final BufferedImage[][] lastFrameBlocks; 39 | private final ItemMapFrame[][] itemMapFrames; 40 | private final LinkedList packets = new LinkedList<>(); 41 | 42 | private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); 43 | 44 | public MapRenderer(Vec pos, Instance instance, ItemFrameMeta.Orientation orientation, int width, int height, float frameRate, float similarity, boolean bundlePacket, boolean slowSend, boolean arbEncode) { 45 | this.instance = instance; 46 | this.width = width; 47 | this.height = height; 48 | this.bundlePacket = bundlePacket; 49 | this.slowSend = slowSend; 50 | this.frameRate = frameRate; 51 | this.similarity = similarity; 52 | this.arbEncode = arbEncode; 53 | 54 | assert width > 0 && height > 0; 55 | assert width % 128 == 0 && height % 128 == 0; 56 | 57 | int numBlocksX = width / 128; 58 | int numBlocksY = height / 128; 59 | lastFrameBlocks = new BufferedImage[numBlocksX][numBlocksY]; 60 | itemMapFrames = new ItemMapFrame[numBlocksX][numBlocksY]; 61 | 62 | int maxY = height / 128 - 1; 63 | for (int x = 0; x < numBlocksX; x++) { 64 | for (int y = 0; y < numBlocksY; y++) { 65 | // Spawn item frame 66 | itemMapFrames[x][y] = new ItemMapFrame(generateUniqueId(id, x, maxY - y), instance, pos.add(x, y, 0.0).asPosition(), orientation); 67 | } 68 | } 69 | 70 | instance.eventNode().addListener(AddEntityToInstanceEvent.class, event -> { 71 | if (event.getEntity() instanceof Player player) { 72 | for (MapDataPacket packet : packets) { 73 | if (packet == null) return; 74 | player.sendPacket(packet); 75 | } 76 | } 77 | }); 78 | } 79 | 80 | private long lastFrameTime = 0; 81 | 82 | @Override 83 | public void render(BufferedImage image) { 84 | render(image, id); 85 | } 86 | 87 | public void render(BufferedImage image, int id) { 88 | if (destroyed) return; 89 | 90 | if (frameRate != -1) { 91 | long currentTime = System.currentTimeMillis(); 92 | float frameDuration = 1000 / frameRate; 93 | if (currentTime - lastFrameTime < frameDuration) return; // Skip the frame it is too soon 94 | 95 | lastFrameTime = currentTime; 96 | } 97 | 98 | if (bundlePacket) { 99 | instance.sendGroupedPacket(new BundlePacket()); 100 | } 101 | 102 | // Break image in 128 103 | BufferedImage resize = resize(image, width, height); 104 | 105 | packets.clear(); 106 | for (int yBlock = 0; yBlock < height / 128; yBlock ++) { 107 | for (int xBlock = 0; xBlock < width / 128; xBlock ++) { 108 | BufferedImage currentBlock = resize.getSubimage(xBlock * 128, yBlock * 128, 128, 128); 109 | if (arbEncode) currentBlock = ImageProcessor.resizeAndEncodeImage(currentBlock); 110 | 111 | if (lastFrameBlocks[xBlock][yBlock] != null && isSimilar(lastFrameBlocks[xBlock][yBlock], currentBlock, similarity)) { 112 | continue; 113 | } 114 | 115 | if (lastFrameBlocks[xBlock][yBlock] != null) { 116 | this.lastFrameBlocks[xBlock][yBlock].flush(); 117 | } 118 | lastFrameBlocks[xBlock][yBlock] = currentBlock; 119 | 120 | var fb = new DirectFramebuffer(); 121 | // Loop 128x128 pixels of the image, based on the x and y locations 122 | for (int yy = 0; yy < 128; yy++) { 123 | for (int xx = 0; xx < 128; xx++) { 124 | int imageX = xBlock * 128 + xx; 125 | int imageY = yBlock * 128 + yy; 126 | 127 | int pixel = resize.getRGB(imageX, imageY); 128 | if (arbEncode) pixel = currentBlock.getRGB(imageX % 128, imageY % 128); 129 | MapColors.PreciseMapColor mappedColor = MapColors.closestColor(pixel); 130 | fb.set(xx, yy, mappedColor.getIndex()); 131 | } 132 | } 133 | 134 | MapDataPacket mapDataPacket = fb.preparePacket(generateUniqueId(id, xBlock, yBlock)); 135 | packets.add(mapDataPacket); 136 | } 137 | } 138 | 139 | if (!slowSend) { 140 | for (MapDataPacket packet : packets) { 141 | if (packet == null) return; 142 | instance.sendGroupedPacket(packet); 143 | } 144 | return; 145 | } 146 | 147 | executor.submit(() -> { 148 | var clone = new LinkedList<>(packets); 149 | try { 150 | for (int i = 0; i < clone.size(); i += amount) { 151 | for (int j = 0; j < amount && (i + j) < clone.size(); j++) { 152 | if (clone.get(i + j) == null) continue; 153 | instance.sendGroupedPacket(clone.get(i + j)); 154 | } 155 | 156 | try { 157 | Thread.sleep(50); 158 | } catch (InterruptedException e) { 159 | throw new RuntimeException(e); 160 | } 161 | } 162 | } finally { 163 | // Clear any remaining references 164 | System.gc(); 165 | clone.clear(); 166 | } 167 | }); 168 | 169 | if (bundlePacket) { 170 | instance.sendGroupedPacket(new BundlePacket()); 171 | } 172 | } 173 | 174 | public void destroy() { 175 | for (int i = 0; i < itemMapFrames.length; i++) { 176 | for (int j = 0; j < itemMapFrames[i].length; j++) { 177 | itemMapFrames[i][j].remove(); 178 | itemMapFrames[i][j] = null; 179 | } 180 | } 181 | 182 | for (int i = 0; i < this.lastFrameBlocks.length; i++) { 183 | for (int j = 0; j < this.lastFrameBlocks[i].length; j++) { 184 | this.lastFrameBlocks[i][j].flush(); 185 | this.lastFrameBlocks[i][j] = null; 186 | } 187 | } 188 | 189 | destroyed = true; 190 | executor.shutdown(); 191 | } 192 | 193 | private boolean isSimilar(BufferedImage img1, BufferedImage img2, float threshold) { 194 | if (img1.getWidth() != img2.getWidth() || img1.getHeight() != img2.getHeight()) { 195 | return false; 196 | } 197 | 198 | int width = img1.getWidth(); 199 | int height = img1.getHeight(); 200 | long diffCount = 0; 201 | long totalPixels = (long) width * height; 202 | 203 | for (int y = 0; y < height; y++) { 204 | for (int x = 0; x < width; x++) { 205 | int rgb1 = img1.getRGB(x, y); 206 | int rgb2 = img2.getRGB(x, y); 207 | if (rgb1 != rgb2) { 208 | diffCount++; 209 | } 210 | } 211 | } 212 | 213 | float similarity = 1.0f - (diffCount / (float)totalPixels); 214 | return similarity >= threshold; 215 | } 216 | 217 | public static int generateUniqueId(int baseId, int x, int y) { 218 | // Generate a unique ID using a combination of baseId, x, and y 219 | return baseId + (x * 1000) + y; // 1000 is an arbitrary number large enough to differentiate x and y 220 | } 221 | 222 | public void setSimilarity(float similarity) { 223 | this.similarity = similarity; 224 | } 225 | 226 | public float getSimilarity() { 227 | return similarity; 228 | } 229 | 230 | public void setFrameRate(float frameRate) { 231 | this.frameRate = frameRate; 232 | } 233 | 234 | public float getFrameRate() { 235 | return frameRate; 236 | } 237 | 238 | public void setSlowSend(boolean slowSend) { 239 | this.slowSend = slowSend; 240 | } 241 | 242 | public boolean getSlowSend() { 243 | return slowSend; 244 | } 245 | 246 | public void setBundlePacket(boolean bundlePacket) { 247 | this.bundlePacket = bundlePacket; 248 | } 249 | 250 | public boolean getBundlePacket() { 251 | return bundlePacket; 252 | } 253 | 254 | public void setAmount(int amount) { 255 | this.amount = amount; 256 | } 257 | 258 | public int getAmount() { 259 | return amount; 260 | } 261 | 262 | public int getId() { return id; } 263 | 264 | public ItemMapFrame[][] getItemMapFrames() { 265 | return itemMapFrames; 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/renderer/map/model/Map.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.renderer.map.model; 2 | 3 | import net.minestom.server.coordinate.Vec; 4 | import net.minestom.server.entity.metadata.other.ItemFrameMeta; 5 | import net.minestom.server.instance.Instance; 6 | 7 | public record Map( 8 | Instance instance, 9 | Vec vec, 10 | ItemFrameMeta.Orientation orientation 11 | ) { 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/renderer/particle/ParticleData.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.renderer.particle; 2 | 3 | import net.kyori.adventure.util.RGBLike; 4 | import net.minestom.server.coordinate.Vec; 5 | import net.minestom.server.network.packet.server.play.ParticlePacket; 6 | import net.minestom.server.particle.Particle; 7 | 8 | public record ParticleData( 9 | RGBLike color, 10 | float scale, 11 | Vec offset 12 | ) { 13 | public ParticlePacket createPacket(Vec pos) { 14 | return new ParticlePacket( 15 | Particle.DUST 16 | .withColor(color) 17 | .withScale(scale), 18 | pos.add(offset), 19 | Vec.ZERO, 20 | 0f, 21 | 0 22 | ); 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/renderer/particle/ParticleImage.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.renderer.particle; 2 | 3 | import codes.shiftmc.streaming.renderer.Renderers; 4 | import net.minestom.server.color.Color; 5 | import net.minestom.server.coordinate.Vec; 6 | import net.minestom.server.instance.Instance; 7 | import net.minestom.server.network.packet.server.play.BundlePacket; 8 | import net.minestom.server.network.packet.server.play.ParticlePacket; 9 | 10 | import java.awt.image.BufferedImage; 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | public class ParticleImage implements Renderers { 16 | 17 | private final Vec pos; 18 | private final Instance instance; 19 | private final int width; 20 | private final int height; 21 | private final float particleSize; 22 | private final float particleSpacing; 23 | 24 | public ParticleImage( 25 | Vec pos, 26 | Instance instance, 27 | int width, 28 | int height, 29 | float particleSize, 30 | float particleSpacing 31 | ) { 32 | this.pos = pos; 33 | this.instance = instance; 34 | this.width = width; 35 | this.height = height; 36 | this.particleSize = particleSize; 37 | this.particleSpacing = particleSpacing; 38 | } 39 | 40 | @Override 41 | public void render(BufferedImage image) { 42 | // Resize image to fit the width and height 43 | var resizedImage = resize(image); 44 | 45 | List particles = new ArrayList<>(); 46 | for (int y = 0; y < height; y++) { 47 | for (int x = 0; x < width; x++) { 48 | int pixel = resizedImage.getRGB(x, y); 49 | 50 | int red = (pixel >> 16) & 0xff; 51 | int green = (pixel >> 8) & 0xff; 52 | int blue = pixel & 0xff; 53 | 54 | var color = new Color(red, green, blue); 55 | var offset = new Vec(x, 0, y).mul(particleSpacing); 56 | 57 | particles.add(new ParticleData(color, particleSize, offset)); 58 | } 59 | } 60 | 61 | var packets = particles.stream() 62 | .map(particle -> particle.createPacket(pos)) 63 | .toArray(ParticlePacket[]::new); 64 | 65 | var packetChunks = splitPackets(packets, 4000); 66 | for (var chunk : packetChunks) { 67 | var bundlePacket = new BundlePacket(); 68 | instance.sendGroupedPacket(bundlePacket); 69 | for (ParticlePacket packet : chunk) { 70 | instance.sendGroupedPacket(packet); 71 | } 72 | instance.sendGroupedPacket(bundlePacket); 73 | } 74 | } 75 | 76 | private List splitPackets(ParticlePacket[] packets, int maxSize) { 77 | List packetChunks = new ArrayList<>(); 78 | for (int i = 0; i < packets.length; i += maxSize) { 79 | int end = Math.min(packets.length, i + maxSize); 80 | packetChunks.add(Arrays.copyOfRange(packets, i, end)); 81 | } 82 | return packetChunks; 83 | } 84 | 85 | private BufferedImage resize(BufferedImage image) { 86 | return resize(image, image.getWidth(), image.getHeight()); 87 | } 88 | 89 | public static BufferedImage resize(BufferedImage image, int width, int height) { 90 | var resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 91 | var graphics = resizedImage.createGraphics(); 92 | graphics.drawImage(image, 0, 0, width, height, null); 93 | graphics.dispose(); 94 | return resizedImage; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/codes/shiftmc/streaming/socket/SocketServer.java: -------------------------------------------------------------------------------- 1 | package codes.shiftmc.streaming.socket; 2 | 3 | import codes.shiftmc.streaming.client.BlazeNetClient; 4 | import codes.shiftmc.streaming.data.BlazeNetData; 5 | import com.corundumstudio.socketio.Configuration; 6 | import com.corundumstudio.socketio.SocketConfig; 7 | import com.corundumstudio.socketio.SocketIOServer; 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.util.Map; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | import java.util.concurrent.Executors; 15 | import java.util.concurrent.ScheduledExecutorService; 16 | import java.util.concurrent.TimeUnit; 17 | import java.util.concurrent.atomic.AtomicInteger; 18 | 19 | public class SocketServer { 20 | 21 | private final Logger LOGGER = LoggerFactory.getLogger(SocketServer.class); 22 | private final SocketIOServer server; 23 | private final Map nameCountMap = new ConcurrentHashMap<>(); 24 | private final int NAME_LIMIT = 20; 25 | 26 | public SocketServer( 27 | String hostname, 28 | int port, 29 | BlazeNetClient blazeNetClient 30 | ) { 31 | var config = new Configuration(); 32 | config.setHostname(hostname); 33 | config.setPort(port); 34 | 35 | var socketConfig = new SocketConfig(); 36 | socketConfig.setReuseAddress(true); 37 | 38 | config.setSocketConfig(socketConfig); 39 | 40 | server = new SocketIOServer(config); 41 | server.start(); 42 | 43 | server.addConnectListener(client -> { 44 | System.out.println("Client connected: " + client.getSessionId()); 45 | }); 46 | 47 | server.addEventListener("move::data", String.class, (client, rawData, ackRequest) -> { 48 | var mapper = new ObjectMapper(); 49 | // Detect what is the model before deserializing, can be blazepose, movenet and posenet 50 | var model = extract("model", rawData); 51 | var poses = extract("poses", rawData); 52 | 53 | if (model == null || poses == null) { 54 | LOGGER.debug("Invalid data received: {}", rawData); 55 | return; 56 | } 57 | 58 | switch (model.toLowerCase()) { 59 | case "blazepose" -> { 60 | try { 61 | var data = mapper.readValue(rawData, BlazeNetData.class); 62 | blazeNetClient.update(data); 63 | } catch (Exception e) { 64 | LOGGER.error("Error while deserializing data", e); 65 | } 66 | } 67 | 68 | case "movenet", "posenet" -> { 69 | throw new UnsupportedOperationException("Model not supported"); 70 | } 71 | 72 | default -> { 73 | LOGGER.debug("Model {} not supported", model); 74 | } 75 | } 76 | }); 77 | 78 | ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); 79 | scheduler.scheduleAtFixedRate(nameCountMap::clear, 1, 1, TimeUnit.SECONDS); 80 | } 81 | 82 | private String extract(String field, String json) { 83 | String key = "\"" + field + "\":"; 84 | int startIndex = json.indexOf(key) + key.length(); 85 | char nextChar = json.charAt(startIndex); 86 | 87 | if (nextChar == '\"') { 88 | startIndex++; 89 | int endIndex = json.indexOf("\"", startIndex); 90 | return json.substring(startIndex, endIndex); 91 | } 92 | 93 | if (nextChar == '[') { 94 | int endIndex = json.indexOf("]", startIndex) + 1; 95 | return json.substring(startIndex, endIndex); 96 | } 97 | 98 | return null; 99 | } 100 | } --------------------------------------------------------------------------------