Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
*.iml
.gradle
/local.properties
/.idea/caches/build_file_checksums.ser
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea
.DS_Store
/build
/captures
.externalNativeBuild
/app/.cxx
*.jks
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# 简介
# Introduction

`Airplay2`是apple在`iOS11.3`中新加的特性,用作视频和音频的局域网投放
This is a Android low latency Airplay server.

`AirplayServer`作为接收端,可以接收来自支持`Airplay2`设备的数据
`Airplay2` is a new feature added by Apple in `iOS11.3` for LAN and video and audio delivery.

`AirplayServer`可运行在`Android`设备,代码99%是C语言编写,方便移植
`AirplayServer` as the receiving end, can receive data from the support `Airplay2` device

# 功能
`AirplayServer` can run on `Android` device, code 99% is written in C language, easy to port

1. mDNS发布服务
2. 握手协议
3. 接收镜像数据
4. MediaCodec硬解与展示
5. 接收音频数据
6. fdk-aac音频解码
7. AudioTrack播放PCM音乐
# Features

# 演示截图
1. mDNS publishing service
2. Handshake agreement
3. Receive mirror data
4. MediaCodec hard solution and display
5. Receive audio data
6. fdk-aac audio decoding
7. AudioTrack plays PCM music

下图是一次屏幕数据和音乐的投放演示,其中`iPhone`的系统是`iOS12`
# Show Screenshot

