├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── themes.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ └── activity_main.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── assets │ │ │ ├── table_s1 │ │ │ ├── table_s2 │ │ │ ├── table_s3 │ │ │ ├── table_s4 │ │ │ ├── table_s10 │ │ │ ├── info-response.xml │ │ │ ├── table_s5 │ │ │ ├── table_s6 │ │ │ ├── table_s7 │ │ │ ├── table_s8 │ │ │ └── table_s9 │ │ ├── java │ │ │ └── com │ │ │ │ ├── cjx │ │ │ │ └── airplayjavademo │ │ │ │ │ ├── model │ │ │ │ │ ├── PCMPacket.java │ │ │ │ │ └── NALPacket.java │ │ │ │ │ ├── MyApplication.java │ │ │ │ │ ├── Logger.java │ │ │ │ │ ├── PlaySurfaceView.java │ │ │ │ │ ├── player │ │ │ │ │ ├── AudioPlayer.java │ │ │ │ │ └── VideoPlayer.java │ │ │ │ │ └── MainActivity.java │ │ │ │ └── github │ │ │ │ └── serezhka │ │ │ │ ├── jap2lib │ │ │ │ ├── rtsp │ │ │ │ │ ├── MediaStreamInfo.java │ │ │ │ │ ├── VideoStreamInfo.java │ │ │ │ │ └── AudioStreamInfo.java │ │ │ │ ├── FairPlayAudioDecryptor.java │ │ │ │ ├── FairPlayVideoDecryptor.java │ │ │ │ ├── SapHash.java │ │ │ │ ├── AirPlayBonjour.java │ │ │ │ ├── ModifiedMD5.java │ │ │ │ ├── FairPlay.java │ │ │ │ ├── AirPlay.java │ │ │ │ ├── Pairing.java │ │ │ │ ├── RTSP.java │ │ │ │ ├── OmgHaxConst.java │ │ │ │ └── OmgHax.java │ │ │ │ └── jap2server │ │ │ │ ├── AirplayDataConsumer.java │ │ │ │ ├── internal │ │ │ │ ├── handler │ │ │ │ │ ├── session │ │ │ │ │ │ ├── SessionManager.java │ │ │ │ │ │ └── Session.java │ │ │ │ │ ├── audio │ │ │ │ │ │ ├── AudioControlHandler.java │ │ │ │ │ │ ├── AudioPacket.java │ │ │ │ │ │ └── AudioHandler.java │ │ │ │ │ ├── control │ │ │ │ │ │ ├── HeartBeatHandler.java │ │ │ │ │ │ ├── FairPlayHandler.java │ │ │ │ │ │ ├── PairingHandler.java │ │ │ │ │ │ ├── ControlHandler.java │ │ │ │ │ │ └── RTSPHandler.java │ │ │ │ │ └── mirroring │ │ │ │ │ │ ├── MirroringHeader.java │ │ │ │ │ │ └── MirroringHandler.java │ │ │ │ ├── AudioReceiver.java │ │ │ │ ├── AudioControlServer.java │ │ │ │ ├── MirroringReceiver.java │ │ │ │ └── ControlServer.java │ │ │ │ └── AirPlayServer.java │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── cjx │ │ │ └── airplayjavademo │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── cjx │ │ └── airplayjavademo │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── README.md ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── gradle.properties ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AirplayAndroidReceiver 2 | java airplay 安卓端实现,优化中,站在巨人的肩膀上 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "AirplayJavaDemo" 2 | include ':app' 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AirplayJavaDemo 3 | -------------------------------------------------------------------------------- /app/src/main/assets/table_s1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/app/src/main/assets/table_s1 -------------------------------------------------------------------------------- /app/src/main/assets/table_s2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/app/src/main/assets/table_s2 -------------------------------------------------------------------------------- /app/src/main/assets/table_s3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/app/src/main/assets/table_s3 -------------------------------------------------------------------------------- /app/src/main/assets/table_s4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/app/src/main/assets/table_s4 -------------------------------------------------------------------------------- /app/src/main/assets/table_s10: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/app/src/main/assets/table_s10 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caijianxiong/AirplayAndroidReceiver/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/java/com/cjx/airplayjavademo/model/PCMPacket.java: -------------------------------------------------------------------------------- 1 | package com.cjx.airplayjavademo.model; 2 | 3 | public class PCMPacket { 4 | public byte[] data; 5 | public long pts; 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/cjx/airplayjavademo/model/NALPacket.java: -------------------------------------------------------------------------------- 1 | package com.cjx.airplayjavademo.model; 2 | 3 | public class NALPacket { 4 | public byte[] nalData = null; 5 | public int nalType = 0; 6 | public long pts = 0; 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2lib/rtsp/MediaStreamInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2lib.rtsp; 2 | 3 | public interface MediaStreamInfo { 4 | 5 | StreamType getStreamType(); 6 | 7 | enum StreamType { 8 | AUDIO, 9 | VIDEO 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jul 02 20:26:49 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | /.idea/ 17 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/AirplayDataConsumer.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server; 2 | 3 | import com.github.serezhka.jap2lib.rtsp.AudioStreamInfo; 4 | import com.github.serezhka.jap2lib.rtsp.VideoStreamInfo; 5 | 6 | public interface AirplayDataConsumer { 7 | 8 | void onVideo(byte[] video); 9 | 10 | void onVideoFormat(VideoStreamInfo videoStreamInfo); 11 | 12 | void onAudio(byte[] audio); 13 | 14 | void onAudioFormat(AudioStreamInfo audioInfo); 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/cjx/airplayjavademo/MyApplication.java: -------------------------------------------------------------------------------- 1 | package com.cjx.airplayjavademo; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | public class MyApplication extends Application { 7 | 8 | private static Context appContext; 9 | 10 | public static Context getAppContext() { 11 | return appContext; 12 | } 13 | 14 | @Override 15 | public void onCreate() { 16 | super.onCreate(); 17 | appContext=this; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/test/java/com/cjx/airplayjavademo/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.cjx.airplayjavademo; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2lib/rtsp/VideoStreamInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2lib.rtsp; 2 | 3 | public class VideoStreamInfo implements MediaStreamInfo { 4 | 5 | private final String streamConnectionID; 6 | 7 | public VideoStreamInfo(String streamConnectionID) { 8 | this.streamConnectionID = streamConnectionID; 9 | } 10 | 11 | @Override 12 | public StreamType getStreamType() { 13 | return StreamType.VIDEO; 14 | } 15 | 16 | public String getStreamConnectionID() { 17 | return streamConnectionID; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/handler/session/SessionManager.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal.handler.session; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public class SessionManager { 7 | 8 | private final Map sessions = new HashMap<>(); 9 | 10 | public Session getSession(String activeRemote) { 11 | synchronized (sessions) { 12 | Session session; 13 | if ((session = sessions.get(activeRemote)) == null) { 14 | session = new Session(); 15 | sessions.put(activeRemote, session); 16 | } 17 | return session; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/cjx/airplayjavademo/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.cjx.airplayjavademo; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("com.cjx.airplayjavademo", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/cjx/airplayjavademo/Logger.java: -------------------------------------------------------------------------------- 1 | package com.cjx.airplayjavademo; 2 | 3 | import android.util.Log; 4 | 5 | public class Logger { 6 | 7 | public static void d(String tag, String format, Object... objects) { 8 | Log.d(tag, String.format(format, objects)); 9 | } 10 | 11 | public static void i(String tag, String format, Object... objects) { 12 | Log.i(tag, String.format(format, objects)); 13 | } 14 | 15 | public static void i(String tag, String msg) { 16 | Log.i(tag, msg); 17 | } 18 | 19 | public static void w(String tag, String format, Object... objects) { 20 | Log.w(tag, String.format(format, objects)); 21 | } 22 | 23 | 24 | public static void e(String tag, String format, Object... objects) { 25 | Log.e(tag, String.format(format, objects)); 26 | } 27 | 28 | public static void e(String tag, String msg, Throwable throwable) { 29 | Log.e(tag, msg, throwable); 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/handler/audio/AudioControlHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal.handler.audio; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.SimpleChannelInboundHandler; 6 | import io.netty.channel.socket.DatagramPacket; 7 | //import org.slf4j.Logger; 8 | //import org.slf4j.LoggerFactory; 9 | 10 | public class AudioControlHandler extends SimpleChannelInboundHandler { 11 | 12 | // private static final Logger log = LoggerFactory.getLogger(AudioControlHandler.class); 13 | 14 | @Override 15 | protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) { 16 | ByteBuf content = msg.content(); 17 | int contentLength = content.readableBytes(); 18 | byte[] contentBytes = new byte[contentLength]; 19 | content.readBytes(contentBytes); 20 | int type = contentBytes[1] & ~0x80; 21 | // log.debug("Got audio control packet, type: {}, length: {}", type, contentLength); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/handler/control/HeartBeatHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal.handler.control; 2 | 3 | import com.github.serezhka.jap2server.internal.handler.session.Session; 4 | import com.github.serezhka.jap2server.internal.handler.session.SessionManager; 5 | import io.netty.channel.ChannelHandler; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 8 | import io.netty.handler.codec.http.FullHttpRequest; 9 | 10 | @ChannelHandler.Sharable 11 | public class HeartBeatHandler extends ControlHandler { 12 | 13 | public HeartBeatHandler(SessionManager sessionManager) { 14 | super(sessionManager); 15 | } 16 | 17 | @Override 18 | protected boolean handleRequest(ChannelHandlerContext ctx, Session session, FullHttpRequest request) { 19 | if (request.uri().equals("/feedback")) { 20 | DefaultFullHttpResponse response = createResponseForRequest(request); 21 | return sendResponse(ctx, request, response); 22 | } 23 | return false; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 10 | 11 | 12 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2lib/FairPlayAudioDecryptor.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2lib; 2 | 3 | import javax.crypto.Cipher; 4 | import javax.crypto.spec.IvParameterSpec; 5 | import javax.crypto.spec.SecretKeySpec; 6 | import java.security.MessageDigest; 7 | import java.util.Arrays; 8 | 9 | class FairPlayAudioDecryptor { 10 | 11 | private final byte[] aesIV; 12 | private final byte[] eaesKey; 13 | 14 | private final Cipher aesCbcDecrypt; 15 | 16 | FairPlayAudioDecryptor(byte[] aesKey, byte[] aesIV, byte[] sharedSecret) throws Exception { 17 | this.aesIV = aesIV; 18 | 19 | MessageDigest sha512Digest = MessageDigest.getInstance("SHA-512"); 20 | sha512Digest.update(aesKey); 21 | sha512Digest.update(sharedSecret); 22 | eaesKey = Arrays.copyOfRange(sha512Digest.digest(), 0, 16); 23 | 24 | aesCbcDecrypt = Cipher.getInstance("AES/CBC/NoPadding"); 25 | } 26 | 27 | void decrypt(byte[] audio, int audioLength) throws Exception { 28 | initAesCbcCipher(); 29 | aesCbcDecrypt.update(audio, 0, audioLength / 16 * 16, audio, 0); 30 | } 31 | 32 | private void initAesCbcCipher() throws Exception { 33 | aesCbcDecrypt.init(Cipher.DECRYPT_MODE, new SecretKeySpec(eaesKey, "AES"), new IvParameterSpec(aesIV)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/AirPlayServer.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server; 2 | 3 | import com.github.serezhka.jap2lib.AirPlayBonjour; 4 | import com.github.serezhka.jap2server.internal.ControlServer; 5 | 6 | public class AirPlayServer { 7 | 8 | private final AirPlayBonjour airPlayBonjour; 9 | private final AirplayDataConsumer airplayDataConsumer; 10 | private final ControlServer controlServer; 11 | 12 | private final String serverName; 13 | private final int airPlayPort; 14 | private final int airTunesPort; 15 | 16 | public AirPlayServer(String serverName, int airPlayPort, int airTunesPort, 17 | AirplayDataConsumer airplayDataConsumer) { 18 | this.serverName = serverName; 19 | airPlayBonjour = new AirPlayBonjour(serverName); 20 | this.airPlayPort = airPlayPort; 21 | this.airTunesPort = airTunesPort; 22 | this.airplayDataConsumer = airplayDataConsumer; 23 | controlServer = new ControlServer(airPlayPort, airTunesPort, airplayDataConsumer); 24 | } 25 | 26 | public void start() throws Exception { 27 | airPlayBonjour.start(airPlayPort, airTunesPort); 28 | new Thread(controlServer).start(); 29 | } 30 | 31 | public void stop() { 32 | airPlayBonjour.stop(); 33 | } 34 | 35 | // TODO On client connected / disconnected 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/handler/control/FairPlayHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal.handler.control; 2 | 3 | import com.github.serezhka.jap2server.internal.handler.session.Session; 4 | import com.github.serezhka.jap2server.internal.handler.session.SessionManager; 5 | import io.netty.buffer.ByteBufInputStream; 6 | import io.netty.buffer.ByteBufOutputStream; 7 | import io.netty.channel.ChannelHandler; 8 | import io.netty.channel.ChannelHandlerContext; 9 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 10 | import io.netty.handler.codec.http.FullHttpRequest; 11 | 12 | @ChannelHandler.Sharable 13 | public class FairPlayHandler extends ControlHandler { 14 | 15 | public FairPlayHandler(SessionManager sessionManager) { 16 | super(sessionManager); 17 | } 18 | 19 | @Override 20 | protected boolean handleRequest(ChannelHandlerContext ctx, Session session, FullHttpRequest request) throws Exception { 21 | String uri = request.uri(); 22 | if ("/fp-setup".equals(uri)) { 23 | DefaultFullHttpResponse response = createResponseForRequest(request); 24 | session.getAirPlay().fairPlaySetup(new ByteBufInputStream(request.content()), 25 | new ByteBufOutputStream(response.content())); 26 | return sendResponse(ctx, request, response); 27 | } 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/cjx/airplayjavademo/PlaySurfaceView.java: -------------------------------------------------------------------------------- 1 | package com.cjx.airplayjavademo; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.SurfaceView; 6 | 7 | public class PlaySurfaceView extends SurfaceView { 8 | 9 | private float mWidth; 10 | private float mHeight; 11 | 12 | public PlaySurfaceView(Context context) { 13 | super(context); 14 | } 15 | 16 | public PlaySurfaceView(Context context, AttributeSet attrs) { 17 | super(context, attrs); 18 | } 19 | 20 | public PlaySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) { 21 | super(context, attrs, defStyleAttr); 22 | } 23 | 24 | public PlaySurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 25 | super(context, attrs, defStyleAttr, defStyleRes); 26 | } 27 | 28 | 29 | public void setMeasure(float width, float height) { 30 | this.mWidth = width; 31 | this.mHeight = height; 32 | } 33 | 34 | @Override 35 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 36 | int width = MeasureSpec.getSize(widthMeasureSpec); 37 | int hight = MeasureSpec.getSize(heightMeasureSpec); 38 | if (this.mWidth > 0) { 39 | width = (int) mWidth; 40 | } 41 | if (this.mHeight > 0) { 42 | hight = (int) mHeight; 43 | } 44 | setMeasuredDimension(width, hight); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/handler/session/Session.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal.handler.session; 2 | 3 | import com.github.serezhka.jap2lib.AirPlay; 4 | 5 | public class Session { 6 | 7 | private final AirPlay airPlay; 8 | 9 | private Thread airPlayReceiverThread; 10 | private Thread audioReceiverThread; 11 | private Thread audioControlServerThread; 12 | 13 | Session() { 14 | airPlay = new AirPlay(); 15 | } 16 | 17 | public AirPlay getAirPlay() { 18 | return airPlay; 19 | } 20 | 21 | public void setAirPlayReceiverThread(Thread airPlayReceiverThread) { 22 | this.airPlayReceiverThread = airPlayReceiverThread; 23 | } 24 | 25 | public void setAudioReceiverThread(Thread audioReceiverThread) { 26 | this.audioReceiverThread = audioReceiverThread; 27 | } 28 | 29 | public void setAudioControlServerThread(Thread audioControlServerThread) { 30 | this.audioControlServerThread = audioControlServerThread; 31 | } 32 | 33 | public boolean isMirroringActive() { 34 | return airPlayReceiverThread != null; 35 | } 36 | 37 | public boolean isAudioActive() { 38 | return audioReceiverThread != null && audioControlServerThread != null; 39 | } 40 | 41 | public void stopMirroring() { 42 | if (airPlayReceiverThread != null) { 43 | airPlayReceiverThread.interrupt(); 44 | airPlayReceiverThread = null; 45 | } 46 | // TODO destroy fair play video decryptor 47 | } 48 | 49 | public void stopAudio() { 50 | if (audioReceiverThread != null) { 51 | audioReceiverThread.interrupt(); 52 | audioReceiverThread = null; 53 | } 54 | if (audioControlServerThread != null) { 55 | audioControlServerThread.interrupt(); 56 | audioControlServerThread = null; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/handler/control/PairingHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal.handler.control; 2 | 3 | import com.github.serezhka.jap2server.internal.handler.session.Session; 4 | import com.github.serezhka.jap2server.internal.handler.session.SessionManager; 5 | 6 | import io.netty.buffer.ByteBuf; 7 | import io.netty.buffer.ByteBufInputStream; 8 | import io.netty.buffer.ByteBufOutputStream; 9 | import io.netty.channel.ChannelHandler; 10 | import io.netty.channel.ChannelHandlerContext; 11 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 12 | import io.netty.handler.codec.http.FullHttpRequest; 13 | 14 | @ChannelHandler.Sharable 15 | public class PairingHandler extends ControlHandler { 16 | 17 | public PairingHandler(SessionManager sessionManager) { 18 | super(sessionManager); 19 | } 20 | 21 | @Override 22 | protected boolean handleRequest(ChannelHandlerContext ctx, Session session, FullHttpRequest request) throws Exception { 23 | String uri = request.uri(); 24 | switch (uri) { 25 | case "/info": { 26 | DefaultFullHttpResponse response = createResponseForRequest(request); 27 | session.getAirPlay().info(new ByteBufOutputStream(response.content())); 28 | return sendResponse(ctx, request, response); 29 | } 30 | case "/pair-setup": { 31 | DefaultFullHttpResponse response = createResponseForRequest(request); 32 | session.getAirPlay().pairSetup(new ByteBufOutputStream(response.content())); 33 | return sendResponse(ctx, request, response); 34 | } 35 | case "/pair-verify": { 36 | DefaultFullHttpResponse response = createResponseForRequest(request); 37 | session.getAirPlay().pairVerify(new ByteBufInputStream(request.content()), 38 | new ByteBufOutputStream(response.content())); 39 | return sendResponse(ctx, request, response); 40 | } 41 | } 42 | return false; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/handler/mirroring/MirroringHeader.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal.handler.mirroring; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | 5 | class MirroringHeader { 6 | private final int payloadSize; 7 | private final short payloadType; 8 | private final short payloadOption; 9 | 10 | private int widthSource; 11 | private int heightSource; 12 | private int width; 13 | private int height; 14 | 15 | MirroringHeader(ByteBuf header) { 16 | this.payloadSize = (int) header.readUnsignedIntLE(); 17 | this.payloadType = (short) (header.readUnsignedShortLE() & 0xff); 18 | this.payloadOption = (short) header.readUnsignedShortLE(); 19 | 20 | if (payloadType == 1) { 21 | header.readerIndex(40); 22 | widthSource = (int) header.readFloatLE(); 23 | heightSource = (int) header.readFloatLE(); 24 | header.readerIndex(56); 25 | width = (int) header.readFloatLE(); 26 | height = (int) header.readFloatLE(); 27 | } 28 | } 29 | 30 | public int getPayloadSize() { 31 | return payloadSize; 32 | } 33 | 34 | public short getPayloadType() { 35 | return payloadType; 36 | } 37 | 38 | public short getPayloadOption() { 39 | return payloadOption; 40 | } 41 | 42 | public int getWidthSource() { 43 | return widthSource; 44 | } 45 | 46 | public void setWidthSource(int widthSource) { 47 | this.widthSource = widthSource; 48 | } 49 | 50 | public int getHeightSource() { 51 | return heightSource; 52 | } 53 | 54 | public void setHeightSource(int heightSource) { 55 | this.heightSource = heightSource; 56 | } 57 | 58 | public int getWidth() { 59 | return width; 60 | } 61 | 62 | public void setWidth(int width) { 63 | this.width = width; 64 | } 65 | 66 | public int getHeight() { 67 | return height; 68 | } 69 | 70 | public void setHeight(int height) { 71 | this.height = height; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | compileSdkVersion 30 7 | 8 | defaultConfig { 9 | applicationId "com.cjx.airplayjavademo" 10 | minSdkVersion 27 11 | targetSdkVersion 30 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | packagingOptions { 19 | 20 | exclude'META-INF/INDEX.LIST' 21 | exclude'META-INF/io.netty.versions.properties' 22 | 23 | } 24 | 25 | buildTypes { 26 | release { 27 | minifyEnabled false 28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | // 32 | // configurations.all { 33 | // resolutionStrategy { 34 | // force 'org.slf4j:slf4j-api:1.7.36' 35 | // } 36 | // } 37 | 38 | compileOptions { 39 | sourceCompatibility JavaVersion.VERSION_1_8 40 | targetCompatibility JavaVersion.VERSION_1_8 41 | } 42 | } 43 | 44 | dependencies { 45 | 46 | implementation 'androidx.appcompat:appcompat:1.2.0' 47 | implementation 'com.google.android.material:material:1.2.1' 48 | implementation 'androidx.constraintlayout:constraintlayout:2.0.1' 49 | testImplementation 'junit:junit:4.+' 50 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 51 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 52 | 53 | 54 | // airplay 55 | implementation 'org.jmdns:jmdns:3.5.7' 56 | implementation 'com.googlecode.plist:dd-plist:1.23' 57 | implementation 'net.i2p.crypto:eddsa:0.3.0' 58 | implementation 'org.whispersystems:curve25519-java:0.5.0' 59 | // netty has slf4j 60 | // implementation 'io.netty:netty-all:4.1.77.Final' 61 | implementation ('io.netty:netty-all:4.1.77.Final'){ 62 | exclude group: 'org.slf4j:slf4j-api' 63 | } 64 | // implementation 'org.slf4j:slf4j-api:1.7.36' 65 | implementation 'org.slf4j:slf4j-simple:1.7.25' 66 | // implementation 'ch.qos.reload4j:reload4j:1.2.19' 67 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/handler/audio/AudioPacket.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal.handler.audio; 2 | 3 | import java.util.function.Consumer; 4 | 5 | public class AudioPacket { 6 | 7 | private final byte[] encodedAudio = new byte[480 * 4]; 8 | 9 | private boolean available; 10 | private int flag; 11 | private int type; 12 | private int sequenceNumber; 13 | private long timestamp; 14 | private long ssrc; 15 | private int encodedAudioSize; 16 | 17 | public AudioPacket available(boolean available) { 18 | this.available = available; 19 | return this; 20 | } 21 | 22 | public AudioPacket flag(int flag) { 23 | this.flag = flag; 24 | return this; 25 | } 26 | 27 | public AudioPacket type(int type) { 28 | this.type = type; 29 | return this; 30 | } 31 | 32 | public AudioPacket sequenceNumber(int sequenceNumber) { 33 | this.sequenceNumber = sequenceNumber; 34 | return this; 35 | } 36 | 37 | public AudioPacket timestamp(long timestamp) { 38 | this.timestamp = timestamp; 39 | return this; 40 | } 41 | 42 | public AudioPacket ssrc(long ssrc) { 43 | this.ssrc = ssrc; 44 | return this; 45 | } 46 | 47 | public AudioPacket encodedAudioSize(int encodedAudioSize) { 48 | this.encodedAudioSize = encodedAudioSize; 49 | return this; 50 | } 51 | 52 | public AudioPacket encodedAudio(Consumer writer) { 53 | writer.accept(encodedAudio); 54 | return this; 55 | } 56 | 57 | public boolean isAvailable() { 58 | return available; 59 | } 60 | 61 | public int getFlag() { 62 | return flag; 63 | } 64 | 65 | public int getType() { 66 | return type; 67 | } 68 | 69 | public int getSequenceNumber() { 70 | return sequenceNumber; 71 | } 72 | 73 | public long getTimestamp() { 74 | return timestamp; 75 | } 76 | 77 | public long getSsrc() { 78 | return ssrc; 79 | } 80 | 81 | public int getEncodedAudioSize() { 82 | return encodedAudioSize; 83 | } 84 | 85 | public byte[] getEncodedAudio() { 86 | return encodedAudio; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/com/cjx/airplayjavademo/player/AudioPlayer.java: -------------------------------------------------------------------------------- 1 | package com.cjx.airplayjavademo.player; 2 | 3 | import android.media.AudioFormat; 4 | import android.media.AudioManager; 5 | import android.media.AudioTrack; 6 | import android.util.Log; 7 | 8 | 9 | import com.cjx.airplayjavademo.model.PCMPacket; 10 | 11 | import java.util.concurrent.BlockingQueue; 12 | import java.util.concurrent.LinkedBlockingQueue; 13 | 14 | public class AudioPlayer extends Thread { 15 | 16 | private static String TAG = "AudioPlayer"; 17 | private AudioTrack mTrack; 18 | private int mChannel = AudioFormat.CHANNEL_OUT_STEREO; 19 | private int mSampleRate = 44100; 20 | private boolean isStopThread = false; 21 | private int mAudioFormat = AudioFormat.ENCODING_PCM_16BIT; 22 | BlockingQueue packets = new LinkedBlockingQueue(500); 23 | 24 | public AudioPlayer() { 25 | this.mTrack = new AudioTrack(AudioManager.STREAM_MUSIC, mSampleRate, mChannel, mAudioFormat, 26 | AudioTrack.getMinBufferSize(mSampleRate, mChannel, mAudioFormat), AudioTrack.MODE_STREAM); 27 | this.mTrack.play(); 28 | } 29 | 30 | public void addPacker(PCMPacket pcmPacket) { 31 | try { 32 | packets.put(pcmPacket); 33 | } catch (InterruptedException e) { 34 | Log.e(TAG, "addPacker: ", e); 35 | } 36 | } 37 | 38 | @Override 39 | public void run() { 40 | super.run(); 41 | while (!isStopThread) { 42 | try { 43 | doPlay(packets.take()); 44 | } catch (InterruptedException e) { 45 | Log.e(TAG, "run: take error: ", e); 46 | } 47 | } 48 | } 49 | 50 | private void doPlay(PCMPacket pcmPacket) { 51 | if (mTrack != null) { 52 | try { 53 | mTrack.write(pcmPacket.data, 0, pcmPacket.data.length); 54 | } catch (Exception e) { 55 | Log.e(TAG, "doPlay: error", e); 56 | } 57 | } 58 | } 59 | 60 | public void stopPlay() { 61 | isStopThread = true; 62 | if (mTrack != null) { 63 | mTrack.flush(); 64 | mTrack.stop(); 65 | mTrack.release(); 66 | packets.clear(); 67 | mTrack = null; 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/AudioReceiver.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal; 2 | 3 | import com.github.serezhka.jap2server.internal.handler.audio.AudioHandler; 4 | import io.netty.bootstrap.Bootstrap; 5 | import io.netty.channel.ChannelFuture; 6 | import io.netty.channel.ChannelInitializer; 7 | import io.netty.channel.EventLoopGroup; 8 | import io.netty.channel.epoll.Epoll; 9 | import io.netty.channel.epoll.EpollDatagramChannel; 10 | import io.netty.channel.epoll.EpollEventLoopGroup; 11 | import io.netty.channel.nio.NioEventLoopGroup; 12 | import io.netty.channel.socket.DatagramChannel; 13 | import io.netty.channel.socket.nio.NioDatagramChannel; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import java.net.InetSocketAddress; 18 | 19 | public class AudioReceiver implements Runnable { 20 | 21 | private static final Logger log = LoggerFactory.getLogger(AudioReceiver.class); 22 | 23 | private final AudioHandler audioHandler; 24 | private final Object monitor; 25 | 26 | private int port; 27 | 28 | public AudioReceiver(AudioHandler audioHandler, Object monitor) { 29 | this.audioHandler = audioHandler; 30 | this.monitor = monitor; 31 | } 32 | 33 | @Override 34 | public void run() { 35 | Bootstrap bootstrap = new Bootstrap(); 36 | EventLoopGroup workerGroup = eventLoopGroup(); 37 | 38 | try { 39 | bootstrap 40 | .group(workerGroup) 41 | .channel(datagramChannelClass()) 42 | .localAddress(new InetSocketAddress(0)) // bind random port 43 | .handler(new ChannelInitializer() { 44 | @Override 45 | public void initChannel(final DatagramChannel ch) { 46 | ch.pipeline().addLast(audioHandler); 47 | } 48 | }); 49 | ChannelFuture channelFuture = bootstrap.bind().sync(); 50 | 51 | log.info("Audio receiver listening on port: {}", 52 | port = ((InetSocketAddress) channelFuture.channel().localAddress()).getPort()); 53 | 54 | synchronized (monitor) { 55 | monitor.notify(); 56 | } 57 | 58 | channelFuture.channel().closeFuture().sync(); 59 | } catch (InterruptedException e) { 60 | log.info("Audio receiver interrupted"); 61 | } finally { 62 | log.info("Audio receiver stopped"); 63 | workerGroup.shutdownGracefully(); 64 | } 65 | } 66 | 67 | public int getPort() { 68 | return port; 69 | } 70 | 71 | private EventLoopGroup eventLoopGroup() { 72 | return Epoll.isAvailable() ? new EpollEventLoopGroup() : new NioEventLoopGroup(); 73 | } 74 | 75 | private Class datagramChannelClass() { 76 | return Epoll.isAvailable() ? EpollDatagramChannel.class : NioDatagramChannel.class; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/AudioControlServer.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal; 2 | 3 | import com.github.serezhka.jap2server.internal.handler.audio.AudioControlHandler; 4 | import io.netty.bootstrap.Bootstrap; 5 | import io.netty.channel.ChannelFuture; 6 | import io.netty.channel.ChannelInitializer; 7 | import io.netty.channel.EventLoopGroup; 8 | import io.netty.channel.epoll.Epoll; 9 | import io.netty.channel.epoll.EpollDatagramChannel; 10 | import io.netty.channel.epoll.EpollEventLoopGroup; 11 | import io.netty.channel.nio.NioEventLoopGroup; 12 | import io.netty.channel.socket.DatagramChannel; 13 | import io.netty.channel.socket.nio.NioDatagramChannel; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import java.net.InetSocketAddress; 18 | 19 | public class AudioControlServer implements Runnable { 20 | 21 | private static final Logger log = LoggerFactory.getLogger(AudioControlServer.class); 22 | 23 | private final Object monitor; 24 | 25 | private int port; 26 | 27 | public AudioControlServer(Object monitor) { 28 | this.monitor = monitor; 29 | } 30 | 31 | @Override 32 | public void run() { 33 | Bootstrap bootstrap = new Bootstrap(); 34 | EventLoopGroup workerGroup = eventLoopGroup(); 35 | AudioControlHandler audioControlHandler = new AudioControlHandler(); 36 | 37 | try { 38 | bootstrap 39 | .group(workerGroup) 40 | .channel(datagramChannelClass()) 41 | .localAddress(new InetSocketAddress(0)) // bind random port 42 | .handler(new ChannelInitializer() { 43 | @Override 44 | public void initChannel(final DatagramChannel ch) { 45 | ch.pipeline().addLast(audioControlHandler); 46 | } 47 | }); 48 | 49 | ChannelFuture channelFuture = bootstrap.bind().sync(); 50 | 51 | log.info("Audio control server listening on port: {}", 52 | port = ((InetSocketAddress) channelFuture.channel().localAddress()).getPort()); 53 | 54 | synchronized (monitor) { 55 | monitor.notify(); 56 | } 57 | 58 | channelFuture.channel().closeFuture().sync(); 59 | } catch (InterruptedException e) { 60 | log.info("Audio control server interrupted"); 61 | } finally { 62 | log.info("Audio control server stopped"); 63 | workerGroup.shutdownGracefully(); 64 | } 65 | } 66 | 67 | public int getPort() { 68 | return port; 69 | } 70 | 71 | private EventLoopGroup eventLoopGroup() { 72 | return Epoll.isAvailable() ? new EpollEventLoopGroup() : new NioEventLoopGroup(); 73 | } 74 | 75 | private Class datagramChannelClass() { 76 | return Epoll.isAvailable() ? EpollDatagramChannel.class : NioDatagramChannel.class; 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/handler/control/ControlHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal.handler.control; 2 | 3 | import android.util.Log; 4 | 5 | import com.github.serezhka.jap2server.internal.handler.session.Session; 6 | import com.github.serezhka.jap2server.internal.handler.session.SessionManager; 7 | 8 | import io.netty.channel.ChannelFuture; 9 | import io.netty.channel.ChannelFutureListener; 10 | import io.netty.channel.ChannelHandlerContext; 11 | import io.netty.channel.ChannelInboundHandlerAdapter; 12 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 13 | import io.netty.handler.codec.http.FullHttpRequest; 14 | import io.netty.handler.codec.http.FullHttpResponse; 15 | import io.netty.handler.codec.http.HttpUtil; 16 | import io.netty.handler.codec.rtsp.RtspResponseStatuses; 17 | import io.netty.handler.codec.rtsp.RtspVersions; 18 | 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | public abstract class ControlHandler extends ChannelInboundHandlerAdapter { 23 | 24 | private static final Logger log = LoggerFactory.getLogger(ControlHandler.class); 25 | 26 | private static final String HEADER_CSEQ = "CSeq"; 27 | private static final String HEADER_ACTIVE_REMOTE = "Active-Remote"; 28 | 29 | private final SessionManager sessionManager; 30 | 31 | protected ControlHandler(SessionManager sessionManager) { 32 | this.sessionManager = sessionManager; 33 | } 34 | 35 | @Override 36 | public final void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 37 | if (!(msg instanceof FullHttpRequest && handleRequest(ctx, (FullHttpRequest) msg))) { 38 | super.channelRead(ctx, msg); 39 | } 40 | } 41 | 42 | private boolean handleRequest(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { 43 | // Log.i("TAG", "handleRequest: " + request.toString()); 44 | return handleRequest(ctx, sessionManager.getSession(request.headers().get(HEADER_ACTIVE_REMOTE)), request); 45 | } 46 | 47 | protected abstract boolean handleRequest(ChannelHandlerContext ctx, Session session, FullHttpRequest request) throws Exception; 48 | 49 | protected DefaultFullHttpResponse createResponseForRequest(FullHttpRequest request) { 50 | DefaultFullHttpResponse response = new DefaultFullHttpResponse(RtspVersions.RTSP_1_0, RtspResponseStatuses.OK); 51 | response.headers().clear(); 52 | 53 | String cSeq = request.headers().get(HEADER_CSEQ); 54 | if (cSeq != null) { 55 | response.headers().add(HEADER_CSEQ, cSeq); 56 | } 57 | 58 | return response; 59 | } 60 | 61 | protected boolean sendResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) { 62 | HttpUtil.setContentLength(response, response.content().readableBytes()); 63 | ChannelFuture future = ctx.writeAndFlush(response); 64 | if (!HttpUtil.isKeepAlive(request)) { 65 | future.addListener(ChannelFutureListener.CLOSE); 66 | } 67 | log.info("Request {} {} is handled!", request.method(), request.uri()); 68 | return true; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2lib/FairPlayVideoDecryptor.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2lib; 2 | 3 | import javax.crypto.Cipher; 4 | import javax.crypto.spec.IvParameterSpec; 5 | import javax.crypto.spec.SecretKeySpec; 6 | import java.nio.charset.StandardCharsets; 7 | import java.security.MessageDigest; 8 | import java.util.Arrays; 9 | 10 | class FairPlayVideoDecryptor { 11 | 12 | private final byte[] aesKey; 13 | private final byte[] sharedSecret; 14 | private final String streamConnectionID; 15 | 16 | private final Cipher aesCtrDecrypt; 17 | private final byte[] og = new byte[16]; 18 | 19 | private int nextDecryptCount; 20 | 21 | FairPlayVideoDecryptor(byte[] aesKey, byte[] sharedSecret, String streamConnectionID) throws Exception { 22 | this.aesKey = aesKey; 23 | this.sharedSecret = sharedSecret; 24 | this.streamConnectionID = streamConnectionID; 25 | 26 | aesCtrDecrypt = Cipher.getInstance("AES/CTR/NoPadding"); 27 | 28 | initAesCtrCipher(); 29 | } 30 | 31 | void decrypt(byte[] video) throws Exception { 32 | if (nextDecryptCount > 0) { 33 | for (int i = 0; i < nextDecryptCount; i++) { 34 | video[i] = (byte) (video[i] ^ og[(16 - nextDecryptCount) + i]); 35 | } 36 | } 37 | 38 | int encryptlen = ((video.length - nextDecryptCount) / 16) * 16; 39 | aesCtrDecrypt.update(video, nextDecryptCount, encryptlen, video, nextDecryptCount); 40 | System.arraycopy(video, nextDecryptCount, video, nextDecryptCount, encryptlen); 41 | 42 | int restlen = (video.length - nextDecryptCount) % 16; 43 | int reststart = video.length - restlen; 44 | nextDecryptCount = 0; 45 | if (restlen > 0) { 46 | Arrays.fill(og, (byte) 0); 47 | System.arraycopy(video, reststart, og, 0, restlen); 48 | aesCtrDecrypt.update(og, 0, 16, og, 0); 49 | System.arraycopy(og, 0, video, reststart, restlen); 50 | nextDecryptCount = 16 - restlen; 51 | } 52 | } 53 | 54 | private void initAesCtrCipher() throws Exception { 55 | MessageDigest sha512Digest = MessageDigest.getInstance("SHA-512"); 56 | sha512Digest.update(aesKey); 57 | sha512Digest.update(sharedSecret); 58 | byte[] eaesKey = sha512Digest.digest(); 59 | 60 | byte[] skey = ("AirPlayStreamKey" + streamConnectionID).getBytes(StandardCharsets.UTF_8); 61 | sha512Digest.update(skey); 62 | sha512Digest.update(eaesKey, 0, 16); 63 | byte[] hash1 = sha512Digest.digest(); 64 | 65 | byte[] siv = ("AirPlayStreamIV" + streamConnectionID).getBytes(StandardCharsets.UTF_8); 66 | sha512Digest.update(siv); 67 | sha512Digest.update(eaesKey, 0, 16); 68 | byte[] hash2 = sha512Digest.digest(); 69 | 70 | byte[] decryptAesKey = new byte[16]; 71 | byte[] decryptAesIV = new byte[16]; 72 | System.arraycopy(hash1, 0, decryptAesKey, 0, 16); 73 | System.arraycopy(hash2, 0, decryptAesIV, 0, 16); 74 | 75 | aesCtrDecrypt.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptAesKey, "AES"), new IvParameterSpec(decryptAesIV)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/MirroringReceiver.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal; 2 | 3 | import com.github.serezhka.jap2server.internal.handler.mirroring.MirroringHandler; 4 | import io.netty.bootstrap.ServerBootstrap; 5 | import io.netty.channel.ChannelFuture; 6 | import io.netty.channel.ChannelInitializer; 7 | import io.netty.channel.ChannelOption; 8 | import io.netty.channel.EventLoopGroup; 9 | import io.netty.channel.epoll.Epoll; 10 | import io.netty.channel.epoll.EpollEventLoopGroup; 11 | import io.netty.channel.epoll.EpollServerSocketChannel; 12 | import io.netty.channel.nio.NioEventLoopGroup; 13 | import io.netty.channel.socket.ServerSocketChannel; 14 | import io.netty.channel.socket.SocketChannel; 15 | import io.netty.channel.socket.nio.NioServerSocketChannel; 16 | //import org.slf4j.Logger; 17 | //import org.slf4j.LoggerFactory; 18 | 19 | import java.net.InetSocketAddress; 20 | 21 | public class MirroringReceiver implements Runnable { 22 | 23 | // private static final Logger log = LoggerFactory.getLogger(MirroringHandler.class); 24 | 25 | private final int port; 26 | private final MirroringHandler mirroringHandler; 27 | 28 | public MirroringReceiver(int port, MirroringHandler mirroringHandler) { 29 | this.port = port; 30 | this.mirroringHandler = mirroringHandler; 31 | } 32 | 33 | @Override 34 | public void run() { 35 | ServerBootstrap serverBootstrap = new ServerBootstrap(); 36 | EventLoopGroup bossGroup = eventLoopGroup(); 37 | EventLoopGroup workerGroup = eventLoopGroup(); 38 | try { 39 | serverBootstrap 40 | .group(bossGroup, workerGroup) 41 | .channel(serverSocketChannelClass()) 42 | .localAddress(new InetSocketAddress(port)) 43 | .childHandler(new ChannelInitializer() { 44 | @Override 45 | public void initChannel(final SocketChannel ch) { 46 | ch.pipeline().addLast(mirroringHandler); 47 | } 48 | }) 49 | .childOption(ChannelOption.TCP_NODELAY, true) 50 | .childOption(ChannelOption.SO_REUSEADDR, true) 51 | .childOption(ChannelOption.SO_KEEPALIVE, true); 52 | ChannelFuture channelFuture = serverBootstrap.bind().sync(); 53 | // log.info("Mirroring receiver listening on port: {}", port); 54 | channelFuture.channel().closeFuture().sync(); 55 | } catch (InterruptedException e) { 56 | // log.info("Mirroring receiver interrupted"); 57 | } finally { 58 | // log.info("Mirroring receiver stopped"); 59 | bossGroup.shutdownGracefully(); 60 | workerGroup.shutdownGracefully(); 61 | } 62 | } 63 | 64 | private EventLoopGroup eventLoopGroup() { 65 | return Epoll.isAvailable() ? new EpollEventLoopGroup() : new NioEventLoopGroup(); 66 | } 67 | 68 | private Class serverSocketChannelClass() { 69 | return Epoll.isAvailable() ? EpollServerSocketChannel.class : NioServerSocketChannel.class; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/assets/info-response.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | audioFormats 6 | 7 | 8 | audioInputFormats 9 | 67108860 10 | audioOutputFormats 11 | 67108860 12 | type 13 | 100 14 | 15 | 16 | audioInputFormats 17 | 67108860 18 | audioOutputFormats 19 | 67108860 20 | type 21 | 101 22 | 23 | 24 | audioLatencies 25 | 26 | 27 | audioType 28 | default 29 | inputLatencyMicros 30 | 31 | type 32 | 100 33 | 34 | 35 | audioType 36 | default 37 | inputLatencyMicros 38 | 39 | type 40 | 101 41 | 42 | 43 | displays 44 | 45 | 46 | features 47 | 14 48 | height 49 | 1080 50 | heightPhysical 51 | 52 | heightPixels 53 | 1080 54 | maxFPS 55 | 30 56 | overscanned 57 | 58 | refreshRate 59 | 60 60 | rotation 61 | 62 | uuid 63 | e5f7a68d-7b0f-4305-984b-974f677a150b 64 | width 65 | 1920 66 | widthPhysical 67 | 68 | widthPixels 69 | 1920 70 | 71 | 72 | features 73 | 130367356919 74 | keepAliveSendStatsAsBody 75 | 1 76 | model 77 | AppleTV2,1 78 | name 79 | Apple TV 80 | pi 81 | b08f5a79-db29-4384-b456-a4784d9e6055 82 | sourceVersion 83 | 220.68 84 | statusFlags 85 | 68 86 | vv 87 | 2 88 | 89 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/handler/audio/AudioHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal.handler.audio; 2 | 3 | import com.github.serezhka.jap2lib.AirPlay; 4 | import com.github.serezhka.jap2server.AirplayDataConsumer; 5 | import io.netty.buffer.ByteBuf; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import io.netty.channel.SimpleChannelInboundHandler; 8 | import io.netty.channel.socket.DatagramPacket; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.util.Arrays; 13 | 14 | public class AudioHandler extends SimpleChannelInboundHandler { 15 | 16 | private static final Logger log = LoggerFactory.getLogger(AudioHandler.class); 17 | 18 | private final AirPlay airPlay; 19 | private final AirplayDataConsumer dataConsumer; 20 | 21 | private final AudioPacket[] buffer = new AudioPacket[512]; 22 | 23 | private int prevSeqNum; 24 | private int packetsInBuffer; 25 | 26 | public AudioHandler(AirPlay airPlay, AirplayDataConsumer dataConsumer) { 27 | this.airPlay = airPlay; 28 | this.dataConsumer = dataConsumer; 29 | for (int i = 0; i < buffer.length; i++) { 30 | buffer[i] = new AudioPacket(); 31 | } 32 | } 33 | 34 | @Override 35 | protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception { 36 | ByteBuf content = msg.content(); 37 | 38 | byte[] headerBytes = new byte[12]; 39 | content.readBytes(headerBytes); 40 | 41 | int flag = headerBytes[0] & 0xFF; 42 | int type = headerBytes[1] & 0x7F; 43 | 44 | int curSeqNo = ((headerBytes[2] & 0xFF) << 8) | (headerBytes[3] & 0xFF); 45 | 46 | long timestamp = (headerBytes[7] & 0xFF) | ((headerBytes[6] & 0xFF) << 8) | 47 | ((headerBytes[5] & 0xFF) << 16) | ((headerBytes[4] & 0xFF) << 24); 48 | 49 | long ssrc = (headerBytes[11] & 0xFF) | ((headerBytes[6] & 0xFF) << 8) | 50 | ((headerBytes[9] & 0xFF) << 16) | ((headerBytes[8] & 0xFF) << 24); 51 | 52 | // TODO handle bad cases (missing packets, curSeqNum - prevSeqNum > buffer.length, ...) 53 | if (curSeqNo <= prevSeqNum) { 54 | return; 55 | } 56 | 57 | log.debug("Got audio packet. flag: {}, type: {}, prevSeqNum: {}, curSecNum: {}, audio packets in buffer: {}", 58 | flag, type, prevSeqNum, curSeqNo, packetsInBuffer); 59 | 60 | AudioPacket audioPacket = buffer[curSeqNo % buffer.length]; 61 | audioPacket 62 | .flag(flag) 63 | .type(type) 64 | .sequenceNumber(curSeqNo) 65 | .timestamp(timestamp) 66 | .ssrc(ssrc) 67 | .available(true) 68 | .encodedAudioSize(content.readableBytes()) 69 | .encodedAudio(packet -> content.readBytes(packet, 0, content.readableBytes())); 70 | packetsInBuffer++; 71 | 72 | while (dequeue(curSeqNo)) { 73 | curSeqNo++; 74 | } 75 | } 76 | 77 | private boolean dequeue(int curSeqNo) throws Exception { 78 | if (curSeqNo - prevSeqNum == 1 || prevSeqNum == 0) { 79 | AudioPacket audioPacket = buffer[curSeqNo % buffer.length]; 80 | if (audioPacket.isAvailable()) { 81 | airPlay.decryptAudio(audioPacket.getEncodedAudio(), audioPacket.getEncodedAudioSize()); 82 | dataConsumer.onAudio(Arrays.copyOfRange(audioPacket.getEncodedAudio(), 0, audioPacket.getEncodedAudioSize())); 83 | audioPacket.available(false); 84 | prevSeqNum = curSeqNo; 85 | packetsInBuffer--; 86 | return true; 87 | } 88 | } 89 | return false; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2lib/SapHash.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2lib; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.ByteOrder; 5 | 6 | class SapHash { 7 | 8 | private final HandGarble handGarble = new HandGarble(); 9 | 10 | private byte rol8(byte input, int count) { 11 | return (byte) (((input << count) & 0xff) | (input & 0xff) >> (8 - count)); 12 | } 13 | 14 | void sap_hash(byte[] blockIn, byte[] keyOut) { 15 | 16 | byte[] buffer0 = {-106, 95, -58, 83, -8, 70, -52, 24, -33, -66, -78, -8, 56, -41, -20, 34, 3, -47, 32, -113}; 17 | byte[] buffer1 = new byte[210]; 18 | byte[] buffer2 = {67, 84, 98, 122, 24, -61, -42, -77, -102, 86, -10, 28, 20, 63, 12, 29, 59, 54, -125, -79, 57, 81, 74, -86, 9, 62, -2, 68, -81, -34, -61, 32, -99, 66, 58}; 19 | byte[] buffer3 = new byte[132]; 20 | byte[] buffer4 = {-19, 37, -47, -69, -68, 39, -97, 2, -94, -87, 17, 0, 12, -77, 82, -64, -67, -29, 27, 73, -57}; 21 | int[] i0_index = {18, 22, 23, 0, 5, 19, 32, 31, 10, 21, 30}; 22 | byte w, x, y, z; 23 | 24 | ByteBuffer block_words = ByteBuffer.wrap(blockIn); 25 | block_words.order(ByteOrder.LITTLE_ENDIAN); 26 | 27 | // Load the input into the buffer 28 | for (int i = 0; i < 210; i++) { 29 | // We need to swap the byte order around so it is the right endianness 30 | int in_word = block_words.getInt(((i % 64) >> 2) * 4); 31 | byte in_byte = (byte) ((in_word >> ((3 - (i % 4)) << 3)) & 0xff); 32 | buffer1[i] = in_byte; 33 | } 34 | 35 | // Next a scrambling 36 | for (int i = 0; i < 840; i++) { 37 | // We have to do unsigned, 32-bit modulo, or we get the wrong indices 38 | x = buffer1[(int) (((i - 155) & 0xffffffffL) % 210)]; 39 | y = buffer1[(int) (((i - 57) & 0xffffffffL) % 210)]; 40 | z = buffer1[(int) (((i - 13) & 0xffffffffL) % 210)]; 41 | w = buffer1[(int) ((i & 0xffffffffL) % 210)]; 42 | buffer1[i % 210] = (byte) ((rol8(y, 5) + (rol8(z, 3) ^ w) - rol8(x, 7)) & 0xff); 43 | } 44 | 45 | // I have no idea what this is doing (yet), but it gives the right output 46 | handGarble.garble(buffer0, buffer1, buffer2, buffer3, buffer4); 47 | 48 | // Fill the output with 0xE1 49 | for (int i = 0; i < 16; i++) { 50 | keyOut[i] = (byte) 0xE1; 51 | } 52 | 53 | // Now we use all the buffers we have calculated to grind out the output. First buffer3 54 | for (int i = 0; i < 11; i++) { 55 | // Note that this is addition (mod 255) and not XOR 56 | // Also note that we only use certain indices 57 | // And that index 3 is hard-coded to be 0x3d (Maybe we can hack this up by changing buffer3[0] to be 0xdc? 58 | if (i == 3) { 59 | keyOut[i] = 0x3d; 60 | } else { 61 | keyOut[i] = (byte) ((keyOut[i] + buffer3[i0_index[i] * 4]) & 0xff); 62 | } 63 | } 64 | 65 | // Then buffer0 66 | for (int i = 0; i < 20; i++) { 67 | keyOut[i % 16] ^= buffer0[i]; 68 | } 69 | 70 | // Then buffer2 71 | for (int i = 0; i < 35; i++) { 72 | keyOut[i % 16] ^= buffer2[i]; 73 | } 74 | 75 | // Do buffer1 76 | for (int i = 0; i < 210; i++) { 77 | keyOut[(i % 16)] ^= buffer1[i]; 78 | } 79 | 80 | // Now we do a kind of reverse-scramble 81 | for (int j = 0; j < 16; j++) { 82 | for (int i = 0; i < 16; i++) { 83 | x = keyOut[(int) (((i - 7) & 0xffffffffL) % 16)]; 84 | y = keyOut[i % 16]; 85 | z = keyOut[(int) (((i - 37) & 0xffffffffL) % 16)]; 86 | w = keyOut[(int) (((i - 177) & 0xffffffffL) % 16)]; 87 | keyOut[i] = (byte) (rol8(x, 1) ^ y ^ rol8(z, 6) ^ rol8(w, 5)); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2lib/AirPlayBonjour.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2lib; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import javax.jmdns.JmmDNS; 7 | import javax.jmdns.ServiceInfo; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | /** 12 | * Registers airplay/airtunes service mdns 13 | */ 14 | public class AirPlayBonjour { 15 | 16 | private static final Logger log = LoggerFactory.getLogger(AirPlayBonjour.class); 17 | 18 | private static final String AIRPLAY_SERVICE_TYPE = "._airplay._tcp.local"; 19 | private static final String AIRTUNES_SERVICE_TYPE = "._raop._tcp.local"; 20 | 21 | private final String serverName; 22 | 23 | private ServiceInfo airPlayService; 24 | private ServiceInfo airTunesService; 25 | 26 | public AirPlayBonjour(String serverName) { 27 | this.serverName = serverName; 28 | } 29 | 30 | public void start(int airPlayPort, int airTunesPort) throws Exception { 31 | airPlayService = ServiceInfo.create(serverName + AIRPLAY_SERVICE_TYPE, 32 | serverName, airPlayPort, 0, 0, airPlayMDNSProps()); 33 | JmmDNS.Factory.getInstance().registerService(airPlayService); 34 | log.info("{} service is registered on port {}", serverName + AIRPLAY_SERVICE_TYPE, airPlayPort); 35 | 36 | String airTunesServerName = "010203040506@" + serverName; 37 | airTunesService = ServiceInfo.create(airTunesServerName + AIRTUNES_SERVICE_TYPE, 38 | airTunesServerName, airTunesPort, 0, 0, airTunesMDNSProps()); 39 | JmmDNS.Factory.getInstance().registerService(airTunesService); 40 | log.info("{} service is registered on port {}", airTunesServerName + AIRTUNES_SERVICE_TYPE, airTunesPort); 41 | } 42 | 43 | public void stop() { 44 | JmmDNS.Factory.getInstance().unregisterService(airPlayService); 45 | log.info("{} service is unregistered", airPlayService.getName()); 46 | JmmDNS.Factory.getInstance().unregisterService(airTunesService); 47 | log.info("{} service is unregistered", airTunesService.getName()); 48 | } 49 | 50 | private Map airPlayMDNSProps() { 51 | HashMap airPlayMDNSProps = new HashMap<>(); 52 | airPlayMDNSProps.put("deviceid", "01:02:03:04:05:06"); 53 | airPlayMDNSProps.put("features", "0x5A7FFFF7,0x1E"); 54 | airPlayMDNSProps.put("srcvers", "220.68"); 55 | airPlayMDNSProps.put("flags", "0x4"); 56 | airPlayMDNSProps.put("vv", "2"); 57 | airPlayMDNSProps.put("model", "AppleTV2,1"); 58 | airPlayMDNSProps.put("rhd", "5.6.0.0"); 59 | airPlayMDNSProps.put("pw", "false"); 60 | airPlayMDNSProps.put("pk", "b07727d6f6cd6e08b58ede525ec3cdeaa252ad9f683feb212ef8a205246554e7"); 61 | airPlayMDNSProps.put("pi", "2e388006-13ba-4041-9a67-25dd4a43d536"); 62 | return airPlayMDNSProps; 63 | } 64 | 65 | private Map airTunesMDNSProps() { 66 | HashMap airTunesMDNSProps = new HashMap<>(); 67 | airTunesMDNSProps.put("ch", "2"); 68 | airTunesMDNSProps.put("cn", "0,1,2,3"); 69 | airTunesMDNSProps.put("da", "true"); 70 | airTunesMDNSProps.put("et", "0,3,5"); 71 | airTunesMDNSProps.put("vv", "2"); 72 | airTunesMDNSProps.put("ft", "0x5A7FFFF7,0x1E"); 73 | airTunesMDNSProps.put("am", "AppleTV2,1"); 74 | airTunesMDNSProps.put("md", "0,1,2"); 75 | airTunesMDNSProps.put("rhd", "5.6.0.0"); 76 | airTunesMDNSProps.put("pw", "false"); 77 | airTunesMDNSProps.put("sr", "44100"); 78 | airTunesMDNSProps.put("ss", "16"); 79 | airTunesMDNSProps.put("sv", "false"); 80 | airTunesMDNSProps.put("tp", "UDP"); 81 | airTunesMDNSProps.put("txtvers", "1"); 82 | airTunesMDNSProps.put("sf", "0x4"); 83 | airTunesMDNSProps.put("vs", "220.68"); 84 | airTunesMDNSProps.put("vn", "65537"); 85 | airTunesMDNSProps.put("pk", "b07727d6f6cd6e08b58ede525ec3cdeaa252ad9f683feb212ef8a205246554e7"); 86 | return airTunesMDNSProps; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2lib/ModifiedMD5.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2lib; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.ByteOrder; 5 | 6 | class ModifiedMD5 { 7 | 8 | private final int[] shift = {7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 9 | 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 10 | 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 11 | 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21}; 12 | 13 | void modified_md5(byte[] originalblockIn, byte[] keyIn, byte[] keyOut) { 14 | byte[] blockIn = new byte[64]; 15 | long A, B, C, D, Z, tmp; 16 | int i; 17 | 18 | System.arraycopy(originalblockIn, 0, blockIn, 0, 64); 19 | 20 | // Each cycle does something like this: 21 | ByteBuffer key_words = ByteBuffer.wrap(keyIn); 22 | key_words.order(ByteOrder.LITTLE_ENDIAN); 23 | A = key_words.getInt() & 0xffffffffL; 24 | B = key_words.getInt() & 0xffffffffL; 25 | C = key_words.getInt() & 0xffffffffL; 26 | D = key_words.getInt() & 0xffffffffL; 27 | for (i = 0; i < 64; i++) { 28 | int input; 29 | int j = 0; 30 | if (i < 16) { 31 | j = i; 32 | } else if (i < 32) { 33 | j = (5 * i + 1) % 16; 34 | } else if (i < 48) { 35 | j = (3 * i + 5) % 16; 36 | } else if (i < 64) { 37 | j = 7 * i % 16; 38 | } 39 | 40 | input = ((blockIn[4 * j] & 0xFF) << 24) | ((blockIn[4 * j + 1] & 0xFF) << 16) | ((blockIn[4 * j + 2] & 0xFF) << 8) | (blockIn[4 * j + 3] & 0xFF); 41 | Z = A + input + (long) ((1L << 32) * Math.abs(Math.sin(i + 1))); 42 | if (i < 16) { 43 | Z = rol(Z + F(B, C, D), shift[i]); 44 | } else if (i < 32) { 45 | Z = rol(Z + G(B, C, D), shift[i]); 46 | } else if (i < 48) { 47 | Z = rol(Z + H(B, C, D), shift[i]); 48 | } else if (i < 64) { 49 | Z = rol(Z + I(B, C, D), shift[i]); 50 | } 51 | Z = Z + B; 52 | tmp = D; 53 | D = C; 54 | C = B; 55 | B = Z; 56 | A = tmp; 57 | if (i == 31) { 58 | // swapsies 59 | swap(blockIn, 4 * (int) (A & 15), 4 * (int) (B & 15)); 60 | swap(blockIn, 4 * (int) (C & 15), 4 * (int) (D & 15)); 61 | swap(blockIn, 4 * (int) ((A & (15 << 4)) >> 4), 4 * (int) ((B & (15 << 4)) >> 4)); 62 | swap(blockIn, 4 * (int) ((A & (15 << 8)) >> 8), 4 * (int) ((B & (15 << 8)) >> 8)); 63 | swap(blockIn, 4 * (int) ((A & (15 << 12)) >> 12), 4 * (int) ((B & (15 << 12)) >> 12)); 64 | } 65 | } 66 | 67 | ByteBuffer key_out = ByteBuffer.wrap(keyOut); 68 | key_out.order(ByteOrder.LITTLE_ENDIAN); 69 | key_out.putInt((int) (key_words.getInt(0) + A)); 70 | key_out.putInt((int) (key_words.getInt(4) + B)); 71 | key_out.putInt((int) (key_words.getInt(8) + C)); 72 | key_out.putInt((int) (key_words.getInt(12) + D)); 73 | } 74 | 75 | private long F(long B, long C, long D) { 76 | return (B & C) | (~B & D); 77 | } 78 | 79 | private long G(long B, long C, long D) { 80 | return (B & D) | (C & ~D); 81 | } 82 | 83 | private long H(long B, long C, long D) { 84 | return B ^ C ^ D; 85 | } 86 | 87 | private long I(long B, long C, long D) { 88 | return C ^ (B | ~D); 89 | } 90 | 91 | private long rol(long input, long count) { 92 | return ((input << count) & 0xffffffffL) | (input & 0xffffffffL) >> (32 - count); 93 | } 94 | 95 | private void swap(byte[] arr, int idxA, int idxB) { 96 | ByteBuffer wrap = ByteBuffer.wrap(arr); 97 | wrap.order(ByteOrder.LITTLE_ENDIAN); 98 | int a = wrap.getInt(idxA); 99 | int b = wrap.getInt(idxB); 100 | wrap.putInt(idxB, a); 101 | wrap.putInt(idxA, b); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/assets/table_s5: -------------------------------------------------------------------------------- 1 | 0x21aa8423 2 | 0x2fa1892a 3 | 0x3dbc9e31 4 | 0x33b79338 5 | 0x1986b007 6 | 0x178dbd0e 7 | 0x590aa15 8 | 0xb9ba71c 9 | 0x51f2ec6b 10 | 0x5ff9e162 11 | 0x4de4f679 12 | 0x43effb70 13 | 0x69ded84f 14 | 0x67d5d546 15 | 0x75c8c25d 16 | 0x7bc3cf54 17 | 0xc11a54b3 18 | 0xcf1159ba 19 | 0xdd0c4ea1 20 | 0xd30743a8 21 | 0xf9366097 22 | 0xf73d6d9e 23 | 0xe5207a85 24 | 0xeb2b778c 25 | 0xb1423cfb 26 | 0xbf4931f2 27 | 0xad5426e9 28 | 0xa35f2be0 29 | 0x896e08df 30 | 0x876505d6 31 | 0x957812cd 32 | 0x9b731fc4 33 | 0xfad13f18 34 | 0xf4da3211 35 | 0xe6c7250a 36 | 0xe8cc2803 37 | 0xc2fd0b3c 38 | 0xccf60635 39 | 0xdeeb112e 40 | 0xd0e01c27 41 | 0x8a895750 42 | 0x84825a59 43 | 0x969f4d42 44 | 0x9894404b 45 | 0xb2a56374 46 | 0xbcae6e7d 47 | 0xaeb37966 48 | 0xa0b8746f 49 | 0x1a61ef88 50 | 0x146ae281 51 | 0x677f59a 52 | 0x87cf893 53 | 0x224ddbac 54 | 0x2c46d6a5 55 | 0x3e5bc1be 56 | 0x3050ccb7 57 | 0x6a3987c0 58 | 0x64328ac9 59 | 0x762f9dd2 60 | 0x782490db 61 | 0x5215b3e4 62 | 0x5c1ebeed 63 | 0x4e03a9f6 64 | 0x4008a4ff 65 | 0x8c5ce955 66 | 0x8257e45c 67 | 0x904af347 68 | 0x9e41fe4e 69 | 0xb470dd71 70 | 0xba7bd078 71 | 0xa866c763 72 | 0xa66dca6a 73 | 0xfc04811d 74 | 0xf20f8c14 75 | 0xe0129b0f 76 | 0xee199606 77 | 0xc428b539 78 | 0xca23b830 79 | 0xd83eaf2b 80 | 0xd635a222 81 | 0x6cec39c5 82 | 0x62e734cc 83 | 0x70fa23d7 84 | 0x7ef12ede 85 | 0x54c00de1 86 | 0x5acb00e8 87 | 0x48d617f3 88 | 0x46dd1afa 89 | 0x1cb4518d 90 | 0x12bf5c84 91 | 0xa24b9f 92 | 0xea94696 93 | 0x249865a9 94 | 0x2a9368a0 95 | 0x388e7fbb 96 | 0x368572b2 97 | 0x5727526e 98 | 0x592c5f67 99 | 0x4b31487c 100 | 0x453a4575 101 | 0x6f0b664a 102 | 0x61006b43 103 | 0x731d7c58 104 | 0x7d167151 105 | 0x277f3a26 106 | 0x2974372f 107 | 0x3b692034 108 | 0x35622d3d 109 | 0x1f530e02 110 | 0x1158030b 111 | 0x3451410 112 | 0xd4e1919 113 | 0xb79782fe 114 | 0xb99c8ff7 115 | 0xab8198ec 116 | 0xa58a95e5 117 | 0x8fbbb6da 118 | 0x81b0bbd3 119 | 0x93adacc8 120 | 0x9da6a1c1 121 | 0xc7cfeab6 122 | 0xc9c4e7bf 123 | 0xdbd9f0a4 124 | 0xd5d2fdad 125 | 0xffe3de92 126 | 0xf1e8d39b 127 | 0xe3f5c480 128 | 0xedfec989 129 | 0x605d5ecf 130 | 0x6e5653c6 131 | 0x7c4b44dd 132 | 0x724049d4 133 | 0x58716aeb 134 | 0x567a67e2 135 | 0x446770f9 136 | 0x4a6c7df0 137 | 0x10053687 138 | 0x1e0e3b8e 139 | 0xc132c95 140 | 0x218219c 141 | 0x282902a3 142 | 0x26220faa 143 | 0x343f18b1 144 | 0x3a3415b8 145 | 0x80ed8e5f 146 | 0x8ee68356 147 | 0x9cfb944d 148 | 0x92f09944 149 | 0xb8c1ba7b 150 | 0xb6cab772 151 | 0xa4d7a069 152 | 0xaadcad60 153 | 0xf0b5e617 154 | 0xfebeeb1e 155 | 0xeca3fc05 156 | 0xe2a8f10c 157 | 0xc899d233 158 | 0xc692df3a 159 | 0xd48fc821 160 | 0xda84c528 161 | 0xbb26e5f4 162 | 0xb52de8fd 163 | 0xa730ffe6 164 | 0xa93bf2ef 165 | 0x830ad1d0 166 | 0x8d01dcd9 167 | 0x9f1ccbc2 168 | 0x9117c6cb 169 | 0xcb7e8dbc 170 | 0xc57580b5 171 | 0xd76897ae 172 | 0xd9639aa7 173 | 0xf352b998 174 | 0xfd59b491 175 | 0xef44a38a 176 | 0xe14fae83 177 | 0x5b963564 178 | 0x559d386d 179 | 0x47802f76 180 | 0x498b227f 181 | 0x63ba0140 182 | 0x6db10c49 183 | 0x7fac1b52 184 | 0x71a7165b 185 | 0x2bce5d2c 186 | 0x25c55025 187 | 0x37d8473e 188 | 0x39d34a37 189 | 0x13e26908 190 | 0x1de96401 191 | 0xff4731a 192 | 0x1ff7e13 193 | 0xcdab33b9 194 | 0xc3a03eb0 195 | 0xd1bd29ab 196 | 0xdfb624a2 197 | 0xf587079d 198 | 0xfb8c0a94 199 | 0xe9911d8f 200 | 0xe79a1086 201 | 0xbdf35bf1 202 | 0xb3f856f8 203 | 0xa1e541e3 204 | 0xafee4cea 205 | 0x85df6fd5 206 | 0x8bd462dc 207 | 0x99c975c7 208 | 0x97c278ce 209 | 0x2d1be329 210 | 0x2310ee20 211 | 0x310df93b 212 | 0x3f06f432 213 | 0x1537d70d 214 | 0x1b3cda04 215 | 0x921cd1f 216 | 0x72ac016 217 | 0x5d438b61 218 | 0x53488668 219 | 0x41559173 220 | 0x4f5e9c7a 221 | 0x656fbf45 222 | 0x6b64b24c 223 | 0x7979a557 224 | 0x7772a85e 225 | 0x16d08882 226 | 0x18db858b 227 | 0xac69290 228 | 0x4cd9f99 229 | 0x2efcbca6 230 | 0x20f7b1af 231 | 0x32eaa6b4 232 | 0x3ce1abbd 233 | 0x6688e0ca 234 | 0x6883edc3 235 | 0x7a9efad8 236 | 0x7495f7d1 237 | 0x5ea4d4ee 238 | 0x50afd9e7 239 | 0x42b2cefc 240 | 0x4cb9c3f5 241 | 0xf6605812 242 | 0xf86b551b 243 | 0xea764200 244 | 0xe47d4f09 245 | 0xce4c6c36 246 | 0xc047613f 247 | 0xd25a7624 248 | 0xdc517b2d 249 | 0x8638305a 250 | 0x88333d53 251 | 0x9a2e2a48 252 | 0x94252741 253 | 0xbe14047e 254 | 0xb01f0977 255 | 0xa2021e6c 256 | 0xac091365 -------------------------------------------------------------------------------- /app/src/main/assets/table_s6: -------------------------------------------------------------------------------- 1 | 0x5ee7493 2 | 0xce07f9e 3 | 0x17f26289 4 | 0x1efc6984 5 | 0x21d658a7 6 | 0x28d853aa 7 | 0x33ca4ebd 8 | 0x3ac445b0 9 | 0x4d9e2cfb 10 | 0x449027f6 11 | 0x5f823ae1 12 | 0x568c31ec 13 | 0x69a600cf 14 | 0x60a80bc2 15 | 0x7bba16d5 16 | 0x72b41dd8 17 | 0x950ec443 18 | 0x9c00cf4e 19 | 0x8712d259 20 | 0x8e1cd954 21 | 0xb136e877 22 | 0xb838e37a 23 | 0xa32afe6d 24 | 0xaa24f560 25 | 0xdd7e9c2b 26 | 0xd4709726 27 | 0xcf628a31 28 | 0xc66c813c 29 | 0xf946b01f 30 | 0xf048bb12 31 | 0xeb5aa605 32 | 0xe254ad08 33 | 0x3e350f28 34 | 0x373b0425 35 | 0x2c291932 36 | 0x2527123f 37 | 0x1a0d231c 38 | 0x13032811 39 | 0x8113506 40 | 0x11f3e0b 41 | 0x76455740 42 | 0x7f4b5c4d 43 | 0x6459415a 44 | 0x6d574a57 45 | 0x527d7b74 46 | 0x5b737079 47 | 0x40616d6e 48 | 0x496f6663 49 | 0xaed5bff8 50 | 0xa7dbb4f5 51 | 0xbcc9a9e2 52 | 0xb5c7a2ef 53 | 0x8aed93cc 54 | 0x83e398c1 55 | 0x98f185d6 56 | 0x91ff8edb 57 | 0xe6a5e790 58 | 0xefabec9d 59 | 0xf4b9f18a 60 | 0xfdb7fa87 61 | 0xc29dcba4 62 | 0xcb93c0a9 63 | 0xd081ddbe 64 | 0xd98fd6b3 65 | 0x734382fe 66 | 0x7a4d89f3 67 | 0x615f94e4 68 | 0x68519fe9 69 | 0x577baeca 70 | 0x5e75a5c7 71 | 0x4567b8d0 72 | 0x4c69b3dd 73 | 0x3b33da96 74 | 0x323dd19b 75 | 0x292fcc8c 76 | 0x2021c781 77 | 0x1f0bf6a2 78 | 0x1605fdaf 79 | 0xd17e0b8 80 | 0x419ebb5 81 | 0xe3a3322e 82 | 0xeaad3923 83 | 0xf1bf2434 84 | 0xf8b12f39 85 | 0xc79b1e1a 86 | 0xce951517 87 | 0xd5870800 88 | 0xdc89030d 89 | 0xabd36a46 90 | 0xa2dd614b 91 | 0xb9cf7c5c 92 | 0xb0c17751 93 | 0x8feb4672 94 | 0x86e54d7f 95 | 0x9df75068 96 | 0x94f95b65 97 | 0x4898f945 98 | 0x4196f248 99 | 0x5a84ef5f 100 | 0x538ae452 101 | 0x6ca0d571 102 | 0x65aede7c 103 | 0x7ebcc36b 104 | 0x77b2c866 105 | 0xe8a12d 106 | 0x9e6aa20 107 | 0x12f4b737 108 | 0x1bfabc3a 109 | 0x24d08d19 110 | 0x2dde8614 111 | 0x36cc9b03 112 | 0x3fc2900e 113 | 0xd8784995 114 | 0xd1764298 115 | 0xca645f8f 116 | 0xc36a5482 117 | 0xfc4065a1 118 | 0xf54e6eac 119 | 0xee5c73bb 120 | 0xe75278b6 121 | 0x900811fd 122 | 0x99061af0 123 | 0x821407e7 124 | 0x8b1a0cea 125 | 0xb4303dc9 126 | 0xbd3e36c4 127 | 0xa62c2bd3 128 | 0xaf2220de 129 | 0xe9af8349 130 | 0xe0a18844 131 | 0xfbb39553 132 | 0xf2bd9e5e 133 | 0xcd97af7d 134 | 0xc499a470 135 | 0xdf8bb967 136 | 0xd685b26a 137 | 0xa1dfdb21 138 | 0xa8d1d02c 139 | 0xb3c3cd3b 140 | 0xbacdc636 141 | 0x85e7f715 142 | 0x8ce9fc18 143 | 0x97fbe10f 144 | 0x9ef5ea02 145 | 0x794f3399 146 | 0x70413894 147 | 0x6b532583 148 | 0x625d2e8e 149 | 0x5d771fad 150 | 0x547914a0 151 | 0x4f6b09b7 152 | 0x466502ba 153 | 0x313f6bf1 154 | 0x383160fc 155 | 0x23237deb 156 | 0x2a2d76e6 157 | 0x150747c5 158 | 0x1c094cc8 159 | 0x71b51df 160 | 0xe155ad2 161 | 0xd274f8f2 162 | 0xdb7af3ff 163 | 0xc068eee8 164 | 0xc966e5e5 165 | 0xf64cd4c6 166 | 0xff42dfcb 167 | 0xe450c2dc 168 | 0xed5ec9d1 169 | 0x9a04a09a 170 | 0x930aab97 171 | 0x8818b680 172 | 0x8116bd8d 173 | 0xbe3c8cae 174 | 0xb73287a3 175 | 0xac209ab4 176 | 0xa52e91b9 177 | 0x42944822 178 | 0x4b9a432f 179 | 0x50885e38 180 | 0x59865535 181 | 0x66ac6416 182 | 0x6fa26f1b 183 | 0x74b0720c 184 | 0x7dbe7901 185 | 0xae4104a 186 | 0x3ea1b47 187 | 0x18f80650 188 | 0x11f60d5d 189 | 0x2edc3c7e 190 | 0x27d23773 191 | 0x3cc02a64 192 | 0x35ce2169 193 | 0x9f027524 194 | 0x960c7e29 195 | 0x8d1e633e 196 | 0x84106833 197 | 0xbb3a5910 198 | 0xb234521d 199 | 0xa9264f0a 200 | 0xa0284407 201 | 0xd7722d4c 202 | 0xde7c2641 203 | 0xc56e3b56 204 | 0xcc60305b 205 | 0xf34a0178 206 | 0xfa440a75 207 | 0xe1561762 208 | 0xe8581c6f 209 | 0xfe2c5f4 210 | 0x6eccef9 211 | 0x1dfed3ee 212 | 0x14f0d8e3 213 | 0x2bdae9c0 214 | 0x22d4e2cd 215 | 0x39c6ffda 216 | 0x30c8f4d7 217 | 0x47929d9c 218 | 0x4e9c9691 219 | 0x558e8b86 220 | 0x5c80808b 221 | 0x63aab1a8 222 | 0x6aa4baa5 223 | 0x71b6a7b2 224 | 0x78b8acbf 225 | 0xa4d90e9f 226 | 0xadd70592 227 | 0xb6c51885 228 | 0xbfcb1388 229 | 0x80e122ab 230 | 0x89ef29a6 231 | 0x92fd34b1 232 | 0x9bf33fbc 233 | 0xeca956f7 234 | 0xe5a75dfa 235 | 0xfeb540ed 236 | 0xf7bb4be0 237 | 0xc8917ac3 238 | 0xc19f71ce 239 | 0xda8d6cd9 240 | 0xd38367d4 241 | 0x3439be4f 242 | 0x3d37b542 243 | 0x2625a855 244 | 0x2f2ba358 245 | 0x1001927b 246 | 0x190f9976 247 | 0x21d8461 248 | 0xb138f6c 249 | 0x7c49e627 250 | 0x7547ed2a 251 | 0x6e55f03d 252 | 0x675bfb30 253 | 0x5871ca13 254 | 0x517fc11e 255 | 0x4a6ddc09 256 | 0x4363d704 -------------------------------------------------------------------------------- /app/src/main/assets/table_s7: -------------------------------------------------------------------------------- 1 | 0xb33a6e73 2 | 0xbe336078 3 | 0xa9287265 4 | 0xa4217c6e 5 | 0x871e565f 6 | 0x8a175854 7 | 0x9d0c4a49 8 | 0x90054442 9 | 0xdb721e2b 10 | 0xd67b1020 11 | 0xc160023d 12 | 0xcc690c36 13 | 0xef562607 14 | 0xe25f280c 15 | 0xf5443a11 16 | 0xf84d341a 17 | 0x63aa8ec3 18 | 0x6ea380c8 19 | 0x79b892d5 20 | 0x74b19cde 21 | 0x578eb6ef 22 | 0x5a87b8e4 23 | 0x4d9caaf9 24 | 0x4095a4f2 25 | 0xbe2fe9b 26 | 0x6ebf090 27 | 0x11f0e28d 28 | 0x1cf9ec86 29 | 0x3fc6c6b7 30 | 0x32cfc8bc 31 | 0x25d4daa1 32 | 0x28ddd4aa 33 | 0x801b508 34 | 0x508bb03 35 | 0x1213a91e 36 | 0x1f1aa715 37 | 0x3c258d24 38 | 0x312c832f 39 | 0x26379132 40 | 0x2b3e9f39 41 | 0x6049c550 42 | 0x6d40cb5b 43 | 0x7a5bd946 44 | 0x7752d74d 45 | 0x546dfd7c 46 | 0x5964f377 47 | 0x4e7fe16a 48 | 0x4376ef61 49 | 0xd89155b8 50 | 0xd5985bb3 51 | 0xc28349ae 52 | 0xcf8a47a5 53 | 0xecb56d94 54 | 0xe1bc639f 55 | 0xf6a77182 56 | 0xfbae7f89 57 | 0xb0d925e0 58 | 0xbdd02beb 59 | 0xaacb39f6 60 | 0xa7c237fd 61 | 0x84fd1dcc 62 | 0x89f413c7 63 | 0x9eef01da 64 | 0x93e60fd1 65 | 0xde4cc385 66 | 0xd345cd8e 67 | 0xc45edf93 68 | 0xc957d198 69 | 0xea68fba9 70 | 0xe761f5a2 71 | 0xf07ae7bf 72 | 0xfd73e9b4 73 | 0xb604b3dd 74 | 0xbb0dbdd6 75 | 0xac16afcb 76 | 0xa11fa1c0 77 | 0x82208bf1 78 | 0x8f2985fa 79 | 0x983297e7 80 | 0x953b99ec 81 | 0xedc2335 82 | 0x3d52d3e 83 | 0x14ce3f23 84 | 0x19c73128 85 | 0x3af81b19 86 | 0x37f11512 87 | 0x20ea070f 88 | 0x2de30904 89 | 0x6694536d 90 | 0x6b9d5d66 91 | 0x7c864f7b 92 | 0x718f4170 93 | 0x52b06b41 94 | 0x5fb9654a 95 | 0x48a27757 96 | 0x45ab795c 97 | 0x657718fe 98 | 0x687e16f5 99 | 0x7f6504e8 100 | 0x726c0ae3 101 | 0x515320d2 102 | 0x5c5a2ed9 103 | 0x4b413cc4 104 | 0x464832cf 105 | 0xd3f68a6 106 | 0x3666ad 107 | 0x172d74b0 108 | 0x1a247abb 109 | 0x391b508a 110 | 0x34125e81 111 | 0x23094c9c 112 | 0x2e004297 113 | 0xb5e7f84e 114 | 0xb8eef645 115 | 0xaff5e458 116 | 0xa2fcea53 117 | 0x81c3c062 118 | 0x8ccace69 119 | 0x9bd1dc74 120 | 0x96d8d27f 121 | 0xddaf8816 122 | 0xd0a6861d 123 | 0xc7bd9400 124 | 0xcab49a0b 125 | 0xe98bb03a 126 | 0xe482be31 127 | 0xf399ac2c 128 | 0xfe90a227 129 | 0x69d62f84 130 | 0x64df218f 131 | 0x73c43392 132 | 0x7ecd3d99 133 | 0x5df217a8 134 | 0x50fb19a3 135 | 0x47e00bbe 136 | 0x4ae905b5 137 | 0x19e5fdc 138 | 0xc9751d7 139 | 0x1b8c43ca 140 | 0x16854dc1 141 | 0x35ba67f0 142 | 0x38b369fb 143 | 0x2fa87be6 144 | 0x22a175ed 145 | 0xb946cf34 146 | 0xb44fc13f 147 | 0xa354d322 148 | 0xae5ddd29 149 | 0x8d62f718 150 | 0x806bf913 151 | 0x9770eb0e 152 | 0x9a79e505 153 | 0xd10ebf6c 154 | 0xdc07b167 155 | 0xcb1ca37a 156 | 0xc615ad71 157 | 0xe52a8740 158 | 0xe823894b 159 | 0xff389b56 160 | 0xf231955d 161 | 0xd2edf4ff 162 | 0xdfe4faf4 163 | 0xc8ffe8e9 164 | 0xc5f6e6e2 165 | 0xe6c9ccd3 166 | 0xebc0c2d8 167 | 0xfcdbd0c5 168 | 0xf1d2dece 169 | 0xbaa584a7 170 | 0xb7ac8aac 171 | 0xa0b798b1 172 | 0xadbe96ba 173 | 0x8e81bc8b 174 | 0x8388b280 175 | 0x9493a09d 176 | 0x999aae96 177 | 0x27d144f 178 | 0xf741a44 179 | 0x186f0859 180 | 0x15660652 181 | 0x36592c63 182 | 0x3b502268 183 | 0x2c4b3075 184 | 0x21423e7e 185 | 0x6a356417 186 | 0x673c6a1c 187 | 0x70277801 188 | 0x7d2e760a 189 | 0x5e115c3b 190 | 0x53185230 191 | 0x4403402d 192 | 0x490a4e26 193 | 0x4a08272 194 | 0x9a98c79 195 | 0x1eb29e64 196 | 0x13bb906f 197 | 0x3084ba5e 198 | 0x3d8db455 199 | 0x2a96a648 200 | 0x279fa843 201 | 0x6ce8f22a 202 | 0x61e1fc21 203 | 0x76faee3c 204 | 0x7bf3e037 205 | 0x58ccca06 206 | 0x55c5c40d 207 | 0x42ded610 208 | 0x4fd7d81b 209 | 0xd43062c2 210 | 0xd9396cc9 211 | 0xce227ed4 212 | 0xc32b70df 213 | 0xe0145aee 214 | 0xed1d54e5 215 | 0xfa0646f8 216 | 0xf70f48f3 217 | 0xbc78129a 218 | 0xb1711c91 219 | 0xa66a0e8c 220 | 0xab630087 221 | 0x885c2ab6 222 | 0x855524bd 223 | 0x924e36a0 224 | 0x9f4738ab 225 | 0xbf9b5909 226 | 0xb2925702 227 | 0xa589451f 228 | 0xa8804b14 229 | 0x8bbf6125 230 | 0x86b66f2e 231 | 0x91ad7d33 232 | 0x9ca47338 233 | 0xd7d32951 234 | 0xdada275a 235 | 0xcdc13547 236 | 0xc0c83b4c 237 | 0xe3f7117d 238 | 0xeefe1f76 239 | 0xf9e50d6b 240 | 0xf4ec0360 241 | 0x6f0bb9b9 242 | 0x6202b7b2 243 | 0x7519a5af 244 | 0x7810aba4 245 | 0x5b2f8195 246 | 0x56268f9e 247 | 0x413d9d83 248 | 0x4c349388 249 | 0x743c9e1 250 | 0xa4ac7ea 251 | 0x1d51d5f7 252 | 0x1058dbfc 253 | 0x3367f1cd 254 | 0x3e6effc6 255 | 0x2975eddb 256 | 0x247ce3d0 -------------------------------------------------------------------------------- /app/src/main/assets/table_s8: -------------------------------------------------------------------------------- 1 | 0xb4469bf0 2 | 0xbf4b92fe 3 | 0xa25c89ec 4 | 0xa95180e2 5 | 0x9872bfc8 6 | 0x937fb6c6 7 | 0x8e68add4 8 | 0x8565a4da 9 | 0xec2ed380 10 | 0xe723da8e 11 | 0xfa34c19c 12 | 0xf139c892 13 | 0xc01af7b8 14 | 0xcb17feb6 15 | 0xd600e5a4 16 | 0xdd0decaa 17 | 0x4960b10 18 | 0xf9b021e 19 | 0x128c190c 20 | 0x19811002 21 | 0x28a22f28 22 | 0x23af2626 23 | 0x3eb83d34 24 | 0x35b5343a 25 | 0x5cfe4360 26 | 0x57f34a6e 27 | 0x4ae4517c 28 | 0x41e95872 29 | 0x70ca6758 30 | 0x7bc76e56 31 | 0x66d07544 32 | 0x6ddd7c4a 33 | 0xcffda02b 34 | 0xc4f0a925 35 | 0xd9e7b237 36 | 0xd2eabb39 37 | 0xe3c98413 38 | 0xe8c48d1d 39 | 0xf5d3960f 40 | 0xfede9f01 41 | 0x9795e85b 42 | 0x9c98e155 43 | 0x818ffa47 44 | 0x8a82f349 45 | 0xbba1cc63 46 | 0xb0acc56d 47 | 0xadbbde7f 48 | 0xa6b6d771 49 | 0x7f2d30cb 50 | 0x742039c5 51 | 0x693722d7 52 | 0x623a2bd9 53 | 0x531914f3 54 | 0x58141dfd 55 | 0x450306ef 56 | 0x4e0e0fe1 57 | 0x274578bb 58 | 0x2c4871b5 59 | 0x315f6aa7 60 | 0x3a5263a9 61 | 0xb715c83 62 | 0x7c558d 63 | 0x1d6b4e9f 64 | 0x16664791 65 | 0x422bed5d 66 | 0x4926e453 67 | 0x5431ff41 68 | 0x5f3cf64f 69 | 0x6e1fc965 70 | 0x6512c06b 71 | 0x7805db79 72 | 0x7308d277 73 | 0x1a43a52d 74 | 0x114eac23 75 | 0xc59b731 76 | 0x754be3f 77 | 0x36778115 78 | 0x3d7a881b 79 | 0x206d9309 80 | 0x2b609a07 81 | 0xf2fb7dbd 82 | 0xf9f674b3 83 | 0xe4e16fa1 84 | 0xefec66af 85 | 0xdecf5985 86 | 0xd5c2508b 87 | 0xc8d54b99 88 | 0xc3d84297 89 | 0xaa9335cd 90 | 0xa19e3cc3 91 | 0xbc8927d1 92 | 0xb7842edf 93 | 0x86a711f5 94 | 0x8daa18fb 95 | 0x90bd03e9 96 | 0x9bb00ae7 97 | 0x3990d686 98 | 0x329ddf88 99 | 0x2f8ac49a 100 | 0x2487cd94 101 | 0x15a4f2be 102 | 0x1ea9fbb0 103 | 0x3bee0a2 104 | 0x8b3e9ac 105 | 0x61f89ef6 106 | 0x6af597f8 107 | 0x77e28cea 108 | 0x7cef85e4 109 | 0x4dccbace 110 | 0x46c1b3c0 111 | 0x5bd6a8d2 112 | 0x50dba1dc 113 | 0x89404666 114 | 0x824d4f68 115 | 0x9f5a547a 116 | 0x94575d74 117 | 0xa574625e 118 | 0xae796b50 119 | 0xb36e7042 120 | 0xb863794c 121 | 0xd1280e16 122 | 0xda250718 123 | 0xc7321c0a 124 | 0xcc3f1504 125 | 0xfd1c2a2e 126 | 0xf6112320 127 | 0xeb063832 128 | 0xe00b313c 129 | 0x439c77b1 130 | 0x48917ebf 131 | 0x558665ad 132 | 0x5e8b6ca3 133 | 0x6fa85389 134 | 0x64a55a87 135 | 0x79b24195 136 | 0x72bf489b 137 | 0x1bf43fc1 138 | 0x10f936cf 139 | 0xdee2ddd 140 | 0x6e324d3 141 | 0x37c01bf9 142 | 0x3ccd12f7 143 | 0x21da09e5 144 | 0x2ad700eb 145 | 0xf34ce751 146 | 0xf841ee5f 147 | 0xe556f54d 148 | 0xee5bfc43 149 | 0xdf78c369 150 | 0xd475ca67 151 | 0xc962d175 152 | 0xc26fd87b 153 | 0xab24af21 154 | 0xa029a62f 155 | 0xbd3ebd3d 156 | 0xb633b433 157 | 0x87108b19 158 | 0x8c1d8217 159 | 0x910a9905 160 | 0x9a07900b 161 | 0x38274c6a 162 | 0x332a4564 163 | 0x2e3d5e76 164 | 0x25305778 165 | 0x14136852 166 | 0x1f1e615c 167 | 0x2097a4e 168 | 0x9047340 169 | 0x604f041a 170 | 0x6b420d14 171 | 0x76551606 172 | 0x7d581f08 173 | 0x4c7b2022 174 | 0x4776292c 175 | 0x5a61323e 176 | 0x516c3b30 177 | 0x88f7dc8a 178 | 0x83fad584 179 | 0x9eedce96 180 | 0x95e0c798 181 | 0xa4c3f8b2 182 | 0xafcef1bc 183 | 0xb2d9eaae 184 | 0xb9d4e3a0 185 | 0xd09f94fa 186 | 0xdb929df4 187 | 0xc68586e6 188 | 0xcd888fe8 189 | 0xfcabb0c2 190 | 0xf7a6b9cc 191 | 0xeab1a2de 192 | 0xe1bcabd0 193 | 0xb5f1011c 194 | 0xbefc0812 195 | 0xa3eb1300 196 | 0xa8e61a0e 197 | 0x99c52524 198 | 0x92c82c2a 199 | 0x8fdf3738 200 | 0x84d23e36 201 | 0xed99496c 202 | 0xe6944062 203 | 0xfb835b70 204 | 0xf08e527e 205 | 0xc1ad6d54 206 | 0xcaa0645a 207 | 0xd7b77f48 208 | 0xdcba7646 209 | 0x52191fc 210 | 0xe2c98f2 211 | 0x133b83e0 212 | 0x18368aee 213 | 0x2915b5c4 214 | 0x2218bcca 215 | 0x3f0fa7d8 216 | 0x3402aed6 217 | 0x5d49d98c 218 | 0x5644d082 219 | 0x4b53cb90 220 | 0x405ec29e 221 | 0x717dfdb4 222 | 0x7a70f4ba 223 | 0x6767efa8 224 | 0x6c6ae6a6 225 | 0xce4a3ac7 226 | 0xc54733c9 227 | 0xd85028db 228 | 0xd35d21d5 229 | 0xe27e1eff 230 | 0xe97317f1 231 | 0xf4640ce3 232 | 0xff6905ed 233 | 0x962272b7 234 | 0x9d2f7bb9 235 | 0x803860ab 236 | 0x8b3569a5 237 | 0xba16568f 238 | 0xb11b5f81 239 | 0xac0c4493 240 | 0xa7014d9d 241 | 0x7e9aaa27 242 | 0x7597a329 243 | 0x6880b83b 244 | 0x638db135 245 | 0x52ae8e1f 246 | 0x59a38711 247 | 0x44b49c03 248 | 0x4fb9950d 249 | 0x26f2e257 250 | 0x2dffeb59 251 | 0x30e8f04b 252 | 0x3be5f945 253 | 0xac6c66f 254 | 0x1cbcf61 255 | 0x1cdcd473 256 | 0x17d1dd7d -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2lib/rtsp/AudioStreamInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2lib.rtsp; 2 | 3 | public class AudioStreamInfo implements MediaStreamInfo { 4 | 5 | private final CompressionType compressionType; 6 | private final AudioFormat audioFormat; 7 | private final int samplesPerFrame; 8 | 9 | private AudioStreamInfo(CompressionType compressionType, AudioFormat audioFormat, int samplesPerFrame) { 10 | this.compressionType = compressionType; 11 | this.audioFormat = audioFormat; 12 | this.samplesPerFrame = samplesPerFrame; 13 | } 14 | 15 | @Override 16 | public StreamType getStreamType() { 17 | return StreamType.AUDIO; 18 | } 19 | 20 | public CompressionType getCompressionType() { 21 | return compressionType; 22 | } 23 | 24 | public AudioFormat getAudioFormat() { 25 | return audioFormat; 26 | } 27 | 28 | public int getSamplesPerFrame() { 29 | return samplesPerFrame; 30 | } 31 | 32 | public enum CompressionType { 33 | LPCM(1), 34 | ALAC(2), 35 | AAC(4), 36 | AAC_ELD(8), 37 | OPUS(32); 38 | 39 | private final int code; 40 | 41 | CompressionType(int code) { 42 | this.code = code; 43 | } 44 | 45 | public static CompressionType fromCode(long code) { 46 | for (CompressionType type : CompressionType.values()) { 47 | if (type.code == code) { 48 | return type; 49 | } 50 | } 51 | throw new IllegalArgumentException("Unknown compression type with code: " + code); 52 | } 53 | } 54 | 55 | public enum AudioFormat { 56 | PCM_8000_16_1(0x4), 57 | PCM_8000_16_2(0x8), 58 | PCM_16000_16_1(0x10), 59 | PCM_16000_16_2(0x20), 60 | PCM_24000_16_1(0x40), 61 | PCM_24000_16_2(0x80), 62 | PCM_32000_16_1(0x100), 63 | PCM_32000_16_2(0x200), 64 | PCM_44100_16_1(0x400), 65 | PCM_44100_16_2(0x800), 66 | PCM_44100_24_1(0x1000), 67 | PCM_44100_24_2(0x2000), 68 | PCM_48000_16_1(0x4000), 69 | PCM_48000_16_2(0x8000), 70 | PCM_48000_24_1(0x10000), 71 | PCM_48000_24_2(0x20000), 72 | ALAC_44100_16_2(0x40000), 73 | ALAC_44100_24_2(0x80000), 74 | ALAC_48000_16_2(0x100000), 75 | ALAC_48000_24_2(0x200000), 76 | AAC_LC_44100_2(0x400000), 77 | AAC_LC_48000_2(0x800000), 78 | AAC_ELD_44100_2(0x1000000), 79 | AAC_ELD_48000_2(0x2000000), 80 | AAC_ELD_16000_1(0x4000000), 81 | AAC_ELD_24000_1(0x8000000), 82 | OPUS_16000_1(0x10000000), 83 | OPUS_24000_1(0x20000000), 84 | OPUS_48000_1(0x40000000), 85 | AAC_ELD_44100_1(0x80000000), 86 | AAC_ELD_48000_1(0x100000000L); 87 | 88 | private final long code; 89 | 90 | AudioFormat(long code) { 91 | this.code = code; 92 | } 93 | 94 | public static AudioFormat fromCode(long code) { 95 | for (AudioFormat format : AudioFormat.values()) { 96 | if (format.code == code) { 97 | return format; 98 | } 99 | } 100 | throw new IllegalArgumentException("Unknown audio format with code: " + code); 101 | } 102 | } 103 | 104 | public static final class AudioStreamInfoBuilder { 105 | private AudioFormat audioFormat; 106 | private CompressionType compressionType; 107 | private int samplesPerFrame; 108 | 109 | public AudioStreamInfoBuilder audioFormat(AudioFormat audioFormat) { 110 | this.audioFormat = audioFormat; 111 | return this; 112 | } 113 | 114 | public AudioStreamInfoBuilder compressionType(CompressionType compressionType) { 115 | this.compressionType = compressionType; 116 | return this; 117 | } 118 | 119 | public AudioStreamInfoBuilder samplesPerFrame(int samplesPerFrame) { 120 | this.samplesPerFrame = samplesPerFrame; 121 | return this; 122 | } 123 | 124 | public AudioStreamInfo build() { 125 | return new AudioStreamInfo(compressionType, audioFormat, samplesPerFrame); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/com/cjx/airplayjavademo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.cjx.airplayjavademo; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.appcompat.app.AppCompatActivity; 5 | 6 | import android.os.Bundle; 7 | import android.util.Log; 8 | import android.view.SurfaceHolder; 9 | import android.view.SurfaceView; 10 | 11 | import com.cjx.airplayjavademo.model.NALPacket; 12 | import com.cjx.airplayjavademo.model.PCMPacket; 13 | import com.cjx.airplayjavademo.player.AudioPlayer; 14 | import com.cjx.airplayjavademo.player.VideoPlayer; 15 | import com.github.serezhka.jap2lib.rtsp.AudioStreamInfo; 16 | import com.github.serezhka.jap2lib.rtsp.VideoStreamInfo; 17 | import com.github.serezhka.jap2server.AirPlayServer; 18 | import com.github.serezhka.jap2server.AirplayDataConsumer; 19 | 20 | import java.util.LinkedList; 21 | 22 | public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback { 23 | 24 | private SurfaceView mSurfaceView; 25 | private AirPlayServer airPlayServer; 26 | private static String TAG = "MainActivity"; 27 | 28 | private VideoPlayer mVideoPlayer; 29 | private AudioPlayer mAudioPlayer; 30 | private final LinkedList mVideoCacheList = new LinkedList<>(); 31 | 32 | 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(R.layout.activity_main); 37 | mSurfaceView = findViewById(R.id.surfaceView); 38 | mSurfaceView.getHolder().addCallback(this); 39 | mAudioPlayer = new AudioPlayer(); 40 | mAudioPlayer.start(); 41 | 42 | airPlayServer = new AirPlayServer("caicai", 7000, 49152, airplayDataConsumer); 43 | 44 | new Thread(new Runnable() { 45 | @Override 46 | public void run() { 47 | try { 48 | airPlayServer.start(); 49 | } catch (Exception e) { 50 | e.printStackTrace(); 51 | } 52 | } 53 | }, "start-server-thread").start(); 54 | 55 | 56 | } 57 | 58 | @Override 59 | protected void onStop() { 60 | super.onStop(); 61 | mAudioPlayer.stopPlay(); 62 | mAudioPlayer = null; 63 | mVideoPlayer.stopVideoPlay(); 64 | mVideoPlayer = null; 65 | airplayDataConsumer = null; 66 | airPlayServer.stop(); 67 | } 68 | 69 | private AirplayDataConsumer airplayDataConsumer = new AirplayDataConsumer() { 70 | @Override 71 | public void onVideo(byte[] video) { 72 | // Logger.i(TAG, "rev video length :%d", video.length); 73 | NALPacket nalPacket=new NALPacket(); 74 | nalPacket.nalData=video; 75 | if (mVideoPlayer != null) { 76 | while (!mVideoCacheList.isEmpty()) { 77 | mVideoPlayer.addPacker(mVideoCacheList.removeFirst()); 78 | } 79 | mVideoPlayer.addPacker(nalPacket); 80 | } else { 81 | mVideoCacheList.add(nalPacket); 82 | } 83 | 84 | } 85 | 86 | @Override 87 | public void onVideoFormat(VideoStreamInfo videoStreamInfo) { 88 | 89 | } 90 | 91 | @Override 92 | public void onAudio(byte[] audio) { 93 | // Logger.i(TAG, "rev audio length :%d", audio.length); 94 | PCMPacket pcmPacket=new PCMPacket(); 95 | pcmPacket.data=audio; 96 | if (mAudioPlayer != null) { 97 | mAudioPlayer.addPacker(pcmPacket); 98 | } 99 | 100 | } 101 | 102 | @Override 103 | public void onAudioFormat(AudioStreamInfo audioInfo) { 104 | 105 | } 106 | }; 107 | 108 | 109 | @Override 110 | public void surfaceCreated(@NonNull SurfaceHolder holder) { 111 | 112 | } 113 | 114 | @Override 115 | public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { 116 | if (mVideoPlayer == null) { 117 | Log.i(TAG, "surfaceChanged: width:" + width + "---height" + height); 118 | mVideoPlayer = new VideoPlayer(holder.getSurface(), width, height); 119 | mVideoPlayer.start(); 120 | } 121 | } 122 | 123 | @Override 124 | public void surfaceDestroyed(@NonNull SurfaceHolder holder) { 125 | 126 | } 127 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2lib/FairPlay.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2lib; 2 | 3 | import net.i2p.crypto.eddsa.Utils; 4 | 5 | //import org.slf4j.Logger; 6 | //import org.slf4j.LoggerFactory; 7 | 8 | import java.io.ByteArrayOutputStream; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.OutputStream; 12 | 13 | class FairPlay { 14 | 15 | // private static final Logger log = LoggerFactory.getLogger(FairPlay.class); 16 | 17 | private final OmgHax omgHax = new OmgHax(); 18 | 19 | private final byte[] keyMsg = new byte[164]; 20 | 21 | void fairPlaySetup(InputStream request, OutputStream response) throws IOException { 22 | // byte[] data = request.readAllBytes(); 23 | byte[] data = readInputStream(request); 24 | if (data[4] != 3) { 25 | // log.error("FairPlay version {} is not supported!", data[4]); 26 | return; 27 | } 28 | if (data.length == 16) { 29 | int mode = data[14]; 30 | byte[][] replyMessage = { 31 | {70, 80, 76, 89, 3, 1, 2, 0, 0, 0, 0, -126, 2, 0, 15, -97, 63, -98, 10, 37, 33, -37, -33, 49, 42, -78, -65, -78, -98, -115, 35, 43, 99, 118, -88, -56, 24, 112, 29, 34, -82, -109, -40, 39, 55, -2, -81, -99, -76, -3, -12, 28, 45, -70, -99, 31, 73, -54, -86, -65, 101, -111, -84, 31, 123, -58, -9, -32, 102, 61, 33, -81, -32, 21, 101, -107, 62, -85, -127, -12, 24, -50, -19, 9, 90, -37, 124, 61, 14, 37, 73, 9, -89, -104, 49, -44, -100, 57, -126, -105, 52, 52, -6, -53, 66, -58, 58, 28, -39, 17, -90, -2, -108, 26, -118, 109, 74, 116, 59, 70, -61, -89, 100, -98, 68, -57, -119, 85, -28, -99, -127, 85, 0, -107, 73, -60, -30, -9, -93, -10, -43, -70}, 32 | {70, 80, 76, 89, 3, 1, 2, 0, 0, 0, 0, -126, 2, 1, -49, 50, -94, 87, 20, -78, 82, 79, -118, -96, -83, 122, -15, 100, -29, 123, -49, 68, 36, -30, 0, 4, 126, -4, 10, -42, 122, -4, -39, 93, -19, 28, 39, 48, -69, 89, 27, -106, 46, -42, 58, -100, 77, -19, -120, -70, -113, -57, -115, -26, 77, -111, -52, -3, 92, 123, 86, -38, -120, -29, 31, 92, -50, -81, -57, 67, 25, -107, -96, 22, 101, -91, 78, 25, 57, -46, 91, -108, -37, 100, -71, -28, 93, -115, 6, 62, 30, 106, -16, 126, -106, 86, 22, 43, 14, -6, 64, 66, 117, -22, 90, 68, -39, 89, 28, 114, 86, -71, -5, -26, 81, 56, -104, -72, 2, 39, 114, 25, -120, 87, 22, 80, -108, 42, -39, 70, 104, -118}, 33 | {70, 80, 76, 89, 3, 1, 2, 0, 0, 0, 0, -126, 2, 2, -63, 105, -93, 82, -18, -19, 53, -79, -116, -35, -100, 88, -42, 79, 22, -63, 81, -102, -119, -21, 83, 23, -67, 13, 67, 54, -51, 104, -10, 56, -1, -99, 1, 106, 91, 82, -73, -6, -110, 22, -78, -74, 84, -126, -57, -124, 68, 17, -127, 33, -94, -57, -2, -40, 61, -73, 17, -98, -111, -126, -86, -41, -47, -116, 112, 99, -30, -92, 87, 85, 89, 16, -81, -98, 14, -4, 118, 52, 125, 22, 64, 67, -128, 127, 88, 30, -28, -5, -28, 44, -87, -34, -36, 27, 94, -78, -93, -86, 61, 46, -51, 89, -25, -18, -25, 11, 54, 41, -14, 42, -3, 22, 29, -121, 115, 83, -35, -71, -102, -36, -114, 7, 0, 110, 86, -8, 80, -50}, 34 | {70, 80, 76, 89, 3, 1, 2, 0, 0, 0, 0, -126, 2, 3, -112, 1, -31, 114, 126, 15, 87, -7, -11, -120, 13, -79, 4, -90, 37, 122, 35, -11, -49, -1, 26, -69, -31, -23, 48, 69, 37, 26, -5, -105, -21, -97, -64, 1, 30, -66, 15, 58, -127, -33, 91, 105, 29, 118, -84, -78, -9, -91, -57, 8, -29, -45, 40, -11, 107, -77, -99, -67, -27, -14, -100, -118, 23, -12, -127, 72, 126, 58, -24, 99, -58, 120, 50, 84, 34, -26, -9, -114, 22, 109, 24, -86, 127, -42, 54, 37, -117, -50, 40, 114, 111, 102, 31, 115, -120, -109, -50, 68, 49, 30, 75, -26, -64, 83, 81, -109, -27, -17, 114, -24, 104, 98, 51, 114, -100, 34, 125, -126, 12, -103, -108, 69, -40, -110, 70, -56, -61, 89}}; 35 | 36 | response.write(replyMessage[mode]); 37 | } else if (data.length == 164) { 38 | System.arraycopy(data, 0, keyMsg, 0, 164); 39 | 40 | byte[] fpHeader = {70, 80, 76, 89, 3, 1, 4, 0, 0, 0, 0, 20}; 41 | response.write(fpHeader); 42 | 43 | response.write(data, 144, 20); 44 | } 45 | } 46 | 47 | public byte[] readInputStream(InputStream inputStream) { 48 | ByteArrayOutputStream outStream = new ByteArrayOutputStream(); 49 | byte[] buffer = new byte[1024]; 50 | int length; 51 | try { 52 | while ((length = inputStream.read(buffer)) != -1) { 53 | outStream.write(buffer, 0, length); 54 | } 55 | } catch (IOException e) { 56 | e.printStackTrace(); 57 | } 58 | 59 | return outStream.toByteArray(); 60 | } 61 | 62 | 63 | byte[] decryptAesKey(byte[] key) { 64 | byte[] aesKey = new byte[16]; 65 | omgHax.decryptAesKey(keyMsg, key, aesKey); 66 | // log.info("FairPlay AES key decrypted: " + Utils.bytesToHex(aesKey)); 67 | return aesKey; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/ControlServer.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal; 2 | 3 | import com.github.serezhka.jap2server.AirplayDataConsumer; 4 | import com.github.serezhka.jap2server.internal.handler.control.FairPlayHandler; 5 | import com.github.serezhka.jap2server.internal.handler.control.HeartBeatHandler; 6 | import com.github.serezhka.jap2server.internal.handler.control.PairingHandler; 7 | import com.github.serezhka.jap2server.internal.handler.control.RTSPHandler; 8 | import com.github.serezhka.jap2server.internal.handler.mirroring.MirroringHandler; 9 | import com.github.serezhka.jap2server.internal.handler.session.SessionManager; 10 | import io.netty.bootstrap.ServerBootstrap; 11 | import io.netty.channel.ChannelFuture; 12 | import io.netty.channel.ChannelInitializer; 13 | import io.netty.channel.ChannelOption; 14 | import io.netty.channel.EventLoopGroup; 15 | import io.netty.channel.epoll.Epoll; 16 | import io.netty.channel.epoll.EpollEventLoopGroup; 17 | import io.netty.channel.epoll.EpollServerSocketChannel; 18 | import io.netty.channel.nio.NioEventLoopGroup; 19 | import io.netty.channel.socket.ServerSocketChannel; 20 | import io.netty.channel.socket.SocketChannel; 21 | import io.netty.channel.socket.nio.NioServerSocketChannel; 22 | import io.netty.handler.codec.http.HttpObjectAggregator; 23 | import io.netty.handler.codec.rtsp.RtspDecoder; 24 | import io.netty.handler.codec.rtsp.RtspEncoder; 25 | import io.netty.handler.logging.LogLevel; 26 | import io.netty.handler.logging.LoggingHandler; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | import java.net.InetSocketAddress; 31 | 32 | public class ControlServer implements Runnable { 33 | 34 | private static final Logger log = LoggerFactory.getLogger(MirroringHandler.class); 35 | 36 | private final PairingHandler pairingHandler; 37 | private final FairPlayHandler fairPlayHandler; 38 | private final RTSPHandler rtspHandler; 39 | private final HeartBeatHandler heartBeatHandler; 40 | 41 | private final int airTunesPort; 42 | 43 | public ControlServer(int airPlayPort, int airTunesPort, AirplayDataConsumer airplayDataConsumer) { 44 | this.airTunesPort = airTunesPort; 45 | SessionManager sessionManager = new SessionManager(); 46 | pairingHandler = new PairingHandler(sessionManager); 47 | fairPlayHandler = new FairPlayHandler(sessionManager); 48 | rtspHandler = new RTSPHandler(airPlayPort, airTunesPort, sessionManager, airplayDataConsumer); 49 | heartBeatHandler = new HeartBeatHandler(sessionManager); 50 | } 51 | 52 | @Override 53 | public void run() { 54 | ServerBootstrap serverBootstrap = new ServerBootstrap(); 55 | EventLoopGroup bossGroup = eventLoopGroup(); 56 | EventLoopGroup workerGroup = eventLoopGroup(); 57 | try { 58 | serverBootstrap 59 | .group(bossGroup, workerGroup) 60 | .channel(serverSocketChannelClass()) 61 | .localAddress(new InetSocketAddress(airTunesPort)) 62 | .childHandler(new ChannelInitializer() { 63 | @Override 64 | public void initChannel(final SocketChannel ch) throws Exception { 65 | ch.pipeline().addLast( 66 | new RtspDecoder(), 67 | new RtspEncoder(), 68 | new HttpObjectAggregator(64 * 1024), 69 | new LoggingHandler(LogLevel.DEBUG), 70 | pairingHandler, 71 | fairPlayHandler, 72 | rtspHandler, 73 | heartBeatHandler); 74 | } 75 | }) 76 | .childOption(ChannelOption.TCP_NODELAY, true) 77 | .childOption(ChannelOption.SO_REUSEADDR, true) 78 | .childOption(ChannelOption.SO_KEEPALIVE, true); 79 | ChannelFuture channelFuture = serverBootstrap.bind().sync(); 80 | log.info("Control server listening on port: {}", airTunesPort); 81 | channelFuture.channel().closeFuture().sync(); 82 | } catch (InterruptedException e) { 83 | throw new RuntimeException(e); 84 | } finally { 85 | log.info("Control server stopped"); 86 | bossGroup.shutdownGracefully(); 87 | workerGroup.shutdownGracefully(); 88 | } 89 | } 90 | 91 | private EventLoopGroup eventLoopGroup() { 92 | return Epoll.isAvailable() ? new EpollEventLoopGroup() : new NioEventLoopGroup(); 93 | } 94 | 95 | private Class serverSocketChannelClass() { 96 | return Epoll.isAvailable() ? EpollServerSocketChannel.class : NioServerSocketChannel.class; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/handler/mirroring/MirroringHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal.handler.mirroring; 2 | 3 | import com.github.serezhka.jap2lib.AirPlay; 4 | import com.github.serezhka.jap2server.AirplayDataConsumer; 5 | import io.netty.buffer.ByteBuf; 6 | import io.netty.buffer.ByteBufAllocator; 7 | import io.netty.channel.ChannelHandlerContext; 8 | import io.netty.channel.SimpleChannelInboundHandler; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | public class MirroringHandler extends SimpleChannelInboundHandler { 13 | 14 | private static final Logger log = LoggerFactory.getLogger(MirroringHandler.class); 15 | 16 | private final ByteBuf headerBuf = ByteBufAllocator.DEFAULT.ioBuffer(128, 128); 17 | private final AirPlay airPlay; 18 | private final AirplayDataConsumer dataConsumer; 19 | 20 | private MirroringHeader header; 21 | private ByteBuf payload; 22 | 23 | public MirroringHandler(AirPlay airPlay, AirplayDataConsumer dataConsumer) { 24 | this.airPlay = airPlay; 25 | this.dataConsumer = dataConsumer; 26 | } 27 | 28 | @Override 29 | protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) { 30 | while (msg.isReadable()) { 31 | 32 | if (header == null) { 33 | msg.readBytes(headerBuf, Math.min(headerBuf.writableBytes(), msg.readableBytes())); 34 | if (headerBuf.writableBytes() == 0) { 35 | header = new MirroringHeader(headerBuf); 36 | headerBuf.clear(); 37 | } 38 | } 39 | 40 | if (header != null && msg.readableBytes() > 0) { 41 | 42 | if (payload == null || payload.writableBytes() == 0) { 43 | payload = ctx.alloc().directBuffer(header.getPayloadSize(), header.getPayloadSize()); 44 | } 45 | 46 | msg.readBytes(payload, Math.min(payload.writableBytes(), msg.readableBytes())); 47 | 48 | if (payload.writableBytes() == 0) { 49 | 50 | byte[] payloadBytes = new byte[header.getPayloadSize()]; 51 | payload.readBytes(payloadBytes); 52 | 53 | try { 54 | if (header.getPayloadType() == 0) { 55 | airPlay.decryptVideo(payloadBytes); 56 | processVideo(payloadBytes); 57 | } else if (header.getPayloadType() == 1) { 58 | processSPSPPS(payload); 59 | } else { 60 | log.debug("Unhandled payload type: {}", header.getPayloadType()); 61 | } 62 | } catch (Exception e) { 63 | e.printStackTrace(); 64 | } 65 | 66 | payload.release(); 67 | payload = null; 68 | header = null; 69 | } 70 | } 71 | } 72 | } 73 | 74 | private void processVideo(byte[] payload) { 75 | 76 | // TODO One nalu per packet? 77 | int nalu_size = 0; 78 | while (nalu_size < payload.length) { 79 | int nc_len = (payload[nalu_size + 3] & 0xFF) | ((payload[nalu_size + 2] & 0xFF) << 8) | ((payload[nalu_size + 1] & 0xFF) << 16) | ((payload[nalu_size] & 0xFF) << 24); 80 | log.debug("payload len: {}, nc_len: {}, nalu_type: {}", payload.length, nc_len, payload[4] & 0x1f); 81 | if (nc_len > 0) { 82 | payload[nalu_size] = 0; 83 | payload[nalu_size + 1] = 0; 84 | payload[nalu_size + 2] = 0; 85 | payload[nalu_size + 3] = 1; 86 | nalu_size += nc_len + 4; 87 | } 88 | if (payload.length - nc_len > 4) { 89 | log.error("Decrypt error!"); 90 | return; 91 | } 92 | } 93 | 94 | dataConsumer.onVideo(payload); 95 | } 96 | 97 | private void processSPSPPS(ByteBuf payload) { 98 | payload.readerIndex(6); 99 | 100 | short spsLen = (short) payload.readUnsignedShort(); 101 | byte[] sequenceParameterSet = new byte[spsLen]; 102 | payload.readBytes(sequenceParameterSet); 103 | 104 | payload.skipBytes(1); // pps count 105 | 106 | short ppsLen = (short) payload.readUnsignedShort(); 107 | byte[] pictureParameterSet = new byte[ppsLen]; 108 | payload.readBytes(pictureParameterSet); 109 | 110 | int spsPpsLen = spsLen + ppsLen + 8; 111 | log.info("SPS PPS length: {}", spsPpsLen); 112 | byte[] spsPps = new byte[spsPpsLen]; 113 | spsPps[0] = 0; 114 | spsPps[1] = 0; 115 | spsPps[2] = 0; 116 | spsPps[3] = 1; 117 | System.arraycopy(sequenceParameterSet, 0, spsPps, 4, spsLen); 118 | spsPps[spsLen + 4] = 0; 119 | spsPps[spsLen + 5] = 0; 120 | spsPps[spsLen + 6] = 0; 121 | spsPps[spsLen + 7] = 1; 122 | System.arraycopy(pictureParameterSet, 0, spsPps, 8 + spsLen, ppsLen); 123 | 124 | dataConsumer.onVideo(spsPps); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2lib/AirPlay.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2lib; 2 | 3 | import android.util.Log; 4 | 5 | import com.github.serezhka.jap2lib.rtsp.MediaStreamInfo; 6 | 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | 10 | /** 11 | * Responds on pairing setup, fairplay setup requests, decrypts data 12 | */ 13 | public class AirPlay { 14 | 15 | private final Pairing pairing; 16 | private final FairPlay fairplay; 17 | private final RTSP rtsp; 18 | 19 | private FairPlayVideoDecryptor fairPlayVideoDecryptor; 20 | private FairPlayAudioDecryptor fairPlayAudioDecryptor; 21 | 22 | public AirPlay() { 23 | pairing = new Pairing(); 24 | fairplay = new FairPlay(); 25 | rtsp = new RTSP(); 26 | } 27 | 28 | /** 29 | * {@code /info} 30 | *

31 | * Writes server info to output stream 32 | */ 33 | public void info(OutputStream out) throws Exception { 34 | pairing.info(out); 35 | } 36 | 37 | /** 38 | * {@code /pair-setup} 39 | *

40 | * Writes EdDSA public key bytes to output stream 41 | */ 42 | public void pairSetup(OutputStream out) throws Exception { 43 | pairing.pairSetup(out); 44 | } 45 | 46 | /** 47 | * {@code /pair-verify} 48 | *

49 | * On first request writes curve25519 public key + encrypted signature bytes to output stream; 50 | * On second request verifies signature 51 | */ 52 | public void pairVerify(InputStream in, OutputStream out) throws Exception { 53 | pairing.pairVerify(in, out); 54 | } 55 | 56 | /** 57 | * Pair was verified successfully 58 | */ 59 | public boolean isPairVerified() { 60 | return pairing.isPairVerified(); 61 | } 62 | 63 | /** 64 | * {@code /fp-setup} 65 | *

66 | * Writes fp-setup response bytes to output stream 67 | */ 68 | public void fairPlaySetup(InputStream in, OutputStream out) throws Exception { 69 | fairplay.fairPlaySetup(in, out); 70 | } 71 | 72 | /** 73 | * Retrieves information about media stream from RTSP SETUP request 74 | * 75 | * @return null if there's no stream info 76 | */ 77 | public MediaStreamInfo rtspGetMediaStreamInfo(InputStream in) throws Exception { 78 | return rtsp.getMediaStreamInfo(in); 79 | } 80 | 81 | public int rtspSetParameterInfo(InputStream in) throws Exception { 82 | int volume = rtsp.getSetParameterVolume(in); 83 | return volume; 84 | } 85 | 86 | 87 | /** 88 | * {@code RTSP SETUP ENCRYPTION} 89 | *

90 | * Retrieves encrypted EAS key and IV 91 | */ 92 | public void rtspSetupEncryption(InputStream in) throws Exception { 93 | rtsp.setup(in); 94 | } 95 | 96 | /** 97 | * {@code RTSP SETUP VIDEO} 98 | *

99 | * Writes video event, data and timing ports info to output stream 100 | */ 101 | public void rtspSetupVideo(OutputStream out, int videoDataPort, int videoEventPort, int videoTimingPort) throws Exception { 102 | rtsp.setupVideo(out, videoDataPort, videoEventPort, videoTimingPort); 103 | } 104 | 105 | /** 106 | * {@code RTSP SETUP AUDIO} 107 | *

108 | * Writes audio control and data ports info to output stream 109 | */ 110 | public void rtspSetupAudio(OutputStream out, int audioDataPort, int audioControlPort) throws Exception { 111 | rtsp.setupAudio(out, audioDataPort, audioControlPort); 112 | } 113 | 114 | public byte[] getFairPlayAesKey() { 115 | return fairplay.decryptAesKey(rtsp.getEncryptedAESKey()); 116 | } 117 | 118 | /** 119 | * @return {@code true} if we got shared secret during pairing, ekey & stream connection id during RTSP SETUP 120 | */ 121 | public boolean isFairPlayVideoDecryptorReady() { 122 | return pairing.getSharedSecret() != null && rtsp.getEncryptedAESKey() != null && rtsp.getStreamConnectionID() != null; 123 | } 124 | 125 | /** 126 | * @return {@code true} if we got shared secret during pairing, ekey & eiv during RTSP SETUP 127 | */ 128 | public boolean isFairPlayAudioDecryptorReady() { 129 | return pairing.getSharedSecret() != null && rtsp.getEncryptedAESKey() != null && rtsp.getEiv() != null; 130 | } 131 | 132 | public void decryptVideo(byte[] video) throws Exception { 133 | if (fairPlayVideoDecryptor == null) { 134 | if (!isFairPlayVideoDecryptorReady()) { 135 | throw new IllegalStateException("FairPlayVideoDecryptor not ready!"); 136 | } 137 | fairPlayVideoDecryptor = new FairPlayVideoDecryptor(getFairPlayAesKey(), pairing.getSharedSecret(), rtsp.getStreamConnectionID()); 138 | } 139 | fairPlayVideoDecryptor.decrypt(video); 140 | } 141 | 142 | public void decryptAudio(byte[] audio, int audioLength) throws Exception { 143 | if (fairPlayAudioDecryptor == null) { 144 | if (!isFairPlayAudioDecryptorReady()) { 145 | throw new IllegalStateException("FairPlayAudioDecryptor not ready!"); 146 | } 147 | fairPlayAudioDecryptor = new FairPlayAudioDecryptor(getFairPlayAesKey(), rtsp.getEiv(), pairing.getSharedSecret()); 148 | } 149 | fairPlayAudioDecryptor.decrypt(audio, audioLength); 150 | } 151 | 152 | public void printPlist(String methodName, InputStream inputStream) { 153 | rtsp.printPlist(methodName, inputStream); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2lib/Pairing.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2lib; 2 | 3 | import android.content.res.AssetManager; 4 | import android.util.Log; 5 | 6 | import com.cjx.airplayjavademo.MyApplication; 7 | import com.dd.plist.BinaryPropertyListWriter; 8 | import com.dd.plist.NSObject; 9 | import com.dd.plist.PropertyListParser; 10 | 11 | import net.i2p.crypto.eddsa.EdDSAEngine; 12 | import net.i2p.crypto.eddsa.EdDSAPublicKey; 13 | import net.i2p.crypto.eddsa.KeyPairGenerator; 14 | import net.i2p.crypto.eddsa.Utils; 15 | import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; 16 | import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; 17 | //import org.slf4j.Logger; 18 | //import org.slf4j.LoggerFactory; 19 | import org.whispersystems.curve25519.Curve25519; 20 | import org.whispersystems.curve25519.Curve25519KeyPair; 21 | 22 | import javax.crypto.BadPaddingException; 23 | import javax.crypto.Cipher; 24 | import javax.crypto.IllegalBlockSizeException; 25 | import javax.crypto.NoSuchPaddingException; 26 | import javax.crypto.spec.IvParameterSpec; 27 | import javax.crypto.spec.SecretKeySpec; 28 | 29 | import java.io.IOException; 30 | import java.io.InputStream; 31 | import java.io.OutputStream; 32 | import java.net.URL; 33 | import java.nio.charset.StandardCharsets; 34 | import java.security.*; 35 | import java.util.Arrays; 36 | 37 | class Pairing { 38 | 39 | // private static final Logger log = LoggerFactory.getLogger(Pairing.class); 40 | 41 | private final KeyPair keyPair; 42 | 43 | private byte[] edTheirs; 44 | private byte[] ecdhOurs; 45 | private byte[] ecdhTheirs; 46 | private byte[] ecdhSecret; 47 | 48 | private boolean pairVerified; 49 | private AssetManager am = MyApplication.getAppContext().getAssets(); 50 | 51 | 52 | Pairing() { 53 | this.keyPair = new KeyPairGenerator().generateKeyPair(); 54 | } 55 | 56 | void info(OutputStream out) throws Exception { 57 | InputStream is = am.open("info-response.xml"); 58 | NSObject serverInfo = PropertyListParser.parse(is); 59 | BinaryPropertyListWriter.write(out, serverInfo); 60 | } 61 | 62 | void pairSetup(OutputStream out) throws IOException { 63 | out.write(((EdDSAPublicKey) keyPair.getPublic()).getAbyte()); 64 | } 65 | 66 | @SuppressWarnings("ResultOfMethodCallIgnored") 67 | void pairVerify(InputStream request, OutputStream response) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, SignatureException, BadPaddingException, IllegalBlockSizeException, IOException { 68 | int flag = request.read(); 69 | request.skip(3); 70 | if (flag > 0) { 71 | request.read(ecdhTheirs = new byte[32]); 72 | request.read(edTheirs = new byte[32]); 73 | 74 | Curve25519 curve25519 = Curve25519.getInstance(Curve25519.BEST); 75 | Curve25519KeyPair curve25519KeyPair = curve25519.generateKeyPair(); 76 | 77 | ecdhOurs = curve25519KeyPair.getPublicKey(); 78 | ecdhSecret = curve25519.calculateAgreement(ecdhTheirs, curve25519KeyPair.getPrivateKey()); 79 | // log.info("Shared secret: " + Utils.bytesToHex(ecdhSecret)); 80 | 81 | Cipher aesCtr128Encrypt = initCipher(); 82 | 83 | byte[] dataToSign = new byte[64]; 84 | System.arraycopy(ecdhOurs, 0, dataToSign, 0, 32); 85 | System.arraycopy(ecdhTheirs, 0, dataToSign, 32, 32); 86 | 87 | EdDSAEngine edDSAEngine = new EdDSAEngine(); 88 | edDSAEngine.initSign(keyPair.getPrivate()); 89 | byte[] signature = edDSAEngine.signOneShot(dataToSign); 90 | 91 | byte[] encryptedSignature = aesCtr128Encrypt.doFinal(signature); 92 | 93 | byte[] responseContent = new byte[ecdhOurs.length + encryptedSignature.length]; 94 | System.arraycopy(ecdhOurs, 0, responseContent, 0, ecdhOurs.length); 95 | System.arraycopy(encryptedSignature, 0, responseContent, ecdhOurs.length, encryptedSignature.length); 96 | 97 | response.write(responseContent); 98 | } else { 99 | byte[] signature = new byte[64]; 100 | request.read(signature); 101 | 102 | Cipher aesCtr128Encrypt = initCipher(); 103 | 104 | byte[] sigBuffer = new byte[64]; 105 | aesCtr128Encrypt.update(sigBuffer); 106 | sigBuffer = aesCtr128Encrypt.doFinal(signature); 107 | 108 | byte[] sigMessage = new byte[64]; 109 | System.arraycopy(ecdhTheirs, 0, sigMessage, 0, 32); 110 | System.arraycopy(ecdhOurs, 0, sigMessage, 32, 32); 111 | 112 | EdDSAEngine edDSAEngine = new EdDSAEngine(); 113 | EdDSAPublicKey edDSAPublicKey = new EdDSAPublicKey(new EdDSAPublicKeySpec(edTheirs, EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519))); 114 | edDSAEngine.initVerify(edDSAPublicKey); 115 | 116 | pairVerified = edDSAEngine.verifyOneShot(sigMessage, sigBuffer); 117 | // log.info("Pair verified: " + pairVerified); 118 | } 119 | } 120 | 121 | boolean isPairVerified() { 122 | return pairVerified; 123 | } 124 | 125 | byte[] getSharedSecret() { 126 | return ecdhSecret; 127 | } 128 | 129 | private Cipher initCipher() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException { 130 | MessageDigest sha512Digest = MessageDigest.getInstance("SHA-512"); 131 | sha512Digest.update("Pair-Verify-AES-Key".getBytes(StandardCharsets.UTF_8)); 132 | sha512Digest.update(ecdhSecret); 133 | byte[] sharedSecretSha512AesKey = Arrays.copyOfRange(sha512Digest.digest(), 0, 16); 134 | 135 | sha512Digest.update("Pair-Verify-AES-IV".getBytes(StandardCharsets.UTF_8)); 136 | sha512Digest.update(ecdhSecret); 137 | byte[] sharedSecretSha512AesIV = Arrays.copyOfRange(sha512Digest.digest(), 0, 16); 138 | 139 | Cipher aesCtr128Encrypt = Cipher.getInstance("AES/CTR/NoPadding"); 140 | aesCtr128Encrypt.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sharedSecretSha512AesKey, "AES"), new IvParameterSpec(sharedSecretSha512AesIV)); 141 | return aesCtr128Encrypt; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/java/com/cjx/airplayjavademo/player/VideoPlayer.java: -------------------------------------------------------------------------------- 1 | package com.cjx.airplayjavademo.player; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.media.MediaCodec; 5 | import android.media.MediaFormat; 6 | import android.os.Handler; 7 | import android.os.HandlerThread; 8 | import android.util.Log; 9 | import android.view.Surface; 10 | 11 | 12 | import com.cjx.airplayjavademo.model.NALPacket; 13 | 14 | import java.nio.ByteBuffer; 15 | import java.util.concurrent.BlockingQueue; 16 | import java.util.concurrent.LinkedBlockingQueue; 17 | 18 | public class VideoPlayer { 19 | private static final String TAG = "VideoPlayer"; 20 | private static final String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC; 21 | private final int mVideoWidth = 540; 22 | private final int mVideoHeight = 960; 23 | private final MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo(); 24 | private MediaCodec mDecoder = null; 25 | private final Surface mSurface; 26 | private BlockingQueue packets = new LinkedBlockingQueue<>(500); 27 | private final HandlerThread mDecodeThread = new HandlerThread("VideoDecoder"); 28 | 29 | private final MediaCodec.Callback mDecoderCallback = new MediaCodec.Callback() { 30 | @Override 31 | public void onInputBufferAvailable(MediaCodec codec, int index) { 32 | try { 33 | NALPacket packet = packets.take(); 34 | codec.getInputBuffer(index).put(packet.nalData); 35 | mDecoder.queueInputBuffer(index, 0, packet.nalData.length, packet.pts, 0); 36 | } catch (InterruptedException e) { 37 | throw new IllegalStateException("Interrupted when is waiting"); 38 | } catch (IllegalStateException e) { 39 | e.printStackTrace(); 40 | } 41 | } 42 | 43 | @Override 44 | public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) { 45 | try { 46 | codec.releaseOutputBuffer(index, true); 47 | } catch (IllegalStateException e) { 48 | e.printStackTrace(); 49 | } 50 | } 51 | 52 | @Override 53 | public void onError(MediaCodec codec, MediaCodec.CodecException e) { 54 | Log.e(TAG, "Decode error", e); 55 | } 56 | 57 | @Override 58 | public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { 59 | 60 | } 61 | }; 62 | 63 | public VideoPlayer(Surface surface, int width, int heigth) { 64 | // this.mVideoWidth=width; 65 | // this.mVideoHeight=heigth; 66 | mSurface = surface; 67 | } 68 | 69 | public void initDecoder() { 70 | mDecodeThread.start(); 71 | try { 72 | // 解码分辨率 73 | Log.i(TAG, "initDecoder: mVideoWidth=" + mVideoWidth + "---mVideoHeight=" + mVideoHeight); 74 | mDecoder = MediaCodec.createDecoderByType(MIME_TYPE); 75 | MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mVideoWidth, mVideoHeight); 76 | mDecoder.configure(format, mSurface, null, 0); 77 | mDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT); 78 | mDecoder.setCallback(mDecoderCallback, new Handler(mDecodeThread.getLooper())); 79 | mDecoder.start(); 80 | } catch (Exception e) { 81 | e.printStackTrace(); 82 | } 83 | } 84 | 85 | public void addPacker(NALPacket nalPacket) { 86 | try { 87 | packets.put(nalPacket); 88 | } catch (InterruptedException e) { 89 | // 队列满了 90 | Log.e(TAG, "run: put error:", e); 91 | } 92 | } 93 | 94 | public void start() { 95 | initDecoder(); 96 | } 97 | 98 | public void stopVideoPlay() { 99 | try { 100 | mDecoder.stop(); 101 | } catch (Exception e) { 102 | e.printStackTrace(); 103 | } 104 | try { 105 | mDecoder.release(); 106 | } catch (Exception e) { 107 | e.printStackTrace(); 108 | } 109 | mDecodeThread.quit(); 110 | packets.clear(); 111 | } 112 | 113 | private void doDecode(NALPacket nalPacket) throws IllegalStateException { 114 | final long timeoutUsec = 10000; 115 | // Log.i(TAG, "doDecode: start"); 116 | if (nalPacket.nalData == null) { 117 | Log.w(TAG, "doDecode: data is null return"); 118 | return; 119 | } 120 | //获取MediaCodec的输入流 121 | ByteBuffer[] decoderInputBuffers = mDecoder.getInputBuffers(); 122 | int inputBufIndex = -10000; 123 | try { 124 | inputBufIndex = mDecoder.dequeueInputBuffer(timeoutUsec);//设置解码等待时间,0为不等待,-1为一直等待,其余为时间单位 125 | } catch (Exception e) { 126 | Log.e(TAG, "dequeueInputBuffer error", e); 127 | } 128 | if (inputBufIndex >= 0) { 129 | ByteBuffer inputBuf = decoderInputBuffers[inputBufIndex]; 130 | inputBuf.put(nalPacket.nalData); 131 | // 输入流入队列 132 | mDecoder.queueInputBuffer(inputBufIndex, 0, nalPacket.nalData.length, nalPacket.pts, 0); 133 | } else { 134 | Log.d(TAG, "dequeueInputBuffer failed"); 135 | } 136 | decode(timeoutUsec); 137 | // workHandler.post(new Runnable() { 138 | // @Override 139 | // public void run() { 140 | // decode(TIMEOUT_USEC); 141 | // } 142 | // }); 143 | // Log.i(TAG, "doDecode: end"); 144 | } 145 | 146 | @SuppressLint("WrongConstant") 147 | private void decode(long timeoutUsec) { 148 | int outputBufferIndex = -10000; 149 | try { 150 | outputBufferIndex = mDecoder.dequeueOutputBuffer(mBufferInfo, timeoutUsec); 151 | } catch (Exception e) { 152 | Log.e(TAG, "doDecode: dequeueOutputBuffer error:" + e.getMessage()); 153 | } 154 | if (outputBufferIndex >= 0) { 155 | mDecoder.releaseOutputBuffer(outputBufferIndex, true); 156 | // try { 157 | // Thread.sleep(50); 158 | // } catch (InterruptedException ie) { 159 | // ie.printStackTrace(); 160 | // } 161 | } else if (outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { 162 | // try { 163 | // Thread.sleep(10); 164 | // } catch (InterruptedException ie) { 165 | // ie.printStackTrace(); 166 | // } 167 | } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { 168 | // not important for us, since we're using Surface 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2lib/RTSP.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2lib; 2 | 3 | import android.util.Log; 4 | 5 | import com.dd.plist.BinaryPropertyListParser; 6 | import com.dd.plist.BinaryPropertyListWriter; 7 | import com.dd.plist.NSArray; 8 | import com.dd.plist.NSDictionary; 9 | import com.dd.plist.NSObject; 10 | import com.dd.plist.NSString; 11 | import com.dd.plist.PropertyListFormatException; 12 | import com.github.serezhka.jap2lib.rtsp.AudioStreamInfo; 13 | import com.github.serezhka.jap2lib.rtsp.MediaStreamInfo; 14 | import com.github.serezhka.jap2lib.rtsp.VideoStreamInfo; 15 | 16 | import net.i2p.crypto.eddsa.Utils; 17 | 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import java.io.BufferedReader; 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.io.InputStreamReader; 25 | import java.io.OutputStream; 26 | import java.io.Reader; 27 | import java.nio.charset.Charset; 28 | import java.nio.charset.StandardCharsets; 29 | import java.util.HashMap; 30 | 31 | class RTSP { 32 | 33 | private static final Logger log = LoggerFactory.getLogger(RTSP.class); 34 | private static String TAG = "RTSP"; 35 | 36 | private String streamConnectionID; 37 | private byte[] encryptedAESKey; 38 | private byte[] eiv; 39 | 40 | MediaStreamInfo getMediaStreamInfo(InputStream rtspSetupPayload) throws Exception { 41 | NSDictionary rtspSetup = (NSDictionary) BinaryPropertyListParser.parse(rtspSetupPayload); 42 | if (rtspSetup.containsKey("streams")) { 43 | // assume one stream info per RTSP SETUP request 44 | HashMap stream = (HashMap) ((Object[]) rtspSetup.get("streams").toJavaObject())[0]; 45 | int type = (int) stream.get("type"); 46 | switch (type) { 47 | 48 | // video 49 | case 110: 50 | Log.i(TAG, "video plist: " + rtspSetup.toXMLPropertyList()); 51 | 52 | if (stream.containsKey("streamConnectionID")) { 53 | streamConnectionID = Long.toUnsignedString((long) stream.get("streamConnectionID")); 54 | } 55 | return new VideoStreamInfo(streamConnectionID); 56 | 57 | // audio 58 | case 96: 59 | Log.i(TAG, "audio plist: " + rtspSetup.toXMLPropertyList()); 60 | 61 | AudioStreamInfo.AudioStreamInfoBuilder builder = new AudioStreamInfo.AudioStreamInfoBuilder(); 62 | if (stream.containsKey("ct")) { 63 | int compressionType = (int) stream.get("ct"); 64 | builder.compressionType(AudioStreamInfo.CompressionType.fromCode(compressionType)); 65 | } 66 | if (stream.containsKey("audioFormat")) { 67 | long audioFormatCode = (int) stream.get("audioFormat"); // FIXME int or long ?! 68 | builder.audioFormat(AudioStreamInfo.AudioFormat.fromCode(audioFormatCode)); 69 | } 70 | if (stream.containsKey("spf")) { 71 | int samplesPerFrame = (int) stream.get("spf"); 72 | builder.samplesPerFrame(samplesPerFrame); 73 | } 74 | return builder.build(); 75 | 76 | default: 77 | log.error("Unknown stream type: {}", type); 78 | } 79 | } else { 80 | Log.w(TAG, "getMediaStreamInfo: other type"); 81 | } 82 | return null; 83 | } 84 | 85 | int getSetParameterVolume(InputStream in) throws IOException, PropertyListFormatException { 86 | StringBuilder textBuilder = new StringBuilder(); 87 | try (Reader reader = new BufferedReader(new InputStreamReader 88 | (in, Charset.forName(StandardCharsets.UTF_8.name())))) { 89 | int c = 0; 90 | while ((c = reader.read()) != -1) { 91 | textBuilder.append((char) c); 92 | } 93 | } 94 | String data = textBuilder.toString().trim(); 95 | // ios 音量 -30----》0由小变大 96 | int volume = -15; 97 | try { 98 | volume = (int) Float.parseFloat(data.split(":")[1]); 99 | } catch (Exception e) { 100 | e.printStackTrace(); 101 | } 102 | Log.i(TAG, "getSetParameterVolume: " + volume); 103 | return volume; 104 | } 105 | 106 | void setup(InputStream in) throws Exception { 107 | NSDictionary request = (NSDictionary) BinaryPropertyListParser.parse(in); 108 | 109 | if (request.containsKey("ekey")) { 110 | encryptedAESKey = (byte[]) request.get("ekey").toJavaObject(); 111 | log.info("Encrypted AES key: " + Utils.bytesToHex(encryptedAESKey)); 112 | } 113 | 114 | if (request.containsKey("eiv")) { 115 | eiv = (byte[]) request.get("eiv").toJavaObject(); 116 | log.info("AES eiv: " + Utils.bytesToHex(eiv)); 117 | } 118 | } 119 | 120 | void setupVideo(OutputStream out, int videoDataPort, int videoEventPort, int videoTimingPort) throws IOException { 121 | NSArray streams = new NSArray(1); 122 | NSDictionary dataStream = new NSDictionary(); 123 | dataStream.put("dataPort", videoDataPort); 124 | dataStream.put("type", 110); 125 | streams.setValue(0, dataStream); 126 | 127 | NSDictionary response = new NSDictionary(); 128 | response.put("streams", streams); 129 | response.put("eventPort", videoEventPort); 130 | response.put("timingPort", videoTimingPort); 131 | BinaryPropertyListWriter.write(out, response); 132 | } 133 | 134 | void setupAudio(OutputStream out, int audioDataPort, int audioControlPort) throws IOException { 135 | NSArray streams = new NSArray(1); 136 | NSDictionary dataStream = new NSDictionary(); 137 | dataStream.put("dataPort", audioDataPort); 138 | dataStream.put("type", 96); 139 | dataStream.put("controlPort", audioControlPort); 140 | streams.setValue(0, dataStream); 141 | 142 | NSDictionary response = new NSDictionary(); 143 | response.put("streams", streams); 144 | BinaryPropertyListWriter.write(out, response); 145 | } 146 | 147 | String getStreamConnectionID() { 148 | return streamConnectionID; 149 | } 150 | 151 | byte[] getEncryptedAESKey() { 152 | return encryptedAESKey; 153 | } 154 | 155 | byte[] getEiv() { 156 | return eiv; 157 | } 158 | 159 | public void printPlist(String rtspMethod, InputStream inputStream) { 160 | 161 | try { 162 | NSDictionary plist = (NSDictionary) BinaryPropertyListParser.parse(inputStream); 163 | Log.i(TAG, rtspMethod + " plist 01: " + plist.toXMLPropertyList()); 164 | } catch (Exception e) { 165 | StringBuilder textBuilder = new StringBuilder(); 166 | try (Reader reader = new BufferedReader(new InputStreamReader 167 | (inputStream, Charset.forName(StandardCharsets.UTF_8.name())))) { 168 | int c = 0; 169 | while ((c = reader.read()) != -1) { 170 | textBuilder.append((char) c); 171 | } 172 | Log.i(TAG, rtspMethod + " plist 02: " + textBuilder.toString()); 173 | } catch (IOException ioException) { 174 | ioException.printStackTrace(); 175 | } 176 | // Log.e(TAG, rtspMethod+ "--printPlist: ", e); 177 | 178 | } 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2server/internal/handler/control/RTSPHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2server.internal.handler.control; 2 | 3 | import com.github.serezhka.jap2lib.rtsp.AudioStreamInfo; 4 | import com.github.serezhka.jap2lib.rtsp.MediaStreamInfo; 5 | import com.github.serezhka.jap2lib.rtsp.VideoStreamInfo; 6 | import com.github.serezhka.jap2server.AirplayDataConsumer; 7 | import com.github.serezhka.jap2server.internal.AudioControlServer; 8 | import com.github.serezhka.jap2server.internal.AudioReceiver; 9 | import com.github.serezhka.jap2server.internal.MirroringReceiver; 10 | import com.github.serezhka.jap2server.internal.handler.audio.AudioHandler; 11 | import com.github.serezhka.jap2server.internal.handler.mirroring.MirroringHandler; 12 | import com.github.serezhka.jap2server.internal.handler.session.Session; 13 | import com.github.serezhka.jap2server.internal.handler.session.SessionManager; 14 | 15 | import io.netty.buffer.ByteBufInputStream; 16 | import io.netty.buffer.ByteBufOutputStream; 17 | import io.netty.channel.ChannelHandler; 18 | import io.netty.channel.ChannelHandlerContext; 19 | import io.netty.handler.codec.http.DefaultFullHttpResponse; 20 | import io.netty.handler.codec.http.FullHttpRequest; 21 | import io.netty.handler.codec.rtsp.RtspMethods; 22 | 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | import java.nio.charset.StandardCharsets; 27 | 28 | @ChannelHandler.Sharable 29 | public class RTSPHandler extends ControlHandler { 30 | 31 | private static final Logger log = LoggerFactory.getLogger(RTSPHandler.class); 32 | 33 | private final AirplayDataConsumer airplayDataConsumer; 34 | private final int airPlayPort; 35 | private final int airTunesPort; 36 | 37 | public RTSPHandler(int airPlayPort, int airTunesPort, SessionManager sessionManager, 38 | AirplayDataConsumer airplayDataConsumer) { 39 | super(sessionManager); 40 | this.airplayDataConsumer = airplayDataConsumer; 41 | this.airPlayPort = airPlayPort; 42 | this.airTunesPort = airTunesPort; 43 | } 44 | 45 | @Override 46 | protected boolean handleRequest(ChannelHandlerContext ctx, Session session, FullHttpRequest request) throws Exception { 47 | DefaultFullHttpResponse response = createResponseForRequest(request); 48 | if (RtspMethods.SETUP.equals(request.method())) { 49 | 50 | MediaStreamInfo mediaStreamInfo = session.getAirPlay().rtspGetMediaStreamInfo(new ByteBufInputStream(request.content())); 51 | if (mediaStreamInfo == null) { 52 | request.content().resetReaderIndex(); 53 | session.getAirPlay().rtspSetupEncryption(new ByteBufInputStream(request.content())); 54 | } else { 55 | switch (mediaStreamInfo.getStreamType()) { 56 | case AUDIO: 57 | AudioStreamInfo audioStreamInfo = (AudioStreamInfo) mediaStreamInfo; 58 | 59 | log.info("Audio format is: {}", audioStreamInfo.getAudioFormat()); 60 | log.info("Audio compression type is: {}", audioStreamInfo.getCompressionType()); 61 | log.info("Audio samples per frame is: {}", audioStreamInfo.getSamplesPerFrame()); 62 | 63 | airplayDataConsumer.onAudioFormat(audioStreamInfo); 64 | 65 | AudioHandler audioHandler = new AudioHandler(session.getAirPlay(), airplayDataConsumer); 66 | AudioReceiver audioReceiver = new AudioReceiver(audioHandler, this); 67 | Thread audioReceiverThread = new Thread(audioReceiver); 68 | session.setAudioReceiverThread(audioReceiverThread); 69 | audioReceiverThread.start(); 70 | synchronized (this) { 71 | wait(); 72 | } 73 | 74 | AudioControlServer audioControlServer = new AudioControlServer(this); 75 | Thread audioControlServerThread = new Thread(audioControlServer); 76 | session.setAudioControlServerThread(audioControlServerThread); 77 | audioControlServerThread.start(); 78 | synchronized (this) { 79 | wait(); 80 | } 81 | 82 | session.getAirPlay().rtspSetupAudio(new ByteBufOutputStream(response.content()), 83 | audioReceiver.getPort(), audioControlServer.getPort()); 84 | 85 | break; 86 | 87 | case VIDEO: 88 | VideoStreamInfo videoStreamInfo = (VideoStreamInfo) mediaStreamInfo; 89 | 90 | airplayDataConsumer.onVideoFormat(videoStreamInfo); 91 | 92 | MirroringHandler mirroringHandler = new MirroringHandler(session.getAirPlay(), airplayDataConsumer); 93 | MirroringReceiver airPlayReceiver = new MirroringReceiver(airPlayPort, mirroringHandler); 94 | Thread airPlayReceiverThread = new Thread(airPlayReceiver); 95 | session.setAirPlayReceiverThread(airPlayReceiverThread); 96 | airPlayReceiverThread.start(); 97 | 98 | session.getAirPlay().rtspSetupVideo(new ByteBufOutputStream(response.content()), airPlayPort, airTunesPort, 7011); 99 | break; 100 | } 101 | } 102 | return sendResponse(ctx, request, response); 103 | } else if (RtspMethods.GET_PARAMETER.equals(request.method())) { 104 | byte[] content = "volume: 1.000000\r\n".getBytes(StandardCharsets.US_ASCII); 105 | response.content().writeBytes(content); 106 | return sendResponse(ctx, request, response); 107 | } else if (RtspMethods.RECORD.equals(request.method())) { 108 | 109 | session.getAirPlay().printPlist("RECORD ",new ByteBufInputStream(request.content())); 110 | response.headers().add("Audio-Latency", "11025"); 111 | response.headers().add("Audio-Jack-Status", "connected; type=analog"); 112 | return sendResponse(ctx, request, response); 113 | } else if (RtspMethods.SET_PARAMETER.equals(request.method())) { 114 | session.getAirPlay().printPlist("SET_PARAMETER ",new ByteBufInputStream(request.content())); 115 | 116 | int volume = session.getAirPlay().rtspSetParameterInfo(new ByteBufInputStream(request.content())); 117 | // todo 设置音量 118 | return sendResponse(ctx, request, response); 119 | } else if ("FLUSH".equals(request.method().toString())) { 120 | return sendResponse(ctx, request, response); 121 | } else if (RtspMethods.TEARDOWN.equals(request.method())) { 122 | session.getAirPlay().printPlist("TEARDOWN ",new ByteBufInputStream(request.content())); 123 | 124 | MediaStreamInfo mediaStreamInfo = session.getAirPlay().rtspGetMediaStreamInfo(new ByteBufInputStream(request.content())); 125 | if (mediaStreamInfo != null) { 126 | switch (mediaStreamInfo.getStreamType()) { 127 | case AUDIO: 128 | session.stopAudio(); 129 | break; 130 | case VIDEO: 131 | session.stopMirroring(); 132 | break; 133 | } 134 | } else { 135 | session.stopAudio(); 136 | session.stopMirroring(); 137 | } 138 | return sendResponse(ctx, request, response); 139 | } else if ("POST".equals(request.method().toString()) && request.uri().equals("/audioMode")) { 140 | session.getAirPlay().printPlist("audioMode ",new ByteBufInputStream(request.content())); 141 | 142 | return sendResponse(ctx, request, response); 143 | } 144 | return false; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2lib/OmgHaxConst.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2lib; 2 | 3 | import android.content.res.AssetManager; 4 | 5 | import com.cjx.airplayjavademo.MyApplication; 6 | 7 | import java.io.*; 8 | import java.net.URL; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | class OmgHaxConst { 13 | 14 | static final byte[] z_key = {26, 100, -7, 96, 108, -29, 1, -87, 84, 72, 27, -44, -85, -127, -4, -58}; 15 | static final byte[] x_key = {-114, -70, 7, -52, -74, 90, -10, 32, 51, -49, -8, 66, -27, -43, 90, 125}; 16 | static final byte[] t_key = {-48, 4, -87, 97, 107, -92, 0, -121, 104, -117, 95, 21, 21, 53, -39, -87}; 17 | static final byte[] index_mangle = {1, 2, 4, 8, 16, 32, 64, -128, 27, 54, 108}; 18 | static final byte[] default_sap = {0, 3, 0, 0, 0, 0, 0, 0, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 121, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 2, 83, 0, 1, -52, 52, 42, 94, 91, 26, 103, 115, -62, 14, 33, -72, 34, 77, -8, 98, 72, 24, 100, -17, -127, 10, -82, 46, 55, 3, -56, -127, -100, 35, 83, -99, -27, -11, -41, 73, -68, 91, 122, 38, 108, 73, 98, -125, -50, 127, 3, -109, 122, -31, -10, 22, -34, 12, 21, -1, 51, -116, -54, -1, -80, -98, -86, -69, -28, 15, 93, 95, 85, -113, -71, 127, 23, 49, -8, -9, -38, 96, -96, -20, 101, 121, -61, 62, -87, -125, 18, -61, -74, 113, 53, -90, 105, 79, -8, 35, 5, -39, -70, 92, 97, 95, -94, 84, -46, -79, -125, 69, -125, -50, -28, 45, 68, 38, -56, 53, -89, -91, -10, -56, 66, 28, 13, -93, -15, -57, 0, 80, -14, -27, 23, -8, -48, -6, 119, -115, -5, -126, -115, 64, -57, -114, -108, 30, 30, 30}; 19 | static final byte[] initial_session_key = {-36, -36, -13, -71, 11, 116, -36, -5, -122, 127, -9, 96, 22, 114, -112, 81}; 20 | static final byte[] static_source_1 = {-6, -100, -83, 77, 75, 104, 38, -116, 127, -13, -120, -103, -34, -110, 46, -107, 30}; 21 | static final byte[] static_source_2 = {-20, 78, 39, 94, -3, -14, -24, 48, -105, -82, 112, -5, -32, 0, 63, 28, 57, -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 9, 0, 0, 0, 0, 0, 0}; 22 | 23 | static final byte[][] message_key = { 24 | {29, 36, 3, 64, -36, -82, -57, -88, 38, 124, 32, -103, 93, 126, -119, 46, -94, 88, -81, -66, -72, 7, -102, 47, -121, 119, -45, -50, 55, 62, 27, 22, 65, 79, 78, -66, 98, 90, 0, 119, -58, -21, -38, 75, -105, 26, 97, -115, 49, 50, 28, -94, 120, -101, 102, 114, 96, -108, 68, -122, -53, 9, -67, 58, 119, 87, -63, 114, 97, 29, 50, -57, -123, -47, -17, -27, 77, -107, 11, -16, -40, 24, -25, 74, -36, 119, -54, 85, 40, 50, -109, 42, 123, 62, 58, -44, -105, -3, 125, 109, -107, 113, 39, -100, 119, 106, 124, -43, -65, -99, 14, -14, 15, 85, -111, 41, -49, -86, 88, 28, 122, -25, -53, -117, 32, 7, 83, -86, 89, 64, 59, 3, -66, 51, 71, 71, 90, 79, -122, 49, -115, 48, -7, 28}, 25 | {-15, -94, 4, 122, -82, -23, -40, -65, -44, -64, 107, 119, -63, 5, -116, -103, -87, -3, 61, 68, -18, 123, 108, 40, 66, 49, 99, -121, 109, -46, 109, 72, -52, 78, -109, 49, 123, 39, 20, -4, 45, 113, 93, -28, -80, -7, 75, -126, 118, 82, -43, 2, 108, -74, -49, 87, -2, -78, -65, -73, 48, 86, 123, -101, 62, 62, -80, 71, 16, 99, -24, 114, 28, 56, 45, 121, -60, 119, 60, -47, -19, 2, 67, 3, 92, -68, 87, -98, 67, 2, 103, -95, -101, -116, -13, 84, -28, 70, -31, 28, 79, -36, -9, -97, -12, 73, 118, 79, 19, -106, -122, -49, -15, 122, 1, -84, -28, -43, 50, 91, 93, 125, -18, -54, -65, 118, -5, 80, -41, -20, -100, -93, -10, 46, -66, -101, -57, -56, 15, -14, -73, 59, -34, -118}, 26 | {24, 110, -45, 115, 94, -23, 90, -113, 102, 63, -15, -72, 74, 98, -39, -64, -46, 8, 19, 97, -53, -13, -83, -90, 38, 77, 58, 123, 6, -75, 81, 86, -2, 102, 10, -40, 58, -86, 71, 73, -45, 124, -61, 104, 112, -48, -106, -128, 106, 5, -112, -17, -81, 67, 66, -60, 46, 80, 76, -106, 19, -75, 46, 76, -128, -94, -115, 35, -18, -30, 94, 120, -12, 61, 101, -54, 113, 79, 104, -98, 75, 67, 88, 123, 71, -106, 64, -127, -118, -104, 108, 4, 51, 15, 47, 28, 51, -114, -22, -95, 79, -88, 55, -109, 23, 29, -115, 24, -87, 106, 27, 7, 124, -74, 8, 88, 31, 18, 0, -6, 55, 77, 127, -70, -91, 0, 107, 114, 120, -100, 51, -24, 65, 7, -73, -63, 103, -101, 118, -69, -35, -111, 61, 61}, 27 | {71, 105, -97, 8, -72, -126, -5, -95, -107, -27, 111, 65, 121, 30, 12, -74, -95, -54, 17, 10, -30, -121, 44, 126, 57, -68, -104, -91, 30, -78, -6, 31, -18, 115, 66, -41, -87, 9, 66, -64, -17, -60, 68, 12, 15, 111, -105, 9, 8, -68, 102, 49, 51, -1, -54, 126, -75, -23, 125, 119, -104, -64, -46, 106, -3, 47, 11, 108, -99, -85, -86, 120, 76, 118, -34, 33, -65, -12, 58, 40, 42, -60, 116, -76, -87, 27, -102, 56, 33, 76, -21, -67, 114, 81, -90, 21, -44, -98, 23, -13, -108, 38, 109, 7, 95, -110, -86, -92, 78, -14, -51, 63, 2, 79, 5, 53, -29, 88, -33, -126, 126, 106, 23, -16, 95, 107, -36, -23, 58, -49, 4, -77, 1, 68, -121, -41, -68, -83, 61, 116, -106, 116, -93, -103}}; 28 | 29 | static final byte[][] message_iv = { 30 | {87, 82, -15, -73, 84, -99, -113, -121, 12, 16, 72, 90, 96, -120, -54, -37}, 31 | {-33, 123, 21, 99, -16, 5, 88, 119, 82, -87, 4, 2, -71, -93, -110, -107}, 32 | {104, -75, 70, 17, -5, 4, -34, 103, 108, -106, -114, -5, -116, -99, -80, -55}, 33 | {39, 7, -117, 33, 35, 54, 30, 122, -36, -99, 11, 17, 83, 84, 105, 13}}; 34 | 35 | static final byte[] table_s1; 36 | static final byte[] table_s2; 37 | static final byte[] table_s3; 38 | static final byte[] table_s4; 39 | static final int[] table_s5; 40 | static final int[] table_s6; 41 | static final int[] table_s7; 42 | static final int[] table_s8; 43 | static final int[] table_s9; 44 | static final byte[] table_s10; 45 | 46 | 47 | static { 48 | try { 49 | AssetManager am = MyApplication.getAppContext().getAssets(); 50 | table_s1 = readBytes(am, "table_s1"); 51 | table_s2 = readBytes(am, "table_s2"); 52 | table_s3 = readBytes(am, "table_s3"); 53 | table_s4 = readBytes(am, "table_s4"); 54 | table_s5 = readInts(am, "table_s5"); 55 | table_s6 = readInts(am, "table_s6"); 56 | table_s7 = readInts(am, "table_s7"); 57 | table_s8 = readInts(am, "table_s8"); 58 | table_s9 = readInts(am, "table_s9"); 59 | table_s10 = readBytes(am, "table_s10"); 60 | } catch (Exception e) { 61 | throw new RuntimeException("Init failed", e); 62 | } 63 | } 64 | 65 | private static byte[] readBytes(AssetManager assetManager, String assetsFileName) throws IOException { 66 | try (InputStream is = assetManager.open(assetsFileName); 67 | ByteArrayOutputStream buffer = new ByteArrayOutputStream()) { 68 | 69 | int nRead; 70 | byte[] data = new byte[1024]; 71 | 72 | while ((nRead = is.read(data, 0, data.length)) != -1) { 73 | buffer.write(data, 0, nRead); 74 | } 75 | 76 | return buffer.toByteArray(); 77 | } 78 | 79 | } 80 | 81 | private static int[] readInts(AssetManager assetManager, String assetsFileName) throws IOException { 82 | try (InputStream is = assetManager.open(assetsFileName); 83 | BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { 84 | 85 | List tmp = new ArrayList<>(); 86 | String line; 87 | while ((line = reader.readLine()) != null) { 88 | tmp.add(line); 89 | } 90 | 91 | return tmp.stream().map(Long::decode).mapToInt(Long::intValue).toArray(); 92 | } 93 | } 94 | 95 | private static byte[] readBytes(URL url) throws IOException { 96 | try (InputStream is = url.openStream(); 97 | ByteArrayOutputStream buffer = new ByteArrayOutputStream()) { 98 | 99 | int nRead; 100 | byte[] data = new byte[1024]; 101 | 102 | while ((nRead = is.read(data, 0, data.length)) != -1) { 103 | buffer.write(data, 0, nRead); 104 | } 105 | 106 | return buffer.toByteArray(); 107 | } 108 | } 109 | 110 | /** 111 | * Files.newBufferedReader(Paths.get(OmgHaxConst.class.getClassLoader().getResource("table_s5").toURI())) 112 | * .lines().map(Long::decode).mapToInt(Long::intValue).toArray(); 113 | * Doesn't work !!! throws java.nio.file.FileSystemNotFoundException 114 | * jar:file:/../java-airplay-lib/build/libs/java-airplay-lib-1.0-SNAPSHOT-all.jar!/table_s1 115 | */ 116 | private static int[] readInts(URL url) throws IOException { 117 | try (InputStream is = url.openStream(); 118 | BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { 119 | 120 | List tmp = new ArrayList<>(); 121 | String line; 122 | while ((line = reader.readLine()) != null) { 123 | tmp.add(line); 124 | } 125 | 126 | return tmp.stream().map(Long::decode).mapToInt(Long::intValue).toArray(); 127 | } 128 | } 129 | 130 | 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/assets/table_s9: -------------------------------------------------------------------------------- 1 | 0x7cba6f01 2 | 0x2c06b734 3 | 0x220ec640 4 | 0x72b21e75 5 | 0x82c46d87 6 | 0xd278b5b2 7 | 0xdc70c4c6 8 | 0x8ccc1cf3 9 | 0xa4a8a905 10 | 0xf4147130 11 | 0xfa1c0044 12 | 0xaaa0d871 13 | 0x5ad6ab83 14 | 0xa6a73b6 15 | 0x46202c2 16 | 0x54dedaf7 17 | 0x2854fa3e 18 | 0x78e8220b 19 | 0x76e0537f 20 | 0x265c8b4a 21 | 0xd62af8b8 22 | 0x8696208d 23 | 0x889e51f9 24 | 0xd82289cc 25 | 0xf0463c3a 26 | 0xa0fae40f 27 | 0xaef2957b 28 | 0xfe4e4d4e 29 | 0xe383ebc 30 | 0x5e84e689 31 | 0x508c97fd 32 | 0x304fc8 33 | 0x613fde08 34 | 0x3183063d 35 | 0x3f8b7749 36 | 0x6f37af7c 37 | 0x9f41dc8e 38 | 0xcffd04bb 39 | 0xc1f575cf 40 | 0x9149adfa 41 | 0xb92d180c 42 | 0xe991c039 43 | 0xe799b14d 44 | 0xb7256978 45 | 0x47531a8a 46 | 0x17efc2bf 47 | 0x19e7b3cb 48 | 0x495b6bfe 49 | 0x35d14b37 50 | 0x656d9302 51 | 0x6b65e276 52 | 0x3bd93a43 53 | 0xcbaf49b1 54 | 0x9b139184 55 | 0x951be0f0 56 | 0xc5a738c5 57 | 0xedc38d33 58 | 0xbd7f5506 59 | 0xb3772472 60 | 0xe3cbfc47 61 | 0x13bd8fb5 62 | 0x43015780 63 | 0x4d0926f4 64 | 0x1db5fec1 65 | 0x8ab92d9 66 | 0x58174aec 67 | 0x561f3b98 68 | 0x6a3e3ad 69 | 0xf6d5905f 70 | 0xa669486a 71 | 0xa861391e 72 | 0xf8dde12b 73 | 0xd0b954dd 74 | 0x80058ce8 75 | 0x8e0dfd9c 76 | 0xdeb125a9 77 | 0x2ec7565b 78 | 0x7e7b8e6e 79 | 0x7073ff1a 80 | 0x20cf272f 81 | 0x5c4507e6 82 | 0xcf9dfd3 83 | 0x2f1aea7 84 | 0x524d7692 85 | 0xa23b0560 86 | 0xf287dd55 87 | 0xfc8fac21 88 | 0xac337414 89 | 0x8457c1e2 90 | 0xd4eb19d7 91 | 0xdae368a3 92 | 0x8a5fb096 93 | 0x7a29c364 94 | 0x2a951b51 95 | 0x249d6a25 96 | 0x7421b210 97 | 0x152e23d0 98 | 0x4592fbe5 99 | 0x4b9a8a91 100 | 0x1b2652a4 101 | 0xeb502156 102 | 0xbbecf963 103 | 0xb5e48817 104 | 0xe5585022 105 | 0xcd3ce5d4 106 | 0x9d803de1 107 | 0x93884c95 108 | 0xc33494a0 109 | 0x3342e752 110 | 0x63fe3f67 111 | 0x6df64e13 112 | 0x3d4a9626 113 | 0x41c0b6ef 114 | 0x117c6eda 115 | 0x1f741fae 116 | 0x4fc8c79b 117 | 0xbfbeb469 118 | 0xef026c5c 119 | 0xe10a1d28 120 | 0xb1b6c51d 121 | 0x99d270eb 122 | 0xc96ea8de 123 | 0xc766d9aa 124 | 0x97da019f 125 | 0x67ac726d 126 | 0x3710aa58 127 | 0x3918db2c 128 | 0x69a40319 129 | 0xec5d851c 130 | 0xbce15d29 131 | 0xb2e92c5d 132 | 0xe255f468 133 | 0x1223879a 134 | 0x429f5faf 135 | 0x4c972edb 136 | 0x1c2bf6ee 137 | 0x344f4318 138 | 0x64f39b2d 139 | 0x6afbea59 140 | 0x3a47326c 141 | 0xca31419e 142 | 0x9a8d99ab 143 | 0x9485e8df 144 | 0xc43930ea 145 | 0xb8b31023 146 | 0xe80fc816 147 | 0xe607b962 148 | 0xb6bb6157 149 | 0x46cd12a5 150 | 0x1671ca90 151 | 0x1879bbe4 152 | 0x48c563d1 153 | 0x60a1d627 154 | 0x301d0e12 155 | 0x3e157f66 156 | 0x6ea9a753 157 | 0x9edfd4a1 158 | 0xce630c94 159 | 0xc06b7de0 160 | 0x90d7a5d5 161 | 0xf1d83415 162 | 0xa164ec20 163 | 0xaf6c9d54 164 | 0xffd04561 165 | 0xfa63693 166 | 0x5f1aeea6 167 | 0x51129fd2 168 | 0x1ae47e7 169 | 0x29caf211 170 | 0x79762a24 171 | 0x777e5b50 172 | 0x27c28365 173 | 0xd7b4f097 174 | 0x870828a2 175 | 0x890059d6 176 | 0xd9bc81e3 177 | 0xa536a12a 178 | 0xf58a791f 179 | 0xfb82086b 180 | 0xab3ed05e 181 | 0x5b48a3ac 182 | 0xbf47b99 183 | 0x5fc0aed 184 | 0x5540d2d8 185 | 0x7d24672e 186 | 0x2d98bf1b 187 | 0x2390ce6f 188 | 0x732c165a 189 | 0x835a65a8 190 | 0xd3e6bd9d 191 | 0xddeecce9 192 | 0x8d5214dc 193 | 0x984c78c4 194 | 0xc8f0a0f1 195 | 0xc6f8d185 196 | 0x964409b0 197 | 0x66327a42 198 | 0x368ea277 199 | 0x3886d303 200 | 0x683a0b36 201 | 0x405ebec0 202 | 0x10e266f5 203 | 0x1eea1781 204 | 0x4e56cfb4 205 | 0xbe20bc46 206 | 0xee9c6473 207 | 0xe0941507 208 | 0xb028cd32 209 | 0xcca2edfb 210 | 0x9c1e35ce 211 | 0x921644ba 212 | 0xc2aa9c8f 213 | 0x32dcef7d 214 | 0x62603748 215 | 0x6c68463c 216 | 0x3cd49e09 217 | 0x14b02bff 218 | 0x440cf3ca 219 | 0x4a0482be 220 | 0x1ab85a8b 221 | 0xeace2979 222 | 0xba72f14c 223 | 0xb47a8038 224 | 0xe4c6580d 225 | 0x85c9c9cd 226 | 0xd57511f8 227 | 0xdb7d608c 228 | 0x8bc1b8b9 229 | 0x7bb7cb4b 230 | 0x2b0b137e 231 | 0x2503620a 232 | 0x75bfba3f 233 | 0x5ddb0fc9 234 | 0xd67d7fc 235 | 0x36fa688 236 | 0x53d37ebd 237 | 0xa3a50d4f 238 | 0xf319d57a 239 | 0xfd11a40e 240 | 0xadad7c3b 241 | 0xd1275cf2 242 | 0x819b84c7 243 | 0x8f93f5b3 244 | 0xdf2f2d86 245 | 0x2f595e74 246 | 0x7fe58641 247 | 0x71edf735 248 | 0x21512f00 249 | 0x9359af6 250 | 0x598942c3 251 | 0x578133b7 252 | 0x73deb82 253 | 0xf74b9870 254 | 0xa7f74045 255 | 0xa9ff3131 256 | 0xf943e904 257 | 0xcdbd827d 258 | 0x7165b72d 259 | 0x7914c323 260 | 0xc5ccf673 261 | 0xb3bf0483 262 | 0xf6731d3 263 | 0x71645dd 264 | 0xbbce708d 265 | 0xdf7b86a5 266 | 0x63a3b3f5 267 | 0x6bd2c7fb 268 | 0xd70af2ab 269 | 0xa179005b 270 | 0x1da1350b 271 | 0x15d04105 272 | 0xa9087455 273 | 0x2328bd29 274 | 0x9ff08879 275 | 0x9781fc77 276 | 0x2b59c927 277 | 0x5d2a3bd7 278 | 0xe1f20e87 279 | 0xe9837a89 280 | 0x555b4fd9 281 | 0x31eeb9f1 282 | 0x8d368ca1 283 | 0x8547f8af 284 | 0x399fcdff 285 | 0x4fec3f0f 286 | 0xf3340a5f 287 | 0xfb457e51 288 | 0x479d4b01 289 | 0x480c8b60 290 | 0xf4d4be30 291 | 0xfca5ca3e 292 | 0x407dff6e 293 | 0x360e0d9e 294 | 0x8ad638ce 295 | 0x82a74cc0 296 | 0x3e7f7990 297 | 0x5aca8fb8 298 | 0xe612bae8 299 | 0xee63cee6 300 | 0x52bbfbb6 301 | 0x24c80946 302 | 0x98103c16 303 | 0x90614818 304 | 0x2cb97d48 305 | 0xa699b434 306 | 0x1a418164 307 | 0x1230f56a 308 | 0xaee8c03a 309 | 0xd89b32ca 310 | 0x6443079a 311 | 0x6c327394 312 | 0xd0ea46c4 313 | 0xb45fb0ec 314 | 0x88785bc 315 | 0xf6f1b2 316 | 0xbc2ec4e2 317 | 0xca5d3612 318 | 0x76850342 319 | 0x7ef4774c 320 | 0xc22c421c 321 | 0xdc405a09 322 | 0x60986f59 323 | 0x68e91b57 324 | 0xd4312e07 325 | 0xa242dcf7 326 | 0x1e9ae9a7 327 | 0x16eb9da9 328 | 0xaa33a8f9 329 | 0xce865ed1 330 | 0x725e6b81 331 | 0x7a2f1f8f 332 | 0xc6f72adf 333 | 0xb084d82f 334 | 0xc5ced7f 335 | 0x42d9971 336 | 0xb8f5ac21 337 | 0x32d5655d 338 | 0x8e0d500d 339 | 0x867c2403 340 | 0x3aa41153 341 | 0x4cd7e3a3 342 | 0xf00fd6f3 343 | 0xf87ea2fd 344 | 0x44a697ad 345 | 0x20136185 346 | 0x9ccb54d5 347 | 0x94ba20db 348 | 0x2862158b 349 | 0x5e11e77b 350 | 0xe2c9d22b 351 | 0xeab8a625 352 | 0x56609375 353 | 0x59f15314 354 | 0xe5296644 355 | 0xed58124a 356 | 0x5180271a 357 | 0x27f3d5ea 358 | 0x9b2be0ba 359 | 0x935a94b4 360 | 0x2f82a1e4 361 | 0x4b3757cc 362 | 0xf7ef629c 363 | 0xff9e1692 364 | 0x434623c2 365 | 0x3535d132 366 | 0x89ede462 367 | 0x819c906c 368 | 0x3d44a53c 369 | 0xb7646c40 370 | 0xbbc5910 371 | 0x3cd2d1e 372 | 0xbf15184e 373 | 0xc966eabe 374 | 0x75bedfee 375 | 0x7dcfabe0 376 | 0xc1179eb0 377 | 0xa5a26898 378 | 0x197a5dc8 379 | 0x110b29c6 380 | 0xadd31c96 381 | 0xdba0ee66 382 | 0x6778db36 383 | 0x6f09af38 384 | 0xd3d19a68 385 | 0x2a579fed 386 | 0x968faabd 387 | 0x9efedeb3 388 | 0x2226ebe3 389 | 0x54551913 390 | 0xe88d2c43 391 | 0xe0fc584d 392 | 0x5c246d1d 393 | 0x38919b35 394 | 0x8449ae65 395 | 0x8c38da6b 396 | 0x30e0ef3b 397 | 0x46931dcb 398 | 0xfa4b289b 399 | 0xf23a5c95 400 | 0x4ee269c5 401 | 0xc4c2a0b9 402 | 0x781a95e9 403 | 0x706be1e7 404 | 0xccb3d4b7 405 | 0xbac02647 406 | 0x6181317 407 | 0xe696719 408 | 0xb2b15249 409 | 0xd604a461 410 | 0x6adc9131 411 | 0x62ade53f 412 | 0xde75d06f 413 | 0xa806229f 414 | 0x14de17cf 415 | 0x1caf63c1 416 | 0xa0775691 417 | 0xafe696f0 418 | 0x133ea3a0 419 | 0x1b4fd7ae 420 | 0xa797e2fe 421 | 0xd1e4100e 422 | 0x6d3c255e 423 | 0x654d5150 424 | 0xd9956400 425 | 0xbd209228 426 | 0x1f8a778 427 | 0x989d376 428 | 0xb551e626 429 | 0xc32214d6 430 | 0x7ffa2186 431 | 0x778b5588 432 | 0xcb5360d8 433 | 0x4173a9a4 434 | 0xfdab9cf4 435 | 0xf5dae8fa 436 | 0x4902ddaa 437 | 0x3f712f5a 438 | 0x83a91a0a 439 | 0x8bd86e04 440 | 0x37005b54 441 | 0x53b5ad7c 442 | 0xef6d982c 443 | 0xe71cec22 444 | 0x5bc4d972 445 | 0x2db72b82 446 | 0x916f1ed2 447 | 0x991e6adc 448 | 0x25c65f8c 449 | 0x3baa4799 450 | 0x877272c9 451 | 0x8f0306c7 452 | 0x33db3397 453 | 0x45a8c167 454 | 0xf970f437 455 | 0xf1018039 456 | 0x4dd9b569 457 | 0x296c4341 458 | 0x95b47611 459 | 0x9dc5021f 460 | 0x211d374f 461 | 0x576ec5bf 462 | 0xebb6f0ef 463 | 0xe3c784e1 464 | 0x5f1fb1b1 465 | 0xd53f78cd 466 | 0x69e74d9d 467 | 0x61963993 468 | 0xdd4e0cc3 469 | 0xab3dfe33 470 | 0x17e5cb63 471 | 0x1f94bf6d 472 | 0xa34c8a3d 473 | 0xc7f97c15 474 | 0x7b214945 475 | 0x73503d4b 476 | 0xcf88081b 477 | 0xb9fbfaeb 478 | 0x523cfbb 479 | 0xd52bbb5 480 | 0xb18a8ee5 481 | 0xbe1b4e84 482 | 0x2c37bd4 483 | 0xab20fda 484 | 0xb66a3a8a 485 | 0xc019c87a 486 | 0x7cc1fd2a 487 | 0x74b08924 488 | 0xc868bc74 489 | 0xacdd4a5c 490 | 0x10057f0c 491 | 0x18740b02 492 | 0xa4ac3e52 493 | 0xd2dfcca2 494 | 0x6e07f9f2 495 | 0x66768dfc 496 | 0xdaaeb8ac 497 | 0x508e71d0 498 | 0xec564480 499 | 0xe427308e 500 | 0x58ff05de 501 | 0x2e8cf72e 502 | 0x9254c27e 503 | 0x9a25b670 504 | 0x26fd8320 505 | 0x42487508 506 | 0xfe904058 507 | 0xf6e13456 508 | 0x4a390106 509 | 0x3c4af3f6 510 | 0x8092c6a6 511 | 0x88e3b2a8 512 | 0x343b87f8 513 | 0xd78d63dd 514 | 0xfb83361 515 | 0x7ecc3d69 516 | 0xa6f96dd5 517 | 0xd50b9da3 518 | 0xd3ecd1f 519 | 0x7c4ac317 520 | 0xa47f93ab 521 | 0x1189bbcf 522 | 0xc9bceb73 523 | 0xb8c8e57b 524 | 0x60fdb5c7 525 | 0x130f45b1 526 | 0xcb3a150d 527 | 0xba4e1b05 528 | 0x627b4bb9 529 | 0x42b23733 530 | 0x9a87678f 531 | 0xebf36987 532 | 0x33c6393b 533 | 0x4034c94d 534 | 0x980199f1 535 | 0xe97597f9 536 | 0x3140c745 537 | 0x84b6ef21 538 | 0x5c83bf9d 539 | 0x2df7b195 540 | 0xf5c2e129 541 | 0x8630115f 542 | 0x5e0541e3 543 | 0x2f714feb 544 | 0xf7441f57 545 | 0x66847e58 546 | 0xbeb12ee4 547 | 0xcfc520ec 548 | 0x17f07050 549 | 0x64028026 550 | 0xbc37d09a 551 | 0xcd43de92 552 | 0x15768e2e 553 | 0xa080a64a 554 | 0x78b5f6f6 555 | 0x9c1f8fe 556 | 0xd1f4a842 557 | 0xa2065834 558 | 0x7a330888 559 | 0xb470680 560 | 0xd372563c 561 | 0xf3bb2ab6 562 | 0x2b8e7a0a 563 | 0x5afa7402 564 | 0x82cf24be 565 | 0xf13dd4c8 566 | 0x29088474 567 | 0x587c8a7c 568 | 0x8049dac0 569 | 0x35bff2a4 570 | 0xed8aa218 571 | 0x9cfeac10 572 | 0x44cbfcac 573 | 0x37390cda 574 | 0xef0c5c66 575 | 0x9e78526e 576 | 0x464d02d2 577 | 0x2a5517cc 578 | 0xf2604770 579 | 0x83144978 580 | 0x5b2119c4 581 | 0x28d3e9b2 582 | 0xf0e6b90e 583 | 0x8192b706 584 | 0x59a7e7ba 585 | 0xec51cfde 586 | 0x34649f62 587 | 0x4510916a 588 | 0x9d25c1d6 589 | 0xeed731a0 590 | 0x36e2611c 591 | 0x47966f14 592 | 0x9fa33fa8 593 | 0xbf6a4322 594 | 0x675f139e 595 | 0x162b1d96 596 | 0xce1e4d2a 597 | 0xbdecbd5c 598 | 0x65d9ede0 599 | 0x14ade3e8 600 | 0xcc98b354 601 | 0x796e9b30 602 | 0xa15bcb8c 603 | 0xd02fc584 604 | 0x81a9538 605 | 0x7be8654e 606 | 0xa3dd35f2 607 | 0xd2a93bfa 608 | 0xa9c6b46 609 | 0x9b5c0a49 610 | 0x43695af5 611 | 0x321d54fd 612 | 0xea280441 613 | 0x99daf437 614 | 0x41efa48b 615 | 0x309baa83 616 | 0xe8aefa3f 617 | 0x5d58d25b 618 | 0x856d82e7 619 | 0xf4198cef 620 | 0x2c2cdc53 621 | 0x5fde2c25 622 | 0x87eb7c99 623 | 0xf69f7291 624 | 0x2eaa222d 625 | 0xe635ea7 626 | 0xd6560e1b 627 | 0xa7220013 628 | 0x7f1750af 629 | 0xce5a0d9 630 | 0xd4d0f065 631 | 0xa5a4fe6d 632 | 0x7d91aed1 633 | 0xc86786b5 634 | 0x1052d609 635 | 0x6126d801 636 | 0xb91388bd 637 | 0xcae178cb 638 | 0x12d42877 639 | 0x63a0267f 640 | 0xbb9576c3 641 | 0x3d90f33a 642 | 0xe5a5a386 643 | 0x94d1ad8e 644 | 0x4ce4fd32 645 | 0x3f160d44 646 | 0xe7235df8 647 | 0x965753f0 648 | 0x4e62034c 649 | 0xfb942b28 650 | 0x23a17b94 651 | 0x52d5759c 652 | 0x8ae02520 653 | 0xf912d556 654 | 0x212785ea 655 | 0x50538be2 656 | 0x8866db5e 657 | 0xa8afa7d4 658 | 0x709af768 659 | 0x1eef960 660 | 0xd9dba9dc 661 | 0xaa2959aa 662 | 0x721c0916 663 | 0x368071e 664 | 0xdb5d57a2 665 | 0x6eab7fc6 666 | 0xb69e2f7a 667 | 0xc7ea2172 668 | 0x1fdf71ce 669 | 0x6c2d81b8 670 | 0xb418d104 671 | 0xc56cdf0c 672 | 0x1d598fb0 673 | 0x8c99eebf 674 | 0x54acbe03 675 | 0x25d8b00b 676 | 0xfdede0b7 677 | 0x8e1f10c1 678 | 0x562a407d 679 | 0x275e4e75 680 | 0xff6b1ec9 681 | 0x4a9d36ad 682 | 0x92a86611 683 | 0xe3dc6819 684 | 0x3be938a5 685 | 0x481bc8d3 686 | 0x902e986f 687 | 0xe15a9667 688 | 0x396fc6db 689 | 0x19a6ba51 690 | 0xc193eaed 691 | 0xb0e7e4e5 692 | 0x68d2b459 693 | 0x1b20442f 694 | 0xc3151493 695 | 0xb2611a9b 696 | 0x6a544a27 697 | 0xdfa26243 698 | 0x79732ff 699 | 0x76e33cf7 700 | 0xaed66c4b 701 | 0xdd249c3d 702 | 0x511cc81 703 | 0x7465c289 704 | 0xac509235 705 | 0xc048872b 706 | 0x187dd797 707 | 0x6909d99f 708 | 0xb13c8923 709 | 0xc2ce7955 710 | 0x1afb29e9 711 | 0x6b8f27e1 712 | 0xb3ba775d 713 | 0x64c5f39 714 | 0xde790f85 715 | 0xaf0d018d 716 | 0x77385131 717 | 0x4caa147 718 | 0xdcfff1fb 719 | 0xad8bfff3 720 | 0x75beaf4f 721 | 0x5577d3c5 722 | 0x8d428379 723 | 0xfc368d71 724 | 0x2403ddcd 725 | 0x57f12dbb 726 | 0x8fc47d07 727 | 0xfeb0730f 728 | 0x268523b3 729 | 0x93730bd7 730 | 0x4b465b6b 731 | 0x3a325563 732 | 0xe20705df 733 | 0x91f5f5a9 734 | 0x49c0a515 735 | 0x38b4ab1d 736 | 0xe081fba1 737 | 0x71419aae 738 | 0xa974ca12 739 | 0xd800c41a 740 | 0x3594a6 741 | 0x73c764d0 742 | 0xabf2346c 743 | 0xda863a64 744 | 0x2b36ad8 745 | 0xb74542bc 746 | 0x6f701200 747 | 0x1e041c08 748 | 0xc6314cb4 749 | 0xb5c3bcc2 750 | 0x6df6ec7e 751 | 0x1c82e276 752 | 0xc4b7b2ca 753 | 0xe47ece40 754 | 0x3c4b9efc 755 | 0x4d3f90f4 756 | 0x950ac048 757 | 0xe6f8303e 758 | 0x3ecd6082 759 | 0x4fb96e8a 760 | 0x978c3e36 761 | 0x227a1652 762 | 0xfa4f46ee 763 | 0x8b3b48e6 764 | 0x530e185a 765 | 0x20fce82c 766 | 0xf8c9b890 767 | 0x89bdb698 768 | 0x5188e624 769 | 0x52ae490 770 | 0x307a5848 771 | 0x44745039 772 | 0x7124ece1 773 | 0x83d49a92 774 | 0xb684264a 775 | 0xc28a2e3b 776 | 0xf7da92e3 777 | 0x1f2f656 778 | 0x34a24a8e 779 | 0x40ac42ff 780 | 0x75fcfe27 781 | 0x870c8854 782 | 0xb25c348c 783 | 0xc6523cfd 784 | 0xf3028025 785 | 0x3a7e0a05 786 | 0xf2eb6dd 787 | 0x7b20beac 788 | 0x4e700274 789 | 0xbc807407 790 | 0x89d0c8df 791 | 0xfddec0ae 792 | 0xc88e7c76 793 | 0x3ea618c3 794 | 0xbf6a41b 795 | 0x7ff8ac6a 796 | 0x4aa810b2 797 | 0xb85866c1 798 | 0x8d08da19 799 | 0xf906d268 800 | 0xcc566eb0 801 | 0xc376121 802 | 0x3967ddf9 803 | 0x4d69d588 804 | 0x78396950 805 | 0x8ac91f23 806 | 0xbf99a3fb 807 | 0xcb97ab8a 808 | 0xfec71752 809 | 0x8ef73e7 810 | 0x3dbfcf3f 811 | 0x49b1c74e 812 | 0x7ce17b96 813 | 0x8e110de5 814 | 0xbb41b13d 815 | 0xcf4fb94c 816 | 0xfa1f0594 817 | 0x33638fb4 818 | 0x633336c 819 | 0x723d3b1d 820 | 0x476d87c5 821 | 0xb59df1b6 822 | 0x80cd4d6e 823 | 0xf4c3451f 824 | 0xc193f9c7 825 | 0x37bb9d72 826 | 0x2eb21aa 827 | 0x76e529db 828 | 0x43b59503 829 | 0xb145e370 830 | 0x84155fa8 831 | 0xf01b57d9 832 | 0xc54beb01 833 | 0xdd5ef56d 834 | 0xe80e49b5 835 | 0x9c0041c4 836 | 0xa950fd1c 837 | 0x5ba08b6f 838 | 0x6ef037b7 839 | 0x1afe3fc6 840 | 0x2fae831e 841 | 0xd986e7ab 842 | 0xecd65b73 843 | 0x98d85302 844 | 0xad88efda 845 | 0x5f7899a9 846 | 0x6a282571 847 | 0x1e262d00 848 | 0x2b7691d8 849 | 0xe20a1bf8 850 | 0xd75aa720 851 | 0xa354af51 852 | 0x96041389 853 | 0x64f465fa 854 | 0x51a4d922 855 | 0x25aad153 856 | 0x10fa6d8b 857 | 0xe6d2093e 858 | 0xd382b5e6 859 | 0xa78cbd97 860 | 0x92dc014f 861 | 0x602c773c 862 | 0x557ccbe4 863 | 0x2172c395 864 | 0x14227f4d 865 | 0xd44370dc 866 | 0xe113cc04 867 | 0x951dc475 868 | 0xa04d78ad 869 | 0x52bd0ede 870 | 0x67edb206 871 | 0x13e3ba77 872 | 0x26b306af 873 | 0xd09b621a 874 | 0xe5cbdec2 875 | 0x91c5d6b3 876 | 0xa4956a6b 877 | 0x56651c18 878 | 0x6335a0c0 879 | 0x173ba8b1 880 | 0x226b1469 881 | 0xeb179e49 882 | 0xde472291 883 | 0xaa492ae0 884 | 0x9f199638 885 | 0x6de9e04b 886 | 0x58b95c93 887 | 0x2cb754e2 888 | 0x19e7e83a 889 | 0xefcf8c8f 890 | 0xda9f3057 891 | 0xae913826 892 | 0x9bc184fe 893 | 0x6931f28d 894 | 0x5c614e55 895 | 0x286f4624 896 | 0x1d3ffafc 897 | 0x18ba037a 898 | 0x2deabfa2 899 | 0x59e4b7d3 900 | 0x6cb40b0b 901 | 0x9e447d78 902 | 0xab14c1a0 903 | 0xdf1ac9d1 904 | 0xea4a7509 905 | 0x1c6211bc 906 | 0x2932ad64 907 | 0x5d3ca515 908 | 0x686c19cd 909 | 0x9a9c6fbe 910 | 0xafccd366 911 | 0xdbc2db17 912 | 0xee9267cf 913 | 0x27eeedef 914 | 0x12be5137 915 | 0x66b05946 916 | 0x53e0e59e 917 | 0xa11093ed 918 | 0x94402f35 919 | 0xe04e2744 920 | 0xd51e9b9c 921 | 0x2336ff29 922 | 0x166643f1 923 | 0x62684b80 924 | 0x5738f758 925 | 0xa5c8812b 926 | 0x90983df3 927 | 0xe4963582 928 | 0xd1c6895a 929 | 0x11a786cb 930 | 0x24f73a13 931 | 0x50f93262 932 | 0x65a98eba 933 | 0x9759f8c9 934 | 0xa2094411 935 | 0xd6074c60 936 | 0xe357f0b8 937 | 0x157f940d 938 | 0x202f28d5 939 | 0x542120a4 940 | 0x61719c7c 941 | 0x9381ea0f 942 | 0xa6d156d7 943 | 0xd2df5ea6 944 | 0xe78fe27e 945 | 0x2ef3685e 946 | 0x1ba3d486 947 | 0x6faddcf7 948 | 0x5afd602f 949 | 0xa80d165c 950 | 0x9d5daa84 951 | 0xe953a2f5 952 | 0xdc031e2d 953 | 0x2a2b7a98 954 | 0x1f7bc640 955 | 0x6b75ce31 956 | 0x5e2572e9 957 | 0xacd5049a 958 | 0x9985b842 959 | 0xed8bb033 960 | 0xd8db0ceb 961 | 0xc0ce1287 962 | 0xf59eae5f 963 | 0x8190a62e 964 | 0xb4c01af6 965 | 0x46306c85 966 | 0x7360d05d 967 | 0x76ed82c 968 | 0x323e64f4 969 | 0xc4160041 970 | 0xf146bc99 971 | 0x8548b4e8 972 | 0xb0180830 973 | 0x42e87e43 974 | 0x77b8c29b 975 | 0x3b6caea 976 | 0x36e67632 977 | 0xff9afc12 978 | 0xcaca40ca 979 | 0xbec448bb 980 | 0x8b94f463 981 | 0x79648210 982 | 0x4c343ec8 983 | 0x383a36b9 984 | 0xd6a8a61 985 | 0xfb42eed4 986 | 0xce12520c 987 | 0xba1c5a7d 988 | 0x8f4ce6a5 989 | 0x7dbc90d6 990 | 0x48ec2c0e 991 | 0x3ce2247f 992 | 0x9b298a7 993 | 0xc9d39736 994 | 0xfc832bee 995 | 0x888d239f 996 | 0xbddd9f47 997 | 0x4f2de934 998 | 0x7a7d55ec 999 | 0xe735d9d 1000 | 0x3b23e145 1001 | 0xcd0b85f0 1002 | 0xf85b3928 1003 | 0x8c553159 1004 | 0xb9058d81 1005 | 0x4bf5fbf2 1006 | 0x7ea5472a 1007 | 0xaab4f5b 1008 | 0x3ffbf383 1009 | 0xf68779a3 1010 | 0xc3d7c57b 1011 | 0xb7d9cd0a 1012 | 0x828971d2 1013 | 0x707907a1 1014 | 0x4529bb79 1015 | 0x3127b308 1016 | 0x4770fd0 1017 | 0xf25f6b65 1018 | 0xc70fd7bd 1019 | 0xb301dfcc 1020 | 0x86516314 1021 | 0x74a11567 1022 | 0x41f1a9bf 1023 | 0x35ffa1ce 1024 | 0xaf1d16 -------------------------------------------------------------------------------- /app/src/main/java/com/github/serezhka/jap2lib/OmgHax.java: -------------------------------------------------------------------------------- 1 | package com.github.serezhka.jap2lib; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.ByteOrder; 5 | import java.util.Arrays; 6 | 7 | import static com.github.serezhka.jap2lib.OmgHaxConst.*; 8 | 9 | class OmgHax { 10 | 11 | private final ModifiedMD5 modifiedMD5 = new ModifiedMD5(); 12 | private final SapHash sapHash = new SapHash(); 13 | 14 | void decryptAesKey(byte[] message3, byte[] cipherText, byte[] keyOut) { 15 | byte[] chunk1 = Arrays.copyOfRange(cipherText, 16, cipherText.length); 16 | byte[] chunk2 = Arrays.copyOfRange(cipherText, 56, cipherText.length); 17 | byte[] blockIn = new byte[16]; 18 | byte[] sapKey = new byte[16]; 19 | int[][] key_schedule = new int[11][4]; 20 | generate_session_key(default_sap, message3, sapKey); 21 | generate_key_schedule(sapKey, key_schedule); 22 | z_xor(chunk2, blockIn, 1); 23 | cycle(blockIn, key_schedule); 24 | for (int i = 0; i < 16; i++) { 25 | keyOut[i] = (byte) (blockIn[i] ^ chunk1[i]); 26 | } 27 | x_xor(keyOut, keyOut, 1); 28 | z_xor(keyOut, keyOut, 1); 29 | } 30 | 31 | void decryptMessage(byte[] messageIn, byte[] decryptedMessage) { 32 | byte[] buffer = new byte[16]; 33 | byte tmp; 34 | int mode = messageIn[12]; // 0,1,2,3 35 | 36 | // For M0-M6 we follow the same pattern 37 | for (int i = 0; i < 8; i++) { 38 | // First, copy in the nth block (we must start with the last one) 39 | for (int j = 0; j < 16; j++) { 40 | if (mode == 3) { 41 | buffer[j] = messageIn[(0x80 - 0x10 * i) + j]; 42 | } else if (mode == 2 || mode == 1 || mode == 0) { 43 | buffer[j] = messageIn[(0x10 * (i + 1)) + j]; 44 | } 45 | } 46 | // do this permutation and update 9 times. Could this be cycle(), or the reverse of cycle()? 47 | for (int j = 0; j < 9; j++) { 48 | int base = 0x80 - 0x10 * j; 49 | 50 | buffer[0x0] = (byte) (message_table_index(base + 0x0)[buffer[0x0] & 0xFF] ^ message_key[mode][base + 0x0]); 51 | buffer[0x4] = (byte) (message_table_index(base + 0x4)[buffer[0x4] & 0xFF] ^ message_key[mode][base + 0x4]); 52 | buffer[0x8] = (byte) (message_table_index(base + 0x8)[buffer[0x8] & 0xFF] ^ message_key[mode][base + 0x8]); 53 | buffer[0xc] = (byte) (message_table_index(base + 0xc)[buffer[0xc] & 0xFF] ^ message_key[mode][base + 0xc]); 54 | 55 | tmp = buffer[0x0d]; 56 | buffer[0xd] = (byte) (message_table_index(base + 0xd)[buffer[0x9] & 0xFF] ^ message_key[mode][base + 0xd]); 57 | buffer[0x9] = (byte) (message_table_index(base + 0x9)[buffer[0x5] & 0xFF] ^ message_key[mode][base + 0x9]); 58 | buffer[0x5] = (byte) (message_table_index(base + 0x5)[buffer[0x1] & 0xFF] ^ message_key[mode][base + 0x5]); 59 | buffer[0x1] = (byte) (message_table_index(base + 0x1)[tmp & 0xFF] ^ message_key[mode][base + 0x1]); 60 | 61 | tmp = buffer[0x02]; 62 | buffer[0x2] = (byte) (message_table_index(base + 0x2)[buffer[0xa] & 0xFF] ^ message_key[mode][base + 0x2]); 63 | buffer[0xa] = (byte) (message_table_index(base + 0xa)[tmp & 0xFF] ^ message_key[mode][base + 0xa]); 64 | tmp = buffer[0x06]; 65 | buffer[0x6] = (byte) (message_table_index(base + 0x6)[buffer[0xe] & 0xFF] ^ message_key[mode][base + 0x6]); 66 | buffer[0xe] = (byte) (message_table_index(base + 0xe)[tmp & 0xFF] ^ message_key[mode][base + 0xe]); 67 | 68 | tmp = buffer[0x3]; 69 | buffer[0x3] = (byte) (message_table_index(base + 0x3)[buffer[0x7] & 0xFF] ^ message_key[mode][base + 0x3]); 70 | buffer[0x7] = (byte) (message_table_index(base + 0x7)[buffer[0xb] & 0xFF] ^ message_key[mode][base + 0x7]); 71 | buffer[0xb] = (byte) (message_table_index(base + 0xb)[buffer[0xf] & 0xFF] ^ message_key[mode][base + 0xb]); 72 | buffer[0xf] = (byte) (message_table_index(base + 0xf)[tmp & 0xFF] ^ message_key[mode][base + 0xf]); 73 | 74 | // Now we must replace the entire buffer with 4 words that we read and xor together 75 | 76 | ByteBuffer block = ByteBuffer.wrap(buffer); 77 | block.order(ByteOrder.LITTLE_ENDIAN); 78 | 79 | block.putInt(table_s9[0x000 + (buffer[0x0] & 0xFF)] ^ 80 | table_s9[0x100 + (buffer[0x1] & 0xFF)] ^ 81 | table_s9[0x200 + (buffer[0x2] & 0xFF)] ^ 82 | table_s9[0x300 + (buffer[0x3] & 0xFF)]); 83 | block.putInt(table_s9[0x000 + (buffer[0x4] & 0xFF)] ^ 84 | table_s9[0x100 + (buffer[0x5] & 0xFF)] ^ 85 | table_s9[0x200 + (buffer[0x6] & 0xFF)] ^ 86 | table_s9[0x300 + (buffer[0x7] & 0xFF)]); 87 | block.putInt(table_s9[0x000 + (buffer[0x8] & 0xFF)] ^ 88 | table_s9[0x100 + (buffer[0x9] & 0xFF)] ^ 89 | table_s9[0x200 + (buffer[0xa] & 0xFF)] ^ 90 | table_s9[0x300 + (buffer[0xb] & 0xFF)]); 91 | block.putInt(table_s9[0x000 + (buffer[0xc] & 0xFF)] ^ 92 | table_s9[0x100 + (buffer[0xd] & 0xFF)] ^ 93 | table_s9[0x200 + (buffer[0xe] & 0xFF)] ^ 94 | table_s9[0x300 + (buffer[0xf] & 0xFF)]); 95 | } 96 | // Next, another permute with a different table 97 | buffer[0x0] = table_s10[(0x0 << 8) + (buffer[0x0] & 0xFF)]; 98 | buffer[0x4] = table_s10[(0x4 << 8) + (buffer[0x4] & 0xFF)]; 99 | buffer[0x8] = table_s10[(0x8 << 8) + (buffer[0x8] & 0xFF)]; 100 | buffer[0xc] = table_s10[(0xc << 8) + (buffer[0xc] & 0xFF)]; 101 | 102 | tmp = buffer[0x0d]; 103 | buffer[0xd] = table_s10[(0xd << 8) + (buffer[0x9] & 0xFF)]; 104 | buffer[0x9] = table_s10[(0x9 << 8) + (buffer[0x5] & 0xFF)]; 105 | buffer[0x5] = table_s10[(0x5 << 8) + (buffer[0x1] & 0xFF)]; 106 | buffer[0x1] = table_s10[(0x1 << 8) + (tmp & 0xFF)]; 107 | 108 | tmp = buffer[0x02]; 109 | buffer[0x2] = table_s10[(0x2 << 8) + (buffer[0xa] & 0xFF)]; 110 | buffer[0xa] = table_s10[(0xa << 8) + (tmp & 0xFF)]; 111 | tmp = buffer[0x06]; 112 | buffer[0x6] = table_s10[(0x6 << 8) + (buffer[0xe] & 0xFF)]; 113 | buffer[0xe] = table_s10[(0xe << 8) + (tmp & 0xFF)]; 114 | 115 | tmp = buffer[0x3]; 116 | buffer[0x3] = table_s10[(0x3 << 8) + (buffer[0x7] & 0xFF)]; 117 | buffer[0x7] = table_s10[(0x7 << 8) + (buffer[0xb] & 0xFF)]; 118 | buffer[0xb] = table_s10[(0xb << 8) + (buffer[0xf] & 0xFF)]; 119 | buffer[0xf] = table_s10[(0xf << 8) + (tmp & 0xFF)]; 120 | 121 | // And finally xor with the previous block of the message, except in mode-2 where we do this in reverse 122 | byte[] xorResult = new byte[16]; 123 | if (mode == 2 || mode == 1 || mode == 0) { 124 | if (i > 0) { 125 | xor_blocks(buffer, Arrays.copyOfRange(messageIn, 0x10 * i, 0x10 * i + 16), xorResult); // remember that the first 0x10 bytes are the header 126 | System.arraycopy(xorResult, 0, decryptedMessage, 0x10 * i, 16); 127 | } else { 128 | xor_blocks(buffer, message_iv[mode], xorResult); 129 | System.arraycopy(xorResult, 0, decryptedMessage, 0x10 * i, 16); 130 | } 131 | 132 | } else { 133 | if (i < 7) { 134 | xor_blocks(buffer, Arrays.copyOfRange(messageIn, 0x70 - 0x10 * i, (0x70 - 0x10 * i) + 16), xorResult); 135 | System.arraycopy(xorResult, 0, decryptedMessage, 0x70 - 0x10 * i, 16); 136 | } else { 137 | xor_blocks(buffer, message_iv[mode], xorResult); 138 | System.arraycopy(xorResult, 0, decryptedMessage, 0x70 - 0x10 * i, 16); 139 | } 140 | } 141 | } 142 | } 143 | 144 | void generate_key_schedule(byte[] key_material, int[][] key_schedule) { 145 | int[] key_data = new int[4]; 146 | for (int i = 0; i < 11; i++) { 147 | key_schedule[i][0] = 0xdeadbeef; 148 | key_schedule[i][1] = 0xdeadbeef; 149 | key_schedule[i][2] = 0xdeadbeef; 150 | key_schedule[i][3] = 0xdeadbeef; 151 | } 152 | byte[] buffer = new byte[16]; 153 | int ti = 0; 154 | // G 155 | t_xor(key_material, buffer); 156 | 157 | ByteBuffer wrap = ByteBuffer.wrap(buffer); 158 | wrap.order(ByteOrder.LITTLE_ENDIAN); 159 | for (int i = 0; i < 4; i++) { 160 | key_data[i] = wrap.getInt(); 161 | } 162 | 163 | for (int round = 0; round < 11; round++) { 164 | // H 165 | key_schedule[round][0] = key_data[0]; 166 | // I 167 | byte[] table1 = table_index(ti); 168 | byte[] table2 = table_index(ti + 1); 169 | byte[] table3 = table_index(ti + 2); 170 | byte[] table4 = table_index(ti + 3); 171 | ti += 4; 172 | 173 | buffer[0] ^= table1[buffer[0x0d] & 0xFF] ^ index_mangle[round]; 174 | buffer[1] ^= table2[buffer[0x0e] & 0xFF]; 175 | buffer[2] ^= table3[buffer[0x0f] & 0xFF]; 176 | buffer[3] ^= table4[buffer[0x0c] & 0xFF]; 177 | 178 | key_data[0] = wrap.getInt(0); 179 | 180 | // H 181 | key_schedule[round][1] = key_data[1]; 182 | // J 183 | key_data[1] ^= key_data[0]; 184 | wrap.putInt(4, key_data[1]); 185 | // H 186 | key_schedule[round][2] = key_data[2]; 187 | // J 188 | key_data[2] ^= key_data[1]; 189 | wrap.putInt(8, key_data[2]); 190 | // K and L 191 | // Implement K and L to fill in other bits of the key schedule 192 | key_schedule[round][3] = key_data[3]; 193 | // J again 194 | key_data[3] ^= key_data[2]; 195 | wrap.putInt(12, key_data[3]); 196 | } 197 | for (int i = 0; i < 11; i++) { 198 | byte[] tmp = new byte[16]; 199 | wrap = ByteBuffer.wrap(tmp); 200 | wrap.order(ByteOrder.LITTLE_ENDIAN); 201 | for (int j = 0; j < 4; j++) { 202 | wrap.putInt(key_schedule[i][j]); 203 | } 204 | 205 | } 206 | } 207 | 208 | void generate_session_key(byte[] oldSap, byte[] messageIn, byte[] sessionKey) { 209 | byte[] decryptedMessage = new byte[128]; 210 | byte[] newSap = new byte[320]; 211 | int round; 212 | byte[] md5 = new byte[16]; 213 | 214 | decryptMessage(messageIn, decryptedMessage); 215 | 216 | System.arraycopy(static_source_1, 0, newSap, 0, 0x11); 217 | System.arraycopy(decryptedMessage, 0, newSap, 0x11, 0x80); 218 | System.arraycopy(oldSap, 0x80, newSap, 0x091, 0x80); 219 | System.arraycopy(static_source_2, 0, newSap, 0x111, 0x2f); 220 | System.arraycopy(initial_session_key, 0, sessionKey, 0, 16); 221 | 222 | for (round = 0; round < 5; round++) { 223 | byte[] base = Arrays.copyOfRange(newSap, round * 64, newSap.length); 224 | modifiedMD5.modified_md5(base, sessionKey, md5); 225 | sapHash.sap_hash(base, sessionKey); 226 | ByteBuffer md5Wrap = ByteBuffer.wrap(md5); 227 | md5Wrap.order(ByteOrder.LITTLE_ENDIAN); 228 | ByteBuffer sessionKeyWrap = ByteBuffer.wrap(sessionKey); 229 | sessionKeyWrap.order(ByteOrder.LITTLE_ENDIAN); 230 | for (int i = 0; i < 4; i++) { 231 | sessionKeyWrap.putInt(i * 4, (int) ((sessionKeyWrap.getInt(i * 4) + md5Wrap.getInt(i * 4)) & 0xffffffffL)); 232 | } 233 | } 234 | 235 | for (int i = 0; i < 16; i += 4) { 236 | byte tmp = sessionKey[i]; 237 | sessionKey[i] = sessionKey[i + 3]; 238 | sessionKey[i + 3] = tmp; 239 | tmp = sessionKey[i + 1]; 240 | sessionKey[i + 1] = sessionKey[i + 2]; 241 | sessionKey[i + 2] = tmp; 242 | } 243 | 244 | // Finally the whole thing is XORd with 121: 245 | for (int i = 0; i < 16; i++) { 246 | sessionKey[i] ^= 121; 247 | } 248 | } 249 | 250 | void cycle(byte[] block, int[][] key_schedule) { 251 | int ptr1, ptr2, ptr3, ptr4, ab; 252 | 253 | ByteBuffer bWords = ByteBuffer.wrap(block); 254 | bWords.order(ByteOrder.LITTLE_ENDIAN); 255 | bWords.putInt(0, bWords.getInt(0) ^ key_schedule[10][0]); 256 | bWords.putInt(4, bWords.getInt(4) ^ key_schedule[10][1]); 257 | bWords.putInt(8, bWords.getInt(8) ^ key_schedule[10][2]); 258 | bWords.putInt(12, bWords.getInt(12) ^ key_schedule[10][3]); 259 | // First, these are permuted 260 | permute_block_1(block); 261 | 262 | for (int round = 0; round < 9; round++) { 263 | // E 264 | // Note that table_s5 is a table of 4-byte words. Therefore we do not need to <<2 these indices 265 | // TODO: Are these just T-tables? 266 | 267 | byte[] key = new byte[16]; 268 | ByteBuffer wrap = ByteBuffer.wrap(key); 269 | wrap.order(ByteOrder.LITTLE_ENDIAN); 270 | for (int i = 0; i < 4; i++) { 271 | wrap.putInt(key_schedule[9 - round][i]); 272 | } 273 | 274 | ptr1 = table_s5[(block[3] & 0xff) ^ (key[3] & 0xff)]; 275 | ptr2 = table_s6[(block[2] & 0xff) ^ (key[2] & 0xff)]; 276 | ptr3 = table_s8[(block[0] & 0xff) ^ (key[0] & 0xff)]; 277 | ptr4 = table_s7[(block[1] & 0xff) ^ (key[1] & 0xff)]; 278 | 279 | // A B 280 | ab = ptr1 ^ ptr2 ^ ptr3 ^ ptr4; 281 | 282 | // C 283 | bWords.putInt(0, ab); 284 | 285 | ptr2 = table_s5[(block[7] & 0xff) ^ (key[7] & 0xff)]; 286 | ptr1 = table_s6[(block[6] & 0xff) ^ (key[6] & 0xff)]; 287 | ptr4 = table_s7[(block[5] & 0xff) ^ (key[5] & 0xff)]; 288 | ptr3 = table_s8[(block[4] & 0xff) ^ (key[4] & 0xff)]; 289 | // A B again 290 | ab = ptr1 ^ ptr2 ^ ptr3 ^ ptr4; 291 | 292 | // D is a bit of a nightmare, but it is really not as complicated as you might think 293 | bWords.putInt(4, ab); 294 | bWords.putInt(8, table_s5[(block[11] & 0xff) ^ (key[11] & 0xff)] ^ 295 | table_s6[(block[10] & 0xff) ^ (key[10] & 0xff)] ^ 296 | table_s7[(block[9] & 0xff) ^ (key[9] & 0xff)] ^ 297 | table_s8[(block[8] & 0xff) ^ (key[8] & 0xff)]); 298 | 299 | bWords.putInt(12, table_s5[(block[15] & 0xff) ^ (key[15] & 0xff)] ^ 300 | table_s6[(block[14] & 0xff) ^ (key[14] & 0xff)] ^ 301 | table_s7[(block[13] & 0xff) ^ (key[13] & 0xff)] ^ 302 | table_s8[(block[12] & 0xff) ^ (key[12] & 0xff)]); 303 | 304 | // In the last round, instead of the permute, we do F 305 | permute_block_2(block, 8 - round); 306 | } 307 | 308 | bWords.putInt(0, bWords.getInt(0) ^ key_schedule[0][0]); 309 | bWords.putInt(4, bWords.getInt(4) ^ key_schedule[0][1]); 310 | bWords.putInt(8, bWords.getInt(8) ^ key_schedule[0][2]); 311 | bWords.putInt(12, bWords.getInt(12) ^ key_schedule[0][3]); 312 | } 313 | 314 | private void xor_blocks(byte[] a, byte[] b, byte[] out) { 315 | for (int i = 0; i < 16; i++) { 316 | out[i] = (byte) (a[i] ^ b[i]); 317 | } 318 | } 319 | 320 | private void z_xor(byte[] in, byte[] out, int blocks) { 321 | for (int j = 0; j < blocks; j++) { 322 | for (int i = 0; i < 16; i++) { 323 | out[j * 16 + i] = (byte) (in[j * 16 + i] ^ z_key[i]); 324 | } 325 | } 326 | } 327 | 328 | private void x_xor(byte[] in, byte[] out, int blocks) { 329 | for (int j = 0; j < blocks; j++) { 330 | for (int i = 0; i < 16; i++) { 331 | out[j * 16 + i] = (byte) (in[j * 16 + i] ^ x_key[i]); 332 | } 333 | } 334 | } 335 | 336 | private void t_xor(byte[] in, byte[] out) { 337 | for (int i = 0; i < 16; i++) { 338 | out[i] = (byte) (in[i] ^ t_key[i]); 339 | } 340 | } 341 | 342 | private byte[] table_index(int i) { 343 | return Arrays.copyOfRange(table_s1, ((31 * i) % 0x28) << 8, table_s1.length); 344 | } 345 | 346 | private byte[] message_table_index(int i) { 347 | return Arrays.copyOfRange(table_s2, (97 * i % 144) << 8, table_s2.length); 348 | } 349 | 350 | private void permute_block_1(byte[] block) { 351 | block[0] = table_s3[block[0] & 0xff]; 352 | block[4] = table_s3[0x400 + (block[4] & 0xff)]; 353 | block[8] = table_s3[0x800 + (block[8] & 0xff)]; 354 | block[12] = table_s3[0xc00 + (block[12] & 0xff)]; 355 | 356 | byte tmp = block[13]; 357 | block[13] = table_s3[0x100 + (block[9] & 0xff)]; 358 | block[9] = table_s3[0xd00 + (block[5] & 0xff)]; 359 | block[5] = table_s3[0x900 + (block[1] & 0xff)]; 360 | block[1] = table_s3[0x500 + (tmp & 0xff)]; 361 | 362 | tmp = block[2]; 363 | block[2] = table_s3[0xa00 + (block[10] & 0xff)]; 364 | block[10] = table_s3[0x200 + (tmp & 0xff)]; 365 | tmp = block[6]; 366 | block[6] = table_s3[0xe00 + (block[14] & 0xff)]; 367 | block[14] = table_s3[0x600 + (tmp & 0xff)]; 368 | 369 | tmp = block[3]; 370 | block[3] = table_s3[0xf00 + (block[7] & 0xff)]; 371 | block[7] = table_s3[0x300 + (block[11] & 0xff)]; 372 | block[11] = table_s3[0x700 + (block[15] & 0xff)]; 373 | block[15] = table_s3[0xb00 + (tmp & 0xff)]; 374 | } 375 | 376 | private byte[] permute_table_2(int i) { 377 | return Arrays.copyOfRange(table_s4, ((71 * i) % 144) << 8, table_s4.length); 378 | } 379 | 380 | private void permute_block_2(byte[] block, int round) { 381 | block[0] = permute_table_2(round * 16 + 0)[(block[0] & 0xff)]; 382 | block[4] = permute_table_2(round * 16 + 4)[(block[4] & 0xff)]; 383 | block[8] = permute_table_2(round * 16 + 8)[(block[8] & 0xff)]; 384 | block[12] = permute_table_2(round * 16 + 12)[(block[12] & 0xff)]; 385 | 386 | byte tmp = block[13]; 387 | block[13] = permute_table_2(round * 16 + 13)[(block[9] & 0xff)]; 388 | block[9] = permute_table_2(round * 16 + 9)[(block[5] & 0xff)]; 389 | block[5] = permute_table_2(round * 16 + 5)[(block[1] & 0xff)]; 390 | block[1] = permute_table_2(round * 16 + 1)[(tmp & 0xff)]; 391 | 392 | tmp = block[2]; 393 | block[2] = permute_table_2(round * 16 + 2)[(block[10] & 0xff)]; 394 | block[10] = permute_table_2(round * 16 + 10)[(tmp & 0xff)]; 395 | tmp = block[6]; 396 | block[6] = permute_table_2(round * 16 + 6)[(block[14] & 0xff)]; 397 | block[14] = permute_table_2(round * 16 + 14)[(tmp & 0xff)]; 398 | 399 | tmp = block[3]; 400 | block[3] = permute_table_2(round * 16 + 3)[(block[7] & 0xff)]; 401 | block[7] = permute_table_2(round * 16 + 7)[(block[11] & 0xff)]; 402 | block[11] = permute_table_2(round * 16 + 11)[(block[15] & 0xff)]; 403 | block[15] = permute_table_2(round * 16 + 15)[(tmp & 0xff)]; 404 | } 405 | } 406 | --------------------------------------------------------------------------------