├── .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 |
5 |
6 |
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 | 
52 | Sem Arb
53 | 
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 | }
--------------------------------------------------------------------------------