![](https://ww1.sinaimg.cn/large/007rAy9hgy1g0l65hwvg7j30u01o0juj.jpg)
![Alt text](docs/IMG_20230329_175147171p_HDR.jpg)
5 changes: 3 additions & 2 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
cmake_minimum_required(VERSION 3.4.1)
cmake_minimum_required(VERSION 3.22.1)
project('airplay-server-c')

add_subdirectory(src/main/cpp/lib/crypto)
add_subdirectory(src/main/cpp/lib/curve25519)
Expand Down Expand Up @@ -31,7 +32,7 @@ find_library( log-lib
log )

target_link_libraries( raop_server
${log-lib}
${log-lib}
play-lib
jdns_sd
ed25519
Expand Down
15 changes: 9 additions & 6 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
apply plugin: 'com.android.application'

android {
compileSdkVersion 27
compileSdk 30
defaultConfig {
namespace "com.fang.myapplication"
applicationId "com.fang.myapplication"
minSdkVersion 22
targetSdkVersion 27
minSdk 30
targetSdk 30
ndkVersion "25.1.8937393"
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
abiFilters "armeabi-v7a"
abiFilters "armeabi-v7a", "arm64-v8a"
cppFlags ""
}
}
ndk {
abiFilters "armeabi-v7a"
abiFilters "armeabi-v7a", "arm64-v8a"
}
}
buildTypes {
Expand All @@ -28,6 +30,7 @@ android {
externalNativeBuild {
cmake {
path "CMakeLists.txt"
version "3.22.1"
}
}
sourceSets {
Expand All @@ -41,7 +44,7 @@ android {

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
5 changes: 2 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.fang.myapplication">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
Expand All @@ -16,7 +15,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand Down
8 changes: 4 additions & 4 deletions app/src/main/cpp/jni_raop_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ void OnRecvAudioData(void *observer, pcm_data_struct *data) {
JNIEnv* jniEnv = NULL;
g_JavaVM->AttachCurrentThread(&jniEnv, NULL);
jclass cls = jniEnv->GetObjectClass(obj);
jmethodID onRecvVideoDataM = jniEnv->GetMethodID(cls, "onRecvAudioData", "([SJ)V");
jmethodID onRecvAudioDataM = jniEnv->GetMethodID(cls, "onRecvAudioData", "([SJ)V");
jniEnv->DeleteLocalRef(cls);
jshortArray sarr = jniEnv->NewShortArray(data->data_len);
if (sarr == NULL) return;
jniEnv->SetShortArrayRegion(sarr, (jint) 0, data->data_len, (jshort *) data->data);
jniEnv->CallVoidMethod(obj, onRecvVideoDataM, sarr, data->pts);
jniEnv->CallVoidMethod(obj, onRecvAudioDataM, sarr, data->pts);
jniEnv->DeleteLocalRef(sarr);
g_JavaVM->DetachCurrentThread();
}
Expand All @@ -40,7 +40,7 @@ void OnRecvVideoData(void *observer, h264_decode_struct *data) {
if (barr == NULL) return;
jniEnv->SetByteArrayRegion(barr, (jint) 0, data->data_len, (jbyte *) data->data);
jniEnv->CallVoidMethod(obj, onRecvVideoDataM, barr, data->frame_type,
data->pts, data->pts);
data->nTimeStamp, data->pts);
jniEnv->DeleteLocalRef(barr);
g_JavaVM->DetachCurrentThread();
}
Expand Down Expand Up @@ -132,5 +132,5 @@ Java_com_fang_myapplication_RaopServer_stop(JNIEnv* env, jobject object, jlong o
jobject obj = (jobject) raop_get_callback_cls(raop);
raop_destroy(raop);
env->DeleteGlobalRef(obj);

LOGD("roap stopped");
}
16 changes: 8 additions & 8 deletions app/src/main/cpp/lib/mirror_buffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct mirror_buffer_s {
int nextDecryptCount;
uint8_t og[16];
/* AES key and IV */
// 需要二次加工才能使用
// Need secondary processing to use
unsigned char aeskey[RAOP_AESKEY_LEN];
unsigned char ecdh_secret[32];
};
Expand Down Expand Up @@ -64,7 +64,7 @@ mirror_buffer_init_aes(mirror_buffer_t *mirror_buffer, uint64_t streamConnection
fwrite(decrypt_aesiv, 16, 1, keyfile);
fclose(keyfile);
#endif
// 需要在外部初始化
// Need to be initialized externally
AES_init_ctx_iv(&mirror_buffer->aes_ctx, decrypt_aeskey, decrypt_aesiv);
mirror_buffer->nextDecryptCount = 0;
}
Expand All @@ -90,20 +90,20 @@ mirror_buffer_init(logger_t *logger,
}

void mirror_buffer_decrypt(mirror_buffer_t *mirror_buffer, unsigned char* input, unsigned char* output, int inputLen) {
// 开始解密
// Start decrypting
if (mirror_buffer->nextDecryptCount > 0) {//mirror_buffer->nextDecryptCount = 10
for (int i = 0; i < mirror_buffer->nextDecryptCount; i++) {
output[i] = (input[i] ^ mirror_buffer->og[(16 - mirror_buffer->nextDecryptCount) + i]);
}
}
// 处理加密的字节
// Handling encrypted bytes
int encryptlen = ((inputLen - mirror_buffer->nextDecryptCount) / 16) * 16;
// aes解密
// Aes decryption
AES_CTR_xcrypt_buffer(&mirror_buffer->aes_ctx, input + mirror_buffer->nextDecryptCount, encryptlen);
// 复制到输出
// Copy to output
memcpy(output + mirror_buffer->nextDecryptCount, input + mirror_buffer->nextDecryptCount, encryptlen);
int outputlength = mirror_buffer->nextDecryptCount + encryptlen;
//处理剩余长度
// Processing remaining length
int restlen = (inputLen - mirror_buffer->nextDecryptCount) % 16;
int reststart = inputLen - restlen;
mirror_buffer->nextDecryptCount = 0;
Expand All @@ -115,7 +115,7 @@ void mirror_buffer_decrypt(mirror_buffer_t *mirror_buffer, unsigned char* input,
output[reststart + j] = mirror_buffer->og[j];
}
outputlength += restlen;
mirror_buffer->nextDecryptCount = 16 - restlen;// 差16-6=10个字节
mirror_buffer->nextDecryptCount = 16 - restlen;// Difference 16-6=10 bytes
}
}

Expand Down
2 changes: 1 addition & 1 deletion app/src/main/cpp/lib/raop.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
#include <android/log.h>

struct raop_s {
/* Callbacks for audio */
/* Callbacks for audio and video */
raop_callbacks_t callbacks;

/* Logger instance */
Expand Down
22 changes: 11 additions & 11 deletions app/src/main/cpp/lib/raop_buffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,26 @@ typedef struct {
unsigned int timestamp;
unsigned int ssrc;

/* 内存大小 */
/* Memory size */
int audio_buffer_size;
/* 解码后长度 */
/* Decoded length */
int audio_buffer_len;
void *audio_buffer;
} raop_buffer_entry_t;

struct raop_buffer_s {
logger_t *logger;
/* 解密使用的key and IV */
/* Key and IV used for decryption */
unsigned char aeskey[RAOP_AESKEY_LEN];
unsigned char aesiv[RAOP_AESIV_LEN];

HANDLE_AACDECODER phandle;

/* First and last seqnum */
int is_empty;
// 播放的序号
// Playing sequence number
unsigned short first_seqnum;
// 收到的序号
// Received serial number
unsigned short last_seqnum;

/* RTP buffer entries */
Expand Down Expand Up @@ -117,7 +117,7 @@ raop_buffer_init_key_iv(raop_buffer_t *raop_buffer,
const unsigned char *ecdh_secret)
{

// 初始化key
// Initialization key
unsigned char eaeskey[64];
memcpy(eaeskey, aeskey, 16);
sha512_context ctx;
Expand Down Expand Up @@ -272,7 +272,7 @@ raop_buffer_queue(raop_buffer_t *raop_buffer, unsigned char *data, unsigned shor
}
int payloadsize = datalen - 12;
#ifdef DUMP_AUDIO
// 未解密的文件
// Undecrypted file
if (file_source != NULL) {
fwrite(&data[12], payloadsize, 1, file_source);
}
Expand All @@ -295,7 +295,7 @@ raop_buffer_queue(raop_buffer_t *raop_buffer, unsigned char *data, unsigned shor
entry->flags = data[0];
entry->type = data[1];
entry->seqnum = seqnum;
// 第4个字节开始是pts
// The 4th byte starts with pts
entry->timestamp = (data[4] << 24) | (data[5] << 16) |
(data[6] << 8) | data[7];
entry->ssrc = (data[8] << 24) | (data[9] << 16) |
Expand All @@ -305,19 +305,19 @@ raop_buffer_queue(raop_buffer_t *raop_buffer, unsigned char *data, unsigned shor
encryptedlen = payloadsize/16*16;
unsigned char packetbuf[payloadsize];
memset(packetbuf, 0, payloadsize);
// 需要在内部初始化
// Need to be initialized internally
AES_CTX aes_ctx_audio;
AES_set_key(&aes_ctx_audio, raop_buffer->aeskey, raop_buffer->aesiv, AES_MODE_128);
AES_convert_key(&aes_ctx_audio);
AES_cbc_decrypt(&aes_ctx_audio, &data[12], packetbuf, encryptedlen);
memcpy(packetbuf+encryptedlen, &data[12+encryptedlen], payloadsize-encryptedlen);
#ifdef DUMP_AUDIO
// 解密的文件
// Decrypted file
if (file_aac != NULL) {
fwrite(packetbuf, payloadsize, 1, file_aac);
}
#endif
// aac解码pcm
// Aac decoding pcm
int ret = 0;
int pkt_size = payloadsize;
UINT valid_size = payloadsize;
Expand Down
10 changes: 5 additions & 5 deletions app/src/main/cpp/lib/raop_handlers.h
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ raop_handler_setup(raop_conn_t *conn,
}


// 解析bplist
// Parsing bplist
plist_t root_node = NULL;
plist_from_bin(data, datalen, &root_node);
plist_t streams_note = plist_dict_get_item(root_node, "streams");
Expand All @@ -275,7 +275,7 @@ raop_handler_setup(raop_conn_t *conn,
unsigned char aeskey[16];
setup++;
logger_log(conn->raop->logger, LOGGER_DEBUG, "SETUP 1");
// 第一次setup
// First setup
plist_t eiv_note = plist_dict_get_item(root_node, "eiv");
char* eiv= NULL;
uint64_t eiv_len = 0;
Expand All @@ -287,12 +287,12 @@ raop_handler_setup(raop_conn_t *conn,
uint64_t ekey_len = 0;
plist_get_data_val(ekey_note, &ekey, &ekey_len);
logger_log(conn->raop->logger, LOGGER_DEBUG, "ekey_len = %llu", ekey_len);
// 时间port
// Time port
uint64_t timing_rport;
plist_t time_note = plist_dict_get_item(root_node, "timingPort");
plist_get_uint_val(time_note, &timing_rport);
logger_log(conn->raop->logger, LOGGER_DEBUG, "timing_rport = %llu", timing_rport);
// ekey是72字节
// ekey is 72 bytes
int ret = fairplay_decrypt(conn->fairplay, ekey, aeskey);
logger_log(conn->raop->logger, LOGGER_DEBUG, "fairplay_decrypt ret = %d", ret);
unsigned char ecdh_secret[32];
Expand Down Expand Up @@ -355,7 +355,7 @@ raop_handler_setup(raop_conn_t *conn,
logger_log(conn->raop->logger, LOGGER_ERR, "RAOP not initialized at SETUP, playing will fail!");
http_response_set_disconnect(response, 1);
}
// 需要返回端口
// Need to return port
/**
* <dict>
<key>streams</key>
Expand Down
Loading