From 0d5b78271e7ee5c7bb3af351724e5fd9e16a20ba Mon Sep 17 00:00:00 2001 From: qiin Date: Thu, 22 Oct 2026 18:09:42 +0800 Subject: [PATCH 01/11] feat(audio): add explicit finite MAT dump and playback transport --- docs/MAT-HANDOFF.md | 40 ++++++++++++++ src/AudioStream.c | 67 ++++++++++++++++++++--- src/Connection.c | 17 ++++++ src/Limelight-internal.h | 3 + src/Limelight.h | 14 +++++ src/MatDepacketizer.c | 48 ++++++++++++++++ src/MatDepacketizer.h | 26 +++++++++ src/MatDumpFile.c | 115 +++++++++++++++++++++++++++++++++++++++ src/MatDumpFile.h | 15 +++++ src/MatDumpProfile.h | 38 +++++++++++++ src/MatPlayback.c | 81 +++++++++++++++++++++++++++ src/MatPlayback.h | 8 +++ src/MatPlaybackProfile.h | 23 ++++++++ src/MatReceiver.c | 60 ++++++++++++++++++++ src/MatReceiver.h | 30 ++++++++++ src/MatWire.h | 47 ++++++++++++++++ src/Rtsp.h | 1 + src/RtspConnection.c | 44 +++++++++++++++ src/RtspParser.c | 9 +++ src/SdpGenerator.c | 19 ++++++- 20 files changed, 696 insertions(+), 9 deletions(-) create mode 100644 docs/MAT-HANDOFF.md create mode 100644 src/MatDepacketizer.c create mode 100644 src/MatDepacketizer.h create mode 100644 src/MatDumpFile.c create mode 100644 src/MatDumpFile.h create mode 100644 src/MatDumpProfile.h create mode 100644 src/MatPlayback.c create mode 100644 src/MatPlayback.h create mode 100644 src/MatPlaybackProfile.h create mode 100644 src/MatReceiver.c create mode 100644 src/MatReceiver.h create mode 100644 src/MatWire.h diff --git a/docs/MAT-HANDOFF.md b/docs/MAT-HANDOFF.md new file mode 100644 index 000000000..66aacc7f9 --- /dev/null +++ b/docs/MAT-HANDOFF.md @@ -0,0 +1,40 @@ +# Experimental finite MAT transport handoff — 2026-10-22 + +This branch targets **mic**, the branch consumed by both parent repositories, +not default master. Baseline: b91c7cd72764320edd6140a16f8614aeb63dc42a. +Keep Sunshine and moonlight-vplus pinned to the same resulting commit; rebuild +all consumers because STREAM_CONFIGURATION and the JNI entry point changed. + +## Contract + +- Codec 4 / `mat-dump-v1`: bounded, app-owned file publication after count/SHA/EOS + validation. Never decode or play this codec. +- Codec 5 / `mat-iec61937-v1`: explicit finite replay, fileless `MatPlayback` -> + full-cycle callbacks. No Opus, AC3 or PCM fallback. Explicit matching RTSP + capability and unique ANNOUNCE confirmation are required. +- Carrier `iec61937-192000-8-s16le`: 61,440 bytes per 20 ms cycle; SMAT logical + clock 48,000 Hz / 960 ticks per cycle. RTP/FEC fragment ticks are not media PTS. +- `MatDepacketizer` -> `MatReceiver` shared by the dump and playback consumers. + Preserve cycle boundaries, epoch/domain/index/PTS; fail rather than conceal + loss or overflow. `MatPlayback` checks count and incremental SHA at EOS before + requesting the consumer's bounded drain; no successful EOS after failed drain. +- Register playback callbacks during audio init, clear only after audio threads + join. The callback owns no borrowed data after return; renderers must copy. +- Duplicate parser flag preserves otherwise-coalesced duplicate header evidence; + strict case-insensitive uniqueness applies to the new playback confirmation. + Existing generic lookup retains legacy last-value-wins semantics. + +## Evidence / limits + +The parent Sunshine branch contains tests under `spatial-object-spike/` and the +cross-repository `docs/ATMOS-MAT-HANDOFF.md`. Recorded acceptance: plain, AES audio +and encrypted RTSP + AES sessions, 178 cycles / 10,936,320 bytes, SHA256 +`79cd69e33adcd1bcffe95673564b6fc727a7aaed7370745d43ec269925a492f6`. +Actual JNI and Java player were exercised with **fake final hardware**, not an +Android HAL or soundbar. Original dump regression, 15 negative negotiation +cases, playback lifecycle cases and existing nine CTests passed. + +This is not infinite/live capture or authenticated audio transport. FNV and +legacy AES-CBC are not a MAC. No claim of Android MAT support, Atmos recognition, +certification or commercial encoding rights is made. Keep finite manifest bounds +and fail-closed behavior until a separate live-source contract is designed. diff --git a/src/AudioStream.c b/src/AudioStream.c index 8f7bc4789..e172359c7 100644 --- a/src/AudioStream.c +++ b/src/AudioStream.c @@ -1,4 +1,27 @@ #include "Limelight-internal.h" +#include "MatDumpFile.h" +#include "MatPlayback.h" +static MAT_PLAYBACK_RECEIVER* matPlayback; +static LI_MAT_PLAYBACK_CALLBACKS matCallbacks; +static bool matPlaybackFailedReported,matPlaybackCompleteReported; +void LiSetMatPlaybackCallbacks(const LI_MAT_PLAYBACK_CALLBACKS* callbacks) { + if(callbacks) matCallbacks=*callbacks; else memset(&matCallbacks,0,sizeof(matCallbacks)); +} +static MAT_DUMP_FILE* matDump; +static void failMatInput(void) { MatDumpFail(matDump); MatPlaybackFail(matPlayback); } +static bool matDumpFailedReported,matDumpPublishedReported; +static void consumeAudio(char* data,int size) { + if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_IEC61937){ + int result=MatPlaybackPush(matPlayback,(unsigned char*)data,(size_t)(size>0?size:0)); + if(result<0 && !matPlaybackFailedReported){matPlaybackFailedReported=true;Limelog("MAT IEC playback failed\n");ListenerCallbacks.connectionTerminated(-1);} + else if(result==2 && !matPlaybackCompleteReported){matPlaybackCompleteReported=true;Limelog("MAT IEC playback SHA/count verified and renderer drained: %s\n",MatDumpProfile.sha256);} + return; + } + if(!AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec)){AudioCallbacks.decodeAndPlaySample(data,size);return;} + int result=MatDumpPush(matDump,(unsigned char*)data,(size_t)(size>0?size:0)); + if(result<0 && !matDumpFailedReported){matDumpFailedReported=true;Limelog("MAT dump failed: preserving partial file\n");ListenerCallbacks.connectionTerminated(-1);} + else if(result==2 && !matDumpPublishedReported){matDumpPublishedReported=true;Limelog("MAT dump SHA256 verified and published: %s (%s)\n",StreamConfig.matDumpPath,MatDumpProfile.sha256);} +} static SOCKET rtpSocket = INVALID_SOCKET; @@ -72,9 +95,11 @@ static void AudioPingThreadProc(void* context) { // Initialize the audio stream and start int initializeAudioStream(void) { - LbqInitializeLinkedBlockingQueue(&packetQueue, 30); + LbqInitializeLinkedBlockingQueue(&packetQueue, AUDIO_CODEC_IS_MAT(StreamConfig.audioCodec)?512:30); RtpaInitializeQueue(&rtpAudioQueue); - lastSeq = 0; + lastSeq = AUDIO_CODEC_IS_MAT(StreamConfig.audioCodec)?3:0; + matDumpFailedReported=matDumpPublishedReported=false; + matPlaybackFailedReported=matPlaybackCompleteReported=false; receivedDataFromPeer = false; pingThreadStarted = false; firstReceiveTime = 0; @@ -107,6 +132,9 @@ int notifyAudioPortNegotiationComplete(void) { // We may receive audio before our threads are started, but that's okay. We'll // drop the first 1 second of audio packets to catch up with the backlog. + // MAT starts the authentic ping only AFTER its receiver/consumer are ready. + // The host waits for it, so no startup data is buffered or discarded. + if(AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec))return 0; int err = PltCreateThread("AudioPing", AudioPingThreadProc, NULL, &udpPingThread); if (err != 0) { return err; @@ -131,6 +159,8 @@ static void freePacketList(PLINKED_BLOCKING_QUEUE_ENTRY entry) { // Tear down the audio stream once we're done with it void destroyAudioStream(void) { + MatDumpClose(matDump);matDump=NULL; + MatPlaybackClose(matPlayback);matPlayback=NULL; if (rtpSocket != INVALID_SOCKET) { if (pingThreadStarted) { PltInterruptThread(&udpPingThread); @@ -157,6 +187,10 @@ static bool queuePacketToLbq(PQUEUED_AUDIO_PACKET* packet) { } else if (err == LBQ_BOUND_EXCEEDED) { Limelog("Audio packet queue overflow\n"); + if(AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec)){ + ListenerCallbacks.connectionTerminated(-1); + return false; // Don't flush a loss-intolerant stream or silently resume. + } // The audio queue is full, so free all existing items and try again freePacketList(LbqFlushQueueItems(&packetQueue)); @@ -171,13 +205,14 @@ static void decodeInputData(PQUEUED_AUDIO_PACKET packet) { // packet. Trigger packet loss concealment logic in libopus by // invoking the decoder with a NULL buffer. if (packet->header.size == 0) { - AudioCallbacks.decodeAndPlaySample(NULL, 0); + consumeAudio(NULL, 0); return; } PRTP_PACKET rtp = (PRTP_PACKET)&packet->data[0]; if (lastSeq != 0 && (unsigned short)(lastSeq + 1) != rtp->sequenceNumber) { Limelog("Network dropped audio data (expected %d, but received %d)\n", lastSeq + 1, rtp->sequenceNumber); + if(AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec)){failMatInput();consumeAudio(NULL,0);return;} } lastSeq = rtp->sequenceNumber; @@ -203,6 +238,7 @@ static void decodeInputData(PQUEUED_AUDIO_PACKET packet) { (unsigned char*)(rtp + 1), dataLength, decryptedOpusData, &dataLength)) { Limelog("Failed to decrypt audio packet (sequence number: %u)\n", rtp->sequenceNumber); + if(AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec)){failMatInput();consumeAudio(NULL,0);} LC_ASSERT_VT(false); return; } @@ -222,7 +258,7 @@ static void decodeInputData(PQUEUED_AUDIO_PACKET packet) { } #endif - AudioCallbacks.decodeAndPlaySample((char*)decryptedOpusData, dataLength); + consumeAudio((char*)decryptedOpusData, dataLength); } else { #ifdef LC_DEBUG @@ -239,7 +275,7 @@ static void decodeInputData(PQUEUED_AUDIO_PACKET packet) { } #endif - AudioCallbacks.decodeAndPlaySample((char*)(rtp + 1), packet->header.size - sizeof(*rtp)); + consumeAudio((char*)(rtp + 1), packet->header.size - sizeof(*rtp)); } } @@ -252,7 +288,7 @@ static void AudioReceiveThreadProc(void* context) { int waitingForAudioMs; packet = NULL; - packetsToDrop = 500 / AudioPacketDuration; + packetsToDrop = AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec)?0:500 / AudioPacketDuration; if (setNonFatalRecvTimeoutMs(rtpSocket, UDP_RECV_POLL_TIMEOUT_MS) < 0) { // SO_RCVTIMEO failed, so use select() to wait @@ -305,7 +341,7 @@ static void AudioReceiveThreadProc(void* context) { receivedDataFromPeer = true; Limelog("Received first audio packet after %d ms\n", waitingForAudioMs); - if (firstReceiveTime != 0) { + if (firstReceiveTime != 0 && !AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec)) { // XXX firstReceiveTime is never set here... // We're already dropping 500ms of audio so this probably doesn't matter packetsToDrop += (uint32_t)(PltGetMillis() - firstReceiveTime) / AudioPacketDuration; @@ -425,6 +461,8 @@ void stopAudioStream(void) { PltJoinThread(&decoderThread); } + MatDumpClose(matDump);matDump=NULL; + MatPlaybackClose(matPlayback);matPlayback=NULL; AudioCallbacks.cleanup(); } @@ -444,13 +482,21 @@ int startAudioStream(void* audioContext, int arFlags) { chosenConfig = NormalQualityOpusConfig; } - chosenConfig.samplesPerFrame = 48 * AudioPacketDuration; + chosenConfig.samplesPerFrame = AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec)?960:48 * AudioPacketDuration; err = AudioCallbacks.init(StreamConfig.audioConfiguration, &chosenConfig, audioContext, arFlags); if (err != 0) { return err; } + if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_DUMP){ + matDump=MatDumpOpen(StreamConfig.matDumpPath,&MatDumpProfile); + if(!matDump){AudioCallbacks.cleanup();return -1;} + } + if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_IEC61937){ + matPlayback=MatPlaybackOpen(&MatDumpProfile,&matCallbacks); + if(!matPlayback){AudioCallbacks.cleanup();return -1;} + } AudioCallbacks.start(); err = PltCreateThread("AudioRecv", AudioReceiveThreadProc, NULL, &receiveThread); @@ -473,6 +519,11 @@ int startAudioStream(void* audioContext, int arFlags) { } } + if(AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec) && !pingThreadStarted){ + err=PltCreateThread("AudioPing",AudioPingThreadProc,NULL,&udpPingThread); + if(err!=0){stopAudioStream();return err;} + pingThreadStarted=true; + } return 0; } diff --git a/src/Connection.c b/src/Connection.c index 2c1d4baee..436549f6b 100644 --- a/src/Connection.c +++ b/src/Connection.c @@ -27,6 +27,7 @@ int AudioPacketDuration; bool AudioEncryptionEnabled; bool ReferenceFrameInvalidationSupported; int NegotiatedAudioCodec; +LI_MAT_DUMP_PROFILE MatDumpProfile; int NegotiatedAudioBitrate; int NegotiatedDynamicHdrFormat; int NegotiatedDynamicHdrFallback; @@ -331,6 +332,7 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre // LiGetNegotiatedAudioCodec() to check what was actually selected. NegotiatedAudioCodec = AUDIO_CODEC_OPUS; NegotiatedAudioBitrate = 0; + memset(&MatDumpProfile,0,sizeof(MatDumpProfile)); switch (StreamConfig.audioCodec) { case AUDIO_CODEC_OPUS: // Default; nothing to do. @@ -355,6 +357,21 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre : (StreamConfig.audioCodec == AUDIO_CODEC_AC3 ? 640000 : 384000); } break; + case AUDIO_CODEC_MAT_IEC61937: + if (!IS_SUNSHINE() || StreamConfig.controlOnly || StreamConfig.matDumpPath[0]) { + Limelog("MAT playback requires Sunshine audio session and no dump path\n"); + err = -1; goto Cleanup; + } + NegotiatedAudioCodec = AUDIO_CODEC_MAT_IEC61937; + break; + case AUDIO_CODEC_MAT_DUMP: + if (!IS_SUNSHINE() || StreamConfig.controlOnly || !StreamConfig.matDumpPath[0] || + !memchr(StreamConfig.matDumpPath,0,sizeof(StreamConfig.matDumpPath))) { + Limelog("MAT dump requires Sunshine, audio session and app-owned output path\n"); + err = -1; goto Cleanup; + } + NegotiatedAudioCodec = AUDIO_CODEC_MAT_DUMP; + break; case AUDIO_CODEC_PCM_S16: if (!IS_SUNSHINE()) { Limelog("PCM passthrough is only supported by Sunshine; falling back to Opus\n"); diff --git a/src/Limelight-internal.h b/src/Limelight-internal.h index 530aa7134..15136260d 100644 --- a/src/Limelight-internal.h +++ b/src/Limelight-internal.h @@ -1,4 +1,7 @@ #pragma once +#include "MatDumpProfile.h" +extern LI_MAT_DUMP_PROFILE MatDumpProfile; +#include "MatPlaybackProfile.h" #include "Platform.h" #include "Limelight.h" diff --git a/src/Limelight.h b/src/Limelight.h index bdabb26e5..8eefececc 100644 --- a/src/Limelight.h +++ b/src/Limelight.h @@ -157,6 +157,7 @@ typedef struct _STREAM_CONFIGURATION { // RS FEC preference: -2=host default, -1=automatic, 0..100=fixed percentage. // Unsupported hosts ignore this preference. PyroWave uses its own protection. int videoFecPercentage; + char matDumpPath[1024]; // App-owned unique dump-only output, copied with config. } STREAM_CONFIGURATION, *PSTREAM_CONFIGURATION; // Initialize the stream configuration, including compatible host-default FEC. @@ -302,6 +303,19 @@ typedef struct _DECODE_UNIT { // delay, no AVR decoder); biggest bandwidth (~1.5 Mbps stereo, ~4.6 Mbps 5.1). // LAN-only in practice. #define AUDIO_CODEC_PCM_S16 3 +#define AUDIO_CODEC_MAT_DUMP 4 +#define AUDIO_CODEC_MAT_IEC61937 5 +#define AUDIO_CODEC_IS_MAT(c) ((c)==AUDIO_CODEC_MAT_DUMP || (c)==AUDIO_CODEC_MAT_IEC61937) + +/* Registered from audio init, cleared after audio threads join. Never called for + * codec4. Return 0 to fail the loss-intolerant session. End is input SHA-verified + * EOS; the renderer must drain before returning success. No PCM/encoded callback. */ +typedef struct { + int (*cycle)(void*, const unsigned char*, int, uint64_t, uint64_t, uint64_t, uint64_t); + int (*end)(void*, uint64_t); + void* context; +} LI_MAT_PLAYBACK_CALLBACKS; +void LiSetMatPlaybackCallbacks(const LI_MAT_PLAYBACK_CALLBACKS* callbacks); // Bitmasks for STREAM_CONFIGURATION.supportedAudioCodecs (and reserved for a // future server-side capability advertisement). The set bit indicates that the diff --git a/src/MatDepacketizer.c b/src/MatDepacketizer.c new file mode 100644 index 000000000..6aa5ce07a --- /dev/null +++ b/src/MatDepacketizer.c @@ -0,0 +1,48 @@ +#include "MatDepacketizer.h" +#include "MatWire.h" + +void MatDepacketizerInit(MAT_DEPACKETIZER* s,MAT_WRITE_CYCLE write,void* context) { + memset(s,0,sizeof(*s));s->write=write;s->context=context;s->stream_hash=2166136261u; +} +static int fail(MAT_DEPACKETIZER* s) { s->failed=1;return -1; } +int MatDepacketizerPush(MAT_DEPACKETIZER* s,const uint8_t* p,size_t n) { + if(s->failed) return -1; + if(!p || n!=MAT_WIRE_PACKET || memcmp(p,"SMAT",4) || p[4]!=1 || + p[6]!=MAT_WIRE_HEADER || p[7] || mat_get32(p+8)!=1 || mat_get32(p+12)!=1 || + !mat_get64(p+16) || !mat_get64(p+24) || mat_get32(p+48)!=48000 || + mat_get32(p+76) || mat_get32(p+72)!=mat_packet_hash(p)) return fail(s); + uint64_t frame=mat_get64(p+32); + if(frame>UINT64_MAX/960 || mat_get64(p+40)!=frame*960) return fail(s); + if(s->started && (s->epoch!=mat_get64(p+16) || s->domain!=mat_get64(p+24))) return fail(s); + if(!s->started) { s->started=1;s->epoch=mat_get64(p+16);s->domain=mat_get64(p+24); } + if(p[5]==MAT_WIRE_SYNC || p[5]==MAT_WIRE_END) { + if(mat_get32(p+52) || mat_get32(p+56) || mat_get32(p+60) || mat_get32(p+64)) return fail(s); + for(unsigned i=MAT_WIRE_HEADER;iended || s->next_frame || s->mask || frame || mat_get32(p+68)) return fail(s); + return 0; + } + if(!frame || frame!=s->next_frame || s->mask || mat_get32(p+68)!=s->stream_hash) return fail(s); + s->ended=1;return 2; // Redundant END packets are deliberately idempotent. + } + if(p[5]!=MAT_WIRE_DATA || s->ended || frame!=s->next_frame || + mat_get32(p+52)!=960 || mat_get32(p+56)!=MAT_WIRE_CYCLE || + p[61] || p[62]!=MAT_WIRE_FRAGMENTS || p[63] || p[60]>=MAT_WIRE_FRAGMENTS || + mat_get32(p+64)!=MAT_WIRE_BODY) return fail(s); + unsigned index=p[60];uint64_t bit=UINT64_C(1)<mask && s->frame_hash!=mat_get32(p+68)) return fail(s); + s->frame_hash=mat_get32(p+68); + if(s->mask&bit) { + if(memcmp(s->cycle+index*MAT_WIRE_BODY,p+MAT_WIRE_HEADER,MAT_WIRE_BODY)) return fail(s); + return 0; + } + memcpy(s->cycle+index*MAT_WIRE_BODY,p+MAT_WIRE_HEADER,MAT_WIRE_BODY);s->mask|=bit; + if(s->mask!=((UINT64_C(1)<cycle,sizeof(s->cycle))!=s->frame_hash || + memcmp(s->cycle,"\x72\xf8\x1f\x4e",4) || (s->cycle[4]&31)!=0x16 || (s->cycle[4]&0x80)) return fail(s); + unsigned pd=(unsigned)s->cycle[6]|(unsigned)s->cycle[7]<<8; + if(pd<2 || pd>MAT_WIRE_CYCLE-8 || (pd&1) || !s->write || !s->write(s->context,s->cycle,sizeof(s->cycle))) return fail(s); + s->stream_hash=mat_hash_update(s->stream_hash,s->cycle,sizeof(s->cycle)); + s->mask=0;++s->next_frame;return 1; +} +int MatDepacketizerFinish(const MAT_DEPACKETIZER* s) { return s->ended && !s->failed && !s->mask; } diff --git a/src/MatDepacketizer.h b/src/MatDepacketizer.h new file mode 100644 index 000000000..424c519b5 --- /dev/null +++ b/src/MatDepacketizer.h @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#ifdef __cplusplus +extern "C" { +#endif +/* Dump-only extension AFTER existing RTP reorder/FEC + decrypt. One bounded + * carrier buffer; in-cycle reorder/identical duplicates allowed. Missing data, + * epoch changes, cross-cycle gaps and missing EOS are terminal, never concealed. + * Callback must copy/write synchronously; zero return means IO failure. */ +typedef int (*MAT_WRITE_CYCLE)(void*,const uint8_t*,size_t); +typedef struct { + uint64_t epoch,domain,next_frame,mask; + uint32_t frame_hash,stream_hash; + unsigned started,ended,failed; + MAT_WRITE_CYCLE write; + void* context; + uint8_t cycle[61440]; +} MAT_DEPACKETIZER; +void MatDepacketizerInit(MAT_DEPACKETIZER*,MAT_WRITE_CYCLE,void*); +/* -1 sticky failure; 0 accepted, 1 whole cycle written, 2 complete EOS. */ +int MatDepacketizerPush(MAT_DEPACKETIZER*,const uint8_t*,size_t); +int MatDepacketizerFinish(const MAT_DEPACKETIZER*); +#ifdef __cplusplus +} +#endif diff --git a/src/MatDumpFile.c b/src/MatDumpFile.c new file mode 100644 index 000000000..03872b228 --- /dev/null +++ b/src/MatDumpFile.c @@ -0,0 +1,115 @@ +#include "MatDumpFile.h" +#include "MatReceiver.h" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#define mat_open _open +#define mat_fdopen _fdopen +#define mat_close _close +#else +#include +#define mat_open open +#define mat_fdopen fdopen +#define mat_close close +#ifndef O_BINARY +#define O_BINARY 0 +#endif +#endif +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) +#include +#else +#include +#endif + +struct MAT_DUMP_FILE { + MAT_RECEIVER receiver; + LI_MAT_DUMP_PROFILE expected; + FILE* file; + char final_path[1024],partial_path[1040],status_path[1040],sha256[65]; + int failed,published,created; +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) + psa_hash_operation_t digest; +#else + EVP_MD_CTX* digest; +#endif +}; +static FILE* exclusive(const char* path) { + int fd=mat_open(path,O_WRONLY|O_CREAT|O_EXCL|O_BINARY,0600); + if(fd<0)return NULL;FILE* f=mat_fdopen(fd,"wb");if(!f)mat_close(fd);return f; +} +static int write_cycle(void* context,const MAT_CYCLE_INFO* info,const uint8_t* p,size_t n) { + MAT_DUMP_FILE* s=context; + if(info->index>=s->expected.cycles || !s->file || fwrite(p,1,n,s->file)!=n)return 0; +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) + return psa_hash_update(&s->digest,p,n)==PSA_SUCCESS; +#else + return EVP_DigestUpdate(s->digest,p,n)==1; +#endif +} +static void status(MAT_DUMP_FILE* s) { + FILE* f=exclusive(s->status_path);if(!f)return; + fprintf(f,"{\"published\":%s,\"completeEOS\":%s,\"failed\":%s,\"cycles\":%llu,\"expectedCycles\":%u,\"sha256\":\"%s\",\"expectedSha256\":\"%s\"}\n", + s->published?"true":"false",MatReceiverFinish(&s->receiver)?"true":"false",s->failed?"true":"false", + (unsigned long long)s->receiver.depacketizer.next_frame,s->expected.cycles,s->sha256,s->expected.sha256);fclose(f); +} +MAT_DUMP_FILE* MatDumpOpen(const char* path,const LI_MAT_DUMP_PROFILE* expected) { + if(!path || !expected || !*path || strlen(path)>=1024)return NULL; + char count[16];snprintf(count,sizeof(count),"%u",expected->cycles);LI_MAT_DUMP_PROFILE checked; + if(!MatProfileSet(&checked,expected->sha256,count))return NULL; + MAT_DUMP_FILE* s=calloc(1,sizeof(*s));if(!s)return NULL;s->expected=checked; + strcpy(s->final_path,path);snprintf(s->partial_path,sizeof(s->partial_path),"%s.partial",path); + snprintf(s->status_path,sizeof(s->status_path),"%s.json",path); + struct stat st;if(stat(path,&st)==0 || stat(s->status_path,&st)==0){free(s);return NULL;} +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) + s->digest=psa_hash_operation_init(); + if(psa_crypto_init()!=PSA_SUCCESS || psa_hash_setup(&s->digest,PSA_ALG_SHA_256)!=PSA_SUCCESS){free(s);return NULL;} +#else + s->digest=EVP_MD_CTX_new(); + if(!s->digest || EVP_DigestInit_ex(s->digest,EVP_sha256(),NULL)!=1){EVP_MD_CTX_free(s->digest);free(s);return NULL;} +#endif + s->file=exclusive(s->partial_path); + if(!s->file){MatDumpClose(s);return NULL;} + s->created=1;MatReceiverInit(&s->receiver,write_cycle,NULL,s);return s; +} +void MatDumpFail(MAT_DUMP_FILE* s) {if(s)s->failed=1;} +int MatDumpPublished(const MAT_DUMP_FILE* s){return s && s->published;} +int MatDumpPush(MAT_DUMP_FILE* s,const unsigned char* p,size_t n) { + if(!s || s->failed)return -1; + int result=MatReceiverPush(&s->receiver,p,n); + if(result<0){s->failed=1;return -1;} + if(result!=2 || s->published)return result; + unsigned char bytes[32];size_t length=0; +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) + if(psa_hash_finish(&s->digest,bytes,sizeof(bytes),&length)!=PSA_SUCCESS)s->failed=1; +#else + unsigned out=0;if(EVP_DigestFinal_ex(s->digest,bytes,&out)!=1)s->failed=1;length=out; +#endif + if(length!=32)s->failed=1; + if(!s->failed)for(unsigned i=0;i<32;i++)snprintf(s->sha256+i*2,3,"%02x",bytes[i]); + if(fflush(s->file))s->failed=1;if(fclose(s->file))s->failed=1;s->file=NULL; + if(s->receiver.depacketizer.next_frame!=s->expected.cycles || strcmp(s->sha256,s->expected.sha256))s->failed=1; + // link() on POSIX refuses an existing destination, unlike rename(). Android + // app-private files support same-directory links. Never overwrite a dump. + if(!s->failed) { +#ifdef _WIN32 + if(rename(s->partial_path,s->final_path))s->failed=1; +#else + if(link(s->partial_path,s->final_path))s->failed=1;else if(unlink(s->partial_path))s->failed=1; +#endif + } + s->published=!s->failed;status(s);return s->failed?-1:2; +} +void MatDumpClose(MAT_DUMP_FILE* s) { + if(!s)return; + if(s->file){if(fclose(s->file))s->failed=1;s->file=NULL;} + if(!s->published && s->created){s->failed=1;status(s);} +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) + psa_hash_abort(&s->digest); +#else + EVP_MD_CTX_free(s->digest); +#endif + free(s); +} diff --git a/src/MatDumpFile.h b/src/MatDumpFile.h new file mode 100644 index 000000000..31c09eb91 --- /dev/null +++ b/src/MatDumpFile.h @@ -0,0 +1,15 @@ +#pragma once +#include "MatDumpProfile.h" +#include +#ifdef __cplusplus +extern "C" { +#endif +typedef struct MAT_DUMP_FILE MAT_DUMP_FILE; +MAT_DUMP_FILE* MatDumpOpen(const char* final_path,const LI_MAT_DUMP_PROFILE* expected); +int MatDumpPush(MAT_DUMP_FILE*,const unsigned char*,size_t); // -1 failed, 0 pending, 2 published +void MatDumpFail(MAT_DUMP_FILE*); +int MatDumpPublished(const MAT_DUMP_FILE*); +void MatDumpClose(MAT_DUMP_FILE*); // Missing EOS remains .partial; never overwrites. +#ifdef __cplusplus +} +#endif diff --git a/src/MatDumpProfile.h b/src/MatDumpProfile.h new file mode 100644 index 000000000..5c8bd81d4 --- /dev/null +++ b/src/MatDumpProfile.h @@ -0,0 +1,38 @@ +#pragma once +#include +#include +#include +#define MAT_DUMP_CODEC 4 +#define MAT_DUMP_NAME "mat-dump-v1" +typedef struct { char sha256[65]; uint32_t cycles; } LI_MAT_DUMP_PROFILE; +/* Strict bounded SDP attributes: exact name, no duplicates or truncation. */ +static inline int MatSdpValue(const char* sdp,const char* name,char* out,size_t cap) { + int found=0;size_t key=strlen(name);out[0]=0; + for(const char* p=sdp;p && *p;) { + const char* end=strchr(p,'\n');if(!end)end=p+strlen(p); + const char* trimmed=end;if(trimmed>p && trimmed[-1]=='\r')--trimmed; + if((size_t)(trimmed-p)>=key+3 && !memcmp(p,"a=",2) && !memcmp(p+2,name,key) && p[key+2]==':') { + size_t n=(size_t)(trimmed-p)-key-3;if(found || n>=cap)return -1; + memcpy(out,p+key+3,n);out[n]=0;found=1; + } + p=*end?end+1:end; + }return found; +} +static inline int MatProfileSet(LI_MAT_DUMP_PROFILE* out,const char* hash,const char* count) { + memset(out,0,sizeof(*out)); + if(!hash || !count || strlen(hash)!=64 || !*count)return 0; + for(unsigned i=0;i<64;i++)if(!((hash[i]>='0'&&hash[i]<='9') || (hash[i]>='a'&&hash[i]<='f')))return 0; + unsigned n=0;for(const char* p=count;*p;p++){if(*p<'0'||*p>'9'||n>1000)return 0;n=n*10+(unsigned)(*p-'0');} + if(!n || n>1000)return 0; + memcpy(out->sha256,hash,65);out->cycles=n;return 1; +} +static inline int MatProfileParse(LI_MAT_DUMP_PROFILE* out,const char* sdp) { + char version[8],hash[65],count[16];memset(out,0,sizeof(*out)); + return MatSdpValue(sdp,"x-ss-audio.mat-dump",version,sizeof(version))==1 && !strcmp(version,"1") && + MatSdpValue(sdp,"x-ss-audio.mat-sha256",hash,sizeof(hash))==1 && + MatSdpValue(sdp,"x-ss-audio.mat-cycles",count,sizeof(count))==1 && MatProfileSet(out,hash,count); +} +static inline int MatProfileConfirm(const LI_MAT_DUMP_PROFILE* expected,const char* codec,const char* hash,const char* count) { + LI_MAT_DUMP_PROFILE got;return codec && !strcmp(codec,MAT_DUMP_NAME) && MatProfileSet(&got,hash,count) && + got.cycles==expected->cycles && !strcmp(got.sha256,expected->sha256); +} diff --git a/src/MatPlayback.c b/src/MatPlayback.c new file mode 100644 index 000000000..2b380ef4a --- /dev/null +++ b/src/MatPlayback.c @@ -0,0 +1,81 @@ +#include "MatPlayback.h" +#include "MatReceiver.h" +#include +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) +#include +#else +#include +#endif +struct MAT_PLAYBACK_RECEIVER { + MAT_RECEIVER receiver; + LI_MAT_DUMP_PROFILE expected; + LI_MAT_PLAYBACK_CALLBACKS callbacks; + int failed; +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) + psa_hash_operation_t digest; +#else + EVP_MD_CTX* digest; +#endif +}; +static int cycle(void* context,const MAT_CYCLE_INFO* info,const uint8_t* p,size_t n) { + MAT_PLAYBACK_RECEIVER* s=context; + if(info->index>=s->expected.cycles) return 0; +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) + if(psa_hash_update(&s->digest,p,n)!=PSA_SUCCESS) return 0; +#else + if(EVP_DigestUpdate(s->digest,p,n)!=1) return 0; +#endif + return s->callbacks.cycle(s->callbacks.context,p,(int)n,info->epoch,info->domain,info->index,info->pts48k); +} +static int end(void* context,uint64_t count) { + MAT_PLAYBACK_RECEIVER* s=context; + unsigned char bytes[32];size_t length=0;char hash[65]={0}; +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) + if(psa_hash_finish(&s->digest,bytes,sizeof(bytes),&length)!=PSA_SUCCESS) return 0; +#else + unsigned out=0; + if(EVP_DigestFinal_ex(s->digest,bytes,&out)!=1) return 0; + length=out; +#endif + if(length!=32 || count!=s->expected.cycles) return 0; + const char* hex="0123456789abcdef"; + for(unsigned i=0;i<32;i++){hash[2*i]=hex[bytes[i]>>4];hash[2*i+1]=hex[bytes[i]&15];} + if(strcmp(hash,s->expected.sha256)) return 0; + return s->callbacks.end(s->callbacks.context,count); +} +MAT_PLAYBACK_RECEIVER* MatPlaybackOpen(const LI_MAT_DUMP_PROFILE* expected,const LI_MAT_PLAYBACK_CALLBACKS* callbacks) { + if(!expected || !callbacks || !callbacks->cycle || !callbacks->end) return NULL; + char count[16];LI_MAT_DUMP_PROFILE checked; + /* Avoid stdio formatting in the reception hot path. */ + uint32_t value=expected->cycles;unsigned n=0; + if(!value || value>1000) return NULL; + do {count[n++]=(char)('0'+value%10);value/=10;} while(value); + count[n]=0;for(unsigned i=0;isha256,count)) return NULL; + MAT_PLAYBACK_RECEIVER* s=calloc(1,sizeof(*s));if(!s)return NULL; + s->expected=checked;s->callbacks=*callbacks; +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) + s->digest=psa_hash_operation_init(); + if(psa_crypto_init()!=PSA_SUCCESS || psa_hash_setup(&s->digest,PSA_ALG_SHA_256)!=PSA_SUCCESS){free(s);return NULL;} +#else + s->digest=EVP_MD_CTX_new(); + if(!s->digest || EVP_DigestInit_ex(s->digest,EVP_sha256(),NULL)!=1){EVP_MD_CTX_free(s->digest);free(s);return NULL;} +#endif + MatReceiverInit(&s->receiver,cycle,end,s);return s; +} +int MatPlaybackPush(MAT_PLAYBACK_RECEIVER* s,const unsigned char* p,size_t n) { + if(!s || s->failed)return -1; + int result=MatReceiverPush(&s->receiver,p,n); + if(result<0)s->failed=1; + return result; +} +void MatPlaybackFail(MAT_PLAYBACK_RECEIVER* s) {if(s){s->failed=1;MatReceiverAbort(&s->receiver);}} +void MatPlaybackClose(MAT_PLAYBACK_RECEIVER* s) { + if(!s)return; +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) + psa_hash_abort(&s->digest); +#else + EVP_MD_CTX_free(s->digest); +#endif + free(s); +} diff --git a/src/MatPlayback.h b/src/MatPlayback.h new file mode 100644 index 000000000..bae8db40b --- /dev/null +++ b/src/MatPlayback.h @@ -0,0 +1,8 @@ +#pragma once +#include "Limelight.h" +#include "MatDumpProfile.h" +typedef struct MAT_PLAYBACK_RECEIVER MAT_PLAYBACK_RECEIVER; +MAT_PLAYBACK_RECEIVER* MatPlaybackOpen(const LI_MAT_DUMP_PROFILE*,const LI_MAT_PLAYBACK_CALLBACKS*); +int MatPlaybackPush(MAT_PLAYBACK_RECEIVER*,const unsigned char*,size_t); +void MatPlaybackFail(MAT_PLAYBACK_RECEIVER*); +void MatPlaybackClose(MAT_PLAYBACK_RECEIVER*); diff --git a/src/MatPlaybackProfile.h b/src/MatPlaybackProfile.h new file mode 100644 index 000000000..8e499312c --- /dev/null +++ b/src/MatPlaybackProfile.h @@ -0,0 +1,23 @@ +#pragma once +#include "MatDumpProfile.h" +#define MAT_PLAYBACK_CODEC 5 +#define MAT_PLAYBACK_NAME "mat-iec61937-v1" +#define MAT_PLAYBACK_CARRIER "iec61937-192000-8-s16le" +#define MAT_PLAYBACK_CLOCK "48000" +/* Finite replay v1 only. No claim of an infinite/live capture source. */ +static inline int MatPlaybackProfileParse(LI_MAT_DUMP_PROFILE* out,const char* sdp) { + char version[8],carrier[48],clock[16],hash[65],count[16]; + memset(out,0,sizeof(*out)); + return MatSdpValue(sdp,"x-ss-audio.mat-playback",version,sizeof(version))==1 && !strcmp(version,"1") && + MatSdpValue(sdp,"x-ss-audio.mat-carrier",carrier,sizeof(carrier))==1 && !strcmp(carrier,MAT_PLAYBACK_CARRIER) && + MatSdpValue(sdp,"x-ss-audio.mat-clock",clock,sizeof(clock))==1 && !strcmp(clock,MAT_PLAYBACK_CLOCK) && + MatSdpValue(sdp,"x-ss-audio.mat-sha256",hash,sizeof(hash))==1 && + MatSdpValue(sdp,"x-ss-audio.mat-cycles",count,sizeof(count))==1 && MatProfileSet(out,hash,count); +} +static inline int MatPlaybackProfileConfirm(const LI_MAT_DUMP_PROFILE* expected,const char* codec, + const char* hash,const char* count,const char* carrier,const char* clock) { + LI_MAT_DUMP_PROFILE got; + return codec && !strcmp(codec,MAT_PLAYBACK_NAME) && carrier && !strcmp(carrier,MAT_PLAYBACK_CARRIER) && + clock && !strcmp(clock,MAT_PLAYBACK_CLOCK) && MatProfileSet(&got,hash,count) && + got.cycles==expected->cycles && !strcmp(got.sha256,expected->sha256); +} diff --git a/src/MatReceiver.c b/src/MatReceiver.c new file mode 100644 index 000000000..d1b922a3d --- /dev/null +++ b/src/MatReceiver.c @@ -0,0 +1,60 @@ +#include "MatReceiver.h" +#include +static int fail(MAT_RECEIVER* s) { + s->failed = 1; s->depacketizer.failed = 1; return -1; +} +int MatCarrierIsValid(const uint8_t* p, size_t n) { + if (!p || n != 61440 || memcmp(p, "\x72\xf8\x1f\x4e", 4) || + (p[4] & 31) != 0x16 || (p[4] & 0x80)) return 0; + unsigned pd = (unsigned)p[6] | (unsigned)p[7] << 8; + return pd >= 2 && pd <= 61432 && !(pd & 1); +} +static int consume(void* context, const uint8_t* p, size_t n) { + MAT_RECEIVER* s = context; + MAT_DEPACKETIZER* d = &s->depacketizer; + MAT_CYCLE_INFO info = {d->epoch, d->domain, d->next_frame, d->next_frame * 960}; + return s->consume && s->consume(s->context, &info, p, n); +} +void MatReceiverInit(MAT_RECEIVER* s, MAT_CONSUME_CYCLE callback, + MAT_COMPLETE_STREAM complete, void* context) { + memset(s, 0, sizeof(*s)); + s->consume = callback; s->complete = complete; s->context = context; + MatDepacketizerInit(&s->depacketizer, consume, s); +} +static int complete(MAT_RECEIVER* s) { + if (!s->completed) { + if (s->complete && !s->complete(s->context, s->depacketizer.next_frame)) return fail(s); + s->completed = 1; + } + return 2; +} +int MatReceiverPush(MAT_RECEIVER* s, const uint8_t* p, size_t n) { + if (s->failed || s->mode == 2) return fail(s); + s->mode = 1; + int result = MatDepacketizerPush(&s->depacketizer, p, n); + if (result < 0) return fail(s); + return result == 2 ? complete(s) : result; +} +int MatReceiverPushRecordingCycle(MAT_RECEIVER* s, const MAT_CYCLE_INFO* i, + const uint8_t* p, size_t n) { + MAT_DEPACKETIZER* d = &s->depacketizer; + if (s->failed || s->completed || s->mode == 1 || !i || !i->epoch || !i->domain || + i->index != d->next_frame || i->index >= UINT64_MAX / 960 || + i->pts48k != i->index * 960 || !MatCarrierIsValid(p, n)) return fail(s); + if (d->started && (i->epoch != d->epoch || i->domain != d->domain)) return fail(s); + s->mode = 2; + d->started = 1; d->epoch = i->epoch; d->domain = i->domain; + if (!s->consume || !s->consume(s->context, i, p, n)) return fail(s); + ++d->next_frame; + return 1; +} +int MatReceiverEndRecording(MAT_RECEIVER* s, uint64_t expectedCycles) { + if (s->failed || s->mode != 2 || !expectedCycles || + expectedCycles != s->depacketizer.next_frame) return fail(s); + s->depacketizer.ended = 1; + return complete(s); +} +int MatReceiverFinish(const MAT_RECEIVER* s) { + return s && !s->failed && s->completed && MatDepacketizerFinish(&s->depacketizer); +} +void MatReceiverAbort(MAT_RECEIVER* s) { if (s) fail(s); } diff --git a/src/MatReceiver.h b/src/MatReceiver.h new file mode 100644 index 000000000..3b97bdaa2 --- /dev/null +++ b/src/MatReceiver.h @@ -0,0 +1,30 @@ +#pragma once +#include "MatDepacketizer.h" +#ifdef __cplusplus +extern "C" { +#endif +typedef struct { + uint64_t epoch, domain, index, pts48k; +} MAT_CYCLE_INFO; +/* Called synchronously. Copy bytes before returning; false is a terminal failure. + * complete means input EOS, NOT that the hardware has finished playing. */ +typedef int (*MAT_CONSUME_CYCLE)(void*, const MAT_CYCLE_INFO*, const uint8_t*, size_t); +typedef int (*MAT_COMPLETE_STREAM)(void*, uint64_t); +typedef struct { + MAT_DEPACKETIZER depacketizer; + MAT_CONSUME_CYCLE consume; + MAT_COMPLETE_STREAM complete; + void* context; + unsigned mode, failed, completed; +} MAT_RECEIVER; +int MatCarrierIsValid(const uint8_t*, size_t); +void MatReceiverInit(MAT_RECEIVER*, MAT_CONSUME_CYCLE, MAT_COMPLETE_STREAM, void*); +int MatReceiverPush(MAT_RECEIVER*, const uint8_t*, size_t); +/* Offline recordings only: never mix raw cycles and SMAT packets in one receiver. */ +int MatReceiverPushRecordingCycle(MAT_RECEIVER*, const MAT_CYCLE_INFO*, const uint8_t*, size_t); +int MatReceiverEndRecording(MAT_RECEIVER*, uint64_t expectedCycles); +int MatReceiverFinish(const MAT_RECEIVER*); +void MatReceiverAbort(MAT_RECEIVER*); +#ifdef __cplusplus +} +#endif diff --git a/src/MatWire.h b/src/MatWire.h new file mode 100644 index 000000000..621e30400 --- /dev/null +++ b/src/MatWire.h @@ -0,0 +1,47 @@ +#pragma once +/* Shared host/client experimental dump profile. NOT an E-AC3/Opus payload or a claim of + * Android MAT decode support. All multibyte fields are little endian; no packed + * structs, native alignment, pointers or untrusted allocation sizes on wire. */ +#include +#include +#include + +#define MAT_WIRE_HEADER 80u +#define MAT_WIRE_BODY 1024u +#define MAT_WIRE_PACKET (MAT_WIRE_HEADER + MAT_WIRE_BODY) +#define MAT_WIRE_CYCLE 61440u +#define MAT_WIRE_FRAGMENTS 60u +#define MAT_WIRE_DATA 1u +#define MAT_WIRE_SYNC 2u +#define MAT_WIRE_END 3u + +static inline uint32_t mat_get32(const uint8_t* p) { + return (uint32_t)p[0] | (uint32_t)p[1]<<8 | (uint32_t)p[2]<<16 | (uint32_t)p[3]<<24; +} +static inline uint64_t mat_get64(const uint8_t* p) { return mat_get32(p) | (uint64_t)mat_get32(p+4)<<32; } +static inline void mat_put32(uint8_t* p, uint32_t v) { for(unsigned i=0;i<4;i++) p[i]=(uint8_t)(v>>(i*8)); } +static inline void mat_put64(uint8_t* p, uint64_t v) { mat_put32(p,(uint32_t)v);mat_put32(p+4,(uint32_t)(v>>32)); } +static inline uint32_t mat_hash_update(uint32_t h, const uint8_t* p, size_t n) { + while(n--) { h^=*p++;h*=16777619u; } return h; +} +static inline uint32_t mat_hash(const uint8_t* p, size_t n) { return mat_hash_update(2166136261u,p,n); } +/* Accidental-corruption detection only: FNV and legacy AES-CBC are NOT MACs. */ +static inline uint32_t mat_packet_hash(const uint8_t* p) { + uint32_t h=2166136261u; + for(unsigned i=0;i=72 && i<76)?0:p[i];h*=16777619u; } + return h; +} +static inline void mat_wire_make(uint8_t* p, uint8_t kind, uint64_t epoch, + uint64_t domain, uint64_t frame, unsigned fragment, uint32_t frame_hash, + const uint8_t* cycle) { + memset(p,0,MAT_WIRE_PACKET);memcpy(p,"SMAT",4);p[4]=1;p[5]=kind;p[6]=MAT_WIRE_HEADER; + mat_put32(p+8,1);mat_put32(p+12,1); /* MAT IEC carrier, carrier-48000 clock */ + mat_put64(p+16,epoch);mat_put64(p+24,domain);mat_put64(p+32,frame); + mat_put64(p+40,frame*960);mat_put32(p+48,48000); + if(kind==MAT_WIRE_DATA) { + mat_put32(p+52,960);mat_put32(p+56,MAT_WIRE_CYCLE); + p[60]=(uint8_t)fragment;p[62]=MAT_WIRE_FRAGMENTS;mat_put32(p+64,MAT_WIRE_BODY); + memcpy(p+MAT_WIRE_HEADER,cycle+fragment*MAT_WIRE_BODY,MAT_WIRE_BODY); + } + mat_put32(p+68,frame_hash);mat_put32(p+72,mat_packet_hash(p)); +} diff --git a/src/Rtsp.h b/src/Rtsp.h index 062b1c181..67b0594bf 100644 --- a/src/Rtsp.h +++ b/src/Rtsp.h @@ -15,6 +15,7 @@ #define SEQ_INVALID -1 #define FLAG_ALLOCATED_OPTION_FIELDS 0x1 +#define FLAG_DUPLICATE_OPTION 0x10 /* Parsed duplicate, even if legacy last-value-wins coalesces it. */ #define FLAG_ALLOCATED_MESSAGE_BUFFER 0x2 #define FLAG_ALLOCATED_OPTION_ITEMS 0x4 #define FLAG_ALLOCATED_PAYLOAD 0x8 diff --git a/src/RtspConnection.c b/src/RtspConnection.c index a517ae750..2574732cb 100644 --- a/src/RtspConnection.c +++ b/src/RtspConnection.c @@ -2,6 +2,26 @@ #include "Limelight-internal.h" #include "Rtsp.h" +/* MAT playback confirmation is security/format critical: duplicates fail even + * if they repeat the same value. Header names are ASCII case-insensitive. */ +static int matHeaderEqual(const char* a,const char* b) { + while(*a && *b) { + unsigned x=(unsigned char)*a++,y=(unsigned char)*b++; + if(x>='A' && x<='Z')x+=32; + if(y>='A' && y<='Z')y+=32; + if(x!=y)return 0; + } + return !*a && !*b; +} +static char* uniqueMatOption(POPTION_ITEM options,const char* name) { + char* value=NULL;unsigned count=0; + for(;options;options=options->next)if(matHeaderEqual(options->option,name)){ + if(options->flags & FLAG_DUPLICATE_OPTION)return NULL; + value=options->content;++count; + } + return count==1?value:NULL; +} + #define RTSP_CONNECT_TIMEOUT_SEC 10 #define RTSP_RECEIVE_TIMEOUT_SEC 15 #define RTSP_RETRY_DELAY_MS 500 @@ -1255,6 +1275,14 @@ int performRtspHandshake(PSERVER_INFORMATION serverInfo) { goto Exit; } + if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_DUMP && !MatProfileParse(&MatDumpProfile,response.payload)) { + Limelog("Host lacks valid MAT dump v1 capability/manifest; refusing implicit fallback\n"); + ret = -1; freeMessage(&response); goto Exit; + } + if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_IEC61937 && !MatPlaybackProfileParse(&MatDumpProfile,response.payload)) { + Limelog("Host lacks explicit finite MAT IEC playback profile; refusing fallback\n"); + ret = -1; freeMessage(&response); goto Exit; + } uint32_t pyrowaveProtocolVersion = 0; uint32_t pyrowaveBitstreamVersion = 0; uint32_t pyrowavePayloadVersion = 0; @@ -1658,6 +1686,22 @@ int performRtspHandshake(PSERVER_INFORMATION serverInfo) { // Sunshine dynamic HDR negotiation result. Hosts without the // extension send no X-SS-Dynamic-HDR header; that is the legacy // outcome (DYNAMIC_HDR_FORMAT_NONE) rather than an error. + if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_DUMP && !MatProfileConfirm(&MatDumpProfile, + getOptionContent(response.options,"X-SS-Audio-Codec"), + getOptionContent(response.options,"X-SS-Mat-SHA256"), + getOptionContent(response.options,"X-SS-Mat-Cycles"))) { + Limelog("MAT dump ANNOUNCE confirmation missing or mismatched\n"); + ret = -1; freeMessage(&response); goto Exit; + } + if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_IEC61937 && !MatPlaybackProfileConfirm(&MatDumpProfile, + uniqueMatOption(response.options,"X-SS-Audio-Codec"), + uniqueMatOption(response.options,"X-SS-Mat-SHA256"), + uniqueMatOption(response.options,"X-SS-Mat-Cycles"), + uniqueMatOption(response.options,"X-SS-Mat-Carrier"), + uniqueMatOption(response.options,"X-SS-Mat-Clock"))) { + Limelog("MAT IEC playback ANNOUNCE confirmation missing or mismatched\n"); + ret = -1; freeMessage(&response); goto Exit; + } if (!parseDynamicHdrNegotiation(&response)) { ret = 415; freeMessage(&response); diff --git a/src/RtspParser.c b/src/RtspParser.c index 208d8533c..6b934eaa1 100644 --- a/src/RtspParser.c +++ b/src/RtspParser.c @@ -163,7 +163,16 @@ int parseRtspMessage(PRTSP_MESSAGE msg, char* rtspMessage, int length) { newOpt->option = opt; newOpt->content = token + 1; // Skip the protocol defined blank space newOpt->next = NULL; + bool duplicate = false; + for (POPTION_ITEM existing = options; existing; existing = existing->next) { + if (!strcmp(existing->option, newOpt->option)) { + existing->flags |= FLAG_DUPLICATE_OPTION; + duplicate = true; + break; + } + } insertOption(&options, newOpt); + if (duplicate) free(newOpt); // insertOption retained the existing node. // Check if we're at the end of the message portion marked by \r\n\r\n // endCheck points to the remainder of messageBuffer after the token diff --git a/src/SdpGenerator.c b/src/SdpGenerator.c index a1bebe1a9..90ffc3d39 100644 --- a/src/SdpGenerator.c +++ b/src/SdpGenerator.c @@ -587,8 +587,22 @@ static PSDP_OPTION getAttributesList(char*urlSafeAddr) { if (NegotiatedAudioCodec != AUDIO_CODEC_OPUS) { const char* codecStr = (NegotiatedAudioCodec == AUDIO_CODEC_AC3) ? "ac3" : (NegotiatedAudioCodec == AUDIO_CODEC_EAC3) ? "eac3" : - (NegotiatedAudioCodec == AUDIO_CODEC_PCM_S16) ? "pcm" : "opus"; + (NegotiatedAudioCodec == AUDIO_CODEC_PCM_S16) ? "pcm" : + (NegotiatedAudioCodec == AUDIO_CODEC_MAT_DUMP) ? MAT_DUMP_NAME : + (NegotiatedAudioCodec == AUDIO_CODEC_MAT_IEC61937) ? MAT_PLAYBACK_NAME : "opus"; err |= addAttributeString(&optionHead, "x-ml-audio.codec", codecStr); + if (AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec)) { + if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_DUMP) + err |= addAttributeString(&optionHead,"x-ml-audio.mat-dump","1"); + else { + err |= addAttributeString(&optionHead,"x-ml-audio.mat-playback","1"); + err |= addAttributeString(&optionHead,"x-ml-audio.mat-carrier",MAT_PLAYBACK_CARRIER); + err |= addAttributeString(&optionHead,"x-ml-audio.mat-clock",MAT_PLAYBACK_CLOCK); + } + err |= addAttributeString(&optionHead,"x-ml-audio.mat-sha256",MatDumpProfile.sha256); + snprintf(payloadStr,sizeof(payloadStr),"%u",MatDumpProfile.cycles); + err |= addAttributeString(&optionHead,"x-ml-audio.mat-cycles",payloadStr); + } if (NegotiatedAudioBitrate > 0) { snprintf(payloadStr, sizeof(payloadStr), "%d", NegotiatedAudioBitrate); err |= addAttributeString(&optionHead, "x-ml-audio.bitrate", payloadStr); @@ -634,6 +648,9 @@ static PSDP_OPTION getAttributesList(char*urlSafeAddr) { } // PCM passthrough: keep frames as small as possible. 5 ms @ 48 kHz = // 240 samples, 5.1ch = 2880 B which fits the 4 KB receive buffer. + else if (AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec)) { + AudioPacketDuration = 1; // FEC transport tick; carrier PTS is in SMAT. + } else if (NegotiatedAudioCodec == AUDIO_CODEC_PCM_S16) { AudioPacketDuration = 5; } From cdd4aaecd7919878a0472e6ca18a65461a85984a Mon Sep 17 00:00:00 2001 From: qiin Date: Thu, 22 Oct 2026 18:21:57 +0800 Subject: [PATCH 02/11] docs(audio): document upstream RTSP merge ownership and regression --- docs/MAT-HANDOFF.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/MAT-HANDOFF.md b/docs/MAT-HANDOFF.md index 66aacc7f9..c4654655f 100644 --- a/docs/MAT-HANDOFF.md +++ b/docs/MAT-HANDOFF.md @@ -5,6 +5,18 @@ not default master. Baseline: b91c7cd72764320edd6140a16f8614aeb63dc42a. Keep Sunshine and moonlight-vplus pinned to the same resulting commit; rebuild all consumers because STREAM_CONFIGURATION and the JNI entry point changed. +Upstream integration on 2026-10-22 includes mic commit +`2aa14ffeb877f9093993d9c9cf3c3440e2d2220d`. +The upstream line-based RTSP parser, whitespace/binary-payload support, +case-insensitive last-value-wins lookup and duplicate-node free are retained. +MAT marks the incoming replacement node before upstream insertion; do not +restore the old caller-side free or set flags only on the overwritten node. +`tests/RtspParserMatTest.c` covers unique, identical/case-folded/triple duplicate +headers plus whitespace and embedded-NUL payload handling. With this regression +the parent's rebuilt static-library CTest suites each contain ten tests. +The parent handoff records fresh end-to-end no-device acceptance separately +from the original historical C proof; no Android hardware claim is added. + ## Contract - Codec 4 / `mat-dump-v1`: bounded, app-owned file publication after count/SHA/EOS From 0396c088890c510473d1962e7d6cc665111555d3 Mon Sep 17 00:00:00 2001 From: qiin Date: Thu, 22 Oct 2026 18:31:13 +0800 Subject: [PATCH 03/11] fix(audio): satisfy strict MAT dump compilation on Linux CI --- src/MatDumpFile.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/MatDumpFile.c b/src/MatDumpFile.c index 03872b228..fd7419f8a 100644 --- a/src/MatDumpFile.c +++ b/src/MatDumpFile.c @@ -38,7 +38,10 @@ struct MAT_DUMP_FILE { }; static FILE* exclusive(const char* path) { int fd=mat_open(path,O_WRONLY|O_CREAT|O_EXCL|O_BINARY,0600); - if(fd<0)return NULL;FILE* f=mat_fdopen(fd,"wb");if(!f)mat_close(fd);return f; + if(fd<0)return NULL; + FILE* f=mat_fdopen(fd,"wb"); + if(!f)mat_close(fd); + return f; } static int write_cycle(void* context,const MAT_CYCLE_INFO* info,const uint8_t* p,size_t n) { MAT_DUMP_FILE* s=context; @@ -89,7 +92,9 @@ int MatDumpPush(MAT_DUMP_FILE* s,const unsigned char* p,size_t n) { #endif if(length!=32)s->failed=1; if(!s->failed)for(unsigned i=0;i<32;i++)snprintf(s->sha256+i*2,3,"%02x",bytes[i]); - if(fflush(s->file))s->failed=1;if(fclose(s->file))s->failed=1;s->file=NULL; + if(fflush(s->file))s->failed=1; + if(fclose(s->file))s->failed=1; + s->file=NULL; if(s->receiver.depacketizer.next_frame!=s->expected.cycles || strcmp(s->sha256,s->expected.sha256))s->failed=1; // link() on POSIX refuses an existing destination, unlike rename(). Android // app-private files support same-directory links. Never overwrite a dump. From 773bebded05624e275106ce1fa4bb9aa32726535 Mon Sep 17 00:00:00 2001 From: qiin Date: Thu, 22 Oct 2026 18:32:05 +0800 Subject: [PATCH 04/11] fix(audio): preserve platform CRT setup before MAT headers --- src/Limelight-internal.h | 2 +- src/MatDumpFile.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Limelight-internal.h b/src/Limelight-internal.h index 90fa0268a..2b5adb575 100644 --- a/src/Limelight-internal.h +++ b/src/Limelight-internal.h @@ -1,9 +1,9 @@ #pragma once +#include "Platform.h" #include "MatDumpProfile.h" extern LI_MAT_DUMP_PROFILE MatDumpProfile; #include "MatPlaybackProfile.h" -#include "Platform.h" #include "Limelight.h" #include "PlatformSockets.h" #include "PlatformThreads.h" diff --git a/src/MatDumpFile.c b/src/MatDumpFile.c index fd7419f8a..b3576890c 100644 --- a/src/MatDumpFile.c +++ b/src/MatDumpFile.c @@ -63,7 +63,7 @@ MAT_DUMP_FILE* MatDumpOpen(const char* path,const LI_MAT_DUMP_PROFILE* expected) char count[16];snprintf(count,sizeof(count),"%u",expected->cycles);LI_MAT_DUMP_PROFILE checked; if(!MatProfileSet(&checked,expected->sha256,count))return NULL; MAT_DUMP_FILE* s=calloc(1,sizeof(*s));if(!s)return NULL;s->expected=checked; - strcpy(s->final_path,path);snprintf(s->partial_path,sizeof(s->partial_path),"%s.partial",path); + memcpy(s->final_path,path,strlen(path)+1);snprintf(s->partial_path,sizeof(s->partial_path),"%s.partial",path); snprintf(s->status_path,sizeof(s->status_path),"%s.json",path); struct stat st;if(stat(path,&st)==0 || stat(s->status_path,&st)==0){free(s);return NULL;} #if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) From 05415ed4f544ee10d9fc6c9735a1f34c4a032a87 Mon Sep 17 00:00:00 2001 From: qiin2333 <414382190@qq.com> Date: Thu, 22 Oct 2026 19:58:24 +0800 Subject: [PATCH 05/11] feat(audio): negotiate preferred MAT or Opus before media startup --- CMakeLists.txt | 19 ++++++++++++++++ src/AudioStream.c | 10 +++++++-- src/Connection.c | 11 ++++++++++ src/Limelight.h | 1 + src/MatPlaybackProfile.h | 7 ++++++ src/RtspConnection.c | 27 ++++++++++++++++++++--- tests/AudioCodecSelectionTest.c | 37 ++++++++++++++++++++++++++++++++ tests/MatPlaybackSelectionTest.c | 27 +++++++++++++++++++++++ 8 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 tests/AudioCodecSelectionTest.c create mode 100644 tests/MatPlaybackSelectionTest.c diff --git a/CMakeLists.txt b/CMakeLists.txt index b62c4c16b..dacfe7328 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,6 +121,15 @@ target_include_directories(moonlight-common-c SYSTEM PRIVATE target_compile_definitions(moonlight-common-c PRIVATE HAS_SOCKLEN_T) if (BUILD_TESTING) + add_executable(mat-playback-selection-tests tests/MatPlaybackSelectionTest.c) + target_include_directories(mat-playback-selection-tests PRIVATE src) + if(MSVC) + target_compile_options(mat-playback-selection-tests PRIVATE /W3 /WX) + else() + target_compile_options(mat-playback-selection-tests PRIVATE -Wall -Wextra -Werror) + endif() + add_test(NAME mat-playback-selection-tests COMMAND mat-playback-selection-tests) + add_executable(rtsp-parser-mat-tests tests/RtspParserMatTest.c src/RtspParser.c) target_include_directories(rtsp-parser-mat-tests PRIVATE src enet/include) target_compile_definitions(rtsp-parser-mat-tests PRIVATE HAS_SOCKLEN_T) @@ -128,6 +137,16 @@ if (BUILD_TESTING) get_target_property(_common_c_type moonlight-common-c TYPE) if(_common_c_type STREQUAL "STATIC_LIBRARY") + add_executable(audio-codec-selection-tests tests/AudioCodecSelectionTest.c) + target_include_directories(audio-codec-selection-tests PRIVATE + enet/include $) + target_compile_definitions(audio-codec-selection-tests PRIVATE + $) + target_compile_options(audio-codec-selection-tests PRIVATE + $) + target_link_libraries(audio-codec-selection-tests PRIVATE moonlight-common-c) + add_test(NAME audio-codec-selection-tests COMMAND audio-codec-selection-tests) + # This test includes the sender and links its internal platform/queue helpers. add_executable(controller-handoff-queue-tests tests/ControllerHandoffQueueTest.c) target_include_directories(controller-handoff-queue-tests PRIVATE src enet/include) diff --git a/src/AudioStream.c b/src/AudioStream.c index e172359c7..8bf41bb9a 100644 --- a/src/AudioStream.c +++ b/src/AudioStream.c @@ -95,9 +95,11 @@ static void AudioPingThreadProc(void* context) { // Initialize the audio stream and start int initializeAudioStream(void) { - LbqInitializeLinkedBlockingQueue(&packetQueue, AUDIO_CODEC_IS_MAT(StreamConfig.audioCodec)?512:30); + // RTSP can still select Opus for a MAT preference. Final receive parameters + // are set in startAudioStream(), before either queue worker starts. + LbqInitializeLinkedBlockingQueue(&packetQueue, 30); RtpaInitializeQueue(&rtpAudioQueue); - lastSeq = AUDIO_CODEC_IS_MAT(StreamConfig.audioCodec)?3:0; + lastSeq = 0; matDumpFailedReported=matDumpPublishedReported=false; matPlaybackFailedReported=matPlaybackCompleteReported=false; receivedDataFromPeer = false; @@ -470,6 +472,10 @@ int startAudioStream(void* audioContext, int arFlags) { int err; OPUS_MULTISTREAM_CONFIGURATION chosenConfig; + LC_ASSERT(packetQueue.currentSize == 0); + packetQueue.sizeBound = AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec) ? 512 : 30; + lastSeq = AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec) ? 3 : 0; + if (HighQualitySurroundEnabled) { LC_ASSERT(HighQualitySurroundSupported); LC_ASSERT(HighQualityOpusConfig.channelCount != 0); diff --git a/src/Connection.c b/src/Connection.c index 436549f6b..7549db2b4 100644 --- a/src/Connection.c +++ b/src/Connection.c @@ -357,6 +357,17 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre : (StreamConfig.audioCodec == AUDIO_CODEC_AC3 ? 640000 : 384000); } break; + case AUDIO_CODEC_MAT_AUTO: + if (StreamConfig.matDumpPath[0]) { + Limelog("MAT preference cannot use a dump path\n"); + err = -1; goto Cleanup; + } + if (!IS_SUNSHINE() || StreamConfig.controlOnly) { + Limelog("MAT preference unavailable for this session; selecting Opus\n"); + break; + } + NegotiatedAudioCodec = AUDIO_CODEC_MAT_IEC61937; + break; case AUDIO_CODEC_MAT_IEC61937: if (!IS_SUNSHINE() || StreamConfig.controlOnly || StreamConfig.matDumpPath[0]) { Limelog("MAT playback requires Sunshine audio session and no dump path\n"); diff --git a/src/Limelight.h b/src/Limelight.h index 8eefececc..170f704a0 100644 --- a/src/Limelight.h +++ b/src/Limelight.h @@ -305,6 +305,7 @@ typedef struct _DECODE_UNIT { #define AUDIO_CODEC_PCM_S16 3 #define AUDIO_CODEC_MAT_DUMP 4 #define AUDIO_CODEC_MAT_IEC61937 5 +#define AUDIO_CODEC_MAT_AUTO 6 // Local preference: select codec 5 or 0 before ANNOUNCE; never a wire codec. #define AUDIO_CODEC_IS_MAT(c) ((c)==AUDIO_CODEC_MAT_DUMP || (c)==AUDIO_CODEC_MAT_IEC61937) /* Registered from audio init, cleared after audio threads join. Never called for diff --git a/src/MatPlaybackProfile.h b/src/MatPlaybackProfile.h index 8e499312c..3cdf57612 100644 --- a/src/MatPlaybackProfile.h +++ b/src/MatPlaybackProfile.h @@ -21,3 +21,10 @@ static inline int MatPlaybackProfileConfirm(const LI_MAT_DUMP_PROFILE* expected, clock && !strcmp(clock,MAT_PLAYBACK_CLOCK) && MatProfileSet(&got,hash,count) && got.cycles==expected->cycles && !strcmp(got.sha256,expected->sha256); } +/* Fallback is legal only during DESCRIBE, before any codec is announced or media is consumed. + * Strict diagnostic requests retain their failure behavior. Never accept a dump-only profile. */ +static inline int MatPlaybackProfileSelect(LI_MAT_DUMP_PROFILE* out,const char* sdp,int allowFallback) { + if(sdp && MatPlaybackProfileParse(out,sdp))return MAT_PLAYBACK_CODEC; + memset(out,0,sizeof(*out)); + return allowFallback?0:-1; +} diff --git a/src/RtspConnection.c b/src/RtspConnection.c index 2574732cb..da0fc1fd6 100644 --- a/src/RtspConnection.c +++ b/src/RtspConnection.c @@ -1279,9 +1279,18 @@ int performRtspHandshake(PSERVER_INFORMATION serverInfo) { Limelog("Host lacks valid MAT dump v1 capability/manifest; refusing implicit fallback\n"); ret = -1; freeMessage(&response); goto Exit; } - if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_IEC61937 && !MatPlaybackProfileParse(&MatDumpProfile,response.payload)) { - Limelog("Host lacks explicit finite MAT IEC playback profile; refusing fallback\n"); - ret = -1; freeMessage(&response); goto Exit; + if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_IEC61937) { + int selected = MatPlaybackProfileSelect(&MatDumpProfile,response.payload, + StreamConfig.audioCodec == AUDIO_CODEC_MAT_AUTO); + if (selected < 0) { + Limelog("Host lacks explicit finite MAT IEC playback profile; refusing fallback\n"); + ret = -1; freeMessage(&response); goto Exit; + } + NegotiatedAudioCodec = selected; + if (selected == AUDIO_CODEC_OPUS) { + NegotiatedAudioBitrate = 0; + Limelog("Host MAT profile unavailable or incompatible; selecting Opus before ANNOUNCE\n"); + } } uint32_t pyrowaveProtocolVersion = 0; uint32_t pyrowaveBitstreamVersion = 0; @@ -1702,6 +1711,18 @@ int performRtspHandshake(PSERVER_INFORMATION serverInfo) { Limelog("MAT IEC playback ANNOUNCE confirmation missing or mismatched\n"); ret = -1; freeMessage(&response); goto Exit; } + if (NegotiatedAudioCodec == AUDIO_CODEC_OPUS) { + const char* confirmedCodec = getOptionContent(response.options,"X-SS-Audio-Codec"); + if ((confirmedCodec && (strcmp(confirmedCodec,"opus") || + !uniqueMatOption(response.options,"X-SS-Audio-Codec"))) || + getOptionContent(response.options,"X-SS-Mat-SHA256") || + getOptionContent(response.options,"X-SS-Mat-Cycles") || + getOptionContent(response.options,"X-SS-Mat-Carrier") || + getOptionContent(response.options,"X-SS-Mat-Clock")) { + Limelog("Unexpected codec/MAT confirmation after selecting Opus; refusing media\n"); + ret = -1; freeMessage(&response); goto Exit; + } + } if (!parseDynamicHdrNegotiation(&response)) { ret = 415; freeMessage(&response); diff --git a/tests/AudioCodecSelectionTest.c b/tests/AudioCodecSelectionTest.c new file mode 100644 index 000000000..609b7e6c0 --- /dev/null +++ b/tests/AudioCodecSelectionTest.c @@ -0,0 +1,37 @@ +/* Exercise the real audio startup with a sink that stops before hardware/threads. */ +#include "../src/AudioStream.c" +#include +static int failures,expectedCodec; +#define CHECK(c) do { if(!(c)) { fprintf(stderr,"line %d: %s\n",__LINE__,#c);failures++;} } while(0) +static int stopBeforeHardware(int configuration,const POPUS_MULTISTREAM_CONFIGURATION format,void* context,int flags) { + (void)configuration;(void)context;(void)flags; + int mat=AUDIO_CODEC_IS_MAT(expectedCodec); + CHECK(packetQueue.sizeBound==(mat?512:30)); + CHECK(lastSeq==(mat?3:0)); + CHECK(format->sampleRate==48000); + CHECK(format->samplesPerFrame==(mat?960:240)); + CHECK(matDump==NULL && matPlayback==NULL); + return -123; +} +int main(void) { + CHECK(initializePlatform()==0); + const int requests[]={AUDIO_CODEC_MAT_AUTO,AUDIO_CODEC_MAT_AUTO,AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_OPUS}; + const int selections[]={AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_OPUS}; + for(unsigned i=0;i +#define CHECK(c) do { if(!(c)) { fprintf(stderr,"line %d: %s\n",__LINE__,#c);return 1;} } while(0) +static const char valid[] = + "v=0\r\na=x-ss-audio.mat-playback:1\r\na=x-ss-audio.mat-carrier:" MAT_PLAYBACK_CARRIER + "\r\na=x-ss-audio.mat-clock:48000\r\na=x-ss-audio.mat-sha256:" + "79cd69e33adcd1bcffe95673564b6fc727a7aaed7370745d43ec269925a492f6" + "\r\na=x-ss-audio.mat-cycles:178\r\n"; +int main(void) { + LI_MAT_DUMP_PROFILE p; + CHECK(MatPlaybackProfileSelect(&p,valid,1)==AUDIO_CODEC_MAT_IEC61937); + CHECK(p.cycles==178); + CHECK(MatPlaybackProfileSelect(&p,"v=0\r\n",1)==AUDIO_CODEC_OPUS); + CHECK(p.cycles==0 && p.sha256[0]==0); + CHECK(MatPlaybackProfileSelect(&p,valid,0)==AUDIO_CODEC_MAT_IEC61937); + CHECK(MatPlaybackProfileSelect(&p,"v=0\r\n",0)==-1); + CHECK(p.cycles==0 && p.sha256[0]==0); + CHECK(MatPlaybackProfileSelect(&p,NULL,1)==AUDIO_CODEC_OPUS); + CHECK(MatPlaybackProfileSelect(&p,"a=x-ss-audio.mat-dump:1\r\n",1)==AUDIO_CODEC_OPUS); + char duplicate[1024];snprintf(duplicate,sizeof(duplicate),"%sa=x-ss-audio.mat-playback:1\r\n",valid); + CHECK(MatPlaybackProfileSelect(&p,duplicate,1)==AUDIO_CODEC_OPUS); + CHECK(p.cycles==0 && p.sha256[0]==0); + CHECK(!AUDIO_CODEC_IS_MAT(AUDIO_CODEC_MAT_AUTO)); + puts("PASS MAT preference selects only confirmed playback; absent/invalid/dump profiles fall back before ANNOUNCE; strict mode rejects"); + return 0; +} From 1ac00894ef5e36caa6625ce81abcc4d499d930de Mon Sep 17 00:00:00 2001 From: qiin2333 <414382190@qq.com> Date: Thu, 22 Oct 2026 21:15:14 +0800 Subject: [PATCH 06/11] feat(audio): negotiate and authenticate continuous live MAT streams --- CMakeLists.txt | 22 ++++++++ src/AudioStream.c | 37 ++++++++++++- src/Connection.c | 11 +++- src/Limelight-internal.h | 2 + src/Limelight.h | 10 ++-- src/MatLiveCrypto.h | 97 +++++++++++++++++++++++++++++++++ src/MatLivePlayback.c | 35 ++++++++++++ src/MatLivePlayback.h | 14 +++++ src/MatLiveProfile.h | 35 ++++++++++++ src/RtspConnection.c | 25 ++++++++- src/SdpGenerator.c | 23 ++++++-- tests/AudioCodecSelectionTest.c | 4 +- tests/MatLiveRtpTest.c | 44 +++++++++++++++ tests/MatLiveTest.c | 47 ++++++++++++++++ 14 files changed, 389 insertions(+), 17 deletions(-) create mode 100644 src/MatLiveCrypto.h create mode 100644 src/MatLivePlayback.c create mode 100644 src/MatLivePlayback.h create mode 100644 src/MatLiveProfile.h create mode 100644 tests/MatLiveRtpTest.c create mode 100644 tests/MatLiveTest.c diff --git a/CMakeLists.txt b/CMakeLists.txt index dacfe7328..9b0ad0aff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,6 +121,17 @@ target_include_directories(moonlight-common-c SYSTEM PRIVATE target_compile_definitions(moonlight-common-c PRIVATE HAS_SOCKLEN_T) if (BUILD_TESTING) + add_executable(mat-live-tests tests/MatLiveTest.c) + target_include_directories(mat-live-tests PRIVATE src) + target_link_libraries(mat-live-tests PRIVATE moonlight-common-c) + if(USE_MBEDTLS) + target_compile_definitions(mat-live-tests PRIVATE USE_MBEDTLS) + else() + target_include_directories(mat-live-tests SYSTEM PRIVATE ${OPENSSL_INCLUDE_DIR}) + target_link_libraries(mat-live-tests PRIVATE ${OPENSSL_CRYPTO_LIBRARY}) + endif() + add_test(NAME mat-live-tests COMMAND mat-live-tests) + add_executable(mat-playback-selection-tests tests/MatPlaybackSelectionTest.c) target_include_directories(mat-playback-selection-tests PRIVATE src) if(MSVC) @@ -137,6 +148,17 @@ if (BUILD_TESTING) get_target_property(_common_c_type moonlight-common-c TYPE) if(_common_c_type STREQUAL "STATIC_LIBRARY") + add_executable(mat-live-rtp-tests tests/MatLiveRtpTest.c) + target_include_directories(mat-live-rtp-tests PRIVATE enet/include + $) + target_compile_definitions(mat-live-rtp-tests PRIVATE + $) + target_link_libraries(mat-live-rtp-tests PRIVATE moonlight-common-c) + if(NOT USE_MBEDTLS) + target_include_directories(mat-live-rtp-tests SYSTEM PRIVATE ${OPENSSL_INCLUDE_DIR}) + target_link_libraries(mat-live-rtp-tests PRIVATE ${OPENSSL_CRYPTO_LIBRARY}) + endif() + add_test(NAME mat-live-rtp-tests COMMAND mat-live-rtp-tests) add_executable(audio-codec-selection-tests tests/AudioCodecSelectionTest.c) target_include_directories(audio-codec-selection-tests PRIVATE enet/include $) diff --git a/src/AudioStream.c b/src/AudioStream.c index 8bf41bb9a..cf43cb15f 100644 --- a/src/AudioStream.c +++ b/src/AudioStream.c @@ -1,16 +1,34 @@ #include "Limelight-internal.h" #include "MatDumpFile.h" #include "MatPlayback.h" +#include "MatLivePlayback.h" +#include +static atomic_bool liveInputEnded; +static atomic_ullong liveLastAuthenticatedMs; +static MAT_LIVE_PLAYBACK* matLive; static MAT_PLAYBACK_RECEIVER* matPlayback; static LI_MAT_PLAYBACK_CALLBACKS matCallbacks; static bool matPlaybackFailedReported,matPlaybackCompleteReported; void LiSetMatPlaybackCallbacks(const LI_MAT_PLAYBACK_CALLBACKS* callbacks) { if(callbacks) matCallbacks=*callbacks; else memset(&matCallbacks,0,sizeof(matCallbacks)); } +static int liveEnd(void* context,uint64_t cycles) { + // A verified END may take time to drain the hardware. Stop the network + // watchdog before that bounded drain, not before authentication. + atomic_store_explicit(&liveInputEnded,true,memory_order_relaxed); + return matCallbacks.end(context,cycles); +} static MAT_DUMP_FILE* matDump; -static void failMatInput(void) { MatDumpFail(matDump); MatPlaybackFail(matPlayback); } +static void failMatInput(void) { MatDumpFail(matDump); MatPlaybackFail(matPlayback);MatLivePlaybackFail(matLive); } static bool matDumpFailedReported,matDumpPublishedReported; static void consumeAudio(char* data,int size) { + if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_LIVE) { + int result=MatLivePlaybackPush(matLive,(unsigned char*)data,(size_t)(size>0?size:0)); + if(result>=0)atomic_store_explicit(&liveLastAuthenticatedMs,PltGetMillis(),memory_order_relaxed); + if(result<0&&!matPlaybackFailedReported){matPlaybackFailedReported=true;Limelog("Live MAT authentication/sequence/source failed\n");ListenerCallbacks.connectionTerminated(-1);} + else if(result==2&&!matPlaybackCompleteReported){matPlaybackCompleteReported=true;Limelog("Live MAT authenticated EOS and renderer drain completed\n");} + return; + } if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_IEC61937){ int result=MatPlaybackPush(matPlayback,(unsigned char*)data,(size_t)(size>0?size:0)); if(result<0 && !matPlaybackFailedReported){matPlaybackFailedReported=true;Limelog("MAT IEC playback failed\n");ListenerCallbacks.connectionTerminated(-1);} @@ -161,6 +179,7 @@ static void freePacketList(PLINKED_BLOCKING_QUEUE_ENTRY entry) { // Tear down the audio stream once we're done with it void destroyAudioStream(void) { + MatLivePlaybackClose(matLive);matLive=NULL; MatDumpClose(matDump);matDump=NULL; MatPlaybackClose(matPlayback);matPlayback=NULL; if (rtpSocket != INVALID_SOCKET) { @@ -219,7 +238,7 @@ static void decodeInputData(PQUEUED_AUDIO_PACKET packet) { lastSeq = rtp->sequenceNumber; - if (AudioEncryptionEnabled) { + if (AudioEncryptionEnabled && NegotiatedAudioCodec!=AUDIO_CODEC_MAT_LIVE) { // We must have room for the AES padding which may be written to the buffer unsigned char decryptedOpusData[ROUND_TO_PKCS7_PADDED_LEN(MAX_PACKET_SIZE)]; unsigned char iv[16] = { 0 }; @@ -289,6 +308,7 @@ static void AudioReceiveThreadProc(void* context) { uint32_t packetsToDrop; int waitingForAudioMs; + packet = NULL; packetsToDrop = AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec)?0:500 / AudioPacketDuration; @@ -303,6 +323,10 @@ static void AudioReceiveThreadProc(void* context) { waitingForAudioMs = 0; while (!PltIsThreadInterrupted(&receiveThread)) { + if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_LIVE && !atomic_load_explicit(&liveInputEnded,memory_order_relaxed) && + PltGetMillis()-atomic_load_explicit(&liveLastAuthenticatedMs,memory_order_relaxed)>=2000) { + Limelog("Live MAT authenticated input timed out (no EOS)\n");ListenerCallbacks.connectionTerminated(-1);break; + } if (packet == NULL) { packet = (PQUEUED_AUDIO_PACKET)malloc(sizeof(*packet)); if (packet == NULL) { @@ -320,7 +344,6 @@ static void AudioReceiveThreadProc(void* context) { } else if (packet->header.size == 0) { // Receive timed out; try again - if (!receivedDataFromPeer) { waitingForAudioMs += UDP_RECV_POLL_TIMEOUT_MS; } @@ -465,6 +488,7 @@ void stopAudioStream(void) { MatDumpClose(matDump);matDump=NULL; MatPlaybackClose(matPlayback);matPlayback=NULL; + MatLivePlaybackClose(matLive);matLive=NULL; AudioCallbacks.cleanup(); } @@ -503,6 +527,13 @@ int startAudioStream(void* audioContext, int arFlags) { matPlayback=MatPlaybackOpen(&MatDumpProfile,&matCallbacks); if(!matPlayback){AudioCallbacks.cleanup();return -1;} } + if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_LIVE) { + LI_MAT_PLAYBACK_CALLBACKS liveCallbacks=matCallbacks;liveCallbacks.end=matCallbacks.end?liveEnd:NULL; + atomic_store_explicit(&liveInputEnded,false,memory_order_relaxed); + atomic_store_explicit(&liveLastAuthenticatedMs,PltGetMillis(),memory_order_relaxed); + matLive=MatLivePlaybackOpen(&MatLiveProfile,(const uint8_t*)StreamConfig.remoteInputAesKey,&liveCallbacks); + if(!matLive){AudioCallbacks.cleanup();return -1;} + } AudioCallbacks.start(); err = PltCreateThread("AudioRecv", AudioReceiveThreadProc, NULL, &receiveThread); diff --git a/src/Connection.c b/src/Connection.c index 7549db2b4..6c624dc72 100644 --- a/src/Connection.c +++ b/src/Connection.c @@ -27,6 +27,7 @@ int AudioPacketDuration; bool AudioEncryptionEnabled; bool ReferenceFrameInvalidationSupported; int NegotiatedAudioCodec; +LI_MAT_LIVE_PROFILE MatLiveProfile; LI_MAT_DUMP_PROFILE MatDumpProfile; int NegotiatedAudioBitrate; int NegotiatedDynamicHdrFormat; @@ -333,6 +334,7 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre NegotiatedAudioCodec = AUDIO_CODEC_OPUS; NegotiatedAudioBitrate = 0; memset(&MatDumpProfile,0,sizeof(MatDumpProfile)); + memset(&MatLiveProfile,0,sizeof(MatLiveProfile)); switch (StreamConfig.audioCodec) { case AUDIO_CODEC_OPUS: // Default; nothing to do. @@ -366,7 +368,14 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre Limelog("MAT preference unavailable for this session; selecting Opus\n"); break; } - NegotiatedAudioCodec = AUDIO_CODEC_MAT_IEC61937; + NegotiatedAudioCodec = AUDIO_CODEC_MAT_LIVE; + break; + case AUDIO_CODEC_MAT_LIVE: + if (!IS_SUNSHINE() || StreamConfig.controlOnly || StreamConfig.matDumpPath[0]) { + Limelog("Live MAT requires a Sunshine audio session and no dump path\n"); + err = -1; goto Cleanup; + } + NegotiatedAudioCodec = AUDIO_CODEC_MAT_LIVE; break; case AUDIO_CODEC_MAT_IEC61937: if (!IS_SUNSHINE() || StreamConfig.controlOnly || StreamConfig.matDumpPath[0]) { diff --git a/src/Limelight-internal.h b/src/Limelight-internal.h index 2b5adb575..57807ed61 100644 --- a/src/Limelight-internal.h +++ b/src/Limelight-internal.h @@ -3,6 +3,8 @@ #include "MatDumpProfile.h" extern LI_MAT_DUMP_PROFILE MatDumpProfile; #include "MatPlaybackProfile.h" +#include "MatLiveProfile.h" +extern LI_MAT_LIVE_PROFILE MatLiveProfile; #include "Limelight.h" #include "PlatformSockets.h" diff --git a/src/Limelight.h b/src/Limelight.h index 170f704a0..5145e2668 100644 --- a/src/Limelight.h +++ b/src/Limelight.h @@ -305,12 +305,14 @@ typedef struct _DECODE_UNIT { #define AUDIO_CODEC_PCM_S16 3 #define AUDIO_CODEC_MAT_DUMP 4 #define AUDIO_CODEC_MAT_IEC61937 5 -#define AUDIO_CODEC_MAT_AUTO 6 // Local preference: select codec 5 or 0 before ANNOUNCE; never a wire codec. -#define AUDIO_CODEC_IS_MAT(c) ((c)==AUDIO_CODEC_MAT_DUMP || (c)==AUDIO_CODEC_MAT_IEC61937) +#define AUDIO_CODEC_MAT_AUTO 6 // Local preference: select codec 7 or 0 before ANNOUNCE; never a wire codec. +#define AUDIO_CODEC_MAT_LIVE 7 +#define AUDIO_CODEC_IS_MAT(c) ((c)==AUDIO_CODEC_MAT_DUMP || (c)==AUDIO_CODEC_MAT_IEC61937 || (c)==AUDIO_CODEC_MAT_LIVE) /* Registered from audio init, cleared after audio threads join. Never called for - * codec4. Return 0 to fail the loss-intolerant session. End is input SHA-verified - * EOS; the renderer must drain before returning success. No PCM/encoded callback. */ + * codec4. Return 0 to fail the loss-intolerant session. End is verified EOS + * (finite SHA/count for 5, authenticated END for 7); the renderer must drain + * before returning success. No PCM/encoded callback. */ typedef struct { int (*cycle)(void*, const unsigned char*, int, uint64_t, uint64_t, uint64_t, uint64_t); int (*end)(void*, uint64_t); diff --git a/src/MatLiveCrypto.h b/src/MatLiveCrypto.h new file mode 100644 index 000000000..3d5bdbab5 --- /dev/null +++ b/src/MatLiveCrypto.h @@ -0,0 +1,97 @@ +#pragma once +#include "MatWire.h" +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) +#include +#else +#include +#include +#endif +#define MAT_LIVE_HEADER 32u +#define MAT_LIVE_TAG 16u +#define MAT_LIVE_PACKET (MAT_LIVE_HEADER+MAT_WIRE_PACKET+MAT_LIVE_TAG) +typedef struct { uint8_t key[16];uint64_t epoch,domain; } MAT_LIVE_CIPHER; +static inline void MatLiveWipe(void* p,size_t n) { volatile uint8_t* b=(volatile uint8_t*)p;while(n--)*b++=0; } +static inline int MatLiveHmac(const uint8_t* key,size_t size,const uint8_t* data,size_t length,uint8_t out[32]) { +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) + psa_key_attributes_t a=PSA_KEY_ATTRIBUTES_INIT;psa_key_id_t id=0;size_t written=0; + psa_set_key_type(&a,PSA_KEY_TYPE_HMAC);psa_set_key_bits(&a,size*8); + psa_set_key_usage_flags(&a,PSA_KEY_USAGE_SIGN_MESSAGE);psa_set_key_algorithm(&a,PSA_ALG_HMAC(PSA_ALG_SHA_256)); + if(psa_crypto_init()!=PSA_SUCCESS || psa_import_key(&a,key,size,&id)!=PSA_SUCCESS)return 0; + psa_status_t result=psa_mac_compute(id,PSA_ALG_HMAC(PSA_ALG_SHA_256),data,length,out,32,&written); + psa_destroy_key(id);return result==PSA_SUCCESS && written==32; +#else + unsigned written=0; + return HMAC(EVP_sha256(),key,(int)size,data,length,out,&written)!=NULL && written==32; +#endif +} +/* RFC5869, one expansion block. A separate key per protocol/epoch/domain prevents + * overlap with GameStream control/audio keys; no RTP16 value is used as a nonce. */ +static inline int MatLiveCipherInit(MAT_LIVE_CIPHER* c,const uint8_t* key,size_t length,uint64_t epoch,uint64_t domain) { + static const uint8_t salt[]="Sunshine MAT live v1"; + static const uint8_t label[]="MATL AES128 GCM"; + uint8_t prk[32],expanded[32],info[sizeof(label)-1+17];int okay=0; + memset(c,0,sizeof(*c)); + if(!key||length!=16||!epoch||epoch>INT64_MAX||!domain||domain>INT64_MAX)return 0; + memcpy(info,label,sizeof(label)-1);mat_put64(info+sizeof(label)-1,epoch); + mat_put64(info+sizeof(label)-1+8,domain);info[sizeof(info)-1]=1; + if(MatLiveHmac(salt,sizeof(salt)-1,key,length,prk) && MatLiveHmac(prk,32,info,sizeof(info),expanded)) { + memcpy(c->key,expanded,16);c->epoch=epoch;c->domain=domain;okay=1; + } + MatLiveWipe(prk,sizeof(prk));MatLiveWipe(expanded,sizeof(expanded));return okay; +} +static inline void MatLiveNonce(uint8_t nonce[12],uint64_t packet) { + memcpy(nonce,"MATL",4);mat_put64(nonce+4,packet); +} +static inline int MatLiveSeal(const MAT_LIVE_CIPHER* c,uint64_t sequence,const uint8_t plain[MAT_WIRE_PACKET],uint8_t wire[MAT_LIVE_PACKET]) { + uint8_t nonce[12];MatLiveNonce(nonce,sequence); + if(!c||!c->epoch||sequence>INT64_MAX)return 0; + memset(wire,0,MAT_LIVE_HEADER);memcpy(wire,"MLIV",4);wire[4]=1;wire[5]=MAT_LIVE_HEADER; + mat_put64(wire+8,c->epoch);mat_put64(wire+16,sequence);mat_put32(wire+24,MAT_WIRE_PACKET); +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) + psa_key_attributes_t a=PSA_KEY_ATTRIBUTES_INIT;psa_key_id_t id=0;size_t written=0; + psa_set_key_type(&a,PSA_KEY_TYPE_AES);psa_set_key_bits(&a,128);psa_set_key_usage_flags(&a,PSA_KEY_USAGE_ENCRYPT); + psa_set_key_algorithm(&a,PSA_ALG_GCM); + if(psa_crypto_init()!=PSA_SUCCESS||psa_import_key(&a,c->key,16,&id)!=PSA_SUCCESS)return 0; + psa_status_t result=psa_aead_encrypt(id,PSA_ALG_GCM,nonce,12,wire,MAT_LIVE_HEADER,plain,MAT_WIRE_PACKET, + wire+MAT_LIVE_HEADER,MAT_WIRE_PACKET+MAT_LIVE_TAG,&written); + psa_destroy_key(id);return result==PSA_SUCCESS && written==MAT_WIRE_PACKET+MAT_LIVE_TAG; +#else + EVP_CIPHER_CTX* ctx=EVP_CIPHER_CTX_new();int n=0,tail=0,okay=0; + if(ctx && EVP_EncryptInit_ex(ctx,EVP_aes_128_gcm(),NULL,NULL,NULL)==1 && + EVP_CIPHER_CTX_ctrl(ctx,EVP_CTRL_GCM_SET_IVLEN,12,NULL)==1 && + EVP_EncryptInit_ex(ctx,NULL,NULL,c->key,nonce)==1 && + EVP_EncryptUpdate(ctx,NULL,&n,wire,MAT_LIVE_HEADER)==1 && + EVP_EncryptUpdate(ctx,wire+MAT_LIVE_HEADER,&n,plain,MAT_WIRE_PACKET)==1 && n==MAT_WIRE_PACKET && + EVP_EncryptFinal_ex(ctx,wire+MAT_LIVE_HEADER+n,&tail)==1 && tail==0 && + EVP_CIPHER_CTX_ctrl(ctx,EVP_CTRL_GCM_GET_TAG,MAT_LIVE_TAG,wire+MAT_LIVE_HEADER+MAT_WIRE_PACKET)==1)okay=1; + EVP_CIPHER_CTX_free(ctx);return okay; +#endif +} +/* The caller must not expose plain before authentication succeeds. */ +static inline int MatLiveOpenPacket(const MAT_LIVE_CIPHER* c,uint64_t sequence,const uint8_t* wire,size_t length,uint8_t plain[MAT_WIRE_PACKET]) { + uint8_t nonce[12];MatLiveNonce(nonce,sequence); + if(!c||!wire||length!=MAT_LIVE_PACKET||memcmp(wire,"MLIV",4)||wire[4]!=1||wire[5]!=MAT_LIVE_HEADER|| + wire[6]||wire[7]||mat_get64(wire+8)!=c->epoch||mat_get64(wire+16)!=sequence|| + sequence>INT64_MAX||mat_get32(wire+24)!=MAT_WIRE_PACKET||mat_get32(wire+28))return 0; +#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) + psa_key_attributes_t a=PSA_KEY_ATTRIBUTES_INIT;psa_key_id_t id=0;size_t written=0; + psa_set_key_type(&a,PSA_KEY_TYPE_AES);psa_set_key_bits(&a,128);psa_set_key_usage_flags(&a,PSA_KEY_USAGE_DECRYPT); + psa_set_key_algorithm(&a,PSA_ALG_GCM); + if(psa_crypto_init()!=PSA_SUCCESS||psa_import_key(&a,c->key,16,&id)!=PSA_SUCCESS)return 0; + psa_status_t result=psa_aead_decrypt(id,PSA_ALG_GCM,nonce,12,wire,MAT_LIVE_HEADER,wire+MAT_LIVE_HEADER, + MAT_WIRE_PACKET+MAT_LIVE_TAG,plain,MAT_WIRE_PACKET,&written); + psa_destroy_key(id); + if(result==PSA_SUCCESS&&written==MAT_WIRE_PACKET)return 1; +#else + EVP_CIPHER_CTX* ctx=EVP_CIPHER_CTX_new();int n=0,tail=0,okay=0; + if(ctx && EVP_DecryptInit_ex(ctx,EVP_aes_128_gcm(),NULL,NULL,NULL)==1 && + EVP_CIPHER_CTX_ctrl(ctx,EVP_CTRL_GCM_SET_IVLEN,12,NULL)==1 && + EVP_DecryptInit_ex(ctx,NULL,NULL,c->key,nonce)==1 && + EVP_DecryptUpdate(ctx,NULL,&n,wire,MAT_LIVE_HEADER)==1 && + EVP_DecryptUpdate(ctx,plain,&n,wire+MAT_LIVE_HEADER,MAT_WIRE_PACKET)==1 && n==MAT_WIRE_PACKET && + EVP_CIPHER_CTX_ctrl(ctx,EVP_CTRL_GCM_SET_TAG,MAT_LIVE_TAG,(void*)(wire+MAT_LIVE_HEADER+MAT_WIRE_PACKET))==1 && + EVP_DecryptFinal_ex(ctx,plain+n,&tail)==1 && tail==0)okay=1; + EVP_CIPHER_CTX_free(ctx);if(okay)return 1; +#endif + MatLiveWipe(plain,MAT_WIRE_PACKET);return 0; +} diff --git a/src/MatLivePlayback.c b/src/MatLivePlayback.c new file mode 100644 index 000000000..35640a8d9 --- /dev/null +++ b/src/MatLivePlayback.c @@ -0,0 +1,35 @@ +#include "MatLivePlayback.h" +#include "MatLiveCrypto.h" +#include "MatReceiver.h" +#include +struct MAT_LIVE_PLAYBACK { + MAT_RECEIVER receiver;MAT_LIVE_CIPHER cipher;LI_MAT_PLAYBACK_CALLBACKS callbacks; + uint64_t nextPacket;int failed; +}; +static int cycle(void* context,const MAT_CYCLE_INFO* info,const uint8_t* bytes,size_t n) { + MAT_LIVE_PLAYBACK* s=context; + return s->callbacks.cycle(s->callbacks.context,bytes,(int)n,info->epoch,info->domain,info->index,info->pts48k); +} +static int end(void* context,uint64_t count) { + MAT_LIVE_PLAYBACK* s=context;return s->callbacks.end(s->callbacks.context,count); +} +MAT_LIVE_PLAYBACK* MatLivePlaybackOpen(const LI_MAT_LIVE_PROFILE* p,const uint8_t key[16],const LI_MAT_PLAYBACK_CALLBACKS* callbacks) { + if(!p||!callbacks||!callbacks->cycle||!callbacks->end)return NULL; + MAT_LIVE_PLAYBACK* s=calloc(1,sizeof(*s));if(!s)return NULL; + if(!MatLiveCipherInit(&s->cipher,key,16,p->epoch,p->domain)){free(s);return NULL;} + s->callbacks=*callbacks;s->nextPacket=4; /* four authenticated transport SYNC packets precede RTP data */ + MatReceiverInit(&s->receiver,cycle,end,s);return s; +} +int MatLivePlaybackPush(MAT_LIVE_PLAYBACK* s,const uint8_t* wire,size_t length) { + uint8_t plain[MAT_WIRE_PACKET]; + if(!s||s->failed)return -1; + if(!MatLiveOpenPacket(&s->cipher,s->nextPacket,wire,length,plain) || + mat_get64(plain+16)!=s->cipher.epoch||mat_get64(plain+24)!=s->cipher.domain) { + MatLivePlaybackFail(s);return -1; + } + int result=MatReceiverPush(&s->receiver,plain,sizeof(plain));MatLiveWipe(plain,sizeof(plain)); + if(result<0){MatLivePlaybackFail(s);return -1;} + ++s->nextPacket;return result; +} +void MatLivePlaybackFail(MAT_LIVE_PLAYBACK* s){if(s){s->failed=1;MatReceiverAbort(&s->receiver);}} +void MatLivePlaybackClose(MAT_LIVE_PLAYBACK* s){if(s){MatLiveWipe(s,sizeof(*s));free(s);}} diff --git a/src/MatLivePlayback.h b/src/MatLivePlayback.h new file mode 100644 index 000000000..5b57fa18c --- /dev/null +++ b/src/MatLivePlayback.h @@ -0,0 +1,14 @@ +#pragma once +#include "Limelight.h" +#include "MatLiveProfile.h" +#ifdef __cplusplus +extern "C" { +#endif +typedef struct MAT_LIVE_PLAYBACK MAT_LIVE_PLAYBACK; +MAT_LIVE_PLAYBACK* MatLivePlaybackOpen(const LI_MAT_LIVE_PROFILE*,const uint8_t key[16],const LI_MAT_PLAYBACK_CALLBACKS*); +int MatLivePlaybackPush(MAT_LIVE_PLAYBACK*,const uint8_t*,size_t); +void MatLivePlaybackFail(MAT_LIVE_PLAYBACK*); +void MatLivePlaybackClose(MAT_LIVE_PLAYBACK*); +#ifdef __cplusplus +} +#endif diff --git a/src/MatLiveProfile.h b/src/MatLiveProfile.h new file mode 100644 index 000000000..3219b75ce --- /dev/null +++ b/src/MatLiveProfile.h @@ -0,0 +1,35 @@ +#pragma once +#include "MatDumpProfile.h" +#include "MatPlaybackProfile.h" +#define MAT_LIVE_CODEC 7 +#define MAT_LIVE_NAME "mat-live-v1" +#define MAT_LIVE_CRYPTO "aes128-gcm-hkdf-sha256" +typedef struct { uint64_t epoch,domain; } LI_MAT_LIVE_PROFILE; +static inline int MatLiveDecimal(const char* text,uint64_t* value) { + uint64_t n=0; + if(!text || !*text || *text=='0')return 0; + for(const char* p=text;*p;p++) { + if(*p<'0'||*p>'9'||n>((uint64_t)INT64_MAX-(unsigned)(*p-'0'))/10)return 0; + n=n*10+(unsigned)(*p-'0'); + } + *value=n;return n!=0; +} +static inline int MatLiveProfileParse(LI_MAT_LIVE_PROFILE* out,const char* sdp) { + char version[8],carrier[48],clock[16],crypto[48],epoch[24],domain[24]; + memset(out,0,sizeof(*out)); + if(!sdp)return 0; + return MatSdpValue(sdp,"x-ss-audio.mat-live",version,sizeof(version))==1 && !strcmp(version,"1") && + MatSdpValue(sdp,"x-ss-audio.mat-live-carrier",carrier,sizeof(carrier))==1 && !strcmp(carrier,MAT_PLAYBACK_CARRIER) && + MatSdpValue(sdp,"x-ss-audio.mat-live-clock",clock,sizeof(clock))==1 && !strcmp(clock,MAT_PLAYBACK_CLOCK) && + MatSdpValue(sdp,"x-ss-audio.mat-live-crypto",crypto,sizeof(crypto))==1 && !strcmp(crypto,MAT_LIVE_CRYPTO) && + MatSdpValue(sdp,"x-ss-audio.mat-live-epoch",epoch,sizeof(epoch))==1 && MatLiveDecimal(epoch,&out->epoch) && + MatSdpValue(sdp,"x-ss-audio.mat-live-domain",domain,sizeof(domain))==1 && MatLiveDecimal(domain,&out->domain); +} +static inline int MatLiveProfileConfirm(const LI_MAT_LIVE_PROFILE* expected,const char* codec, + const char* carrier,const char* clock,const char* crypto,const char* epoch,const char* domain) { + uint64_t e=0,d=0; + return expected && codec && !strcmp(codec,MAT_LIVE_NAME) && + carrier && !strcmp(carrier,MAT_PLAYBACK_CARRIER) && clock && !strcmp(clock,MAT_PLAYBACK_CLOCK) && + crypto && !strcmp(crypto,MAT_LIVE_CRYPTO) && MatLiveDecimal(epoch,&e) && MatLiveDecimal(domain,&d) && + e==expected->epoch && d==expected->domain; +} diff --git a/src/RtspConnection.c b/src/RtspConnection.c index da0fc1fd6..f333a39fb 100644 --- a/src/RtspConnection.c +++ b/src/RtspConnection.c @@ -580,7 +580,8 @@ static bool requestDescribe(PRTSP_MESSAGE response, int* error) { if (addOption(&request, "Accept", "application/sdp") && addOption(&request, "If-Modified-Since", - "Thu, 01 Jan 1970 00:00:00 GMT")) { + "Thu, 01 Jan 1970 00:00:00 GMT") && + (NegotiatedAudioCodec != AUDIO_CODEC_MAT_LIVE || addOption(&request,"X-ML-Mat-Live","1"))) { ret = transactRtspMessage(&request, response, true, error); } else { @@ -1279,6 +1280,15 @@ int performRtspHandshake(PSERVER_INFORMATION serverInfo) { Limelog("Host lacks valid MAT dump v1 capability/manifest; refusing implicit fallback\n"); ret = -1; freeMessage(&response); goto Exit; } + if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_LIVE && !MatLiveProfileParse(&MatLiveProfile,response.payload)) { + memset(&MatLiveProfile,0,sizeof(MatLiveProfile)); + if(StreamConfig.audioCodec!=AUDIO_CODEC_MAT_AUTO) { + Limelog("Host has no compatible live MAT source/profile\n"); + ret=-1;freeMessage(&response);goto Exit; + } + NegotiatedAudioCodec=AUDIO_CODEC_OPUS;NegotiatedAudioBitrate=0; + Limelog("Live MAT source unavailable; selecting Opus before ANNOUNCE (no replay fallback)\n"); + } if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_IEC61937) { int selected = MatPlaybackProfileSelect(&MatDumpProfile,response.payload, StreamConfig.audioCodec == AUDIO_CODEC_MAT_AUTO); @@ -1718,11 +1728,22 @@ int performRtspHandshake(PSERVER_INFORMATION serverInfo) { getOptionContent(response.options,"X-SS-Mat-SHA256") || getOptionContent(response.options,"X-SS-Mat-Cycles") || getOptionContent(response.options,"X-SS-Mat-Carrier") || - getOptionContent(response.options,"X-SS-Mat-Clock")) { + getOptionContent(response.options,"X-SS-Mat-Clock") || + getOptionContent(response.options,"X-SS-Mat-Live-Epoch") || + getOptionContent(response.options,"X-SS-Mat-Live-Domain") || + getOptionContent(response.options,"X-SS-Mat-Live-Crypto")) { Limelog("Unexpected codec/MAT confirmation after selecting Opus; refusing media\n"); ret = -1; freeMessage(&response); goto Exit; } } + if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_LIVE && !MatLiveProfileConfirm(&MatLiveProfile, + uniqueMatOption(response.options,"X-SS-Audio-Codec"), + uniqueMatOption(response.options,"X-SS-Mat-Carrier"),uniqueMatOption(response.options,"X-SS-Mat-Clock"), + uniqueMatOption(response.options,"X-SS-Mat-Live-Crypto"),uniqueMatOption(response.options,"X-SS-Mat-Live-Epoch"), + uniqueMatOption(response.options,"X-SS-Mat-Live-Domain"))) { + Limelog("Live MAT ANNOUNCE confirmation missing or mismatched\n"); + ret=-1;freeMessage(&response);goto Exit; + } if (!parseDynamicHdrNegotiation(&response)) { ret = 415; freeMessage(&response); diff --git a/src/SdpGenerator.c b/src/SdpGenerator.c index 90ffc3d39..b1e7a5863 100644 --- a/src/SdpGenerator.c +++ b/src/SdpGenerator.c @@ -589,19 +589,32 @@ static PSDP_OPTION getAttributesList(char*urlSafeAddr) { (NegotiatedAudioCodec == AUDIO_CODEC_EAC3) ? "eac3" : (NegotiatedAudioCodec == AUDIO_CODEC_PCM_S16) ? "pcm" : (NegotiatedAudioCodec == AUDIO_CODEC_MAT_DUMP) ? MAT_DUMP_NAME : - (NegotiatedAudioCodec == AUDIO_CODEC_MAT_IEC61937) ? MAT_PLAYBACK_NAME : "opus"; + (NegotiatedAudioCodec == AUDIO_CODEC_MAT_IEC61937) ? MAT_PLAYBACK_NAME : + (NegotiatedAudioCodec == AUDIO_CODEC_MAT_LIVE) ? MAT_LIVE_NAME : "opus"; err |= addAttributeString(&optionHead, "x-ml-audio.codec", codecStr); if (AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec)) { - if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_DUMP) + if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_LIVE) { + err |= addAttributeString(&optionHead,"x-ml-audio.mat-live","1"); + err |= addAttributeString(&optionHead,"x-ml-audio.mat-live-carrier",MAT_PLAYBACK_CARRIER); + err |= addAttributeString(&optionHead,"x-ml-audio.mat-live-clock",MAT_PLAYBACK_CLOCK); + err |= addAttributeString(&optionHead,"x-ml-audio.mat-live-crypto",MAT_LIVE_CRYPTO); + snprintf(payloadStr,sizeof(payloadStr),"%llu",(unsigned long long)MatLiveProfile.epoch); + err |= addAttributeString(&optionHead,"x-ml-audio.mat-live-epoch",payloadStr); + snprintf(payloadStr,sizeof(payloadStr),"%llu",(unsigned long long)MatLiveProfile.domain); + err |= addAttributeString(&optionHead,"x-ml-audio.mat-live-domain",payloadStr); + } + else if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_DUMP) err |= addAttributeString(&optionHead,"x-ml-audio.mat-dump","1"); else { err |= addAttributeString(&optionHead,"x-ml-audio.mat-playback","1"); err |= addAttributeString(&optionHead,"x-ml-audio.mat-carrier",MAT_PLAYBACK_CARRIER); err |= addAttributeString(&optionHead,"x-ml-audio.mat-clock",MAT_PLAYBACK_CLOCK); } - err |= addAttributeString(&optionHead,"x-ml-audio.mat-sha256",MatDumpProfile.sha256); - snprintf(payloadStr,sizeof(payloadStr),"%u",MatDumpProfile.cycles); - err |= addAttributeString(&optionHead,"x-ml-audio.mat-cycles",payloadStr); + if(NegotiatedAudioCodec!=AUDIO_CODEC_MAT_LIVE) { + err |= addAttributeString(&optionHead,"x-ml-audio.mat-sha256",MatDumpProfile.sha256); + snprintf(payloadStr,sizeof(payloadStr),"%u",MatDumpProfile.cycles); + err |= addAttributeString(&optionHead,"x-ml-audio.mat-cycles",payloadStr); + } } if (NegotiatedAudioBitrate > 0) { snprintf(payloadStr, sizeof(payloadStr), "%d", NegotiatedAudioBitrate); diff --git a/tests/AudioCodecSelectionTest.c b/tests/AudioCodecSelectionTest.c index 609b7e6c0..da4eafd7a 100644 --- a/tests/AudioCodecSelectionTest.c +++ b/tests/AudioCodecSelectionTest.c @@ -15,8 +15,8 @@ static int stopBeforeHardware(int configuration,const POPUS_MULTISTREAM_CONFIGUR } int main(void) { CHECK(initializePlatform()==0); - const int requests[]={AUDIO_CODEC_MAT_AUTO,AUDIO_CODEC_MAT_AUTO,AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_OPUS}; - const int selections[]={AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_OPUS}; + const int requests[]={AUDIO_CODEC_MAT_AUTO,AUDIO_CODEC_MAT_AUTO,AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_LIVE}; + const int selections[]={AUDIO_CODEC_MAT_LIVE,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_LIVE}; for(unsigned i=0;i +static int failures,cycles,ends,terminated; +#define CHECK(x) do{if(!(x)){fprintf(stderr,"line %d: %s\n",__LINE__,#x);++failures;}}while(0) +static int cycle(void* c,const uint8_t* bytes,int n,uint64_t e,uint64_t d,uint64_t index,uint64_t pts){(void)c;(void)bytes;CHECK(n==61440 && e==123 && d==7 && index==(unsigned)cycles && pts==index*960);++cycles;return 1;} +static int end(void* c,uint64_t n){(void)c;CHECK(n==(unsigned)cycles);++ends;return 1;} +static void terminate(int error){(void)error;++terminated;} +static void packet(uint64_t sequence,const uint8_t* payload,size_t size){ + PQUEUED_AUDIO_PACKET p=calloc(1,sizeof(*p));if(!p){++failures;return;} + p->header.size=sizeof(RTP_PACKET)+(int)size;PRTP_PACKET r=(PRTP_PACKET)p->data; + r->header=0x80;r->packetType=97;r->sequenceNumber=(uint16_t)sequence;r->timestamp=(uint32_t)sequence; + memcpy(r+1,payload,size);int status=RtpaAddPacket(&rtpAudioQueue,r,(uint16_t)p->header.size); + if(RTPQ_HANDLE_NOW(status))decodeInputData(p); + else { + uint16_t length;PQUEUED_AUDIO_PACKET queued; + while((queued=(PQUEUED_AUDIO_PACKET)RtpaGetQueuedPacket(&rtpAudioQueue,sizeof(QUEUED_AUDIO_PACKET_HEADER),&length))!=NULL){queued->header.size=length;decodeInputData(queued);free(queued);} + } + free(p); +} +int main(void){ + CHECK(initializePlatform()==0);AppVersionQuad[0]=7;AppVersionQuad[1]=1;AppVersionQuad[2]=431;AppVersionQuad[3]=-1; + NegotiatedAudioCodec=AUDIO_CODEC_MAT_LIVE;AudioEncryptionEnabled=true;AudioPacketDuration=1;ListenerCallbacks.connectionTerminated=terminate; + RtpaInitializeQueue(&rtpAudioQueue);lastSeq=3; + LI_MAT_LIVE_PROFILE profile={123,7};uint8_t key[16]={};LI_MAT_PLAYBACK_CALLBACKS cb={0};cb.cycle=cycle;cb.end=end; + matLive=MatLivePlaybackOpen(&profile,key,&cb);CHECK(matLive); + MAT_LIVE_CIPHER cipher;CHECK(MatLiveCipherInit(&cipher,key,16,123,7)); + uint8_t plain[MAT_WIRE_PACKET],wire[MAT_LIVE_PACKET],carrier[MAT_WIRE_CYCLE]={0}; + memcpy(carrier,"\x72\xf8\x1f\x4e\x16\x00\x02\x00",8); + mat_wire_make(plain,MAT_WIRE_SYNC,123,7,0,0,0,NULL);uint64_t next=0; + for(unsigned i=0;i<4;i++){CHECK(MatLiveSeal(&cipher,next,plain,wire));packet(next++,wire,sizeof(wire));} + uint32_t hash=2166136261u; + for(unsigned i=0;i<1300;i++){ + carrier[8]=(uint8_t)i;uint32_t h=mat_hash(carrier,sizeof(carrier)); + for(unsigned j=0;j<60;j++){mat_wire_make(plain,MAT_WIRE_DATA,123,7,i,j,h,carrier);CHECK(MatLiveSeal(&cipher,next,plain,wire));packet(next++,wire,sizeof(wire));} + hash=mat_hash_update(hash,carrier,sizeof(carrier)); + } + mat_wire_make(plain,MAT_WIRE_END,123,7,1300,0,hash,NULL); + for(unsigned i=0;i<4;i++){CHECK(MatLiveSeal(&cipher,next,plain,wire));packet(next++,wire,sizeof(wire));} + CHECK(cycles==1300 && ends==1 && !terminated && lastSeq==(uint16_t)(next-1)); + MatLivePlaybackClose(matLive);matLive=NULL;RtpaCleanupQueue(&rtpAudioQueue);cleanupPlatform(); + if(failures)return 1;puts("PASS actual RTP queue / AudioStream: 78008 packets, RTP16 wrap, legacy CBC bypass, exact authenticated cycles and EOS");return 0; +} diff --git a/tests/MatLiveTest.c b/tests/MatLiveTest.c new file mode 100644 index 000000000..c9f79bce1 --- /dev/null +++ b/tests/MatLiveTest.c @@ -0,0 +1,47 @@ +/* Synthetic carrier bytes exercise transport only. No recorded MAT input or Dolby decode claim. */ +#include "MatLivePlayback.h" +#include "MatLiveCrypto.h" +#include +#include +static int failures,cycles,ends,reject; +#define CHECK(x) do{if(!(x)){fprintf(stderr,"line %d: %s\n",__LINE__,#x);++failures;}}while(0) +static int cycle(void* c,const uint8_t* b,int n,uint64_t e,uint64_t d,uint64_t i,uint64_t pts){ + (void)c;CHECK(n==61440 && b[0]==0x72 && e==123 && d==7 && i==(unsigned)cycles && pts==i*960);++cycles;return !reject; +} +static int end(void* c,uint64_t count){(void)c;CHECK(count==(unsigned)cycles);++ends;return !reject;} +static uint8_t key[16],carrier[MAT_WIRE_CYCLE],plain[MAT_WIRE_PACKET],wire[MAT_LIVE_PACKET]; +static LI_MAT_LIVE_PROFILE profile={123,7}; +static MAT_LIVE_CIPHER cipher; +static MAT_LIVE_PLAYBACK* open_receiver(void){LI_MAT_PLAYBACK_CALLBACKS cb={0};cb.cycle=cycle;cb.end=end;cycles=ends=reject=0;return MatLivePlaybackOpen(&profile,key,&cb);} +static int push(MAT_LIVE_PLAYBACK* receiver,uint64_t sequence){CHECK(MatLiveSeal(&cipher,sequence,plain,wire));return MatLivePlaybackPush(receiver,wire,sizeof(wire));} +int main(int argc,char** argv){ + for(unsigned i=0;i<16;i++)key[i]=(uint8_t)i; + CHECK(MatLiveCipherInit(&cipher,key,16,123,7)); + if(argc==2){for(unsigned i=0;i>8);uint32_t h=mat_hash(carrier,sizeof(carrier)); + for(unsigned j=0;j<60;j++){mat_wire_make(plain,MAT_WIRE_DATA,123,7,i,j,h,carrier);CHECK(push(receiver,packet++)==(j==59?1:0));} + hash=mat_hash_update(hash,carrier,sizeof(carrier)); + } + CHECK(packet==78004 && cycles==1300 && !ends); + mat_wire_make(plain,MAT_WIRE_END,123,7,1300,0,hash,NULL); + for(unsigned j=0;j<4;j++)CHECK(push(receiver,packet++)==2); + CHECK(ends==1);MatLivePlaybackClose(receiver); + // Missing EOS/close must never drain the renderer as a successful stream. + receiver=open_receiver();mat_wire_make(plain,MAT_WIRE_DATA,123,7,0,0,mat_hash(carrier,sizeof(carrier)),carrier);CHECK(push(receiver,4)==0);MatLivePlaybackClose(receiver);CHECK(!ends); + // Replay, gaps, ciphertext/tag/header corruption, wrong key/domain and sticky failure. + for(unsigned test=0;test<8;test++){ + receiver=open_receiver();mat_wire_make(plain,MAT_WIRE_DATA,123,7,0,0,mat_hash(carrier,sizeof(carrier)),carrier); + if(test==5){MAT_LIVE_CIPHER wrong=cipher;wrong.key[0]^=1;CHECK(MatLiveSeal(&wrong,4,plain,wire));} + else if(test==6){mat_put64(plain+24,8);mat_put32(plain+72,mat_packet_hash(plain));CHECK(MatLiveSeal(&cipher,4,plain,wire));} + else CHECK(MatLiveSeal(&cipher,test==0?3:test==1?5:4,plain,wire)); + if(test==2)wire[32]^=1;if(test==3)wire[sizeof(wire)-1]^=1;if(test==4)wire[8]^=1; + if(test==7){CHECK(MatLivePlaybackPush(receiver,wire,sizeof(wire))==0);} + CHECK(MatLivePlaybackPush(receiver,wire,sizeof(wire))<0);CHECK(MatLivePlaybackPush(receiver,wire,sizeof(wire))<0);CHECK(!cycles&&!ends);MatLivePlaybackClose(receiver); + } + receiver=open_receiver();reject=1;for(unsigned j=0;j<60;j++){mat_wire_make(plain,MAT_WIRE_DATA,123,7,0,j,mat_hash(carrier,sizeof(carrier)),carrier);CHECK(push(receiver,4+j)==(j==59?-1:0));}CHECK(cycles==1&&!ends);MatLivePlaybackClose(receiver); + uint64_t number;CHECK(!MatLiveDecimal("01",&number));CHECK(!MatLiveDecimal("0",&number));CHECK(!MatLiveDecimal("9223372036854775808",&number));CHECK(MatLiveDecimal("9223372036854775807",&number)); + if(failures)return 1;puts("PASS live MAT: 78000 authenticated data packets, RTP16 wrap, integrity/replay/gap/identity/EOF/callback failures");return 0; +} From 28193765d9c3d0c17ff8b35fa1e01dfe03ab9d7b Mon Sep 17 00:00:00 2001 From: qiin2333 <414382190@qq.com> Date: Thu, 22 Oct 2026 21:16:43 +0800 Subject: [PATCH 07/11] test(audio): keep live MAT receiver tests independent of DLL exports --- CMakeLists.txt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b0ad0aff..e5fa5d743 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,11 +121,16 @@ target_include_directories(moonlight-common-c SYSTEM PRIVATE target_compile_definitions(moonlight-common-c PRIVATE HAS_SOCKLEN_T) if (BUILD_TESTING) - add_executable(mat-live-tests tests/MatLiveTest.c) + add_executable(mat-live-tests tests/MatLiveTest.c src/MatLivePlayback.c src/MatReceiver.c src/MatDepacketizer.c) target_include_directories(mat-live-tests PRIVATE src) - target_link_libraries(mat-live-tests PRIVATE moonlight-common-c) if(USE_MBEDTLS) target_compile_definitions(mat-live-tests PRIVATE USE_MBEDTLS) + target_include_directories(mat-live-tests SYSTEM PRIVATE ${MBEDTLS_INCLUDE_DIRS}) + if(MBEDTLS_FOUND) + target_link_libraries(mat-live-tests PRIVATE ${MBEDCRYPTO_LIBRARY}) + else() + target_link_libraries(mat-live-tests PRIVATE mbedcrypto) + endif() else() target_include_directories(mat-live-tests SYSTEM PRIVATE ${OPENSSL_INCLUDE_DIR}) target_link_libraries(mat-live-tests PRIVATE ${OPENSSL_CRYPTO_LIBRARY}) From 8a51e978a4e03c12145200df659d74fd8183b8f2 Mon Sep 17 00:00:00 2001 From: qiin2333 <414382190@qq.com> Date: Thu, 22 Oct 2026 21:23:58 +0800 Subject: [PATCH 08/11] fix(audio): reject finite metadata in live MAT confirmation --- src/RtspConnection.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/RtspConnection.c b/src/RtspConnection.c index f333a39fb..a3f4b6fce 100644 --- a/src/RtspConnection.c +++ b/src/RtspConnection.c @@ -1736,11 +1736,13 @@ int performRtspHandshake(PSERVER_INFORMATION serverInfo) { ret = -1; freeMessage(&response); goto Exit; } } - if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_LIVE && !MatLiveProfileConfirm(&MatLiveProfile, + if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_LIVE && + (getOptionContent(response.options,"X-SS-Mat-SHA256") || getOptionContent(response.options,"X-SS-Mat-Cycles") || + !MatLiveProfileConfirm(&MatLiveProfile, uniqueMatOption(response.options,"X-SS-Audio-Codec"), uniqueMatOption(response.options,"X-SS-Mat-Carrier"),uniqueMatOption(response.options,"X-SS-Mat-Clock"), uniqueMatOption(response.options,"X-SS-Mat-Live-Crypto"),uniqueMatOption(response.options,"X-SS-Mat-Live-Epoch"), - uniqueMatOption(response.options,"X-SS-Mat-Live-Domain"))) { + uniqueMatOption(response.options,"X-SS-Mat-Live-Domain")))) { Limelog("Live MAT ANNOUNCE confirmation missing or mismatched\n"); ret=-1;freeMessage(&response);goto Exit; } From c6b340440a14997145438d2a9f2059852c257b69 Mon Sep 17 00:00:00 2001 From: qiin2333 <414382190@qq.com> Date: Fri, 23 Oct 2026 10:16:06 +0800 Subject: [PATCH 09/11] refactor(audio): keep MAT file and offline input policy outside common transport --- docs/MAT-HANDOFF.md | 34 ++++++++- src/AudioStream.c | 25 ++----- src/Connection.c | 17 ++--- src/Limelight.h | 9 ++- src/MatDumpFile.c | 120 -------------------------------- src/MatDumpFile.h | 15 ---- src/MatDumpProfile.h | 17 +---- src/MatLiveProfile.h | 9 ++- src/MatPlaybackProfile.h | 4 +- src/MatProfile.h | 19 +++++ src/MatReceiver.c | 22 +----- src/MatReceiver.h | 5 +- tests/AudioCodecSelectionTest.c | 42 +++++++++-- 13 files changed, 115 insertions(+), 223 deletions(-) delete mode 100644 src/MatDumpFile.c delete mode 100644 src/MatDumpFile.h create mode 100644 src/MatProfile.h diff --git a/docs/MAT-HANDOFF.md b/docs/MAT-HANDOFF.md index c4654655f..e06fc5254 100644 --- a/docs/MAT-HANDOFF.md +++ b/docs/MAT-HANDOFF.md @@ -1,4 +1,36 @@ -# Experimental finite MAT transport handoff — 2026-10-22 +# MAT transport boundary — 2026-10-23 + +This PR targets **mic**. It contains network negotiation, authenticated live +transport, bounded depacketization and synchronous cycle/EOS callbacks. It does +not own files, offline recording input, native capture, encoding or hardware. + +- Codec 7 / `mat-live-v1` uses HKDF-SHA256, AES-128-GCM with authenticated headers + and a 64-bit packet counter. Network failure is terminal; it cannot synthesize + a successful EOS. Normal MAT preference selects live7 or Opus0, never replay. +- `MatProfile.h` contains shared SDP parsing and carrier/clock constants; the + live profile no longer depends on finite dump/playback profile headers. +- Explicit finite codec4/5 profiles remain protocol features. Both validate + count/SHA and deliver complete cycles/EOS through `LiSetMatPlaybackCallbacks`. + Codec4 is diagnostic data, never device playback. Consumers own any storage + policy, publication or bounded hardware drain; errors terminate the stream. +- `MatDumpFile.c/.h`, `STREAM_CONFIGURATION.matDumpPath` and + `MatReceiverPushRecordingCycle`/`MatReceiverEndRecording` are removed. The + Sunshine parent contains the migrated diagnostic writer, independent raw-cycle + adapter and mock replay tool under `spatial-object-spike/mat-diagnostics`. +- Rebuild consumers: the unpublished configuration ABI changed, and diagnostic + codec4 now requires registered callbacks. The historical Android PR stays on + its old shared revision; new acceptance uses mock frontends against this PR. + +Static CTest has 14 tests, including production AudioStream codec4/5 callback +delivery, SHA/count rejection and consumer failure. Parent software acceptance +also covers the independent crypto oracle, RTSP negotiation, UDP/FEC, RTP wrap, +native-source contracts and migrated diagnostic failure cases. Generated test +bytes do not establish native capture or hardware/Atmos playback acceptance. + +## Historical finite handoff — 2026-10-22 + +The following records the earlier API and acceptance only; it does not describe +the current file ownership, recording input or live transport boundary above. This branch targets **mic**, the branch consumed by both parent repositories, not default master. Baseline: b91c7cd72764320edd6140a16f8614aeb63dc42a. diff --git a/src/AudioStream.c b/src/AudioStream.c index cf43cb15f..2ad74095a 100644 --- a/src/AudioStream.c +++ b/src/AudioStream.c @@ -1,5 +1,4 @@ #include "Limelight-internal.h" -#include "MatDumpFile.h" #include "MatPlayback.h" #include "MatLivePlayback.h" #include @@ -18,9 +17,7 @@ static int liveEnd(void* context,uint64_t cycles) { atomic_store_explicit(&liveInputEnded,true,memory_order_relaxed); return matCallbacks.end(context,cycles); } -static MAT_DUMP_FILE* matDump; -static void failMatInput(void) { MatDumpFail(matDump); MatPlaybackFail(matPlayback);MatLivePlaybackFail(matLive); } -static bool matDumpFailedReported,matDumpPublishedReported; +static void failMatInput(void) { MatPlaybackFail(matPlayback);MatLivePlaybackFail(matLive); } static void consumeAudio(char* data,int size) { if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_LIVE) { int result=MatLivePlaybackPush(matLive,(unsigned char*)data,(size_t)(size>0?size:0)); @@ -29,16 +26,13 @@ static void consumeAudio(char* data,int size) { else if(result==2&&!matPlaybackCompleteReported){matPlaybackCompleteReported=true;Limelog("Live MAT authenticated EOS and renderer drain completed\n");} return; } - if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_IEC61937){ + if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_IEC61937 || NegotiatedAudioCodec==AUDIO_CODEC_MAT_DUMP){ int result=MatPlaybackPush(matPlayback,(unsigned char*)data,(size_t)(size>0?size:0)); - if(result<0 && !matPlaybackFailedReported){matPlaybackFailedReported=true;Limelog("MAT IEC playback failed\n");ListenerCallbacks.connectionTerminated(-1);} - else if(result==2 && !matPlaybackCompleteReported){matPlaybackCompleteReported=true;Limelog("MAT IEC playback SHA/count verified and renderer drained: %s\n",MatDumpProfile.sha256);} + if(result<0 && !matPlaybackFailedReported){matPlaybackFailedReported=true;Limelog("Finite MAT receive/consumer failed\n");ListenerCallbacks.connectionTerminated(-1);} + else if(result==2 && !matPlaybackCompleteReported){matPlaybackCompleteReported=true;Limelog("Finite MAT SHA/count verified and consumer completed: %s\n",MatDumpProfile.sha256);} return; } - if(!AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec)){AudioCallbacks.decodeAndPlaySample(data,size);return;} - int result=MatDumpPush(matDump,(unsigned char*)data,(size_t)(size>0?size:0)); - if(result<0 && !matDumpFailedReported){matDumpFailedReported=true;Limelog("MAT dump failed: preserving partial file\n");ListenerCallbacks.connectionTerminated(-1);} - else if(result==2 && !matDumpPublishedReported){matDumpPublishedReported=true;Limelog("MAT dump SHA256 verified and published: %s (%s)\n",StreamConfig.matDumpPath,MatDumpProfile.sha256);} + AudioCallbacks.decodeAndPlaySample(data,size); } static SOCKET rtpSocket = INVALID_SOCKET; @@ -118,7 +112,6 @@ int initializeAudioStream(void) { LbqInitializeLinkedBlockingQueue(&packetQueue, 30); RtpaInitializeQueue(&rtpAudioQueue); lastSeq = 0; - matDumpFailedReported=matDumpPublishedReported=false; matPlaybackFailedReported=matPlaybackCompleteReported=false; receivedDataFromPeer = false; pingThreadStarted = false; @@ -180,7 +173,6 @@ static void freePacketList(PLINKED_BLOCKING_QUEUE_ENTRY entry) { // Tear down the audio stream once we're done with it void destroyAudioStream(void) { MatLivePlaybackClose(matLive);matLive=NULL; - MatDumpClose(matDump);matDump=NULL; MatPlaybackClose(matPlayback);matPlayback=NULL; if (rtpSocket != INVALID_SOCKET) { if (pingThreadStarted) { @@ -486,7 +478,6 @@ void stopAudioStream(void) { PltJoinThread(&decoderThread); } - MatDumpClose(matDump);matDump=NULL; MatPlaybackClose(matPlayback);matPlayback=NULL; MatLivePlaybackClose(matLive);matLive=NULL; AudioCallbacks.cleanup(); @@ -519,11 +510,7 @@ int startAudioStream(void* audioContext, int arFlags) { return err; } - if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_DUMP){ - matDump=MatDumpOpen(StreamConfig.matDumpPath,&MatDumpProfile); - if(!matDump){AudioCallbacks.cleanup();return -1;} - } - if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_IEC61937){ + if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_IEC61937 || NegotiatedAudioCodec==AUDIO_CODEC_MAT_DUMP){ matPlayback=MatPlaybackOpen(&MatDumpProfile,&matCallbacks); if(!matPlayback){AudioCallbacks.cleanup();return -1;} } diff --git a/src/Connection.c b/src/Connection.c index 6c624dc72..e91ed19b4 100644 --- a/src/Connection.c +++ b/src/Connection.c @@ -360,10 +360,6 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre } break; case AUDIO_CODEC_MAT_AUTO: - if (StreamConfig.matDumpPath[0]) { - Limelog("MAT preference cannot use a dump path\n"); - err = -1; goto Cleanup; - } if (!IS_SUNSHINE() || StreamConfig.controlOnly) { Limelog("MAT preference unavailable for this session; selecting Opus\n"); break; @@ -371,23 +367,22 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre NegotiatedAudioCodec = AUDIO_CODEC_MAT_LIVE; break; case AUDIO_CODEC_MAT_LIVE: - if (!IS_SUNSHINE() || StreamConfig.controlOnly || StreamConfig.matDumpPath[0]) { - Limelog("Live MAT requires a Sunshine audio session and no dump path\n"); + if (!IS_SUNSHINE() || StreamConfig.controlOnly) { + Limelog("Live MAT requires a Sunshine audio session\n"); err = -1; goto Cleanup; } NegotiatedAudioCodec = AUDIO_CODEC_MAT_LIVE; break; case AUDIO_CODEC_MAT_IEC61937: - if (!IS_SUNSHINE() || StreamConfig.controlOnly || StreamConfig.matDumpPath[0]) { - Limelog("MAT playback requires Sunshine audio session and no dump path\n"); + if (!IS_SUNSHINE() || StreamConfig.controlOnly) { + Limelog("MAT playback requires a Sunshine audio session\n"); err = -1; goto Cleanup; } NegotiatedAudioCodec = AUDIO_CODEC_MAT_IEC61937; break; case AUDIO_CODEC_MAT_DUMP: - if (!IS_SUNSHINE() || StreamConfig.controlOnly || !StreamConfig.matDumpPath[0] || - !memchr(StreamConfig.matDumpPath,0,sizeof(StreamConfig.matDumpPath))) { - Limelog("MAT dump requires Sunshine, audio session and app-owned output path\n"); + if (!IS_SUNSHINE() || StreamConfig.controlOnly) { + Limelog("MAT dump requires a Sunshine audio session and registered consumer\n"); err = -1; goto Cleanup; } NegotiatedAudioCodec = AUDIO_CODEC_MAT_DUMP; diff --git a/src/Limelight.h b/src/Limelight.h index 5145e2668..aaca35082 100644 --- a/src/Limelight.h +++ b/src/Limelight.h @@ -157,7 +157,6 @@ typedef struct _STREAM_CONFIGURATION { // RS FEC preference: -2=host default, -1=automatic, 0..100=fixed percentage. // Unsupported hosts ignore this preference. PyroWave uses its own protection. int videoFecPercentage; - char matDumpPath[1024]; // App-owned unique dump-only output, copied with config. } STREAM_CONFIGURATION, *PSTREAM_CONFIGURATION; // Initialize the stream configuration, including compatible host-default FEC. @@ -309,10 +308,10 @@ typedef struct _DECODE_UNIT { #define AUDIO_CODEC_MAT_LIVE 7 #define AUDIO_CODEC_IS_MAT(c) ((c)==AUDIO_CODEC_MAT_DUMP || (c)==AUDIO_CODEC_MAT_IEC61937 || (c)==AUDIO_CODEC_MAT_LIVE) -/* Registered from audio init, cleared after audio threads join. Never called for - * codec4. Return 0 to fail the loss-intolerant session. End is verified EOS - * (finite SHA/count for 5, authenticated END for 7); the renderer must drain - * before returning success. No PCM/encoded callback. */ +/* Registered from audio init, cleared after audio threads join. Codec4 uses a + * diagnostic consumer, never a renderer. Return 0 to fail the loss-intolerant session. End is verified EOS + * (finite SHA/count for 4/5, authenticated END for 7); consumers own any file + * publication or bounded hardware drain before returning success. No PCM/encoded callback. */ typedef struct { int (*cycle)(void*, const unsigned char*, int, uint64_t, uint64_t, uint64_t, uint64_t); int (*end)(void*, uint64_t); diff --git a/src/MatDumpFile.c b/src/MatDumpFile.c deleted file mode 100644 index b3576890c..000000000 --- a/src/MatDumpFile.c +++ /dev/null @@ -1,120 +0,0 @@ -#include "MatDumpFile.h" -#include "MatReceiver.h" -#include -#include -#include -#include -#ifdef _WIN32 -#include -#define mat_open _open -#define mat_fdopen _fdopen -#define mat_close _close -#else -#include -#define mat_open open -#define mat_fdopen fdopen -#define mat_close close -#ifndef O_BINARY -#define O_BINARY 0 -#endif -#endif -#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) -#include -#else -#include -#endif - -struct MAT_DUMP_FILE { - MAT_RECEIVER receiver; - LI_MAT_DUMP_PROFILE expected; - FILE* file; - char final_path[1024],partial_path[1040],status_path[1040],sha256[65]; - int failed,published,created; -#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) - psa_hash_operation_t digest; -#else - EVP_MD_CTX* digest; -#endif -}; -static FILE* exclusive(const char* path) { - int fd=mat_open(path,O_WRONLY|O_CREAT|O_EXCL|O_BINARY,0600); - if(fd<0)return NULL; - FILE* f=mat_fdopen(fd,"wb"); - if(!f)mat_close(fd); - return f; -} -static int write_cycle(void* context,const MAT_CYCLE_INFO* info,const uint8_t* p,size_t n) { - MAT_DUMP_FILE* s=context; - if(info->index>=s->expected.cycles || !s->file || fwrite(p,1,n,s->file)!=n)return 0; -#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) - return psa_hash_update(&s->digest,p,n)==PSA_SUCCESS; -#else - return EVP_DigestUpdate(s->digest,p,n)==1; -#endif -} -static void status(MAT_DUMP_FILE* s) { - FILE* f=exclusive(s->status_path);if(!f)return; - fprintf(f,"{\"published\":%s,\"completeEOS\":%s,\"failed\":%s,\"cycles\":%llu,\"expectedCycles\":%u,\"sha256\":\"%s\",\"expectedSha256\":\"%s\"}\n", - s->published?"true":"false",MatReceiverFinish(&s->receiver)?"true":"false",s->failed?"true":"false", - (unsigned long long)s->receiver.depacketizer.next_frame,s->expected.cycles,s->sha256,s->expected.sha256);fclose(f); -} -MAT_DUMP_FILE* MatDumpOpen(const char* path,const LI_MAT_DUMP_PROFILE* expected) { - if(!path || !expected || !*path || strlen(path)>=1024)return NULL; - char count[16];snprintf(count,sizeof(count),"%u",expected->cycles);LI_MAT_DUMP_PROFILE checked; - if(!MatProfileSet(&checked,expected->sha256,count))return NULL; - MAT_DUMP_FILE* s=calloc(1,sizeof(*s));if(!s)return NULL;s->expected=checked; - memcpy(s->final_path,path,strlen(path)+1);snprintf(s->partial_path,sizeof(s->partial_path),"%s.partial",path); - snprintf(s->status_path,sizeof(s->status_path),"%s.json",path); - struct stat st;if(stat(path,&st)==0 || stat(s->status_path,&st)==0){free(s);return NULL;} -#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) - s->digest=psa_hash_operation_init(); - if(psa_crypto_init()!=PSA_SUCCESS || psa_hash_setup(&s->digest,PSA_ALG_SHA_256)!=PSA_SUCCESS){free(s);return NULL;} -#else - s->digest=EVP_MD_CTX_new(); - if(!s->digest || EVP_DigestInit_ex(s->digest,EVP_sha256(),NULL)!=1){EVP_MD_CTX_free(s->digest);free(s);return NULL;} -#endif - s->file=exclusive(s->partial_path); - if(!s->file){MatDumpClose(s);return NULL;} - s->created=1;MatReceiverInit(&s->receiver,write_cycle,NULL,s);return s; -} -void MatDumpFail(MAT_DUMP_FILE* s) {if(s)s->failed=1;} -int MatDumpPublished(const MAT_DUMP_FILE* s){return s && s->published;} -int MatDumpPush(MAT_DUMP_FILE* s,const unsigned char* p,size_t n) { - if(!s || s->failed)return -1; - int result=MatReceiverPush(&s->receiver,p,n); - if(result<0){s->failed=1;return -1;} - if(result!=2 || s->published)return result; - unsigned char bytes[32];size_t length=0; -#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) - if(psa_hash_finish(&s->digest,bytes,sizeof(bytes),&length)!=PSA_SUCCESS)s->failed=1; -#else - unsigned out=0;if(EVP_DigestFinal_ex(s->digest,bytes,&out)!=1)s->failed=1;length=out; -#endif - if(length!=32)s->failed=1; - if(!s->failed)for(unsigned i=0;i<32;i++)snprintf(s->sha256+i*2,3,"%02x",bytes[i]); - if(fflush(s->file))s->failed=1; - if(fclose(s->file))s->failed=1; - s->file=NULL; - if(s->receiver.depacketizer.next_frame!=s->expected.cycles || strcmp(s->sha256,s->expected.sha256))s->failed=1; - // link() on POSIX refuses an existing destination, unlike rename(). Android - // app-private files support same-directory links. Never overwrite a dump. - if(!s->failed) { -#ifdef _WIN32 - if(rename(s->partial_path,s->final_path))s->failed=1; -#else - if(link(s->partial_path,s->final_path))s->failed=1;else if(unlink(s->partial_path))s->failed=1; -#endif - } - s->published=!s->failed;status(s);return s->failed?-1:2; -} -void MatDumpClose(MAT_DUMP_FILE* s) { - if(!s)return; - if(s->file){if(fclose(s->file))s->failed=1;s->file=NULL;} - if(!s->published && s->created){s->failed=1;status(s);} -#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) - psa_hash_abort(&s->digest); -#else - EVP_MD_CTX_free(s->digest); -#endif - free(s); -} diff --git a/src/MatDumpFile.h b/src/MatDumpFile.h deleted file mode 100644 index 31c09eb91..000000000 --- a/src/MatDumpFile.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once -#include "MatDumpProfile.h" -#include -#ifdef __cplusplus -extern "C" { -#endif -typedef struct MAT_DUMP_FILE MAT_DUMP_FILE; -MAT_DUMP_FILE* MatDumpOpen(const char* final_path,const LI_MAT_DUMP_PROFILE* expected); -int MatDumpPush(MAT_DUMP_FILE*,const unsigned char*,size_t); // -1 failed, 0 pending, 2 published -void MatDumpFail(MAT_DUMP_FILE*); -int MatDumpPublished(const MAT_DUMP_FILE*); -void MatDumpClose(MAT_DUMP_FILE*); // Missing EOS remains .partial; never overwrites. -#ifdef __cplusplus -} -#endif diff --git a/src/MatDumpProfile.h b/src/MatDumpProfile.h index 5c8bd81d4..8b840337a 100644 --- a/src/MatDumpProfile.h +++ b/src/MatDumpProfile.h @@ -1,23 +1,8 @@ #pragma once -#include -#include -#include +#include "MatProfile.h" #define MAT_DUMP_CODEC 4 #define MAT_DUMP_NAME "mat-dump-v1" typedef struct { char sha256[65]; uint32_t cycles; } LI_MAT_DUMP_PROFILE; -/* Strict bounded SDP attributes: exact name, no duplicates or truncation. */ -static inline int MatSdpValue(const char* sdp,const char* name,char* out,size_t cap) { - int found=0;size_t key=strlen(name);out[0]=0; - for(const char* p=sdp;p && *p;) { - const char* end=strchr(p,'\n');if(!end)end=p+strlen(p); - const char* trimmed=end;if(trimmed>p && trimmed[-1]=='\r')--trimmed; - if((size_t)(trimmed-p)>=key+3 && !memcmp(p,"a=",2) && !memcmp(p+2,name,key) && p[key+2]==':') { - size_t n=(size_t)(trimmed-p)-key-3;if(found || n>=cap)return -1; - memcpy(out,p+key+3,n);out[n]=0;found=1; - } - p=*end?end+1:end; - }return found; -} static inline int MatProfileSet(LI_MAT_DUMP_PROFILE* out,const char* hash,const char* count) { memset(out,0,sizeof(*out)); if(!hash || !count || strlen(hash)!=64 || !*count)return 0; diff --git a/src/MatLiveProfile.h b/src/MatLiveProfile.h index 3219b75ce..de541430d 100644 --- a/src/MatLiveProfile.h +++ b/src/MatLiveProfile.h @@ -1,6 +1,5 @@ #pragma once -#include "MatDumpProfile.h" -#include "MatPlaybackProfile.h" +#include "MatProfile.h" #define MAT_LIVE_CODEC 7 #define MAT_LIVE_NAME "mat-live-v1" #define MAT_LIVE_CRYPTO "aes128-gcm-hkdf-sha256" @@ -19,8 +18,8 @@ static inline int MatLiveProfileParse(LI_MAT_LIVE_PROFILE* out,const char* sdp) memset(out,0,sizeof(*out)); if(!sdp)return 0; return MatSdpValue(sdp,"x-ss-audio.mat-live",version,sizeof(version))==1 && !strcmp(version,"1") && - MatSdpValue(sdp,"x-ss-audio.mat-live-carrier",carrier,sizeof(carrier))==1 && !strcmp(carrier,MAT_PLAYBACK_CARRIER) && - MatSdpValue(sdp,"x-ss-audio.mat-live-clock",clock,sizeof(clock))==1 && !strcmp(clock,MAT_PLAYBACK_CLOCK) && + MatSdpValue(sdp,"x-ss-audio.mat-live-carrier",carrier,sizeof(carrier))==1 && !strcmp(carrier,MAT_CARRIER) && + MatSdpValue(sdp,"x-ss-audio.mat-live-clock",clock,sizeof(clock))==1 && !strcmp(clock,MAT_CLOCK) && MatSdpValue(sdp,"x-ss-audio.mat-live-crypto",crypto,sizeof(crypto))==1 && !strcmp(crypto,MAT_LIVE_CRYPTO) && MatSdpValue(sdp,"x-ss-audio.mat-live-epoch",epoch,sizeof(epoch))==1 && MatLiveDecimal(epoch,&out->epoch) && MatSdpValue(sdp,"x-ss-audio.mat-live-domain",domain,sizeof(domain))==1 && MatLiveDecimal(domain,&out->domain); @@ -29,7 +28,7 @@ static inline int MatLiveProfileConfirm(const LI_MAT_LIVE_PROFILE* expected,cons const char* carrier,const char* clock,const char* crypto,const char* epoch,const char* domain) { uint64_t e=0,d=0; return expected && codec && !strcmp(codec,MAT_LIVE_NAME) && - carrier && !strcmp(carrier,MAT_PLAYBACK_CARRIER) && clock && !strcmp(clock,MAT_PLAYBACK_CLOCK) && + carrier && !strcmp(carrier,MAT_CARRIER) && clock && !strcmp(clock,MAT_CLOCK) && crypto && !strcmp(crypto,MAT_LIVE_CRYPTO) && MatLiveDecimal(epoch,&e) && MatLiveDecimal(domain,&d) && e==expected->epoch && d==expected->domain; } diff --git a/src/MatPlaybackProfile.h b/src/MatPlaybackProfile.h index 3cdf57612..31d291ab5 100644 --- a/src/MatPlaybackProfile.h +++ b/src/MatPlaybackProfile.h @@ -2,8 +2,8 @@ #include "MatDumpProfile.h" #define MAT_PLAYBACK_CODEC 5 #define MAT_PLAYBACK_NAME "mat-iec61937-v1" -#define MAT_PLAYBACK_CARRIER "iec61937-192000-8-s16le" -#define MAT_PLAYBACK_CLOCK "48000" +#define MAT_PLAYBACK_CARRIER MAT_CARRIER +#define MAT_PLAYBACK_CLOCK MAT_CLOCK /* Finite replay v1 only. No claim of an infinite/live capture source. */ static inline int MatPlaybackProfileParse(LI_MAT_DUMP_PROFILE* out,const char* sdp) { char version[8],carrier[48],clock[16],hash[65],count[16]; diff --git a/src/MatProfile.h b/src/MatProfile.h new file mode 100644 index 000000000..0769e3480 --- /dev/null +++ b/src/MatProfile.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include +#include +#define MAT_CARRIER "iec61937-192000-8-s16le" +#define MAT_CLOCK "48000" +/* Exact SDP attribute name, no duplicate values or truncation. */ +static inline int MatSdpValue(const char* sdp,const char* name,char* out,size_t cap) { + int found=0;size_t key=strlen(name);out[0]=0; + for(const char* p=sdp;p && *p;) { + const char* end=strchr(p,'\n');if(!end)end=p+strlen(p); + const char* trimmed=end;if(trimmed>p && trimmed[-1]=='\r')--trimmed; + if((size_t)(trimmed-p)>=key+3 && !memcmp(p,"a=",2) && !memcmp(p+2,name,key) && p[key+2]==':') { + size_t n=(size_t)(trimmed-p)-key-3;if(found || n>=cap)return -1; + memcpy(out,p+key+3,n);out[n]=0;found=1; + } + p=*end?end+1:end; + }return found; +} diff --git a/src/MatReceiver.c b/src/MatReceiver.c index d1b922a3d..ec7eabf65 100644 --- a/src/MatReceiver.c +++ b/src/MatReceiver.c @@ -29,31 +29,11 @@ static int complete(MAT_RECEIVER* s) { return 2; } int MatReceiverPush(MAT_RECEIVER* s, const uint8_t* p, size_t n) { - if (s->failed || s->mode == 2) return fail(s); - s->mode = 1; + if (s->failed) return fail(s); int result = MatDepacketizerPush(&s->depacketizer, p, n); if (result < 0) return fail(s); return result == 2 ? complete(s) : result; } -int MatReceiverPushRecordingCycle(MAT_RECEIVER* s, const MAT_CYCLE_INFO* i, - const uint8_t* p, size_t n) { - MAT_DEPACKETIZER* d = &s->depacketizer; - if (s->failed || s->completed || s->mode == 1 || !i || !i->epoch || !i->domain || - i->index != d->next_frame || i->index >= UINT64_MAX / 960 || - i->pts48k != i->index * 960 || !MatCarrierIsValid(p, n)) return fail(s); - if (d->started && (i->epoch != d->epoch || i->domain != d->domain)) return fail(s); - s->mode = 2; - d->started = 1; d->epoch = i->epoch; d->domain = i->domain; - if (!s->consume || !s->consume(s->context, i, p, n)) return fail(s); - ++d->next_frame; - return 1; -} -int MatReceiverEndRecording(MAT_RECEIVER* s, uint64_t expectedCycles) { - if (s->failed || s->mode != 2 || !expectedCycles || - expectedCycles != s->depacketizer.next_frame) return fail(s); - s->depacketizer.ended = 1; - return complete(s); -} int MatReceiverFinish(const MAT_RECEIVER* s) { return s && !s->failed && s->completed && MatDepacketizerFinish(&s->depacketizer); } diff --git a/src/MatReceiver.h b/src/MatReceiver.h index 3b97bdaa2..95fa9522c 100644 --- a/src/MatReceiver.h +++ b/src/MatReceiver.h @@ -15,14 +15,11 @@ typedef struct { MAT_CONSUME_CYCLE consume; MAT_COMPLETE_STREAM complete; void* context; - unsigned mode, failed, completed; + unsigned failed, completed; } MAT_RECEIVER; int MatCarrierIsValid(const uint8_t*, size_t); void MatReceiverInit(MAT_RECEIVER*, MAT_CONSUME_CYCLE, MAT_COMPLETE_STREAM, void*); int MatReceiverPush(MAT_RECEIVER*, const uint8_t*, size_t); -/* Offline recordings only: never mix raw cycles and SMAT packets in one receiver. */ -int MatReceiverPushRecordingCycle(MAT_RECEIVER*, const MAT_CYCLE_INFO*, const uint8_t*, size_t); -int MatReceiverEndRecording(MAT_RECEIVER*, uint64_t expectedCycles); int MatReceiverFinish(const MAT_RECEIVER*); void MatReceiverAbort(MAT_RECEIVER*); #ifdef __cplusplus diff --git a/tests/AudioCodecSelectionTest.c b/tests/AudioCodecSelectionTest.c index da4eafd7a..ded609e67 100644 --- a/tests/AudioCodecSelectionTest.c +++ b/tests/AudioCodecSelectionTest.c @@ -1,8 +1,41 @@ /* Exercise the real audio startup with a sink that stops before hardware/threads. */ #include "../src/AudioStream.c" +#include "MatWire.h" +#include "MatReceiver.h" #include static int failures,expectedCodec; #define CHECK(c) do { if(!(c)) { fprintf(stderr,"line %d: %s\n",__LINE__,#c);failures++;} } while(0) +static int received,ended,terminated,rejectCycle,rejectEnd; +static int consumeCycle(void* context,const unsigned char* bytes,int n,uint64_t epoch,uint64_t domain,uint64_t index,uint64_t pts) { + (void)context;CHECK(MatCarrierIsValid(bytes,(size_t)n));CHECK(epoch==1&&domain==1&&index==0&&pts==0); + ++received;return !rejectCycle; +} +static int consumeEnd(void* context,uint64_t count){(void)context;CHECK(count==1);++ended;return !rejectEnd;} +static void terminate(int error){(void)error;++terminated;} +static void finiteCallbacks(void) { + unsigned char carrier[MAT_WIRE_CYCLE]={0},packet[MAT_WIRE_PACKET]; + memcpy(carrier,"\x72\xf8\x1f\x4e\x16\x00\x02\x00",8); + const char* hash="b51894f456731ba3171663abe5305090f366ee778eb56cece1009e90005f4898"; + LI_MAT_PLAYBACK_CALLBACKS callbacks={consumeCycle,consumeEnd,NULL}; + LiSetMatPlaybackCallbacks(&callbacks);ListenerCallbacks.connectionTerminated=terminate; + for(int codec=AUDIO_CODEC_MAT_DUMP;codec<=AUDIO_CODEC_MAT_IEC61937;codec++)for(int test=0;test<5;test++) { + received=ended=terminated=0;rejectCycle=test==1;rejectEnd=test==2; + matPlaybackFailedReported=matPlaybackCompleteReported=false;NegotiatedAudioCodec=codec; + CHECK(MatProfileSet(&MatDumpProfile,hash,test==4?"2":"1")); + if(test==3)MatDumpProfile.sha256[0]='0'; + matPlayback=MatPlaybackOpen(&MatDumpProfile,&matCallbacks);CHECK(matPlayback!=NULL); + for(unsigned fragment=0;fragmentsampleRate==48000); CHECK(format->samplesPerFrame==(mat?960:240)); - CHECK(matDump==NULL && matPlayback==NULL); + CHECK(matLive==NULL && matPlayback==NULL); return -123; } int main(void) { CHECK(initializePlatform()==0); - const int requests[]={AUDIO_CODEC_MAT_AUTO,AUDIO_CODEC_MAT_AUTO,AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_LIVE}; - const int selections[]={AUDIO_CODEC_MAT_LIVE,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_LIVE}; + finiteCallbacks(); + const int requests[]={AUDIO_CODEC_MAT_AUTO,AUDIO_CODEC_MAT_AUTO,AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_LIVE,AUDIO_CODEC_MAT_DUMP}; + const int selections[]={AUDIO_CODEC_MAT_LIVE,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_LIVE,AUDIO_CODEC_MAT_DUMP}; for(unsigned i=0;i Date: Fri, 23 Oct 2026 10:48:21 +0800 Subject: [PATCH 10/11] fix(audio): guard MAT inactivity and supported static builds --- .github/workflows/ci.yml | 7 +++++++ CMakeLists.txt | 3 +++ docs/MAT-HANDOFF.md | 9 +++++++++ src/AudioStream.c | 36 +++++++++++++++++++++------------ src/MatPlayback.c | 6 +++--- tests/AudioCodecSelectionTest.c | 34 +++++++++++++++++++++++++------ 6 files changed, 73 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d828cad1..bee920495 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,3 +79,10 @@ jobs: mkdir build_release cmake ${{ matrix.cmake_args }} -DCMAKE_BUILD_TYPE=Release -B build_release -S . cmake --build build_release --config Release + + - name: Test Windows static client + if: startsWith(matrix.os, 'windows') + run: | + cmake ${{ matrix.cmake_args }} -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DBUILD_TESTING=ON -B build_static -S . + cmake --build build_static --config Release + ctest --test-dir build_static -C Release --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index e5fa5d743..68d9c78a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -159,6 +159,9 @@ if (BUILD_TESTING) target_compile_definitions(mat-live-rtp-tests PRIVATE $) target_link_libraries(mat-live-rtp-tests PRIVATE moonlight-common-c) + if(MSVC) + target_compile_options(mat-live-rtp-tests PRIVATE /experimental:c11atomics) + endif() if(NOT USE_MBEDTLS) target_include_directories(mat-live-rtp-tests SYSTEM PRIVATE ${OPENSSL_INCLUDE_DIR}) target_link_libraries(mat-live-rtp-tests PRIVATE ${OPENSSL_CRYPTO_LIBRARY}) diff --git a/docs/MAT-HANDOFF.md b/docs/MAT-HANDOFF.md index e06fc5254..b5160c4e9 100644 --- a/docs/MAT-HANDOFF.md +++ b/docs/MAT-HANDOFF.md @@ -27,6 +27,15 @@ also covers the independent crypto oracle, RTSP negotiation, UDP/FEC, RTP wrap, native-source contracts and migrated diagnostic failure cases. Generated test bytes do not establish native capture or hardware/Atmos playback acceptance. +Review fixes: both live and explicit finite input have a two-second inactivity +deadline, stopped only after verified END before the consumer's bounded drain. +The receive thread loads the last accepted timestamp before sampling its clock. +MSVC static RTP tests explicitly enable C11 atomics and now run in Windows CI; +finite digest contexts use the OpenSSL 1.0.2-compatible create/destroy names. +Local MinGW and MSVC static CTest suites pass; this does not claim a runtime test +against an installed OpenSSL 1.0.2 SDK. Parent diagnostics exercise real receive +threads with generated UDP data and missing END/final-fragment failures. + ## Historical finite handoff — 2026-10-22 The following records the earlier API and acceptance only; it does not describe diff --git a/src/AudioStream.c b/src/AudioStream.c index 2ad74095a..69ab4e53b 100644 --- a/src/AudioStream.c +++ b/src/AudioStream.c @@ -2,8 +2,8 @@ #include "MatPlayback.h" #include "MatLivePlayback.h" #include -static atomic_bool liveInputEnded; -static atomic_ullong liveLastAuthenticatedMs; +static atomic_bool matInputEnded; +static atomic_ullong matLastAcceptedMs; static MAT_LIVE_PLAYBACK* matLive; static MAT_PLAYBACK_RECEIVER* matPlayback; static LI_MAT_PLAYBACK_CALLBACKS matCallbacks; @@ -11,23 +11,32 @@ static bool matPlaybackFailedReported,matPlaybackCompleteReported; void LiSetMatPlaybackCallbacks(const LI_MAT_PLAYBACK_CALLBACKS* callbacks) { if(callbacks) matCallbacks=*callbacks; else memset(&matCallbacks,0,sizeof(matCallbacks)); } -static int liveEnd(void* context,uint64_t cycles) { +static int matEnd(void* context,uint64_t cycles) { // A verified END may take time to drain the hardware. Stop the network // watchdog before that bounded drain, not before authentication. - atomic_store_explicit(&liveInputEnded,true,memory_order_relaxed); + atomic_store_explicit(&matInputEnded,true,memory_order_relaxed); return matCallbacks.end(context,cycles); } +static bool matInputTimedOut(void) { + if(!AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec)||atomic_load_explicit(&matInputEnded,memory_order_relaxed))return false; + // The decoder can publish a newer timestamp while the receive thread runs. + // Load it before reading the clock so unsigned subtraction cannot underflow. + uint64_t last=atomic_load_explicit(&matLastAcceptedMs,memory_order_relaxed); + uint64_t now=PltGetMillis(); + return now>=last && now-last>=2000; +} static void failMatInput(void) { MatPlaybackFail(matPlayback);MatLivePlaybackFail(matLive); } static void consumeAudio(char* data,int size) { if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_LIVE) { int result=MatLivePlaybackPush(matLive,(unsigned char*)data,(size_t)(size>0?size:0)); - if(result>=0)atomic_store_explicit(&liveLastAuthenticatedMs,PltGetMillis(),memory_order_relaxed); + if(result>=0)atomic_store_explicit(&matLastAcceptedMs,PltGetMillis(),memory_order_relaxed); if(result<0&&!matPlaybackFailedReported){matPlaybackFailedReported=true;Limelog("Live MAT authentication/sequence/source failed\n");ListenerCallbacks.connectionTerminated(-1);} else if(result==2&&!matPlaybackCompleteReported){matPlaybackCompleteReported=true;Limelog("Live MAT authenticated EOS and renderer drain completed\n");} return; } if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_IEC61937 || NegotiatedAudioCodec==AUDIO_CODEC_MAT_DUMP){ int result=MatPlaybackPush(matPlayback,(unsigned char*)data,(size_t)(size>0?size:0)); + if(result>=0)atomic_store_explicit(&matLastAcceptedMs,PltGetMillis(),memory_order_relaxed); if(result<0 && !matPlaybackFailedReported){matPlaybackFailedReported=true;Limelog("Finite MAT receive/consumer failed\n");ListenerCallbacks.connectionTerminated(-1);} else if(result==2 && !matPlaybackCompleteReported){matPlaybackCompleteReported=true;Limelog("Finite MAT SHA/count verified and consumer completed: %s\n",MatDumpProfile.sha256);} return; @@ -315,9 +324,8 @@ static void AudioReceiveThreadProc(void* context) { waitingForAudioMs = 0; while (!PltIsThreadInterrupted(&receiveThread)) { - if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_LIVE && !atomic_load_explicit(&liveInputEnded,memory_order_relaxed) && - PltGetMillis()-atomic_load_explicit(&liveLastAuthenticatedMs,memory_order_relaxed)>=2000) { - Limelog("Live MAT authenticated input timed out (no EOS)\n");ListenerCallbacks.connectionTerminated(-1);break; + if(matInputTimedOut()) { + Limelog("MAT validated input timed out (no verified EOS)\n");ListenerCallbacks.connectionTerminated(-1);break; } if (packet == NULL) { packet = (PQUEUED_AUDIO_PACKET)malloc(sizeof(*packet)); @@ -510,15 +518,17 @@ int startAudioStream(void* audioContext, int arFlags) { return err; } + LI_MAT_PLAYBACK_CALLBACKS callbacks=matCallbacks;callbacks.end=matCallbacks.end?matEnd:NULL; + if(AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec)) { + atomic_store_explicit(&matInputEnded,false,memory_order_relaxed); + atomic_store_explicit(&matLastAcceptedMs,PltGetMillis(),memory_order_relaxed); + } if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_IEC61937 || NegotiatedAudioCodec==AUDIO_CODEC_MAT_DUMP){ - matPlayback=MatPlaybackOpen(&MatDumpProfile,&matCallbacks); + matPlayback=MatPlaybackOpen(&MatDumpProfile,&callbacks); if(!matPlayback){AudioCallbacks.cleanup();return -1;} } if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_LIVE) { - LI_MAT_PLAYBACK_CALLBACKS liveCallbacks=matCallbacks;liveCallbacks.end=matCallbacks.end?liveEnd:NULL; - atomic_store_explicit(&liveInputEnded,false,memory_order_relaxed); - atomic_store_explicit(&liveLastAuthenticatedMs,PltGetMillis(),memory_order_relaxed); - matLive=MatLivePlaybackOpen(&MatLiveProfile,(const uint8_t*)StreamConfig.remoteInputAesKey,&liveCallbacks); + matLive=MatLivePlaybackOpen(&MatLiveProfile,(const uint8_t*)StreamConfig.remoteInputAesKey,&callbacks); if(!matLive){AudioCallbacks.cleanup();return -1;} } AudioCallbacks.start(); diff --git a/src/MatPlayback.c b/src/MatPlayback.c index 2b380ef4a..5ac3482fd 100644 --- a/src/MatPlayback.c +++ b/src/MatPlayback.c @@ -58,8 +58,8 @@ MAT_PLAYBACK_RECEIVER* MatPlaybackOpen(const LI_MAT_DUMP_PROFILE* expected,const s->digest=psa_hash_operation_init(); if(psa_crypto_init()!=PSA_SUCCESS || psa_hash_setup(&s->digest,PSA_ALG_SHA_256)!=PSA_SUCCESS){free(s);return NULL;} #else - s->digest=EVP_MD_CTX_new(); - if(!s->digest || EVP_DigestInit_ex(s->digest,EVP_sha256(),NULL)!=1){EVP_MD_CTX_free(s->digest);free(s);return NULL;} + s->digest=EVP_MD_CTX_create(); + if(!s->digest || EVP_DigestInit_ex(s->digest,EVP_sha256(),NULL)!=1){EVP_MD_CTX_destroy(s->digest);free(s);return NULL;} #endif MatReceiverInit(&s->receiver,cycle,end,s);return s; } @@ -75,7 +75,7 @@ void MatPlaybackClose(MAT_PLAYBACK_RECEIVER* s) { #if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) psa_hash_abort(&s->digest); #else - EVP_MD_CTX_free(s->digest); + EVP_MD_CTX_destroy(s->digest); #endif free(s); } diff --git a/tests/AudioCodecSelectionTest.c b/tests/AudioCodecSelectionTest.c index ded609e67..aab9aa583 100644 --- a/tests/AudioCodecSelectionTest.c +++ b/tests/AudioCodecSelectionTest.c @@ -1,16 +1,24 @@ /* Exercise the real audio startup with a sink that stops before hardware/threads. */ +#define PltGetMillis MatTestGetMillis #include "../src/AudioStream.c" +#undef PltGetMillis #include "MatWire.h" #include "MatReceiver.h" #include static int failures,expectedCodec; +static uint64_t testNow; +static bool publishDuringClock; +uint64_t MatTestGetMillis(void) { + if(publishDuringClock)atomic_store_explicit(&matLastAcceptedMs,testNow+1,memory_order_relaxed); + return testNow; +} #define CHECK(c) do { if(!(c)) { fprintf(stderr,"line %d: %s\n",__LINE__,#c);failures++;} } while(0) static int received,ended,terminated,rejectCycle,rejectEnd; static int consumeCycle(void* context,const unsigned char* bytes,int n,uint64_t epoch,uint64_t domain,uint64_t index,uint64_t pts) { (void)context;CHECK(MatCarrierIsValid(bytes,(size_t)n));CHECK(epoch==1&&domain==1&&index==0&&pts==0); ++received;return !rejectCycle; } -static int consumeEnd(void* context,uint64_t count){(void)context;CHECK(count==1);++ended;return !rejectEnd;} +static int consumeEnd(void* context,uint64_t count){(void)context;CHECK(count==1);testNow+=5000;CHECK(!matInputTimedOut());++ended;return !rejectEnd;} static void terminate(int error){(void)error;++terminated;} static void finiteCallbacks(void) { unsigned char carrier[MAT_WIRE_CYCLE]={0},packet[MAT_WIRE_PACKET]; @@ -18,24 +26,37 @@ static void finiteCallbacks(void) { const char* hash="b51894f456731ba3171663abe5305090f366ee778eb56cece1009e90005f4898"; LI_MAT_PLAYBACK_CALLBACKS callbacks={consumeCycle,consumeEnd,NULL}; LiSetMatPlaybackCallbacks(&callbacks);ListenerCallbacks.connectionTerminated=terminate; - for(int codec=AUDIO_CODEC_MAT_DUMP;codec<=AUDIO_CODEC_MAT_IEC61937;codec++)for(int test=0;test<5;test++) { + for(int codec=AUDIO_CODEC_MAT_DUMP;codec<=AUDIO_CODEC_MAT_IEC61937;codec++)for(int test=0;test<6;test++) { received=ended=terminated=0;rejectCycle=test==1;rejectEnd=test==2; matPlaybackFailedReported=matPlaybackCompleteReported=false;NegotiatedAudioCodec=codec; CHECK(MatProfileSet(&MatDumpProfile,hash,test==4?"2":"1")); if(test==3)MatDumpProfile.sha256[0]='0'; - matPlayback=MatPlaybackOpen(&MatDumpProfile,&matCallbacks);CHECK(matPlayback!=NULL); + LI_MAT_PLAYBACK_CALLBACKS guarded=matCallbacks;guarded.end=matEnd; + testNow=1000;atomic_store(&matInputEnded,false);atomic_store(&matLastAcceptedMs,testNow); + matPlayback=MatPlaybackOpen(&MatDumpProfile,&guarded);CHECK(matPlayback!=NULL); for(unsigned fragment=0;fragment0&&test<5)); MatPlaybackClose(matPlayback);matPlayback=NULL; } LiSetMatPlaybackCallbacks(NULL); CHECK(MatPlaybackOpen(&MatDumpProfile,&matCallbacks)==NULL); } +static void watchdog(void) { + const int codecs[]={AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_DUMP,AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_MAT_LIVE}; + for(unsigned i=0;i Date: Fri, 23 Oct 2026 11:19:38 +0800 Subject: [PATCH 11/11] refactor(audio): keep only live MAT production transport --- CMakeLists.txt | 9 --- docs/MAT-HANDOFF.md | 125 +++++++++---------------------- src/AudioStream.c | 17 +---- src/Connection.c | 20 +---- src/Limelight-internal.h | 3 - src/Limelight.h | 12 ++- src/MatDepacketizer.c | 1 - src/MatDepacketizer.h | 5 +- src/MatDumpProfile.h | 23 ------ src/MatPlayback.c | 81 -------------------- src/MatPlayback.h | 8 -- src/MatPlaybackProfile.h | 30 -------- src/MatReceiver.c | 3 - src/MatReceiver.h | 1 - src/RtspConnection.c | 33 -------- src/SdpGenerator.c | 32 ++------ tests/AudioCodecSelectionTest.c | 41 +++++----- tests/MatPlaybackSelectionTest.c | 27 ------- 18 files changed, 77 insertions(+), 394 deletions(-) delete mode 100644 src/MatDumpProfile.h delete mode 100644 src/MatPlayback.c delete mode 100644 src/MatPlayback.h delete mode 100644 src/MatPlaybackProfile.h delete mode 100644 tests/MatPlaybackSelectionTest.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 68d9c78a8..abeff0e90 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -137,15 +137,6 @@ if (BUILD_TESTING) endif() add_test(NAME mat-live-tests COMMAND mat-live-tests) - add_executable(mat-playback-selection-tests tests/MatPlaybackSelectionTest.c) - target_include_directories(mat-playback-selection-tests PRIVATE src) - if(MSVC) - target_compile_options(mat-playback-selection-tests PRIVATE /W3 /WX) - else() - target_compile_options(mat-playback-selection-tests PRIVATE -Wall -Wextra -Werror) - endif() - add_test(NAME mat-playback-selection-tests COMMAND mat-playback-selection-tests) - add_executable(rtsp-parser-mat-tests tests/RtspParserMatTest.c src/RtspParser.c) target_include_directories(rtsp-parser-mat-tests PRIVATE src enet/include) target_compile_definitions(rtsp-parser-mat-tests PRIVATE HAS_SOCKLEN_T) diff --git a/docs/MAT-HANDOFF.md b/docs/MAT-HANDOFF.md index b5160c4e9..e18e9deeb 100644 --- a/docs/MAT-HANDOFF.md +++ b/docs/MAT-HANDOFF.md @@ -1,93 +1,38 @@ -# MAT transport boundary — 2026-10-23 +# Live MAT transport boundary - 2026-10-23 -This PR targets **mic**. It contains network negotiation, authenticated live -transport, bounded depacketization and synchronous cycle/EOS callbacks. It does -not own files, offline recording input, native capture, encoding or hardware. +This PR targets **mic**. It owns negotiation, authenticated live transport, +bounded reassembly and synchronous cycle/EOS callbacks. It does not own files, +offline input, native capture, encoding or hardware rendering. - Codec 7 / `mat-live-v1` uses HKDF-SHA256, AES-128-GCM with authenticated headers - and a 64-bit packet counter. Network failure is terminal; it cannot synthesize - a successful EOS. Normal MAT preference selects live7 or Opus0, never replay. -- `MatProfile.h` contains shared SDP parsing and carrier/clock constants; the - live profile no longer depends on finite dump/playback profile headers. -- Explicit finite codec4/5 profiles remain protocol features. Both validate - count/SHA and deliver complete cycles/EOS through `LiSetMatPlaybackCallbacks`. - Codec4 is diagnostic data, never device playback. Consumers own any storage - policy, publication or bounded hardware drain; errors terminate the stream. -- `MatDumpFile.c/.h`, `STREAM_CONFIGURATION.matDumpPath` and - `MatReceiverPushRecordingCycle`/`MatReceiverEndRecording` are removed. The - Sunshine parent contains the migrated diagnostic writer, independent raw-cycle - adapter and mock replay tool under `spatial-object-spike/mat-diagnostics`. -- Rebuild consumers: the unpublished configuration ABI changed, and diagnostic - codec4 now requires registered callbacks. The historical Android PR stays on - its old shared revision; new acceptance uses mock frontends against this PR. - -Static CTest has 14 tests, including production AudioStream codec4/5 callback -delivery, SHA/count rejection and consumer failure. Parent software acceptance -also covers the independent crypto oracle, RTSP negotiation, UDP/FEC, RTP wrap, -native-source contracts and migrated diagnostic failure cases. Generated test -bytes do not establish native capture or hardware/Atmos playback acceptance. - -Review fixes: both live and explicit finite input have a two-second inactivity -deadline, stopped only after verified END before the consumer's bounded drain. -The receive thread loads the last accepted timestamp before sampling its clock. -MSVC static RTP tests explicitly enable C11 atomics and now run in Windows CI; -finite digest contexts use the OpenSSL 1.0.2-compatible create/destroy names. -Local MinGW and MSVC static CTest suites pass; this does not claim a runtime test -against an installed OpenSSL 1.0.2 SDK. Parent diagnostics exercise real receive -threads with generated UDP data and missing END/final-fragment failures. - -## Historical finite handoff — 2026-10-22 - -The following records the earlier API and acceptance only; it does not describe -the current file ownership, recording input or live transport boundary above. - -This branch targets **mic**, the branch consumed by both parent repositories, -not default master. Baseline: b91c7cd72764320edd6140a16f8614aeb63dc42a. -Keep Sunshine and moonlight-vplus pinned to the same resulting commit; rebuild -all consumers because STREAM_CONFIGURATION and the JNI entry point changed. - -Upstream integration on 2026-10-22 includes mic commit -`2aa14ffeb877f9093993d9c9cf3c3440e2d2220d`. -The upstream line-based RTSP parser, whitespace/binary-payload support, -case-insensitive last-value-wins lookup and duplicate-node free are retained. -MAT marks the incoming replacement node before upstream insertion; do not -restore the old caller-side free or set flags only on the overwritten node. -`tests/RtspParserMatTest.c` covers unique, identical/case-folded/triple duplicate -headers plus whitespace and embedded-NUL payload handling. With this regression -the parent's rebuilt static-library CTest suites each contain ten tests. -The parent handoff records fresh end-to-end no-device acceptance separately -from the original historical C proof; no Android hardware claim is added. - -## Contract - -- Codec 4 / `mat-dump-v1`: bounded, app-owned file publication after count/SHA/EOS - validation. Never decode or play this codec. -- Codec 5 / `mat-iec61937-v1`: explicit finite replay, fileless `MatPlayback` -> - full-cycle callbacks. No Opus, AC3 or PCM fallback. Explicit matching RTSP - capability and unique ANNOUNCE confirmation are required. -- Carrier `iec61937-192000-8-s16le`: 61,440 bytes per 20 ms cycle; SMAT logical - clock 48,000 Hz / 960 ticks per cycle. RTP/FEC fragment ticks are not media PTS. -- `MatDepacketizer` -> `MatReceiver` shared by the dump and playback consumers. - Preserve cycle boundaries, epoch/domain/index/PTS; fail rather than conceal - loss or overflow. `MatPlayback` checks count and incremental SHA at EOS before - requesting the consumer's bounded drain; no successful EOS after failed drain. -- Register playback callbacks during audio init, clear only after audio threads - join. The callback owns no borrowed data after return; renderers must copy. -- Duplicate parser flag preserves otherwise-coalesced duplicate header evidence; - strict case-insensitive uniqueness applies to the new playback confirmation. - Existing generic lookup retains legacy last-value-wins semantics. - -## Evidence / limits - -The parent Sunshine branch contains tests under `spatial-object-spike/` and the -cross-repository `docs/ATMOS-MAT-HANDOFF.md`. Recorded acceptance: plain, AES audio -and encrypted RTSP + AES sessions, 178 cycles / 10,936,320 bytes, SHA256 -`79cd69e33adcd1bcffe95673564b6fc727a7aaed7370745d43ec269925a492f6`. -Actual JNI and Java player were exercised with **fake final hardware**, not an -Android HAL or soundbar. Original dump regression, 15 negative negotiation -cases, playback lifecycle cases and existing nine CTests passed. - -This is not infinite/live capture or authenticated audio transport. FNV and -legacy AES-CBC are not a MAC. No claim of Android MAT support, Atmos recognition, -certification or commercial encoding rights is made. Keep finite manifest bounds -and fail-closed behavior until a separate live-source contract is designed. + and a 64-bit packet counter. RTP/FEC operates on ciphertext; authentication + precedes reassembly. Loss, corruption, gaps and consumer failure are terminal. +- Local MAT preference 6 selects live7 or Opus0 before ANNOUNCE. After selection + there is no codec change. Removed finite codec IDs 4/5 fail before networking; + they are reserved and must not be reused. Old finite-only hosts cause AUTO to + select Opus; stale or mismatched MAT acknowledgements fail the handshake. +- Carrier `iec61937-192000-8-s16le` carries 61,440 bytes per 20 ms cycle. The SMAT + clock is 48,000 Hz / 960 ticks per cycle. RTP fragment ticks are not media PTS. +- `LiSetMatPlaybackCallbacks` registers the live consumer during audio init and + is cleared after threads join. Consumers copy borrowed bytes before returning; + zero means failure. The end callback requires authenticated, complete END and + must finish any bounded drain before returning success. Closing or timing out + cannot synthesize successful EOS. +- The two-second watchdog measures accepted live input. It stops only after + verified END, before the consumer drain. Its timestamp is loaded before the + clock to avoid racing a newer publication. Unrelated Opus behavior is retained. +- Finite profile negotiation, SHA/count receiver, recording APIs and finish + polling are removed. Standalone file checks and mock offline input live in the + parent Sunshine `spatial-object-spike/mat-diagnostics` directory. + +Static CTest has 13 tests, including live callback failures, missing END timeout, +drain semantics, RTP wrap/FEC, authenticated integrity and strict RTSP parser +uniqueness. Windows CI also builds/runs static tests with MSVC C11 atomics. +Parent software acceptance adds an independent crypto oracle, actual UDP receive +threads, controlled RTSP negotiation, source/pipe contracts and diagnostics. + +Rebuild consumers after these unpublished API removals. The historical Android +app remains pinned to its previous shared revision; current clients are mocks. +Generated bytes establish software behavior only. Earlier finite replay/native +reports do not establish current native capture, hardware playback or Atmos +recognition. Historical APIs/results are available in Git history. diff --git a/src/AudioStream.c b/src/AudioStream.c index 69ab4e53b..e48dc4fed 100644 --- a/src/AudioStream.c +++ b/src/AudioStream.c @@ -1,11 +1,9 @@ #include "Limelight-internal.h" -#include "MatPlayback.h" #include "MatLivePlayback.h" #include static atomic_bool matInputEnded; static atomic_ullong matLastAcceptedMs; static MAT_LIVE_PLAYBACK* matLive; -static MAT_PLAYBACK_RECEIVER* matPlayback; static LI_MAT_PLAYBACK_CALLBACKS matCallbacks; static bool matPlaybackFailedReported,matPlaybackCompleteReported; void LiSetMatPlaybackCallbacks(const LI_MAT_PLAYBACK_CALLBACKS* callbacks) { @@ -25,7 +23,7 @@ static bool matInputTimedOut(void) { uint64_t now=PltGetMillis(); return now>=last && now-last>=2000; } -static void failMatInput(void) { MatPlaybackFail(matPlayback);MatLivePlaybackFail(matLive); } +static void failMatInput(void) { MatLivePlaybackFail(matLive); } static void consumeAudio(char* data,int size) { if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_LIVE) { int result=MatLivePlaybackPush(matLive,(unsigned char*)data,(size_t)(size>0?size:0)); @@ -34,13 +32,6 @@ static void consumeAudio(char* data,int size) { else if(result==2&&!matPlaybackCompleteReported){matPlaybackCompleteReported=true;Limelog("Live MAT authenticated EOS and renderer drain completed\n");} return; } - if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_IEC61937 || NegotiatedAudioCodec==AUDIO_CODEC_MAT_DUMP){ - int result=MatPlaybackPush(matPlayback,(unsigned char*)data,(size_t)(size>0?size:0)); - if(result>=0)atomic_store_explicit(&matLastAcceptedMs,PltGetMillis(),memory_order_relaxed); - if(result<0 && !matPlaybackFailedReported){matPlaybackFailedReported=true;Limelog("Finite MAT receive/consumer failed\n");ListenerCallbacks.connectionTerminated(-1);} - else if(result==2 && !matPlaybackCompleteReported){matPlaybackCompleteReported=true;Limelog("Finite MAT SHA/count verified and consumer completed: %s\n",MatDumpProfile.sha256);} - return; - } AudioCallbacks.decodeAndPlaySample(data,size); } @@ -182,7 +173,6 @@ static void freePacketList(PLINKED_BLOCKING_QUEUE_ENTRY entry) { // Tear down the audio stream once we're done with it void destroyAudioStream(void) { MatLivePlaybackClose(matLive);matLive=NULL; - MatPlaybackClose(matPlayback);matPlayback=NULL; if (rtpSocket != INVALID_SOCKET) { if (pingThreadStarted) { PltInterruptThread(&udpPingThread); @@ -486,7 +476,6 @@ void stopAudioStream(void) { PltJoinThread(&decoderThread); } - MatPlaybackClose(matPlayback);matPlayback=NULL; MatLivePlaybackClose(matLive);matLive=NULL; AudioCallbacks.cleanup(); } @@ -523,10 +512,6 @@ int startAudioStream(void* audioContext, int arFlags) { atomic_store_explicit(&matInputEnded,false,memory_order_relaxed); atomic_store_explicit(&matLastAcceptedMs,PltGetMillis(),memory_order_relaxed); } - if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_IEC61937 || NegotiatedAudioCodec==AUDIO_CODEC_MAT_DUMP){ - matPlayback=MatPlaybackOpen(&MatDumpProfile,&callbacks); - if(!matPlayback){AudioCallbacks.cleanup();return -1;} - } if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_LIVE) { matLive=MatLivePlaybackOpen(&MatLiveProfile,(const uint8_t*)StreamConfig.remoteInputAesKey,&callbacks); if(!matLive){AudioCallbacks.cleanup();return -1;} diff --git a/src/Connection.c b/src/Connection.c index e91ed19b4..a19d57603 100644 --- a/src/Connection.c +++ b/src/Connection.c @@ -28,7 +28,6 @@ bool AudioEncryptionEnabled; bool ReferenceFrameInvalidationSupported; int NegotiatedAudioCodec; LI_MAT_LIVE_PROFILE MatLiveProfile; -LI_MAT_DUMP_PROFILE MatDumpProfile; int NegotiatedAudioBitrate; int NegotiatedDynamicHdrFormat; int NegotiatedDynamicHdrFallback; @@ -333,7 +332,6 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre // LiGetNegotiatedAudioCodec() to check what was actually selected. NegotiatedAudioCodec = AUDIO_CODEC_OPUS; NegotiatedAudioBitrate = 0; - memset(&MatDumpProfile,0,sizeof(MatDumpProfile)); memset(&MatLiveProfile,0,sizeof(MatLiveProfile)); switch (StreamConfig.audioCodec) { case AUDIO_CODEC_OPUS: @@ -373,20 +371,10 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre } NegotiatedAudioCodec = AUDIO_CODEC_MAT_LIVE; break; - case AUDIO_CODEC_MAT_IEC61937: - if (!IS_SUNSHINE() || StreamConfig.controlOnly) { - Limelog("MAT playback requires a Sunshine audio session\n"); - err = -1; goto Cleanup; - } - NegotiatedAudioCodec = AUDIO_CODEC_MAT_IEC61937; - break; - case AUDIO_CODEC_MAT_DUMP: - if (!IS_SUNSHINE() || StreamConfig.controlOnly) { - Limelog("MAT dump requires a Sunshine audio session and registered consumer\n"); - err = -1; goto Cleanup; - } - NegotiatedAudioCodec = AUDIO_CODEC_MAT_DUMP; - break; + case 4: // Retired finite diagnostic profiles; never silently reinterpret. + case 5: + Limelog("Finite MAT codecs are not supported; request live MAT or Opus\n"); + err = -1; goto Cleanup; case AUDIO_CODEC_PCM_S16: if (!IS_SUNSHINE()) { Limelog("PCM passthrough is only supported by Sunshine; falling back to Opus\n"); diff --git a/src/Limelight-internal.h b/src/Limelight-internal.h index 57807ed61..f2b35a86d 100644 --- a/src/Limelight-internal.h +++ b/src/Limelight-internal.h @@ -1,8 +1,5 @@ #pragma once #include "Platform.h" -#include "MatDumpProfile.h" -extern LI_MAT_DUMP_PROFILE MatDumpProfile; -#include "MatPlaybackProfile.h" #include "MatLiveProfile.h" extern LI_MAT_LIVE_PROFILE MatLiveProfile; diff --git a/src/Limelight.h b/src/Limelight.h index aaca35082..9d357f3cc 100644 --- a/src/Limelight.h +++ b/src/Limelight.h @@ -302,16 +302,14 @@ typedef struct _DECODE_UNIT { // delay, no AVR decoder); biggest bandwidth (~1.5 Mbps stereo, ~4.6 Mbps 5.1). // LAN-only in practice. #define AUDIO_CODEC_PCM_S16 3 -#define AUDIO_CODEC_MAT_DUMP 4 -#define AUDIO_CODEC_MAT_IEC61937 5 +// Values 4 and 5 are retired finite diagnostic profiles; they are rejected. #define AUDIO_CODEC_MAT_AUTO 6 // Local preference: select codec 7 or 0 before ANNOUNCE; never a wire codec. #define AUDIO_CODEC_MAT_LIVE 7 -#define AUDIO_CODEC_IS_MAT(c) ((c)==AUDIO_CODEC_MAT_DUMP || (c)==AUDIO_CODEC_MAT_IEC61937 || (c)==AUDIO_CODEC_MAT_LIVE) +#define AUDIO_CODEC_IS_MAT(c) ((c)==AUDIO_CODEC_MAT_LIVE) -/* Registered from audio init, cleared after audio threads join. Codec4 uses a - * diagnostic consumer, never a renderer. Return 0 to fail the loss-intolerant session. End is verified EOS - * (finite SHA/count for 4/5, authenticated END for 7); consumers own any file - * publication or bounded hardware drain before returning success. No PCM/encoded callback. */ +/* Registered from audio init, cleared after audio threads join. Return 0 to fail + * the loss-intolerant live session. End is authenticated EOS; consumers complete + * any bounded drain before returning success. No PCM/encoded callback. */ typedef struct { int (*cycle)(void*, const unsigned char*, int, uint64_t, uint64_t, uint64_t, uint64_t); int (*end)(void*, uint64_t); diff --git a/src/MatDepacketizer.c b/src/MatDepacketizer.c index 6aa5ce07a..788041b2c 100644 --- a/src/MatDepacketizer.c +++ b/src/MatDepacketizer.c @@ -45,4 +45,3 @@ int MatDepacketizerPush(MAT_DEPACKETIZER* s,const uint8_t* p,size_t n) { s->stream_hash=mat_hash_update(s->stream_hash,s->cycle,sizeof(s->cycle)); s->mask=0;++s->next_frame;return 1; } -int MatDepacketizerFinish(const MAT_DEPACKETIZER* s) { return s->ended && !s->failed && !s->mask; } diff --git a/src/MatDepacketizer.h b/src/MatDepacketizer.h index 424c519b5..6d05b3b31 100644 --- a/src/MatDepacketizer.h +++ b/src/MatDepacketizer.h @@ -4,10 +4,10 @@ #ifdef __cplusplus extern "C" { #endif -/* Dump-only extension AFTER existing RTP reorder/FEC + decrypt. One bounded +/* Live carrier reassembly AFTER existing RTP reorder/FEC + authentication. One bounded * carrier buffer; in-cycle reorder/identical duplicates allowed. Missing data, * epoch changes, cross-cycle gaps and missing EOS are terminal, never concealed. - * Callback must copy/write synchronously; zero return means IO failure. */ + * Callback must copy/write synchronously; zero return means consumer failure. */ typedef int (*MAT_WRITE_CYCLE)(void*,const uint8_t*,size_t); typedef struct { uint64_t epoch,domain,next_frame,mask; @@ -20,7 +20,6 @@ typedef struct { void MatDepacketizerInit(MAT_DEPACKETIZER*,MAT_WRITE_CYCLE,void*); /* -1 sticky failure; 0 accepted, 1 whole cycle written, 2 complete EOS. */ int MatDepacketizerPush(MAT_DEPACKETIZER*,const uint8_t*,size_t); -int MatDepacketizerFinish(const MAT_DEPACKETIZER*); #ifdef __cplusplus } #endif diff --git a/src/MatDumpProfile.h b/src/MatDumpProfile.h deleted file mode 100644 index 8b840337a..000000000 --- a/src/MatDumpProfile.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once -#include "MatProfile.h" -#define MAT_DUMP_CODEC 4 -#define MAT_DUMP_NAME "mat-dump-v1" -typedef struct { char sha256[65]; uint32_t cycles; } LI_MAT_DUMP_PROFILE; -static inline int MatProfileSet(LI_MAT_DUMP_PROFILE* out,const char* hash,const char* count) { - memset(out,0,sizeof(*out)); - if(!hash || !count || strlen(hash)!=64 || !*count)return 0; - for(unsigned i=0;i<64;i++)if(!((hash[i]>='0'&&hash[i]<='9') || (hash[i]>='a'&&hash[i]<='f')))return 0; - unsigned n=0;for(const char* p=count;*p;p++){if(*p<'0'||*p>'9'||n>1000)return 0;n=n*10+(unsigned)(*p-'0');} - if(!n || n>1000)return 0; - memcpy(out->sha256,hash,65);out->cycles=n;return 1; -} -static inline int MatProfileParse(LI_MAT_DUMP_PROFILE* out,const char* sdp) { - char version[8],hash[65],count[16];memset(out,0,sizeof(*out)); - return MatSdpValue(sdp,"x-ss-audio.mat-dump",version,sizeof(version))==1 && !strcmp(version,"1") && - MatSdpValue(sdp,"x-ss-audio.mat-sha256",hash,sizeof(hash))==1 && - MatSdpValue(sdp,"x-ss-audio.mat-cycles",count,sizeof(count))==1 && MatProfileSet(out,hash,count); -} -static inline int MatProfileConfirm(const LI_MAT_DUMP_PROFILE* expected,const char* codec,const char* hash,const char* count) { - LI_MAT_DUMP_PROFILE got;return codec && !strcmp(codec,MAT_DUMP_NAME) && MatProfileSet(&got,hash,count) && - got.cycles==expected->cycles && !strcmp(got.sha256,expected->sha256); -} diff --git a/src/MatPlayback.c b/src/MatPlayback.c deleted file mode 100644 index 5ac3482fd..000000000 --- a/src/MatPlayback.c +++ /dev/null @@ -1,81 +0,0 @@ -#include "MatPlayback.h" -#include "MatReceiver.h" -#include -#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) -#include -#else -#include -#endif -struct MAT_PLAYBACK_RECEIVER { - MAT_RECEIVER receiver; - LI_MAT_DUMP_PROFILE expected; - LI_MAT_PLAYBACK_CALLBACKS callbacks; - int failed; -#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) - psa_hash_operation_t digest; -#else - EVP_MD_CTX* digest; -#endif -}; -static int cycle(void* context,const MAT_CYCLE_INFO* info,const uint8_t* p,size_t n) { - MAT_PLAYBACK_RECEIVER* s=context; - if(info->index>=s->expected.cycles) return 0; -#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) - if(psa_hash_update(&s->digest,p,n)!=PSA_SUCCESS) return 0; -#else - if(EVP_DigestUpdate(s->digest,p,n)!=1) return 0; -#endif - return s->callbacks.cycle(s->callbacks.context,p,(int)n,info->epoch,info->domain,info->index,info->pts48k); -} -static int end(void* context,uint64_t count) { - MAT_PLAYBACK_RECEIVER* s=context; - unsigned char bytes[32];size_t length=0;char hash[65]={0}; -#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) - if(psa_hash_finish(&s->digest,bytes,sizeof(bytes),&length)!=PSA_SUCCESS) return 0; -#else - unsigned out=0; - if(EVP_DigestFinal_ex(s->digest,bytes,&out)!=1) return 0; - length=out; -#endif - if(length!=32 || count!=s->expected.cycles) return 0; - const char* hex="0123456789abcdef"; - for(unsigned i=0;i<32;i++){hash[2*i]=hex[bytes[i]>>4];hash[2*i+1]=hex[bytes[i]&15];} - if(strcmp(hash,s->expected.sha256)) return 0; - return s->callbacks.end(s->callbacks.context,count); -} -MAT_PLAYBACK_RECEIVER* MatPlaybackOpen(const LI_MAT_DUMP_PROFILE* expected,const LI_MAT_PLAYBACK_CALLBACKS* callbacks) { - if(!expected || !callbacks || !callbacks->cycle || !callbacks->end) return NULL; - char count[16];LI_MAT_DUMP_PROFILE checked; - /* Avoid stdio formatting in the reception hot path. */ - uint32_t value=expected->cycles;unsigned n=0; - if(!value || value>1000) return NULL; - do {count[n++]=(char)('0'+value%10);value/=10;} while(value); - count[n]=0;for(unsigned i=0;isha256,count)) return NULL; - MAT_PLAYBACK_RECEIVER* s=calloc(1,sizeof(*s));if(!s)return NULL; - s->expected=checked;s->callbacks=*callbacks; -#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) - s->digest=psa_hash_operation_init(); - if(psa_crypto_init()!=PSA_SUCCESS || psa_hash_setup(&s->digest,PSA_ALG_SHA_256)!=PSA_SUCCESS){free(s);return NULL;} -#else - s->digest=EVP_MD_CTX_create(); - if(!s->digest || EVP_DigestInit_ex(s->digest,EVP_sha256(),NULL)!=1){EVP_MD_CTX_destroy(s->digest);free(s);return NULL;} -#endif - MatReceiverInit(&s->receiver,cycle,end,s);return s; -} -int MatPlaybackPush(MAT_PLAYBACK_RECEIVER* s,const unsigned char* p,size_t n) { - if(!s || s->failed)return -1; - int result=MatReceiverPush(&s->receiver,p,n); - if(result<0)s->failed=1; - return result; -} -void MatPlaybackFail(MAT_PLAYBACK_RECEIVER* s) {if(s){s->failed=1;MatReceiverAbort(&s->receiver);}} -void MatPlaybackClose(MAT_PLAYBACK_RECEIVER* s) { - if(!s)return; -#if defined(USE_MBEDTLS) || defined(USE_PSA_CRYPTO) - psa_hash_abort(&s->digest); -#else - EVP_MD_CTX_destroy(s->digest); -#endif - free(s); -} diff --git a/src/MatPlayback.h b/src/MatPlayback.h deleted file mode 100644 index bae8db40b..000000000 --- a/src/MatPlayback.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once -#include "Limelight.h" -#include "MatDumpProfile.h" -typedef struct MAT_PLAYBACK_RECEIVER MAT_PLAYBACK_RECEIVER; -MAT_PLAYBACK_RECEIVER* MatPlaybackOpen(const LI_MAT_DUMP_PROFILE*,const LI_MAT_PLAYBACK_CALLBACKS*); -int MatPlaybackPush(MAT_PLAYBACK_RECEIVER*,const unsigned char*,size_t); -void MatPlaybackFail(MAT_PLAYBACK_RECEIVER*); -void MatPlaybackClose(MAT_PLAYBACK_RECEIVER*); diff --git a/src/MatPlaybackProfile.h b/src/MatPlaybackProfile.h deleted file mode 100644 index 31d291ab5..000000000 --- a/src/MatPlaybackProfile.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once -#include "MatDumpProfile.h" -#define MAT_PLAYBACK_CODEC 5 -#define MAT_PLAYBACK_NAME "mat-iec61937-v1" -#define MAT_PLAYBACK_CARRIER MAT_CARRIER -#define MAT_PLAYBACK_CLOCK MAT_CLOCK -/* Finite replay v1 only. No claim of an infinite/live capture source. */ -static inline int MatPlaybackProfileParse(LI_MAT_DUMP_PROFILE* out,const char* sdp) { - char version[8],carrier[48],clock[16],hash[65],count[16]; - memset(out,0,sizeof(*out)); - return MatSdpValue(sdp,"x-ss-audio.mat-playback",version,sizeof(version))==1 && !strcmp(version,"1") && - MatSdpValue(sdp,"x-ss-audio.mat-carrier",carrier,sizeof(carrier))==1 && !strcmp(carrier,MAT_PLAYBACK_CARRIER) && - MatSdpValue(sdp,"x-ss-audio.mat-clock",clock,sizeof(clock))==1 && !strcmp(clock,MAT_PLAYBACK_CLOCK) && - MatSdpValue(sdp,"x-ss-audio.mat-sha256",hash,sizeof(hash))==1 && - MatSdpValue(sdp,"x-ss-audio.mat-cycles",count,sizeof(count))==1 && MatProfileSet(out,hash,count); -} -static inline int MatPlaybackProfileConfirm(const LI_MAT_DUMP_PROFILE* expected,const char* codec, - const char* hash,const char* count,const char* carrier,const char* clock) { - LI_MAT_DUMP_PROFILE got; - return codec && !strcmp(codec,MAT_PLAYBACK_NAME) && carrier && !strcmp(carrier,MAT_PLAYBACK_CARRIER) && - clock && !strcmp(clock,MAT_PLAYBACK_CLOCK) && MatProfileSet(&got,hash,count) && - got.cycles==expected->cycles && !strcmp(got.sha256,expected->sha256); -} -/* Fallback is legal only during DESCRIBE, before any codec is announced or media is consumed. - * Strict diagnostic requests retain their failure behavior. Never accept a dump-only profile. */ -static inline int MatPlaybackProfileSelect(LI_MAT_DUMP_PROFILE* out,const char* sdp,int allowFallback) { - if(sdp && MatPlaybackProfileParse(out,sdp))return MAT_PLAYBACK_CODEC; - memset(out,0,sizeof(*out)); - return allowFallback?0:-1; -} diff --git a/src/MatReceiver.c b/src/MatReceiver.c index ec7eabf65..13890e7bd 100644 --- a/src/MatReceiver.c +++ b/src/MatReceiver.c @@ -34,7 +34,4 @@ int MatReceiverPush(MAT_RECEIVER* s, const uint8_t* p, size_t n) { if (result < 0) return fail(s); return result == 2 ? complete(s) : result; } -int MatReceiverFinish(const MAT_RECEIVER* s) { - return s && !s->failed && s->completed && MatDepacketizerFinish(&s->depacketizer); -} void MatReceiverAbort(MAT_RECEIVER* s) { if (s) fail(s); } diff --git a/src/MatReceiver.h b/src/MatReceiver.h index 95fa9522c..eb0643c52 100644 --- a/src/MatReceiver.h +++ b/src/MatReceiver.h @@ -20,7 +20,6 @@ typedef struct { int MatCarrierIsValid(const uint8_t*, size_t); void MatReceiverInit(MAT_RECEIVER*, MAT_CONSUME_CYCLE, MAT_COMPLETE_STREAM, void*); int MatReceiverPush(MAT_RECEIVER*, const uint8_t*, size_t); -int MatReceiverFinish(const MAT_RECEIVER*); void MatReceiverAbort(MAT_RECEIVER*); #ifdef __cplusplus } diff --git a/src/RtspConnection.c b/src/RtspConnection.c index a3f4b6fce..6678f7845 100644 --- a/src/RtspConnection.c +++ b/src/RtspConnection.c @@ -1276,10 +1276,6 @@ int performRtspHandshake(PSERVER_INFORMATION serverInfo) { goto Exit; } - if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_DUMP && !MatProfileParse(&MatDumpProfile,response.payload)) { - Limelog("Host lacks valid MAT dump v1 capability/manifest; refusing implicit fallback\n"); - ret = -1; freeMessage(&response); goto Exit; - } if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_LIVE && !MatLiveProfileParse(&MatLiveProfile,response.payload)) { memset(&MatLiveProfile,0,sizeof(MatLiveProfile)); if(StreamConfig.audioCodec!=AUDIO_CODEC_MAT_AUTO) { @@ -1289,19 +1285,6 @@ int performRtspHandshake(PSERVER_INFORMATION serverInfo) { NegotiatedAudioCodec=AUDIO_CODEC_OPUS;NegotiatedAudioBitrate=0; Limelog("Live MAT source unavailable; selecting Opus before ANNOUNCE (no replay fallback)\n"); } - if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_IEC61937) { - int selected = MatPlaybackProfileSelect(&MatDumpProfile,response.payload, - StreamConfig.audioCodec == AUDIO_CODEC_MAT_AUTO); - if (selected < 0) { - Limelog("Host lacks explicit finite MAT IEC playback profile; refusing fallback\n"); - ret = -1; freeMessage(&response); goto Exit; - } - NegotiatedAudioCodec = selected; - if (selected == AUDIO_CODEC_OPUS) { - NegotiatedAudioBitrate = 0; - Limelog("Host MAT profile unavailable or incompatible; selecting Opus before ANNOUNCE\n"); - } - } uint32_t pyrowaveProtocolVersion = 0; uint32_t pyrowaveBitstreamVersion = 0; uint32_t pyrowavePayloadVersion = 0; @@ -1705,22 +1688,6 @@ int performRtspHandshake(PSERVER_INFORMATION serverInfo) { // Sunshine dynamic HDR negotiation result. Hosts without the // extension send no X-SS-Dynamic-HDR header; that is the legacy // outcome (DYNAMIC_HDR_FORMAT_NONE) rather than an error. - if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_DUMP && !MatProfileConfirm(&MatDumpProfile, - getOptionContent(response.options,"X-SS-Audio-Codec"), - getOptionContent(response.options,"X-SS-Mat-SHA256"), - getOptionContent(response.options,"X-SS-Mat-Cycles"))) { - Limelog("MAT dump ANNOUNCE confirmation missing or mismatched\n"); - ret = -1; freeMessage(&response); goto Exit; - } - if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_IEC61937 && !MatPlaybackProfileConfirm(&MatDumpProfile, - uniqueMatOption(response.options,"X-SS-Audio-Codec"), - uniqueMatOption(response.options,"X-SS-Mat-SHA256"), - uniqueMatOption(response.options,"X-SS-Mat-Cycles"), - uniqueMatOption(response.options,"X-SS-Mat-Carrier"), - uniqueMatOption(response.options,"X-SS-Mat-Clock"))) { - Limelog("MAT IEC playback ANNOUNCE confirmation missing or mismatched\n"); - ret = -1; freeMessage(&response); goto Exit; - } if (NegotiatedAudioCodec == AUDIO_CODEC_OPUS) { const char* confirmedCodec = getOptionContent(response.options,"X-SS-Audio-Codec"); if ((confirmedCodec && (strcmp(confirmedCodec,"opus") || diff --git a/src/SdpGenerator.c b/src/SdpGenerator.c index b1e7a5863..2046da38c 100644 --- a/src/SdpGenerator.c +++ b/src/SdpGenerator.c @@ -588,33 +588,17 @@ static PSDP_OPTION getAttributesList(char*urlSafeAddr) { const char* codecStr = (NegotiatedAudioCodec == AUDIO_CODEC_AC3) ? "ac3" : (NegotiatedAudioCodec == AUDIO_CODEC_EAC3) ? "eac3" : (NegotiatedAudioCodec == AUDIO_CODEC_PCM_S16) ? "pcm" : - (NegotiatedAudioCodec == AUDIO_CODEC_MAT_DUMP) ? MAT_DUMP_NAME : - (NegotiatedAudioCodec == AUDIO_CODEC_MAT_IEC61937) ? MAT_PLAYBACK_NAME : (NegotiatedAudioCodec == AUDIO_CODEC_MAT_LIVE) ? MAT_LIVE_NAME : "opus"; err |= addAttributeString(&optionHead, "x-ml-audio.codec", codecStr); if (AUDIO_CODEC_IS_MAT(NegotiatedAudioCodec)) { - if(NegotiatedAudioCodec==AUDIO_CODEC_MAT_LIVE) { - err |= addAttributeString(&optionHead,"x-ml-audio.mat-live","1"); - err |= addAttributeString(&optionHead,"x-ml-audio.mat-live-carrier",MAT_PLAYBACK_CARRIER); - err |= addAttributeString(&optionHead,"x-ml-audio.mat-live-clock",MAT_PLAYBACK_CLOCK); - err |= addAttributeString(&optionHead,"x-ml-audio.mat-live-crypto",MAT_LIVE_CRYPTO); - snprintf(payloadStr,sizeof(payloadStr),"%llu",(unsigned long long)MatLiveProfile.epoch); - err |= addAttributeString(&optionHead,"x-ml-audio.mat-live-epoch",payloadStr); - snprintf(payloadStr,sizeof(payloadStr),"%llu",(unsigned long long)MatLiveProfile.domain); - err |= addAttributeString(&optionHead,"x-ml-audio.mat-live-domain",payloadStr); - } - else if (NegotiatedAudioCodec == AUDIO_CODEC_MAT_DUMP) - err |= addAttributeString(&optionHead,"x-ml-audio.mat-dump","1"); - else { - err |= addAttributeString(&optionHead,"x-ml-audio.mat-playback","1"); - err |= addAttributeString(&optionHead,"x-ml-audio.mat-carrier",MAT_PLAYBACK_CARRIER); - err |= addAttributeString(&optionHead,"x-ml-audio.mat-clock",MAT_PLAYBACK_CLOCK); - } - if(NegotiatedAudioCodec!=AUDIO_CODEC_MAT_LIVE) { - err |= addAttributeString(&optionHead,"x-ml-audio.mat-sha256",MatDumpProfile.sha256); - snprintf(payloadStr,sizeof(payloadStr),"%u",MatDumpProfile.cycles); - err |= addAttributeString(&optionHead,"x-ml-audio.mat-cycles",payloadStr); - } + err |= addAttributeString(&optionHead,"x-ml-audio.mat-live","1"); + err |= addAttributeString(&optionHead,"x-ml-audio.mat-live-carrier",MAT_CARRIER); + err |= addAttributeString(&optionHead,"x-ml-audio.mat-live-clock",MAT_CLOCK); + err |= addAttributeString(&optionHead,"x-ml-audio.mat-live-crypto",MAT_LIVE_CRYPTO); + snprintf(payloadStr,sizeof(payloadStr),"%llu",(unsigned long long)MatLiveProfile.epoch); + err |= addAttributeString(&optionHead,"x-ml-audio.mat-live-epoch",payloadStr); + snprintf(payloadStr,sizeof(payloadStr),"%llu",(unsigned long long)MatLiveProfile.domain); + err |= addAttributeString(&optionHead,"x-ml-audio.mat-live-domain",payloadStr); } if (NegotiatedAudioBitrate > 0) { snprintf(payloadStr, sizeof(payloadStr), "%d", NegotiatedAudioBitrate); diff --git a/tests/AudioCodecSelectionTest.c b/tests/AudioCodecSelectionTest.c index aab9aa583..41abbe88a 100644 --- a/tests/AudioCodecSelectionTest.c +++ b/tests/AudioCodecSelectionTest.c @@ -4,6 +4,7 @@ #undef PltGetMillis #include "MatWire.h" #include "MatReceiver.h" +#include "MatLiveCrypto.h" #include static int failures,expectedCodec; static uint64_t testNow; @@ -20,35 +21,37 @@ static int consumeCycle(void* context,const unsigned char* bytes,int n,uint64_t } static int consumeEnd(void* context,uint64_t count){(void)context;CHECK(count==1);testNow+=5000;CHECK(!matInputTimedOut());++ended;return !rejectEnd;} static void terminate(int error){(void)error;++terminated;} -static void finiteCallbacks(void) { - unsigned char carrier[MAT_WIRE_CYCLE]={0},packet[MAT_WIRE_PACKET]; +static void liveCallbacks(void) { + unsigned char carrier[MAT_WIRE_CYCLE]={0},packet[MAT_WIRE_PACKET],wire[MAT_LIVE_PACKET],key[16]={0}; memcpy(carrier,"\x72\xf8\x1f\x4e\x16\x00\x02\x00",8); - const char* hash="b51894f456731ba3171663abe5305090f366ee778eb56cece1009e90005f4898"; + MAT_LIVE_CIPHER cipher; + LI_MAT_LIVE_PROFILE profile={1,1}; + CHECK(MatLiveCipherInit(&cipher,key,sizeof(key),1,1)); LI_MAT_PLAYBACK_CALLBACKS callbacks={consumeCycle,consumeEnd,NULL}; LiSetMatPlaybackCallbacks(&callbacks);ListenerCallbacks.connectionTerminated=terminate; - for(int codec=AUDIO_CODEC_MAT_DUMP;codec<=AUDIO_CODEC_MAT_IEC61937;codec++)for(int test=0;test<6;test++) { + for(int test=0;test<4;test++) { received=ended=terminated=0;rejectCycle=test==1;rejectEnd=test==2; - matPlaybackFailedReported=matPlaybackCompleteReported=false;NegotiatedAudioCodec=codec; - CHECK(MatProfileSet(&MatDumpProfile,hash,test==4?"2":"1")); - if(test==3)MatDumpProfile.sha256[0]='0'; + matPlaybackFailedReported=matPlaybackCompleteReported=false;NegotiatedAudioCodec=AUDIO_CODEC_MAT_LIVE; LI_MAT_PLAYBACK_CALLBACKS guarded=matCallbacks;guarded.end=matEnd; testNow=1000;atomic_store(&matInputEnded,false);atomic_store(&matLastAcceptedMs,testNow); - matPlayback=MatPlaybackOpen(&MatDumpProfile,&guarded);CHECK(matPlayback!=NULL); + matLive=MatLivePlaybackOpen(&profile,key,&guarded);CHECK(matLive!=NULL); for(unsigned fragment=0;fragment0&&test<5)); - MatPlaybackClose(matPlayback);matPlayback=NULL; + CHECK(received==1);CHECK(ended==(test==0||test==2));CHECK(terminated==(test==1||test==2)); + MatLivePlaybackClose(matLive);matLive=NULL; } LiSetMatPlaybackCallbacks(NULL); - CHECK(MatPlaybackOpen(&MatDumpProfile,&matCallbacks)==NULL); + CHECK(MatLivePlaybackOpen(&profile,key,&matCallbacks)==NULL); } static void watchdog(void) { - const int codecs[]={AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_DUMP,AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_MAT_LIVE}; + const int codecs[]={AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_LIVE}; for(unsigned i=0;isampleRate==48000); CHECK(format->samplesPerFrame==(mat?960:240)); - CHECK(matLive==NULL && matPlayback==NULL); + CHECK(matLive==NULL); return -123; } int main(void) { CHECK(initializePlatform()==0); - finiteCallbacks(); + liveCallbacks(); watchdog(); - const int requests[]={AUDIO_CODEC_MAT_AUTO,AUDIO_CODEC_MAT_AUTO,AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_LIVE,AUDIO_CODEC_MAT_DUMP}; - const int selections[]={AUDIO_CODEC_MAT_LIVE,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_IEC61937,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_LIVE,AUDIO_CODEC_MAT_DUMP}; + const int requests[]={AUDIO_CODEC_MAT_AUTO,AUDIO_CODEC_MAT_AUTO,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_LIVE}; + const int selections[]={AUDIO_CODEC_MAT_LIVE,AUDIO_CODEC_OPUS,AUDIO_CODEC_OPUS,AUDIO_CODEC_MAT_LIVE}; for(unsigned i=0;i -#define CHECK(c) do { if(!(c)) { fprintf(stderr,"line %d: %s\n",__LINE__,#c);return 1;} } while(0) -static const char valid[] = - "v=0\r\na=x-ss-audio.mat-playback:1\r\na=x-ss-audio.mat-carrier:" MAT_PLAYBACK_CARRIER - "\r\na=x-ss-audio.mat-clock:48000\r\na=x-ss-audio.mat-sha256:" - "79cd69e33adcd1bcffe95673564b6fc727a7aaed7370745d43ec269925a492f6" - "\r\na=x-ss-audio.mat-cycles:178\r\n"; -int main(void) { - LI_MAT_DUMP_PROFILE p; - CHECK(MatPlaybackProfileSelect(&p,valid,1)==AUDIO_CODEC_MAT_IEC61937); - CHECK(p.cycles==178); - CHECK(MatPlaybackProfileSelect(&p,"v=0\r\n",1)==AUDIO_CODEC_OPUS); - CHECK(p.cycles==0 && p.sha256[0]==0); - CHECK(MatPlaybackProfileSelect(&p,valid,0)==AUDIO_CODEC_MAT_IEC61937); - CHECK(MatPlaybackProfileSelect(&p,"v=0\r\n",0)==-1); - CHECK(p.cycles==0 && p.sha256[0]==0); - CHECK(MatPlaybackProfileSelect(&p,NULL,1)==AUDIO_CODEC_OPUS); - CHECK(MatPlaybackProfileSelect(&p,"a=x-ss-audio.mat-dump:1\r\n",1)==AUDIO_CODEC_OPUS); - char duplicate[1024];snprintf(duplicate,sizeof(duplicate),"%sa=x-ss-audio.mat-playback:1\r\n",valid); - CHECK(MatPlaybackProfileSelect(&p,duplicate,1)==AUDIO_CODEC_OPUS); - CHECK(p.cycles==0 && p.sha256[0]==0); - CHECK(!AUDIO_CODEC_IS_MAT(AUDIO_CODEC_MAT_AUTO)); - puts("PASS MAT preference selects only confirmed playback; absent/invalid/dump profiles fall back before ANNOUNCE; strict mode rejects"); - return 0; -}