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
34 changes: 32 additions & 2 deletions src/Connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ uint16_t RtspPortNumber;
uint16_t ControlPortNumber;
uint16_t AudioPortNumber;
uint16_t VideoPortNumber;
uint16_t MicPortNumber;
SS_PING AudioPingPayload;
SS_PING VideoPingPayload;
SS_PING MicPingPayload;
uint32_t ControlConnectData;
uint32_t SunshineFeatureFlags;
uint32_t EncryptionFeaturesSupported;
Expand All @@ -50,7 +52,8 @@ static const char* stageNames[STAGE_MAX] = {
"control stream establishment",
"video stream establishment",
"audio stream establishment",
"input stream establishment"
"input stream establishment",
"microphone stream initialization"
};

// Get the name of the current stage based on its number
Expand All @@ -73,6 +76,12 @@ void LiStopConnection(void) {
// Set the interrupted flag
LiInterruptConnection();

if (stage == STAGE_MICROPHONE_STREAM_INIT) {
Limelog("Stopping microphone stream...");
destroyMicrophoneStream();
stage--;
Limelog("done\n");
}
if (stage == STAGE_INPUT_STREAM_START) {
Limelog("Stopping input stream...");
stopInputStream();
Expand Down Expand Up @@ -517,7 +526,28 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre
LC_ASSERT(stage == STAGE_INPUT_STREAM_START);
ListenerCallbacks.stageComplete(STAGE_INPUT_STREAM_START);
Limelog("done\n");


// Initialize microphone stream if enabled and port was negotiated
if (StreamConfig.enableMic && MicPortNumber != 0) {
Limelog("Initializing microphone stream...");
ListenerCallbacks.stageStarting(STAGE_MICROPHONE_STREAM_INIT);
err = initializeMicrophoneStream();
if (err != 0) {
Limelog("failed: %d (microphone will be unavailable)\n", err);
// Don't fail the connection for mic initialization failure
err = 0; // Reset error so connection continues
}
stage++;
LC_ASSERT(stage == STAGE_MICROPHONE_STREAM_INIT);
ListenerCallbacks.stageComplete(STAGE_MICROPHONE_STREAM_INIT);
Limelog("done\n");
}
else {
// Skip when microphone is disabled
stage++;
LC_ASSERT(stage == STAGE_MICROPHONE_STREAM_INIT);
}

// Wiggle the mouse a bit to wake the display up
LiSendMouseMoveEvent(1, 1);
PltSleepMs(10);
Expand Down
18 changes: 18 additions & 0 deletions src/Input.h
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,22 @@ typedef struct _SS_CONTROLLER_BATTERY_PACKET {
uint8_t zero[1]; // Alignment/reserved
} SS_CONTROLLER_BATTERY_PACKET, *PSS_CONTROLLER_BATTERY_PACKET;

// ============================================================================
// Reserved interfaces for future microphone control via Input Stream
// Currently microphone is controlled via RTSP SETUP/PLAY, not Input Stream.
// These definitions are kept for protocol reference (Sunshine compatibility).
// ============================================================================

// Microphone control command magic (send via Input Stream, not implemented)
#define SS_MICROPHONE_MAGIC 0x55000008
#define MIC_CONTROL_START 0x01
#define MIC_CONTROL_STOP 0x02

// Dynamic microphone configuration (for example, sample rate: 48kHz, channel count: 1, opus bitrate: 64kbps)
typedef struct _MIC_STREAM_CONFIGURATION {
int sampleRate;
int channelCount;
int bitrate;
} MIC_STREAM_CONFIGURATION, *PMIC_STREAM_CONFIGURATION;

#pragma pack(pop)
13 changes: 13 additions & 0 deletions src/Limelight-internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ extern uint16_t RtspPortNumber;
extern uint16_t ControlPortNumber;
extern uint16_t AudioPortNumber;
extern uint16_t VideoPortNumber;
extern uint16_t MicPortNumber;

extern SS_PING AudioPingPayload;
extern SS_PING VideoPingPayload;
extern SS_PING MicPingPayload;
extern uint32_t ControlConnectData;

extern uint32_t SunshineFeatureFlags;
Expand All @@ -48,11 +50,17 @@ extern uint32_t SunshineFeatureFlags;
#define SS_ENC_CONTROL_V2 0x01
#define SS_ENC_VIDEO 0x02
#define SS_ENC_AUDIO 0x04
#define SS_ENC_MICROPHONE 0x08

extern uint32_t EncryptionFeaturesSupported;
extern uint32_t EncryptionFeaturesRequested;
extern uint32_t EncryptionFeaturesEnabled;

// Microphone RTP stream values
#define MIC_PACKET_MAGIC 0x12345678
#define MIC_PACKET_TYPE_OPUS 0x61
#define MAX_MIC_PACKET_SIZE 1400

// ENet channel ID values
#define CTRL_CHANNEL_GENERIC 0x00
#define CTRL_CHANNEL_URGENT 0x01 // IDR, LTR ACK and RFI
Expand Down Expand Up @@ -151,3 +159,8 @@ int initializeInputStream(void);
void destroyInputStream(void);
int startInputStream(void);
int stopInputStream(void);

int initializeMicrophoneStream(void);
void destroyMicrophoneStream(void);
int sendMicrophoneOpusData(const unsigned char* opusData, int opusLength);
bool isMicrophoneEncryptionEnabled(void);
15 changes: 10 additions & 5 deletions src/Limelight.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ extern "C" {
#define COLOR_RANGE_FULL 1

// Values for 'encryptionFlags' field below
#define ENCFLG_NONE 0x00000000
#define ENCFLG_AUDIO 0x00000001
#define ENCFLG_VIDEO 0x00000002
#define ENCFLG_ALL 0xFFFFFFFF
#define ENCFLG_NONE 0x00000000
#define ENCFLG_AUDIO 0x00000001
#define ENCFLG_VIDEO 0x00000002
#define ENCFLG_MICROPHONE 0x00000004
#define ENCFLG_ALL 0xFFFFFFFF

// This function returns a string that you SHOULD append to the /launch and /resume
// query parameter string. This is used to enable certain extended functionality
Expand Down Expand Up @@ -100,6 +101,9 @@ typedef struct _STREAM_CONFIGURATION {
// in /launch and /resume requests.
char remoteInputAesKey[16];
char remoteInputAesIv[16];

// Specifies whether to enable microphone streaming from the client to host
bool enableMic;
} STREAM_CONFIGURATION, *PSTREAM_CONFIGURATION;

// Use this function to zero the stream configuration when allocated on the stack or heap
Expand Down Expand Up @@ -382,7 +386,8 @@ void LiInitializeAudioCallbacks(PAUDIO_RENDERER_CALLBACKS arCallbacks);
#define STAGE_VIDEO_STREAM_START 9
#define STAGE_AUDIO_STREAM_START 10
#define STAGE_INPUT_STREAM_START 11
#define STAGE_MAX 12
#define STAGE_MICROPHONE_STREAM_INIT 12
#define STAGE_MAX 13

// This callback is invoked to indicate that a stage of initialization is about to begin
typedef void(*ConnListenerStageStarting)(int stage);
Expand Down
161 changes: 161 additions & 0 deletions src/MicrophoneStream.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#include "Limelight-internal.h"
#include "PlatformSockets.h"

#define MIC_IV_LEN 16
#define MIC_HEADER_FLAGS 0x00

static SOCKET micSocket = INVALID_SOCKET;
static PPLT_CRYPTO_CONTEXT micEncryptionCtx = NULL;

// Microphone encryption state
static uint32_t micRiKeyId = 0;
static uint16_t micSequenceNumber = 0;

#pragma pack(push, 1)
typedef struct _MICROPHONE_PACKET_HEADER {
uint8_t flags;
uint8_t packetType;
uint16_t sequenceNumber;
uint32_t timestamp;
uint32_t ssrc;
} MICROPHONE_PACKET_HEADER, *PMICROPHONE_PACKET_HEADER;
#pragma pack(pop)

int initializeMicrophoneStream(void) {
if (micSocket != INVALID_SOCKET) {
return 0;
}

micEncryptionCtx = PltCreateCryptoContext();
if (micEncryptionCtx == NULL) {
return -1;
}

// Initialize riKeyId from the first 4 bytes of remoteInputAesIv
memcpy(&micRiKeyId, StreamConfig.remoteInputAesIv, sizeof(micRiKeyId));
micRiKeyId = BE32(micRiKeyId);

micSequenceNumber = 0;

micSocket = bindUdpSocket(RemoteAddr.ss_family, &LocalAddr, AddrLen, 0, SOCK_QOS_TYPE_AUDIO);
if (micSocket == INVALID_SOCKET) {
PltDestroyCryptoContext(micEncryptionCtx);
micEncryptionCtx = NULL;
return LastSocketFail();
}

return 0;
}

void destroyMicrophoneStream(void) {
if (micSocket != INVALID_SOCKET) {
closeSocket(micSocket);
micSocket = INVALID_SOCKET;
}

if (micEncryptionCtx != NULL) {
PltDestroyCryptoContext(micEncryptionCtx);
micEncryptionCtx = NULL;
}

micRiKeyId = 0;
micSequenceNumber = 0;
}

int sendMicrophoneOpusData(const unsigned char* opusData, int opusLength) {
LC_SOCKADDR saddr;
MICROPHONE_PACKET_HEADER header;
unsigned char packet[MAX_MIC_PACKET_SIZE];
int packetLength = 0;
int err = 0;

// Validate socket is initialized
if (micSocket == INVALID_SOCKET) {
return -1;
}

// Validate input parameters
if (opusData == NULL || opusLength <= 0) {
return -1;
}

// Validate opusLength doesn't exceed max payload size
if (opusLength > MAX_MIC_PACKET_SIZE - (int)sizeof(header)) {
Limelog("MIC: Input data too large (%d)\n", opusLength);
return -1;
}

// Initialize header
memset(&header, 0, sizeof(header));
header.flags = MIC_HEADER_FLAGS;
header.packetType = MIC_PACKET_TYPE_OPUS;
header.sequenceNumber = LE16(micSequenceNumber);
header.timestamp = LE32((uint32_t)PltGetMillis());
header.ssrc = LE32(MIC_PACKET_MAGIC);

if ((EncryptionFeaturesEnabled & SS_ENC_MICROPHONE) && micEncryptionCtx != NULL) {
unsigned char iv[MIC_IV_LEN] = {0};
unsigned char encryptedData[ROUND_TO_PKCS7_PADDED_LEN(MAX_MIC_PACKET_SIZE)];
int encryptedLength = (int)sizeof(encryptedData);

// IV = riKeyId + sequenceNumber in big-endian
uint32_t ivSeq = BE32(micRiKeyId + micSequenceNumber);
memcpy(iv, &ivSeq, sizeof(ivSeq));

if (!PltEncryptMessage(micEncryptionCtx,
ALGORITHM_AES_CBC,
CIPHER_FLAG_RESET_IV | CIPHER_FLAG_FINISH | CIPHER_FLAG_PAD_TO_BLOCK_SIZE,
(unsigned char*)StreamConfig.remoteInputAesKey,
sizeof(StreamConfig.remoteInputAesKey),
iv, sizeof(iv),
NULL, 0,
(unsigned char*)opusData, opusLength,
encryptedData, &encryptedLength)) {
Limelog("MIC: Encryption failed\n");
return -1;
}

// Validate encrypted length is reasonable
if (encryptedLength < 0 || encryptedLength > (int)sizeof(encryptedData)) {
Limelog("MIC: Invalid encrypted length (%d)\n", encryptedLength);
return -1;
}

packetLength = (int)sizeof(header) + encryptedLength;
if (packetLength > MAX_MIC_PACKET_SIZE || packetLength > (int)sizeof(packet)) {
Limelog("MIC: Encrypted packet too large (%d > %d)\n", packetLength, MAX_MIC_PACKET_SIZE);
return -1;
}

// Safe copy with bounds check
memcpy(packet, &header, sizeof(header));
memcpy(packet + sizeof(header), encryptedData, encryptedLength);
}
else {
packetLength = (int)sizeof(header) + opusLength;
if (packetLength > MAX_MIC_PACKET_SIZE || packetLength > (int)sizeof(packet)) {
Limelog("MIC: Packet too large (%d > %d)\n", packetLength, MAX_MIC_PACKET_SIZE);
return -1;
}

// Safe copy with bounds check
memcpy(packet, &header, sizeof(header));
memcpy(packet + sizeof(header), opusData, opusLength);
}

++micSequenceNumber;

memcpy(&saddr, &RemoteAddr, sizeof(saddr));
SET_PORT(&saddr, MicPortNumber);

err = sendto(micSocket, (const char*)packet, packetLength, 0, (struct sockaddr*)&saddr, AddrLen);
if (err < 0) {
return LastSocketError();
}

return err;
}

bool isMicrophoneEncryptionEnabled(void) {
return (EncryptionFeaturesEnabled & SS_ENC_MICROPHONE) != 0;
}
Loading